banhaten 0.1.3 → 0.2.0

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 (258) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/CONTRIBUTING.md +44 -0
  3. package/README.md +65 -13
  4. package/SECURITY.md +15 -0
  5. package/banhaten.config.example.json +0 -2
  6. package/docs/design-system/README.md +9 -0
  7. package/docs/design-system/adoption-quality-remediation.md +87 -0
  8. package/docs/design-system/agent-usage.md +13 -0
  9. package/docs/design-system/appearance-presets.md +6 -1
  10. package/docs/design-system/component-docs-pattern.md +81 -0
  11. package/docs/design-system/component-token-consistency-audit.md +13 -3
  12. package/docs/design-system/components/README.md +3 -2
  13. package/docs/design-system/components/accordion.md +16 -0
  14. package/docs/design-system/components/activity-feed.md +15 -1
  15. package/docs/design-system/components/alert-dialog.md +17 -0
  16. package/docs/design-system/components/alert.md +46 -14
  17. package/docs/design-system/components/aspect-ratio.md +15 -0
  18. package/docs/design-system/components/attribute.md +16 -0
  19. package/docs/design-system/components/autocomplete.md +19 -0
  20. package/docs/design-system/components/avatar.md +18 -0
  21. package/docs/design-system/components/badge.md +19 -0
  22. package/docs/design-system/components/banner.md +15 -1
  23. package/docs/design-system/components/breadcrumbs.md +14 -0
  24. package/docs/design-system/components/button-group.md +34 -0
  25. package/docs/design-system/components/button.md +18 -0
  26. package/docs/design-system/components/card.md +18 -1
  27. package/docs/design-system/components/carousel.md +20 -3
  28. package/docs/design-system/components/catalog-components.md +15 -1
  29. package/docs/design-system/components/chart.md +15 -0
  30. package/docs/design-system/components/checkbox.md +16 -0
  31. package/docs/design-system/components/collapsible.md +15 -0
  32. package/docs/design-system/components/command.md +15 -0
  33. package/docs/design-system/components/context-menu.md +15 -0
  34. package/docs/design-system/components/date-picker.md +21 -1
  35. package/docs/design-system/components/divider.md +16 -0
  36. package/docs/design-system/components/empty-state.md +17 -0
  37. package/docs/design-system/components/field.md +17 -0
  38. package/docs/design-system/components/file-upload.md +14 -0
  39. package/docs/design-system/components/hover-card.md +15 -0
  40. package/docs/design-system/components/input-group.md +17 -0
  41. package/docs/design-system/components/input-otp.md +18 -0
  42. package/docs/design-system/components/input.md +43 -7
  43. package/docs/design-system/components/item.md +72 -0
  44. package/docs/design-system/components/kbd.md +18 -0
  45. package/docs/design-system/components/label.md +16 -0
  46. package/docs/design-system/components/menu.md +17 -0
  47. package/docs/design-system/components/menubar.md +15 -0
  48. package/docs/design-system/components/modal.md +25 -4
  49. package/docs/design-system/components/native-select.md +18 -0
  50. package/docs/design-system/components/navigation-menu.md +15 -0
  51. package/docs/design-system/components/onboarding-step-list-item.md +15 -1
  52. package/docs/design-system/components/page-header.md +15 -1
  53. package/docs/design-system/components/pagination.md +30 -8
  54. package/docs/design-system/components/popover.md +19 -0
  55. package/docs/design-system/components/progress-slider.md +18 -0
  56. package/docs/design-system/components/progress.md +17 -0
  57. package/docs/design-system/components/radio-card.md +16 -0
  58. package/docs/design-system/components/radio-group.md +17 -0
  59. package/docs/design-system/components/resizable.md +15 -0
  60. package/docs/design-system/components/scroll-area.md +15 -0
  61. package/docs/design-system/components/segmented-control.md +71 -0
  62. package/docs/design-system/components/select.md +44 -7
  63. package/docs/design-system/components/sheet.md +17 -0
  64. package/docs/design-system/components/sidebar.md +15 -0
  65. package/docs/design-system/components/skeleton.md +18 -0
  66. package/docs/design-system/components/slideout.md +14 -0
  67. package/docs/design-system/components/slider.md +20 -0
  68. package/docs/design-system/components/social-button.md +21 -3
  69. package/docs/design-system/components/spinner.md +16 -0
  70. package/docs/design-system/components/steps.md +14 -0
  71. package/docs/design-system/components/table.md +15 -1
  72. package/docs/design-system/components/tabs.md +17 -0
  73. package/docs/design-system/components/tag.md +17 -0
  74. package/docs/design-system/components/textarea.md +16 -0
  75. package/docs/design-system/components/timeline.md +14 -0
  76. package/docs/design-system/components/toast.md +17 -0
  77. package/docs/design-system/components/toggle.md +17 -0
  78. package/docs/design-system/components/toolbar.md +21 -1
  79. package/docs/design-system/components/tooltip.md +18 -1
  80. package/docs/design-system/density-and-elevation.md +44 -0
  81. package/docs/design-system/direction-and-localization.md +23 -0
  82. package/docs/design-system/form-recipes.md +73 -0
  83. package/docs/design-system/gap-promotion-policy.md +21 -0
  84. package/docs/design-system/gap-promotion-report.md +166 -0
  85. package/docs/design-system/installation.md +68 -0
  86. package/docs/design-system/launch-comparison-shadcn.md +98 -0
  87. package/docs/design-system/registry-and-cli.md +30 -7
  88. package/docs/design-system/release-policy.md +30 -0
  89. package/docs/design-system/roadmap.md +14 -0
  90. package/docs/design-system/support-matrix.md +23 -0
  91. package/docs/design-system/tabs.md +1 -1
  92. package/docs/design-system/token-governance.md +23 -1
  93. package/package.json +29 -15
  94. package/registry/component-contract.json +40 -0
  95. package/registry/components/alert-dialog.tsx +1 -1
  96. package/registry/components/alert.tsx +86 -65
  97. package/registry/components/autocomplete.tsx +23 -9
  98. package/registry/components/avatar.tsx +7 -7
  99. package/registry/components/badge.tsx +1 -1
  100. package/registry/components/button-group.tsx +321 -4
  101. package/registry/components/button.tsx +38 -17
  102. package/registry/components/card.tsx +68 -59
  103. package/registry/components/carousel.tsx +53 -5
  104. package/registry/components/chart.tsx +36 -3
  105. package/registry/components/checkbox.tsx +3 -3
  106. package/registry/components/collapsible.tsx +1 -1
  107. package/registry/components/command.tsx +1 -1
  108. package/registry/components/context-menu.tsx +1 -1
  109. package/registry/components/date-picker.tsx +245 -104
  110. package/registry/components/expanded/ActivityFeed.tsx +6 -15
  111. package/registry/components/expanded/Banner.tsx +3 -3
  112. package/registry/components/expanded/Breadcrumbs.tsx +5 -5
  113. package/registry/components/expanded/CatalogComponentsShowcase.tsx +13 -13
  114. package/registry/components/expanded/CatalogDivider.tsx +1 -1
  115. package/registry/components/expanded/EmptyState.tsx +1 -1
  116. package/registry/components/expanded/FileUpload.tsx +9 -2
  117. package/registry/components/expanded/OnboardingStepListItem.tsx +2 -4
  118. package/registry/components/expanded/PageHeader.tsx +13 -13
  119. package/registry/components/expanded/Slideout.tsx +15 -415
  120. package/registry/components/expanded/Steps.tsx +4 -4
  121. package/registry/components/expanded/Table.tsx +116 -69
  122. package/registry/components/expanded/Timeline.tsx +3 -3
  123. package/registry/components/expanded/activityFeed.css +1 -1
  124. package/registry/components/expanded/breadcrumbs.css +1 -1
  125. package/registry/components/expanded/divider.css +1 -1
  126. package/registry/components/expanded/emptyState.css +1 -1
  127. package/registry/components/expanded/fileUpload.css +6 -6
  128. package/registry/components/expanded/slideout.css +9 -685
  129. package/registry/components/expanded/steps.css +3 -3
  130. package/registry/components/expanded/table.css +2 -2
  131. package/registry/components/expanded/timeline.css +2 -2
  132. package/registry/components/input-content.ts +1 -0
  133. package/registry/components/input-otp.tsx +13 -3
  134. package/registry/components/input.tsx +68 -43
  135. package/registry/components/item.tsx +189 -0
  136. package/registry/components/kbd.tsx +111 -4
  137. package/registry/components/menu.tsx +31 -245
  138. package/registry/components/menubar.tsx +1 -1
  139. package/registry/components/modal.tsx +142 -102
  140. package/registry/components/native-select.tsx +2 -2
  141. package/registry/components/pagination.tsx +80 -58
  142. package/registry/components/popover.tsx +162 -664
  143. package/registry/components/radio-card.tsx +116 -87
  144. package/registry/components/segmented-control.tsx +87 -0
  145. package/registry/components/select-content.tsx +2 -2
  146. package/registry/components/select.tsx +52 -16
  147. package/registry/components/sheet.tsx +1 -1
  148. package/registry/components/sidebar.tsx +1 -1
  149. package/registry/components/skeleton.css +0 -3
  150. package/registry/components/social-button.tsx +47 -25
  151. package/registry/components/spinner.tsx +18 -55
  152. package/registry/components/tabs.css +1 -1
  153. package/registry/components/tabs.tsx +8 -4
  154. package/registry/components/tag.tsx +2 -2
  155. package/registry/components/textarea.tsx +3 -3
  156. package/registry/components/toast.tsx +16 -11
  157. package/registry/components/toggle.tsx +2 -2
  158. package/registry/components/toolbar.tsx +89 -17
  159. package/registry/components/tooltip.tsx +1 -1
  160. package/registry/examples/alert-demo.tsx +25 -5
  161. package/registry/examples/attribute-demo.tsx +1 -1
  162. package/registry/examples/autocomplete-demo.tsx +3 -3
  163. package/registry/examples/avatar-demo.tsx +7 -7
  164. package/registry/examples/badge-demo.tsx +1 -1
  165. package/registry/examples/button-demo.tsx +6 -6
  166. package/registry/examples/button-group-demo.tsx +23 -2
  167. package/registry/examples/card-demo.tsx +46 -8
  168. package/registry/examples/carousel-demo.tsx +20 -2
  169. package/registry/examples/chart-demo.tsx +1 -1
  170. package/registry/examples/checkbox-demo.tsx +5 -5
  171. package/registry/examples/collection-filter-bar-demo.tsx +135 -0
  172. package/registry/examples/command-demo.tsx +3 -3
  173. package/registry/examples/{expanded/command-bar-demo.tsx → command-palette-demo.tsx} +5 -15
  174. package/registry/examples/context-menu-demo.tsx +1 -1
  175. package/registry/examples/control-density-contract-demo.tsx +61 -0
  176. package/registry/examples/date-picker-demo.tsx +12 -12
  177. package/registry/examples/divider-demo.tsx +3 -3
  178. package/registry/examples/expanded/banner-demo.tsx +1 -1
  179. package/registry/examples/expanded/breadcrumbs-demo.tsx +1 -1
  180. package/registry/examples/expanded/empty-state-demo.tsx +1 -1
  181. package/registry/examples/expanded/file-upload-demo.tsx +1 -1
  182. package/registry/examples/expanded/onboarding-step-demo.tsx +1 -1
  183. package/registry/examples/expanded/slideout-demo.tsx +2 -2
  184. package/registry/examples/expanded/steps-demo.tsx +1 -1
  185. package/registry/examples/expanded/tabs-demo.tsx +1 -1
  186. package/registry/examples/expanded/timeline-demo.tsx +1 -1
  187. package/registry/examples/hover-card-demo.tsx +2 -2
  188. package/registry/examples/input-demo.tsx +6 -6
  189. package/registry/examples/input-otp-demo.tsx +4 -4
  190. package/registry/examples/item-demo.tsx +46 -0
  191. package/registry/examples/kbd-demo.tsx +1 -1
  192. package/registry/examples/label-demo.tsx +1 -1
  193. package/registry/examples/menu-demo.tsx +1 -1
  194. package/registry/examples/modal-demo.tsx +67 -9
  195. package/registry/examples/navigation-menu-demo.tsx +1 -1
  196. package/registry/examples/pagination-demo.tsx +6 -3
  197. package/registry/examples/popover-demo.tsx +19 -19
  198. package/registry/examples/progress-demo.tsx +1 -1
  199. package/registry/examples/progress-slider-demo.tsx +1 -1
  200. package/registry/examples/radio-card-demo.tsx +4 -4
  201. package/registry/examples/radio-group-demo.tsx +2 -2
  202. package/registry/examples/resizable-demo.tsx +1 -1
  203. package/registry/examples/results-toolbar-demo.tsx +44 -0
  204. package/registry/examples/scroll-area-demo.tsx +3 -3
  205. package/registry/examples/segmented-control-demo.tsx +39 -0
  206. package/registry/examples/select-demo.tsx +1 -1
  207. package/registry/examples/sidebar-demo.tsx +1 -1
  208. package/registry/examples/slider-demo.tsx +3 -3
  209. package/registry/examples/social-button-demo.tsx +18 -8
  210. package/registry/examples/spinner-demo.tsx +13 -13
  211. package/registry/examples/tabs-demo.tsx +1 -1
  212. package/registry/examples/tag-demo.tsx +1 -1
  213. package/registry/examples/textarea-demo.tsx +4 -4
  214. package/registry/examples/toast-demo.tsx +1 -1
  215. package/registry/examples/toggle-demo.tsx +6 -6
  216. package/registry/examples/tooltip-demo.tsx +4 -4
  217. package/registry/guidance/USAGE.md +27 -0
  218. package/registry/hooks/use-direction.ts +193 -13
  219. package/registry/index.json +5311 -377
  220. package/registry/styles/globals.css +2440 -4061
  221. package/registry.json +119 -149
  222. package/schema/config.schema.json +11 -8
  223. package/schema/registry.schema.json +150 -45
  224. package/schema/tokens.schema.json +134 -34
  225. package/src/cli/command-line.js +194 -0
  226. package/src/cli/config-editors.js +9 -0
  227. package/src/cli/doctor.js +399 -0
  228. package/src/cli/file-transaction.js +295 -0
  229. package/src/cli/global-css.js +153 -0
  230. package/src/cli/index.js +567 -1613
  231. package/src/cli/install-plan.js +592 -0
  232. package/src/cli/install-receipt.js +171 -0
  233. package/src/cli/jsonc.js +203 -0
  234. package/src/cli/package-install-state.js +254 -0
  235. package/src/cli/package-manager.js +238 -0
  236. package/src/cli/package-plan.js +185 -0
  237. package/src/cli/package-version.js +97 -0
  238. package/src/cli/registry-query.js +207 -0
  239. package/src/cli/reporting.js +55 -0
  240. package/src/cli/schema-validation.js +19 -0
  241. package/src/cli/source-scanner.js +686 -0
  242. package/src/cli/vite-config.js +478 -0
  243. package/tokens/banhaten.tokens.json +4312 -309
  244. package/docs/design-system/components/command-bar.md +0 -57
  245. package/registry/components/expanded/BannerBoard.tsx +0 -225
  246. package/registry/components/expanded/CatalogTag.tsx +0 -85
  247. package/registry/components/expanded/CommandBar.tsx +0 -386
  248. package/registry/components/expanded/IconExplorer.tsx +0 -612
  249. package/registry/components/expanded/TimelineShowcase.tsx +0 -158
  250. package/registry/components/expanded/commandBar.css +0 -470
  251. package/registry/components/expanded/iconExplorer.css +0 -764
  252. package/registry/components/expanded/iconPacks.ts +0 -866
  253. package/registry/components/expanded/tag.css +0 -148
  254. package/registry/examples/expanded/icons-demo.tsx +0 -5
  255. package/tokens/Color mode.zip +0 -0
  256. package/tokens/Numbers.zip +0 -0
  257. package/tokens/Radius.zip +0 -0
  258. package/tokens/Theme.zip +0 -0
@@ -31,15 +31,15 @@ const demoRtlTextareaProps = {
31
31
 
32
32
  export function TextareaDemo() {
33
33
  return (
34
- <div className="grid gap-6">
35
- <div className="flex flex-wrap items-start gap-6">
34
+ <div className="grid gap-[var(--bh-space-5xl-24)]">
35
+ <div className="flex flex-wrap items-start gap-[var(--bh-space-5xl-24)]">
36
36
  <Textarea {...demoTextareaProps} />
37
37
  <Textarea {...demoTextareaProps} state="filled" />
38
38
  <Textarea {...demoTextareaProps} hasError state="placeholder" />
39
39
  <Textarea {...demoTextareaProps} state="disabled" />
40
40
  </div>
41
41
 
42
- <div className="flex flex-wrap items-start gap-6">
42
+ <div className="flex flex-wrap items-start gap-[var(--bh-space-5xl-24)]">
43
43
  <Textarea {...demoTextareaProps} type="tags" state="placeholder" />
44
44
  <Textarea
45
45
  {...demoTextareaProps}
@@ -62,7 +62,7 @@ export function TextareaDemo() {
62
62
  />
63
63
  </div>
64
64
 
65
- <div dir="rtl" className="flex flex-wrap items-start gap-6">
65
+ <div dir="rtl" className="flex flex-wrap items-start gap-[var(--bh-space-5xl-24)]">
66
66
  <Textarea {...demoRtlTextareaProps} dir="rtl" />
67
67
  <Textarea {...demoRtlTextareaProps} dir="rtl" state="filled" />
68
68
  <Textarea {...demoRtlTextareaProps} dir="rtl" hasError state="placeholder" />
@@ -3,7 +3,7 @@ import { Toaster, toast } from "@/components/ui/toast"
3
3
 
4
4
  export function ToastDemo() {
5
5
  return (
6
- <div className="grid gap-4">
6
+ <div className="grid gap-[var(--bh-space-3xl-16)]">
7
7
  <div className="flex flex-wrap gap-[var(--bh-space-md-8)]">
8
8
  <Button size="sm" onClick={() => toast.success("Project saved")}>
9
9
  Success
@@ -6,8 +6,8 @@ export function ToggleDemo() {
6
6
  const [checked, setChecked] = React.useState(true)
7
7
 
8
8
  return (
9
- <div className="grid gap-6">
10
- <div className="flex flex-wrap items-center gap-4">
9
+ <div className="grid gap-[var(--bh-space-5xl-24)]">
10
+ <div className="flex flex-wrap items-center gap-[var(--bh-space-3xl-16)]">
11
11
  <Toggle
12
12
  checked={checked}
13
13
  onCheckedChange={setChecked}
@@ -18,14 +18,14 @@ export function ToggleDemo() {
18
18
  <Toggle aria-label="Disabled toggle" disabled />
19
19
  </div>
20
20
 
21
- <div className="grid gap-4 sm:grid-cols-2">
21
+ <div className="grid gap-[var(--bh-space-3xl-16)] sm:grid-cols-2">
22
22
  <ToggleField
23
23
  defaultChecked
24
24
  label="Label"
25
25
  description="Supporting text"
26
26
  />
27
27
  <ToggleField
28
- className="w-56"
28
+ className="w-[var(--bh-space-15xl-224)]"
29
29
  controlPosition="end"
30
30
  label="Label"
31
31
  />
@@ -42,14 +42,14 @@ export function ToggleDemo() {
42
42
  />
43
43
  </div>
44
44
 
45
- <div dir="rtl" className="grid gap-4 sm:grid-cols-2">
45
+ <div dir="rtl" className="grid gap-[var(--bh-space-3xl-16)] sm:grid-cols-2">
46
46
  <ToggleField
47
47
  defaultChecked
48
48
  label={"\u0627\u0644\u062a\u0633\u0645\u064a\u0629"}
49
49
  description={"\u0646\u0635 \u0645\u0633\u0627\u0639\u062f"}
50
50
  />
51
51
  <ToggleField
52
- className="w-56"
52
+ className="w-[var(--bh-space-15xl-224)]"
53
53
  controlPosition="end"
54
54
  label={"\u0627\u0644\u062a\u0633\u0645\u064a\u0629"}
55
55
  />
@@ -17,8 +17,8 @@ const rtlSupportText =
17
17
  export function TooltipDemo() {
18
18
  return (
19
19
  <TooltipProvider delayDuration={0}>
20
- <div className="grid gap-8">
21
- <div className="flex flex-wrap items-center gap-8">
20
+ <div className="grid gap-[var(--bh-space-6xl-32)]">
21
+ <div className="flex flex-wrap items-center gap-[var(--bh-space-6xl-32)]">
22
22
  <Tooltip defaultOpen>
23
23
  <TooltipTrigger asChild>
24
24
  <Button size="icon" variant="secondary" aria-label="More information">
@@ -53,7 +53,7 @@ export function TooltipDemo() {
53
53
  </Tooltip>
54
54
  </div>
55
55
 
56
- <div className="flex flex-wrap items-center gap-8">
56
+ <div className="flex flex-wrap items-center gap-[var(--bh-space-6xl-32)]">
57
57
  <Tooltip defaultOpen>
58
58
  <TooltipTrigger asChild>
59
59
  <Button variant="secondary">Dark large</Button>
@@ -82,7 +82,7 @@ export function TooltipDemo() {
82
82
  </Tooltip>
83
83
  </div>
84
84
 
85
- <div dir="rtl" className="flex flex-wrap items-center gap-8">
85
+ <div dir="rtl" className="flex flex-wrap items-center gap-[var(--bh-space-6xl-32)]">
86
86
  <Tooltip defaultOpen>
87
87
  <TooltipTrigger asChild>
88
88
  <Button size="icon" variant="secondary" aria-label={rtlLabel}>
@@ -0,0 +1,27 @@
1
+ # Banhaten implementation rules
2
+
3
+ This project uses the Banhaten design system. Treat the installed component API and registry metadata as the source of truth; do not redraw controls from memory or infer defaults from the first documented variant.
4
+
5
+ ## Before changing UI
6
+
7
+ 1. Run `npx banhaten search <need>` to find the intended component.
8
+ 2. Run `npx banhaten docs <component>` to inspect explicit defaults, recommended uses, avoid rules, and recipes.
9
+ 3. Run `npx banhaten view <component>` before wrapping or extending its internals.
10
+ 4. Prefer the documented golden recipe when the screen resembles a collection filter bar or results toolbar.
11
+
12
+ ## Composition rules
13
+
14
+ - Use one semantic `density` across controls in the same row: `compact` is 32px, `default` is 36px, and `comfortable` is 40px.
15
+ - Do not align Banhaten controls with raw height, padding, radius, or font-size utilities. Width and responsive layout utilities are allowed.
16
+ - Use `DatePicker` or `DateRangePicker` for dates. Do not simulate calendar fields with `Input` or native date inputs.
17
+ - Pass explicit `leadingIcon` and `trailingIcon` nodes. Use `showAtSign` or `showInfo` only for those exact semantic affordances. The legacy `hasLeadingIcon` and `hasInformationIcon` flags are deprecated.
18
+ - Use `shortcutKeys={["Mod", "K"]}` or `KbdShortcut` for shortcuts. Never hard-code a single Command glyph for every platform.
19
+ - Use `SegmentedControl` for small mutually exclusive view, density, period, or numeric choices. Do not rebuild joined numeric buttons locally.
20
+ - Use functional `Select` for option menus. `ToolbarSelect` is only a trigger-shaped toolbar primitive.
21
+ - Preserve inherited `dir`; use logical spacing and alignment utilities for RTL-safe composition.
22
+
23
+ ## Before finishing
24
+
25
+ Run `npx banhaten doctor` and inspect every diagnostic. Run `npx banhaten diff` before updating installed components, then execute the product's type, behavior, accessibility, and visual tests.
26
+
27
+ Canonical recipes live in `docs/design-system/form-recipes.md` in the Banhaten package.
@@ -4,24 +4,204 @@ import * as React from "react"
4
4
 
5
5
  export type Direction = "ltr" | "rtl"
6
6
 
7
- export function useDirection(defaultDirection: Direction = "ltr") {
8
- const [direction, setDirection] = React.useState<Direction>(defaultDirection)
7
+ type DirectionStore = {
8
+ listeners: Set<() => void>
9
+ observer?: MutationObserver
10
+ }
11
+
12
+ const DirectionContext = React.createContext<Direction | undefined>(undefined)
13
+ const directionStores = new WeakMap<Document, DirectionStore>()
14
+ const noop = () => undefined
15
+
16
+ export type DirectionProviderProps = {
17
+ children: React.ReactNode
18
+ dir: Direction
19
+ }
20
+
21
+ export function DirectionProvider({
22
+ children,
23
+ dir,
24
+ }: DirectionProviderProps) {
25
+ return React.createElement(DirectionContext.Provider, { value: dir }, children)
26
+ }
27
+
28
+ export function resolveDirection(
29
+ explicitDirection?: Direction,
30
+ element?: Element | null
31
+ ): Direction {
32
+ if (explicitDirection) return explicitDirection
33
+
34
+ let ancestor = element?.parentElement
35
+ while (ancestor) {
36
+ const inheritedDirection = normalizeDirection(ancestor.getAttribute("dir"))
37
+ if (inheritedDirection) return inheritedDirection
38
+ ancestor = ancestor.parentElement
39
+ }
40
+
41
+ const ownerDocument = element?.ownerDocument ?? getBrowserDocument()
42
+ const documentDirection = normalizeDirection(
43
+ ownerDocument?.documentElement.getAttribute("dir")
44
+ )
45
+
46
+ return documentDirection ?? "ltr"
47
+ }
48
+
49
+ export function useResolvedDirection<T extends Element>(
50
+ explicitDirection: Direction | undefined,
51
+ elementRef: React.RefObject<T | null>
52
+ ) {
53
+ const contextDirection = React.useContext(DirectionContext)
54
+ const preferredDirection = explicitDirection ?? contextDirection
55
+
56
+ const subscribe = React.useCallback(
57
+ (listener: () => void) => {
58
+ if (preferredDirection) return noop
59
+
60
+ const ownerDocument =
61
+ elementRef.current?.ownerDocument ?? getBrowserDocument()
62
+
63
+ return ownerDocument
64
+ ? subscribeToDirectionChanges(ownerDocument, listener)
65
+ : noop
66
+ },
67
+ [elementRef, preferredDirection]
68
+ )
69
+ const getSnapshot = React.useCallback(
70
+ () => resolveDirection(preferredDirection, elementRef.current),
71
+ [elementRef, preferredDirection]
72
+ )
73
+ const getServerSnapshot = React.useCallback(
74
+ () => preferredDirection ?? "ltr",
75
+ [preferredDirection]
76
+ )
77
+
78
+ return React.useSyncExternalStore(
79
+ subscribe,
80
+ getSnapshot,
81
+ getServerSnapshot
82
+ )
83
+ }
84
+
85
+ export function useDirection(defaultDirection?: Direction) {
86
+ const contextDirection = React.useContext(DirectionContext)
87
+ const preferredDirection = defaultDirection ?? contextDirection
88
+ const observedDirection = useDocumentDirection(preferredDirection)
89
+ const observedDirectionRef = React.useRef(observedDirection)
90
+ const [directionOverride, setDirectionOverride] = React.useState<
91
+ Direction | undefined
92
+ >(undefined)
93
+
94
+ observedDirectionRef.current = observedDirection
95
+
96
+ const setDirection = React.useCallback<
97
+ React.Dispatch<React.SetStateAction<Direction>>
98
+ >((nextDirection) => {
99
+ setDirectionOverride((currentDirection) => {
100
+ const selectedDirection =
101
+ currentDirection ?? observedDirectionRef.current
102
+
103
+ return typeof nextDirection === "function"
104
+ ? nextDirection(selectedDirection)
105
+ : nextDirection
106
+ })
107
+ }, [])
9
108
 
10
109
  React.useEffect(() => {
11
- const htmlDirection = document.documentElement.getAttribute("dir")
110
+ if (defaultDirection === undefined && contextDirection) {
111
+ setDirectionOverride(contextDirection)
112
+ }
113
+ }, [contextDirection, defaultDirection])
114
+
115
+ return [directionOverride ?? observedDirection, setDirection] as const
116
+ }
117
+
118
+ function useDocumentDirection(preferredDirection?: Direction) {
119
+ const subscribe = React.useCallback(
120
+ (listener: () => void) => {
121
+ if (preferredDirection) return noop
122
+
123
+ const ownerDocument = getBrowserDocument()
124
+ return ownerDocument
125
+ ? subscribeToDirectionChanges(ownerDocument, listener)
126
+ : noop
127
+ },
128
+ [preferredDirection]
129
+ )
130
+ const getSnapshot = React.useCallback(
131
+ () => resolveDirection(preferredDirection),
132
+ [preferredDirection]
133
+ )
134
+ const getServerSnapshot = React.useCallback(
135
+ () => preferredDirection ?? "ltr",
136
+ [preferredDirection]
137
+ )
138
+
139
+ return React.useSyncExternalStore(
140
+ subscribe,
141
+ getSnapshot,
142
+ getServerSnapshot
143
+ )
144
+ }
145
+
146
+ function subscribeToDirectionChanges(
147
+ ownerDocument: Document,
148
+ listener: () => void
149
+ ) {
150
+ const store = getDirectionStore(ownerDocument)
151
+ store.listeners.add(listener)
152
+
153
+ if (store.listeners.size === 1) {
154
+ startDirectionObserver(ownerDocument, store)
155
+ }
156
+
157
+ return () => {
158
+ store.listeners.delete(listener)
12
159
 
13
- if (htmlDirection === "ltr" || htmlDirection === "rtl") {
14
- setDirection(htmlDirection)
15
- return
160
+ if (store.listeners.size === 0) {
161
+ store.observer?.disconnect()
162
+ store.observer = undefined
16
163
  }
164
+ }
165
+ }
17
166
 
18
- document.documentElement.setAttribute("dir", defaultDirection)
19
- }, [defaultDirection])
167
+ function getDirectionStore(ownerDocument: Document) {
168
+ const existingStore = directionStores.get(ownerDocument)
169
+ if (existingStore) return existingStore
20
170
 
21
- const updateDirection = React.useCallback((nextDirection: Direction) => {
22
- document.documentElement.setAttribute("dir", nextDirection)
23
- setDirection(nextDirection)
24
- }, [])
171
+ const store: DirectionStore = { listeners: new Set() }
172
+ directionStores.set(ownerDocument, store)
173
+ return store
174
+ }
175
+
176
+ function startDirectionObserver(
177
+ ownerDocument: Document,
178
+ store: DirectionStore
179
+ ) {
180
+ const MutationObserverConstructor =
181
+ ownerDocument.defaultView?.MutationObserver
182
+ const directionRoot = ownerDocument.documentElement
183
+
184
+ if (!MutationObserverConstructor || !directionRoot) return
185
+
186
+ store.observer = new MutationObserverConstructor(() => {
187
+ for (const listener of [...store.listeners]) {
188
+ listener()
189
+ }
190
+ })
191
+ store.observer.observe(directionRoot, {
192
+ attributeFilter: ["dir"],
193
+ attributes: true,
194
+ subtree: true,
195
+ })
196
+ }
197
+
198
+ function getBrowserDocument() {
199
+ return typeof document === "undefined" ? undefined : document
200
+ }
25
201
 
26
- return [direction, updateDirection] as const
202
+ function normalizeDirection(value?: string | null): Direction | undefined {
203
+ const normalizedValue = value?.trim().toLowerCase()
204
+ return normalizedValue === "ltr" || normalizedValue === "rtl"
205
+ ? normalizedValue
206
+ : undefined
27
207
  }