@solidrt/components 0.0.55 → 0.0.57
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 +18 -15
- package/README.md +40 -17
- package/docs/button.md +2 -2
- package/docs/checkbox.md +1 -1
- package/docs/context-menu.md +1 -1
- package/docs/focus-nav.md +16 -3
- package/docs/modal.md +1 -1
- package/docs/policy.md +1 -1
- package/docs/progress-bar.md +1 -1
- package/docs/radio.md +1 -1
- package/docs/segmented-control.md +1 -1
- package/docs/select.md +1 -1
- package/docs/switch.md +1 -1
- package/docs/theme.md +8 -0
- package/docs/tooltip.md +1 -1
- package/docs/types.md +3 -1
- package/package.json +3 -3
- package/src/badge.tsx +4 -2
- package/src/button.tsx +16 -15
- package/src/card.tsx +5 -4
- package/src/checkbox.tsx +45 -21
- package/src/context-menu.tsx +22 -14
- package/src/divider.tsx +3 -2
- package/src/editor-field.tsx +116 -19
- package/src/field.tsx +3 -1
- package/src/focus-nav.ts +152 -53
- package/src/index.ts +1 -1
- package/src/item.tsx +11 -14
- package/src/modal.tsx +7 -1
- package/src/motion.tsx +122 -0
- package/src/nav-shell.tsx +24 -13
- package/src/press.ts +6 -11
- package/src/pressable.tsx +7 -4
- package/src/progress-bar.tsx +19 -6
- package/src/radio.tsx +36 -7
- package/src/segmented-control.tsx +70 -20
- package/src/select.tsx +58 -43
- package/src/slider.tsx +7 -3
- package/src/spinner.tsx +2 -0
- package/src/switch.tsx +30 -11
- package/src/text.tsx +3 -2
- package/src/theme.ts +25 -1
- package/src/tooltip.tsx +10 -4
- package/src/types.ts +74 -6
- package/src/view.tsx +4 -3
- package/src/window.tsx +2 -1
package/AGENTS.md
CHANGED
|
@@ -53,10 +53,13 @@ objects at that level - and the two compose freely in one tree.
|
|
|
53
53
|
for one custom shape never means giving up `View`/`Text` elsewhere.
|
|
54
54
|
- Focus navigation: `createFocusNav` moves real focus across `focusable`
|
|
55
55
|
elements (Button is focusable by default; Pressable opt-in) - spatially on
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
`navigate`, sequentially on `cycle` (reading order, wrapping), activating
|
|
57
|
+
on `select`: three input-map actions (`uiActions`), never a device read.
|
|
58
|
+
Bare, it binds the standard set (`uiBindings`: arrows, Tab, Enter, Space,
|
|
59
|
+
remote center; dpad, left stick, south on every pad) on its own map,
|
|
60
|
+
`nav.input`; or pass `input`. Spread `nav.handlers` on the window. A
|
|
61
|
+
focused press control activates only through `select` (createPress reads
|
|
62
|
+
no key) and draws Button's ring under `policy.focusRing` (true when a
|
|
60
63
|
keyboard OR gamepad/remote is present). An open Modal traps navigation
|
|
61
64
|
inside itself automatically.
|
|
62
65
|
|
|
@@ -75,11 +78,11 @@ One bullet per module, generated from the first paragraph of its docs/ file.
|
|
|
75
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.
|
|
76
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.
|
|
77
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.
|
|
78
|
-
- `Button` - A themed press target over `Pressable`: a padded, centered box with a label. `variant` picks the visual role - `primary` (accent fill, the default), `secondary`, `ghost` (no fill until hover), `danger` (destructive) - with fill,
|
|
79
|
-
- `createFocusNav` - Focus navigation for pointer-free control (TV remote, keyboard, gamepad), moving real focus across the elements declaring `focusable`.
|
|
80
|
-
- `Switch` - An on/off toggle: the track fills with `primary` when on and `surfaceAlt` when off
|
|
81
|
-
- `Checkbox` - A checkbox: filled with `primary` and a drawn checkmark when checked, an empty bordered box otherwise. Controlled via `checked`/`onChange`, or uncontrolled via `defaultChecked`. The mark is the `theme.icons.check` slot when a theme sets one. `style` overrides the box colors, border, and radius.
|
|
82
|
-
- `RadioGroup / Radio` - A single-selection pair: `RadioGroup` owns the selected value (controlled via `value`/`onChange`, or uncontrolled via `defaultValue`) and shares it with its `Radio` children; each `Radio` is a ring with an inner dot when selected. A string/number child of `Radio` renders as a themed label beside the ring; anything else as-is. `disabled` on the group disables every option, on a `Radio` just that one.
|
|
81
|
+
- `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
|
+
- `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
|
+
- `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.
|
|
84
|
+
- `Checkbox` - A checkbox: filled with `primary` and a drawn checkmark when checked, an empty bordered box otherwise - the fill fades and the mark pops in and out. Controlled via `checked`/`onChange`, or uncontrolled via `defaultChecked`. The mark is the `theme.icons.check` slot when a theme sets one. `style` overrides the box colors, border, and radius.
|
|
85
|
+
- `RadioGroup / Radio` - A single-selection pair: `RadioGroup` owns the selected value (controlled via `value`/`onChange`, or uncontrolled via `defaultValue`) and shares it with its `Radio` children; each `Radio` is a ring with an inner dot when selected - the ring color fades and the dot pops in and out. A string/number child of `Radio` renders as a themed label beside the ring; anything else as-is. `disabled` on the group disables every option, on a `Radio` just that one.
|
|
83
86
|
- `Slider` - A horizontal slider: the groove fills up to the thumb, and pressing or dragging the track sets the value from the pointer position. Controlled via `value`/`onChange` (fires while dragging), or uncontrolled via `defaultValue` (defaults to `min`). `min`/`max` default to 0/100; `step` snaps to an increment, omitted the value is continuous. The drag keeps tracking when the pointer drifts off the track, and an enclosing ScrollView never takes it over.
|
|
84
87
|
- `Card` - A themed surface container: a padded column box with a `surface` fill, a subtle `border` stroke, and rounded corners, recoloring live on a theme switch. Pass a `title` for a heading, or lay out the content yourself; override paint via `style`, spacing/sizing via `layout`.
|
|
85
88
|
- `Item` - A list row: `startContent` (icon, avatar, checkbox), a `label` with an optional `description` under it, and `endContent` (badge, timestamp, action) pushed to the end. String/number label and description render as themed body and muted body text; anything else as-is. The dense-data workhorse: rows compose with `<For>` inside a plain column view or `ScrollView` - there is no List wrapper, because a column IS the list. Paddings and gaps are density-scaled, so a `<Density>` region compacts rows wholesale.
|
|
@@ -87,13 +90,13 @@ One bullet per module, generated from the first paragraph of its docs/ file.
|
|
|
87
90
|
- `Divider` - A thin rule in the theme `border` color. It stretches across its container on the cross axis: full width inside a column, full height inside a row (pass `orientation="vertical"`). `thickness` defaults to 1px; add spacing with `layout` margins, and override the color via `style.backgroundColor`.
|
|
88
91
|
- `Badge` - A small rounded pill for counts, labels, and status. `variant` picks the role: `primary` (accent fill, the default), `neutral` (subtle surface), `danger`. A string/number child renders as the themed label, anything else as-is (an icon, a dot, ...). Override the fill via `style.backgroundColor` and the label color via `style.color`.
|
|
89
92
|
- `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`.
|
|
90
|
-
- `ProgressBar` - A horizontal progress bar: determinate when given a `value` in `[0, 1]` (the fill grows from the left), 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).
|
|
93
|
+
- `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).
|
|
91
94
|
- `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.
|
|
92
|
-
- `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. 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.
|
|
93
|
-
- `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, 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"`).
|
|
94
|
-
- `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. The option list is not scrollable yet, so keep it short. The
|
|
95
|
-
- `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
|
|
96
|
-
- `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. `items` is a `ContextMenuItem[]` (`{ label, onSelect?, disabled? }`); pressing outside closes without selecting.
|
|
95
|
+
- `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.
|
|
96
|
+
- `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
|
+
- `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
|
+
- `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.
|
|
97
100
|
- `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`.
|
|
98
101
|
- `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`).
|
|
99
102
|
- `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
|
@@ -63,6 +63,14 @@ Corner radius is set once: `radius` in a theme definition is a single number, th
|
|
|
63
63
|
setTheme({ radius: 4 }) // sm 2, md 4, lg 6
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
### Motion
|
|
67
|
+
|
|
68
|
+
`motion` holds the three durations (ms) every built-in component transition draws from, so one theme edit retimes the whole package: `fast` (default 100) is press/hover feedback, `base` (150) the color and opacity fades - state changes, the theme cross-fade (a `setTheme` fades every themed color rather than snapping), popup enter/exit - and `slow` (250) the travel of a control's moving parts (switch knob, segmented indicator, progress fill). `policy.motion` gates whether these play at all; a per-instance `transition` prop overrides them per property.
|
|
69
|
+
|
|
70
|
+
```jsx
|
|
71
|
+
setTheme({ motion: { base: 250, slow: 400 } }) // a slower, calmer app
|
|
72
|
+
```
|
|
73
|
+
|
|
66
74
|
### Per-component overrides
|
|
67
75
|
|
|
68
76
|
`theme.components` restyles a component everywhere without wrapping it: a `StyleProps` object per component name, merged between the component's themed defaults and each instance's `style` prop (instance style still wins).
|
|
@@ -93,7 +101,7 @@ The fields:
|
|
|
93
101
|
|
|
94
102
|
- `interaction` (`"touch" | "desktop" | "hybrid"`) - which affordances a component shows (hover states vs. long-press). `Tooltip`, `Select`, and `ContextMenu` fork on it.
|
|
95
103
|
- `density` (`"comfortable" | "compact" | "dense"`) - control/hit-target/spacing scale; drives `densityScale()` (1 / 0.85 / 0.7). A `<Density>` region overrides it per subtree.
|
|
96
|
-
- `motion` (`"normal" | "reduced" | "none"`) - animation intensity.
|
|
104
|
+
- `motion` (`"normal" | "reduced" | "none"`) - animation intensity. Gates every built-in component transition: `reduced` keeps the color/opacity fades (a fade is not movement) but snaps everything that travels or scales, and halves the indeterminate `Spinner`/`ProgressBar` speeds; `none` snaps it all and parks the indeterminate loops.
|
|
97
105
|
- `focusRing` (`boolean`) - whether focused controls draw a visible focus indicator (true when a keyboard or gamepad/remote is present).
|
|
98
106
|
- `textScale` (`number`) - multiplier on type-scale font sizes; defaults to the OS text-scale preference.
|
|
99
107
|
- `textWeightDelta` (`number`) - weight compensation (steps of 100) for light-on-dark text on low-DPI displays.
|
|
@@ -117,7 +125,9 @@ Most components group their props into two objects, split by one rule: `layout`
|
|
|
117
125
|
|
|
118
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).
|
|
119
127
|
|
|
120
|
-
Controls
|
|
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
|
+
|
|
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.
|
|
121
131
|
|
|
122
132
|
API: `StyleProps`, `TextLayoutProps`, `Option`, `TransitionProps`, `ComponentTransition`, `TransitionViewProp`, `TransitionStyleProp`, `TransitionScrollProp` - typed and commented in [src/types.ts](./src/types.ts).
|
|
123
133
|
|
|
@@ -354,7 +364,7 @@ API: `Pressable`, `PressableProps`, `PressState` - typed and commented in [src/p
|
|
|
354
364
|
|
|
355
365
|
### Button
|
|
356
366
|
|
|
357
|
-
A themed press target over `Pressable`: a padded, centered box with a label. `variant` picks the visual role - `primary` (accent fill, the default), `secondary`, `ghost` (no fill until hover), `danger` (destructive) - with fill,
|
|
367
|
+
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, ...).
|
|
358
368
|
|
|
359
369
|
```jsx
|
|
360
370
|
import { Button } from "@solidrt/components"
|
|
@@ -372,34 +382,47 @@ An `onPress` returning a promise makes the button an async action: while it is u
|
|
|
372
382
|
<Button onPress={async () => { await save() }}>Save</Button>
|
|
373
383
|
```
|
|
374
384
|
|
|
375
|
-
A focused Button (see `createFocusNav`) draws a ring in the theme `ring` color under the `focusRing` policy (text-colored by default so it stays visible on primary-filled buttons) and activates on Enter, Space,
|
|
385
|
+
A focused Button (see `createFocusNav`) draws a ring in the theme `ring` color under the `focusRing` policy (text-colored by default so it stays visible on primary-filled buttons) and activates on the navigation's `select` action (Enter, Space, a remote's center key or a pad's south button under the standard bindings). `focusable` (default true) opts out of focus-navigation candidacy; disabled buttons are never candidates.
|
|
376
386
|
|
|
377
387
|
API: `Button`, `ButtonProps`, `ButtonVariant` - typed and commented in [src/button.tsx](./src/button.tsx).
|
|
378
388
|
|
|
379
389
|
### createFocusNav
|
|
380
390
|
|
|
381
|
-
Focus navigation for pointer-free control (TV remote, keyboard, gamepad), moving real focus across the elements declaring `focusable`.
|
|
391
|
+
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.
|
|
382
392
|
|
|
383
393
|
```jsx
|
|
384
394
|
import { createFocusNav } from "@solidrt/components"
|
|
385
395
|
|
|
386
396
|
function App() {
|
|
387
397
|
let nav = createFocusNav()
|
|
388
|
-
return <
|
|
398
|
+
return <Window {...nav.handlers}>...</Window>
|
|
389
399
|
}
|
|
390
400
|
```
|
|
391
401
|
|
|
392
|
-
|
|
402
|
+
Spreading the map's handlers on the window is what keeps it cooperative: key events bubble from the focused node, so a focused TextInput keeps its caret keys (and a typed Space) and navigation only sees what nothing else consumed. Pads need no handler; their sources poll.
|
|
403
|
+
|
|
404
|
+
Rebind on the nav's own map, or bring a map that also holds the app's actions:
|
|
405
|
+
|
|
406
|
+
```jsx
|
|
407
|
+
let nav = createFocusNav()
|
|
408
|
+
nav.input.bind("select", gamepad().button("east"))
|
|
409
|
+
|
|
410
|
+
let input = createInputMap({ ...uiActions, pause: "button" })
|
|
411
|
+
input.bind(uiBindings({ keyboard, gamepad: gamepad() }))
|
|
412
|
+
input.bind("pause", keyboard.key("Escape"), gamepad().button("start"))
|
|
413
|
+
let nav = createFocusNav({ input })
|
|
414
|
+
return <Window {...input.handlers}>...</Window>
|
|
415
|
+
```
|
|
393
416
|
|
|
394
417
|
When the focused control disappears (an action replacing it, a screen change), focus lands on the nearest candidate to where it sat as soon as the successor is laid out - the ring follows a Disconnect button into the Connect button that replaces it. A deliberate blur (tapping outside, dismissing the keyboard) stays blurred; the next press resumes at the nearest candidate.
|
|
395
418
|
|
|
396
419
|
An open `Modal` traps navigation inside itself with no extra wiring (topmost wins when stacked); pass `scope: () => nodeOrNull` to trap into some other subtree instead. `move`/`tab`/`activate` are exposed for custom triggers.
|
|
397
420
|
|
|
398
|
-
API: `createFocusNav`, `FocusNavOptions` - typed and commented in [src/focus-nav.ts](./src/focus-nav.ts).
|
|
421
|
+
API: `createFocusNav`, `uiActions`, `uiBindings`, `FocusNavOptions`, `UiActions`, `UiDevices` - typed and commented in [src/focus-nav.ts](./src/focus-nav.ts).
|
|
399
422
|
|
|
400
423
|
### Switch
|
|
401
424
|
|
|
402
|
-
An on/off toggle: the track fills with `primary` when on and `surfaceAlt` when off
|
|
425
|
+
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.
|
|
403
426
|
|
|
404
427
|
```jsx
|
|
405
428
|
import { Switch } from "@solidrt/components"
|
|
@@ -415,7 +438,7 @@ API: `Switch`, `SwitchProps` - typed and commented in [src/switch.tsx](./src/swi
|
|
|
415
438
|
|
|
416
439
|
### Checkbox
|
|
417
440
|
|
|
418
|
-
A checkbox: filled with `primary` and a drawn checkmark when checked, an empty bordered box otherwise. Controlled via `checked`/`onChange`, or uncontrolled via `defaultChecked`. The mark is the `theme.icons.check` slot when a theme sets one. `style` overrides the box colors, border, and radius.
|
|
441
|
+
A checkbox: filled with `primary` and a drawn checkmark when checked, an empty bordered box otherwise - the fill fades and the mark pops in and out. Controlled via `checked`/`onChange`, or uncontrolled via `defaultChecked`. The mark is the `theme.icons.check` slot when a theme sets one. `style` overrides the box colors, border, and radius.
|
|
419
442
|
|
|
420
443
|
```jsx
|
|
421
444
|
import { Checkbox } from "@solidrt/components"
|
|
@@ -427,7 +450,7 @@ API: `Checkbox`, `CheckboxProps` - typed and commented in [src/checkbox.tsx](./s
|
|
|
427
450
|
|
|
428
451
|
### RadioGroup / Radio
|
|
429
452
|
|
|
430
|
-
A single-selection pair: `RadioGroup` owns the selected value (controlled via `value`/`onChange`, or uncontrolled via `defaultValue`) and shares it with its `Radio` children; each `Radio` is a ring with an inner dot when selected. A string/number child of `Radio` renders as a themed label beside the ring; anything else as-is. `disabled` on the group disables every option, on a `Radio` just that one.
|
|
453
|
+
A single-selection pair: `RadioGroup` owns the selected value (controlled via `value`/`onChange`, or uncontrolled via `defaultValue`) and shares it with its `Radio` children; each `Radio` is a ring with an inner dot when selected - the ring color fades and the dot pops in and out. A string/number child of `Radio` renders as a themed label beside the ring; anything else as-is. `disabled` on the group disables every option, on a `Radio` just that one.
|
|
431
454
|
|
|
432
455
|
```jsx
|
|
433
456
|
import { RadioGroup, Radio } from "@solidrt/components"
|
|
@@ -550,7 +573,7 @@ API: `Spinner`, `SpinnerProps` - typed and commented in [src/spinner.tsx](./src/
|
|
|
550
573
|
|
|
551
574
|
### ProgressBar
|
|
552
575
|
|
|
553
|
-
A horizontal progress bar: determinate when given a `value` in `[0, 1]` (the fill grows from the left), 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).
|
|
576
|
+
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).
|
|
554
577
|
|
|
555
578
|
```jsx
|
|
556
579
|
import { ProgressBar } from "@solidrt/components"
|
|
@@ -581,7 +604,7 @@ API: `Portal`, `PortalProps` - typed and commented in [src/portal.tsx](./src/por
|
|
|
581
604
|
|
|
582
605
|
### Modal
|
|
583
606
|
|
|
584
|
-
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. 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.
|
|
607
|
+
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.
|
|
585
608
|
|
|
586
609
|
```jsx
|
|
587
610
|
import { Modal, Card, Button } from "@solidrt/components"
|
|
@@ -599,7 +622,7 @@ API: `Modal`, `ModalProps` - typed and commented in [src/modal.tsx](./src/modal.
|
|
|
599
622
|
|
|
600
623
|
### Tooltip
|
|
601
624
|
|
|
602
|
-
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, 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"`).
|
|
625
|
+
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"`).
|
|
603
626
|
|
|
604
627
|
```jsx
|
|
605
628
|
import { Tooltip, Button } from "@solidrt/components"
|
|
@@ -613,7 +636,7 @@ API: `Tooltip`, `TooltipProps` - typed and commented in [src/tooltip.tsx](./src/
|
|
|
613
636
|
|
|
614
637
|
### Select
|
|
615
638
|
|
|
616
|
-
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. The option list is not scrollable yet, so keep it short. The
|
|
639
|
+
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.
|
|
617
640
|
|
|
618
641
|
```jsx
|
|
619
642
|
import { Select } from "@solidrt/components"
|
|
@@ -631,7 +654,7 @@ API: `Select`, `SelectProps` - typed and commented in [src/select.tsx](./src/sel
|
|
|
631
654
|
|
|
632
655
|
### SegmentedControl
|
|
633
656
|
|
|
634
|
-
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
|
|
657
|
+
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`.
|
|
635
658
|
|
|
636
659
|
```jsx
|
|
637
660
|
import { SegmentedControl } from "@solidrt/components"
|
|
@@ -647,7 +670,7 @@ API: `SegmentedControl`, `SegmentedControlProps` - typed and commented in [src/s
|
|
|
647
670
|
|
|
648
671
|
### ContextMenu
|
|
649
672
|
|
|
650
|
-
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. `items` is a `ContextMenuItem[]` (`{ label, onSelect?, disabled? }`); pressing outside closes without selecting.
|
|
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.
|
|
651
674
|
|
|
652
675
|
```jsx
|
|
653
676
|
import { ContextMenu } from "@solidrt/components"
|
package/docs/button.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Button
|
|
2
2
|
|
|
3
|
-
A themed press target over `Pressable`: a padded, centered box with a label. `variant` picks the visual role - `primary` (accent fill, the default), `secondary`, `ghost` (no fill until hover), `danger` (destructive) - with fill,
|
|
3
|
+
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, ...).
|
|
4
4
|
|
|
5
5
|
```jsx
|
|
6
6
|
import { Button } from "@solidrt/components"
|
|
@@ -18,4 +18,4 @@ An `onPress` returning a promise makes the button an async action: while it is u
|
|
|
18
18
|
<Button onPress={async () => { await save() }}>Save</Button>
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
A focused Button (see `createFocusNav`) draws a ring in the theme `ring` color under the `focusRing` policy (text-colored by default so it stays visible on primary-filled buttons) and activates on Enter, Space,
|
|
21
|
+
A focused Button (see `createFocusNav`) draws a ring in the theme `ring` color under the `focusRing` policy (text-colored by default so it stays visible on primary-filled buttons) and activates on the navigation's `select` action (Enter, Space, a remote's center key or a pad's south button under the standard bindings). `focusable` (default true) opts out of focus-navigation candidacy; disabled buttons are never candidates.
|
package/docs/checkbox.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Checkbox
|
|
2
2
|
|
|
3
|
-
A checkbox: filled with `primary` and a drawn checkmark when checked, an empty bordered box otherwise. Controlled via `checked`/`onChange`, or uncontrolled via `defaultChecked`. The mark is the `theme.icons.check` slot when a theme sets one. `style` overrides the box colors, border, and radius.
|
|
3
|
+
A checkbox: filled with `primary` and a drawn checkmark when checked, an empty bordered box otherwise - the fill fades and the mark pops in and out. Controlled via `checked`/`onChange`, or uncontrolled via `defaultChecked`. The mark is the `theme.icons.check` slot when a theme sets one. `style` overrides the box colors, border, and radius.
|
|
4
4
|
|
|
5
5
|
```jsx
|
|
6
6
|
import { Checkbox } from "@solidrt/components"
|
package/docs/context-menu.md
CHANGED
|
@@ -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. `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 (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.
|
|
4
4
|
|
|
5
5
|
```jsx
|
|
6
6
|
import { ContextMenu } from "@solidrt/components"
|
package/docs/focus-nav.md
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
# createFocusNav
|
|
2
2
|
|
|
3
|
-
Focus navigation for pointer-free control (TV remote, keyboard, gamepad), moving real focus across the elements declaring `focusable`.
|
|
3
|
+
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.
|
|
4
4
|
|
|
5
5
|
```jsx
|
|
6
6
|
import { createFocusNav } from "@solidrt/components"
|
|
7
7
|
|
|
8
8
|
function App() {
|
|
9
9
|
let nav = createFocusNav()
|
|
10
|
-
return <
|
|
10
|
+
return <Window {...nav.handlers}>...</Window>
|
|
11
11
|
}
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Spreading the map's handlers on the window is what keeps it cooperative: key events bubble from the focused node, so a focused TextInput keeps its caret keys (and a typed Space) and navigation only sees what nothing else consumed. Pads need no handler; their sources poll.
|
|
15
|
+
|
|
16
|
+
Rebind on the nav's own map, or bring a map that also holds the app's actions:
|
|
17
|
+
|
|
18
|
+
```jsx
|
|
19
|
+
let nav = createFocusNav()
|
|
20
|
+
nav.input.bind("select", gamepad().button("east"))
|
|
21
|
+
|
|
22
|
+
let input = createInputMap({ ...uiActions, pause: "button" })
|
|
23
|
+
input.bind(uiBindings({ keyboard, gamepad: gamepad() }))
|
|
24
|
+
input.bind("pause", keyboard.key("Escape"), gamepad().button("start"))
|
|
25
|
+
let nav = createFocusNav({ input })
|
|
26
|
+
return <Window {...input.handlers}>...</Window>
|
|
27
|
+
```
|
|
15
28
|
|
|
16
29
|
When the focused control disappears (an action replacing it, a screen change), focus lands on the nearest candidate to where it sat as soon as the successor is laid out - the ring follows a Disconnect button into the Connect button that replaces it. A deliberate blur (tapping outside, dismissing the keyboard) stays blurred; the next press resumes at the nearest candidate.
|
|
17
30
|
|
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. 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 calls `onClose` (unless `dismissable` is false), 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"
|
package/docs/policy.md
CHANGED
|
@@ -6,7 +6,7 @@ The fields:
|
|
|
6
6
|
|
|
7
7
|
- `interaction` (`"touch" | "desktop" | "hybrid"`) - which affordances a component shows (hover states vs. long-press). `Tooltip`, `Select`, and `ContextMenu` fork on it.
|
|
8
8
|
- `density` (`"comfortable" | "compact" | "dense"`) - control/hit-target/spacing scale; drives `densityScale()` (1 / 0.85 / 0.7). A `<Density>` region overrides it per subtree.
|
|
9
|
-
- `motion` (`"normal" | "reduced" | "none"`) - animation intensity.
|
|
9
|
+
- `motion` (`"normal" | "reduced" | "none"`) - animation intensity. Gates every built-in component transition: `reduced` keeps the color/opacity fades (a fade is not movement) but snaps everything that travels or scales, and halves the indeterminate `Spinner`/`ProgressBar` speeds; `none` snaps it all and parks the indeterminate loops.
|
|
10
10
|
- `focusRing` (`boolean`) - whether focused controls draw a visible focus indicator (true when a keyboard or gamepad/remote is present).
|
|
11
11
|
- `textScale` (`number`) - multiplier on type-scale font sizes; defaults to the OS text-scale preference.
|
|
12
12
|
- `textWeightDelta` (`number`) - weight compensation (steps of 100) for light-on-dark text on low-DPI displays.
|
package/docs/progress-bar.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ProgressBar
|
|
2
2
|
|
|
3
|
-
A horizontal progress bar: determinate when given a `value` in `[0, 1]` (the fill grows from the left), 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).
|
|
3
|
+
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).
|
|
4
4
|
|
|
5
5
|
```jsx
|
|
6
6
|
import { ProgressBar } from "@solidrt/components"
|
package/docs/radio.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# RadioGroup / Radio
|
|
2
2
|
|
|
3
|
-
A single-selection pair: `RadioGroup` owns the selected value (controlled via `value`/`onChange`, or uncontrolled via `defaultValue`) and shares it with its `Radio` children; each `Radio` is a ring with an inner dot when selected. A string/number child of `Radio` renders as a themed label beside the ring; anything else as-is. `disabled` on the group disables every option, on a `Radio` just that one.
|
|
3
|
+
A single-selection pair: `RadioGroup` owns the selected value (controlled via `value`/`onChange`, or uncontrolled via `defaultValue`) and shares it with its `Radio` children; each `Radio` is a ring with an inner dot when selected - the ring color fades and the dot pops in and out. A string/number child of `Radio` renders as a themed label beside the ring; anything else as-is. `disabled` on the group disables every option, on a `Radio` just that one.
|
|
4
4
|
|
|
5
5
|
```jsx
|
|
6
6
|
import { RadioGroup, Radio } from "@solidrt/components"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SegmentedControl
|
|
2
2
|
|
|
3
|
-
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
|
|
3
|
+
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`.
|
|
4
4
|
|
|
5
5
|
```jsx
|
|
6
6
|
import { SegmentedControl } from "@solidrt/components"
|
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. The option list is not scrollable yet, so keep it short. The
|
|
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, 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"
|
package/docs/switch.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Switch
|
|
2
2
|
|
|
3
|
-
An on/off toggle: the track fills with `primary` when on and `surfaceAlt` when off
|
|
3
|
+
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.
|
|
4
4
|
|
|
5
5
|
```jsx
|
|
6
6
|
import { Switch } from "@solidrt/components"
|
package/docs/theme.md
CHANGED
|
@@ -47,6 +47,14 @@ Corner radius is set once: `radius` in a theme definition is a single number, th
|
|
|
47
47
|
setTheme({ radius: 4 }) // sm 2, md 4, lg 6
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## Motion
|
|
51
|
+
|
|
52
|
+
`motion` holds the three durations (ms) every built-in component transition draws from, so one theme edit retimes the whole package: `fast` (default 100) is press/hover feedback, `base` (150) the color and opacity fades - state changes, the theme cross-fade (a `setTheme` fades every themed color rather than snapping), popup enter/exit - and `slow` (250) the travel of a control's moving parts (switch knob, segmented indicator, progress fill). `policy.motion` gates whether these play at all; a per-instance `transition` prop overrides them per property.
|
|
53
|
+
|
|
54
|
+
```jsx
|
|
55
|
+
setTheme({ motion: { base: 250, slow: 400 } }) // a slower, calmer app
|
|
56
|
+
```
|
|
57
|
+
|
|
50
58
|
## Per-component overrides
|
|
51
59
|
|
|
52
60
|
`theme.components` restyles a component everywhere without wrapping it: a `StyleProps` object per component name, merged between the component's themed defaults and each instance's `style` prop (instance style still wins).
|
package/docs/tooltip.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Tooltip
|
|
2
2
|
|
|
3
|
-
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, 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"`).
|
|
3
|
+
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"`).
|
|
4
4
|
|
|
5
5
|
```jsx
|
|
6
6
|
import { Tooltip, Button } from "@solidrt/components"
|
package/docs/types.md
CHANGED
|
@@ -6,4 +6,6 @@ Most components group their props into two objects, split by one rule: `layout`
|
|
|
6
6
|
|
|
7
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).
|
|
8
8
|
|
|
9
|
-
Controls
|
|
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
|
+
|
|
11
|
+
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/components",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.57",
|
|
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.
|
|
24
|
-
"@solidrt/core": "0.0.
|
|
23
|
+
"@solidjs/signals": "2.0.0-rc.6",
|
|
24
|
+
"@solidrt/core": "0.0.57"
|
|
25
25
|
}
|
|
26
26
|
}
|
package/src/badge.tsx
CHANGED
|
@@ -5,7 +5,8 @@ import { space } from "./spacing"
|
|
|
5
5
|
import { typeStyle, typeWeight, lightOnDark } from "./typography"
|
|
6
6
|
import { policy } from "./policy"
|
|
7
7
|
import type { StyleProps, TransitionProps } from "./types"
|
|
8
|
-
import { splitTransition, transitionEndFor } from "./types"
|
|
8
|
+
import { splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
|
|
9
|
+
import { colorFade } from "./motion"
|
|
9
10
|
|
|
10
11
|
export type BadgeVariant = "primary" | "neutral" | "danger"
|
|
11
12
|
|
|
@@ -65,9 +66,10 @@ export function Badge(props: BadgeProps) {
|
|
|
65
66
|
rotate={styled().rotate}
|
|
66
67
|
opacity={styled().opacity}
|
|
67
68
|
>
|
|
68
|
-
<d-rect transition={split().background} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={bg()} radius={radius()} />
|
|
69
|
+
<d-rect transition={withTransitionDefaults(split().background, colorFade())} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={bg()} radius={radius()} />
|
|
69
70
|
<Show when={isText()} fallback={resolved()}>
|
|
70
71
|
<text
|
|
72
|
+
transition={colorFade()}
|
|
71
73
|
color={fg()}
|
|
72
74
|
{...typeStyle("caption", labelOnDark())}
|
|
73
75
|
fontWeight={typeWeight(600, theme.text.caption.size * policy.textScale, labelOnDark())}
|
package/src/button.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Show, children } from "@solidrt/core"
|
|
2
|
-
import { createPress
|
|
1
|
+
import { Show, children, withAlpha } from "@solidrt/core"
|
|
2
|
+
import { createPress } from "./press"
|
|
3
3
|
import { theme } from "./theme"
|
|
4
4
|
import { policy } from "./policy"
|
|
5
5
|
import { space } from "./spacing"
|
|
@@ -7,7 +7,8 @@ import { typeStyle, lightOnDark } from "./typography"
|
|
|
7
7
|
import { Spinner } from "./spinner"
|
|
8
8
|
import type { LayoutProps } from "@solidrt/core"
|
|
9
9
|
import type { StyleProps, TransitionProps } from "./types"
|
|
10
|
-
import { splitTransition, transitionEndFor } from "./types"
|
|
10
|
+
import { splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
|
|
11
|
+
import { colorFade, scaleFeedback, pressScale, PressFeedback } from "./motion"
|
|
11
12
|
|
|
12
13
|
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger"
|
|
13
14
|
export type ButtonSize = "sm" | "md" | "lg"
|
|
@@ -42,8 +43,8 @@ export interface ButtonProps extends TransitionProps {
|
|
|
42
43
|
const SIZE_WIDTH: Record<ButtonSize, number> = { sm: 88, md: 120, lg: 160 }
|
|
43
44
|
|
|
44
45
|
// A themed press target: a padded, centered, accent-colored box with a label.
|
|
45
|
-
// Press feedback is a slight scale
|
|
46
|
-
//
|
|
46
|
+
// Press feedback is a slight sprung scale plus the overlayPressed tint, hover
|
|
47
|
+
// feedback the overlayHover tint (non-touch interaction policies only), all
|
|
47
48
|
// reactive reads of the press state so no nodes are recreated. Override the
|
|
48
49
|
// box via style and the padding/sizing via layout; because hover is an
|
|
49
50
|
// overlay, it composes over a caller-set backgroundColor too. When disabled,
|
|
@@ -75,10 +76,6 @@ export function Button(props: ButtonProps) {
|
|
|
75
76
|
: theme.color.surface
|
|
76
77
|
: colors().fill
|
|
77
78
|
let bg = () => styled().backgroundColor ?? idleFill()
|
|
78
|
-
// The hover feedback: the theme's overlay tint drawn over the fill, so it
|
|
79
|
-
// composes with any backgroundColor (variant, theme override, or caller).
|
|
80
|
-
let overlay = (s: PressState) =>
|
|
81
|
-
s.hovered && !props.disabled && policy.interaction !== "touch" ? theme.color.overlayHover : "transparent"
|
|
82
79
|
let radius = () => styled().borderRadius ?? theme.radius.md
|
|
83
80
|
let label = () => (props.disabled ? theme.color.textMuted : colors().label)
|
|
84
81
|
// Resolved once via children(): reading the raw children getter builds a new
|
|
@@ -101,14 +98,14 @@ export function Button(props: ButtonProps) {
|
|
|
101
98
|
// Always a number: a scale that flips from a number back to undefined
|
|
102
99
|
// hits the transform decoder, which rejects null. Multiply so a
|
|
103
100
|
// caller-set scale is preserved under the press feedback.
|
|
104
|
-
scale: (styled().scale ?? 1) * (press.pressed()
|
|
101
|
+
scale: (styled().scale ?? 1) * pressScale(press.pressed()),
|
|
105
102
|
})
|
|
106
103
|
|
|
107
104
|
let split = () => splitTransition(props.transition)
|
|
108
105
|
|
|
109
106
|
return (
|
|
110
107
|
<view
|
|
111
|
-
transition={split().root}
|
|
108
|
+
transition={withTransitionDefaults(split().root, scaleFeedback())}
|
|
112
109
|
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
113
110
|
ref={(n: { id: number }) => {
|
|
114
111
|
press.ref(n)
|
|
@@ -134,10 +131,14 @@ export function Button(props: ButtonProps) {
|
|
|
134
131
|
focusable={(props.focusable ?? true) && props.disabled !== true}
|
|
135
132
|
pointerEvents={props.disabled ? "none" : undefined}
|
|
136
133
|
>
|
|
137
|
-
<d-rect transition={split().background} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={style().backgroundColor ?? "transparent"} radius={style().borderRadius} />
|
|
138
|
-
<
|
|
134
|
+
<d-rect transition={withTransitionDefaults(split().background, colorFade())} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={style().backgroundColor ?? "transparent"} radius={style().borderRadius} />
|
|
135
|
+
<PressFeedback
|
|
136
|
+
pressed={press.pressed() && !props.disabled}
|
|
137
|
+
hovered={press.hovered() && !props.disabled && policy.interaction !== "touch"}
|
|
138
|
+
radius={style().borderRadius}
|
|
139
|
+
/>
|
|
139
140
|
<Show when={isText()} fallback={resolved()}>
|
|
140
|
-
<text color={press.pending() ?
|
|
141
|
+
<text transition={colorFade()} color={press.pending() ? withAlpha(label(), 0) : label()} {...typeStyle("body", labelOnDark())}>
|
|
141
142
|
{resolved()}
|
|
142
143
|
</text>
|
|
143
144
|
</Show>
|
|
@@ -149,7 +150,7 @@ export function Button(props: ButtonProps) {
|
|
|
149
150
|
<Show when={(style().borderWidth ?? 0) > 0}>
|
|
150
151
|
<d-rect
|
|
151
152
|
drawStyle="stroke"
|
|
152
|
-
transition={split().border}
|
|
153
|
+
transition={withTransitionDefaults(split().border, colorFade())}
|
|
153
154
|
onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
154
155
|
color={style().borderColor ?? "transparent"}
|
|
155
156
|
strokeWidth={style().borderWidth}
|