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