@robr0/design-system 0.2.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.
Files changed (66) hide show
  1. package/README.md +8 -6
  2. package/components/Accordion/Accordion.js +0 -1
  3. package/components/Avatar/Avatar.js +1 -0
  4. package/components/Breadcrumb/Breadcrumb.d.ts +7 -1
  5. package/components/Breadcrumb/Breadcrumb.js +3 -2
  6. package/components/Button/Button.css +18 -0
  7. package/components/Button/Button.d.ts +6 -0
  8. package/components/Button/Button.js +13 -3
  9. package/components/Card/Card.css +6 -0
  10. package/components/CircularButton/CircularButton.css +11 -0
  11. package/components/CircularButton/CircularButton.d.ts +7 -1
  12. package/components/CircularButton/CircularButton.js +15 -4
  13. package/components/CodeBlock/CodeBlock.js +10 -1
  14. package/components/ColorPicker/ColorPicker.css +278 -0
  15. package/components/ColorPicker/ColorPicker.d.ts +50 -0
  16. package/components/ColorPicker/ColorPicker.js +338 -0
  17. package/components/Combobox/Combobox.css +0 -36
  18. package/components/Combobox/Combobox.js +90 -82
  19. package/components/CommandPalette/CommandPalette.css +1 -15
  20. package/components/CommandPalette/CommandPalette.js +6 -5
  21. package/components/ContextMenu/ContextMenu.css +262 -0
  22. package/components/ContextMenu/ContextMenu.d.ts +26 -0
  23. package/components/ContextMenu/ContextMenu.js +350 -0
  24. package/components/DateInput/DateInput.css +0 -36
  25. package/components/DateInput/DateInput.js +37 -33
  26. package/components/DatePicker/DatePicker.js +45 -34
  27. package/components/Dialog/Dialog.js +5 -4
  28. package/components/Drawer/Drawer.js +7 -2
  29. package/components/Dropdown/Dropdown.css +0 -36
  30. package/components/Dropdown/Dropdown.js +119 -109
  31. package/components/DropdownMenu/DropdownMenu.js +10 -7
  32. package/components/Field/Field.css +80 -0
  33. package/components/Field/Field.d.ts +50 -0
  34. package/components/Field/Field.js +58 -0
  35. package/components/Field/FieldContext.d.ts +42 -0
  36. package/components/Field/FieldContext.js +7 -0
  37. package/components/FileInput/FileInput.css +0 -36
  38. package/components/FileInput/FileInput.js +111 -103
  39. package/components/Input/Input.css +0 -43
  40. package/components/Input/Input.js +51 -46
  41. package/components/Kbd/Kbd.css +28 -0
  42. package/components/Kbd/Kbd.d.ts +19 -0
  43. package/components/Kbd/Kbd.js +14 -0
  44. package/components/Popover/Popover.d.ts +0 -6
  45. package/components/Popover/Popover.js +22 -16
  46. package/components/ProgressBar/ProgressBar.js +1 -1
  47. package/components/SelectionCard/SelectionCard.css +6 -2
  48. package/components/Skeleton/Skeleton.css +2 -0
  49. package/components/Spinner/Spinner.css +11 -0
  50. package/components/Spinner/Spinner.d.ts +2 -2
  51. package/components/Swatch/Swatch.css +52 -0
  52. package/components/Swatch/Swatch.d.ts +34 -0
  53. package/components/Swatch/Swatch.js +44 -0
  54. package/components/Table/Table.css +19 -0
  55. package/components/Table/Table.js +1 -1
  56. package/components/Textarea/Textarea.css +0 -43
  57. package/components/Textarea/Textarea.js +37 -35
  58. package/components/ToggleSwitch/ToggleSwitch.css +7 -2
  59. package/components/registry.d.ts +47 -10
  60. package/components/registry.js +5 -1
  61. package/components/registry.json +498 -56
  62. package/components/registry.json.d.ts +498 -56
  63. package/components/registry.json.js +4 -1
  64. package/index.d.ts +8 -1
  65. package/index.js +16 -1
  66. package/package.json +1 -1
package/README.md CHANGED
@@ -18,7 +18,7 @@ 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 -->56<!-- /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)** |
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)** |
22
22
 
23
23
  ---
24
24
 
@@ -27,17 +27,19 @@ The **live site** is the portfolio built on the design system. **Storybook** is
27
27
  ### Components
28
28
 
29
29
  <!-- component-list:start -->
30
- 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 · Nav · Pagination · Popover · Progress bar · Quote · Radio button · Section title · Segmented control · Selection card · Skeleton · Slider · Spinner · Stat · Table · Tabs · Textarea · Timeline · Toast · Toggle group · Toggle switch · Tooltip
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
31
31
  <!-- component-list:end -->
32
32
 
33
33
  ### Using the package
34
34
 
35
- 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:
36
36
 
37
37
  ```bash
38
38
  npm install @robr0/design-system
39
39
  ```
40
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
+
41
43
  Import the token stylesheet once (it carries the primitives, semantic tokens, and both themes), then use components:
42
44
 
43
45
  ```tsx
@@ -51,7 +53,7 @@ Deep imports work too (`@robr0/design-system/components/Button/Button`). Chart c
51
53
  import { BarChart, LineChart } from '@robr0/design-system/charts';
52
54
  ```
53
55
 
54
- **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`.
55
57
 
56
58
  - **Dark mode**: set `data-theme="dark"` on the root element (light is the default).
57
59
  - **Font**: the whole type scale chains to one token. Load any font you like and override it:
@@ -107,10 +109,10 @@ Full spec in [`design.md`](design.md).
107
109
 
108
110
  ## Quality & CI
109
111
 
110
- 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:
111
113
 
112
114
  ```bash
113
- npm run verify # lint + library type-check + package build + story tests + Storybook build + website build
115
+ npm run verify # lint + library type-check + package build + story tests + Storybook build + website lint + build
114
116
  ```
115
117
 
116
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.
@@ -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 })
@@ -46,6 +46,7 @@ const Avatar = ({
46
46
  "span",
47
47
  {
48
48
  className: `${baseClass}__status ${baseClass}__status--${status}`,
49
+ role: "img",
49
50
  "aria-label": status
50
51
  }
51
52
  )
@@ -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
  ============================================ */
@@ -13,6 +13,12 @@ type ButtonOwnProps = {
13
13
  size?: 'default' | 'compact';
14
14
  /** Whether the button is disabled */
15
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;
16
22
  /** Optional href — renders as <a> instead of <button> */
17
23
  href?: string;
18
24
  /** Optional target attribute for links */
@@ -1,5 +1,6 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
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
6
  const Button = React.forwardRef(
@@ -10,6 +11,7 @@ const Button = React.forwardRef(
10
11
  variant,
11
12
  size = "default",
12
13
  disabled,
14
+ loading,
13
15
  href,
14
16
  target,
15
17
  rel,
@@ -25,27 +27,32 @@ const Button = React.forwardRef(
25
27
  const resolvedVariant = variant ?? priority ?? "primary";
26
28
  const isDisabled = disabled ?? state === "disabled";
27
29
  const resolvedState = state ?? (isDisabled ? "disabled" : "default");
30
+ const isLoading = Boolean(loading);
28
31
  const classes = [
29
32
  baseClass,
30
33
  `${baseClass}--${resolvedVariant}`,
31
34
  `${baseClass}--${resolvedState}`,
32
35
  `${baseClass}--${size}`,
36
+ isLoading && `${baseClass}--loading`,
33
37
  className
34
38
  ].filter(Boolean).join(" ");
35
39
  const iconClass = "material-symbols-rounded";
36
- const leftIcon = iconLeft || icon;
40
+ const spinnerSize = size === "compact" ? "sm" : "md";
41
+ const leftIcon = isLoading ? /* @__PURE__ */ jsx(Spinner, { size: spinnerSize, variant: "inherit" }) : iconLeft || icon;
37
42
  const renderIcon = (iconProp) => {
38
43
  if (typeof iconProp === "string") {
39
44
  return /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon ${iconClass}`, "aria-hidden": "true", children: iconProp });
40
45
  }
41
46
  return /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: iconProp });
42
47
  };
48
+ const needsAccessibleName = !text || !label;
49
+ const fallbackAriaLabel = needsAccessibleName && label && !rest["aria-label"] ? label : void 0;
43
50
  const content = /* @__PURE__ */ jsxs(Fragment, { children: [
44
51
  leftIcon && renderIcon(leftIcon),
45
52
  text && /* @__PURE__ */ jsx("span", { className: `${baseClass}__text`, children: label }),
46
53
  iconRight && renderIcon(iconRight)
47
54
  ] });
48
- if (href && !isDisabled) {
55
+ if (href && !isDisabled && !isLoading) {
49
56
  return /* @__PURE__ */ jsx(
50
57
  "a",
51
58
  {
@@ -56,6 +63,7 @@ const Button = React.forwardRef(
56
63
  target,
57
64
  rel,
58
65
  "aria-current": ariaCurrent ? "page" : void 0,
66
+ "aria-label": fallbackAriaLabel ?? rest["aria-label"],
59
67
  children: content
60
68
  }
61
69
  );
@@ -67,7 +75,9 @@ const Button = React.forwardRef(
67
75
  ref,
68
76
  type: "button",
69
77
  className: classes,
70
- disabled: isDisabled,
78
+ disabled: isDisabled || isLoading,
79
+ "aria-busy": isLoading || void 0,
80
+ "aria-label": fallbackAriaLabel ?? rest["aria-label"],
71
81
  children: content
72
82
  }
73
83
  );
@@ -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
 
@@ -196,6 +196,17 @@
196
196
  background-color: var(--color-action-passive-bg-active);
197
197
  }
198
198
 
199
+ /* ============================================
200
+ LOADING
201
+ Keeps the variant's full-colour appearance (no disabled dim);
202
+ the spinner replaces the icon and inherits its colour.
203
+ ============================================ */
204
+
205
+ .ds-circular-button--loading {
206
+ cursor: progress;
207
+ pointer-events: none;
208
+ }
209
+
199
210
  /* ============================================
200
211
  FOCUS
201
212
  ============================================ */
@@ -7,6 +7,12 @@ export interface CircularButtonProps {
7
7
  state?: 'default' | 'hover' | 'active' | 'disabled';
8
8
  /** Button size */
9
9
  size?: 'default' | 'compact';
10
+ /**
11
+ * Shows a spinner in place of the icon and blocks interaction while an
12
+ * async action runs. Keeps the variant's full-colour appearance (unlike
13
+ * the disabled state) and sets `aria-busy` on the rendered element.
14
+ */
15
+ loading?: boolean;
10
16
  /** Accessible label */
11
17
  ariaLabel: string;
12
18
  /** Optional click handler */
@@ -27,4 +33,4 @@ export interface CircularButtonProps {
27
33
  * A round button containing a single icon, available in
28
34
  * primary and secondary variants with default and compact sizes.
29
35
  */
30
- export declare const CircularButton: ({ icon, priority, state, size, ariaLabel, onClick, href, target, rel, "aria-describedby": ariaDescribedby, className, }: CircularButtonProps) => import("react").JSX.Element;
36
+ export declare const CircularButton: ({ icon, priority, state, size, loading, ariaLabel, onClick, href, target, rel, "aria-describedby": ariaDescribedby, className, }: CircularButtonProps) => import("react").JSX.Element;
@@ -1,4 +1,5 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
+ import { Spinner } from "../Spinner/Spinner.js";
2
3
  import "./CircularButton.css";
3
4
  import "../../fonts/material-symbols.css";
4
5
  const CircularButton = ({
@@ -6,6 +7,7 @@ const CircularButton = ({
6
7
  priority = "primary",
7
8
  state = "default",
8
9
  size = "default",
10
+ loading,
9
11
  ariaLabel,
10
12
  onClick,
11
13
  href,
@@ -18,10 +20,18 @@ const CircularButton = ({
18
20
  const variantClass = `${baseClass}--${priority}`;
19
21
  const stateClass = `${baseClass}--${state}`;
20
22
  const sizeClass = `${baseClass}--${size}`;
21
- const classes = [baseClass, variantClass, stateClass, sizeClass, className].filter(Boolean).join(" ");
23
+ const isLoading = Boolean(loading);
24
+ const classes = [
25
+ baseClass,
26
+ variantClass,
27
+ stateClass,
28
+ sizeClass,
29
+ isLoading && `${baseClass}--loading`,
30
+ className
31
+ ].filter(Boolean).join(" ");
22
32
  const isDisabled = state === "disabled";
23
- const children = /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon material-symbols-rounded`, "aria-hidden": "true", children: icon });
24
- if (href && !isDisabled) {
33
+ const children = isLoading ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: /* @__PURE__ */ jsx(Spinner, { size: size === "compact" ? "sm" : "md", variant: "inherit" }) }) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon material-symbols-rounded`, "aria-hidden": "true", children: icon });
34
+ if (href && !isDisabled && !isLoading) {
25
35
  return /* @__PURE__ */ jsx(
26
36
  "a",
27
37
  {
@@ -42,7 +52,8 @@ const CircularButton = ({
42
52
  type: "button",
43
53
  className: classes,
44
54
  onClick,
45
- disabled: isDisabled,
55
+ disabled: isDisabled || isLoading,
56
+ "aria-busy": isLoading || void 0,
46
57
  "aria-label": ariaLabel,
47
58
  "aria-describedby": ariaDescribedby,
48
59
  children
@@ -77,7 +77,16 @@ const CodeBlock = ({
77
77
  }
78
78
  )
79
79
  ] }),
80
- /* @__PURE__ */ jsx("div", { id: panelId, className: `${baseClass}__panel`, "aria-hidden": collapsed || void 0, children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__panel-inner`, children: /* @__PURE__ */ jsx("pre", { className: `${baseClass}__pre`, children: /* @__PURE__ */ jsx("code", { className: `${baseClass}__code`, children: code }) }) }) })
80
+ /* @__PURE__ */ jsx("div", { id: panelId, className: `${baseClass}__panel`, "aria-hidden": collapsed || void 0, children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__panel-inner`, children: /* @__PURE__ */ jsx(
81
+ "pre",
82
+ {
83
+ className: `${baseClass}__pre`,
84
+ tabIndex: collapsed ? -1 : 0,
85
+ role: "region",
86
+ "aria-label": filename ? `Code: ${filename}` : language ? `Code: ${language}` : "Code",
87
+ children: /* @__PURE__ */ jsx("code", { className: `${baseClass}__code`, children: code })
88
+ }
89
+ ) }) })
81
90
  ] });
82
91
  };
83
92
  export {
@@ -0,0 +1,278 @@
1
+ /* ============================================
2
+ COLORPICKER COMPONENT
3
+ Swatch trigger + saturation/hue/alpha panel
4
+
5
+ Colour-mixing constants: the white/black
6
+ gradient overlays and the hue spectrum are
7
+ colour-space physics, not theme decisions —
8
+ they are written as hsl() literals because no
9
+ semantic token can represent them. Everything
10
+ themeable uses tokens as usual.
11
+
12
+ ds-allow-file(color): colour-space physics —
13
+ the gradients and spectrum cannot be theme
14
+ tokens.
15
+ ============================================ */
16
+
17
+ /* Base */
18
+
19
+ .ds-colorpicker {
20
+ position: relative;
21
+ /* Field owns no layout — the root class stacks label, trigger and helper */
22
+ display: inline-flex;
23
+ flex-direction: column;
24
+ align-items: flex-start;
25
+ gap: var(--gap-sm);
26
+ }
27
+
28
+ /* Trigger */
29
+
30
+ .ds-colorpicker__trigger {
31
+ display: inline-flex;
32
+ align-items: center;
33
+ gap: var(--gap-sm);
34
+ padding: var(--padding-xxs);
35
+ border: var(--border-xs) solid var(--color-input-border-primary);
36
+ border-radius: var(--radius-md);
37
+ background-color: transparent;
38
+ cursor: pointer;
39
+ transition: border-color var(--motion-duration-base) var(--motion-ease-standard);
40
+ }
41
+
42
+ .ds-colorpicker__trigger:hover:not(:disabled) {
43
+ border-color: var(--color-input-border-hover);
44
+ }
45
+
46
+ .ds-colorpicker__trigger:focus-visible {
47
+ outline: none;
48
+ border-color: var(--color-input-border-selected);
49
+ }
50
+
51
+ .ds-colorpicker--open .ds-colorpicker__trigger {
52
+ border-color: var(--color-input-border-selected);
53
+ }
54
+
55
+ .ds-colorpicker__trigger-swatch {
56
+ width: 24px;
57
+ height: 24px;
58
+ border-radius: var(--radius-sm);
59
+ /* Checkerboard shows through translucent colours */
60
+ background-color: var(--color-bg-page-primary);
61
+ background-image:
62
+ linear-gradient(var(--ds-cp-color), var(--ds-cp-color)),
63
+ repeating-conic-gradient(var(--color-bg-container-tertiary) 0% 25%, transparent 0% 50%);
64
+ background-size: 100% 100%, 8px 8px;
65
+ box-shadow: inset 0 0 0 var(--border-xs) var(--color-bg-container-border);
66
+ flex-shrink: 0;
67
+ }
68
+
69
+ .ds-colorpicker__trigger-text {
70
+ font-family: var(--font-paragraph-sm-family);
71
+ font-size: var(--font-paragraph-sm-size);
72
+ line-height: var(--font-paragraph-sm-line-height);
73
+ color: var(--color-text-primary);
74
+ font-variant-numeric: tabular-nums;
75
+ padding-right: var(--padding-xxs);
76
+ }
77
+
78
+ /* Sizes */
79
+
80
+ .ds-colorpicker--compact .ds-colorpicker__trigger-swatch {
81
+ width: 20px;
82
+ height: 20px;
83
+ }
84
+
85
+ /* Panel */
86
+
87
+ .ds-colorpicker__panel {
88
+ position: absolute;
89
+ top: 100%;
90
+ left: 0;
91
+ margin-top: var(--gap-xxs);
92
+ width: 240px;
93
+ padding: var(--padding-sm-md);
94
+ display: flex;
95
+ flex-direction: column;
96
+ gap: var(--gap-sm-md);
97
+ background-color: var(--color-bg-page-primary);
98
+ border: var(--border-xs) solid var(--color-input-border-primary);
99
+ border-radius: var(--radius-md);
100
+ box-shadow: var(--shadow-floating);
101
+ z-index: 10;
102
+ }
103
+
104
+ /* Saturation / brightness area */
105
+
106
+ .ds-colorpicker__saturation {
107
+ position: relative;
108
+ height: 150px;
109
+ border-radius: var(--radius-sm);
110
+ cursor: crosshair;
111
+ touch-action: none;
112
+ background:
113
+ linear-gradient(to top, hsl(0 0% 0%), transparent),
114
+ linear-gradient(to right, hsl(0 0% 100%), transparent),
115
+ hsl(var(--ds-cp-hue) 100% 50%);
116
+ }
117
+
118
+ .ds-colorpicker__saturation:focus-visible {
119
+ outline: var(--border-md) solid var(--color-input-border-selected);
120
+ outline-offset: var(--border-md);
121
+ }
122
+
123
+ .ds-colorpicker__handle {
124
+ position: absolute;
125
+ left: var(--ds-cp-x);
126
+ top: var(--ds-cp-y);
127
+ transform: translate(-50%, -50%);
128
+ width: 14px;
129
+ height: 14px;
130
+ border-radius: var(--radius-full);
131
+ border: var(--border-md) solid hsl(0 0% 100%);
132
+ box-shadow: var(--shadow-floating);
133
+ pointer-events: none;
134
+ }
135
+
136
+ /* Sliders + preview */
137
+
138
+ .ds-colorpicker__controls {
139
+ display: flex;
140
+ align-items: center;
141
+ gap: var(--gap-sm-md);
142
+ }
143
+
144
+ .ds-colorpicker__sliders {
145
+ flex: 1;
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: var(--gap-sm);
149
+ min-width: 0;
150
+ }
151
+
152
+ .ds-colorpicker__hue,
153
+ .ds-colorpicker__alpha {
154
+ appearance: none;
155
+ -webkit-appearance: none;
156
+ width: 100%;
157
+ height: 12px;
158
+ border-radius: var(--radius-full);
159
+ outline: none;
160
+ cursor: pointer;
161
+ }
162
+
163
+ .ds-colorpicker__hue {
164
+ background: linear-gradient(
165
+ to right,
166
+ hsl(0 100% 50%),
167
+ hsl(60 100% 50%),
168
+ hsl(120 100% 50%),
169
+ hsl(180 100% 50%),
170
+ hsl(240 100% 50%),
171
+ hsl(300 100% 50%),
172
+ hsl(360 100% 50%)
173
+ );
174
+ }
175
+
176
+ .ds-colorpicker__alpha {
177
+ background-color: var(--color-bg-page-primary);
178
+ background-image:
179
+ linear-gradient(to right, transparent, var(--ds-cp-solid)),
180
+ repeating-conic-gradient(var(--color-bg-container-tertiary) 0% 25%, transparent 0% 50%);
181
+ background-size: 100% 100%, 8px 8px;
182
+ }
183
+
184
+ .ds-colorpicker__hue::-webkit-slider-thumb,
185
+ .ds-colorpicker__alpha::-webkit-slider-thumb {
186
+ appearance: none;
187
+ -webkit-appearance: none;
188
+ width: 16px;
189
+ height: 16px;
190
+ border-radius: var(--radius-full);
191
+ background: hsl(0 0% 100%);
192
+ border: var(--border-xs) solid var(--color-bg-container-border);
193
+ box-shadow: var(--shadow-floating);
194
+ }
195
+
196
+ .ds-colorpicker__hue::-moz-range-thumb,
197
+ .ds-colorpicker__alpha::-moz-range-thumb {
198
+ width: 16px;
199
+ height: 16px;
200
+ border-radius: var(--radius-full);
201
+ background: hsl(0 0% 100%);
202
+ border: var(--border-xs) solid var(--color-bg-container-border);
203
+ box-shadow: var(--shadow-floating);
204
+ }
205
+
206
+ .ds-colorpicker__hue:focus-visible,
207
+ .ds-colorpicker__alpha:focus-visible {
208
+ outline: var(--border-md) solid var(--color-input-border-selected);
209
+ outline-offset: var(--border-md);
210
+ }
211
+
212
+ .ds-colorpicker__preview {
213
+ width: 32px;
214
+ height: 32px;
215
+ border-radius: var(--radius-sm);
216
+ background-color: var(--color-bg-page-primary);
217
+ background-image:
218
+ linear-gradient(var(--ds-cp-color), var(--ds-cp-color)),
219
+ repeating-conic-gradient(var(--color-bg-container-tertiary) 0% 25%, transparent 0% 50%);
220
+ background-size: 100% 100%, 8px 8px;
221
+ box-shadow: inset 0 0 0 var(--border-xs) var(--color-bg-container-border);
222
+ flex-shrink: 0;
223
+ }
224
+
225
+ /* Hex + alpha inputs */
226
+
227
+ .ds-colorpicker__inputs {
228
+ display: flex;
229
+ align-items: center;
230
+ gap: var(--gap-sm);
231
+ padding: var(--padding-xxs) var(--padding-sm);
232
+ border: var(--border-xs) solid var(--color-input-border-primary);
233
+ border-radius: var(--radius-md);
234
+ transition: border-color var(--motion-duration-base) var(--motion-ease-standard);
235
+ }
236
+
237
+ .ds-colorpicker__inputs:focus-within {
238
+ border-color: var(--color-input-border-selected);
239
+ }
240
+
241
+ .ds-colorpicker__hash {
242
+ font-family: var(--font-paragraph-sm-family);
243
+ font-size: var(--font-paragraph-sm-size);
244
+ color: var(--color-text-tertiary);
245
+ }
246
+
247
+ .ds-colorpicker__hex-field {
248
+ flex: 1;
249
+ min-width: 0;
250
+ border: none;
251
+ background: transparent;
252
+ outline: none;
253
+ padding: 0;
254
+ font-family: var(--font-paragraph-sm-family);
255
+ font-size: var(--font-paragraph-sm-size);
256
+ line-height: var(--font-paragraph-sm-line-height);
257
+ color: var(--color-text-primary);
258
+ text-transform: uppercase;
259
+ font-variant-numeric: tabular-nums;
260
+ }
261
+
262
+ .ds-colorpicker__alpha-value {
263
+ font-family: var(--font-paragraph-sm-family);
264
+ font-size: var(--font-paragraph-sm-size);
265
+ color: var(--color-text-secondary);
266
+ font-variant-numeric: tabular-nums;
267
+ }
268
+
269
+ /* States */
270
+
271
+ .ds-colorpicker--disabled .ds-colorpicker__trigger {
272
+ opacity: 0.4;
273
+ cursor: not-allowed;
274
+ }
275
+
276
+ .ds-colorpicker--error .ds-colorpicker__trigger {
277
+ border-color: var(--color-status-error-border);
278
+ }
@@ -0,0 +1,50 @@
1
+ import { default as React } from 'react';
2
+ /** Props owned by ColorPicker itself — everything else falls through to the trigger <button>. */
3
+ type ColorPickerOwnProps = {
4
+ /** Field label text */
5
+ label?: string;
6
+ /** Current colour as a hex string — 3, 6 or 8 digit, with or without `#` */
7
+ value?: string;
8
+ /** Initial colour for uncontrolled use */
9
+ defaultValue?: string;
10
+ /**
11
+ * Convenience callback receiving the colour as an uppercase hex string
12
+ * (`#RRGGBB`, or `#RRGGBBAA` when `showAlpha` and alpha < 100%). Fires
13
+ * live while dragging.
14
+ */
15
+ onValueChange?: (value: string) => void;
16
+ /** Show the current hex value as text inside the trigger */
17
+ showText?: boolean;
18
+ /** Add an alpha (opacity) slider and emit 8-digit hex when alpha < 100% */
19
+ showAlpha?: boolean;
20
+ /** Component size */
21
+ size?: 'default' | 'compact';
22
+ /** Whether the picker is disabled */
23
+ disabled?: boolean;
24
+ /** Whether the field is required */
25
+ required?: boolean;
26
+ /** Error state */
27
+ error?: boolean;
28
+ /** Helper or error message */
29
+ helperText?: string;
30
+ /**
31
+ * When set, a hidden `<input type="hidden">` carries the current hex value
32
+ * under this name so the picker participates in native form submission.
33
+ */
34
+ name?: string;
35
+ /** Additional CSS classes — applied to the wrapper, not the trigger */
36
+ className?: string;
37
+ };
38
+ export interface ColorPickerProps extends ColorPickerOwnProps, Omit<React.ComponentPropsWithoutRef<'button'>, keyof ColorPickerOwnProps | 'type'> {
39
+ }
40
+ /**
41
+ * Colour picker: a swatch trigger that opens a panel with a
42
+ * saturation/brightness area, hue slider, optional alpha slider and a hex
43
+ * field. Works controlled (`value` + `onValueChange`) or uncontrolled
44
+ * (`defaultValue`).
45
+ *
46
+ * Forwards a ref to the trigger `<button>` and spreads unrecognised props
47
+ * onto it.
48
+ */
49
+ export declare const ColorPicker: React.ForwardRefExoticComponent<ColorPickerProps & React.RefAttributes<HTMLButtonElement>>;
50
+ export {};