diff --git a/AGENTS.md b/AGENTS.md index b7bf356..78a08ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,8 +34,8 @@ feat!: change CLI argument format ## No React -This application does NOT use React. We use `remix/component` for UI components. -Do not introduce React, Preact, or any other UI framework. +This application does NOT use React. We use `remix/ui` for UI components. Do not +introduce React, Preact, or any other UI framework. ### Remix Components vs React Components @@ -48,7 +48,7 @@ functionality receive a `Handle` as their first argument. Components that don't need the handle should omit it entirely: ```tsx -import type { Handle } from 'remix/component' +import { on, type Handle } from 'remix/ui' // Simple component that doesn't use the handle - no handle parameter needed function Greeting() { @@ -66,12 +66,12 @@ function Counter(handle: Handle) { return () => (
@@ -85,7 +85,7 @@ function Counter(handle: Handle) { For components that need state, use the closure above the return to store state: ```tsx -import type { Handle } from 'remix/component' +import { on, type Handle } from 'remix/ui' function Counter(handle: Handle) { // State lives in the closure @@ -101,7 +101,7 @@ function Counter(handle: Handle) { return () => (
Count: {count} - +
) } @@ -117,7 +117,7 @@ the returned function receives **regular props** (for rendering): > latest values. The setup prop is captured once at setup time and may be stale. ```tsx -import type { Handle } from 'remix/component' +import { css, type Handle } from 'remix/ui' function UserCard( handle: Handle, @@ -149,26 +149,36 @@ function UserCard( #### Event Handling -Use `on={{ eventName: handler }}` instead of `onClick`: +Use `mix` with the `on(...)` mixin instead of `onClick` or the removed +`on={{ ... }}` prop: ```tsx - - +import { on } from 'remix/ui' + + +('input', handleInput), + on('blur', handleBlur), + ]} +/> ``` #### CSS-in-JS -Use the `css` prop for inline styles with pseudo-selector support: +Use `mix` with the `css(...)` mixin for static styles with pseudo-selector +support. Use the standard `style` prop for dynamic CSS values. ```tsx - @@ -209,17 +219,19 @@ function DataLoader(handle: Handle) { } ``` -#### The `connect` Prop (No refs!) +#### DOM references -Remix components do **NOT** support React-style refs. Instead, use the `connect` -prop to detect when an element has been added to the screen and get a reference -to the DOM node. +Remix components do **NOT** support React-style refs. Instead, use `mix` with +the `ref(...)` mixin to detect when an element has been added to the screen and +get a reference to the DOM node. ```tsx +import { ref } from 'remix/ui' + function MyComponent() { return (
{ + mix={ref((node, signal) => { // This runs when the element is added to the DOM console.log('Element added to screen:', node) @@ -227,7 +239,7 @@ function MyComponent() { signal.addEventListener('abort', () => { console.log('Element removed from screen') }) - }} + })} > Hello World
@@ -247,13 +259,15 @@ function MyComponent() { **Example with DOM manipulation:** ```tsx +import { ref, type Handle } from 'remix/ui' + function AutoFocusInput(handle: Handle) { return () => ( { + mix={ref((input) => { input.focus() - }} + })} /> ) } @@ -262,12 +276,14 @@ function AutoFocusInput(handle: Handle) { **Example with cleanup:** ```tsx +import { ref, type Handle } from 'remix/ui' + function ResizeAware(handle: Handle) { let width = 0 return () => (
{ + mix={ref((node, signal) => { const observer = new ResizeObserver((entries) => { width = entries[0].contentRect.width handle.update() @@ -277,7 +293,7 @@ function ResizeAware(handle: Handle) { signal.addEventListener('abort', () => { observer.disconnect() }) - }} + })} > Width: {width}px
@@ -297,7 +313,7 @@ A parent component provides context using `handle.context.set()`. The context type is declared as a generic parameter on `Handle`: ```tsx -import type { Handle } from 'remix/component' +import type { Handle } from 'remix/ui' function ThemeProvider(handle: Handle<{ theme: 'light' | 'dark' }>) { // Set context value for all descendants @@ -318,7 +334,7 @@ Descendant components retrieve context using `handle.context.get()`, passing the provider component as the key: ```tsx -import type { Handle } from 'remix/component' +import type { Handle } from 'remix/ui' function ThemedButton(handle: Handle) { // Get context from nearest ancestor ThemeProvider @@ -326,10 +342,10 @@ function ThemedButton(handle: Handle) { return () => ( @@ -351,7 +367,7 @@ function ThemedButton(handle: Handle) { **Full Example with Multiple Consumers:** ```tsx -import type { Handle } from 'remix/component' +import { css, type Handle } from 'remix/ui' // Provider component with typed context function UserProvider( @@ -380,12 +396,12 @@ function UserBadge(handle: Handle) { return () => ( {ctx?.user.role} diff --git a/app/client/counter.tsx b/app/client/counter.tsx index 819efbd..dbadcd0 100644 --- a/app/client/counter.tsx +++ b/app/client/counter.tsx @@ -1,19 +1,20 @@ -import type { Handle } from 'remix/component' +import { on, type Handle } from 'remix/ui' type CounterSetup = { initial?: number } +type CounterProps = { setup?: CounterSetup } -export function Counter(handle: Handle, props: { setup?: CounterSetup } = {}) { - let count = props.setup?.initial ?? 0 +export function Counter(handle: Handle) { + let count = handle.props.setup?.initial ?? 0 return () => ( @@ -758,7 +763,7 @@ export function EditingWorkspace(handle: Handle) { disabled={ queueLoading || queuedCount === 0 || Boolean(runningTask) } - on={{ click: startNextTask }} + mix={[on('click', startNextTask)]} > Run next @@ -766,7 +771,7 @@ export function EditingWorkspace(handle: Handle) { class="button button--ghost" type="button" disabled={queueLoading || !runningTask} - on={{ click: markActiveDone }} + mix={[on('click', markActiveDone)]} > Mark running done @@ -774,13 +779,13 @@ export function EditingWorkspace(handle: Handle) { class="button button--danger" type="button" disabled={queueLoading || !runningTask} - on={{ - click: () => { + mix={[ + on('click', () => { if (runningTask) { cancelActiveTask(runningTask.id) } - }, - }} + }), + ]} > Cancel running @@ -788,7 +793,7 @@ export function EditingWorkspace(handle: Handle) { class="button button--ghost" type="button" disabled={queueLoading || completedCount === 0} - on={{ click: clearCompletedTasks }} + mix={[on('click', clearCompletedTasks)]} > Clear completed @@ -806,12 +811,12 @@ export function EditingWorkspace(handle: Handle) { { + mix={[ + on('change', (event) => { const target = event.currentTarget as HTMLSelectElement updateSecondaryChapter(target.value) - }, - }} + }), + ]} > {chapters.map((chapter) => ( @@ -840,7 +845,7 @@ export function EditingWorkspace(handle: Handle) { class="button button--primary" type="button" disabled={!primaryChapterId} - on={{ click: queueChapterEdit }} + mix={[on('click', queueChapterEdit)]} > Edit chapter @@ -848,7 +853,7 @@ export function EditingWorkspace(handle: Handle) { class="button button--ghost" type="button" disabled={!canCombineChapters} - on={{ click: queueCombineChapters }} + mix={[on('click', queueCombineChapters)]} > Combine chapters @@ -867,14 +872,16 @@ export function EditingWorkspace(handle: Handle) { @@ -893,14 +900,14 @@ export function EditingWorkspace(handle: Handle) { @@ -1002,7 +1009,11 @@ export function EditingWorkspace(handle: Handle) { @@ -1011,7 +1022,11 @@ export function EditingWorkspace(handle: Handle) { @@ -1037,7 +1052,7 @@ export function EditingWorkspace(handle: Handle) { @@ -1064,66 +1079,68 @@ export function EditingWorkspace(handle: Handle) { src={previewUrl} controls preload="metadata" - connect={(node: HTMLVideoElement, signal) => { - previewNode = node - const handleLoadedMetadata = () => { - const nextDuration = Number(node.duration) - previewDuration = Number.isFinite(nextDuration) - ? nextDuration - : 0 - previewReady = previewDuration > 0 - previewError = '' - syncVideoToPlayhead(playhead) - handle.update() - } - const handleTimeUpdate = () => { - if (!previewReady || previewDuration <= 0) return - if (isScrubbing) return - if (!previewPlaying) return - const mapped = - (node.currentTime / previewDuration) * duration - if (Math.abs(mapped - lastSyncedPlayhead) <= 0.05) { - return + mix={[ + ref((node, signal) => { + previewNode = node + const handleLoadedMetadata = () => { + const nextDuration = Number(node.duration) + previewDuration = Number.isFinite(nextDuration) + ? nextDuration + : 0 + previewReady = previewDuration > 0 + previewError = '' + syncVideoToPlayhead(playhead) + handle.update() } - playhead = clamp(mapped, 0, duration) - handle.update() - } - const handlePlay = () => { - previewPlaying = true - handle.update() - } - const handlePause = () => { - previewPlaying = false - handle.update() - } - const handleError = () => { - previewError = 'Unable to load the preview video.' - previewReady = false - previewPlaying = false - handle.update() - } - node.addEventListener( - 'loadedmetadata', - handleLoadedMetadata, - ) - node.addEventListener('timeupdate', handleTimeUpdate) - node.addEventListener('play', handlePlay) - node.addEventListener('pause', handlePause) - node.addEventListener('error', handleError) - signal.addEventListener('abort', () => { - node.removeEventListener( + const handleTimeUpdate = () => { + if (!previewReady || previewDuration <= 0) return + if (isScrubbing) return + if (!previewPlaying) return + const mapped = + (node.currentTime / previewDuration) * duration + if (Math.abs(mapped - lastSyncedPlayhead) <= 0.05) { + return + } + playhead = clamp(mapped, 0, duration) + handle.update() + } + const handlePlay = () => { + previewPlaying = true + handle.update() + } + const handlePause = () => { + previewPlaying = false + handle.update() + } + const handleError = () => { + previewError = 'Unable to load the preview video.' + previewReady = false + previewPlaying = false + handle.update() + } + node.addEventListener( 'loadedmetadata', handleLoadedMetadata, ) - node.removeEventListener('timeupdate', handleTimeUpdate) - node.removeEventListener('play', handlePlay) - node.removeEventListener('pause', handlePause) - node.removeEventListener('error', handleError) - if (previewNode === node) { - previewNode = null - } - }) - }} + node.addEventListener('timeupdate', handleTimeUpdate) + node.addEventListener('play', handlePlay) + node.addEventListener('pause', handlePause) + node.addEventListener('error', handleError) + signal.addEventListener('abort', () => { + node.removeEventListener( + 'loadedmetadata', + handleLoadedMetadata, + ) + node.removeEventListener('timeupdate', handleTimeUpdate) + node.removeEventListener('play', handlePlay) + node.removeEventListener('pause', handlePause) + node.removeEventListener('error', handleError) + if (previewNode === node) { + previewNode = null + } + }) + }), + ]} />
Preview {formatTimestamp(previewTime)} @@ -1150,7 +1167,11 @@ export function EditingWorkspace(handle: Handle) { range.id === selectedRangeId && 'is-selected', )} style={`--range-left:${(range.start / duration) * 100}%; --range-width:${((range.end - range.start) / duration) * 100}%`} - on={{ click: () => selectRange(range.id) }} + mix={[ + on('click', () => + selectRange(range.id), + ), + ]} title={`${range.reason} (${formatTimestamp(range.start)} - ${formatTimestamp(range.end)})`} /> ))} @@ -1173,43 +1194,43 @@ export function EditingWorkspace(handle: Handle) { max={duration} step={PLAYHEAD_STEP} value={playhead} - on={{ - input: (event) => { + mix={[ + on('input', (event) => { const target = event.currentTarget as HTMLInputElement startScrubbing() setPlayhead(Number(target.value)) - }, - pointerdown: startScrubbing, - pointerup: stopScrubbing, - pointercancel: stopScrubbing, - keydown: startScrubbing, - keyup: stopScrubbing, - blur: stopScrubbing, - }} + }), + on('pointerdown', startScrubbing), + on('pointerup', stopScrubbing), + on('pointercancel', stopScrubbing), + on('keydown', startScrubbing), + on('keyup', stopScrubbing), + on('blur', stopScrubbing), + ]} /> @@ -1243,14 +1264,14 @@ export function EditingWorkspace(handle: Handle) { max={duration} step="0.1" value={selectedRange.start.toFixed(2)} - on={{ - input: (event) => { + mix={[ + on('input', (event) => { const target = event.currentTarget as HTMLInputElement updateCutRange(selectedRange.id, { start: Number(target.value), }) - }, - }} + }), + ]} /> @@ -1315,7 +1340,11 @@ export function EditingWorkspace(handle: Handle) { @@ -1371,12 +1404,12 @@ export function EditingWorkspace(handle: Handle) { class="text-input" type="text" value={chapter.outputName} - on={{ - input: (event) => { + mix={[ + on('input', (event) => { const target = event.currentTarget as HTMLInputElement updateChapterOutput(chapter.id, target.value) - }, - }} + }), + ]} />