@godxjp/ui 19.6.0 → 20.0.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 (137) hide show
  1. package/dist/components/data-display/card.d.ts +2 -2
  2. package/dist/components/data-display/collapsible.d.ts +23 -4
  3. package/dist/components/data-display/collapsible.js +119 -4
  4. package/dist/components/data-display/index.d.ts +3 -1
  5. package/dist/components/data-display/index.js +2 -0
  6. package/dist/components/data-display/legend.d.ts +15 -0
  7. package/dist/components/data-display/legend.js +11 -0
  8. package/dist/components/data-display/list-row.js +1 -1
  9. package/dist/components/data-display/popover.d.ts +58 -5
  10. package/dist/components/data-display/popover.js +206 -27
  11. package/dist/components/data-display/progress.d.ts +50 -2
  12. package/dist/components/data-display/progress.js +57 -9
  13. package/dist/components/data-entry/calendar.d.ts +1 -1
  14. package/dist/components/data-entry/calendar.js +67 -32
  15. package/dist/components/data-entry/checkbox.d.ts +19 -6
  16. package/dist/components/data-entry/checkbox.js +55 -16
  17. package/dist/components/data-entry/choice-option.d.ts +1 -1
  18. package/dist/components/data-entry/date-range-picker.d.ts +1 -1
  19. package/dist/components/data-entry/field.js +0 -1
  20. package/dist/components/data-entry/form.js +1 -1
  21. package/dist/components/data-entry/label.d.ts +3 -2
  22. package/dist/components/data-entry/label.js +23 -10
  23. package/dist/components/data-entry/month-picker.d.ts +1 -1
  24. package/dist/components/data-entry/month-range-picker.d.ts +1 -1
  25. package/dist/components/data-entry/transfer.js +16 -6
  26. package/dist/components/data-entry/tree-select-strategy.d.ts +1 -1
  27. package/dist/components/data-entry/tree-select.js +29 -12
  28. package/dist/components/data-entry/tree-utils.d.ts +1 -1
  29. package/dist/components/data-entry/upload.js +7 -2
  30. package/dist/components/feedback/dialog.d.ts +105 -38
  31. package/dist/components/feedback/dialog.js +272 -194
  32. package/dist/components/feedback/overlay-close-focus.d.ts +31 -0
  33. package/dist/components/feedback/overlay-close-focus.js +31 -0
  34. package/dist/components/feedback/overlay-header-tone.d.ts +1 -1
  35. package/dist/components/feedback/sheet.d.ts +53 -11
  36. package/dist/components/feedback/sheet.js +150 -81
  37. package/dist/components/feedback/tooltip.d.ts +51 -7
  38. package/dist/components/feedback/tooltip.js +107 -25
  39. package/dist/components/general/button.js +2 -2
  40. package/dist/components/general/logo.d.ts +17 -0
  41. package/dist/components/general/logo.js +22 -16
  42. package/dist/components/layout/app-shell.d.ts +1 -1
  43. package/dist/components/layout/app-shell.js +52 -8
  44. package/dist/components/layout/aspect-ratio.js +0 -1
  45. package/dist/components/layout/auth-divider.js +0 -1
  46. package/dist/components/layout/flex.d.ts +1 -1
  47. package/dist/components/layout/flex.js +19 -2
  48. package/dist/components/layout/nav-surface.d.ts +26 -0
  49. package/dist/components/layout/nav-surface.js +17 -0
  50. package/dist/components/layout/org-switcher.d.ts +5 -1
  51. package/dist/components/layout/org-switcher.js +5 -3
  52. package/dist/components/layout/separator.js +0 -1
  53. package/dist/components/layout/sidebar.js +4 -1
  54. package/dist/components/layout/split-pane.d.ts +14 -1
  55. package/dist/components/layout/split-pane.js +26 -13
  56. package/dist/components/layout/topbar-item.js +1 -2
  57. package/dist/components/navigation/app-setting-picker.js +21 -2
  58. package/dist/components/navigation/app-setting-toggle.d.ts +16 -0
  59. package/dist/components/navigation/app-setting-toggle.js +96 -0
  60. package/dist/components/navigation/dropdown-menu.d.ts +199 -18
  61. package/dist/components/navigation/dropdown-menu.js +344 -117
  62. package/dist/components/navigation/index.d.ts +2 -0
  63. package/dist/components/navigation/index.js +2 -0
  64. package/dist/components/navigation/pagination-utils.d.ts +2 -1
  65. package/dist/components/navigation/tabs.d.ts +47 -6
  66. package/dist/components/navigation/tabs.js +152 -90
  67. package/dist/components/ui/accordion.d.ts +50 -5
  68. package/dist/components/ui/accordion.js +239 -33
  69. package/dist/components/ui/aspect-ratio.d.ts +23 -2
  70. package/dist/components/ui/aspect-ratio.js +15 -13
  71. package/dist/components/ui/avatar.d.ts +29 -4
  72. package/dist/components/ui/avatar.js +111 -25
  73. package/dist/components/ui/hover-card.d.ts +42 -4
  74. package/dist/components/ui/hover-card.js +183 -27
  75. package/dist/components/ui/label.js +0 -1
  76. package/dist/components/ui/segmented.d.ts +3 -3
  77. package/dist/components/ui/separator.d.ts +8 -2
  78. package/dist/components/ui/separator.js +17 -9
  79. package/dist/components/ui/toggle-group.d.ts +50 -5
  80. package/dist/components/ui/toggle-group.js +79 -20
  81. package/dist/components/ui/toggle.d.ts +31 -5
  82. package/dist/components/ui/toggle.js +42 -3
  83. package/dist/i18n/messages/en.json +8 -0
  84. package/dist/i18n/messages/ja.json +8 -0
  85. package/dist/i18n/messages/vi.json +8 -0
  86. package/dist/lib/control-styles.d.ts +1 -1
  87. package/dist/lib/control-styles.js +1 -1
  88. package/dist/lib/slot.d.ts +32 -0
  89. package/dist/lib/slot.js +22 -0
  90. package/dist/lib/variants.d.ts +22 -3
  91. package/dist/lib/variants.js +56 -1
  92. package/dist/props/components/app.prop.d.ts +25 -1
  93. package/dist/props/components/data-display.prop.d.ts +17 -0
  94. package/dist/props/components/data-entry.prop.d.ts +32 -4
  95. package/dist/props/components/index.d.ts +1 -1
  96. package/dist/props/components/layout.prop.d.ts +118 -5
  97. package/dist/props/components/navigation.prop.d.ts +1 -1
  98. package/dist/props/registry.d.ts +22 -2
  99. package/dist/props/registry.js +33 -2
  100. package/dist/props/vocabulary/content.prop.d.ts +1 -1
  101. package/dist/props/vocabulary/index.d.ts +2 -2
  102. package/dist/props/vocabulary/interaction.prop.d.ts +17 -1
  103. package/dist/props/vocabulary/layout.prop.d.ts +57 -1
  104. package/dist/styles/control.css +142 -1
  105. package/dist/styles/core.css +5 -2
  106. package/dist/styles/data-display-layout.css +72 -0
  107. package/dist/styles/focus-ring.css +4 -0
  108. package/dist/styles/index.css +5 -2
  109. package/dist/styles/layout.css +106 -0
  110. package/dist/styles/shell-layout.css +140 -8
  111. package/dist/styles/text-layout.css +5 -4
  112. package/dist/tokens/base.css +1 -1
  113. package/dist/tokens/components/data-display.css +11 -0
  114. package/dist/tokens/components/shell.css +3 -1
  115. package/dist/tokens/foundation.css +2 -0
  116. package/docs/COMPONENTS.md +9 -3
  117. package/docs/DESIGN-AUTHORITY.md +96 -71
  118. package/docs/FRAME-COVERAGE-REPORT.md +5 -3
  119. package/docs/README.md +14 -14
  120. package/docs/WHAT-BELONGS-HERE.md +179 -0
  121. package/docs/data-display/legend.tsx +145 -0
  122. package/docs/data-display/progress.tsx +32 -0
  123. package/docs/data-entry/segmented.tsx +1 -1
  124. package/docs/data-entry/select.tsx +2 -2
  125. package/docs/feedback/sheet.tsx +1 -1
  126. package/docs/layout/app-shell-arrangements.tsx +225 -0
  127. package/docs/layout/aspect-ratio.tsx +1 -1
  128. package/docs/navigation/app-setting-picker.tsx +26 -1
  129. package/docs/navigation/app-setting-toggle.tsx +111 -0
  130. package/package.json +11 -8
  131. package/scripts/_agent-setup.mjs +165 -3
  132. package/scripts/consumer-rule.md +98 -0
  133. package/scripts/guinea-pig-skill.md +322 -0
  134. package/scripts/init-guinea-pig.mjs +62 -0
  135. package/scripts/postinstall.mjs +13 -2
  136. package/scripts/ui-audit.mjs +115 -14
  137. /package/dist/tokens/{antd.generated.css → derived.css} +0 -0
@@ -1,9 +1,18 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import * as TabsPrimitive from "@radix-ui/react-tabs";
4
+ import {
5
+ Tab as AriaTab,
6
+ TabList as AriaTabList,
7
+ TabPanel as AriaTabPanel,
8
+ Tabs as AriaTabs
9
+ } from "react-aria-components";
5
10
  import { cn } from "../../lib/utils.js";
6
11
  import { useKeepActiveTabVisible } from "./tabs-scroll.js";
12
+ const TabsFrameContext = React.createContext({
13
+ orientation: "horizontal",
14
+ selectionSuppressed: false
15
+ });
7
16
  function resolveFallbackTabValue(items, requested) {
8
17
  if (!items || items.length === 0) return requested;
9
18
  if (requested !== void 0) {
@@ -12,34 +21,57 @@ function resolveFallbackTabValue(items, requested) {
12
21
  }
13
22
  return items.find((item) => !item.disabled)?.value;
14
23
  }
24
+ function withDomProps(element, slot, raw, domProps, own) {
25
+ const dom = domProps;
26
+ return React.createElement(element, {
27
+ "data-slot": slot,
28
+ ...raw,
29
+ ...dom,
30
+ id: raw.id ?? dom.id,
31
+ ...own
32
+ });
33
+ }
15
34
  function Tabs({
16
35
  className,
17
36
  orientation = "horizontal",
18
37
  items,
19
38
  value,
20
39
  defaultValue,
40
+ onValueChange,
41
+ activationMode,
42
+ dir,
21
43
  variant = "default",
22
44
  listClassName,
23
45
  contentClassName,
46
+ children,
24
47
  ...props
25
48
  }) {
26
49
  const resolvedDefault = resolveFallbackTabValue(items, defaultValue);
27
- return /* @__PURE__ */ jsx(
28
- TabsPrimitive.Root,
50
+ const selectionSuppressed = value === void 0 && items != null && items.length > 0 && resolvedDefault === void 0;
51
+ const frame = React.useMemo(
52
+ () => ({ orientation, selectionSuppressed }),
53
+ [orientation, selectionSuppressed]
54
+ );
55
+ return /* @__PURE__ */ jsx(TabsFrameContext.Provider, { value: frame, children: /* @__PURE__ */ jsx(
56
+ AriaTabs,
29
57
  {
30
- "data-slot": "tabs",
31
- "data-orientation": orientation,
32
- "data-variant": variant,
33
58
  orientation,
34
- value,
35
- defaultValue: value === void 0 ? resolvedDefault : void 0,
59
+ keyboardActivation: activationMode,
60
+ selectedKey: value,
61
+ defaultSelectedKey: value === void 0 ? resolvedDefault : void 0,
62
+ onSelectionChange: onValueChange ? (key) => {
63
+ onValueChange(String(key));
64
+ } : void 0,
36
65
  className: cn(
37
66
  // Structure only.
38
67
  // live in src/styles/navigation-layout.css so the gap reads --tabs-root-gap and a service
39
68
  "group/tabs flex data-[orientation=horizontal]:flex-col",
40
69
  className
41
70
  ),
42
- ...props,
71
+ render: (domProps) => withDomProps("div", "tabs", { dir, ...props }, domProps, {
72
+ "data-orientation": orientation,
73
+ "data-variant": variant
74
+ }),
43
75
  children: items ? /* @__PURE__ */ jsxs(Fragment, { children: [
44
76
  /* @__PURE__ */ jsx(
45
77
  TabsList,
@@ -103,91 +135,121 @@ function Tabs({
103
135
  },
104
136
  item.value
105
137
  ))
106
- ] }) : props.children
138
+ ] }) : children
107
139
  }
108
- );
140
+ ) });
109
141
  }
110
- const TabsList = React.forwardRef(({ className, variant = "default", ...props }, ref) => {
111
- const listRef = React.useRef(null);
112
- const setListRef = React.useCallback(
113
- (node) => {
114
- listRef.current = node;
115
- if (typeof ref === "function") ref(node);
116
- else if (ref) ref.current = node;
117
- },
118
- [ref]
119
- );
120
- useKeepActiveTabVisible(listRef);
121
- return /* @__PURE__ */ jsx(
122
- TabsPrimitive.List,
123
- {
124
- ref: setListRef,
125
- "data-slot": "tabs-list",
126
- "data-variant": variant,
127
- className: cn(
128
- // `min-w-0 max-w-full` let the list shrink to (and never exceed) whatever width its
129
- // ancestors actually give it instead of forcing them wider; horizontal orientation then
130
- // scrolls its own overflow rather than clipping/hiding long localized labels in a narrow
131
- // Hidden scrollbar keeps the strip visually clean while staying
132
- // swipeable on touch and reachable via keyboard (arrow-key roving focus still scrolls the
133
- // newly-focused trigger into view natively); `useKeepActiveTabVisible` above re-pins the
134
- // Vertical
135
- // orientation is untouched — it already stacks in a column and is sized by its own
136
- // `h-*`/`w-*` overrides.
137
- // justify-center-SAFE: the strip is a centred flex box that also scrolls. Plain `center`
138
- // splits the overflow across BOTH edges, and scrollLeft only ever covers the trailing one
139
- // so the leading tab sat permanently outside the scrollport (an unreachable control at
140
- // 320px, WCAG 2.2 SC 2.1.1). `safe` falls back to start alignment exactly when it
141
- // overflows, and still centres whenever the tabs fit.
142
- "group/tabs-list text-muted-foreground data-[variant=default]:bg-muted inline-flex w-fit max-w-full min-w-0 items-center justify-center-safe rounded-lg p-1 group-data-[orientation=vertical]/tabs:flex-col data-[orientation=horizontal]:[scrollbar-width:none] data-[orientation=horizontal]:overflow-x-auto data-[orientation=horizontal]:overflow-y-hidden data-[variant=line]:gap-1 data-[variant=line]:rounded-none data-[variant=line]:bg-transparent [&[data-orientation=horizontal]::-webkit-scrollbar]:hidden",
143
- className
144
- ),
145
- ...props
146
- }
147
- );
148
- });
149
- TabsList.displayName = TabsPrimitive.List.displayName;
150
- const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
151
- TabsPrimitive.Trigger,
152
- {
153
- ref,
154
- "data-slot": "tabs-trigger",
155
- className: cn(
156
- // SELECTED IS A BORDER TINT, NOT A RING — and that is a WCAG fix, not a style preference.
157
- // It used to be `ring-1 ring-primary/25`, which writes `--tw-ring-shadow` in the UTILITIES
158
- // layer and therefore overwrote the focus ring that focus-ring.css feeds from `components`.
159
- // Measured: a focused ACTIVE tab painted `oklab(… / 0.25) 0 0 0 1px` and nothing else — a
160
- // 1px indicator at 25% alpha, failing both clauses of SC 2.4.13 (2px perimeter, ≥3:1),
161
- // while every other control in the library carried a full 2px ring. The trigger already
162
- // owns `border border-transparent`, so tinting that border reproduces the selected hairline
163
- // at the same colour and width with no layout change, and leaves `--tw-ring-shadow` free for
164
- // the focus ring. `shadow-sm` stays: its composite READS `--tw-ring-shadow`, so the lift and
165
- // the ring coexist.
166
- //
167
- // No `focus-visible:outline-1` either. It was the fallback that survived the clobber — a 1px
168
- // currentColor line squeezed between the border and the ring once both paint. The ring is
169
- // the indicator now; two marks for one state is what this pass exists to remove.
170
- //
171
- // The line indicator lives in src/styles/navigation-layout.css so it reads --tabs-indicator-*.
172
- // Selected and focused stay visually distinct (WCAG 2.4.7): selected is a 1px hairline in the
173
- // border, focused is the 2px ring plus its halo outside it.
174
- "text-muted-foreground ring-offset-background hover:text-foreground ui-focus-ring data-[state=active]:bg-background data-[state=active]:text-foreground group-data-[variant=default]/tabs-list:data-[state=active]:border-primary/25 relative inline-flex flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=line]/tabs-list:border-e-0 group-data-[variant=line]/tabs-list:border-b-0 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none",
175
- className
176
- ),
177
- ...props
142
+ const TabsList = React.forwardRef(
143
+ ({ className, variant = "default", loop: _loop, children, ...props }, ref) => {
144
+ const listRef = React.useRef(null);
145
+ const setListRef = React.useCallback(
146
+ (node) => {
147
+ listRef.current = node;
148
+ if (typeof ref === "function") ref(node);
149
+ else if (ref) ref.current = node;
150
+ },
151
+ [ref]
152
+ );
153
+ useKeepActiveTabVisible(listRef);
154
+ return /* @__PURE__ */ jsx(
155
+ AriaTabList,
156
+ {
157
+ ref: setListRef,
158
+ className: cn(
159
+ // `min-w-0 max-w-full` let the list shrink to (and never exceed) whatever width its
160
+ // ancestors actually give it instead of forcing them wider; horizontal orientation then
161
+ // scrolls its own overflow rather than clipping/hiding long localized labels in a narrow
162
+ // Hidden scrollbar keeps the strip visually clean while staying
163
+ // swipeable on touch and reachable via keyboard (arrow-key roving focus still scrolls the
164
+ // newly-focused trigger into view natively); `useKeepActiveTabVisible` above re-pins the
165
+ // Vertical
166
+ // orientation is untouched — it already stacks in a column and is sized by its own
167
+ // `h-*`/`w-*` overrides.
168
+ // justify-center-SAFE: the strip is a centred flex box that also scrolls. Plain `center`
169
+ // splits the overflow across BOTH edges, and scrollLeft only ever covers the trailing one
170
+ // so the leading tab sat permanently outside the scrollport (an unreachable control at
171
+ // 320px, WCAG 2.2 SC 2.1.1). `safe` falls back to start alignment exactly when it
172
+ // overflows, and still centres whenever the tabs fit.
173
+ "group/tabs-list text-muted-foreground data-[variant=default]:bg-muted inline-flex w-fit max-w-full min-w-0 items-center justify-center-safe rounded-lg p-1 group-data-[orientation=vertical]/tabs:flex-col data-[orientation=horizontal]:[scrollbar-width:none] data-[orientation=horizontal]:overflow-x-auto data-[orientation=horizontal]:overflow-y-hidden data-[variant=line]:gap-1 data-[variant=line]:rounded-none data-[variant=line]:bg-transparent [&[data-orientation=horizontal]::-webkit-scrollbar]:hidden",
174
+ className
175
+ ),
176
+ render: (domProps) => withDomProps("div", "tabs-list", props, domProps, { "data-variant": variant }),
177
+ children
178
+ }
179
+ );
180
+ }
181
+ );
182
+ TabsList.displayName = "TabsList";
183
+ const TabsTrigger = React.forwardRef(
184
+ ({ className, value, disabled, children, ...props }, ref) => {
185
+ const { orientation, selectionSuppressed } = React.useContext(TabsFrameContext);
186
+ return /* @__PURE__ */ jsx(
187
+ AriaTab,
188
+ {
189
+ ref,
190
+ id: value,
191
+ isDisabled: disabled,
192
+ className: cn(
193
+ // SELECTED IS A BORDER TINT, NOT A RING — and that is a WCAG fix, not a style preference.
194
+ // It used to be `ring-1 ring-primary/25`, which writes `--tw-ring-shadow` in the UTILITIES
195
+ // layer and therefore overwrote the focus ring that focus-ring.css feeds from `components`.
196
+ // Measured: a focused ACTIVE tab painted `oklab(… / 0.25) 0 0 0 1px` and nothing else — a
197
+ // 1px indicator at 25% alpha, failing both clauses of SC 2.4.13 (2px perimeter, ≥3:1),
198
+ // while every other control in the library carried a full 2px ring. The trigger already
199
+ // owns `border border-transparent`, so tinting that border reproduces the selected hairline
200
+ // at the same colour and width with no layout change, and leaves `--tw-ring-shadow` free for
201
+ // the focus ring. `shadow-sm` stays: its composite READS `--tw-ring-shadow`, so the lift and
202
+ // the ring coexist.
203
+ //
204
+ // No `focus-visible:outline-1` either. It was the fallback that survived the clobber — a 1px
205
+ // currentColor line squeezed between the border and the ring once both paint. The ring is
206
+ // the indicator now; two marks for one state is what this pass exists to remove.
207
+ //
208
+ // The line indicator lives in src/styles/navigation-layout.css so it reads --tabs-indicator-*.
209
+ // Selected and focused stay visually distinct (WCAG 2.4.7): selected is a 1px hairline in the
210
+ // border, focused is the 2px ring plus its halo outside it.
211
+ "text-muted-foreground ring-offset-background hover:text-foreground ui-focus-ring data-[state=active]:bg-background data-[state=active]:text-foreground group-data-[variant=default]/tabs-list:data-[state=active]:border-primary/25 relative inline-flex flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=line]/tabs-list:border-e-0 group-data-[variant=line]/tabs-list:border-b-0 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none",
212
+ className
213
+ ),
214
+ render: (domProps, renderProps) => withDomProps("button", "tabs-trigger", { type: "button", ...props }, domProps, {
215
+ // `data-state` is what `src/styles/navigation-layout.css` and `tabs-scroll.ts` both
216
+ // key on. RAC only writes `data-selected` on the selected tab, so it is re-emitted
217
+ // here for BOTH states, exactly as Radix did.
218
+ "data-state": renderProps.isSelected && !selectionSuppressed ? "active" : "inactive",
219
+ "aria-selected": renderProps.isSelected && !selectionSuppressed,
220
+ "data-orientation": orientation,
221
+ disabled,
222
+ tabIndex: props.tabIndex ?? domProps.tabIndex
223
+ }),
224
+ children
225
+ }
226
+ );
178
227
  }
179
- ));
180
- TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
181
- const TabsContent = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
182
- TabsPrimitive.Content,
183
- {
184
- ref,
185
- "data-slot": "tabs-content",
186
- className: cn("ui-focus-ring flex-1 outline-none", className),
187
- ...props
228
+ );
229
+ TabsTrigger.displayName = "TabsTrigger";
230
+ const TabsContent = React.forwardRef(
231
+ ({ className, value, forceMount, children, ...props }, ref) => {
232
+ const { orientation, selectionSuppressed } = React.useContext(TabsFrameContext);
233
+ if (selectionSuppressed && !forceMount) return null;
234
+ return /* @__PURE__ */ jsx(
235
+ AriaTabPanel,
236
+ {
237
+ ref,
238
+ id: value,
239
+ shouldForceMount: forceMount,
240
+ className: cn("ui-focus-ring flex-1 outline-none", className),
241
+ render: (domProps, renderProps) => withDomProps("div", "tabs-content", props, domProps, {
242
+ // A force-mounted panel whose tab is not selected: Radix marked it `data-state="inactive"`
243
+ // and `hidden`; RAC marks it inert. The state hook is re-emitted so CSS keys the same way.
244
+ "data-state": renderProps.isInert || selectionSuppressed ? "inactive" : "active",
245
+ "data-orientation": orientation
246
+ }),
247
+ children
248
+ }
249
+ );
188
250
  }
189
- ));
190
- TabsContent.displayName = TabsPrimitive.Content.displayName;
251
+ );
252
+ TabsContent.displayName = "TabsContent";
191
253
  export {
192
254
  Tabs,
193
255
  TabsContent,
@@ -1,6 +1,51 @@
1
1
  import * as React from "react";
2
- import * as AccordionPrimitive from "@radix-ui/react-accordion";
3
- export declare const Accordion: React.ForwardRefExoticComponent<(AccordionPrimitive.AccordionMultipleProps | AccordionPrimitive.AccordionSingleProps) & React.RefAttributes<HTMLDivElement>>;
4
- export declare const AccordionItem: React.ForwardRefExoticComponent<Omit<AccordionPrimitive.AccordionItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
5
- export declare const AccordionTrigger: React.ForwardRefExoticComponent<Omit<AccordionPrimitive.AccordionTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
6
- export declare const AccordionContent: React.ForwardRefExoticComponent<Omit<AccordionPrimitive.AccordionContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
2
+ interface AccordionBaseProps extends Omit<React.ComponentPropsWithRef<"div">, "dir"> {
3
+ /** Trục xếp mục đổi cặp phím mũi tên điều hướng. */
4
+ orientation?: "vertical" | "horizontal";
5
+ /** Chiều viết; chỉ đổi mũi tên trái/phải khi `orientation="horizontal"`. */
6
+ dir?: "ltr" | "rtl";
7
+ /** Khoá mọi mục. */
8
+ disabled?: boolean;
9
+ }
10
+ interface AccordionSingleProps extends AccordionBaseProps {
11
+ /** Chỉ một mục mở tại một thời điểm. */
12
+ type: "single";
13
+ /** Mục đang mở, có kiểm soát. */
14
+ value?: string;
15
+ /** Mục mở ban đầu khi không kiểm soát. */
16
+ defaultValue?: string;
17
+ /** Gọi khi mục đang mở đổi; chuỗi rỗng nghĩa là đã đóng hết. */
18
+ onValueChange?: (value: string) => void;
19
+ /** Cho phép đóng chính mục đang mở. Mặc định sai, như Radix. */
20
+ collapsible?: boolean;
21
+ }
22
+ interface AccordionMultipleProps extends AccordionBaseProps {
23
+ /** Nhiều mục mở độc lập. */
24
+ type: "multiple";
25
+ /** Các mục đang mở, có kiểm soát. */
26
+ value?: string[];
27
+ /** Các mục mở ban đầu khi không kiểm soát. */
28
+ defaultValue?: string[];
29
+ /** Gọi khi tập mục đang mở đổi. */
30
+ onValueChange?: (value: string[]) => void;
31
+ }
32
+ type AccordionProps = AccordionSingleProps | AccordionMultipleProps;
33
+ export declare function Accordion(accordionProps: AccordionProps): React.JSX.Element;
34
+ interface AccordionItemProps extends React.ComponentPropsWithRef<"div"> {
35
+ /** Khoá riêng mục này. */
36
+ disabled?: boolean;
37
+ /** Khoá định danh của mục — khớp với `value` của `Accordion`. */
38
+ value: string;
39
+ }
40
+ export declare function AccordionItem({ className, disabled, value, children, ref, ...props }: AccordionItemProps): React.JSX.Element;
41
+ interface AccordionTriggerProps extends React.ComponentPropsWithRef<"button"> {
42
+ /** Mượn thẻ của con thay vì dựng `<button>` riêng. */
43
+ asChild?: boolean;
44
+ }
45
+ export declare function AccordionTrigger({ asChild, className, children, onClick, ...props }: AccordionTriggerProps): React.JSX.Element;
46
+ interface AccordionContentProps extends React.ComponentPropsWithRef<"div"> {
47
+ /** Giữ nội dung trong DOM cả khi đóng. */
48
+ forceMount?: true;
49
+ }
50
+ export declare function AccordionContent({ className, forceMount, children, ref, ...props }: AccordionContentProps): React.JSX.Element;
51
+ export {};
@@ -1,45 +1,251 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import * as AccordionPrimitive from "@radix-ui/react-accordion";
4
+ import { chain, mergeRefs } from "@react-aria/utils";
5
+ import {
6
+ ButtonContext,
7
+ Disclosure,
8
+ DisclosureGroup,
9
+ DisclosureGroupStateContext,
10
+ DisclosurePanel,
11
+ DisclosureStateContext,
12
+ useSlottedContext
13
+ } from "react-aria-components";
5
14
  import { ChevronDown } from "lucide-react";
6
15
  import { cn } from "../../lib/utils.js";
7
- const Accordion = AccordionPrimitive.Root;
8
- const AccordionItem = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
9
- AccordionPrimitive.Item,
10
- {
11
- ref,
12
- "data-slot": "accordion-item",
13
- className: cn("ui-accordion-item", className),
14
- ...props
16
+ import { Slot } from "../../lib/slot.js";
17
+ const AccordionGroupContext = React.createContext({
18
+ orientation: "vertical"
19
+ });
20
+ const AccordionItemContext = React.createContext(null);
21
+ function useAccordionItem(component) {
22
+ const item = React.useContext(AccordionItemContext);
23
+ const state = React.useContext(DisclosureStateContext);
24
+ if (!item || !state) {
25
+ throw new Error(`\`${component}\` ph\u1EA3i n\u1EB1m trong \`AccordionItem\`.`);
15
26
  }
16
- ));
17
- AccordionItem.displayName = "AccordionItem";
18
- const AccordionTrigger = React.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsx(AccordionPrimitive.Header, { className: "ui-accordion-header", children: /* @__PURE__ */ jsxs(
19
- AccordionPrimitive.Trigger,
20
- {
21
- ref,
22
- "data-slot": "accordion-trigger",
23
- className: cn("ui-accordion-trigger", className),
24
- ...props,
25
- children: [
26
- children,
27
- /* @__PURE__ */ jsx(ChevronDown, { className: "ui-accordion-chevron", "aria-hidden": "true" })
28
- ]
27
+ return { item, state };
28
+ }
29
+ const TRIGGER_SELECTOR = '[data-slot="accordion-trigger"]:not([disabled])';
30
+ function toArray(value) {
31
+ if (value === void 0) {
32
+ return [];
29
33
  }
30
- ) }));
31
- AccordionTrigger.displayName = "AccordionTrigger";
32
- const AccordionContent = React.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsx(
33
- AccordionPrimitive.Content,
34
- {
34
+ return Array.isArray(value) ? value : [value];
35
+ }
36
+ function Accordion(accordionProps) {
37
+ const {
38
+ type,
39
+ value,
40
+ defaultValue,
41
+ onValueChange,
42
+ collapsible,
43
+ orientation = "vertical",
44
+ dir,
45
+ disabled,
46
+ onKeyDown,
47
+ children,
35
48
  ref,
36
- "data-slot": "accordion-content",
37
- className: cn("ui-accordion-content", className),
38
- ...props,
39
- children: /* @__PURE__ */ jsx("div", { className: "ui-accordion-content-inner", children })
49
+ ...props
50
+ } = accordionProps;
51
+ const [uncontrolled, setUncontrolled] = React.useState(() => toArray(defaultValue));
52
+ const isControlled = value !== void 0;
53
+ const expandedKeys = new Set(isControlled ? toArray(value) : uncontrolled);
54
+ function handleExpandedChange(keys) {
55
+ const next = [...keys].map(String);
56
+ if (type === "single" && !collapsible && next.length === 0) {
57
+ return;
58
+ }
59
+ if (!isControlled) {
60
+ setUncontrolled(next);
61
+ }
62
+ onValueChange?.(type === "multiple" ? next : next[0] ?? "");
63
+ }
64
+ function moveFocus(event) {
65
+ const from = event.target.closest?.(TRIGGER_SELECTOR);
66
+ if (!from) {
67
+ return;
68
+ }
69
+ const horizontal = orientation === "horizontal";
70
+ const forward = horizontal ? dir === "rtl" ? "ArrowLeft" : "ArrowRight" : "ArrowDown";
71
+ const backward = horizontal ? dir === "rtl" ? "ArrowRight" : "ArrowLeft" : "ArrowUp";
72
+ const triggers = [...event.currentTarget.querySelectorAll(TRIGGER_SELECTOR)];
73
+ const at = triggers.indexOf(from);
74
+ if (at < 0) {
75
+ return;
76
+ }
77
+ let to;
78
+ if (event.key === forward) {
79
+ to = (at + 1) % triggers.length;
80
+ } else if (event.key === backward) {
81
+ to = (at - 1 + triggers.length) % triggers.length;
82
+ } else if (event.key === "Home") {
83
+ to = 0;
84
+ } else if (event.key === "End") {
85
+ to = triggers.length - 1;
86
+ } else {
87
+ return;
88
+ }
89
+ event.preventDefault();
90
+ triggers[to].focus();
40
91
  }
41
- ));
42
- AccordionContent.displayName = "AccordionContent";
92
+ return /* @__PURE__ */ jsx(
93
+ DisclosureGroup,
94
+ {
95
+ allowsMultipleExpanded: type === "multiple",
96
+ expandedKeys,
97
+ onExpandedChange: handleExpandedChange,
98
+ isDisabled: disabled,
99
+ render: (racProps) => {
100
+ const {
101
+ className: _racClass,
102
+ "data-rac": _rac,
103
+ ref: racRef,
104
+ ...rest
105
+ } = racProps;
106
+ return /* @__PURE__ */ jsx(
107
+ "div",
108
+ {
109
+ ...props,
110
+ ...rest,
111
+ ref: mergeRefs(ref, racRef),
112
+ dir,
113
+ "data-orientation": orientation,
114
+ onKeyDown: chain(onKeyDown, moveFocus)
115
+ }
116
+ );
117
+ },
118
+ children: /* @__PURE__ */ jsx(AccordionGroupContext.Provider, { value: { orientation }, children })
119
+ }
120
+ );
121
+ }
122
+ function AccordionItem({
123
+ className,
124
+ disabled,
125
+ value,
126
+ children,
127
+ ref,
128
+ ...props
129
+ }) {
130
+ const { orientation } = React.useContext(AccordionGroupContext);
131
+ const groupState = React.useContext(DisclosureGroupStateContext);
132
+ const isDisabled = Boolean(disabled || groupState?.isDisabled);
133
+ return /* @__PURE__ */ jsx(
134
+ Disclosure,
135
+ {
136
+ id: value,
137
+ isDisabled,
138
+ render: (racProps, { isExpanded }) => {
139
+ const {
140
+ className: _racClass,
141
+ "data-rac": _rac,
142
+ ref: racRef,
143
+ ...rest
144
+ } = racProps;
145
+ return /* @__PURE__ */ jsx(
146
+ "div",
147
+ {
148
+ "data-slot": "accordion-item",
149
+ className: cn("ui-accordion-item", className),
150
+ ...props,
151
+ ...rest,
152
+ ref: mergeRefs(ref, racRef),
153
+ "data-state": isExpanded ? "open" : "closed",
154
+ "data-disabled": isDisabled ? "" : void 0,
155
+ "data-orientation": orientation,
156
+ children: /* @__PURE__ */ jsx(AccordionItemContext.Provider, { value: { orientation, isDisabled }, children })
157
+ }
158
+ );
159
+ }
160
+ }
161
+ );
162
+ }
163
+ function AccordionTrigger({
164
+ asChild,
165
+ className,
166
+ children,
167
+ onClick,
168
+ ...props
169
+ }) {
170
+ const { item, state } = useAccordionItem("AccordionTrigger");
171
+ const trigger = useSlottedContext(ButtonContext, "trigger");
172
+ const Comp = asChild ? Slot : "button";
173
+ const dataState = state.isExpanded ? "open" : "closed";
174
+ const dataDisabled = item.isDisabled ? "" : void 0;
175
+ return /* @__PURE__ */ jsx(
176
+ "h3",
177
+ {
178
+ className: "ui-accordion-header",
179
+ "data-orientation": item.orientation,
180
+ "data-state": dataState,
181
+ "data-disabled": dataDisabled,
182
+ children: /* @__PURE__ */ jsxs(
183
+ Comp,
184
+ {
185
+ type: "button",
186
+ id: trigger?.id,
187
+ "aria-controls": state.isExpanded ? trigger?.["aria-controls"] : void 0,
188
+ "aria-expanded": state.isExpanded,
189
+ "data-slot": "accordion-trigger",
190
+ "data-state": dataState,
191
+ "data-disabled": dataDisabled,
192
+ "data-orientation": item.orientation,
193
+ disabled: item.isDisabled,
194
+ className: cn("ui-accordion-trigger", className),
195
+ ...props,
196
+ onClick: chain(onClick, () => {
197
+ if (!item.isDisabled) {
198
+ state.toggle();
199
+ }
200
+ }),
201
+ children: [
202
+ children,
203
+ /* @__PURE__ */ jsx(ChevronDown, { className: "ui-accordion-chevron", "aria-hidden": "true" })
204
+ ]
205
+ }
206
+ )
207
+ }
208
+ );
209
+ }
210
+ function AccordionContent({
211
+ className,
212
+ forceMount,
213
+ children,
214
+ ref,
215
+ ...props
216
+ }) {
217
+ const { item, state } = useAccordionItem("AccordionContent");
218
+ return /* @__PURE__ */ jsx(
219
+ DisclosurePanel,
220
+ {
221
+ role: "region",
222
+ render: (racProps) => {
223
+ const {
224
+ className: _racClass,
225
+ "data-rac": _rac,
226
+ children: _racChildren,
227
+ ref: racRef,
228
+ ...rest
229
+ } = racProps;
230
+ return /* @__PURE__ */ jsx(
231
+ "div",
232
+ {
233
+ "data-slot": "accordion-content",
234
+ className: cn("ui-accordion-content", className),
235
+ ...props,
236
+ ...rest,
237
+ ref: mergeRefs(ref, racRef),
238
+ "data-state": state.isExpanded ? "open" : "closed",
239
+ "data-disabled": item.isDisabled ? "" : void 0,
240
+ "data-orientation": item.orientation,
241
+ children: state.isExpanded || forceMount ? /* @__PURE__ */ jsx("div", { className: "ui-accordion-content-inner", children }) : null
242
+ }
243
+ );
244
+ },
245
+ children: null
246
+ }
247
+ );
248
+ }
43
249
  export {
44
250
  Accordion,
45
251
  AccordionContent,