@sovereignfs/ui 0.21.2 → 0.23.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 (39) hide show
  1. package/dist/Checkbox.module.css +97 -0
  2. package/dist/Dialog.module.css +8 -4
  3. package/dist/DragHandleRow.module.css +51 -0
  4. package/dist/FormField.module.css +5 -0
  5. package/dist/Input.module.css +10 -0
  6. package/dist/Select.module.css +9 -0
  7. package/dist/Textarea.module.css +39 -0
  8. package/dist/Toast.module.css +3 -3
  9. package/dist/Toggle.module.css +1 -1
  10. package/dist/index.d.ts +58 -5
  11. package/dist/index.js +220 -98
  12. package/dist/tokens/semantic.css +6 -0
  13. package/package.json +1 -1
  14. package/src/components/Checkbox/Checkbox.module.css +97 -0
  15. package/src/components/Checkbox/Checkbox.tsx +68 -0
  16. package/src/components/Dialog/Dialog.module.css +8 -4
  17. package/src/components/DragHandleRow/DragHandleRow.module.css +51 -0
  18. package/src/components/DragHandleRow/DragHandleRow.tsx +61 -0
  19. package/src/components/FormField/FormField.module.css +5 -0
  20. package/src/components/FormField/FormField.tsx +36 -11
  21. package/src/components/FormField/__tests__/FormField.test.tsx +83 -0
  22. package/src/components/Input/Input.module.css +10 -0
  23. package/src/components/Select/Select.module.css +9 -0
  24. package/src/components/Select/Select.tsx +10 -2
  25. package/src/components/Textarea/Textarea.module.css +39 -0
  26. package/src/components/Textarea/Textarea.tsx +15 -0
  27. package/src/components/Textarea/__tests__/Textarea.test.tsx +29 -0
  28. package/src/components/Toast/Toast.module.css +3 -3
  29. package/src/components/Toggle/Toggle.module.css +1 -1
  30. package/src/index.ts +7 -1
  31. package/src/scroll-lock.ts +8 -2
  32. package/src/stories/Checkbox.stories.tsx +45 -0
  33. package/src/stories/DesignSystemOverview.stories.tsx +64 -6
  34. package/src/stories/DragHandleRow.stories.tsx +41 -0
  35. package/src/stories/FormField.stories.tsx +8 -8
  36. package/src/stories/MobilePatterns.stories.tsx +4 -4
  37. package/src/stories/Textarea.stories.tsx +51 -0
  38. package/src/stories/TokenGallery.stories.tsx +2 -0
  39. package/src/tokens/semantic.css +6 -0
@@ -11,6 +11,7 @@ import { EmptyState } from '../components/EmptyState/EmptyState';
11
11
  import { FormField } from '../components/FormField/FormField';
12
12
  import { Icon } from '../components/Icon/Icon';
13
13
  import { Input } from '../components/Input/Input';
14
+ import { Textarea } from '../components/Textarea/Textarea';
14
15
  import { NavTabs } from '../components/NavTabs/NavTabs';
15
16
  import { PageHeader } from '../components/PageHeader/PageHeader';
16
17
  import { Popover } from '../components/Popover/Popover';
@@ -22,6 +23,8 @@ import { Tabs } from '../components/Tabs/Tabs';
22
23
  import { ToastProvider, useToast } from '../components/Toast/Toast';
23
24
  import { Toggle } from '../components/Toggle/Toggle';
24
25
  import { Tooltip } from '../components/Tooltip/Tooltip';
26
+ import { Checkbox } from '../components/Checkbox/Checkbox';
27
+ import { DragHandleRow } from '../components/DragHandleRow/DragHandleRow';
25
28
 
26
29
  // ---------------------------------------------------------------------------
27
30
  // Shared primitives
@@ -1181,7 +1184,7 @@ font-weight: var(--sv-font-weight-bold); /* 700 */`}</Code>
1181
1184
  <section style={{ marginBottom: 'var(--sv-space-12)' }}>
1182
1185
  <SectionHeader
1183
1186
  title="Component gallery"
1184
- subtitle="All 13 components — click each story in the sidebar for the full API, variants, and controls."
1187
+ subtitle="All 15 components — click each story in the sidebar for the full API, variants, and controls."
1185
1188
  />
1186
1189
 
1187
1190
  <div style={{ marginBottom: 'var(--sv-space-6)' }}>
@@ -1384,18 +1387,31 @@ font-weight: var(--sv-font-weight-bold); /* 700 */`}</Code>
1384
1387
  <ComponentCard
1385
1388
  name="FormField"
1386
1389
  importLine="import { FormField } from '@sovereignfs/ui';"
1387
- usage="Accessible label + input wrapper. Wires hint and error text to the child via aria-describedby. Always pair with an Input and matching htmlFor/id."
1390
+ usage="Accessible label + input wrapper. The render-prop `children` receives field props (id, aria-describedby, aria-invalid) to spread onto the control, so hint/error text stays wired to it."
1388
1391
  >
1389
1392
  <div style={{ display: 'flex', flexDirection: 'column', gap: 12, width: '100%' }}>
1390
- <FormField label="Email" hint="We'll never share this." htmlFor="ov-email">
1391
- <Input id="ov-email" type="email" placeholder="you@example.com" />
1393
+ <FormField label="Email" hint="We'll never share this." id="ov-email">
1394
+ {(field) => <Input {...field} type="email" placeholder="you@example.com" />}
1392
1395
  </FormField>
1393
- <FormField label="Password" error="Must be 8+ characters." htmlFor="ov-pw">
1394
- <Input id="ov-pw" type="password" />
1396
+ <FormField label="Password" error="Must be 8+ characters." id="ov-pw">
1397
+ {(field) => <Input {...field} type="password" />}
1395
1398
  </FormField>
1396
1399
  </div>
1397
1400
  </ComponentCard>
1398
1401
 
1402
+ {/* Textarea */}
1403
+ <ComponentCard
1404
+ name="Textarea"
1405
+ importLine="import { Textarea } from '@sovereignfs/ui';"
1406
+ usage="The primitive multi-line text field. Forwards all native textarea props. Pair with FormField or a <label> for accessibility."
1407
+ >
1408
+ <Textarea
1409
+ aria-label="Description"
1410
+ placeholder="Add a description…"
1411
+ style={{ width: '100%' }}
1412
+ />
1413
+ </ComponentCard>
1414
+
1399
1415
  {/* PageHeader */}
1400
1416
  <ComponentCard
1401
1417
  name="PageHeader"
@@ -1493,6 +1509,48 @@ font-weight: var(--sv-font-weight-bold); /* 700 */`}</Code>
1493
1509
  </Tooltip>
1494
1510
  </div>
1495
1511
  </ComponentCard>
1512
+
1513
+ {/* Checkbox */}
1514
+ <ComponentCard
1515
+ name="Checkbox"
1516
+ importLine="import { Checkbox } from '@sovereignfs/ui';"
1517
+ usage="Accessible checkbox with optional animated strike-through on the label when checked. Used in task lists."
1518
+ >
1519
+ {(() => {
1520
+ const [a, setA] = useState(false);
1521
+ const [b, setB] = useState(true);
1522
+ return (
1523
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
1524
+ <Checkbox checked={a} onChange={setA} label="Unchecked task" strikeThrough />
1525
+ <Checkbox checked={b} onChange={setB} label="Completed task" strikeThrough />
1526
+ </div>
1527
+ );
1528
+ })()}
1529
+ </ComponentCard>
1530
+
1531
+ {/* DragHandleRow */}
1532
+ <ComponentCard
1533
+ name="DragHandleRow"
1534
+ importLine="import { DragHandleRow } from '@sovereignfs/ui';"
1535
+ usage="Row wrapper with a drag handle that appears on hover. Attach dnd-kit sortable props via handleProps."
1536
+ >
1537
+ <div style={{ width: '100%' }}>
1538
+ {['First item', 'Second item', 'Third item'].map((label) => (
1539
+ <DragHandleRow key={label}>
1540
+ <div
1541
+ style={{
1542
+ padding: 'var(--sv-space-2) var(--sv-space-1)',
1543
+ fontSize: 'var(--sv-font-size-sm)',
1544
+ color: 'var(--sv-color-text-primary)',
1545
+ borderBottom: '1px solid var(--sv-color-border)',
1546
+ }}
1547
+ >
1548
+ {label}
1549
+ </div>
1550
+ </DragHandleRow>
1551
+ ))}
1552
+ </div>
1553
+ </ComponentCard>
1496
1554
  </div>
1497
1555
  </section>
1498
1556
 
@@ -0,0 +1,41 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { DragHandleRow } from '../components/DragHandleRow/DragHandleRow';
3
+
4
+ const meta: Meta<typeof DragHandleRow> = {
5
+ title: 'Components/DragHandleRow',
6
+ component: DragHandleRow,
7
+ parameters: { layout: 'padded' },
8
+ tags: ['autodocs'],
9
+ };
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof DragHandleRow>;
13
+
14
+ const rowStyle = {
15
+ padding: 'var(--sv-space-3) var(--sv-space-2)',
16
+ borderBottom: '1px solid var(--sv-color-border)',
17
+ fontSize: 'var(--sv-font-size-sm)',
18
+ color: 'var(--sv-color-text-primary)',
19
+ width: '320px',
20
+ };
21
+
22
+ export const Default: Story = {
23
+ render: () => (
24
+ <div>
25
+ {['Buy groceries', 'Call the dentist', 'Review pull request'].map((label) => (
26
+ <DragHandleRow key={label}>
27
+ <div style={rowStyle}>{label}</div>
28
+ </DragHandleRow>
29
+ ))}
30
+ </div>
31
+ ),
32
+ name: 'List of rows (hover to reveal handle)',
33
+ };
34
+
35
+ export const Dragging: Story = {
36
+ render: () => (
37
+ <DragHandleRow isDragging>
38
+ <div style={rowStyle}>This row is being dragged</div>
39
+ </DragHandleRow>
40
+ ),
41
+ };
@@ -14,8 +14,8 @@ type Story = StoryObj<typeof meta>;
14
14
  export const Default: Story = {
15
15
  args: {
16
16
  label: 'Email address',
17
- htmlFor: 'email',
18
- children: <Input id="email" type="email" placeholder="you@example.com" />,
17
+ id: 'email',
18
+ children: (field) => <Input {...field} type="email" placeholder="you@example.com" />,
19
19
  },
20
20
  };
21
21
 
@@ -23,8 +23,8 @@ export const WithHint: Story = {
23
23
  args: {
24
24
  label: 'Username',
25
25
  hint: 'Letters, numbers, and underscores only.',
26
- htmlFor: 'username',
27
- children: <Input id="username" placeholder="your_username" />,
26
+ id: 'username',
27
+ children: (field) => <Input {...field} placeholder="your_username" />,
28
28
  },
29
29
  };
30
30
 
@@ -32,8 +32,8 @@ export const WithError: Story = {
32
32
  args: {
33
33
  label: 'Password',
34
34
  error: 'Password must be at least 8 characters.',
35
- htmlFor: 'password',
36
- children: <Input id="password" type="password" />,
35
+ id: 'password',
36
+ children: (field) => <Input {...field} type="password" />,
37
37
  },
38
38
  };
39
39
 
@@ -41,7 +41,7 @@ export const Required: Story = {
41
41
  args: {
42
42
  label: 'Full name',
43
43
  required: true,
44
- htmlFor: 'name',
45
- children: <Input id="name" placeholder="Jane Smith" />,
44
+ id: 'name',
45
+ children: (field) => <Input {...field} placeholder="Jane Smith" />,
46
46
  },
47
47
  };
@@ -405,11 +405,11 @@ function MobilePatternsDoc() {
405
405
  gap: 'var(--sv-space-3)',
406
406
  }}
407
407
  >
408
- <FormField label="Display name" htmlFor="mp-name">
409
- <Input id="mp-name" placeholder="Your name" />
408
+ <FormField label="Display name" id="mp-name">
409
+ {(field) => <Input {...field} placeholder="Your name" />}
410
410
  </FormField>
411
- <FormField label="Email" htmlFor="mp-email">
412
- <Input id="mp-email" type="email" placeholder="you@example.com" />
411
+ <FormField label="Email" id="mp-email">
412
+ {(field) => <Input {...field} type="email" placeholder="you@example.com" />}
413
413
  </FormField>
414
414
  <Button type="submit">Save changes</Button>
415
415
  </div>
@@ -0,0 +1,51 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { Textarea } from '../components/Textarea/Textarea';
3
+
4
+ // Wrap in a label so a11y checks pass (Textarea is a bare <textarea> — no
5
+ // built-in label association).
6
+ function Labeled({ label, ...props }: React.ComponentProps<typeof Textarea> & { label: string }) {
7
+ return (
8
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6, width: 280 }}>
9
+ <label
10
+ htmlFor="sb-textarea"
11
+ style={{ fontSize: 14, color: 'var(--sv-color-text-primary)', fontFamily: 'system-ui' }}
12
+ >
13
+ {label}
14
+ </label>
15
+ <Textarea id="sb-textarea" {...props} />
16
+ </div>
17
+ );
18
+ }
19
+
20
+ const meta = {
21
+ title: 'Components/Textarea',
22
+ component: Textarea,
23
+ parameters: {
24
+ layout: 'centered',
25
+ docs: {
26
+ description: {
27
+ component:
28
+ 'The primitive multi-line text field. RSC-safe; forwards all native `<textarea>` props. No label is built-in — always pair with a `<label>` (or `FormField`) for accessibility.',
29
+ },
30
+ },
31
+ },
32
+ } satisfies Meta<typeof Textarea>;
33
+
34
+ export default meta;
35
+ type Story = StoryObj<typeof meta>;
36
+
37
+ export const Default: Story = {
38
+ render: (_args) => <Labeled label="Description" placeholder="Add a description…" />,
39
+ };
40
+
41
+ export const WithValue: Story = {
42
+ render: (_args) => <Labeled label="Notes" defaultValue="Follow up next week." />,
43
+ };
44
+
45
+ export const CustomRows: Story = {
46
+ render: (_args) => <Labeled label="Message" rows={8} placeholder="Write your message…" />,
47
+ };
48
+
49
+ export const Disabled: Story = {
50
+ render: (_args) => <Labeled label="Read-only field" defaultValue="Cannot be changed" disabled />,
51
+ };
@@ -19,6 +19,7 @@ const SEMANTIC_COLORS = [
19
19
  '--sv-color-border-strong',
20
20
  '--sv-color-accent',
21
21
  '--sv-color-accent-hover',
22
+ '--sv-color-accent-subtle',
22
23
  '--sv-color-focus-ring',
23
24
  '--sv-color-error-surface',
24
25
  '--sv-color-error-text',
@@ -82,6 +83,7 @@ const SHADOW_TOKENS = [
82
83
  '--sv-shadow-hover',
83
84
  '--sv-shadow-popover',
84
85
  '--sv-shadow-overlay',
86
+ '--sv-shadow-control',
85
87
  ];
86
88
 
87
89
  // ---------------------------------------------------------------------------
@@ -25,6 +25,10 @@
25
25
 
26
26
  --sv-color-accent: var(--sv-grey-900);
27
27
  --sv-color-accent-hover: var(--sv-grey-700);
28
+ /* Derived from --sv-color-accent via color-mix, so it re-themes automatically —
29
+ no separate dark-mode override needed. Tinted background for badges/chips
30
+ paired with --sv-color-accent text. */
31
+ --sv-color-accent-subtle: color-mix(in srgb, var(--sv-color-accent) 12%, transparent);
28
32
 
29
33
  --sv-color-focus-ring: var(--sv-grey-900);
30
34
 
@@ -56,6 +60,7 @@
56
60
  --sv-shadow-popover:
57
61
  0 4px 16px rgba(0, 0, 0, 0.14), 0 2px 6px rgba(0, 0, 0, 0.08); /* e2 — floating panels */
58
62
  --sv-shadow-overlay: 0 10px 38px rgba(0, 0, 0, 0.18), 0 6px 12px rgba(0, 0, 0, 0.12);
63
+ --sv-shadow-control: 0 1px 3px rgba(0, 0, 0, 0.2); /* small interactive-control shadows, e.g. Toggle thumb */
59
64
 
60
65
  /*
61
66
  * Instance identity tokens (RFC 0027 / RFC 0032 rename). Distinct namespace from --sv-color-*:
@@ -108,4 +113,5 @@
108
113
  --sv-shadow-hover: 0 2px 6px rgba(0, 0, 0, 0.3);
109
114
  --sv-shadow-popover: 0 4px 16px rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.3);
110
115
  --sv-shadow-overlay: 0 10px 38px rgba(0, 0, 0, 0.6), 0 6px 12px rgba(0, 0, 0, 0.5);
116
+ --sv-shadow-control: 0 1px 3px rgba(0, 0, 0, 0.5);
111
117
  }