@robr0/design-system 0.13.0 → 0.14.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 +9 -3
- package/components/Banner/Banner.css +110 -0
- package/components/Banner/Banner.d.ts +36 -0
- package/components/Banner/Banner.js +59 -0
- package/components/ContributionGraph/ContributionGraph.css +7 -1
- package/components/ContributionGraph/ContributionGraph.d.ts +9 -1
- package/components/ContributionGraph/ContributionGraph.js +59 -43
- package/components/EmptyState/EmptyState.css +5 -0
- package/components/FunnelChart/FunnelChart.css +3 -2
- package/components/FunnelChart/FunnelChart.d.ts +12 -4
- package/components/FunnelChart/FunnelChart.js +48 -28
- package/components/Gauge/Gauge.css +30 -13
- 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 +89 -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/Rating/Rating.css +74 -0
- package/components/Rating/Rating.d.ts +41 -0
- package/components/Rating/Rating.js +124 -0
- package/components/Sparkline/Sparkline.d.ts +5 -3
- package/components/Sparkline/Sparkline.js +30 -1
- package/components/SplitButton/SplitButton.css +93 -0
- package/components/SplitButton/SplitButton.d.ts +43 -0
- package/components/SplitButton/SplitButton.js +66 -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/registry.json +48 -0
- package/components/registry.json.d.ts +48 -0
- package/components/registry.json.js +1 -1
- package/index.d.ts +6 -0
- package/index.js +15 -0
- package/package.json +5 -1
- package/tokens/motion.d.ts +9 -4
- package/tokens/motion.js +5 -1
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 -->120<!-- /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 · 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
|
|
32
32
|
<!-- component-list:end -->
|
|
33
33
|
|
|
34
34
|
### Ambient background
|
|
@@ -136,6 +136,12 @@ The answering half is a Claude-backed route (`website/src/app/api/chat/route.ts`
|
|
|
136
136
|
|
|
137
137
|
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
138
|
|
|
139
|
+
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:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
claude mcp add --transport http robr0-ds https://robertritacca.com/api/mcp
|
|
143
|
+
```
|
|
144
|
+
|
|
139
145
|
---
|
|
140
146
|
|
|
141
147
|
## Tech
|
|
@@ -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
|
+
};
|
|
@@ -4,14 +4,20 @@
|
|
|
4
4
|
day, coloured by the contribution colour ramp
|
|
5
5
|
============================================ */
|
|
6
6
|
|
|
7
|
+
/* Card chrome, header, and padding come from the shared chart styles
|
|
8
|
+
(Chart.css); this file owns the grid, legend, and cell ramp. */
|
|
9
|
+
|
|
7
10
|
.ds-contribution-graph {
|
|
8
11
|
--ds-contribution-cell: 12px;
|
|
9
12
|
--ds-contribution-gap: 3px;
|
|
10
13
|
|
|
14
|
+
min-width: 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.ds-contribution-graph__content {
|
|
11
18
|
display: flex;
|
|
12
19
|
flex-direction: column;
|
|
13
20
|
gap: var(--gap-sm);
|
|
14
|
-
width: 100%;
|
|
15
21
|
min-width: 0;
|
|
16
22
|
}
|
|
17
23
|
|
|
@@ -9,6 +9,12 @@ export interface ContributionDay {
|
|
|
9
9
|
export interface ContributionGraphProps {
|
|
10
10
|
/** One entry per day, ordered oldest to newest */
|
|
11
11
|
days: ContributionDay[];
|
|
12
|
+
/** Chart title, in the shared chart header. */
|
|
13
|
+
title?: string;
|
|
14
|
+
/** Description text below the title. */
|
|
15
|
+
subtitle?: string;
|
|
16
|
+
/** Strip the card chrome (border, padding, fill) when the chart sits inside another panel that supplies the surface */
|
|
17
|
+
bare?: boolean;
|
|
12
18
|
/** Summary line shown under the grid, e.g. "496 contributions in the last year" */
|
|
13
19
|
caption?: string;
|
|
14
20
|
/** Show month labels above the grid */
|
|
@@ -22,5 +28,7 @@ export interface ContributionGraphProps {
|
|
|
22
28
|
* ContributionGraph renders a GitHub-style contribution heatmap:
|
|
23
29
|
* one cell per day, arranged in weekday rows and week columns,
|
|
24
30
|
* coloured by activity level via the --color-chart-contribution-* ramp.
|
|
31
|
+
* It wears the chart family's card chrome (title, subtitle, padding) and
|
|
32
|
+
* takes `bare` to drop it inside a panel that supplies the surface.
|
|
25
33
|
*/
|
|
26
|
-
export declare const ContributionGraph: ({ days, caption, showMonthLabels, showLegend, className, }: ContributionGraphProps) => import("react").JSX.Element;
|
|
34
|
+
export declare const ContributionGraph: ({ days, title, subtitle, bare, caption, showMonthLabels, showLegend, className, }: ContributionGraphProps) => import("react").JSX.Element;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import "../Chart/Chart.css";
|
|
2
3
|
import "./ContributionGraph.css";
|
|
3
4
|
const MONTH_LABELS = [
|
|
4
5
|
"Jan",
|
|
@@ -62,6 +63,9 @@ const buildMonthLabels = (weeks) => {
|
|
|
62
63
|
};
|
|
63
64
|
const ContributionGraph = ({
|
|
64
65
|
days,
|
|
66
|
+
title,
|
|
67
|
+
subtitle,
|
|
68
|
+
bare = false,
|
|
65
69
|
caption,
|
|
66
70
|
showMonthLabels = true,
|
|
67
71
|
showLegend = true,
|
|
@@ -69,53 +73,65 @@ const ContributionGraph = ({
|
|
|
69
73
|
}) => {
|
|
70
74
|
const weeks = buildWeeks(days);
|
|
71
75
|
const monthLabels = buildMonthLabels(weeks);
|
|
72
|
-
const
|
|
76
|
+
const chartClass = "ds-chart";
|
|
77
|
+
const classes = [
|
|
78
|
+
chartClass,
|
|
79
|
+
bare && `${chartClass}--bare`,
|
|
80
|
+
"ds-contribution-graph",
|
|
81
|
+
className
|
|
82
|
+
].filter(Boolean).join(" ");
|
|
73
83
|
const columnTemplate = `repeat(${weeks.length}, minmax(var(--ds-contribution-cell), 1fr))`;
|
|
74
84
|
return /* @__PURE__ */ jsxs("div", { className: classes, children: [
|
|
75
|
-
/* @__PURE__ */
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
`${label}-${weekIndex}`
|
|
90
|
-
))
|
|
91
|
-
}
|
|
92
|
-
),
|
|
93
|
-
/* @__PURE__ */ jsx(
|
|
94
|
-
"div",
|
|
95
|
-
{
|
|
96
|
-
className: "ds-contribution-graph__grid",
|
|
97
|
-
style: { gridTemplateColumns: columnTemplate },
|
|
98
|
-
role: "img",
|
|
99
|
-
"aria-label": caption ?? "Contribution activity",
|
|
100
|
-
children: weeks.map((week, weekIndex) => /* @__PURE__ */ jsx("div", { className: "ds-contribution-graph__week", children: week.map(
|
|
101
|
-
(day, dayIndex) => day ? /* @__PURE__ */ jsx(
|
|
102
|
-
"div",
|
|
85
|
+
(title || subtitle) && /* @__PURE__ */ jsx("div", { className: `${chartClass}__header`, children: /* @__PURE__ */ jsxs("div", { className: `${chartClass}__header-text`, children: [
|
|
86
|
+
title && /* @__PURE__ */ jsx("h3", { className: `${chartClass}__title`, children: title }),
|
|
87
|
+
subtitle && /* @__PURE__ */ jsx("p", { className: `${chartClass}__subtitle`, children: subtitle })
|
|
88
|
+
] }) }),
|
|
89
|
+
/* @__PURE__ */ jsxs("div", { className: `${chartClass}__body ds-contribution-graph__content`, children: [
|
|
90
|
+
/* @__PURE__ */ jsxs("div", { className: "ds-contribution-graph__scroll", children: [
|
|
91
|
+
showMonthLabels && /* @__PURE__ */ jsx(
|
|
92
|
+
"div",
|
|
93
|
+
{
|
|
94
|
+
className: "ds-contribution-graph__months",
|
|
95
|
+
style: { gridTemplateColumns: columnTemplate },
|
|
96
|
+
"aria-hidden": "true",
|
|
97
|
+
children: monthLabels.map(({ weekIndex, label }) => /* @__PURE__ */ jsx(
|
|
98
|
+
"span",
|
|
103
99
|
{
|
|
104
|
-
className:
|
|
105
|
-
|
|
100
|
+
className: "ds-contribution-graph__month-label",
|
|
101
|
+
style: { gridColumnStart: weekIndex + 1 },
|
|
102
|
+
children: label
|
|
106
103
|
},
|
|
107
|
-
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
104
|
+
`${label}-${weekIndex}`
|
|
105
|
+
))
|
|
106
|
+
}
|
|
107
|
+
),
|
|
108
|
+
/* @__PURE__ */ jsx(
|
|
109
|
+
"div",
|
|
110
|
+
{
|
|
111
|
+
className: "ds-contribution-graph__grid",
|
|
112
|
+
style: { gridTemplateColumns: columnTemplate },
|
|
113
|
+
role: "img",
|
|
114
|
+
"aria-label": caption ?? "Contribution activity",
|
|
115
|
+
children: weeks.map((week, weekIndex) => /* @__PURE__ */ jsx("div", { className: "ds-contribution-graph__week", children: week.map(
|
|
116
|
+
(day, dayIndex) => day ? /* @__PURE__ */ jsx(
|
|
117
|
+
"div",
|
|
118
|
+
{
|
|
119
|
+
className: `ds-contribution-graph__cell ds-contribution-graph__cell--level-${day.level}`,
|
|
120
|
+
title: formatTooltip(day)
|
|
121
|
+
},
|
|
122
|
+
day.date
|
|
123
|
+
) : /* @__PURE__ */ jsx("div", { className: "ds-contribution-graph__cell ds-contribution-graph__cell--empty" }, `pad-${dayIndex}`)
|
|
124
|
+
) }, weekIndex))
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
] }),
|
|
128
|
+
(caption || showLegend) && /* @__PURE__ */ jsxs("div", { className: "ds-contribution-graph__footer", children: [
|
|
129
|
+
caption && /* @__PURE__ */ jsx("span", { className: "ds-contribution-graph__caption", children: caption }),
|
|
130
|
+
showLegend && /* @__PURE__ */ jsxs("div", { className: "ds-contribution-graph__legend", "aria-hidden": "true", children: [
|
|
131
|
+
/* @__PURE__ */ jsx("span", { className: "ds-contribution-graph__legend-label", children: "Less" }),
|
|
132
|
+
[0, 1, 2, 3, 4].map((level) => /* @__PURE__ */ jsx("div", { className: `ds-contribution-graph__cell ds-contribution-graph__cell--level-${level}` }, level)),
|
|
133
|
+
/* @__PURE__ */ jsx("span", { className: "ds-contribution-graph__legend-label", children: "More" })
|
|
134
|
+
] })
|
|
119
135
|
] })
|
|
120
136
|
] })
|
|
121
137
|
] });
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
gap: var(--gap-md); /* 16px */
|
|
14
14
|
padding: var(--padding-xl); /* 32px */
|
|
15
15
|
width: 100%;
|
|
16
|
+
/* width: 100% plus own padding — border-box locally (as ContactCard,
|
|
17
|
+
Checkbox, Composer, SelectionCard and Textarea do), so a consumer
|
|
18
|
+
without a global reset doesn't get a horizontal scrollbar wherever
|
|
19
|
+
this is embedded. */
|
|
20
|
+
box-sizing: border-box;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
/* ============================================
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
token-only.
|
|
10
10
|
============================================ */
|
|
11
11
|
|
|
12
|
-
/* Base
|
|
12
|
+
/* Base — card chrome, header, and padding come from the shared chart
|
|
13
|
+
styles (Chart.css); this file owns the stage row alone. */
|
|
13
14
|
|
|
14
|
-
.ds-funnel-
|
|
15
|
+
.ds-funnel-chart__stages {
|
|
15
16
|
display: flex;
|
|
16
17
|
align-items: center;
|
|
17
18
|
gap: var(--gap-xxs);
|
|
@@ -15,7 +15,13 @@ export interface FunnelStage {
|
|
|
15
15
|
type FunnelChartOwnProps = {
|
|
16
16
|
/** Ordered stages, first stage widest. Each later stage's height is its share of the first. */
|
|
17
17
|
data: FunnelStage[];
|
|
18
|
-
/** Chart
|
|
18
|
+
/** Chart title, in the shared chart header. */
|
|
19
|
+
title?: string;
|
|
20
|
+
/** Description text below the title. */
|
|
21
|
+
subtitle?: string;
|
|
22
|
+
/** Strip the card chrome (border, padding, fill) when the chart sits inside another panel that supplies the surface */
|
|
23
|
+
bare?: boolean;
|
|
24
|
+
/** Chart area height in pixels. */
|
|
19
25
|
height?: number;
|
|
20
26
|
/**
|
|
21
27
|
* Floor percentage for a stage's height, so steep drop-offs stay readable.
|
|
@@ -32,9 +38,11 @@ export interface FunnelChartProps extends FunnelChartOwnProps, Omit<React.Compon
|
|
|
32
38
|
* step down with each stage's share of the first, each carrying a centred
|
|
33
39
|
* percentage pill. Pure JSX and CSS computed from props: no recharts, no
|
|
34
40
|
* hooks, no browser APIs, so it deliberately omits 'use client' and renders
|
|
35
|
-
* from a Server Component.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
41
|
+
* from a Server Component. It wears the chart family's card chrome (title,
|
|
42
|
+
* subtitle, padding) and takes `bare` to drop it inside a Panel, like every
|
|
43
|
+
* other chart. Degenerate data stays safe: an empty array renders an empty
|
|
44
|
+
* stage row, and a zero or negative first value draws every stage at the
|
|
45
|
+
* floor height.
|
|
38
46
|
*/
|
|
39
47
|
export declare const FunnelChart: React.ForwardRefExoticComponent<FunnelChartProps & React.RefAttributes<HTMLDivElement>>;
|
|
40
48
|
export {};
|
|
@@ -1,11 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
|
+
import "../Chart/Chart.css";
|
|
3
4
|
import "./FunnelChart.css";
|
|
4
5
|
const SERIES_COUNT = 7;
|
|
5
6
|
const FunnelChart = React.forwardRef(
|
|
6
|
-
({
|
|
7
|
+
({
|
|
8
|
+
data,
|
|
9
|
+
title,
|
|
10
|
+
subtitle,
|
|
11
|
+
bare = false,
|
|
12
|
+
height = 190,
|
|
13
|
+
minStageShare = 16,
|
|
14
|
+
className = "",
|
|
15
|
+
...rest
|
|
16
|
+
}, ref) => {
|
|
7
17
|
const baseClass = "ds-funnel-chart";
|
|
8
|
-
const
|
|
18
|
+
const chartClass = "ds-chart";
|
|
19
|
+
const classes = [
|
|
20
|
+
chartClass,
|
|
21
|
+
bare && `${chartClass}--bare`,
|
|
22
|
+
baseClass,
|
|
23
|
+
className
|
|
24
|
+
].filter(Boolean).join(" ");
|
|
9
25
|
const firstValue = data.length > 0 ? data[0].value : 0;
|
|
10
26
|
const clampShare = (share) => Math.min(Math.max(share, minStageShare), 100);
|
|
11
27
|
const stages = data.map((stage, i) => ({
|
|
@@ -14,32 +30,36 @@ const FunnelChart = React.forwardRef(
|
|
|
14
30
|
color: `var(--color-chart-series-${i % SERIES_COUNT + 1})`
|
|
15
31
|
}));
|
|
16
32
|
const ariaLabel = stages.length > 0 ? `Funnel: ${stages.map((s) => `${s.label} ${s.displayValue ?? s.value.toLocaleString()}`).join(", ")}` : "Funnel, no data";
|
|
17
|
-
return /* @__PURE__ */
|
|
18
|
-
"div",
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
|
|
34
|
+
(title || subtitle) && /* @__PURE__ */ jsx("div", { className: `${chartClass}__header`, children: /* @__PURE__ */ jsxs("div", { className: `${chartClass}__header-text`, children: [
|
|
35
|
+
title && /* @__PURE__ */ jsx("h3", { className: `${chartClass}__title`, children: title }),
|
|
36
|
+
subtitle && /* @__PURE__ */ jsx("p", { className: `${chartClass}__subtitle`, children: subtitle })
|
|
37
|
+
] }) }),
|
|
38
|
+
/* @__PURE__ */ jsx(
|
|
39
|
+
"div",
|
|
40
|
+
{
|
|
41
|
+
className: `${chartClass}__body ${baseClass}__stages`,
|
|
42
|
+
style: { height },
|
|
43
|
+
role: "img",
|
|
44
|
+
"aria-label": ariaLabel,
|
|
45
|
+
children: stages.map((stage, i) => /* @__PURE__ */ jsx(
|
|
46
|
+
"div",
|
|
47
|
+
{
|
|
48
|
+
className: `${baseClass}__stage`,
|
|
49
|
+
style: {
|
|
50
|
+
"--funnel-stage-size": stage.share,
|
|
51
|
+
"--funnel-stage-color": stage.color
|
|
52
|
+
},
|
|
53
|
+
children: /* @__PURE__ */ jsxs("span", { className: `${baseClass}__pct`, children: [
|
|
54
|
+
Math.round(firstValue > 0 ? stage.value / firstValue * 100 : 0),
|
|
55
|
+
"%"
|
|
56
|
+
] })
|
|
33
57
|
},
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
`${stage.label}-${i}`
|
|
40
|
-
))
|
|
41
|
-
}
|
|
42
|
-
);
|
|
58
|
+
`${stage.label}-${i}`
|
|
59
|
+
))
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
] });
|
|
43
63
|
}
|
|
44
64
|
);
|
|
45
65
|
FunnelChart.displayName = "FunnelChart";
|
|
@@ -1,20 +1,34 @@
|
|
|
1
1
|
/* ============================================
|
|
2
2
|
GAUGE COMPONENT
|
|
3
3
|
Radial dial for a single bounded reading.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Card chrome, header, and padding come from
|
|
5
|
+
the shared chart styles (Chart.css) — this
|
|
6
|
+
file owns the dial alone. Colour flows
|
|
7
|
+
through `color`: the value arc draws with
|
|
8
|
+
currentColor, so a tone is one custom
|
|
9
|
+
property swap.
|
|
7
10
|
============================================ */
|
|
8
11
|
|
|
9
|
-
/* Base */
|
|
12
|
+
/* Base — the dial centres in the shared chart body */
|
|
10
13
|
|
|
11
|
-
.ds-
|
|
14
|
+
.ds-gauge__body {
|
|
15
|
+
display: flex;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Unlike the fluid recharts bodies, the dial has intrinsic size — a bare
|
|
20
|
+
gauge shrink-wraps it so it sits inline in a Stat row or tile. */
|
|
21
|
+
.ds-gauge.ds-chart--bare {
|
|
22
|
+
width: auto;
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.ds-gauge__dial {
|
|
12
27
|
position: relative;
|
|
13
28
|
display: inline-flex;
|
|
14
29
|
align-items: center;
|
|
15
30
|
justify-content: center;
|
|
16
31
|
flex-shrink: 0;
|
|
17
|
-
color: var(--color-action-primary-bg);
|
|
18
32
|
}
|
|
19
33
|
|
|
20
34
|
.ds-gauge__svg {
|
|
@@ -25,7 +39,7 @@
|
|
|
25
39
|
|
|
26
40
|
.ds-gauge__track {
|
|
27
41
|
fill: none;
|
|
28
|
-
stroke: var(--color-bg-container-
|
|
42
|
+
stroke: var(--color-bg-container-secondary);
|
|
29
43
|
stroke-linecap: round;
|
|
30
44
|
}
|
|
31
45
|
|
|
@@ -48,12 +62,15 @@
|
|
|
48
62
|
pointer-events: none;
|
|
49
63
|
}
|
|
50
64
|
|
|
65
|
+
/* The centre pair mirrors the chart family's radial centre label:
|
|
66
|
+
a light sub-display numeral over a caption. */
|
|
67
|
+
|
|
51
68
|
.ds-gauge__value {
|
|
52
|
-
font-family: var(--font-
|
|
53
|
-
font-size: var(--font-
|
|
54
|
-
font-weight: var(--font-
|
|
55
|
-
line-height: var(--font-
|
|
56
|
-
letter-spacing: var(--font-
|
|
69
|
+
font-family: var(--font-sub-display-family);
|
|
70
|
+
font-size: var(--font-sub-display-size);
|
|
71
|
+
font-weight: var(--font-sub-display-weight);
|
|
72
|
+
line-height: var(--font-sub-display-line-height);
|
|
73
|
+
letter-spacing: var(--font-sub-display-letter-spacing);
|
|
57
74
|
color: var(--color-text-primary);
|
|
58
75
|
}
|
|
59
76
|
|
|
@@ -69,7 +86,7 @@
|
|
|
69
86
|
/* Tones */
|
|
70
87
|
|
|
71
88
|
.ds-gauge--accent {
|
|
72
|
-
color: var(--color-
|
|
89
|
+
color: var(--color-chart-series-1);
|
|
73
90
|
}
|
|
74
91
|
|
|
75
92
|
/* The status hues come from the border tokens, not the text tokens: the
|