@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,484 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
import type { TextColor, TextSize } from "../text/text.variants";
|
|
4
|
+
|
|
5
|
+
/** What a ticked box means. The same six `Badge` paints, and for the same reason. */
|
|
6
|
+
export const CHECKBOX_COLORS = ["default", "primary", "success", "warning", "destructive", "info"] as const;
|
|
7
|
+
|
|
8
|
+
export const CHECKBOX_SIZES = ["sm", "md", "lg"] as const;
|
|
9
|
+
|
|
10
|
+
/** Which side of its label the box sits on. */
|
|
11
|
+
export const CHECKBOX_ALIGNMENTS = ["start", "end"] as const;
|
|
12
|
+
|
|
13
|
+
export type CheckboxColor = (typeof CHECKBOX_COLORS)[number];
|
|
14
|
+
export type CheckboxSize = (typeof CHECKBOX_SIZES)[number];
|
|
15
|
+
export type CheckboxAlignment = (typeof CHECKBOX_ALIGNMENTS)[number];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Theme token whose value gives the tick its colour.
|
|
19
|
+
*
|
|
20
|
+
* The checkbox's counterpart to `BADGE_FOREGROUND_TOKEN`, and it exists for the
|
|
21
|
+
* same reason: a colour that has to reach an SVG paint prop cannot be a class.
|
|
22
|
+
* Every entry names the `-foreground` of the fill its own `indicator` slot
|
|
23
|
+
* paints, so the glyph and the surface under it are never a shade apart — a test
|
|
24
|
+
* asserts the pair rather than trusting two maps to stay in step.
|
|
25
|
+
*/
|
|
26
|
+
export const CHECKBOX_GLYPH_TOKEN: Record<CheckboxColor, string> = {
|
|
27
|
+
default: "secondary-foreground",
|
|
28
|
+
primary: "primary-foreground",
|
|
29
|
+
success: "success-foreground",
|
|
30
|
+
warning: "warning-foreground",
|
|
31
|
+
destructive: "destructive-foreground",
|
|
32
|
+
info: "info-foreground",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** The tick's colour once the box is reporting an invalid value. */
|
|
36
|
+
export const CHECKBOX_INVALID_GLYPH_TOKEN = "destructive-foreground";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Theme token each colour paints its filled surface with.
|
|
40
|
+
*
|
|
41
|
+
* The same value the `indicator` slot names as a `bg-*`, as a token this time,
|
|
42
|
+
* because the border interpolates *to* it and a colour being animated has to be
|
|
43
|
+
* a value rather than a class. A test pins the two against each other.
|
|
44
|
+
*
|
|
45
|
+
* `default` is `secondary` rather than a colour of its own: this theme's
|
|
46
|
+
* `primary` is a near-black, and its neutral fill already exists.
|
|
47
|
+
*/
|
|
48
|
+
export const CHECKBOX_SURFACE_TOKEN: Record<CheckboxColor, string> = {
|
|
49
|
+
default: "secondary",
|
|
50
|
+
primary: "primary",
|
|
51
|
+
success: "success",
|
|
52
|
+
warning: "warning",
|
|
53
|
+
destructive: "destructive",
|
|
54
|
+
info: "info",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The `rounded-*` step the box wears at each size.
|
|
59
|
+
*
|
|
60
|
+
* Named here as well as written into the `box` slot so the fill can be given
|
|
61
|
+
* the same corner — see {@link resolveCheckboxFillRadius}. A test asserts the
|
|
62
|
+
* two still agree.
|
|
63
|
+
*/
|
|
64
|
+
export const CHECKBOX_RADIUS_STEP: Record<CheckboxSize, "xs" | "sm"> = { sm: "xs", md: "xs", lg: "sm" };
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Width of the box's border in points — Tailwind's bare `border` utility.
|
|
68
|
+
*
|
|
69
|
+
* Restated here because two other things have to agree with it: the `indicator`
|
|
70
|
+
* slot's `-inset-px`, which is how the fill reaches past the padding box to
|
|
71
|
+
* cover the border ring, and `CheckboxBox`'s `onLayout`, which subtracts it
|
|
72
|
+
* twice to turn the measured border box back into the padding box the tick clip
|
|
73
|
+
* is positioned in.
|
|
74
|
+
*/
|
|
75
|
+
export const CHECKBOX_BORDER_WIDTH = 1;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* What each of those steps multiplies `--radius` by, restating `tokens.css`.
|
|
79
|
+
*
|
|
80
|
+
* It has to be restated: the corner scale is declared `@theme inline`, so
|
|
81
|
+
* Tailwind substitutes each step into the utilities that use it and emits no
|
|
82
|
+
* `--radius-xs` variable at all. `--radius` is the only one that survives to
|
|
83
|
+
* runtime, so anything computing a corner in JavaScript has to do the
|
|
84
|
+
* multiplication itself. `checkbox.variants.test.ts` pins both numbers against
|
|
85
|
+
* `tokens.css`, so retuning the scale fails the build rather than quietly
|
|
86
|
+
* leaving the fill behind.
|
|
87
|
+
*/
|
|
88
|
+
export const CHECKBOX_RADIUS_MULTIPLIER: Record<"sm" | "xs", number> = { xs: 0.4, sm: 0.6 };
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Corner radius of the animated fill, in points.
|
|
92
|
+
*
|
|
93
|
+
* The box's **own** radius, not a tighter one. The fill spans the whole border
|
|
94
|
+
* box — see the `indicator` slot's `-inset-px` — so it wears the same corner the
|
|
95
|
+
* box does and runs underneath the border ring rather than stopping against it.
|
|
96
|
+
*
|
|
97
|
+
* It was this minus the border width until it was not, which is the rule for two
|
|
98
|
+
* rounded rectangles to stay concentric — and concentric was the bug. Two curves
|
|
99
|
+
* that coincide are rasterised twice, on two layers, and antialiased
|
|
100
|
+
* independently: at a corner pixel where each gives coverage `a` the composite
|
|
101
|
+
* covers `2a - a²`, so `(1 - a)²` of the box's own `bg-card` bleeds through as a
|
|
102
|
+
* dull arc. The straight edges are pixel-aligned and show nothing, which is why
|
|
103
|
+
* the artifact was corner-only. Overlapping the two removes the shared edge
|
|
104
|
+
* instead of concealing it, and holds on both platforms even though iOS paints a
|
|
105
|
+
* `CALayer` border above its sublayers and Android paints it below.
|
|
106
|
+
*
|
|
107
|
+
* A function of the live `--radius` rather than a table of points, because
|
|
108
|
+
* `--radius` is a consumer's to set — a pasted theme retunes every corner in
|
|
109
|
+
* the package, and a fill left at a number tuned for the default would come
|
|
110
|
+
* away from the border the moment anyone did. Clamped at zero so a
|
|
111
|
+
* square-cornered theme draws a square fill rather than a negative radius.
|
|
112
|
+
*
|
|
113
|
+
* Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
|
|
114
|
+
*/
|
|
115
|
+
export function resolveCheckboxFillRadius(size: CheckboxSize, radius: number): number {
|
|
116
|
+
const step = CHECKBOX_RADIUS_MULTIPLIER[CHECKBOX_RADIUS_STEP[size]];
|
|
117
|
+
|
|
118
|
+
return Math.max(0, radius * step);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The border of a box that is not filled — the same chrome a field wears. */
|
|
122
|
+
export const CHECKBOX_REST_BORDER_TOKEN = "input";
|
|
123
|
+
|
|
124
|
+
/** The border, filled or not, once the box is reporting an invalid value. */
|
|
125
|
+
export const CHECKBOX_INVALID_BORDER_TOKEN = "destructive";
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Styling for every part of a checkbox.
|
|
129
|
+
*
|
|
130
|
+
* One slotted `tv()` rather than a call per part, so `size` is declared once
|
|
131
|
+
* across a box, the glyph inside it, the row's gap and the step the label asks
|
|
132
|
+
* its preset for. The sibling file is what makes that possible: the slot set is
|
|
133
|
+
* read by files that cannot import each other's roots without closing a cycle.
|
|
134
|
+
*
|
|
135
|
+
* **`color` paints the `indicator`, not the box.** The indicator is an
|
|
136
|
+
* absolute-fill layer that is invisible until the box is ticked, so a colour
|
|
137
|
+
* needs no unchecked branch — an unticked box is `border-input bg-card` at every
|
|
138
|
+
* colour, the same chrome a field wears. That leaves `compoundVariants` holding
|
|
139
|
+
* one cell rather than a thirty-six cell matrix.
|
|
140
|
+
*
|
|
141
|
+
* **The border is not a class at all.** It fades from the field chrome to the
|
|
142
|
+
* fill's own colour as the surface scales out to meet it, which is a value being
|
|
143
|
+
* interpolated rather than a class being swapped. {@link CHECKBOX_SURFACE_TOKEN}
|
|
144
|
+
* and {@link resolveCheckboxBorderTokens} name the two ends; the base keeps
|
|
145
|
+
* `border-input` as the resting appearance the animated style starts from.
|
|
146
|
+
*
|
|
147
|
+
* `overflow-hidden` on the box is load-bearing, not tidiness. The fill overhangs
|
|
148
|
+
* the border box by a point on every side, and on Android a border is painted
|
|
149
|
+
* below its children rather than above them — so the clip is what terminates the
|
|
150
|
+
* fill at the box's corner rather than a point outside it.
|
|
151
|
+
*
|
|
152
|
+
* `border` sits in the base rather than on the filled branch. A border declared
|
|
153
|
+
* only where it shows would move the glyph inside by a point the moment the box
|
|
154
|
+
* was ticked.
|
|
155
|
+
*
|
|
156
|
+
* **The box reads the shared icon scale, two steps above its own glyph.** It
|
|
157
|
+
* mints no scale of its own: a checkbox is a glyph in a box, both measurements
|
|
158
|
+
* are already on `--spacing-icon-*`, and a private `--spacing-checkbox-*` would
|
|
159
|
+
* be three numbers that have to be retuned in step with three others forever.
|
|
160
|
+
* Two steps is the gap that leaves the tick breathing room — 18/14, 20/16,
|
|
161
|
+
* 24/18 — and a test pins the offset rather than the numbers, so the scale can
|
|
162
|
+
* be retuned without the test becoming a transcript of it.
|
|
163
|
+
*
|
|
164
|
+
* **The `label` slot holds layout and nothing else** — no size, no weight, no
|
|
165
|
+
* colour. `Checkbox.Label` renders the `Text.Label` preset and passes a step and
|
|
166
|
+
* a colour through {@link resolveCheckboxLabelSize} and
|
|
167
|
+
* {@link resolveCheckboxLabelColor}, so the type scale lives in exactly one
|
|
168
|
+
* place. A `text-sm font-medium` here would be a second definition of
|
|
169
|
+
* `Text.Label` that could drift from it — `Field`'s rule, and the reason `Input`
|
|
170
|
+
* ships no label part at all. The `View` slots hold no `text-*` either: a React
|
|
171
|
+
* Native `View` does not cascade colour to a `Text` descendant. See AGENTS.md
|
|
172
|
+
* rule 1.
|
|
173
|
+
*
|
|
174
|
+
* **The row aligns to the top, and the box carries a margin that puts it back.**
|
|
175
|
+
* A wrapped label centred against its own paragraph drifts the box down the side
|
|
176
|
+
* of it instead of leaving it on the first line — the same thing `Input` refuses
|
|
177
|
+
* for a multiline field's decorators. `items-start` fixes that and would break
|
|
178
|
+
* the ordinary single-line case, so each size adds a top margin of half the
|
|
179
|
+
* difference between the label's line box and the box: 20/24/28 against
|
|
180
|
+
* 18/20/24, which is 1pt, 2pt and 2pt. One line then renders exactly as
|
|
181
|
+
* `items-center` did, and every line after it is a bonus rather than a
|
|
182
|
+
* regression.
|
|
183
|
+
*
|
|
184
|
+
* `opacity-50` for a disabled checkbox goes on the **row**, so the label fades
|
|
185
|
+
* with the box it names rather than standing at full strength beside a ghost.
|
|
186
|
+
*
|
|
187
|
+
* Free of React Native imports so it stays unit-testable — `bun test` cannot
|
|
188
|
+
* parse React Native's Flow-typed source. See AGENTS.md.
|
|
189
|
+
*/
|
|
190
|
+
export const checkboxVariants = tv({
|
|
191
|
+
slots: {
|
|
192
|
+
root: "flex-row items-start",
|
|
193
|
+
box: "items-center justify-center overflow-hidden border border-input bg-card",
|
|
194
|
+
/**
|
|
195
|
+
* The animated fill. Runs under the box's border, and under the tick.
|
|
196
|
+
*
|
|
197
|
+
* `-inset-px` rather than `inset-0`, because an absolute child is positioned
|
|
198
|
+
* against the padding box: a negative inset of one border width is what
|
|
199
|
+
* reaches the border box. The overlap is what keeps the fill's edge and the
|
|
200
|
+
* border's from coinciding — see {@link resolveCheckboxFillRadius}.
|
|
201
|
+
*/
|
|
202
|
+
indicator: "absolute -inset-px",
|
|
203
|
+
/**
|
|
204
|
+
* Clips the tick. Pinned to the box's left edge with an animated width, so
|
|
205
|
+
* the glyph is revealed across rather than grown into.
|
|
206
|
+
*/
|
|
207
|
+
tick: "absolute inset-y-0 left-0 overflow-hidden",
|
|
208
|
+
/** Holds the glyph at the box's centre while the clip above it moves. */
|
|
209
|
+
tickInner: "h-full items-center justify-center",
|
|
210
|
+
/** Layout only. The treatment is the `Text.Label` preset's. */
|
|
211
|
+
label: "shrink",
|
|
212
|
+
/** Edge length the tick inherits. */
|
|
213
|
+
glyph: "",
|
|
214
|
+
/** `Checkbox.Group`'s column. */
|
|
215
|
+
group: "gap-4",
|
|
216
|
+
},
|
|
217
|
+
variants: {
|
|
218
|
+
// Paints the fill and nothing else — see the note above on why the box
|
|
219
|
+
// needs no unchecked branch per colour.
|
|
220
|
+
color: {
|
|
221
|
+
default: { indicator: "bg-secondary" },
|
|
222
|
+
primary: { indicator: "bg-primary" },
|
|
223
|
+
success: { indicator: "bg-success" },
|
|
224
|
+
warning: { indicator: "bg-warning" },
|
|
225
|
+
destructive: { indicator: "bg-destructive" },
|
|
226
|
+
info: { indicator: "bg-info" },
|
|
227
|
+
},
|
|
228
|
+
size: {
|
|
229
|
+
// The box's top margin is half the difference between the label's line
|
|
230
|
+
// box and the box itself — 20/24/28 against 18/20/24 — so a one-line
|
|
231
|
+
// label reads as centred and a wrapped one keeps the box on its first
|
|
232
|
+
// line. See the note above the slots.
|
|
233
|
+
// The box and the glyph inside it read the *same* scale, two steps
|
|
234
|
+
// apart — see the note above the slots.
|
|
235
|
+
sm: { root: "gap-2", box: "mt-px size-icon-md rounded-xs", glyph: "size-icon-xs" },
|
|
236
|
+
md: { root: "gap-2.5", box: "mt-0.5 size-icon-lg rounded-xs", glyph: "size-icon-sm" },
|
|
237
|
+
lg: { root: "gap-3", box: "mt-0.5 size-icon-xl rounded-sm", glyph: "size-icon-md" },
|
|
238
|
+
},
|
|
239
|
+
alignment: {
|
|
240
|
+
start: {},
|
|
241
|
+
// `flex-row-reverse` rather than a branch in the JSX: the box is
|
|
242
|
+
// rendered first either way, so the decision stays a class and the whole
|
|
243
|
+
// matrix stays reachable from `bun test`.
|
|
244
|
+
end: { root: "flex-row-reverse", label: "grow" },
|
|
245
|
+
},
|
|
246
|
+
// The empty `false` branches are load-bearing typing, not placeholders.
|
|
247
|
+
// `tv` derives the prop type from the declared keys, so a map with only
|
|
248
|
+
// `true` types the prop as `true` rather than `boolean` and rejects
|
|
249
|
+
// `checkboxVariants({ isFilled })`. See the note in button.variants.ts.
|
|
250
|
+
//
|
|
251
|
+
// Named `isFilled` rather than `isChecked` because checked and
|
|
252
|
+
// indeterminate both paint the surface and only the glyph tells them
|
|
253
|
+
// apart. `resolveCheckboxFilled` is that translation.
|
|
254
|
+
isFilled: { true: {}, false: {} },
|
|
255
|
+
isInvalid: { true: {}, false: {} },
|
|
256
|
+
isDisabled: { true: { root: "opacity-50" }, false: {} },
|
|
257
|
+
},
|
|
258
|
+
compoundVariants: [
|
|
259
|
+
// Invalid outranks the colour on the fill, whether the box is ticked or
|
|
260
|
+
// not. A checkbox that stayed green while its value was rejected would
|
|
261
|
+
// drop the only signal it has, exactly while the value is being corrected
|
|
262
|
+
// — the precedence `Input` sets between invalid and focus.
|
|
263
|
+
//
|
|
264
|
+
// The *border* is not here. It interpolates between two token values as
|
|
265
|
+
// the fill approaches the edge, so it is a style rather than a class —
|
|
266
|
+
// see `resolveCheckboxBorderTokens`.
|
|
267
|
+
{ isInvalid: true, class: { indicator: "bg-destructive" } },
|
|
268
|
+
],
|
|
269
|
+
defaultVariants: {
|
|
270
|
+
color: "default",
|
|
271
|
+
size: "md",
|
|
272
|
+
alignment: "start",
|
|
273
|
+
isFilled: false,
|
|
274
|
+
isInvalid: false,
|
|
275
|
+
isDisabled: false,
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* [unfilled, filled] for the fill's two tracks, plus the tick's timing.
|
|
281
|
+
*
|
|
282
|
+
* The fill fades and scales **from the centre of the box**. It does not travel:
|
|
283
|
+
* a translation would make the surface arrive from one edge, which reads as a
|
|
284
|
+
* panel sliding in rather than as a box being filled, and there is no direction
|
|
285
|
+
* a checkbox is filled *from*.
|
|
286
|
+
*
|
|
287
|
+
* The tick is not part of that. It is clipped by a container whose width opens
|
|
288
|
+
* from the box's left edge, so the glyph is drawn on rather than faded up —
|
|
289
|
+
* which is what makes ticking look like ticking, and unticking look like the
|
|
290
|
+
* stroke being taken back. `tickDelay` holds it until the surface it is drawn on
|
|
291
|
+
* is most of the way there; starting both at once reads as one blurred event.
|
|
292
|
+
*
|
|
293
|
+
* One shared progress value drives all of it through `interpolate`, so the
|
|
294
|
+
* tracks cannot drift out of step and there is one animation rather than three.
|
|
295
|
+
*
|
|
296
|
+
* Pure data, so a test can pin that every track travels and that the filled end
|
|
297
|
+
* is a finished box rather than something stopped mid-animation.
|
|
298
|
+
*/
|
|
299
|
+
export const CHECKBOX_INDICATOR_ANIMATION = {
|
|
300
|
+
opacity: [0, 1],
|
|
301
|
+
scale: [0.8, 1],
|
|
302
|
+
/** Fraction of the fill's travel that passes before the tick starts to draw. */
|
|
303
|
+
tickDelay: 0.25,
|
|
304
|
+
/**
|
|
305
|
+
* Fraction that passes before the border starts taking the fill's colour.
|
|
306
|
+
*
|
|
307
|
+
* Later than the tick, because the border should read as the surface
|
|
308
|
+
* *reaching* it rather than as a second thing changing alongside it. The fill
|
|
309
|
+
* scales 0.8 → 1, so it is only near the edge in the last of its travel.
|
|
310
|
+
*/
|
|
311
|
+
borderDelay: 0.55,
|
|
312
|
+
durationMs: 140,
|
|
313
|
+
} as const;
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Points of slop that bring a bare box up to the 44pt minimum touch target.
|
|
317
|
+
*
|
|
318
|
+
* New to this package, and deliberate: `Badge.CloseButton` needs none because it
|
|
319
|
+
* sits inside a padded capsule, while a bare `md` checkbox is a 20pt square with
|
|
320
|
+
* nothing around it.
|
|
321
|
+
*/
|
|
322
|
+
export const CHECKBOX_HIT_SLOP: Record<CheckboxSize, number> = { sm: 13, md: 12, lg: 10 };
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Whether the box paints its surface.
|
|
326
|
+
*
|
|
327
|
+
* Indeterminate fills exactly as checked does — only the glyph differs, a dash
|
|
328
|
+
* instead of a tick — so the two collapse to one visual state here and the tv()
|
|
329
|
+
* axis is named for that rather than for `checked`.
|
|
330
|
+
*
|
|
331
|
+
* Pure, so the matrix is reachable from `bun test`. See AGENTS.md.
|
|
332
|
+
*/
|
|
333
|
+
export function resolveCheckboxFilled({
|
|
334
|
+
isChecked,
|
|
335
|
+
isIndeterminate,
|
|
336
|
+
}: {
|
|
337
|
+
isChecked: boolean;
|
|
338
|
+
isIndeterminate: boolean;
|
|
339
|
+
}): boolean {
|
|
340
|
+
return isChecked || isIndeterminate;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** The axes a checkbox can take from itself, its group, or the field around it. */
|
|
344
|
+
export type CheckboxAxes = {
|
|
345
|
+
color?: CheckboxColor;
|
|
346
|
+
size?: CheckboxSize;
|
|
347
|
+
alignment?: CheckboxAlignment;
|
|
348
|
+
isInvalid?: boolean;
|
|
349
|
+
isDisabled?: boolean;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/** What an enclosing `Field` contributes — state only; it has no opinion on colour. */
|
|
353
|
+
export type CheckboxFieldAxes = { isInvalid?: boolean; isDisabled?: boolean };
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The axes a checkbox actually draws with, given the wrappers around it.
|
|
357
|
+
*
|
|
358
|
+
* The ladder is `own ?? group ?? field ?? default`, on every axis, and the
|
|
359
|
+
* middle rung is where this differs from `Input`.
|
|
360
|
+
*
|
|
361
|
+
* `Input`'s ladder puts its group **first**, because `Input.Group` owns the one
|
|
362
|
+
* box a grouped field renders into — a field's own `variant` there would be a
|
|
363
|
+
* second answer to a question already settled. `Checkbox.Group` owns no box. It
|
|
364
|
+
* is a state controller that also carries shared defaults, which makes it the
|
|
365
|
+
* same kind of thing as `Field`: a wrapper a control can override. So "make the
|
|
366
|
+
* group `lg`" and "make this one destructive" are different questions, and both get
|
|
367
|
+
* an answer.
|
|
368
|
+
*
|
|
369
|
+
* `??` throughout and never `||`, so an explicit `false` opts a control out of
|
|
370
|
+
* an invalid field rather than reading as an absence.
|
|
371
|
+
*
|
|
372
|
+
* Pure, so the whole ladder is reachable from `bun test`. See AGENTS.md.
|
|
373
|
+
*/
|
|
374
|
+
export function resolveCheckboxAxes({
|
|
375
|
+
own,
|
|
376
|
+
group,
|
|
377
|
+
field,
|
|
378
|
+
}: {
|
|
379
|
+
own: CheckboxAxes;
|
|
380
|
+
group?: CheckboxAxes;
|
|
381
|
+
field?: CheckboxFieldAxes;
|
|
382
|
+
}): Required<CheckboxAxes> {
|
|
383
|
+
return {
|
|
384
|
+
color: own.color ?? group?.color ?? "default",
|
|
385
|
+
size: own.size ?? group?.size ?? "md",
|
|
386
|
+
alignment: own.alignment ?? group?.alignment ?? "start",
|
|
387
|
+
isInvalid: own.isInvalid ?? group?.isInvalid ?? field?.isInvalid ?? false,
|
|
388
|
+
isDisabled: own.isDisabled ?? group?.isDisabled ?? field?.isDisabled ?? false,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* The value list a group holds after one entry is toggled.
|
|
394
|
+
*
|
|
395
|
+
* Appends when absent and filters when present, so the order values were checked
|
|
396
|
+
* in survives and a duplicate that somehow got in is cleared rather than halved.
|
|
397
|
+
* Always a new array: React bails out of a re-render on an unchanged reference,
|
|
398
|
+
* so a mutation here would flip the state and leave the screen alone.
|
|
399
|
+
*
|
|
400
|
+
* Pure, so it is reachable from `bun test`. See AGENTS.md.
|
|
401
|
+
*/
|
|
402
|
+
export function toggleCheckedValue(current: readonly string[], value: string): string[] {
|
|
403
|
+
return current.includes(value) ? current.filter((entry) => entry !== value) : [...current, value];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* The `Text` step `Checkbox.Label` hands its preset.
|
|
408
|
+
*
|
|
409
|
+
* A size, never a class. The checkbox's own step names map onto `TEXT_SIZES`'
|
|
410
|
+
* own, so the two scales stay level without either restating the other.
|
|
411
|
+
*
|
|
412
|
+
* Pure, so it is reachable from `bun test`. See AGENTS.md.
|
|
413
|
+
*/
|
|
414
|
+
export function resolveCheckboxLabelSize(size: CheckboxSize): TextSize {
|
|
415
|
+
switch (size) {
|
|
416
|
+
case "sm":
|
|
417
|
+
return "sm";
|
|
418
|
+
case "lg":
|
|
419
|
+
return "lg";
|
|
420
|
+
default:
|
|
421
|
+
return "md";
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The `Text` colour `Checkbox.Label` hands its preset.
|
|
427
|
+
*
|
|
428
|
+
* Returning `undefined` is meaningful rather than lazy: `Text`'s colour axis
|
|
429
|
+
* emits nothing when it is not named, so the label falls through to
|
|
430
|
+
* `Text.Label`'s own `text-foreground`. The same contract `resolveFieldTextColor`
|
|
431
|
+
* uses, and it is what lets the label say "leave it alone".
|
|
432
|
+
*
|
|
433
|
+
* Pure, so it is reachable from `bun test`. See AGENTS.md.
|
|
434
|
+
*/
|
|
435
|
+
export function resolveCheckboxLabelColor(isInvalid: boolean): TextColor | undefined {
|
|
436
|
+
return isInvalid ? "destructive" : undefined;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* The slop a checkbox's tap target takes beyond the box itself.
|
|
441
|
+
*
|
|
442
|
+
* Only a bare box gets any. Once there is a label the whole row is the target
|
|
443
|
+
* and is already wide, and slop on top of that would overlap the next row's —
|
|
444
|
+
* making a tap between two checkboxes ambiguous, which is worse than the target
|
|
445
|
+
* being merely adequate.
|
|
446
|
+
*
|
|
447
|
+
* Pure, so it is reachable from `bun test`. See AGENTS.md.
|
|
448
|
+
*/
|
|
449
|
+
export function resolveCheckboxHitSlop({
|
|
450
|
+
size,
|
|
451
|
+
hasLabel,
|
|
452
|
+
}: {
|
|
453
|
+
size: CheckboxSize;
|
|
454
|
+
hasLabel: boolean;
|
|
455
|
+
}): number | undefined {
|
|
456
|
+
return hasLabel ? undefined : CHECKBOX_HIT_SLOP[size];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* The two theme tokens a box's border travels between.
|
|
461
|
+
*
|
|
462
|
+
* `rest` is what an unfilled box wears and `active` is what a filled one settles
|
|
463
|
+
* on — the fill's own colour, so the border reads as the surface having reached
|
|
464
|
+
* the edge rather than as an outline that changed on its own.
|
|
465
|
+
*
|
|
466
|
+
* An invalid box returns destructive for **both**, so there is nothing to fade: the
|
|
467
|
+
* border is the signal that the value is wrong, and it has to be there before
|
|
468
|
+
* the box is ticked as much as after. That is the same precedence `Input` sets
|
|
469
|
+
* between invalid and focus, expressed as a pair of endpoints instead of a
|
|
470
|
+
* compound variant.
|
|
471
|
+
*
|
|
472
|
+
* Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
|
|
473
|
+
*/
|
|
474
|
+
export function resolveCheckboxBorderTokens({ color, isInvalid }: { color: CheckboxColor; isInvalid: boolean }): {
|
|
475
|
+
rest: string;
|
|
476
|
+
active: string;
|
|
477
|
+
} {
|
|
478
|
+
if (isInvalid) {
|
|
479
|
+
return { active: CHECKBOX_INVALID_BORDER_TOKEN, rest: CHECKBOX_INVALID_BORDER_TOKEN };
|
|
480
|
+
}
|
|
481
|
+
return { active: CHECKBOX_SURFACE_TOKEN[color], rest: CHECKBOX_REST_BORDER_TOKEN };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export type CheckboxVariantProps = VariantProps<typeof checkboxVariants>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export { Checkbox, type CheckboxProps } from "./checkbox";
|
|
2
|
+
export {
|
|
3
|
+
type CheckboxContextValue,
|
|
4
|
+
type CheckboxGroupContextValue,
|
|
5
|
+
CheckboxGroupProvider,
|
|
6
|
+
CheckboxProvider,
|
|
7
|
+
useCheckbox,
|
|
8
|
+
useCheckboxContext,
|
|
9
|
+
useCheckboxGroup,
|
|
10
|
+
useCheckboxGroupContext,
|
|
11
|
+
} from "./checkbox.context";
|
|
12
|
+
export type { CheckboxLabelProps } from "./checkbox.types";
|
|
13
|
+
export {
|
|
14
|
+
CHECKBOX_ALIGNMENTS,
|
|
15
|
+
CHECKBOX_BORDER_WIDTH,
|
|
16
|
+
CHECKBOX_COLORS,
|
|
17
|
+
CHECKBOX_GLYPH_TOKEN,
|
|
18
|
+
CHECKBOX_HIT_SLOP,
|
|
19
|
+
CHECKBOX_INDICATOR_ANIMATION,
|
|
20
|
+
CHECKBOX_INVALID_BORDER_TOKEN,
|
|
21
|
+
CHECKBOX_INVALID_GLYPH_TOKEN,
|
|
22
|
+
CHECKBOX_RADIUS_STEP,
|
|
23
|
+
CHECKBOX_REST_BORDER_TOKEN,
|
|
24
|
+
CHECKBOX_SIZES,
|
|
25
|
+
CHECKBOX_SURFACE_TOKEN,
|
|
26
|
+
type CheckboxAlignment,
|
|
27
|
+
type CheckboxAxes,
|
|
28
|
+
type CheckboxColor,
|
|
29
|
+
type CheckboxFieldAxes,
|
|
30
|
+
type CheckboxSize,
|
|
31
|
+
type CheckboxVariantProps,
|
|
32
|
+
checkboxVariants,
|
|
33
|
+
resolveCheckboxAxes,
|
|
34
|
+
resolveCheckboxBorderTokens,
|
|
35
|
+
resolveCheckboxFilled,
|
|
36
|
+
resolveCheckboxFillRadius,
|
|
37
|
+
resolveCheckboxHitSlop,
|
|
38
|
+
resolveCheckboxLabelColor,
|
|
39
|
+
resolveCheckboxLabelSize,
|
|
40
|
+
toggleCheckedValue,
|
|
41
|
+
} from "./checkbox.variants";
|
|
42
|
+
export type { CheckboxGroupProps } from "./checkbox-group";
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Field
|
|
2
|
+
|
|
3
|
+
A form field's layout, and the one place its state is written down. Root plus
|
|
4
|
+
`Field.Set`, `Field.Legend`, `Field.Group`, `Field.Content`, `Field.Label`,
|
|
5
|
+
`Field.Description`, `Field.Error` and `Field.Separator`.
|
|
6
|
+
|
|
7
|
+
`import { Field } from "@delacour/react-native-ui/field";`
|
|
8
|
+
|
|
9
|
+
## Files
|
|
10
|
+
|
|
11
|
+
| File | What it holds |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `index.ts` | → `@delacour/react-native-ui/field` |
|
|
14
|
+
| `field.tsx` | Root + the `Object.assign` compound surface |
|
|
15
|
+
| `field-set.tsx` | `Field.Set` |
|
|
16
|
+
| `field-legend.tsx` | `Field.Legend` |
|
|
17
|
+
| `field-group.tsx` | `Field.Group` |
|
|
18
|
+
| `field-content.tsx` | `Field.Content` |
|
|
19
|
+
| `field-label.tsx` | `Field.Label` |
|
|
20
|
+
| `field-description.tsx` | `Field.Description` |
|
|
21
|
+
| `field-error.tsx` | `Field.Error` |
|
|
22
|
+
| `field-separator.tsx` | `Field.Separator` |
|
|
23
|
+
| `field.context.tsx` | `FieldProvider`, `useField()`, `useFieldContext()` |
|
|
24
|
+
| `field.types.ts` | Prop types shared by two or more parts |
|
|
25
|
+
| `field.variants.ts` | Pure `tv()` slots + resolvers, no RN imports |
|
|
26
|
+
| `field.variants.test.ts` | |
|
|
27
|
+
|
|
28
|
+
## Design
|
|
29
|
+
|
|
30
|
+
- **Orientations**: `vertical` (default), `horizontal`.
|
|
31
|
+
- **The cascade is a context, and it had to be.** `<Field isInvalid>` reddens the
|
|
32
|
+
control inside it, not just its own label. On the web shadcn does that with
|
|
33
|
+
`group-data-[invalid=true]/field:` — a parent-scoped selector. Uniwind has no
|
|
34
|
+
equivalent: its compiler reads `data-*` off a **single flat selector**
|
|
35
|
+
(`bundler/css-processor/processor.ts`) and its runtime matches them against
|
|
36
|
+
`props[attribute]` on **the component carrying the class**
|
|
37
|
+
(`core/native/store.ts`), so no class on a `Field` can reach the
|
|
38
|
+
[`Input`](../input/AGENTS.md) inside it. There is no `group-*`, no `peer-*`,
|
|
39
|
+
no `:has()`. Do not go looking for one again.
|
|
40
|
+
- **The whole row drives the control, once one offers a press.** A control
|
|
41
|
+
registers a callback through the same context the state cascades down, and the
|
|
42
|
+
row becomes a `Pressable` with `feedback="none"` that calls it — so tapping
|
|
43
|
+
"Accept the terms", or the description under it, ticks the
|
|
44
|
+
[`Checkbox`](../checkbox/AGENTS.md) beside
|
|
45
|
+
it. A checkbox in a form is a small square next to a sentence, and the sentence
|
|
46
|
+
is what people aim at. This is [`Input.Group`](../input/AGENTS.md)'s trick one level out: that group
|
|
47
|
+
is a pressable whose press focuses the field through a ref on its context.
|
|
48
|
+
`resolveFieldInteractive` is the decision and it is pure, so `bun test` reaches
|
|
49
|
+
it. A field of static text registers nothing and stays a `View` — mounting a
|
|
50
|
+
detector regardless would put one under every label and description in a form,
|
|
51
|
+
the thing [`Badge`](../badge/AGENTS.md) refuses for a list of fifty tags. The row is
|
|
52
|
+
`accessible={false}`, so the control stays the element a screen reader sees,
|
|
53
|
+
and the inner detector claims a tap on the box itself rather than firing both.
|
|
54
|
+
A field holds one control, so a second registration replaces the first.
|
|
55
|
+
- **The label names the control, through the same context.** `Field.Label`
|
|
56
|
+
registers its text with `registerLabel`, the mirror of `registerPress`: a
|
|
57
|
+
control hands its press up, a label hands its name down. A control with no
|
|
58
|
+
text of its own — a [Slider](../slider/AGENTS.md) thumb is a capsule — reads
|
|
59
|
+
`field.label` as its `accessibilityLabel`, so "Volume" is announced rather than
|
|
60
|
+
a bare "40". React Native has no `<label for>`; this is that association, done
|
|
61
|
+
by hand. `resolveFieldLabelText` decides what qualifies and it is strict on
|
|
62
|
+
purpose: a plain string or number, trimmed, and nothing else. A label built
|
|
63
|
+
from elements has no text this side of a render, and guessing at one — an
|
|
64
|
+
icon's key, say — would name the control after the wrong thing. State rather
|
|
65
|
+
than a ref for the same reason as the press: the control renders differently
|
|
66
|
+
for it, and the label registers in an effect, so it costs one render on mount.
|
|
67
|
+
- **A data-attribute class would also leave `bun test`.** Even for a part styling
|
|
68
|
+
itself, `data-invalid:text-destructive` moves the decision from `field.variants.ts`
|
|
69
|
+
into uniwind's runtime matcher, where no unit test can see it. The parts style
|
|
70
|
+
themselves from `tv()` booleans; the context is only for crossing a component
|
|
71
|
+
boundary.
|
|
72
|
+
- **The text parts render the `Text` presets and pass a colour, never a scale.**
|
|
73
|
+
`Field.Label` *is* [`Text.Label`](../text/AGENTS.md); `Field.Description` and `Field.Error` are
|
|
74
|
+
`Text.Caption`. `resolveFieldTextColor` picks the colour and returns
|
|
75
|
+
`undefined` to mean "leave the preset's own alone" — which is exactly what
|
|
76
|
+
`Text`'s unnamed axes do. A `text-sm font-medium` in a slot here would be a
|
|
77
|
+
second definition of `Text.Label`, the thing that kept [`Input`](../input/AGENTS.md) from shipping a
|
|
78
|
+
label part at all. A test asserts the text slots carry no size, weight or
|
|
79
|
+
colour.
|
|
80
|
+
- **The gap ladder is the component.** `content` 0.5 → `root` 1.5 → `set` 4 →
|
|
81
|
+
`group` 5. A label attaches to the control beneath it rather than the one above
|
|
82
|
+
purely because the gap inside a field is tighter than the gap between two, and
|
|
83
|
+
nothing else is doing that work. The test pins the **ordering**, not the
|
|
84
|
+
numbers, so the spacing can be retuned without the test becoming a transcript
|
|
85
|
+
of it.
|
|
86
|
+
- **Only the label fades when disabled.** The control dims itself, and a dimmed
|
|
87
|
+
description stacked on a dimmed control reads as two problems rather than one
|
|
88
|
+
state. The description stays muted when invalid too, so an appearing
|
|
89
|
+
`Field.Error` is the one line that changed.
|
|
90
|
+
- **`Field.Error` renders nothing when it has no children**, so
|
|
91
|
+
`<Field.Error>{error}</Field.Error>` removes itself once the value is fixed.
|
|
92
|
+
It is deliberately **not** gated on `isInvalid`: a part that swallowed children
|
|
93
|
+
a caller actually wrote, because of a prop on a sibling, would be a part whose
|
|
94
|
+
absence is unexplainable from the call site. shadcn's `errors` array prop is
|
|
95
|
+
not ported — it exists to accept react-hook-form and Standard Schema shapes,
|
|
96
|
+
and this package takes no form dependency.
|
|
97
|
+
- **`Field.Separator` draws two rules, not one with a label on top.** The web
|
|
98
|
+
version absolutely-positions a single rule and punches a hole in it with an
|
|
99
|
+
opaque `bg-background` label, which is invisible only while the separator sits
|
|
100
|
+
on exactly that colour — on a card or a sheet the hole shows as a block of the
|
|
101
|
+
wrong shade. Two rules and a gap assume nothing about what is behind them. The
|
|
102
|
+
playground's `/field/grouping` has the card case on screen.
|
|
103
|
+
- **`Field.Group` inserts no dividers**, unlike [`ListGroup`](../list-group/AGENTS.md). A list of rows
|
|
104
|
+
without lines is a wall of text; fields are already held apart by whitespace,
|
|
105
|
+
and a rule between every one is noise.
|
|
106
|
+
- **There is no `Field.Title`.** On the web it exists because a `<div>` is not a
|
|
107
|
+
`<label>` — label-styled text with nothing to point `htmlFor` at. React Native
|
|
108
|
+
has neither element nor association, so it and `Field.Label` would render the
|
|
109
|
+
same `Text`.
|
|
110
|
+
- **A set holds no state.** `isInvalid` and `isDisabled` live on each `Field`,
|
|
111
|
+
because a whole section going `destructive` says less than the one field that is
|
|
112
|
+
actually wrong.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { useFieldContext } from "./field.context";
|
|
4
|
+
import { fieldVariants } from "./field.variants";
|
|
5
|
+
|
|
6
|
+
export type FieldContentProps = ViewProps & { className?: string };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A label and its description, bundled into one block beside a control.
|
|
10
|
+
*
|
|
11
|
+
* Only needed in a horizontal field, and only when there is a description: it
|
|
12
|
+
* makes the text one flex child so the row lays out as `[text] [control]`
|
|
13
|
+
* instead of `[label] [description] [control]`. `flex-1` is what pushes the
|
|
14
|
+
* control to the far edge; `min-w-0` is what lets the text wrap instead of
|
|
15
|
+
* forcing the row wider than the screen.
|
|
16
|
+
*/
|
|
17
|
+
export function FieldContent({ className, ...props }: FieldContentProps): ReactElement {
|
|
18
|
+
const field = useFieldContext();
|
|
19
|
+
const isInvalid = field?.isInvalid ?? false;
|
|
20
|
+
const isDisabled = field?.isDisabled ?? false;
|
|
21
|
+
|
|
22
|
+
return <View className={fieldVariants({ isDisabled, isInvalid }).content({ className })} {...props} />;
|
|
23
|
+
}
|
|
24
|
+
FieldContent.displayName = "DelacourUI.Field.Content";
|