@blurrow/ui 0.1.0 → 0.2.1
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 +60 -20
- package/dist/Accordion.d.ts +17 -0
- package/dist/Accordion.js +138 -0
- package/dist/Accordion.js.map +1 -0
- package/dist/Alert.d.ts +13 -0
- package/dist/Alert.js +69 -0
- package/dist/Alert.js.map +1 -0
- package/dist/AuthorCard.d.ts +12 -0
- package/dist/AuthorCard.js +57 -0
- package/dist/AuthorCard.js.map +1 -0
- package/dist/Avatar.d.ts +21 -0
- package/dist/Avatar.js +96 -0
- package/dist/Avatar.js.map +1 -0
- package/dist/Badge.d.ts +12 -0
- package/dist/Badge.js +46 -0
- package/dist/Badge.js.map +1 -0
- package/dist/Banner.d.ts +15 -0
- package/dist/Banner.js +68 -0
- package/dist/Banner.js.map +1 -0
- package/dist/Breadcrumb.d.ts +13 -0
- package/dist/Breadcrumb.js +45 -0
- package/dist/Breadcrumb.js.map +1 -0
- package/dist/Button.d.ts +19 -0
- package/dist/Button.js +282 -0
- package/dist/Button.js.map +1 -0
- package/dist/ButtonGroup.d.ts +19 -0
- package/dist/ButtonGroup.js +62 -0
- package/dist/ButtonGroup.js.map +1 -0
- package/dist/Card.d.ts +40 -0
- package/dist/Card.js +175 -0
- package/dist/Card.js.map +1 -0
- package/dist/CardShell.d.ts +18 -0
- package/dist/CardShell.js +58 -0
- package/dist/CardShell.js.map +1 -0
- package/dist/Carousel.d.ts +22 -0
- package/dist/Carousel.js +247 -0
- package/dist/Carousel.js.map +1 -0
- package/dist/Checkbox.d.ts +15 -0
- package/dist/Checkbox.js +84 -0
- package/dist/Checkbox.js.map +1 -0
- package/dist/CloseButton.d.ts +12 -0
- package/dist/CloseButton.js +63 -0
- package/dist/CloseButton.js.map +1 -0
- package/dist/CodeBlock.d.ts +12 -0
- package/dist/CodeBlock.js +221 -0
- package/dist/CodeBlock.js.map +1 -0
- package/dist/ColorPicker.d.ts +24 -0
- package/dist/ColorPicker.js +366 -0
- package/dist/ColorPicker.js.map +1 -0
- package/dist/Combobox.d.ts +34 -0
- package/dist/Combobox.js +255 -0
- package/dist/Combobox.js.map +1 -0
- package/dist/CommandPalette.d.ts +25 -0
- package/dist/CommandPalette.js +304 -0
- package/dist/CommandPalette.js.map +1 -0
- package/dist/ContextMenu.d.ts +16 -0
- package/dist/ContextMenu.js +209 -0
- package/dist/ContextMenu.js.map +1 -0
- package/dist/DataList.d.ts +15 -0
- package/dist/DataList.js +42 -0
- package/dist/DataList.js.map +1 -0
- package/dist/DatePicker.d.ts +65 -0
- package/dist/DatePicker.js +391 -0
- package/dist/DatePicker.js.map +1 -0
- package/dist/DateRangePicker.d.ts +34 -0
- package/dist/DateRangePicker.js +180 -0
- package/dist/DateRangePicker.js.map +1 -0
- package/dist/Drawer.d.ts +17 -0
- package/dist/Drawer.js +132 -0
- package/dist/Drawer.js.map +1 -0
- package/dist/DropdownMenu.d.ts +23 -0
- package/dist/DropdownMenu.js +185 -0
- package/dist/DropdownMenu.js.map +1 -0
- package/dist/EmptyState.d.ts +13 -0
- package/dist/EmptyState.js +55 -0
- package/dist/EmptyState.js.map +1 -0
- package/dist/FieldMessage.d.ts +26 -0
- package/dist/FieldMessage.js +95 -0
- package/dist/FieldMessage.js.map +1 -0
- package/dist/FileUpload.d.ts +32 -0
- package/dist/FileUpload.js +253 -0
- package/dist/FileUpload.js.map +1 -0
- package/dist/Floating.d.ts +32 -0
- package/dist/Floating.js +95 -0
- package/dist/Floating.js.map +1 -0
- package/dist/HoverCard.d.ts +18 -0
- package/dist/HoverCard.js +90 -0
- package/dist/HoverCard.js.map +1 -0
- package/dist/Input.d.ts +17 -0
- package/dist/Input.js +96 -0
- package/dist/Input.js.map +1 -0
- package/dist/Kbd.d.ts +9 -0
- package/dist/Kbd.js +24 -0
- package/dist/Kbd.js.map +1 -0
- package/dist/Modal.d.ts +15 -0
- package/dist/Modal.js +109 -0
- package/dist/Modal.js.map +1 -0
- package/dist/NumberInput.d.ts +23 -0
- package/dist/NumberInput.js +162 -0
- package/dist/NumberInput.js.map +1 -0
- package/dist/OtpInput.d.ts +26 -0
- package/dist/OtpInput.js +173 -0
- package/dist/OtpInput.js.map +1 -0
- package/dist/Pagination.d.ts +11 -0
- package/dist/Pagination.js +148 -0
- package/dist/Pagination.js.map +1 -0
- package/dist/Popover.d.ts +13 -0
- package/dist/Popover.js +98 -0
- package/dist/Popover.js.map +1 -0
- package/dist/Progress.d.ts +12 -0
- package/dist/Progress.js +61 -0
- package/dist/Progress.js.map +1 -0
- package/dist/QuoteCard.d.ts +13 -0
- package/dist/QuoteCard.js +92 -0
- package/dist/QuoteCard.js.map +1 -0
- package/dist/Radio.d.ts +19 -0
- package/dist/Radio.js +161 -0
- package/dist/Radio.js.map +1 -0
- package/dist/RadioCards.d.ts +27 -0
- package/dist/RadioCards.js +205 -0
- package/dist/RadioCards.js.map +1 -0
- package/dist/ReactionBar.d.ts +15 -0
- package/dist/ReactionBar.js +92 -0
- package/dist/ReactionBar.js.map +1 -0
- package/dist/ReadingProgress.d.ts +8 -0
- package/dist/ReadingProgress.js +51 -0
- package/dist/ReadingProgress.js.map +1 -0
- package/dist/ReadingTime.d.ts +9 -0
- package/dist/ReadingTime.js +39 -0
- package/dist/ReadingTime.js.map +1 -0
- package/dist/Select.d.ts +20 -0
- package/dist/Select.js +244 -0
- package/dist/Select.js.map +1 -0
- package/dist/Separator.d.ts +14 -0
- package/dist/Separator.js +33 -0
- package/dist/Separator.js.map +1 -0
- package/dist/ShareButtons.d.ts +10 -0
- package/dist/ShareButtons.js +107 -0
- package/dist/ShareButtons.js.map +1 -0
- package/dist/Sidebar.d.ts +30 -0
- package/dist/Sidebar.js +140 -0
- package/dist/Sidebar.js.map +1 -0
- package/dist/Skeleton.d.ts +15 -0
- package/dist/Skeleton.js +67 -0
- package/dist/Skeleton.js.map +1 -0
- package/dist/Slider.d.ts +27 -0
- package/dist/Slider.js +182 -0
- package/dist/Slider.js.map +1 -0
- package/dist/Spinner.d.ts +12 -0
- package/dist/Spinner.js +24 -0
- package/dist/Spinner.js.map +1 -0
- package/dist/SpotlightCard.d.ts +17 -0
- package/dist/SpotlightCard.js +87 -0
- package/dist/SpotlightCard.js.map +1 -0
- package/dist/Stat.d.ts +19 -0
- package/dist/Stat.js +71 -0
- package/dist/Stat.js.map +1 -0
- package/dist/StatCard.d.ts +24 -0
- package/dist/StatCard.js +212 -0
- package/dist/StatCard.js.map +1 -0
- package/dist/Steps.d.ts +14 -0
- package/dist/Steps.js +94 -0
- package/dist/Steps.js.map +1 -0
- package/dist/Table.d.ts +21 -0
- package/dist/Table.js +93 -0
- package/dist/Table.js.map +1 -0
- package/dist/TableOfContents.d.ts +17 -0
- package/dist/TableOfContents.js +92 -0
- package/dist/TableOfContents.js.map +1 -0
- package/dist/Tabs.d.ts +17 -0
- package/dist/Tabs.js +123 -0
- package/dist/Tabs.js.map +1 -0
- package/dist/TagInput.d.ts +16 -0
- package/dist/TagInput.js +169 -0
- package/dist/TagInput.js.map +1 -0
- package/dist/Textarea.d.ts +13 -0
- package/dist/Textarea.js +74 -0
- package/dist/Textarea.js.map +1 -0
- package/dist/TimePicker.d.ts +28 -0
- package/dist/TimePicker.js +264 -0
- package/dist/TimePicker.js.map +1 -0
- package/dist/Timeline.d.ts +19 -0
- package/dist/Timeline.js +76 -0
- package/dist/Timeline.js.map +1 -0
- package/dist/Toast.d.ts +18 -0
- package/dist/Toast.js +318 -0
- package/dist/Toast.js.map +1 -0
- package/dist/Toggle.d.ts +13 -0
- package/dist/Toggle.js +84 -0
- package/dist/Toggle.js.map +1 -0
- package/dist/Tooltip.d.ts +13 -0
- package/dist/Tooltip.js +138 -0
- package/dist/Tooltip.js.map +1 -0
- package/dist/cli.js +1644 -0
- package/dist/index.d.ts +68 -631
- package/dist/index.js +67 -5558
- package/dist/index.js.map +1 -1
- package/dist/registry/Accordion.tsx +140 -0
- package/dist/registry/Alert.tsx +95 -0
- package/dist/registry/AuthorCard.tsx +67 -0
- package/dist/registry/Avatar.tsx +116 -0
- package/dist/registry/Badge.tsx +55 -0
- package/dist/registry/Banner.tsx +86 -0
- package/dist/registry/Breadcrumb.tsx +60 -0
- package/dist/registry/Button.tsx +361 -0
- package/dist/registry/ButtonGroup.tsx +82 -0
- package/dist/registry/Card.tsx +241 -0
- package/dist/registry/CardShell.tsx +78 -0
- package/dist/registry/Carousel.tsx +296 -0
- package/dist/registry/Checkbox.tsx +104 -0
- package/dist/registry/CloseButton.tsx +68 -0
- package/dist/registry/CodeBlock.tsx +202 -0
- package/dist/registry/ColorPicker.tsx +404 -0
- package/dist/registry/Combobox.tsx +318 -0
- package/dist/registry/CommandPalette.tsx +356 -0
- package/dist/registry/ContextMenu.tsx +241 -0
- package/dist/registry/DataList.tsx +49 -0
- package/dist/registry/DatePicker.tsx +494 -0
- package/dist/registry/DateRangePicker.tsx +232 -0
- package/dist/registry/Drawer.tsx +166 -0
- package/dist/registry/DropdownMenu.tsx +219 -0
- package/dist/registry/EmptyState.tsx +55 -0
- package/dist/registry/FieldMessage.tsx +123 -0
- package/dist/registry/FileUpload.tsx +318 -0
- package/dist/registry/Floating.tsx +134 -0
- package/dist/registry/HoverCard.tsx +113 -0
- package/dist/registry/Input.tsx +108 -0
- package/dist/registry/Kbd.tsx +23 -0
- package/dist/registry/Modal.tsx +130 -0
- package/dist/registry/NumberInput.tsx +183 -0
- package/dist/registry/OtpInput.tsx +219 -0
- package/dist/registry/Pagination.tsx +160 -0
- package/dist/registry/Popover.tsx +116 -0
- package/dist/registry/Progress.tsx +66 -0
- package/dist/registry/QuoteCard.tsx +93 -0
- package/dist/registry/Radio.tsx +205 -0
- package/dist/registry/RadioCards.tsx +247 -0
- package/dist/registry/ReactionBar.tsx +101 -0
- package/dist/registry/ReadingProgress.tsx +51 -0
- package/dist/registry/ReadingTime.tsx +35 -0
- package/dist/registry/Select.tsx +265 -0
- package/dist/registry/Separator.tsx +52 -0
- package/dist/registry/ShareButtons.tsx +134 -0
- package/dist/registry/Sidebar.tsx +182 -0
- package/dist/registry/Skeleton.tsx +83 -0
- package/dist/registry/Slider.tsx +229 -0
- package/dist/registry/Spinner.tsx +32 -0
- package/dist/registry/SpotlightCard.tsx +106 -0
- package/dist/registry/Stat.tsx +82 -0
- package/dist/registry/StatCard.tsx +256 -0
- package/dist/registry/Steps.tsx +109 -0
- package/dist/registry/Table.tsx +123 -0
- package/dist/registry/TableOfContents.tsx +116 -0
- package/dist/registry/Tabs.tsx +136 -0
- package/dist/registry/TagInput.tsx +191 -0
- package/dist/registry/Textarea.tsx +84 -0
- package/dist/registry/TimePicker.tsx +315 -0
- package/dist/registry/Timeline.tsx +102 -0
- package/dist/registry/Toast.tsx +388 -0
- package/dist/registry/Toggle.tsx +96 -0
- package/dist/registry/Tooltip.tsx +160 -0
- package/dist/registry/styles.css +1207 -0
- package/dist/registry/useFloatingPanel.ts +112 -0
- package/dist/registry/useModalBehavior.ts +77 -0
- package/dist/styles.css +868 -62
- package/dist/useFloatingPanel.d.ts +24 -0
- package/dist/useFloatingPanel.js +89 -0
- package/dist/useFloatingPanel.js.map +1 -0
- package/dist/useModalBehavior.d.ts +15 -0
- package/dist/useModalBehavior.js +57 -0
- package/dist/useModalBehavior.js.map +1 -0
- package/package.json +23 -6
package/dist/TagInput.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ../../components/ui/TagInput.tsx
|
|
4
|
+
import { useState, useRef, useEffect, useId } from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
function TagInput({
|
|
7
|
+
value,
|
|
8
|
+
defaultValue = [],
|
|
9
|
+
onChange,
|
|
10
|
+
placeholder = "add a tag\u2026",
|
|
11
|
+
label,
|
|
12
|
+
hint,
|
|
13
|
+
maxTags,
|
|
14
|
+
fullWidth = false,
|
|
15
|
+
className = ""
|
|
16
|
+
}) {
|
|
17
|
+
const [internal, setInternal] = useState(defaultValue);
|
|
18
|
+
const [input, setInput] = useState("");
|
|
19
|
+
const [animTags, setAnimTags] = useState(
|
|
20
|
+
() => (value ?? defaultValue).map((t) => ({ text: t, state: "visible" }))
|
|
21
|
+
);
|
|
22
|
+
const inputRef = useRef(null);
|
|
23
|
+
const uid = useId();
|
|
24
|
+
const prevValueRef = useRef(value ?? defaultValue);
|
|
25
|
+
const tags = value !== void 0 ? value : internal;
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const prev = prevValueRef.current;
|
|
28
|
+
const next = tags;
|
|
29
|
+
const added = next.filter((t) => !prev.includes(t));
|
|
30
|
+
const removed = prev.filter((t) => !next.includes(t));
|
|
31
|
+
if (added.length === 0 && removed.length === 0) return;
|
|
32
|
+
setAnimTags((current) => {
|
|
33
|
+
const updated = current.map(
|
|
34
|
+
(at) => removed.includes(at.text) ? { ...at, state: "exiting" } : at
|
|
35
|
+
);
|
|
36
|
+
added.forEach((t) => {
|
|
37
|
+
if (!updated.find((at) => at.text === t)) {
|
|
38
|
+
updated.push({ text: t, state: "entering" });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return updated;
|
|
42
|
+
});
|
|
43
|
+
if (added.length > 0) {
|
|
44
|
+
requestAnimationFrame(() => {
|
|
45
|
+
setAnimTags(
|
|
46
|
+
(current) => current.map(
|
|
47
|
+
(at) => at.state === "entering" ? { ...at, state: "visible" } : at
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (removed.length > 0) {
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
setAnimTags((current) => current.filter((at) => at.state !== "exiting"));
|
|
55
|
+
}, 180);
|
|
56
|
+
}
|
|
57
|
+
prevValueRef.current = next;
|
|
58
|
+
}, [tags]);
|
|
59
|
+
const update = (next) => {
|
|
60
|
+
setInternal(next);
|
|
61
|
+
onChange?.(next);
|
|
62
|
+
};
|
|
63
|
+
const addTag = (raw) => {
|
|
64
|
+
const tag = raw.trim();
|
|
65
|
+
if (!tag || tags.includes(tag)) return;
|
|
66
|
+
if (maxTags && tags.length >= maxTags) return;
|
|
67
|
+
update([...tags, tag]);
|
|
68
|
+
setInput("");
|
|
69
|
+
};
|
|
70
|
+
const removeTag = (index) => {
|
|
71
|
+
const tagText = tags[index];
|
|
72
|
+
if (!tagText) return;
|
|
73
|
+
setAnimTags(
|
|
74
|
+
(current) => current.map(
|
|
75
|
+
(at) => at.text === tagText ? { ...at, state: "exiting" } : at
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
update(tags.filter((_, i) => i !== index));
|
|
80
|
+
}, 180);
|
|
81
|
+
};
|
|
82
|
+
const handleKeyDown = (e) => {
|
|
83
|
+
if (e.key === "Enter" || e.key === ",") {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
addTag(input);
|
|
86
|
+
}
|
|
87
|
+
if (e.key === "Backspace" && !input && tags.length > 0) {
|
|
88
|
+
removeTag(tags.length - 1);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
return /* @__PURE__ */ jsxs("div", { className: ["flex flex-col gap-1.5", fullWidth ? "w-full" : ""].join(" "), children: [
|
|
92
|
+
label && /* @__PURE__ */ jsx("label", { htmlFor: `${uid}-input`, className: "text-[12.5px] font-medium text-ink-label tracking-[-0.005em]", children: label }),
|
|
93
|
+
/* @__PURE__ */ jsxs(
|
|
94
|
+
"div",
|
|
95
|
+
{
|
|
96
|
+
onClick: () => inputRef.current?.focus(),
|
|
97
|
+
className: [
|
|
98
|
+
"flex flex-wrap items-center gap-1.5 rounded-xl px-3 py-2.5",
|
|
99
|
+
"transition-[box-shadow,background] duration-200 ease-out cursor-text",
|
|
100
|
+
"[background:var(--field)] [box-shadow:var(--field-shadow)]",
|
|
101
|
+
"hover:[background:var(--field-hover)] hover:[box-shadow:var(--field-shadow-hover)]",
|
|
102
|
+
"focus-within:[background:var(--field-focus)] focus-within:[box-shadow:var(--field-shadow-focus)]",
|
|
103
|
+
fullWidth ? "w-full" : "",
|
|
104
|
+
className
|
|
105
|
+
].filter(Boolean).join(" "),
|
|
106
|
+
children: [
|
|
107
|
+
animTags.map((at) => /* @__PURE__ */ jsxs(
|
|
108
|
+
"span",
|
|
109
|
+
{
|
|
110
|
+
className: [
|
|
111
|
+
"inline-flex items-center gap-1 pl-2 pr-1 py-0.5 rounded-[7px] text-[11.5px] font-medium text-ink-label select-none",
|
|
112
|
+
"transition-all duration-180 ease-[cubic-bezier(0.34,1.4,0.64,1)]",
|
|
113
|
+
at.state === "entering" ? "opacity-0 scale-75" : "",
|
|
114
|
+
at.state === "visible" ? "opacity-100 scale-100" : "",
|
|
115
|
+
at.state === "exiting" ? "opacity-0 scale-75" : ""
|
|
116
|
+
].join(" "),
|
|
117
|
+
style: {
|
|
118
|
+
/* Tags are small raised faces sitting in the sunken field */
|
|
119
|
+
background: "var(--face)",
|
|
120
|
+
boxShadow: "var(--face-shadow)"
|
|
121
|
+
},
|
|
122
|
+
children: [
|
|
123
|
+
at.text,
|
|
124
|
+
/* @__PURE__ */ jsx(
|
|
125
|
+
"button",
|
|
126
|
+
{
|
|
127
|
+
type: "button",
|
|
128
|
+
onClick: (e) => {
|
|
129
|
+
e.stopPropagation();
|
|
130
|
+
const idx = tags.indexOf(at.text);
|
|
131
|
+
if (idx !== -1) removeTag(idx);
|
|
132
|
+
},
|
|
133
|
+
"aria-label": `Remove ${at.text}`,
|
|
134
|
+
className: "w-4 h-4 inline-flex items-center justify-center rounded-[5px] text-ink-faint hover:text-ink hover:bg-ink/[0.06] cursor-pointer transition-colors",
|
|
135
|
+
children: /* @__PURE__ */ jsx("svg", { width: "8", height: "8", fill: "none", stroke: "currentColor", strokeWidth: "2.5", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) })
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
]
|
|
139
|
+
},
|
|
140
|
+
at.text
|
|
141
|
+
)),
|
|
142
|
+
/* @__PURE__ */ jsx(
|
|
143
|
+
"input",
|
|
144
|
+
{
|
|
145
|
+
ref: inputRef,
|
|
146
|
+
id: `${uid}-input`,
|
|
147
|
+
type: "text",
|
|
148
|
+
value: input,
|
|
149
|
+
onChange: (e) => setInput(e.target.value),
|
|
150
|
+
onKeyDown: handleKeyDown,
|
|
151
|
+
onBlur: () => {
|
|
152
|
+
if (input) addTag(input);
|
|
153
|
+
},
|
|
154
|
+
placeholder: tags.length === 0 ? placeholder : "",
|
|
155
|
+
"aria-label": label ? void 0 : placeholder,
|
|
156
|
+
"aria-describedby": hint ? `${uid}-hint` : void 0,
|
|
157
|
+
className: "flex-1 min-w-[80px] bg-transparent text-[13.5px] text-ink placeholder:text-ink-faint outline-none py-0.5"
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
),
|
|
163
|
+
hint && /* @__PURE__ */ jsx("p", { id: `${uid}-hint`, className: "text-[11.5px] text-ink-muted mt-0.5", children: hint })
|
|
164
|
+
] });
|
|
165
|
+
}
|
|
166
|
+
export {
|
|
167
|
+
TagInput
|
|
168
|
+
};
|
|
169
|
+
//# sourceMappingURL=TagInput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../components/ui/TagInput.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useState, useRef, useEffect, useId } from \"react\";\n\ninterface TagInputProps {\n value?: string[];\n defaultValue?: string[];\n onChange?: (tags: string[]) => void;\n placeholder?: string;\n label?: string;\n hint?: string;\n maxTags?: number;\n fullWidth?: boolean;\n className?: string;\n}\n\ninterface AnimatedTag {\n text: string;\n state: \"entering\" | \"visible\" | \"exiting\";\n}\n\nexport function TagInput({\n value,\n defaultValue = [],\n onChange,\n placeholder = \"add a tag…\",\n label,\n hint,\n maxTags,\n fullWidth = false,\n className = \"\",\n}: TagInputProps) {\n const [internal, setInternal] = useState<string[]>(defaultValue);\n const [input, setInput] = useState(\"\");\n const [animTags, setAnimTags] = useState<AnimatedTag[]>(() =>\n (value ?? defaultValue).map((t) => ({ text: t, state: \"visible\" as const }))\n );\n const inputRef = useRef<HTMLInputElement>(null);\n const uid = useId();\n const prevValueRef = useRef<string[]>(value ?? defaultValue);\n\n const tags = value !== undefined ? value : internal;\n\n useEffect(() => {\n const prev = prevValueRef.current;\n const next = tags;\n\n const added = next.filter((t) => !prev.includes(t));\n const removed = prev.filter((t) => !next.includes(t));\n\n if (added.length === 0 && removed.length === 0) return;\n\n setAnimTags((current) => {\n const updated = current.map((at) =>\n removed.includes(at.text) ? { ...at, state: \"exiting\" as const } : at\n );\n added.forEach((t) => {\n if (!updated.find((at) => at.text === t)) {\n updated.push({ text: t, state: \"entering\" });\n }\n });\n return updated;\n });\n\n if (added.length > 0) {\n requestAnimationFrame(() => {\n setAnimTags((current) =>\n current.map((at) =>\n at.state === \"entering\" ? { ...at, state: \"visible\" } : at\n )\n );\n });\n }\n\n if (removed.length > 0) {\n setTimeout(() => {\n setAnimTags((current) => current.filter((at) => at.state !== \"exiting\"));\n }, 180);\n }\n\n prevValueRef.current = next;\n }, [tags]);\n\n const update = (next: string[]) => {\n setInternal(next);\n onChange?.(next);\n };\n\n const addTag = (raw: string) => {\n const tag = raw.trim();\n if (!tag || tags.includes(tag)) return;\n if (maxTags && tags.length >= maxTags) return;\n update([...tags, tag]);\n setInput(\"\");\n };\n\n const removeTag = (index: number) => {\n const tagText = tags[index];\n if (!tagText) return;\n setAnimTags((current) =>\n current.map((at) =>\n at.text === tagText ? { ...at, state: \"exiting\" as const } : at\n )\n );\n setTimeout(() => {\n update(tags.filter((_, i) => i !== index));\n }, 180);\n };\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\" || e.key === \",\") {\n e.preventDefault();\n addTag(input);\n }\n if (e.key === \"Backspace\" && !input && tags.length > 0) {\n removeTag(tags.length - 1);\n }\n };\n\n return (\n <div className={[\"flex flex-col gap-1.5\", fullWidth ? \"w-full\" : \"\"].join(\" \")}>\n {label && (\n <label htmlFor={`${uid}-input`} className=\"text-[12.5px] font-medium text-ink-label tracking-[-0.005em]\">\n {label}\n </label>\n )}\n <div\n onClick={() => inputRef.current?.focus()}\n className={[\n \"flex flex-wrap items-center gap-1.5 rounded-xl px-3 py-2.5\",\n \"transition-[box-shadow,background] duration-200 ease-out cursor-text\",\n \"[background:var(--field)] [box-shadow:var(--field-shadow)]\",\n \"hover:[background:var(--field-hover)] hover:[box-shadow:var(--field-shadow-hover)]\",\n \"focus-within:[background:var(--field-focus)] focus-within:[box-shadow:var(--field-shadow-focus)]\",\n fullWidth ? \"w-full\" : \"\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {animTags.map((at) => (\n <span\n key={at.text}\n className={[\n \"inline-flex items-center gap-1 pl-2 pr-1 py-0.5 rounded-[7px] text-[11.5px] font-medium text-ink-label select-none\",\n \"transition-all duration-180 ease-[cubic-bezier(0.34,1.4,0.64,1)]\",\n at.state === \"entering\" ? \"opacity-0 scale-75\" : \"\",\n at.state === \"visible\" ? \"opacity-100 scale-100\" : \"\",\n at.state === \"exiting\" ? \"opacity-0 scale-75\" : \"\",\n ].join(\" \")}\n style={{\n /* Tags are small raised faces sitting in the sunken field */\n background: \"var(--face)\",\n boxShadow: \"var(--face-shadow)\",\n }}\n >\n {at.text}\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n const idx = tags.indexOf(at.text);\n if (idx !== -1) removeTag(idx);\n }}\n aria-label={`Remove ${at.text}`}\n className=\"w-4 h-4 inline-flex items-center justify-center rounded-[5px] text-ink-faint hover:text-ink hover:bg-ink/[0.06] cursor-pointer transition-colors\"\n >\n <svg width=\"8\" height=\"8\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </span>\n ))}\n <input\n ref={inputRef}\n id={`${uid}-input`}\n type=\"text\"\n value={input}\n onChange={(e) => setInput(e.target.value)}\n onKeyDown={handleKeyDown}\n onBlur={() => { if (input) addTag(input); }}\n placeholder={tags.length === 0 ? placeholder : \"\"}\n aria-label={label ? undefined : placeholder}\n aria-describedby={hint ? `${uid}-hint` : undefined}\n className=\"flex-1 min-w-[80px] bg-transparent text-[13.5px] text-ink placeholder:text-ink-faint outline-none py-0.5\"\n />\n </div>\n {hint && <p id={`${uid}-hint`} className=\"text-[11.5px] text-ink-muted mt-0.5\">{hint}</p>}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
5
|
+
label?: string;
|
|
6
|
+
hint?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
fullWidth?: boolean;
|
|
9
|
+
autoResize?: boolean;
|
|
10
|
+
}
|
|
11
|
+
declare function Textarea({ label, hint, error, fullWidth, autoResize, className, id, rows, onChange, ...rest }: TextareaProps): react_jsx_runtime.JSX.Element;
|
|
12
|
+
|
|
13
|
+
export { Textarea };
|
package/dist/Textarea.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ../../components/ui/Textarea.tsx
|
|
4
|
+
import { useRef, useCallback, useEffect } from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
function Textarea({
|
|
7
|
+
label,
|
|
8
|
+
hint,
|
|
9
|
+
error,
|
|
10
|
+
fullWidth = false,
|
|
11
|
+
autoResize = false,
|
|
12
|
+
className = "",
|
|
13
|
+
id,
|
|
14
|
+
rows = 3,
|
|
15
|
+
onChange,
|
|
16
|
+
...rest
|
|
17
|
+
}) {
|
|
18
|
+
const textareaId = id ?? label?.toLowerCase().replace(/\s+/g, "-");
|
|
19
|
+
const messageId = textareaId && (error || hint) ? `${textareaId}-message` : void 0;
|
|
20
|
+
const ref = useRef(null);
|
|
21
|
+
const resize = useCallback(() => {
|
|
22
|
+
if (!autoResize || !ref.current) return;
|
|
23
|
+
ref.current.style.height = "auto";
|
|
24
|
+
ref.current.style.height = ref.current.scrollHeight + "px";
|
|
25
|
+
}, [autoResize]);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
resize();
|
|
28
|
+
}, [resize]);
|
|
29
|
+
const handleChange = (e) => {
|
|
30
|
+
resize();
|
|
31
|
+
onChange?.(e);
|
|
32
|
+
};
|
|
33
|
+
return /* @__PURE__ */ jsxs("div", { className: ["group/field flex flex-col gap-1.5", fullWidth ? "w-full" : ""].join(" "), children: [
|
|
34
|
+
label && /* @__PURE__ */ jsx(
|
|
35
|
+
"label",
|
|
36
|
+
{
|
|
37
|
+
htmlFor: textareaId,
|
|
38
|
+
className: "text-[12.5px] font-medium text-ink-label tracking-[-0.005em] transition-colors duration-200 group-focus-within/field:text-ink-strong",
|
|
39
|
+
children: label
|
|
40
|
+
}
|
|
41
|
+
),
|
|
42
|
+
/* @__PURE__ */ jsx(
|
|
43
|
+
"textarea",
|
|
44
|
+
{
|
|
45
|
+
ref,
|
|
46
|
+
id: textareaId,
|
|
47
|
+
rows,
|
|
48
|
+
onChange: handleChange,
|
|
49
|
+
className: [
|
|
50
|
+
"field rounded-xl px-4 py-3 text-[13.5px] text-ink resize-none scroll-thin",
|
|
51
|
+
"placeholder:text-ink-faint placeholder:transition-colors placeholder:duration-200 focus:placeholder:text-ink-ghost",
|
|
52
|
+
"disabled:opacity-40 disabled:cursor-not-allowed",
|
|
53
|
+
fullWidth ? "w-full" : "",
|
|
54
|
+
className
|
|
55
|
+
].filter(Boolean).join(" "),
|
|
56
|
+
"aria-invalid": error ? true : void 0,
|
|
57
|
+
"aria-describedby": messageId,
|
|
58
|
+
...rest
|
|
59
|
+
}
|
|
60
|
+
),
|
|
61
|
+
error && /* @__PURE__ */ jsxs("p", { id: messageId, role: "alert", className: "text-[11.5px] text-danger font-medium flex items-center gap-1 mt-0.5", children: [
|
|
62
|
+
/* @__PURE__ */ jsxs("svg", { width: "11", height: "11", fill: "none", stroke: "currentColor", strokeWidth: "2", viewBox: "0 0 24 24", children: [
|
|
63
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
64
|
+
/* @__PURE__ */ jsx("path", { strokeLinecap: "round", d: "M12 8v4m0 4h.01" })
|
|
65
|
+
] }),
|
|
66
|
+
error
|
|
67
|
+
] }),
|
|
68
|
+
hint && !error && /* @__PURE__ */ jsx("p", { id: messageId, className: "text-[11.5px] text-ink-muted mt-0.5", children: hint })
|
|
69
|
+
] });
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
Textarea
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=Textarea.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../components/ui/Textarea.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useRef, useCallback, useEffect } from \"react\";\n\ninterface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {\n label?: string;\n hint?: string;\n error?: string;\n fullWidth?: boolean;\n autoResize?: boolean;\n}\n\nexport function Textarea({\n label,\n hint,\n error,\n fullWidth = false,\n autoResize = false,\n className = \"\",\n id,\n rows = 3,\n onChange,\n ...rest\n}: TextareaProps) {\n const textareaId = id ?? label?.toLowerCase().replace(/\\s+/g, \"-\");\n const messageId = textareaId && (error || hint) ? `${textareaId}-message` : undefined;\n const ref = useRef<HTMLTextAreaElement>(null);\n\n const resize = useCallback(() => {\n if (!autoResize || !ref.current) return;\n ref.current.style.height = \"auto\";\n ref.current.style.height = ref.current.scrollHeight + \"px\";\n }, [autoResize]);\n\n useEffect(() => { resize(); }, [resize]);\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n resize();\n onChange?.(e);\n };\n\n return (\n <div className={[\"group/field flex flex-col gap-1.5\", fullWidth ? \"w-full\" : \"\"].join(\" \")}>\n {label && (\n <label\n htmlFor={textareaId}\n className=\"text-[12.5px] font-medium text-ink-label tracking-[-0.005em] transition-colors duration-200 group-focus-within/field:text-ink-strong\"\n >\n {label}\n </label>\n )}\n <textarea\n ref={ref}\n id={textareaId}\n rows={rows}\n onChange={handleChange}\n className={[\n \"field rounded-xl px-4 py-3 text-[13.5px] text-ink resize-none scroll-thin\",\n \"placeholder:text-ink-faint placeholder:transition-colors placeholder:duration-200 focus:placeholder:text-ink-ghost\",\n \"disabled:opacity-40 disabled:cursor-not-allowed\",\n fullWidth ? \"w-full\" : \"\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n aria-invalid={error ? true : undefined}\n aria-describedby={messageId}\n {...rest}\n />\n {error && (\n <p id={messageId} role=\"alert\" className=\"text-[11.5px] text-danger font-medium flex items-center gap-1 mt-0.5\">\n <svg width=\"11\" height=\"11\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" viewBox=\"0 0 24 24\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path strokeLinecap=\"round\" d=\"M12 8v4m0 4h.01\" />\n </svg>\n {error}\n </p>\n )}\n {hint && !error && (\n <p id={messageId} className=\"text-[11.5px] text-ink-muted mt-0.5\">{hint}</p>\n )}\n </div>\n );\n}\n"],"mappings":";;;AAEA,SAAgB,QAAQ,aAAa,iBAAiB;AA0C9C,cA2BE,YA3BF;AAhCD,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,YAAY;AAAA,EACZ;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,aAAa,MAAM,OAAO,YAAY,EAAE,QAAQ,QAAQ,GAAG;AACjE,QAAM,YAAY,eAAe,SAAS,QAAQ,GAAG,UAAU,aAAa;AAC5E,QAAM,MAAM,OAA4B,IAAI;AAE5C,QAAM,SAAS,YAAY,MAAM;AAC/B,QAAI,CAAC,cAAc,CAAC,IAAI,QAAS;AACjC,QAAI,QAAQ,MAAM,SAAS;AAC3B,QAAI,QAAQ,MAAM,SAAS,IAAI,QAAQ,eAAe;AAAA,EACxD,GAAG,CAAC,UAAU,CAAC;AAEf,YAAU,MAAM;AAAE,WAAO;AAAA,EAAG,GAAG,CAAC,MAAM,CAAC;AAEvC,QAAM,eAAe,CAAC,MAA8C;AAClE,WAAO;AACP,eAAW,CAAC;AAAA,EACd;AAEA,SACE,qBAAC,SAAI,WAAW,CAAC,qCAAqC,YAAY,WAAW,EAAE,EAAE,KAAK,GAAG,GACtF;AAAA,aACC;AAAA,MAAC;AAAA;AAAA,QACC,SAAS;AAAA,QACT,WAAU;AAAA,QAET;AAAA;AAAA,IACH;AAAA,IAEF;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,IAAI;AAAA,QACJ;AAAA,QACA,UAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA,YAAY,WAAW;AAAA,UACvB;AAAA,QACF,EACG,OAAO,OAAO,EACd,KAAK,GAAG;AAAA,QACX,gBAAc,QAAQ,OAAO;AAAA,QAC7B,oBAAkB;AAAA,QACjB,GAAG;AAAA;AAAA,IACN;AAAA,IACC,SACC,qBAAC,OAAE,IAAI,WAAW,MAAK,SAAQ,WAAU,wEACvC;AAAA,2BAAC,SAAI,OAAM,MAAK,QAAO,MAAK,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,SAAQ,aACpF;AAAA,4BAAC,YAAO,IAAG,MAAK,IAAG,MAAK,GAAE,MAAK;AAAA,QAC/B,oBAAC,UAAK,eAAc,SAAQ,GAAE,mBAAkB;AAAA,SAClD;AAAA,MACC;AAAA,OACH;AAAA,IAED,QAAQ,CAAC,SACR,oBAAC,OAAE,IAAI,WAAW,WAAU,uCAAuC,gBAAK;AAAA,KAE5E;AAEJ;","names":[]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface TimePickerProps {
|
|
4
|
+
/** 24-hour "HH:mm", like "14:30" */
|
|
5
|
+
value?: string | null;
|
|
6
|
+
defaultValue?: string | null;
|
|
7
|
+
onChange?: (time: string | null) => void;
|
|
8
|
+
/** Minutes between choices in the minute column */
|
|
9
|
+
step?: number;
|
|
10
|
+
/** Show 1–12 with am / pm instead of 0–23 */
|
|
11
|
+
hour12?: boolean;
|
|
12
|
+
/** Earliest and latest times, as "HH:mm" */
|
|
13
|
+
min?: string;
|
|
14
|
+
max?: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
hint?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
clearable?: boolean;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
fullWidth?: boolean;
|
|
22
|
+
name?: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
declare function formatTime(t: string, hour12: boolean): string;
|
|
26
|
+
declare function TimePicker({ value, defaultValue, onChange, step, hour12, min, max, label, hint, error, placeholder, clearable, disabled, fullWidth, name, className, }: TimePickerProps): react_jsx_runtime.JSX.Element;
|
|
27
|
+
|
|
28
|
+
export { TimePicker, formatTime };
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ../../components/ui/TimePicker.tsx
|
|
4
|
+
import { useId, useRef, useState } from "react";
|
|
5
|
+
import { ClearKey } from "./DatePicker.js";
|
|
6
|
+
import { Portal } from "./Floating.js";
|
|
7
|
+
import { FieldLabel, FieldMessage } from "./FieldMessage.js";
|
|
8
|
+
import { useFloatingPanel } from "./useFloatingPanel.js";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
var pad = (n) => String(n).padStart(2, "0");
|
|
11
|
+
var toMinutes = (t) => {
|
|
12
|
+
if (!t) return null;
|
|
13
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(t);
|
|
14
|
+
return m ? Number(m[1]) * 60 + Number(m[2]) : null;
|
|
15
|
+
};
|
|
16
|
+
var fromMinutes = (m) => `${pad(Math.floor(m / 60))}:${pad(m % 60)}`;
|
|
17
|
+
function formatTime(t, hour12) {
|
|
18
|
+
const m = toMinutes(t);
|
|
19
|
+
if (m === null) return t;
|
|
20
|
+
const h = Math.floor(m / 60);
|
|
21
|
+
return hour12 ? `${h % 12 || 12}:${pad(m % 60)} ${h < 12 ? "am" : "pm"}` : fromMinutes(m);
|
|
22
|
+
}
|
|
23
|
+
function TimePicker({
|
|
24
|
+
value,
|
|
25
|
+
defaultValue = null,
|
|
26
|
+
onChange,
|
|
27
|
+
step = 15,
|
|
28
|
+
hour12 = false,
|
|
29
|
+
min,
|
|
30
|
+
max,
|
|
31
|
+
label,
|
|
32
|
+
hint,
|
|
33
|
+
error,
|
|
34
|
+
placeholder = "pick a time",
|
|
35
|
+
clearable = false,
|
|
36
|
+
disabled = false,
|
|
37
|
+
fullWidth = false,
|
|
38
|
+
name,
|
|
39
|
+
className = ""
|
|
40
|
+
}) {
|
|
41
|
+
const uid = useId();
|
|
42
|
+
const ref = useRef(null);
|
|
43
|
+
const buttonRef = useRef(null);
|
|
44
|
+
const [internal, setInternal] = useState(defaultValue);
|
|
45
|
+
const current = value !== void 0 ? value : internal;
|
|
46
|
+
const total = toMinutes(current);
|
|
47
|
+
const { open, mounted, toggle, close, setFloating, panelStyle } = useFloatingPanel(ref, {
|
|
48
|
+
onEscape: () => buttonRef.current?.focus()
|
|
49
|
+
});
|
|
50
|
+
const messageId = error || hint ? `${uid}-message` : void 0;
|
|
51
|
+
const lo = toMinutes(min) ?? 0;
|
|
52
|
+
const hi = toMinutes(max) ?? 24 * 60 - 1;
|
|
53
|
+
const hour = total === null ? null : Math.floor(total / 60);
|
|
54
|
+
const minute = total === null ? null : total % 60;
|
|
55
|
+
const set = (t) => {
|
|
56
|
+
setInternal(t);
|
|
57
|
+
onChange?.(t);
|
|
58
|
+
};
|
|
59
|
+
const choose = (h, m) => {
|
|
60
|
+
let t = h * 60 + m;
|
|
61
|
+
if (t < lo) t = lo;
|
|
62
|
+
if (t > hi) t = hi;
|
|
63
|
+
set(fromMinutes(t));
|
|
64
|
+
};
|
|
65
|
+
const hours = hour12 ? Array.from({ length: 12 }, (_, i) => i === 0 ? 12 : i) : Array.from({ length: 24 }, (_, i) => i);
|
|
66
|
+
const minutes = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step).filter((m) => m < 60);
|
|
67
|
+
const isPm = hour !== null && hour >= 12;
|
|
68
|
+
const to24 = (h12, pm) => h12 % 12 + (pm ? 12 : 0);
|
|
69
|
+
const hourOff = (h24) => h24 * 60 + 59 < lo || h24 * 60 > hi;
|
|
70
|
+
const minuteOff = (m) => hour !== null && (hour * 60 + m < lo || hour * 60 + m > hi);
|
|
71
|
+
const columns = [
|
|
72
|
+
{
|
|
73
|
+
id: "hour",
|
|
74
|
+
label: "hour",
|
|
75
|
+
cells: hours.map((h) => {
|
|
76
|
+
const h24 = hour12 ? to24(h, isPm) : h;
|
|
77
|
+
return {
|
|
78
|
+
key: `h${h}`,
|
|
79
|
+
label: hour12 ? String(h) : pad(h),
|
|
80
|
+
selected: hour === h24,
|
|
81
|
+
off: hourOff(h24),
|
|
82
|
+
pick: () => choose(h24, minute ?? 0)
|
|
83
|
+
};
|
|
84
|
+
})
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "minute",
|
|
88
|
+
label: "minute",
|
|
89
|
+
cells: minutes.map((m) => ({
|
|
90
|
+
key: `m${m}`,
|
|
91
|
+
label: pad(m),
|
|
92
|
+
selected: minute === m,
|
|
93
|
+
off: minuteOff(m),
|
|
94
|
+
pick: () => {
|
|
95
|
+
choose(hour ?? Math.floor(lo / 60), m);
|
|
96
|
+
close();
|
|
97
|
+
buttonRef.current?.focus();
|
|
98
|
+
}
|
|
99
|
+
}))
|
|
100
|
+
}
|
|
101
|
+
];
|
|
102
|
+
if (hour12) {
|
|
103
|
+
columns.push({
|
|
104
|
+
id: "period",
|
|
105
|
+
label: "am or pm",
|
|
106
|
+
cells: ["am", "pm"].map((p) => {
|
|
107
|
+
const pm = p === "pm";
|
|
108
|
+
const h24 = to24(hour === null ? 12 : hour % 12 || 12, pm);
|
|
109
|
+
return {
|
|
110
|
+
key: p,
|
|
111
|
+
label: p,
|
|
112
|
+
selected: hour !== null && isPm === pm,
|
|
113
|
+
off: pm ? hi < 12 * 60 : lo >= 12 * 60,
|
|
114
|
+
pick: () => choose(h24, minute ?? 0)
|
|
115
|
+
};
|
|
116
|
+
})
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const colRefs = useRef({});
|
|
120
|
+
const attachColumn = (id) => (el) => {
|
|
121
|
+
colRefs.current[id] = el;
|
|
122
|
+
const sel = el?.querySelector('[aria-selected="true"]');
|
|
123
|
+
if (!el || el.dataset.scrolled) return;
|
|
124
|
+
el.dataset.scrolled = "1";
|
|
125
|
+
if (sel) el.scrollTop = sel.offsetTop - el.clientHeight / 2 + sel.offsetHeight / 2;
|
|
126
|
+
if (id === "hour") el.focus({ preventScroll: true });
|
|
127
|
+
};
|
|
128
|
+
const onColumnKey = (ci) => (e) => {
|
|
129
|
+
const col = columns[ci];
|
|
130
|
+
const enabled = col.cells.map((c, i) => c.off ? -1 : i).filter((i) => i >= 0);
|
|
131
|
+
const at = col.cells.findIndex((c) => c.selected);
|
|
132
|
+
const pos = enabled.indexOf(at);
|
|
133
|
+
const focusCol = (n) => colRefs.current[columns[Math.max(0, Math.min(columns.length - 1, n))].id]?.focus();
|
|
134
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
const d = e.key === "ArrowDown" ? 1 : -1;
|
|
137
|
+
const next = enabled[pos < 0 ? 0 : (pos + d + enabled.length) % enabled.length];
|
|
138
|
+
if (col.id === "minute") choose(hour ?? Math.floor(lo / 60), minutes[next]);
|
|
139
|
+
else col.cells[next]?.pick();
|
|
140
|
+
requestAnimationFrame(
|
|
141
|
+
() => colRefs.current[col.id]?.querySelector('[aria-selected="true"]')?.scrollIntoView({ block: "nearest" })
|
|
142
|
+
);
|
|
143
|
+
} else if (e.key === "ArrowRight") {
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
focusCol(ci + 1);
|
|
146
|
+
} else if (e.key === "ArrowLeft") {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
focusCol(ci - 1);
|
|
149
|
+
} else if (e.key === "Enter") {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
close();
|
|
152
|
+
buttonRef.current?.focus();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
return /* @__PURE__ */ jsxs("div", { className: ["group/field flex flex-col gap-1.5", fullWidth ? "w-full" : ""].join(" "), children: [
|
|
156
|
+
label && /* @__PURE__ */ jsx(FieldLabel, { id: `${uid}-label`, children: label }),
|
|
157
|
+
/* @__PURE__ */ jsxs("div", { ref, className: ["relative", fullWidth ? "w-full" : "inline-block w-44", className].join(" "), children: [
|
|
158
|
+
/* @__PURE__ */ jsxs(
|
|
159
|
+
"button",
|
|
160
|
+
{
|
|
161
|
+
ref: buttonRef,
|
|
162
|
+
type: "button",
|
|
163
|
+
disabled,
|
|
164
|
+
onClick: toggle,
|
|
165
|
+
"aria-haspopup": "dialog",
|
|
166
|
+
"aria-expanded": open,
|
|
167
|
+
"aria-labelledby": label ? `${uid}-label ${uid}-value` : void 0,
|
|
168
|
+
"aria-invalid": error ? true : void 0,
|
|
169
|
+
"aria-describedby": messageId,
|
|
170
|
+
"data-open": open || void 0,
|
|
171
|
+
className: [
|
|
172
|
+
"field group/time relative isolate flex w-full cursor-pointer items-center gap-2.5 rounded-xl py-3 pl-3.5 text-left text-[13.5px]",
|
|
173
|
+
clearable && current ? "pr-10" : "pr-4",
|
|
174
|
+
"focus:outline-none disabled:opacity-40 disabled:pointer-events-none"
|
|
175
|
+
].join(" "),
|
|
176
|
+
children: [
|
|
177
|
+
/* @__PURE__ */ jsxs(
|
|
178
|
+
"svg",
|
|
179
|
+
{
|
|
180
|
+
"aria-hidden": true,
|
|
181
|
+
width: "15",
|
|
182
|
+
height: "15",
|
|
183
|
+
fill: "none",
|
|
184
|
+
stroke: "currentColor",
|
|
185
|
+
strokeWidth: "2",
|
|
186
|
+
strokeLinecap: "round",
|
|
187
|
+
viewBox: "0 0 24 24",
|
|
188
|
+
className: [
|
|
189
|
+
"shrink-0 transition-[color,transform] duration-300 ease-[var(--ease-spring)]",
|
|
190
|
+
open ? "scale-110 text-ink-soft" : "text-ink-faint group-hover/time:text-ink-body"
|
|
191
|
+
].join(" "),
|
|
192
|
+
children: [
|
|
193
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
|
|
194
|
+
/* @__PURE__ */ jsx("path", { d: "M12 7v5l3 2" })
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
),
|
|
198
|
+
/* @__PURE__ */ jsx("span", { id: `${uid}-value`, className: ["tabular-nums", current ? "font-medium text-ink" : "text-ink-faint"].join(" "), children: current ? formatTime(current, hour12) : placeholder })
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
),
|
|
202
|
+
clearable && current && !disabled && /* @__PURE__ */ jsx(
|
|
203
|
+
ClearKey,
|
|
204
|
+
{
|
|
205
|
+
label: "clear time",
|
|
206
|
+
onClear: () => {
|
|
207
|
+
set(null);
|
|
208
|
+
buttonRef.current?.focus();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
),
|
|
212
|
+
name && /* @__PURE__ */ jsx("input", { type: "hidden", name, value: current ?? "" })
|
|
213
|
+
] }),
|
|
214
|
+
mounted && /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx(
|
|
215
|
+
"div",
|
|
216
|
+
{
|
|
217
|
+
ref: setFloating,
|
|
218
|
+
role: "dialog",
|
|
219
|
+
"aria-label": label ? `choose ${label}` : "choose a time",
|
|
220
|
+
className: "rounded-[16px] p-1.5",
|
|
221
|
+
style: panelStyle,
|
|
222
|
+
children: /* @__PURE__ */ jsx("div", { className: "flex gap-1 rounded-[11px] bg-panel-face p-1.5", style: { boxShadow: "var(--panel-face-shadow)" }, children: columns.map((col, ci) => /* @__PURE__ */ jsx(
|
|
223
|
+
"div",
|
|
224
|
+
{
|
|
225
|
+
ref: attachColumn(col.id),
|
|
226
|
+
role: "listbox",
|
|
227
|
+
"aria-label": col.label,
|
|
228
|
+
tabIndex: 0,
|
|
229
|
+
onKeyDown: onColumnKey(ci),
|
|
230
|
+
className: "flex max-h-56 w-14 flex-col gap-0.5 overflow-y-auto rounded-[9px] p-0.5 scroll-thin outline-none focus-visible:ring-2 focus-visible:ring-ink",
|
|
231
|
+
style: { scrollbarWidth: "none" },
|
|
232
|
+
children: col.cells.map((c) => /* @__PURE__ */ jsx(
|
|
233
|
+
"div",
|
|
234
|
+
{
|
|
235
|
+
role: "option",
|
|
236
|
+
"aria-selected": c.selected,
|
|
237
|
+
"aria-disabled": c.off || void 0,
|
|
238
|
+
onClick: () => !c.off && c.pick(),
|
|
239
|
+
className: [
|
|
240
|
+
"grid h-8 shrink-0 place-items-center rounded-[8px] text-[13px] tabular-nums transition-colors duration-100",
|
|
241
|
+
c.off ? "cursor-not-allowed text-ink-ghost" : c.selected ? "cursor-pointer font-semibold text-ink-inverse" : "cursor-pointer text-ink-body hover:bg-surface-active hover:text-ink"
|
|
242
|
+
].join(" "),
|
|
243
|
+
style: c.selected && !c.off ? {
|
|
244
|
+
background: "var(--face-inverse)",
|
|
245
|
+
boxShadow: "var(--face-inverse-shadow)",
|
|
246
|
+
textShadow: "var(--emboss-inverse)"
|
|
247
|
+
} : void 0,
|
|
248
|
+
children: c.label
|
|
249
|
+
},
|
|
250
|
+
c.key
|
|
251
|
+
))
|
|
252
|
+
},
|
|
253
|
+
col.id
|
|
254
|
+
)) })
|
|
255
|
+
}
|
|
256
|
+
) }),
|
|
257
|
+
/* @__PURE__ */ jsx(FieldMessage, { id: messageId, error, hint })
|
|
258
|
+
] });
|
|
259
|
+
}
|
|
260
|
+
export {
|
|
261
|
+
TimePicker,
|
|
262
|
+
formatTime
|
|
263
|
+
};
|
|
264
|
+
//# sourceMappingURL=TimePicker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../components/ui/TimePicker.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useId, useRef, useState } from \"react\";\nimport { ClearKey } from \"./DatePicker\";\nimport { Portal } from \"./Floating\";\nimport { FieldLabel, FieldMessage } from \"./FieldMessage\";\nimport { useFloatingPanel } from \"./useFloatingPanel\";\n\ninterface TimePickerProps {\n /** 24-hour \"HH:mm\", like \"14:30\" */\n value?: string | null;\n defaultValue?: string | null;\n onChange?: (time: string | null) => void;\n /** Minutes between choices in the minute column */\n step?: number;\n /** Show 1–12 with am / pm instead of 0–23 */\n hour12?: boolean;\n /** Earliest and latest times, as \"HH:mm\" */\n min?: string;\n max?: string;\n label?: string;\n hint?: string;\n error?: string;\n placeholder?: string;\n clearable?: boolean;\n disabled?: boolean;\n fullWidth?: boolean;\n name?: string;\n className?: string;\n}\n\nconst pad = (n: number) => String(n).padStart(2, \"0\");\nconst toMinutes = (t?: string | null) => {\n if (!t) return null;\n const m = /^(\\d{1,2}):(\\d{2})$/.exec(t);\n return m ? Number(m[1]) * 60 + Number(m[2]) : null;\n};\nconst fromMinutes = (m: number) => `${pad(Math.floor(m / 60))}:${pad(m % 60)}`;\n\nexport function formatTime(t: string, hour12: boolean) {\n const m = toMinutes(t);\n if (m === null) return t;\n const h = Math.floor(m / 60);\n return hour12 ? `${h % 12 || 12}:${pad(m % 60)} ${h < 12 ? \"am\" : \"pm\"}` : fromMinutes(m);\n}\n\ntype Column = \"hour\" | \"minute\" | \"period\";\n\n/* A field that opens a panel of columns: hours, minutes and, for a\n 12-hour clock, am / pm. The chosen cell in each is a raised key in the\n inverse material. Arrows move within a column and between columns;\n Enter or picking a minute closes it. */\nexport function TimePicker({\n value,\n defaultValue = null,\n onChange,\n step = 15,\n hour12 = false,\n min,\n max,\n label,\n hint,\n error,\n placeholder = \"pick a time\",\n clearable = false,\n disabled = false,\n fullWidth = false,\n name,\n className = \"\",\n}: TimePickerProps) {\n const uid = useId();\n const ref = useRef<HTMLDivElement>(null);\n const buttonRef = useRef<HTMLButtonElement>(null);\n const [internal, setInternal] = useState<string | null>(defaultValue);\n const current = value !== undefined ? value : internal;\n const total = toMinutes(current);\n const { open, mounted, toggle, close, setFloating, panelStyle } = useFloatingPanel(ref, {\n onEscape: () => buttonRef.current?.focus(),\n });\n const messageId = error || hint ? `${uid}-message` : undefined;\n\n const lo = toMinutes(min) ?? 0;\n const hi = toMinutes(max) ?? 24 * 60 - 1;\n const hour = total === null ? null : Math.floor(total / 60);\n const minute = total === null ? null : total % 60;\n\n const set = (t: string | null) => {\n setInternal(t);\n onChange?.(t);\n };\n\n /* Picks a time, nudging the minutes into range when the hour changes */\n const choose = (h: number, m: number) => {\n let t = h * 60 + m;\n if (t < lo) t = lo;\n if (t > hi) t = hi;\n set(fromMinutes(t));\n };\n\n const hours = hour12 ? Array.from({ length: 12 }, (_, i) => (i === 0 ? 12 : i)) : Array.from({ length: 24 }, (_, i) => i);\n const minutes = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step).filter((m) => m < 60);\n const isPm = hour !== null && hour >= 12;\n const to24 = (h12: number, pm: boolean) => (h12 % 12) + (pm ? 12 : 0);\n\n const hourOff = (h24: number) => h24 * 60 + 59 < lo || h24 * 60 > hi;\n const minuteOff = (m: number) => hour !== null && (hour * 60 + m < lo || hour * 60 + m > hi);\n\n type Cell = { key: string; label: string; selected: boolean; off: boolean; pick: () => void };\n const columns: { id: Column; label: string; cells: Cell[] }[] = [\n {\n id: \"hour\",\n label: \"hour\",\n cells: hours.map((h) => {\n const h24 = hour12 ? to24(h, isPm) : h;\n return {\n key: `h${h}`,\n label: hour12 ? String(h) : pad(h),\n selected: hour === h24,\n off: hourOff(h24),\n pick: () => choose(h24, minute ?? 0),\n };\n }),\n },\n {\n id: \"minute\",\n label: \"minute\",\n cells: minutes.map((m) => ({\n key: `m${m}`,\n label: pad(m),\n selected: minute === m,\n off: minuteOff(m),\n pick: () => {\n choose(hour ?? Math.floor(lo / 60), m);\n close();\n buttonRef.current?.focus();\n },\n })),\n },\n ];\n if (hour12) {\n columns.push({\n id: \"period\",\n label: \"am or pm\",\n cells: ([\"am\", \"pm\"] as const).map((p) => {\n const pm = p === \"pm\";\n const h24 = to24(hour === null ? 12 : hour % 12 || 12, pm);\n return {\n key: p,\n label: p,\n selected: hour !== null && isPm === pm,\n off: pm ? hi < 12 * 60 : lo >= 12 * 60,\n pick: () => choose(h24, minute ?? 0),\n };\n }),\n });\n }\n\n const colRefs = useRef<Record<string, HTMLDivElement | null>>({});\n\n /* Each column scrolls its chosen cell into the middle when it mounts */\n const attachColumn = (id: Column) => (el: HTMLDivElement | null) => {\n colRefs.current[id] = el;\n const sel = el?.querySelector<HTMLElement>('[aria-selected=\"true\"]');\n if (!el || el.dataset.scrolled) return;\n el.dataset.scrolled = \"1\";\n if (sel) el.scrollTop = sel.offsetTop - el.clientHeight / 2 + sel.offsetHeight / 2;\n // The panel lives at the end of <body>, so focus goes to it rather than waiting for Tab\n if (id === \"hour\") el.focus({ preventScroll: true });\n };\n\n const onColumnKey = (ci: number) => (e: React.KeyboardEvent) => {\n const col = columns[ci];\n const enabled = col.cells.map((c, i) => (c.off ? -1 : i)).filter((i) => i >= 0);\n const at = col.cells.findIndex((c) => c.selected);\n const pos = enabled.indexOf(at);\n const focusCol = (n: number) =>\n colRefs.current[columns[Math.max(0, Math.min(columns.length - 1, n))].id]?.focus();\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n const d = e.key === \"ArrowDown\" ? 1 : -1;\n const next = enabled[pos < 0 ? 0 : (pos + d + enabled.length) % enabled.length];\n // the minute column closes on pick; from the keyboard, just move\n if (col.id === \"minute\") choose(hour ?? Math.floor(lo / 60), minutes[next]);\n else col.cells[next]?.pick();\n requestAnimationFrame(() =>\n colRefs.current[col.id]?.querySelector('[aria-selected=\"true\"]')?.scrollIntoView({ block: \"nearest\" }),\n );\n } else if (e.key === \"ArrowRight\") {\n e.preventDefault();\n focusCol(ci + 1);\n } else if (e.key === \"ArrowLeft\") {\n e.preventDefault();\n focusCol(ci - 1);\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n close();\n buttonRef.current?.focus();\n }\n };\n\n return (\n <div className={[\"group/field flex flex-col gap-1.5\", fullWidth ? \"w-full\" : \"\"].join(\" \")}>\n {label && <FieldLabel id={`${uid}-label`}>{label}</FieldLabel>}\n <div ref={ref} className={[\"relative\", fullWidth ? \"w-full\" : \"inline-block w-44\", className].join(\" \")}>\n {/* The field's error styling keys off aria-invalid, as Input's does */}\n {/* eslint-disable-next-line jsx-a11y/role-supports-aria-props */}\n <button\n ref={buttonRef}\n type=\"button\"\n disabled={disabled}\n onClick={toggle}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n aria-labelledby={label ? `${uid}-label ${uid}-value` : undefined}\n aria-invalid={error ? true : undefined}\n aria-describedby={messageId}\n data-open={open || undefined}\n className={[\n \"field group/time relative isolate flex w-full cursor-pointer items-center gap-2.5 rounded-xl py-3 pl-3.5 text-left text-[13.5px]\",\n clearable && current ? \"pr-10\" : \"pr-4\",\n \"focus:outline-none disabled:opacity-40 disabled:pointer-events-none\",\n ].join(\" \")}\n >\n <svg\n aria-hidden\n width=\"15\"\n height=\"15\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n viewBox=\"0 0 24 24\"\n className={[\n \"shrink-0 transition-[color,transform] duration-300 ease-[var(--ease-spring)]\",\n open ? \"scale-110 text-ink-soft\" : \"text-ink-faint group-hover/time:text-ink-body\",\n ].join(\" \")}\n >\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M12 7v5l3 2\" />\n </svg>\n <span id={`${uid}-value`} className={[\"tabular-nums\", current ? \"font-medium text-ink\" : \"text-ink-faint\"].join(\" \")}>\n {current ? formatTime(current, hour12) : placeholder}\n </span>\n </button>\n {clearable && current && !disabled && (\n <ClearKey\n label=\"clear time\"\n onClear={() => {\n set(null);\n buttonRef.current?.focus();\n }}\n />\n )}\n {name && <input type=\"hidden\" name={name} value={current ?? \"\"} />}\n </div>\n\n {mounted && (\n <Portal>\n <div\n ref={setFloating}\n role=\"dialog\"\n aria-label={label ? `choose ${label}` : \"choose a time\"}\n className=\"rounded-[16px] p-1.5\"\n style={panelStyle}\n >\n <div className=\"flex gap-1 rounded-[11px] bg-panel-face p-1.5\" style={{ boxShadow: \"var(--panel-face-shadow)\" }}>\n {columns.map((col, ci) => (\n <div\n key={col.id}\n ref={attachColumn(col.id)}\n role=\"listbox\"\n aria-label={col.label}\n tabIndex={0}\n onKeyDown={onColumnKey(ci)}\n className=\"flex max-h-56 w-14 flex-col gap-0.5 overflow-y-auto rounded-[9px] p-0.5 scroll-thin outline-none focus-visible:ring-2 focus-visible:ring-ink\"\n style={{ scrollbarWidth: \"none\" }}\n >\n {col.cells.map((c) => (\n <div\n key={c.key}\n role=\"option\"\n aria-selected={c.selected}\n aria-disabled={c.off || undefined}\n onClick={() => !c.off && c.pick()}\n className={[\n \"grid h-8 shrink-0 place-items-center rounded-[8px] text-[13px] tabular-nums transition-colors duration-100\",\n c.off\n ? \"cursor-not-allowed text-ink-ghost\"\n : c.selected\n ? \"cursor-pointer font-semibold text-ink-inverse\"\n : \"cursor-pointer text-ink-body hover:bg-surface-active hover:text-ink\",\n ].join(\" \")}\n style={\n c.selected && !c.off\n ? {\n background: \"var(--face-inverse)\",\n boxShadow: \"var(--face-inverse-shadow)\",\n textShadow: \"var(--emboss-inverse)\",\n }\n : undefined\n }\n >\n {c.label}\n </div>\n ))}\n </div>\n ))}\n </div>\n </div>\n </Portal>\n )}\n <FieldMessage id={messageId} error={error} hint={hint} />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
type TimelineVariant = "default" | "success" | "warning" | "accent";
|
|
5
|
+
interface TimelineItem {
|
|
6
|
+
key: string;
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
date?: string;
|
|
10
|
+
icon?: React.ReactNode;
|
|
11
|
+
variant?: TimelineVariant;
|
|
12
|
+
}
|
|
13
|
+
interface TimelineProps {
|
|
14
|
+
items: TimelineItem[];
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
declare function Timeline({ items, className }: TimelineProps): react_jsx_runtime.JSX.Element;
|
|
18
|
+
|
|
19
|
+
export { Timeline };
|