@delacour/react-native-ui 0.1.0-alpha.20260925053522
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/README.md +113 -0
- package/package.json +125 -0
- package/src/components/accordion/AGENTS.md +193 -0
- package/src/components/accordion/accordion-content.tsx +125 -0
- package/src/components/accordion/accordion-description.tsx +17 -0
- package/src/components/accordion/accordion-indicator.tsx +104 -0
- package/src/components/accordion/accordion-item.tsx +78 -0
- package/src/components/accordion/accordion-title.tsx +18 -0
- package/src/components/accordion/accordion-trigger.tsx +148 -0
- package/src/components/accordion/accordion.context.tsx +153 -0
- package/src/components/accordion/accordion.tsx +253 -0
- package/src/components/accordion/accordion.types.ts +11 -0
- package/src/components/accordion/accordion.variants.test.ts +434 -0
- package/src/components/accordion/accordion.variants.ts +358 -0
- package/src/components/accordion/index.ts +45 -0
- package/src/components/badge/AGENTS.md +83 -0
- package/src/components/badge/badge-close-button.tsx +48 -0
- package/src/components/badge/badge-end-content.tsx +15 -0
- package/src/components/badge/badge-label.tsx +24 -0
- package/src/components/badge/badge-start-content.tsx +16 -0
- package/src/components/badge/badge.context.tsx +64 -0
- package/src/components/badge/badge.tsx +192 -0
- package/src/components/badge/badge.types.ts +10 -0
- package/src/components/badge/badge.variants.test.ts +269 -0
- package/src/components/badge/badge.variants.ts +238 -0
- package/src/components/badge/index.ts +17 -0
- package/src/components/bottom-sheet/AGENTS.md +242 -0
- package/src/components/bottom-sheet/bottom-sheet-background.tsx +35 -0
- package/src/components/bottom-sheet/bottom-sheet-close.tsx +60 -0
- package/src/components/bottom-sheet/bottom-sheet-container.tsx +246 -0
- package/src/components/bottom-sheet/bottom-sheet-content.tsx +66 -0
- package/src/components/bottom-sheet/bottom-sheet-description.tsx +29 -0
- package/src/components/bottom-sheet/bottom-sheet-footer.tsx +150 -0
- package/src/components/bottom-sheet/bottom-sheet-handle.tsx +47 -0
- package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +180 -0
- package/src/components/bottom-sheet/bottom-sheet-portal.tsx +72 -0
- package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +105 -0
- package/src/components/bottom-sheet/bottom-sheet-title.tsx +24 -0
- package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +73 -0
- package/src/components/bottom-sheet/bottom-sheet.context.tsx +135 -0
- package/src/components/bottom-sheet/bottom-sheet.tsx +125 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +293 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +220 -0
- package/src/components/bottom-sheet/index.ts +40 -0
- package/src/components/bottom-sheet/use-bottom-sheet-input.ts +125 -0
- package/src/components/button/AGENTS.md +217 -0
- package/src/components/button/button-end-content.tsx +15 -0
- package/src/components/button/button-group-separator.tsx +37 -0
- package/src/components/button/button-group-text.tsx +101 -0
- package/src/components/button/button-group.tsx +158 -0
- package/src/components/button/button-label.tsx +24 -0
- package/src/components/button/button-start-content.tsx +15 -0
- package/src/components/button/button.context.tsx +187 -0
- package/src/components/button/button.tsx +273 -0
- package/src/components/button/button.types.ts +10 -0
- package/src/components/button/button.variants.test.ts +811 -0
- package/src/components/button/button.variants.ts +480 -0
- package/src/components/button/index.ts +50 -0
- package/src/components/chart/AGENTS.md +210 -0
- package/src/components/chart/chart-area.tsx +93 -0
- package/src/components/chart/chart-bar.tsx +103 -0
- package/src/components/chart/chart-bars.tsx +75 -0
- package/src/components/chart/chart-candlestick.tsx +60 -0
- package/src/components/chart/chart-grid.tsx +33 -0
- package/src/components/chart/chart-legend.tsx +41 -0
- package/src/components/chart/chart-line.tsx +50 -0
- package/src/components/chart/chart-scatter.tsx +35 -0
- package/src/components/chart/chart-tooltip-dot.tsx +70 -0
- package/src/components/chart/chart-tooltip-x.tsx +121 -0
- package/src/components/chart/chart-tooltip-y.tsx +55 -0
- package/src/components/chart/chart-tooltip.tsx +133 -0
- package/src/components/chart/chart-x-axis.tsx +27 -0
- package/src/components/chart/chart-y-axis.tsx +21 -0
- package/src/components/chart/chart.context.tsx +127 -0
- package/src/components/chart/chart.tsx +448 -0
- package/src/components/chart/chart.types.ts +108 -0
- package/src/components/chart/chart.variants.test.ts +893 -0
- package/src/components/chart/chart.variants.ts +726 -0
- package/src/components/chart/index.ts +103 -0
- package/src/components/chart/pie-chart-center.tsx +40 -0
- package/src/components/chart/pie-chart-label.tsx +56 -0
- package/src/components/chart/pie-chart-slice.tsx +45 -0
- package/src/components/chart/pie-chart-tooltip.tsx +85 -0
- package/src/components/chart/pie-chart.context.tsx +77 -0
- package/src/components/chart/pie-chart.tsx +247 -0
- package/src/components/chart/use-chart-font.ts +25 -0
- package/src/components/chart/use-chart-palette.ts +38 -0
- package/src/components/checkbox/AGENTS.md +185 -0
- package/src/components/checkbox/checkbox-box.tsx +152 -0
- package/src/components/checkbox/checkbox-group.tsx +107 -0
- package/src/components/checkbox/checkbox-label.tsx +34 -0
- package/src/components/checkbox/checkbox.context.tsx +136 -0
- package/src/components/checkbox/checkbox.tsx +258 -0
- package/src/components/checkbox/checkbox.types.ts +14 -0
- package/src/components/checkbox/checkbox.variants.test.ts +634 -0
- package/src/components/checkbox/checkbox.variants.ts +484 -0
- package/src/components/checkbox/index.ts +42 -0
- package/src/components/field/AGENTS.md +112 -0
- package/src/components/field/field-content.tsx +24 -0
- package/src/components/field/field-description.tsx +29 -0
- package/src/components/field/field-error.tsx +61 -0
- package/src/components/field/field-group.tsx +22 -0
- package/src/components/field/field-label.tsx +47 -0
- package/src/components/field/field-legend.tsx +34 -0
- package/src/components/field/field-separator.tsx +81 -0
- package/src/components/field/field-set.tsx +22 -0
- package/src/components/field/field.context.tsx +95 -0
- package/src/components/field/field.tsx +157 -0
- package/src/components/field/field.types.ts +15 -0
- package/src/components/field/field.variants.test.ts +240 -0
- package/src/components/field/field.variants.ts +156 -0
- package/src/components/field/index.ts +22 -0
- package/src/components/icon/AGENTS.md +104 -0
- package/src/components/icon/icon.context.tsx +29 -0
- package/src/components/icon/icon.tsx +92 -0
- package/src/components/icon/icon.variants.test.ts +111 -0
- package/src/components/icon/icon.variants.ts +79 -0
- package/src/components/icon/index.ts +11 -0
- package/src/components/input/AGENTS.md +126 -0
- package/src/components/input/index.ts +27 -0
- package/src/components/input/input-group-decorator.tsx +84 -0
- package/src/components/input/input-group-prefix.tsx +16 -0
- package/src/components/input/input-group-suffix.tsx +15 -0
- package/src/components/input/input-group.tsx +140 -0
- package/src/components/input/input.context.tsx +87 -0
- package/src/components/input/input.tsx +184 -0
- package/src/components/input/input.types.ts +10 -0
- package/src/components/input/input.variants.test.ts +369 -0
- package/src/components/input/input.variants.ts +329 -0
- package/src/components/list-group/AGENTS.md +67 -0
- package/src/components/list-group/index.ts +20 -0
- package/src/components/list-group/list-group-item-content.tsx +10 -0
- package/src/components/list-group/list-group-item-description.tsx +12 -0
- package/src/components/list-group/list-group-item-prefix.tsx +30 -0
- package/src/components/list-group/list-group-item-suffix.tsx +55 -0
- package/src/components/list-group/list-group-item-title.tsx +17 -0
- package/src/components/list-group/list-group-item.tsx +87 -0
- package/src/components/list-group/list-group.context.tsx +65 -0
- package/src/components/list-group/list-group.tsx +116 -0
- package/src/components/list-group/list-group.types.ts +14 -0
- package/src/components/list-group/list-group.variants.test.ts +223 -0
- package/src/components/list-group/list-group.variants.ts +107 -0
- package/src/components/pressable/AGENTS.md +74 -0
- package/src/components/pressable/index.ts +9 -0
- package/src/components/pressable/pressable.tsx +261 -0
- package/src/components/pressable/pressable.variants.test.ts +128 -0
- package/src/components/pressable/pressable.variants.ts +80 -0
- package/src/components/provider/AGENTS.md +90 -0
- package/src/components/provider/index.ts +1 -0
- package/src/components/provider/provider.tsx +105 -0
- package/src/components/radio/AGENTS.md +245 -0
- package/src/components/radio/index.ts +35 -0
- package/src/components/radio/radio-group.tsx +126 -0
- package/src/components/radio/radio-indicator.tsx +91 -0
- package/src/components/radio/radio-label.tsx +39 -0
- package/src/components/radio/radio.context.tsx +132 -0
- package/src/components/radio/radio.tsx +215 -0
- package/src/components/radio/radio.variants.test.ts +580 -0
- package/src/components/radio/radio.variants.ts +271 -0
- package/src/components/screen/AGENTS.md +287 -0
- package/src/components/screen/index.ts +85 -0
- package/src/components/screen/screen-chat-list.tsx +485 -0
- package/src/components/screen/screen-content.tsx +69 -0
- package/src/components/screen/screen-debug.ts +43 -0
- package/src/components/screen/screen-error.tsx +61 -0
- package/src/components/screen/screen-flat-list.tsx +88 -0
- package/src/components/screen/screen-footer-background.tsx +60 -0
- package/src/components/screen/screen-footer.tsx +170 -0
- package/src/components/screen/screen-header.tsx +36 -0
- package/src/components/screen/screen-legend-list.tsx +105 -0
- package/src/components/screen/screen-list-component.tsx +23 -0
- package/src/components/screen/screen-loading.tsx +50 -0
- package/src/components/screen/screen-navbar-back-button.tsx +79 -0
- package/src/components/screen/screen-navbar-background.tsx +45 -0
- package/src/components/screen/screen-navbar-subtitle.tsx +26 -0
- package/src/components/screen/screen-navbar-title.tsx +31 -0
- package/src/components/screen/screen-navbar.tsx +154 -0
- package/src/components/screen/screen-root.tsx +38 -0
- package/src/components/screen/screen-scroll-area.tsx +116 -0
- package/src/components/screen/screen-scroll-shadow.tsx +186 -0
- package/src/components/screen/screen-section-list.tsx +81 -0
- package/src/components/screen/screen-view.tsx +59 -0
- package/src/components/screen/screen.context.tsx +167 -0
- package/src/components/screen/screen.tsx +114 -0
- package/src/components/screen/screen.types.ts +66 -0
- package/src/components/screen/screen.variants.test.ts +602 -0
- package/src/components/screen/screen.variants.ts +475 -0
- package/src/components/screen/use-screen-scroll-insets.ts +218 -0
- package/src/components/separator/AGENTS.md +30 -0
- package/src/components/separator/index.ts +8 -0
- package/src/components/separator/separator.tsx +85 -0
- package/src/components/slider/AGENTS.md +274 -0
- package/src/components/slider/index.ts +50 -0
- package/src/components/slider/slider-fill.tsx +68 -0
- package/src/components/slider/slider-output.tsx +55 -0
- package/src/components/slider/slider-thumb.tsx +193 -0
- package/src/components/slider/slider-track.tsx +233 -0
- package/src/components/slider/slider.context.tsx +118 -0
- package/src/components/slider/slider.tsx +321 -0
- package/src/components/slider/slider.types.ts +26 -0
- package/src/components/slider/slider.variants.test.ts +856 -0
- package/src/components/slider/slider.variants.ts +661 -0
- package/src/components/spinner/AGENTS.md +73 -0
- package/src/components/spinner/index.ts +17 -0
- package/src/components/spinner/spinner-arc.tsx +90 -0
- package/src/components/spinner/spinner-content.tsx +63 -0
- package/src/components/spinner/spinner.context.tsx +52 -0
- package/src/components/spinner/spinner.tsx +128 -0
- package/src/components/spinner/spinner.variants.test.ts +273 -0
- package/src/components/spinner/spinner.variants.ts +187 -0
- package/src/components/switch/AGENTS.md +213 -0
- package/src/components/switch/index.ts +42 -0
- package/src/components/switch/switch-content.tsx +111 -0
- package/src/components/switch/switch-end-content.tsx +18 -0
- package/src/components/switch/switch-start-content.tsx +20 -0
- package/src/components/switch/switch-thumb.tsx +102 -0
- package/src/components/switch/switch.context.tsx +79 -0
- package/src/components/switch/switch.tsx +423 -0
- package/src/components/switch/switch.types.ts +14 -0
- package/src/components/switch/switch.variants.test.ts +570 -0
- package/src/components/switch/switch.variants.ts +511 -0
- package/src/components/tabs/AGENTS.md +287 -0
- package/src/components/tabs/index.ts +69 -0
- package/src/components/tabs/tabs-content.tsx +65 -0
- package/src/components/tabs/tabs-indicator.tsx +97 -0
- package/src/components/tabs/tabs-label.tsx +62 -0
- package/src/components/tabs/tabs-list.tsx +139 -0
- package/src/components/tabs/tabs-pager.tsx +59 -0
- package/src/components/tabs/tabs-scroll-view.tsx +171 -0
- package/src/components/tabs/tabs-separator.tsx +73 -0
- package/src/components/tabs/tabs-trigger.tsx +210 -0
- package/src/components/tabs/tabs.context.tsx +294 -0
- package/src/components/tabs/tabs.tsx +435 -0
- package/src/components/tabs/tabs.types.ts +13 -0
- package/src/components/tabs/tabs.variants.test.ts +1020 -0
- package/src/components/tabs/tabs.variants.ts +670 -0
- package/src/components/text/AGENTS.md +95 -0
- package/src/components/text/index.ts +25 -0
- package/src/components/text/text.context.tsx +60 -0
- package/src/components/text/text.tsx +251 -0
- package/src/components/text/text.variants.test.ts +422 -0
- package/src/components/text/text.variants.ts +282 -0
- package/src/display-name.test.ts +145 -0
- package/src/docs.test.ts +98 -0
- package/src/expo/navigation-theme.tsx +58 -0
- package/src/hooks/use-controllable-state.ts +45 -0
- package/src/hooks/use-keyboard-state-sync.tsx +147 -0
- package/src/hooks/use-navigation-theme.ts +78 -0
- package/src/hooks/use-theme-color.ts +44 -0
- package/src/icons/central.ts +1 -0
- package/src/lib/cn.test.ts +136 -0
- package/src/lib/cn.ts +29 -0
- package/src/lib/color.test.ts +80 -0
- package/src/lib/color.ts +79 -0
- package/src/lib/compose-refs.test.ts +64 -0
- package/src/lib/compose-refs.ts +37 -0
- package/src/lib/keyboard-animation.test.ts +33 -0
- package/src/lib/keyboard-animation.ts +28 -0
- package/src/lib/merge-props.test.ts +82 -0
- package/src/lib/merge-props.ts +47 -0
- package/src/lib/navigation-theme.test.ts +49 -0
- package/src/lib/navigation-theme.ts +53 -0
- package/src/lib/slot.tsx +45 -0
- package/src/lib/tv.ts +20 -0
- package/src/styles/base.css +2 -0
- package/src/styles/geometry.test.ts +117 -0
- package/src/styles/index.css +3 -0
- package/src/styles/theme-tokens.test.ts +252 -0
- package/src/styles/theme.css +447 -0
- package/src/styles/tokens.css +132 -0
- package/src/styles/tokens.test.ts +181 -0
- package/src/styles/tokens.ts +86 -0
- package/src/uniwind-env.d.ts +1 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Separator
|
|
2
|
+
|
|
3
|
+
A one-pixel rule, hidden from assistive technology — a line between every row
|
|
4
|
+
carries nothing a screen reader can use, and announcing them buries the rows.
|
|
5
|
+
|
|
6
|
+
`import { Separator } from "@delacour/react-native-ui/separator";`
|
|
7
|
+
|
|
8
|
+
## Files
|
|
9
|
+
|
|
10
|
+
| File | What it holds |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `index.ts` | → `@delacour/react-native-ui/separator` |
|
|
13
|
+
| `separator.tsx` | The `tv()` and the component, in one file |
|
|
14
|
+
|
|
15
|
+
## Design
|
|
16
|
+
|
|
17
|
+
- **`separatorVariants` lives in `separator.tsx`**, above the component, not in a
|
|
18
|
+
`*.variants.ts` sibling — the pattern C carve-out. That file imports React
|
|
19
|
+
Native, so the `tv()` is not reachable from `bun test`; the exclusive-axis and
|
|
20
|
+
`self-stretch` rules below are stated in its doc comment instead of asserted.
|
|
21
|
+
- **Orientations**: `horizontal` (default), `vertical`.
|
|
22
|
+
- **The long axis is `self-stretch`, never `w-full` / `h-full`.** Yoga resolves
|
|
23
|
+
a percentage length against the parent's content box and then adds the
|
|
24
|
+
margins on top, so an inset `w-full` line starts 16pt in and runs 16pt past
|
|
25
|
+
the far edge — a gap down one side and none down the other. Stretching
|
|
26
|
+
subtracts the margins instead, which is what an inset divider needs. Do not
|
|
27
|
+
"fix" this back to a percentage width.
|
|
28
|
+
- **A filled box, not a border**, so a caller insets it with a plain `mx-*`
|
|
29
|
+
without fighting a border's own box model. This is how
|
|
30
|
+
[`ListGroup`](../list-group/AGENTS.md) positions the dividers it inserts.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import type { VariantProps } from "tailwind-variants";
|
|
4
|
+
import { tv } from "../../lib/tv";
|
|
5
|
+
|
|
6
|
+
export const SEPARATOR_ORIENTATIONS = ["horizontal", "vertical"] as const;
|
|
7
|
+
|
|
8
|
+
export type SeparatorOrientation = (typeof SEPARATOR_ORIENTATIONS)[number];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Styling for a separator.
|
|
12
|
+
*
|
|
13
|
+
* The line is a filled box rather than a border, so a caller can inset it with
|
|
14
|
+
* a plain `mx-*` / `my-*` without fighting a border's own box model — which is
|
|
15
|
+
* how `ListGroup` positions the dividers it inserts between its rows.
|
|
16
|
+
*
|
|
17
|
+
* The long axis is `self-stretch`, never `w-full` / `h-full`. Yoga resolves a
|
|
18
|
+
* percentage length against the parent's content box and then adds the margins
|
|
19
|
+
* on top, so an inset `w-full` line starts 16pt in and runs 16pt past the far
|
|
20
|
+
* edge — a gap down one side and none down the other. Stretching subtracts the
|
|
21
|
+
* margins instead, which is what an inset divider actually needs.
|
|
22
|
+
*
|
|
23
|
+
* The two orientations are exclusive: `horizontal` draws `h-px` and no `w-px`,
|
|
24
|
+
* `vertical` the reverse, so the hairline is only ever on one axis.
|
|
25
|
+
*
|
|
26
|
+
* Declared here rather than in a `*.variants.ts` sibling because a separator is
|
|
27
|
+
* a single styled element with no compound parts and no pure resolvers — there
|
|
28
|
+
* is no slot set for another file to share. The cost is that `bun test` cannot
|
|
29
|
+
* reach it: this module imports React Native, whose Flow-typed source Bun's
|
|
30
|
+
* transpiler cannot parse. See AGENTS.md.
|
|
31
|
+
*/
|
|
32
|
+
export const separatorVariants = tv({
|
|
33
|
+
base: "self-stretch bg-border",
|
|
34
|
+
variants: {
|
|
35
|
+
orientation: {
|
|
36
|
+
horizontal: "h-px",
|
|
37
|
+
vertical: "w-px",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
defaultVariants: {
|
|
41
|
+
orientation: "horizontal",
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type SeparatorVariantProps = VariantProps<typeof separatorVariants>;
|
|
46
|
+
|
|
47
|
+
export type SeparatorProps = ViewProps & {
|
|
48
|
+
/** Axis the line runs along. A vertical separator needs a parent with a height. */
|
|
49
|
+
orientation?: SeparatorOrientation;
|
|
50
|
+
className?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A one-pixel rule dividing content.
|
|
55
|
+
*
|
|
56
|
+
* Hidden from assistive technology: the line carries no information a screen
|
|
57
|
+
* reader can use, and announcing one between every row of a list would bury the
|
|
58
|
+
* rows themselves.
|
|
59
|
+
*
|
|
60
|
+
* `ListGroup` inserts these between its rows automatically, so this is written
|
|
61
|
+
* out by hand only for a divider elsewhere — or to place one inside a
|
|
62
|
+
* `ListGroup` deliberately, which suppresses the automatic one at that point.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* <Separator className="my-4" />
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* <View className="flex-row items-center gap-3">
|
|
69
|
+
* <Text>Left</Text>
|
|
70
|
+
* <Separator className="h-4" orientation="vertical" />
|
|
71
|
+
* <Text>Right</Text>
|
|
72
|
+
* </View>
|
|
73
|
+
*/
|
|
74
|
+
export function Separator({ orientation = "horizontal", className, ...props }: SeparatorProps): ReactElement {
|
|
75
|
+
return (
|
|
76
|
+
<View
|
|
77
|
+
accessibilityElementsHidden
|
|
78
|
+
accessible={false}
|
|
79
|
+
className={separatorVariants({ className, orientation })}
|
|
80
|
+
importantForAccessibility="no-hide-descendants"
|
|
81
|
+
{...props}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
Separator.displayName = "DelacourUI.Separator";
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Slider
|
|
2
|
+
|
|
3
|
+
A value picked by dragging along a track — one value, or a range. Compound root
|
|
4
|
+
plus `Slider.Output`, `Slider.Track`, `Slider.Fill` and `Slider.Thumb`. The
|
|
5
|
+
package's first drag-driven control, and its first `Gesture.Pan()`.
|
|
6
|
+
|
|
7
|
+
`import { Slider } from "@delacour/react-native-ui/slider";`
|
|
8
|
+
|
|
9
|
+
## Files
|
|
10
|
+
|
|
11
|
+
| File | What it holds |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `index.ts` | → `@delacour/react-native-ui/slider` |
|
|
14
|
+
| `slider.tsx` | Root + the `Object.assign` compound surface |
|
|
15
|
+
| `slider-output.tsx` | `Slider.Output`, the `Text.Label` readout |
|
|
16
|
+
| `slider-track.tsx` | `Slider.Track` — owns the pan, the measurement, the haptic |
|
|
17
|
+
| `slider-fill.tsx` | `Slider.Fill` |
|
|
18
|
+
| `slider-thumb.tsx` | `Slider.Thumb`, and the whole accessibility surface |
|
|
19
|
+
| `slider.context.tsx` | `SliderContext`, `useSlider()`, `useSliderContext()`, `useSliderPart()` |
|
|
20
|
+
| `slider.types.ts` | Prop types shared by two or more parts |
|
|
21
|
+
| `slider.variants.ts` | Pure `tv()` slots + the geometry worklets, no RN imports |
|
|
22
|
+
| `slider.variants.test.ts` | |
|
|
23
|
+
|
|
24
|
+
## Design
|
|
25
|
+
|
|
26
|
+
- **Colours**: `default`, `primary`, `success`, `warning`, `destructive`, `info` —
|
|
27
|
+
Badge's and Checkbox's set. **Sizes**: `sm`, `md`, `lg`, driving the groove's
|
|
28
|
+
thickness, the thumb's diameter and the readout's type step.
|
|
29
|
+
**Orientations**: `horizontal`, `vertical`.
|
|
30
|
+
- **The anatomy is written out, never assembled from props.** A range's thumb
|
|
31
|
+
count is *data*, so `Slider.Track` takes a function and is handed the settled
|
|
32
|
+
state to map over. [Radio](../radio/AGENTS.md) composes its own indicator in
|
|
33
|
+
because a radio has exactly one; a slider does not know how many it has until
|
|
34
|
+
it is told.
|
|
35
|
+
|
|
36
|
+
## Gesture
|
|
37
|
+
|
|
38
|
+
- **The root is not a `Pressable`, and neither is the thumb.** Three reasons and
|
|
39
|
+
all of them structural. [Pressable](../pressable/AGENTS.md) mounts a
|
|
40
|
+
`Gesture.Tap()` whose `onEnd` fires `onPress`, so every tap-to-position would
|
|
41
|
+
also fire a press. Its root `Animated.View` already owns `opacity` and
|
|
42
|
+
`transform` through a `useAnimatedStyle` of its own, and the thumb's position
|
|
43
|
+
*is* a `transform` — two animated styles on one node fight for the same prop,
|
|
44
|
+
the rule `Radio.Indicator` states and the reason `Radio` takes no `asChild`.
|
|
45
|
+
And a thumb wrapped in its own `Pressable` would nest a descendant `Tap`
|
|
46
|
+
inside the track's ancestor `Pan`, leaving two recognisers to negotiate for
|
|
47
|
+
one drag. What is inherited is the *vocabulary*: `HapticFeedback` and
|
|
48
|
+
`playHaptic` come from `pressable.tsx`, which is exported for exactly this —
|
|
49
|
+
one haptic switch in the library, never a second.
|
|
50
|
+
- **`Slider.Thumb` holds no gesture at all.** One pan on the track drives every
|
|
51
|
+
thumb, because a press 40pt along an empty groove should still lift the thumb it
|
|
52
|
+
is about to move, and a per-thumb gesture cannot know that. The grabbed scale is
|
|
53
|
+
therefore driven by an `activeIndex` shared value the track writes, not by a
|
|
54
|
+
press state the thumb owns.
|
|
55
|
+
- **The value is written in `onBegin`, not only in `onUpdate`.** This is the one
|
|
56
|
+
that will bite a rewrite. A pan activates on the first *movement*, so a
|
|
57
|
+
stationary tap never reaches `onStart` or `onUpdate`: a slider that computed
|
|
58
|
+
only there would tick, lift its thumb, and then not move it. `onFinalize` is
|
|
59
|
+
likewise where the drag is reported finished, because it is the only callback
|
|
60
|
+
that fires on every path, the never-activated one included.
|
|
61
|
+
- **`minDistance(0)` is what wins the touch from a scroll view**, and it is not
|
|
62
|
+
tuning. `Screen.ScrollArea` renders React Native's own `ScrollView`
|
|
63
|
+
(`Animated.ScrollView`), not Gesture Handler's, so there is no sibling handler
|
|
64
|
+
to negotiate with — the two race, and a pan that activates on the first move
|
|
65
|
+
beats a scroll view's ten-point slop on both platforms. Which is also why there
|
|
66
|
+
is **no `activeOffsetX`**: waiting for the axis to declare itself hands the
|
|
67
|
+
scroll the first move and puts a dead zone at the start of every drag.
|
|
68
|
+
**`blocksExternalGesture` is not the escape hatch it looks like** — it resolves
|
|
69
|
+
a ref to a handler tag, a plain `ScrollView` has none, and Gesture Handler drops
|
|
70
|
+
the call without an error. If Android ever hands a drag to the scroll anyway,
|
|
71
|
+
the documented fix is a nested `GestureHandlerRootView` around the slider, not
|
|
72
|
+
an offset filter.
|
|
73
|
+
- **`shouldCancelWhenOutside(false)`**, where `Pressable`'s tap sets it `true`.
|
|
74
|
+
Dragging a thumb to the far end routinely leaves the track's bounds, and the
|
|
75
|
+
value has to keep tracking rather than the gesture giving up half way.
|
|
76
|
+
- **On iOS, a slider inside a scroll view feels late until
|
|
77
|
+
`delaysContentTouches={false}`.** UIScrollView's default holds touch delivery to
|
|
78
|
+
its descendants for about 150ms while it decides whether the touch is a scroll.
|
|
79
|
+
Nothing inside this component can reach that — it is a prop on the scrollable,
|
|
80
|
+
so it belongs at the call site.
|
|
81
|
+
- **The groove is not the touch target on its own.** A `sm` track is sixteen
|
|
82
|
+
points, so the drag is claimed on a transparent `touchArea` whose padding brings
|
|
83
|
+
it up to 44. The thickness and that padding live in the **same compound cell**,
|
|
84
|
+
because they are one number: they sum to 44 at every size, and a test asserts
|
|
85
|
+
the sum rather than the parts so the ladder can be retuned without the test
|
|
86
|
+
becoming a transcript of it. Split them across two variants and a retune of the
|
|
87
|
+
thickness silently shrinks the target.
|
|
88
|
+
- **That padding is on the cross axis only**, and this is the load-bearing half.
|
|
89
|
+
The two boxes share an origin along the axis the value is measured on, so the
|
|
90
|
+
pan reads its offset straight off the touch with no gutter to correct for. Pad
|
|
91
|
+
the main axis and every value is wrong by the padding, silently, and visibly
|
|
92
|
+
only at the ends.
|
|
93
|
+
|
|
94
|
+
## Colour
|
|
95
|
+
|
|
96
|
+
- **The colour paints the fill, the capsule and the knob — never the groove.**
|
|
97
|
+
An empty groove is the same chrome at every colour, the way an unticked
|
|
98
|
+
checkbox is `border-input bg-card` however it is coloured, and a test asserts
|
|
99
|
+
that. Invalid outranks the colour on all three, the precedence
|
|
100
|
+
[Checkbox](../checkbox/AGENTS.md) sets on its border.
|
|
101
|
+
- **The capsule takes the fill's own colour, and the knob takes that colour's
|
|
102
|
+
`-foreground`.** The first is what makes the handle read as the leading end of
|
|
103
|
+
the fill rather than as something sitting on top of it — same colour, no seam.
|
|
104
|
+
The second is rule 11 doing its job: a single pale knob would be unreadable on
|
|
105
|
+
`warning`, whose foreground is near-black, so the knob follows the surface it
|
|
106
|
+
sits on. A test pins the pair rather than trusting two maps to stay in step, and
|
|
107
|
+
checks every token it names exists in both variants of `theme.css`.
|
|
108
|
+
- **`default` and `primary` name different tokens this theme tunes to the same
|
|
109
|
+
value.** `foreground` is the page's ink and `primary` is the brand's action
|
|
110
|
+
colour; both are `#262626` today, which is the situation
|
|
111
|
+
[Badge](../badge/AGENTS.md) already documents for its neutral end. Collapsing
|
|
112
|
+
them into one token would be the drift, not the duplication — an app that
|
|
113
|
+
re-themes `primary` to blue wants `color="primary"` blue and `color="default"`
|
|
114
|
+
still ink. A test pins that the four *semantic* colours stay distinct from
|
|
115
|
+
each other and from both neutrals, and that every token named is declared in
|
|
116
|
+
**both** variants of `theme.css`.
|
|
117
|
+
|
|
118
|
+
## The handle
|
|
119
|
+
|
|
120
|
+
- **The handle is two nodes: a capsule and a knob.** The capsule carries the
|
|
121
|
+
colour, the size and the position; the knob is the pale bar inside it, held off
|
|
122
|
+
the capsule's edge by its padding, and the only thing that moves when a finger
|
|
123
|
+
lands. One node could not be both the surface and the thing inset within it.
|
|
124
|
+
- **Neither takes a shadow**, and the capsule takes no border either. Nothing else
|
|
125
|
+
in this package draws a shadow, and React Native's shadow props diverge between
|
|
126
|
+
platforms in a way a flat fill does not; a test sweeps the whole matrix for the
|
|
127
|
+
absence. The capsule is a solid block of the fill's colour, so its edge is
|
|
128
|
+
already the boundary between the fill and the groove — a border would be a
|
|
129
|
+
second line drawn over one that is already there.
|
|
130
|
+
- **The capsule is flush across the track and longer along it.** The flush half is
|
|
131
|
+
geometry rather than decoration: it is what lets `fillExtent` land exactly on
|
|
132
|
+
both extremes — one capsule's length of fill at the minimum, so the handle covers
|
|
133
|
+
it completely and a slider at rest shows a plain groove; the track's full length
|
|
134
|
+
at the maximum, with no sliver past the handle; and one capsule's length again
|
|
135
|
+
for a collapsed range, so the fill does not blink out from under two handles
|
|
136
|
+
dragged together. Inset the capsule inside the track by any padding and every one
|
|
137
|
+
of those is off by the inset, at every size. The long axis is two steps up from
|
|
138
|
+
the short one, which is what makes a handle you can tell apart from the groove.
|
|
139
|
+
- **Both are plain spacing steps, not tokens.** They are numbers read in one
|
|
140
|
+
component, which is the trade `Radio` already makes for the dot inside its ring.
|
|
141
|
+
`Checkbox` reads `--spacing-icon-*` for its square and should keep doing so: a
|
|
142
|
+
glyph in a box is a mark on the icon scale, where a slider's handle is the body
|
|
143
|
+
of the control itself. This is why the handle stopped reading that scale.
|
|
144
|
+
- **The capsule's size lives in the same six compound cells as the track's
|
|
145
|
+
thickness**, not in a `size`-only variant, because its two axes differ and only
|
|
146
|
+
the orientation knows which is which.
|
|
147
|
+
|
|
148
|
+
## Geometry
|
|
149
|
+
|
|
150
|
+
- **The track still centres the thumb, and now has nothing to centre.** An
|
|
151
|
+
absolutely-positioned child with no cross-axis inset is placed at the static
|
|
152
|
+
position the parent's `items-center` decides. That did the work while the thumb
|
|
153
|
+
overhung a hairline groove; it is a no-op now the two are the same size. It
|
|
154
|
+
stays because it is load-bearing again the moment those sizes are allowed to
|
|
155
|
+
differ, and because it is why the track is `flex-row` when horizontal:
|
|
156
|
+
`items-center` centres on the *cross* axis, and a column track would centre the
|
|
157
|
+
wrong one.
|
|
158
|
+
- **Every length is measured, never tabulated.** `trackSize` and `thumbSize` come
|
|
159
|
+
from their own `onLayout` — `size-5` cannot be read from JavaScript, and a
|
|
160
|
+
table of numbers here would be `tokens.css` restated in TypeScript, the drift
|
|
161
|
+
`tokens.test.ts` exists to catch everywhere else. A measured `0` therefore means
|
|
162
|
+
**not measured yet**, never "a track with no length": every geometry helper
|
|
163
|
+
guards `travel <= 0` and the thumb renders at `opacity: 0` until the track has
|
|
164
|
+
reported, because a thumb drawn before then sits at a garbage offset for a frame
|
|
165
|
+
and reads as a flicker on every mount.
|
|
166
|
+
- **The fill runs to the thumb's far edge**, which is the `+ thumbSize` in
|
|
167
|
+
`fillExtent` and the reason both extremes come out exact. It used to stop at the
|
|
168
|
+
thumb's *centre*, which was correct but invisible while a large disc overhung a
|
|
169
|
+
hairline groove; with the thumb now the same size as the track, a half-thumb of
|
|
170
|
+
bar would sit unfilled at the maximum in plain view. A lone thumb fills from the
|
|
171
|
+
start of the track, because that is what a single value means; a range fills
|
|
172
|
+
*between* its thumbs, because the ends are what the caller excluded. The extent
|
|
173
|
+
is floored at one thumb so a collapsed range never disappears.
|
|
174
|
+
- **The pixel arithmetic is a function, not four lines in `slider-fill.tsx`.**
|
|
175
|
+
`fillExtent` earns its place because what it encodes is not self-evident — it is
|
|
176
|
+
the whole justification for the thumb's size — and inline in a `.tsx` no test
|
|
177
|
+
could reach it. `travel` and the touch's `position` are the opposite case and
|
|
178
|
+
stay written out: `trackSize - thumbSize` and `along - thumbSize / 2` appear in
|
|
179
|
+
three files and are self-evident, and routing them through a cross-module
|
|
180
|
+
worklet would add a call in three per-frame paths to hide arithmetic nobody
|
|
181
|
+
would get wrong.
|
|
182
|
+
- **The vertical axis turns around in exactly two places**: `valueFromOffset`'s
|
|
183
|
+
inversion and the sign of the thumb's translate. Not in a `flex-col-reverse` —
|
|
184
|
+
the fill and the thumb are absolutely positioned, so a `flexDirection` never
|
|
185
|
+
reaches them, and a reversed column flips every `justify-*` inside the track as
|
|
186
|
+
well. A test asserts the two orientations are mirror images, reading a vertical
|
|
187
|
+
track from the far end and a horizontal one from the near end and demanding the
|
|
188
|
+
same value. A vertical slider needs a **definite height** from its parent.
|
|
189
|
+
|
|
190
|
+
## Value and state
|
|
191
|
+
|
|
192
|
+
- **Both ends of the range are always stops**, even when the step does not divide
|
|
193
|
+
it. 0–100 by 7 reaches 0, 7, 14 … 98 and then 100, because a slider whose
|
|
194
|
+
maximum cannot be reached by dragging all the way to the end is a slider that
|
|
195
|
+
lies about its own range. A tie goes to the regular stop, so the extra one only
|
|
196
|
+
ever appears at the very end of the drag.
|
|
197
|
+
- **Snapping happens on the UI thread, and that is what bounds the re-renders.**
|
|
198
|
+
`positions` holds *snapped* values, so the mirror back to React fires on a
|
|
199
|
+
step crossing rather than on a frame — a full-width drag at the default step
|
|
200
|
+
is a few dozen commits, not a hundred and twenty a second. `step={0}` is
|
|
201
|
+
continuous and does re-render per frame; that is the trade for
|
|
202
|
+
`formatOptions`, since `Intl` is not available to a worklet and a readout
|
|
203
|
+
derived on the UI thread could not format a currency. If it ever profiles
|
|
204
|
+
badly the escape hatch is an `Animated.Text` fed by a `useDerivedValue`, which
|
|
205
|
+
[Text](../text/AGENTS.md) already renders.
|
|
206
|
+
- **`positions` is one `SharedValue<number[]>`, reassigned and never mutated.**
|
|
207
|
+
One shared value per thumb is not available — a thumb count is data and hooks
|
|
208
|
+
cannot be called in a loop — so the array is the shape. `positions.value[0] = x`
|
|
209
|
+
updates nothing and fails **silently**: an array element has no setter behind
|
|
210
|
+
it. Always build a new array and assign it.
|
|
211
|
+
- **A drag stops the root syncing the shared value from React state.** The mirror
|
|
212
|
+
hop back would otherwise round-trip through a render and land on the thumb a
|
|
213
|
+
frame late, dragging it backwards on every commit — a jitter only a fast drag
|
|
214
|
+
reveals. The guard is a ref, because nothing renders differently for it. Its
|
|
215
|
+
counterpart is `settledDrags`, a counter bumped on release whose only job is to
|
|
216
|
+
give the sync effect something to re-run on: a **controlled parent that rejects
|
|
217
|
+
a dragged value** leaves `current` unchanged, and without the token the thumb
|
|
218
|
+
would stay where the finger let go instead of snapping back.
|
|
219
|
+
- **The shape is the caller's, and it is locked on first render.** A slider given
|
|
220
|
+
a number reports a number; one given an array reports an array. Switching warns
|
|
221
|
+
in development and follows the caller, the lock-and-warn `useControllableState`
|
|
222
|
+
already runs for controlled versus uncontrolled — a slider that silently started
|
|
223
|
+
reporting an array to a caller holding a number is a bug with no error attached.
|
|
224
|
+
|
|
225
|
+
## Haptics
|
|
226
|
+
|
|
227
|
+
- **The haptic is rate-limited by distance, not by a clock.** "Tick when the
|
|
228
|
+
snapped value changed" is not a limit on its own: 0–100 in whole steps across a
|
|
229
|
+
300pt track is a step every three points, and a flick crosses a hundred of them
|
|
230
|
+
in a fifth of a second — a buzz, and a hundred synchronous calls into the haptic
|
|
231
|
+
engine to produce it. `shouldTickHaptic` gates on `SLIDER_HAPTIC_MIN_TRAVEL`,
|
|
232
|
+
which keeps the rule pure so `bun test` reaches it and makes it degrade the right
|
|
233
|
+
way: a coarse scale ticks on every stop, a fine one thins to a cadence a hand can
|
|
234
|
+
feel. Either **end** of the range always ticks — it is the one moment a slider
|
|
235
|
+
has something to say the screen does not already show. The grab itself always
|
|
236
|
+
confirms, the way a press does. A continuous slider never ticks, because there
|
|
237
|
+
is no stop to land on.
|
|
238
|
+
|
|
239
|
+
## Accessibility
|
|
240
|
+
|
|
241
|
+
- **`accessibilityRole="adjustable"` on the thumb, and this is the package's first
|
|
242
|
+
`accessibilityValue`.** It is not polish: the thumb holds no gesture, so without
|
|
243
|
+
`accessibilityActions` and `onAccessibilityAction` there is no assistive path to
|
|
244
|
+
the value **at all** — a VoiceOver or TalkBack swipe would have nothing to call.
|
|
245
|
+
The increment steps by `step`, or by a tenth of the range when the slider is
|
|
246
|
+
continuous. `updateValue` is the one way into the value that has no gesture
|
|
247
|
+
behind it, and it runs the same snap and the same clamp the pan does.
|
|
248
|
+
|
|
249
|
+
## Worklets
|
|
250
|
+
|
|
251
|
+
- **A worklet crosses back to JS with `scheduleOnRN`**, never `runOnJS` — see
|
|
252
|
+
[Pressable](../pressable/AGENTS.md). `onFinalize` queues `setDragging(false)`
|
|
253
|
+
*before* `commitEnd`, and the order is load-bearing: the root has to have
|
|
254
|
+
stopped treating this as a live drag before it is asked to re-sync.
|
|
255
|
+
- **Every exported worklet in `slider.variants.ts` is flat.** None of them calls
|
|
256
|
+
another. A module-scope worklet is rewritten into a factory call that runs at
|
|
257
|
+
import time in source order, so a worklet calling a sibling works only while
|
|
258
|
+
the sibling happens to be declared first — and a tidy-up that reorders the
|
|
259
|
+
file crashes the UI thread with `undefined is not a function`. That is the
|
|
260
|
+
real shape of the `clampUnit` incident the [Screen](../screen/AGENTS.md)
|
|
261
|
+
section records: not "cross-module is unsafe" — `screen.variants.ts`'s own
|
|
262
|
+
resolvers are imported into `useAnimatedStyle` and work — but "a module-scope
|
|
263
|
+
worklet must not depend on one declared below it". The pan's own shared helper
|
|
264
|
+
lives *inside* the `useMemo` beside its callers, where ordinary closure
|
|
265
|
+
capture applies.
|
|
266
|
+
|
|
267
|
+
## Axes
|
|
268
|
+
|
|
269
|
+
- **There is no `Slider.Group`**, so the axis ladder is two rungs rather than
|
|
270
|
+
three: the slider's own props, then an enclosing [Field](../field/AGENTS.md).
|
|
271
|
+
A `Field` reaches the two *state* axes only, and a test pins that it cannot
|
|
272
|
+
acquire a paint axis by accident. The slider does **not** register
|
|
273
|
+
`field.registerPress` the way a `Checkbox` does — a row-wide press has no
|
|
274
|
+
meaning for a control whose value is a position.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export { Slider, type SliderProps } from "./slider";
|
|
2
|
+
export {
|
|
3
|
+
type SliderContextValue,
|
|
4
|
+
SliderProvider,
|
|
5
|
+
useSlider,
|
|
6
|
+
useSliderContext,
|
|
7
|
+
} from "./slider.context";
|
|
8
|
+
export type { SliderRenderChildren, SliderRenderProps } from "./slider.types";
|
|
9
|
+
export {
|
|
10
|
+
clampThumb,
|
|
11
|
+
fillBounds,
|
|
12
|
+
fillExtent,
|
|
13
|
+
formatSliderValue,
|
|
14
|
+
fromValueArray,
|
|
15
|
+
nearestThumbIndex,
|
|
16
|
+
progressOf,
|
|
17
|
+
resolveSliderAxes,
|
|
18
|
+
resolveThumbAccessibilityLabel,
|
|
19
|
+
SLIDER_COLORS,
|
|
20
|
+
SLIDER_DEFAULT_COLOR,
|
|
21
|
+
SLIDER_DEFAULT_ORIENTATION,
|
|
22
|
+
SLIDER_DEFAULT_SIZE,
|
|
23
|
+
SLIDER_HAPTIC_MIN_TRAVEL,
|
|
24
|
+
SLIDER_MAX_VALUE,
|
|
25
|
+
SLIDER_MIN_VALUE,
|
|
26
|
+
SLIDER_ORIENTATIONS,
|
|
27
|
+
SLIDER_OUTPUT_TEXT_SIZE,
|
|
28
|
+
SLIDER_RANGE_SEPARATOR,
|
|
29
|
+
SLIDER_SIZES,
|
|
30
|
+
SLIDER_STEP,
|
|
31
|
+
SLIDER_THUMB_ANIMATION,
|
|
32
|
+
SLIDER_THUMB_NAMES,
|
|
33
|
+
SLIDER_THUMB_SPRING,
|
|
34
|
+
type SliderAxes,
|
|
35
|
+
type SliderColor,
|
|
36
|
+
type SliderFieldAxes,
|
|
37
|
+
type SliderOrientation,
|
|
38
|
+
type SliderOwnAxes,
|
|
39
|
+
type SliderSize,
|
|
40
|
+
type SliderVariantProps,
|
|
41
|
+
shouldTickHaptic,
|
|
42
|
+
sliderVariants,
|
|
43
|
+
snapToStep,
|
|
44
|
+
toValueArray,
|
|
45
|
+
valueFromOffset,
|
|
46
|
+
} from "./slider.variants";
|
|
47
|
+
export type { SliderFillProps } from "./slider-fill";
|
|
48
|
+
export type { SliderOutputProps } from "./slider-output";
|
|
49
|
+
export type { SliderThumbProps } from "./slider-thumb";
|
|
50
|
+
export type { SliderTrackProps } from "./slider-track";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type { ViewProps } from "react-native";
|
|
3
|
+
import Animated, { useAnimatedStyle } from "react-native-reanimated";
|
|
4
|
+
import { useSliderPart } from "./slider.context";
|
|
5
|
+
import { fillBounds, fillExtent, sliderVariants } from "./slider.variants";
|
|
6
|
+
|
|
7
|
+
export type SliderFillProps = Omit<ViewProps, "children" | "style"> & {
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The painted part of the groove.
|
|
13
|
+
*
|
|
14
|
+
* A lone thumb fills from the start of the track, because that is what a single
|
|
15
|
+
* value means — how far along. A range fills *between* its own thumbs, because
|
|
16
|
+
* the ends are what the caller excluded. {@link fillBounds} is that decision and
|
|
17
|
+
* is pure, so `bun test` sweeps it.
|
|
18
|
+
*
|
|
19
|
+
* **The far end lands on the thumb's far edge**, which is what makes both extremes
|
|
20
|
+
* exact: one thumb's width of fill at the minimum, so the handle covers it
|
|
21
|
+
* completely and a slider at rest shows a plain track, and the track's full length
|
|
22
|
+
* at the maximum, with no sliver of empty groove past the handle. That works only
|
|
23
|
+
* because the thumb's diameter is the track's thickness — {@link fillExtent} is
|
|
24
|
+
* where the arithmetic lives, and where `bun test` can hold it down.
|
|
25
|
+
*
|
|
26
|
+
* The extent is an animated style rather than a class because it is a measured
|
|
27
|
+
* length in points, and a class cannot name one. The *colour* is a class, which
|
|
28
|
+
* is what keeps the whole colour matrix inside `bun test`.
|
|
29
|
+
*
|
|
30
|
+
* Nothing here reads React state: the fill follows the shared value the pan
|
|
31
|
+
* writes, so it tracks the finger at the display's refresh rate rather than at
|
|
32
|
+
* React's.
|
|
33
|
+
*/
|
|
34
|
+
export function SliderFill({ className, ...props }: SliderFillProps): ReactElement {
|
|
35
|
+
const { positions, trackSize, thumbSize, minValue, maxValue, orientation, color, size, isInvalid, isDisabled } =
|
|
36
|
+
useSliderPart("Slider.Fill");
|
|
37
|
+
const isVertical = orientation === "vertical";
|
|
38
|
+
|
|
39
|
+
const fillStyle = useAnimatedStyle(() => {
|
|
40
|
+
const thumb = thumbSize.value;
|
|
41
|
+
const values = positions.value;
|
|
42
|
+
const bounds = fillBounds(values, minValue, maxValue);
|
|
43
|
+
// Both helpers hold themselves at zero until the layouts have landed. A
|
|
44
|
+
// measured 0 means "not measured yet", never "a track with no length", and
|
|
45
|
+
// dividing into it would put NaN on the UI thread — which no later frame
|
|
46
|
+
// recovers from.
|
|
47
|
+
const { offset, extent } = fillExtent({
|
|
48
|
+
end: bounds.end,
|
|
49
|
+
isRange: values.length > 1,
|
|
50
|
+
start: bounds.start,
|
|
51
|
+
thumbSize: thumb,
|
|
52
|
+
travel: trackSize.value - thumb,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// The same keys every frame, so Reanimated is never asked to swap a style's
|
|
56
|
+
// shape mid-animation.
|
|
57
|
+
return isVertical ? { bottom: offset, height: extent } : { left: offset, width: extent };
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Animated.View
|
|
62
|
+
className={sliderVariants({ color, isDisabled, isInvalid, orientation, size }).fill({ className })}
|
|
63
|
+
style={fillStyle}
|
|
64
|
+
{...props}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
SliderFill.displayName = "DelacourUI.Slider.Fill";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Text, type TextPresetProps } from "../text";
|
|
3
|
+
import { useSliderPart } from "./slider.context";
|
|
4
|
+
import type { SliderRenderChildren } from "./slider.types";
|
|
5
|
+
import { formatSliderValue, SLIDER_OUTPUT_TEXT_SIZE, sliderVariants } from "./slider.variants";
|
|
6
|
+
|
|
7
|
+
export type SliderOutputProps = Omit<TextPresetProps, "children"> & {
|
|
8
|
+
/** Custom content, or a function called with the slider's settled state. */
|
|
9
|
+
children?: SliderRenderChildren;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The current value, formatted.
|
|
14
|
+
*
|
|
15
|
+
* Renders `Text.Label` and names a size, never a scale of its own. The weight and
|
|
16
|
+
* the colour belong to the preset — restating them here would be a second
|
|
17
|
+
* definition of `Text.Label` that could drift from it, the rule `Field` is built
|
|
18
|
+
* on. Naming the size is what lets the readout still track the slider's own axis,
|
|
19
|
+
* since `Text`'s size axis is built to beat its preset.
|
|
20
|
+
*
|
|
21
|
+
* A range is joined with an en dash. `formatOptions` goes straight to
|
|
22
|
+
* `Intl.NumberFormat`, so a currency or a percentage needs nothing but the
|
|
23
|
+
* options object the platform already understands.
|
|
24
|
+
*
|
|
25
|
+
* **It reads React state, not the shared value the thumb reads**, and that is the
|
|
26
|
+
* one place in this component where a value crosses back to the JS thread during
|
|
27
|
+
* a drag. `Intl` is not available to a worklet, so a formatted readout cannot be
|
|
28
|
+
* derived on the UI thread without giving up `formatOptions` entirely. The cost
|
|
29
|
+
* is bounded instead: the pan snaps on the UI thread and only a *changed* snapped
|
|
30
|
+
* value is mirrored across, so a full-width drag at the default step is a few
|
|
31
|
+
* dozen commits. A continuous slider — `step={0}` — has no stops to change on and
|
|
32
|
+
* does re-render per frame; if that ever profiles badly, the escape hatch is an
|
|
33
|
+
* `Animated.Text` fed by a `useDerivedValue`, which `Text` already renders and
|
|
34
|
+
* which trades `formatOptions` away.
|
|
35
|
+
*/
|
|
36
|
+
export function SliderOutput({ children, className, size, ...props }: SliderOutputProps): ReactElement {
|
|
37
|
+
const context = useSliderPart("Slider.Output");
|
|
38
|
+
const { color, size: sliderSize, isDisabled, isInvalid, orientation, values, formatOptions } = context;
|
|
39
|
+
|
|
40
|
+
const content =
|
|
41
|
+
typeof children === "function"
|
|
42
|
+
? children(context.renderProps)
|
|
43
|
+
: (children ?? formatSliderValue(values, formatOptions));
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Text.Label
|
|
47
|
+
className={sliderVariants({ color, isDisabled, isInvalid, orientation, size: sliderSize }).output({ className })}
|
|
48
|
+
size={size ?? SLIDER_OUTPUT_TEXT_SIZE[sliderSize]}
|
|
49
|
+
{...props}
|
|
50
|
+
>
|
|
51
|
+
{content}
|
|
52
|
+
</Text.Label>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
SliderOutput.displayName = "DelacourUI.Slider.Output";
|