@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.
Files changed (272) hide show
  1. package/README.md +60 -20
  2. package/dist/Accordion.d.ts +17 -0
  3. package/dist/Accordion.js +138 -0
  4. package/dist/Accordion.js.map +1 -0
  5. package/dist/Alert.d.ts +13 -0
  6. package/dist/Alert.js +69 -0
  7. package/dist/Alert.js.map +1 -0
  8. package/dist/AuthorCard.d.ts +12 -0
  9. package/dist/AuthorCard.js +57 -0
  10. package/dist/AuthorCard.js.map +1 -0
  11. package/dist/Avatar.d.ts +21 -0
  12. package/dist/Avatar.js +96 -0
  13. package/dist/Avatar.js.map +1 -0
  14. package/dist/Badge.d.ts +12 -0
  15. package/dist/Badge.js +46 -0
  16. package/dist/Badge.js.map +1 -0
  17. package/dist/Banner.d.ts +15 -0
  18. package/dist/Banner.js +68 -0
  19. package/dist/Banner.js.map +1 -0
  20. package/dist/Breadcrumb.d.ts +13 -0
  21. package/dist/Breadcrumb.js +45 -0
  22. package/dist/Breadcrumb.js.map +1 -0
  23. package/dist/Button.d.ts +19 -0
  24. package/dist/Button.js +282 -0
  25. package/dist/Button.js.map +1 -0
  26. package/dist/ButtonGroup.d.ts +19 -0
  27. package/dist/ButtonGroup.js +62 -0
  28. package/dist/ButtonGroup.js.map +1 -0
  29. package/dist/Card.d.ts +40 -0
  30. package/dist/Card.js +175 -0
  31. package/dist/Card.js.map +1 -0
  32. package/dist/CardShell.d.ts +18 -0
  33. package/dist/CardShell.js +58 -0
  34. package/dist/CardShell.js.map +1 -0
  35. package/dist/Carousel.d.ts +22 -0
  36. package/dist/Carousel.js +247 -0
  37. package/dist/Carousel.js.map +1 -0
  38. package/dist/Checkbox.d.ts +15 -0
  39. package/dist/Checkbox.js +84 -0
  40. package/dist/Checkbox.js.map +1 -0
  41. package/dist/CloseButton.d.ts +12 -0
  42. package/dist/CloseButton.js +63 -0
  43. package/dist/CloseButton.js.map +1 -0
  44. package/dist/CodeBlock.d.ts +12 -0
  45. package/dist/CodeBlock.js +221 -0
  46. package/dist/CodeBlock.js.map +1 -0
  47. package/dist/ColorPicker.d.ts +24 -0
  48. package/dist/ColorPicker.js +366 -0
  49. package/dist/ColorPicker.js.map +1 -0
  50. package/dist/Combobox.d.ts +34 -0
  51. package/dist/Combobox.js +255 -0
  52. package/dist/Combobox.js.map +1 -0
  53. package/dist/CommandPalette.d.ts +25 -0
  54. package/dist/CommandPalette.js +304 -0
  55. package/dist/CommandPalette.js.map +1 -0
  56. package/dist/ContextMenu.d.ts +16 -0
  57. package/dist/ContextMenu.js +209 -0
  58. package/dist/ContextMenu.js.map +1 -0
  59. package/dist/DataList.d.ts +15 -0
  60. package/dist/DataList.js +42 -0
  61. package/dist/DataList.js.map +1 -0
  62. package/dist/DatePicker.d.ts +65 -0
  63. package/dist/DatePicker.js +391 -0
  64. package/dist/DatePicker.js.map +1 -0
  65. package/dist/DateRangePicker.d.ts +34 -0
  66. package/dist/DateRangePicker.js +180 -0
  67. package/dist/DateRangePicker.js.map +1 -0
  68. package/dist/Drawer.d.ts +17 -0
  69. package/dist/Drawer.js +132 -0
  70. package/dist/Drawer.js.map +1 -0
  71. package/dist/DropdownMenu.d.ts +23 -0
  72. package/dist/DropdownMenu.js +185 -0
  73. package/dist/DropdownMenu.js.map +1 -0
  74. package/dist/EmptyState.d.ts +13 -0
  75. package/dist/EmptyState.js +55 -0
  76. package/dist/EmptyState.js.map +1 -0
  77. package/dist/FieldMessage.d.ts +26 -0
  78. package/dist/FieldMessage.js +95 -0
  79. package/dist/FieldMessage.js.map +1 -0
  80. package/dist/FileUpload.d.ts +32 -0
  81. package/dist/FileUpload.js +253 -0
  82. package/dist/FileUpload.js.map +1 -0
  83. package/dist/Floating.d.ts +32 -0
  84. package/dist/Floating.js +95 -0
  85. package/dist/Floating.js.map +1 -0
  86. package/dist/HoverCard.d.ts +18 -0
  87. package/dist/HoverCard.js +90 -0
  88. package/dist/HoverCard.js.map +1 -0
  89. package/dist/Input.d.ts +17 -0
  90. package/dist/Input.js +96 -0
  91. package/dist/Input.js.map +1 -0
  92. package/dist/Kbd.d.ts +9 -0
  93. package/dist/Kbd.js +24 -0
  94. package/dist/Kbd.js.map +1 -0
  95. package/dist/Modal.d.ts +15 -0
  96. package/dist/Modal.js +109 -0
  97. package/dist/Modal.js.map +1 -0
  98. package/dist/NumberInput.d.ts +23 -0
  99. package/dist/NumberInput.js +162 -0
  100. package/dist/NumberInput.js.map +1 -0
  101. package/dist/OtpInput.d.ts +26 -0
  102. package/dist/OtpInput.js +173 -0
  103. package/dist/OtpInput.js.map +1 -0
  104. package/dist/Pagination.d.ts +11 -0
  105. package/dist/Pagination.js +148 -0
  106. package/dist/Pagination.js.map +1 -0
  107. package/dist/Popover.d.ts +13 -0
  108. package/dist/Popover.js +98 -0
  109. package/dist/Popover.js.map +1 -0
  110. package/dist/Progress.d.ts +12 -0
  111. package/dist/Progress.js +61 -0
  112. package/dist/Progress.js.map +1 -0
  113. package/dist/QuoteCard.d.ts +13 -0
  114. package/dist/QuoteCard.js +92 -0
  115. package/dist/QuoteCard.js.map +1 -0
  116. package/dist/Radio.d.ts +19 -0
  117. package/dist/Radio.js +161 -0
  118. package/dist/Radio.js.map +1 -0
  119. package/dist/RadioCards.d.ts +27 -0
  120. package/dist/RadioCards.js +205 -0
  121. package/dist/RadioCards.js.map +1 -0
  122. package/dist/ReactionBar.d.ts +15 -0
  123. package/dist/ReactionBar.js +92 -0
  124. package/dist/ReactionBar.js.map +1 -0
  125. package/dist/ReadingProgress.d.ts +8 -0
  126. package/dist/ReadingProgress.js +51 -0
  127. package/dist/ReadingProgress.js.map +1 -0
  128. package/dist/ReadingTime.d.ts +9 -0
  129. package/dist/ReadingTime.js +39 -0
  130. package/dist/ReadingTime.js.map +1 -0
  131. package/dist/Select.d.ts +20 -0
  132. package/dist/Select.js +244 -0
  133. package/dist/Select.js.map +1 -0
  134. package/dist/Separator.d.ts +14 -0
  135. package/dist/Separator.js +33 -0
  136. package/dist/Separator.js.map +1 -0
  137. package/dist/ShareButtons.d.ts +10 -0
  138. package/dist/ShareButtons.js +107 -0
  139. package/dist/ShareButtons.js.map +1 -0
  140. package/dist/Sidebar.d.ts +30 -0
  141. package/dist/Sidebar.js +140 -0
  142. package/dist/Sidebar.js.map +1 -0
  143. package/dist/Skeleton.d.ts +15 -0
  144. package/dist/Skeleton.js +67 -0
  145. package/dist/Skeleton.js.map +1 -0
  146. package/dist/Slider.d.ts +27 -0
  147. package/dist/Slider.js +182 -0
  148. package/dist/Slider.js.map +1 -0
  149. package/dist/Spinner.d.ts +12 -0
  150. package/dist/Spinner.js +24 -0
  151. package/dist/Spinner.js.map +1 -0
  152. package/dist/SpotlightCard.d.ts +17 -0
  153. package/dist/SpotlightCard.js +87 -0
  154. package/dist/SpotlightCard.js.map +1 -0
  155. package/dist/Stat.d.ts +19 -0
  156. package/dist/Stat.js +71 -0
  157. package/dist/Stat.js.map +1 -0
  158. package/dist/StatCard.d.ts +24 -0
  159. package/dist/StatCard.js +212 -0
  160. package/dist/StatCard.js.map +1 -0
  161. package/dist/Steps.d.ts +14 -0
  162. package/dist/Steps.js +94 -0
  163. package/dist/Steps.js.map +1 -0
  164. package/dist/Table.d.ts +21 -0
  165. package/dist/Table.js +93 -0
  166. package/dist/Table.js.map +1 -0
  167. package/dist/TableOfContents.d.ts +17 -0
  168. package/dist/TableOfContents.js +92 -0
  169. package/dist/TableOfContents.js.map +1 -0
  170. package/dist/Tabs.d.ts +17 -0
  171. package/dist/Tabs.js +123 -0
  172. package/dist/Tabs.js.map +1 -0
  173. package/dist/TagInput.d.ts +16 -0
  174. package/dist/TagInput.js +169 -0
  175. package/dist/TagInput.js.map +1 -0
  176. package/dist/Textarea.d.ts +13 -0
  177. package/dist/Textarea.js +74 -0
  178. package/dist/Textarea.js.map +1 -0
  179. package/dist/TimePicker.d.ts +28 -0
  180. package/dist/TimePicker.js +264 -0
  181. package/dist/TimePicker.js.map +1 -0
  182. package/dist/Timeline.d.ts +19 -0
  183. package/dist/Timeline.js +76 -0
  184. package/dist/Timeline.js.map +1 -0
  185. package/dist/Toast.d.ts +18 -0
  186. package/dist/Toast.js +318 -0
  187. package/dist/Toast.js.map +1 -0
  188. package/dist/Toggle.d.ts +13 -0
  189. package/dist/Toggle.js +84 -0
  190. package/dist/Toggle.js.map +1 -0
  191. package/dist/Tooltip.d.ts +13 -0
  192. package/dist/Tooltip.js +138 -0
  193. package/dist/Tooltip.js.map +1 -0
  194. package/dist/cli.js +1644 -0
  195. package/dist/index.d.ts +68 -631
  196. package/dist/index.js +67 -5558
  197. package/dist/index.js.map +1 -1
  198. package/dist/registry/Accordion.tsx +140 -0
  199. package/dist/registry/Alert.tsx +95 -0
  200. package/dist/registry/AuthorCard.tsx +67 -0
  201. package/dist/registry/Avatar.tsx +116 -0
  202. package/dist/registry/Badge.tsx +55 -0
  203. package/dist/registry/Banner.tsx +86 -0
  204. package/dist/registry/Breadcrumb.tsx +60 -0
  205. package/dist/registry/Button.tsx +361 -0
  206. package/dist/registry/ButtonGroup.tsx +82 -0
  207. package/dist/registry/Card.tsx +241 -0
  208. package/dist/registry/CardShell.tsx +78 -0
  209. package/dist/registry/Carousel.tsx +296 -0
  210. package/dist/registry/Checkbox.tsx +104 -0
  211. package/dist/registry/CloseButton.tsx +68 -0
  212. package/dist/registry/CodeBlock.tsx +202 -0
  213. package/dist/registry/ColorPicker.tsx +404 -0
  214. package/dist/registry/Combobox.tsx +318 -0
  215. package/dist/registry/CommandPalette.tsx +356 -0
  216. package/dist/registry/ContextMenu.tsx +241 -0
  217. package/dist/registry/DataList.tsx +49 -0
  218. package/dist/registry/DatePicker.tsx +494 -0
  219. package/dist/registry/DateRangePicker.tsx +232 -0
  220. package/dist/registry/Drawer.tsx +166 -0
  221. package/dist/registry/DropdownMenu.tsx +219 -0
  222. package/dist/registry/EmptyState.tsx +55 -0
  223. package/dist/registry/FieldMessage.tsx +123 -0
  224. package/dist/registry/FileUpload.tsx +318 -0
  225. package/dist/registry/Floating.tsx +134 -0
  226. package/dist/registry/HoverCard.tsx +113 -0
  227. package/dist/registry/Input.tsx +108 -0
  228. package/dist/registry/Kbd.tsx +23 -0
  229. package/dist/registry/Modal.tsx +130 -0
  230. package/dist/registry/NumberInput.tsx +183 -0
  231. package/dist/registry/OtpInput.tsx +219 -0
  232. package/dist/registry/Pagination.tsx +160 -0
  233. package/dist/registry/Popover.tsx +116 -0
  234. package/dist/registry/Progress.tsx +66 -0
  235. package/dist/registry/QuoteCard.tsx +93 -0
  236. package/dist/registry/Radio.tsx +205 -0
  237. package/dist/registry/RadioCards.tsx +247 -0
  238. package/dist/registry/ReactionBar.tsx +101 -0
  239. package/dist/registry/ReadingProgress.tsx +51 -0
  240. package/dist/registry/ReadingTime.tsx +35 -0
  241. package/dist/registry/Select.tsx +265 -0
  242. package/dist/registry/Separator.tsx +52 -0
  243. package/dist/registry/ShareButtons.tsx +134 -0
  244. package/dist/registry/Sidebar.tsx +182 -0
  245. package/dist/registry/Skeleton.tsx +83 -0
  246. package/dist/registry/Slider.tsx +229 -0
  247. package/dist/registry/Spinner.tsx +32 -0
  248. package/dist/registry/SpotlightCard.tsx +106 -0
  249. package/dist/registry/Stat.tsx +82 -0
  250. package/dist/registry/StatCard.tsx +256 -0
  251. package/dist/registry/Steps.tsx +109 -0
  252. package/dist/registry/Table.tsx +123 -0
  253. package/dist/registry/TableOfContents.tsx +116 -0
  254. package/dist/registry/Tabs.tsx +136 -0
  255. package/dist/registry/TagInput.tsx +191 -0
  256. package/dist/registry/Textarea.tsx +84 -0
  257. package/dist/registry/TimePicker.tsx +315 -0
  258. package/dist/registry/Timeline.tsx +102 -0
  259. package/dist/registry/Toast.tsx +388 -0
  260. package/dist/registry/Toggle.tsx +96 -0
  261. package/dist/registry/Tooltip.tsx +160 -0
  262. package/dist/registry/styles.css +1207 -0
  263. package/dist/registry/useFloatingPanel.ts +112 -0
  264. package/dist/registry/useModalBehavior.ts +77 -0
  265. package/dist/styles.css +868 -62
  266. package/dist/useFloatingPanel.d.ts +24 -0
  267. package/dist/useFloatingPanel.js +89 -0
  268. package/dist/useFloatingPanel.js.map +1 -0
  269. package/dist/useModalBehavior.d.ts +15 -0
  270. package/dist/useModalBehavior.js +57 -0
  271. package/dist/useModalBehavior.js.map +1 -0
  272. 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
+ }