@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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useId, ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
interface AccordionItem {
|
|
6
|
+
key: string;
|
|
7
|
+
title: string;
|
|
8
|
+
content: ReactNode;
|
|
9
|
+
defaultOpen?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface AccordionProps {
|
|
13
|
+
items: AccordionItem[];
|
|
14
|
+
allowMultiple?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Accordion({ items, allowMultiple = false, className }: AccordionProps) {
|
|
19
|
+
const [openKeys, setOpenKeys] = useState<Set<string>>(() => {
|
|
20
|
+
const defaults = new Set<string>();
|
|
21
|
+
for (const item of items) {
|
|
22
|
+
if (item.defaultOpen) {
|
|
23
|
+
defaults.add(item.key);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return defaults;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const toggle = useCallback(
|
|
30
|
+
(key: string) => {
|
|
31
|
+
setOpenKeys((prev) => {
|
|
32
|
+
const next = new Set(prev);
|
|
33
|
+
if (next.has(key)) {
|
|
34
|
+
next.delete(key);
|
|
35
|
+
} else {
|
|
36
|
+
if (allowMultiple) {
|
|
37
|
+
next.add(key);
|
|
38
|
+
} else {
|
|
39
|
+
next.clear();
|
|
40
|
+
next.add(key);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return next;
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
[allowMultiple]
|
|
47
|
+
);
|
|
48
|
+
const uid = useId();
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className={["rounded-[18px] p-1.5", className].filter(Boolean).join(" ")}
|
|
53
|
+
style={{ background: "var(--panel-bezel)", boxShadow: "var(--panel-shadow)" }}
|
|
54
|
+
>
|
|
55
|
+
<div
|
|
56
|
+
className="bg-panel-face rounded-[13px] overflow-hidden p-1"
|
|
57
|
+
style={{ boxShadow: "var(--panel-face-shadow)" }}
|
|
58
|
+
>
|
|
59
|
+
{items.map((item, index) => {
|
|
60
|
+
const isOpen = openKeys.has(item.key);
|
|
61
|
+
const isLast = index === items.length - 1;
|
|
62
|
+
const headerId = `${uid}-${item.key}-header`;
|
|
63
|
+
const panelId = `${uid}-${item.key}-panel`;
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
key={item.key}
|
|
68
|
+
className={[
|
|
69
|
+
"relative",
|
|
70
|
+
// Hairline between rows, inset so it doesn't touch the rounded corners
|
|
71
|
+
!isLast &&
|
|
72
|
+
"after:absolute after:left-3 after:right-3 after:bottom-0 after:h-px after:bg-line",
|
|
73
|
+
]
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.join(" ")}
|
|
76
|
+
>
|
|
77
|
+
<h3>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
id={headerId}
|
|
81
|
+
aria-expanded={isOpen}
|
|
82
|
+
aria-controls={panelId}
|
|
83
|
+
onClick={() => toggle(item.key)}
|
|
84
|
+
className={[
|
|
85
|
+
"group/acc w-full flex items-center justify-between gap-3 py-3 px-3 text-left text-[13.5px] font-medium cursor-pointer rounded-[10px]",
|
|
86
|
+
"transition-colors duration-150",
|
|
87
|
+
isOpen ? "text-ink" : "text-ink-label hover:text-ink hover:bg-surface-hover",
|
|
88
|
+
].join(" ")}
|
|
89
|
+
>
|
|
90
|
+
<span>{item.title}</span>
|
|
91
|
+
{/* Plus that turns into a minus: says "more inside", not "navigate" */}
|
|
92
|
+
<span
|
|
93
|
+
aria-hidden
|
|
94
|
+
className="relative shrink-0 w-6 h-6 rounded-[7px] flex items-center justify-center text-ink-muted group-hover/acc:text-ink transition-colors"
|
|
95
|
+
style={{ background: "var(--face)", boxShadow: "var(--face-shadow)" }}
|
|
96
|
+
>
|
|
97
|
+
<span className="absolute w-2.5 h-[1.5px] rounded-full bg-current" />
|
|
98
|
+
<span
|
|
99
|
+
className="absolute w-2.5 h-[1.5px] rounded-full bg-current"
|
|
100
|
+
style={{
|
|
101
|
+
transform: isOpen ? "rotate(0deg)" : "rotate(90deg)",
|
|
102
|
+
transition: "transform 300ms var(--ease-spring)",
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
</span>
|
|
106
|
+
</button>
|
|
107
|
+
</h3>
|
|
108
|
+
|
|
109
|
+
<div
|
|
110
|
+
id={panelId}
|
|
111
|
+
role="region"
|
|
112
|
+
aria-labelledby={headerId}
|
|
113
|
+
// Closed panels are removed from the tab order and the a11y tree
|
|
114
|
+
inert={!isOpen}
|
|
115
|
+
style={{
|
|
116
|
+
display: "grid",
|
|
117
|
+
gridTemplateRows: isOpen ? "1fr" : "0fr",
|
|
118
|
+
transition: "grid-template-rows 320ms var(--ease-out-soft)",
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<div className="overflow-hidden">
|
|
122
|
+
<div
|
|
123
|
+
className="px-3 pb-4 pt-0.5 text-[13px] text-ink-body leading-relaxed"
|
|
124
|
+
style={{
|
|
125
|
+
opacity: isOpen ? 1 : 0,
|
|
126
|
+
transform: isOpen ? "translateY(0)" : "translateY(-4px)",
|
|
127
|
+
transition: "opacity 240ms ease-out, transform 320ms var(--ease-out-soft)",
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
{item.content}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
})}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { CloseButton } from "./CloseButton";
|
|
3
|
+
|
|
4
|
+
type AlertVariant = "default" | "success" | "warning" | "error";
|
|
5
|
+
|
|
6
|
+
interface AlertProps {
|
|
7
|
+
variant?: AlertVariant;
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
onDismiss?: () => void;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Tiles share Toast's lit-from-above gradients, so an alert and the toast
|
|
15
|
+
it came from look like the same object. */
|
|
16
|
+
const variantConfig: Record<AlertVariant, {
|
|
17
|
+
tile: string;
|
|
18
|
+
icon: React.ReactNode;
|
|
19
|
+
}> = {
|
|
20
|
+
default: {
|
|
21
|
+
tile: "var(--face-inverse)",
|
|
22
|
+
icon: (
|
|
23
|
+
<svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.75" viewBox="0 0 24 24">
|
|
24
|
+
<circle cx="12" cy="12" r="10" />
|
|
25
|
+
<path strokeLinecap="round" d="M12 8v4m0 4h.01" />
|
|
26
|
+
</svg>
|
|
27
|
+
),
|
|
28
|
+
},
|
|
29
|
+
success: {
|
|
30
|
+
tile: "var(--tile-success)",
|
|
31
|
+
icon: (
|
|
32
|
+
<svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
33
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
34
|
+
</svg>
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
warning: {
|
|
38
|
+
tile: "var(--tile-warning)",
|
|
39
|
+
icon: (
|
|
40
|
+
<svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.75" viewBox="0 0 24 24">
|
|
41
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
|
42
|
+
</svg>
|
|
43
|
+
),
|
|
44
|
+
},
|
|
45
|
+
error: {
|
|
46
|
+
tile: "var(--tile-error)",
|
|
47
|
+
icon: (
|
|
48
|
+
<svg width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
49
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
50
|
+
</svg>
|
|
51
|
+
),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function Alert({ variant = "default", title, description, onDismiss, className = "" }: AlertProps) {
|
|
56
|
+
const { tile, icon } = variantConfig[variant];
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
// Errors and warnings interrupt; everything else waits its turn
|
|
61
|
+
role={variant === "error" || variant === "warning" ? "alert" : "status"}
|
|
62
|
+
className={["rounded-[18px] p-1.5", className].join(" ")}
|
|
63
|
+
style={{ background: "var(--panel-bezel)", boxShadow: "var(--panel-shadow)" }}
|
|
64
|
+
>
|
|
65
|
+
<div
|
|
66
|
+
className="bg-panel-face rounded-[13px] pl-3 pr-2 py-3 flex items-start gap-3"
|
|
67
|
+
style={{ boxShadow: "var(--panel-face-shadow)" }}
|
|
68
|
+
>
|
|
69
|
+
<div
|
|
70
|
+
aria-hidden
|
|
71
|
+
className={[
|
|
72
|
+
"shrink-0 w-7 h-7 rounded-[8px] flex items-center justify-center",
|
|
73
|
+
// the default tile is the inverse face; status tiles stay white-on-colour
|
|
74
|
+
tile === "var(--face-inverse)" ? "text-ink-inverse" : "text-white",
|
|
75
|
+
].join(" ")}
|
|
76
|
+
style={{
|
|
77
|
+
background: tile,
|
|
78
|
+
boxShadow: "var(--tile-status-shadow)",
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{icon}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div className="flex-1 min-w-0 pt-[5px]">
|
|
85
|
+
<p className="text-[13px] font-semibold text-ink leading-tight tracking-[-0.01em]">{title}</p>
|
|
86
|
+
{description && (
|
|
87
|
+
<p className="text-[12.5px] text-ink-muted mt-1 leading-relaxed">{description}</p>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{onDismiss && <CloseButton size="sm" label="Dismiss" onClick={onDismiss} />}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Avatar } from "./Avatar";
|
|
3
|
+
import { Button } from "./Button";
|
|
4
|
+
|
|
5
|
+
interface AuthorCardProps {
|
|
6
|
+
name: string;
|
|
7
|
+
bio?: string;
|
|
8
|
+
avatarSrc?: string;
|
|
9
|
+
onFollow?: () => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function AuthorCard({
|
|
14
|
+
name,
|
|
15
|
+
bio,
|
|
16
|
+
avatarSrc,
|
|
17
|
+
onFollow,
|
|
18
|
+
className = "",
|
|
19
|
+
}: AuthorCardProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className={[
|
|
23
|
+
"rim relative rounded-[22px] p-1.5",
|
|
24
|
+
className,
|
|
25
|
+
]
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.join(" ")}
|
|
28
|
+
style={{ background: "var(--panel-bezel)", boxShadow: "var(--card-shadow)" }}
|
|
29
|
+
>
|
|
30
|
+
<div className="relative bg-panel-face rounded-[17px] overflow-hidden" style={{ boxShadow: "var(--panel-face-shadow)" }}>
|
|
31
|
+
{/* Cover: a sunken well, so the avatar visibly sits on top of it */}
|
|
32
|
+
<div
|
|
33
|
+
aria-hidden
|
|
34
|
+
className="h-20 relative overflow-hidden"
|
|
35
|
+
style={{
|
|
36
|
+
background:
|
|
37
|
+
"var(--cover)",
|
|
38
|
+
boxShadow: "var(--cover-shadow)",
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
<div className="px-6 pb-6">
|
|
42
|
+
<div className="-mt-8 mb-4">
|
|
43
|
+
<Avatar
|
|
44
|
+
src={avatarSrc}
|
|
45
|
+
name={name}
|
|
46
|
+
size="lg"
|
|
47
|
+
className="ring-[3.5px] ring-panel-face shadow-[var(--avatar-lift)]"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
<div className="flex items-start justify-between gap-4">
|
|
51
|
+
<div className="flex-1 min-w-0">
|
|
52
|
+
<p className="text-[15px] font-semibold text-ink leading-snug truncate tracking-[-0.01em]">{name}</p>
|
|
53
|
+
{bio && (
|
|
54
|
+
<p className="text-[13px] text-ink-muted leading-relaxed mt-1 line-clamp-2">{bio}</p>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
{onFollow && (
|
|
58
|
+
<Button variant="primary" size="sm" onClick={onFollow} className="shrink-0">
|
|
59
|
+
follow
|
|
60
|
+
</Button>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
type AvatarSize = "sm" | "md" | "lg";
|
|
4
|
+
|
|
5
|
+
interface AvatarProps {
|
|
6
|
+
src?: string;
|
|
7
|
+
alt?: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
size?: AvatarSize;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const sizeStyles: Record<AvatarSize, string> = {
|
|
14
|
+
sm: "w-7 h-7 text-[10px]",
|
|
15
|
+
md: "w-9 h-9 text-[12px]",
|
|
16
|
+
lg: "w-12 h-12 text-[14px]",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function getInitials(name: string): string {
|
|
20
|
+
return name
|
|
21
|
+
.split(" ")
|
|
22
|
+
.map((w) => w[0])
|
|
23
|
+
.slice(0, 2)
|
|
24
|
+
.join("")
|
|
25
|
+
.toLowerCase();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function Avatar({
|
|
29
|
+
src,
|
|
30
|
+
alt = "",
|
|
31
|
+
name,
|
|
32
|
+
size = "md",
|
|
33
|
+
className = "",
|
|
34
|
+
}: AvatarProps) {
|
|
35
|
+
const initials = name ? getInitials(name) : "?";
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
className={[
|
|
40
|
+
"relative shrink-0 rounded-full overflow-hidden flex items-center justify-center font-semibold text-ink-body select-none",
|
|
41
|
+
sizeStyles[size],
|
|
42
|
+
className,
|
|
43
|
+
]
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.join(" ")}
|
|
46
|
+
style={{ background: "var(--face)" }}
|
|
47
|
+
>
|
|
48
|
+
{src ? (
|
|
49
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
50
|
+
<img src={src} alt={alt || name || ""} loading="lazy" className="w-full h-full object-cover" />
|
|
51
|
+
) : (
|
|
52
|
+
<span role="img" aria-label={name ?? "Unknown"}>{initials}</span>
|
|
53
|
+
)}
|
|
54
|
+
<span
|
|
55
|
+
aria-hidden
|
|
56
|
+
className="pointer-events-none absolute inset-0 rounded-full"
|
|
57
|
+
style={{
|
|
58
|
+
boxShadow: "var(--avatar-rim)",
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface AvatarGroupProps {
|
|
66
|
+
children: React.ReactNode;
|
|
67
|
+
max?: number;
|
|
68
|
+
size?: AvatarSize;
|
|
69
|
+
className?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const overlapStyles: Record<AvatarSize, string> = {
|
|
73
|
+
sm: "-ml-2",
|
|
74
|
+
md: "-ml-2.5",
|
|
75
|
+
lg: "-ml-3",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export function AvatarGroup({
|
|
79
|
+
children,
|
|
80
|
+
max = 5,
|
|
81
|
+
size = "md",
|
|
82
|
+
className = "",
|
|
83
|
+
}: AvatarGroupProps) {
|
|
84
|
+
const items = React.Children.toArray(children);
|
|
85
|
+
const visible = items.slice(0, max);
|
|
86
|
+
const overflow = items.length - max;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className={["flex items-center", className].join(" ")}>
|
|
90
|
+
{visible.map((child, i) => (
|
|
91
|
+
<div
|
|
92
|
+
key={i}
|
|
93
|
+
className={[
|
|
94
|
+
"ring-2 ring-panel-face rounded-full",
|
|
95
|
+
i > 0 ? overlapStyles[size] : "",
|
|
96
|
+
].join(" ")}
|
|
97
|
+
>
|
|
98
|
+
{child}
|
|
99
|
+
</div>
|
|
100
|
+
))}
|
|
101
|
+
{overflow > 0 && (
|
|
102
|
+
<div
|
|
103
|
+
className={[
|
|
104
|
+
"ring-2 ring-panel-face rounded-full flex items-center justify-center font-semibold text-ink-body select-none",
|
|
105
|
+
overlapStyles[size],
|
|
106
|
+
sizeStyles[size],
|
|
107
|
+
].join(" ")}
|
|
108
|
+
style={{ background: "var(--well)", boxShadow: "var(--well-shadow)" }}
|
|
109
|
+
aria-label={`${overflow} more`}
|
|
110
|
+
>
|
|
111
|
+
+{overflow}
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
type BadgeVariant = "default" | "muted" | "accent";
|
|
4
|
+
|
|
5
|
+
interface BadgeProps {
|
|
6
|
+
variant?: BadgeVariant;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const variantStyles: Record<BadgeVariant, string> = {
|
|
12
|
+
default: "text-ink-inverse",
|
|
13
|
+
muted: "text-ink-body",
|
|
14
|
+
accent: "text-accent",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/* Badges are small raised faces, lit from the same top edge as buttons. */
|
|
18
|
+
const variantSurfaces: Record<BadgeVariant, React.CSSProperties> = {
|
|
19
|
+
default: {
|
|
20
|
+
background: "var(--face-inverse)",
|
|
21
|
+
boxShadow: "var(--badge-inverse-shadow)",
|
|
22
|
+
textShadow: "var(--emboss-inverse)",
|
|
23
|
+
},
|
|
24
|
+
muted: {
|
|
25
|
+
background: "var(--badge)",
|
|
26
|
+
boxShadow: "var(--badge-shadow)",
|
|
27
|
+
textShadow: "var(--emboss)",
|
|
28
|
+
},
|
|
29
|
+
accent: {
|
|
30
|
+
background: "var(--badge-accent)",
|
|
31
|
+
boxShadow: "var(--badge-accent-shadow)",
|
|
32
|
+
textShadow: "var(--emboss-soft)",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function Badge({
|
|
37
|
+
variant = "muted",
|
|
38
|
+
children,
|
|
39
|
+
className = "",
|
|
40
|
+
}: BadgeProps) {
|
|
41
|
+
return (
|
|
42
|
+
<span
|
|
43
|
+
className={[
|
|
44
|
+
"inline-flex items-center px-2 py-0.5 rounded-md text-[11px] font-medium tracking-wide",
|
|
45
|
+
variantStyles[variant],
|
|
46
|
+
className,
|
|
47
|
+
]
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.join(" ")}
|
|
50
|
+
style={variantSurfaces[variant]}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
</span>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
import { CloseButton } from "./CloseButton";
|
|
5
|
+
|
|
6
|
+
type BannerVariant = "default" | "accent" | "warning";
|
|
7
|
+
|
|
8
|
+
/* A banner is a dark bezel holding a dark face — the same build as the
|
|
9
|
+
primary button, stretched to full width. On a dark page it stays a
|
|
10
|
+
dark strip (the mid material) rather than a full-width pale bar. */
|
|
11
|
+
const variantStyles: Record<
|
|
12
|
+
BannerVariant,
|
|
13
|
+
{ bezel: string; bezelShadow: string; face: string; faceShadow: string; text: string; tone: "light" | "dark" }
|
|
14
|
+
> = {
|
|
15
|
+
default: {
|
|
16
|
+
bezel: "var(--banner-bezel)",
|
|
17
|
+
bezelShadow: "var(--banner-bezel-shadow)",
|
|
18
|
+
face: "var(--banner-face)",
|
|
19
|
+
faceShadow: "var(--banner-face-shadow)",
|
|
20
|
+
text: "text-white",
|
|
21
|
+
tone: "dark",
|
|
22
|
+
},
|
|
23
|
+
accent: {
|
|
24
|
+
bezel: "var(--bezel-violet)",
|
|
25
|
+
bezelShadow: "var(--bezel-violet-shadow)",
|
|
26
|
+
face: "var(--face-violet)",
|
|
27
|
+
faceShadow: "var(--face-violet-shadow)",
|
|
28
|
+
text: "text-white",
|
|
29
|
+
tone: "dark",
|
|
30
|
+
},
|
|
31
|
+
warning: {
|
|
32
|
+
bezel: "var(--bezel-warning)",
|
|
33
|
+
bezelShadow: "var(--bezel-warning-shadow)",
|
|
34
|
+
face: "var(--face-warning)",
|
|
35
|
+
faceShadow: "var(--face-warning-shadow)",
|
|
36
|
+
text: "text-warning-ink",
|
|
37
|
+
tone: "light",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function Banner({
|
|
42
|
+
variant = "default",
|
|
43
|
+
children,
|
|
44
|
+
icon,
|
|
45
|
+
dismissible = false,
|
|
46
|
+
onDismiss,
|
|
47
|
+
action,
|
|
48
|
+
className = "",
|
|
49
|
+
}: {
|
|
50
|
+
variant?: BannerVariant;
|
|
51
|
+
children: ReactNode;
|
|
52
|
+
icon?: ReactNode;
|
|
53
|
+
dismissible?: boolean;
|
|
54
|
+
onDismiss?: () => void;
|
|
55
|
+
action?: ReactNode;
|
|
56
|
+
className?: string;
|
|
57
|
+
}) {
|
|
58
|
+
const s = variantStyles[variant];
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
role="region"
|
|
63
|
+
aria-label="Announcement"
|
|
64
|
+
className={`w-full rounded-[18px] p-1 ${className}`}
|
|
65
|
+
style={{ background: s.bezel, boxShadow: s.bezelShadow }}
|
|
66
|
+
>
|
|
67
|
+
<div
|
|
68
|
+
className={`rounded-[14px] pl-4 pr-2 py-2.5 flex items-center gap-3 ${s.text}`}
|
|
69
|
+
style={{ background: s.face, boxShadow: s.faceShadow }}
|
|
70
|
+
>
|
|
71
|
+
{icon && <span aria-hidden className="flex-shrink-0 opacity-80">{icon}</span>}
|
|
72
|
+
|
|
73
|
+
{/* Left-aligned so it reads in one sweep with the icon beside it */}
|
|
74
|
+
<span className="text-[13px] font-medium flex-1 min-w-0 leading-relaxed">
|
|
75
|
+
{children}
|
|
76
|
+
</span>
|
|
77
|
+
|
|
78
|
+
{action && <span className="flex-shrink-0">{action}</span>}
|
|
79
|
+
|
|
80
|
+
{dismissible && (
|
|
81
|
+
<CloseButton size="sm" tone={s.tone} label="Dismiss announcement" onClick={onDismiss} />
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface BreadcrumbItem {
|
|
4
|
+
label: string;
|
|
5
|
+
href?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface BreadcrumbProps {
|
|
9
|
+
items: BreadcrumbItem[];
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Breadcrumb({ items, className = "" }: BreadcrumbProps) {
|
|
14
|
+
return (
|
|
15
|
+
<nav aria-label="Breadcrumb" className={className}>
|
|
16
|
+
<ol className="flex items-center gap-1 flex-wrap">
|
|
17
|
+
{items.map((item, i) => {
|
|
18
|
+
const isLast = i === items.length - 1;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<li key={i} className="flex items-center gap-1 min-w-0">
|
|
22
|
+
{i > 0 && (
|
|
23
|
+
<svg
|
|
24
|
+
aria-hidden
|
|
25
|
+
width="12"
|
|
26
|
+
height="12"
|
|
27
|
+
fill="none"
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
strokeWidth="2"
|
|
30
|
+
viewBox="0 0 24 24"
|
|
31
|
+
className="text-ink-ghost shrink-0"
|
|
32
|
+
>
|
|
33
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
34
|
+
</svg>
|
|
35
|
+
)}
|
|
36
|
+
{isLast || !item.href ? (
|
|
37
|
+
<span
|
|
38
|
+
aria-current={isLast ? "page" : undefined}
|
|
39
|
+
className={[
|
|
40
|
+
"text-[12.5px] px-1.5 py-0.5 truncate",
|
|
41
|
+
isLast ? "font-medium text-ink" : "text-ink-muted",
|
|
42
|
+
].join(" ")}
|
|
43
|
+
>
|
|
44
|
+
{item.label}
|
|
45
|
+
</span>
|
|
46
|
+
) : (
|
|
47
|
+
<a
|
|
48
|
+
href={item.href}
|
|
49
|
+
className="text-[12.5px] text-ink-muted hover:text-ink px-1.5 py-0.5 rounded-[6px] hover:bg-ink/[0.05] transition-colors truncate"
|
|
50
|
+
>
|
|
51
|
+
{item.label}
|
|
52
|
+
</a>
|
|
53
|
+
)}
|
|
54
|
+
</li>
|
|
55
|
+
);
|
|
56
|
+
})}
|
|
57
|
+
</ol>
|
|
58
|
+
</nav>
|
|
59
|
+
);
|
|
60
|
+
}
|