@solidrt/components 0.0.60 → 0.0.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -73,11 +73,13 @@ One bullet per module, generated from the first paragraph of its docs/ file.
73
73
  - `Text` - Themed text in a layout box. `variant` picks a typography role from the theme's type scale (`caption`/`label`/`body`/`title`/`heading`, default `body`); `color` picks a semantic theme color (`text`, `textMuted`, `primary`, `onPrimary`, `danger`, default `text`), with `muted` as sugar for `color="textMuted"`. Font fields go in `layout` (they affect measurement) and individually override the role; `style.color` still wins over `color`.
74
74
  - `Image` - Loads and displays an image from a URL or raw bytes (`src: string | Uint8Array`). URL loads are shared runtime-wide: mounts of the same URL reuse one fetch and one texture, and the bytes are cached on disk (fetched with `cache: "force-cache"` - no freshness check, so use versioned URLs for content that changes). Concurrent asset fetches are kept polite with a per-host limit; a failed load rejects the mounts sharing it and a later remount retries.
75
75
  - `SafeArea` - Wraps its children in a view padded clear of system UI intrusions (status bars, home indicators, notches). Top and bottom insets are applied by default; pass `false` to opt out of an edge, or a number to apply the inset with that minimum padding.
76
- - `TextInput` - Text input, single-line by default; `multiline` wraps at the field's width and edits across lines (Enter inserts a newline, Up/Down move by line; grows with content up to `maxRows` unless `layout.height` fixes the box, and scrolls to the caret). Controlled via `value`/`onInput`, or uncontrolled via `defaultValue`; `onSubmit` fires on Enter (single-line only). Also `placeholder`, `maxLength`, `autoFocus`, `disabled`, `onFocus`/`onBlur`, and `hints` for IME behavior (keyboard type, capitalization, autocorrect - identifier-like fields want `{ capitalize: "none", autocorrect: false }`).
77
- - `RichTextEditor` - Edits a rich text `Document` (styled runs, paragraph attributes) in the same field as `TextInput`: always multiline, same caret, keys, wrapping, and scrolling. Controlled via `value`/`onInput`, or uncontrolled via `defaultValue` (start from `plainDocument("")`). Formatting is driven through `editorRef`, which hands you the document buffer - the component ships no toolbar; the app renders its own controls.
76
+ - `TextInput` - Text input, single-line by default; `multiline` wraps at the field's width and edits across lines (Enter inserts a newline, Up/Down move by line; grows with content up to `maxRows` unless its layout sizes the box, and scrolls to the caret). Controlled via `value`/`onInput`, or uncontrolled via `defaultValue`; `onSubmit` fires on Enter (single-line only). Also `placeholder`, `maxLength`, `autoFocus`, `disabled`, `onFocus`/`onBlur`, and `hints` for IME behavior (keyboard type, capitalization, autocorrect - identifier-like fields want `{ capitalize: "none", autocorrect: false }`). The font fields of `layout` (`fontSize`, `fontFamily`, `lineHeight`, `fontStyle`, `fontWeight`) shape the text as on `Text`, with the rows, caret and scrolling following.
77
+ - `RichTextEditor` - Edits a rich text `Document` (styled runs, paragraph attributes) in the same field as `TextInput`: always multiline, same caret, keys, wrapping, scrolling and sizing, and the same font fields on `layout` (the base font the document's runs style from). Controlled via `value`/`onInput`, or uncontrolled via `defaultValue` (start from `plainDocument("")`). Formatting is driven through `editorRef`, which hands you the document buffer - the component ships no toolbar; the app renders its own controls.
78
78
  - `Document model` - The value model behind `RichTextEditor`: a `Document` is `{ text, runs, blocks }` - plain text plus attributed runs (inline formatting) and per-paragraph blocks. `plainDocument(text)` builds one from a string; `createDocumentBuffer(doc)` wraps one in the editing API (`format`, `formatBlock`, `insertAtom`, `attributes`, selection and edits) that `editorRef` hands out. `ATOM` is the inline-atom placeholder character (U+FFFC) for embedded objects.
79
- - `ScrollView` - A scrollable region; vertical by default, `horizontal` to flip. Both the wheel and dragging scroll the content: the drag activates after a small movement threshold along the scroll axis, also when it starts on a pressable (the press is cancelled and its feedback retracts), and keeps scrolling when the pointer leaves the box. Scrolling glides: the offset springs to each new target (250 ms, critically damped), so a wheel notch never jumps and a burst of notches reads as one motion; a dragging finger is tracked exactly, without the spring. No momentum/fling yet.
80
- - `Pressable` - A pressable box: `onPress` fires on a primary-button press released over the box; a drag out of the box (or a non-primary button) does not fire it, and a drag back in restores the pressed state. `children` and `style` may each be a function of the live `{ pressed, hovered, pending }` state, so the box restyles on press/hover without extra signals - read the state inside the prop or child expression, never eagerly into a local.
79
+ - `ScrollView` - A scrollable region; vertical by default, `horizontal` to flip. Both the wheel and dragging scroll the content: the drag activates after a small movement threshold along the scroll axis, also when it starts on a pressable (the press is cancelled and its feedback retracts), and keeps scrolling when the pointer leaves the box. Scrolling glides: the offset springs to each new target (250 ms, critically damped), so a wheel notch never jumps and a burst of notches reads as one motion; a dragging finger is tracked exactly, without the spring. A lift at speed flings: the content keeps moving from the finger's release speed and decays (iOS's deceleration), as one runtime-side animation to a projected destination, clamped to the range (it slows into an edge, no bounce). A finger landing on a moving list holds it where it is, a tap included.
80
+ - `Dismissible` - Swipe-to-dismiss. The content follows the finger sideways; a swipe that qualifies (enough travel and speed, within 30 degrees of the axis, core's swipe recognizer) carries it out of the box at its own speed and reports `onDismiss` with the direction, a drag that stops short springs back. `direction` narrows which way dismisses (`"left"`, `"right"`, default both); a drag the other way is not started, and a vertical drag is left to an enclosing ScrollView. The recognizer takes the pointer at its movement slop, so a pressable row still presses on a tap and retracts once the drag is one. Mouse and touch alike.
81
+ - `Carousel` - A pager. The children are the pages, each one box wide, laid side by side and moved with the finger: a horizontal swipe (core's swipe recognizer) turns the page, a drag that stops short snaps to the nearest one, and the page settles under a spring. Only horizontal drags are taken, so a vertical ScrollView around or inside it scrolls as before. `index` controls the page (pair it with `onChange`); without it the carousel keeps its own page and still reports turns.
82
+ - `Pressable` - A pressable box: `onPress` fires on a primary-button press released over the box; a drag out of the box (or a non-primary button) does not fire it, and a drag back in restores the pressed state. `children` and `style` may each be a function of the live `{ pressed, hovered, focused, pending }` state, so the box restyles on press/hover without extra signals - read the state inside the prop or child expression, never eagerly into a local.
81
83
  - `Button` - A themed press target over `Pressable`: a padded, centered box with a label. A press shrinks it slightly on a quick spring and tints it with `overlayPressed`; hover tints with `overlayHover` (non-touch policies). `variant` picks the visual role - `primary` (accent fill, the default), `secondary`, `ghost` (no fill until hover), `danger` (destructive) - with fill, tints, and label color from the matching theme tokens; no variant draws a border. `size` (`sm`/`md`/`lg`) pins a minimum width so a row of buttons lines up (a longer label still expands past it); omitted, the button sizes to its content. A string or number child renders as the themed label; any other child renders as-is (an icon, a row, ...).
82
84
  - `createFocusNav` - Focus navigation for pointer-free control (TV remote, keyboard, gamepad), moving real focus across the elements declaring `focusable`. It consumes three UI actions from an input map and reads no device: `navigate` (vec2) moves spatially, picking the nearest candidate in the pressed direction by on-screen boxes; `cycle` (axis) walks visual reading order - rows top to bottom, left to right - wrapping at the ends; `select` (button) activates the focused control; a held direction repeats. Created bare, the nav binds the standard set itself (`uiBindings`: arrows, Tab and Shift+Tab, Enter, Space and the remote center key on the keyboard; dpad, left stick and south on every pad) on a map of its own, reachable as `nav.input` for rebinding; given `input`, it consumes the app's map instead. Spread `nav.handlers` on the window. Nothing is focused until the first navigation press; pointer input works unchanged throughout. Every interactive control (Button, Item, TextInput, RichTextEditor, Checkbox, Radio, Switch, Select and its options, SegmentedControl segments, Slider) is a candidate unless disabled, and draws the theme's `ring` color at `borderWidth.focus` while focused under the `focusRing` policy; the Slider steps its value with the arrow keys.
83
85
  - `Switch` - An on/off toggle: the track fills with `primary` when on and `surfaceAlt` when off (a fade), and the thumb springs across - the `knob` transition entry retimes that travel. Controlled via `value`/`onChange`, or uncontrolled via `defaultValue`. Built on `Pressable`, so `disabled` takes no pointer events. `style` overrides the track colors and radius.
@@ -96,7 +98,7 @@ One bullet per module, generated from the first paragraph of its docs/ file.
96
98
  - `Tooltip` - A hover-only affordance: under the `desktop`/`hybrid` interaction policies, resting a mouse pointer on the wrapped content shows a bubble near it after `delay` (default 500ms). Under the `touch` policy it never shows, so tooltip content must stay non-essential. The bubble is portal-mounted at the window root, clamped to the window edges, takes no pointer events, fades in and out, and hides on leave and on press. A string/number `content` renders as themed body text; anything else as-is. `placement` picks the side (`"top"`, the default, or `"bottom"`).
97
99
  - `Select` - A single-choice picker whose presentation forks on the interaction policy: `desktop`/`hybrid` opens an anchored dropdown under the trigger (flipping above when there is no room), `touch` opens a bottom sheet over a scrim. Same contract either way: `options` is an `Option[]` (`{ value, label }`), controlled via `value`/`onChange` or uncontrolled via `defaultValue`; pressing outside closes without a change. `placeholder` shows in the trigger while nothing is selected. Both presentations fade in and out, and the trigger's chevron flips while open. The option list is not scrollable yet, so keep it short. The chevron is the `theme.icons.chevronDown` slot when a theme sets one.
98
100
  - `SegmentedControl` - A single-choice row of equal-width segments joined flush: only the control's outermost corners are rounded, hairline dividers separate the segments, and the active segment is one `primary` indicator that springs between segments on a selection change - the `indicator` transition entry retimes it. Hovered segments tint with the theme `overlayHover` under non-touch interaction policies. `options` is an `Option[]`; controlled via `value`/`onChange`, or uncontrolled via `defaultValue`. Override the inactive fill via `style.backgroundColor` and the outer radius via `style.borderRadius`.
99
- - `ContextMenu` - Secondary actions on the wrapped content. The opening gesture follows the physical pointer: right-click for a mouse, long-press (500ms, cancelled by finger travel) for touch. The presentation forks on the interaction policy: `touch` gets a bottom sheet over a scrim, `desktop`/`hybrid` an anchored menu at the pointer that flips up near the bottom edge. Both presentations fade in and out. `items` is a `ContextMenuItem[]` (`{ label, onSelect?, disabled? }`); pressing outside closes without selecting.
101
+ - `ContextMenu` - Secondary actions on the wrapped content. The opening gesture follows the physical pointer: right-click for a mouse, long-press (500 ms, cancelled by finger travel; core's long-press recognizer) for touch and pen. The long-press wins the finger at its timer: a pressable inside retracts and does not fire on the lift, and a scroll that started first keeps the finger. The presentation forks on the interaction policy: `touch` gets a bottom sheet over a scrim, `desktop`/`hybrid` an anchored menu at the pointer that flips up near the bottom edge. Both presentations fade in and out. `items` is a `ContextMenuItem[]` (`{ label, onSelect?, disabled? }`); pressing outside closes without selecting.
100
102
  - `NavShell` - An app shell that arranges primary navigation around the content per `policy.navigation`: bottom tabs under it (`bottomTabs`), a narrow rail (`rail`), or a wide sidebar (`sidebar`) beside it. The content is a single stable node; switching arrangement only flips the shell's flex direction and remounts the stateless nav strip, so page state survives a resize across a breakpoint. `items` is a `NavItem[]` (`{ value, label, icon? }`; the icon renders above the label in tabs/rail, beside it in the sidebar); controlled via `value`/`onChange`, or uncontrolled via `defaultValue`. Safe areas are the caller's concern: wrap the shell in `SafeArea`.
101
103
  - `SplitView` - A list-detail container driven by `policy.layout`: `twoPane` shows the `list` pane (width `listWidth`, default `theme.size.splitViewList`) beside the `detail` pane, `singlePane` shows one at a time per `showDetail`. Keep pane state (selection, scroll) in the app, not in the panes: crossing a breakpoint re-arranges and can remount them. It draws no chrome and adds no padding; a back affordance in the single-pane detail is the app's to render (fork on `policy.layout`).
102
104
  - `QrCode` - Renders a QR code for `data` out of primitives: same-color modules in a row collapse into one box, drawn on a light quiet-zone panel; the grid recomputes only when `data` or `level` changes. It paints black on white by default (not the theme) so it stays scannable through a theme switch; override `color`/`background` only if the contrast still holds. `moduleSize` (default 6) is pixels per module, `margin` (default 16) the quiet zone (keep it non-zero), `level` the error correction (`L`/`M`/`Q`/`H`, default `M`: higher tolerates more damage but caps data length sooner), `radius` the panel's corner radius.
package/README.md CHANGED
@@ -123,13 +123,13 @@ Most components group their props into two objects, split by one rule: `layout`
123
123
 
124
124
  `StyleProps` is that paint set. `TextLayoutProps` extends `LayoutProps` with the font fields (`fontFamily`, `fontSize`, `lineHeight`, `fontStyle`, `fontWeight`, `textAlign`, `maxLines`) because text shaping affects measurement; note `lineHeight` is a multiplier of `fontSize` (the theme uses 1.3-1.6), not a pixel value. `Option` (`{ value, label }`) is the shared shape of the single-choice controls (`Select`, `SegmentedControl`): shared shapes go through this module so components never import a sibling.
125
125
 
126
- `TransitionProps` (`transition`, `onTransitionEnd`) is the third top-level group, in the component's own vocabulary rather than core's: a declaration names the view-level properties (`opacity`, `x`, `y`, `scale*`, `rotate*`, `origin*`, `perspective`, `clipRadius`) and the style ones (`backgroundColor`, `borderColor`, `borderWidth`, `borderRadius`), plus `all`, a shorthand string, and `stagger` - `<Button transition={{ backgroundColor: { duration: 300 }, opacity: "200ms ease-out" }}>`. Core's paint names (`color`, `radius`, `strokeWidth`) are rejected by the types: a component is a root view plus the rects it draws for `style`, and `splitTransition` hands each entry to the node that owns it (the background rect gets `backgroundColor`/`borderRadius`, the stroke rect `borderColor`/`borderWidth`/`borderRadius`, the root view the rest). `onTransitionEnd` reports the component name (`backgroundColor`, not `color`). `Text` adds `color` (its text node), `ScrollView` adds `scrollX`/`scrollY` (its viewport).
126
+ `TransitionProps` (`transition`, `onTransitionEnd`) is the third top-level group, in the component's own vocabulary rather than core's: a declaration names the view-level properties (`opacity`, `x`, `y`, `scale*`, `rotate*`, `origin*`, `perspective`, `clipRadius`) and the style ones (`backgroundColor`, `borderColor`, `borderWidth`, `borderRadius`), plus `all`, a shorthand string, `stagger` and `layout` (the layout slide of the root view, as in core: `true` borrows `all`, and `all` alone never slides) - `<Button transition={{ backgroundColor: { duration: 300 }, opacity: "200ms ease-out", layout: true }}>`. Core's paint names (`color`, `radius`, `strokeWidth`) are rejected by the types: a component is a root view plus the rects it draws for `style`, and `splitTransition` hands each entry to the node that owns it (the background rect gets `backgroundColor`/`borderRadius`, the stroke rect `borderColor`/`borderWidth`/`borderRadius`, the root view the rest). `onTransitionEnd` reports the component name (`backgroundColor`, not `color`). `Text` adds `color` (its text node), `ScrollView` adds `scrollX`/`scrollY` (its viewport).
127
127
 
128
128
  Controls with a moving part of their own name it as an extra entry: `Switch` `knob` (the thumb's travel), `SegmentedControl` `indicator` (the active-segment slide), `ProgressBar` `fill` (the determinate glide) - `<Switch transition={{ knob: "150ms" }}>` retimes just that part. `Slider` deliberately has no parts: its thumb and fill track the drag 1:1, and a transition would rubber-band it.
129
129
 
130
130
  The components also ship built-in motion with no props at all: state and theme colors fade, a press shrinks the free-standing controls on a quick spring and fades the overlay tints, marks (checkmark, radio dot) pop in and out, moving parts travel on springs, and the overlays (`Modal`, `Tooltip`, the `Select`/`ContextMenu` popups) fade in and out. Timing comes from `theme.motion` (`fast`/`base`/`slow`), and `policy.motion` gates it: `reduced` keeps the fades but snaps everything that moves, `none` snaps it all. A caller's `transition` entry overrides the built-in for that property, and `transition={null}` suppresses a component's built-ins outright.
131
131
 
132
- API: `StyleProps`, `TextLayoutProps`, `Option`, `TransitionProps`, `ComponentTransition`, `TransitionViewProp`, `TransitionStyleProp`, `TransitionScrollProp` - typed and commented in [src/types.ts](./src/types.ts).
132
+ API: `StyleProps`, `FontProps`, `TextLayoutProps`, `EditorLayoutProps`, `Option`, `TransitionProps`, `ComponentTransition`, `TransitionViewProp`, `TransitionStyleProp`, `TransitionScrollProp` - typed and commented in [src/types.ts](./src/types.ts).
133
133
 
134
134
  ## Typography helpers
135
135
 
@@ -245,7 +245,7 @@ API: `SafeArea` - typed and commented in [src/safe-area.tsx](./src/safe-area.tsx
245
245
 
246
246
  ### TextInput
247
247
 
248
- Text input, single-line by default; `multiline` wraps at the field's width and edits across lines (Enter inserts a newline, Up/Down move by line; grows with content up to `maxRows` unless `layout.height` fixes the box, and scrolls to the caret). Controlled via `value`/`onInput`, or uncontrolled via `defaultValue`; `onSubmit` fires on Enter (single-line only). Also `placeholder`, `maxLength`, `autoFocus`, `disabled`, `onFocus`/`onBlur`, and `hints` for IME behavior (keyboard type, capitalization, autocorrect - identifier-like fields want `{ capitalize: "none", autocorrect: false }`).
248
+ Text input, single-line by default; `multiline` wraps at the field's width and edits across lines (Enter inserts a newline, Up/Down move by line; grows with content up to `maxRows` unless its layout sizes the box, and scrolls to the caret). Controlled via `value`/`onInput`, or uncontrolled via `defaultValue`; `onSubmit` fires on Enter (single-line only). Also `placeholder`, `maxLength`, `autoFocus`, `disabled`, `onFocus`/`onBlur`, and `hints` for IME behavior (keyboard type, capitalization, autocorrect - identifier-like fields want `{ capitalize: "none", autocorrect: false }`). The font fields of `layout` (`fontSize`, `fontFamily`, `lineHeight`, `fontStyle`, `fontWeight`) shape the text as on `Text`, with the rows, caret and scrolling following.
249
249
 
250
250
  ```jsx
251
251
  import { TextInput } from "@solidrt/components"
@@ -265,13 +265,22 @@ function NameField() {
265
265
  }
266
266
  ```
267
267
 
268
- `style` overrides the themed colors, border, and radius. `autoFocus` focuses on mount (the on-screen keyboard still waits for a tap).
268
+ `style` overrides the themed colors, border, and radius (`borderWidth: 0` draws no border at all, and no focus ring). The mouse cursor is the I-beam over the field. `autoFocus` focuses on mount (the on-screen keyboard still waits for a tap). A tap anywhere outside the field blurs it, as does Escape.
269
+
270
+ A multiline field sizes like a flex item. Unconstrained, it grows with its content, up to `maxRows` rows and then scrolls. Sized by its layout it is a fixed box that scrolls to the caret: an explicit `height`, or `flexGrow: 1` in a parent with a height (the field fills what is left and scrolls once the text outgrows it), or a parent too small for the content (the field shrinks to fit rather than overflowing).
271
+
272
+ ```jsx
273
+ <View layout={{ flexDirection: "column", height: 400 }}>
274
+ <Text>Notes</Text>
275
+ <TextInput multiline layout={{ flexGrow: 1, fontSize: 18 }} />
276
+ </View>
277
+ ```
269
278
 
270
279
  API: `TextInput`, `TextInputProps` - typed and commented in [src/text-input.tsx](./src/text-input.tsx).
271
280
 
272
281
  ### RichTextEditor
273
282
 
274
- Edits a rich text `Document` (styled runs, paragraph attributes) in the same field as `TextInput`: always multiline, same caret, keys, wrapping, and scrolling. Controlled via `value`/`onInput`, or uncontrolled via `defaultValue` (start from `plainDocument("")`). Formatting is driven through `editorRef`, which hands you the document buffer - the component ships no toolbar; the app renders its own controls.
283
+ Edits a rich text `Document` (styled runs, paragraph attributes) in the same field as `TextInput`: always multiline, same caret, keys, wrapping, scrolling and sizing, and the same font fields on `layout` (the base font the document's runs style from). Controlled via `value`/`onInput`, or uncontrolled via `defaultValue` (start from `plainDocument("")`). Formatting is driven through `editorRef`, which hands you the document buffer - the component ships no toolbar; the app renders its own controls.
275
284
 
276
285
  ```jsx
277
286
  import { RichTextEditor, plainDocument } from "@solidrt/components"
@@ -306,7 +315,7 @@ API: `createDocumentBuffer`, `plainDocument`, `ATOM`, `Document`, `DocumentRun`,
306
315
 
307
316
  ### ScrollView
308
317
 
309
- A scrollable region; vertical by default, `horizontal` to flip. Both the wheel and dragging scroll the content: the drag activates after a small movement threshold along the scroll axis, also when it starts on a pressable (the press is cancelled and its feedback retracts), and keeps scrolling when the pointer leaves the box. Scrolling glides: the offset springs to each new target (250 ms, critically damped), so a wheel notch never jumps and a burst of notches reads as one motion; a dragging finger is tracked exactly, without the spring. No momentum/fling yet.
318
+ A scrollable region; vertical by default, `horizontal` to flip. Both the wheel and dragging scroll the content: the drag activates after a small movement threshold along the scroll axis, also when it starts on a pressable (the press is cancelled and its feedback retracts), and keeps scrolling when the pointer leaves the box. Scrolling glides: the offset springs to each new target (250 ms, critically damped), so a wheel notch never jumps and a burst of notches reads as one motion; a dragging finger is tracked exactly, without the spring. A lift at speed flings: the content keeps moving from the finger's release speed and decays (iOS's deceleration), as one runtime-side animation to a projected destination, clamped to the range (it slows into an edge, no bounce). A finger landing on a moving list holds it where it is, a tap included.
310
319
 
311
320
  ```jsx
312
321
  import { ScrollView, Text } from "@solidrt/components"
@@ -342,9 +351,48 @@ The underlying geometry primitive `createScroll` is available from `@solidrt/cor
342
351
 
343
352
  API: `ScrollView`, `ScrollViewProps` - typed and commented in [src/scroll-view.tsx](./src/scroll-view.tsx).
344
353
 
354
+ ### Dismissible
355
+
356
+ Swipe-to-dismiss. The content follows the finger sideways; a swipe that qualifies (enough travel and speed, within 30 degrees of the axis, core's swipe recognizer) carries it out of the box at its own speed and reports `onDismiss` with the direction, a drag that stops short springs back. `direction` narrows which way dismisses (`"left"`, `"right"`, default both); a drag the other way is not started, and a vertical drag is left to an enclosing ScrollView. The recognizer takes the pointer at its movement slop, so a pressable row still presses on a tap and retracts once the drag is one. Mouse and touch alike.
357
+
358
+ ```jsx
359
+ import { Dismissible, Item } from "@solidrt/components"
360
+ import { For } from "@solidrt/core"
361
+
362
+ <For each={mails()}>
363
+ {(mail) => (
364
+ <Dismissible direction="left" onDismiss={() => archive(mail.id)}>
365
+ <Item label={mail.subject} description={mail.from} onPress={() => open(mail)} />
366
+ </Dismissible>
367
+ )}
368
+ </For>
369
+ ```
370
+
371
+ The box does not remove itself: after `onDismiss` the content sits parked past the edge until the caller drops the row (a `layout` or `exit` transition on the row then plays the collapse).
372
+
373
+ API: `Dismissible`, `DismissibleProps`, `DismissDirection` - typed and commented in [src/dismissible.tsx](./src/dismissible.tsx).
374
+
375
+ ### Carousel
376
+
377
+ A pager. The children are the pages, each one box wide, laid side by side and moved with the finger: a horizontal swipe (core's swipe recognizer) turns the page, a drag that stops short snaps to the nearest one, and the page settles under a spring. Only horizontal drags are taken, so a vertical ScrollView around or inside it scrolls as before. `index` controls the page (pair it with `onChange`); without it the carousel keeps its own page and still reports turns.
378
+
379
+ ```jsx
380
+ import { Carousel, Card, Text } from "@solidrt/components"
381
+
382
+ <Carousel layout={{ height: 200 }} onChange={(i) => setPage(i)}>
383
+ <Card title="One"><Text>First page</Text></Card>
384
+ <Card title="Two"><Text>Second page</Text></Card>
385
+ <Card title="Three"><Text>Third page</Text></Card>
386
+ </Carousel>
387
+ ```
388
+
389
+ Page indicators are the app's: read the index from `onChange` and draw dots beside it.
390
+
391
+ API: `Carousel`, `CarouselProps` - typed and commented in [src/carousel.tsx](./src/carousel.tsx).
392
+
345
393
  ### Pressable
346
394
 
347
- A pressable box: `onPress` fires on a primary-button press released over the box; a drag out of the box (or a non-primary button) does not fire it, and a drag back in restores the pressed state. `children` and `style` may each be a function of the live `{ pressed, hovered, pending }` state, so the box restyles on press/hover without extra signals - read the state inside the prop or child expression, never eagerly into a local.
395
+ A pressable box: `onPress` fires on a primary-button press released over the box; a drag out of the box (or a non-primary button) does not fire it, and a drag back in restores the pressed state. `children` and `style` may each be a function of the live `{ pressed, hovered, focused, pending }` state, so the box restyles on press/hover without extra signals - read the state inside the prop or child expression, never eagerly into a local.
348
396
 
349
397
  ```jsx
350
398
  import { Pressable, Text } from "@solidrt/components"
@@ -358,7 +406,7 @@ import { Pressable, Text } from "@solidrt/components"
358
406
  </Pressable>
359
407
  ```
360
408
 
361
- `disabled` takes no pointer events. When pressables nest, the innermost one wins the press. An `onPress` returning a promise sets `pending` until it settles; presses meanwhile are ignored, so async actions cannot double-fire.
409
+ The mouse cursor is the pointer hand over the box (`cursor` overrides it). `disabled` takes no pointer events. When pressables nest, the innermost one wins the press. An `onPress` returning a promise sets `pending` until it settles; presses meanwhile are ignored, so async actions cannot double-fire.
362
410
 
363
411
  API: `Pressable`, `PressableProps`, `PressState` - typed and commented in [src/pressable.tsx](./src/pressable.tsx).
364
412
 
@@ -670,7 +718,7 @@ API: `SegmentedControl`, `SegmentedControlProps` - typed and commented in [src/s
670
718
 
671
719
  ### ContextMenu
672
720
 
673
- Secondary actions on the wrapped content. The opening gesture follows the physical pointer: right-click for a mouse, long-press (500ms, cancelled by finger travel) for touch. The presentation forks on the interaction policy: `touch` gets a bottom sheet over a scrim, `desktop`/`hybrid` an anchored menu at the pointer that flips up near the bottom edge. Both presentations fade in and out. `items` is a `ContextMenuItem[]` (`{ label, onSelect?, disabled? }`); pressing outside closes without selecting.
721
+ Secondary actions on the wrapped content. The opening gesture follows the physical pointer: right-click for a mouse, long-press (500 ms, cancelled by finger travel; core's long-press recognizer) for touch and pen. The long-press wins the finger at its timer: a pressable inside retracts and does not fire on the lift, and a scroll that started first keeps the finger. The presentation forks on the interaction policy: `touch` gets a bottom sheet over a scrim, `desktop`/`hybrid` an anchored menu at the pointer that flips up near the bottom edge. Both presentations fade in and out. `items` is a `ContextMenuItem[]` (`{ label, onSelect?, disabled? }`); pressing outside closes without selecting.
674
722
 
675
723
  ```jsx
676
724
  import { ContextMenu } from "@solidrt/components"
@@ -0,0 +1,15 @@
1
+ # Carousel
2
+
3
+ A pager. The children are the pages, each one box wide, laid side by side and moved with the finger: a horizontal swipe (core's swipe recognizer) turns the page, a drag that stops short snaps to the nearest one, and the page settles under a spring. Only horizontal drags are taken, so a vertical ScrollView around or inside it scrolls as before. `index` controls the page (pair it with `onChange`); without it the carousel keeps its own page and still reports turns.
4
+
5
+ ```jsx
6
+ import { Carousel, Card, Text } from "@solidrt/components"
7
+
8
+ <Carousel layout={{ height: 200 }} onChange={(i) => setPage(i)}>
9
+ <Card title="One"><Text>First page</Text></Card>
10
+ <Card title="Two"><Text>Second page</Text></Card>
11
+ <Card title="Three"><Text>Third page</Text></Card>
12
+ </Carousel>
13
+ ```
14
+
15
+ Page indicators are the app's: read the index from `onChange` and draw dots beside it.
@@ -1,6 +1,6 @@
1
1
  # ContextMenu
2
2
 
3
- Secondary actions on the wrapped content. The opening gesture follows the physical pointer: right-click for a mouse, long-press (500ms, cancelled by finger travel) for touch. The presentation forks on the interaction policy: `touch` gets a bottom sheet over a scrim, `desktop`/`hybrid` an anchored menu at the pointer that flips up near the bottom edge. Both presentations fade in and out. `items` is a `ContextMenuItem[]` (`{ label, onSelect?, disabled? }`); pressing outside closes without selecting.
3
+ Secondary actions on the wrapped content. The opening gesture follows the physical pointer: right-click for a mouse, long-press (500 ms, cancelled by finger travel; core's long-press recognizer) for touch and pen. The long-press wins the finger at its timer: a pressable inside retracts and does not fire on the lift, and a scroll that started first keeps the finger. The presentation forks on the interaction policy: `touch` gets a bottom sheet over a scrim, `desktop`/`hybrid` an anchored menu at the pointer that flips up near the bottom edge. Both presentations fade in and out. `items` is a `ContextMenuItem[]` (`{ label, onSelect?, disabled? }`); pressing outside closes without selecting.
4
4
 
5
5
  ```jsx
6
6
  import { ContextMenu } from "@solidrt/components"
@@ -0,0 +1,18 @@
1
+ # Dismissible
2
+
3
+ Swipe-to-dismiss. The content follows the finger sideways; a swipe that qualifies (enough travel and speed, within 30 degrees of the axis, core's swipe recognizer) carries it out of the box at its own speed and reports `onDismiss` with the direction, a drag that stops short springs back. `direction` narrows which way dismisses (`"left"`, `"right"`, default both); a drag the other way is not started, and a vertical drag is left to an enclosing ScrollView. The recognizer takes the pointer at its movement slop, so a pressable row still presses on a tap and retracts once the drag is one. Mouse and touch alike.
4
+
5
+ ```jsx
6
+ import { Dismissible, Item } from "@solidrt/components"
7
+ import { For } from "@solidrt/core"
8
+
9
+ <For each={mails()}>
10
+ {(mail) => (
11
+ <Dismissible direction="left" onDismiss={() => archive(mail.id)}>
12
+ <Item label={mail.subject} description={mail.from} onPress={() => open(mail)} />
13
+ </Dismissible>
14
+ )}
15
+ </For>
16
+ ```
17
+
18
+ The box does not remove itself: after `onDismiss` the content sits parked past the edge until the caller drops the row (a `layout` or `exit` transition on the row then plays the collapse).
package/docs/pressable.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Pressable
2
2
 
3
- A pressable box: `onPress` fires on a primary-button press released over the box; a drag out of the box (or a non-primary button) does not fire it, and a drag back in restores the pressed state. `children` and `style` may each be a function of the live `{ pressed, hovered, pending }` state, so the box restyles on press/hover without extra signals - read the state inside the prop or child expression, never eagerly into a local.
3
+ A pressable box: `onPress` fires on a primary-button press released over the box; a drag out of the box (or a non-primary button) does not fire it, and a drag back in restores the pressed state. `children` and `style` may each be a function of the live `{ pressed, hovered, focused, pending }` state, so the box restyles on press/hover without extra signals - read the state inside the prop or child expression, never eagerly into a local.
4
4
 
5
5
  ```jsx
6
6
  import { Pressable, Text } from "@solidrt/components"
@@ -14,4 +14,4 @@ import { Pressable, Text } from "@solidrt/components"
14
14
  </Pressable>
15
15
  ```
16
16
 
17
- `disabled` takes no pointer events. When pressables nest, the innermost one wins the press. An `onPress` returning a promise sets `pending` until it settles; presses meanwhile are ignored, so async actions cannot double-fire.
17
+ The mouse cursor is the pointer hand over the box (`cursor` overrides it). `disabled` takes no pointer events. When pressables nest, the innermost one wins the press. An `onPress` returning a promise sets `pending` until it settles; presses meanwhile are ignored, so async actions cannot double-fire.
@@ -1,6 +1,6 @@
1
1
  # RichTextEditor
2
2
 
3
- Edits a rich text `Document` (styled runs, paragraph attributes) in the same field as `TextInput`: always multiline, same caret, keys, wrapping, and scrolling. Controlled via `value`/`onInput`, or uncontrolled via `defaultValue` (start from `plainDocument("")`). Formatting is driven through `editorRef`, which hands you the document buffer - the component ships no toolbar; the app renders its own controls.
3
+ Edits a rich text `Document` (styled runs, paragraph attributes) in the same field as `TextInput`: always multiline, same caret, keys, wrapping, scrolling and sizing, and the same font fields on `layout` (the base font the document's runs style from). Controlled via `value`/`onInput`, or uncontrolled via `defaultValue` (start from `plainDocument("")`). Formatting is driven through `editorRef`, which hands you the document buffer - the component ships no toolbar; the app renders its own controls.
4
4
 
5
5
  ```jsx
6
6
  import { RichTextEditor, plainDocument } from "@solidrt/components"
@@ -1,6 +1,6 @@
1
1
  # ScrollView
2
2
 
3
- A scrollable region; vertical by default, `horizontal` to flip. Both the wheel and dragging scroll the content: the drag activates after a small movement threshold along the scroll axis, also when it starts on a pressable (the press is cancelled and its feedback retracts), and keeps scrolling when the pointer leaves the box. Scrolling glides: the offset springs to each new target (250 ms, critically damped), so a wheel notch never jumps and a burst of notches reads as one motion; a dragging finger is tracked exactly, without the spring. No momentum/fling yet.
3
+ A scrollable region; vertical by default, `horizontal` to flip. Both the wheel and dragging scroll the content: the drag activates after a small movement threshold along the scroll axis, also when it starts on a pressable (the press is cancelled and its feedback retracts), and keeps scrolling when the pointer leaves the box. Scrolling glides: the offset springs to each new target (250 ms, critically damped), so a wheel notch never jumps and a burst of notches reads as one motion; a dragging finger is tracked exactly, without the spring. A lift at speed flings: the content keeps moving from the finger's release speed and decays (iOS's deceleration), as one runtime-side animation to a projected destination, clamped to the range (it slows into an edge, no bounce). A finger landing on a moving list holds it where it is, a tap included.
4
4
 
5
5
  ```jsx
6
6
  import { ScrollView, Text } from "@solidrt/components"
@@ -1,6 +1,6 @@
1
1
  # TextInput
2
2
 
3
- Text input, single-line by default; `multiline` wraps at the field's width and edits across lines (Enter inserts a newline, Up/Down move by line; grows with content up to `maxRows` unless `layout.height` fixes the box, and scrolls to the caret). Controlled via `value`/`onInput`, or uncontrolled via `defaultValue`; `onSubmit` fires on Enter (single-line only). Also `placeholder`, `maxLength`, `autoFocus`, `disabled`, `onFocus`/`onBlur`, and `hints` for IME behavior (keyboard type, capitalization, autocorrect - identifier-like fields want `{ capitalize: "none", autocorrect: false }`).
3
+ Text input, single-line by default; `multiline` wraps at the field's width and edits across lines (Enter inserts a newline, Up/Down move by line; grows with content up to `maxRows` unless its layout sizes the box, and scrolls to the caret). Controlled via `value`/`onInput`, or uncontrolled via `defaultValue`; `onSubmit` fires on Enter (single-line only). Also `placeholder`, `maxLength`, `autoFocus`, `disabled`, `onFocus`/`onBlur`, and `hints` for IME behavior (keyboard type, capitalization, autocorrect - identifier-like fields want `{ capitalize: "none", autocorrect: false }`). The font fields of `layout` (`fontSize`, `fontFamily`, `lineHeight`, `fontStyle`, `fontWeight`) shape the text as on `Text`, with the rows, caret and scrolling following.
4
4
 
5
5
  ```jsx
6
6
  import { TextInput } from "@solidrt/components"
@@ -20,4 +20,13 @@ function NameField() {
20
20
  }
21
21
  ```
22
22
 
23
- `style` overrides the themed colors, border, and radius. `autoFocus` focuses on mount (the on-screen keyboard still waits for a tap).
23
+ `style` overrides the themed colors, border, and radius (`borderWidth: 0` draws no border at all, and no focus ring). The mouse cursor is the I-beam over the field. `autoFocus` focuses on mount (the on-screen keyboard still waits for a tap). A tap anywhere outside the field blurs it, as does Escape.
24
+
25
+ A multiline field sizes like a flex item. Unconstrained, it grows with its content, up to `maxRows` rows and then scrolls. Sized by its layout it is a fixed box that scrolls to the caret: an explicit `height`, or `flexGrow: 1` in a parent with a height (the field fills what is left and scrolls once the text outgrows it), or a parent too small for the content (the field shrinks to fit rather than overflowing).
26
+
27
+ ```jsx
28
+ <View layout={{ flexDirection: "column", height: 400 }}>
29
+ <Text>Notes</Text>
30
+ <TextInput multiline layout={{ flexGrow: 1, fontSize: 18 }} />
31
+ </View>
32
+ ```
package/docs/types.md CHANGED
@@ -4,7 +4,7 @@ Most components group their props into two objects, split by one rule: `layout`
4
4
 
5
5
  `StyleProps` is that paint set. `TextLayoutProps` extends `LayoutProps` with the font fields (`fontFamily`, `fontSize`, `lineHeight`, `fontStyle`, `fontWeight`, `textAlign`, `maxLines`) because text shaping affects measurement; note `lineHeight` is a multiplier of `fontSize` (the theme uses 1.3-1.6), not a pixel value. `Option` (`{ value, label }`) is the shared shape of the single-choice controls (`Select`, `SegmentedControl`): shared shapes go through this module so components never import a sibling.
6
6
 
7
- `TransitionProps` (`transition`, `onTransitionEnd`) is the third top-level group, in the component's own vocabulary rather than core's: a declaration names the view-level properties (`opacity`, `x`, `y`, `scale*`, `rotate*`, `origin*`, `perspective`, `clipRadius`) and the style ones (`backgroundColor`, `borderColor`, `borderWidth`, `borderRadius`), plus `all`, a shorthand string, and `stagger` - `<Button transition={{ backgroundColor: { duration: 300 }, opacity: "200ms ease-out" }}>`. Core's paint names (`color`, `radius`, `strokeWidth`) are rejected by the types: a component is a root view plus the rects it draws for `style`, and `splitTransition` hands each entry to the node that owns it (the background rect gets `backgroundColor`/`borderRadius`, the stroke rect `borderColor`/`borderWidth`/`borderRadius`, the root view the rest). `onTransitionEnd` reports the component name (`backgroundColor`, not `color`). `Text` adds `color` (its text node), `ScrollView` adds `scrollX`/`scrollY` (its viewport).
7
+ `TransitionProps` (`transition`, `onTransitionEnd`) is the third top-level group, in the component's own vocabulary rather than core's: a declaration names the view-level properties (`opacity`, `x`, `y`, `scale*`, `rotate*`, `origin*`, `perspective`, `clipRadius`) and the style ones (`backgroundColor`, `borderColor`, `borderWidth`, `borderRadius`), plus `all`, a shorthand string, `stagger` and `layout` (the layout slide of the root view, as in core: `true` borrows `all`, and `all` alone never slides) - `<Button transition={{ backgroundColor: { duration: 300 }, opacity: "200ms ease-out", layout: true }}>`. Core's paint names (`color`, `radius`, `strokeWidth`) are rejected by the types: a component is a root view plus the rects it draws for `style`, and `splitTransition` hands each entry to the node that owns it (the background rect gets `backgroundColor`/`borderRadius`, the stroke rect `borderColor`/`borderWidth`/`borderRadius`, the root view the rest). `onTransitionEnd` reports the component name (`backgroundColor`, not `color`). `Text` adds `color` (its text node), `ScrollView` adds `scrollX`/`scrollY` (its viewport).
8
8
 
9
9
  Controls with a moving part of their own name it as an extra entry: `Switch` `knob` (the thumb's travel), `SegmentedControl` `indicator` (the active-segment slide), `ProgressBar` `fill` (the determinate glide) - `<Switch transition={{ knob: "150ms" }}>` retimes just that part. `Slider` deliberately has no parts: its thumb and fill track the drag 1:1, and a transition would rubber-band it.
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/components",
3
- "version": "0.0.60",
3
+ "version": "0.0.61",
4
4
  "license": "MIT",
5
5
  "funding": "https://github.com/sponsors/wellawaretech",
6
6
  "author": "Antoine van Wel",
@@ -20,7 +20,7 @@
20
20
  "qrcode-generator": "^2.0.4"
21
21
  },
22
22
  "peerDependencies": {
23
- "@solidjs/signals": "2.0.0-rc.8",
24
- "@solidrt/core": "0.0.60"
23
+ "@solidjs/signals": "2.0.0-rc.9",
24
+ "@solidrt/core": "0.0.61"
25
25
  }
26
26
  }
package/src/button.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { Show, children, withAlpha } from "@solidrt/core"
1
+ import { Show, children, untrack, withAlpha } from "@solidrt/core"
2
2
  import { createPress } from "./press"
3
3
  import { theme } from "./theme"
4
4
  import { policy } from "./policy"
@@ -20,10 +20,10 @@ export interface ButtonProps extends TransitionProps {
20
20
  // Visual role: primary (accent fill), secondary (darker-blue fill), ghost
21
21
  // (no fill until hover), danger (destructive accent fill). None draw a border.
22
22
  variant?: ButtonVariant
23
- // Fixed-width preset: pins the button to a set width (a longer label still
24
- // expands past it), so a row of buttons lines up. Omitted, the button
25
- // stretches to the container's width (the default). Padding is the same at
26
- // every size.
23
+ // Width preset: pins a minimum width (a longer label still expands past
24
+ // it), so a row of buttons lines up. Omitted, the button sizes to its
25
+ // content; a full-width button is the caller's `layout={{ width: "100%" }}`
26
+ // (or a stretching column parent). Padding is the same at every size.
27
27
  size?: ButtonSize
28
28
  // A returned promise makes this an async action: the button shows a
29
29
  // centered spinner in place of the label (geometry unchanged) and ignores
@@ -109,7 +109,10 @@ export function Button(props: ButtonProps) {
109
109
  onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
110
110
  ref={(n: { id: number }) => {
111
111
  press.ref(n)
112
- props.ref?.(n)
112
+ // A ref callback runs in the element's owned scope, where a prop
113
+ // read warns in dev (STRICT_READ_UNTRACKED); the caller's ref is a
114
+ // one-shot, read untracked.
115
+ untrack(() => props.ref)?.(n)
113
116
  }}
114
117
  repaintBoundary
115
118
  flexDirection="row"
@@ -120,7 +123,7 @@ export function Button(props: ButtonProps) {
120
123
  paddingBottom={space("md")}
121
124
  paddingLeft={space("lg")}
122
125
  paddingRight={space("lg")}
123
- {...(props.size ? { minWidth: SIZE_WIDTH[props.size] } : { width: "100%" })}
126
+ minWidth={props.size ? SIZE_WIDTH[props.size] : undefined}
124
127
  {...props.layout}
125
128
  x={style().x}
126
129
  y={style().y}
@@ -128,6 +131,7 @@ export function Button(props: ButtonProps) {
128
131
  rotate={style().rotate}
129
132
  opacity={style().opacity}
130
133
  {...press.handlers}
134
+ cursor="pointer"
131
135
  focusable={(props.focusable ?? true) && props.disabled !== true}
132
136
  pointerEvents={props.disabled ? "none" : undefined}
133
137
  >
@@ -0,0 +1,112 @@
1
+ import { children, createSignal, createSwipe, getLayoutBox, onLayout, untrack } from "@solidrt/core"
2
+ import type { LayoutProps } from "@solidrt/core"
3
+ import type { StyleProps, TransitionProps, TransitionStyleProp, TransitionViewProp } from "./types"
4
+ import { splitTransition, transitionEndFor } from "./types"
5
+
6
+ export interface CarouselProps extends TransitionProps<TransitionViewProp | TransitionStyleProp> {
7
+ /** The pages, in order; each fills the box. */
8
+ children?: any
9
+ ref?: (node: { id: number }) => void
10
+ layout?: LayoutProps
11
+ style?: StyleProps
12
+ /** The page shown; controlled when given (pair it with onChange). */
13
+ index?: number
14
+ /** The page changed by a swipe or a snap. */
15
+ onChange?: (index: number) => void
16
+ }
17
+
18
+ // The spring a page settles with after a swipe or a snap.
19
+ const PAGE_SPRING = { duration: 300 }
20
+
21
+ /**
22
+ * A pager: the children are pages, one box wide each, laid side by side
23
+ * and moved with the finger. A swipe (core's swipe recognizer) turns the
24
+ * page; a drag that stops short snaps to the nearest one. Only horizontal
25
+ * drags are taken, so a vertical ScrollView around or inside it keeps its
26
+ * own. `index` controls the page; without it the carousel keeps its own,
27
+ * reporting turns through `onChange` either way.
28
+ */
29
+ export function Carousel(props: CarouselProps) {
30
+ let node: { id: number } | undefined
31
+ let [own, setOwn] = createSignal(0)
32
+ let index = () => props.index ?? own()
33
+ let [width, setWidth] = createSignal(0)
34
+ let [drag, setDrag] = createSignal(0)
35
+ let [dragging, setDragging] = createSignal(false)
36
+ let pages = children(() => props.children)
37
+ let count = () => pages.toArray().length
38
+
39
+ onLayout(() => {
40
+ let w = (node && getLayoutBox(node)?.width) || 0
41
+ if (w !== width()) setWidth(w)
42
+ })
43
+
44
+ let go = (next: number) => {
45
+ let clamped = Math.max(0, Math.min(next, count() - 1))
46
+ if (clamped !== untrack(index)) {
47
+ if (props.index === undefined) setOwn(clamped)
48
+ props.onChange?.(clamped)
49
+ }
50
+ }
51
+ let swiped = false
52
+ let swipe = createSwipe({
53
+ directions: ["Left", "Right"],
54
+ onSwipeStart: () => {
55
+ swiped = false
56
+ setDragging(true)
57
+ },
58
+ onSwipeMove: dx => setDrag(drag() + dx),
59
+ onSwipe: direction => {
60
+ swiped = true
61
+ go(index() + (direction === "Left" ? 1 : -1))
62
+ },
63
+ onSwipeEnd: () => {
64
+ // Without a swipe the nearest page wins: the drag counts as pages.
65
+ if (!swiped && width() > 0) go(Math.round(index() - drag() / width()))
66
+ setDrag(0)
67
+ setDragging(false)
68
+ },
69
+ })
70
+
71
+ let split = () => splitTransition(props.transition)
72
+ let x = () => -index() * width() + drag()
73
+
74
+ return (
75
+ <view
76
+ transition={split().root}
77
+ onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
78
+ ref={(n: { id: number }) => {
79
+ node = n
80
+ props.ref?.(n)
81
+ }}
82
+ overflow="hidden"
83
+ {...props.layout}
84
+ x={props.style?.x}
85
+ y={props.style?.y}
86
+ scale={props.style?.scale}
87
+ rotate={props.style?.rotate}
88
+ opacity={props.style?.opacity}
89
+ clipRadius={props.style?.borderRadius}
90
+ {...swipe.handlers}
91
+ >
92
+ {props.style?.backgroundColor != null || props.style?.borderRadius != null ? (
93
+ <d-rect
94
+ transition={split().background}
95
+ onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
96
+ color={props.style?.backgroundColor ?? "transparent"}
97
+ radius={props.style?.borderRadius}
98
+ />
99
+ ) : null}
100
+ {/* transition before x: props apply in source order, so the spring
101
+ is declared again before the settling write lands in the flush
102
+ that ends the drag. */}
103
+ <view flexDirection="row" alignItems="stretch" transition={dragging() ? null : { x: PAGE_SPRING }} x={x()}>
104
+ {pages.toArray().map(page => (
105
+ <view width={width()} flexShrink={0} flexDirection="column">
106
+ {page}
107
+ </view>
108
+ ))}
109
+ </view>
110
+ </view>
111
+ )
112
+ }
package/src/checkbox.tsx CHANGED
@@ -72,6 +72,7 @@ export function Checkbox(props: CheckboxProps) {
72
72
  rotate={style().rotate}
73
73
  opacity={style().opacity}
74
74
  {...press.handlers}
75
+ cursor="pointer"
75
76
  focusable={!props.disabled}
76
77
  pointerEvents={props.disabled ? "none" : undefined}
77
78
  >
@@ -1,4 +1,4 @@
1
- import { createSignal, onCleanup, createPortal, onLayout, getBoundingBox, Show, For, env } from "@solidrt/core"
1
+ import { createSignal, createLongPress, createPortal, onLayout, getBoundingBox, Show, For, env } from "@solidrt/core"
2
2
  import type { LayoutProps, PointerEvent } from "@solidrt/core"
3
3
  import { createPress } from "./press"
4
4
  import { theme } from "./theme"
@@ -22,51 +22,31 @@ export interface ContextMenuProps extends TransitionProps {
22
22
  layout?: LayoutProps
23
23
  }
24
24
 
25
- const LONG_PRESS_MS = 500
26
- // Finger travel (window px) that cancels a pending long-press.
27
- const MOVE_SLOP = 8
28
25
  // Minimum distance kept between the menu and the window edges.
29
26
  let margin = () => theme.spacing.sm
30
27
 
31
28
  /**
32
29
  * Secondary actions on the wrapped content. The opening gesture follows the
33
- * physical pointer: right-click for a mouse, long-press for touch. The
34
- * presentation forks on the interaction policy: touch gets a bottom sheet over
35
- * a scrim, desktop/hybrid an anchored menu at the pointer. Pressing outside
36
- * closes without selecting.
30
+ * physical pointer: right-click for a mouse, long-press for touch and pen
31
+ * (core's long-press recognizer: it wins the finger at its timer, so a
32
+ * pressable inside retracts and never fires, and a scroll that started
33
+ * first keeps it). The presentation forks on the interaction policy: touch
34
+ * gets a bottom sheet over a scrim, desktop/hybrid an anchored menu at the
35
+ * pointer. Pressing outside closes without selecting.
37
36
  */
38
37
  export function ContextMenu(props: ContextMenuProps) {
39
38
  let [open, setOpen] = createSignal(false)
40
39
  let [point, setPoint] = createSignal({ x: 0, y: 0 })
41
- let timer: ReturnType<typeof setTimeout> | undefined
42
- let downAt: { x: number; y: number } | undefined
43
40
 
44
41
  let openAt = (x: number, y: number) => {
45
42
  setPoint({ x, y })
46
43
  setOpen(true)
47
44
  }
48
- let cancelHold = () => {
49
- clearTimeout(timer)
50
- downAt = undefined
51
- }
52
- onCleanup(cancelHold)
45
+ let longPress = createLongPress({ onLongPress: at => openAt(at.clientX, at.clientY) })
53
46
 
54
47
  let handleDown = (e: PointerEvent) => {
55
- if (e.button === 2) {
56
- cancelHold()
57
- openAt(e.clientX, e.clientY)
58
- } else if (e.pointerType === "touch") {
59
- downAt = { x: e.clientX, y: e.clientY }
60
- clearTimeout(timer)
61
- timer = setTimeout(() => {
62
- if (downAt) openAt(downAt.x, downAt.y)
63
- downAt = undefined
64
- }, LONG_PRESS_MS)
65
- }
66
- }
67
- let handleMove = (e: PointerEvent) => {
68
- if (!downAt) return
69
- if (Math.abs(e.clientX - downAt.x) > MOVE_SLOP || Math.abs(e.clientY - downAt.y) > MOVE_SLOP) cancelHold()
48
+ if (e.button === 2) openAt(e.clientX, e.clientY)
49
+ else if (e.pointerType !== "mouse") longPress.handlers.onPointerDown(e)
70
50
  }
71
51
 
72
52
  let choose = (item: ContextMenuItem) => {
@@ -89,6 +69,7 @@ export function ContextMenu(props: ContextMenuProps) {
89
69
  paddingLeft={space("md")}
90
70
  paddingRight={space("md")}
91
71
  {...press.handlers}
72
+ cursor="pointer"
92
73
  pointerEvents={p.item.disabled ? "none" : undefined}
93
74
  >
94
75
  <PressFeedback
@@ -189,9 +170,8 @@ export function ContextMenu(props: ContextMenuProps) {
189
170
  transition={splitTransition(props.transition).root}
190
171
  onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
191
172
  onPointerDown={handleDown}
192
- onPointerMove={handleMove}
193
- onPointerUp={cancelHold}
194
- onPointerLeave={cancelHold}
173
+ onPointerMove={longPress.handlers.onPointerMove}
174
+ onPointerUp={longPress.handlers.onPointerUp}
195
175
  {...props.layout}
196
176
  >
197
177
  {props.children}