@robr0/design-system 0.13.0 → 0.15.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 +15 -6
- package/charts.d.ts +1 -0
- package/charts.js +2 -0
- package/components/AnchorNav/AnchorNav.css +74 -0
- package/components/AnchorNav/AnchorNav.d.ts +7 -0
- package/components/AnchorNav/AnchorNav.js +74 -21
- package/components/Banner/Banner.css +110 -0
- package/components/Banner/Banner.d.ts +36 -0
- package/components/Banner/Banner.js +59 -0
- package/components/Button/Button.css +51 -0
- package/components/Button/Button.d.ts +2 -2
- package/components/Chart/Chart.css +4 -1
- package/components/CircularButton/CircularButton.css +46 -0
- package/components/CircularButton/CircularButton.d.ts +13 -2
- package/components/CircularButton/CircularButton.js +19 -16
- package/components/CommandPalette/CommandPalette.css +26 -1
- package/components/CommandPalette/CommandPalette.js +11 -7
- package/components/Composer/Composer.css +18 -0
- package/components/Composer/Composer.d.ts +4 -2
- package/components/Composer/Composer.js +1 -0
- package/components/ContributionGraph/ContributionGraph.css +28 -1
- package/components/ContributionGraph/ContributionGraph.d.ts +9 -1
- package/components/ContributionGraph/ContributionGraph.js +66 -42
- package/components/EmptyState/EmptyState.css +5 -0
- package/components/Figure/Figure.css +14 -4
- package/components/Figure/Figure.js +11 -20
- package/components/FunnelChart/FunnelChart.css +11 -35
- package/components/FunnelChart/FunnelChart.d.ts +26 -15
- package/components/FunnelChart/FunnelChart.js +82 -34
- package/components/Gauge/Gauge.css +39 -12
- package/components/Gauge/Gauge.d.ts +16 -8
- package/components/Gauge/Gauge.js +78 -62
- package/components/HoverCard/HoverCard.css +97 -0
- package/components/HoverCard/HoverCard.d.ts +29 -0
- package/components/HoverCard/HoverCard.js +83 -0
- package/components/ImageCompare/ImageCompare.css +112 -0
- package/components/ImageCompare/ImageCompare.d.ts +40 -0
- package/components/ImageCompare/ImageCompare.js +134 -0
- package/components/LinkList/LinkList.d.ts +3 -1
- package/components/LinkList/LinkList.js +4 -3
- package/components/Meter/Meter.css +98 -0
- package/components/Meter/Meter.d.ts +36 -0
- package/components/Meter/Meter.js +48 -0
- package/components/NotificationCenter/NotificationCenter.css +11 -3
- package/components/ProgressBar/ProgressBar.css +1 -1
- package/components/Rating/Rating.css +74 -0
- package/components/Rating/Rating.d.ts +41 -0
- package/components/Rating/Rating.js +124 -0
- package/components/SegmentedControl/SegmentedControl.css +23 -0
- package/components/SegmentedControl/SegmentedControl.d.ts +3 -1
- package/components/SegmentedControl/SegmentedControl.js +3 -1
- package/components/Slider/Slider.css +1 -1
- package/components/Slider/Slider.js +1 -1
- package/components/SourceTrail/SourceTrail.css +209 -0
- package/components/SourceTrail/SourceTrail.d.ts +49 -0
- package/components/SourceTrail/SourceTrail.js +108 -0
- package/components/Sparkline/Sparkline.css +26 -2
- package/components/Sparkline/Sparkline.d.ts +5 -3
- package/components/Sparkline/Sparkline.js +32 -2
- package/components/Spinner/Spinner.css +1 -1
- package/components/SplitButton/SplitButton.css +93 -0
- package/components/SplitButton/SplitButton.d.ts +43 -0
- package/components/SplitButton/SplitButton.js +67 -0
- package/components/StreamingText/StreamingText.d.ts +27 -9
- package/components/StreamingText/StreamingText.js +17 -29
- package/components/StreamingText/useStreamReveal.d.ts +70 -0
- package/components/StreamingText/useStreamReveal.js +121 -0
- package/components/ToggleGroup/ToggleGroup.css +17 -0
- package/components/ToggleGroup/ToggleGroup.d.ts +2 -0
- package/components/ToggleGroup/ToggleGroup.js +2 -0
- package/components/ToolCall/ToolCall.css +7 -2
- package/components/Tooltip/Tooltip.css +24 -7
- package/components/Tooltip/Tooltip.d.ts +2 -1
- package/components/UsageCard/UsageCard.css +30 -0
- package/components/UsageCard/UsageCard.d.ts +48 -0
- package/components/UsageCard/UsageCard.js +54 -0
- package/components/registry.d.ts +8 -2
- package/components/registry.json +89 -14
- package/components/registry.json.d.ts +89 -14
- package/components/registry.json.js +1 -1
- package/index.d.ts +8 -1
- package/index.js +19 -2
- package/package.json +5 -1
- package/tokens/motion.d.ts +9 -4
- package/tokens/motion.js +5 -1
- package/tokens/registry.json +4 -0
- package/tokens/registry.json.d.ts +4 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +11 -2
- package/tokens/tokens-light.css +25 -11
- package/tokens/tokens-primitives.css +3 -0
package/README.md
CHANGED
|
@@ -18,8 +18,8 @@ The **live site** is the portfolio built on the design system. **Storybook** is
|
|
|
18
18
|
| Part | Description |
|
|
19
19
|
|---|---|
|
|
20
20
|
| **Portfolio website** (`/website`) | Next.js app with case studies, work history, writing, and about pages, all built exclusively with the design system components below. **[Live site →](https://robertritacca.com/)** |
|
|
21
|
-
| **Design system** (`/src`) | <!-- component-count -->
|
|
22
|
-
| **AI layer** (`ai` components in `/src`, chat in `/website`, `/evals`) | A site-wide chat that answers questions about the work: built from the library's own `ai` components, grounded in a corpus generated from the site's published content, and scored by a golden-set eval. |
|
|
21
|
+
| **Design system** (`/src`) | <!-- component-count -->122<!-- /component-count --> React components, a three-tier token architecture, dark mode, a WebGL2 ambient background that themes itself from your tokens, and a full documentation site. Built to production standards. **[Storybook →](https://design-system-iota-one.vercel.app/?path=/docs/robr0-ds--docs)** |
|
|
22
|
+
| **AI layer** (`ai` components in `/src`, chat in `/website`, `/evals`) | A site-wide chat that answers questions about the work: built from the library's own `ai` components, grounded in a corpus generated from the site's published content, and scored by a golden-set eval. An MCP endpoint serves the same public data, plus the component prop API, to any agent. |
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
@@ -28,7 +28,7 @@ The **live site** is the portfolio built on the design system. **Storybook** is
|
|
|
28
28
|
### Components
|
|
29
29
|
|
|
30
30
|
<!-- component-list:start -->
|
|
31
|
-
Accordion · Agent plan · Agent status · AI button · Alert · Alert dialog · Anchor nav · App layout · App sidebar · Area chart · Avatar · Avatar group · Badge · Bar chart · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gauge · Globe · Input · Instructions · Interrupt card · Kbd · Legend tile · Line chart · Link list · Map callout · Map legend · Message actions · Message card · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radar chart · Radial chart · Radio button · Reasoning · Scatter chart · Section title · Segmented control · Selection card · Shader field · Skeleton · Slider · Source chip · Sparkline · Spinner · Split pane · Stacked bar chart · Stat · Stepper · Streaming text · Swatch · Table · Tabs · Tag input · Textarea · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Tooltip · Tree view · Treemap
|
|
31
|
+
Accordion · Agent plan · Agent status · AI button · Alert · Alert dialog · Anchor nav · App layout · App sidebar · Area chart · Avatar · Avatar group · Badge · Banner · Bar chart · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gauge · Globe · Hover card · Image compare · Input · Instructions · Interrupt card · Kbd · Legend tile · Line chart · Link list · Map callout · Map legend · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Scatter chart · Section title · Segmented control · Selection card · Shader field · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Stepper · Streaming text · Swatch · Table · Tabs · Tag input · Textarea · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Tooltip · Tree view · Treemap · Usage card
|
|
32
32
|
<!-- component-list:end -->
|
|
33
33
|
|
|
34
34
|
### Ambient background
|
|
@@ -71,7 +71,7 @@ import '@robr0/design-system/tokens/tokens.css';
|
|
|
71
71
|
import { Button, Card, Badge } from '@robr0/design-system';
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
Deep imports work too (`@robr0/design-system/components/Button/Button`). The Recharts-backed charts live behind a separate entry so the optional `recharts` peer dependency is only needed if you use them;
|
|
74
|
+
Deep imports work too (`@robr0/design-system/components/Button/Button`). The Recharts-backed charts live behind a separate entry so the optional `recharts` peer dependency is only needed if you use them; the dependency-free charts export from the main barrel with everything else:
|
|
75
75
|
|
|
76
76
|
```tsx
|
|
77
77
|
import { BarChart, LineChart } from '@robr0/design-system/charts';
|
|
@@ -84,7 +84,7 @@ import { BarChart, LineChart } from '@robr0/design-system/charts';
|
|
|
84
84
|
```css
|
|
85
85
|
:root { --font-family-primary: 'Inter', sans-serif; }
|
|
86
86
|
```
|
|
87
|
-
- **Colours, radius, spacing**: every semantic token chains to a primitive, so overriding a primitive re-themes everything built on it. The action colour is theme-split by design (light fills run teal-08/09/10, dark inverts to teal-05/04/03),
|
|
87
|
+
- **Colours, radius, spacing**: every semantic token chains to a primitive, so overriding a primitive re-themes everything built on it. The action colour is theme-split by design (light fills run teal-08/09/10, dark inverts to teal-05/04/03), and the six fill steps below are the core of a rebrand:
|
|
88
88
|
```css
|
|
89
89
|
:root {
|
|
90
90
|
--primitive-teal-08: #6D31D3; /* light fill */
|
|
@@ -96,6 +96,7 @@ import { BarChart, LineChart } from '@robr0/design-system/charts';
|
|
|
96
96
|
--primitive-radius-full: 12px; /* pill buttons become rounded rectangles */
|
|
97
97
|
}
|
|
98
98
|
```
|
|
99
|
+
A complete rebrand re-keys the whole teal ramp (01-10): the remaining steps carry the primary label, active icons, hover/selected input borders, and the AI gradient, so stopping at the six fills leaves those teal. The easiest way to get all of it right is the [playground](https://www.robertritacca.com/playground): restyle the system live and copy out a complete, paste-ready override.
|
|
99
100
|
|
|
100
101
|
Icons use a bundled Material Symbols Rounded variable font (woff2): components import it themselves, so they need no extra setup. If you render raw `.material-symbols-rounded` spans of your own, import `@robr0/design-system/fonts/material-symbols.css` once. Nunito Sans, the system's default typeface, is intentionally *not* bundled: load it yourself (e.g. Google Fonts or `next/font`) or override `--font-family-primary`.
|
|
101
102
|
|
|
@@ -136,6 +137,14 @@ The answering half is a Claude-backed route (`website/src/app/api/chat/route.ts`
|
|
|
136
137
|
|
|
137
138
|
Answers are measured, not assumed. `evals/chat` holds a golden set that runs through the real route (persona, corpus, guardrails, all of it) with `npm run eval:chat`; it costs real API calls, so it runs on demand and never in CI. A second validator fails the build when a golden-set fact is missing from the corpus, which keeps the eval and the site describing the same system.
|
|
138
139
|
|
|
140
|
+
The AI layer also serves machines directly. The live site exposes a Model Context Protocol endpoint at `https://robertritacca.com/api/mcp`: a stateless Streamable HTTP server with five tools covering the component list, per-component prop APIs, the design tokens, install setup, and full-text search over the same corpus the chat reads. No key, no account; every tool reads only published, generated data. The prop API is built from the same JSDoc that produces the published `.d.ts`, so an agent consuming the package reads the exact contract npm ships:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
claude mcp add --transport http robr0-ds https://robertritacca.com/api/mcp
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The same generated data is served as files too: every component's prop contract lives at `https://robertritacca.com/components/<slug>.md` beside its docs page, and a generated agent skill (a `SKILL.md` plus its `references/components.md` catalogue, under `https://robertritacca.com/skill/robr0-design-system/`) can be saved as a pair into a project's own skills folder so a coding agent carries the library's install, theming and catalogue rules into every session. The [setup guide](https://robertritacca.com/docs/get-started) has the two-command install.
|
|
147
|
+
|
|
139
148
|
---
|
|
140
149
|
|
|
141
150
|
## Tech
|
|
@@ -157,7 +166,7 @@ Every push and pull request runs a GitHub Actions pipeline ([`ci.yml`](.github/w
|
|
|
157
166
|
npm run verify # lint + library type-check + package build + story tests + Storybook build + website lint + build + built-HTML checks
|
|
158
167
|
```
|
|
159
168
|
|
|
160
|
-
CI also guards against documentation drift: generated
|
|
169
|
+
CI also guards against documentation drift: every generated surface (the `validate-registry` entry in the root `package.json` is the authoritative list, from this README's component count to the per-component markdown and the consumer agent skill) is rebuilt from its source registries on every build, and CI fails if the committed copies are stale. The numbers on the site are never hand-written.
|
|
161
170
|
|
|
162
171
|
---
|
|
163
172
|
|
package/charts.d.ts
CHANGED
package/charts.js
CHANGED
|
@@ -8,10 +8,12 @@ import { RadialChart } from "./components/Chart/RadialChart.js";
|
|
|
8
8
|
import { ScatterChart } from "./components/Chart/ScatterChart.js";
|
|
9
9
|
import { StackedBarChart } from "./components/Chart/StackedBarChart.js";
|
|
10
10
|
import { Treemap } from "./components/Chart/Treemap.js";
|
|
11
|
+
import { FunnelChart } from "./components/FunnelChart/FunnelChart.js";
|
|
11
12
|
export {
|
|
12
13
|
AreaChart,
|
|
13
14
|
BarChart,
|
|
14
15
|
ComboChart,
|
|
16
|
+
FunnelChart,
|
|
15
17
|
LineChart,
|
|
16
18
|
PieChart,
|
|
17
19
|
RadarChart,
|
|
@@ -61,3 +61,77 @@
|
|
|
61
61
|
font-weight: var(--font-paragraph-em-weight);
|
|
62
62
|
border-left-color: var(--color-text-primary);
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
/* ============================================
|
|
66
|
+
FLOATING VARIANT
|
|
67
|
+
Collapsed to a minimap of short lines; the
|
|
68
|
+
full panel expands over it on hover, focus
|
|
69
|
+
or tap. The caller positions the nav itself
|
|
70
|
+
(typically fixed on the right page edge).
|
|
71
|
+
============================================ */
|
|
72
|
+
.ds-anchor-nav--floating {
|
|
73
|
+
position: relative;
|
|
74
|
+
display: inline-block;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.ds-anchor-nav__lines {
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
align-items: flex-end;
|
|
81
|
+
gap: var(--gap-sm);
|
|
82
|
+
/* A comfortable hover/tap target around the thin lines */
|
|
83
|
+
padding: var(--padding-sm);
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.ds-anchor-nav__line {
|
|
88
|
+
/* One line per section: bespoke minimap geometry, specced in design.md */
|
|
89
|
+
width: 16px;
|
|
90
|
+
height: var(--border-md);
|
|
91
|
+
border-radius: var(--radius-full);
|
|
92
|
+
background-color: var(--color-divider);
|
|
93
|
+
transition:
|
|
94
|
+
width var(--motion-duration-fast) var(--motion-ease-standard),
|
|
95
|
+
background-color var(--motion-duration-fast) var(--motion-ease-standard);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.ds-anchor-nav__line--active {
|
|
99
|
+
width: 24px;
|
|
100
|
+
background-color: var(--color-text-primary);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.ds-anchor-nav__panel {
|
|
104
|
+
position: absolute;
|
|
105
|
+
z-index: 10;
|
|
106
|
+
top: 50%;
|
|
107
|
+
right: 0;
|
|
108
|
+
min-width: 200px;
|
|
109
|
+
max-width: 280px;
|
|
110
|
+
/* A long list scrolls inside the panel rather than past the viewport */
|
|
111
|
+
max-height: 70vh;
|
|
112
|
+
overflow-y: auto;
|
|
113
|
+
padding: var(--padding-md);
|
|
114
|
+
border: var(--border-xs) solid var(--color-bg-container-border);
|
|
115
|
+
border-radius: var(--radius-sm);
|
|
116
|
+
/* Page background, not container: the container fill is semi-transparent
|
|
117
|
+
in dark mode, and a floating panel must not show text through itself */
|
|
118
|
+
background-color: var(--color-bg-page-primary);
|
|
119
|
+
box-shadow: var(--shadow-floating);
|
|
120
|
+
|
|
121
|
+
/* Hidden by default — but never visibility: hidden, because the links must
|
|
122
|
+
stay focusable so tabbing into the nav expands it via :focus-within */
|
|
123
|
+
opacity: 0;
|
|
124
|
+
pointer-events: none;
|
|
125
|
+
transform: translate(var(--gap-xxs), -50%);
|
|
126
|
+
transition:
|
|
127
|
+
opacity var(--motion-duration-fast) var(--motion-ease-standard),
|
|
128
|
+
transform var(--motion-duration-fast) var(--motion-ease-standard);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.ds-anchor-nav--floating:hover .ds-anchor-nav__panel,
|
|
132
|
+
.ds-anchor-nav--floating:focus-within .ds-anchor-nav__panel,
|
|
133
|
+
.ds-anchor-nav--open .ds-anchor-nav__panel {
|
|
134
|
+
opacity: 1;
|
|
135
|
+
pointer-events: auto;
|
|
136
|
+
transform: translate(0, -50%);
|
|
137
|
+
}
|
|
@@ -8,6 +8,8 @@ export interface AnchorNavItem {
|
|
|
8
8
|
type AnchorNavOwnProps = {
|
|
9
9
|
/** The on-page sections to list, in document order */
|
|
10
10
|
items: AnchorNavItem[];
|
|
11
|
+
/** Visual form: `list` renders the inline "On this page" rail; `floating` collapses to a stack of short lines that expands into a panel on hover, keyboard focus, or tap, so it can ride a page edge without taking column width */
|
|
12
|
+
variant?: 'list' | 'floating';
|
|
11
13
|
/** Header text above the list; pass an empty string to render no header */
|
|
12
14
|
title?: string;
|
|
13
15
|
/** Material Symbols icon name beside the header; pass an empty string for none */
|
|
@@ -28,6 +30,11 @@ export interface AnchorNavProps extends AnchorNavOwnProps, Omit<React.ComponentP
|
|
|
28
30
|
* has passed `offset` as current. Pass `activeId` to control the highlight
|
|
29
31
|
* instead (no listener is attached). The active link carries
|
|
30
32
|
* `aria-current="location"`.
|
|
33
|
+
*
|
|
34
|
+
* The `floating` variant collapses the list to a minimap of short lines and
|
|
35
|
+
* expands the full panel on hover or keyboard focus (the links stay focusable
|
|
36
|
+
* while collapsed, so tabbing in opens it). A tap opens it where hover
|
|
37
|
+
* doesn't exist; Escape or a click elsewhere closes it again.
|
|
31
38
|
*/
|
|
32
39
|
export declare const AnchorNav: React.ForwardRefExoticComponent<AnchorNavProps & React.RefAttributes<HTMLElement>>;
|
|
33
40
|
export {};
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsxs,
|
|
2
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
3
3
|
import React, { useRef, useImperativeHandle, useState, useCallback, useEffect } from "react";
|
|
4
4
|
import "./AnchorNav.css";
|
|
5
5
|
const AnchorNav = React.forwardRef(
|
|
6
6
|
({
|
|
7
7
|
items,
|
|
8
|
+
variant = "list",
|
|
8
9
|
title = "On this page",
|
|
9
10
|
icon = "toc",
|
|
10
11
|
activeId,
|
|
11
12
|
onActiveChange,
|
|
12
13
|
offset = 96,
|
|
13
14
|
className,
|
|
15
|
+
onKeyDown,
|
|
14
16
|
...rest
|
|
15
17
|
}, ref) => {
|
|
16
18
|
const navRef = useRef(null);
|
|
17
19
|
useImperativeHandle(ref, () => navRef.current);
|
|
20
|
+
const floating = variant === "floating";
|
|
21
|
+
const [open, setOpen] = useState(false);
|
|
18
22
|
const isControlled = activeId !== void 0;
|
|
19
23
|
const [trackedId, setTrackedId] = useState(void 0);
|
|
20
24
|
const currentId = isControlled ? activeId : trackedId;
|
|
@@ -54,26 +58,75 @@ const AnchorNav = React.forwardRef(
|
|
|
54
58
|
if (frame !== 0) window.cancelAnimationFrame(frame);
|
|
55
59
|
};
|
|
56
60
|
}, [isControlled, items, offset, setActive]);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!open) return;
|
|
63
|
+
const onPointerDown = (event) => {
|
|
64
|
+
if (navRef.current && !navRef.current.contains(event.target)) setOpen(false);
|
|
65
|
+
};
|
|
66
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
67
|
+
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
68
|
+
}, [open]);
|
|
69
|
+
const handleKeyDown = (event) => {
|
|
70
|
+
onKeyDown?.(event);
|
|
71
|
+
if (floating && event.key === "Escape") {
|
|
72
|
+
setOpen(false);
|
|
73
|
+
const focused = document.activeElement;
|
|
74
|
+
if (focused instanceof HTMLElement && navRef.current?.contains(focused)) focused.blur();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const classes = [
|
|
78
|
+
"ds-anchor-nav",
|
|
79
|
+
floating && "ds-anchor-nav--floating",
|
|
80
|
+
floating && open && "ds-anchor-nav--open",
|
|
81
|
+
className
|
|
82
|
+
].filter(Boolean).join(" ");
|
|
83
|
+
const header = title ? /* @__PURE__ */ jsxs("p", { className: "ds-anchor-nav__header", children: [
|
|
84
|
+
icon && /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded ds-anchor-nav__icon", "aria-hidden": "true", children: icon }),
|
|
85
|
+
title
|
|
86
|
+
] }) : null;
|
|
87
|
+
const list = /* @__PURE__ */ jsx("ul", { className: "ds-anchor-nav__list", children: items.map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
88
|
+
"a",
|
|
89
|
+
{
|
|
90
|
+
href: `#${item.id}`,
|
|
91
|
+
className: item.id === currentId ? "ds-anchor-nav__link ds-anchor-nav__link--active" : "ds-anchor-nav__link",
|
|
92
|
+
"aria-current": item.id === currentId ? "location" : void 0,
|
|
93
|
+
onClick: (event) => {
|
|
94
|
+
if (!isControlled) setActive(item.id);
|
|
95
|
+
else onActiveChangeRef.current?.(item.id);
|
|
96
|
+
if (floating) {
|
|
97
|
+
setOpen(false);
|
|
98
|
+
event.currentTarget.blur();
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
children: item.label
|
|
102
|
+
}
|
|
103
|
+
) }, item.id)) });
|
|
104
|
+
return /* @__PURE__ */ jsx(
|
|
105
|
+
"nav",
|
|
106
|
+
{
|
|
107
|
+
ref: navRef,
|
|
108
|
+
"aria-label": title || "On this page",
|
|
109
|
+
className: classes,
|
|
110
|
+
...rest,
|
|
111
|
+
onKeyDown: handleKeyDown,
|
|
112
|
+
children: floating ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
113
|
+
/* @__PURE__ */ jsx("div", { className: "ds-anchor-nav__lines", "aria-hidden": "true", onClick: () => setOpen(true), children: items.map((item) => /* @__PURE__ */ jsx(
|
|
114
|
+
"span",
|
|
115
|
+
{
|
|
116
|
+
className: item.id === currentId ? "ds-anchor-nav__line ds-anchor-nav__line--active" : "ds-anchor-nav__line"
|
|
117
|
+
},
|
|
118
|
+
item.id
|
|
119
|
+
)) }),
|
|
120
|
+
/* @__PURE__ */ jsxs("div", { className: "ds-anchor-nav__panel", children: [
|
|
121
|
+
header,
|
|
122
|
+
list
|
|
123
|
+
] })
|
|
124
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
125
|
+
header,
|
|
126
|
+
list
|
|
127
|
+
] })
|
|
128
|
+
}
|
|
129
|
+
);
|
|
77
130
|
}
|
|
78
131
|
);
|
|
79
132
|
AnchorNav.displayName = "AnchorNav";
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
BANNER COMPONENT
|
|
3
|
+
Full-width page-level status strip
|
|
4
|
+
============================================ */
|
|
5
|
+
|
|
6
|
+
.ds-banner {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: space-between;
|
|
10
|
+
gap: var(--gap-lg);
|
|
11
|
+
width: 100%;
|
|
12
|
+
padding: var(--padding-sm) var(--padding-lg);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.ds-banner__body {
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: var(--gap-sm);
|
|
19
|
+
min-width: 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.ds-banner--center .ds-banner__body {
|
|
23
|
+
flex: 1;
|
|
24
|
+
justify-content: center;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.ds-banner__icon {
|
|
28
|
+
--icon-size: var(--icon-size-sm);
|
|
29
|
+
flex-shrink: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.ds-banner__content {
|
|
33
|
+
margin: 0;
|
|
34
|
+
font-family: var(--font-paragraph-sm-family);
|
|
35
|
+
font-size: var(--font-paragraph-sm-size);
|
|
36
|
+
font-weight: var(--font-paragraph-sm-weight);
|
|
37
|
+
line-height: var(--font-paragraph-sm-line-height);
|
|
38
|
+
letter-spacing: var(--font-paragraph-sm-letter-spacing);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.ds-banner__title {
|
|
42
|
+
font-family: var(--font-paragraph-sm-em-family);
|
|
43
|
+
font-weight: var(--font-paragraph-sm-em-weight);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.ds-banner__action {
|
|
47
|
+
display: inline-flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
flex-shrink: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ============================================
|
|
53
|
+
DISMISS
|
|
54
|
+
============================================ */
|
|
55
|
+
|
|
56
|
+
.ds-banner__dismiss {
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
padding: var(--padding-xxs);
|
|
61
|
+
border: none;
|
|
62
|
+
background: none;
|
|
63
|
+
color: inherit;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
border-radius: var(--radius-full);
|
|
66
|
+
flex-shrink: 0;
|
|
67
|
+
transition: opacity var(--motion-duration-fast) var(--motion-ease-standard);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.ds-banner__dismiss .material-symbols-rounded {
|
|
71
|
+
--icon-size: var(--icon-size-sm);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ds-banner__dismiss:hover {
|
|
75
|
+
opacity: 0.7;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ds-banner__dismiss:focus-visible {
|
|
79
|
+
outline: 2px solid var(--color-action-primary-bg);
|
|
80
|
+
outline-offset: 2px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ============================================
|
|
84
|
+
VARIANTS
|
|
85
|
+
============================================ */
|
|
86
|
+
|
|
87
|
+
.ds-banner--info {
|
|
88
|
+
background-color: var(--color-status-info-bg);
|
|
89
|
+
color: var(--color-status-info-text);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.ds-banner--positive {
|
|
93
|
+
background-color: var(--color-status-positive-bg);
|
|
94
|
+
color: var(--color-status-positive-text);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.ds-banner--warning {
|
|
98
|
+
background-color: var(--color-status-warning-bg);
|
|
99
|
+
color: var(--color-status-warning-text);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.ds-banner--error {
|
|
103
|
+
background-color: var(--color-status-error-bg);
|
|
104
|
+
color: var(--color-status-error-text);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.ds-banner--neutral {
|
|
108
|
+
background-color: var(--color-status-neutral-bg);
|
|
109
|
+
color: var(--color-status-neutral-text);
|
|
110
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** Props owned by Banner itself — everything else falls through to the root node. */
|
|
3
|
+
type BannerOwnProps = {
|
|
4
|
+
/** Status variant determines colour and default icon */
|
|
5
|
+
variant?: 'info' | 'positive' | 'warning' | 'error' | 'neutral';
|
|
6
|
+
/** Short leading emphasis before the body text. Deliberately shadows the native `title` attribute — a banner never needs a hover tooltip. */
|
|
7
|
+
title?: string;
|
|
8
|
+
/** Banner body content — a single line of text, rendered inside the banner's paragraph */
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
/** Custom icon override — Material Symbol name */
|
|
11
|
+
icon?: string;
|
|
12
|
+
/** Action slot on the trailing edge, for a compact Button or link */
|
|
13
|
+
action?: React.ReactNode;
|
|
14
|
+
/** Whether the banner shows a dismiss button */
|
|
15
|
+
dismissible?: boolean;
|
|
16
|
+
/** Callback when the dismiss button is clicked */
|
|
17
|
+
onDismiss?: () => void;
|
|
18
|
+
/** Horizontal alignment of the banner content */
|
|
19
|
+
align?: 'start' | 'center';
|
|
20
|
+
/** Additional CSS classes */
|
|
21
|
+
className?: string;
|
|
22
|
+
};
|
|
23
|
+
export interface BannerProps extends BannerOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof BannerOwnProps> {
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Full-width status strip for page-level announcements: a release notice, a
|
|
27
|
+
* degraded-service warning, an environment flag. The third sibling in the
|
|
28
|
+
* feedback family — Alert sits inline in the layout, Toast interrupts and
|
|
29
|
+
* leaves, Banner spans the page and stays until dismissed.
|
|
30
|
+
*
|
|
31
|
+
* Stateless like Alert: `dismissible` shows the close control and `onDismiss`
|
|
32
|
+
* reports the click; removing the banner is the consumer's render decision.
|
|
33
|
+
* Forwards a ref to the root element and spreads unrecognised props onto it.
|
|
34
|
+
*/
|
|
35
|
+
export declare const Banner: React.ForwardRefExoticComponent<BannerProps & React.RefAttributes<HTMLDivElement>>;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import "./Banner.css";
|
|
4
|
+
import "../../fonts/material-symbols.css";
|
|
5
|
+
const defaultIcons = {
|
|
6
|
+
info: "info",
|
|
7
|
+
positive: "check_circle",
|
|
8
|
+
warning: "warning",
|
|
9
|
+
error: "error",
|
|
10
|
+
neutral: "campaign"
|
|
11
|
+
};
|
|
12
|
+
const Banner = React.forwardRef(
|
|
13
|
+
({
|
|
14
|
+
variant = "info",
|
|
15
|
+
title,
|
|
16
|
+
children,
|
|
17
|
+
icon,
|
|
18
|
+
action,
|
|
19
|
+
dismissible = false,
|
|
20
|
+
onDismiss,
|
|
21
|
+
align = "start",
|
|
22
|
+
className = "",
|
|
23
|
+
...rest
|
|
24
|
+
}, ref) => {
|
|
25
|
+
const baseClass = "ds-banner";
|
|
26
|
+
const classes = [
|
|
27
|
+
baseClass,
|
|
28
|
+
`${baseClass}--${variant}`,
|
|
29
|
+
align === "center" ? `${baseClass}--center` : "",
|
|
30
|
+
className
|
|
31
|
+
].filter(Boolean).join(" ");
|
|
32
|
+
const iconName = icon || defaultIcons[variant];
|
|
33
|
+
return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, role: "status", children: [
|
|
34
|
+
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__body`, children: [
|
|
35
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__icon material-symbols-rounded`, "aria-hidden": "true", children: iconName }),
|
|
36
|
+
/* @__PURE__ */ jsxs("p", { className: `${baseClass}__content`, children: [
|
|
37
|
+
title && /* @__PURE__ */ jsx("strong", { className: `${baseClass}__title`, children: title }),
|
|
38
|
+
title && children ? " " : null,
|
|
39
|
+
children
|
|
40
|
+
] }),
|
|
41
|
+
action && /* @__PURE__ */ jsx("span", { className: `${baseClass}__action`, children: action })
|
|
42
|
+
] }),
|
|
43
|
+
dismissible && /* @__PURE__ */ jsx(
|
|
44
|
+
"button",
|
|
45
|
+
{
|
|
46
|
+
type: "button",
|
|
47
|
+
className: `${baseClass}__dismiss`,
|
|
48
|
+
onClick: onDismiss,
|
|
49
|
+
"aria-label": "Dismiss banner",
|
|
50
|
+
children: /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", "aria-hidden": "true", children: "close" })
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
] });
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
Banner.displayName = "Banner";
|
|
57
|
+
export {
|
|
58
|
+
Banner
|
|
59
|
+
};
|
|
@@ -182,6 +182,35 @@
|
|
|
182
182
|
cursor: not-allowed;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/* ============================================
|
|
186
|
+
NEUTRAL BUTTON (SOLID GREY)
|
|
187
|
+
Filled like primary but in the neutral greys —
|
|
188
|
+
unlike tertiary, the background is always shown.
|
|
189
|
+
============================================ */
|
|
190
|
+
|
|
191
|
+
.ds-button--neutral {
|
|
192
|
+
background-color: var(--color-action-neutral-bg);
|
|
193
|
+
border: none;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.ds-button--neutral .ds-button__text {
|
|
197
|
+
color: var(--color-action-neutral-text);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.ds-button--neutral.ds-button--hover {
|
|
201
|
+
background-color: var(--color-action-neutral-bg-hover);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.ds-button--neutral.ds-button--active {
|
|
205
|
+
background-color: var(--color-action-neutral-bg-active);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.ds-button--neutral.ds-button--disabled {
|
|
209
|
+
background-color: var(--color-action-neutral-bg);
|
|
210
|
+
opacity: 0.4;
|
|
211
|
+
cursor: not-allowed;
|
|
212
|
+
}
|
|
213
|
+
|
|
185
214
|
/* ============================================
|
|
186
215
|
DESTRUCTIVE BUTTON
|
|
187
216
|
For dangerous actions: delete, remove, etc.
|
|
@@ -266,6 +295,10 @@
|
|
|
266
295
|
color: var(--color-icon-primary);
|
|
267
296
|
}
|
|
268
297
|
|
|
298
|
+
.ds-button--neutral .ds-button__icon {
|
|
299
|
+
color: var(--color-action-neutral-text);
|
|
300
|
+
}
|
|
301
|
+
|
|
269
302
|
.ds-button--destructive .ds-button__icon {
|
|
270
303
|
color: var(--color-core-accent-coral);
|
|
271
304
|
}
|
|
@@ -315,6 +348,10 @@
|
|
|
315
348
|
background-color: var(--color-action-passive-bg-hover);
|
|
316
349
|
}
|
|
317
350
|
|
|
351
|
+
.ds-button--neutral:not(.ds-button--disabled):hover {
|
|
352
|
+
background-color: var(--color-action-neutral-bg-hover);
|
|
353
|
+
}
|
|
354
|
+
|
|
318
355
|
.ds-button--destructive:not(.ds-button--disabled):hover {
|
|
319
356
|
background-color: var(--color-core-accent-coral);
|
|
320
357
|
}
|
|
@@ -359,6 +396,10 @@
|
|
|
359
396
|
background-color: var(--color-action-passive-bg-active);
|
|
360
397
|
}
|
|
361
398
|
|
|
399
|
+
.ds-button--neutral:not(.ds-button--disabled):active {
|
|
400
|
+
background-color: var(--color-action-neutral-bg-active);
|
|
401
|
+
}
|
|
402
|
+
|
|
362
403
|
.ds-button--destructive:not(.ds-button--disabled):active {
|
|
363
404
|
background-color: var(--color-core-accent-coral);
|
|
364
405
|
}
|
|
@@ -382,6 +423,16 @@
|
|
|
382
423
|
pointer-events: none;
|
|
383
424
|
}
|
|
384
425
|
|
|
426
|
+
/* The spinner is a box, not a glyph: left inline in the icon slot, it sits
|
|
427
|
+
on the text baseline and the strut's descender pushes it above centre.
|
|
428
|
+
Flex centres it geometrically — the same fix as CircularButton's loading
|
|
429
|
+
icon. */
|
|
430
|
+
.ds-button--loading .ds-button__icon {
|
|
431
|
+
display: inline-flex;
|
|
432
|
+
align-items: center;
|
|
433
|
+
justify-content: center;
|
|
434
|
+
}
|
|
435
|
+
|
|
385
436
|
/* The generic icon-slot svg sizing must not distort the spinner —
|
|
386
437
|
it sizes itself from the Spinner size prop */
|
|
387
438
|
.ds-button .ds-button__icon .ds-spinner__circle {
|
|
@@ -8,7 +8,7 @@ type ButtonOwnProps = {
|
|
|
8
8
|
/** Icon for right side — Material Symbol name (string) or custom element (ReactNode) */
|
|
9
9
|
iconRight?: string | React.ReactNode;
|
|
10
10
|
/** Visual treatment */
|
|
11
|
-
variant?: 'primary' | 'secondary' | 'tertiary' | 'destructive';
|
|
11
|
+
variant?: 'primary' | 'secondary' | 'tertiary' | 'neutral' | 'destructive';
|
|
12
12
|
/** Button size */
|
|
13
13
|
size?: 'default' | 'compact';
|
|
14
14
|
/** Whether the button is disabled */
|
|
@@ -63,7 +63,7 @@ export interface ButtonProps extends ButtonOwnProps, Omit<React.ComponentPropsWi
|
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
65
|
* Button component from Figma design system.
|
|
66
|
-
* Supports primary, secondary, tertiary and destructive variants.
|
|
66
|
+
* Supports primary, secondary, tertiary, neutral and destructive variants.
|
|
67
67
|
* Icons can be placed on the left and/or right side of the text.
|
|
68
68
|
*
|
|
69
69
|
* Renders a `<button>`, or an `<a>` when `href` is supplied. Forwards a ref to
|
|
@@ -149,7 +149,10 @@
|
|
|
149
149
|
|
|
150
150
|
.ds-chart__tooltip {
|
|
151
151
|
padding: var(--padding-sm) var(--padding-md);
|
|
152
|
-
background-color: var(--color-bg-
|
|
152
|
+
background-color: var(--color-bg-glass);
|
|
153
|
+
/* The glass token is deliberately translucent; this blur is what keeps
|
|
154
|
+
the chart's marks readable through the overlay. */
|
|
155
|
+
backdrop-filter: blur(24px);
|
|
153
156
|
border: var(--border-xs) solid var(--color-bg-container-border);
|
|
154
157
|
border-radius: var(--radius-sm); /* 8px */
|
|
155
158
|
box-shadow: var(--shadow-floating);
|