@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
|
@@ -20,8 +20,8 @@ export function Pagination({
|
|
|
20
20
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
21
21
|
const btnRefs = useRef<Record<string, HTMLButtonElement | null>>({});
|
|
22
22
|
|
|
23
|
-
const measurePill = () => {
|
|
24
|
-
const btn = btnRefs.current[String(
|
|
23
|
+
const measurePill = (page: number) => {
|
|
24
|
+
const btn = btnRefs.current[String(page)];
|
|
25
25
|
const container = containerRef.current;
|
|
26
26
|
if (!btn || !container) return;
|
|
27
27
|
const cr = container.getBoundingClientRect();
|
|
@@ -29,18 +29,15 @@ export function Pagination({
|
|
|
29
29
|
setPill({ left: br.left - cr.left, width: br.width, ready: true });
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
useLayoutEffect(() => {
|
|
33
|
-
measurePill();
|
|
34
|
-
}, []);
|
|
35
|
-
|
|
36
32
|
/* Only the thumb moves: it glides to the new page without overshoot,
|
|
37
|
-
and the numbers themselves stay still.
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
and the numbers themselves stay still. Measured before paint, so it
|
|
34
|
+
is never seen out of place. */
|
|
35
|
+
useLayoutEffect(() => {
|
|
36
|
+
measurePill(currentPage);
|
|
40
37
|
}, [currentPage]);
|
|
41
38
|
|
|
42
39
|
useEffect(() => {
|
|
43
|
-
const ro = new ResizeObserver(() => measurePill());
|
|
40
|
+
const ro = new ResizeObserver(() => measurePill(currentPage));
|
|
44
41
|
if (containerRef.current) ro.observe(containerRef.current);
|
|
45
42
|
return () => ro.disconnect();
|
|
46
43
|
}, [currentPage]);
|
|
@@ -58,9 +55,9 @@ export function Pagination({
|
|
|
58
55
|
onClick={() => onPageChange(currentPage - 1)}
|
|
59
56
|
disabled={currentPage <= 1}
|
|
60
57
|
aria-label="Previous page"
|
|
61
|
-
className="group/nav relative overflow-hidden h-8 px-3 rounded-[9px] flex items-center gap-1.5 text-[13px] font-medium text-
|
|
58
|
+
className="group/nav relative overflow-hidden h-8 px-3 rounded-[9px] flex items-center gap-1.5 text-[13px] font-medium text-ink-body hover:text-ink transition-colors cursor-pointer disabled:opacity-30 disabled:pointer-events-none"
|
|
62
59
|
>
|
|
63
|
-
<span className="pointer-events-none absolute inset-0 rounded-[9px] opacity-0 group-hover/nav:opacity-100 transition-opacity duration-200 [background:var(--face
|
|
60
|
+
<span className="pointer-events-none absolute inset-0 rounded-[9px] opacity-0 group-hover/nav:opacity-100 transition-opacity duration-200 [background:var(--face)] [box-shadow:var(--thumb-shadow)]" />
|
|
64
61
|
<svg className="relative z-10" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
65
62
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
|
66
63
|
</svg>
|
|
@@ -75,7 +72,7 @@ export function Pagination({
|
|
|
75
72
|
style={{
|
|
76
73
|
left: pill.left,
|
|
77
74
|
width: pill.width,
|
|
78
|
-
background: "var(--face
|
|
75
|
+
background: "var(--face)",
|
|
79
76
|
boxShadow: "var(--thumb-shadow)",
|
|
80
77
|
transition: "left 340ms var(--ease-out-soft), width 340ms var(--ease-out-soft)",
|
|
81
78
|
}}
|
|
@@ -87,7 +84,7 @@ export function Pagination({
|
|
|
87
84
|
<span
|
|
88
85
|
key={`dot-${i}`}
|
|
89
86
|
aria-hidden
|
|
90
|
-
className="w-8 h-8 flex items-center justify-center text-[11px] text-
|
|
87
|
+
className="w-8 h-8 flex items-center justify-center text-[11px] text-ink-faint select-none relative z-10"
|
|
91
88
|
>
|
|
92
89
|
···
|
|
93
90
|
</span>
|
|
@@ -103,8 +100,8 @@ export function Pagination({
|
|
|
103
100
|
className={[
|
|
104
101
|
"relative z-10 min-w-8 h-8 px-1 rounded-[9px] flex items-center justify-center text-[13px] font-medium transition-colors duration-200 cursor-pointer tabular-nums",
|
|
105
102
|
slot === currentPage
|
|
106
|
-
? "text-
|
|
107
|
-
: "text-
|
|
103
|
+
? "text-ink"
|
|
104
|
+
: "text-ink-muted hover:text-ink",
|
|
108
105
|
].join(" ")}
|
|
109
106
|
>
|
|
110
107
|
{slot}
|
|
@@ -117,9 +114,9 @@ export function Pagination({
|
|
|
117
114
|
onClick={() => onPageChange(currentPage + 1)}
|
|
118
115
|
disabled={currentPage >= totalPages}
|
|
119
116
|
aria-label="Next page"
|
|
120
|
-
className="group/nav relative overflow-hidden h-8 px-3 rounded-[9px] flex items-center gap-1.5 text-[13px] font-medium text-
|
|
117
|
+
className="group/nav relative overflow-hidden h-8 px-3 rounded-[9px] flex items-center gap-1.5 text-[13px] font-medium text-ink-body hover:text-ink transition-colors cursor-pointer disabled:opacity-30 disabled:pointer-events-none"
|
|
121
118
|
>
|
|
122
|
-
<span className="pointer-events-none absolute inset-0 rounded-[9px] opacity-0 group-hover/nav:opacity-100 transition-opacity duration-200 [background:var(--face
|
|
119
|
+
<span className="pointer-events-none absolute inset-0 rounded-[9px] opacity-0 group-hover/nav:opacity-100 transition-opacity duration-200 [background:var(--face)] [box-shadow:var(--thumb-shadow)]" />
|
|
123
120
|
<span className="relative z-10 hidden sm:inline">next</span>
|
|
124
121
|
<svg className="relative z-10" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
125
122
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useState, useRef, useEffect, useId } from "react";
|
|
4
|
+
import { ENTER_FROM, LAYER, ORIGIN, Portal, useAnchoredPosition } from "./Floating";
|
|
4
5
|
|
|
5
6
|
type PopoverPosition = "top" | "bottom" | "left" | "right";
|
|
6
7
|
|
|
@@ -11,29 +12,6 @@ interface PopoverProps {
|
|
|
11
12
|
className?: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
/* Anchored with margins on a wrapper so the panel's own transform is free
|
|
15
|
-
for the enter motion, which scales out from the trigger side. */
|
|
16
|
-
const positionStyles: Record<PopoverPosition, string> = {
|
|
17
|
-
top: "bottom-full left-1/2 -translate-x-1/2 pb-2",
|
|
18
|
-
bottom: "top-full left-1/2 -translate-x-1/2 pt-2",
|
|
19
|
-
left: "right-full top-1/2 -translate-y-1/2 pr-2",
|
|
20
|
-
right: "left-full top-1/2 -translate-y-1/2 pl-2",
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const origin: Record<PopoverPosition, string> = {
|
|
24
|
-
top: "origin-bottom",
|
|
25
|
-
bottom: "origin-top",
|
|
26
|
-
left: "origin-right",
|
|
27
|
-
right: "origin-left",
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const enterFrom: Record<PopoverPosition, string> = {
|
|
31
|
-
top: "translate-y-1.5",
|
|
32
|
-
bottom: "-translate-y-1.5",
|
|
33
|
-
left: "translate-x-1.5",
|
|
34
|
-
right: "-translate-x-1.5",
|
|
35
|
-
};
|
|
36
|
-
|
|
37
15
|
export function Popover({
|
|
38
16
|
trigger,
|
|
39
17
|
children,
|
|
@@ -44,29 +22,42 @@ export function Popover({
|
|
|
44
22
|
const [visible, setVisible] = useState(false);
|
|
45
23
|
const [mounted, setMounted] = useState(false);
|
|
46
24
|
const ref = useRef<HTMLDivElement>(null);
|
|
47
|
-
const panelRef = useRef<HTMLDivElement>(null);
|
|
48
25
|
const uid = useId();
|
|
26
|
+
// Portalled and pinned beside the trigger, flipping if there's no room
|
|
27
|
+
const { setFloating, floatingRef, side } = useAnchoredPosition(ref, { open: mounted, side: position, align: "center" });
|
|
28
|
+
|
|
29
|
+
/* Mounted and faded out during render; the effect only times the rest */
|
|
30
|
+
if (open && !mounted) setMounted(true);
|
|
31
|
+
if (!open && visible) setVisible(false);
|
|
49
32
|
|
|
50
33
|
useEffect(() => {
|
|
51
34
|
if (open) {
|
|
52
|
-
|
|
53
|
-
|
|
35
|
+
requestAnimationFrame(() =>
|
|
36
|
+
requestAnimationFrame(() => {
|
|
37
|
+
setVisible(true);
|
|
38
|
+
// The panel lives at the end of <body>; bring focus to it so Tab reaches its contents
|
|
39
|
+
const panel = floatingRef.current;
|
|
40
|
+
const first = panel?.querySelector<HTMLElement>(
|
|
41
|
+
'button:not([disabled]), [href], input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])',
|
|
42
|
+
);
|
|
43
|
+
(first ?? panel)?.focus({ preventScroll: true });
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
54
46
|
} else {
|
|
55
|
-
setVisible(false);
|
|
56
47
|
const t = setTimeout(() => setMounted(false), 180);
|
|
57
48
|
return () => clearTimeout(t);
|
|
58
49
|
}
|
|
59
|
-
}, [open]);
|
|
50
|
+
}, [open, floatingRef]);
|
|
60
51
|
|
|
61
52
|
useEffect(() => {
|
|
62
53
|
function onOutside(e: MouseEvent) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
const t = e.target as Node;
|
|
55
|
+
if (ref.current?.contains(t) || floatingRef.current?.contains(t)) return;
|
|
56
|
+
setOpen(false);
|
|
66
57
|
}
|
|
67
58
|
document.addEventListener("mousedown", onOutside);
|
|
68
59
|
return () => document.removeEventListener("mousedown", onOutside);
|
|
69
|
-
}, []);
|
|
60
|
+
}, [floatingRef]);
|
|
70
61
|
|
|
71
62
|
useEffect(() => {
|
|
72
63
|
if (!open) return;
|
|
@@ -92,29 +83,33 @@ export function Popover({
|
|
|
92
83
|
: trigger}
|
|
93
84
|
</div>
|
|
94
85
|
{mounted && (
|
|
95
|
-
<
|
|
86
|
+
<Portal>
|
|
96
87
|
<div
|
|
97
|
-
ref={
|
|
88
|
+
ref={setFloating}
|
|
98
89
|
id={`${uid}-popover`}
|
|
99
90
|
role="dialog"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
91
|
+
tabIndex={-1}
|
|
92
|
+
className="outline-none min-w-[220px] max-w-[calc(100vw-16px)] rounded-[16px] p-1.5 transition-[opacity,transform] duration-200 ease-[var(--ease-spring)]"
|
|
93
|
+
style={{
|
|
94
|
+
position: "fixed",
|
|
95
|
+
top: 0,
|
|
96
|
+
left: 0,
|
|
97
|
+
zIndex: LAYER.floating,
|
|
98
|
+
transformOrigin: ORIGIN[side],
|
|
99
|
+
opacity: visible ? 1 : 0,
|
|
100
|
+
transform: visible ? "none" : `${ENTER_FROM[side]} scale(0.96)`,
|
|
101
|
+
background: "var(--panel-bezel)",
|
|
102
|
+
boxShadow: "var(--overlay-shadow)",
|
|
103
|
+
}}
|
|
109
104
|
>
|
|
110
105
|
<div
|
|
111
|
-
className="bg-
|
|
106
|
+
className="bg-panel-face rounded-[11px] p-3.5 text-[13px] text-ink-body leading-relaxed"
|
|
112
107
|
style={{ boxShadow: "var(--panel-face-shadow)" }}
|
|
113
108
|
>
|
|
114
109
|
{children}
|
|
115
110
|
</div>
|
|
116
111
|
</div>
|
|
117
|
-
</
|
|
112
|
+
</Portal>
|
|
118
113
|
)}
|
|
119
114
|
</div>
|
|
120
115
|
);
|
|
@@ -31,10 +31,10 @@ export function Progress({
|
|
|
31
31
|
{(label || showValue) && (
|
|
32
32
|
<div className="flex items-center justify-between gap-4">
|
|
33
33
|
{label && (
|
|
34
|
-
<span className="text-[13px] font-medium text-
|
|
34
|
+
<span className="text-[13px] font-medium text-ink-label truncate">{label}</span>
|
|
35
35
|
)}
|
|
36
36
|
{showValue && (
|
|
37
|
-
<span aria-hidden className="text-[12px] tabular-nums text-
|
|
37
|
+
<span aria-hidden className="text-[12px] tabular-nums text-ink-muted shrink-0">
|
|
38
38
|
{Math.round(pct)}%
|
|
39
39
|
</span>
|
|
40
40
|
)}
|
|
@@ -55,8 +55,8 @@ export function Progress({
|
|
|
55
55
|
width: `${displayed}%`,
|
|
56
56
|
/* Never collapses below a pill, so 1% still reads as started */
|
|
57
57
|
minWidth: displayed > 0 ? 6 : 0,
|
|
58
|
-
background: "var(--face-
|
|
59
|
-
boxShadow: "
|
|
58
|
+
background: "var(--face-inverse)",
|
|
59
|
+
boxShadow: "var(--fill-shadow)",
|
|
60
60
|
transition: "width 700ms var(--ease-out-soft)",
|
|
61
61
|
}}
|
|
62
62
|
/>
|
|
@@ -33,18 +33,18 @@ export function QuoteCard({
|
|
|
33
33
|
alt=""
|
|
34
34
|
loading="lazy"
|
|
35
35
|
className="w-7 h-7 rounded-full object-cover shrink-0"
|
|
36
|
-
style={{ boxShadow: dark ? "0 0 0 0.5px
|
|
36
|
+
style={{ boxShadow: dark ? "0 0 0 0.5px var(--photo-edge-dark)" : "0 0 0 0.5px var(--photo-edge)" }}
|
|
37
37
|
/>
|
|
38
38
|
) : (
|
|
39
39
|
<span
|
|
40
40
|
aria-hidden
|
|
41
41
|
className={[
|
|
42
42
|
"w-7 h-7 rounded-full flex items-center justify-center text-[11.5px] font-semibold shrink-0",
|
|
43
|
-
dark ? "text-white" : "text-
|
|
43
|
+
dark ? "text-white" : "text-ink-label",
|
|
44
44
|
].join(" ")}
|
|
45
45
|
style={{
|
|
46
|
-
background: dark ? "var(--tile-dark)" : "var(--
|
|
47
|
-
boxShadow: dark ? "var(--tile-dark-shadow)" : "var(--tile-
|
|
46
|
+
background: dark ? "var(--tile-dark)" : "var(--tile)",
|
|
47
|
+
boxShadow: dark ? "var(--tile-dark-shadow)" : "var(--tile-shadow)",
|
|
48
48
|
}}
|
|
49
49
|
>
|
|
50
50
|
{initial}
|
|
@@ -54,7 +54,7 @@ export function QuoteCard({
|
|
|
54
54
|
<cite
|
|
55
55
|
className={[
|
|
56
56
|
"not-italic font-semibold tracking-[-0.008em]",
|
|
57
|
-
dark ? "text-white" : "text-
|
|
57
|
+
dark ? "text-white" : "text-ink",
|
|
58
58
|
].join(" ")}
|
|
59
59
|
>
|
|
60
60
|
{author}
|
|
@@ -72,7 +72,7 @@ export function QuoteCard({
|
|
|
72
72
|
width="22"
|
|
73
73
|
height="17"
|
|
74
74
|
viewBox="0 0 56 44"
|
|
75
|
-
className={["shrink-0 mb-auto", dark ? "text-white/25" : "text-
|
|
75
|
+
className={["shrink-0 mb-auto", dark ? "text-white/25" : "text-ink-ghost"].join(" ")}
|
|
76
76
|
>
|
|
77
77
|
<path
|
|
78
78
|
fill="currentColor"
|
|
@@ -82,7 +82,7 @@ export function QuoteCard({
|
|
|
82
82
|
<blockquote
|
|
83
83
|
className={[
|
|
84
84
|
"m-0 pt-8 text-[19px] leading-[1.55] tracking-[-0.015em] text-pretty",
|
|
85
|
-
dark ? "text-neutral-50" : "text-
|
|
85
|
+
dark ? "text-neutral-50" : "text-ink",
|
|
86
86
|
].join(" ")}
|
|
87
87
|
>
|
|
88
88
|
{quote}
|
package/dist/registry/Radio.tsx
CHANGED
|
@@ -33,6 +33,21 @@ interface Blob {
|
|
|
33
33
|
|
|
34
34
|
const LEAD = "260ms var(--ease-out-soft)";
|
|
35
35
|
const TRAIL = "480ms cubic-bezier(0.34, 1.25, 0.64, 1) 50ms";
|
|
36
|
+
|
|
37
|
+
/* Where the face sits over a well, in the group's own coordinates. Read
|
|
38
|
+
straight off the DOM, so setting it from an effect is a measurement,
|
|
39
|
+
not derived state */
|
|
40
|
+
function wellBox(group: HTMLElement | null, el: HTMLElement | null, dir: Blob["dir"]): Blob | null {
|
|
41
|
+
if (!group || !el) return null;
|
|
42
|
+
return {
|
|
43
|
+
top: el.offsetTop,
|
|
44
|
+
bottom: group.clientHeight - el.offsetTop - el.offsetHeight,
|
|
45
|
+
left: el.offsetLeft,
|
|
46
|
+
size: el.offsetWidth,
|
|
47
|
+
dir,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
export function RadioGroup({
|
|
37
52
|
options,
|
|
38
53
|
value,
|
|
@@ -59,24 +74,13 @@ export function RadioGroup({
|
|
|
59
74
|
const selectedIndex = options.findIndex((o) => o.value === selected);
|
|
60
75
|
const tabStop = selectedIndex >= 0 ? selectedIndex : enabled[0];
|
|
61
76
|
|
|
62
|
-
const measure = (dir: Blob["dir"]) =>
|
|
63
|
-
|
|
64
|
-
const el = refs.current[prevIndex.current];
|
|
65
|
-
if (!group || !el) return setBlob(null);
|
|
66
|
-
setBlob({
|
|
67
|
-
top: el.offsetTop,
|
|
68
|
-
bottom: group.clientHeight - el.offsetTop - el.offsetHeight,
|
|
69
|
-
left: el.offsetLeft,
|
|
70
|
-
size: el.offsetWidth,
|
|
71
|
-
dir,
|
|
72
|
-
});
|
|
73
|
-
};
|
|
77
|
+
const measure = (dir: Blob["dir"]) =>
|
|
78
|
+
setBlob(wellBox(groupRef.current, refs.current[prevIndex.current], dir));
|
|
74
79
|
|
|
75
80
|
useLayoutEffect(() => {
|
|
76
81
|
const prev = prevIndex.current;
|
|
77
82
|
prevIndex.current = selectedIndex;
|
|
78
83
|
measure(prev < 0 || selectedIndex < 0 ? 0 : selectedIndex > prev ? 1 : selectedIndex < prev ? -1 : 0);
|
|
79
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
84
|
}, [selectedIndex]);
|
|
81
85
|
|
|
82
86
|
// Rewrapped descriptions move the wells; follow them without animating.
|
|
@@ -92,7 +96,6 @@ export function RadioGroup({
|
|
|
92
96
|
});
|
|
93
97
|
ro.observe(group);
|
|
94
98
|
return () => ro.disconnect();
|
|
95
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
99
|
}, []);
|
|
97
100
|
|
|
98
101
|
const move = (from: number, step: 1 | -1) => {
|
|
@@ -121,11 +124,11 @@ export function RadioGroup({
|
|
|
121
124
|
].join(" ")}
|
|
122
125
|
>
|
|
123
126
|
<div className="flex flex-col gap-0.5 min-w-0">
|
|
124
|
-
<span id={`${uid}-${i}-label`} className="text-[13px] font-medium text-
|
|
127
|
+
<span id={`${uid}-${i}-label`} className="text-[13px] font-medium text-ink leading-snug">
|
|
125
128
|
{opt.label}
|
|
126
129
|
</span>
|
|
127
130
|
{opt.description && (
|
|
128
|
-
<span id={`${uid}-${i}-desc`} className="text-[12px] text-
|
|
131
|
+
<span id={`${uid}-${i}-desc`} className="text-[12px] text-ink-muted leading-relaxed">
|
|
129
132
|
{opt.description}
|
|
130
133
|
</span>
|
|
131
134
|
)}
|
|
@@ -155,7 +158,7 @@ export function RadioGroup({
|
|
|
155
158
|
className={[
|
|
156
159
|
"shrink-0 w-[22px] h-[22px] rounded-full cursor-pointer",
|
|
157
160
|
"transition-[box-shadow] duration-200 ease-out",
|
|
158
|
-
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-
|
|
161
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ink",
|
|
159
162
|
active ? "" : "group-hover/radio:[box-shadow:var(--field-shadow-hover)]",
|
|
160
163
|
].join(" ")}
|
|
161
164
|
style={{ background: "var(--field)", boxShadow: "var(--field-shadow)" }}
|
|
@@ -174,8 +177,8 @@ export function RadioGroup({
|
|
|
174
177
|
bottom: blob.bottom,
|
|
175
178
|
left: blob.left,
|
|
176
179
|
width: blob.size,
|
|
177
|
-
background: "var(--face-
|
|
178
|
-
boxShadow: "var(--face-
|
|
180
|
+
background: "var(--face-inverse)",
|
|
181
|
+
boxShadow: "var(--face-inverse-shadow)",
|
|
179
182
|
transition:
|
|
180
183
|
blob.dir === 0
|
|
181
184
|
? "none"
|
|
@@ -187,11 +190,11 @@ export function RadioGroup({
|
|
|
187
190
|
{/* The dot rides the trailing edge's spring, arriving as it settles */}
|
|
188
191
|
<span
|
|
189
192
|
aria-hidden
|
|
190
|
-
className="pointer-events-none absolute w-2 h-2 rounded-full bg-
|
|
193
|
+
className="pointer-events-none absolute w-2 h-2 rounded-full bg-ink-inverse"
|
|
191
194
|
style={{
|
|
192
195
|
top: blob.top + (blob.size - 8) / 2,
|
|
193
196
|
left: blob.left + (blob.size - 8) / 2,
|
|
194
|
-
boxShadow: "
|
|
197
|
+
boxShadow: "var(--dot-shadow)",
|
|
195
198
|
transition: blob.dir === 0 ? "none" : `top ${TRAIL}`,
|
|
196
199
|
}}
|
|
197
200
|
/>
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export interface RadioCardOption {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
/** Something on the right of the title, like a price */
|
|
11
|
+
aside?: React.ReactNode;
|
|
12
|
+
icon?: React.ReactNode;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface RadioCardsProps {
|
|
17
|
+
options: RadioCardOption[];
|
|
18
|
+
value?: string;
|
|
19
|
+
defaultValue?: string;
|
|
20
|
+
onChange?: (value: string) => void;
|
|
21
|
+
/** Read out as the group's name */
|
|
22
|
+
label?: string;
|
|
23
|
+
/** Options per row from the small breakpoint up; they stack on phones */
|
|
24
|
+
columns?: 1 | 2 | 3;
|
|
25
|
+
name?: string;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const COLS = { 1: "", 2: "sm:grid-cols-2", 3: "sm:grid-cols-3" } as const;
|
|
30
|
+
|
|
31
|
+
// RadioGroup's timings: the leading edge leaves at once, the trailing
|
|
32
|
+
// edge follows a beat later on a spring and settles into place
|
|
33
|
+
const LEAD = "260ms var(--ease-out-soft)";
|
|
34
|
+
const TRAIL = "480ms cubic-bezier(0.34, 1.25, 0.64, 1) 50ms";
|
|
35
|
+
|
|
36
|
+
/* Bigger choices, like a plan or a delivery speed: the segmented control
|
|
37
|
+
from Tabs, grown up. The options sit flat in one sunken well and a
|
|
38
|
+
single raised thumb flows to whichever is chosen, across rows and
|
|
39
|
+
columns: its leading edge goes first and the trailing edge catches
|
|
40
|
+
up, so it stretches toward the new option the way RadioGroup's
|
|
41
|
+
selection does, and you can see which way it went. Each keeps RadioGroup's own control on its right, so the two
|
|
42
|
+
read as one family. One Tab stop, arrows move. */
|
|
43
|
+
export function RadioCards({
|
|
44
|
+
options,
|
|
45
|
+
value,
|
|
46
|
+
defaultValue = "",
|
|
47
|
+
onChange,
|
|
48
|
+
label,
|
|
49
|
+
columns = 1,
|
|
50
|
+
name,
|
|
51
|
+
className = "",
|
|
52
|
+
}: RadioCardsProps) {
|
|
53
|
+
const uid = useId();
|
|
54
|
+
const [internal, setInternal] = useState(defaultValue);
|
|
55
|
+
const selected = value !== undefined ? value : internal;
|
|
56
|
+
const wellRef = useRef<HTMLDivElement>(null);
|
|
57
|
+
const refs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
58
|
+
const thumbRef = useRef<HTMLSpanElement>(null);
|
|
59
|
+
// Where the thumb last landed, to tell which way the next move goes
|
|
60
|
+
const last = useRef<{ top: number; left: number } | null>(null);
|
|
61
|
+
|
|
62
|
+
const enabled = options.map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0);
|
|
63
|
+
const selectedIndex = options.findIndex((o) => o.value === selected);
|
|
64
|
+
const tabStop = selectedIndex >= 0 ? selectedIndex : enabled[0];
|
|
65
|
+
|
|
66
|
+
const selectedRef = useRef(selectedIndex);
|
|
67
|
+
|
|
68
|
+
/* The thumb is held by its four edges, measured from the well's, and
|
|
69
|
+
written straight onto it, so a move never re-renders anything.
|
|
70
|
+
Heading forward on an axis, the far edge leads and the near one
|
|
71
|
+
trails on a spring; heading back, the other way round. */
|
|
72
|
+
const place = (index: number, animate: boolean) => {
|
|
73
|
+
const thumb = thumbRef.current;
|
|
74
|
+
const el = refs.current[index];
|
|
75
|
+
const well = wellRef.current;
|
|
76
|
+
if (!thumb || !well) return;
|
|
77
|
+
if (!el) {
|
|
78
|
+
thumb.style.opacity = "0";
|
|
79
|
+
last.current = null;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const top = el.offsetTop;
|
|
83
|
+
const left = el.offsetLeft;
|
|
84
|
+
const from = animate ? last.current : null;
|
|
85
|
+
const edge = (d: number, near: string, far: string) =>
|
|
86
|
+
d === 0 ? [] : d > 0 ? [`${near} ${TRAIL}`, `${far} ${LEAD}`] : [`${near} ${LEAD}`, `${far} ${TRAIL}`];
|
|
87
|
+
const dy = from ? Math.sign(top - from.top) : 0;
|
|
88
|
+
const dx = from ? Math.sign(left - from.left) : 0;
|
|
89
|
+
thumb.style.transition = [...edge(dy, "top", "bottom"), ...edge(dx, "left", "right")].join(", ") || "none";
|
|
90
|
+
thumb.style.top = `${top}px`;
|
|
91
|
+
thumb.style.left = `${left}px`;
|
|
92
|
+
thumb.style.right = `${well.clientWidth - left - el.offsetWidth}px`;
|
|
93
|
+
thumb.style.bottom = `${well.clientHeight - top - el.offsetHeight}px`;
|
|
94
|
+
thumb.style.opacity = "1";
|
|
95
|
+
last.current = { top, left };
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Before paint, so the thumb is never seen out of place
|
|
99
|
+
useLayoutEffect(() => {
|
|
100
|
+
selectedRef.current = selectedIndex;
|
|
101
|
+
place(selectedIndex, true);
|
|
102
|
+
}, [selectedIndex]);
|
|
103
|
+
|
|
104
|
+
// Rewrapped text or a new breakpoint moves the options; follow them
|
|
105
|
+
// without animating. An observer reports once as soon as it's attached;
|
|
106
|
+
// that one is skipped, or it would cut short a move already under way.
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const well = wellRef.current;
|
|
109
|
+
if (!well) return;
|
|
110
|
+
let attached = false;
|
|
111
|
+
const ro = new ResizeObserver(() => {
|
|
112
|
+
if (attached) place(selectedRef.current, false);
|
|
113
|
+
attached = true;
|
|
114
|
+
});
|
|
115
|
+
ro.observe(well);
|
|
116
|
+
return () => ro.disconnect();
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
const select = (i: number) => {
|
|
120
|
+
const o = options[i];
|
|
121
|
+
if (!o || o.disabled) return;
|
|
122
|
+
setInternal(o.value);
|
|
123
|
+
onChange?.(o.value);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const move = (from: number, step: 1 | -1) => {
|
|
127
|
+
const at = enabled.indexOf(from);
|
|
128
|
+
const next = enabled[(at + step + enabled.length) % enabled.length];
|
|
129
|
+
refs.current[next]?.focus();
|
|
130
|
+
select(next);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div
|
|
135
|
+
ref={wellRef}
|
|
136
|
+
role="radiogroup"
|
|
137
|
+
aria-label={label}
|
|
138
|
+
className={["relative grid grid-cols-1 gap-1 rounded-[18px] p-1.5", COLS[columns], className].join(" ")}
|
|
139
|
+
style={{ background: "var(--well)", boxShadow: "var(--well-shadow)" }}
|
|
140
|
+
>
|
|
141
|
+
<span
|
|
142
|
+
ref={thumbRef}
|
|
143
|
+
aria-hidden
|
|
144
|
+
className="pointer-events-none absolute rounded-[13px] opacity-0"
|
|
145
|
+
style={{ background: "var(--face)", boxShadow: "var(--thumb-shadow)" }}
|
|
146
|
+
/>
|
|
147
|
+
|
|
148
|
+
{options.map((o, i) => {
|
|
149
|
+
const on = i === selectedIndex;
|
|
150
|
+
return (
|
|
151
|
+
<button
|
|
152
|
+
key={o.value}
|
|
153
|
+
ref={(el) => {
|
|
154
|
+
refs.current[i] = el;
|
|
155
|
+
}}
|
|
156
|
+
type="button"
|
|
157
|
+
role="radio"
|
|
158
|
+
aria-checked={on}
|
|
159
|
+
aria-labelledby={`${uid}-${i}-label`}
|
|
160
|
+
aria-describedby={o.description ? `${uid}-${i}-desc` : undefined}
|
|
161
|
+
disabled={o.disabled}
|
|
162
|
+
tabIndex={i === tabStop ? 0 : -1}
|
|
163
|
+
onClick={() => select(i)}
|
|
164
|
+
onKeyDown={(e) => {
|
|
165
|
+
if (e.key === "ArrowDown" || e.key === "ArrowRight") {
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
move(i, 1);
|
|
168
|
+
} else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
move(i, -1);
|
|
171
|
+
}
|
|
172
|
+
}}
|
|
173
|
+
className={[
|
|
174
|
+
"group/card relative z-10 flex items-start gap-3 rounded-[13px] px-4 py-3.5 text-left",
|
|
175
|
+
"focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ink",
|
|
176
|
+
"disabled:opacity-40 disabled:cursor-not-allowed",
|
|
177
|
+
on ? "cursor-default" : "cursor-pointer",
|
|
178
|
+
].join(" ")}
|
|
179
|
+
>
|
|
180
|
+
{o.icon && (
|
|
181
|
+
<span
|
|
182
|
+
aria-hidden
|
|
183
|
+
className={[
|
|
184
|
+
"mt-px flex h-5 w-5 shrink-0 items-center justify-center transition-colors duration-200",
|
|
185
|
+
on ? "text-ink" : "text-ink-faint group-hover/card:text-ink-body",
|
|
186
|
+
].join(" ")}
|
|
187
|
+
>
|
|
188
|
+
{o.icon}
|
|
189
|
+
</span>
|
|
190
|
+
)}
|
|
191
|
+
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
192
|
+
<span className="flex items-baseline justify-between gap-3">
|
|
193
|
+
<span
|
|
194
|
+
id={`${uid}-${i}-label`}
|
|
195
|
+
className={[
|
|
196
|
+
"text-[13.5px] font-medium leading-snug transition-colors duration-150",
|
|
197
|
+
on ? "text-ink" : "text-ink-soft group-hover/card:text-ink",
|
|
198
|
+
].join(" ")}
|
|
199
|
+
>
|
|
200
|
+
{o.label}
|
|
201
|
+
</span>
|
|
202
|
+
{o.aside && (
|
|
203
|
+
<span
|
|
204
|
+
className={[
|
|
205
|
+
"shrink-0 text-[13px] tabular-nums transition-colors duration-150",
|
|
206
|
+
on ? "font-medium text-ink" : "text-ink-muted",
|
|
207
|
+
].join(" ")}
|
|
208
|
+
>
|
|
209
|
+
{o.aside}
|
|
210
|
+
</span>
|
|
211
|
+
)}
|
|
212
|
+
</span>
|
|
213
|
+
{o.description && (
|
|
214
|
+
<span id={`${uid}-${i}-desc`} className="text-[12px] leading-relaxed text-ink-muted">
|
|
215
|
+
{o.description}
|
|
216
|
+
</span>
|
|
217
|
+
)}
|
|
218
|
+
</span>
|
|
219
|
+
{/* RadioGroup's control: a sunken well, and when chosen a face in
|
|
220
|
+
the inverse material with a dot pressed into it */}
|
|
221
|
+
<span
|
|
222
|
+
aria-hidden
|
|
223
|
+
className={[
|
|
224
|
+
"relative mt-px h-[18px] w-[18px] shrink-0 rounded-full transition-[box-shadow] duration-200",
|
|
225
|
+
on ? "" : "group-hover/card:[box-shadow:var(--field-shadow-hover)]",
|
|
226
|
+
].join(" ")}
|
|
227
|
+
style={{ background: "var(--field)", boxShadow: "var(--field-shadow)" }}
|
|
228
|
+
>
|
|
229
|
+
<span
|
|
230
|
+
className="absolute inset-0 grid place-items-center rounded-full transition-[opacity,transform] duration-300 ease-[var(--ease-spring)]"
|
|
231
|
+
style={{
|
|
232
|
+
background: "var(--face-inverse)",
|
|
233
|
+
boxShadow: "var(--face-inverse-shadow)",
|
|
234
|
+
opacity: on ? 1 : 0,
|
|
235
|
+
transform: on ? "scale(1)" : "scale(0.4)",
|
|
236
|
+
}}
|
|
237
|
+
>
|
|
238
|
+
<span className="h-1.5 w-1.5 rounded-full bg-ink-inverse" style={{ boxShadow: "var(--dot-shadow)" }} />
|
|
239
|
+
</span>
|
|
240
|
+
</span>
|
|
241
|
+
</button>
|
|
242
|
+
);
|
|
243
|
+
})}
|
|
244
|
+
{name && <input type="hidden" name={name} value={selected} />}
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
@@ -68,14 +68,14 @@ export function ReactionBar({
|
|
|
68
68
|
aria-label={`${reaction.label}, ${reaction.count}`}
|
|
69
69
|
className={[
|
|
70
70
|
"inline-flex items-center gap-1.5 rounded-[11px] pl-2.5 pr-3 py-[7px] text-[13px] font-medium cursor-pointer",
|
|
71
|
-
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-
|
|
72
|
-
reaction.active ? "text-
|
|
71
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ink",
|
|
72
|
+
reaction.active ? "text-ink-inverse" : "text-ink-body hover:text-ink",
|
|
73
73
|
].join(" ")}
|
|
74
74
|
style={{
|
|
75
75
|
/* Off is a raised face; on is the dark face — pressed in and lit */
|
|
76
|
-
background: reaction.active ? "var(--face-
|
|
77
|
-
boxShadow: reaction.active ? "var(--face-
|
|
78
|
-
textShadow: reaction.active ? "
|
|
76
|
+
background: reaction.active ? "var(--face-inverse)" : "var(--face)",
|
|
77
|
+
boxShadow: reaction.active ? "var(--face-inverse-shadow)" : "var(--face-shadow)",
|
|
78
|
+
textShadow: reaction.active ? "var(--emboss-inverse)" : "var(--emboss)",
|
|
79
79
|
transform: isAnimating ? "scale(1.08)" : "scale(1)",
|
|
80
80
|
transition:
|
|
81
81
|
"transform 250ms var(--ease-spring), color 200ms ease, box-shadow 200ms ease",
|