@blurrow/ui 0.2.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 +48 -36
- 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 +172 -5
- 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 +6 -6
- package/dist/registry/Alert.tsx +13 -10
- package/dist/registry/AuthorCard.tsx +7 -7
- package/dist/registry/Avatar.tsx +5 -5
- package/dist/registry/Badge.tsx +12 -15
- package/dist/registry/Banner.tsx +15 -18
- package/dist/registry/Breadcrumb.tsx +3 -3
- package/dist/registry/Button.tsx +58 -69
- package/dist/registry/ButtonGroup.tsx +9 -9
- package/dist/registry/Card.tsx +15 -15
- package/dist/registry/CardShell.tsx +6 -6
- package/dist/registry/Carousel.tsx +19 -15
- package/dist/registry/Checkbox.tsx +14 -9
- package/dist/registry/CloseButton.tsx +2 -2
- package/dist/registry/CodeBlock.tsx +15 -15
- package/dist/registry/ColorPicker.tsx +404 -0
- package/dist/registry/Combobox.tsx +318 -0
- package/dist/registry/CommandPalette.tsx +31 -24
- package/dist/registry/ContextMenu.tsx +241 -0
- package/dist/registry/DataList.tsx +4 -4
- package/dist/registry/DatePicker.tsx +494 -0
- package/dist/registry/DateRangePicker.tsx +232 -0
- package/dist/registry/Drawer.tsx +17 -15
- package/dist/registry/DropdownMenu.tsx +39 -77
- package/dist/registry/EmptyState.tsx +8 -8
- 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 +54 -43
- package/dist/registry/Kbd.tsx +4 -5
- package/dist/registry/Modal.tsx +17 -15
- package/dist/registry/NumberInput.tsx +183 -0
- package/dist/registry/OtpInput.tsx +219 -0
- package/dist/registry/Pagination.tsx +15 -18
- package/dist/registry/Popover.tsx +40 -45
- package/dist/registry/Progress.tsx +4 -4
- package/dist/registry/QuoteCard.tsx +7 -7
- package/dist/registry/Radio.tsx +24 -21
- package/dist/registry/RadioCards.tsx +247 -0
- package/dist/registry/ReactionBar.tsx +5 -5
- package/dist/registry/ReadingProgress.tsx +2 -2
- package/dist/registry/ReadingTime.tsx +2 -2
- package/dist/registry/Select.tsx +28 -53
- package/dist/registry/Separator.tsx +52 -0
- package/dist/registry/ShareButtons.tsx +2 -2
- package/dist/registry/Sidebar.tsx +18 -18
- package/dist/registry/Skeleton.tsx +1 -1
- package/dist/registry/Slider.tsx +229 -0
- package/dist/registry/Spinner.tsx +32 -0
- package/dist/registry/SpotlightCard.tsx +6 -6
- package/dist/registry/Stat.tsx +4 -4
- package/dist/registry/StatCard.tsx +19 -19
- package/dist/registry/Steps.tsx +7 -7
- package/dist/registry/Table.tsx +12 -10
- package/dist/registry/TableOfContents.tsx +8 -8
- package/dist/registry/Tabs.tsx +6 -6
- package/dist/registry/TagInput.tsx +9 -9
- package/dist/registry/Textarea.tsx +5 -5
- package/dist/registry/TimePicker.tsx +315 -0
- package/dist/registry/Timeline.tsx +14 -14
- package/dist/registry/Toast.tsx +15 -12
- package/dist/registry/Toggle.tsx +7 -10
- package/dist/registry/Tooltip.tsx +21 -73
- package/dist/registry/styles.css +868 -62
- package/dist/registry/useFloatingPanel.ts +112 -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 +1 -1
|
@@ -42,8 +42,8 @@ export function ReadingProgress({ className = "" }: ReadingProgressProps) {
|
|
|
42
42
|
style={{
|
|
43
43
|
// transform instead of width: no layout on every scroll frame
|
|
44
44
|
transform: `scaleX(${progress / 100})`,
|
|
45
|
-
background: "
|
|
46
|
-
boxShadow: "
|
|
45
|
+
background: "var(--progress-bar)",
|
|
46
|
+
boxShadow: "var(--progress-bar-glow)",
|
|
47
47
|
}}
|
|
48
48
|
/>
|
|
49
49
|
</div>
|
|
@@ -9,7 +9,7 @@ export function ReadingTime({ minutes, className = "" }: ReadingTimeProps) {
|
|
|
9
9
|
return (
|
|
10
10
|
<span
|
|
11
11
|
className={[
|
|
12
|
-
"inline-flex items-center gap-1.5 text-[12px] font-medium text-
|
|
12
|
+
"inline-flex items-center gap-1.5 text-[12px] font-medium text-ink-muted",
|
|
13
13
|
className,
|
|
14
14
|
]
|
|
15
15
|
.filter(Boolean)
|
|
@@ -24,7 +24,7 @@ export function ReadingTime({ minutes, className = "" }: ReadingTimeProps) {
|
|
|
24
24
|
strokeWidth="2"
|
|
25
25
|
strokeLinecap="round"
|
|
26
26
|
strokeLinejoin="round"
|
|
27
|
-
className="text-
|
|
27
|
+
className="text-ink-faint"
|
|
28
28
|
>
|
|
29
29
|
<circle cx="12" cy="12" r="10" />
|
|
30
30
|
<polyline points="12 6 12 12 16 14" />
|
package/dist/registry/Select.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useState, useRef, useEffect, useId } from "react";
|
|
4
|
+
import { Portal } from "./Floating";
|
|
5
|
+
import { useFloatingPanel } from "./useFloatingPanel";
|
|
4
6
|
|
|
5
7
|
interface SelectOption {
|
|
6
8
|
value: string;
|
|
@@ -33,41 +35,20 @@ export function Select({
|
|
|
33
35
|
fullWidth = false,
|
|
34
36
|
className = "",
|
|
35
37
|
}: SelectProps) {
|
|
36
|
-
const [open, setOpen] = useState(false);
|
|
37
|
-
// The list stays mounted while it folds away, and only unfolds once painted
|
|
38
|
-
const [mounted, setMounted] = useState(false);
|
|
39
|
-
const [entered, setEntered] = useState(false);
|
|
40
|
-
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
41
38
|
const [active, setActive] = useState(0);
|
|
42
39
|
const ref = useRef<HTMLDivElement>(null);
|
|
43
40
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
44
41
|
const listRef = useRef<HTMLDivElement>(null);
|
|
45
42
|
const uid = useId();
|
|
43
|
+
// The list is portalled to the body and pinned under the field
|
|
44
|
+
const { open, mounted, close, show, setFloating, panelStyle } = useFloatingPanel(ref, {
|
|
45
|
+
matchWidth: true,
|
|
46
|
+
onEscape: () => buttonRef.current?.focus(),
|
|
47
|
+
});
|
|
46
48
|
|
|
47
49
|
const selectedIndex = options.findIndex((o) => o.value === value);
|
|
48
50
|
const selected = options[selectedIndex];
|
|
49
51
|
|
|
50
|
-
const close = () => {
|
|
51
|
-
setOpen(false);
|
|
52
|
-
setEntered(false);
|
|
53
|
-
if (closeTimer.current) clearTimeout(closeTimer.current);
|
|
54
|
-
closeTimer.current = setTimeout(() => setMounted(false), 200);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
useEffect(() => () => {
|
|
58
|
-
if (closeTimer.current) clearTimeout(closeTimer.current);
|
|
59
|
-
}, []);
|
|
60
|
-
|
|
61
|
-
useEffect(() => {
|
|
62
|
-
function onOutside(e: MouseEvent) {
|
|
63
|
-
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
64
|
-
close();
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
document.addEventListener("mousedown", onOutside);
|
|
68
|
-
return () => document.removeEventListener("mousedown", onOutside);
|
|
69
|
-
}, []);
|
|
70
|
-
|
|
71
52
|
useEffect(() => {
|
|
72
53
|
if (!open) return;
|
|
73
54
|
listRef.current
|
|
@@ -77,10 +58,7 @@ export function Select({
|
|
|
77
58
|
|
|
78
59
|
const openList = () => {
|
|
79
60
|
setActive(selectedIndex >= 0 ? selectedIndex : 0);
|
|
80
|
-
|
|
81
|
-
setOpen(true);
|
|
82
|
-
setMounted(true);
|
|
83
|
-
requestAnimationFrame(() => requestAnimationFrame(() => setEntered(true)));
|
|
61
|
+
show();
|
|
84
62
|
};
|
|
85
63
|
|
|
86
64
|
const pick = (index: number) => {
|
|
@@ -143,7 +121,7 @@ export function Select({
|
|
|
143
121
|
{label && (
|
|
144
122
|
<span
|
|
145
123
|
id={`${uid}-label`}
|
|
146
|
-
className="text-[12.5px] font-medium text-
|
|
124
|
+
className="text-[12.5px] font-medium text-ink-label tracking-[-0.005em]"
|
|
147
125
|
>
|
|
148
126
|
{label}
|
|
149
127
|
</span>
|
|
@@ -156,6 +134,9 @@ export function Select({
|
|
|
156
134
|
<button
|
|
157
135
|
ref={buttonRef}
|
|
158
136
|
type="button"
|
|
137
|
+
/* A select-only combobox: the role is what lets the button point
|
|
138
|
+
at the active option while focus stays on it */
|
|
139
|
+
role="combobox"
|
|
159
140
|
disabled={disabled}
|
|
160
141
|
onClick={() => (open ? close() : openList())}
|
|
161
142
|
onKeyDown={onKeyDown}
|
|
@@ -166,7 +147,7 @@ export function Select({
|
|
|
166
147
|
aria-activedescendant={open ? `${uid}-opt-${active}` : undefined}
|
|
167
148
|
className={[
|
|
168
149
|
"group/select relative isolate inline-flex items-center justify-between gap-3 w-full",
|
|
169
|
-
"rounded-xl px-4 py-3 text-[13.5px] font-medium text-
|
|
150
|
+
"rounded-xl px-4 py-3 text-[13.5px] font-medium text-ink",
|
|
170
151
|
"field cursor-pointer select-none",
|
|
171
152
|
"focus:outline-none disabled:opacity-40 disabled:pointer-events-none",
|
|
172
153
|
].join(" ")}
|
|
@@ -176,7 +157,7 @@ export function Select({
|
|
|
176
157
|
id={`${uid}-value`}
|
|
177
158
|
className={[
|
|
178
159
|
"relative z-10 truncate",
|
|
179
|
-
selected ? "text-
|
|
160
|
+
selected ? "text-ink" : "text-ink-faint font-normal",
|
|
180
161
|
].join(" ")}
|
|
181
162
|
>
|
|
182
163
|
{selected ? selected.label : placeholder}
|
|
@@ -191,7 +172,7 @@ export function Select({
|
|
|
191
172
|
viewBox="0 0 24 24"
|
|
192
173
|
className={[
|
|
193
174
|
"relative z-10 shrink-0 transition-[transform,color] duration-300 ease-[var(--ease-spring)]",
|
|
194
|
-
open ? "rotate-180 text-
|
|
175
|
+
open ? "rotate-180 text-ink" : "text-ink-faint group-hover/select:text-ink-label",
|
|
195
176
|
].join(" ")}
|
|
196
177
|
>
|
|
197
178
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
|
@@ -203,7 +184,7 @@ export function Select({
|
|
|
203
184
|
className="block absolute inset-y-0 w-[30%] -left-[30%] animate-input-shimmer"
|
|
204
185
|
style={{
|
|
205
186
|
background:
|
|
206
|
-
"linear-gradient(90deg, transparent 0%,
|
|
187
|
+
"linear-gradient(90deg, transparent 0%, var(--shimmer) 50%, transparent 100%)",
|
|
207
188
|
}}
|
|
208
189
|
/>
|
|
209
190
|
</span>
|
|
@@ -211,23 +192,14 @@ export function Select({
|
|
|
211
192
|
</button>
|
|
212
193
|
|
|
213
194
|
{mounted && (
|
|
195
|
+
<Portal>
|
|
214
196
|
<div
|
|
215
|
-
|
|
197
|
+
ref={setFloating}
|
|
198
|
+
className="p-1.5 rounded-[16px]"
|
|
216
199
|
style={{
|
|
217
|
-
|
|
200
|
+
...panelStyle,
|
|
218
201
|
maxWidth: "min(420px, calc(100vw - 2rem))",
|
|
219
202
|
width: "max-content",
|
|
220
|
-
background: "var(--panel-bezel)",
|
|
221
|
-
boxShadow: "var(--overlay-shadow)",
|
|
222
|
-
/* Unfolds downward out of the field: a clip reveals it top to
|
|
223
|
-
bottom while it fades and settles. The clip's sides reach
|
|
224
|
-
past the panel so its shadow is never cut off. */
|
|
225
|
-
opacity: entered ? 1 : 0,
|
|
226
|
-
transform: entered ? "none" : "translateY(-4px) scale(0.98)",
|
|
227
|
-
clipPath: entered ? "inset(-48px -48px -48px -48px)" : "inset(-48px -48px 55% -48px)",
|
|
228
|
-
transition: entered
|
|
229
|
-
? "opacity 180ms ease-out, transform 360ms var(--ease-out-soft), clip-path 360ms var(--ease-out-soft)"
|
|
230
|
-
: "opacity 160ms ease-in, transform 200ms ease-in, clip-path 200ms ease-in",
|
|
231
203
|
}}
|
|
232
204
|
>
|
|
233
205
|
<div
|
|
@@ -235,7 +207,7 @@ export function Select({
|
|
|
235
207
|
id={`${uid}-list`}
|
|
236
208
|
role="listbox"
|
|
237
209
|
aria-labelledby={label ? `${uid}-label` : undefined}
|
|
238
|
-
className="bg-
|
|
210
|
+
className="bg-panel-face rounded-[11px] p-1 flex flex-col gap-0.5 max-h-72 overflow-y-auto scroll-thin"
|
|
239
211
|
style={{ boxShadow: "var(--panel-face-shadow)" }}
|
|
240
212
|
>
|
|
241
213
|
{options.map((opt, i) => {
|
|
@@ -254,8 +226,10 @@ export function Select({
|
|
|
254
226
|
className={[
|
|
255
227
|
"flex items-center justify-between gap-3 w-full px-2.5 py-2 text-[13px] rounded-[8px] cursor-pointer",
|
|
256
228
|
"transition-colors duration-100",
|
|
257
|
-
isSelected ? "font-medium
|
|
258
|
-
|
|
229
|
+
isSelected ? "font-medium" : "",
|
|
230
|
+
// one ink per state: utilities sort by name, so text-ink-body would beat text-ink
|
|
231
|
+
isSelected || isActive ? "text-ink" : "text-ink-body",
|
|
232
|
+
isActive ? "bg-surface-active/80" : "",
|
|
259
233
|
].join(" ")}
|
|
260
234
|
style={{
|
|
261
235
|
animation: `select-option-in 300ms var(--ease-out-soft) ${40 + i * 22}ms both`,
|
|
@@ -265,7 +239,7 @@ export function Select({
|
|
|
265
239
|
{isSelected && (
|
|
266
240
|
<svg
|
|
267
241
|
aria-hidden
|
|
268
|
-
className="shrink-0 text-
|
|
242
|
+
className="shrink-0 text-ink"
|
|
269
243
|
width="13"
|
|
270
244
|
height="13"
|
|
271
245
|
fill="none"
|
|
@@ -281,10 +255,11 @@ export function Select({
|
|
|
281
255
|
})}
|
|
282
256
|
</div>
|
|
283
257
|
</div>
|
|
258
|
+
</Portal>
|
|
284
259
|
)}
|
|
285
260
|
</div>
|
|
286
261
|
|
|
287
|
-
{hint && <p className="text-[11.5px] text-
|
|
262
|
+
{hint && <p className="text-[11.5px] text-ink-muted mt-0.5">{hint}</p>}
|
|
288
263
|
</div>
|
|
289
264
|
);
|
|
290
265
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface SeparatorProps {
|
|
4
|
+
orientation?: "horizontal" | "vertical";
|
|
5
|
+
/** A few words set into the middle of the line, like "or" */
|
|
6
|
+
label?: React.ReactNode;
|
|
7
|
+
/** Only draws a line; screen readers skip it */
|
|
8
|
+
decorative?: boolean;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* A groove scored into the surface: a dark line with a lit lip just
|
|
13
|
+
under it, the way a cut catches light from above. */
|
|
14
|
+
function Groove({ vertical }: { vertical: boolean }) {
|
|
15
|
+
return (
|
|
16
|
+
<span
|
|
17
|
+
aria-hidden
|
|
18
|
+
className={vertical ? "block w-px self-stretch" : "block h-px flex-1"}
|
|
19
|
+
style={{
|
|
20
|
+
background: "var(--groove)",
|
|
21
|
+
boxShadow: vertical ? "var(--groove-lip)" : "var(--groove-lip-below)",
|
|
22
|
+
}}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Separator({ orientation = "horizontal", label, decorative = false, className = "" }: SeparatorProps) {
|
|
28
|
+
const vertical = orientation === "vertical";
|
|
29
|
+
const a11y = decorative
|
|
30
|
+
? { "aria-hidden": true as const }
|
|
31
|
+
: { role: "separator", "aria-orientation": orientation };
|
|
32
|
+
|
|
33
|
+
if (vertical) {
|
|
34
|
+
return (
|
|
35
|
+
<span {...a11y} className={["inline-flex self-stretch", className].join(" ")}>
|
|
36
|
+
<Groove vertical />
|
|
37
|
+
</span>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div {...a11y} className={["flex w-full items-center gap-3", className].join(" ")}>
|
|
43
|
+
<Groove vertical={false} />
|
|
44
|
+
{label && (
|
|
45
|
+
<>
|
|
46
|
+
<span className="shrink-0 text-[11.5px] font-medium text-ink-faint">{label}</span>
|
|
47
|
+
<Groove vertical={false} />
|
|
48
|
+
</>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -110,7 +110,7 @@ export function ShareButtons({
|
|
|
110
110
|
onClick: handleCopy as () => void,
|
|
111
111
|
content: (
|
|
112
112
|
<>
|
|
113
|
-
<span key={String(copied)} className={["animate-checkbox-in", copied ? "text-
|
|
113
|
+
<span key={String(copied)} className={["animate-checkbox-in", copied ? "text-success-soft" : ""].join(" ")}>
|
|
114
114
|
{copied ? <CheckIcon /> : <LinkIcon />}
|
|
115
115
|
</span>
|
|
116
116
|
<span className="w-[62px] text-left">{copied ? "copied" : "copy link"}</span>
|
|
@@ -124,7 +124,7 @@ export function ShareButtons({
|
|
|
124
124
|
|
|
125
125
|
return (
|
|
126
126
|
<div className={className}>
|
|
127
|
-
<p className="text-[12px] font-medium text-
|
|
127
|
+
<p className="text-[12px] font-medium text-ink-muted lowercase mb-3">
|
|
128
128
|
Share
|
|
129
129
|
</p>
|
|
130
130
|
<ButtonGroup label="Share" items={keys} />
|
|
@@ -53,8 +53,8 @@ export function Sidebar({
|
|
|
53
53
|
setPill({ top: br.top - nr.top, height: br.height, ready: true });
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// Before paint, so the pill is never seen out of place
|
|
57
|
+
useLayoutEffect(() => { measurePill(current); }, [current]);
|
|
58
58
|
|
|
59
59
|
useEffect(() => {
|
|
60
60
|
const ro = new ResizeObserver(() => measurePill(current));
|
|
@@ -73,27 +73,27 @@ export function Sidebar({
|
|
|
73
73
|
className={["rounded-[20px] p-1.5", className].join(" ")}
|
|
74
74
|
style={{ background: "var(--panel-bezel)", boxShadow: "var(--panel-shadow)" }}
|
|
75
75
|
>
|
|
76
|
-
<div className="bg-
|
|
76
|
+
<div className="bg-panel-face rounded-[15px] overflow-hidden" style={{ boxShadow: "var(--panel-face-shadow)" }}>
|
|
77
77
|
{header && (
|
|
78
|
-
<div className="px-3 py-3 border-b border-
|
|
78
|
+
<div className="px-3 py-3 border-b border-line flex items-center gap-2.5">
|
|
79
79
|
{header.logo && (
|
|
80
80
|
<div
|
|
81
|
-
className="w-7 h-7 rounded-[8px] flex items-center justify-center shrink-0 text-
|
|
82
|
-
style={{ background: "var(--face-
|
|
81
|
+
className="w-7 h-7 rounded-[8px] flex items-center justify-center shrink-0 text-ink-inverse"
|
|
82
|
+
style={{ background: "var(--face-inverse)", boxShadow: "var(--face-inverse-shadow)" }}
|
|
83
83
|
>
|
|
84
84
|
{header.logo}
|
|
85
85
|
</div>
|
|
86
86
|
)}
|
|
87
87
|
<div className="flex-1 min-w-0">
|
|
88
|
-
<p className="text-[13px] font-semibold text-
|
|
88
|
+
<p className="text-[13px] font-semibold text-ink leading-none truncate">{header.title}</p>
|
|
89
89
|
{header.subtitle && (
|
|
90
|
-
<p className="text-[11px] text-
|
|
90
|
+
<p className="text-[11px] text-ink-faint mt-0.5 leading-none truncate">{header.subtitle}</p>
|
|
91
91
|
)}
|
|
92
92
|
</div>
|
|
93
93
|
<button
|
|
94
94
|
type="button"
|
|
95
95
|
aria-label="Switch workspace"
|
|
96
|
-
className="shrink-0 w-6 h-6 flex items-center justify-center text-
|
|
96
|
+
className="shrink-0 w-6 h-6 flex items-center justify-center text-ink-faint hover:text-ink transition-colors cursor-pointer rounded-[7px] hover:bg-surface-active"
|
|
97
97
|
>
|
|
98
98
|
<svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
99
99
|
<path strokeLinecap="round" strokeLinejoin="round" d="M8 9l4-4 4 4M8 15l4 4 4-4" />
|
|
@@ -110,8 +110,8 @@ export function Sidebar({
|
|
|
110
110
|
style={{
|
|
111
111
|
top: pill.top,
|
|
112
112
|
height: pill.height,
|
|
113
|
-
background: "var(--face-
|
|
114
|
-
boxShadow: "var(--face-
|
|
113
|
+
background: "var(--face-inverse)",
|
|
114
|
+
boxShadow: "var(--face-inverse-shadow)",
|
|
115
115
|
transition: "top 300ms var(--ease-spring), height 300ms var(--ease-spring)",
|
|
116
116
|
}}
|
|
117
117
|
/>
|
|
@@ -120,7 +120,7 @@ export function Sidebar({
|
|
|
120
120
|
{sections.map((section, si) => (
|
|
121
121
|
<div key={si} className="flex flex-col gap-0.5">
|
|
122
122
|
{section.title && (
|
|
123
|
-
<p className="relative z-10 px-2.5 pt-1 pb-1.5 text-[11.5px] font-medium text-
|
|
123
|
+
<p className="relative z-10 px-2.5 pt-1 pb-1.5 text-[11.5px] font-medium text-ink-faint lowercase">
|
|
124
124
|
{section.title}
|
|
125
125
|
</p>
|
|
126
126
|
)}
|
|
@@ -139,8 +139,8 @@ export function Sidebar({
|
|
|
139
139
|
"transition-colors duration-200 cursor-pointer",
|
|
140
140
|
"disabled:opacity-35 disabled:pointer-events-none",
|
|
141
141
|
isActive
|
|
142
|
-
? "text-
|
|
143
|
-
: "text-
|
|
142
|
+
? "text-ink-inverse"
|
|
143
|
+
: "text-ink-body hover:text-ink hover:bg-surface-active/80",
|
|
144
144
|
].join(" ")}
|
|
145
145
|
>
|
|
146
146
|
{item.icon && (
|
|
@@ -148,8 +148,8 @@ export function Sidebar({
|
|
|
148
148
|
className={[
|
|
149
149
|
"shrink-0 w-[22px] h-[22px] rounded-md flex items-center justify-center transition-colors duration-200",
|
|
150
150
|
isActive
|
|
151
|
-
? "bg-
|
|
152
|
-
: "text-
|
|
151
|
+
? "bg-ink-inverse/12 shadow-[inset_0_1px_0_var(--inverse-lip)]"
|
|
152
|
+
: "text-ink-muted [background:var(--face)] [box-shadow:var(--face-shadow)]",
|
|
153
153
|
].join(" ")}
|
|
154
154
|
>
|
|
155
155
|
{item.icon}
|
|
@@ -163,8 +163,8 @@ export function Sidebar({
|
|
|
163
163
|
className={[
|
|
164
164
|
"shrink-0 text-[11px] font-medium px-1.5 py-0.5 rounded-md tabular-nums leading-none transition-colors duration-200",
|
|
165
165
|
isActive
|
|
166
|
-
? "bg-
|
|
167
|
-
: "bg-
|
|
166
|
+
? "bg-ink-inverse/20 text-ink-inverse/80"
|
|
167
|
+
: "bg-surface-active text-ink-muted",
|
|
168
168
|
].join(" ")}
|
|
169
169
|
>
|
|
170
170
|
{item.badge}
|
|
@@ -44,7 +44,7 @@ export function Skeleton({
|
|
|
44
44
|
className={["rounded-[20px] p-1.5", className].join(" ")}
|
|
45
45
|
style={{ background: "var(--panel-bezel)", boxShadow: "var(--panel-shadow)" }}
|
|
46
46
|
>
|
|
47
|
-
<div className="bg-
|
|
47
|
+
<div className="bg-panel-face rounded-[15px] p-1.5" style={{ boxShadow: "var(--panel-face-shadow)" }}>
|
|
48
48
|
<div className={[base, "w-full rounded-[11px]"].join(" ")} style={{ ...well, height: height ?? 160 }} />
|
|
49
49
|
<div className="px-3.5 pt-4 pb-3.5 flex flex-col gap-2.5">
|
|
50
50
|
<div className={[base, "h-3.5 w-3/5"].join(" ")} style={well} />
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useId, useRef, useState } from "react";
|
|
4
|
+
import { FieldLabel } from "./FieldMessage";
|
|
5
|
+
|
|
6
|
+
type SliderValue = number | [number, number];
|
|
7
|
+
|
|
8
|
+
interface SliderProps<V extends SliderValue> {
|
|
9
|
+
/** One number for a slider, two for a range */
|
|
10
|
+
value?: V;
|
|
11
|
+
defaultValue?: V;
|
|
12
|
+
onChange?: (value: V) => void;
|
|
13
|
+
/** Fires once the knob is let go, for work too heavy to do while dragging */
|
|
14
|
+
onCommit?: (value: V) => void;
|
|
15
|
+
min?: number;
|
|
16
|
+
max?: number;
|
|
17
|
+
step?: number;
|
|
18
|
+
label?: string;
|
|
19
|
+
/** Show the value beside the label; pass a function to format it */
|
|
20
|
+
showValue?: boolean | ((value: V) => React.ReactNode);
|
|
21
|
+
/** Small labels under the track, at these values */
|
|
22
|
+
marks?: number[];
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
/** Name of a hidden input (two for a range: name[0], name[1]) */
|
|
25
|
+
name?: string;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));
|
|
30
|
+
const decimals = (n: number) => (String(n).split(".")[1] ?? "").length;
|
|
31
|
+
|
|
32
|
+
/* A sunken track with the filled part in the inverse material, and a
|
|
33
|
+
raised knob (or two, for a range) that you drag along it. The knob
|
|
34
|
+
swells while held; arrows step, shift steps by ten, page keys by a
|
|
35
|
+
tenth of the range, home and end jump to the ends. */
|
|
36
|
+
export function Slider<V extends SliderValue = number>({
|
|
37
|
+
value,
|
|
38
|
+
defaultValue,
|
|
39
|
+
onChange,
|
|
40
|
+
onCommit,
|
|
41
|
+
min = 0,
|
|
42
|
+
max = 100,
|
|
43
|
+
step = 1,
|
|
44
|
+
label,
|
|
45
|
+
showValue = false,
|
|
46
|
+
marks,
|
|
47
|
+
disabled = false,
|
|
48
|
+
name,
|
|
49
|
+
className = "",
|
|
50
|
+
}: SliderProps<V>) {
|
|
51
|
+
const uid = useId();
|
|
52
|
+
const trackRef = useRef<HTMLDivElement>(null);
|
|
53
|
+
const knobs = useRef<(HTMLSpanElement | null)[]>([]);
|
|
54
|
+
const isRange = Array.isArray(value ?? defaultValue);
|
|
55
|
+
const [internal, setInternal] = useState<V>(() => (defaultValue ?? value ?? (min as V)) as V);
|
|
56
|
+
const current = (value !== undefined ? value : internal) as V;
|
|
57
|
+
const values: number[] = Array.isArray(current) ? [...current] : [current as number];
|
|
58
|
+
const [dragging, setDragging] = useState<number | null>(null);
|
|
59
|
+
const digits = decimals(step);
|
|
60
|
+
|
|
61
|
+
const snap = (n: number) => Number(clamp(Math.round((n - min) / step) * step + min, min, max).toFixed(digits));
|
|
62
|
+
const pct = (n: number) => ((n - min) / (max - min)) * 100;
|
|
63
|
+
|
|
64
|
+
const emit = (next: number[], commit = false) => {
|
|
65
|
+
const v = (isRange ? [next[0], next[1]] : next[0]) as V;
|
|
66
|
+
setInternal(v);
|
|
67
|
+
if (next.some((n, i) => n !== values[i])) onChange?.(v);
|
|
68
|
+
if (commit) onCommit?.(v);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/* Each knob is kept on its own side of the other */
|
|
72
|
+
const setAt = (i: number, n: number, commit = false) => {
|
|
73
|
+
const next = [...values];
|
|
74
|
+
next[i] = snap(n);
|
|
75
|
+
if (isRange) next[i] = i === 0 ? Math.min(next[0], values[1]) : Math.max(next[1], values[0]);
|
|
76
|
+
emit(next, commit);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const fromPointer = (clientX: number) => {
|
|
80
|
+
const r = trackRef.current!.getBoundingClientRect();
|
|
81
|
+
return min + clamp((clientX - r.left) / r.width, 0, 1) * (max - min);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const onPointerDown = (e: React.PointerEvent) => {
|
|
85
|
+
if (disabled || e.button !== 0) return;
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
const n = fromPointer(e.clientX);
|
|
88
|
+
// Grab whichever knob is nearer; on a tie, the one that can move that way
|
|
89
|
+
let i = 0;
|
|
90
|
+
if (isRange) {
|
|
91
|
+
const d0 = Math.abs(n - values[0]);
|
|
92
|
+
const d1 = Math.abs(n - values[1]);
|
|
93
|
+
i = d1 < d0 || (d0 === d1 && n > values[1]) ? 1 : 0;
|
|
94
|
+
}
|
|
95
|
+
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
96
|
+
setDragging(i);
|
|
97
|
+
knobs.current[i]?.focus();
|
|
98
|
+
setAt(i, n);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const onKeyDown = (i: number) => (e: React.KeyboardEvent) => {
|
|
102
|
+
const big = Math.max(step, (max - min) / 10);
|
|
103
|
+
const moves: Record<string, number> = {
|
|
104
|
+
ArrowRight: step * (e.shiftKey ? 10 : 1),
|
|
105
|
+
ArrowUp: step * (e.shiftKey ? 10 : 1),
|
|
106
|
+
ArrowLeft: -step * (e.shiftKey ? 10 : 1),
|
|
107
|
+
ArrowDown: -step * (e.shiftKey ? 10 : 1),
|
|
108
|
+
PageUp: big,
|
|
109
|
+
PageDown: -big,
|
|
110
|
+
};
|
|
111
|
+
if (e.key in moves) {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
setAt(i, values[i] + moves[e.key], true);
|
|
114
|
+
} else if (e.key === "Home") {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
setAt(i, min, true);
|
|
117
|
+
} else if (e.key === "End") {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
setAt(i, max, true);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const shown =
|
|
124
|
+
typeof showValue === "function"
|
|
125
|
+
? showValue(current)
|
|
126
|
+
: showValue
|
|
127
|
+
? values.join(" – ")
|
|
128
|
+
: null;
|
|
129
|
+
|
|
130
|
+
const lo = isRange ? pct(values[0]) : 0;
|
|
131
|
+
const hi = pct(values[isRange ? 1 : 0]);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div className={["flex w-full flex-col gap-2.5", disabled ? "opacity-40" : "", className].join(" ")}>
|
|
135
|
+
{(label || shown !== null) && (
|
|
136
|
+
<div className="flex items-center justify-between gap-4">
|
|
137
|
+
{label && <FieldLabel id={`${uid}-label`}>{label}</FieldLabel>}
|
|
138
|
+
{shown !== null && <span className="text-[12.5px] tabular-nums text-ink-muted">{shown}</span>}
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
<div
|
|
143
|
+
className={["relative flex h-6 touch-none items-center", disabled ? "pointer-events-none" : "cursor-pointer"].join(" ")}
|
|
144
|
+
onPointerDown={onPointerDown}
|
|
145
|
+
onPointerMove={(e) => dragging !== null && setAt(dragging, fromPointer(e.clientX))}
|
|
146
|
+
onPointerUp={() => {
|
|
147
|
+
if (dragging !== null) onCommit?.(current);
|
|
148
|
+
setDragging(null);
|
|
149
|
+
}}
|
|
150
|
+
onPointerCancel={() => setDragging(null)}
|
|
151
|
+
>
|
|
152
|
+
{/* The track: a groove cut into the surface */}
|
|
153
|
+
<div
|
|
154
|
+
ref={trackRef}
|
|
155
|
+
className="relative h-2 w-full rounded-full"
|
|
156
|
+
style={{ background: "var(--track)", boxShadow: "var(--track-shadow)" }}
|
|
157
|
+
>
|
|
158
|
+
<div
|
|
159
|
+
className="absolute inset-y-px rounded-full"
|
|
160
|
+
style={{
|
|
161
|
+
left: `calc(${lo}% + ${isRange ? 0 : 1}px)`,
|
|
162
|
+
width: `calc(${hi - lo}% - ${isRange ? 0 : 1}px)`,
|
|
163
|
+
background: "var(--face-inverse)",
|
|
164
|
+
boxShadow: "var(--fill-shadow)",
|
|
165
|
+
transition: dragging === null ? "left 200ms var(--ease-out-soft), width 200ms var(--ease-out-soft)" : "none",
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{values.map((v, i) => (
|
|
171
|
+
<span
|
|
172
|
+
key={i}
|
|
173
|
+
ref={(el) => {
|
|
174
|
+
knobs.current[i] = el;
|
|
175
|
+
}}
|
|
176
|
+
role="slider"
|
|
177
|
+
tabIndex={disabled ? -1 : 0}
|
|
178
|
+
aria-labelledby={label ? `${uid}-label` : undefined}
|
|
179
|
+
aria-label={isRange ? (i === 0 ? "minimum" : "maximum") : label ? undefined : "value"}
|
|
180
|
+
aria-valuemin={isRange && i === 1 ? values[0] : min}
|
|
181
|
+
aria-valuemax={isRange && i === 0 ? values[1] : max}
|
|
182
|
+
aria-valuenow={v}
|
|
183
|
+
aria-disabled={disabled || undefined}
|
|
184
|
+
onKeyDown={onKeyDown(i)}
|
|
185
|
+
className="absolute top-1/2 h-5 w-5 rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ink focus-visible:ring-offset-2 focus-visible:ring-offset-page"
|
|
186
|
+
style={{
|
|
187
|
+
left: `${pct(v)}%`,
|
|
188
|
+
// the last-dragged knob of a range sits on top, so two at one spot can be pulled apart
|
|
189
|
+
zIndex: dragging === i ? 2 : 1,
|
|
190
|
+
background: "var(--face)",
|
|
191
|
+
boxShadow: "var(--knob-shadow)",
|
|
192
|
+
transform: `translate(-50%, -50%) scale(${dragging === i ? 1.15 : 1})`,
|
|
193
|
+
transition:
|
|
194
|
+
dragging === null
|
|
195
|
+
? "left 200ms var(--ease-out-soft), transform 220ms var(--ease-spring)"
|
|
196
|
+
: "transform 220ms var(--ease-spring)",
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
{/* A small dimple on the knob's crown */}
|
|
200
|
+
<span
|
|
201
|
+
aria-hidden
|
|
202
|
+
className="absolute left-1/2 top-1/2 h-1.5 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full"
|
|
203
|
+
style={{ background: "var(--track)", boxShadow: "var(--track-shadow)" }}
|
|
204
|
+
/>
|
|
205
|
+
</span>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
{marks && marks.length > 0 && (
|
|
210
|
+
<div aria-hidden className="relative h-4">
|
|
211
|
+
{marks.map((m) => (
|
|
212
|
+
<span
|
|
213
|
+
key={m}
|
|
214
|
+
className="absolute -translate-x-1/2 text-[11px] tabular-nums text-ink-faint"
|
|
215
|
+
style={{ left: `${pct(m)}%` }}
|
|
216
|
+
>
|
|
217
|
+
{m}
|
|
218
|
+
</span>
|
|
219
|
+
))}
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
{name &&
|
|
224
|
+
values.map((v, i) => (
|
|
225
|
+
<input key={i} type="hidden" name={isRange ? `${name}[${i}]` : name} value={v} />
|
|
226
|
+
))}
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
type SpinnerSize = "sm" | "md" | "lg";
|
|
4
|
+
|
|
5
|
+
interface SpinnerProps {
|
|
6
|
+
size?: SpinnerSize | number;
|
|
7
|
+
/** Read out to screen readers; the spinner is silent without it */
|
|
8
|
+
label?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const SIZES: Record<SpinnerSize, number> = { sm: 14, md: 18, lg: 24 };
|
|
13
|
+
|
|
14
|
+
/* A short arc chasing round a faint ring. It takes the text colour, so
|
|
15
|
+
inside a primary button it's light and on the page it's ink. */
|
|
16
|
+
export function Spinner({ size = "md", label, className = "" }: SpinnerProps) {
|
|
17
|
+
const px = typeof size === "number" ? size : SIZES[size];
|
|
18
|
+
return (
|
|
19
|
+
<span
|
|
20
|
+
role={label ? "status" : undefined}
|
|
21
|
+
aria-label={label}
|
|
22
|
+
aria-hidden={label ? undefined : true}
|
|
23
|
+
className={["inline-flex shrink-0", className].join(" ")}
|
|
24
|
+
style={{ width: px, height: px }}
|
|
25
|
+
>
|
|
26
|
+
<svg viewBox="0 0 24 24" width={px} height={px} fill="none" className="animate-spin">
|
|
27
|
+
<circle cx="12" cy="12" r="9.5" stroke="currentColor" strokeWidth="2.5" opacity="0.18" />
|
|
28
|
+
<path d="M21.5 12a9.5 9.5 0 0 0-9.5-9.5" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
|
|
29
|
+
</svg>
|
|
30
|
+
</span>
|
|
31
|
+
);
|
|
32
|
+
}
|