@diwauhris/ui 1.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/README.md +75 -0
- package/lib/assets/ui.css +154 -0
- package/lib/index.cjs +183 -0
- package/lib/index.mjs +65 -0
- package/lib/index10.cjs +33 -0
- package/lib/index10.cjs.map +1 -0
- package/lib/index10.mjs +33 -0
- package/lib/index10.mjs.map +1 -0
- package/lib/index11.cjs +16 -0
- package/lib/index11.cjs.map +1 -0
- package/lib/index11.mjs +16 -0
- package/lib/index11.mjs.map +1 -0
- package/lib/index12.cjs +16 -0
- package/lib/index12.cjs.map +1 -0
- package/lib/index12.mjs +15 -0
- package/lib/index12.mjs.map +1 -0
- package/lib/index13.cjs +57 -0
- package/lib/index13.cjs.map +1 -0
- package/lib/index13.mjs +57 -0
- package/lib/index13.mjs.map +1 -0
- package/lib/index14.cjs +10 -0
- package/lib/index14.cjs.map +1 -0
- package/lib/index14.mjs +10 -0
- package/lib/index14.mjs.map +1 -0
- package/lib/index15.cjs +30 -0
- package/lib/index15.cjs.map +1 -0
- package/lib/index15.mjs +30 -0
- package/lib/index15.mjs.map +1 -0
- package/lib/index16.cjs +28 -0
- package/lib/index16.cjs.map +1 -0
- package/lib/index16.mjs +26 -0
- package/lib/index16.mjs.map +1 -0
- package/lib/index17.cjs +43 -0
- package/lib/index17.cjs.map +1 -0
- package/lib/index17.mjs +43 -0
- package/lib/index17.mjs.map +1 -0
- package/lib/index18.cjs +59 -0
- package/lib/index18.cjs.map +1 -0
- package/lib/index18.mjs +59 -0
- package/lib/index18.mjs.map +1 -0
- package/lib/index19.cjs +67 -0
- package/lib/index19.cjs.map +1 -0
- package/lib/index19.mjs +67 -0
- package/lib/index19.mjs.map +1 -0
- package/lib/index2.cjs +33 -0
- package/lib/index2.mjs +13 -0
- package/lib/index20.cjs +79 -0
- package/lib/index20.cjs.map +1 -0
- package/lib/index20.mjs +79 -0
- package/lib/index20.mjs.map +1 -0
- package/lib/index21.cjs +105 -0
- package/lib/index21.cjs.map +1 -0
- package/lib/index21.mjs +104 -0
- package/lib/index21.mjs.map +1 -0
- package/lib/index22.cjs +50 -0
- package/lib/index22.cjs.map +1 -0
- package/lib/index22.mjs +50 -0
- package/lib/index22.mjs.map +1 -0
- package/lib/index23.cjs +143 -0
- package/lib/index23.cjs.map +1 -0
- package/lib/index23.mjs +143 -0
- package/lib/index23.mjs.map +1 -0
- package/lib/index25.cjs +177 -0
- package/lib/index25.cjs.map +1 -0
- package/lib/index25.mjs +175 -0
- package/lib/index25.mjs.map +1 -0
- package/lib/index26.cjs +108 -0
- package/lib/index26.cjs.map +1 -0
- package/lib/index26.mjs +108 -0
- package/lib/index26.mjs.map +1 -0
- package/lib/index27.cjs +120 -0
- package/lib/index27.cjs.map +1 -0
- package/lib/index27.mjs +120 -0
- package/lib/index27.mjs.map +1 -0
- package/lib/index28.cjs +54 -0
- package/lib/index28.cjs.map +1 -0
- package/lib/index28.mjs +54 -0
- package/lib/index28.mjs.map +1 -0
- package/lib/index29.cjs +100 -0
- package/lib/index29.cjs.map +1 -0
- package/lib/index29.mjs +100 -0
- package/lib/index29.mjs.map +1 -0
- package/lib/index3.cjs +38 -0
- package/lib/index3.cjs.map +1 -0
- package/lib/index3.mjs +38 -0
- package/lib/index3.mjs.map +1 -0
- package/lib/index30.cjs +27 -0
- package/lib/index30.cjs.map +1 -0
- package/lib/index30.mjs +27 -0
- package/lib/index30.mjs.map +1 -0
- package/lib/index31.cjs +72 -0
- package/lib/index31.cjs.map +1 -0
- package/lib/index31.mjs +72 -0
- package/lib/index31.mjs.map +1 -0
- package/lib/index32.cjs +19 -0
- package/lib/index32.cjs.map +1 -0
- package/lib/index32.mjs +19 -0
- package/lib/index32.mjs.map +1 -0
- package/lib/index33.cjs +25 -0
- package/lib/index33.cjs.map +1 -0
- package/lib/index33.mjs +25 -0
- package/lib/index33.mjs.map +1 -0
- package/lib/index34.cjs +55 -0
- package/lib/index34.cjs.map +1 -0
- package/lib/index34.mjs +51 -0
- package/lib/index34.mjs.map +1 -0
- package/lib/index35.cjs +25 -0
- package/lib/index35.cjs.map +1 -0
- package/lib/index35.mjs +25 -0
- package/lib/index35.mjs.map +1 -0
- package/lib/index36.cjs +54 -0
- package/lib/index36.cjs.map +1 -0
- package/lib/index36.mjs +49 -0
- package/lib/index36.mjs.map +1 -0
- package/lib/index37.cjs +130 -0
- package/lib/index37.cjs.map +1 -0
- package/lib/index37.mjs +128 -0
- package/lib/index37.mjs.map +1 -0
- package/lib/index38.cjs +106 -0
- package/lib/index38.cjs.map +1 -0
- package/lib/index38.mjs +102 -0
- package/lib/index38.mjs.map +1 -0
- package/lib/index39.cjs +137 -0
- package/lib/index39.cjs.map +1 -0
- package/lib/index39.mjs +137 -0
- package/lib/index39.mjs.map +1 -0
- package/lib/index4.cjs +24 -0
- package/lib/index4.cjs.map +1 -0
- package/lib/index4.mjs +24 -0
- package/lib/index4.mjs.map +1 -0
- package/lib/index40.cjs +130 -0
- package/lib/index40.cjs.map +1 -0
- package/lib/index40.mjs +126 -0
- package/lib/index40.mjs.map +1 -0
- package/lib/index41.cjs +60 -0
- package/lib/index41.cjs.map +1 -0
- package/lib/index41.mjs +58 -0
- package/lib/index41.mjs.map +1 -0
- package/lib/index42.cjs +105 -0
- package/lib/index42.cjs.map +1 -0
- package/lib/index42.mjs +105 -0
- package/lib/index42.mjs.map +1 -0
- package/lib/index43.cjs +47 -0
- package/lib/index43.cjs.map +1 -0
- package/lib/index43.mjs +42 -0
- package/lib/index43.mjs.map +1 -0
- package/lib/index44.cjs +40 -0
- package/lib/index44.cjs.map +1 -0
- package/lib/index44.mjs +35 -0
- package/lib/index44.mjs.map +1 -0
- package/lib/index45.cjs +91 -0
- package/lib/index45.cjs.map +1 -0
- package/lib/index45.mjs +91 -0
- package/lib/index45.mjs.map +1 -0
- package/lib/index46.cjs +180 -0
- package/lib/index46.cjs.map +1 -0
- package/lib/index46.mjs +180 -0
- package/lib/index46.mjs.map +1 -0
- package/lib/index47.cjs +61 -0
- package/lib/index47.cjs.map +1 -0
- package/lib/index47.mjs +61 -0
- package/lib/index47.mjs.map +1 -0
- package/lib/index48.cjs +165 -0
- package/lib/index48.cjs.map +1 -0
- package/lib/index48.mjs +161 -0
- package/lib/index48.mjs.map +1 -0
- package/lib/index49.cjs +83 -0
- package/lib/index49.cjs.map +1 -0
- package/lib/index49.mjs +83 -0
- package/lib/index49.mjs.map +1 -0
- package/lib/index5.cjs +39 -0
- package/lib/index5.cjs.map +1 -0
- package/lib/index5.mjs +39 -0
- package/lib/index5.mjs.map +1 -0
- package/lib/index50.cjs +100 -0
- package/lib/index50.cjs.map +1 -0
- package/lib/index50.mjs +100 -0
- package/lib/index50.mjs.map +1 -0
- package/lib/index51.cjs +116 -0
- package/lib/index51.cjs.map +1 -0
- package/lib/index51.mjs +113 -0
- package/lib/index51.mjs.map +1 -0
- package/lib/index52.cjs +93 -0
- package/lib/index52.cjs.map +1 -0
- package/lib/index52.mjs +93 -0
- package/lib/index52.mjs.map +1 -0
- package/lib/index53.cjs +120 -0
- package/lib/index53.cjs.map +1 -0
- package/lib/index53.mjs +120 -0
- package/lib/index53.mjs.map +1 -0
- package/lib/index54.cjs +53 -0
- package/lib/index54.cjs.map +1 -0
- package/lib/index54.mjs +50 -0
- package/lib/index54.mjs.map +1 -0
- package/lib/index55.cjs +130 -0
- package/lib/index55.cjs.map +1 -0
- package/lib/index55.mjs +127 -0
- package/lib/index55.mjs.map +1 -0
- package/lib/index56.cjs +36 -0
- package/lib/index56.cjs.map +1 -0
- package/lib/index56.mjs +35 -0
- package/lib/index56.mjs.map +1 -0
- package/lib/index57.cjs +47 -0
- package/lib/index57.cjs.map +1 -0
- package/lib/index57.mjs +47 -0
- package/lib/index57.mjs.map +1 -0
- package/lib/index58.cjs +97 -0
- package/lib/index58.cjs.map +1 -0
- package/lib/index58.mjs +97 -0
- package/lib/index58.mjs.map +1 -0
- package/lib/index59.cjs +120 -0
- package/lib/index59.cjs.map +1 -0
- package/lib/index59.mjs +116 -0
- package/lib/index59.mjs.map +1 -0
- package/lib/index6.cjs +29 -0
- package/lib/index6.cjs.map +1 -0
- package/lib/index6.mjs +29 -0
- package/lib/index6.mjs.map +1 -0
- package/lib/index60.cjs +34 -0
- package/lib/index60.cjs.map +1 -0
- package/lib/index60.mjs +34 -0
- package/lib/index60.mjs.map +1 -0
- package/lib/index61.cjs +62 -0
- package/lib/index61.cjs.map +1 -0
- package/lib/index61.mjs +62 -0
- package/lib/index61.mjs.map +1 -0
- package/lib/index62.cjs +213 -0
- package/lib/index62.cjs.map +1 -0
- package/lib/index62.mjs +213 -0
- package/lib/index62.mjs.map +1 -0
- package/lib/index63.cjs +42 -0
- package/lib/index63.cjs.map +1 -0
- package/lib/index63.mjs +42 -0
- package/lib/index63.mjs.map +1 -0
- package/lib/index64.cjs +86 -0
- package/lib/index64.cjs.map +1 -0
- package/lib/index64.mjs +86 -0
- package/lib/index64.mjs.map +1 -0
- package/lib/index65.cjs +156 -0
- package/lib/index65.cjs.map +1 -0
- package/lib/index65.mjs +156 -0
- package/lib/index65.mjs.map +1 -0
- package/lib/index66.cjs +107 -0
- package/lib/index66.cjs.map +1 -0
- package/lib/index66.mjs +106 -0
- package/lib/index66.mjs.map +1 -0
- package/lib/index67.cjs +52 -0
- package/lib/index67.cjs.map +1 -0
- package/lib/index67.mjs +52 -0
- package/lib/index67.mjs.map +1 -0
- package/lib/index68.cjs +125 -0
- package/lib/index68.cjs.map +1 -0
- package/lib/index68.mjs +115 -0
- package/lib/index68.mjs.map +1 -0
- package/lib/index69.cjs +34 -0
- package/lib/index69.cjs.map +1 -0
- package/lib/index69.mjs +29 -0
- package/lib/index69.mjs.map +1 -0
- package/lib/index7.cjs +17 -0
- package/lib/index7.cjs.map +1 -0
- package/lib/index7.mjs +17 -0
- package/lib/index7.mjs.map +1 -0
- package/lib/index8.cjs +86 -0
- package/lib/index8.cjs.map +1 -0
- package/lib/index8.mjs +86 -0
- package/lib/index8.mjs.map +1 -0
- package/lib/index9.cjs +29 -0
- package/lib/index9.cjs.map +1 -0
- package/lib/index9.mjs +29 -0
- package/lib/index9.mjs.map +1 -0
- package/lib/types/index.d.ts +1 -0
- package/package.json +68 -0
package/lib/index53.cjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
2
|
+
let react = require("react");
|
|
3
|
+
//#region src/ui-library/gallery/menu/Menu.tsx
|
|
4
|
+
/**
|
|
5
|
+
* Menu — Design System Component
|
|
6
|
+
*
|
|
7
|
+
* A dropdown menu anchored to a trigger. Opens on trigger click,
|
|
8
|
+
* closes on outside-click or Escape. Full keyboard navigation:
|
|
9
|
+
* ArrowDown/ArrowUp to move between items, Home/End for first/last,
|
|
10
|
+
* Escape to close.
|
|
11
|
+
*
|
|
12
|
+
* Follows WAI-ARIA Menu Button pattern:
|
|
13
|
+
* - trigger: role="button", aria-haspopup="menu", aria-expanded
|
|
14
|
+
* - panel: role="menu"
|
|
15
|
+
* - items: role="menuitem"
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* <Menu
|
|
19
|
+
* trigger={<button>Actions</button>}
|
|
20
|
+
* items={[
|
|
21
|
+
* { label: 'Edit', icon: <Pencil size={14} />, onClick: handleEdit },
|
|
22
|
+
* { label: 'Delete', icon: <Trash2 size={14} />, onClick: handleDelete, tone: 'danger' },
|
|
23
|
+
* ]}
|
|
24
|
+
* />
|
|
25
|
+
*/
|
|
26
|
+
function Menu({ trigger, items, ariaLabel = "Menu", align = "right" }) {
|
|
27
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
28
|
+
const containerRef = (0, react.useRef)(null);
|
|
29
|
+
const triggerRef = (0, react.useRef)(null);
|
|
30
|
+
const itemRefs = (0, react.useRef)([]);
|
|
31
|
+
(0, react.useEffect)(() => {
|
|
32
|
+
if (!open) return;
|
|
33
|
+
const handlePointerDown = (e) => {
|
|
34
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
35
|
+
setOpen(false);
|
|
36
|
+
(triggerRef.current?.querySelector("button,a,[tabindex]"))?.focus();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const handleKeyDown = (e) => {
|
|
40
|
+
if (e.key === "Escape") {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
setOpen(false);
|
|
43
|
+
(triggerRef.current?.querySelector("button,a,[tabindex]"))?.focus();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
document.addEventListener("mousedown", handlePointerDown);
|
|
47
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
48
|
+
window.setTimeout(() => {
|
|
49
|
+
itemRefs.current.find((r) => r && !r.disabled)?.focus();
|
|
50
|
+
}, 0);
|
|
51
|
+
return () => {
|
|
52
|
+
document.removeEventListener("mousedown", handlePointerDown);
|
|
53
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
54
|
+
};
|
|
55
|
+
}, [open]);
|
|
56
|
+
const focusItem = (index) => {
|
|
57
|
+
const next = (index % items.length + items.length) % items.length;
|
|
58
|
+
itemRefs.current[next]?.focus();
|
|
59
|
+
};
|
|
60
|
+
const handleItemKeyDown = (e, index) => {
|
|
61
|
+
if (e.key === "ArrowDown") {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
focusItem(index + 1);
|
|
64
|
+
} else if (e.key === "ArrowUp") {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
focusItem(index - 1);
|
|
67
|
+
} else if (e.key === "Home") {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
focusItem(0);
|
|
70
|
+
} else if (e.key === "End") {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
focusItem(items.length - 1);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
76
|
+
className: "relative inline-block",
|
|
77
|
+
ref: containerRef,
|
|
78
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
79
|
+
ref: triggerRef,
|
|
80
|
+
"aria-expanded": open,
|
|
81
|
+
"aria-haspopup": "menu",
|
|
82
|
+
onClick: () => setOpen((v) => !v),
|
|
83
|
+
className: "cursor-pointer",
|
|
84
|
+
children: trigger
|
|
85
|
+
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
86
|
+
role: "menu",
|
|
87
|
+
"aria-label": ariaLabel,
|
|
88
|
+
className: `absolute top-full ${align === "left" ? "left-0" : "right-0"} z-30 mt-1 min-w-[11rem] overflow-hidden rounded-xl border border-slate-200 bg-white py-1 shadow-xl`,
|
|
89
|
+
children: items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
90
|
+
ref: (node) => {
|
|
91
|
+
itemRefs.current[index] = node;
|
|
92
|
+
},
|
|
93
|
+
type: "button",
|
|
94
|
+
role: "menuitem",
|
|
95
|
+
disabled: item.disabled,
|
|
96
|
+
onClick: () => {
|
|
97
|
+
setOpen(false);
|
|
98
|
+
item.onClick();
|
|
99
|
+
},
|
|
100
|
+
onKeyDown: (e) => handleItemKeyDown(e, index),
|
|
101
|
+
className: [
|
|
102
|
+
"flex w-full items-center gap-2.5 px-3.5 py-2 text-left text-sm font-semibold transition",
|
|
103
|
+
"border-l-2 border-transparent",
|
|
104
|
+
"focus-visible:outline-none focus-visible:bg-[#eef2f8]",
|
|
105
|
+
"disabled:cursor-not-allowed disabled:opacity-40",
|
|
106
|
+
item.tone === "danger" ? "text-rose-600 hover:bg-rose-50 hover:border-l-rose-400" : "text-slate-700 hover:bg-[#eef2f8] hover:border-l-brand-blue"
|
|
107
|
+
].filter(Boolean).join(" "),
|
|
108
|
+
children: [item.icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
109
|
+
className: "shrink-0 text-current opacity-60",
|
|
110
|
+
"aria-hidden": "true",
|
|
111
|
+
children: item.icon
|
|
112
|
+
}), item.label]
|
|
113
|
+
}, item.label))
|
|
114
|
+
})]
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
exports.Menu = Menu;
|
|
119
|
+
|
|
120
|
+
//# sourceMappingURL=index53.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index53.cjs","names":[],"sources":["../src/ui-library/gallery/menu/Menu.tsx"],"sourcesContent":["/**\n * Menu — Design System Component\n *\n * A dropdown menu anchored to a trigger. Opens on trigger click,\n * closes on outside-click or Escape. Full keyboard navigation:\n * ArrowDown/ArrowUp to move between items, Home/End for first/last,\n * Escape to close.\n *\n * Follows WAI-ARIA Menu Button pattern:\n * - trigger: role=\"button\", aria-haspopup=\"menu\", aria-expanded\n * - panel: role=\"menu\"\n * - items: role=\"menuitem\"\n *\n * Usage:\n * <Menu\n * trigger={<button>Actions</button>}\n * items={[\n * { label: 'Edit', icon: <Pencil size={14} />, onClick: handleEdit },\n * { label: 'Delete', icon: <Trash2 size={14} />, onClick: handleDelete, tone: 'danger' },\n * ]}\n * />\n */\n\nimport { useEffect, useRef, useState, type ReactNode } from 'react';\n\nexport type MenuItemTone = 'default' | 'danger';\n\nexport interface MenuItem {\n label: string;\n icon?: ReactNode;\n onClick: () => void;\n tone?: MenuItemTone;\n disabled?: boolean;\n}\n\nexport interface MenuProps {\n /** The trigger element. Should be a visible button-like control. */\n trigger: ReactNode;\n /** Menu items. */\n items: MenuItem[];\n /** Accessible label for the panel (aria-label). Defaults to \"Menu\". */\n ariaLabel?: string;\n /**\n * Panel alignment relative to the trigger.\n * 'left' — panel left edge aligns with trigger left edge\n * 'right' — panel right edge aligns with trigger right edge (default)\n */\n align?: 'left' | 'right';\n}\n\nexport function Menu({\n trigger,\n items,\n ariaLabel = 'Menu',\n align = 'right',\n}: MenuProps) {\n const [open, setOpen] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLDivElement>(null);\n const itemRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n // ── Outside click and Escape ────────────────────────────────────────────────\n useEffect(() => {\n if (!open) return;\n\n const handlePointerDown = (e: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n setOpen(false);\n // Return focus to the trigger's first focusable child\n const btn = triggerRef.current?.querySelector<HTMLElement>('button,a,[tabindex]');\n btn?.focus();\n }\n };\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n setOpen(false);\n // Return focus to the trigger's first focusable child\n const btn = triggerRef.current?.querySelector<HTMLElement>('button,a,[tabindex]');\n btn?.focus();\n }\n };\n\n document.addEventListener('mousedown', handlePointerDown);\n document.addEventListener('keydown', handleKeyDown);\n // Focus first non-disabled item on open\n window.setTimeout(() => {\n const first = itemRefs.current.find((r) => r && !r.disabled);\n first?.focus();\n }, 0);\n\n return () => {\n document.removeEventListener('mousedown', handlePointerDown);\n document.removeEventListener('keydown', handleKeyDown);\n };\n }, [open]);\n\n const focusItem = (index: number) => {\n const next = ((index % items.length) + items.length) % items.length;\n itemRefs.current[next]?.focus();\n };\n\n const handleItemKeyDown = (e: React.KeyboardEvent, index: number) => {\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n focusItem(index + 1);\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n focusItem(index - 1);\n } else if (e.key === 'Home') {\n e.preventDefault();\n focusItem(0);\n } else if (e.key === 'End') {\n e.preventDefault();\n focusItem(items.length - 1);\n }\n };\n\n const panelAlign = align === 'left' ? 'left-0' : 'right-0';\n\n return (\n <div className=\"relative inline-block\" ref={containerRef}>\n {/* Trigger wrapper — captures the aria-expanded/haspopup states so screen\n readers announce menu availability, then forwards click to consumer trigger. */}\n <div\n ref={triggerRef}\n aria-expanded={open}\n aria-haspopup=\"menu\"\n onClick={() => setOpen((v) => !v)}\n className=\"cursor-pointer\"\n >\n {trigger}\n </div>\n\n {/* Panel */}\n {open && (\n <div\n role=\"menu\"\n aria-label={ariaLabel}\n className={`absolute top-full ${panelAlign} z-30 mt-1 min-w-[11rem] overflow-hidden rounded-xl border border-slate-200 bg-white py-1 shadow-xl`}\n >\n {items.map((item, index) => (\n <button\n key={item.label}\n ref={(node) => { itemRefs.current[index] = node; }}\n type=\"button\"\n role=\"menuitem\"\n disabled={item.disabled}\n onClick={() => {\n setOpen(false);\n item.onClick();\n }}\n onKeyDown={(e) => handleItemKeyDown(e, index)}\n className={[\n 'flex w-full items-center gap-2.5 px-3.5 py-2 text-left text-sm font-semibold transition',\n 'border-l-2 border-transparent',\n 'focus-visible:outline-none focus-visible:bg-[#eef2f8]',\n 'disabled:cursor-not-allowed disabled:opacity-40',\n item.tone === 'danger'\n ? 'text-rose-600 hover:bg-rose-50 hover:border-l-rose-400'\n : 'text-slate-700 hover:bg-[#eef2f8] hover:border-l-brand-blue',\n ]\n .filter(Boolean)\n .join(' ')}\n >\n {item.icon && (\n <span className=\"shrink-0 text-current opacity-60\" aria-hidden=\"true\">\n {item.icon}\n </span>\n )}\n {item.label}\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
package/lib/index53.mjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
//#region src/ui-library/gallery/menu/Menu.tsx
|
|
4
|
+
/**
|
|
5
|
+
* Menu — Design System Component
|
|
6
|
+
*
|
|
7
|
+
* A dropdown menu anchored to a trigger. Opens on trigger click,
|
|
8
|
+
* closes on outside-click or Escape. Full keyboard navigation:
|
|
9
|
+
* ArrowDown/ArrowUp to move between items, Home/End for first/last,
|
|
10
|
+
* Escape to close.
|
|
11
|
+
*
|
|
12
|
+
* Follows WAI-ARIA Menu Button pattern:
|
|
13
|
+
* - trigger: role="button", aria-haspopup="menu", aria-expanded
|
|
14
|
+
* - panel: role="menu"
|
|
15
|
+
* - items: role="menuitem"
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* <Menu
|
|
19
|
+
* trigger={<button>Actions</button>}
|
|
20
|
+
* items={[
|
|
21
|
+
* { label: 'Edit', icon: <Pencil size={14} />, onClick: handleEdit },
|
|
22
|
+
* { label: 'Delete', icon: <Trash2 size={14} />, onClick: handleDelete, tone: 'danger' },
|
|
23
|
+
* ]}
|
|
24
|
+
* />
|
|
25
|
+
*/
|
|
26
|
+
function Menu({ trigger, items, ariaLabel = "Menu", align = "right" }) {
|
|
27
|
+
const [open, setOpen] = useState(false);
|
|
28
|
+
const containerRef = useRef(null);
|
|
29
|
+
const triggerRef = useRef(null);
|
|
30
|
+
const itemRefs = useRef([]);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!open) return;
|
|
33
|
+
const handlePointerDown = (e) => {
|
|
34
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
35
|
+
setOpen(false);
|
|
36
|
+
(triggerRef.current?.querySelector("button,a,[tabindex]"))?.focus();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const handleKeyDown = (e) => {
|
|
40
|
+
if (e.key === "Escape") {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
setOpen(false);
|
|
43
|
+
(triggerRef.current?.querySelector("button,a,[tabindex]"))?.focus();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
document.addEventListener("mousedown", handlePointerDown);
|
|
47
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
48
|
+
window.setTimeout(() => {
|
|
49
|
+
itemRefs.current.find((r) => r && !r.disabled)?.focus();
|
|
50
|
+
}, 0);
|
|
51
|
+
return () => {
|
|
52
|
+
document.removeEventListener("mousedown", handlePointerDown);
|
|
53
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
54
|
+
};
|
|
55
|
+
}, [open]);
|
|
56
|
+
const focusItem = (index) => {
|
|
57
|
+
const next = (index % items.length + items.length) % items.length;
|
|
58
|
+
itemRefs.current[next]?.focus();
|
|
59
|
+
};
|
|
60
|
+
const handleItemKeyDown = (e, index) => {
|
|
61
|
+
if (e.key === "ArrowDown") {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
focusItem(index + 1);
|
|
64
|
+
} else if (e.key === "ArrowUp") {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
focusItem(index - 1);
|
|
67
|
+
} else if (e.key === "Home") {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
focusItem(0);
|
|
70
|
+
} else if (e.key === "End") {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
focusItem(items.length - 1);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
76
|
+
className: "relative inline-block",
|
|
77
|
+
ref: containerRef,
|
|
78
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
79
|
+
ref: triggerRef,
|
|
80
|
+
"aria-expanded": open,
|
|
81
|
+
"aria-haspopup": "menu",
|
|
82
|
+
onClick: () => setOpen((v) => !v),
|
|
83
|
+
className: "cursor-pointer",
|
|
84
|
+
children: trigger
|
|
85
|
+
}), open && /* @__PURE__ */ jsx("div", {
|
|
86
|
+
role: "menu",
|
|
87
|
+
"aria-label": ariaLabel,
|
|
88
|
+
className: `absolute top-full ${align === "left" ? "left-0" : "right-0"} z-30 mt-1 min-w-[11rem] overflow-hidden rounded-xl border border-slate-200 bg-white py-1 shadow-xl`,
|
|
89
|
+
children: items.map((item, index) => /* @__PURE__ */ jsxs("button", {
|
|
90
|
+
ref: (node) => {
|
|
91
|
+
itemRefs.current[index] = node;
|
|
92
|
+
},
|
|
93
|
+
type: "button",
|
|
94
|
+
role: "menuitem",
|
|
95
|
+
disabled: item.disabled,
|
|
96
|
+
onClick: () => {
|
|
97
|
+
setOpen(false);
|
|
98
|
+
item.onClick();
|
|
99
|
+
},
|
|
100
|
+
onKeyDown: (e) => handleItemKeyDown(e, index),
|
|
101
|
+
className: [
|
|
102
|
+
"flex w-full items-center gap-2.5 px-3.5 py-2 text-left text-sm font-semibold transition",
|
|
103
|
+
"border-l-2 border-transparent",
|
|
104
|
+
"focus-visible:outline-none focus-visible:bg-[#eef2f8]",
|
|
105
|
+
"disabled:cursor-not-allowed disabled:opacity-40",
|
|
106
|
+
item.tone === "danger" ? "text-rose-600 hover:bg-rose-50 hover:border-l-rose-400" : "text-slate-700 hover:bg-[#eef2f8] hover:border-l-brand-blue"
|
|
107
|
+
].filter(Boolean).join(" "),
|
|
108
|
+
children: [item.icon && /* @__PURE__ */ jsx("span", {
|
|
109
|
+
className: "shrink-0 text-current opacity-60",
|
|
110
|
+
"aria-hidden": "true",
|
|
111
|
+
children: item.icon
|
|
112
|
+
}), item.label]
|
|
113
|
+
}, item.label))
|
|
114
|
+
})]
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
export { Menu };
|
|
119
|
+
|
|
120
|
+
//# sourceMappingURL=index53.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index53.mjs","names":[],"sources":["../src/ui-library/gallery/menu/Menu.tsx"],"sourcesContent":["/**\n * Menu — Design System Component\n *\n * A dropdown menu anchored to a trigger. Opens on trigger click,\n * closes on outside-click or Escape. Full keyboard navigation:\n * ArrowDown/ArrowUp to move between items, Home/End for first/last,\n * Escape to close.\n *\n * Follows WAI-ARIA Menu Button pattern:\n * - trigger: role=\"button\", aria-haspopup=\"menu\", aria-expanded\n * - panel: role=\"menu\"\n * - items: role=\"menuitem\"\n *\n * Usage:\n * <Menu\n * trigger={<button>Actions</button>}\n * items={[\n * { label: 'Edit', icon: <Pencil size={14} />, onClick: handleEdit },\n * { label: 'Delete', icon: <Trash2 size={14} />, onClick: handleDelete, tone: 'danger' },\n * ]}\n * />\n */\n\nimport { useEffect, useRef, useState, type ReactNode } from 'react';\n\nexport type MenuItemTone = 'default' | 'danger';\n\nexport interface MenuItem {\n label: string;\n icon?: ReactNode;\n onClick: () => void;\n tone?: MenuItemTone;\n disabled?: boolean;\n}\n\nexport interface MenuProps {\n /** The trigger element. Should be a visible button-like control. */\n trigger: ReactNode;\n /** Menu items. */\n items: MenuItem[];\n /** Accessible label for the panel (aria-label). Defaults to \"Menu\". */\n ariaLabel?: string;\n /**\n * Panel alignment relative to the trigger.\n * 'left' — panel left edge aligns with trigger left edge\n * 'right' — panel right edge aligns with trigger right edge (default)\n */\n align?: 'left' | 'right';\n}\n\nexport function Menu({\n trigger,\n items,\n ariaLabel = 'Menu',\n align = 'right',\n}: MenuProps) {\n const [open, setOpen] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLDivElement>(null);\n const itemRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n // ── Outside click and Escape ────────────────────────────────────────────────\n useEffect(() => {\n if (!open) return;\n\n const handlePointerDown = (e: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n setOpen(false);\n // Return focus to the trigger's first focusable child\n const btn = triggerRef.current?.querySelector<HTMLElement>('button,a,[tabindex]');\n btn?.focus();\n }\n };\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n setOpen(false);\n // Return focus to the trigger's first focusable child\n const btn = triggerRef.current?.querySelector<HTMLElement>('button,a,[tabindex]');\n btn?.focus();\n }\n };\n\n document.addEventListener('mousedown', handlePointerDown);\n document.addEventListener('keydown', handleKeyDown);\n // Focus first non-disabled item on open\n window.setTimeout(() => {\n const first = itemRefs.current.find((r) => r && !r.disabled);\n first?.focus();\n }, 0);\n\n return () => {\n document.removeEventListener('mousedown', handlePointerDown);\n document.removeEventListener('keydown', handleKeyDown);\n };\n }, [open]);\n\n const focusItem = (index: number) => {\n const next = ((index % items.length) + items.length) % items.length;\n itemRefs.current[next]?.focus();\n };\n\n const handleItemKeyDown = (e: React.KeyboardEvent, index: number) => {\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n focusItem(index + 1);\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n focusItem(index - 1);\n } else if (e.key === 'Home') {\n e.preventDefault();\n focusItem(0);\n } else if (e.key === 'End') {\n e.preventDefault();\n focusItem(items.length - 1);\n }\n };\n\n const panelAlign = align === 'left' ? 'left-0' : 'right-0';\n\n return (\n <div className=\"relative inline-block\" ref={containerRef}>\n {/* Trigger wrapper — captures the aria-expanded/haspopup states so screen\n readers announce menu availability, then forwards click to consumer trigger. */}\n <div\n ref={triggerRef}\n aria-expanded={open}\n aria-haspopup=\"menu\"\n onClick={() => setOpen((v) => !v)}\n className=\"cursor-pointer\"\n >\n {trigger}\n </div>\n\n {/* Panel */}\n {open && (\n <div\n role=\"menu\"\n aria-label={ariaLabel}\n className={`absolute top-full ${panelAlign} z-30 mt-1 min-w-[11rem] overflow-hidden rounded-xl border border-slate-200 bg-white py-1 shadow-xl`}\n >\n {items.map((item, index) => (\n <button\n key={item.label}\n ref={(node) => { itemRefs.current[index] = node; }}\n type=\"button\"\n role=\"menuitem\"\n disabled={item.disabled}\n onClick={() => {\n setOpen(false);\n item.onClick();\n }}\n onKeyDown={(e) => handleItemKeyDown(e, index)}\n className={[\n 'flex w-full items-center gap-2.5 px-3.5 py-2 text-left text-sm font-semibold transition',\n 'border-l-2 border-transparent',\n 'focus-visible:outline-none focus-visible:bg-[#eef2f8]',\n 'disabled:cursor-not-allowed disabled:opacity-40',\n item.tone === 'danger'\n ? 'text-rose-600 hover:bg-rose-50 hover:border-l-rose-400'\n : 'text-slate-700 hover:bg-[#eef2f8] hover:border-l-brand-blue',\n ]\n .filter(Boolean)\n .join(' ')}\n >\n {item.icon && (\n <span className=\"shrink-0 text-current opacity-60\" aria-hidden=\"true\">\n {item.icon}\n </span>\n )}\n {item.label}\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,SAAgB,KAAK,EACnB,SACA,OACA,YAAY,QACZ,QAAQ,WACI;CACZ,MAAM,CAAC,MAAM,WAAW,SAAS,KAAK;CACtC,MAAM,eAAe,OAAuB,IAAI;CAChD,MAAM,aAAa,OAAuB,IAAI;CAC9C,MAAM,WAAW,OAAwC,CAAC,CAAC;CAG3D,gBAAgB;EACd,IAAI,CAAC,MAAM;EAEX,MAAM,qBAAqB,MAAkB;GAC3C,IAAI,aAAa,WAAW,CAAC,aAAa,QAAQ,SAAS,EAAE,MAAc,GAAG;IAC5E,QAAQ,KAAK;IAGb,CADY,WAAW,SAAS,cAA2B,qBAAqB,EAAA,EAC3E,MAAM;GACb;EACF;EACA,MAAM,iBAAiB,MAAqB;GAC1C,IAAI,EAAE,QAAQ,UAAU;IACtB,EAAE,eAAe;IACjB,QAAQ,KAAK;IAGb,CADY,WAAW,SAAS,cAA2B,qBAAqB,EAAA,EAC3E,MAAM;GACb;EACF;EAEA,SAAS,iBAAiB,aAAa,iBAAiB;EACxD,SAAS,iBAAiB,WAAW,aAAa;EAElD,OAAO,iBAAiB;GAEtB,SADuB,QAAQ,MAAM,MAAM,KAAK,CAAC,EAAE,QACnD,CAAA,EAAO,MAAM;EACf,GAAG,CAAC;EAEJ,aAAa;GACX,SAAS,oBAAoB,aAAa,iBAAiB;GAC3D,SAAS,oBAAoB,WAAW,aAAa;EACvD;CACF,GAAG,CAAC,IAAI,CAAC;CAET,MAAM,aAAa,UAAkB;EACnC,MAAM,QAAS,QAAQ,MAAM,SAAU,MAAM,UAAU,MAAM;EAC7D,SAAS,QAAQ,KAAK,EAAE,MAAM;CAChC;CAEA,MAAM,qBAAqB,GAAwB,UAAkB;EACnE,IAAI,EAAE,QAAQ,aAAa;GACzB,EAAE,eAAe;GACjB,UAAU,QAAQ,CAAC;EACrB,OAAO,IAAI,EAAE,QAAQ,WAAW;GAC9B,EAAE,eAAe;GACjB,UAAU,QAAQ,CAAC;EACrB,OAAO,IAAI,EAAE,QAAQ,QAAQ;GAC3B,EAAE,eAAe;GACjB,UAAU,CAAC;EACb,OAAO,IAAI,EAAE,QAAQ,OAAO;GAC1B,EAAE,eAAe;GACjB,UAAU,MAAM,SAAS,CAAC;EAC5B;CACF;CAIA,OACE,qBAAC,OAAD;EAAK,WAAU;EAAwB,KAAK;EAA5C,UAAA,CAGE,oBAAC,OAAD;GACE,KAAK;GACL,iBAAe;GACf,iBAAc;GACd,eAAe,SAAS,MAAM,CAAC,CAAC;GAChC,WAAU;GAET,UAAA;EACE,CAAA,GAGJ,QACC,oBAAC,OAAD;GACE,MAAK;GACL,cAAY;GACZ,WAAW,qBArBA,UAAU,SAAS,WAAW,UAqBE;GAE1C,UAAA,MAAM,KAAK,MAAM,UAChB,qBAAC,UAAD;IAEE,MAAM,SAAS;KAAE,SAAS,QAAQ,SAAS;IAAM;IACjD,MAAK;IACL,MAAK;IACL,UAAU,KAAK;IACf,eAAe;KACb,QAAQ,KAAK;KACb,KAAK,QAAQ;IACf;IACA,YAAY,MAAM,kBAAkB,GAAG,KAAK;IAC5C,WAAW;KACT;KACA;KACA;KACA;KACA,KAAK,SAAS,WACV,2DACA;IACN,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;IArBb,UAAA,CAuBG,KAAK,QACJ,oBAAC,QAAD;KAAM,WAAU;KAAmC,eAAY;KAC5D,UAAA,KAAK;IACF,CAAA,GAEP,KAAK,KACA;GA5BD,GAAA,KAAK,KA4BJ,CACT;EACE,CAAA,CAEJ;;AAET"}
|
package/lib/index54.cjs
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
2
|
+
//#region src/ui-library/gallery/card/Card.tsx
|
|
3
|
+
var VARIANT_CLASS = {
|
|
4
|
+
default: "border border-slate-200 bg-white",
|
|
5
|
+
outlined: "border-2 border-slate-200 bg-white",
|
|
6
|
+
elevated: "border border-slate-100 bg-white shadow-lg shadow-slate-100",
|
|
7
|
+
interactive: "border border-slate-200 bg-white cursor-pointer transition hover:border-brand-blue/40 hover:shadow-md active:scale-[.99]"
|
|
8
|
+
};
|
|
9
|
+
function Card({ variant = "default", children, className = "", onClick }) {
|
|
10
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(onClick ? "button" : "div", {
|
|
11
|
+
type: onClick ? "button" : void 0,
|
|
12
|
+
onClick,
|
|
13
|
+
className: `overflow-hidden rounded-lg ${VARIANT_CLASS[variant]} ${className}`,
|
|
14
|
+
children
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function CardHeader({ title, description, actions }) {
|
|
18
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
19
|
+
className: "flex items-start justify-between gap-3 px-5 py-4 border-b-2 border-slate-100",
|
|
20
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
21
|
+
className: "min-w-0",
|
|
22
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
23
|
+
className: "font-heading text-sm font-bold text-slate-900 truncate",
|
|
24
|
+
children: title
|
|
25
|
+
}), description && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
26
|
+
className: "mt-0.5 text-xs font-medium text-slate-400 leading-relaxed",
|
|
27
|
+
children: description
|
|
28
|
+
})]
|
|
29
|
+
}), actions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30
|
+
className: "shrink-0",
|
|
31
|
+
children: actions
|
|
32
|
+
})]
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function CardContent({ children, className = "" }) {
|
|
36
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
37
|
+
className: `px-5 py-4 ${className}`,
|
|
38
|
+
children
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function CardFooter({ children, className = "" }) {
|
|
42
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
43
|
+
className: `flex items-center justify-end gap-2 border-t border-slate-100 px-5 py-3 ${className}`,
|
|
44
|
+
children
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
exports.Card = Card;
|
|
49
|
+
exports.CardContent = CardContent;
|
|
50
|
+
exports.CardFooter = CardFooter;
|
|
51
|
+
exports.CardHeader = CardHeader;
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=index54.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index54.cjs","names":[],"sources":["../src/ui-library/gallery/card/Card.tsx"],"sourcesContent":["/**\n * Card — Design System Component\n *\n * An elevated surface for grouping related content.\n *\n * Composition:\n * <Card>\n * <CardHeader title=\"…\" description=\"…\" actions={…} />\n * <CardContent>…</CardContent>\n * <CardFooter>…</CardFooter>\n * </Card>\n *\n * Variants: default | outlined | elevated | interactive\n */\n\nimport type { ReactNode } from 'react';\n\nexport type CardVariant = 'default' | 'outlined' | 'elevated' | 'interactive';\n\nexport interface CardProps {\n variant?: CardVariant;\n children: ReactNode;\n className?: string;\n /** When variant=\"interactive\", called on click */\n onClick?: () => void;\n}\n\nexport interface CardHeaderProps {\n title: string;\n description?: string;\n /** Optional content aligned to the right (badge, menu, button) */\n actions?: ReactNode;\n}\n\nexport interface CardContentProps {\n children: ReactNode;\n className?: string;\n}\n\nexport interface CardFooterProps {\n children: ReactNode;\n className?: string;\n}\n\n// ── Style maps ────────────────────────────────────────────────────────────────\n\nconst VARIANT_CLASS: Record<CardVariant, string> = {\n default: 'border border-slate-200 bg-white',\n outlined: 'border-2 border-slate-200 bg-white',\n elevated: 'border border-slate-100 bg-white shadow-lg shadow-slate-100',\n interactive: 'border border-slate-200 bg-white cursor-pointer transition hover:border-brand-blue/40 hover:shadow-md active:scale-[.99]',\n};\n\n// ── Components ────────────────────────────────────────────────────────────────\n\nexport function Card({ variant = 'default', children, className = '', onClick }: CardProps) {\n const Tag = onClick ? 'button' : 'div';\n return (\n <Tag\n type={onClick ? 'button' : undefined}\n onClick={onClick}\n className={`overflow-hidden rounded-lg ${VARIANT_CLASS[variant]} ${className}`}\n >\n {children}\n </Tag>\n );\n}\n\nexport function CardHeader({ title, description, actions }: CardHeaderProps) {\n return (\n <div className=\"flex items-start justify-between gap-3 px-5 py-4 border-b-2 border-slate-100\">\n <div className=\"min-w-0\">\n <h3 className=\"font-heading text-sm font-bold text-slate-900 truncate\">{title}</h3>\n {description && (\n <p className=\"mt-0.5 text-xs font-medium text-slate-400 leading-relaxed\">{description}</p>\n )}\n </div>\n {actions && <div className=\"shrink-0\">{actions}</div>}\n </div>\n );\n}\n\nexport function CardContent({ children, className = '' }: CardContentProps) {\n return <div className={`px-5 py-4 ${className}`}>{children}</div>;\n}\n\nexport function CardFooter({ children, className = '' }: CardFooterProps) {\n return (\n <div className={`flex items-center justify-end gap-2 border-t border-slate-100 px-5 py-3 ${className}`}>\n {children}\n </div>\n );\n}\n"],"mappings":";;AA8CA,IAAM,gBAA6C;CACjD,SAAa;CACb,UAAa;CACb,UAAa;CACb,aAAa;AACf;AAIA,SAAgB,KAAK,EAAE,UAAU,WAAW,UAAU,YAAY,IAAI,WAAsB;CAE1F,OACE,iBAAA,GAAA,kBAAA,IAAA,CAFU,UAAU,WAAW,OAE/B;EACE,MAAM,UAAU,WAAW,KAAA;EAClB;EACT,WAAW,8BAA8B,cAAc,SAAS,GAAG;EAElE;CACE,CAAA;AAET;AAEA,SAAgB,WAAW,EAAE,OAAO,aAAa,WAA4B;CAC3E,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD;IAAI,WAAU;IAA0D,UAAA;GAAU,CAAA,GACjF,eACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;IAAG,WAAU;IAA6D,UAAA;GAAe,CAAA,CAExF;EACJ,CAAA,GAAA,WAAW,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;GAAK,WAAU;GAAY,UAAA;EAAa,CAAA,CACjD;;AAET;AAEA,SAAgB,YAAY,EAAE,UAAU,YAAY,MAAwB;CAC1E,OAAO,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;EAAK,WAAW,aAAa;EAAc;CAAc,CAAA;AAClE;AAEA,SAAgB,WAAW,EAAE,UAAU,YAAY,MAAuB;CACxE,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;EAAK,WAAW,2EAA2E;EACxF;CACE,CAAA;AAET"}
|
package/lib/index54.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
//#region src/ui-library/gallery/card/Card.tsx
|
|
3
|
+
var VARIANT_CLASS = {
|
|
4
|
+
default: "border border-slate-200 bg-white",
|
|
5
|
+
outlined: "border-2 border-slate-200 bg-white",
|
|
6
|
+
elevated: "border border-slate-100 bg-white shadow-lg shadow-slate-100",
|
|
7
|
+
interactive: "border border-slate-200 bg-white cursor-pointer transition hover:border-brand-blue/40 hover:shadow-md active:scale-[.99]"
|
|
8
|
+
};
|
|
9
|
+
function Card({ variant = "default", children, className = "", onClick }) {
|
|
10
|
+
return /* @__PURE__ */ jsx(onClick ? "button" : "div", {
|
|
11
|
+
type: onClick ? "button" : void 0,
|
|
12
|
+
onClick,
|
|
13
|
+
className: `overflow-hidden rounded-lg ${VARIANT_CLASS[variant]} ${className}`,
|
|
14
|
+
children
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function CardHeader({ title, description, actions }) {
|
|
18
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
19
|
+
className: "flex items-start justify-between gap-3 px-5 py-4 border-b-2 border-slate-100",
|
|
20
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
21
|
+
className: "min-w-0",
|
|
22
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
23
|
+
className: "font-heading text-sm font-bold text-slate-900 truncate",
|
|
24
|
+
children: title
|
|
25
|
+
}), description && /* @__PURE__ */ jsx("p", {
|
|
26
|
+
className: "mt-0.5 text-xs font-medium text-slate-400 leading-relaxed",
|
|
27
|
+
children: description
|
|
28
|
+
})]
|
|
29
|
+
}), actions && /* @__PURE__ */ jsx("div", {
|
|
30
|
+
className: "shrink-0",
|
|
31
|
+
children: actions
|
|
32
|
+
})]
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function CardContent({ children, className = "" }) {
|
|
36
|
+
return /* @__PURE__ */ jsx("div", {
|
|
37
|
+
className: `px-5 py-4 ${className}`,
|
|
38
|
+
children
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function CardFooter({ children, className = "" }) {
|
|
42
|
+
return /* @__PURE__ */ jsx("div", {
|
|
43
|
+
className: `flex items-center justify-end gap-2 border-t border-slate-100 px-5 py-3 ${className}`,
|
|
44
|
+
children
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { Card, CardContent, CardFooter, CardHeader };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=index54.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index54.mjs","names":[],"sources":["../src/ui-library/gallery/card/Card.tsx"],"sourcesContent":["/**\n * Card — Design System Component\n *\n * An elevated surface for grouping related content.\n *\n * Composition:\n * <Card>\n * <CardHeader title=\"…\" description=\"…\" actions={…} />\n * <CardContent>…</CardContent>\n * <CardFooter>…</CardFooter>\n * </Card>\n *\n * Variants: default | outlined | elevated | interactive\n */\n\nimport type { ReactNode } from 'react';\n\nexport type CardVariant = 'default' | 'outlined' | 'elevated' | 'interactive';\n\nexport interface CardProps {\n variant?: CardVariant;\n children: ReactNode;\n className?: string;\n /** When variant=\"interactive\", called on click */\n onClick?: () => void;\n}\n\nexport interface CardHeaderProps {\n title: string;\n description?: string;\n /** Optional content aligned to the right (badge, menu, button) */\n actions?: ReactNode;\n}\n\nexport interface CardContentProps {\n children: ReactNode;\n className?: string;\n}\n\nexport interface CardFooterProps {\n children: ReactNode;\n className?: string;\n}\n\n// ── Style maps ────────────────────────────────────────────────────────────────\n\nconst VARIANT_CLASS: Record<CardVariant, string> = {\n default: 'border border-slate-200 bg-white',\n outlined: 'border-2 border-slate-200 bg-white',\n elevated: 'border border-slate-100 bg-white shadow-lg shadow-slate-100',\n interactive: 'border border-slate-200 bg-white cursor-pointer transition hover:border-brand-blue/40 hover:shadow-md active:scale-[.99]',\n};\n\n// ── Components ────────────────────────────────────────────────────────────────\n\nexport function Card({ variant = 'default', children, className = '', onClick }: CardProps) {\n const Tag = onClick ? 'button' : 'div';\n return (\n <Tag\n type={onClick ? 'button' : undefined}\n onClick={onClick}\n className={`overflow-hidden rounded-lg ${VARIANT_CLASS[variant]} ${className}`}\n >\n {children}\n </Tag>\n );\n}\n\nexport function CardHeader({ title, description, actions }: CardHeaderProps) {\n return (\n <div className=\"flex items-start justify-between gap-3 px-5 py-4 border-b-2 border-slate-100\">\n <div className=\"min-w-0\">\n <h3 className=\"font-heading text-sm font-bold text-slate-900 truncate\">{title}</h3>\n {description && (\n <p className=\"mt-0.5 text-xs font-medium text-slate-400 leading-relaxed\">{description}</p>\n )}\n </div>\n {actions && <div className=\"shrink-0\">{actions}</div>}\n </div>\n );\n}\n\nexport function CardContent({ children, className = '' }: CardContentProps) {\n return <div className={`px-5 py-4 ${className}`}>{children}</div>;\n}\n\nexport function CardFooter({ children, className = '' }: CardFooterProps) {\n return (\n <div className={`flex items-center justify-end gap-2 border-t border-slate-100 px-5 py-3 ${className}`}>\n {children}\n </div>\n );\n}\n"],"mappings":";;AA8CA,IAAM,gBAA6C;CACjD,SAAa;CACb,UAAa;CACb,UAAa;CACb,aAAa;AACf;AAIA,SAAgB,KAAK,EAAE,UAAU,WAAW,UAAU,YAAY,IAAI,WAAsB;CAE1F,OACE,oBAFU,UAAU,WAAW,OAE/B;EACE,MAAM,UAAU,WAAW,KAAA;EAClB;EACT,WAAW,8BAA8B,cAAc,SAAS,GAAG;EAElE;CACE,CAAA;AAET;AAEA,SAAgB,WAAW,EAAE,OAAO,aAAa,WAA4B;CAC3E,OACE,qBAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,oBAAC,MAAD;IAAI,WAAU;IAA0D,UAAA;GAAU,CAAA,GACjF,eACC,oBAAC,KAAD;IAAG,WAAU;IAA6D,UAAA;GAAe,CAAA,CAExF;EACJ,CAAA,GAAA,WAAW,oBAAC,OAAD;GAAK,WAAU;GAAY,UAAA;EAAa,CAAA,CACjD;;AAET;AAEA,SAAgB,YAAY,EAAE,UAAU,YAAY,MAAwB;CAC1E,OAAO,oBAAC,OAAD;EAAK,WAAW,aAAa;EAAc;CAAc,CAAA;AAClE;AAEA,SAAgB,WAAW,EAAE,UAAU,YAAY,MAAuB;CACxE,OACE,oBAAC,OAAD;EAAK,WAAW,2EAA2E;EACxF;CACE,CAAA;AAET"}
|
package/lib/index55.cjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
2
|
+
let react = require("react");
|
|
3
|
+
let lucide_react = require("lucide-react");
|
|
4
|
+
//#region src/ui-library/gallery/accordion/Accordion.tsx
|
|
5
|
+
/**
|
|
6
|
+
* Accordion — Design System Component
|
|
7
|
+
*
|
|
8
|
+
* Disclosure/accordion pattern with support for single-open and
|
|
9
|
+
* multiple-open modes, controlled/uncontrolled state, and disabled items.
|
|
10
|
+
*
|
|
11
|
+
* WAI-ARIA Accordion pattern:
|
|
12
|
+
* - Trigger is a <button> with aria-expanded and aria-controls
|
|
13
|
+
* - Content panel has a stable id referenced by aria-controls
|
|
14
|
+
* - Disabled triggers have aria-disabled + disabled attribute
|
|
15
|
+
*
|
|
16
|
+
* Keyboard:
|
|
17
|
+
* - Tab moves between triggers
|
|
18
|
+
* - Enter / Space toggle the focused trigger
|
|
19
|
+
*
|
|
20
|
+
* Composition:
|
|
21
|
+
* <Accordion type="single" defaultValue="item-1">
|
|
22
|
+
* <AccordionItem value="item-1">
|
|
23
|
+
* <AccordionTrigger>Section heading</AccordionTrigger>
|
|
24
|
+
* <AccordionContent>Content here.</AccordionContent>
|
|
25
|
+
* </AccordionItem>
|
|
26
|
+
* </Accordion>
|
|
27
|
+
*/
|
|
28
|
+
var AccordionContext = (0, react.createContext)(null);
|
|
29
|
+
function useAccordionContext() {
|
|
30
|
+
const ctx = (0, react.useContext)(AccordionContext);
|
|
31
|
+
if (!ctx) throw new Error("AccordionItem must be used inside <Accordion>");
|
|
32
|
+
return ctx;
|
|
33
|
+
}
|
|
34
|
+
var ItemContext = (0, react.createContext)(null);
|
|
35
|
+
function useItemContext() {
|
|
36
|
+
const ctx = (0, react.useContext)(ItemContext);
|
|
37
|
+
if (!ctx) throw new Error("AccordionTrigger/AccordionContent must be inside <AccordionItem>");
|
|
38
|
+
return ctx;
|
|
39
|
+
}
|
|
40
|
+
function Accordion({ type = "single", defaultValue, value: controlledValue, onValueChange, children, className = "" }) {
|
|
41
|
+
const isControlled = controlledValue !== void 0;
|
|
42
|
+
const [uncontrolled, setUncontrolled] = (0, react.useState)(() => {
|
|
43
|
+
if (!defaultValue) return /* @__PURE__ */ new Set();
|
|
44
|
+
return new Set(Array.isArray(defaultValue) ? defaultValue : [defaultValue]);
|
|
45
|
+
});
|
|
46
|
+
const openValues = isControlled ? new Set(Array.isArray(controlledValue) ? controlledValue : [controlledValue]) : uncontrolled;
|
|
47
|
+
const toggle = (val) => {
|
|
48
|
+
let next;
|
|
49
|
+
if (type === "single") next = openValues.has(val) ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([val]);
|
|
50
|
+
else {
|
|
51
|
+
next = new Set(openValues);
|
|
52
|
+
if (next.has(val)) next.delete(val);
|
|
53
|
+
else next.add(val);
|
|
54
|
+
}
|
|
55
|
+
if (!isControlled) setUncontrolled(next);
|
|
56
|
+
if (onValueChange) onValueChange(type === "single" ? next.values().next().value ?? "" : [...next]);
|
|
57
|
+
};
|
|
58
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccordionContext.Provider, {
|
|
59
|
+
value: {
|
|
60
|
+
type,
|
|
61
|
+
openValues,
|
|
62
|
+
toggle
|
|
63
|
+
},
|
|
64
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
65
|
+
className: `divide-y divide-slate-200 rounded-xl border border-slate-200 ${className}`,
|
|
66
|
+
children
|
|
67
|
+
})
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function AccordionItem({ value, disabled = false, children, className = "" }) {
|
|
71
|
+
const baseId = (0, react.useId)().replace(/:/g, "");
|
|
72
|
+
const panelId = `${baseId}-panel`;
|
|
73
|
+
const triggerId = `${baseId}-trigger`;
|
|
74
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ItemContext.Provider, {
|
|
75
|
+
value: {
|
|
76
|
+
value,
|
|
77
|
+
disabled,
|
|
78
|
+
panelId,
|
|
79
|
+
triggerId
|
|
80
|
+
},
|
|
81
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
82
|
+
className: `${className}`,
|
|
83
|
+
children
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function AccordionTrigger({ children, className = "" }) {
|
|
88
|
+
const { openValues, toggle } = useAccordionContext();
|
|
89
|
+
const { value, disabled, panelId, triggerId } = useItemContext();
|
|
90
|
+
const isOpen = openValues.has(value);
|
|
91
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
92
|
+
id: triggerId,
|
|
93
|
+
type: "button",
|
|
94
|
+
"aria-expanded": isOpen,
|
|
95
|
+
"aria-controls": panelId,
|
|
96
|
+
"aria-disabled": disabled || void 0,
|
|
97
|
+
disabled,
|
|
98
|
+
onClick: () => toggle(value),
|
|
99
|
+
className: [
|
|
100
|
+
"flex w-full items-center justify-between px-5 py-4 text-left text-sm font-semibold",
|
|
101
|
+
"transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-blue/30",
|
|
102
|
+
disabled ? "cursor-not-allowed text-slate-300" : "text-slate-800 hover:bg-slate-50",
|
|
103
|
+
className
|
|
104
|
+
].filter(Boolean).join(" "),
|
|
105
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, {
|
|
106
|
+
size: 16,
|
|
107
|
+
"aria-hidden": "true",
|
|
108
|
+
className: `shrink-0 text-slate-400 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`
|
|
109
|
+
})]
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function AccordionContent({ children, className = "" }) {
|
|
113
|
+
const { openValues } = useAccordionContext();
|
|
114
|
+
const { value, panelId, triggerId } = useItemContext();
|
|
115
|
+
if (!openValues.has(value)) return null;
|
|
116
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
117
|
+
id: panelId,
|
|
118
|
+
role: "region",
|
|
119
|
+
"aria-labelledby": triggerId,
|
|
120
|
+
className: `border-t border-slate-100 px-5 py-4 text-sm font-medium text-slate-600 ${className}`,
|
|
121
|
+
children
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
exports.Accordion = Accordion;
|
|
126
|
+
exports.AccordionContent = AccordionContent;
|
|
127
|
+
exports.AccordionItem = AccordionItem;
|
|
128
|
+
exports.AccordionTrigger = AccordionTrigger;
|
|
129
|
+
|
|
130
|
+
//# sourceMappingURL=index55.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index55.cjs","names":[],"sources":["../src/ui-library/gallery/accordion/Accordion.tsx"],"sourcesContent":["/**\n * Accordion — Design System Component\n *\n * Disclosure/accordion pattern with support for single-open and\n * multiple-open modes, controlled/uncontrolled state, and disabled items.\n *\n * WAI-ARIA Accordion pattern:\n * - Trigger is a <button> with aria-expanded and aria-controls\n * - Content panel has a stable id referenced by aria-controls\n * - Disabled triggers have aria-disabled + disabled attribute\n *\n * Keyboard:\n * - Tab moves between triggers\n * - Enter / Space toggle the focused trigger\n *\n * Composition:\n * <Accordion type=\"single\" defaultValue=\"item-1\">\n * <AccordionItem value=\"item-1\">\n * <AccordionTrigger>Section heading</AccordionTrigger>\n * <AccordionContent>Content here.</AccordionContent>\n * </AccordionItem>\n * </Accordion>\n */\n\nimport { createContext, useContext, useId, useState, type ReactNode } from 'react';\nimport { ChevronDown } from 'lucide-react';\n\n// ── Context ───────────────────────────────────────────────────────────────────\n\ninterface AccordionContextValue {\n type: 'single' | 'multiple';\n openValues: Set<string>;\n toggle: (value: string) => void;\n}\n\nconst AccordionContext = createContext<AccordionContextValue | null>(null);\n\nfunction useAccordionContext() {\n const ctx = useContext(AccordionContext);\n if (!ctx) throw new Error('AccordionItem must be used inside <Accordion>');\n return ctx;\n}\n\ninterface ItemContextValue {\n value: string;\n disabled: boolean;\n panelId: string;\n triggerId: string;\n}\n\nconst ItemContext = createContext<ItemContextValue | null>(null);\n\nfunction useItemContext() {\n const ctx = useContext(ItemContext);\n if (!ctx) throw new Error('AccordionTrigger/AccordionContent must be inside <AccordionItem>');\n return ctx;\n}\n\n// ── Accordion (root) ──────────────────────────────────────────────────────────\n\nexport interface AccordionProps {\n /**\n * 'single' — only one item open at a time (default)\n * 'multiple' — any number of items can be open simultaneously\n */\n type?: 'single' | 'multiple';\n /** Uncontrolled: initial open value(s) */\n defaultValue?: string | string[];\n /** Controlled: open value(s) */\n value?: string | string[];\n /** Controlled: called when open values change */\n onValueChange?: (value: string | string[]) => void;\n children: ReactNode;\n className?: string;\n}\n\nexport function Accordion({\n type = 'single',\n defaultValue,\n value: controlledValue,\n onValueChange,\n children,\n className = '',\n}: AccordionProps) {\n const isControlled = controlledValue !== undefined;\n\n const [uncontrolled, setUncontrolled] = useState<Set<string>>(() => {\n if (!defaultValue) return new Set();\n return new Set(Array.isArray(defaultValue) ? defaultValue : [defaultValue]);\n });\n\n const openValues: Set<string> = isControlled\n ? new Set(Array.isArray(controlledValue) ? controlledValue : [controlledValue])\n : uncontrolled;\n\n const toggle = (val: string) => {\n let next: Set<string>;\n if (type === 'single') {\n next = openValues.has(val) ? new Set() : new Set([val]);\n } else {\n next = new Set(openValues);\n if (next.has(val)) next.delete(val); else next.add(val);\n }\n if (!isControlled) setUncontrolled(next);\n if (onValueChange) {\n onValueChange(type === 'single' ? (next.values().next().value ?? '') : [...next]);\n }\n };\n\n return (\n <AccordionContext.Provider value={{ type, openValues, toggle }}>\n <div className={`divide-y divide-slate-200 rounded-xl border border-slate-200 ${className}`}>\n {children}\n </div>\n </AccordionContext.Provider>\n );\n}\n\n// ── AccordionItem ─────────────────────────────────────────────────────────────\n\nexport interface AccordionItemProps {\n value: string;\n disabled?: boolean;\n children: ReactNode;\n className?: string;\n}\n\nexport function AccordionItem({ value, disabled = false, children, className = '' }: AccordionItemProps) {\n const baseId = useId().replace(/:/g, '');\n const panelId = `${baseId}-panel`;\n const triggerId = `${baseId}-trigger`;\n\n return (\n <ItemContext.Provider value={{ value, disabled, panelId, triggerId }}>\n <div className={`${className}`}>\n {children}\n </div>\n </ItemContext.Provider>\n );\n}\n\n// ── AccordionTrigger ──────────────────────────────────────────────────────────\n\nexport interface AccordionTriggerProps {\n children: ReactNode;\n className?: string;\n}\n\nexport function AccordionTrigger({ children, className = '' }: AccordionTriggerProps) {\n const { openValues, toggle } = useAccordionContext();\n const { value, disabled, panelId, triggerId } = useItemContext();\n const isOpen = openValues.has(value);\n\n return (\n <button\n id={triggerId}\n type=\"button\"\n aria-expanded={isOpen}\n aria-controls={panelId}\n aria-disabled={disabled || undefined}\n disabled={disabled}\n onClick={() => toggle(value)}\n className={[\n 'flex w-full items-center justify-between px-5 py-4 text-left text-sm font-semibold',\n 'transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-blue/30',\n disabled\n ? 'cursor-not-allowed text-slate-300'\n : 'text-slate-800 hover:bg-slate-50',\n className,\n ]\n .filter(Boolean)\n .join(' ')}\n >\n <span>{children}</span>\n <ChevronDown\n size={16}\n aria-hidden=\"true\"\n className={`shrink-0 text-slate-400 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}\n />\n </button>\n );\n}\n\n// ── AccordionContent ──────────────────────────────────────────────────────────\n\nexport interface AccordionContentProps {\n children: ReactNode;\n className?: string;\n}\n\nexport function AccordionContent({ children, className = '' }: AccordionContentProps) {\n const { openValues } = useAccordionContext();\n const { value, panelId, triggerId } = useItemContext();\n const isOpen = openValues.has(value);\n\n if (!isOpen) return null;\n\n return (\n <div\n id={panelId}\n role=\"region\"\n aria-labelledby={triggerId}\n className={`border-t border-slate-100 px-5 py-4 text-sm font-medium text-slate-600 ${className}`}\n >\n {children}\n </div>\n );\n}\n"],"mappings":"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"}
|