@robr0/design-system 0.1.0 → 0.3.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 +13 -9
- package/components/Accordion/Accordion.js +0 -1
- package/components/Avatar/Avatar.js +1 -0
- package/components/Breadcrumb/Breadcrumb.css +9 -1
- package/components/Breadcrumb/Breadcrumb.d.ts +7 -1
- package/components/Breadcrumb/Breadcrumb.js +3 -2
- package/components/Button/Button.css +18 -0
- package/components/Button/Button.d.ts +38 -16
- package/components/Button/Button.js +76 -58
- package/components/Card/Card.css +6 -0
- package/components/Card/Card.d.ts +17 -5
- package/components/Card/Card.js +101 -80
- package/components/Checkbox/Checkbox.d.ts +24 -12
- package/components/Checkbox/Checkbox.js +99 -85
- package/components/CircularButton/CircularButton.css +11 -0
- package/components/CircularButton/CircularButton.d.ts +7 -1
- package/components/CircularButton/CircularButton.js +15 -4
- package/components/CodeBlock/CodeBlock.js +10 -1
- package/components/ColorPicker/ColorPicker.css +278 -0
- package/components/ColorPicker/ColorPicker.d.ts +50 -0
- package/components/ColorPicker/ColorPicker.js +338 -0
- package/components/Combobox/Combobox.css +0 -36
- package/components/Combobox/Combobox.d.ts +19 -7
- package/components/Combobox/Combobox.js +106 -89
- package/components/CommandPalette/CommandPalette.css +1 -15
- package/components/CommandPalette/CommandPalette.js +6 -5
- package/components/ContextMenu/ContextMenu.css +262 -0
- package/components/ContextMenu/ContextMenu.d.ts +26 -0
- package/components/ContextMenu/ContextMenu.js +350 -0
- package/components/DateInput/DateInput.css +0 -36
- package/components/DateInput/DateInput.d.ts +18 -21
- package/components/DateInput/DateInput.js +76 -64
- package/components/DatePicker/DatePicker.d.ts +9 -2
- package/components/DatePicker/DatePicker.js +137 -128
- package/components/Dialog/Dialog.d.ts +15 -4
- package/components/Dialog/Dialog.js +136 -118
- package/components/Drawer/Drawer.d.ts +15 -4
- package/components/Drawer/Drawer.js +138 -119
- package/components/Dropdown/Dropdown.css +0 -36
- package/components/Dropdown/Dropdown.d.ts +24 -8
- package/components/Dropdown/Dropdown.js +219 -169
- package/components/DropdownMenu/DropdownMenu.js +10 -7
- package/components/Field/Field.css +80 -0
- package/components/Field/Field.d.ts +50 -0
- package/components/Field/Field.js +58 -0
- package/components/Field/FieldContext.d.ts +42 -0
- package/components/Field/FieldContext.js +7 -0
- package/components/FileInput/FileInput.css +0 -36
- package/components/FileInput/FileInput.d.ts +13 -17
- package/components/FileInput/FileInput.js +168 -141
- package/components/Input/Input.css +0 -43
- package/components/Input/Input.d.ts +23 -22
- package/components/Input/Input.js +93 -66
- package/components/Kbd/Kbd.css +28 -0
- package/components/Kbd/Kbd.d.ts +19 -0
- package/components/Kbd/Kbd.js +14 -0
- package/components/Popover/Popover.d.ts +0 -6
- package/components/Popover/Popover.js +22 -16
- package/components/ProgressBar/ProgressBar.js +1 -1
- package/components/RadioButton/RadioButton.d.ts +26 -13
- package/components/RadioButton/RadioButton.js +95 -70
- package/components/SelectionCard/SelectionCard.css +6 -2
- package/components/SelectionCard/SelectionCard.d.ts +10 -4
- package/components/SelectionCard/SelectionCard.js +72 -56
- package/components/Skeleton/Skeleton.css +2 -0
- package/components/Slider/Slider.d.ts +19 -10
- package/components/Slider/Slider.js +50 -40
- package/components/Spinner/Spinner.css +11 -0
- package/components/Spinner/Spinner.d.ts +2 -2
- package/components/Swatch/Swatch.css +52 -0
- package/components/Swatch/Swatch.d.ts +34 -0
- package/components/Swatch/Swatch.js +44 -0
- package/components/Table/Table.css +19 -0
- package/components/Table/Table.d.ts +10 -2
- package/components/Table/Table.js +58 -58
- package/components/Textarea/Textarea.css +0 -43
- package/components/Textarea/Textarea.d.ts +21 -22
- package/components/Textarea/Textarea.js +78 -67
- package/components/ToggleGroup/ToggleGroup.d.ts +18 -8
- package/components/ToggleGroup/ToggleGroup.js +61 -44
- package/components/ToggleSwitch/ToggleSwitch.css +7 -2
- package/components/ToggleSwitch/ToggleSwitch.d.ts +17 -9
- package/components/ToggleSwitch/ToggleSwitch.js +57 -44
- package/components/registry.d.ts +47 -10
- package/components/registry.js +5 -1
- package/components/registry.json +498 -56
- package/components/registry.json.d.ts +498 -56
- package/components/registry.json.js +4 -1
- package/index.d.ts +8 -1
- package/index.js +16 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# robr0 — Portfolio + Design System
|
|
2
2
|
|
|
3
3
|
[](https://github.com/robritacca-dotcom/design-system/actions/workflows/ci.yml)
|
|
4
|
+
<!-- npm-badge:start -->
|
|
5
|
+
[](https://www.npmjs.com/package/@robr0/design-system)
|
|
6
|
+
<!-- npm-badge:end -->
|
|
4
7
|
|
|
5
8
|
A personal portfolio site built on a custom React design system I designed and engineered from scratch. The DS isn't a side project — it's the backbone every portfolio page is built on, ensuring the work is presented consistently and with the same craft standards I apply professionally.
|
|
6
9
|
|
|
@@ -15,7 +18,7 @@ The **live site** is the portfolio built on the design system. **Storybook** is
|
|
|
15
18
|
| Part | Description |
|
|
16
19
|
|---|---|
|
|
17
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/)** |
|
|
18
|
-
| **Design system** (`/src`) | <!-- component-count -->
|
|
21
|
+
| **Design system** (`/src`) | <!-- component-count -->61<!-- /component-count --> React components, a three-tier token architecture, dark mode, and a full documentation site. Built to production standards. **[Storybook →](https://design-system-iota-one.vercel.app/?path=/docs/robr0-ds--docs)** |
|
|
19
22
|
|
|
20
23
|
---
|
|
21
24
|
|
|
@@ -24,17 +27,19 @@ The **live site** is the portfolio built on the design system. **Storybook** is
|
|
|
24
27
|
### Components
|
|
25
28
|
|
|
26
29
|
<!-- component-list:start -->
|
|
27
|
-
Accordion · Alert · Alert dialog · App layout · App sidebar · Avatar · Badge · Breadcrumb · Button · Button group · Card · Carousel · Chart · Checkbox · Chip · Circular button · Code block · Combobox · Command palette · Contact card · Contribution graph · Date input · Date picker · Dialog · Divider · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Figure · File input · Input · Instructions · Link list ·
|
|
30
|
+
Accordion · Alert · Alert dialog · App layout · App sidebar · Avatar · Badge · Breadcrumb · Button · Button group · Card · Carousel · Chart · Checkbox · Chip · Circular button · Code block · Colour picker · Combobox · Command palette · Contact card · Context menu · Contribution graph · Date input · Date picker · Dialog · Divider · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Field · Figure · File input · Input · Instructions · Kbd · Link list · Navigation · Pagination · Popover · Progress bar · Quote · Radio button · Section title · Segmented control · Selection card · Skeleton · Slider · Spinner · Stat · Swatch · Table · Tabs · Textarea · Timeline · Toast · Toggle group · Toggle switch · Tooltip
|
|
28
31
|
<!-- component-list:end -->
|
|
29
32
|
|
|
30
33
|
### Using the package
|
|
31
34
|
|
|
32
|
-
The design system is published as [`@robr0/design-system`](https://www.npmjs.com/package/@robr0/design-system) (React 19+ is a peer dependency):
|
|
35
|
+
The design system is published as [`@robr0/design-system`](https://www.npmjs.com/package/@robr0/design-system) (React 19+ is a peer dependency) — the **[full setup guide](https://www.robertritacca.com/docs/get-started)** covers install, dark mode, fonts, and re-theming:
|
|
33
36
|
|
|
34
37
|
```bash
|
|
35
38
|
npm install @robr0/design-system
|
|
36
39
|
```
|
|
37
40
|
|
|
41
|
+
The package is ESM-only and resolved via `exports` subpaths: use a bundler that handles CSS and font imports from `node_modules` (Vite, Next.js, webpack), and set TypeScript's `moduleResolution` to `"bundler"` (or `"nodenext"`).
|
|
42
|
+
|
|
38
43
|
Import the token stylesheet once (it carries the primitives, semantic tokens, and both themes), then use components:
|
|
39
44
|
|
|
40
45
|
```tsx
|
|
@@ -48,7 +53,7 @@ Deep imports work too (`@robr0/design-system/components/Button/Button`). Chart c
|
|
|
48
53
|
import { BarChart, LineChart } from '@robr0/design-system/charts';
|
|
49
54
|
```
|
|
50
55
|
|
|
51
|
-
**Theming and customization** happen through CSS variables — no configuration API:
|
|
56
|
+
**Theming and customization** happen through CSS variables — no configuration API. Components are provider-free, with one exception: wrap your tree in `ToastProvider` if (and only if) you use the toast queue via `useToast`.
|
|
52
57
|
|
|
53
58
|
- **Dark mode**: set `data-theme="dark"` on the root element (light is the default).
|
|
54
59
|
- **Font**: the whole type scale chains to one token. Load any font you like and override it:
|
|
@@ -104,10 +109,10 @@ Full spec in [`design.md`](design.md).
|
|
|
104
109
|
|
|
105
110
|
## Quality & CI
|
|
106
111
|
|
|
107
|
-
Every push and pull request runs a GitHub Actions pipeline ([`ci.yml`](.github/workflows/ci.yml)) with four jobs: **lint + library build**, **story tests** (every Storybook story rendered in headless Chromium via Vitest), **Storybook build**, and **website build**. The same checklist runs locally with one command:
|
|
112
|
+
Every push and pull request runs a GitHub Actions pipeline ([`ci.yml`](.github/workflows/ci.yml)) with four jobs: **lint + library build**, **story tests** (every Storybook story rendered in headless Chromium via Vitest), **Storybook build**, and **website lint + build**. The same checklist runs locally with one command:
|
|
108
113
|
|
|
109
114
|
```bash
|
|
110
|
-
npm run verify # lint + library build + story tests + Storybook build + website build
|
|
115
|
+
npm run verify # lint + library type-check + package build + story tests + Storybook build + website lint + build
|
|
111
116
|
```
|
|
112
117
|
|
|
113
118
|
CI also guards against documentation drift: generated surfaces (this README's component count and list, the website's skills pages, the published CLAUDE.md/design.md blueprints) are rebuilt from their source registries on every build, and CI fails if the committed copies are stale. The numbers on the site are never hand-written.
|
|
@@ -120,8 +125,7 @@ CI also guards against documentation drift: generated surfaces (this README's co
|
|
|
120
125
|
# Storybook (the library's dev sandbox)
|
|
121
126
|
npm run storybook # http://localhost:6006
|
|
122
127
|
|
|
123
|
-
# Portfolio + documentation website
|
|
124
|
-
cd website
|
|
128
|
+
# Portfolio + documentation website (npm workspace — install once at the root)
|
|
125
129
|
npm install
|
|
126
|
-
npm run dev
|
|
130
|
+
npm run dev --workspace website # http://localhost:3000
|
|
127
131
|
```
|
|
@@ -59,7 +59,6 @@ const Accordion = ({
|
|
|
59
59
|
"div",
|
|
60
60
|
{
|
|
61
61
|
id: `${baseClass}-panel-${item.id}`,
|
|
62
|
-
role: "region",
|
|
63
62
|
"aria-labelledby": `${baseClass}-header-${item.id}`,
|
|
64
63
|
className: `${baseClass}__panel`,
|
|
65
64
|
children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, children: item.content })
|
|
@@ -74,8 +74,16 @@
|
|
|
74
74
|
SEPARATOR
|
|
75
75
|
============================================ */
|
|
76
76
|
|
|
77
|
+
/*
|
|
78
|
+
Size via --icon-size, never font-size: .material-symbols-rounded derives
|
|
79
|
+
the glyph size AND the layout box from that one variable. Setting
|
|
80
|
+
font-size alone shrank the glyph but left a 24px box around it, so the
|
|
81
|
+
chevron floated above the text baseline. --icon-size-sm (20px) also
|
|
82
|
+
matches the 20px line-height of the paragraph-sm labels either side.
|
|
83
|
+
*/
|
|
77
84
|
.ds-breadcrumb__separator {
|
|
78
|
-
|
|
85
|
+
--icon-size: var(--icon-size-sm);
|
|
86
|
+
flex: none;
|
|
79
87
|
color: var(--color-text-tertiary);
|
|
80
88
|
user-select: none;
|
|
81
89
|
}
|
|
@@ -11,9 +11,15 @@ export interface BreadcrumbProps {
|
|
|
11
11
|
maxItems?: number;
|
|
12
12
|
/** Additional CSS classes */
|
|
13
13
|
className?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Accessible name for the nav landmark. Override when more than one
|
|
16
|
+
* Breadcrumb can appear on a page — identically-named landmarks are
|
|
17
|
+
* indistinguishable to assistive technology.
|
|
18
|
+
*/
|
|
19
|
+
ariaLabel?: string;
|
|
14
20
|
}
|
|
15
21
|
/**
|
|
16
22
|
* Breadcrumb shows the user's location within a page hierarchy.
|
|
17
23
|
* Supports automatic collapsing of middle items when the list exceeds maxItems.
|
|
18
24
|
*/
|
|
19
|
-
export declare const Breadcrumb: ({ items, maxItems, className, }: BreadcrumbProps) => import("react").JSX.Element;
|
|
25
|
+
export declare const Breadcrumb: ({ items, maxItems, className, ariaLabel, }: BreadcrumbProps) => import("react").JSX.Element;
|
|
@@ -4,7 +4,8 @@ import "../../fonts/material-symbols.css";
|
|
|
4
4
|
const Breadcrumb = ({
|
|
5
5
|
items,
|
|
6
6
|
maxItems,
|
|
7
|
-
className = ""
|
|
7
|
+
className = "",
|
|
8
|
+
ariaLabel
|
|
8
9
|
}) => {
|
|
9
10
|
const baseClass = "ds-breadcrumb";
|
|
10
11
|
const classes = [baseClass, className].filter(Boolean).join(" ");
|
|
@@ -20,7 +21,7 @@ const Breadcrumb = ({
|
|
|
20
21
|
} else {
|
|
21
22
|
visibleItems = [...items];
|
|
22
23
|
}
|
|
23
|
-
return /* @__PURE__ */ jsx("nav", { className: classes, "aria-label": "Breadcrumb", children: /* @__PURE__ */ jsx("ol", { className: `${baseClass}__list`, children: visibleItems.map((item, index) => {
|
|
24
|
+
return /* @__PURE__ */ jsx("nav", { className: classes, "aria-label": ariaLabel || "Breadcrumb", children: /* @__PURE__ */ jsx("ol", { className: `${baseClass}__list`, children: visibleItems.map((item, index) => {
|
|
24
25
|
const isLast = index === visibleItems.length - 1;
|
|
25
26
|
if (item === "ellipsis") {
|
|
26
27
|
return /* @__PURE__ */ jsxs("li", { className: `${baseClass}__item`, "aria-hidden": "true", children: [
|
|
@@ -371,6 +371,24 @@
|
|
|
371
371
|
color: var(--color-status-error-bg);
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
+
/* ============================================
|
|
375
|
+
LOADING
|
|
376
|
+
Keeps the variant's full-colour appearance (no disabled dim);
|
|
377
|
+
the spinner occupies the left icon slot and inherits its colour.
|
|
378
|
+
============================================ */
|
|
379
|
+
|
|
380
|
+
.ds-button--loading {
|
|
381
|
+
cursor: progress;
|
|
382
|
+
pointer-events: none;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/* The generic icon-slot svg sizing must not distort the spinner —
|
|
386
|
+
it sizes itself from the Spinner size prop */
|
|
387
|
+
.ds-button .ds-button__icon .ds-spinner__circle {
|
|
388
|
+
width: 100%;
|
|
389
|
+
height: 100%;
|
|
390
|
+
}
|
|
391
|
+
|
|
374
392
|
/* ============================================
|
|
375
393
|
FOCUS
|
|
376
394
|
============================================ */
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
|
-
|
|
2
|
+
/** Props owned by Button itself — everything else falls through to the DOM node. */
|
|
3
|
+
type ButtonOwnProps = {
|
|
3
4
|
/** Button text content */
|
|
4
5
|
label?: string;
|
|
5
6
|
/** Icon for left side — Material Symbol name (string) or custom element (ReactNode) */
|
|
6
7
|
iconLeft?: string | React.ReactNode;
|
|
7
8
|
/** Icon for right side — Material Symbol name (string) or custom element (ReactNode) */
|
|
8
9
|
iconRight?: string | React.ReactNode;
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
/** Button state */
|
|
12
|
-
state?: 'default' | 'hover' | 'active' | 'disabled';
|
|
10
|
+
/** Visual treatment */
|
|
11
|
+
variant?: 'primary' | 'secondary' | 'tertiary' | 'destructive';
|
|
13
12
|
/** Button size */
|
|
14
13
|
size?: 'default' | 'compact';
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
|
|
14
|
+
/** Whether the button is disabled */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Shows a spinner in the left icon slot and blocks interaction while an
|
|
18
|
+
* async action runs. Keeps the variant's full-colour appearance (unlike
|
|
19
|
+
* `disabled`) and sets `aria-busy` on the rendered element.
|
|
20
|
+
*/
|
|
21
|
+
loading?: boolean;
|
|
19
22
|
/** Optional href — renders as <a> instead of <button> */
|
|
20
23
|
href?: string;
|
|
21
24
|
/** Optional target attribute for links */
|
|
@@ -24,16 +27,35 @@ export interface ButtonProps {
|
|
|
24
27
|
rel?: string;
|
|
25
28
|
/** Marks this link as the current page (adds aria-current="page") */
|
|
26
29
|
ariaCurrent?: boolean;
|
|
27
|
-
/** Id of an element describing this button — set automatically by Tooltip */
|
|
28
|
-
'aria-describedby'?: string;
|
|
29
30
|
/** Additional CSS classes */
|
|
30
31
|
className?: string;
|
|
31
|
-
/** @deprecated Use
|
|
32
|
+
/** @deprecated Use `variant` instead. */
|
|
33
|
+
priority?: 'primary' | 'secondary' | 'tertiary' | 'destructive';
|
|
34
|
+
/**
|
|
35
|
+
* @deprecated Use `disabled` for the disabled state.
|
|
36
|
+
*
|
|
37
|
+
* Documentation-only affordance for rendering a *static* interaction state in
|
|
38
|
+
* Storybook and the showcase site. Real hover/active styling comes from CSS
|
|
39
|
+
* pseudo-classes and needs no prop — for docs, prefer `className="ds-button--hover"`.
|
|
40
|
+
*/
|
|
41
|
+
state?: 'default' | 'hover' | 'active' | 'disabled';
|
|
42
|
+
/** @deprecated Use `iconLeft` instead. */
|
|
32
43
|
icon?: string;
|
|
44
|
+
/**
|
|
45
|
+
* @deprecated Will be removed once `label` loses its default in the next major;
|
|
46
|
+
* an icon-only button will simply omit `label`.
|
|
47
|
+
*/
|
|
48
|
+
text?: boolean;
|
|
49
|
+
};
|
|
50
|
+
export interface ButtonProps extends ButtonOwnProps, Omit<React.ComponentPropsWithoutRef<'button'>, keyof ButtonOwnProps | 'type'> {
|
|
33
51
|
}
|
|
34
52
|
/**
|
|
35
|
-
* Button component from Figma design system
|
|
36
|
-
* Supports primary
|
|
37
|
-
* Icons can be placed on the left and/or right side of the text
|
|
53
|
+
* Button component from Figma design system.
|
|
54
|
+
* Supports primary, secondary, tertiary and destructive variants.
|
|
55
|
+
* Icons can be placed on the left and/or right side of the text.
|
|
56
|
+
*
|
|
57
|
+
* Renders a `<button>`, or an `<a>` when `href` is supplied. Forwards a ref to
|
|
58
|
+
* whichever element it renders, and spreads unrecognised props onto it.
|
|
38
59
|
*/
|
|
39
|
-
export declare const Button:
|
|
60
|
+
export declare const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement | HTMLAnchorElement>>;
|
|
61
|
+
export {};
|
|
@@ -1,71 +1,89 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import "react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Spinner } from "../Spinner/Spinner.js";
|
|
3
4
|
import "./Button.css";
|
|
4
5
|
import "../../fonts/material-symbols.css";
|
|
5
|
-
const Button = (
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
6
|
+
const Button = React.forwardRef(
|
|
7
|
+
({
|
|
8
|
+
label = "Button",
|
|
9
|
+
iconLeft,
|
|
10
|
+
iconRight,
|
|
11
|
+
variant,
|
|
12
|
+
size = "default",
|
|
13
|
+
disabled,
|
|
14
|
+
loading,
|
|
15
|
+
href,
|
|
16
|
+
target,
|
|
17
|
+
rel,
|
|
18
|
+
ariaCurrent,
|
|
19
|
+
className = "",
|
|
20
|
+
priority,
|
|
21
|
+
state,
|
|
22
|
+
icon,
|
|
23
|
+
text = true,
|
|
24
|
+
...rest
|
|
25
|
+
}, ref) => {
|
|
26
|
+
const baseClass = "ds-button";
|
|
27
|
+
const resolvedVariant = variant ?? priority ?? "primary";
|
|
28
|
+
const isDisabled = disabled ?? state === "disabled";
|
|
29
|
+
const resolvedState = state ?? (isDisabled ? "disabled" : "default");
|
|
30
|
+
const isLoading = Boolean(loading);
|
|
31
|
+
const classes = [
|
|
32
|
+
baseClass,
|
|
33
|
+
`${baseClass}--${resolvedVariant}`,
|
|
34
|
+
`${baseClass}--${resolvedState}`,
|
|
35
|
+
`${baseClass}--${size}`,
|
|
36
|
+
isLoading && `${baseClass}--loading`,
|
|
37
|
+
className
|
|
38
|
+
].filter(Boolean).join(" ");
|
|
39
|
+
const iconClass = "material-symbols-rounded";
|
|
40
|
+
const spinnerSize = size === "compact" ? "sm" : "md";
|
|
41
|
+
const leftIcon = isLoading ? /* @__PURE__ */ jsx(Spinner, { size: spinnerSize, variant: "inherit" }) : iconLeft || icon;
|
|
42
|
+
const renderIcon = (iconProp) => {
|
|
43
|
+
if (typeof iconProp === "string") {
|
|
44
|
+
return /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon ${iconClass}`, "aria-hidden": "true", children: iconProp });
|
|
45
|
+
}
|
|
46
|
+
return /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: iconProp });
|
|
47
|
+
};
|
|
48
|
+
const needsAccessibleName = !text || !label;
|
|
49
|
+
const fallbackAriaLabel = needsAccessibleName && label && !rest["aria-label"] ? label : void 0;
|
|
50
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
51
|
+
leftIcon && renderIcon(leftIcon),
|
|
52
|
+
text && /* @__PURE__ */ jsx("span", { className: `${baseClass}__text`, children: label }),
|
|
53
|
+
iconRight && renderIcon(iconRight)
|
|
54
|
+
] });
|
|
55
|
+
if (href && !isDisabled && !isLoading) {
|
|
56
|
+
return /* @__PURE__ */ jsx(
|
|
57
|
+
"a",
|
|
58
|
+
{
|
|
59
|
+
...rest,
|
|
60
|
+
ref,
|
|
61
|
+
className: classes,
|
|
62
|
+
href,
|
|
63
|
+
target,
|
|
64
|
+
rel,
|
|
65
|
+
"aria-current": ariaCurrent ? "page" : void 0,
|
|
66
|
+
"aria-label": fallbackAriaLabel ?? rest["aria-label"],
|
|
67
|
+
children: content
|
|
68
|
+
}
|
|
69
|
+
);
|
|
34
70
|
}
|
|
35
|
-
return /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: iconProp });
|
|
36
|
-
};
|
|
37
|
-
const children = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
38
|
-
leftIcon && renderIcon(leftIcon),
|
|
39
|
-
text && /* @__PURE__ */ jsx("span", { className: `${baseClass}__text`, children: label }),
|
|
40
|
-
iconRight && renderIcon(iconRight)
|
|
41
|
-
] });
|
|
42
|
-
if (href && !isDisabled) {
|
|
43
71
|
return /* @__PURE__ */ jsx(
|
|
44
|
-
"
|
|
72
|
+
"button",
|
|
45
73
|
{
|
|
74
|
+
...rest,
|
|
75
|
+
ref,
|
|
76
|
+
type: "button",
|
|
46
77
|
className: classes,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"aria-describedby": ariaDescribedby,
|
|
52
|
-
onClick,
|
|
53
|
-
children
|
|
78
|
+
disabled: isDisabled || isLoading,
|
|
79
|
+
"aria-busy": isLoading || void 0,
|
|
80
|
+
"aria-label": fallbackAriaLabel ?? rest["aria-label"],
|
|
81
|
+
children: content
|
|
54
82
|
}
|
|
55
83
|
);
|
|
56
84
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
{
|
|
60
|
-
type: "button",
|
|
61
|
-
className: classes,
|
|
62
|
-
onClick,
|
|
63
|
-
disabled: isDisabled,
|
|
64
|
-
"aria-describedby": ariaDescribedby,
|
|
65
|
-
children
|
|
66
|
-
}
|
|
67
|
-
);
|
|
68
|
-
};
|
|
85
|
+
);
|
|
86
|
+
Button.displayName = "Button";
|
|
69
87
|
export {
|
|
70
88
|
Button
|
|
71
89
|
};
|
package/components/Card/Card.css
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
|
|
26
26
|
/* Interaction */
|
|
27
27
|
transition: transform var(--motion-duration-slow) var(--motion-ease-standard), box-shadow var(--motion-duration-slow) var(--motion-ease-standard);
|
|
28
|
+
/* ds-allow(color): transparent resting state of the sanctioned Card
|
|
29
|
+
hover-lift shadow (see design.md — Do's and Don'ts). */
|
|
28
30
|
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -34,6 +36,8 @@
|
|
|
34
36
|
|
|
35
37
|
.ds-card--interactive:hover {
|
|
36
38
|
transform: translateY(-4px);
|
|
39
|
+
/* ds-allow(color): the sanctioned Card hover-lift shadow — deliberately
|
|
40
|
+
heavier than --shadow-floating (see design.md — Do's and Don'ts). */
|
|
37
41
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -80,6 +84,8 @@
|
|
|
80
84
|
|
|
81
85
|
.ds-card--case-study:hover {
|
|
82
86
|
transform: translateY(-4px);
|
|
87
|
+
/* ds-allow(color): same sanctioned hover-lift shadow as the interactive
|
|
88
|
+
variant. */
|
|
83
89
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
84
90
|
}
|
|
85
91
|
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
|
-
|
|
2
|
+
/** Props owned by Card itself — everything else falls through to the DOM node. */
|
|
3
|
+
type CardOwnProps = {
|
|
3
4
|
/** Card variant */
|
|
4
5
|
variant?: 'default' | 'case-study';
|
|
5
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Card title displayed below the preview.
|
|
8
|
+
* Note: this shadows the native `title` tooltip attribute, which Card does not expose.
|
|
9
|
+
*/
|
|
6
10
|
title: string;
|
|
7
11
|
/** Preview content rendered inside the card (default variant only) */
|
|
8
12
|
children?: React.ReactNode;
|
|
9
13
|
/** Whether the card is interactive (hoverable) */
|
|
10
14
|
interactive?: boolean;
|
|
11
|
-
/** Click handler */
|
|
12
|
-
onClick?: () => void;
|
|
13
15
|
/** Additional CSS classes */
|
|
14
16
|
className?: string;
|
|
15
17
|
/** Navigation href — renders the card as an <a> tag */
|
|
@@ -26,5 +28,15 @@ export interface CardProps {
|
|
|
26
28
|
dek?: string;
|
|
27
29
|
/** Render as a disabled placeholder (no href, dimmed, not interactive) */
|
|
28
30
|
placeholder?: boolean;
|
|
31
|
+
};
|
|
32
|
+
export interface CardProps extends CardOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof CardOwnProps> {
|
|
29
33
|
}
|
|
30
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Card renders a preview tile, or a case-study tile when `variant="case-study"`.
|
|
36
|
+
*
|
|
37
|
+
* Renders a `<div>`, or an `<a>` in the case-study variant when `href` is
|
|
38
|
+
* supplied. Forwards a ref to whichever element it renders and spreads
|
|
39
|
+
* unrecognised props onto it.
|
|
40
|
+
*/
|
|
41
|
+
export declare const Card: React.ForwardRefExoticComponent<CardProps & React.RefAttributes<HTMLDivElement | HTMLAnchorElement>>;
|
|
42
|
+
export {};
|
package/components/Card/Card.js
CHANGED
|
@@ -1,90 +1,111 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import "react";
|
|
2
|
+
import React from "react";
|
|
3
3
|
import "./Card.css";
|
|
4
4
|
import "../../fonts/material-symbols.css";
|
|
5
|
-
const Card = (
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
5
|
+
const Card = React.forwardRef(
|
|
6
|
+
({
|
|
7
|
+
variant = "default",
|
|
8
|
+
title,
|
|
9
|
+
children,
|
|
10
|
+
interactive = false,
|
|
11
|
+
onClick,
|
|
12
|
+
onKeyDown,
|
|
13
|
+
className = "",
|
|
14
|
+
href,
|
|
15
|
+
coverSrc,
|
|
16
|
+
coverAlt,
|
|
17
|
+
companyLogo,
|
|
18
|
+
companyName,
|
|
19
|
+
dek,
|
|
20
|
+
placeholder = false,
|
|
21
|
+
...rest
|
|
22
|
+
}, ref) => {
|
|
23
|
+
if (variant === "case-study") {
|
|
24
|
+
const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
25
|
+
/* @__PURE__ */ jsx("div", { className: "ds-card--case-study__cover-wrap", children: coverSrc ? /* @__PURE__ */ jsx(
|
|
26
|
+
"img",
|
|
27
|
+
{
|
|
28
|
+
src: coverSrc,
|
|
29
|
+
alt: coverAlt ?? `${title} cover`,
|
|
30
|
+
className: "ds-card--case-study__cover-image"
|
|
31
|
+
}
|
|
32
|
+
) : /* @__PURE__ */ jsx("div", { className: "ds-card--case-study__cover-placeholder", children: companyLogo && /* @__PURE__ */ jsx(
|
|
33
|
+
"img",
|
|
34
|
+
{
|
|
35
|
+
src: companyLogo,
|
|
36
|
+
alt: "",
|
|
37
|
+
className: "ds-card--case-study__cover-placeholder-logo"
|
|
38
|
+
}
|
|
39
|
+
) }) }),
|
|
40
|
+
/* @__PURE__ */ jsxs("div", { className: "ds-card--case-study__body", children: [
|
|
41
|
+
(companyLogo || companyName) && /* @__PURE__ */ jsxs("div", { className: "ds-card--case-study__eyebrow", children: [
|
|
42
|
+
companyLogo && /* @__PURE__ */ jsx("img", { src: companyLogo, alt: "", className: "ds-card--case-study__company-logo" }),
|
|
43
|
+
companyName && /* @__PURE__ */ jsx("span", { className: "ds-card--case-study__company-name", children: companyName })
|
|
44
|
+
] }),
|
|
45
|
+
/* @__PURE__ */ jsx("h3", { className: "ds-card--case-study__title", children: title }),
|
|
46
|
+
dek && /* @__PURE__ */ jsx("p", { className: "ds-card--case-study__dek", children: dek })
|
|
47
|
+
] })
|
|
48
|
+
] });
|
|
49
|
+
const classes2 = [
|
|
50
|
+
"ds-card",
|
|
51
|
+
"ds-card--case-study",
|
|
52
|
+
placeholder ? "ds-card--case-study--placeholder" : "",
|
|
53
|
+
className
|
|
54
|
+
].filter(Boolean).join(" ");
|
|
55
|
+
if (placeholder || !href) {
|
|
56
|
+
return /* @__PURE__ */ jsx(
|
|
57
|
+
"div",
|
|
58
|
+
{
|
|
59
|
+
...rest,
|
|
60
|
+
ref,
|
|
61
|
+
className: classes2,
|
|
62
|
+
"aria-disabled": "true",
|
|
63
|
+
children: inner
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return /* @__PURE__ */ jsx(
|
|
68
|
+
"a",
|
|
24
69
|
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
70
|
+
...rest,
|
|
71
|
+
ref,
|
|
72
|
+
href,
|
|
73
|
+
className: classes2,
|
|
74
|
+
onClick,
|
|
75
|
+
children: inner
|
|
28
76
|
}
|
|
29
|
-
)
|
|
30
|
-
"img",
|
|
31
|
-
{
|
|
32
|
-
src: companyLogo,
|
|
33
|
-
alt: "",
|
|
34
|
-
className: "ds-card--case-study__cover-placeholder-logo"
|
|
35
|
-
}
|
|
36
|
-
) }) }),
|
|
37
|
-
/* @__PURE__ */ jsxs("div", { className: "ds-card--case-study__body", children: [
|
|
38
|
-
(companyLogo || companyName) && /* @__PURE__ */ jsxs("div", { className: "ds-card--case-study__eyebrow", children: [
|
|
39
|
-
companyLogo && /* @__PURE__ */ jsx(
|
|
40
|
-
"img",
|
|
41
|
-
{
|
|
42
|
-
src: companyLogo,
|
|
43
|
-
alt: "",
|
|
44
|
-
className: "ds-card--case-study__company-logo"
|
|
45
|
-
}
|
|
46
|
-
),
|
|
47
|
-
companyName && /* @__PURE__ */ jsx("span", { className: "ds-card--case-study__company-name", children: companyName })
|
|
48
|
-
] }),
|
|
49
|
-
/* @__PURE__ */ jsx("h3", { className: "ds-card--case-study__title", children: title }),
|
|
50
|
-
dek && /* @__PURE__ */ jsx("p", { className: "ds-card--case-study__dek", children: dek })
|
|
51
|
-
] })
|
|
52
|
-
] });
|
|
53
|
-
const classes2 = [
|
|
54
|
-
"ds-card",
|
|
55
|
-
"ds-card--case-study",
|
|
56
|
-
placeholder ? "ds-card--case-study--placeholder" : "",
|
|
57
|
-
className
|
|
58
|
-
].filter(Boolean).join(" ");
|
|
59
|
-
if (placeholder || !href) {
|
|
60
|
-
return /* @__PURE__ */ jsx("div", { className: classes2, "aria-disabled": "true", children: inner });
|
|
77
|
+
);
|
|
61
78
|
}
|
|
62
|
-
|
|
79
|
+
const baseClass = "ds-card";
|
|
80
|
+
const classes = [baseClass, interactive ? `${baseClass}--interactive` : "", className].filter(Boolean).join(" ");
|
|
81
|
+
const handleKeyDown = (e) => {
|
|
82
|
+
onKeyDown?.(e);
|
|
83
|
+
if (!interactive) return;
|
|
84
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
onClick?.(e);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
return /* @__PURE__ */ jsxs(
|
|
90
|
+
"div",
|
|
91
|
+
{
|
|
92
|
+
...rest,
|
|
93
|
+
ref,
|
|
94
|
+
className: classes,
|
|
95
|
+
onClick: interactive ? onClick : void 0,
|
|
96
|
+
onKeyDown: handleKeyDown,
|
|
97
|
+
role: interactive ? "button" : void 0,
|
|
98
|
+
tabIndex: interactive ? 0 : void 0,
|
|
99
|
+
"aria-label": interactive ? title : void 0,
|
|
100
|
+
children: [
|
|
101
|
+
/* @__PURE__ */ jsx("div", { className: `${baseClass}__preview`, children }),
|
|
102
|
+
/* @__PURE__ */ jsx("h3", { className: `${baseClass}__title`, children: title })
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
);
|
|
63
106
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const classes = [baseClass, interactiveClass, className].filter(Boolean).join(" ");
|
|
67
|
-
return /* @__PURE__ */ jsxs(
|
|
68
|
-
"div",
|
|
69
|
-
{
|
|
70
|
-
className: classes,
|
|
71
|
-
onClick: interactive ? onClick : void 0,
|
|
72
|
-
onKeyDown: interactive ? (e) => {
|
|
73
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
74
|
-
e.preventDefault();
|
|
75
|
-
onClick?.();
|
|
76
|
-
}
|
|
77
|
-
} : void 0,
|
|
78
|
-
role: interactive ? "button" : void 0,
|
|
79
|
-
tabIndex: interactive ? 0 : void 0,
|
|
80
|
-
"aria-label": interactive ? title : void 0,
|
|
81
|
-
children: [
|
|
82
|
-
/* @__PURE__ */ jsx("div", { className: `${baseClass}__preview`, children }),
|
|
83
|
-
/* @__PURE__ */ jsx("h3", { className: `${baseClass}__title`, children: title })
|
|
84
|
-
]
|
|
85
|
-
}
|
|
86
|
-
);
|
|
87
|
-
};
|
|
107
|
+
);
|
|
108
|
+
Card.displayName = "Card";
|
|
88
109
|
export {
|
|
89
110
|
Card
|
|
90
111
|
};
|