@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
@@ -0,0 +1,19 @@
1
+ import { default as React } from 'react';
2
+ /** Props owned by Kbd itself — everything else falls through to the <kbd> element. */
3
+ type KbdOwnProps = {
4
+ /** Key size */
5
+ size?: 'default' | 'compact';
6
+ /** Additional CSS classes */
7
+ className?: string;
8
+ /** The key legend — e.g. ⌘, K, Esc, Shift */
9
+ children?: React.ReactNode;
10
+ };
11
+ export interface KbdProps extends KbdOwnProps, Omit<React.ComponentPropsWithoutRef<'kbd'>, keyof KbdOwnProps> {
12
+ }
13
+ /**
14
+ * Kbd renders a single keyboard key as a keycap, for shortcut hints in
15
+ * menus and docs prose. Compose several for a chord: `<Kbd>⌘</Kbd><Kbd>K</Kbd>`.
16
+ * Purely presentational — safe to render from a Server Component.
17
+ */
18
+ export declare const Kbd: React.ForwardRefExoticComponent<KbdProps & React.RefAttributes<HTMLElement>>;
19
+ export {};
@@ -0,0 +1,14 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import "./Kbd.css";
4
+ const Kbd = React.forwardRef(
5
+ ({ size = "default", className = "", children, ...rest }, ref) => {
6
+ const baseClass = "ds-kbd";
7
+ const classes = [baseClass, `${baseClass}--${size}`, className].filter(Boolean).join(" ");
8
+ return /* @__PURE__ */ jsx("kbd", { ...rest, ref, className: classes, children });
9
+ }
10
+ );
11
+ Kbd.displayName = "Kbd";
12
+ export {
13
+ Kbd
14
+ };
@@ -19,10 +19,4 @@ export interface PopoverProps {
19
19
  /** Additional CSS classes for the popover panel */
20
20
  className?: string;
21
21
  }
22
- /**
23
- * Popover component for contextual overlays.
24
- * Wraps a trigger element and displays content in a
25
- * positioned panel. Supports click and hover triggers
26
- * with automatic outside-click dismissal.
27
- */
28
22
  export declare const Popover: ({ children, content, size, position, trigger, open: controlledOpen, onOpenChange, ariaLabel, className, }: PopoverProps) => React.JSX.Element;
@@ -1,6 +1,8 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
- import { useState, useRef, useCallback, useEffect } from "react";
2
+ import React, { useState, useRef, useCallback, useEffect } from "react";
3
3
  import "./Popover.css";
4
+ const INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["button", "a", "input", "select", "textarea", "summary"]);
5
+ const canCarryPopoverSemantics = (el) => typeof el.type === "string" ? INTERACTIVE_TAGS.has(el.type) : true;
4
6
  const Popover = ({
5
7
  children,
6
8
  content,
@@ -73,26 +75,30 @@ const Popover = ({
73
75
  onMouseEnter: handleMouseEnter,
74
76
  onMouseLeave: handleMouseLeave,
75
77
  children: [
76
- /* @__PURE__ */ jsx(
77
- "div",
78
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__trigger`, onClick: handleClick, children: React.isValidElement(children) && canCarryPopoverSemantics(children) ? React.cloneElement(children, {
79
+ "aria-expanded": isOpen,
80
+ "aria-haspopup": "dialog",
81
+ ...ariaLabel ? { "aria-label": ariaLabel } : {}
82
+ }) : trigger === "click" ? /* @__PURE__ */ jsx(
83
+ "button",
78
84
  {
79
- className: `${baseClass}__trigger`,
80
- onClick: handleClick,
81
- role: trigger === "click" ? "button" : void 0,
82
- tabIndex: trigger === "click" ? 0 : void 0,
85
+ type: "button",
83
86
  "aria-expanded": isOpen,
84
- "aria-label": ariaLabel,
85
87
  "aria-haspopup": "dialog",
86
- onKeyDown: (e) => {
87
- if (trigger === "click" && (e.key === "Enter" || e.key === " ")) {
88
- e.preventDefault();
89
- setOpen(!isOpen);
90
- }
91
- },
88
+ "aria-label": ariaLabel,
92
89
  children
93
90
  }
94
- ),
95
- /* @__PURE__ */ jsx("div", { className: panelClasses, role: "dialog", "aria-hidden": !isOpen, children: content })
91
+ ) : children }),
92
+ /* @__PURE__ */ jsx(
93
+ "div",
94
+ {
95
+ className: panelClasses,
96
+ role: "dialog",
97
+ "aria-label": ariaLabel || "Popover",
98
+ "aria-hidden": !isOpen,
99
+ children: content
100
+ }
101
+ )
96
102
  ]
97
103
  }
98
104
  );
@@ -14,7 +14,7 @@ const ProgressBar = ({
14
14
  `${baseClass}--${size}`,
15
15
  className
16
16
  ].filter(Boolean).join(" ");
17
- return /* @__PURE__ */ jsxs("div", { className: classes, role: "progressbar", "aria-valuenow": clamped, "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": ariaLabel, children: [
17
+ return /* @__PURE__ */ jsxs("div", { className: classes, role: "progressbar", "aria-valuenow": clamped, "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": ariaLabel || "Progress", children: [
18
18
  /* @__PURE__ */ jsx("div", { className: `${baseClass}__track`, children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__fill`, style: { width: `${clamped}%` } }) }),
19
19
  showLabel && /* @__PURE__ */ jsxs("span", { className: `${baseClass}__label`, children: [
20
20
  clamped,
@@ -232,8 +232,12 @@
232
232
  .ds-selection-card__toggle-icon {
233
233
  /* Intentionally off the --icon-size-* scale — same reason as ToggleSwitch:
234
234
  this glyph lives inside the 20x20 toggle thumb, and the smallest scale
235
- step (20px) would fill it completely. */
236
- font-size: 14px;
235
+ step (20px) would fill it completely. Sized via --icon-size, not
236
+ font-size, so the icon's box shrinks with the glyph (see ToggleSwitch).
237
+
238
+ ds-allow(icon-size): 14px glyph inside the 20px thumb — the smallest
239
+ scale step would fill it edge to edge. */
240
+ --icon-size: 14px;
237
241
  color: var(--color-action-primary-bg);
238
242
  }
239
243
 
@@ -22,6 +22,8 @@
22
22
  var(--color-bg-container-primary) 50%,
23
23
  transparent 100%
24
24
  );
25
+ /* ds-allow(motion): the shimmer needs a symmetric in-out curve and no
26
+ --motion-ease-* token provides one — the duration is tokenized. */
25
27
  animation: ds-skeleton-shimmer var(--motion-duration-loop-shimmer) ease-in-out infinite;
26
28
  }
27
29
 
@@ -59,6 +59,17 @@
59
59
  stroke: var(--color-text-secondary);
60
60
  }
61
61
 
62
+ /* Inherit — both circles draw in currentColor so the spinner matches the
63
+ text/icon colour of whatever control contains it (e.g. a loading Button) */
64
+ .ds-spinner--inherit .ds-spinner__track {
65
+ stroke: currentColor;
66
+ opacity: 0.25;
67
+ }
68
+
69
+ .ds-spinner--inherit .ds-spinner__arc {
70
+ stroke: currentColor;
71
+ }
72
+
62
73
  /* ============================================
63
74
  ANIMATION
64
75
  ============================================ */
@@ -1,8 +1,8 @@
1
1
  export interface SpinnerProps {
2
2
  /** Size of the spinner */
3
3
  size?: 'sm' | 'md' | 'lg';
4
- /** Visual variant */
5
- variant?: 'primary' | 'neutral';
4
+ /** Visual variant — `inherit` draws the spinner in `currentColor`, for use inside coloured controls */
5
+ variant?: 'primary' | 'neutral' | 'inherit';
6
6
  /** Accessible label */
7
7
  label?: string;
8
8
  /** Additional CSS classes */
@@ -0,0 +1,52 @@
1
+ /* ============================================
2
+ SWATCH COMPONENT
3
+ Clickable colour tile for palettes and pickers
4
+ ============================================ */
5
+
6
+ /* Base */
7
+
8
+ .ds-swatch {
9
+ width: 24px;
10
+ height: 24px;
11
+ padding: 0;
12
+ border: none;
13
+ border-radius: var(--radius-full);
14
+ background-color: var(--ds-swatch-color);
15
+ /* A hairline inset keeps very light colours visible on the white floor */
16
+ box-shadow: inset 0 0 0 var(--border-xs) var(--color-bg-container-border);
17
+ cursor: pointer;
18
+ flex-shrink: 0;
19
+ transition: box-shadow var(--motion-duration-base) var(--motion-ease-standard);
20
+ }
21
+
22
+ /* Sizes */
23
+
24
+ .ds-swatch--compact {
25
+ width: 20px;
26
+ height: 20px;
27
+ }
28
+
29
+ /* Shapes */
30
+
31
+ .ds-swatch--square {
32
+ border-radius: var(--radius-sm);
33
+ }
34
+
35
+ /* States */
36
+
37
+ .ds-swatch--selected {
38
+ box-shadow:
39
+ inset 0 0 0 var(--border-xs) var(--color-bg-container-border),
40
+ 0 0 0 var(--border-md) var(--color-bg-page-primary),
41
+ 0 0 0 calc(var(--border-md) * 2) var(--color-text-primary);
42
+ }
43
+
44
+ .ds-swatch:focus-visible {
45
+ outline: var(--border-md) solid var(--color-input-border-selected);
46
+ outline-offset: var(--border-md);
47
+ }
48
+
49
+ .ds-swatch--disabled {
50
+ opacity: 0.4;
51
+ cursor: not-allowed;
52
+ }
@@ -0,0 +1,34 @@
1
+ import { default as React } from 'react';
2
+ /** Props owned by Swatch itself — everything else falls through to the <button>. */
3
+ type SwatchOwnProps = {
4
+ /** The colour this swatch shows — any valid CSS colour, typically a hex value */
5
+ value: string;
6
+ /**
7
+ * Accessible name for the swatch. Defaults to the colour value, which is
8
+ * meaningful but terse — prefer a human name ("Teal 07") when you have one.
9
+ */
10
+ label?: string;
11
+ /** Selected state — renders the selection ring and sets aria-pressed */
12
+ selected?: boolean;
13
+ /** Swatch size */
14
+ size?: 'default' | 'compact';
15
+ /** Corner treatment — circle matches the site's preset grids; square suits picker triggers */
16
+ shape?: 'circle' | 'square';
17
+ /** Whether the swatch is disabled */
18
+ disabled?: boolean;
19
+ /** Additional CSS classes */
20
+ className?: string;
21
+ };
22
+ export interface SwatchProps extends SwatchOwnProps, Omit<React.ComponentPropsWithoutRef<'button'>, keyof SwatchOwnProps | 'type'> {
23
+ }
24
+ /**
25
+ * A clickable colour tile — the building block for preset palettes and
26
+ * picker triggers. Renders a `<button>` whose background is the `value`
27
+ * colour; `selected` draws the theme-aware selection ring.
28
+ *
29
+ * Purely presentational (no hooks), so it can be rendered from a Server
30
+ * Component; interactivity arrives through the native `onClick` you pass.
31
+ * Forwards a ref to the `<button>` and spreads unrecognised props onto it.
32
+ */
33
+ export declare const Swatch: React.ForwardRefExoticComponent<SwatchProps & React.RefAttributes<HTMLButtonElement>>;
34
+ export {};
@@ -0,0 +1,44 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import "./Swatch.css";
4
+ const Swatch = React.forwardRef(
5
+ ({
6
+ value,
7
+ label,
8
+ selected = false,
9
+ size = "default",
10
+ shape = "circle",
11
+ disabled = false,
12
+ className = "",
13
+ style,
14
+ ...rest
15
+ }, ref) => {
16
+ const baseClass = "ds-swatch";
17
+ const classes = [
18
+ baseClass,
19
+ size === "compact" ? `${baseClass}--compact` : "",
20
+ shape === "square" ? `${baseClass}--square` : "",
21
+ selected ? `${baseClass}--selected` : "",
22
+ disabled ? `${baseClass}--disabled` : "",
23
+ className
24
+ ].filter(Boolean).join(" ");
25
+ const swatchStyle = { ...style, "--ds-swatch-color": value };
26
+ return /* @__PURE__ */ jsx(
27
+ "button",
28
+ {
29
+ ...rest,
30
+ ref,
31
+ type: "button",
32
+ className: classes,
33
+ style: swatchStyle,
34
+ disabled,
35
+ "aria-pressed": selected,
36
+ "aria-label": rest["aria-label"] ?? label ?? value
37
+ }
38
+ );
39
+ }
40
+ );
41
+ Swatch.displayName = "Swatch";
42
+ export {
43
+ Swatch
44
+ };
@@ -184,3 +184,22 @@
184
184
  line-height: var(--font-paragraph-sm-line-height);
185
185
  letter-spacing: var(--font-paragraph-sm-letter-spacing);
186
186
  }
187
+
188
+ /* ============================================
189
+ SCREEN-READER-ONLY
190
+ Fallback text for header cells with no visible
191
+ label (checkbox / actions columns) — an empty
192
+ <th> is unannounced.
193
+ ============================================ */
194
+
195
+ .ds-table__sr-only {
196
+ position: absolute;
197
+ width: 1px;
198
+ height: 1px;
199
+ padding: 0;
200
+ margin: -1px;
201
+ overflow: hidden;
202
+ clip: rect(0, 0, 0, 0);
203
+ white-space: nowrap;
204
+ border: 0;
205
+ }
@@ -38,7 +38,7 @@ const Table = React.forwardRef(
38
38
  width: col.width,
39
39
  textAlign: col.align || "left"
40
40
  },
41
- children: col.header
41
+ children: col.header || /* @__PURE__ */ jsx("span", { className: `${baseClass}__sr-only`, children: col.key })
42
42
  },
43
43
  col.key
44
44
  )) }) }),
@@ -14,19 +14,6 @@
14
14
  LABEL
15
15
  ============================================ */
16
16
 
17
- .ds-textarea__label {
18
- font-family: var(--font-paragraph-em-family);
19
- font-size: var(--font-paragraph-em-size);
20
- font-weight: var(--font-paragraph-em-weight);
21
- line-height: var(--font-paragraph-em-line-height);
22
- letter-spacing: var(--font-paragraph-em-letter-spacing);
23
- color: var(--color-text-primary);
24
- }
25
-
26
- .ds-textarea__required {
27
- color: var(--color-core-accent-coral);
28
- }
29
-
30
17
  /* ============================================
31
18
  FIELD
32
19
  ============================================ */
@@ -69,24 +56,6 @@
69
56
  FOOTER (helper + counter)
70
57
  ============================================ */
71
58
 
72
- .ds-textarea__footer {
73
- display: flex;
74
- align-items: flex-start;
75
- justify-content: space-between;
76
- gap: var(--gap-sm);
77
- }
78
-
79
- .ds-textarea__helper {
80
- font-family: var(--font-paragraph-sm-family);
81
- font-size: var(--font-paragraph-sm-size);
82
- font-weight: var(--font-paragraph-sm-weight);
83
- line-height: var(--font-paragraph-sm-line-height);
84
- letter-spacing: var(--font-paragraph-sm-letter-spacing);
85
- color: var(--color-text-tertiary);
86
- margin: 0;
87
- flex: 1;
88
- }
89
-
90
59
  .ds-textarea__counter {
91
60
  font-family: var(--font-paragraph-sm-family);
92
61
  font-size: var(--font-paragraph-sm-size);
@@ -108,10 +77,6 @@
108
77
  border-color: var(--color-status-error-border);
109
78
  }
110
79
 
111
- .ds-textarea--error .ds-textarea__helper {
112
- color: var(--color-status-error-border);
113
- }
114
-
115
80
  .ds-textarea--error .ds-textarea__counter {
116
81
  color: var(--color-status-error-border);
117
82
  }
@@ -120,10 +85,6 @@
120
85
  DISABLED STATE
121
86
  ============================================ */
122
87
 
123
- .ds-textarea--disabled .ds-textarea__label {
124
- color: var(--color-input-text-disabled);
125
- }
126
-
127
88
  .ds-textarea--disabled .ds-textarea__field {
128
89
  background-color: var(--color-input-bg-disabled);
129
90
  border-color: var(--color-input-border-disabled);
@@ -146,7 +107,3 @@
146
107
  letter-spacing: var(--font-paragraph-sm-letter-spacing);
147
108
  }
148
109
 
149
- .ds-textarea--compact .ds-textarea__label {
150
- font-size: var(--font-paragraph-sm-size);
151
- line-height: var(--font-paragraph-sm-line-height);
152
- }
@@ -1,5 +1,6 @@
1
- import { jsxs, jsx } from "react/jsx-runtime";
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import React from "react";
3
+ import { Field } from "../Field/Field.js";
3
4
  import "./Textarea.css";
4
5
  const Textarea = React.forwardRef(
5
6
  ({
@@ -37,44 +38,45 @@ const Textarea = React.forwardRef(
37
38
  };
38
39
  const inputId = id || name || label?.toLowerCase().replace(/\s+/g, "-");
39
40
  const charCount = value?.length || 0;
40
- return /* @__PURE__ */ jsxs("div", { className: classes, children: [
41
- label && /* @__PURE__ */ jsxs("label", { className: `${baseClass}__label`, htmlFor: inputId, children: [
41
+ return /* @__PURE__ */ jsx(
42
+ Field,
43
+ {
44
+ className: classes,
42
45
  label,
43
- required && /* @__PURE__ */ jsxs("span", { className: `${baseClass}__required`, "aria-hidden": "true", children: [
44
- " ",
45
- "*"
46
- ] })
47
- ] }),
48
- /* @__PURE__ */ jsx(
49
- "textarea",
50
- {
51
- ...rest,
52
- ref,
53
- className: `${baseClass}__field`,
54
- id: inputId,
55
- name,
56
- value,
57
- placeholder,
58
- rows,
59
- disabled,
60
- required,
61
- maxLength,
62
- "aria-label": ariaLabel || rest["aria-label"] || label,
63
- "aria-invalid": error,
64
- "aria-describedby": helperText ? `${inputId}-helper` : rest["aria-describedby"],
65
- onChange: handleChange,
66
- style: { resize, ...style }
67
- }
68
- ),
69
- /* @__PURE__ */ jsxs("div", { className: `${baseClass}__footer`, children: [
70
- helperText && /* @__PURE__ */ jsx("p", { className: `${baseClass}__helper`, id: `${inputId}-helper`, children: helperText }),
71
- maxLength && /* @__PURE__ */ jsxs("span", { className: `${baseClass}__counter`, "aria-live": "polite", "aria-atomic": "true", children: [
46
+ helperText,
47
+ error,
48
+ required,
49
+ disabled,
50
+ size,
51
+ id: inputId,
52
+ aside: maxLength ? /* @__PURE__ */ jsxs("span", { className: `${baseClass}__counter`, "aria-live": "polite", "aria-atomic": "true", children: [
72
53
  charCount,
73
54
  "/",
74
55
  maxLength
75
- ] })
76
- ] })
77
- ] });
56
+ ] }) : void 0,
57
+ children: /* @__PURE__ */ jsx(
58
+ "textarea",
59
+ {
60
+ ...rest,
61
+ ref,
62
+ className: `${baseClass}__field`,
63
+ id: inputId,
64
+ name,
65
+ value,
66
+ placeholder,
67
+ rows,
68
+ disabled,
69
+ required,
70
+ maxLength,
71
+ "aria-label": ariaLabel || rest["aria-label"] || label,
72
+ "aria-invalid": error,
73
+ "aria-describedby": helperText ? `${inputId}-helper` : rest["aria-describedby"],
74
+ onChange: handleChange,
75
+ style: { resize, ...style }
76
+ }
77
+ )
78
+ }
79
+ );
78
80
  }
79
81
  );
80
82
  Textarea.displayName = "Textarea";
@@ -59,7 +59,10 @@
59
59
  Set via --icon-size (not font-size): .material-symbols-rounded derives
60
60
  font-size, width, and height from it together. Overriding font-size
61
61
  alone leaves the box at the 24px default, so the glyph hangs off-centre
62
- inside the thumb. */
62
+ inside the thumb.
63
+
64
+ ds-allow(icon-size): 14px glyph inside the 20px thumb — the smallest
65
+ scale step would fill it edge to edge. */
63
66
  --icon-size: 14px;
64
67
  color: var(--color-action-primary-bg);
65
68
  }
@@ -123,7 +126,9 @@
123
126
 
124
127
  .ds-toggle-switch--compact .ds-toggle-switch__thumb-icon {
125
128
  /* Off-scale for the same reason as the default size — the compact thumb
126
- is only 16x16. */
129
+ is only 16x16.
130
+
131
+ ds-allow(icon-size): 12px glyph inside the 16px compact thumb. */
127
132
  --icon-size: 12px;
128
133
  }
129
134
 
@@ -1,19 +1,56 @@
1
1
  /**
2
- * COMPONENT REGISTRY — the single source of truth for the public
3
- * component count.
2
+ * COMPONENT REGISTRY — the single source of truth for the public component
3
+ * list, count, and per-component metadata.
4
+ *
5
+ * The data lives in registry.json. Each entry in `components` carries:
6
+ *
7
+ * name the folder under src/components (also the exported name)
8
+ * label the display name — "Navigation" for the Nav folder
9
+ * slug the website route segment — stored, not derived, so the
10
+ * Nav → navigation exception is plain data rather than a
11
+ * special case duplicated across validators
12
+ * description one line, used for the page title, the sidebar search index,
13
+ * and anywhere the component is summarised
14
+ * category one of `componentCategories`
15
+ * client whether the component declares 'use client' — i.e. whether it
16
+ * uses hooks or defines handlers. Presentational components are
17
+ * deliberately false so consumers can render them from a React
18
+ * Server Component.
4
19
  *
5
- * The data lives in registry.json: `components` is the official list of
6
- * public components (one entry per folder in src/components), and
7
20
  * `docOnlyHelpers` names the internal documentation helpers that are
8
21
  * deliberately excluded from the public count.
9
22
  *
10
- * scripts/validate-component-registry.mjs compares the registry against
11
- * the filesystem on every build (library, Storybook, and website), so
12
- * the registry cannot silently drift from reality.
23
+ * scripts/validate-component-registry.mjs compares the registry against the
24
+ * filesystem on every build (library, Storybook, and website), so it cannot
25
+ * silently drift from reality.
26
+ *
27
+ * Anywhere a component count is displayed — the Storybook overview, the
28
+ * website mega-nav, case studies — import COMPONENT_COUNT from here. Never
29
+ * hardcode the number.
30
+ */
31
+ export interface ComponentMeta {
32
+ /** Folder under src/components, and the exported component name */
33
+ name: string;
34
+ /** Display name — may differ from `name` (Nav → "Navigation") */
35
+ label: string;
36
+ /** Website route segment under /components */
37
+ slug: string;
38
+ /** One-line summary, used for page titles and search */
39
+ description: string;
40
+ /** Grouping — one of `componentCategories` */
41
+ category: string;
42
+ /** Whether the component declares 'use client' */
43
+ client: boolean;
44
+ }
45
+ /** Full metadata for every public component, alphabetical by name. */
46
+ export declare const componentMetadata: readonly ComponentMeta[];
47
+ /** The categories a component may belong to. */
48
+ export declare const componentCategories: readonly string[];
49
+ /**
50
+ * The official public component list, names only.
13
51
  *
14
- * Anywhere a component count is displayed the Storybook overview,
15
- * the website mega-nav, case studies — import COMPONENT_COUNT from
16
- * here. Never hardcode the number.
52
+ * Unchanged in shape from before the metadata migration, so every existing
53
+ * consumer keeps working without an edit.
17
54
  */
18
55
  export declare const componentRegistry: readonly string[];
19
56
  /** The official public component count. */
@@ -1,7 +1,11 @@
1
1
  import registry from "./registry.json.js";
2
- const componentRegistry = registry.components;
2
+ const componentMetadata = registry.components;
3
+ const componentCategories = registry.categories;
4
+ const componentRegistry = registry.components.map((c) => c.name);
3
5
  const COMPONENT_COUNT = registry.components.length;
4
6
  export {
5
7
  COMPONENT_COUNT,
8
+ componentCategories,
9
+ componentMetadata,
6
10
  componentRegistry
7
11
  };