@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.
Files changed (91) hide show
  1. package/README.md +13 -9
  2. package/components/Accordion/Accordion.js +0 -1
  3. package/components/Avatar/Avatar.js +1 -0
  4. package/components/Breadcrumb/Breadcrumb.css +9 -1
  5. package/components/Breadcrumb/Breadcrumb.d.ts +7 -1
  6. package/components/Breadcrumb/Breadcrumb.js +3 -2
  7. package/components/Button/Button.css +18 -0
  8. package/components/Button/Button.d.ts +38 -16
  9. package/components/Button/Button.js +76 -58
  10. package/components/Card/Card.css +6 -0
  11. package/components/Card/Card.d.ts +17 -5
  12. package/components/Card/Card.js +101 -80
  13. package/components/Checkbox/Checkbox.d.ts +24 -12
  14. package/components/Checkbox/Checkbox.js +99 -85
  15. package/components/CircularButton/CircularButton.css +11 -0
  16. package/components/CircularButton/CircularButton.d.ts +7 -1
  17. package/components/CircularButton/CircularButton.js +15 -4
  18. package/components/CodeBlock/CodeBlock.js +10 -1
  19. package/components/ColorPicker/ColorPicker.css +278 -0
  20. package/components/ColorPicker/ColorPicker.d.ts +50 -0
  21. package/components/ColorPicker/ColorPicker.js +338 -0
  22. package/components/Combobox/Combobox.css +0 -36
  23. package/components/Combobox/Combobox.d.ts +19 -7
  24. package/components/Combobox/Combobox.js +106 -89
  25. package/components/CommandPalette/CommandPalette.css +1 -15
  26. package/components/CommandPalette/CommandPalette.js +6 -5
  27. package/components/ContextMenu/ContextMenu.css +262 -0
  28. package/components/ContextMenu/ContextMenu.d.ts +26 -0
  29. package/components/ContextMenu/ContextMenu.js +350 -0
  30. package/components/DateInput/DateInput.css +0 -36
  31. package/components/DateInput/DateInput.d.ts +18 -21
  32. package/components/DateInput/DateInput.js +76 -64
  33. package/components/DatePicker/DatePicker.d.ts +9 -2
  34. package/components/DatePicker/DatePicker.js +137 -128
  35. package/components/Dialog/Dialog.d.ts +15 -4
  36. package/components/Dialog/Dialog.js +136 -118
  37. package/components/Drawer/Drawer.d.ts +15 -4
  38. package/components/Drawer/Drawer.js +138 -119
  39. package/components/Dropdown/Dropdown.css +0 -36
  40. package/components/Dropdown/Dropdown.d.ts +24 -8
  41. package/components/Dropdown/Dropdown.js +219 -169
  42. package/components/DropdownMenu/DropdownMenu.js +10 -7
  43. package/components/Field/Field.css +80 -0
  44. package/components/Field/Field.d.ts +50 -0
  45. package/components/Field/Field.js +58 -0
  46. package/components/Field/FieldContext.d.ts +42 -0
  47. package/components/Field/FieldContext.js +7 -0
  48. package/components/FileInput/FileInput.css +0 -36
  49. package/components/FileInput/FileInput.d.ts +13 -17
  50. package/components/FileInput/FileInput.js +168 -141
  51. package/components/Input/Input.css +0 -43
  52. package/components/Input/Input.d.ts +23 -22
  53. package/components/Input/Input.js +93 -66
  54. package/components/Kbd/Kbd.css +28 -0
  55. package/components/Kbd/Kbd.d.ts +19 -0
  56. package/components/Kbd/Kbd.js +14 -0
  57. package/components/Popover/Popover.d.ts +0 -6
  58. package/components/Popover/Popover.js +22 -16
  59. package/components/ProgressBar/ProgressBar.js +1 -1
  60. package/components/RadioButton/RadioButton.d.ts +26 -13
  61. package/components/RadioButton/RadioButton.js +95 -70
  62. package/components/SelectionCard/SelectionCard.css +6 -2
  63. package/components/SelectionCard/SelectionCard.d.ts +10 -4
  64. package/components/SelectionCard/SelectionCard.js +72 -56
  65. package/components/Skeleton/Skeleton.css +2 -0
  66. package/components/Slider/Slider.d.ts +19 -10
  67. package/components/Slider/Slider.js +50 -40
  68. package/components/Spinner/Spinner.css +11 -0
  69. package/components/Spinner/Spinner.d.ts +2 -2
  70. package/components/Swatch/Swatch.css +52 -0
  71. package/components/Swatch/Swatch.d.ts +34 -0
  72. package/components/Swatch/Swatch.js +44 -0
  73. package/components/Table/Table.css +19 -0
  74. package/components/Table/Table.d.ts +10 -2
  75. package/components/Table/Table.js +58 -58
  76. package/components/Textarea/Textarea.css +0 -43
  77. package/components/Textarea/Textarea.d.ts +21 -22
  78. package/components/Textarea/Textarea.js +78 -67
  79. package/components/ToggleGroup/ToggleGroup.d.ts +18 -8
  80. package/components/ToggleGroup/ToggleGroup.js +61 -44
  81. package/components/ToggleSwitch/ToggleSwitch.css +7 -2
  82. package/components/ToggleSwitch/ToggleSwitch.d.ts +17 -9
  83. package/components/ToggleSwitch/ToggleSwitch.js +57 -44
  84. package/components/registry.d.ts +47 -10
  85. package/components/registry.js +5 -1
  86. package/components/registry.json +498 -56
  87. package/components/registry.json.d.ts +498 -56
  88. package/components/registry.json.js +4 -1
  89. package/index.d.ts +8 -1
  90. package/index.js +16 -1
  91. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import "react";
2
+ import React from "react";
3
3
  import "./SelectionCard.css";
4
4
  const CheckIcon = () => /* @__PURE__ */ jsx(
5
5
  "svg",
@@ -22,64 +22,80 @@ const CheckIcon = () => /* @__PURE__ */ jsx(
22
22
  )
23
23
  }
24
24
  );
25
- const SelectionCard = ({
26
- mode = "radio",
27
- options,
28
- value,
29
- onChange,
30
- name,
31
- className = ""
32
- }) => {
33
- const baseClass = "ds-selection-card";
34
- const selectedValues = Array.isArray(value) ? value : value !== void 0 ? [value] : [];
35
- const isSelected = (optionValue) => selectedValues.includes(optionValue);
36
- const handleSelect = (option) => {
37
- if (option.disabled || !onChange) return;
38
- if (mode === "radio") {
39
- onChange(option.value);
40
- } else {
41
- const newValues = isSelected(option.value) ? selectedValues.filter((v) => v !== option.value) : [...selectedValues, option.value];
42
- onChange(newValues);
43
- }
44
- };
45
- const handleKeyDown = (e, option) => {
46
- if (e.key === " " || e.key === "Enter") {
47
- e.preventDefault();
48
- handleSelect(option);
49
- }
50
- };
51
- const groupRole = mode === "radio" ? "radiogroup" : "group";
52
- const itemRole = mode === "radio" ? "radio" : mode === "toggle" ? "switch" : "checkbox";
53
- const groupClasses = [`${baseClass}-group`, className].filter(Boolean).join(" ");
54
- return /* @__PURE__ */ jsx("div", { className: groupClasses, role: groupRole, "aria-label": name, children: options.map((option) => {
55
- const selected = isSelected(option.value);
56
- const cardClasses = [
57
- baseClass,
58
- selected ? `${baseClass}--selected` : "",
59
- option.disabled ? `${baseClass}--disabled` : ""
60
- ].filter(Boolean).join(" ");
61
- return /* @__PURE__ */ jsxs(
25
+ const SelectionCard = React.forwardRef(
26
+ ({ mode = "radio", options, value, onValueChange, onChange, name, className = "", ...rest }, ref) => {
27
+ const baseClass = "ds-selection-card";
28
+ const selectedValues = Array.isArray(value) ? value : value !== void 0 ? [value] : [];
29
+ const isSelected = (optionValue) => selectedValues.includes(optionValue);
30
+ const handleSelect = (option) => {
31
+ if (option.disabled) return;
32
+ const next = mode === "radio" ? option.value : isSelected(option.value) ? selectedValues.filter((v) => v !== option.value) : [...selectedValues, option.value];
33
+ onValueChange?.(next);
34
+ onChange?.(next);
35
+ };
36
+ const handleKeyDown = (e, option) => {
37
+ if (e.key === " " || e.key === "Enter") {
38
+ e.preventDefault();
39
+ handleSelect(option);
40
+ }
41
+ };
42
+ const groupRole = mode === "radio" ? "radiogroup" : "group";
43
+ const itemRole = mode === "radio" ? "radio" : mode === "toggle" ? "switch" : "checkbox";
44
+ const groupClasses = [`${baseClass}-group`, className].filter(Boolean).join(" ");
45
+ return /* @__PURE__ */ jsx(
62
46
  "div",
63
47
  {
64
- className: cardClasses,
65
- role: itemRole,
66
- "aria-checked": selected,
67
- "aria-disabled": option.disabled,
68
- tabIndex: option.disabled ? -1 : 0,
69
- onClick: () => handleSelect(option),
70
- onKeyDown: (e) => handleKeyDown(e, option),
71
- children: [
72
- /* @__PURE__ */ jsxs("div", { className: `${baseClass}__content`, children: [
73
- /* @__PURE__ */ jsx("span", { className: `${baseClass}__label`, children: option.label }),
74
- option.description && /* @__PURE__ */ jsx("span", { className: `${baseClass}__description`, children: option.description })
75
- ] }),
76
- /* @__PURE__ */ jsx("div", { className: `${baseClass}__indicator`, children: mode === "radio" ? /* @__PURE__ */ jsx("div", { className: `${baseClass}__radio`, children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__radio-dot` }) }) : mode === "toggle" ? /* @__PURE__ */ jsx("div", { className: `${baseClass}__toggle ${selected ? "" : `${baseClass}__toggle--off`}`, children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__toggle-thumb`, children: /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded ds-selection-card__toggle-icon", "aria-hidden": "true", children: "check" }) }) }) : /* @__PURE__ */ jsx("div", { className: `${baseClass}__checkbox`, children: /* @__PURE__ */ jsx(CheckIcon, {}) }) })
77
- ]
78
- },
79
- option.value
48
+ ...rest,
49
+ ref,
50
+ className: groupClasses,
51
+ role: groupRole,
52
+ "aria-label": name || rest["aria-label"],
53
+ children: options.map((option) => {
54
+ const selected = isSelected(option.value);
55
+ const cardClasses = [
56
+ baseClass,
57
+ selected ? `${baseClass}--selected` : "",
58
+ option.disabled ? `${baseClass}--disabled` : ""
59
+ ].filter(Boolean).join(" ");
60
+ return /* @__PURE__ */ jsxs(
61
+ "div",
62
+ {
63
+ className: cardClasses,
64
+ role: itemRole,
65
+ "aria-checked": selected,
66
+ "aria-disabled": option.disabled,
67
+ tabIndex: option.disabled ? -1 : 0,
68
+ onClick: () => handleSelect(option),
69
+ onKeyDown: (e) => handleKeyDown(e, option),
70
+ children: [
71
+ /* @__PURE__ */ jsxs("div", { className: `${baseClass}__content`, children: [
72
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__label`, children: option.label }),
73
+ option.description && /* @__PURE__ */ jsx("span", { className: `${baseClass}__description`, children: option.description })
74
+ ] }),
75
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__indicator`, children: mode === "radio" ? /* @__PURE__ */ jsx("div", { className: `${baseClass}__radio`, children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__radio-dot` }) }) : mode === "toggle" ? /* @__PURE__ */ jsx(
76
+ "div",
77
+ {
78
+ className: `${baseClass}__toggle ${selected ? "" : `${baseClass}__toggle--off`}`,
79
+ children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__toggle-thumb`, children: /* @__PURE__ */ jsx(
80
+ "span",
81
+ {
82
+ className: "material-symbols-rounded ds-selection-card__toggle-icon",
83
+ "aria-hidden": "true",
84
+ children: "check"
85
+ }
86
+ ) })
87
+ }
88
+ ) : /* @__PURE__ */ jsx("div", { className: `${baseClass}__checkbox`, children: /* @__PURE__ */ jsx(CheckIcon, {}) }) })
89
+ ]
90
+ },
91
+ option.value
92
+ );
93
+ })
94
+ }
80
95
  );
81
- }) });
82
- };
96
+ }
97
+ );
98
+ SelectionCard.displayName = "SelectionCard";
83
99
  export {
84
100
  SelectionCard
85
101
  };
@@ -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
 
@@ -1,5 +1,6 @@
1
1
  import { default as React } from 'react';
2
- export interface SliderProps {
2
+ /** Props owned by Slider itself — everything else falls through to the <input type="range">. */
3
+ type SliderOwnProps = {
3
4
  /** Current value */
4
5
  value?: number;
5
6
  /** Minimum value */
@@ -8,18 +9,26 @@ export interface SliderProps {
8
9
  max?: number;
9
10
  /** Step increment */
10
11
  step?: number;
11
- /** Disabled state */
12
- disabled?: boolean;
13
- /** Size */
12
+ /** Component size (not the native character-width `size` attribute) */
14
13
  size?: 'default' | 'compact';
15
- /** Change handler */
16
- onChange?: (value: number) => void;
17
- /** Accessible label */
18
- ariaLabel?: string;
19
- /** Additional CSS classes */
14
+ /**
15
+ * Convenience callback receiving the numeric value directly.
16
+ * Fires alongside `onChange`, which keeps the standard React event signature
17
+ * so form libraries work unmodified.
18
+ */
19
+ onValueChange?: (value: number) => void;
20
+ /** Additional CSS classes — applied to the wrapper, not the <input> */
20
21
  className?: string;
22
+ /** @deprecated Pass the native `aria-label` attribute instead. */
23
+ ariaLabel?: string;
24
+ };
25
+ export interface SliderProps extends SliderOwnProps, Omit<React.ComponentPropsWithoutRef<'input'>, keyof SliderOwnProps | 'type'> {
21
26
  }
22
27
  /**
23
28
  * Slider allows selecting a value from within a given range.
29
+ *
30
+ * Forwards a ref to the underlying `<input type="range">` and spreads
31
+ * unrecognised props onto it.
24
32
  */
25
- export declare const Slider: ({ value, min, max, step, disabled, size, onChange, ariaLabel, className, }: SliderProps) => React.JSX.Element;
33
+ export declare const Slider: React.ForwardRefExoticComponent<SliderProps & React.RefAttributes<HTMLInputElement>>;
34
+ export {};
@@ -1,46 +1,56 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import "react";
2
+ import React from "react";
3
3
  import "./Slider.css";
4
- const Slider = ({
5
- value = 50,
6
- min = 0,
7
- max = 100,
8
- step = 1,
9
- disabled = false,
10
- size = "default",
11
- onChange,
12
- ariaLabel = "Slider",
13
- className = ""
14
- }) => {
15
- const baseClass = "ds-slider";
16
- const classes = [
17
- baseClass,
18
- `${baseClass}--${size}`,
19
- disabled ? `${baseClass}--disabled` : "",
20
- className
21
- ].filter(Boolean).join(" ");
22
- const percentage = (value - min) / (max - min) * 100;
23
- const handleChange = (e) => {
24
- onChange?.(Number(e.target.value));
25
- };
26
- return /* @__PURE__ */ jsx("div", { className: classes, children: /* @__PURE__ */ jsx(
27
- "input",
28
- {
29
- type: "range",
30
- className: `${baseClass}__input`,
31
- value,
32
- min,
33
- max,
34
- step,
35
- disabled,
36
- onChange: handleChange,
37
- "aria-label": ariaLabel,
38
- style: {
39
- background: `linear-gradient(to right, var(--color-action-primary-bg) ${percentage}%, var(--color-bg-container-secondary) ${percentage}%)`
4
+ const Slider = React.forwardRef(
5
+ ({
6
+ value = 50,
7
+ min = 0,
8
+ max = 100,
9
+ step = 1,
10
+ disabled = false,
11
+ size = "default",
12
+ onChange,
13
+ onValueChange,
14
+ ariaLabel = "Slider",
15
+ className = "",
16
+ style,
17
+ ...rest
18
+ }, ref) => {
19
+ const baseClass = "ds-slider";
20
+ const classes = [
21
+ baseClass,
22
+ `${baseClass}--${size}`,
23
+ disabled ? `${baseClass}--disabled` : "",
24
+ className
25
+ ].filter(Boolean).join(" ");
26
+ const percentage = (value - min) / (max - min) * 100;
27
+ const handleChange = (e) => {
28
+ onChange?.(e);
29
+ onValueChange?.(Number(e.target.value));
30
+ };
31
+ return /* @__PURE__ */ jsx("div", { className: classes, children: /* @__PURE__ */ jsx(
32
+ "input",
33
+ {
34
+ ...rest,
35
+ ref,
36
+ type: "range",
37
+ className: `${baseClass}__input`,
38
+ value,
39
+ min,
40
+ max,
41
+ step,
42
+ disabled,
43
+ onChange: handleChange,
44
+ "aria-label": ariaLabel || rest["aria-label"],
45
+ style: {
46
+ background: `linear-gradient(to right, var(--color-action-primary-bg) ${percentage}%, var(--color-bg-container-secondary) ${percentage}%)`,
47
+ ...style
48
+ }
40
49
  }
41
- }
42
- ) });
43
- };
50
+ ) });
51
+ }
52
+ );
53
+ Slider.displayName = "Slider";
44
54
  export {
45
55
  Slider
46
56
  };
@@ -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
+ }
@@ -15,7 +15,8 @@ export interface TableRow {
15
15
  /** Cell values keyed by column key — each accepts any React node */
16
16
  cells: Record<string, React.ReactNode>;
17
17
  }
18
- export interface TableProps {
18
+ /** Props owned by Table itself — everything else falls through to the <table>. */
19
+ type TableOwnProps = {
19
20
  /** Column definitions */
20
21
  columns: TableColumn[];
21
22
  /** Row data */
@@ -36,10 +37,17 @@ export interface TableProps {
36
37
  captionHidden?: boolean;
37
38
  /** Additional CSS classes */
38
39
  className?: string;
40
+ };
41
+ export interface TableProps extends TableOwnProps, Omit<React.ComponentPropsWithoutRef<'table'>, keyof TableOwnProps> {
39
42
  }
40
43
  /**
41
44
  * Table component from Figma design system.
42
45
  * Data table with flexible cell content — supports text, icons,
43
46
  * inputs, buttons, and any other React nodes inside cells.
47
+ *
48
+ * Forwards a ref to the `<table>` element and spreads unrecognised props onto
49
+ * it. When `bordered`, the table is wrapped in a container div — the ref and
50
+ * spread props still target the inner `<table>`.
44
51
  */
45
- export declare const Table: ({ columns, rows, size, striped, bordered, caption, captionHidden, className, }: TableProps) => React.JSX.Element;
52
+ export declare const Table: React.ForwardRefExoticComponent<TableProps & React.RefAttributes<HTMLTableElement>>;
53
+ export {};
@@ -1,65 +1,65 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
- import "react";
2
+ import React from "react";
3
3
  import "./Table.css";
4
- const Table = ({
5
- columns,
6
- rows,
7
- size = "default",
8
- striped = false,
9
- bordered = false,
10
- caption,
11
- captionHidden = false,
12
- className = ""
13
- }) => {
14
- const baseClass = "ds-table";
15
- const sizeClass = `${baseClass}--${size}`;
16
- const stripedClass = striped ? `${baseClass}--striped` : "";
17
- const borderedClass = bordered ? `${baseClass}--bordered` : "";
18
- const classes = [baseClass, sizeClass, stripedClass, borderedClass, className].filter(Boolean).join(" ");
19
- const table = /* @__PURE__ */ jsxs("table", { className: classes, children: [
20
- caption && /* @__PURE__ */ jsx(
21
- "caption",
22
- {
23
- className: captionHidden ? `${baseClass}__caption--hidden` : `${baseClass}__caption`,
24
- children: caption
25
- }
26
- ),
27
- /* @__PURE__ */ jsx("thead", { className: `${baseClass}__head`, children: /* @__PURE__ */ jsx("tr", { className: `${baseClass}__row ${baseClass}__row--header`, children: columns.map((col) => /* @__PURE__ */ jsx(
28
- "th",
29
- {
30
- className: `${baseClass}__header-cell`,
31
- scope: "col",
32
- style: {
33
- width: col.width,
34
- textAlign: col.align || "left"
4
+ const Table = React.forwardRef(
5
+ ({
6
+ columns,
7
+ rows,
8
+ size = "default",
9
+ striped = false,
10
+ bordered = false,
11
+ caption,
12
+ captionHidden = false,
13
+ className = "",
14
+ ...rest
15
+ }, ref) => {
16
+ const baseClass = "ds-table";
17
+ const classes = [
18
+ baseClass,
19
+ `${baseClass}--${size}`,
20
+ striped ? `${baseClass}--striped` : "",
21
+ bordered ? `${baseClass}--bordered` : "",
22
+ className
23
+ ].filter(Boolean).join(" ");
24
+ const table = /* @__PURE__ */ jsxs("table", { ...rest, ref, className: classes, children: [
25
+ caption && /* @__PURE__ */ jsx(
26
+ "caption",
27
+ {
28
+ className: captionHidden ? `${baseClass}__caption--hidden` : `${baseClass}__caption`,
29
+ children: caption
30
+ }
31
+ ),
32
+ /* @__PURE__ */ jsx("thead", { className: `${baseClass}__head`, children: /* @__PURE__ */ jsx("tr", { className: `${baseClass}__row ${baseClass}__row--header`, children: columns.map((col) => /* @__PURE__ */ jsx(
33
+ "th",
34
+ {
35
+ className: `${baseClass}__header-cell`,
36
+ scope: "col",
37
+ style: {
38
+ width: col.width,
39
+ textAlign: col.align || "left"
40
+ },
41
+ children: col.header || /* @__PURE__ */ jsx("span", { className: `${baseClass}__sr-only`, children: col.key })
35
42
  },
36
- children: col.header
37
- },
38
- col.key
39
- )) }) }),
40
- /* @__PURE__ */ jsx("tbody", { className: `${baseClass}__body`, children: rows.map((row) => /* @__PURE__ */ jsx("tr", { className: `${baseClass}__row`, children: columns.map((col) => /* @__PURE__ */ jsx(
41
- "td",
42
- {
43
- className: `${baseClass}__cell`,
44
- children: /* @__PURE__ */ jsx(
45
- "div",
46
- {
47
- className: `${baseClass}__cell-content`,
48
- style: {
49
- justifyContent: col.align === "center" ? "center" : col.align === "right" ? "flex-end" : "flex-start"
50
- },
51
- children: row.cells[col.key]
52
- }
53
- )
54
- },
55
- col.key
56
- )) }, row.id)) })
57
- ] });
58
- if (bordered) {
59
- return /* @__PURE__ */ jsx("div", { className: `${baseClass}__wrapper`, children: table });
43
+ col.key
44
+ )) }) }),
45
+ /* @__PURE__ */ jsx("tbody", { className: `${baseClass}__body`, children: rows.map((row) => /* @__PURE__ */ jsx("tr", { className: `${baseClass}__row`, children: columns.map((col) => /* @__PURE__ */ jsx("td", { className: `${baseClass}__cell`, children: /* @__PURE__ */ jsx(
46
+ "div",
47
+ {
48
+ className: `${baseClass}__cell-content`,
49
+ style: {
50
+ justifyContent: col.align === "center" ? "center" : col.align === "right" ? "flex-end" : "flex-start"
51
+ },
52
+ children: row.cells[col.key]
53
+ }
54
+ ) }, col.key)) }, row.id)) })
55
+ ] });
56
+ if (bordered) {
57
+ return /* @__PURE__ */ jsx("div", { className: `${baseClass}__wrapper`, children: table });
58
+ }
59
+ return table;
60
60
  }
61
- return table;
62
- };
61
+ );
62
+ Table.displayName = "Table";
63
63
  export {
64
64
  Table
65
65
  };