@sovereignfs/ui 0.41.0 → 0.45.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 (174) hide show
  1. package/dist/Accordion.module.css +3 -0
  2. package/dist/Alert.module.css +62 -0
  3. package/dist/AspectRatio.module.css +15 -0
  4. package/dist/Badge.module.css +75 -9
  5. package/dist/Breadcrumb.module.css +49 -0
  6. package/dist/Button.module.css +12 -3
  7. package/dist/ButtonGroup.module.css +29 -0
  8. package/dist/Card.module.css +6 -0
  9. package/dist/Checkbox.module.css +3 -1
  10. package/dist/Collapsible.module.css +70 -0
  11. package/dist/Combobox.module.css +122 -0
  12. package/dist/Command.module.css +80 -0
  13. package/dist/ConfirmDialog.module.css +5 -1
  14. package/dist/DataTable.module.css +28 -0
  15. package/dist/DatePicker.module.css +6 -2
  16. package/dist/Dialog.module.css +3 -0
  17. package/dist/HoverCard.module.css +3 -0
  18. package/dist/Input.module.css +6 -1
  19. package/dist/Item.module.css +70 -0
  20. package/dist/Kbd.module.css +14 -0
  21. package/dist/Label.module.css +11 -0
  22. package/dist/Marker.module.css +33 -0
  23. package/dist/Menu.module.css +8 -1
  24. package/dist/Message.module.css +99 -0
  25. package/dist/MessageScroller.module.css +54 -0
  26. package/dist/NavigationMenu.module.css +53 -0
  27. package/dist/Pagination.module.css +98 -0
  28. package/dist/Progress.module.css +20 -0
  29. package/dist/RadioGroup.module.css +88 -0
  30. package/dist/Resizable.module.css +70 -0
  31. package/dist/ScrollArea.module.css +20 -0
  32. package/dist/Select.module.css +7 -3
  33. package/dist/Slider.module.css +95 -0
  34. package/dist/Table.module.css +45 -0
  35. package/dist/Tabs.module.css +6 -1
  36. package/dist/Toggle.module.css +6 -1
  37. package/dist/Typography.module.css +47 -0
  38. package/dist/index.d.ts +617 -5
  39. package/dist/index.js +1847 -499
  40. package/dist/tokens/primitives.css +22 -8
  41. package/package.json +1 -1
  42. package/src/components/Accordion/Accordion.module.css +3 -0
  43. package/src/components/Accordion/Accordion.stories.tsx +110 -0
  44. package/src/components/Accordion/Accordion.tsx +53 -0
  45. package/src/components/Accordion/__tests__/Accordion.test.tsx +61 -0
  46. package/src/components/Alert/Alert.module.css +62 -0
  47. package/src/components/Alert/Alert.stories.tsx +96 -0
  48. package/src/components/Alert/Alert.tsx +58 -0
  49. package/src/components/Alert/__tests__/Alert.test.tsx +75 -0
  50. package/src/components/AspectRatio/AspectRatio.module.css +15 -0
  51. package/src/components/AspectRatio/AspectRatio.stories.tsx +78 -0
  52. package/src/components/AspectRatio/AspectRatio.tsx +21 -0
  53. package/src/components/AspectRatio/__tests__/AspectRatio.test.tsx +27 -0
  54. package/src/components/Badge/Badge.module.css +75 -9
  55. package/src/components/Badge/Badge.stories.tsx +109 -1
  56. package/src/components/Badge/Badge.tsx +30 -2
  57. package/src/components/Badge/__tests__/Badge.test.tsx +26 -2
  58. package/src/components/Breadcrumb/Breadcrumb.module.css +49 -0
  59. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +47 -0
  60. package/src/components/Breadcrumb/Breadcrumb.tsx +57 -0
  61. package/src/components/Breadcrumb/__tests__/Breadcrumb.test.tsx +52 -0
  62. package/src/components/Button/Button.module.css +12 -3
  63. package/src/components/Button/Button.stories.tsx +3 -2
  64. package/src/components/Button/Button.tsx +1 -1
  65. package/src/components/Button/__tests__/Button.test.tsx +10 -0
  66. package/src/components/ButtonGroup/ButtonGroup.module.css +29 -0
  67. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +57 -0
  68. package/src/components/ButtonGroup/ButtonGroup.tsx +27 -0
  69. package/src/components/ButtonGroup/__tests__/ButtonGroup.test.tsx +29 -0
  70. package/src/components/Card/Card.module.css +6 -0
  71. package/src/components/Checkbox/Checkbox.module.css +3 -1
  72. package/src/components/Collapsible/Collapsible.module.css +70 -0
  73. package/src/components/Collapsible/Collapsible.stories.tsx +62 -0
  74. package/src/components/Collapsible/Collapsible.tsx +66 -0
  75. package/src/components/Collapsible/__tests__/Collapsible.test.tsx +71 -0
  76. package/src/components/Combobox/Combobox.module.css +122 -0
  77. package/src/components/Combobox/Combobox.stories.tsx +79 -0
  78. package/src/components/Combobox/Combobox.tsx +186 -0
  79. package/src/components/Combobox/__tests__/Combobox.test.tsx +158 -0
  80. package/src/components/Command/Command.module.css +80 -0
  81. package/src/components/Command/Command.stories.tsx +104 -0
  82. package/src/components/Command/Command.tsx +168 -0
  83. package/src/components/Command/__tests__/Command.test.tsx +162 -0
  84. package/src/components/ConfirmDialog/ConfirmDialog.module.css +5 -1
  85. package/src/components/ContextMenu/ContextMenu.stories.tsx +81 -0
  86. package/src/components/ContextMenu/ContextMenu.tsx +100 -0
  87. package/src/components/ContextMenu/__tests__/ContextMenu.test.tsx +87 -0
  88. package/src/components/DataTable/DataTable.module.css +28 -0
  89. package/src/components/DataTable/DataTable.stories.tsx +74 -0
  90. package/src/components/DataTable/DataTable.tsx +154 -0
  91. package/src/components/DataTable/__tests__/DataTable.test.tsx +123 -0
  92. package/src/components/DatePicker/DatePicker.module.css +6 -2
  93. package/src/components/Dialog/Dialog.module.css +3 -0
  94. package/src/components/HoverCard/HoverCard.module.css +3 -0
  95. package/src/components/HoverCard/HoverCard.stories.tsx +66 -0
  96. package/src/components/HoverCard/HoverCard.tsx +111 -0
  97. package/src/components/HoverCard/__tests__/HoverCard.test.tsx +99 -0
  98. package/src/components/Icon/icons/circle-check.tsx +21 -0
  99. package/src/components/Icon/icons/circle-x.tsx +22 -0
  100. package/src/components/Icon/icons/index.ts +8 -0
  101. package/src/components/Input/Input.module.css +6 -1
  102. package/src/components/Item/Item.module.css +70 -0
  103. package/src/components/Item/Item.stories.tsx +62 -0
  104. package/src/components/Item/Item.tsx +54 -0
  105. package/src/components/Item/__tests__/Item.test.tsx +39 -0
  106. package/src/components/Kbd/Kbd.module.css +14 -0
  107. package/src/components/Kbd/Kbd.stories.tsx +32 -0
  108. package/src/components/Kbd/Kbd.tsx +12 -0
  109. package/src/components/Kbd/__tests__/Kbd.test.tsx +14 -0
  110. package/src/components/Label/Label.module.css +11 -0
  111. package/src/components/Label/Label.stories.tsx +40 -0
  112. package/src/components/Label/Label.tsx +24 -0
  113. package/src/components/Label/__tests__/Label.test.tsx +32 -0
  114. package/src/components/Marker/Marker.module.css +33 -0
  115. package/src/components/Marker/Marker.stories.tsx +39 -0
  116. package/src/components/Marker/Marker.tsx +34 -0
  117. package/src/components/Marker/__tests__/Marker.test.tsx +23 -0
  118. package/src/components/Menu/Menu.module.css +8 -1
  119. package/src/components/Menu/Menu.tsx +30 -8
  120. package/src/components/Menubar/Menubar.stories.tsx +52 -0
  121. package/src/components/Menubar/Menubar.tsx +42 -0
  122. package/src/components/Menubar/__tests__/Menubar.test.tsx +45 -0
  123. package/src/components/Message/Message.module.css +99 -0
  124. package/src/components/Message/Message.stories.tsx +79 -0
  125. package/src/components/Message/Message.tsx +41 -0
  126. package/src/components/Message/__tests__/Message.test.tsx +46 -0
  127. package/src/components/MessageScroller/MessageScroller.module.css +54 -0
  128. package/src/components/MessageScroller/MessageScroller.stories.tsx +84 -0
  129. package/src/components/MessageScroller/MessageScroller.tsx +72 -0
  130. package/src/components/MessageScroller/__tests__/MessageScroller.test.tsx +105 -0
  131. package/src/components/NavigationMenu/NavigationMenu.module.css +53 -0
  132. package/src/components/NavigationMenu/NavigationMenu.stories.tsx +58 -0
  133. package/src/components/NavigationMenu/NavigationMenu.tsx +125 -0
  134. package/src/components/NavigationMenu/__tests__/NavigationMenu.test.tsx +93 -0
  135. package/src/components/Pagination/Pagination.module.css +98 -0
  136. package/src/components/Pagination/Pagination.stories.tsx +49 -0
  137. package/src/components/Pagination/Pagination.tsx +87 -0
  138. package/src/components/Pagination/__tests__/Pagination.test.tsx +56 -0
  139. package/src/components/Progress/Progress.module.css +20 -0
  140. package/src/components/Progress/Progress.stories.tsx +40 -0
  141. package/src/components/Progress/Progress.tsx +35 -0
  142. package/src/components/Progress/__tests__/Progress.test.tsx +31 -0
  143. package/src/components/RadioGroup/RadioGroup.module.css +88 -0
  144. package/src/components/RadioGroup/RadioGroup.stories.tsx +69 -0
  145. package/src/components/RadioGroup/RadioGroup.tsx +99 -0
  146. package/src/components/RadioGroup/__tests__/RadioGroup.test.tsx +59 -0
  147. package/src/components/Resizable/Resizable.module.css +70 -0
  148. package/src/components/Resizable/Resizable.stories.tsx +82 -0
  149. package/src/components/Resizable/Resizable.tsx +313 -0
  150. package/src/components/Resizable/__tests__/Resizable.test.tsx +128 -0
  151. package/src/components/ScrollArea/ScrollArea.module.css +20 -0
  152. package/src/components/ScrollArea/ScrollArea.stories.tsx +57 -0
  153. package/src/components/ScrollArea/ScrollArea.tsx +27 -0
  154. package/src/components/ScrollArea/__tests__/ScrollArea.test.tsx +39 -0
  155. package/src/components/Select/Select.module.css +7 -3
  156. package/src/components/Slider/Slider.module.css +95 -0
  157. package/src/components/Slider/Slider.stories.tsx +64 -0
  158. package/src/components/Slider/Slider.tsx +77 -0
  159. package/src/components/Slider/__tests__/Slider.test.tsx +45 -0
  160. package/src/components/Table/Table.module.css +45 -0
  161. package/src/components/Table/Table.stories.tsx +125 -0
  162. package/src/components/Table/Table.tsx +51 -0
  163. package/src/components/Table/__tests__/Table.test.tsx +50 -0
  164. package/src/components/Tabs/Tabs.module.css +6 -1
  165. package/src/components/Toggle/Toggle.module.css +6 -1
  166. package/src/components/Typography/Typography.module.css +47 -0
  167. package/src/components/Typography/Typography.stories.tsx +62 -0
  168. package/src/components/Typography/Typography.tsx +30 -0
  169. package/src/components/Typography/__tests__/Typography.test.tsx +34 -0
  170. package/src/index.ts +78 -1
  171. package/src/stories/DesignSystemOverview.stories.tsx +614 -2
  172. package/src/stories/MobilePatterns.stories.tsx +8 -2
  173. package/src/stories/TokenGallery.stories.tsx +112 -16
  174. package/src/tokens/primitives.css +22 -8
@@ -0,0 +1,95 @@
1
+ .root {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--sv-space-2);
5
+ }
6
+
7
+ .label {
8
+ font-size: var(--sv-font-size-sm);
9
+ color: var(--sv-color-text-primary);
10
+ }
11
+
12
+ .slider {
13
+ appearance: none;
14
+ width: 100%;
15
+ height: 20px;
16
+ background: transparent;
17
+ cursor: pointer;
18
+ margin: 0;
19
+ }
20
+
21
+ /* Expand the vertical hit area for touch dragging without thickening the
22
+ visible track — same rationale as Button/Checkbox's touch-target
23
+ expansion, gated to the primary pointer. */
24
+ @media (pointer: coarse) {
25
+ .slider {
26
+ height: var(--sv-touch-target-min, 44px);
27
+ }
28
+ }
29
+
30
+ .slider:disabled {
31
+ opacity: 0.5;
32
+ cursor: not-allowed;
33
+ }
34
+
35
+ .slider:focus-visible {
36
+ outline: 2px solid var(--sv-color-focus-ring);
37
+ outline-offset: 2px;
38
+ }
39
+
40
+ /* Track — WebKit/Blink. The filled portion is a gradient computed from the
41
+ --slider-fill custom property the component sets inline; no JS-measured
42
+ pixel math, just a CSS variable driving a hard color stop. */
43
+ .slider::-webkit-slider-runnable-track {
44
+ height: 4px;
45
+ border-radius: var(--sv-radius-full);
46
+ background: linear-gradient(
47
+ to right,
48
+ var(--sv-color-accent) var(--slider-fill),
49
+ var(--sv-color-border-strong) var(--slider-fill)
50
+ );
51
+ }
52
+
53
+ .slider::-webkit-slider-thumb {
54
+ appearance: none;
55
+ width: 16px;
56
+ height: 16px;
57
+ margin-top: -6px;
58
+ border-radius: var(--sv-radius-full);
59
+ background: var(--sv-color-accent);
60
+ box-shadow: var(--sv-shadow-control);
61
+ cursor: pointer;
62
+ }
63
+
64
+ .slider:disabled::-webkit-slider-thumb {
65
+ cursor: not-allowed;
66
+ }
67
+
68
+ /* Track — Firefox. Separate track/progress pseudo-elements instead of a
69
+ gradient — Firefox renders the filled portion natively via
70
+ ::-moz-range-progress. */
71
+ .slider::-moz-range-track {
72
+ height: 4px;
73
+ border-radius: var(--sv-radius-full);
74
+ background: var(--sv-color-border-strong);
75
+ }
76
+
77
+ .slider::-moz-range-progress {
78
+ height: 4px;
79
+ border-radius: var(--sv-radius-full);
80
+ background: var(--sv-color-accent);
81
+ }
82
+
83
+ .slider::-moz-range-thumb {
84
+ width: 16px;
85
+ height: 16px;
86
+ border: none;
87
+ border-radius: var(--sv-radius-full);
88
+ background: var(--sv-color-accent);
89
+ box-shadow: var(--sv-shadow-control);
90
+ cursor: pointer;
91
+ }
92
+
93
+ .slider:disabled::-moz-range-thumb {
94
+ cursor: not-allowed;
95
+ }
@@ -0,0 +1,64 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { Slider } from './Slider';
4
+
5
+ const meta = {
6
+ title: 'Components/Slider',
7
+ component: Slider,
8
+ parameters: {
9
+ layout: 'padded',
10
+ docs: {
11
+ description: {
12
+ component:
13
+ 'Single-thumb range input. A native <input type="range"> under custom styling — arrow keys, Home/End, and touch-drag all come from the browser.',
14
+ },
15
+ },
16
+ },
17
+ args: {
18
+ value: 50,
19
+ onChange: () => {},
20
+ min: 0,
21
+ max: 100,
22
+ 'aria-label': 'Value',
23
+ },
24
+ } satisfies Meta<typeof Slider>;
25
+
26
+ export default meta;
27
+ type Story = StoryObj<typeof meta>;
28
+
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export const Default: Story = {
32
+ render: (args) => {
33
+ const [value, setValue] = useState(args.value);
34
+ return <Slider {...args} value={value} onChange={setValue} />;
35
+ },
36
+ };
37
+
38
+ export const WithLabel: Story = {
39
+ render: (args) => {
40
+ const [value, setValue] = useState(args.value);
41
+ return <Slider {...args} label="Volume" value={value} onChange={setValue} />;
42
+ },
43
+ };
44
+
45
+ export const AtMin: Story = {
46
+ args: { value: 0 },
47
+ };
48
+
49
+ export const AtMax: Story = {
50
+ args: { value: 100 },
51
+ };
52
+
53
+ export const SteppedRange: Story = {
54
+ render: (_args) => {
55
+ const [value, setValue] = useState(5);
56
+ return (
57
+ <Slider value={value} onChange={setValue} min={0} max={10} step={1} label="Rating (0–10)" />
58
+ );
59
+ },
60
+ };
61
+
62
+ export const Disabled: Story = {
63
+ args: { disabled: true },
64
+ };
@@ -0,0 +1,77 @@
1
+ 'use client';
2
+
3
+ import { useId } from 'react';
4
+ import type { CSSProperties } from 'react';
5
+ import styles from './Slider.module.css';
6
+
7
+ export interface SliderProps {
8
+ value: number;
9
+ onChange: (value: number) => void;
10
+ min: number;
11
+ max: number;
12
+ step?: number;
13
+ /** Renders a visible label above the track. Omit and pass `aria-label`
14
+ * instead for a standalone, unlabeled slider. */
15
+ label?: string;
16
+ disabled?: boolean;
17
+ id?: string;
18
+ className?: string;
19
+ 'aria-label'?: string;
20
+ 'aria-describedby'?: string;
21
+ 'aria-invalid'?: boolean;
22
+ required?: boolean;
23
+ }
24
+
25
+ /**
26
+ * Slider — single-thumb range input.
27
+ *
28
+ * A native `<input type="range">` under custom track/thumb styling — arrow
29
+ * key (adjust by `step`), Home/End (jump to min/max), and touch-drag
30
+ * support all come from the browser, nothing hand-rolled. Dual-thumb/range
31
+ * selection is out of scope until a consumer needs it.
32
+ */
33
+ export function Slider({
34
+ value,
35
+ onChange,
36
+ min,
37
+ max,
38
+ step,
39
+ label,
40
+ disabled,
41
+ id,
42
+ className,
43
+ 'aria-label': ariaLabel,
44
+ 'aria-describedby': ariaDescribedBy,
45
+ 'aria-invalid': ariaInvalid,
46
+ required,
47
+ }: SliderProps) {
48
+ const generatedId = useId();
49
+ const inputId = id ?? generatedId;
50
+ const fillPercent = max > min ? ((value - min) / (max - min)) * 100 : 0;
51
+
52
+ return (
53
+ <div className={[styles.root, className].filter(Boolean).join(' ')}>
54
+ {label && (
55
+ <label htmlFor={inputId} className={styles.label}>
56
+ {label}
57
+ </label>
58
+ )}
59
+ <input
60
+ id={inputId}
61
+ type="range"
62
+ min={min}
63
+ max={max}
64
+ step={step}
65
+ value={value}
66
+ disabled={disabled}
67
+ required={required}
68
+ aria-label={label ? undefined : ariaLabel}
69
+ aria-describedby={ariaDescribedBy}
70
+ aria-invalid={ariaInvalid}
71
+ className={styles.slider}
72
+ style={{ '--slider-fill': `${fillPercent}%` } as CSSProperties}
73
+ onChange={(e) => onChange(Number(e.target.value))}
74
+ />
75
+ </div>
76
+ );
77
+ }
@@ -0,0 +1,45 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+ import { cleanup, render, screen, fireEvent } from '@testing-library/react';
4
+ import { Slider } from '../Slider';
5
+
6
+ afterEach(cleanup);
7
+
8
+ describe('Slider', () => {
9
+ it('renders a range input with the given min/max/value', () => {
10
+ render(<Slider value={50} onChange={() => {}} min={0} max={100} aria-label="Volume" />);
11
+ const input = screen.getByRole('slider') as HTMLInputElement;
12
+ expect(input.min).toBe('0');
13
+ expect(input.max).toBe('100');
14
+ expect(input.value).toBe('50');
15
+ });
16
+
17
+ it('calls onChange with a number when the value changes', () => {
18
+ const onChange = vi.fn();
19
+ render(<Slider value={50} onChange={onChange} min={0} max={100} aria-label="Volume" />);
20
+ fireEvent.change(screen.getByRole('slider'), { target: { value: '75' } });
21
+ expect(onChange).toHaveBeenCalledWith(75);
22
+ });
23
+
24
+ it('respects a step prop', () => {
25
+ render(<Slider value={5} onChange={() => {}} min={0} max={10} step={5} aria-label="Volume" />);
26
+ expect((screen.getByRole('slider') as HTMLInputElement).step).toBe('5');
27
+ });
28
+
29
+ it('renders a visible label and associates it via htmlFor', () => {
30
+ render(<Slider value={50} onChange={() => {}} min={0} max={100} label="Volume" />);
31
+ expect(screen.getByRole('slider', { name: 'Volume' })).toBeDefined();
32
+ });
33
+
34
+ it('falls back to aria-label when no visible label is given', () => {
35
+ render(<Slider value={50} onChange={() => {}} min={0} max={100} aria-label="Volume" />);
36
+ expect(screen.getByRole('slider', { name: 'Volume' })).toBeDefined();
37
+ });
38
+
39
+ it('disables the input when disabled is set', () => {
40
+ render(
41
+ <Slider value={50} onChange={() => {}} min={0} max={100} aria-label="Volume" disabled />,
42
+ );
43
+ expect(screen.getByRole('slider')).toHaveProperty('disabled', true);
44
+ });
45
+ });
@@ -0,0 +1,45 @@
1
+ /* Horizontal scroll container — masked-edge fade + hidden scrollbar, same
2
+ technique as NavTabs. Always active (not gated to a narrow viewport):
3
+ unlike NavTabs' item count, a table's column count is content-driven and
4
+ can overflow at any screen size. */
5
+ .scrollContainer {
6
+ overflow-x: auto;
7
+ scrollbar-width: none;
8
+ -webkit-overflow-scrolling: touch;
9
+ mask-image: linear-gradient(to right, black calc(100% - 32px), transparent 100%);
10
+ }
11
+
12
+ .scrollContainer::-webkit-scrollbar {
13
+ display: none;
14
+ }
15
+
16
+ .table {
17
+ width: 100%;
18
+ border-collapse: collapse;
19
+ font-size: var(--sv-font-size-sm);
20
+ }
21
+
22
+ .head .row {
23
+ border-bottom: 1px solid var(--sv-color-border-strong);
24
+ }
25
+
26
+ .body .row {
27
+ border-bottom: 1px solid var(--sv-color-border);
28
+ }
29
+
30
+ .body .row:last-child {
31
+ border-bottom: none;
32
+ }
33
+
34
+ .th {
35
+ padding: var(--sv-space-2) var(--sv-space-3);
36
+ text-align: left;
37
+ font-weight: var(--sv-font-weight-semibold);
38
+ color: var(--sv-color-text-muted);
39
+ white-space: nowrap;
40
+ }
41
+
42
+ .td {
43
+ padding: var(--sv-space-2) var(--sv-space-3);
44
+ color: var(--sv-color-text-primary);
45
+ }
@@ -0,0 +1,125 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from './Table';
3
+
4
+ const meta = {
5
+ title: 'Components/Table',
6
+ component: Table,
7
+ parameters: {
8
+ layout: 'padded',
9
+ docs: {
10
+ description: {
11
+ component:
12
+ 'Thin, styled wrappers around the native table elements. Not a data grid — no sort/filter/virtualization. Horizontal scroll (masked-edge fade, hidden scrollbar) at any viewport size.',
13
+ },
14
+ },
15
+ },
16
+ args: { children: null },
17
+ } satisfies Meta<typeof Table>;
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof meta>;
21
+
22
+ // ---------------------------------------------------------------------------
23
+
24
+ const ROWS = [
25
+ { name: 'sovereign-tasks', status: 'Active', version: '1.4.2' },
26
+ { name: 'sovereign-ledger', status: 'Active', version: '2.0.0' },
27
+ { name: 'sovereign-shopper', status: 'Disabled', version: '0.9.1' },
28
+ ];
29
+
30
+ export const Default: Story = {
31
+ render: () => (
32
+ <Table>
33
+ <TableHead>
34
+ <TableRow>
35
+ <TableHeaderCell>Plugin</TableHeaderCell>
36
+ <TableHeaderCell>Status</TableHeaderCell>
37
+ <TableHeaderCell>Version</TableHeaderCell>
38
+ </TableRow>
39
+ </TableHead>
40
+ <TableBody>
41
+ {ROWS.map((row) => (
42
+ <TableRow key={row.name}>
43
+ <TableCell>{row.name}</TableCell>
44
+ <TableCell>{row.status}</TableCell>
45
+ <TableCell>{row.version}</TableCell>
46
+ </TableRow>
47
+ ))}
48
+ </TableBody>
49
+ </Table>
50
+ ),
51
+ };
52
+
53
+ export const WithoutHeader: Story = {
54
+ render: () => (
55
+ <Table>
56
+ <TableBody>
57
+ {ROWS.map((row) => (
58
+ <TableRow key={row.name}>
59
+ <TableCell>{row.name}</TableCell>
60
+ <TableCell>{row.status}</TableCell>
61
+ </TableRow>
62
+ ))}
63
+ </TableBody>
64
+ </Table>
65
+ ),
66
+ };
67
+
68
+ export const LongContent: Story = {
69
+ render: () => (
70
+ <Table>
71
+ <TableHead>
72
+ <TableRow>
73
+ <TableHeaderCell>Plugin</TableHeaderCell>
74
+ <TableHeaderCell>Description</TableHeaderCell>
75
+ <TableHeaderCell>Author</TableHeaderCell>
76
+ <TableHeaderCell>Repository</TableHeaderCell>
77
+ <TableHeaderCell>Status</TableHeaderCell>
78
+ <TableHeaderCell>Version</TableHeaderCell>
79
+ </TableRow>
80
+ </TableHead>
81
+ <TableBody>
82
+ {ROWS.map((row) => (
83
+ <TableRow key={row.name}>
84
+ <TableCell>{row.name}</TableCell>
85
+ <TableCell>A longer description column to force horizontal overflow</TableCell>
86
+ <TableCell>sovereign-community</TableCell>
87
+ <TableCell>github.com/sovereignfs/{row.name}</TableCell>
88
+ <TableCell>{row.status}</TableCell>
89
+ <TableCell>{row.version}</TableCell>
90
+ </TableRow>
91
+ ))}
92
+ </TableBody>
93
+ </Table>
94
+ ),
95
+ };
96
+
97
+ export const NarrowViewport: Story = {
98
+ parameters: { viewport: { defaultViewport: 'mobile1' } },
99
+ render: () => (
100
+ <Table>
101
+ <TableHead>
102
+ <TableRow>
103
+ <TableHeaderCell>Plugin</TableHeaderCell>
104
+ <TableHeaderCell>Description</TableHeaderCell>
105
+ <TableHeaderCell>Author</TableHeaderCell>
106
+ <TableHeaderCell>Repository</TableHeaderCell>
107
+ <TableHeaderCell>Status</TableHeaderCell>
108
+ <TableHeaderCell>Version</TableHeaderCell>
109
+ </TableRow>
110
+ </TableHead>
111
+ <TableBody>
112
+ {ROWS.map((row) => (
113
+ <TableRow key={row.name}>
114
+ <TableCell>{row.name}</TableCell>
115
+ <TableCell>A longer description column to force horizontal overflow</TableCell>
116
+ <TableCell>sovereign-community</TableCell>
117
+ <TableCell>github.com/sovereignfs/{row.name}</TableCell>
118
+ <TableCell>{row.status}</TableCell>
119
+ <TableCell>{row.version}</TableCell>
120
+ </TableRow>
121
+ ))}
122
+ </TableBody>
123
+ </Table>
124
+ ),
125
+ };
@@ -0,0 +1,51 @@
1
+ import type { ReactNode, ThHTMLAttributes, TdHTMLAttributes } from 'react';
2
+ import styles from './Table.module.css';
3
+
4
+ export interface TableProps {
5
+ children: ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ /**
10
+ * Table — thin, styled wrappers around the native table elements.
11
+ *
12
+ * Not a data grid: no sort, filter, or virtualization. `Table` wraps its
13
+ * content in a horizontally-scrolling container (masked-edge fade, hidden
14
+ * scrollbar — same technique as `NavTabs`) so a table wider than its
15
+ * container scrolls instead of overflowing the page, at any viewport size.
16
+ */
17
+ export function Table({ children, className }: TableProps) {
18
+ return (
19
+ <div className={styles.scrollContainer}>
20
+ <table className={[styles.table, className].filter(Boolean).join(' ')}>{children}</table>
21
+ </div>
22
+ );
23
+ }
24
+
25
+ export function TableHead({ children }: { children: ReactNode }) {
26
+ return <thead className={styles.head}>{children}</thead>;
27
+ }
28
+
29
+ export function TableBody({ children }: { children: ReactNode }) {
30
+ return <tbody className={styles.body}>{children}</tbody>;
31
+ }
32
+
33
+ export function TableRow({ children }: { children: ReactNode }) {
34
+ return <tr className={styles.row}>{children}</tr>;
35
+ }
36
+
37
+ export function TableHeaderCell({ children, ...rest }: ThHTMLAttributes<HTMLTableCellElement>) {
38
+ return (
39
+ <th className={styles.th} {...rest}>
40
+ {children}
41
+ </th>
42
+ );
43
+ }
44
+
45
+ export function TableCell({ children, ...rest }: TdHTMLAttributes<HTMLTableCellElement>) {
46
+ return (
47
+ <td className={styles.td} {...rest}>
48
+ {children}
49
+ </td>
50
+ );
51
+ }
@@ -0,0 +1,50 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, describe, expect, it } from 'vitest';
3
+ import { cleanup, render, screen } from '@testing-library/react';
4
+ import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from '../Table';
5
+
6
+ afterEach(cleanup);
7
+
8
+ function Demo() {
9
+ return (
10
+ <Table>
11
+ <TableHead>
12
+ <TableRow>
13
+ <TableHeaderCell>Name</TableHeaderCell>
14
+ <TableHeaderCell>Status</TableHeaderCell>
15
+ </TableRow>
16
+ </TableHead>
17
+ <TableBody>
18
+ <TableRow>
19
+ <TableCell>Alpha</TableCell>
20
+ <TableCell>Active</TableCell>
21
+ </TableRow>
22
+ <TableRow>
23
+ <TableCell>Beta</TableCell>
24
+ <TableCell>Disabled</TableCell>
25
+ </TableRow>
26
+ </TableBody>
27
+ </Table>
28
+ );
29
+ }
30
+
31
+ describe('Table', () => {
32
+ it('renders a real table element', () => {
33
+ render(<Demo />);
34
+ expect(screen.getByRole('table')).toBeDefined();
35
+ });
36
+
37
+ it('renders header cells with columnheader role', () => {
38
+ render(<Demo />);
39
+ expect(screen.getAllByRole('columnheader').map((el) => el.textContent)).toEqual([
40
+ 'Name',
41
+ 'Status',
42
+ ]);
43
+ });
44
+
45
+ it('renders the correct number of body rows and cells', () => {
46
+ render(<Demo />);
47
+ expect(screen.getAllByRole('row').length).toBe(3); // 1 header + 2 body
48
+ expect(screen.getAllByRole('cell').length).toBe(4);
49
+ });
50
+ });
@@ -12,7 +12,12 @@
12
12
 
13
13
  .tab {
14
14
  flex-shrink: 0;
15
- padding: var(--sv-space-2) var(--sv-space-4);
15
+ /* 36px rendered height, matching this scale's other 36px controls (Input,
16
+ Select). Vertical padding hardcoded to hit the exact height, same
17
+ reasoning as Button — accounting for the 2px border-bottom below,
18
+ which (content-box) adds to the rendered height on top of font-size +
19
+ padding. */
20
+ padding: 10px var(--sv-space-4);
16
21
  border: none;
17
22
  border-bottom: 2px solid transparent;
18
23
  /* Negative margin pulls the 2px border flush with the tablist border-bottom */
@@ -1,5 +1,10 @@
1
1
  .track {
2
- /* 38×22px pill — spec dimensions */
2
+ /* 38×22px pill — spec dimensions. Sizing audit: despite the component
3
+ name, this renders as a binary pill switch (role="switch"), not a
4
+ pressable toggle button — reviewed against this scale's other compact
5
+ controls with that in mind. Kept dimensions unchanged, generously
6
+ sized for touch-friendliness. (The name vs. actual behavior mismatch
7
+ is a separate, pre-existing naming note, not addressed here.) */
3
8
  display: inline-flex;
4
9
  align-items: center;
5
10
  width: 38px;
@@ -0,0 +1,47 @@
1
+ .h1 {
2
+ margin: 0;
3
+ font-size: var(--sv-font-size-2xl);
4
+ font-weight: var(--sv-font-weight-semibold);
5
+ color: var(--sv-color-text-primary);
6
+ }
7
+
8
+ .h2 {
9
+ margin: 0;
10
+ font-size: var(--sv-font-size-xl);
11
+ font-weight: var(--sv-font-weight-semibold);
12
+ color: var(--sv-color-text-primary);
13
+ }
14
+
15
+ .h3 {
16
+ margin: 0;
17
+ font-size: var(--sv-font-size-lg);
18
+ font-weight: var(--sv-font-weight-semibold);
19
+ color: var(--sv-color-text-primary);
20
+ }
21
+
22
+ .h4 {
23
+ margin: 0;
24
+ font-size: var(--sv-font-size-md);
25
+ font-weight: var(--sv-font-weight-semibold);
26
+ color: var(--sv-color-text-primary);
27
+ }
28
+
29
+ .body {
30
+ margin: 0;
31
+ font-size: var(--sv-font-size-sm);
32
+ color: var(--sv-color-text-primary);
33
+ }
34
+
35
+ .caption {
36
+ margin: 0;
37
+ font-size: var(--sv-font-size-caption);
38
+ color: var(--sv-color-text-muted);
39
+ }
40
+
41
+ .label {
42
+ font-size: var(--sv-font-size-label);
43
+ font-weight: var(--sv-font-weight-semibold);
44
+ color: var(--sv-color-text-muted);
45
+ text-transform: uppercase;
46
+ letter-spacing: 0.04em;
47
+ }
@@ -0,0 +1,62 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { Typography } from './Typography';
3
+
4
+ const meta = {
5
+ title: 'Components/Typography',
6
+ component: Typography,
7
+ parameters: { layout: 'padded' },
8
+ args: {
9
+ variant: 'body',
10
+ children: 'The quick brown fox',
11
+ },
12
+ } satisfies Meta<typeof Typography>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export const H1: Story = {
20
+ args: { variant: 'h1' },
21
+ };
22
+
23
+ export const H2: Story = {
24
+ args: { variant: 'h2' },
25
+ };
26
+
27
+ export const H3: Story = {
28
+ args: { variant: 'h3' },
29
+ };
30
+
31
+ export const H4: Story = {
32
+ args: { variant: 'h4' },
33
+ };
34
+
35
+ export const Body: Story = {
36
+ args: { variant: 'body' },
37
+ };
38
+
39
+ export const Caption: Story = {
40
+ args: { variant: 'caption' },
41
+ };
42
+
43
+ export const LabelVariant: Story = {
44
+ name: 'Label',
45
+ args: { variant: 'label', children: 'Section label' },
46
+ };
47
+
48
+ export const AllVariants: Story = {
49
+ render: () => (
50
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
51
+ <Typography variant="h1">Heading 1</Typography>
52
+ <Typography variant="h2">Heading 2</Typography>
53
+ <Typography variant="h3">Heading 3</Typography>
54
+ <Typography variant="h4">Heading 4</Typography>
55
+ <Typography variant="body">
56
+ Body copy — the default reading size for paragraphs and descriptions.
57
+ </Typography>
58
+ <Typography variant="caption">Caption — secondary, supporting copy.</Typography>
59
+ <Typography variant="label">Section label</Typography>
60
+ </div>
61
+ ),
62
+ };