@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.
- package/dist/components/data-display/card.d.ts +2 -2
- package/dist/components/data-display/collapsible.d.ts +23 -4
- package/dist/components/data-display/collapsible.js +119 -4
- package/dist/components/data-display/index.d.ts +3 -1
- package/dist/components/data-display/index.js +2 -0
- package/dist/components/data-display/legend.d.ts +15 -0
- package/dist/components/data-display/legend.js +11 -0
- package/dist/components/data-display/list-row.js +1 -1
- package/dist/components/data-display/popover.d.ts +58 -5
- package/dist/components/data-display/popover.js +206 -27
- package/dist/components/data-display/progress.d.ts +50 -2
- package/dist/components/data-display/progress.js +57 -9
- package/dist/components/data-entry/calendar.d.ts +1 -1
- package/dist/components/data-entry/calendar.js +67 -32
- package/dist/components/data-entry/checkbox.d.ts +19 -6
- package/dist/components/data-entry/checkbox.js +55 -16
- package/dist/components/data-entry/choice-option.d.ts +1 -1
- package/dist/components/data-entry/date-range-picker.d.ts +1 -1
- package/dist/components/data-entry/field.js +0 -1
- package/dist/components/data-entry/form.js +1 -1
- package/dist/components/data-entry/label.d.ts +3 -2
- package/dist/components/data-entry/label.js +23 -10
- package/dist/components/data-entry/month-picker.d.ts +1 -1
- package/dist/components/data-entry/month-range-picker.d.ts +1 -1
- package/dist/components/data-entry/transfer.js +16 -6
- package/dist/components/data-entry/tree-select-strategy.d.ts +1 -1
- package/dist/components/data-entry/tree-select.js +29 -12
- package/dist/components/data-entry/tree-utils.d.ts +1 -1
- package/dist/components/data-entry/upload.js +7 -2
- package/dist/components/feedback/dialog.d.ts +105 -38
- package/dist/components/feedback/dialog.js +272 -194
- package/dist/components/feedback/overlay-close-focus.d.ts +31 -0
- package/dist/components/feedback/overlay-close-focus.js +31 -0
- package/dist/components/feedback/overlay-header-tone.d.ts +1 -1
- package/dist/components/feedback/sheet.d.ts +53 -11
- package/dist/components/feedback/sheet.js +150 -81
- package/dist/components/feedback/tooltip.d.ts +51 -7
- package/dist/components/feedback/tooltip.js +107 -25
- package/dist/components/general/button.js +2 -2
- package/dist/components/general/logo.d.ts +17 -0
- package/dist/components/general/logo.js +22 -16
- package/dist/components/layout/app-shell.d.ts +1 -1
- package/dist/components/layout/app-shell.js +52 -8
- package/dist/components/layout/aspect-ratio.js +0 -1
- package/dist/components/layout/auth-divider.js +0 -1
- package/dist/components/layout/flex.d.ts +1 -1
- package/dist/components/layout/flex.js +19 -2
- package/dist/components/layout/nav-surface.d.ts +26 -0
- package/dist/components/layout/nav-surface.js +17 -0
- package/dist/components/layout/org-switcher.d.ts +5 -1
- package/dist/components/layout/org-switcher.js +5 -3
- package/dist/components/layout/separator.js +0 -1
- package/dist/components/layout/sidebar.js +4 -1
- package/dist/components/layout/split-pane.d.ts +14 -1
- package/dist/components/layout/split-pane.js +26 -13
- package/dist/components/layout/topbar-item.js +1 -2
- package/dist/components/navigation/app-setting-picker.js +21 -2
- package/dist/components/navigation/app-setting-toggle.d.ts +16 -0
- package/dist/components/navigation/app-setting-toggle.js +96 -0
- package/dist/components/navigation/dropdown-menu.d.ts +199 -18
- package/dist/components/navigation/dropdown-menu.js +344 -117
- package/dist/components/navigation/index.d.ts +2 -0
- package/dist/components/navigation/index.js +2 -0
- package/dist/components/navigation/pagination-utils.d.ts +2 -1
- package/dist/components/navigation/tabs.d.ts +47 -6
- package/dist/components/navigation/tabs.js +152 -90
- package/dist/components/ui/accordion.d.ts +50 -5
- package/dist/components/ui/accordion.js +239 -33
- package/dist/components/ui/aspect-ratio.d.ts +23 -2
- package/dist/components/ui/aspect-ratio.js +15 -13
- package/dist/components/ui/avatar.d.ts +29 -4
- package/dist/components/ui/avatar.js +111 -25
- package/dist/components/ui/hover-card.d.ts +42 -4
- package/dist/components/ui/hover-card.js +183 -27
- package/dist/components/ui/label.js +0 -1
- package/dist/components/ui/segmented.d.ts +3 -3
- package/dist/components/ui/separator.d.ts +8 -2
- package/dist/components/ui/separator.js +17 -9
- package/dist/components/ui/toggle-group.d.ts +50 -5
- package/dist/components/ui/toggle-group.js +79 -20
- package/dist/components/ui/toggle.d.ts +31 -5
- package/dist/components/ui/toggle.js +42 -3
- package/dist/i18n/messages/en.json +8 -0
- package/dist/i18n/messages/ja.json +8 -0
- package/dist/i18n/messages/vi.json +8 -0
- package/dist/lib/control-styles.d.ts +1 -1
- package/dist/lib/control-styles.js +1 -1
- package/dist/lib/slot.d.ts +32 -0
- package/dist/lib/slot.js +22 -0
- package/dist/lib/variants.d.ts +22 -3
- package/dist/lib/variants.js +56 -1
- package/dist/props/components/app.prop.d.ts +25 -1
- package/dist/props/components/data-display.prop.d.ts +17 -0
- package/dist/props/components/data-entry.prop.d.ts +32 -4
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/components/layout.prop.d.ts +118 -5
- package/dist/props/components/navigation.prop.d.ts +1 -1
- package/dist/props/registry.d.ts +22 -2
- package/dist/props/registry.js +33 -2
- package/dist/props/vocabulary/content.prop.d.ts +1 -1
- package/dist/props/vocabulary/index.d.ts +2 -2
- package/dist/props/vocabulary/interaction.prop.d.ts +17 -1
- package/dist/props/vocabulary/layout.prop.d.ts +57 -1
- package/dist/styles/control.css +142 -1
- package/dist/styles/core.css +5 -2
- package/dist/styles/data-display-layout.css +72 -0
- package/dist/styles/focus-ring.css +4 -0
- package/dist/styles/index.css +5 -2
- package/dist/styles/layout.css +106 -0
- package/dist/styles/shell-layout.css +140 -8
- package/dist/styles/text-layout.css +5 -4
- package/dist/tokens/base.css +1 -1
- package/dist/tokens/components/data-display.css +11 -0
- package/dist/tokens/components/shell.css +3 -1
- package/dist/tokens/foundation.css +2 -0
- package/docs/COMPONENTS.md +9 -3
- package/docs/DESIGN-AUTHORITY.md +96 -71
- package/docs/FRAME-COVERAGE-REPORT.md +5 -3
- package/docs/README.md +14 -14
- package/docs/WHAT-BELONGS-HERE.md +179 -0
- package/docs/data-display/legend.tsx +145 -0
- package/docs/data-display/progress.tsx +32 -0
- package/docs/data-entry/segmented.tsx +1 -1
- package/docs/data-entry/select.tsx +2 -2
- package/docs/feedback/sheet.tsx +1 -1
- package/docs/layout/app-shell-arrangements.tsx +225 -0
- package/docs/layout/aspect-ratio.tsx +1 -1
- package/docs/navigation/app-setting-picker.tsx +26 -1
- package/docs/navigation/app-setting-toggle.tsx +111 -0
- package/package.json +11 -8
- package/scripts/_agent-setup.mjs +165 -3
- package/scripts/consumer-rule.md +98 -0
- package/scripts/guinea-pig-skill.md +322 -0
- package/scripts/init-guinea-pig.mjs +62 -0
- package/scripts/postinstall.mjs +13 -2
- package/scripts/ui-audit.mjs +115 -14
- /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
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
] }) :
|
|
138
|
+
] }) : children
|
|
107
139
|
}
|
|
108
|
-
);
|
|
140
|
+
) });
|
|
109
141
|
}
|
|
110
|
-
const TabsList = React.forwardRef(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
{
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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 =
|
|
181
|
-
const TabsContent = React.forwardRef(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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 =
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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,
|