@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/index27.mjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { File, UploadCloud, X } from "lucide-react";
|
|
4
|
+
//#region src/ui-library/gallery/file-upload/FileUpload.tsx
|
|
5
|
+
/**
|
|
6
|
+
* FileUpload — Design System Component
|
|
7
|
+
*
|
|
8
|
+
* A styled file-selection trigger. Wraps a native <input type="file"> with a
|
|
9
|
+
* drag-and-drop zone. Presentation-only — no HTTP upload logic.
|
|
10
|
+
*
|
|
11
|
+
* States: idle, drag-active, selected, disabled
|
|
12
|
+
*
|
|
13
|
+
* Accessibility:
|
|
14
|
+
* - The visible zone is a <label> wrapping a visually-hidden <input>.
|
|
15
|
+
* - <input> has aria-label describing the expected file.
|
|
16
|
+
* - Keyboard: Tab to focus the input; Enter/Space to open the picker.
|
|
17
|
+
* - Disabled: input has disabled attr; zone has aria-disabled="true".
|
|
18
|
+
*/
|
|
19
|
+
function FileUpload({ onFiles, accept, multiple = false, disabled = false, "aria-label": ariaLabel = "Upload file", className = "" }) {
|
|
20
|
+
const inputRef = useRef(null);
|
|
21
|
+
const [dragging, setDragging] = useState(false);
|
|
22
|
+
const [selected, setSelected] = useState([]);
|
|
23
|
+
const handleFiles = (files) => {
|
|
24
|
+
if (!files || files.length === 0) return;
|
|
25
|
+
const arr = Array.from(files);
|
|
26
|
+
setSelected(multiple ? arr : [arr[0]]);
|
|
27
|
+
onFiles?.(multiple ? arr : [arr[0]]);
|
|
28
|
+
};
|
|
29
|
+
const handleChange = (e) => {
|
|
30
|
+
handleFiles(e.target.files);
|
|
31
|
+
e.target.value = "";
|
|
32
|
+
};
|
|
33
|
+
const handleDrop = (e) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
setDragging(false);
|
|
36
|
+
if (disabled) return;
|
|
37
|
+
handleFiles(e.dataTransfer.files);
|
|
38
|
+
};
|
|
39
|
+
const handleDragOver = (e) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
if (!disabled) setDragging(true);
|
|
42
|
+
};
|
|
43
|
+
const handleDragLeave = () => setDragging(false);
|
|
44
|
+
const removeFile = (i) => {
|
|
45
|
+
setSelected((prev) => prev.filter((_, idx) => idx !== i));
|
|
46
|
+
};
|
|
47
|
+
const zoneClass = [
|
|
48
|
+
"flex flex-col items-center justify-center gap-3",
|
|
49
|
+
"rounded-2xl border-2 border-dashed p-8 text-center cursor-pointer",
|
|
50
|
+
"transition-colors focus-within:outline-none focus-within:ring-2 focus-within:ring-brand-blue/20",
|
|
51
|
+
dragging ? "border-brand-blue/60 bg-blue-50" : disabled ? "border-slate-100 bg-slate-50/40 cursor-not-allowed" : "border-slate-200 bg-white hover:border-brand-sky/50 hover:bg-[#eef2f8]",
|
|
52
|
+
className
|
|
53
|
+
].filter(Boolean).join(" ");
|
|
54
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
55
|
+
className: "space-y-3",
|
|
56
|
+
children: [/* @__PURE__ */ jsxs("label", {
|
|
57
|
+
className: zoneClass,
|
|
58
|
+
onDrop: handleDrop,
|
|
59
|
+
onDragOver: handleDragOver,
|
|
60
|
+
onDragLeave: handleDragLeave,
|
|
61
|
+
"aria-disabled": disabled,
|
|
62
|
+
children: [
|
|
63
|
+
/* @__PURE__ */ jsx("input", {
|
|
64
|
+
ref: inputRef,
|
|
65
|
+
type: "file",
|
|
66
|
+
accept,
|
|
67
|
+
multiple,
|
|
68
|
+
disabled,
|
|
69
|
+
onChange: handleChange,
|
|
70
|
+
"aria-label": ariaLabel,
|
|
71
|
+
className: "sr-only"
|
|
72
|
+
}),
|
|
73
|
+
/* @__PURE__ */ jsx(UploadCloud, {
|
|
74
|
+
size: 32,
|
|
75
|
+
className: dragging ? "text-brand-blue" : disabled ? "text-slate-300" : "text-slate-400",
|
|
76
|
+
"aria-hidden": "true"
|
|
77
|
+
}),
|
|
78
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
|
|
79
|
+
className: `text-sm font-semibold ${disabled ? "text-slate-400" : "text-slate-700"}`,
|
|
80
|
+
children: dragging ? "Drop to upload" : "Click or drag files here"
|
|
81
|
+
}), accept && /* @__PURE__ */ jsx("p", {
|
|
82
|
+
className: "mt-0.5 text-xs font-medium text-slate-400",
|
|
83
|
+
children: accept
|
|
84
|
+
})] })
|
|
85
|
+
]
|
|
86
|
+
}), selected.length > 0 && /* @__PURE__ */ jsx("ul", {
|
|
87
|
+
className: "space-y-1.5",
|
|
88
|
+
"aria-label": "Selected files",
|
|
89
|
+
children: selected.map((file, i) => /* @__PURE__ */ jsxs("li", {
|
|
90
|
+
className: "flex items-center gap-3 rounded-xl border border-slate-200 bg-white px-3 py-2.5",
|
|
91
|
+
children: [
|
|
92
|
+
/* @__PURE__ */ jsx(File, {
|
|
93
|
+
size: 14,
|
|
94
|
+
className: "shrink-0 text-brand-sky",
|
|
95
|
+
"aria-hidden": "true"
|
|
96
|
+
}),
|
|
97
|
+
/* @__PURE__ */ jsx("span", {
|
|
98
|
+
className: "min-w-0 flex-1 truncate text-xs font-semibold text-slate-700",
|
|
99
|
+
children: file.name
|
|
100
|
+
}),
|
|
101
|
+
/* @__PURE__ */ jsxs("span", {
|
|
102
|
+
className: "shrink-0 text-[10px] font-medium text-slate-400",
|
|
103
|
+
children: [(file.size / 1024).toFixed(0), " KB"]
|
|
104
|
+
}),
|
|
105
|
+
/* @__PURE__ */ jsx("button", {
|
|
106
|
+
type: "button",
|
|
107
|
+
onClick: () => removeFile(i),
|
|
108
|
+
"aria-label": `Remove ${file.name}`,
|
|
109
|
+
className: "ml-1 shrink-0 rounded p-0.5 text-slate-400 hover:bg-rose-50 hover:text-rose-500 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-200",
|
|
110
|
+
children: /* @__PURE__ */ jsx(X, { size: 12 })
|
|
111
|
+
})
|
|
112
|
+
]
|
|
113
|
+
}, `${file.name}-${i}`))
|
|
114
|
+
})]
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
export { FileUpload };
|
|
119
|
+
|
|
120
|
+
//# sourceMappingURL=index27.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index27.mjs","names":[],"sources":["../src/ui-library/gallery/file-upload/FileUpload.tsx"],"sourcesContent":["/**\n * FileUpload — Design System Component\n *\n * A styled file-selection trigger. Wraps a native <input type=\"file\"> with a\n * drag-and-drop zone. Presentation-only — no HTTP upload logic.\n *\n * States: idle, drag-active, selected, disabled\n *\n * Accessibility:\n * - The visible zone is a <label> wrapping a visually-hidden <input>.\n * - <input> has aria-label describing the expected file.\n * - Keyboard: Tab to focus the input; Enter/Space to open the picker.\n * - Disabled: input has disabled attr; zone has aria-disabled=\"true\".\n */\n\nimport { useRef, useState, type ChangeEvent, type DragEvent } from 'react';\nimport { UploadCloud, X, File } from 'lucide-react';\n\nexport interface FileUploadProps {\n /** Called when files are selected via picker or drop. */\n onFiles?: (files: File[]) => void;\n /** Accepted MIME types / extensions, forwarded to <input accept>. */\n accept?: string;\n /** Allow selecting multiple files. Default: false */\n multiple?: boolean;\n /** Disable the upload zone. */\n disabled?: boolean;\n /** Accessible label describing the expected file type. */\n 'aria-label'?: string;\n /** Additional class on the root element. */\n className?: string;\n}\n\nexport function FileUpload({\n onFiles,\n accept,\n multiple = false,\n disabled = false,\n 'aria-label': ariaLabel = 'Upload file',\n className = '',\n}: FileUploadProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n const [dragging, setDragging] = useState(false);\n const [selected, setSelected] = useState<File[]>([]);\n\n const handleFiles = (files: FileList | null) => {\n if (!files || files.length === 0) return;\n const arr = Array.from(files);\n setSelected(multiple ? arr : [arr[0]!]);\n onFiles?.(multiple ? arr : [arr[0]!]);\n };\n\n const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n handleFiles(e.target.files);\n // Reset so same file can be re-selected\n e.target.value = '';\n };\n\n const handleDrop = (e: DragEvent<HTMLLabelElement>) => {\n e.preventDefault();\n setDragging(false);\n if (disabled) return;\n handleFiles(e.dataTransfer.files);\n };\n\n const handleDragOver = (e: DragEvent<HTMLLabelElement>) => {\n e.preventDefault();\n if (!disabled) setDragging(true);\n };\n\n const handleDragLeave = () => setDragging(false);\n\n const removeFile = (i: number) => {\n setSelected((prev) => prev.filter((_, idx) => idx !== i));\n };\n\n const zoneClass = [\n 'flex flex-col items-center justify-center gap-3',\n 'rounded-2xl border-2 border-dashed p-8 text-center cursor-pointer',\n 'transition-colors focus-within:outline-none focus-within:ring-2 focus-within:ring-brand-blue/20',\n dragging\n ? 'border-brand-blue/60 bg-blue-50'\n : disabled\n ? 'border-slate-100 bg-slate-50/40 cursor-not-allowed'\n : 'border-slate-200 bg-white hover:border-brand-sky/50 hover:bg-[#eef2f8]',\n className,\n ]\n .filter(Boolean)\n .join(' ');\n\n return (\n <div className=\"space-y-3\">\n <label\n className={zoneClass}\n onDrop={handleDrop}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n aria-disabled={disabled}\n >\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n onChange={handleChange}\n aria-label={ariaLabel}\n className=\"sr-only\"\n />\n <UploadCloud\n size={32}\n className={dragging ? 'text-brand-blue' : disabled ? 'text-slate-300' : 'text-slate-400'}\n aria-hidden=\"true\"\n />\n <div>\n <p className={`text-sm font-semibold ${disabled ? 'text-slate-400' : 'text-slate-700'}`}>\n {dragging ? 'Drop to upload' : 'Click or drag files here'}\n </p>\n {accept && (\n <p className=\"mt-0.5 text-xs font-medium text-slate-400\">{accept}</p>\n )}\n </div>\n </label>\n\n {selected.length > 0 && (\n <ul className=\"space-y-1.5\" aria-label=\"Selected files\">\n {selected.map((file, i) => (\n <li\n key={`${file.name}-${i}`}\n className=\"flex items-center gap-3 rounded-xl border border-slate-200 bg-white px-3 py-2.5\"\n >\n <File size={14} className=\"shrink-0 text-brand-sky\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate text-xs font-semibold text-slate-700\">\n {file.name}\n </span>\n <span className=\"shrink-0 text-[10px] font-medium text-slate-400\">\n {(file.size / 1024).toFixed(0)} KB\n </span>\n <button\n type=\"button\"\n onClick={() => removeFile(i)}\n aria-label={`Remove ${file.name}`}\n className=\"ml-1 shrink-0 rounded p-0.5 text-slate-400 hover:bg-rose-50 hover:text-rose-500 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-200\"\n >\n <X size={12} />\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiCA,SAAgB,WAAW,EACzB,SACA,QACA,WAAW,OACX,WAAW,OACX,cAAc,YAAY,eAC1B,YAAY,MACM;CAClB,MAAM,WAAW,OAAyB,IAAI;CAC9C,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAC9C,MAAM,CAAC,UAAU,eAAe,SAAiB,CAAC,CAAC;CAEnD,MAAM,eAAe,UAA2B;EAC9C,IAAI,CAAC,SAAS,MAAM,WAAW,GAAG;EAClC,MAAM,MAAM,MAAM,KAAK,KAAK;EAC5B,YAAY,WAAW,MAAM,CAAC,IAAI,EAAG,CAAC;EACtC,UAAU,WAAW,MAAM,CAAC,IAAI,EAAG,CAAC;CACtC;CAEA,MAAM,gBAAgB,MAAqC;EACzD,YAAY,EAAE,OAAO,KAAK;EAE1B,EAAE,OAAO,QAAQ;CACnB;CAEA,MAAM,cAAc,MAAmC;EACrD,EAAE,eAAe;EACjB,YAAY,KAAK;EACjB,IAAI,UAAU;EACd,YAAY,EAAE,aAAa,KAAK;CAClC;CAEA,MAAM,kBAAkB,MAAmC;EACzD,EAAE,eAAe;EACjB,IAAI,CAAC,UAAU,YAAY,IAAI;CACjC;CAEA,MAAM,wBAAwB,YAAY,KAAK;CAE/C,MAAM,cAAc,MAAc;EAChC,aAAa,SAAS,KAAK,QAAQ,GAAG,QAAQ,QAAQ,CAAC,CAAC;CAC1D;CAEA,MAAM,YAAY;EAChB;EACA;EACA;EACA,WACI,oCACA,WACA,uDACA;EACJ;CACF,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;CAEX,OACE,qBAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,qBAAC,SAAD;GACE,WAAW;GACX,QAAQ;GACR,YAAY;GACZ,aAAa;GACb,iBAAe;GALjB,UAAA;IAOE,oBAAC,SAAD;KACE,KAAK;KACL,MAAK;KACG;KACE;KACA;KACV,UAAU;KACV,cAAY;KACZ,WAAU;IACX,CAAA;IACD,oBAAC,aAAD;KACE,MAAM;KACN,WAAW,WAAW,oBAAoB,WAAW,mBAAmB;KACxE,eAAY;IACb,CAAA;IACD,qBAAC,OAAD,EAAA,UAAA,CACE,oBAAC,KAAD;KAAG,WAAW,yBAAyB,WAAW,mBAAmB;KAClE,UAAA,WAAW,mBAAmB;IAC9B,CAAA,GACF,UACC,oBAAC,KAAD;KAAG,WAAU;KAA6C,UAAA;IAAU,CAAA,CAEnE,EAAA,CAAA;GACA;EAEN,CAAA,GAAA,SAAS,SAAS,KACjB,oBAAC,MAAD;GAAI,WAAU;GAAc,cAAW;GACpC,UAAA,SAAS,KAAK,MAAM,MACnB,qBAAC,MAAD;IAEE,WAAU;IAFZ,UAAA;KAIE,oBAAC,MAAD;MAAM,MAAM;MAAI,WAAU;MAA0B,eAAY;KAAQ,CAAA;KACxE,oBAAC,QAAD;MAAM,WAAU;MACb,UAAA,KAAK;KACF,CAAA;KACN,qBAAC,QAAD;MAAM,WAAU;MAAhB,UAAA,EACI,KAAK,OAAO,KAAA,CAAM,QAAQ,CAAC,GAAE,KAC3B;;KACN,oBAAC,UAAD;MACE,MAAK;MACL,eAAe,WAAW,CAAC;MAC3B,cAAY,UAAU,KAAK;MAC3B,WAAU;MAEV,UAAA,oBAAC,GAAD,EAAG,MAAM,GAAK,CAAA;KACR,CAAA;IACN;GAlBG,GAAA,GAAG,KAAK,KAAK,GAAG,GAkBnB,CACL;EACC,CAAA,CAEH;;AAET"}
|
package/lib/index28.cjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
2
|
+
let lucide_react = require("lucide-react");
|
|
3
|
+
//#region src/ui-library/gallery/filter-chip/FilterChip.tsx
|
|
4
|
+
/**
|
|
5
|
+
* FilterChip — Design System Component
|
|
6
|
+
*
|
|
7
|
+
* A removable tag/chip for displaying active filters.
|
|
8
|
+
* Purely presentational — no filter state, no URL state, no search logic.
|
|
9
|
+
*
|
|
10
|
+
* Accessibility:
|
|
11
|
+
* - The remove button has an aria-label describing what will be removed.
|
|
12
|
+
* - Disabled chips prevent removal.
|
|
13
|
+
* - The chip itself is not interactive — only the remove button is.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* <FilterChip label="Department: HR" onRemove={() => removeFilter('department')} />
|
|
17
|
+
* <FilterChip label="Status: Active" onRemove={handleRemove} />
|
|
18
|
+
* <FilterChip label="Read only" /> // no remove button
|
|
19
|
+
* <FilterChip label="Locked" disabled /> // disabled state
|
|
20
|
+
*/
|
|
21
|
+
function FilterChip({ label, onRemove, disabled = false, removeLabel = "Remove filter", className = "" }) {
|
|
22
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
23
|
+
className: [
|
|
24
|
+
"inline-flex max-w-[20rem] items-center gap-1.5 rounded-full border",
|
|
25
|
+
"py-1 pl-3 text-xs font-semibold",
|
|
26
|
+
onRemove ? "pr-1.5" : "pr-3",
|
|
27
|
+
disabled ? "border-slate-200 bg-slate-50 text-slate-400" : "border-brand-blue/30 bg-blue-50 text-brand-blue",
|
|
28
|
+
className
|
|
29
|
+
].filter(Boolean).join(" "),
|
|
30
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31
|
+
className: "truncate",
|
|
32
|
+
children: label
|
|
33
|
+
}), onRemove && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
34
|
+
type: "button",
|
|
35
|
+
onClick: !disabled ? onRemove : void 0,
|
|
36
|
+
disabled,
|
|
37
|
+
"aria-label": removeLabel,
|
|
38
|
+
className: [
|
|
39
|
+
"flex h-4 w-4 shrink-0 items-center justify-center rounded-full transition",
|
|
40
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30",
|
|
41
|
+
disabled ? "cursor-not-allowed text-slate-300" : "text-brand-blue hover:bg-blue-100 hover:text-brand-navy"
|
|
42
|
+
].filter(Boolean).join(" "),
|
|
43
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, {
|
|
44
|
+
size: 10,
|
|
45
|
+
"aria-hidden": "true",
|
|
46
|
+
strokeWidth: 3
|
|
47
|
+
})
|
|
48
|
+
})]
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
exports.FilterChip = FilterChip;
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=index28.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index28.cjs","names":[],"sources":["../src/ui-library/gallery/filter-chip/FilterChip.tsx"],"sourcesContent":["/**\n * FilterChip — Design System Component\n *\n * A removable tag/chip for displaying active filters.\n * Purely presentational — no filter state, no URL state, no search logic.\n *\n * Accessibility:\n * - The remove button has an aria-label describing what will be removed.\n * - Disabled chips prevent removal.\n * - The chip itself is not interactive — only the remove button is.\n *\n * Usage:\n * <FilterChip label=\"Department: HR\" onRemove={() => removeFilter('department')} />\n * <FilterChip label=\"Status: Active\" onRemove={handleRemove} />\n * <FilterChip label=\"Read only\" /> // no remove button\n * <FilterChip label=\"Locked\" disabled /> // disabled state\n */\n\nimport { X } from 'lucide-react';\nimport type { ReactNode } from 'react';\n\nexport interface FilterChipProps {\n /** Label content — string or ReactNode */\n label: ReactNode;\n /** When provided, renders a remove (×) button */\n onRemove?: () => void;\n /** Disables the remove button */\n disabled?: boolean;\n /** Optional accessible label for the remove button (defaults to \"Remove filter\") */\n removeLabel?: string;\n className?: string;\n}\n\nexport function FilterChip({\n label,\n onRemove,\n disabled = false,\n removeLabel = 'Remove filter',\n className = '',\n}: FilterChipProps) {\n return (\n <span\n className={[\n 'inline-flex max-w-[20rem] items-center gap-1.5 rounded-full border',\n 'py-1 pl-3 text-xs font-semibold',\n onRemove ? 'pr-1.5' : 'pr-3',\n disabled\n ? 'border-slate-200 bg-slate-50 text-slate-400'\n : 'border-brand-blue/30 bg-blue-50 text-brand-blue',\n className,\n ]\n .filter(Boolean)\n .join(' ')}\n >\n {/* Label — truncate long text */}\n <span className=\"truncate\">{label}</span>\n\n {/* Remove button */}\n {onRemove && (\n <button\n type=\"button\"\n onClick={!disabled ? onRemove : undefined}\n disabled={disabled}\n aria-label={removeLabel}\n className={[\n 'flex h-4 w-4 shrink-0 items-center justify-center rounded-full transition',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30',\n disabled\n ? 'cursor-not-allowed text-slate-300'\n : 'text-brand-blue hover:bg-blue-100 hover:text-brand-navy',\n ]\n .filter(Boolean)\n .join(' ')}\n >\n <X size={10} aria-hidden=\"true\" strokeWidth={3} />\n </button>\n )}\n </span>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,WAAW,EACzB,OACA,UACA,WAAW,OACX,cAAc,iBACd,YAAY,MACM;CAClB,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;EACE,WAAW;GACT;GACA;GACA,WAAW,WAAW;GACtB,WACI,gDACA;GACJ;EACF,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;EAXb,UAAA,CAcE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;GAAM,WAAU;GAAY,UAAA;EAAY,CAAA,GAGvC,YACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;GACE,MAAK;GACL,SAAS,CAAC,WAAW,WAAW,KAAA;GACtB;GACV,cAAY;GACZ,WAAW;IACT;IACA;IACA,WACI,sCACA;GACN,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;GAEX,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,aAAA,GAAD;IAAG,MAAM;IAAI,eAAY;IAAO,aAAa;GAAI,CAAA;EAC3C,CAAA,CAEN;;AAEV"}
|
package/lib/index28.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { X } from "lucide-react";
|
|
3
|
+
//#region src/ui-library/gallery/filter-chip/FilterChip.tsx
|
|
4
|
+
/**
|
|
5
|
+
* FilterChip — Design System Component
|
|
6
|
+
*
|
|
7
|
+
* A removable tag/chip for displaying active filters.
|
|
8
|
+
* Purely presentational — no filter state, no URL state, no search logic.
|
|
9
|
+
*
|
|
10
|
+
* Accessibility:
|
|
11
|
+
* - The remove button has an aria-label describing what will be removed.
|
|
12
|
+
* - Disabled chips prevent removal.
|
|
13
|
+
* - The chip itself is not interactive — only the remove button is.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* <FilterChip label="Department: HR" onRemove={() => removeFilter('department')} />
|
|
17
|
+
* <FilterChip label="Status: Active" onRemove={handleRemove} />
|
|
18
|
+
* <FilterChip label="Read only" /> // no remove button
|
|
19
|
+
* <FilterChip label="Locked" disabled /> // disabled state
|
|
20
|
+
*/
|
|
21
|
+
function FilterChip({ label, onRemove, disabled = false, removeLabel = "Remove filter", className = "" }) {
|
|
22
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
23
|
+
className: [
|
|
24
|
+
"inline-flex max-w-[20rem] items-center gap-1.5 rounded-full border",
|
|
25
|
+
"py-1 pl-3 text-xs font-semibold",
|
|
26
|
+
onRemove ? "pr-1.5" : "pr-3",
|
|
27
|
+
disabled ? "border-slate-200 bg-slate-50 text-slate-400" : "border-brand-blue/30 bg-blue-50 text-brand-blue",
|
|
28
|
+
className
|
|
29
|
+
].filter(Boolean).join(" "),
|
|
30
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
31
|
+
className: "truncate",
|
|
32
|
+
children: label
|
|
33
|
+
}), onRemove && /* @__PURE__ */ jsx("button", {
|
|
34
|
+
type: "button",
|
|
35
|
+
onClick: !disabled ? onRemove : void 0,
|
|
36
|
+
disabled,
|
|
37
|
+
"aria-label": removeLabel,
|
|
38
|
+
className: [
|
|
39
|
+
"flex h-4 w-4 shrink-0 items-center justify-center rounded-full transition",
|
|
40
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30",
|
|
41
|
+
disabled ? "cursor-not-allowed text-slate-300" : "text-brand-blue hover:bg-blue-100 hover:text-brand-navy"
|
|
42
|
+
].filter(Boolean).join(" "),
|
|
43
|
+
children: /* @__PURE__ */ jsx(X, {
|
|
44
|
+
size: 10,
|
|
45
|
+
"aria-hidden": "true",
|
|
46
|
+
strokeWidth: 3
|
|
47
|
+
})
|
|
48
|
+
})]
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { FilterChip };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=index28.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index28.mjs","names":[],"sources":["../src/ui-library/gallery/filter-chip/FilterChip.tsx"],"sourcesContent":["/**\n * FilterChip — Design System Component\n *\n * A removable tag/chip for displaying active filters.\n * Purely presentational — no filter state, no URL state, no search logic.\n *\n * Accessibility:\n * - The remove button has an aria-label describing what will be removed.\n * - Disabled chips prevent removal.\n * - The chip itself is not interactive — only the remove button is.\n *\n * Usage:\n * <FilterChip label=\"Department: HR\" onRemove={() => removeFilter('department')} />\n * <FilterChip label=\"Status: Active\" onRemove={handleRemove} />\n * <FilterChip label=\"Read only\" /> // no remove button\n * <FilterChip label=\"Locked\" disabled /> // disabled state\n */\n\nimport { X } from 'lucide-react';\nimport type { ReactNode } from 'react';\n\nexport interface FilterChipProps {\n /** Label content — string or ReactNode */\n label: ReactNode;\n /** When provided, renders a remove (×) button */\n onRemove?: () => void;\n /** Disables the remove button */\n disabled?: boolean;\n /** Optional accessible label for the remove button (defaults to \"Remove filter\") */\n removeLabel?: string;\n className?: string;\n}\n\nexport function FilterChip({\n label,\n onRemove,\n disabled = false,\n removeLabel = 'Remove filter',\n className = '',\n}: FilterChipProps) {\n return (\n <span\n className={[\n 'inline-flex max-w-[20rem] items-center gap-1.5 rounded-full border',\n 'py-1 pl-3 text-xs font-semibold',\n onRemove ? 'pr-1.5' : 'pr-3',\n disabled\n ? 'border-slate-200 bg-slate-50 text-slate-400'\n : 'border-brand-blue/30 bg-blue-50 text-brand-blue',\n className,\n ]\n .filter(Boolean)\n .join(' ')}\n >\n {/* Label — truncate long text */}\n <span className=\"truncate\">{label}</span>\n\n {/* Remove button */}\n {onRemove && (\n <button\n type=\"button\"\n onClick={!disabled ? onRemove : undefined}\n disabled={disabled}\n aria-label={removeLabel}\n className={[\n 'flex h-4 w-4 shrink-0 items-center justify-center rounded-full transition',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30',\n disabled\n ? 'cursor-not-allowed text-slate-300'\n : 'text-brand-blue hover:bg-blue-100 hover:text-brand-navy',\n ]\n .filter(Boolean)\n .join(' ')}\n >\n <X size={10} aria-hidden=\"true\" strokeWidth={3} />\n </button>\n )}\n </span>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,WAAW,EACzB,OACA,UACA,WAAW,OACX,cAAc,iBACd,YAAY,MACM;CAClB,OACE,qBAAC,QAAD;EACE,WAAW;GACT;GACA;GACA,WAAW,WAAW;GACtB,WACI,gDACA;GACJ;EACF,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;EAXb,UAAA,CAcE,oBAAC,QAAD;GAAM,WAAU;GAAY,UAAA;EAAY,CAAA,GAGvC,YACC,oBAAC,UAAD;GACE,MAAK;GACL,SAAS,CAAC,WAAW,WAAW,KAAA;GACtB;GACV,cAAY;GACZ,WAAW;IACT;IACA;IACA,WACI,sCACA;GACN,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;GAEX,UAAA,oBAAC,GAAD;IAAG,MAAM;IAAI,eAAY;IAAO,aAAa;GAAI,CAAA;EAC3C,CAAA,CAEN;;AAEV"}
|
package/lib/index29.cjs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
2
|
+
let react = require("react");
|
|
3
|
+
//#region src/ui-library/gallery/segmented-control/SegmentedControl.tsx
|
|
4
|
+
/**
|
|
5
|
+
* SegmentedControl — Design System Component
|
|
6
|
+
*
|
|
7
|
+
* An inline pill-shaped control for switching between mutually exclusive
|
|
8
|
+
* options. Semantically equivalent to a group of radio buttons but styled
|
|
9
|
+
* as a connected pill strip.
|
|
10
|
+
*
|
|
11
|
+
* ARIA: uses role="radiogroup" on the container and role="radio" on each
|
|
12
|
+
* option, matching the WAI-ARIA authoring guide for mutually exclusive
|
|
13
|
+
* single-selection controls that are not tab panels.
|
|
14
|
+
*
|
|
15
|
+
* Keyboard: Arrow keys move selection within the group (roving tabIndex).
|
|
16
|
+
* This matches the native radio group keyboard pattern.
|
|
17
|
+
*
|
|
18
|
+
* API:
|
|
19
|
+
* <SegmentedControl
|
|
20
|
+
* options={[{ value: 'a', label: 'Option A' }, ...]}
|
|
21
|
+
* defaultValue="a" // uncontrolled
|
|
22
|
+
* value={value} // controlled
|
|
23
|
+
* onValueChange={setValue} // controlled callback
|
|
24
|
+
* aria-label="View mode"
|
|
25
|
+
* />
|
|
26
|
+
*/
|
|
27
|
+
function SegmentedControl({ options, defaultValue, value: controlledValue, onValueChange, disabled = false, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, className = "" }) {
|
|
28
|
+
const isControlled = controlledValue !== void 0;
|
|
29
|
+
const [uncontrolledValue, setUncontrolledValue] = (0, react.useState)(defaultValue ?? options.find((o) => !o.disabled)?.value ?? "");
|
|
30
|
+
const groupId = (0, react.useId)();
|
|
31
|
+
const activeValue = isControlled ? controlledValue : uncontrolledValue;
|
|
32
|
+
const select = (val) => {
|
|
33
|
+
if (disabled) return;
|
|
34
|
+
if (!isControlled) setUncontrolledValue(val);
|
|
35
|
+
onValueChange?.(val);
|
|
36
|
+
};
|
|
37
|
+
const enabledOptions = options.filter((o) => !o.disabled);
|
|
38
|
+
const handleKeyDown = (e) => {
|
|
39
|
+
if (disabled) return;
|
|
40
|
+
const currentIdx = enabledOptions.findIndex((o) => o.value === activeValue);
|
|
41
|
+
let nextIdx = null;
|
|
42
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
nextIdx = (currentIdx + 1) % enabledOptions.length;
|
|
45
|
+
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
nextIdx = (currentIdx - 1 + enabledOptions.length) % enabledOptions.length;
|
|
48
|
+
} else if (e.key === "Home") {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
nextIdx = 0;
|
|
51
|
+
} else if (e.key === "End") {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
nextIdx = enabledOptions.length - 1;
|
|
54
|
+
}
|
|
55
|
+
if (nextIdx !== null) {
|
|
56
|
+
const nextValue = enabledOptions[nextIdx]?.value;
|
|
57
|
+
if (nextValue !== void 0) {
|
|
58
|
+
select(nextValue);
|
|
59
|
+
const id = `${groupId}-${nextValue}`;
|
|
60
|
+
document.getElementById(id)?.focus();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
65
|
+
role: "radiogroup",
|
|
66
|
+
"aria-label": ariaLabel,
|
|
67
|
+
"aria-labelledby": ariaLabelledby,
|
|
68
|
+
"aria-disabled": disabled || void 0,
|
|
69
|
+
onKeyDown: handleKeyDown,
|
|
70
|
+
className: [
|
|
71
|
+
"inline-flex rounded-xl bg-slate-100 p-1",
|
|
72
|
+
disabled ? "opacity-50 pointer-events-none" : "",
|
|
73
|
+
className
|
|
74
|
+
].filter(Boolean).join(" "),
|
|
75
|
+
children: options.map((opt) => {
|
|
76
|
+
const isSelected = opt.value === activeValue;
|
|
77
|
+
const isDisabled = disabled || opt.disabled;
|
|
78
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
79
|
+
id: `${groupId}-${opt.value}`,
|
|
80
|
+
type: "button",
|
|
81
|
+
role: "radio",
|
|
82
|
+
"aria-checked": isSelected,
|
|
83
|
+
"aria-disabled": isDisabled || void 0,
|
|
84
|
+
disabled: isDisabled,
|
|
85
|
+
tabIndex: isSelected ? 0 : -1,
|
|
86
|
+
onClick: () => !isDisabled && select(opt.value),
|
|
87
|
+
className: [
|
|
88
|
+
"rounded-lg px-3 py-1.5 text-xs font-bold transition-all duration-150",
|
|
89
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30 focus-visible:ring-offset-1",
|
|
90
|
+
isSelected ? "bg-white text-slate-900 shadow-sm" : isDisabled ? "text-slate-300 cursor-not-allowed" : "text-slate-500 hover:text-slate-700"
|
|
91
|
+
].filter(Boolean).join(" "),
|
|
92
|
+
children: opt.label
|
|
93
|
+
}, opt.value);
|
|
94
|
+
})
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
exports.SegmentedControl = SegmentedControl;
|
|
99
|
+
|
|
100
|
+
//# sourceMappingURL=index29.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index29.cjs","names":[],"sources":["../src/ui-library/gallery/segmented-control/SegmentedControl.tsx"],"sourcesContent":["/**\n * SegmentedControl — Design System Component\n *\n * An inline pill-shaped control for switching between mutually exclusive\n * options. Semantically equivalent to a group of radio buttons but styled\n * as a connected pill strip.\n *\n * ARIA: uses role=\"radiogroup\" on the container and role=\"radio\" on each\n * option, matching the WAI-ARIA authoring guide for mutually exclusive\n * single-selection controls that are not tab panels.\n *\n * Keyboard: Arrow keys move selection within the group (roving tabIndex).\n * This matches the native radio group keyboard pattern.\n *\n * API:\n * <SegmentedControl\n * options={[{ value: 'a', label: 'Option A' }, ...]}\n * defaultValue=\"a\" // uncontrolled\n * value={value} // controlled\n * onValueChange={setValue} // controlled callback\n * aria-label=\"View mode\"\n * />\n */\n\nimport { useId, useState, type KeyboardEvent } from 'react';\n\nexport interface SegmentedControlOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface SegmentedControlProps {\n options: SegmentedControlOption[];\n /** Uncontrolled: initial selected value */\n defaultValue?: string;\n /** Controlled: current selected value */\n value?: string;\n /** Controlled: called on value change */\n onValueChange?: (value: string) => void;\n /** Disables the entire control */\n disabled?: boolean;\n /** Accessible label for the group */\n 'aria-label'?: string;\n /** Associate with an external label element */\n 'aria-labelledby'?: string;\n className?: string;\n}\n\nexport function SegmentedControl({\n options,\n defaultValue,\n value: controlledValue,\n onValueChange,\n disabled = false,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n className = '',\n}: SegmentedControlProps) {\n const isControlled = controlledValue !== undefined;\n const [uncontrolledValue, setUncontrolledValue] = useState(\n defaultValue ?? options.find((o) => !o.disabled)?.value ?? '',\n );\n const groupId = useId();\n\n const activeValue = isControlled ? controlledValue : uncontrolledValue;\n\n const select = (val: string) => {\n if (disabled) return;\n if (!isControlled) setUncontrolledValue(val);\n onValueChange?.(val);\n };\n\n const enabledOptions = options.filter((o) => !o.disabled);\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (disabled) return;\n const currentIdx = enabledOptions.findIndex((o) => o.value === activeValue);\n\n let nextIdx: number | null = null;\n if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {\n e.preventDefault();\n nextIdx = (currentIdx + 1) % enabledOptions.length;\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {\n e.preventDefault();\n nextIdx = (currentIdx - 1 + enabledOptions.length) % enabledOptions.length;\n } else if (e.key === 'Home') {\n e.preventDefault();\n nextIdx = 0;\n } else if (e.key === 'End') {\n e.preventDefault();\n nextIdx = enabledOptions.length - 1;\n }\n\n if (nextIdx !== null) {\n const nextValue = enabledOptions[nextIdx]?.value;\n if (nextValue !== undefined) {\n select(nextValue);\n // Move DOM focus to the newly-selected button\n const id = `${groupId}-${nextValue}`;\n document.getElementById(id)?.focus();\n }\n }\n };\n\n return (\n <div\n role=\"radiogroup\"\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n aria-disabled={disabled || undefined}\n onKeyDown={handleKeyDown}\n className={[\n 'inline-flex rounded-xl bg-slate-100 p-1',\n disabled ? 'opacity-50 pointer-events-none' : '',\n className,\n ]\n .filter(Boolean)\n .join(' ')}\n >\n {options.map((opt) => {\n const isSelected = opt.value === activeValue;\n const isDisabled = disabled || opt.disabled;\n\n return (\n <button\n key={opt.value}\n id={`${groupId}-${opt.value}`}\n type=\"button\"\n role=\"radio\"\n aria-checked={isSelected}\n aria-disabled={isDisabled || undefined}\n disabled={isDisabled}\n tabIndex={isSelected ? 0 : -1}\n onClick={() => !isDisabled && select(opt.value)}\n className={[\n 'rounded-lg px-3 py-1.5 text-xs font-bold transition-all duration-150',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30 focus-visible:ring-offset-1',\n isSelected\n ? 'bg-white text-slate-900 shadow-sm'\n : isDisabled\n ? 'text-slate-300 cursor-not-allowed'\n : 'text-slate-500 hover:text-slate-700',\n ]\n .filter(Boolean)\n .join(' ')}\n >\n {opt.label}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,SAAgB,iBAAiB,EAC/B,SACA,cACA,OAAO,iBACP,eACA,WAAW,OACX,cAAc,WACd,mBAAmB,gBACnB,YAAY,MACY;CACxB,MAAM,eAAe,oBAAoB,KAAA;CACzC,MAAM,CAAC,mBAAmB,yBAAA,GAAwB,MAAA,SAAA,CAChD,gBAAgB,QAAQ,MAAM,MAAM,CAAC,EAAE,QAAQ,CAAC,EAAE,SAAS,EAC7D;CACA,MAAM,WAAA,GAAU,MAAA,MAAA,CAAM;CAEtB,MAAM,cAAc,eAAe,kBAAkB;CAErD,MAAM,UAAU,QAAgB;EAC9B,IAAI,UAAU;EACd,IAAI,CAAC,cAAc,qBAAqB,GAAG;EAC3C,gBAAgB,GAAG;CACrB;CAEA,MAAM,iBAAiB,QAAQ,QAAQ,MAAM,CAAC,EAAE,QAAQ;CAExD,MAAM,iBAAiB,MAAqC;EAC1D,IAAI,UAAU;EACd,MAAM,aAAa,eAAe,WAAW,MAAM,EAAE,UAAU,WAAW;EAE1E,IAAI,UAAyB;EAC7B,IAAI,EAAE,QAAQ,gBAAgB,EAAE,QAAQ,aAAa;GACnD,EAAE,eAAe;GACjB,WAAW,aAAa,KAAK,eAAe;EAC9C,OAAO,IAAI,EAAE,QAAQ,eAAe,EAAE,QAAQ,WAAW;GACvD,EAAE,eAAe;GACjB,WAAW,aAAa,IAAI,eAAe,UAAU,eAAe;EACtE,OAAO,IAAI,EAAE,QAAQ,QAAQ;GAC3B,EAAE,eAAe;GACjB,UAAU;EACZ,OAAO,IAAI,EAAE,QAAQ,OAAO;GAC1B,EAAE,eAAe;GACjB,UAAU,eAAe,SAAS;EACpC;EAEA,IAAI,YAAY,MAAM;GACpB,MAAM,YAAY,eAAe,QAAQ,EAAE;GAC3C,IAAI,cAAc,KAAA,GAAW;IAC3B,OAAO,SAAS;IAEhB,MAAM,KAAK,GAAG,QAAQ,GAAG;IACzB,SAAS,eAAe,EAAE,CAAC,EAAE,MAAM;GACrC;EACF;CACF;CAEA,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;EACE,MAAK;EACL,cAAY;EACZ,mBAAiB;EACjB,iBAAe,YAAY,KAAA;EAC3B,WAAW;EACX,WAAW;GACT;GACA,WAAW,mCAAmC;GAC9C;EACF,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;EAEV,UAAA,QAAQ,KAAK,QAAQ;GACpB,MAAM,aAAa,IAAI,UAAU;GACjC,MAAM,aAAa,YAAY,IAAI;GAEnC,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;IAEE,IAAI,GAAG,QAAQ,GAAG,IAAI;IACtB,MAAK;IACL,MAAK;IACL,gBAAc;IACd,iBAAe,cAAc,KAAA;IAC7B,UAAU;IACV,UAAU,aAAa,IAAI;IAC3B,eAAe,CAAC,cAAc,OAAO,IAAI,KAAK;IAC9C,WAAW;KACT;KACA;KACA,aACI,sCACA,aACE,sCACA;IACR,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;IAEV,UAAA,IAAI;GACC,GAtBD,IAAI,KAsBH;EAEZ,CAAC;CACE,CAAA;AAET"}
|
package/lib/index29.mjs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useId, useState } from "react";
|
|
3
|
+
//#region src/ui-library/gallery/segmented-control/SegmentedControl.tsx
|
|
4
|
+
/**
|
|
5
|
+
* SegmentedControl — Design System Component
|
|
6
|
+
*
|
|
7
|
+
* An inline pill-shaped control for switching between mutually exclusive
|
|
8
|
+
* options. Semantically equivalent to a group of radio buttons but styled
|
|
9
|
+
* as a connected pill strip.
|
|
10
|
+
*
|
|
11
|
+
* ARIA: uses role="radiogroup" on the container and role="radio" on each
|
|
12
|
+
* option, matching the WAI-ARIA authoring guide for mutually exclusive
|
|
13
|
+
* single-selection controls that are not tab panels.
|
|
14
|
+
*
|
|
15
|
+
* Keyboard: Arrow keys move selection within the group (roving tabIndex).
|
|
16
|
+
* This matches the native radio group keyboard pattern.
|
|
17
|
+
*
|
|
18
|
+
* API:
|
|
19
|
+
* <SegmentedControl
|
|
20
|
+
* options={[{ value: 'a', label: 'Option A' }, ...]}
|
|
21
|
+
* defaultValue="a" // uncontrolled
|
|
22
|
+
* value={value} // controlled
|
|
23
|
+
* onValueChange={setValue} // controlled callback
|
|
24
|
+
* aria-label="View mode"
|
|
25
|
+
* />
|
|
26
|
+
*/
|
|
27
|
+
function SegmentedControl({ options, defaultValue, value: controlledValue, onValueChange, disabled = false, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, className = "" }) {
|
|
28
|
+
const isControlled = controlledValue !== void 0;
|
|
29
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? options.find((o) => !o.disabled)?.value ?? "");
|
|
30
|
+
const groupId = useId();
|
|
31
|
+
const activeValue = isControlled ? controlledValue : uncontrolledValue;
|
|
32
|
+
const select = (val) => {
|
|
33
|
+
if (disabled) return;
|
|
34
|
+
if (!isControlled) setUncontrolledValue(val);
|
|
35
|
+
onValueChange?.(val);
|
|
36
|
+
};
|
|
37
|
+
const enabledOptions = options.filter((o) => !o.disabled);
|
|
38
|
+
const handleKeyDown = (e) => {
|
|
39
|
+
if (disabled) return;
|
|
40
|
+
const currentIdx = enabledOptions.findIndex((o) => o.value === activeValue);
|
|
41
|
+
let nextIdx = null;
|
|
42
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
nextIdx = (currentIdx + 1) % enabledOptions.length;
|
|
45
|
+
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
nextIdx = (currentIdx - 1 + enabledOptions.length) % enabledOptions.length;
|
|
48
|
+
} else if (e.key === "Home") {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
nextIdx = 0;
|
|
51
|
+
} else if (e.key === "End") {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
nextIdx = enabledOptions.length - 1;
|
|
54
|
+
}
|
|
55
|
+
if (nextIdx !== null) {
|
|
56
|
+
const nextValue = enabledOptions[nextIdx]?.value;
|
|
57
|
+
if (nextValue !== void 0) {
|
|
58
|
+
select(nextValue);
|
|
59
|
+
const id = `${groupId}-${nextValue}`;
|
|
60
|
+
document.getElementById(id)?.focus();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return /* @__PURE__ */ jsx("div", {
|
|
65
|
+
role: "radiogroup",
|
|
66
|
+
"aria-label": ariaLabel,
|
|
67
|
+
"aria-labelledby": ariaLabelledby,
|
|
68
|
+
"aria-disabled": disabled || void 0,
|
|
69
|
+
onKeyDown: handleKeyDown,
|
|
70
|
+
className: [
|
|
71
|
+
"inline-flex rounded-xl bg-slate-100 p-1",
|
|
72
|
+
disabled ? "opacity-50 pointer-events-none" : "",
|
|
73
|
+
className
|
|
74
|
+
].filter(Boolean).join(" "),
|
|
75
|
+
children: options.map((opt) => {
|
|
76
|
+
const isSelected = opt.value === activeValue;
|
|
77
|
+
const isDisabled = disabled || opt.disabled;
|
|
78
|
+
return /* @__PURE__ */ jsx("button", {
|
|
79
|
+
id: `${groupId}-${opt.value}`,
|
|
80
|
+
type: "button",
|
|
81
|
+
role: "radio",
|
|
82
|
+
"aria-checked": isSelected,
|
|
83
|
+
"aria-disabled": isDisabled || void 0,
|
|
84
|
+
disabled: isDisabled,
|
|
85
|
+
tabIndex: isSelected ? 0 : -1,
|
|
86
|
+
onClick: () => !isDisabled && select(opt.value),
|
|
87
|
+
className: [
|
|
88
|
+
"rounded-lg px-3 py-1.5 text-xs font-bold transition-all duration-150",
|
|
89
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30 focus-visible:ring-offset-1",
|
|
90
|
+
isSelected ? "bg-white text-slate-900 shadow-sm" : isDisabled ? "text-slate-300 cursor-not-allowed" : "text-slate-500 hover:text-slate-700"
|
|
91
|
+
].filter(Boolean).join(" "),
|
|
92
|
+
children: opt.label
|
|
93
|
+
}, opt.value);
|
|
94
|
+
})
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
export { SegmentedControl };
|
|
99
|
+
|
|
100
|
+
//# sourceMappingURL=index29.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index29.mjs","names":[],"sources":["../src/ui-library/gallery/segmented-control/SegmentedControl.tsx"],"sourcesContent":["/**\n * SegmentedControl — Design System Component\n *\n * An inline pill-shaped control for switching between mutually exclusive\n * options. Semantically equivalent to a group of radio buttons but styled\n * as a connected pill strip.\n *\n * ARIA: uses role=\"radiogroup\" on the container and role=\"radio\" on each\n * option, matching the WAI-ARIA authoring guide for mutually exclusive\n * single-selection controls that are not tab panels.\n *\n * Keyboard: Arrow keys move selection within the group (roving tabIndex).\n * This matches the native radio group keyboard pattern.\n *\n * API:\n * <SegmentedControl\n * options={[{ value: 'a', label: 'Option A' }, ...]}\n * defaultValue=\"a\" // uncontrolled\n * value={value} // controlled\n * onValueChange={setValue} // controlled callback\n * aria-label=\"View mode\"\n * />\n */\n\nimport { useId, useState, type KeyboardEvent } from 'react';\n\nexport interface SegmentedControlOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface SegmentedControlProps {\n options: SegmentedControlOption[];\n /** Uncontrolled: initial selected value */\n defaultValue?: string;\n /** Controlled: current selected value */\n value?: string;\n /** Controlled: called on value change */\n onValueChange?: (value: string) => void;\n /** Disables the entire control */\n disabled?: boolean;\n /** Accessible label for the group */\n 'aria-label'?: string;\n /** Associate with an external label element */\n 'aria-labelledby'?: string;\n className?: string;\n}\n\nexport function SegmentedControl({\n options,\n defaultValue,\n value: controlledValue,\n onValueChange,\n disabled = false,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n className = '',\n}: SegmentedControlProps) {\n const isControlled = controlledValue !== undefined;\n const [uncontrolledValue, setUncontrolledValue] = useState(\n defaultValue ?? options.find((o) => !o.disabled)?.value ?? '',\n );\n const groupId = useId();\n\n const activeValue = isControlled ? controlledValue : uncontrolledValue;\n\n const select = (val: string) => {\n if (disabled) return;\n if (!isControlled) setUncontrolledValue(val);\n onValueChange?.(val);\n };\n\n const enabledOptions = options.filter((o) => !o.disabled);\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (disabled) return;\n const currentIdx = enabledOptions.findIndex((o) => o.value === activeValue);\n\n let nextIdx: number | null = null;\n if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {\n e.preventDefault();\n nextIdx = (currentIdx + 1) % enabledOptions.length;\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {\n e.preventDefault();\n nextIdx = (currentIdx - 1 + enabledOptions.length) % enabledOptions.length;\n } else if (e.key === 'Home') {\n e.preventDefault();\n nextIdx = 0;\n } else if (e.key === 'End') {\n e.preventDefault();\n nextIdx = enabledOptions.length - 1;\n }\n\n if (nextIdx !== null) {\n const nextValue = enabledOptions[nextIdx]?.value;\n if (nextValue !== undefined) {\n select(nextValue);\n // Move DOM focus to the newly-selected button\n const id = `${groupId}-${nextValue}`;\n document.getElementById(id)?.focus();\n }\n }\n };\n\n return (\n <div\n role=\"radiogroup\"\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n aria-disabled={disabled || undefined}\n onKeyDown={handleKeyDown}\n className={[\n 'inline-flex rounded-xl bg-slate-100 p-1',\n disabled ? 'opacity-50 pointer-events-none' : '',\n className,\n ]\n .filter(Boolean)\n .join(' ')}\n >\n {options.map((opt) => {\n const isSelected = opt.value === activeValue;\n const isDisabled = disabled || opt.disabled;\n\n return (\n <button\n key={opt.value}\n id={`${groupId}-${opt.value}`}\n type=\"button\"\n role=\"radio\"\n aria-checked={isSelected}\n aria-disabled={isDisabled || undefined}\n disabled={isDisabled}\n tabIndex={isSelected ? 0 : -1}\n onClick={() => !isDisabled && select(opt.value)}\n className={[\n 'rounded-lg px-3 py-1.5 text-xs font-bold transition-all duration-150',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30 focus-visible:ring-offset-1',\n isSelected\n ? 'bg-white text-slate-900 shadow-sm'\n : isDisabled\n ? 'text-slate-300 cursor-not-allowed'\n : 'text-slate-500 hover:text-slate-700',\n ]\n .filter(Boolean)\n .join(' ')}\n >\n {opt.label}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,SAAgB,iBAAiB,EAC/B,SACA,cACA,OAAO,iBACP,eACA,WAAW,OACX,cAAc,WACd,mBAAmB,gBACnB,YAAY,MACY;CACxB,MAAM,eAAe,oBAAoB,KAAA;CACzC,MAAM,CAAC,mBAAmB,wBAAwB,SAChD,gBAAgB,QAAQ,MAAM,MAAM,CAAC,EAAE,QAAQ,CAAC,EAAE,SAAS,EAC7D;CACA,MAAM,UAAU,MAAM;CAEtB,MAAM,cAAc,eAAe,kBAAkB;CAErD,MAAM,UAAU,QAAgB;EAC9B,IAAI,UAAU;EACd,IAAI,CAAC,cAAc,qBAAqB,GAAG;EAC3C,gBAAgB,GAAG;CACrB;CAEA,MAAM,iBAAiB,QAAQ,QAAQ,MAAM,CAAC,EAAE,QAAQ;CAExD,MAAM,iBAAiB,MAAqC;EAC1D,IAAI,UAAU;EACd,MAAM,aAAa,eAAe,WAAW,MAAM,EAAE,UAAU,WAAW;EAE1E,IAAI,UAAyB;EAC7B,IAAI,EAAE,QAAQ,gBAAgB,EAAE,QAAQ,aAAa;GACnD,EAAE,eAAe;GACjB,WAAW,aAAa,KAAK,eAAe;EAC9C,OAAO,IAAI,EAAE,QAAQ,eAAe,EAAE,QAAQ,WAAW;GACvD,EAAE,eAAe;GACjB,WAAW,aAAa,IAAI,eAAe,UAAU,eAAe;EACtE,OAAO,IAAI,EAAE,QAAQ,QAAQ;GAC3B,EAAE,eAAe;GACjB,UAAU;EACZ,OAAO,IAAI,EAAE,QAAQ,OAAO;GAC1B,EAAE,eAAe;GACjB,UAAU,eAAe,SAAS;EACpC;EAEA,IAAI,YAAY,MAAM;GACpB,MAAM,YAAY,eAAe,QAAQ,EAAE;GAC3C,IAAI,cAAc,KAAA,GAAW;IAC3B,OAAO,SAAS;IAEhB,MAAM,KAAK,GAAG,QAAQ,GAAG;IACzB,SAAS,eAAe,EAAE,CAAC,EAAE,MAAM;GACrC;EACF;CACF;CAEA,OACE,oBAAC,OAAD;EACE,MAAK;EACL,cAAY;EACZ,mBAAiB;EACjB,iBAAe,YAAY,KAAA;EAC3B,WAAW;EACX,WAAW;GACT;GACA,WAAW,mCAAmC;GAC9C;EACF,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;EAEV,UAAA,QAAQ,KAAK,QAAQ;GACpB,MAAM,aAAa,IAAI,UAAU;GACjC,MAAM,aAAa,YAAY,IAAI;GAEnC,OACE,oBAAC,UAAD;IAEE,IAAI,GAAG,QAAQ,GAAG,IAAI;IACtB,MAAK;IACL,MAAK;IACL,gBAAc;IACd,iBAAe,cAAc,KAAA;IAC7B,UAAU;IACV,UAAU,aAAa,IAAI;IAC3B,eAAe,CAAC,cAAc,OAAO,IAAI,KAAK;IAC9C,WAAW;KACT;KACA;KACA,aACI,sCACA,aACE,sCACA;IACR,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;IAEV,UAAA,IAAI;GACC,GAtBD,IAAI,KAsBH;EAEZ,CAAC;CACE,CAAA;AAET"}
|
package/lib/index3.cjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
2
|
+
//#region src/ui-library/gallery/button/Button.tsx
|
|
3
|
+
var VARIANT_STYLES = {
|
|
4
|
+
primary: "font-bold bg-brand-blue text-white shadow-lg shadow-brand-blue/25 hover:bg-brand-navy hover:shadow-xl hover:shadow-brand-blue/30 active:bg-brand-navy focus-visible:ring-brand-blue/30 disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none",
|
|
5
|
+
dark: "font-semibold bg-slate-900 text-white shadow-lg shadow-slate-200 hover:bg-slate-800 active:bg-slate-950 focus-visible:ring-slate-300 disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none",
|
|
6
|
+
outline: "font-semibold border border-slate-300 bg-white text-slate-700 shadow-sm hover:border-brand-blue/40 hover:bg-blue-50/40 hover:text-brand-blue active:bg-slate-100 focus-visible:ring-brand-blue/20 disabled:border-slate-200 disabled:bg-white disabled:text-slate-300 disabled:shadow-none",
|
|
7
|
+
ghost: "font-semibold bg-transparent text-slate-700 hover:bg-slate-100 active:bg-slate-200 focus-visible:ring-slate-200 disabled:text-slate-300",
|
|
8
|
+
danger: "font-bold bg-rose-600 text-white shadow-lg shadow-rose-100 hover:bg-rose-700 active:bg-rose-800 focus-visible:ring-rose-300 disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none"
|
|
9
|
+
};
|
|
10
|
+
var SIZE_STYLES = {
|
|
11
|
+
sm: "h-9 gap-1.5 rounded-md px-3 text-xs",
|
|
12
|
+
md: "h-10 gap-2 rounded-md px-4 text-sm",
|
|
13
|
+
lg: "h-11 gap-2.5 rounded-md px-6 text-base"
|
|
14
|
+
};
|
|
15
|
+
var BASE = "inline-flex items-center justify-center transition-all duration-150 ease-out active:scale-[.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:active:scale-100 select-none";
|
|
16
|
+
function Button({ variant = "primary", size = "md", loading = false, leftIcon, rightIcon, fullWidth = false, children, className = "", disabled, ...props }) {
|
|
17
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
18
|
+
type: "button",
|
|
19
|
+
disabled: disabled || loading,
|
|
20
|
+
className: [
|
|
21
|
+
BASE,
|
|
22
|
+
VARIANT_STYLES[variant],
|
|
23
|
+
SIZE_STYLES[size],
|
|
24
|
+
fullWidth ? "w-full" : "",
|
|
25
|
+
className
|
|
26
|
+
].filter(Boolean).join(" "),
|
|
27
|
+
...props,
|
|
28
|
+
children: [
|
|
29
|
+
leftIcon && !loading && leftIcon,
|
|
30
|
+
children,
|
|
31
|
+
rightIcon && !loading && rightIcon
|
|
32
|
+
]
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
exports.Button = Button;
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=index3.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index3.cjs","names":[],"sources":["../src/ui-library/gallery/button/Button.tsx"],"sourcesContent":["/**\n * Button — Design System Component\n *\n * A reusable button component designed from first principles for the\n * organization's design system. Independent of the HRIS btnPrimary token.\n *\n * Variants: primary | dark | outline | ghost | danger\n * Sizes: sm | md | lg\n *\n * ── Loading Convention ────────────────────────────────────────────────────────\n *\n * The `loading` prop signals that an async operation is in progress.\n *\n * What `loading` does:\n * - Sets the HTML `disabled` attribute (prevents interaction, keyboard, click)\n * - Suppresses leftIcon and rightIcon slots (icons are hidden during loading)\n * - Does NOT inject a spinner or change the label automatically\n *\n * The caller owns the loading representation inside `children`:\n *\n * <Button loading={saving}>\n * {saving\n * ? <><Loader2 size={15} className=\"animate-spin\" aria-hidden=\"true\" /> Saving…</>\n * : <><Save size={15} aria-hidden=\"true\" /> Save Changes</>}\n * </Button>\n *\n * Rationale: The label and icon during loading are context-dependent (\"Saving...\",\n * \"Submitting...\", \"Uploading...\"). Hardcoding them in the component would require\n * a `loadingLabel` prop and make the component harder to compose.\n *\n * Accessibility:\n * - The `disabled` attribute is sufficient. Screen readers announce the disabled\n * state. No `aria-busy` is added because the button itself is not busy — the\n * surrounding operation is. If the page has a loading region, apply aria-busy\n * to that region instead.\n * - The Loader2 icon inside children should always have aria-hidden=\"true\" so\n * the spinner does not duplicate the label in screen reader output.\n *\n * Dimension stability:\n * - Because children is caller-controlled, the button may resize between default\n * and loading states if the label length changes (\"Save\" → \"Saving...\").\n * Use `min-w` on the button when stable width is required in a layout.\n *\n * loading + disabled interaction:\n * - Both `loading` and `disabled` independently disable the button.\n * - Passing both is valid but redundant. The visual result is identical.\n * - Prefer `loading` when the state is transient; `disabled` when it is permanent.\n *\n * How future design-system components should document loading:\n * - Follow this same pattern: caller provides children, component provides the\n * disabled/suppression behavior.\n * - Document the convention in the component's JSDoc, not in a separate file.\n * - Show a loading example in the component's playground and code examples section.\n * ─────────────────────────────────────────────────────────────────────────────\n */\n\nimport type { ReactNode } from 'react';\n\nexport type ButtonVariant = 'primary' | 'dark' | 'outline' | 'ghost' | 'danger';\nexport type ButtonSize = 'sm' | 'md' | 'lg';\n\nexport interface ButtonProps extends React.ComponentProps<'button'> {\n variant?: ButtonVariant;\n size?: ButtonSize;\n loading?: boolean;\n leftIcon?: ReactNode;\n rightIcon?: ReactNode;\n fullWidth?: boolean;\n children?: ReactNode;\n}\n\n// ── Style maps ────────────────────────────────────────────────────────────────\n\nconst VARIANT_STYLES: Record<ButtonVariant, string> = {\n primary:\n 'font-bold bg-brand-blue text-white shadow-lg shadow-brand-blue/25 hover:bg-brand-navy hover:shadow-xl hover:shadow-brand-blue/30 active:bg-brand-navy focus-visible:ring-brand-blue/30 disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none',\n dark:\n 'font-semibold bg-slate-900 text-white shadow-lg shadow-slate-200 hover:bg-slate-800 active:bg-slate-950 focus-visible:ring-slate-300 disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none',\n outline:\n 'font-semibold border border-slate-300 bg-white text-slate-700 shadow-sm hover:border-brand-blue/40 hover:bg-blue-50/40 hover:text-brand-blue active:bg-slate-100 focus-visible:ring-brand-blue/20 disabled:border-slate-200 disabled:bg-white disabled:text-slate-300 disabled:shadow-none',\n ghost:\n 'font-semibold bg-transparent text-slate-700 hover:bg-slate-100 active:bg-slate-200 focus-visible:ring-slate-200 disabled:text-slate-300',\n danger:\n 'font-bold bg-rose-600 text-white shadow-lg shadow-rose-100 hover:bg-rose-700 active:bg-rose-800 focus-visible:ring-rose-300 disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none',\n};\n\nconst SIZE_STYLES: Record<ButtonSize, string> = {\n sm: 'h-9 gap-1.5 rounded-md px-3 text-xs',\n md: 'h-10 gap-2 rounded-md px-4 text-sm',\n lg: 'h-11 gap-2.5 rounded-md px-6 text-base',\n};\n\nconst BASE =\n 'inline-flex items-center justify-center transition-all duration-150 ease-out active:scale-[.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:active:scale-100 select-none';\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport function Button({\n variant = 'primary',\n size = 'md',\n loading = false,\n leftIcon,\n rightIcon,\n fullWidth = false,\n children,\n className = '',\n disabled,\n ...props\n}: ButtonProps) {\n return (\n <button\n type=\"button\"\n disabled={disabled || loading}\n className={[\n BASE,\n VARIANT_STYLES[variant],\n SIZE_STYLES[size],\n fullWidth ? 'w-full' : '',\n className,\n ]\n .filter(Boolean)\n .join(' ')}\n {...props}\n >\n {leftIcon && !loading && leftIcon}\n {children}\n {rightIcon && !loading && rightIcon}\n </button>\n );\n}\n"],"mappings":";;AAyEA,IAAM,iBAAgD;CACpD,SACE;CACF,MACE;CACF,SACE;CACF,OACE;CACF,QACE;AACJ;AAEA,IAAM,cAA0C;CAC9C,IAAI;CACJ,IAAI;CACJ,IAAI;AACN;AAEA,IAAM,OACJ;AAIF,SAAgB,OAAO,EACrB,UAAU,WACV,OAAO,MACP,UAAU,OACV,UACA,WACA,YAAY,OACZ,UACA,YAAY,IACZ,UACA,GAAG,SACW;CACd,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;EACE,MAAK;EACL,UAAU,YAAY;EACtB,WAAW;GACT;GACA,eAAe;GACf,YAAY;GACZ,YAAY,WAAW;GACvB;EACF,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;EACX,GAAI;EAZN,UAAA;GAcG,YAAY,CAAC,WAAW;GACxB;GACA,aAAa,CAAC,WAAW;EACpB;;AAEZ"}
|
package/lib/index3.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsxs } from "react/jsx-runtime";
|
|
2
|
+
//#region src/ui-library/gallery/button/Button.tsx
|
|
3
|
+
var VARIANT_STYLES = {
|
|
4
|
+
primary: "font-bold bg-brand-blue text-white shadow-lg shadow-brand-blue/25 hover:bg-brand-navy hover:shadow-xl hover:shadow-brand-blue/30 active:bg-brand-navy focus-visible:ring-brand-blue/30 disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none",
|
|
5
|
+
dark: "font-semibold bg-slate-900 text-white shadow-lg shadow-slate-200 hover:bg-slate-800 active:bg-slate-950 focus-visible:ring-slate-300 disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none",
|
|
6
|
+
outline: "font-semibold border border-slate-300 bg-white text-slate-700 shadow-sm hover:border-brand-blue/40 hover:bg-blue-50/40 hover:text-brand-blue active:bg-slate-100 focus-visible:ring-brand-blue/20 disabled:border-slate-200 disabled:bg-white disabled:text-slate-300 disabled:shadow-none",
|
|
7
|
+
ghost: "font-semibold bg-transparent text-slate-700 hover:bg-slate-100 active:bg-slate-200 focus-visible:ring-slate-200 disabled:text-slate-300",
|
|
8
|
+
danger: "font-bold bg-rose-600 text-white shadow-lg shadow-rose-100 hover:bg-rose-700 active:bg-rose-800 focus-visible:ring-rose-300 disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none"
|
|
9
|
+
};
|
|
10
|
+
var SIZE_STYLES = {
|
|
11
|
+
sm: "h-9 gap-1.5 rounded-md px-3 text-xs",
|
|
12
|
+
md: "h-10 gap-2 rounded-md px-4 text-sm",
|
|
13
|
+
lg: "h-11 gap-2.5 rounded-md px-6 text-base"
|
|
14
|
+
};
|
|
15
|
+
var BASE = "inline-flex items-center justify-center transition-all duration-150 ease-out active:scale-[.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:active:scale-100 select-none";
|
|
16
|
+
function Button({ variant = "primary", size = "md", loading = false, leftIcon, rightIcon, fullWidth = false, children, className = "", disabled, ...props }) {
|
|
17
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
18
|
+
type: "button",
|
|
19
|
+
disabled: disabled || loading,
|
|
20
|
+
className: [
|
|
21
|
+
BASE,
|
|
22
|
+
VARIANT_STYLES[variant],
|
|
23
|
+
SIZE_STYLES[size],
|
|
24
|
+
fullWidth ? "w-full" : "",
|
|
25
|
+
className
|
|
26
|
+
].filter(Boolean).join(" "),
|
|
27
|
+
...props,
|
|
28
|
+
children: [
|
|
29
|
+
leftIcon && !loading && leftIcon,
|
|
30
|
+
children,
|
|
31
|
+
rightIcon && !loading && rightIcon
|
|
32
|
+
]
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { Button };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=index3.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index3.mjs","names":[],"sources":["../src/ui-library/gallery/button/Button.tsx"],"sourcesContent":["/**\n * Button — Design System Component\n *\n * A reusable button component designed from first principles for the\n * organization's design system. Independent of the HRIS btnPrimary token.\n *\n * Variants: primary | dark | outline | ghost | danger\n * Sizes: sm | md | lg\n *\n * ── Loading Convention ────────────────────────────────────────────────────────\n *\n * The `loading` prop signals that an async operation is in progress.\n *\n * What `loading` does:\n * - Sets the HTML `disabled` attribute (prevents interaction, keyboard, click)\n * - Suppresses leftIcon and rightIcon slots (icons are hidden during loading)\n * - Does NOT inject a spinner or change the label automatically\n *\n * The caller owns the loading representation inside `children`:\n *\n * <Button loading={saving}>\n * {saving\n * ? <><Loader2 size={15} className=\"animate-spin\" aria-hidden=\"true\" /> Saving…</>\n * : <><Save size={15} aria-hidden=\"true\" /> Save Changes</>}\n * </Button>\n *\n * Rationale: The label and icon during loading are context-dependent (\"Saving...\",\n * \"Submitting...\", \"Uploading...\"). Hardcoding them in the component would require\n * a `loadingLabel` prop and make the component harder to compose.\n *\n * Accessibility:\n * - The `disabled` attribute is sufficient. Screen readers announce the disabled\n * state. No `aria-busy` is added because the button itself is not busy — the\n * surrounding operation is. If the page has a loading region, apply aria-busy\n * to that region instead.\n * - The Loader2 icon inside children should always have aria-hidden=\"true\" so\n * the spinner does not duplicate the label in screen reader output.\n *\n * Dimension stability:\n * - Because children is caller-controlled, the button may resize between default\n * and loading states if the label length changes (\"Save\" → \"Saving...\").\n * Use `min-w` on the button when stable width is required in a layout.\n *\n * loading + disabled interaction:\n * - Both `loading` and `disabled` independently disable the button.\n * - Passing both is valid but redundant. The visual result is identical.\n * - Prefer `loading` when the state is transient; `disabled` when it is permanent.\n *\n * How future design-system components should document loading:\n * - Follow this same pattern: caller provides children, component provides the\n * disabled/suppression behavior.\n * - Document the convention in the component's JSDoc, not in a separate file.\n * - Show a loading example in the component's playground and code examples section.\n * ─────────────────────────────────────────────────────────────────────────────\n */\n\nimport type { ReactNode } from 'react';\n\nexport type ButtonVariant = 'primary' | 'dark' | 'outline' | 'ghost' | 'danger';\nexport type ButtonSize = 'sm' | 'md' | 'lg';\n\nexport interface ButtonProps extends React.ComponentProps<'button'> {\n variant?: ButtonVariant;\n size?: ButtonSize;\n loading?: boolean;\n leftIcon?: ReactNode;\n rightIcon?: ReactNode;\n fullWidth?: boolean;\n children?: ReactNode;\n}\n\n// ── Style maps ────────────────────────────────────────────────────────────────\n\nconst VARIANT_STYLES: Record<ButtonVariant, string> = {\n primary:\n 'font-bold bg-brand-blue text-white shadow-lg shadow-brand-blue/25 hover:bg-brand-navy hover:shadow-xl hover:shadow-brand-blue/30 active:bg-brand-navy focus-visible:ring-brand-blue/30 disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none',\n dark:\n 'font-semibold bg-slate-900 text-white shadow-lg shadow-slate-200 hover:bg-slate-800 active:bg-slate-950 focus-visible:ring-slate-300 disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none',\n outline:\n 'font-semibold border border-slate-300 bg-white text-slate-700 shadow-sm hover:border-brand-blue/40 hover:bg-blue-50/40 hover:text-brand-blue active:bg-slate-100 focus-visible:ring-brand-blue/20 disabled:border-slate-200 disabled:bg-white disabled:text-slate-300 disabled:shadow-none',\n ghost:\n 'font-semibold bg-transparent text-slate-700 hover:bg-slate-100 active:bg-slate-200 focus-visible:ring-slate-200 disabled:text-slate-300',\n danger:\n 'font-bold bg-rose-600 text-white shadow-lg shadow-rose-100 hover:bg-rose-700 active:bg-rose-800 focus-visible:ring-rose-300 disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none',\n};\n\nconst SIZE_STYLES: Record<ButtonSize, string> = {\n sm: 'h-9 gap-1.5 rounded-md px-3 text-xs',\n md: 'h-10 gap-2 rounded-md px-4 text-sm',\n lg: 'h-11 gap-2.5 rounded-md px-6 text-base',\n};\n\nconst BASE =\n 'inline-flex items-center justify-center transition-all duration-150 ease-out active:scale-[.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:active:scale-100 select-none';\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport function Button({\n variant = 'primary',\n size = 'md',\n loading = false,\n leftIcon,\n rightIcon,\n fullWidth = false,\n children,\n className = '',\n disabled,\n ...props\n}: ButtonProps) {\n return (\n <button\n type=\"button\"\n disabled={disabled || loading}\n className={[\n BASE,\n VARIANT_STYLES[variant],\n SIZE_STYLES[size],\n fullWidth ? 'w-full' : '',\n className,\n ]\n .filter(Boolean)\n .join(' ')}\n {...props}\n >\n {leftIcon && !loading && leftIcon}\n {children}\n {rightIcon && !loading && rightIcon}\n </button>\n );\n}\n"],"mappings":";;AAyEA,IAAM,iBAAgD;CACpD,SACE;CACF,MACE;CACF,SACE;CACF,OACE;CACF,QACE;AACJ;AAEA,IAAM,cAA0C;CAC9C,IAAI;CACJ,IAAI;CACJ,IAAI;AACN;AAEA,IAAM,OACJ;AAIF,SAAgB,OAAO,EACrB,UAAU,WACV,OAAO,MACP,UAAU,OACV,UACA,WACA,YAAY,OACZ,UACA,YAAY,IACZ,UACA,GAAG,SACW;CACd,OACE,qBAAC,UAAD;EACE,MAAK;EACL,UAAU,YAAY;EACtB,WAAW;GACT;GACA,eAAe;GACf,YAAY;GACZ,YAAY,WAAW;GACvB;EACF,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;EACX,GAAI;EAZN,UAAA;GAcG,YAAY,CAAC,WAAW;GACxB;GACA,aAAa,CAAC,WAAW;EACpB;;AAEZ"}
|