@solidrt/components 0.0.70 → 0.0.72

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,7 +73,7 @@ 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 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.
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), `onCancel` on Escape or the platform's back, each followed by the blur. 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
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
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.
@@ -95,11 +95,11 @@ One bullet per module, generated from the first paragraph of its docs/ file.
95
95
  - `Spinner` - An indeterminate spinner: a 270-degree arc that rotates continuously, driven by core `onFrame`, so it participates in demand-driven rendering and stops when unmounted. `size` (diameter, default 24), `thickness` (default 3), `speed` (revolutions per second, default 1). Color comes from the theme `primary`; override via `style.color`.
96
96
  - `ProgressBar` - A horizontal progress bar: determinate when given a `value` in `[0, 1]` (the fill grows from the left, gliding to each new value - the `fill` transition entry retimes it), indeterminate when `value` is undefined (a short segment slides back and forth, driven by core `onFrame`). Track is `surfaceAlt`, fill is `primary`; override via `style.backgroundColor` (track) and `style.color` (fill), and box the track with `borderColor`/`borderWidth`. The ends follow `radius.full` through `pill`, and the fill glows with the theme's accent glow.
97
97
  - `Portal` - Renders its child somewhere other than its lexical position: by default at the window root, so overlays (modals, menus, tooltips) escape the clipping and stacking of their surrounding layout; `mount` targets another node captured from a `ref` instead. A thin JSX wrapper over core `createPortal`. The child should be a single element with `position="absolute"`, since it is inserted into the window's flex root. Portals cannot mount during the app's initial render, so gate them behind a signal that starts false.
98
- - `Modal` - A centered overlay rendered at the window root via core `createPortal`: it fills the window with a dimming backdrop (theme `scrim`; override via `backdropColor`, `"transparent"` for no dim) and centers `children` on top, the whole overlay fading in at mount and out on removal (an exiting modal takes no hits). Control visibility by mounting/unmounting it, e.g. `<Show when={open()}>`; the gating signal must start false since portals cannot mount during the initial render. Pressing the backdrop calls `onClose` (unless `dismissable` is false), pressing the content does not, and while mounted the modal traps `createFocusNav` inside itself.
98
+ - `Modal` - A centered overlay rendered at the window root via core `createPortal`: it fills the window with a dimming backdrop (theme `scrim`; override via `backdropColor`, `"transparent"` for no dim) and centers `children` on top, the whole overlay fading in at mount and out on removal (an exiting modal takes no hits). Control visibility by mounting/unmounting it, e.g. `<Show when={open()}>`; the gating signal must start false since portals cannot mount during the initial render. Pressing the backdrop, Escape and the platform's back call `onClose` (unless `dismissable` is false, which keeps the modal up and still stops a back from reaching the screen beneath), pressing the content does not, and while mounted the modal traps `createFocusNav` inside itself.
99
99
  - `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, hides on leave and on press, and glows with the theme's overlay glow. A string/number `content` renders as themed body text; anything else as-is. `placement` picks the side (`"top"`, the default, or `"bottom"`).
100
- - `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, the dropdown glows with the theme's overlay glow, 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.
100
+ - `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, Escape and the platform's back close without a change. `placeholder` shows in the trigger while nothing is selected. Both presentations fade in and out, the dropdown glows with the theme's overlay glow, 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.
101
101
  - `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`, and box the control with `borderColor`/`borderWidth`. The indicator glows with the theme's accent glow.
102
- - `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, and the anchored menu glows with the theme's overlay glow. `items` is a `ContextMenuItem[]` (`{ label, onSelect?, disabled? }`); pressing outside closes without selecting.
102
+ - `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, and the anchored menu glows with the theme's overlay glow. `items` is a `ContextMenuItem[]` (`{ label, onSelect?, disabled? }`); pressing outside, Escape and the platform's back close without selecting.
103
103
  - `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`. With `@solidrt/router` the shell is the tabs route's component, `value`/`onChange` wired to `useTabs()` (`active()`/`select(path)`) and `<Outlet />` as the content; the router then keeps a stack per tab and the tabs mounted.
104
104
  - `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`).
105
105
  - `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
@@ -276,7 +276,7 @@ API: `SafeArea` - typed and commented in [src/safe-area.tsx](./src/safe-area.tsx
276
276
 
277
277
  ### TextInput
278
278
 
279
- 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.
279
+ 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), `onCancel` on Escape or the platform's back, each followed by the blur. 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.
280
280
 
281
281
  ```jsx
282
282
  import { TextInput } from "@solidrt/components"
@@ -296,7 +296,11 @@ function NameField() {
296
296
  }
297
297
  ```
298
298
 
299
- `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.
299
+ `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.
300
+
301
+ Leaving the field: Enter submits (`onSubmit` with the value, then the blur); Escape and the platform's back (the Android button, a pad's back button) cancel (`onCancel`, then the blur). `onCancel` only reports: the value stays as it is, and reverting it is the app's call. `onBlur` runs after either, and on a tap outside.
302
+
303
+ Keys the focused field keeps for itself, which never reach a parent `onKeyDown` or the window's shortcuts: Backspace, Delete, Left and Right, Home and End, Enter (and a remote's center key), Escape, Ctrl/Cmd+A and Ctrl/Cmd+V, Ctrl/Cmd+C and Ctrl/Cmd+X when text is selected, and every printable key while typing. A multiline field also keeps Up and Down. Everything else bubbles on (Tab, Up and Down in a single-line field, PageUp, function keys, Ctrl+S, ...), so a chat field that wants Up for its history wraps the field in a `View` with an `onKeyDown`.
300
304
 
301
305
  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).
302
306
 
@@ -374,7 +378,7 @@ createEffect(
374
378
  <ScrollView scrollRef={setScroll}>...</ScrollView>
375
379
  ```
376
380
 
377
- The range changes whenever the content or the viewport changes size. The first fill (nothing was scrollable before it, whether it mounted with the view or arrived a second later) lands instantly, as a chat opens at its end; after that the view follows the end only if it was at the previous end, and the spring makes that follow a glide. The handle arrives once the component has settled, after an effect's first compute, so hold it in a signal (a setter can be passed as the ref) rather than a plain variable, which the effect would find unset and never track. The offset is read untracked: the policy reacts to the range, not to every scroll.
381
+ The range changes whenever the content or the viewport changes size. The first fill (nothing was scrollable before it, whether it mounted with the view or arrived a second later) lands instantly, as a chat opens at its end; after that the view follows the end only if it was at the previous end, and the spring makes that follow a glide. The handle arrives during mount, outside any reactive scope, so a signal setter can be passed as the ref; holding it in a signal lets the effect track its arrival. The offset is read untracked: the policy reacts to the range, not to every scroll.
378
382
 
379
383
  A `scrollX`/`scrollY` entry in `transition` replaces the default spring: `transition={{ scrollY: { duration: 400, bounce: 0.2 } }}` (keep it a spring rather than a tween, because the wheel retargets mid-flight). The other entries animate the box itself and its background/border as on any component.
380
384
 
@@ -705,7 +709,7 @@ API: `Portal`, `PortalProps` - typed and commented in [src/portal.tsx](./src/por
705
709
 
706
710
  ### Modal
707
711
 
708
- A centered overlay rendered at the window root via core `createPortal`: it fills the window with a dimming backdrop (theme `scrim`; override via `backdropColor`, `"transparent"` for no dim) and centers `children` on top, the whole overlay fading in at mount and out on removal (an exiting modal takes no hits). Control visibility by mounting/unmounting it, e.g. `<Show when={open()}>`; the gating signal must start false since portals cannot mount during the initial render. Pressing the backdrop calls `onClose` (unless `dismissable` is false), pressing the content does not, and while mounted the modal traps `createFocusNav` inside itself.
712
+ A centered overlay rendered at the window root via core `createPortal`: it fills the window with a dimming backdrop (theme `scrim`; override via `backdropColor`, `"transparent"` for no dim) and centers `children` on top, the whole overlay fading in at mount and out on removal (an exiting modal takes no hits). Control visibility by mounting/unmounting it, e.g. `<Show when={open()}>`; the gating signal must start false since portals cannot mount during the initial render. Pressing the backdrop, Escape and the platform's back call `onClose` (unless `dismissable` is false, which keeps the modal up and still stops a back from reaching the screen beneath), pressing the content does not, and while mounted the modal traps `createFocusNav` inside itself.
709
713
 
710
714
  ```jsx
711
715
  import { Modal, Card, Button } from "@solidrt/components"
@@ -737,7 +741,7 @@ API: `Tooltip`, `TooltipProps` - typed and commented in [src/tooltip.tsx](./src/
737
741
 
738
742
  ### Select
739
743
 
740
- 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, the dropdown glows with the theme's overlay glow, 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.
744
+ 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, Escape and the platform's back close without a change. `placeholder` shows in the trigger while nothing is selected. Both presentations fade in and out, the dropdown glows with the theme's overlay glow, 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.
741
745
 
742
746
  ```jsx
743
747
  import { Select } from "@solidrt/components"
@@ -771,7 +775,7 @@ API: `SegmentedControl`, `SegmentedControlProps` - typed and commented in [src/s
771
775
 
772
776
  ### ContextMenu
773
777
 
774
- 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, and the anchored menu glows with the theme's overlay glow. `items` is a `ContextMenuItem[]` (`{ label, onSelect?, disabled? }`); pressing outside closes without selecting.
778
+ 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, and the anchored menu glows with the theme's overlay glow. `items` is a `ContextMenuItem[]` (`{ label, onSelect?, disabled? }`); pressing outside, Escape and the platform's back close without selecting.
775
779
 
776
780
  ```jsx
777
781
  import { ContextMenu } from "@solidrt/components"
@@ -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 (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, and the anchored menu glows with the theme's overlay glow. `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, and the anchored menu glows with the theme's overlay glow. `items` is a `ContextMenuItem[]` (`{ label, onSelect?, disabled? }`); pressing outside, Escape and the platform's back close without selecting.
4
4
 
5
5
  ```jsx
6
6
  import { ContextMenu } from "@solidrt/components"
package/docs/modal.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Modal
2
2
 
3
- A centered overlay rendered at the window root via core `createPortal`: it fills the window with a dimming backdrop (theme `scrim`; override via `backdropColor`, `"transparent"` for no dim) and centers `children` on top, the whole overlay fading in at mount and out on removal (an exiting modal takes no hits). Control visibility by mounting/unmounting it, e.g. `<Show when={open()}>`; the gating signal must start false since portals cannot mount during the initial render. Pressing the backdrop calls `onClose` (unless `dismissable` is false), pressing the content does not, and while mounted the modal traps `createFocusNav` inside itself.
3
+ A centered overlay rendered at the window root via core `createPortal`: it fills the window with a dimming backdrop (theme `scrim`; override via `backdropColor`, `"transparent"` for no dim) and centers `children` on top, the whole overlay fading in at mount and out on removal (an exiting modal takes no hits). Control visibility by mounting/unmounting it, e.g. `<Show when={open()}>`; the gating signal must start false since portals cannot mount during the initial render. Pressing the backdrop, Escape and the platform's back call `onClose` (unless `dismissable` is false, which keeps the modal up and still stops a back from reaching the screen beneath), pressing the content does not, and while mounted the modal traps `createFocusNav` inside itself.
4
4
 
5
5
  ```jsx
6
6
  import { Modal, Card, Button } from "@solidrt/components"
@@ -28,7 +28,7 @@ createEffect(
28
28
  <ScrollView scrollRef={setScroll}>...</ScrollView>
29
29
  ```
30
30
 
31
- The range changes whenever the content or the viewport changes size. The first fill (nothing was scrollable before it, whether it mounted with the view or arrived a second later) lands instantly, as a chat opens at its end; after that the view follows the end only if it was at the previous end, and the spring makes that follow a glide. The handle arrives once the component has settled, after an effect's first compute, so hold it in a signal (a setter can be passed as the ref) rather than a plain variable, which the effect would find unset and never track. The offset is read untracked: the policy reacts to the range, not to every scroll.
31
+ The range changes whenever the content or the viewport changes size. The first fill (nothing was scrollable before it, whether it mounted with the view or arrived a second later) lands instantly, as a chat opens at its end; after that the view follows the end only if it was at the previous end, and the spring makes that follow a glide. The handle arrives during mount, outside any reactive scope, so a signal setter can be passed as the ref; holding it in a signal lets the effect track its arrival. The offset is read untracked: the policy reacts to the range, not to every scroll.
32
32
 
33
33
  A `scrollX`/`scrollY` entry in `transition` replaces the default spring: `transition={{ scrollY: { duration: 400, bounce: 0.2 } }}` (keep it a spring rather than a tween, because the wheel retargets mid-flight). The other entries animate the box itself and its background/border as on any component.
34
34
 
package/docs/select.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Select
2
2
 
3
- 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, the dropdown glows with the theme's overlay glow, 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.
3
+ 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, Escape and the platform's back close without a change. `placeholder` shows in the trigger while nothing is selected. Both presentations fade in and out, the dropdown glows with the theme's overlay glow, 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.
4
4
 
5
5
  ```jsx
6
6
  import { Select } 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 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.
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), `onCancel` on Escape or the platform's back, each followed by the blur. 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,7 +20,11 @@ function NameField() {
20
20
  }
21
21
  ```
22
22
 
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.
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.
24
+
25
+ Leaving the field: Enter submits (`onSubmit` with the value, then the blur); Escape and the platform's back (the Android button, a pad's back button) cancel (`onCancel`, then the blur). `onCancel` only reports: the value stays as it is, and reverting it is the app's call. `onBlur` runs after either, and on a tap outside.
26
+
27
+ Keys the focused field keeps for itself, which never reach a parent `onKeyDown` or the window's shortcuts: Backspace, Delete, Left and Right, Home and End, Enter (and a remote's center key), Escape, Ctrl/Cmd+A and Ctrl/Cmd+V, Ctrl/Cmd+C and Ctrl/Cmd+X when text is selected, and every printable key while typing. A multiline field also keeps Up and Down. Everything else bubbles on (Tab, Up and Down in a single-line field, PageUp, function keys, Ctrl+S, ...), so a chat field that wants Up for its history wraps the field in a `View` with an `onKeyDown`.
24
28
 
25
29
  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
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/components",
3
- "version": "0.0.70",
3
+ "version": "0.0.72",
4
4
  "license": "MIT",
5
5
  "funding": "https://github.com/sponsors/wellawaretech",
6
6
  "author": "Antoine van Wel",
@@ -19,8 +19,11 @@
19
19
  "dependencies": {
20
20
  "qrcode-generator": "^2.0.4"
21
21
  },
22
+ "devDependencies": {
23
+ "@solidrt/test": "0.0.72"
24
+ },
22
25
  "peerDependencies": {
23
26
  "@solidjs/signals": "2.0.0-rc.13",
24
- "@solidrt/core": "0.0.70"
27
+ "@solidrt/core": "0.0.72"
25
28
  }
26
29
  }
package/src/card.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { Show, createRadialGradient, createSignal, getLayoutBox, withAlpha } from "@solidrt/core"
1
+ import { Show, createRadialGradient, createSignal, getLayoutBox, untrack, withAlpha } from "@solidrt/core"
2
2
  import type { LayoutProps, PointerEvent } from "@solidrt/core"
3
3
  import { theme } from "./theme"
4
4
  import { policy } from "./policy"
@@ -78,7 +78,10 @@ export function Card(props: CardProps) {
78
78
  onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
79
79
  ref={(n: { id: number }) => {
80
80
  node = n
81
- props.ref?.(n)
81
+ // A ref callback runs in the element's owned scope, where a prop
82
+ // read warns in dev (STRICT_READ_UNTRACKED); the caller's ref is a
83
+ // one-shot, read untracked.
84
+ untrack(() => props.ref)?.(n)
82
85
  }}
83
86
  repaintBoundary
84
87
  flexDirection="column"
package/src/carousel.tsx CHANGED
@@ -82,7 +82,10 @@ export function Carousel(props: CarouselProps) {
82
82
  onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
83
83
  ref={(n: { id: number }) => {
84
84
  node = n
85
- props.ref?.(n)
85
+ // A ref callback runs in the element's owned scope, where a prop
86
+ // read warns in dev (STRICT_READ_UNTRACKED); the caller's ref is a
87
+ // one-shot, read untracked.
88
+ untrack(() => props.ref)?.(n)
86
89
  }}
87
90
  overflow="hidden"
88
91
  {...props.layout}
@@ -1,4 +1,4 @@
1
- import { createSignal, createLongPress, createPortal, onLayout, getBoundingBox, Show, For, env } from "@solidrt/core"
1
+ import { createEffect, createSignal, createLongPress, createPortal, onBack, 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"
@@ -33,7 +33,8 @@ let margin = () => theme.spacing.sm
33
33
  * pressable inside retracts and never fires, and a scroll that started
34
34
  * first keeps it). The presentation forks on the interaction policy: touch
35
35
  * gets a bottom sheet over a scrim, desktop/hybrid an anchored menu at the
36
- * pointer. Pressing outside closes without selecting.
36
+ * pointer. Pressing outside, Escape and the user's back close without
37
+ * selecting.
37
38
  */
38
39
  export function ContextMenu(props: ContextMenuProps) {
39
40
  let [open, setOpen] = createSignal(false)
@@ -55,6 +56,19 @@ export function ContextMenu(props: ContextMenuProps) {
55
56
  item.onSelect?.()
56
57
  }
57
58
 
59
+ // Open, the menu is one step of core's back stack: Escape and the user's
60
+ // back close it as a press outside does, selecting nothing.
61
+ createEffect(
62
+ () => open(),
63
+ (isOpen) => {
64
+ if (!isOpen) return
65
+ return onBack((e) => {
66
+ e.preventDefault()
67
+ setOpen(false)
68
+ })
69
+ },
70
+ )
71
+
58
72
  let bodyText = (color: string) => ({ ...typeStyle("body"), color, maxLines: 1 })
59
73
 
60
74
  let ItemRow = (p: { item: ContextMenuItem; padY: number }) => {
@@ -1,4 +1,4 @@
1
- import { createSignal, createSwipe, getLayoutBox } from "@solidrt/core"
1
+ import { createSignal, createSwipe, getLayoutBox, untrack } from "@solidrt/core"
2
2
  import type { LayoutProps, SwipeDirection, TransitionEndEvent } from "@solidrt/core"
3
3
  import type { StyleProps, TransitionProps, TransitionStyleProp, TransitionViewProp } from "./types"
4
4
  import { splitTransition, transitionEndFor } from "./types"
@@ -98,7 +98,10 @@ export function Dismissible(props: DismissibleProps) {
98
98
  onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
99
99
  ref={(n: { id: number }) => {
100
100
  node = n
101
- props.ref?.(n)
101
+ // A ref callback runs in the element's owned scope, where a prop
102
+ // read warns in dev (STRICT_READ_UNTRACKED); the caller's ref is a
103
+ // one-shot, read untracked.
104
+ untrack(() => props.ref)?.(n)
102
105
  }}
103
106
  overflow="hidden"
104
107
  {...props.layout}
@@ -12,6 +12,7 @@ import {
12
12
  createSignal,
13
13
  onCleanup,
14
14
  focusedNode,
15
+ onBack,
15
16
  setFocus,
16
17
  startTextInput,
17
18
  textInputActive,
@@ -63,6 +64,8 @@ export interface EditorFieldProps extends TransitionProps {
63
64
  renderLine: (r: LineRender) => Element
64
65
 
65
66
  onSubmit?: (value: string) => void
67
+ /** The user backed out of the field (Escape, or the platform's back): fires, then the field blurs. The value is left as it is. */
68
+ onCancel?: () => void
66
69
  onFocus?: () => void
67
70
  onBlur?: () => void
68
71
  placeholder?: string
@@ -91,7 +94,8 @@ export interface EditorFieldProps extends TransitionProps {
91
94
  // range replace it (the buffer's own behavior). Ctrl/Cmd+C/X/V go through
92
95
  // navigator.clipboard (copy and cut need a range; single-line paste flattens
93
96
  // line breaks). A tap outside the field blurs it (core's outside-tap blur in
94
- // window.ts), as does Escape.
97
+ // window.ts). Escape and the user's back (core's onBack stack: the Android
98
+ // button, the pad's back) cancel: onCancel fires, then the field blurs.
95
99
  // Font options compared field by field: the same keys with the same values.
96
100
  function sameFont(a: MeasureTextOptions, b: MeasureTextOptions): boolean {
97
101
  let ka = Object.keys(a) as (keyof MeasureTextOptions)[]
@@ -324,7 +328,7 @@ export function EditorField(props: EditorFieldProps) {
324
328
  // The remote center key's `key` is "Unidentified"; match its code.
325
329
  activateField()
326
330
  } else if (e.key === "Escape") {
327
- if (node) setFocus(null)
331
+ cancel()
328
332
  } else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && textInputActive()) {
329
333
  // A printable key is this field's text (it arrives as a text-input
330
334
  // event); consumed here so a typed Space never reaches the window as
@@ -371,6 +375,28 @@ export function EditorField(props: EditorFieldProps) {
371
375
  }
372
376
  }
373
377
 
378
+ // Cancel mirrors submit: the callback, then the blur. Escape reaches it
379
+ // through the key handler (the focused field consumes the key, so a window
380
+ // shortcut on Escape never fires on the same press); the user's back
381
+ // through the back stack, where the field is a step while focused. Focus
382
+ // comes after any enclosing overlay mounted, so the field tops the stack
383
+ // and a back leaves the field before it closes the overlay.
384
+ let cancel = () => {
385
+ props.onCancel?.()
386
+ setFocus(null)
387
+ }
388
+
389
+ createEffect(
390
+ () => focused(),
391
+ (isFocused) => {
392
+ if (!isFocused) return
393
+ return onBack((e) => {
394
+ e.preventDefault()
395
+ cancel()
396
+ })
397
+ },
398
+ )
399
+
374
400
  let unregisterNav: (() => void) | null = null
375
401
 
376
402
  onCleanup(() => {
package/src/image.tsx CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createImage, createEffect, Loading, Errored, pct } from "@solidrt/core"
2
- import type { ImageSource, LayoutProps, Pct, PointerProps, TextureProps } from "@solidrt/core"
2
+ import type { ImageSource, InputProps, LayoutProps, Pct, TextureProps } from "@solidrt/core"
3
3
  import type { StyleProps, TransitionProps } from "./types"
4
4
  import { splitTransition, transitionEndFor } from "./types"
5
5
 
6
- export interface ImageProps extends PointerProps, TransitionProps {
6
+ export interface ImageProps extends InputProps, TransitionProps {
7
7
  src: string | Uint8Array
8
8
  /**
9
9
  * How the image maps into the Image's box (CSS object-fit): "fill"
package/src/modal.tsx CHANGED
@@ -1,19 +1,21 @@
1
- import { createPortal, onCleanup } from "@solidrt/core"
2
- import type { Color, PointerEvent } from "@solidrt/core"
1
+ import { createPortal, onBack, onCleanup } from "@solidrt/core"
2
+ import type { Color } from "@solidrt/core"
3
3
  import { theme } from "./theme"
4
4
  import { pushNavScope } from "./focus-nav"
5
5
  import { colorFade, popupFade } from "./motion"
6
6
 
7
7
  export interface ModalProps {
8
- // Called when the backdrop (the area around the content) is pressed, unless
9
- // `dismissable` is false.
8
+ // Called when the backdrop (the area around the content) is pressed, or on
9
+ // Escape or the platform's back, unless `dismissable` is false.
10
10
  onClose?: () => void
11
11
  // The modal content, centered over the backdrop.
12
12
  children?: any
13
13
  // Scrim color behind the content. Defaults to the theme scrim; pass
14
14
  // "transparent" for no dim.
15
15
  backdropColor?: Color
16
- // Whether pressing the backdrop calls onClose. Defaults to true.
16
+ // Whether a backdrop press, Escape or back calls onClose. Defaults to true.
17
+ // False keeps the modal up until the app removes it; back still stops at
18
+ // the modal, never reaching the screen beneath.
17
19
  dismissable?: boolean
18
20
  }
19
21
 
@@ -30,12 +32,24 @@ export interface ModalProps {
30
32
  * works because pointer events dispatch to the whole hit path with no
31
33
  * stopPropagation, so the content is kept a sibling of the backdrop (not a
32
34
  * child): a press on the content never has the backdrop on its path.
35
+ *
36
+ * While mounted the modal is one step of core's back stack: Escape and the
37
+ * user's back close it as the backdrop does. Registered at mount, so a field
38
+ * focused inside it (which registers at focus) sits above it and a back
39
+ * leaves the field first, the modal next.
33
40
  */
34
41
  export function Modal(props: ModalProps) {
35
- let dismiss = (_e: PointerEvent) => {
42
+ let dismiss = () => {
36
43
  if (props.dismissable !== false) props.onClose?.()
37
44
  }
38
45
 
46
+ // A modal that is not dismissable still takes the step and does nothing:
47
+ // it wants an answer, and the screen beneath must not get the back.
48
+ onBack((e) => {
49
+ e.preventDefault()
50
+ dismiss()
51
+ })
52
+
39
53
  // While mounted, the modal is a focus-navigation trap: its container tops
40
54
  // the nav scope stack, so createFocusNav only reaches controls inside it.
41
55
  let popNavScope: (() => void) | null = null
package/src/pressable.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import { children, untrack } from "@solidrt/core"
2
- import type { Element, LayoutProps, PointerProps } from "@solidrt/core"
2
+ import type { Element, InputProps, LayoutProps } from "@solidrt/core"
3
3
  import type { StyleProps, TransitionProps } from "./types"
4
4
  import { splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
5
5
  import { createPress, type PressState } from "./press"
@@ -7,7 +7,7 @@ import { colorFade, scaleFeedback } from "./motion"
7
7
 
8
8
  export type { PressState } from "./press"
9
9
 
10
- export interface PressableProps extends PointerProps, TransitionProps {
10
+ export interface PressableProps extends InputProps, TransitionProps {
11
11
  // children and style may be functions of the press state, so a caller can
12
12
  // restyle on press/hover without wiring their own signals. The state is live
13
13
  // (getters, not a snapshot): read it inside a prop or child expression, never
@@ -1,10 +1,10 @@
1
- import { createPan, createScroll, createSignal, getBoundingBoxViewport, getLayoutBox, onSettled, untrack } from "@solidrt/core"
2
- import type { LayoutProps, PointerEvent, PointerProps, Scroll, TransitionCurve, TransitionEndEvent, WheelEvent } from "@solidrt/core"
1
+ import { callRef, createPan, createScroll, createSignal, getBoundingBoxViewport, getLayoutBox } from "@solidrt/core"
2
+ import type { InputProps, LayoutProps, PointerEvent, Scroll, TransitionCurve, TransitionEndEvent, WheelEvent } from "@solidrt/core"
3
3
  import type { StyleProps, TransitionProps, TransitionScrollProp, TransitionStyleProp, TransitionViewProp } from "./types"
4
4
  import { splitTransition, transitionEndFor } from "./types"
5
5
 
6
6
  export interface ScrollViewProps
7
- extends PointerProps,
7
+ extends InputProps,
8
8
  TransitionProps<TransitionViewProp | TransitionStyleProp | TransitionScrollProp> {
9
9
  children?: any
10
10
  ref?: (node: { id: number }) => void
@@ -14,8 +14,8 @@ export interface ScrollViewProps
14
14
  horizontal?: boolean
15
15
  /** Receives the scroll handle (offset, range, scrollTo) for driving the view
16
16
  * from app code; scroll policies such as following a growing log are written
17
- * against it. Called once the component has settled, outside any reactive
18
- * scope, so a signal setter can be passed directly. */
17
+ * against it. Called during mount, outside any reactive scope, so a signal
18
+ * setter can be passed directly. */
19
19
  scrollRef?: (scroll: Scroll) => void
20
20
  }
21
21
 
@@ -72,11 +72,7 @@ export function ScrollView(props: ScrollViewProps) {
72
72
  () => content,
73
73
  { axis: props.horizontal ? "horizontal" : "vertical" },
74
74
  )
75
- // Handed out from onSettled rather than the body: the body is an owned
76
- // scope, where a signal write (an app passing its setter) is refused.
77
- onSettled(() => {
78
- untrack(() => props.scrollRef)?.(scroll)
79
- })
75
+ callRef(() => props.scrollRef, scroll)
80
76
 
81
77
  // Content follows the finger: it moves opposite to scroll offsets, which
82
78
  // grow toward the bottom/right. The lift's velocity (parent-frame px/s,
package/src/select.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { createSignal, createPortal, onLayout, getBoundingBox, Show, For, env } from "@solidrt/core"
1
+ import { createEffect, createSignal, createPortal, onBack, onLayout, getBoundingBox, Show, For, env } from "@solidrt/core"
2
2
  import type { LayoutProps } from "@solidrt/core"
3
3
  import { createPress } from "./press"
4
4
  import { theme } from "./theme"
@@ -33,7 +33,8 @@ let margin = () => theme.spacing.sm
33
33
  * A single-choice picker whose presentation forks on the interaction policy:
34
34
  * desktop/hybrid opens an anchored dropdown under the trigger (flipping above
35
35
  * when there is no room), touch opens a bottom sheet over a scrim. Same
36
- * value/onChange contract either way; pressing outside closes without a change.
36
+ * value/onChange contract either way; pressing outside, Escape and the user's
37
+ * back close without a change.
37
38
  * The option list is not scrollable yet, so keep it short.
38
39
  */
39
40
  export function Select<T>(props: SelectProps<T>) {
@@ -51,6 +52,19 @@ export function Select<T>(props: SelectProps<T>) {
51
52
  props.onChange?.(v)
52
53
  }
53
54
 
55
+ // Open, the picker is one step of core's back stack: Escape and the user's
56
+ // back close it as a press outside does, without a change.
57
+ createEffect(
58
+ () => open(),
59
+ (isOpen) => {
60
+ if (!isOpen) return
61
+ return onBack((e) => {
62
+ e.preventDefault()
63
+ setOpen(false)
64
+ })
65
+ },
66
+ )
67
+
54
68
  let bodyText = (color: string) => ({ ...typeStyle("body"), color, maxLines: 1 })
55
69
 
56
70
  // One option row, shared by both presentations; only the vertical padding
@@ -10,6 +10,13 @@ export interface TextInputProps extends TransitionProps {
10
10
  defaultValue?: string
11
11
  onInput?: (value: string) => void
12
12
  onSubmit?: (value: string) => void
13
+ /**
14
+ * The user backed out of the field: Escape, or the platform's back (the
15
+ * Android button, a pad's back). Fires, then the field blurs, as `onSubmit`
16
+ * does on Enter; `onBlur` follows either. Reports only: the value stays as
17
+ * it is, reverting it is the app's call.
18
+ */
19
+ onCancel?: () => void
13
20
  onFocus?: () => void
14
21
  onBlur?: () => void
15
22
 
@@ -75,6 +82,7 @@ export function TextInput(props: TextInputProps) {
75
82
  </d-text>
76
83
  )}
77
84
  onSubmit={props.onSubmit}
85
+ onCancel={props.onCancel}
78
86
  onFocus={props.onFocus}
79
87
  onBlur={props.onBlur}
80
88
  placeholder={props.placeholder}
package/src/text.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createMemo } from "@solidrt/core"
2
- import type { PointerProps } from "@solidrt/core"
2
+ import type { InputProps } from "@solidrt/core"
3
3
  import type { StyleProps, TextLayoutProps, TransitionProps, TransitionViewProp } from "./types"
4
4
  import { splitTextLayout, splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
5
5
  import { theme, type TextVariant } from "./theme"
@@ -10,7 +10,7 @@ import { colorFade } from "./motion"
10
10
  // make sense as a text fill; style.color takes raw values for anything else.
11
11
  export type TextColor = "text" | "textMuted" | "primary" | "onPrimary" | "danger"
12
12
 
13
- export interface TextProps extends PointerProps, TransitionProps<TransitionViewProp | "color"> {
13
+ export interface TextProps extends InputProps, TransitionProps<TransitionViewProp | "color"> {
14
14
  children?: any
15
15
  // Typography role from the theme's type scale; defaults to "body". Explicit
16
16
  // layout font props override the role's fields individually. fontSize
package/src/view.tsx CHANGED
@@ -1,9 +1,9 @@
1
- import type { LayoutProps, PointerProps } from "@solidrt/core"
1
+ import type { InputProps, LayoutProps } from "@solidrt/core"
2
2
  import type { StyleProps, TransitionProps } from "./types"
3
3
  import { splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
4
4
  import { colorFade } from "./motion"
5
5
 
6
- export interface ViewProps extends PointerProps, TransitionProps {
6
+ export interface ViewProps extends InputProps, TransitionProps {
7
7
  children?: any
8
8
  ref?: (node: { id: number }) => void
9
9
  layout?: LayoutProps
package/src/window.tsx CHANGED
@@ -1,11 +1,11 @@
1
1
  import { createEffect, displayScale, warmText } from "@solidrt/core"
2
- import type { LayoutProps, PointerProps, WindowShaderProps } from "@solidrt/core"
2
+ import type { InputProps, LayoutProps, WindowShaderProps } from "@solidrt/core"
3
3
  import type { StyleProps } from "./types"
4
4
  import { colorFade } from "./motion"
5
5
  import { theme } from "./theme"
6
6
  import { typeScaleStyles } from "./typography"
7
7
 
8
- export interface WindowProps extends PointerProps {
8
+ export interface WindowProps extends InputProps {
9
9
  children?: any
10
10
  title?: string
11
11
  fullscreen?: boolean