Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 77 additions & 77 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions oxlint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default defineConfig({
// React
'react/jsx-key': 'warn',
'react/rules-of-hooks': 'error',
'react/react-compiler': 'error',
'react/self-closing-comp': 'warn',
'react/jsx-no-useless-fragment': 'warn',

Expand Down
24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,30 +41,30 @@
"typecheck:watch": "tsgo --noEmit --watch"
},
"dependencies": {
"@base-ui/react": "~1.5.0",
"@libsql/client": "~0.17.3",
"@base-ui/react": "~1.6.0",
"@libsql/client": "~0.17.4",
"@octokit/request-error": "~7.1.0",
"@octokit/rest": "~22.0.1",
"@radix-ui/colors": "~3.0.0",
"@tanstack/react-form": "~1.33.0",
"@tanstack/react-query": "~5.101.0",
"@tanstack/react-router": "~1.170.15",
"@tanstack/react-router": "~1.170.16",
"@tanstack/react-router-with-query": "~1.130.17",
"@tanstack/react-start": "~1.168.25",
"@trpc/client": "~11.17.0",
"@trpc/react-query": "~11.17.0",
"@trpc/server": "~11.17.0",
"@tanstack/react-start": "~1.168.26",
"@trpc/client": "~11.18.0",
"@trpc/react-query": "~11.18.0",
"@trpc/server": "~11.18.0",
"@types/bun": "~1.3.14",
"airtable": "~0.12.2",
"cmdk": "~1.1.1",
"colorjs.io": "~0.6.1",
"dataloader": "~2.2.3",
"drizzle-orm": "1.0.0-rc.4-5d5b77c",
"lucide-react": "~1.18.0",
"lucide-react": "~1.21.0",
"markdown-it": "~14.2.0",
"mime-types": "~3.0.2",
"motion": "~12.40.0",
"openai": "~6.42.0",
"openai": "~6.44.0",
"react": "~19.2.7",
"react-dom": "~19.2.7",
"react-scan": "~0.5.7",
Expand All @@ -82,12 +82,12 @@
"@types/node": "~25.9.3",
"@types/react": "~19.2.17",
"@types/react-dom": "~19.2.3",
"@typescript/native-preview": "7.0.0-dev.20260613.1",
"@typescript/native-preview": "7.0.0-dev.20260618.1",
"@vitejs/plugin-react": "~6.0.2",
"babel-plugin-react-compiler": "~1.0.0",
"drizzle-kit": "1.0.0-rc.4-5d5b77c",
"oxfmt": "~0.54.0",
"oxlint": "~1.69.0",
"oxfmt": "~0.55.0",
"oxlint": "~1.70.0",
"oxlint-tsgolint": "~0.23.0",
"pg": "~8.21.0",
"typescript": "~6.0.3"
Expand Down
35 changes: 15 additions & 20 deletions src/app/components/masonry.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useReducer, useRef, useState, type ReactNode } from 'react';
import { useEffect, useRef, useState, type ReactNode } from 'react';
import { styled } from '@/styled-system/jsx';

interface MasonryProps<T> {
Expand Down Expand Up @@ -46,9 +46,8 @@ export function Masonry<T>({
}: MasonryProps<T>) {
const containerRef = useRef<HTMLDivElement>(null);
const [containerWidth, setContainerWidth] = useState(0);
const heightsRef = useRef(new Map<string, number>());
const [heights, setHeights] = useState<Map<string, number>>(() => new Map());
const observerRef = useRef<ResizeObserver | null>(null);
const [, rerender] = useReducer((x: number) => x + 1, 0);

// Observe container width
useEffect(() => {
Expand All @@ -65,17 +64,19 @@ export function Masonry<T>({
// Single observer for all children
useEffect(() => {
const ro = new ResizeObserver((entries) => {
let changed = false;
for (const entry of entries) {
const key = (entry.target as HTMLElement).dataset.masonryKey;
if (!key) continue;
const h = entry.borderBoxSize[0]?.blockSize ?? 0;
if (heightsRef.current.get(key) !== h) {
heightsRef.current.set(key, h);
changed = true;
setHeights((prev) => {
let next: Map<string, number> | null = null;
for (const entry of entries) {
const key = (entry.target as HTMLElement).dataset.masonryKey;
if (!key) continue;
const h = entry.borderBoxSize[0]?.blockSize ?? 0;
if (prev.get(key) !== h) {
next ??= new Map(prev);
next.set(key, h);
}
}
}
if (changed) rerender();
return next ?? prev;
});
});
observerRef.current = ro;
return () => ro.disconnect();
Expand All @@ -93,13 +94,7 @@ export function Masonry<T>({

const columnCount = Math.max(1, Math.floor((containerWidth + gap) / (columnWidth + gap)));
const keys = items.map(keyExtractor);
const { positions, height } = computePositions(
keys,
heightsRef.current,
columnCount,
containerWidth,
gap
);
const { positions, height } = computePositions(keys, heights, columnCount, containerWidth, gap);

return (
<styled.div
Expand Down
17 changes: 6 additions & 11 deletions src/app/components/media-grid.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { MediaSelect } from '@hozo/schema/media';
import { Trash2Icon } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useState } from 'react';
import { css } from '@/styled-system/css';
import { styled } from '@/styled-system/jsx';
import { Button } from './button';
Expand Down Expand Up @@ -31,18 +31,13 @@ function MediaGrid({ media, className, onDelete }: MediaGridProps) {
// Limit to 8 media items maximum
const displayMedia = media.slice(0, 8);
const count = displayMedia.length;
const images = useMemo(
() => displayMedia.filter((item) => item.type === 'image'),
[displayMedia]
);
const images = displayMedia.filter((item) => item.type === 'image');
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);

const imageIndexById = useMemo(() => {
return images.reduce<Map<number, number>>((map, item, index) => {
map.set(item.id, index);
return map;
}, new Map());
}, [images]);
const imageIndexById = images.reduce<Map<number, number>>((map, item, index) => {
map.set(item.id, index);
return map;
}, new Map());

if (count === 0) return null;

Expand Down
93 changes: 42 additions & 51 deletions src/app/lib/keyboard-shortcuts/context.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {
createContext,
useCallback,
useEffectEvent,
useLayoutEffect,
useMemo,
useRef,
useState,
useSyncExternalStore,
} from 'react';
import { isInputElement, matchesShortcut, parseShortcut } from './parse';
Expand Down Expand Up @@ -91,11 +92,7 @@ interface KeyboardShortcutProviderProps {
* over shortcuts registered higher up.
*/
export function KeyboardShortcutProvider({ children }: KeyboardShortcutProviderProps) {
const storeRef = useRef<ReturnType<typeof createShortcutStore>>(null);
if (!storeRef.current) {
storeRef.current = createShortcutStore();
}
const store = storeRef.current;
const [store] = useState(createShortcutStore);

const { shortcuts, activeScope } = useSyncExternalStore(
store.subscribe,
Expand Down Expand Up @@ -124,60 +121,54 @@ export function KeyboardShortcutProvider({ children }: KeyboardShortcutProviderP
[shortcuts]
);

// Handle keyboard events
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
// Skip if no shortcuts registered
if (shortcuts.length === 0) return;

// Check if we're in an input element
const inInput = isInputElement(event.target);

// Find matching shortcuts, sorted by priority (highest first = most recently registered)
const matching = [...shortcuts]
.sort((a, b) => b.priority - a.priority)
.filter((shortcut) => {
// Check if shortcut matches key combination
if (!matchesShortcut(event, shortcut.parsed)) return false;

// Check scope
const shortcutScope = shortcut.scope ?? 'global';
if (shortcutScope !== 'global' && shortcutScope !== activeScope) return false;

// Check input restriction
if (inInput && !shortcut.allowInInput) return false;

// Check custom condition
if (shortcut.when && !shortcut.when()) return false;

return true;
});

// Execute the highest priority matching shortcut
const shortcut = matching[0];
if (shortcut) {
if (shortcut.preventDefault !== false) {
event.preventDefault();
}
shortcut.callback(event);
// Handle keyboard events. The Effect Event keeps the listener stable while
// always reading the latest shortcuts and activeScope.
const handleKeyDown = useEffectEvent((event: KeyboardEvent) => {
// Skip if no shortcuts registered
if (shortcuts.length === 0) return;

// Check if we're in an input element
const inInput = isInputElement(event.target);

// Find matching shortcuts, sorted by priority (highest first = most recently registered)
const matching = [...shortcuts]
.sort((a, b) => b.priority - a.priority)
.filter((shortcut) => {
// Check if shortcut matches key combination
if (!matchesShortcut(event, shortcut.parsed)) return false;

// Check scope
const shortcutScope = shortcut.scope ?? 'global';
if (shortcutScope !== 'global' && shortcutScope !== activeScope) return false;

// Check input restriction
if (inInput && !shortcut.allowInInput) return false;

// Check custom condition
if (shortcut.when && !shortcut.when()) return false;

return true;
});

// Execute the highest priority matching shortcut
const shortcut = matching[0];
if (shortcut) {
if (shortcut.preventDefault !== false) {
event.preventDefault();
}
},
[shortcuts, activeScope]
);
shortcut.callback(event);
}
});

// Set up global event listener in an effect for SSR safety
const handleKeyDownRef = useRef(handleKeyDown);
handleKeyDownRef.current = handleKeyDown;

useLayoutEffect(() => {
// Guard for SSR/non-browser environments
if (typeof document === 'undefined') return;

const listener = (event: KeyboardEvent) => handleKeyDownRef.current(event);
document.addEventListener('keydown', listener);
document.addEventListener('keydown', handleKeyDown);

return () => {
document.removeEventListener('keydown', listener);
document.removeEventListener('keydown', handleKeyDown);
};
}, []);

Expand Down
23 changes: 10 additions & 13 deletions src/app/lib/keyboard-shortcuts/use-keyboard-shortcut.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useContext, useLayoutEffect, useRef } from 'react';
import { useContext, useEffectEvent, useLayoutEffect } from 'react';
import { KeyboardShortcutContext } from './context';
import type {
KeyboardShortcutConfig,
Expand Down Expand Up @@ -49,20 +49,17 @@ export function useKeyboardShortcut(
options: UseKeyboardShortcutOptions
): void {
const context = useContext(KeyboardShortcutContext);
// `register` is referentially stable, so the effect re-registers only when the
// shortcut's own options change — not on every context update.
const register = context?.register;

const { allowInInput, category, description, enabled, preventDefault, scope, when } = options;

// Use refs to avoid re-registering on every render
const callbackRef = useRef(callback);
const contextRef = useRef(context);

// Update refs on each render
callbackRef.current = callback;
contextRef.current = context;
// Keep the registered callback fresh without re-registering the shortcut.
const onTrigger = useEffectEvent((event: KeyboardEvent) => callback(event));

useLayoutEffect(() => {
const ctx = contextRef.current;
if (!ctx) return;
if (!register) return;

// Don't register if disabled
if (enabled === false) return;
Expand All @@ -75,16 +72,16 @@ export function useKeyboardShortcut(
id,
keys,
description,
callback: (event) => callbackRef.current(event),
callback: onTrigger,
scope,
when,
allowInInput,
preventDefault,
category,
};

return ctx.register(config);
}, [allowInInput, category, description, enabled, keys, preventDefault, scope, when]);
return register(config);
}, [register, allowInInput, category, description, enabled, keys, preventDefault, scope, when]);
}

/**
Expand Down
24 changes: 11 additions & 13 deletions src/app/routes/records/-components/record-lookup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,20 +278,18 @@ function RelationshipSelectorRoot({
{ enabled: targetId !== null }
);

/* --------------------------------------------------
* Reset unsaved state when the popover closes, unless
* the target is controlled externally (initialTargetId) or editing mode.
* -------------------------------------------------- */
useEffect(() => {
if (!open && !initialTargetId && !link) {
// Reset unsaved state when the popover closes, unless the target is
// controlled externally (initialTargetId) or we're editing an existing link.
const handleOpenChange = (nextOpen: boolean) => {
setOpen(nextOpen);
if (nextOpen) return;
if (!initialTargetId && !link) {
setTargetId(null);
setPredicate(null);
}
if (!open) {
altRef.current = false;
setAltPressed(false);
}
}, [open, initialTargetId, link]);
altRef.current = false;
setAltPressed(false);
};

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
Expand Down Expand Up @@ -337,7 +335,7 @@ function RelationshipSelectorRoot({
{
onSuccess: (updatedLink) => {
onComplete?.(updatedLink.sourceId, updatedLink.targetId, updatedLink.predicate);
setOpen(false);
handleOpenChange(false);
},
}
);
Expand All @@ -361,7 +359,7 @@ function RelationshipSelectorRoot({

return (
<RelationshipSelectorContext.Provider value={contextValue}>
<Popover.Root open={open} onOpenChange={setOpen}>
<Popover.Root open={open} onOpenChange={handleOpenChange}>
{children}
</Popover.Root>
</RelationshipSelectorContext.Provider>
Expand Down