@godxjp/ui 19.6.0 → 20.1.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 (326) hide show
  1. package/README.md +8 -2
  2. package/dist/app/theme-axes.d.ts +2 -0
  3. package/dist/app/theme-axes.js +45 -0
  4. package/dist/components/charts/chart-cartesian.d.ts +2 -1
  5. package/dist/components/charts/chart-cartesian.js +52 -3
  6. package/dist/components/charts/chart-category-axis.d.ts +55 -0
  7. package/dist/components/charts/chart-category-axis.js +93 -0
  8. package/dist/components/charts/chart-frame.d.ts +10 -1
  9. package/dist/components/charts/chart-frame.js +19 -3
  10. package/dist/components/charts/compact-bar-trend.d.ts +1 -1
  11. package/dist/components/charts/compact-bar-trend.js +2 -0
  12. package/dist/components/charts/pie-chart.d.ts +1 -1
  13. package/dist/components/charts/pie-chart.js +12 -1
  14. package/dist/components/charts/recharts-peer.d.ts +49 -0
  15. package/dist/components/charts/recharts-peer.js +45 -0
  16. package/dist/components/data-display/card.d.ts +14 -3
  17. package/dist/components/data-display/card.js +22 -4
  18. package/dist/components/data-display/code-block.js +6 -2
  19. package/dist/components/data-display/collapsible.d.ts +23 -4
  20. package/dist/components/data-display/collapsible.js +119 -4
  21. package/dist/components/data-display/data-table.d.ts +39 -4
  22. package/dist/components/data-display/data-table.js +802 -270
  23. package/dist/components/data-display/descriptions.d.ts +27 -6
  24. package/dist/components/data-display/descriptions.js +58 -17
  25. package/dist/components/data-display/index.d.ts +5 -1
  26. package/dist/components/data-display/index.js +4 -0
  27. package/dist/components/data-display/legend.d.ts +15 -0
  28. package/dist/components/data-display/legend.js +11 -0
  29. package/dist/components/data-display/list-row.d.ts +10 -1
  30. package/dist/components/data-display/list-row.js +8 -2
  31. package/dist/components/data-display/popover.d.ts +60 -7
  32. package/dist/components/data-display/popover.js +268 -39
  33. package/dist/components/data-display/progress.d.ts +50 -2
  34. package/dist/components/data-display/progress.js +57 -9
  35. package/dist/components/data-display/range-timeline.d.ts +38 -0
  36. package/dist/components/data-display/range-timeline.js +161 -0
  37. package/dist/components/data-display/scroll-area.js +13 -2
  38. package/dist/components/data-display/service-launcher-card.js +10 -10
  39. package/dist/components/data-display/table.d.ts +11 -3
  40. package/dist/components/data-display/table.js +48 -18
  41. package/dist/components/data-display/tree-list.js +4 -9
  42. package/dist/components/data-entry/calendar.d.ts +2 -2
  43. package/dist/components/data-entry/calendar.js +79 -33
  44. package/dist/components/data-entry/cascader.d.ts +1 -1
  45. package/dist/components/data-entry/cascader.js +188 -24
  46. package/dist/components/data-entry/checkbox.d.ts +19 -6
  47. package/dist/components/data-entry/checkbox.js +55 -16
  48. package/dist/components/data-entry/choice-option.d.ts +1 -1
  49. package/dist/components/data-entry/color-picker.d.ts +1 -1
  50. package/dist/components/data-entry/color-picker.js +17 -5
  51. package/dist/components/data-entry/control-appearance.d.ts +64 -0
  52. package/dist/components/data-entry/control-appearance.js +39 -0
  53. package/dist/components/data-entry/control-surface.d.ts +61 -0
  54. package/dist/components/data-entry/control-surface.js +39 -0
  55. package/dist/components/data-entry/date-picker.d.ts +1 -1
  56. package/dist/components/data-entry/date-picker.js +344 -113
  57. package/dist/components/data-entry/date-range-picker.d.ts +2 -2
  58. package/dist/components/data-entry/date-range-picker.js +278 -140
  59. package/dist/components/data-entry/field.js +0 -1
  60. package/dist/components/data-entry/form-field.d.ts +1 -1
  61. package/dist/components/data-entry/form-field.js +39 -4
  62. package/dist/components/data-entry/form.d.ts +4 -0
  63. package/dist/components/data-entry/form.js +5 -3
  64. package/dist/components/data-entry/index.d.ts +7 -3
  65. package/dist/components/data-entry/index.js +10 -1
  66. package/dist/components/data-entry/input-otp.d.ts +1 -1
  67. package/dist/components/data-entry/input.d.ts +12 -34
  68. package/dist/components/data-entry/input.js +92 -24
  69. package/dist/components/data-entry/label.d.ts +3 -2
  70. package/dist/components/data-entry/label.js +23 -10
  71. package/dist/components/data-entry/month-picker.d.ts +2 -2
  72. package/dist/components/data-entry/month-picker.js +47 -10
  73. package/dist/components/data-entry/month-range-picker.d.ts +2 -2
  74. package/dist/components/data-entry/month-range-picker.js +51 -11
  75. package/dist/components/data-entry/number-input.d.ts +7 -0
  76. package/dist/components/data-entry/number-input.js +147 -98
  77. package/dist/components/data-entry/password-input.d.ts +1 -1
  78. package/dist/components/data-entry/radio.d.ts +1 -1
  79. package/dist/components/data-entry/radio.js +61 -13
  80. package/dist/components/data-entry/search-input.d.ts +1 -1
  81. package/dist/components/data-entry/search-input.js +16 -2
  82. package/dist/components/data-entry/search-select.d.ts +2 -2
  83. package/dist/components/data-entry/search-select.js +209 -78
  84. package/dist/components/data-entry/select.d.ts +22 -4
  85. package/dist/components/data-entry/select.js +215 -163
  86. package/dist/components/data-entry/slider.d.ts +9 -1
  87. package/dist/components/data-entry/slider.js +87 -9
  88. package/dist/components/data-entry/switch.d.ts +3 -0
  89. package/dist/components/data-entry/switch.js +29 -3
  90. package/dist/components/data-entry/textarea.d.ts +14 -56
  91. package/dist/components/data-entry/textarea.js +80 -33
  92. package/dist/components/data-entry/time-picker.d.ts +2 -2
  93. package/dist/components/data-entry/time-picker.js +333 -109
  94. package/dist/components/data-entry/time-range-picker.d.ts +5 -0
  95. package/dist/components/data-entry/time-range-picker.js +89 -0
  96. package/dist/components/data-entry/transfer.d.ts +1 -1
  97. package/dist/components/data-entry/transfer.js +99 -31
  98. package/dist/components/data-entry/tree-select-strategy.d.ts +1 -1
  99. package/dist/components/data-entry/tree-select.d.ts +1 -1
  100. package/dist/components/data-entry/tree-select.js +197 -92
  101. package/dist/components/data-entry/tree-utils.d.ts +1 -1
  102. package/dist/components/data-entry/tree-utils.js +7 -14
  103. package/dist/components/data-entry/upload-files.d.ts +2 -0
  104. package/dist/components/data-entry/upload-files.js +31 -0
  105. package/dist/components/data-entry/upload-request.d.ts +4 -0
  106. package/dist/components/data-entry/upload-request.js +53 -0
  107. package/dist/components/data-entry/upload-types.d.ts +28 -0
  108. package/dist/components/data-entry/upload-types.js +2 -0
  109. package/dist/components/data-entry/upload.d.ts +2 -2
  110. package/dist/components/data-entry/upload.js +482 -117
  111. package/dist/components/feedback/dialog.d.ts +105 -38
  112. package/dist/components/feedback/dialog.js +268 -193
  113. package/dist/components/feedback/overlay-close-focus.d.ts +31 -0
  114. package/dist/components/feedback/overlay-close-focus.js +31 -0
  115. package/dist/components/feedback/overlay-header-tone.d.ts +1 -1
  116. package/dist/components/feedback/sheet.d.ts +53 -11
  117. package/dist/components/feedback/sheet.js +150 -81
  118. package/dist/components/feedback/tooltip.d.ts +51 -7
  119. package/dist/components/feedback/tooltip.js +107 -25
  120. package/dist/components/general/button.d.ts +1 -1
  121. package/dist/components/general/button.js +7 -3
  122. package/dist/components/general/index.d.ts +1 -0
  123. package/dist/components/general/index.js +2 -0
  124. package/dist/components/general/logo.d.ts +17 -0
  125. package/dist/components/general/logo.js +22 -16
  126. package/dist/components/general/typography.d.ts +3 -0
  127. package/dist/components/general/typography.js +15 -1
  128. package/dist/components/general/visually-hidden.d.ts +3 -0
  129. package/dist/components/general/visually-hidden.js +9 -0
  130. package/dist/components/layout/app-launcher.d.ts +34 -0
  131. package/dist/components/layout/app-launcher.js +228 -0
  132. package/dist/components/layout/app-shell.d.ts +3 -1
  133. package/dist/components/layout/app-shell.js +69 -12
  134. package/dist/components/layout/aspect-ratio.js +0 -1
  135. package/dist/components/layout/auth-divider.js +0 -1
  136. package/dist/components/layout/breadcrumb.d.ts +14 -2
  137. package/dist/components/layout/breadcrumb.js +46 -4
  138. package/dist/components/layout/flex.d.ts +1 -1
  139. package/dist/components/layout/flex.js +41 -3
  140. package/dist/components/layout/index.d.ts +3 -0
  141. package/dist/components/layout/index.js +5 -1
  142. package/dist/components/layout/nav-surface.d.ts +26 -0
  143. package/dist/components/layout/nav-surface.js +17 -0
  144. package/dist/components/layout/org-switcher.d.ts +5 -1
  145. package/dist/components/layout/org-switcher.js +25 -4
  146. package/dist/components/layout/page-container.d.ts +1 -1
  147. package/dist/components/layout/page-container.js +5 -3
  148. package/dist/components/layout/responsive-grid.d.ts +16 -2
  149. package/dist/components/layout/responsive-grid.js +29 -2
  150. package/dist/components/layout/separator.js +0 -1
  151. package/dist/components/layout/sidebar.js +4 -1
  152. package/dist/components/layout/split-pane.d.ts +14 -1
  153. package/dist/components/layout/split-pane.js +26 -13
  154. package/dist/components/layout/topbar-item.d.ts +3 -0
  155. package/dist/components/layout/topbar-item.js +19 -5
  156. package/dist/components/layout/topbar.d.ts +1 -1
  157. package/dist/components/layout/topbar.js +27 -6
  158. package/dist/components/navigation/app-setting-picker.js +21 -2
  159. package/dist/components/navigation/app-setting-toggle.d.ts +16 -0
  160. package/dist/components/navigation/app-setting-toggle.js +96 -0
  161. package/dist/components/navigation/dropdown-menu.d.ts +227 -18
  162. package/dist/components/navigation/dropdown-menu.js +402 -118
  163. package/dist/components/navigation/index.d.ts +2 -0
  164. package/dist/components/navigation/index.js +2 -0
  165. package/dist/components/navigation/pagination-utils.d.ts +2 -1
  166. package/dist/components/navigation/pagination.d.ts +2 -2
  167. package/dist/components/navigation/pagination.js +155 -83
  168. package/dist/components/navigation/steps.d.ts +2 -2
  169. package/dist/components/navigation/steps.js +29 -4
  170. package/dist/components/navigation/tabs.d.ts +29 -16
  171. package/dist/components/navigation/tabs.js +374 -144
  172. package/dist/components/ui/accordion.d.ts +50 -5
  173. package/dist/components/ui/accordion.js +239 -33
  174. package/dist/components/ui/aspect-ratio.d.ts +23 -2
  175. package/dist/components/ui/aspect-ratio.js +15 -13
  176. package/dist/components/ui/avatar.d.ts +29 -4
  177. package/dist/components/ui/avatar.js +111 -25
  178. package/dist/components/ui/hover-card.d.ts +42 -4
  179. package/dist/components/ui/hover-card.js +183 -27
  180. package/dist/components/ui/input-otp.d.ts +26 -28
  181. package/dist/components/ui/input-otp.js +50 -7
  182. package/dist/components/ui/label.js +0 -1
  183. package/dist/components/ui/password-input.d.ts +36 -2
  184. package/dist/components/ui/password-input.js +27 -7
  185. package/dist/components/ui/rating.d.ts +25 -0
  186. package/dist/components/ui/rating.js +67 -27
  187. package/dist/components/ui/segmented.d.ts +12 -3
  188. package/dist/components/ui/segmented.js +17 -2
  189. package/dist/components/ui/separator.d.ts +12 -2
  190. package/dist/components/ui/separator.js +30 -9
  191. package/dist/components/ui/tag-input.d.ts +45 -0
  192. package/dist/components/ui/tag-input.js +109 -27
  193. package/dist/components/ui/toggle-group.d.ts +50 -5
  194. package/dist/components/ui/toggle-group.js +79 -20
  195. package/dist/components/ui/toggle.d.ts +31 -5
  196. package/dist/components/ui/toggle.js +42 -3
  197. package/dist/form/form-context.d.ts +29 -0
  198. package/dist/form/form-context.js +44 -4
  199. package/dist/form/form-field-array.d.ts +22 -0
  200. package/dist/form/form-field-array.js +50 -0
  201. package/dist/form/form-field-control.d.ts +2 -1
  202. package/dist/form/form-field-control.js +116 -40
  203. package/dist/form/form-root.d.ts +2 -1
  204. package/dist/form/form-root.js +142 -23
  205. package/dist/form/index.d.ts +3 -1
  206. package/dist/form/index.js +12 -1
  207. package/dist/i18n/messages/en.json +64 -5
  208. package/dist/i18n/messages/ja.json +64 -5
  209. package/dist/i18n/messages/vi.json +64 -5
  210. package/dist/inertia/index.d.ts +20 -0
  211. package/dist/inertia/index.js +60 -1
  212. package/dist/lib/control-styles.d.ts +25 -3
  213. package/dist/lib/control-styles.js +9 -3
  214. package/dist/lib/datetime/picker-format.d.ts +8 -0
  215. package/dist/lib/datetime/picker-format.js +54 -0
  216. package/dist/lib/slot.d.ts +32 -0
  217. package/dist/lib/slot.js +22 -0
  218. package/dist/lib/variants.d.ts +22 -3
  219. package/dist/lib/variants.js +56 -1
  220. package/dist/props/components/app.prop.d.ts +25 -1
  221. package/dist/props/components/charts.prop.d.ts +21 -0
  222. package/dist/props/components/data-display.prop.d.ts +72 -4
  223. package/dist/props/components/data-entry.prop.d.ts +718 -46
  224. package/dist/props/components/form.prop.d.ts +142 -4
  225. package/dist/props/components/general.prop.d.ts +17 -1
  226. package/dist/props/components/index.d.ts +3 -3
  227. package/dist/props/components/layout.prop.d.ts +281 -11
  228. package/dist/props/components/navigation.prop.d.ts +114 -4
  229. package/dist/props/registry.d.ts +397 -11
  230. package/dist/props/registry.js +526 -11
  231. package/dist/props/vocabulary/content.prop.d.ts +1 -1
  232. package/dist/props/vocabulary/data.prop.d.ts +179 -1
  233. package/dist/props/vocabulary/index.d.ts +5 -5
  234. package/dist/props/vocabulary/interaction.prop.d.ts +68 -3
  235. package/dist/props/vocabulary/layout.prop.d.ts +57 -1
  236. package/dist/props/vocabulary/navigation.prop.d.ts +40 -0
  237. package/dist/props/vocabulary/shared.prop.d.ts +33 -0
  238. package/dist/styles/badge-layout.css +2 -1
  239. package/dist/styles/card-layout.css +26 -4
  240. package/dist/styles/chart-layout.css +15 -0
  241. package/dist/styles/control.css +657 -15
  242. package/dist/styles/core.css +5 -2
  243. package/dist/styles/data-display-layout.css +313 -6
  244. package/dist/styles/data-entry-layout.css +13 -0
  245. package/dist/styles/focus-ring.css +4 -0
  246. package/dist/styles/index.css +5 -2
  247. package/dist/styles/layout.css +327 -4
  248. package/dist/styles/navigation-layout.css +227 -0
  249. package/dist/styles/shell-layout.css +369 -13
  250. package/dist/styles/table-layout.css +95 -6
  251. package/dist/styles/text-layout.css +10 -4
  252. package/dist/tokens/base.css +2 -1
  253. package/dist/tokens/components/badge.css +1 -0
  254. package/dist/tokens/components/chart.css +6 -0
  255. package/dist/tokens/components/control.css +79 -2
  256. package/dist/tokens/components/data-display.css +20 -0
  257. package/dist/tokens/components/descriptions.css +11 -0
  258. package/dist/tokens/components/flex.css +8 -0
  259. package/dist/tokens/components/navigation.css +55 -0
  260. package/dist/tokens/components/shell.css +53 -3
  261. package/dist/tokens/components/table.css +14 -0
  262. package/dist/tokens/foundation.css +2 -0
  263. package/dist/tokens/semantic/layout.css +9 -0
  264. package/docs/COMPONENTS.md +9 -3
  265. package/docs/CONSUMER-RULES.md +13 -0
  266. package/docs/DESIGN-AUTHORITY.md +145 -83
  267. package/docs/DEVELOPMENT.md +4 -3
  268. package/docs/FORMS.md +151 -6
  269. package/docs/FRAME-COVERAGE-REPORT.md +32 -17
  270. package/docs/README.md +15 -15
  271. package/docs/STANDARDS-vocabulary-tokens.md +1 -1
  272. package/docs/TESTING.md +15 -6
  273. package/docs/WHAT-BELONGS-HERE.md +179 -0
  274. package/docs/charts/cjk-category-axis.tsx +81 -0
  275. package/docs/data-display/card/index.tsx +22 -0
  276. package/docs/data-display/data-table/examples/antd-parity.tsx +257 -0
  277. package/docs/data-display/data-table/index.tsx +23 -0
  278. package/docs/data-display/descriptions.tsx +41 -0
  279. package/docs/data-display/legend.tsx +145 -0
  280. package/docs/data-display/progress.tsx +32 -0
  281. package/docs/data-display/scroll-area.tsx +31 -25
  282. package/docs/data-display/table.tsx +104 -45
  283. package/docs/data-display/timeline.tsx +41 -0
  284. package/docs/data-entry/date-picker.tsx +85 -0
  285. package/docs/data-entry/date-range-picker.tsx +26 -0
  286. package/docs/data-entry/form-dynamic-fields.tsx +199 -0
  287. package/docs/data-entry/form.tsx +74 -4
  288. package/docs/data-entry/input-otp.tsx +24 -0
  289. package/docs/data-entry/input.tsx +46 -1
  290. package/docs/data-entry/month-range-picker.tsx +1 -1
  291. package/docs/data-entry/segmented.tsx +1 -1
  292. package/docs/data-entry/select.tsx +29 -2
  293. package/docs/data-entry/switch.tsx +41 -0
  294. package/docs/data-entry/textarea.tsx +38 -0
  295. package/docs/data-entry/time-picker.tsx +85 -1
  296. package/docs/data-entry/time-range-picker.tsx +55 -0
  297. package/docs/data-entry/transfer.tsx +17 -0
  298. package/docs/data-entry/upload.tsx +56 -12
  299. package/docs/feedback/sheet.tsx +1 -1
  300. package/docs/feedback/tooltip.tsx +1 -1
  301. package/docs/general/activity.tsx +2 -2
  302. package/docs/general/button/index.tsx +14 -1
  303. package/docs/general/typography.tsx +14 -1
  304. package/docs/layout/app-launcher.tsx +151 -0
  305. package/docs/layout/app-shell-arrangements.tsx +225 -0
  306. package/docs/layout/app-shell.tsx +11 -0
  307. package/docs/layout/aspect-ratio.tsx +1 -1
  308. package/docs/layout/flex.tsx +50 -0
  309. package/docs/layout/responsive-grid.tsx +21 -1
  310. package/docs/layout/topbar.tsx +5 -9
  311. package/docs/navigation/app-setting-picker.tsx +37 -1
  312. package/docs/navigation/app-setting-toggle.tsx +111 -0
  313. package/docs/navigation/breadcrumb.tsx +48 -0
  314. package/docs/navigation/dropdown-menu.tsx +21 -0
  315. package/docs/navigation/pagination.tsx +52 -0
  316. package/docs/navigation/steps.tsx +37 -0
  317. package/docs/navigation/tabs.tsx +97 -1
  318. package/docs/query/button-refetch.tsx +1 -0
  319. package/package.json +28 -8
  320. package/scripts/_agent-setup.mjs +182 -3
  321. package/scripts/consumer-rule.md +98 -0
  322. package/scripts/guinea-pig-skill.md +322 -0
  323. package/scripts/init-guinea-pig.mjs +84 -0
  324. package/scripts/postinstall.mjs +13 -2
  325. package/scripts/ui-audit.mjs +351 -37
  326. /package/dist/tokens/{antd.generated.css → derived.css} +0 -0
@@ -1,9 +1,20 @@
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";
10
+ import { Plus, X } from "lucide-react";
11
+ import { useTranslation } from "../../i18n/use-translation.js";
5
12
  import { cn } from "../../lib/utils.js";
6
13
  import { useKeepActiveTabVisible } from "./tabs-scroll.js";
14
+ const TabsFrameContext = React.createContext({
15
+ orientation: "horizontal",
16
+ selectionSuppressed: false
17
+ });
7
18
  function resolveFallbackTabValue(items, requested) {
8
19
  if (!items || items.length === 0) return requested;
9
20
  if (requested !== void 0) {
@@ -12,182 +23,401 @@ function resolveFallbackTabValue(items, requested) {
12
23
  }
13
24
  return items.find((item) => !item.disabled)?.value;
14
25
  }
26
+ function withDomProps(element, slot, raw, domProps, own) {
27
+ const dom = domProps;
28
+ return React.createElement(element, {
29
+ "data-slot": slot,
30
+ ...raw,
31
+ ...dom,
32
+ id: raw.id ?? dom.id,
33
+ ...own
34
+ });
35
+ }
36
+ function resolveTabsExtra(extra) {
37
+ if (extra === void 0 || extra === null || extra === false) return {};
38
+ const isSlotMap = typeof extra === "object" && !React.isValidElement(extra) && !Array.isArray(extra) && ("start" in extra || "end" in extra);
39
+ if (isSlotMap) return extra;
40
+ return { end: extra };
41
+ }
42
+ function resolveTabsAxis(tabPlacement, orientation) {
43
+ if (tabPlacement) {
44
+ return {
45
+ placement: tabPlacement,
46
+ orientation: orientation ?? (tabPlacement === "start" || tabPlacement === "end" ? "vertical" : "horizontal")
47
+ };
48
+ }
49
+ return {
50
+ placement: orientation === "vertical" ? "start" : "top",
51
+ orientation: orientation ?? "horizontal"
52
+ };
53
+ }
15
54
  function Tabs({
16
55
  className,
17
- orientation = "horizontal",
56
+ orientation,
18
57
  items,
19
58
  value,
20
59
  defaultValue,
60
+ onValueChange,
61
+ activationMode,
62
+ dir,
21
63
  variant = "default",
64
+ tabPlacement,
65
+ size = "md",
66
+ centered,
67
+ extra,
68
+ destroyOnHidden = true,
69
+ onEdit,
70
+ addIcon,
71
+ hideAdd,
22
72
  listClassName,
23
73
  contentClassName,
74
+ children,
24
75
  ...props
25
76
  }) {
77
+ const { t } = useTranslation();
26
78
  const resolvedDefault = resolveFallbackTabValue(items, defaultValue);
27
- return /* @__PURE__ */ jsx(
28
- TabsPrimitive.Root,
79
+ const { placement, orientation: resolvedOrientation } = resolveTabsAxis(
80
+ tabPlacement,
81
+ orientation
82
+ );
83
+ const selectionSuppressed = value === void 0 && items != null && items.length > 0 && resolvedDefault === void 0;
84
+ const frame = React.useMemo(
85
+ () => ({ orientation: resolvedOrientation, selectionSuppressed }),
86
+ [resolvedOrientation, selectionSuppressed]
87
+ );
88
+ const editable = variant === "editable-card";
89
+ const [mirroredValue, setMirroredValue] = React.useState(
90
+ value ?? resolvedDefault
91
+ );
92
+ React.useEffect(() => {
93
+ if (value !== void 0) setMirroredValue(value);
94
+ }, [value]);
95
+ const activeValue = value ?? mirroredValue;
96
+ const handleValueChange = React.useCallback(
97
+ (next) => {
98
+ setMirroredValue(next);
99
+ onValueChange?.(next);
100
+ },
101
+ [onValueChange]
102
+ );
103
+ const { start: extraStart, end: extraEnd } = resolveTabsExtra(extra);
104
+ const showAdd = editable && !hideAdd && Boolean(onEdit);
105
+ const needsBar = Boolean(extraStart || extraEnd || showAdd);
106
+ const card = variant === "card" || variant === "editable-card";
107
+ const sizeTriggerClassName = cn(
108
+ size === "sm" && "min-h-[var(--tabs-trigger-height-sm)] px-[var(--tabs-trigger-padding-x-sm)] text-[length:var(--tabs-trigger-font-size-sm)]",
109
+ size === "md" && "min-h-[var(--tabs-trigger-height-md)] px-[var(--tabs-trigger-padding-x-md)] text-[length:var(--tabs-trigger-font-size-md)]",
110
+ size === "lg" && "min-h-[var(--tabs-trigger-height-lg)] px-[var(--tabs-trigger-padding-x-lg)] text-[length:var(--tabs-trigger-font-size-lg)]"
111
+ );
112
+ const list = items ? /* @__PURE__ */ jsx(
113
+ TabsList,
114
+ {
115
+ "data-slot": "tabs-list",
116
+ variant: variant === "line" ? "line" : "default",
117
+ className: cn(
118
+ // The inset override is an arbitrary value reading the knob (never `p-0`): it must
119
+ // stay a UTILITY so tailwind-merge still drops the strip's own padding step.
120
+ //
121
+ // IT IS SPLIT INTO `px-`/`py-` FOR THE FOCUS RING (gh#376). The strip clips its
122
+ // block axis (`overflow-y: hidden` beside `overflow-x: auto`), and this variant's
123
+ // inset is 0px, so a focused trigger had NO block headroom at all — measured in
124
+ // Chromium, 0px top and bottom, i.e. the entire ring was shaved. The headroom has to
125
+ // come from layout: `overflow-y: visible` next to `overflow-x: auto` computes back
126
+ // to `auto` per spec (a second scroll container, not a ring), and Chromium honours
127
+ // `overflow-clip-margin` only when BOTH axes are `clip`. So the block padding carries
128
+ // the ring's outer reach and an equal negative block margin hands it straight back —
129
+ // the ring paints inside the scrollport and the strip's OUTER box is unchanged
130
+ // (measured: 38px before and after). Both values read the same token the ring is
131
+ // sized from, so the two can never drift apart.
132
+ //
133
+ // The cost, recorded rather than hidden: the rail hairline (`border-b`, drawn at the
134
+ // border box) now sits the ring's reach below the triggers instead of flush. That is
135
+ // where a hand-composed <TabsList variant="line"> already put it — its `p-1` gives
136
+ // the same block inset — so the two construction paths now agree. A service that
137
+ // wants the active bar parked back on the hairline raises `--tabs-indicator-offset`,
138
+ // which exists for exactly that.
139
+ variant === "line" && "my-[calc(-1_*_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))] h-auto w-full justify-start border-b px-[var(--tabs-list-line-space-inset)] py-[calc(var(--tabs-list-line-space-inset)_+_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))]",
140
+ // CARD strip. The list keeps `data-variant="default"` on purpose (a hand-composed
141
+ // <TabsList> must be unaffected), so the card face is selected from the ROOT — but the
142
+ // three properties the base list already claims as utilities (`bg-muted`, `p-1`,
143
+ // `rounded-lg`) have to be replaced with utilities too, or the components layer loses.
144
+ card && "w-full items-end justify-start gap-[var(--tabs-card-list-space-gap)] rounded-[var(--tabs-card-list-radius)] p-[var(--tabs-card-list-space-inset)] data-[variant=default]:bg-transparent",
145
+ // CENTERED. Two independent moves, because the strip has two shapes: the pill/card strip
146
+ // is `w-fit` (auto inline margins centre the BOX) and the line strip is `w-full`
147
+ // (`justify-content` centres its CONTENT). `safe` keeps the overflow rule the strip
148
+ // already depends on — plain `center` strands the leading tab outside the scrollport.
149
+ centered && "mx-auto justify-center-safe",
150
+ listClassName
151
+ ),
152
+ children: items.map((item) => {
153
+ const removable = editable && Boolean(onEdit) && item.closable !== false && !item.disabled;
154
+ return /* @__PURE__ */ jsxs(
155
+ TabsTrigger,
156
+ {
157
+ value: item.value,
158
+ disabled: item.disabled,
159
+ "aria-keyshortcuts": removable ? "Delete" : void 0,
160
+ onKeyDown: removable ? (event) => {
161
+ if (event.key !== "Delete" && event.key !== "Backspace") return;
162
+ event.preventDefault();
163
+ onEdit?.(item.value, "remove");
164
+ } : void 0,
165
+ className: cn(
166
+ // Geometry only — the active underline is the token-owned `::after` bar the
167
+ // line variant already owns (never a second, hand-rolled border-b indicator).
168
+ // They are arbitrary-value utilities so tailwind-merge keeps dropping the pill
169
+ // trigger's own radius / padding steps from the base class list.
170
+ variant === "line" && "rounded-[var(--tabs-trigger-line-radius)] px-[var(--tabs-trigger-line-padding-x)] py-[var(--tabs-trigger-line-padding-y)]",
171
+ sizeTriggerClassName,
172
+ // A centred strip must let its triggers SHRINK to their labels first. The base
173
+ // trigger is `flex-1`, so on a full-width (`line`) strip they already fill every
174
+ // pixel and `justify-content` has nothing left to centre — measured, the leading and
175
+ // trailing gaps were both 0px with `centered` on and off.
176
+ centered && "flex-none",
177
+ // CARD face: a real boundary on every side, rounded on the leading block edge only.
178
+ // The ACTIVE face's block-end edge is the SURFACE colour, never `transparent`: the
179
+ // rail is an inset shadow at the strip's padding-box edge, so a see-through border
180
+ // would let that 1px of rail run straight across the tab it is joined to.
181
+ card && "data-[state=active]:border-b-background rounded-[var(--tabs-card-radius)_var(--tabs-card-radius)_0_0] border-[color:hsl(var(--border))] bg-[hsl(var(--tabs-card-background,var(--muted)))]"
182
+ ),
183
+ children: [
184
+ item.icon ? /* @__PURE__ */ jsx(
185
+ "span",
186
+ {
187
+ "data-slot": "tabs-trigger-icon",
188
+ className: "ui-tabs-trigger-icon",
189
+ "aria-hidden": "true",
190
+ children: item.icon
191
+ }
192
+ ) : null,
193
+ item.label,
194
+ removable ? (
195
+ // A POINTER SHORTCUT, not a control — and that is measured, not stylistic. antd's own
196
+ // TabNode puts a real <button> beside the role="tab" element; rendered through
197
+ // vitest-axe that markup fails `aria-required-children`, because a tablist may own
198
+ // nothing but tabs and axe walks straight through the generic wrapper to find the
199
+ // button. Putting the button INSIDE the tab is worse still (`nested-interactive`,
200
+ // plus a <button> in a <button> is invalid HTML). So the × is an aria-hidden span and
201
+ // the real, announced route is the Delete/Backspace shortcut above — the WAI-ARIA APG
202
+ // "tabs with deletion" shape.
203
+ /* @__PURE__ */ jsx(
204
+ "span",
205
+ {
206
+ "data-slot": "tabs-tab-remove",
207
+ className: "ui-tabs-tab-remove",
208
+ "aria-hidden": "true",
209
+ title: t("navigation.tabs.removeTab"),
210
+ onPointerDown: (event) => {
211
+ event.preventDefault();
212
+ event.stopPropagation();
213
+ },
214
+ onMouseDown: (event) => {
215
+ event.preventDefault();
216
+ event.stopPropagation();
217
+ },
218
+ onClick: (event) => {
219
+ event.preventDefault();
220
+ event.stopPropagation();
221
+ onEdit?.(item.value, "remove");
222
+ },
223
+ children: item.closeIcon ?? /* @__PURE__ */ jsx(X, { className: "ui-tabs-tab-remove-icon", "aria-hidden": "true" })
224
+ }
225
+ )
226
+ ) : null
227
+ ]
228
+ },
229
+ item.value
230
+ );
231
+ })
232
+ }
233
+ ) : null;
234
+ return /* @__PURE__ */ jsx(TabsFrameContext.Provider, { value: frame, children: /* @__PURE__ */ jsx(
235
+ AriaTabs,
29
236
  {
30
237
  "data-slot": "tabs",
31
- "data-orientation": orientation,
238
+ "data-orientation": resolvedOrientation,
32
239
  "data-variant": variant,
33
- orientation,
34
- value,
35
- defaultValue: value === void 0 ? resolvedDefault : void 0,
240
+ "data-placement": placement,
241
+ "data-size": size,
242
+ "data-centered": centered ? "true" : void 0,
243
+ orientation: resolvedOrientation,
244
+ keyboardActivation: activationMode,
245
+ selectedKey: value,
246
+ defaultSelectedKey: value === void 0 ? resolvedDefault : void 0,
247
+ onSelectionChange: (key) => handleValueChange(String(key)),
36
248
  className: cn(
37
- // Structure only.
38
- // live in src/styles/navigation-layout.css so the gap reads --tabs-root-gap and a service
249
+ // Structure only. The paint (and the placement flip, which is `order`/`flex-direction`)
250
+ // lives in src/styles/navigation-layout.css so the gap reads --tabs-root-gap and a service
251
+ // can retune it.
39
252
  "group/tabs flex data-[orientation=horizontal]:flex-col",
253
+ // PLACEMENT is a flex REVERSAL, never a re-ordered tree: the strip stays first in the DOM
254
+ // at every placement, so reading order and the APG tablist → tabpanel relationship do not
255
+ // depend on which edge the bar is painted on. Both classes carry the same modifier as the
256
+ // base step they replace, so tailwind-merge drops that step instead of stacking two
257
+ // flex-direction values on one element.
258
+ placement === "bottom" && "data-[orientation=horizontal]:flex-col-reverse",
259
+ placement === "end" && "flex-row-reverse",
40
260
  className
41
261
  ),
42
- ...props,
262
+ render: (domProps) => withDomProps("div", "tabs", { dir, ...props }, domProps, {
263
+ "data-orientation": resolvedOrientation,
264
+ "data-variant": variant,
265
+ "data-placement": placement,
266
+ "data-size": size,
267
+ "data-centered": centered ? "true" : void 0
268
+ }),
43
269
  children: items ? /* @__PURE__ */ jsxs(Fragment, { children: [
44
- /* @__PURE__ */ jsx(
45
- TabsList,
46
- {
47
- "data-slot": "tabs-list",
48
- variant: variant === "line" ? "line" : "default",
49
- className: cn(
50
- // The inset override is an arbitrary value reading the knob (never `p-0`): it must
51
- // stay a UTILITY so tailwind-merge still drops the strip's own padding step.
52
- //
53
- // IT IS SPLIT INTO `px-`/`py-` FOR THE FOCUS RING (gh#376). The strip clips its
54
- // block axis (`overflow-y: hidden` beside `overflow-x: auto`), and this variant's
55
- // inset is 0px, so a focused trigger had NO block headroom at all — measured in
56
- // Chromium, 0px top and bottom, i.e. the entire ring was shaved. The headroom has to
57
- // come from layout: `overflow-y: visible` next to `overflow-x: auto` computes back
58
- // to `auto` per spec (a second scroll container, not a ring), and Chromium honours
59
- // `overflow-clip-margin` only when BOTH axes are `clip`. So the block padding carries
60
- // the ring's outer reach and an equal negative block margin hands it straight back —
61
- // the ring paints inside the scrollport and the strip's OUTER box is unchanged
62
- // (measured: 38px before and after). Both values read the same token the ring is
63
- // sized from, so the two can never drift apart.
64
- //
65
- // The cost, recorded rather than hidden: the rail hairline (`border-b`, drawn at the
66
- // border box) now sits the ring's reach below the triggers instead of flush. That is
67
- // where a hand-composed <TabsList variant="line"> already put it — its `p-1` gives
68
- // the same block inset — so the two construction paths now agree. A service that
69
- // wants the active bar parked back on the hairline raises `--tabs-indicator-offset`,
70
- // which exists for exactly that.
71
- variant === "line" && "my-[calc(-1_*_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))] h-auto w-full justify-start border-b px-[var(--tabs-list-line-space-inset)] py-[calc(var(--tabs-list-line-space-inset)_+_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))]",
72
- variant === "card" && "w-full justify-start",
73
- listClassName
74
- ),
75
- children: items.map((item) => /* @__PURE__ */ jsx(
76
- TabsTrigger,
77
- {
78
- value: item.value,
79
- disabled: item.disabled,
80
- className: cn(
81
- // Geometry only — the active underline is the token-owned `::after` bar the
82
- // line variant already owns (never a second, hand-rolled border-b indicator).
83
- // utilities so tailwind-merge keeps dropping the pill trigger's own radius /
84
- // padding steps from the base class list.
85
- // `card` adds NOTHING here — its list is forwarded as data-variant="default", so
86
- // the active lift already comes from the base trigger's
87
- // `group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm`.
88
- variant === "line" && "rounded-[var(--tabs-trigger-line-radius)] px-[var(--tabs-trigger-line-padding-x)] py-[var(--tabs-trigger-line-padding-y)]"
89
- ),
90
- children: item.label
91
- },
92
- item.value
93
- ))
94
- }
95
- ),
270
+ needsBar ? /* @__PURE__ */ jsxs("div", { "data-slot": "tabs-bar", className: "ui-tabs-bar", children: [
271
+ extraStart ? /* @__PURE__ */ jsx("div", { "data-slot": "tabs-extra", "data-side": "start", className: "ui-tabs-extra", children: extraStart }) : null,
272
+ list,
273
+ showAdd ? /* @__PURE__ */ jsx(
274
+ "button",
275
+ {
276
+ type: "button",
277
+ "data-slot": "tabs-add",
278
+ className: "ui-tabs-add",
279
+ "aria-label": t("navigation.tabs.addTab"),
280
+ onClick: (event) => onEdit?.(event, "add"),
281
+ children: addIcon ?? /* @__PURE__ */ jsx(Plus, { className: "ui-tabs-add-icon", "aria-hidden": "true" })
282
+ }
283
+ ) : null,
284
+ extraEnd ? /* @__PURE__ */ jsx("div", { "data-slot": "tabs-extra", "data-side": "end", className: "ui-tabs-extra", children: extraEnd }) : null
285
+ ] }) : list,
96
286
  items.map((item) => /* @__PURE__ */ jsx(
97
287
  TabsContent,
98
288
  {
99
289
  value: item.value,
100
290
  "data-slot": "tabs-panel",
291
+ forceMount: destroyOnHidden ? void 0 : true,
292
+ hidden: destroyOnHidden ? void 0 : item.value !== activeValue,
101
293
  className: contentClassName,
102
294
  children: item.content
103
295
  },
104
296
  item.value
105
297
  ))
106
- ] }) : props.children
298
+ ] }) : children
107
299
  }
108
- );
300
+ ) });
109
301
  }
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
302
+ const TabsList = React.forwardRef(
303
+ ({ className, variant = "default", loop: _loop, children, ...props }, ref) => {
304
+ const listRef = React.useRef(null);
305
+ const setListRef = React.useCallback(
306
+ (node) => {
307
+ listRef.current = node;
308
+ if (typeof ref === "function") ref(node);
309
+ else if (ref) ref.current = node;
310
+ },
311
+ [ref]
312
+ );
313
+ useKeepActiveTabVisible(listRef);
314
+ return /* @__PURE__ */ jsx(
315
+ AriaTabList,
316
+ {
317
+ ref: setListRef,
318
+ className: cn(
319
+ // `min-w-0 max-w-full` let the list shrink to (and never exceed) whatever width its
320
+ // ancestors actually give it instead of forcing them wider; horizontal orientation then
321
+ // scrolls its own overflow rather than clipping/hiding long localized labels in a narrow
322
+ // Hidden scrollbar keeps the strip visually clean while staying
323
+ // swipeable on touch and reachable via keyboard (arrow-key roving focus still scrolls the
324
+ // newly-focused trigger into view natively); `useKeepActiveTabVisible` above re-pins the
325
+ // Vertical
326
+ // orientation is untouched — it already stacks in a column and is sized by its own
327
+ // `h-*`/`w-*` overrides.
328
+ // justify-center-SAFE: the strip is a centred flex box that also scrolls. Plain `center`
329
+ // splits the overflow across BOTH edges, and scrollLeft only ever covers the trailing one
330
+ // so the leading tab sat permanently outside the scrollport (an unreachable control at
331
+ // 320px, WCAG 2.2 SC 2.1.1). `safe` falls back to start alignment exactly when it
332
+ // overflows, and still centres whenever the tabs fit.
333
+ "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",
334
+ className
335
+ ),
336
+ render: (domProps) => withDomProps("div", "tabs-list", props, domProps, { "data-variant": variant }),
337
+ children
338
+ }
339
+ );
340
+ }
341
+ );
342
+ TabsList.displayName = "TabsList";
343
+ const TabsTrigger = React.forwardRef(
344
+ ({ className, value, disabled, children, onKeyDown, ...props }, ref) => {
345
+ const { orientation, selectionSuppressed } = React.useContext(TabsFrameContext);
346
+ return /* @__PURE__ */ jsx(
347
+ AriaTab,
348
+ {
349
+ ref,
350
+ id: value,
351
+ isDisabled: disabled,
352
+ className: cn(
353
+ // SELECTED IS A BORDER TINT, NOT A RING — and that is a WCAG fix, not a style preference.
354
+ // It used to be `ring-1 ring-primary/25`, which writes `--tw-ring-shadow` in the UTILITIES
355
+ // layer and therefore overwrote the focus ring that focus-ring.css feeds from `components`.
356
+ // Measured: a focused ACTIVE tab painted `oklab(… / 0.25) 0 0 0 1px` and nothing else — a
357
+ // 1px indicator at 25% alpha, failing both clauses of SC 2.4.13 (2px perimeter, ≥3:1),
358
+ // while every other control in the library carried a full 2px ring. The trigger already
359
+ // owns `border border-transparent`, so tinting that border reproduces the selected hairline
360
+ // at the same colour and width with no layout change, and leaves `--tw-ring-shadow` free for
361
+ // the focus ring. `shadow-sm` stays: its composite READS `--tw-ring-shadow`, so the lift and
362
+ // the ring coexist.
363
+ //
364
+ // No `focus-visible:outline-1` either. It was the fallback that survived the clobber — a 1px
365
+ // currentColor line squeezed between the border and the ring once both paint. The ring is
366
+ // the indicator now; two marks for one state is what this pass exists to remove.
367
+ //
368
+ // The line indicator lives in src/styles/navigation-layout.css so it reads --tabs-indicator-*.
369
+ // Selected and focused stay visually distinct (WCAG 2.4.7): selected is a 1px hairline in the
370
+ // border, focused is the 2px ring plus its halo outside it.
371
+ "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",
372
+ className
373
+ ),
374
+ render: (domProps, renderProps) => withDomProps("button", "tabs-trigger", { type: "button", ...props }, domProps, {
375
+ // `data-state` is what `src/styles/navigation-layout.css` and `tabs-scroll.ts` both
376
+ // key on. RAC only writes `data-selected` on the selected tab, so it is re-emitted
377
+ // here for BOTH states, exactly as Radix did.
378
+ "data-state": renderProps.isSelected && !selectionSuppressed ? "active" : "inactive",
379
+ "aria-selected": renderProps.isSelected && !selectionSuppressed,
380
+ "data-orientation": orientation,
381
+ disabled,
382
+ onKeyDown: (event) => {
383
+ onKeyDown?.(event);
384
+ if (!event.defaultPrevented) {
385
+ domProps.onKeyDown?.(
386
+ event
387
+ );
388
+ }
389
+ },
390
+ tabIndex: props.tabIndex ?? domProps.tabIndex
391
+ }),
392
+ children
393
+ }
394
+ );
178
395
  }
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
396
+ );
397
+ TabsTrigger.displayName = "TabsTrigger";
398
+ const TabsContent = React.forwardRef(
399
+ ({ className, value, forceMount, children, ...props }, ref) => {
400
+ const { orientation, selectionSuppressed } = React.useContext(TabsFrameContext);
401
+ if (selectionSuppressed && !forceMount) return null;
402
+ return /* @__PURE__ */ jsx(
403
+ AriaTabPanel,
404
+ {
405
+ ref,
406
+ id: value,
407
+ shouldForceMount: forceMount,
408
+ className: cn("ui-focus-ring flex-1 outline-none", className),
409
+ render: (domProps, renderProps) => withDomProps("div", "tabs-content", props, domProps, {
410
+ // A force-mounted panel whose tab is not selected: Radix marked it `data-state="inactive"`
411
+ // and `hidden`; RAC marks it inert. The state hook is re-emitted so CSS keys the same way.
412
+ "data-state": renderProps.isInert || selectionSuppressed ? "inactive" : "active",
413
+ "data-orientation": orientation
414
+ }),
415
+ children
416
+ }
417
+ );
188
418
  }
189
- ));
190
- TabsContent.displayName = TabsPrimitive.Content.displayName;
419
+ );
420
+ TabsContent.displayName = "TabsContent";
191
421
  export {
192
422
  Tabs,
193
423
  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 {};