@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,358 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
import type { IconSize } from "../icon/icon.variants";
|
|
4
|
+
|
|
5
|
+
/** How the surface is painted. `ListGroup`'s set — an accordion is the same kind of thing. */
|
|
6
|
+
export const ACCORDION_VARIANTS = ["default", "secondary", "tertiary", "transparent"] as const;
|
|
7
|
+
|
|
8
|
+
export const ACCORDION_SIZES = ["sm", "md", "lg"] as const;
|
|
9
|
+
|
|
10
|
+
/** Whether one item may be open at a time, or any number of them. */
|
|
11
|
+
export const ACCORDION_SELECTION_MODES = ["single", "multiple"] as const;
|
|
12
|
+
|
|
13
|
+
export type AccordionVariant = (typeof ACCORDION_VARIANTS)[number];
|
|
14
|
+
export type AccordionSize = (typeof ACCORDION_SIZES)[number];
|
|
15
|
+
export type AccordionSelectionMode = (typeof ACCORDION_SELECTION_MODES)[number];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The axes an accordion falls back to when nothing names one.
|
|
19
|
+
*
|
|
20
|
+
* Named once because two places read them — `defaultVariants` below and the root,
|
|
21
|
+
* which settles them before `tv` is ever called. A test pins the pair, since a
|
|
22
|
+
* drift between them is an accordion that renders at one size and reports another.
|
|
23
|
+
*/
|
|
24
|
+
export const ACCORDION_DEFAULT_VARIANT: AccordionVariant = "default";
|
|
25
|
+
export const ACCORDION_DEFAULT_SIZE: AccordionSize = "md";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The step every glyph in an accordion is drawn at — the indicator, or an icon a
|
|
29
|
+
* caller composes into a trigger.
|
|
30
|
+
*
|
|
31
|
+
* One step rather than two, so a chevron and a leading glyph on the same row are
|
|
32
|
+
* the same mark. `Switch` makes the same call for the two ends of its track. The
|
|
33
|
+
* `glyph` slot writes the class out, because Tailwind's scanner is static, and a
|
|
34
|
+
* test pins the two together.
|
|
35
|
+
*/
|
|
36
|
+
export const ACCORDION_GLYPH_STEP: Record<AccordionSize, IconSize> = { sm: "xs", md: "sm", lg: "md" };
|
|
37
|
+
|
|
38
|
+
/** Theme token an `Icon` composed into a trigger inherits. */
|
|
39
|
+
export const ACCORDION_FOREGROUND_TOKEN = "foreground";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Theme token the indicator inherits.
|
|
43
|
+
*
|
|
44
|
+
* Quieter than the title it sits beside, and a test pins that the two differ.
|
|
45
|
+
* The chevron is chrome telling you the row opens; the title is the content. Paint
|
|
46
|
+
* both `foreground` and the glyph carries the same weight as the words.
|
|
47
|
+
*/
|
|
48
|
+
export const ACCORDION_INDICATOR_TOKEN = "muted-foreground";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The one spring an accordion runs.
|
|
52
|
+
*
|
|
53
|
+
* **The panel's height, the panel's opacity and the indicator's rotation all read
|
|
54
|
+
* it**, off a single `progress` shared value, so they cannot drift out of step by
|
|
55
|
+
* a frame. Two springs is the failure this constant exists to prevent: a stiffer
|
|
56
|
+
* layout than chevron leaves the glyph visibly lagging the panel on every tap.
|
|
57
|
+
*
|
|
58
|
+
* **Critically damped**, which is `Switch`'s rule turned inside out. A switch damps
|
|
59
|
+
* its thumb because an overshoot squashes against a capsule that clips it; a panel
|
|
60
|
+
* damps its height because an overshoot draws the panel *taller* than its content
|
|
61
|
+
* measured, flashing the surface behind it for a frame at the end of every expand.
|
|
62
|
+
* A test pins the damping ratio at or above one, and below the point where it crawls.
|
|
63
|
+
*/
|
|
64
|
+
export const ACCORDION_SPRING = { damping: 26, mass: 0.4, stiffness: 400 } as const;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Where the indicator points at each end of the travel, in degrees.
|
|
68
|
+
*
|
|
69
|
+
* A half turn, so a chevron pointing down at rest points up when the panel is
|
|
70
|
+
* open. Interpolated off the same `progress` as the height rather than driven by
|
|
71
|
+
* an effect of its own, so the two are in phase by construction.
|
|
72
|
+
*/
|
|
73
|
+
export const ACCORDION_INDICATOR_ROTATION = { collapsed: 0, expanded: 180 } as const;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* What {@link AccordionItemContextValue.contentHeight} holds before a panel has
|
|
77
|
+
* ever reported its own layout.
|
|
78
|
+
*
|
|
79
|
+
* Negative rather than zero, because the two mean different things and the
|
|
80
|
+
* difference is load-bearing. A panel that measured `0` is a real answer — a
|
|
81
|
+
* panel whose content rendered nothing — and an item that treated it as "still
|
|
82
|
+
* waiting" would never start its spring, leaving the indicator stuck pointing the
|
|
83
|
+
* wrong way for an empty panel. Only a value no layout can produce can mean
|
|
84
|
+
* *unmeasured*.
|
|
85
|
+
*
|
|
86
|
+
* The height style therefore floors it: `progress * max(contentHeight, 0)`.
|
|
87
|
+
*/
|
|
88
|
+
export const ACCORDION_UNMEASURED = -1;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The window of the travel the panel's opacity ramps across.
|
|
92
|
+
*
|
|
93
|
+
* Tied to `progress` like everything else, but ahead of it: a panel whose opacity
|
|
94
|
+
* tracked its height linearly would be half transparent at the midpoint of every
|
|
95
|
+
* expand, which reads as content struggling to arrive rather than as a panel
|
|
96
|
+
* opening. Closing the fade at {@link ACCORDION_CONTENT_FADE.end} leaves the last
|
|
97
|
+
* of the travel to the height alone.
|
|
98
|
+
*/
|
|
99
|
+
export const ACCORDION_CONTENT_FADE = { start: 0.1, end: 0.6 } as const;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Styling for every part of an accordion.
|
|
103
|
+
*
|
|
104
|
+
* One slotted `tv()` rather than a call per part, because the parts cannot import
|
|
105
|
+
* the root without closing a cycle (AGENTS.md rule 3) yet all of them read the
|
|
106
|
+
* same `size`.
|
|
107
|
+
*
|
|
108
|
+
* **Size is not decoration.** It drives the trigger's metrics, the title and
|
|
109
|
+
* description type scale, the indicator's glyph step, the panel's padding *and*
|
|
110
|
+
* the divider inset. Those six belong on one axis rather than in six places that
|
|
111
|
+
* can drift apart — the divider inset tracking the trigger's own padding is
|
|
112
|
+
* asserted as a pair for exactly that reason, and so is the panel's.
|
|
113
|
+
*
|
|
114
|
+
* **The variant paints the root and nothing else.** Only the root has a surface;
|
|
115
|
+
* a trigger or a panel that also changed with it would be a second source for one
|
|
116
|
+
* colour. A test pins that.
|
|
117
|
+
*
|
|
118
|
+
* **`overflow-hidden` on the root is load-bearing, not tidiness.** A panel
|
|
119
|
+
* expanding inside it would otherwise square off the group's own rounded corners
|
|
120
|
+
* — `ListGroup`'s rule, and the same one that makes a pressed trigger fade to the
|
|
121
|
+
* edge of its own box.
|
|
122
|
+
*
|
|
123
|
+
* **`overflow-hidden` on `content` is the disclosure itself.** That slot's height
|
|
124
|
+
* is an animated style running from zero to the height its contents measured, so
|
|
125
|
+
* without the clip a collapsed panel would draw its content straight over the row
|
|
126
|
+
* below it.
|
|
127
|
+
*
|
|
128
|
+
* **`contentInner` is out of flow, and the panel measures itself before the clip
|
|
129
|
+
* is allowed to constrain anything.** Both halves were found the hard way on a
|
|
130
|
+
* simulator. In normal flow the child is laid out against the clip's own height,
|
|
131
|
+
* so a panel measured while the clip sits at `height: 0` reports its padding and
|
|
132
|
+
* nothing else — sixteen points for a paragraph. Out of flow it measures its
|
|
133
|
+
* content honestly, but a view that *mounts* absolutely into a parent already at
|
|
134
|
+
* `height: 0` never fires `onLayout` at all: the frame is empty, no layout event
|
|
135
|
+
* is emitted, and the panel waits for a measurement that arrives only when
|
|
136
|
+
* something unrelated forces a relayout — minutes later, or never.
|
|
137
|
+
*
|
|
138
|
+
* So the clip has two phases, and `accordion-content.tsx` owns the switch: while
|
|
139
|
+
* the panel has never measured it is positioned absolutely over its own item, which
|
|
140
|
+
* takes it out of the item's layout entirely and keeps it invisible; once it has a
|
|
141
|
+
* height it becomes the animated clip. Nothing moves on screen between the two,
|
|
142
|
+
* because the travel has not started yet.
|
|
143
|
+
*
|
|
144
|
+
* **The disabled fade lands on `item`, never on `trigger`.** The trigger is a
|
|
145
|
+
* `Pressable`, which now multiplies a className's opacity into its press rather
|
|
146
|
+
* than overwriting it — but the item already fades, header and panel together,
|
|
147
|
+
* and a fade on the trigger as well would compound to a quarter.
|
|
148
|
+
*
|
|
149
|
+
* No slot worn by a `View` carries `text-*`; a React Native `View` does not
|
|
150
|
+
* cascade colour to a `Text` descendant, so the treatment lives on the `title` and
|
|
151
|
+
* `description` slots. See AGENTS.md rule 1.
|
|
152
|
+
*
|
|
153
|
+
* Free of React Native imports so it stays unit-testable — `bun test` cannot parse
|
|
154
|
+
* React Native's Flow-typed source. See AGENTS.md.
|
|
155
|
+
*/
|
|
156
|
+
export const accordionVariants = tv({
|
|
157
|
+
slots: {
|
|
158
|
+
/** The clipped, rounded surface every item sits in. */
|
|
159
|
+
root: "overflow-hidden border border-transparent",
|
|
160
|
+
/** Positioning only for a divider the root inserts — the line is a `Separator`. */
|
|
161
|
+
divider: "",
|
|
162
|
+
/** One item's column: its trigger, then its panel. Wears the disabled fade. */
|
|
163
|
+
item: "",
|
|
164
|
+
/** The row that opens the item. A `Pressable`, so it carries no opacity of its own. */
|
|
165
|
+
trigger: "w-full flex-row items-center",
|
|
166
|
+
/** The trigger's text column, taking whatever width the indicator leaves. */
|
|
167
|
+
triggerContent: "min-w-0 flex-1 justify-center gap-0.5",
|
|
168
|
+
/** The box the indicator turns inside. Its rotation is an animated style. */
|
|
169
|
+
indicator: "items-center justify-center",
|
|
170
|
+
/** Edge length any glyph in an accordion inherits. */
|
|
171
|
+
glyph: "",
|
|
172
|
+
/** The trigger's primary line. Carries its own colour — a `View` cannot cascade one. */
|
|
173
|
+
title: "font-medium text-foreground",
|
|
174
|
+
/** The trigger's secondary line, a step down in scale and on the muted token. */
|
|
175
|
+
description: "text-muted-foreground",
|
|
176
|
+
/** The clip. Its height is an animated style, never a class. */
|
|
177
|
+
content: "w-full overflow-hidden",
|
|
178
|
+
/** The measured layer, out of flow so no clip height can squash what it reports. */
|
|
179
|
+
contentInner: "absolute top-0 right-0 left-0",
|
|
180
|
+
},
|
|
181
|
+
variants: {
|
|
182
|
+
variant: {
|
|
183
|
+
default: { root: "border-border bg-card" },
|
|
184
|
+
secondary: { root: "bg-secondary" },
|
|
185
|
+
tertiary: { root: "bg-tertiary" },
|
|
186
|
+
transparent: { root: "bg-transparent" },
|
|
187
|
+
},
|
|
188
|
+
size: {
|
|
189
|
+
sm: {
|
|
190
|
+
root: "rounded-md",
|
|
191
|
+
trigger: "min-h-12 gap-2.5 px-3 py-3",
|
|
192
|
+
divider: "mx-3",
|
|
193
|
+
contentInner: "px-3 pb-3",
|
|
194
|
+
glyph: "size-icon-xs",
|
|
195
|
+
title: "text-sm",
|
|
196
|
+
description: "text-xs",
|
|
197
|
+
},
|
|
198
|
+
md: {
|
|
199
|
+
root: "rounded-md",
|
|
200
|
+
trigger: "min-h-14 gap-3 px-4 py-3.5",
|
|
201
|
+
divider: "mx-4",
|
|
202
|
+
contentInner: "px-4 pb-4",
|
|
203
|
+
glyph: "size-icon-sm",
|
|
204
|
+
title: "text-base",
|
|
205
|
+
description: "text-sm",
|
|
206
|
+
},
|
|
207
|
+
lg: {
|
|
208
|
+
root: "rounded-md",
|
|
209
|
+
trigger: "min-h-16 gap-3.5 px-5 py-4",
|
|
210
|
+
divider: "mx-5",
|
|
211
|
+
contentInner: "px-5 pb-5",
|
|
212
|
+
glyph: "size-icon-md",
|
|
213
|
+
title: "text-lg",
|
|
214
|
+
description: "text-base",
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
// The empty `false` branch is load-bearing typing, not a placeholder. `tv`
|
|
218
|
+
// derives the prop type from the declared keys, so a map with only `true`
|
|
219
|
+
// types the prop as `true` rather than `boolean`.
|
|
220
|
+
isDisabled: { true: { item: "opacity-50" }, false: {} },
|
|
221
|
+
},
|
|
222
|
+
defaultVariants: {
|
|
223
|
+
variant: ACCORDION_DEFAULT_VARIANT,
|
|
224
|
+
size: ACCORDION_DEFAULT_SIZE,
|
|
225
|
+
isDisabled: false,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
export type AccordionVariantProps = VariantProps<typeof accordionVariants>;
|
|
230
|
+
|
|
231
|
+
/** An accordion where one item is open at a time, or none. */
|
|
232
|
+
export type AccordionSingleSelection = {
|
|
233
|
+
selectionMode?: "single";
|
|
234
|
+
/** Controlled. `null` is a controlled *empty*, not an absence. */
|
|
235
|
+
value?: string | null;
|
|
236
|
+
/** Which item starts open while uncontrolled. */
|
|
237
|
+
defaultValue?: string | null;
|
|
238
|
+
onValueChange?: (value: string | null) => void;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/** An accordion where any number of items are open at once. */
|
|
242
|
+
export type AccordionMultipleSelection = {
|
|
243
|
+
selectionMode: "multiple";
|
|
244
|
+
/** Controlled. */
|
|
245
|
+
value?: string[];
|
|
246
|
+
/** Which items start open while uncontrolled. */
|
|
247
|
+
defaultValue?: string[];
|
|
248
|
+
onValueChange?: (value: string[]) => void;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The two shapes an accordion's state can take.
|
|
253
|
+
*
|
|
254
|
+
* A **true** discriminated union, so `selectionMode="single"` with an array
|
|
255
|
+
* `defaultValue` is a type error rather than something the toggle has to defend
|
|
256
|
+
* against at runtime. Intersecting a union like this with a widened
|
|
257
|
+
* `defaultValue?: string | string[]` defeats it entirely — every member re-admits
|
|
258
|
+
* both shapes, and the callback ends up typed as a function *returning* a union
|
|
259
|
+
* rather than a union of functions.
|
|
260
|
+
*
|
|
261
|
+
* Lives here rather than in `accordion.types.ts` because that file imports React
|
|
262
|
+
* Native's `ViewProps`, and the root's selection maths has to stay reachable from
|
|
263
|
+
* `bun test`.
|
|
264
|
+
*/
|
|
265
|
+
export type AccordionSelection = AccordionSingleSelection | AccordionMultipleSelection;
|
|
266
|
+
|
|
267
|
+
/** Whether one item's value is in the expanded set. */
|
|
268
|
+
export function isItemExpanded(expanded: readonly string[], value: string): boolean {
|
|
269
|
+
return expanded.includes(value);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Normalises every shape a caller can hold into the one list the root keeps.
|
|
274
|
+
*
|
|
275
|
+
* The root holds a single `string[]` whichever mode is in play, because
|
|
276
|
+
* `useControllableState` cannot be called conditionally and two hooks would be two
|
|
277
|
+
* pieces of state for one answer. The caller's shape is converted at the boundary
|
|
278
|
+
* instead — here on the way in, and by a narrow on `selectionMode` on the way out.
|
|
279
|
+
*
|
|
280
|
+
* It copies rather than aliasing, so a caller's array cannot be handed to React as
|
|
281
|
+
* state it still holds a reference to.
|
|
282
|
+
*/
|
|
283
|
+
export function toExpandedList(value: string | readonly string[] | null | undefined): string[] {
|
|
284
|
+
if (value === null || value === undefined) return [];
|
|
285
|
+
return typeof value === "string" ? [value] : [...value];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The whole state transition a tap on a trigger makes.
|
|
290
|
+
*
|
|
291
|
+
* Pure, so `bun test` reaches the entire matrix — the trade `toggleCheckedValue`
|
|
292
|
+
* already makes for `Checkbox.Group`.
|
|
293
|
+
*
|
|
294
|
+
* **A real change is always a new array**, because React bails out of a re-render
|
|
295
|
+
* on an unchanged reference and a mutation would flip the state while leaving the
|
|
296
|
+
* screen alone. **A refused tap returns its own input**, by identity, so the root
|
|
297
|
+
* can skip it — a rejected tap must not re-render and must not report an
|
|
298
|
+
* `onValueChange` for a change that did not happen.
|
|
299
|
+
*
|
|
300
|
+
* **`isCollapsible` bounds the set, never a single item.** In multiple mode that
|
|
301
|
+
* distinction is the whole point: an item still closes while others stay open, and
|
|
302
|
+
* only the last one is refused. Reading it as "no item may ever close" makes a
|
|
303
|
+
* multiple accordion add-only, which is a control that fills up and never empties.
|
|
304
|
+
*/
|
|
305
|
+
export function toggleExpandedValue({
|
|
306
|
+
expanded,
|
|
307
|
+
value,
|
|
308
|
+
selectionMode,
|
|
309
|
+
isCollapsible,
|
|
310
|
+
}: {
|
|
311
|
+
expanded: readonly string[];
|
|
312
|
+
value: string;
|
|
313
|
+
selectionMode: AccordionSelectionMode;
|
|
314
|
+
isCollapsible: boolean;
|
|
315
|
+
}): readonly string[] {
|
|
316
|
+
const isExpanded = isItemExpanded(expanded, value);
|
|
317
|
+
|
|
318
|
+
if (selectionMode === "single") {
|
|
319
|
+
if (!isExpanded) return [value];
|
|
320
|
+
return isCollapsible ? [] : expanded;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!isExpanded) return [...expanded, value];
|
|
324
|
+
if (!isCollapsible && expanded.length === 1) return expanded;
|
|
325
|
+
return expanded.filter((entry) => entry !== value);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** What one item was given at its own call site. */
|
|
329
|
+
export type AccordionItemOwnAxes = { isDisabled?: boolean };
|
|
330
|
+
|
|
331
|
+
/** What the enclosing accordion publishes. */
|
|
332
|
+
export type AccordionRootAxes = { isDisabled?: boolean };
|
|
333
|
+
|
|
334
|
+
/** Every axis an item settles, ready to hand to {@link accordionVariants}. */
|
|
335
|
+
export type AccordionItemAxes = { isDisabled: boolean };
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Settles one item's axes from the two places they can come from.
|
|
339
|
+
*
|
|
340
|
+
* A two-rung ladder — the item's own prop first, the accordion last — so
|
|
341
|
+
* `<Accordion isDisabled>` dims every row with nothing said at a call site, while
|
|
342
|
+
* `isDisabled={false}` still opts one item back in. `??` throughout and never
|
|
343
|
+
* `||`, so an explicit `false` is a value rather than an absence.
|
|
344
|
+
*
|
|
345
|
+
* The accordion reaches this one *state* axis only; it cannot acquire a paint axis
|
|
346
|
+
* by accident, and a test pins that.
|
|
347
|
+
*
|
|
348
|
+
* Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
|
|
349
|
+
*/
|
|
350
|
+
export function resolveAccordionItemAxes({
|
|
351
|
+
own,
|
|
352
|
+
root,
|
|
353
|
+
}: {
|
|
354
|
+
own?: AccordionItemOwnAxes;
|
|
355
|
+
root?: AccordionRootAxes | null;
|
|
356
|
+
}): AccordionItemAxes {
|
|
357
|
+
return { isDisabled: own?.isDisabled ?? root?.isDisabled ?? false };
|
|
358
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export { Accordion, type AccordionBaseProps, type AccordionProps } from "./accordion";
|
|
2
|
+
export {
|
|
3
|
+
type AccordionContextValue,
|
|
4
|
+
type AccordionItemContextValue,
|
|
5
|
+
AccordionItemProvider,
|
|
6
|
+
AccordionProvider,
|
|
7
|
+
useAccordion,
|
|
8
|
+
useAccordionContext,
|
|
9
|
+
useAccordionItem,
|
|
10
|
+
useAccordionItemContext,
|
|
11
|
+
} from "./accordion.context";
|
|
12
|
+
export type { AccordionTextProps } from "./accordion.types";
|
|
13
|
+
export {
|
|
14
|
+
ACCORDION_CONTENT_FADE,
|
|
15
|
+
ACCORDION_DEFAULT_SIZE,
|
|
16
|
+
ACCORDION_DEFAULT_VARIANT,
|
|
17
|
+
ACCORDION_FOREGROUND_TOKEN,
|
|
18
|
+
ACCORDION_GLYPH_STEP,
|
|
19
|
+
ACCORDION_INDICATOR_ROTATION,
|
|
20
|
+
ACCORDION_INDICATOR_TOKEN,
|
|
21
|
+
ACCORDION_SELECTION_MODES,
|
|
22
|
+
ACCORDION_SIZES,
|
|
23
|
+
ACCORDION_SPRING,
|
|
24
|
+
ACCORDION_UNMEASURED,
|
|
25
|
+
ACCORDION_VARIANTS,
|
|
26
|
+
type AccordionItemAxes,
|
|
27
|
+
type AccordionItemOwnAxes,
|
|
28
|
+
type AccordionMultipleSelection,
|
|
29
|
+
type AccordionRootAxes,
|
|
30
|
+
type AccordionSelection,
|
|
31
|
+
type AccordionSelectionMode,
|
|
32
|
+
type AccordionSingleSelection,
|
|
33
|
+
type AccordionSize,
|
|
34
|
+
type AccordionVariant,
|
|
35
|
+
type AccordionVariantProps,
|
|
36
|
+
accordionVariants,
|
|
37
|
+
isItemExpanded,
|
|
38
|
+
resolveAccordionItemAxes,
|
|
39
|
+
toExpandedList,
|
|
40
|
+
toggleExpandedValue,
|
|
41
|
+
} from "./accordion.variants";
|
|
42
|
+
export type { AccordionContentProps } from "./accordion-content";
|
|
43
|
+
export type { AccordionIndicatorProps, AccordionIndicatorState } from "./accordion-indicator";
|
|
44
|
+
export type { AccordionItemProps } from "./accordion-item";
|
|
45
|
+
export type { AccordionTriggerProps } from "./accordion-trigger";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Badge
|
|
2
|
+
|
|
3
|
+
A compact label for status, category or count. Compound root plus `Badge.Label`,
|
|
4
|
+
`Badge.StartContent`, `Badge.EndContent` and `Badge.CloseButton`.
|
|
5
|
+
|
|
6
|
+
`import { Badge } from "@delacour/react-native-ui/badge";`
|
|
7
|
+
|
|
8
|
+
## Files
|
|
9
|
+
|
|
10
|
+
| File | What it holds |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `index.ts` | → `@delacour/react-native-ui/badge` |
|
|
13
|
+
| `badge.tsx` | Root + the `Object.assign` compound surface |
|
|
14
|
+
| `badge-label.tsx` | `Badge.Label` |
|
|
15
|
+
| `badge-start-content.tsx` | `Badge.StartContent` |
|
|
16
|
+
| `badge-end-content.tsx` | `Badge.EndContent` |
|
|
17
|
+
| `badge-close-button.tsx` | `Badge.CloseButton`, the dismiss pressable |
|
|
18
|
+
| `badge.context.tsx` | `BadgeContext`, `useBadge()`, `useBadgeContext()`, `useBadgePart()` |
|
|
19
|
+
| `badge.types.ts` | Prop types shared by two or more parts |
|
|
20
|
+
| `badge.variants.ts` | Pure `tv()` slots + `resolveBadgeInteractive`, no RN imports |
|
|
21
|
+
| `badge.variants.test.ts` | |
|
|
22
|
+
|
|
23
|
+
## Design
|
|
24
|
+
|
|
25
|
+
- **Two axes, not one.** `variant` says how the surface is painted — `solid`,
|
|
26
|
+
`soft`, `outline`, `ghost` — and `color` says what it means — `default`,
|
|
27
|
+
`primary`, `success`, `warning`, `destructive`, `info`. **Sizes**: `sm`, `md`, `lg`.
|
|
28
|
+
[`Button`](../button/AGENTS.md) collapses the two into a single enum, and a
|
|
29
|
+
badge deliberately does not: six semantic colours are the point of this
|
|
30
|
+
component rather than an afterthought, and one axis would need thirteen names
|
|
31
|
+
to say what two say with ten. Neither axis paints a surface alone, so all
|
|
32
|
+
twenty-four pairings live in `compoundVariants`; a test asserts every cell is
|
|
33
|
+
distinct, because two cells collapsing means a caller can set an axis and see
|
|
34
|
+
nothing change.
|
|
35
|
+
- **A badge is content until it is given something to do.** With no `onPress`
|
|
36
|
+
and no `onLongPress` the root is a plain `View`. Mounting a `GestureDetector`
|
|
37
|
+
regardless would put one under every tag in a list of fifty and announce each
|
|
38
|
+
of them to assistive technology as a button with no action. `resolveBadgeInteractive`
|
|
39
|
+
is that decision, and it is pure so `bun test` reaches it. Supply either
|
|
40
|
+
handler and the root becomes a [`Pressable`](../pressable/AGENTS.md),
|
|
41
|
+
inheriting `feedback`, `haptic` and the rest; only the default differs,
|
|
42
|
+
`scale`.
|
|
43
|
+
- **`onClose` is its own pressable, never a mode of the root.** The dismiss
|
|
44
|
+
control is a `Badge.CloseButton` composed in at the end, so its tap is claimed
|
|
45
|
+
by the inner detector and never also fires the badge's `onPress`. It presses
|
|
46
|
+
with `fade` rather than the root's `scale` — a spring on a glyph that small
|
|
47
|
+
reads as a jitter. Reach for the part by hand only to place it somewhere other
|
|
48
|
+
than last.
|
|
49
|
+
- **A size is padding, never a height.** `Text` respects OS font scaling, so a
|
|
50
|
+
fixed height clips the label at a large accessibility step instead of growing
|
|
51
|
+
with it — and unlike `h-button-*` or `h-navbar-row`, a badge lines up against
|
|
52
|
+
no chrome that would force the number. A test asserts the root carries no
|
|
53
|
+
`h-*` at any size. The icon step indexes the shared `--spacing-icon-*` scale,
|
|
54
|
+
so a glyph in a badge matches every other glyph in the library.
|
|
55
|
+
- **`self-start` is load-bearing.** A badge is sized by its content, and inside a
|
|
56
|
+
gap column every child is stretch-aligned by default — without it a one-word
|
|
57
|
+
badge spans the whole screen.
|
|
58
|
+
- **The border is reserved on every variant, transparent until `outline`
|
|
59
|
+
colours it.** Declaring it only where it shows would make the badge two points
|
|
60
|
+
wider the moment a caller switched variant. `overflow-hidden` is likewise not
|
|
61
|
+
tidiness: a pressed badge fades to the edge of its own capsule.
|
|
62
|
+
- **The neutral end of the matrix reuses tokens the theme already has.** This
|
|
63
|
+
theme's `primary` is a near-black whose tint *is* the neutral fill, so a
|
|
64
|
+
`--primary-soft` would duplicate `secondary` exactly. `soft` takes
|
|
65
|
+
`tertiary` for `primary` and `muted` for `default` instead — two fills the
|
|
66
|
+
theme already tunes per mode. The four semantic colours did get real tokens:
|
|
67
|
+
`success-soft`, `warning-soft` and `info-soft` joined the existing
|
|
68
|
+
`destructive-soft` in `theme.css`, foregrounds included.
|
|
69
|
+
- **`BADGE_FOREGROUND_TOKEN` is nested, `Record<variant, Record<color, string>>`,**
|
|
70
|
+
so adding a colour is a compile error in four places rather than a silent gap
|
|
71
|
+
in one. A test pins each entry to the token its own `label` slot resolves to —
|
|
72
|
+
two maps that can drift is how a badge ends up with a grey glyph beside white
|
|
73
|
+
text — and asserts every token it names is declared in **both** variants of
|
|
74
|
+
`theme.css`.
|
|
75
|
+
- **Icons are composed, never passed as props**, the way
|
|
76
|
+
[`Button`](../button/AGENTS.md) does it. The root wraps its subtree in an
|
|
77
|
+
`IconDefaultsProvider` and a `TextClassProvider`, so a bare `<Icon>` or
|
|
78
|
+
`<Text>` inside a badge comes out at the right size and colour with nothing
|
|
79
|
+
said at the call site. One text treatment covers the whole subtree, which is
|
|
80
|
+
the condition [Text](../text/AGENTS.md) sets for publishing into the cascade.
|
|
81
|
+
- **String children** are wrapped in a `Badge.Label` automatically, consecutive
|
|
82
|
+
strings collapsing into one — the same rule, and the same reason, as
|
|
83
|
+
[`Button`](../button/AGENTS.md).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { IconCrossSmall } from "../../icons/central";
|
|
3
|
+
import { Icon } from "../icon";
|
|
4
|
+
import { Pressable, type PressableProps } from "../pressable";
|
|
5
|
+
import { useBadgePart } from "./badge.context";
|
|
6
|
+
import { badgeVariants } from "./badge.variants";
|
|
7
|
+
|
|
8
|
+
export type BadgeCloseButtonProps = Omit<PressableProps, "asChild" | "busy" | "children" | "disabled">;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The badge's trailing dismiss control.
|
|
12
|
+
*
|
|
13
|
+
* A pressable of its own rather than a mode of the root, which is what keeps a
|
|
14
|
+
* dismiss from also firing the badge's `onPress`: the two gestures belong to two
|
|
15
|
+
* detectors, and the inner one claims the tap.
|
|
16
|
+
*
|
|
17
|
+
* The root composes one in whenever `onClose` is set, so reach for this by hand
|
|
18
|
+
* only to place it somewhere other than last — before an `EndContent`, say.
|
|
19
|
+
*
|
|
20
|
+
* The glyph is left bare: it inherits the badge's icon size and its surface's
|
|
21
|
+
* colour from the root's `IconDefaultsProvider`, so a dismiss on a `destructive`
|
|
22
|
+
* badge is tinted without being told.
|
|
23
|
+
*
|
|
24
|
+
* `fade` rather than the root's `scale` — a spring on a glyph this small reads
|
|
25
|
+
* as a jitter rather than as a press.
|
|
26
|
+
*/
|
|
27
|
+
export function BadgeCloseButton({
|
|
28
|
+
accessibilityLabel = "Remove",
|
|
29
|
+
className,
|
|
30
|
+
feedback = "fade",
|
|
31
|
+
...props
|
|
32
|
+
}: BadgeCloseButtonProps): ReactElement {
|
|
33
|
+
const { size, isDisabled } = useBadgePart("Badge.CloseButton");
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Pressable
|
|
37
|
+
accessibilityLabel={accessibilityLabel}
|
|
38
|
+
accessibilityRole="button"
|
|
39
|
+
className={badgeVariants({ size }).closeButton({ className })}
|
|
40
|
+
disabled={isDisabled}
|
|
41
|
+
feedback={feedback}
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
<Icon icon={IconCrossSmall} />
|
|
45
|
+
</Pressable>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
BadgeCloseButton.displayName = "DelacourUI.Badge.CloseButton";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import type { BadgeSlotProps } from "./badge.types";
|
|
4
|
+
import { badgeVariants } from "./badge.variants";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A centred wrapper for trailing content that is not an `Icon`.
|
|
8
|
+
*
|
|
9
|
+
* An `Icon` needs no wrapper: it inherits the badge's icon size and its
|
|
10
|
+
* surface's colour from the root's `IconDefaultsProvider`.
|
|
11
|
+
*/
|
|
12
|
+
export function BadgeEndContent({ className, ...props }: BadgeSlotProps): ReactElement {
|
|
13
|
+
return <View className={badgeVariants().endContent({ className })} {...props} />;
|
|
14
|
+
}
|
|
15
|
+
BadgeEndContent.displayName = "DelacourUI.Badge.EndContent";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type { TextProps } from "react-native";
|
|
3
|
+
import { Text } from "../text";
|
|
4
|
+
import { useBadgePart } from "./badge.context";
|
|
5
|
+
import { badgeVariants } from "./badge.variants";
|
|
6
|
+
|
|
7
|
+
export type BadgeLabelProps = TextProps & { className?: string };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The badge's text.
|
|
11
|
+
*
|
|
12
|
+
* Carries its own colour and type scale, read from the badge's context: a React
|
|
13
|
+
* Native `View` does not cascade colour to a `Text` descendant, so a colour set
|
|
14
|
+
* on the root would be lost.
|
|
15
|
+
*
|
|
16
|
+
* The class is passed rather than inherited even though the badge publishes the
|
|
17
|
+
* same one through `TextClassProvider` — the two agree, and stating it here
|
|
18
|
+
* keeps the label correct if it is ever rendered somewhere the provider is not.
|
|
19
|
+
*/
|
|
20
|
+
export function BadgeLabel({ className, ...props }: BadgeLabelProps): ReactElement {
|
|
21
|
+
const { variant, color, size } = useBadgePart("Badge.Label");
|
|
22
|
+
return <Text className={badgeVariants({ size, variant, color }).label({ className })} {...props} />;
|
|
23
|
+
}
|
|
24
|
+
BadgeLabel.displayName = "DelacourUI.Badge.Label";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import type { BadgeSlotProps } from "./badge.types";
|
|
4
|
+
import { badgeVariants } from "./badge.variants";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A centred wrapper for leading content that is not an `Icon` — a status dot, an
|
|
8
|
+
* avatar.
|
|
9
|
+
*
|
|
10
|
+
* An `Icon` needs no wrapper: it inherits the badge's icon size and its
|
|
11
|
+
* surface's colour from the root's `IconDefaultsProvider`.
|
|
12
|
+
*/
|
|
13
|
+
export function BadgeStartContent({ className, ...props }: BadgeSlotProps): ReactElement {
|
|
14
|
+
return <View className={badgeVariants().startContent({ className })} {...props} />;
|
|
15
|
+
}
|
|
16
|
+
BadgeStartContent.displayName = "DelacourUI.Badge.StartContent";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createContext, type ReactElement, type ReactNode, use } from "react";
|
|
2
|
+
import type { BadgeColor, BadgeSize, BadgeVariant } from "./badge.variants";
|
|
3
|
+
|
|
4
|
+
export type BadgeContextValue = {
|
|
5
|
+
/** Size of the badge. */
|
|
6
|
+
size: BadgeSize;
|
|
7
|
+
/** How the badge's surface is painted. */
|
|
8
|
+
variant: BadgeVariant;
|
|
9
|
+
/** What the badge's surface means. */
|
|
10
|
+
color: BadgeColor;
|
|
11
|
+
/** Whether the badge is disabled. */
|
|
12
|
+
isDisabled: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const BadgeContext = createContext<BadgeContextValue | null>(null);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Supplies the enclosing badge's variant, colour, size and state to its subtree.
|
|
19
|
+
*
|
|
20
|
+
* Lives in its own module, importing nothing but `badge.variants`, so a part can
|
|
21
|
+
* read it without importing `./badge`. That import would close a cycle, and
|
|
22
|
+
* Metro serves a partially initialised module for a cycle — leaving the context
|
|
23
|
+
* `undefined` at import time and red-boxing the app on a cold start.
|
|
24
|
+
*/
|
|
25
|
+
export function BadgeProvider({ value, children }: { value: BadgeContextValue; children: ReactNode }): ReactElement {
|
|
26
|
+
return <BadgeContext value={value}>{children}</BadgeContext>;
|
|
27
|
+
}
|
|
28
|
+
BadgeProvider.displayName = "DelacourUI.Badge.Provider";
|
|
29
|
+
|
|
30
|
+
/** The enclosing badge's context, or null outside a `<Badge>`. */
|
|
31
|
+
export function useBadgeContext(): BadgeContextValue | null {
|
|
32
|
+
return use(BadgeContext);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reads the enclosing badge's variant, colour, size and state.
|
|
37
|
+
*
|
|
38
|
+
* Lets a custom child style itself to match without the badge having to pass
|
|
39
|
+
* props down through every slot. Throws outside a `<Badge>` — use
|
|
40
|
+
* {@link useBadgeContext} where the enclosing badge is optional.
|
|
41
|
+
*/
|
|
42
|
+
export function useBadge(): BadgeContextValue {
|
|
43
|
+
const context = useBadgeContext();
|
|
44
|
+
if (!context) {
|
|
45
|
+
throw new Error("useBadge must be called inside a <Badge>.");
|
|
46
|
+
}
|
|
47
|
+
return context;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The enclosing badge's context, for a compound part that cannot work without
|
|
52
|
+
* one.
|
|
53
|
+
*
|
|
54
|
+
* Internal: deliberately not re-exported from `index.ts`. A caller outside the
|
|
55
|
+
* library wants {@link useBadge}, whose error message names the hook rather than
|
|
56
|
+
* a part.
|
|
57
|
+
*/
|
|
58
|
+
export function useBadgePart(component: string): BadgeContextValue {
|
|
59
|
+
const context = useBadgeContext();
|
|
60
|
+
if (!context) {
|
|
61
|
+
throw new Error(`${component} must be rendered inside a <Badge>.`);
|
|
62
|
+
}
|
|
63
|
+
return context;
|
|
64
|
+
}
|