banhaten 0.4.0 → 0.5.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.
- package/CHANGELOG.md +139 -100
- package/CONTRIBUTING.md +46 -44
- package/README.md +192 -190
- package/SECURITY.md +15 -15
- package/banhaten.config.example.json +11 -11
- package/docs/design-spacing.md +13 -0
- package/docs/design-system/README.md +22 -20
- package/docs/design-system/adoption-quality-remediation.md +87 -87
- package/docs/design-system/agent-usage.md +13 -13
- package/docs/design-system/appearance-presets.md +189 -189
- package/docs/design-system/appearances/default.md +94 -94
- package/docs/design-system/appearances/rounded.md +95 -95
- package/docs/design-system/appearances/sharp.md +95 -95
- package/docs/design-system/component-docs-pattern.md +81 -81
- package/docs/design-system/component-implementation-ownership-audit.md +245 -245
- package/docs/design-system/component-quality-audit-2026-08-20.md +225 -225
- package/docs/design-system/component-showcase-consistency-report.md +219 -219
- package/docs/design-system/component-token-consistency-audit.md +173 -173
- package/docs/design-system/components/README.md +90 -76
- package/docs/design-system/components/accordion.md +53 -53
- package/docs/design-system/components/activity-feed.md +94 -94
- package/docs/design-system/components/activity-heatmap.md +57 -57
- package/docs/design-system/components/agent-input.md +85 -0
- package/docs/design-system/components/alert-dialog.md +72 -72
- package/docs/design-system/components/alert.md +97 -97
- package/docs/design-system/components/approval-card.md +134 -0
- package/docs/design-system/components/aspect-ratio.md +45 -45
- package/docs/design-system/components/attribute.md +88 -88
- package/docs/design-system/components/autocomplete.md +75 -75
- package/docs/design-system/components/avatar.md +54 -54
- package/docs/design-system/components/badge.md +54 -54
- package/docs/design-system/components/banner.md +87 -87
- package/docs/design-system/components/breadcrumbs.md +176 -175
- package/docs/design-system/components/button-group.md +98 -98
- package/docs/design-system/components/button.md +83 -80
- package/docs/design-system/components/card.md +88 -88
- package/docs/design-system/components/carousel.md +46 -46
- package/docs/design-system/components/catalog-components.md +47 -47
- package/docs/design-system/components/chart.md +95 -24
- package/docs/design-system/components/checkbox.md +54 -54
- package/docs/design-system/components/collapsible.md +67 -53
- package/docs/design-system/components/command.md +62 -62
- package/docs/design-system/components/context-menu.md +46 -46
- package/docs/design-system/components/date-picker.md +81 -81
- package/docs/design-system/components/divider.md +102 -102
- package/docs/design-system/components/empty-state.md +56 -56
- package/docs/design-system/components/field.md +70 -70
- package/docs/design-system/components/file-upload.md +187 -187
- package/docs/design-system/components/hover-card.md +48 -48
- package/docs/design-system/components/icons.md +48 -48
- package/docs/design-system/components/inline-citations.md +93 -0
- package/docs/design-system/components/input-group.md +58 -58
- package/docs/design-system/components/input-otp.md +56 -56
- package/docs/design-system/components/input.md +114 -60
- package/docs/design-system/components/item.md +59 -59
- package/docs/design-system/components/kbd.md +51 -49
- package/docs/design-system/components/label.md +49 -49
- package/docs/design-system/components/menu.md +71 -61
- package/docs/design-system/components/menubar.md +47 -47
- package/docs/design-system/components/modal.md +105 -105
- package/docs/design-system/components/native-select.md +54 -54
- package/docs/design-system/components/navigation-menu.md +50 -50
- package/docs/design-system/components/onboarding-step-list-item.md +82 -82
- package/docs/design-system/components/page-header.md +93 -86
- package/docs/design-system/components/pagination.md +57 -57
- package/docs/design-system/components/popover.md +60 -60
- package/docs/design-system/components/progress-slider.md +50 -50
- package/docs/design-system/components/progress.md +76 -76
- package/docs/design-system/components/radio-card.md +51 -51
- package/docs/design-system/components/radio-group.md +57 -57
- package/docs/design-system/components/resizable.md +44 -44
- package/docs/design-system/components/rolling-count.md +61 -0
- package/docs/design-system/components/scroll-area.md +51 -51
- package/docs/design-system/components/segmented-control.md +54 -54
- package/docs/design-system/components/select.md +68 -62
- package/docs/design-system/components/sheet.md +67 -67
- package/docs/design-system/components/shimmer-text.md +67 -0
- package/docs/design-system/components/sidebar.md +110 -99
- package/docs/design-system/components/skeleton.md +75 -75
- package/docs/design-system/components/slideout.md +65 -65
- package/docs/design-system/components/slider.md +63 -63
- package/docs/design-system/components/social-button.md +50 -50
- package/docs/design-system/components/spinner.md +62 -62
- package/docs/design-system/components/steps.md +65 -65
- package/docs/design-system/components/streaming-text.md +82 -0
- package/docs/design-system/components/table.md +419 -414
- package/docs/design-system/components/tabs.md +59 -55
- package/docs/design-system/components/tag.md +80 -80
- package/docs/design-system/components/task-list.md +95 -0
- package/docs/design-system/components/textarea.md +50 -50
- package/docs/design-system/components/thinking-reasoning.md +72 -0
- package/docs/design-system/components/thinking-state.md +67 -0
- package/docs/design-system/components/timeline.md +83 -83
- package/docs/design-system/components/toast.md +58 -58
- package/docs/design-system/components/toggle.md +81 -81
- package/docs/design-system/components/toolbar.md +92 -87
- package/docs/design-system/components/tooltip.md +94 -92
- package/docs/design-system/components/typography.md +18 -18
- package/docs/design-system/density-and-elevation.md +65 -65
- package/docs/design-system/design-system-test-missing-items.md +368 -368
- package/docs/design-system/direction-and-localization.md +23 -23
- package/docs/design-system/form-recipes.md +73 -73
- package/docs/design-system/gap-promotion-policy.md +21 -21
- package/docs/design-system/icons.md +74 -74
- package/docs/design-system/installation.md +70 -68
- package/docs/design-system/launch-comparison-shadcn.md +98 -98
- package/docs/design-system/registry-and-cli.md +64 -64
- package/docs/design-system/release-policy.md +30 -30
- package/docs/design-system/roadmap.md +14 -14
- package/docs/design-system/support-matrix.md +23 -23
- package/docs/design-system/tabs.md +54 -53
- package/docs/design-system/template-gap-audit.md +32 -0
- package/docs/design-system/templates.md +59 -0
- package/docs/design-system/token-governance.md +72 -59
- package/package.json +98 -98
- package/registry/component-contract.json +40 -40
- package/registry/components/accordion.tsx +155 -155
- package/registry/components/agent-input-parts.tsx +552 -0
- package/registry/components/agent-input-state.ts +156 -0
- package/registry/components/agent-input.css +307 -0
- package/registry/components/agent-input.tsx +382 -0
- package/registry/components/alert-dialog.tsx +297 -297
- package/registry/components/alert.tsx +293 -293
- package/registry/components/approval-card.css +409 -0
- package/registry/components/approval-card.tsx +401 -0
- package/registry/components/approval-frame.tsx +108 -0
- package/registry/components/approval-questions.tsx +559 -0
- package/registry/components/aspect-ratio.tsx +30 -30
- package/registry/components/attribute.tsx +454 -454
- package/registry/components/autocomplete.tsx +598 -651
- package/registry/components/avatar.tsx +385 -385
- package/registry/components/badge.tsx +629 -629
- package/registry/components/button-group.tsx +565 -565
- package/registry/components/button.tsx +127 -123
- package/registry/components/card.tsx +638 -638
- package/registry/components/carousel.tsx +296 -296
- package/registry/components/chart-block-data.ts +177 -0
- package/registry/components/chart-block.css +171 -0
- package/registry/components/chart-block.tsx +260 -0
- package/registry/components/chart-interactions.ts +123 -0
- package/registry/components/chart-options.cartesian.ts +162 -0
- package/registry/components/chart-options.paint.ts +99 -0
- package/registry/components/chart-options.parts.ts +72 -0
- package/registry/components/chart-options.radial.ts +109 -0
- package/registry/components/chart-options.ts +44 -0
- package/registry/components/chart-options.types.ts +67 -0
- package/registry/components/chart.tsx +203 -203
- package/registry/components/checkbox.tsx +247 -247
- package/registry/components/collapsible.tsx +109 -69
- package/registry/components/command.tsx +208 -208
- package/registry/components/context-menu.tsx +236 -236
- package/registry/components/data-chart.tsx +40 -0
- package/registry/components/date-picker.tsx +1289 -1289
- package/registry/components/echart-theme.ts +246 -0
- package/registry/components/echart.css +49 -0
- package/registry/components/echart.tsx +178 -0
- package/registry/components/expanded/ActivityFeed.tsx +227 -227
- package/registry/components/expanded/ActivityHeatmap.tsx +64 -64
- package/registry/components/expanded/Banner.tsx +179 -179
- package/registry/components/expanded/Breadcrumbs.tsx +131 -131
- package/registry/components/expanded/CatalogComponentsShowcase.tsx +205 -205
- package/registry/components/expanded/CatalogDivider.tsx +49 -49
- package/registry/components/expanded/EmptyState.tsx +155 -155
- package/registry/components/expanded/FileUpload.tsx +543 -543
- package/registry/components/expanded/OnboardingStepListItem.tsx +60 -60
- package/registry/components/expanded/PageHeader.tsx +179 -166
- package/registry/components/expanded/Slideout.tsx +108 -108
- package/registry/components/expanded/Steps.tsx +402 -402
- package/registry/components/expanded/Table.tsx +1092 -1071
- package/registry/components/expanded/Timeline.tsx +203 -203
- package/registry/components/expanded/activityFeed.css +248 -248
- package/registry/components/expanded/activityHeatmap.css +66 -66
- package/registry/components/expanded/banner.css +260 -260
- package/registry/components/expanded/breadcrumbs.css +140 -140
- package/registry/components/expanded/catalogComponentsShowcase.css +87 -87
- package/registry/components/expanded/divider.css +75 -75
- package/registry/components/expanded/emptyState.css +111 -111
- package/registry/components/expanded/fileUpload.css +457 -457
- package/registry/components/expanded/onboardingStepListItem.css +135 -135
- package/registry/components/expanded/pageHeader.css +248 -290
- package/registry/components/expanded/slideout.css +276 -276
- package/registry/components/expanded/steps.css +356 -356
- package/registry/components/expanded/table.css +606 -606
- package/registry/components/expanded/timeline.css +286 -286
- package/registry/components/field.tsx +283 -283
- package/registry/components/hover-card.tsx +48 -48
- package/registry/components/inline-citations.css +126 -0
- package/registry/components/inline-citations.tsx +292 -0
- package/registry/components/input-content.ts +107 -107
- package/registry/components/input-group.tsx +130 -130
- package/registry/components/input-otp.tsx +584 -584
- package/registry/components/input.tsx +990 -979
- package/registry/components/item.tsx +189 -189
- package/registry/components/kbd.tsx +246 -245
- package/registry/components/label.tsx +78 -78
- package/registry/components/menu.tsx +978 -986
- package/registry/components/menubar.tsx +226 -226
- package/registry/components/modal.tsx +872 -872
- package/registry/components/native-select.tsx +205 -205
- package/registry/components/navigation-menu.tsx +171 -171
- package/registry/components/pagination.css +36 -36
- package/registry/components/pagination.tsx +560 -560
- package/registry/components/popover.tsx +338 -338
- package/registry/components/progress-slider.tsx +216 -216
- package/registry/components/radio-card.tsx +676 -676
- package/registry/components/radio-group.tsx +570 -570
- package/registry/components/resizable.tsx +108 -108
- package/registry/components/rolling-count.css +37 -0
- package/registry/components/rolling-count.tsx +110 -0
- package/registry/components/scroll-area.tsx +91 -91
- package/registry/components/segmented-control.tsx +87 -87
- package/registry/components/select-content.tsx +218 -218
- package/registry/components/select.tsx +937 -932
- package/registry/components/sheet.tsx +305 -305
- package/registry/components/shimmer-text.css +69 -0
- package/registry/components/shimmer-text.tsx +43 -0
- package/registry/components/sidebar.tsx +1433 -1230
- package/registry/components/skeleton.css +54 -54
- package/registry/components/skeleton.tsx +482 -482
- package/registry/components/slider.tsx +380 -380
- package/registry/components/social-button.tsx +387 -387
- package/registry/components/spinner.tsx +78 -78
- package/registry/components/streaming-text.css +45 -0
- package/registry/components/streaming-text.tsx +165 -0
- package/registry/components/table-elements.tsx +128 -128
- package/registry/components/tabs.css +222 -218
- package/registry/components/tabs.tsx +241 -241
- package/registry/components/tag.tsx +423 -423
- package/registry/components/task-list.css +328 -0
- package/registry/components/task-list.tsx +429 -0
- package/registry/components/textarea.tsx +610 -610
- package/registry/components/thinking-reasoning.css +178 -0
- package/registry/components/thinking-reasoning.tsx +253 -0
- package/registry/components/thinking-state.css +8 -0
- package/registry/components/thinking-state.tsx +46 -0
- package/registry/components/toast.tsx +136 -136
- package/registry/components/toggle.tsx +256 -256
- package/registry/components/toolbar.tsx +539 -539
- package/registry/components/tooltip.tsx +433 -431
- package/registry/examples/agent-input-demo.tsx +90 -0
- package/registry/examples/alert-demo.tsx +34 -34
- package/registry/examples/alert-dialog-demo.tsx +42 -42
- package/registry/examples/approval-card-demo.tsx +83 -0
- package/registry/examples/aspect-ratio-demo.tsx +11 -11
- package/registry/examples/attribute-demo.tsx +65 -65
- package/registry/examples/autocomplete-demo.tsx +109 -109
- package/registry/examples/avatar-demo.tsx +132 -132
- package/registry/examples/badge-demo.tsx +69 -69
- package/registry/examples/button-demo.tsx +86 -83
- package/registry/examples/button-group-demo.tsx +63 -63
- package/registry/examples/card-demo.tsx +86 -86
- package/registry/examples/carousel-demo.tsx +43 -43
- package/registry/examples/chart-block-demo.tsx +5 -0
- package/registry/examples/chart-demo.tsx +18 -33
- package/registry/examples/checkbox-demo.tsx +62 -62
- package/registry/examples/collapsible-demo.tsx +16 -16
- package/registry/examples/collection-filter-bar-demo.tsx +135 -135
- package/registry/examples/command-demo.tsx +42 -42
- package/registry/examples/command-palette-demo.tsx +235 -235
- package/registry/examples/context-menu-demo.tsx +29 -29
- package/registry/examples/control-density-contract-demo.tsx +65 -61
- package/registry/examples/date-picker-demo.tsx +127 -127
- package/registry/examples/divider-demo.tsx +17 -17
- package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -29
- package/registry/examples/expanded/banner-demo.tsx +48 -48
- package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -59
- package/registry/examples/expanded/empty-state-demo.tsx +39 -39
- package/registry/examples/expanded/file-upload-demo.tsx +60 -60
- package/registry/examples/expanded/onboarding-step-demo.tsx +11 -11
- package/registry/examples/expanded/slideout-demo.tsx +15 -15
- package/registry/examples/expanded/steps-demo.tsx +29 -29
- package/registry/examples/expanded/tabs-demo.tsx +13 -13
- package/registry/examples/expanded/timeline-demo.tsx +18 -18
- package/registry/examples/field-demo.tsx +51 -51
- package/registry/examples/hover-card-demo.tsx +23 -23
- package/registry/examples/inline-citations-demo.tsx +31 -0
- package/registry/examples/input-actions-demo.tsx +36 -0
- package/registry/examples/input-demo.tsx +87 -87
- package/registry/examples/input-group-demo.tsx +16 -16
- package/registry/examples/input-otp-demo.tsx +72 -72
- package/registry/examples/item-demo.tsx +46 -46
- package/registry/examples/kbd-demo.tsx +12 -12
- package/registry/examples/label-demo.tsx +20 -20
- package/registry/examples/menu-demo.tsx +122 -122
- package/registry/examples/menubar-demo.tsx +34 -34
- package/registry/examples/modal-demo.tsx +74 -74
- package/registry/examples/native-select-demo.tsx +16 -16
- package/registry/examples/navigation-menu-demo.tsx +29 -29
- package/registry/examples/pagination-demo.tsx +20 -20
- package/registry/examples/popover-demo.tsx +547 -546
- package/registry/examples/progress-demo.tsx +37 -37
- package/registry/examples/progress-slider-demo.tsx +29 -29
- package/registry/examples/radio-card-demo.tsx +51 -51
- package/registry/examples/radio-group-demo.tsx +62 -62
- package/registry/examples/resizable-demo.tsx +22 -22
- package/registry/examples/results-toolbar-demo.tsx +44 -44
- package/registry/examples/rolling-count-demo.tsx +39 -0
- package/registry/examples/scroll-area-demo.tsx +15 -15
- package/registry/examples/segmented-control-demo.tsx +39 -39
- package/registry/examples/select-demo.tsx +87 -87
- package/registry/examples/sheet-demo.tsx +47 -47
- package/registry/examples/shimmer-text-demo.tsx +20 -0
- package/registry/examples/sidebar-demo.tsx +101 -101
- package/registry/examples/slider-demo.tsx +31 -31
- package/registry/examples/social-button-demo.tsx +61 -61
- package/registry/examples/spinner-demo.tsx +59 -59
- package/registry/examples/streaming-text-demo.tsx +28 -0
- package/registry/examples/tabs-demo.tsx +13 -13
- package/registry/examples/tag-demo.tsx +29 -29
- package/registry/examples/task-list-demo.tsx +74 -0
- package/registry/examples/textarea-demo.tsx +79 -79
- package/registry/examples/thinking-reasoning-demo.tsx +74 -0
- package/registry/examples/thinking-state-demo.tsx +13 -0
- package/registry/examples/toast-demo.tsx +35 -35
- package/registry/examples/toggle-demo.tsx +59 -59
- package/registry/examples/tooltip-demo.tsx +115 -115
- package/registry/guidance/USAGE.md +27 -27
- package/registry/guidance/charts.LICENSE.txt +24 -0
- package/registry/hooks/use-controllable-state.ts +33 -0
- package/registry/hooks/use-direction.ts +207 -207
- package/registry/hooks/use-listbox-navigation.ts +73 -0
- package/registry/hooks/use-motion-end.ts +55 -0
- package/registry/hooks/use-reduced-motion.ts +28 -0
- package/registry/index.json +1403 -155
- package/registry/styles/globals.css +946 -1088
- package/registry.json +361 -8
- package/schema/config.schema.json +51 -51
- package/schema/registry.schema.json +194 -190
- package/schema/tokens.schema.json +167 -163
- package/src/cli/command-line.js +194 -194
- package/src/cli/config-editors.js +9 -9
- package/src/cli/doctor.js +399 -399
- package/src/cli/file-transaction.js +302 -295
- package/src/cli/global-css.js +153 -153
- package/src/cli/index.js +1002 -1009
- package/src/cli/install-plan.js +592 -592
- package/src/cli/install-receipt.js +171 -171
- package/src/cli/jsonc.js +203 -203
- package/src/cli/package-install-state.js +254 -254
- package/src/cli/package-manager.js +238 -238
- package/src/cli/package-plan.js +185 -185
- package/src/cli/package-version.js +97 -97
- package/src/cli/registry-query.js +207 -207
- package/src/cli/reporting.js +55 -55
- package/src/cli/schema-validation.js +19 -19
- package/src/cli/source-scanner.js +686 -686
- package/src/cli/vite-config.js +478 -478
- package/tokens/banhaten.tokens.json +306 -192
|
@@ -1,207 +1,207 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
|
|
5
|
-
export type Direction = "ltr" | "rtl"
|
|
6
|
-
|
|
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
|
-
}, [])
|
|
108
|
-
|
|
109
|
-
React.useEffect(() => {
|
|
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)
|
|
159
|
-
|
|
160
|
-
if (store.listeners.size === 0) {
|
|
161
|
-
store.observer?.disconnect()
|
|
162
|
-
store.observer = undefined
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function getDirectionStore(ownerDocument: Document) {
|
|
168
|
-
const existingStore = directionStores.get(ownerDocument)
|
|
169
|
-
if (existingStore) return existingStore
|
|
170
|
-
|
|
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
|
-
}
|
|
201
|
-
|
|
202
|
-
function normalizeDirection(value?: string | null): Direction | undefined {
|
|
203
|
-
const normalizedValue = value?.trim().toLowerCase()
|
|
204
|
-
return normalizedValue === "ltr" || normalizedValue === "rtl"
|
|
205
|
-
? normalizedValue
|
|
206
|
-
: undefined
|
|
207
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
export type Direction = "ltr" | "rtl"
|
|
6
|
+
|
|
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
|
+
}, [])
|
|
108
|
+
|
|
109
|
+
React.useEffect(() => {
|
|
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)
|
|
159
|
+
|
|
160
|
+
if (store.listeners.size === 0) {
|
|
161
|
+
store.observer?.disconnect()
|
|
162
|
+
store.observer = undefined
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getDirectionStore(ownerDocument: Document) {
|
|
168
|
+
const existingStore = directionStores.get(ownerDocument)
|
|
169
|
+
if (existingStore) return existingStore
|
|
170
|
+
|
|
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
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizeDirection(value?: string | null): Direction | undefined {
|
|
203
|
+
const normalizedValue = value?.trim().toLowerCase()
|
|
204
|
+
return normalizedValue === "ltr" || normalizedValue === "rtl"
|
|
205
|
+
? normalizedValue
|
|
206
|
+
: undefined
|
|
207
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Active-option state for a listbox that a text field drives through
|
|
7
|
+
* `aria-activedescendant`. Arrow moves wrap and skip disabled options; the
|
|
8
|
+
* active option stays put while it remains valid and otherwise falls back to
|
|
9
|
+
* the first enabled one, so filtering never strands it.
|
|
10
|
+
*/
|
|
11
|
+
export function useListboxNavigation<T>({
|
|
12
|
+
idPrefix,
|
|
13
|
+
isDisabled,
|
|
14
|
+
open,
|
|
15
|
+
options,
|
|
16
|
+
}: {
|
|
17
|
+
idPrefix: string
|
|
18
|
+
isDisabled?: (option: T) => boolean
|
|
19
|
+
open: boolean
|
|
20
|
+
options: readonly T[]
|
|
21
|
+
}) {
|
|
22
|
+
const [activeIndex, setActiveIndex] = React.useState(-1)
|
|
23
|
+
const isDisabledRef = React.useRef(isDisabled)
|
|
24
|
+
isDisabledRef.current = isDisabled
|
|
25
|
+
|
|
26
|
+
const enabledIndexes = () =>
|
|
27
|
+
options.flatMap((option, index) =>
|
|
28
|
+
isDisabledRef.current?.(option) ? [] : [index]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
if (!open) {
|
|
33
|
+
setActiveIndex(-1)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
setActiveIndex((current) => {
|
|
37
|
+
const enabled = options.flatMap((option, index) =>
|
|
38
|
+
isDisabledRef.current?.(option) ? [] : [index]
|
|
39
|
+
)
|
|
40
|
+
return enabled.includes(current) ? current : enabled[0] ?? -1
|
|
41
|
+
})
|
|
42
|
+
}, [open, options])
|
|
43
|
+
|
|
44
|
+
/** Moves by one enabled option, wrapping at either end. */
|
|
45
|
+
const move = (offset: 1 | -1) => {
|
|
46
|
+
const enabled = enabledIndexes()
|
|
47
|
+
setActiveIndex((current) => {
|
|
48
|
+
if (enabled.length === 0) return -1
|
|
49
|
+
const position = enabled.indexOf(current)
|
|
50
|
+
if (position === -1) return offset > 0 ? enabled[0] : enabled[enabled.length - 1]
|
|
51
|
+
return enabled[(position + offset + enabled.length) % enabled.length]
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Jumps to the first or last enabled option. */
|
|
56
|
+
const moveToEdge = (edge: "first" | "last") => {
|
|
57
|
+
const enabled = enabledIndexes()
|
|
58
|
+
setActiveIndex((edge === "first" ? enabled[0] : enabled[enabled.length - 1]) ?? -1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const optionId = (index: number) => `${idPrefix}-option-${index}`
|
|
62
|
+
const current =
|
|
63
|
+
open && activeIndex >= 0 && activeIndex < options.length ? activeIndex : -1
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
activeIndex: current,
|
|
67
|
+
activeOptionId: current >= 0 ? optionId(current) : undefined,
|
|
68
|
+
move,
|
|
69
|
+
moveToEdge,
|
|
70
|
+
optionId,
|
|
71
|
+
setActiveIndex,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Calls `done` once the CSS animations and transitions running on `element`
|
|
7
|
+
* have finished, so script timing follows the stylesheet and its motion
|
|
8
|
+
* tokens instead of copying their values. When nothing is running (styles
|
|
9
|
+
* missing, `animation: none`, or no Web Animations support) it calls `done`
|
|
10
|
+
* right away. Returns a function that cancels the pending call.
|
|
11
|
+
*/
|
|
12
|
+
export function waitForMotion(element: Element, done: () => void) {
|
|
13
|
+
let pending = true
|
|
14
|
+
const running =
|
|
15
|
+
typeof element.getAnimations === "function"
|
|
16
|
+
? element
|
|
17
|
+
.getAnimations()
|
|
18
|
+
.filter(
|
|
19
|
+
(animation) => animation.effect?.getTiming().iterations !== Infinity
|
|
20
|
+
)
|
|
21
|
+
: []
|
|
22
|
+
|
|
23
|
+
if (running.length === 0) {
|
|
24
|
+
done()
|
|
25
|
+
} else {
|
|
26
|
+
void Promise.allSettled(running.map((animation) => animation.finished)).then(
|
|
27
|
+
() => {
|
|
28
|
+
if (pending) done()
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return () => {
|
|
34
|
+
pending = false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Calls `onDone` after the motion that turning `active` on starts on the
|
|
40
|
+
* element in `ref`, for example an exit animation before unmounting.
|
|
41
|
+
*/
|
|
42
|
+
export function useMotionEnd(
|
|
43
|
+
ref: React.RefObject<Element | null>,
|
|
44
|
+
active: boolean,
|
|
45
|
+
onDone: () => void
|
|
46
|
+
) {
|
|
47
|
+
const onDoneRef = React.useRef(onDone)
|
|
48
|
+
onDoneRef.current = onDone
|
|
49
|
+
|
|
50
|
+
React.useLayoutEffect(() => {
|
|
51
|
+
const element = ref.current
|
|
52
|
+
if (!active || !element) return
|
|
53
|
+
return waitForMotion(element, () => onDoneRef.current())
|
|
54
|
+
}, [active, ref])
|
|
55
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
const query = "(prefers-reduced-motion: reduce)"
|
|
6
|
+
|
|
7
|
+
function canMatchMedia() {
|
|
8
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function subscribe(onChange: () => void) {
|
|
12
|
+
if (!canMatchMedia()) return () => undefined
|
|
13
|
+
const media = window.matchMedia(query)
|
|
14
|
+
media.addEventListener?.("change", onChange)
|
|
15
|
+
return () => media.removeEventListener?.("change", onChange)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getSnapshot() {
|
|
19
|
+
return canMatchMedia() && window.matchMedia(query).matches
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether the user asked for reduced motion. Re-renders when that setting
|
|
24
|
+
* changes, and reads false where it cannot be queried (server, tests).
|
|
25
|
+
*/
|
|
26
|
+
export function useReducedMotion() {
|
|
27
|
+
return React.useSyncExternalStore(subscribe, getSnapshot, () => false)
|
|
28
|
+
}
|