@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.
Files changed (273) hide show
  1. package/README.md +113 -0
  2. package/package.json +125 -0
  3. package/src/components/accordion/AGENTS.md +193 -0
  4. package/src/components/accordion/accordion-content.tsx +125 -0
  5. package/src/components/accordion/accordion-description.tsx +17 -0
  6. package/src/components/accordion/accordion-indicator.tsx +104 -0
  7. package/src/components/accordion/accordion-item.tsx +78 -0
  8. package/src/components/accordion/accordion-title.tsx +18 -0
  9. package/src/components/accordion/accordion-trigger.tsx +148 -0
  10. package/src/components/accordion/accordion.context.tsx +153 -0
  11. package/src/components/accordion/accordion.tsx +253 -0
  12. package/src/components/accordion/accordion.types.ts +11 -0
  13. package/src/components/accordion/accordion.variants.test.ts +434 -0
  14. package/src/components/accordion/accordion.variants.ts +358 -0
  15. package/src/components/accordion/index.ts +45 -0
  16. package/src/components/badge/AGENTS.md +83 -0
  17. package/src/components/badge/badge-close-button.tsx +48 -0
  18. package/src/components/badge/badge-end-content.tsx +15 -0
  19. package/src/components/badge/badge-label.tsx +24 -0
  20. package/src/components/badge/badge-start-content.tsx +16 -0
  21. package/src/components/badge/badge.context.tsx +64 -0
  22. package/src/components/badge/badge.tsx +192 -0
  23. package/src/components/badge/badge.types.ts +10 -0
  24. package/src/components/badge/badge.variants.test.ts +269 -0
  25. package/src/components/badge/badge.variants.ts +238 -0
  26. package/src/components/badge/index.ts +17 -0
  27. package/src/components/bottom-sheet/AGENTS.md +242 -0
  28. package/src/components/bottom-sheet/bottom-sheet-background.tsx +35 -0
  29. package/src/components/bottom-sheet/bottom-sheet-close.tsx +60 -0
  30. package/src/components/bottom-sheet/bottom-sheet-container.tsx +246 -0
  31. package/src/components/bottom-sheet/bottom-sheet-content.tsx +66 -0
  32. package/src/components/bottom-sheet/bottom-sheet-description.tsx +29 -0
  33. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +150 -0
  34. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +47 -0
  35. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +180 -0
  36. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +72 -0
  37. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +105 -0
  38. package/src/components/bottom-sheet/bottom-sheet-title.tsx +24 -0
  39. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +73 -0
  40. package/src/components/bottom-sheet/bottom-sheet.context.tsx +135 -0
  41. package/src/components/bottom-sheet/bottom-sheet.tsx +125 -0
  42. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +293 -0
  43. package/src/components/bottom-sheet/bottom-sheet.variants.ts +220 -0
  44. package/src/components/bottom-sheet/index.ts +40 -0
  45. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +125 -0
  46. package/src/components/button/AGENTS.md +217 -0
  47. package/src/components/button/button-end-content.tsx +15 -0
  48. package/src/components/button/button-group-separator.tsx +37 -0
  49. package/src/components/button/button-group-text.tsx +101 -0
  50. package/src/components/button/button-group.tsx +158 -0
  51. package/src/components/button/button-label.tsx +24 -0
  52. package/src/components/button/button-start-content.tsx +15 -0
  53. package/src/components/button/button.context.tsx +187 -0
  54. package/src/components/button/button.tsx +273 -0
  55. package/src/components/button/button.types.ts +10 -0
  56. package/src/components/button/button.variants.test.ts +811 -0
  57. package/src/components/button/button.variants.ts +480 -0
  58. package/src/components/button/index.ts +50 -0
  59. package/src/components/chart/AGENTS.md +210 -0
  60. package/src/components/chart/chart-area.tsx +93 -0
  61. package/src/components/chart/chart-bar.tsx +103 -0
  62. package/src/components/chart/chart-bars.tsx +75 -0
  63. package/src/components/chart/chart-candlestick.tsx +60 -0
  64. package/src/components/chart/chart-grid.tsx +33 -0
  65. package/src/components/chart/chart-legend.tsx +41 -0
  66. package/src/components/chart/chart-line.tsx +50 -0
  67. package/src/components/chart/chart-scatter.tsx +35 -0
  68. package/src/components/chart/chart-tooltip-dot.tsx +70 -0
  69. package/src/components/chart/chart-tooltip-x.tsx +121 -0
  70. package/src/components/chart/chart-tooltip-y.tsx +55 -0
  71. package/src/components/chart/chart-tooltip.tsx +133 -0
  72. package/src/components/chart/chart-x-axis.tsx +27 -0
  73. package/src/components/chart/chart-y-axis.tsx +21 -0
  74. package/src/components/chart/chart.context.tsx +127 -0
  75. package/src/components/chart/chart.tsx +448 -0
  76. package/src/components/chart/chart.types.ts +108 -0
  77. package/src/components/chart/chart.variants.test.ts +893 -0
  78. package/src/components/chart/chart.variants.ts +726 -0
  79. package/src/components/chart/index.ts +103 -0
  80. package/src/components/chart/pie-chart-center.tsx +40 -0
  81. package/src/components/chart/pie-chart-label.tsx +56 -0
  82. package/src/components/chart/pie-chart-slice.tsx +45 -0
  83. package/src/components/chart/pie-chart-tooltip.tsx +85 -0
  84. package/src/components/chart/pie-chart.context.tsx +77 -0
  85. package/src/components/chart/pie-chart.tsx +247 -0
  86. package/src/components/chart/use-chart-font.ts +25 -0
  87. package/src/components/chart/use-chart-palette.ts +38 -0
  88. package/src/components/checkbox/AGENTS.md +185 -0
  89. package/src/components/checkbox/checkbox-box.tsx +152 -0
  90. package/src/components/checkbox/checkbox-group.tsx +107 -0
  91. package/src/components/checkbox/checkbox-label.tsx +34 -0
  92. package/src/components/checkbox/checkbox.context.tsx +136 -0
  93. package/src/components/checkbox/checkbox.tsx +258 -0
  94. package/src/components/checkbox/checkbox.types.ts +14 -0
  95. package/src/components/checkbox/checkbox.variants.test.ts +634 -0
  96. package/src/components/checkbox/checkbox.variants.ts +484 -0
  97. package/src/components/checkbox/index.ts +42 -0
  98. package/src/components/field/AGENTS.md +112 -0
  99. package/src/components/field/field-content.tsx +24 -0
  100. package/src/components/field/field-description.tsx +29 -0
  101. package/src/components/field/field-error.tsx +61 -0
  102. package/src/components/field/field-group.tsx +22 -0
  103. package/src/components/field/field-label.tsx +47 -0
  104. package/src/components/field/field-legend.tsx +34 -0
  105. package/src/components/field/field-separator.tsx +81 -0
  106. package/src/components/field/field-set.tsx +22 -0
  107. package/src/components/field/field.context.tsx +95 -0
  108. package/src/components/field/field.tsx +157 -0
  109. package/src/components/field/field.types.ts +15 -0
  110. package/src/components/field/field.variants.test.ts +240 -0
  111. package/src/components/field/field.variants.ts +156 -0
  112. package/src/components/field/index.ts +22 -0
  113. package/src/components/icon/AGENTS.md +104 -0
  114. package/src/components/icon/icon.context.tsx +29 -0
  115. package/src/components/icon/icon.tsx +92 -0
  116. package/src/components/icon/icon.variants.test.ts +111 -0
  117. package/src/components/icon/icon.variants.ts +79 -0
  118. package/src/components/icon/index.ts +11 -0
  119. package/src/components/input/AGENTS.md +126 -0
  120. package/src/components/input/index.ts +27 -0
  121. package/src/components/input/input-group-decorator.tsx +84 -0
  122. package/src/components/input/input-group-prefix.tsx +16 -0
  123. package/src/components/input/input-group-suffix.tsx +15 -0
  124. package/src/components/input/input-group.tsx +140 -0
  125. package/src/components/input/input.context.tsx +87 -0
  126. package/src/components/input/input.tsx +184 -0
  127. package/src/components/input/input.types.ts +10 -0
  128. package/src/components/input/input.variants.test.ts +369 -0
  129. package/src/components/input/input.variants.ts +329 -0
  130. package/src/components/list-group/AGENTS.md +67 -0
  131. package/src/components/list-group/index.ts +20 -0
  132. package/src/components/list-group/list-group-item-content.tsx +10 -0
  133. package/src/components/list-group/list-group-item-description.tsx +12 -0
  134. package/src/components/list-group/list-group-item-prefix.tsx +30 -0
  135. package/src/components/list-group/list-group-item-suffix.tsx +55 -0
  136. package/src/components/list-group/list-group-item-title.tsx +17 -0
  137. package/src/components/list-group/list-group-item.tsx +87 -0
  138. package/src/components/list-group/list-group.context.tsx +65 -0
  139. package/src/components/list-group/list-group.tsx +116 -0
  140. package/src/components/list-group/list-group.types.ts +14 -0
  141. package/src/components/list-group/list-group.variants.test.ts +223 -0
  142. package/src/components/list-group/list-group.variants.ts +107 -0
  143. package/src/components/pressable/AGENTS.md +74 -0
  144. package/src/components/pressable/index.ts +9 -0
  145. package/src/components/pressable/pressable.tsx +261 -0
  146. package/src/components/pressable/pressable.variants.test.ts +128 -0
  147. package/src/components/pressable/pressable.variants.ts +80 -0
  148. package/src/components/provider/AGENTS.md +90 -0
  149. package/src/components/provider/index.ts +1 -0
  150. package/src/components/provider/provider.tsx +105 -0
  151. package/src/components/radio/AGENTS.md +245 -0
  152. package/src/components/radio/index.ts +35 -0
  153. package/src/components/radio/radio-group.tsx +126 -0
  154. package/src/components/radio/radio-indicator.tsx +91 -0
  155. package/src/components/radio/radio-label.tsx +39 -0
  156. package/src/components/radio/radio.context.tsx +132 -0
  157. package/src/components/radio/radio.tsx +215 -0
  158. package/src/components/radio/radio.variants.test.ts +580 -0
  159. package/src/components/radio/radio.variants.ts +271 -0
  160. package/src/components/screen/AGENTS.md +287 -0
  161. package/src/components/screen/index.ts +85 -0
  162. package/src/components/screen/screen-chat-list.tsx +485 -0
  163. package/src/components/screen/screen-content.tsx +69 -0
  164. package/src/components/screen/screen-debug.ts +43 -0
  165. package/src/components/screen/screen-error.tsx +61 -0
  166. package/src/components/screen/screen-flat-list.tsx +88 -0
  167. package/src/components/screen/screen-footer-background.tsx +60 -0
  168. package/src/components/screen/screen-footer.tsx +170 -0
  169. package/src/components/screen/screen-header.tsx +36 -0
  170. package/src/components/screen/screen-legend-list.tsx +105 -0
  171. package/src/components/screen/screen-list-component.tsx +23 -0
  172. package/src/components/screen/screen-loading.tsx +50 -0
  173. package/src/components/screen/screen-navbar-back-button.tsx +79 -0
  174. package/src/components/screen/screen-navbar-background.tsx +45 -0
  175. package/src/components/screen/screen-navbar-subtitle.tsx +26 -0
  176. package/src/components/screen/screen-navbar-title.tsx +31 -0
  177. package/src/components/screen/screen-navbar.tsx +154 -0
  178. package/src/components/screen/screen-root.tsx +38 -0
  179. package/src/components/screen/screen-scroll-area.tsx +116 -0
  180. package/src/components/screen/screen-scroll-shadow.tsx +186 -0
  181. package/src/components/screen/screen-section-list.tsx +81 -0
  182. package/src/components/screen/screen-view.tsx +59 -0
  183. package/src/components/screen/screen.context.tsx +167 -0
  184. package/src/components/screen/screen.tsx +114 -0
  185. package/src/components/screen/screen.types.ts +66 -0
  186. package/src/components/screen/screen.variants.test.ts +602 -0
  187. package/src/components/screen/screen.variants.ts +475 -0
  188. package/src/components/screen/use-screen-scroll-insets.ts +218 -0
  189. package/src/components/separator/AGENTS.md +30 -0
  190. package/src/components/separator/index.ts +8 -0
  191. package/src/components/separator/separator.tsx +85 -0
  192. package/src/components/slider/AGENTS.md +274 -0
  193. package/src/components/slider/index.ts +50 -0
  194. package/src/components/slider/slider-fill.tsx +68 -0
  195. package/src/components/slider/slider-output.tsx +55 -0
  196. package/src/components/slider/slider-thumb.tsx +193 -0
  197. package/src/components/slider/slider-track.tsx +233 -0
  198. package/src/components/slider/slider.context.tsx +118 -0
  199. package/src/components/slider/slider.tsx +321 -0
  200. package/src/components/slider/slider.types.ts +26 -0
  201. package/src/components/slider/slider.variants.test.ts +856 -0
  202. package/src/components/slider/slider.variants.ts +661 -0
  203. package/src/components/spinner/AGENTS.md +73 -0
  204. package/src/components/spinner/index.ts +17 -0
  205. package/src/components/spinner/spinner-arc.tsx +90 -0
  206. package/src/components/spinner/spinner-content.tsx +63 -0
  207. package/src/components/spinner/spinner.context.tsx +52 -0
  208. package/src/components/spinner/spinner.tsx +128 -0
  209. package/src/components/spinner/spinner.variants.test.ts +273 -0
  210. package/src/components/spinner/spinner.variants.ts +187 -0
  211. package/src/components/switch/AGENTS.md +213 -0
  212. package/src/components/switch/index.ts +42 -0
  213. package/src/components/switch/switch-content.tsx +111 -0
  214. package/src/components/switch/switch-end-content.tsx +18 -0
  215. package/src/components/switch/switch-start-content.tsx +20 -0
  216. package/src/components/switch/switch-thumb.tsx +102 -0
  217. package/src/components/switch/switch.context.tsx +79 -0
  218. package/src/components/switch/switch.tsx +423 -0
  219. package/src/components/switch/switch.types.ts +14 -0
  220. package/src/components/switch/switch.variants.test.ts +570 -0
  221. package/src/components/switch/switch.variants.ts +511 -0
  222. package/src/components/tabs/AGENTS.md +287 -0
  223. package/src/components/tabs/index.ts +69 -0
  224. package/src/components/tabs/tabs-content.tsx +65 -0
  225. package/src/components/tabs/tabs-indicator.tsx +97 -0
  226. package/src/components/tabs/tabs-label.tsx +62 -0
  227. package/src/components/tabs/tabs-list.tsx +139 -0
  228. package/src/components/tabs/tabs-pager.tsx +59 -0
  229. package/src/components/tabs/tabs-scroll-view.tsx +171 -0
  230. package/src/components/tabs/tabs-separator.tsx +73 -0
  231. package/src/components/tabs/tabs-trigger.tsx +210 -0
  232. package/src/components/tabs/tabs.context.tsx +294 -0
  233. package/src/components/tabs/tabs.tsx +435 -0
  234. package/src/components/tabs/tabs.types.ts +13 -0
  235. package/src/components/tabs/tabs.variants.test.ts +1020 -0
  236. package/src/components/tabs/tabs.variants.ts +670 -0
  237. package/src/components/text/AGENTS.md +95 -0
  238. package/src/components/text/index.ts +25 -0
  239. package/src/components/text/text.context.tsx +60 -0
  240. package/src/components/text/text.tsx +251 -0
  241. package/src/components/text/text.variants.test.ts +422 -0
  242. package/src/components/text/text.variants.ts +282 -0
  243. package/src/display-name.test.ts +145 -0
  244. package/src/docs.test.ts +98 -0
  245. package/src/expo/navigation-theme.tsx +58 -0
  246. package/src/hooks/use-controllable-state.ts +45 -0
  247. package/src/hooks/use-keyboard-state-sync.tsx +147 -0
  248. package/src/hooks/use-navigation-theme.ts +78 -0
  249. package/src/hooks/use-theme-color.ts +44 -0
  250. package/src/icons/central.ts +1 -0
  251. package/src/lib/cn.test.ts +136 -0
  252. package/src/lib/cn.ts +29 -0
  253. package/src/lib/color.test.ts +80 -0
  254. package/src/lib/color.ts +79 -0
  255. package/src/lib/compose-refs.test.ts +64 -0
  256. package/src/lib/compose-refs.ts +37 -0
  257. package/src/lib/keyboard-animation.test.ts +33 -0
  258. package/src/lib/keyboard-animation.ts +28 -0
  259. package/src/lib/merge-props.test.ts +82 -0
  260. package/src/lib/merge-props.ts +47 -0
  261. package/src/lib/navigation-theme.test.ts +49 -0
  262. package/src/lib/navigation-theme.ts +53 -0
  263. package/src/lib/slot.tsx +45 -0
  264. package/src/lib/tv.ts +20 -0
  265. package/src/styles/base.css +2 -0
  266. package/src/styles/geometry.test.ts +117 -0
  267. package/src/styles/index.css +3 -0
  268. package/src/styles/theme-tokens.test.ts +252 -0
  269. package/src/styles/theme.css +447 -0
  270. package/src/styles/tokens.css +132 -0
  271. package/src/styles/tokens.test.ts +181 -0
  272. package/src/styles/tokens.ts +86 -0
  273. 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
+ }