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.
- package/README.md +54 -2
- package/package.json +51 -4
- package/src/brand/BrandFooter.tsx +45 -0
- package/src/brand/BrandHeading.tsx +24 -0
- package/src/brand/BrandLockup.tsx +47 -0
- package/src/brand/JoinedLogo.tsx +67 -0
- package/src/brand/JoinedMark.tsx +76 -0
- package/src/brand/geometry.ts +26 -0
- package/src/brand/index.ts +11 -0
- package/src/brand/name.ts +5 -0
- package/src/brand/paint.tsx +56 -0
- package/src/components/Action.tsx +44 -0
- package/src/components/AddressSelector.tsx +99 -0
- package/src/components/BarChart.tsx +132 -0
- package/src/components/Calendar.tsx +284 -0
- package/src/components/CalendarAgenda.tsx +87 -0
- package/src/components/CalendarMonth.tsx +223 -0
- package/src/components/CalendarSchedule.tsx +210 -0
- package/src/components/Card.tsx +80 -0
- package/src/components/Charts.tsx +64 -0
- package/src/components/Chat.tsx +35 -0
- package/src/components/CitySelector.tsx +81 -0
- package/src/components/Clock.tsx +196 -0
- package/src/components/CodeInput.tsx +183 -0
- package/src/components/Container.tsx +20 -0
- package/src/components/Content.tsx +50 -0
- package/src/components/Data.tsx +43 -0
- package/src/components/DataInput.tsx +107 -0
- package/src/components/DateField.tsx +162 -0
- package/src/components/DonutChart.tsx +123 -0
- package/src/components/Drawer.tsx +110 -0
- package/src/components/Feedback.tsx +37 -0
- package/src/components/FileUploader.tsx +377 -0
- package/src/components/FunnelChart.tsx +83 -0
- package/src/components/Glyph.tsx +330 -0
- package/src/components/GoogleSignIn.tsx +67 -0
- package/src/components/GridSystem.tsx +131 -0
- package/src/components/HeatmapCalendar.tsx +167 -0
- package/src/components/KanbanBoard.tsx +421 -0
- package/src/components/Kpi.tsx +61 -0
- package/src/components/LayoutPrimitives.tsx +86 -0
- package/src/components/Lists.tsx +19 -0
- package/src/components/LocationSelector.tsx +49 -0
- package/src/components/MessageList.tsx +80 -0
- package/src/components/Nav.tsx +69 -0
- package/src/components/Navigation.tsx +52 -0
- package/src/components/Notification.tsx +115 -0
- package/src/components/NotificationTrigger.tsx +248 -0
- package/src/components/Overlay.tsx +50 -0
- package/src/components/Page.tsx +143 -0
- package/src/components/PageTabs.tsx +28 -0
- package/src/components/PickerShell.tsx +138 -0
- package/src/components/PillNav.tsx +194 -0
- package/src/components/Primitives.tsx +30 -0
- package/src/components/Rating.tsx +78 -0
- package/src/components/Responsive.tsx +128 -0
- package/src/components/ResponsiveFrame.tsx +122 -0
- package/src/components/Segment.tsx +190 -0
- package/src/components/Sparkline.tsx +52 -0
- package/src/components/StateSelector.tsx +39 -0
- package/src/components/Sticky.tsx +95 -0
- package/src/components/Table.tsx +344 -0
- package/src/components/ThemeToggle.tsx +48 -0
- package/src/components/Tile.tsx +23 -0
- package/src/components/TimeColumns.tsx +155 -0
- package/src/components/TimeDial.tsx +260 -0
- package/src/components/TimeField.tsx +196 -0
- package/src/components/TimeInput.tsx +213 -0
- package/src/components/TimeSlots.tsx +83 -0
- package/src/components/Timeline.tsx +111 -0
- package/src/components/TokenDemo.tsx +378 -0
- package/src/components/Tree.tsx +283 -0
- package/src/components/TrendChart.tsx +211 -0
- package/src/components/breakpoints.ts +42 -0
- package/src/components/calendarTypes.ts +26 -0
- package/src/components/chartMath.ts +125 -0
- package/src/components/chartParts.tsx +116 -0
- package/src/components/codeInputRules.ts +58 -0
- package/src/components/date.ts +87 -0
- package/src/components/hooks.ts +53 -0
- package/src/components/index.ts +195 -0
- package/src/components/kanban.ts +78 -0
- package/src/components/motion.ts +78 -0
- package/src/components/places-search.tsx +73 -0
- package/src/components/places.ts +209 -0
- package/src/components/size.ts +3 -0
- package/src/components/time.ts +83 -0
- package/src/components/useFileDrop.ts +104 -0
- package/src/places/geoapify.ts +129 -0
- package/src/styles/astryx.css +5 -0
- package/src/styles/components/brand.css +98 -0
- package/src/styles/components/calendar.css +680 -0
- package/src/styles/components/charts.css +659 -0
- package/src/styles/components/code-input.css +233 -0
- package/src/styles/components/google.css +11 -0
- package/src/styles/components/message-list.css +153 -0
- package/src/styles/components/notification.css +350 -0
- package/src/styles/components/picker.css +114 -0
- package/src/styles/components/pill-nav.css +179 -0
- package/src/styles/components/responsive.css +419 -0
- package/src/styles/components/sticky.css +23 -0
- package/src/styles/components/table.css +267 -0
- package/src/styles/components/time.css +526 -0
- package/src/styles/components/timeline.css +426 -0
- package/src/styles/components/tree.css +269 -0
- package/src/styles/components.css +423 -0
- package/src/styles/joined.css +4 -0
- package/src/styles/theme.css +79 -0
- package/src/styles/tokens.css +338 -0
- package/src/theme/JoinedProvider.tsx +50 -0
- package/src/theme/index.ts +3 -0
- package/src/theme/joined.d.ts +11 -0
- package/src/theme/joined.js +569 -0
- package/src/theme/joined.theme.ts +74 -0
- package/src/theme/joined.variants.d.ts +16 -0
- 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
|
+
}
|