sid-ui 0.0.0-stage → 0.1.0

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 (116) hide show
  1. package/README.md +54 -2
  2. package/package.json +51 -4
  3. package/src/brand/BrandFooter.tsx +45 -0
  4. package/src/brand/BrandHeading.tsx +24 -0
  5. package/src/brand/BrandLockup.tsx +47 -0
  6. package/src/brand/JoinedLogo.tsx +67 -0
  7. package/src/brand/JoinedMark.tsx +76 -0
  8. package/src/brand/geometry.ts +26 -0
  9. package/src/brand/index.ts +11 -0
  10. package/src/brand/name.ts +5 -0
  11. package/src/brand/paint.tsx +56 -0
  12. package/src/components/Action.tsx +44 -0
  13. package/src/components/AddressSelector.tsx +99 -0
  14. package/src/components/BarChart.tsx +132 -0
  15. package/src/components/Calendar.tsx +284 -0
  16. package/src/components/CalendarAgenda.tsx +87 -0
  17. package/src/components/CalendarMonth.tsx +223 -0
  18. package/src/components/CalendarSchedule.tsx +210 -0
  19. package/src/components/Card.tsx +80 -0
  20. package/src/components/Charts.tsx +64 -0
  21. package/src/components/Chat.tsx +35 -0
  22. package/src/components/CitySelector.tsx +81 -0
  23. package/src/components/Clock.tsx +196 -0
  24. package/src/components/CodeInput.tsx +183 -0
  25. package/src/components/Container.tsx +20 -0
  26. package/src/components/Content.tsx +50 -0
  27. package/src/components/Data.tsx +43 -0
  28. package/src/components/DataInput.tsx +107 -0
  29. package/src/components/DateField.tsx +162 -0
  30. package/src/components/DonutChart.tsx +123 -0
  31. package/src/components/Drawer.tsx +110 -0
  32. package/src/components/Feedback.tsx +37 -0
  33. package/src/components/FileUploader.tsx +377 -0
  34. package/src/components/FunnelChart.tsx +83 -0
  35. package/src/components/Glyph.tsx +330 -0
  36. package/src/components/GoogleSignIn.tsx +67 -0
  37. package/src/components/GridSystem.tsx +131 -0
  38. package/src/components/HeatmapCalendar.tsx +167 -0
  39. package/src/components/KanbanBoard.tsx +421 -0
  40. package/src/components/Kpi.tsx +61 -0
  41. package/src/components/LayoutPrimitives.tsx +86 -0
  42. package/src/components/Lists.tsx +19 -0
  43. package/src/components/LocationSelector.tsx +49 -0
  44. package/src/components/MessageList.tsx +80 -0
  45. package/src/components/Nav.tsx +69 -0
  46. package/src/components/Navigation.tsx +52 -0
  47. package/src/components/Notification.tsx +115 -0
  48. package/src/components/NotificationTrigger.tsx +248 -0
  49. package/src/components/Overlay.tsx +50 -0
  50. package/src/components/Page.tsx +143 -0
  51. package/src/components/PageTabs.tsx +28 -0
  52. package/src/components/PickerShell.tsx +138 -0
  53. package/src/components/PillNav.tsx +194 -0
  54. package/src/components/Primitives.tsx +30 -0
  55. package/src/components/Rating.tsx +78 -0
  56. package/src/components/Responsive.tsx +128 -0
  57. package/src/components/ResponsiveFrame.tsx +122 -0
  58. package/src/components/Segment.tsx +190 -0
  59. package/src/components/Sparkline.tsx +52 -0
  60. package/src/components/StateSelector.tsx +39 -0
  61. package/src/components/Sticky.tsx +95 -0
  62. package/src/components/Table.tsx +344 -0
  63. package/src/components/ThemeToggle.tsx +48 -0
  64. package/src/components/Tile.tsx +23 -0
  65. package/src/components/TimeColumns.tsx +155 -0
  66. package/src/components/TimeDial.tsx +260 -0
  67. package/src/components/TimeField.tsx +196 -0
  68. package/src/components/TimeInput.tsx +213 -0
  69. package/src/components/TimeSlots.tsx +83 -0
  70. package/src/components/Timeline.tsx +111 -0
  71. package/src/components/TokenDemo.tsx +378 -0
  72. package/src/components/Tree.tsx +283 -0
  73. package/src/components/TrendChart.tsx +211 -0
  74. package/src/components/breakpoints.ts +42 -0
  75. package/src/components/calendarTypes.ts +26 -0
  76. package/src/components/chartMath.ts +125 -0
  77. package/src/components/chartParts.tsx +116 -0
  78. package/src/components/codeInputRules.ts +58 -0
  79. package/src/components/date.ts +87 -0
  80. package/src/components/hooks.ts +53 -0
  81. package/src/components/index.ts +195 -0
  82. package/src/components/kanban.ts +78 -0
  83. package/src/components/motion.ts +78 -0
  84. package/src/components/places-search.tsx +73 -0
  85. package/src/components/places.ts +209 -0
  86. package/src/components/size.ts +3 -0
  87. package/src/components/time.ts +83 -0
  88. package/src/components/useFileDrop.ts +104 -0
  89. package/src/places/geoapify.ts +129 -0
  90. package/src/styles/astryx.css +5 -0
  91. package/src/styles/components/brand.css +98 -0
  92. package/src/styles/components/calendar.css +680 -0
  93. package/src/styles/components/charts.css +659 -0
  94. package/src/styles/components/code-input.css +233 -0
  95. package/src/styles/components/google.css +11 -0
  96. package/src/styles/components/message-list.css +153 -0
  97. package/src/styles/components/notification.css +350 -0
  98. package/src/styles/components/picker.css +114 -0
  99. package/src/styles/components/pill-nav.css +179 -0
  100. package/src/styles/components/responsive.css +419 -0
  101. package/src/styles/components/sticky.css +23 -0
  102. package/src/styles/components/table.css +267 -0
  103. package/src/styles/components/time.css +526 -0
  104. package/src/styles/components/timeline.css +426 -0
  105. package/src/styles/components/tree.css +269 -0
  106. package/src/styles/components.css +423 -0
  107. package/src/styles/joined.css +4 -0
  108. package/src/styles/theme.css +79 -0
  109. package/src/styles/tokens.css +338 -0
  110. package/src/theme/JoinedProvider.tsx +50 -0
  111. package/src/theme/index.ts +3 -0
  112. package/src/theme/joined.d.ts +11 -0
  113. package/src/theme/joined.js +569 -0
  114. package/src/theme/joined.theme.ts +74 -0
  115. package/src/theme/joined.variants.d.ts +16 -0
  116. package/src/theme/theme.css +786 -0
@@ -0,0 +1,83 @@
1
+ "use client";
2
+
3
+ import { displayTime, fromMinutes, minutesOf, range, type HourCycle } from "./time";
4
+
5
+ import type { ControlSize } from "./size";
6
+
7
+ /** Where the day splits when `grouped` is on, in minutes since midnight. */
8
+ const DAY_PARTS = [
9
+ { label: "Morning", from: 0 },
10
+ { label: "Afternoon", from: 12 * 60 },
11
+ { label: "Evening", from: 17 * 60 },
12
+ ] as const;
13
+
14
+ export interface TimeSlotsProps {
15
+ value: string;
16
+ onChange?: (value: string) => void;
17
+ /** First slot, "HH:mm". */
18
+ start?: string;
19
+ /** Last slot, inclusive. */
20
+ end?: string;
21
+ /** Minutes between slots. */
22
+ interval?: number;
23
+ hourCycle?: HourCycle;
24
+ /** Slots that exist but can't be booked. */
25
+ unavailable?: string[];
26
+ /** Splits the list under Morning / Afternoon / Evening. */
27
+ grouped?: boolean;
28
+ size?: ControlSize;
29
+ disabled?: boolean;
30
+ }
31
+
32
+ /** A grid of bookable times — pick one tap, see what's taken at a glance. */
33
+ export function TimeSlots({
34
+ value,
35
+ onChange,
36
+ start = "09:00",
37
+ end = "17:00",
38
+ interval = 30,
39
+ hourCycle = "12h",
40
+ unavailable = [],
41
+ grouped = false,
42
+ size = "md",
43
+ disabled,
44
+ }: TimeSlotsProps) {
45
+ const slots = range(minutesOf(start), minutesOf(end), interval).map(fromMinutes);
46
+ const groups = grouped
47
+ ? DAY_PARTS.map((part, index) => ({
48
+ label: part.label,
49
+ slots: slots.filter((slot) => {
50
+ const at = minutesOf(slot);
51
+ return at >= part.from && at < (DAY_PARTS[index + 1]?.from ?? Infinity);
52
+ }),
53
+ })).filter((group) => group.slots.length > 0)
54
+ : [{ label: "", slots }];
55
+
56
+ return (
57
+ <div className={`os-slots os-slots-${size}`} role="radiogroup" aria-label="Available times">
58
+ {groups.map((group) => (
59
+ <div key={group.label || "all"} className="os-slots-group">
60
+ {group.label && <p className="os-slots-heading">{group.label}</p>}
61
+ <div className="os-slots-grid">
62
+ {group.slots.map((slot) => {
63
+ const taken = unavailable.includes(slot);
64
+ return (
65
+ <button
66
+ key={slot}
67
+ type="button"
68
+ role="radio"
69
+ aria-checked={slot === value}
70
+ className="os-slot"
71
+ disabled={disabled || taken}
72
+ onClick={() => onChange?.(slot)}
73
+ >
74
+ {displayTime(slot, hourCycle)}
75
+ </button>
76
+ );
77
+ })}
78
+ </div>
79
+ </div>
80
+ ))}
81
+ </div>
82
+ );
83
+ }
@@ -0,0 +1,111 @@
1
+ import { Fragment, type ReactNode } from "react";
2
+
3
+ import { Glyph } from "./Glyph";
4
+
5
+ export type TimelineTone = "neutral" | "accent" | "success" | "warning" | "danger";
6
+
7
+ /** done fills the marker and the rail behind it; current glows; upcoming stays hollow. */
8
+ export type TimelineStatus = "done" | "current" | "upcoming";
9
+
10
+ export interface TimelineItem {
11
+ id: string;
12
+ title: string;
13
+ time?: string;
14
+ description?: string;
15
+ meta?: ReactNode;
16
+ /** Replaces the dot — an initial, icon, or avatar. */
17
+ marker?: ReactNode;
18
+ tone?: TimelineTone;
19
+ status?: TimelineStatus;
20
+ /** Items that share a group sit under one heading, such as "Today". */
21
+ group?: string;
22
+ }
23
+
24
+ /**
25
+ * rail — the default: dots on a line, time beside the title.
26
+ * compact — a log: time in its own column, one line per event.
27
+ * cards — each event on its own surface.
28
+ * alternate — events swap sides of a centred rail.
29
+ * horizontal — a left-to-right track, like milestones.
30
+ * activity — larger markers for people and icons, feed-style.
31
+ */
32
+ export type TimelineVariant =
33
+ "rail" | "compact" | "cards" | "alternate" | "horizontal" | "activity";
34
+
35
+ export interface TimelineProps {
36
+ items: TimelineItem[];
37
+ variant?: TimelineVariant;
38
+ /** Accessible name for the list. */
39
+ label?: string;
40
+ }
41
+
42
+ function Marker({ item }: { item: TimelineItem }) {
43
+ const status = item.status ?? "plain";
44
+ const content = item.marker ?? (status === "done" ? <Glyph name="check" /> : null);
45
+ return (
46
+ <span
47
+ className={[
48
+ "os-tl-marker",
49
+ `os-tl-tone-${item.tone ?? "accent"}`,
50
+ `os-tl-${status}`,
51
+ item.marker != null && "os-tl-marker-custom",
52
+ ]
53
+ .filter(Boolean)
54
+ .join(" ")}
55
+ aria-hidden
56
+ >
57
+ {content}
58
+ </span>
59
+ );
60
+ }
61
+
62
+ /** A sequence of events. Tone colors the marker; the title always says what happened. */
63
+ export function Timeline({ items, variant = "rail", label = "Timeline" }: TimelineProps) {
64
+ const grouped = variant !== "horizontal";
65
+
66
+ return (
67
+ <ol className={`os-tl os-tl-${variant}`} aria-label={label}>
68
+ {items.map((item, index) => {
69
+ const heading =
70
+ grouped && item.group && item.group !== items[index - 1]?.group ? item.group : null;
71
+ const nextStatus = items[index + 1]?.status;
72
+ return (
73
+ <Fragment key={item.id}>
74
+ {heading && (
75
+ <li className="os-tl-heading" role="presentation">
76
+ <span>{heading}</span>
77
+ </li>
78
+ )}
79
+ <li
80
+ className={[
81
+ "os-tl-item",
82
+ `os-tl-item-${item.status ?? "plain"}`,
83
+ nextStatus === "upcoming" && "os-tl-item-before-upcoming",
84
+ ]
85
+ .filter(Boolean)
86
+ .join(" ")}
87
+ aria-current={item.status === "current" ? "step" : undefined}
88
+ >
89
+ {variant === "compact" && item.time && (
90
+ <time className="os-tl-time os-tl-time-lead">{item.time}</time>
91
+ )}
92
+ <span className="os-tl-track">
93
+ <Marker item={item} />
94
+ </span>
95
+ <div className="os-tl-body">
96
+ <div className="os-tl-head">
97
+ <p className="os-tl-title">{item.title}</p>
98
+ {variant !== "compact" && item.time && (
99
+ <time className="os-tl-time">{item.time}</time>
100
+ )}
101
+ </div>
102
+ {item.description && <p className="os-tl-desc">{item.description}</p>}
103
+ {item.meta && <div className="os-tl-meta">{item.meta}</div>}
104
+ </div>
105
+ </li>
106
+ </Fragment>
107
+ );
108
+ })}
109
+ </ol>
110
+ );
111
+ }
@@ -0,0 +1,378 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ import { Button } from "./Action";
6
+
7
+ type Theme = "dark" | "light";
8
+
9
+ const COLOR_GROUPS: { title: string; tokens: string[] }[] = [
10
+ {
11
+ title: "Meta blue ramp",
12
+ tokens: [
13
+ "meta-blue-50",
14
+ "meta-blue-100",
15
+ "meta-blue-200",
16
+ "meta-blue-300",
17
+ "meta-blue-400",
18
+ "meta-blue-500",
19
+ "meta-blue-600",
20
+ "meta-blue-700",
21
+ "meta-blue-800",
22
+ "meta-blue-900",
23
+ "meta-blue-950",
24
+ ],
25
+ },
26
+ {
27
+ title: "Canvas & surface",
28
+ tokens: ["canvas", "surface", "surface-sunken", "surface-hover", "surface-selected"],
29
+ },
30
+ { title: "Border", tokens: ["border-subtle", "border-default", "border-strong", "border-focus"] },
31
+ { title: "Ink", tokens: ["ink", "ink-muted", "ink-faint", "on-primary"] },
32
+ {
33
+ title: "Primary & link",
34
+ tokens: ["primary", "primary-hover", "primary-active", "primary-bg", "link", "link-hover"],
35
+ },
36
+ {
37
+ title: "Status",
38
+ tokens: [
39
+ "success",
40
+ "success-bg",
41
+ "warning",
42
+ "warning-bg",
43
+ "danger",
44
+ "danger-hover",
45
+ "danger-bg",
46
+ ],
47
+ },
48
+ { title: "Overlay", tokens: ["overlay"] },
49
+ ];
50
+
51
+ const RADII = ["radius-sm", "radius-md", "radius-lg", "radius-xl", "radius-pill", "radius-full"];
52
+ const ELEVATIONS = ["elevation-0", "elevation-1", "elevation-2", "elevation-3"];
53
+ const BORDER_WIDTHS = ["border-width-hairline", "border-width-thick"];
54
+ const OPACITIES = ["opacity-disabled", "opacity-overlay", "opacity-hover-scrim"];
55
+ const DURATIONS = ["duration-fast", "duration-normal", "duration-slow"];
56
+ const EASINGS = ["ease-standard", "ease-decelerate", "ease-accelerate"];
57
+ const TYPE_STYLES = [
58
+ "display",
59
+ "h1",
60
+ "h2",
61
+ "h3",
62
+ "body-lg",
63
+ "body",
64
+ "body-strong",
65
+ "body-sm",
66
+ "label",
67
+ "caption",
68
+ "button",
69
+ ];
70
+
71
+ const ALL_TOKENS = [
72
+ ...COLOR_GROUPS.flatMap((g) => g.tokens),
73
+ ...RADII,
74
+ ...ELEVATIONS,
75
+ ...BORDER_WIDTHS,
76
+ ...OPACITIES,
77
+ ...DURATIONS,
78
+ ...EASINGS,
79
+ ];
80
+
81
+ /** Reads every token's resolved value off <html> so the table shows what the
82
+ active theme actually paints, not the source-file default. */
83
+ function readTokens(): Record<string, string> {
84
+ const style = getComputedStyle(document.documentElement);
85
+ const out: Record<string, string> = {};
86
+ for (const t of ALL_TOKENS) out[t] = style.getPropertyValue(`--${t}`).trim();
87
+ return out;
88
+ }
89
+
90
+ function Group({ title, children }: { title: string; children: React.ReactNode }) {
91
+ return (
92
+ <div className="os-token-group">
93
+ <h3 className="h3" style={{ marginBottom: 12 }}>
94
+ {title}
95
+ </h3>
96
+ {children}
97
+ </div>
98
+ );
99
+ }
100
+
101
+ function Value({ children }: { children: string }) {
102
+ return <code className="caption text-ink-muted">{children || "—"}</code>;
103
+ }
104
+
105
+ /**
106
+ * A toggleable, self-describing demo of every Joined design token: a sample
107
+ * rendered with the token beside its name and live resolved value. Includes a
108
+ * dark/light switch because most values differ per theme.
109
+ */
110
+ export function TokenDemo({ defaultOpen = false }: { defaultOpen?: boolean }) {
111
+ const [open, setOpen] = useState(defaultOpen);
112
+ const [theme, setTheme] = useState<Theme>("dark");
113
+ const [values, setValues] = useState<Record<string, string>>({});
114
+ const [motionKey, setMotionKey] = useState(0);
115
+
116
+ useEffect(() => {
117
+ if (!open) return;
118
+ const current = document.documentElement.getAttribute("data-theme");
119
+ if (current === "light" || current === "dark") setTheme(current);
120
+ setValues(readTokens());
121
+ }, [open]);
122
+
123
+ const show = (next: boolean) => {
124
+ setOpen(next);
125
+ if (!next) return;
126
+ const current = document.documentElement.getAttribute("data-theme");
127
+ if (current === "light" || current === "dark") setTheme(current);
128
+ setValues(readTokens());
129
+ };
130
+
131
+ const switchTheme = (next: Theme) => {
132
+ document.documentElement.setAttribute("data-theme", next);
133
+ setTheme(next);
134
+ setValues(readTokens());
135
+ };
136
+
137
+ return (
138
+ <div className="os-token-demo">
139
+ <div className="os-token-demo-toolbar">
140
+ <label className="os-check">
141
+ <input type="checkbox" checked={open} onChange={(e) => show(e.target.checked)} />
142
+ <span className="body-strong">Show design token demo</span>
143
+ </label>
144
+ {open && (
145
+ <div role="group" aria-label="Theme">
146
+ <Button
147
+ label="Dark"
148
+ size="sm"
149
+ variant={theme === "dark" ? "secondary" : "ghost"}
150
+ onClick={() => switchTheme("dark")}
151
+ />
152
+ <Button
153
+ label="Light"
154
+ size="sm"
155
+ variant={theme === "light" ? "secondary" : "ghost"}
156
+ onClick={() => switchTheme("light")}
157
+ />
158
+ </div>
159
+ )}
160
+ </div>
161
+
162
+ {open && (
163
+ <div style={{ marginTop: 24 }}>
164
+ <Group title="Color">
165
+ {COLOR_GROUPS.map((g) => (
166
+ <div key={g.title} style={{ marginBottom: 16 }}>
167
+ <p className="label text-ink-muted" style={{ marginBottom: 8 }}>
168
+ {g.title}
169
+ </p>
170
+ <div className="os-swatch-grid">
171
+ {g.tokens.map((t) => (
172
+ <div key={t} className="os-swatch">
173
+ <div className="os-swatch-chip" style={{ background: `var(--${t})` }} />
174
+ <div className="os-swatch-body">
175
+ <span className="caption text-ink">--{t}</span>
176
+ <Value>{values[t]}</Value>
177
+ </div>
178
+ </div>
179
+ ))}
180
+ </div>
181
+ </div>
182
+ ))}
183
+ </Group>
184
+
185
+ <Group title="Radius">
186
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 16 }}>
187
+ {RADII.map((t) => (
188
+ <div
189
+ key={t}
190
+ style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 8 }}
191
+ >
192
+ <div
193
+ style={{
194
+ width: 64,
195
+ height: 64,
196
+ border: "1px solid var(--border-default)",
197
+ background: "var(--primary-bg)",
198
+ borderRadius: `var(--${t})`,
199
+ }}
200
+ />
201
+ <span className="caption text-ink">--{t}</span>
202
+ <Value>{values[t]}</Value>
203
+ </div>
204
+ ))}
205
+ </div>
206
+ </Group>
207
+
208
+ <Group title="Elevation">
209
+ <div
210
+ style={{
211
+ display: "flex",
212
+ flexWrap: "wrap",
213
+ gap: 24,
214
+ padding: 24,
215
+ background: "var(--canvas)",
216
+ borderRadius: 8,
217
+ }}
218
+ >
219
+ {ELEVATIONS.map((t) => (
220
+ <div
221
+ key={t}
222
+ style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 8 }}
223
+ >
224
+ <div
225
+ style={{
226
+ width: 96,
227
+ height: 64,
228
+ borderRadius: 8,
229
+ border: "1px solid var(--border-subtle)",
230
+ background: "var(--surface)",
231
+ boxShadow: `var(--${t})`,
232
+ }}
233
+ />
234
+ <span className="caption text-ink">--{t}</span>
235
+ <Value>{values[t]}</Value>
236
+ </div>
237
+ ))}
238
+ </div>
239
+ </Group>
240
+
241
+ <Group title="Border width">
242
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 24 }}>
243
+ {BORDER_WIDTHS.map((t) => (
244
+ <div
245
+ key={t}
246
+ style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 8 }}
247
+ >
248
+ <div
249
+ style={{
250
+ width: 96,
251
+ height: 48,
252
+ borderRadius: 6,
253
+ background: "var(--surface-sunken)",
254
+ borderStyle: "solid",
255
+ borderColor: "var(--border-strong)",
256
+ borderWidth: `var(--${t})`,
257
+ }}
258
+ />
259
+ <span className="caption text-ink">--{t}</span>
260
+ <Value>{values[t]}</Value>
261
+ </div>
262
+ ))}
263
+ </div>
264
+ </Group>
265
+
266
+ <Group title="Opacity">
267
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 24 }}>
268
+ {OPACITIES.map((t) => (
269
+ <div
270
+ key={t}
271
+ style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 8 }}
272
+ >
273
+ <div
274
+ style={{
275
+ position: "relative",
276
+ width: 96,
277
+ height: 48,
278
+ overflow: "hidden",
279
+ borderRadius: 6,
280
+ background: "var(--primary)",
281
+ }}
282
+ >
283
+ <div
284
+ style={{
285
+ position: "absolute",
286
+ inset: 0,
287
+ background: "var(--ink)",
288
+ opacity: `var(--${t})`,
289
+ }}
290
+ />
291
+ </div>
292
+ <span className="caption text-ink">--{t}</span>
293
+ <Value>{values[t]}</Value>
294
+ </div>
295
+ ))}
296
+ </div>
297
+ </Group>
298
+
299
+ <Group title="Motion">
300
+ <p className="body-sm text-ink-muted" style={{ marginBottom: 12 }}>
301
+ Each row pairs a duration with an easing. Press play to run them all.
302
+ </p>
303
+ <Button
304
+ label="Play"
305
+ size="sm"
306
+ variant="secondary"
307
+ onClick={() => setMotionKey((k) => k + 1)}
308
+ />
309
+ <div style={{ marginTop: 16, display: "flex", flexDirection: "column", gap: 12 }}>
310
+ {DURATIONS.map((d, i) => {
311
+ const e = EASINGS[i];
312
+ return (
313
+ <div key={d} style={{ display: "flex", alignItems: "center", gap: 16 }}>
314
+ <div style={{ width: 220, flex: "none" }}>
315
+ <span className="caption text-ink">
316
+ --{d} / --{e}
317
+ </span>
318
+ <Value>{`${values[d]} · ${values[e]}`}</Value>
319
+ </div>
320
+ <div
321
+ style={{
322
+ position: "relative",
323
+ height: 24,
324
+ flex: 1,
325
+ borderRadius: 999,
326
+ background: "var(--surface-sunken)",
327
+ }}
328
+ >
329
+ <div
330
+ key={motionKey}
331
+ style={{
332
+ position: "absolute",
333
+ top: 4,
334
+ height: 16,
335
+ width: 16,
336
+ borderRadius: 999,
337
+ background: "var(--primary)",
338
+ left: "4px",
339
+ animation:
340
+ motionKey > 0
341
+ ? `os-token-slide var(--${d}) var(--${e}) forwards`
342
+ : "none",
343
+ }}
344
+ />
345
+ </div>
346
+ </div>
347
+ );
348
+ })}
349
+ </div>
350
+ <style>{`@keyframes os-token-slide { to { left: calc(100% - 20px); } }`}</style>
351
+ </Group>
352
+
353
+ <Group title="Type styles">
354
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
355
+ {TYPE_STYLES.map((cls) => (
356
+ <div
357
+ key={cls}
358
+ style={{
359
+ display: "flex",
360
+ alignItems: "baseline",
361
+ gap: 16,
362
+ borderBottom: "1px solid var(--border-subtle)",
363
+ paddingBottom: 8,
364
+ }}
365
+ >
366
+ <code className="caption text-ink-muted" style={{ width: 96, flex: "none" }}>
367
+ .{cls}
368
+ </code>
369
+ <span className={cls}>Sealed rooms, invited bidders.</span>
370
+ </div>
371
+ ))}
372
+ </div>
373
+ </Group>
374
+ </div>
375
+ )}
376
+ </div>
377
+ );
378
+ }