@sovereignfs/ui 0.21.3 → 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 (36) hide show
  1. package/dist/Checkbox.module.css +97 -0
  2. package/dist/DragHandleRow.module.css +51 -0
  3. package/dist/FormField.module.css +5 -0
  4. package/dist/Input.module.css +10 -0
  5. package/dist/Select.module.css +9 -0
  6. package/dist/Textarea.module.css +39 -0
  7. package/dist/Toast.module.css +3 -3
  8. package/dist/Toggle.module.css +1 -1
  9. package/dist/index.d.ts +58 -5
  10. package/dist/index.js +218 -96
  11. package/dist/tokens/semantic.css +6 -0
  12. package/package.json +1 -1
  13. package/src/components/Checkbox/Checkbox.module.css +97 -0
  14. package/src/components/Checkbox/Checkbox.tsx +68 -0
  15. package/src/components/DragHandleRow/DragHandleRow.module.css +51 -0
  16. package/src/components/DragHandleRow/DragHandleRow.tsx +61 -0
  17. package/src/components/FormField/FormField.module.css +5 -0
  18. package/src/components/FormField/FormField.tsx +36 -11
  19. package/src/components/FormField/__tests__/FormField.test.tsx +83 -0
  20. package/src/components/Input/Input.module.css +10 -0
  21. package/src/components/Select/Select.module.css +9 -0
  22. package/src/components/Select/Select.tsx +10 -2
  23. package/src/components/Textarea/Textarea.module.css +39 -0
  24. package/src/components/Textarea/Textarea.tsx +15 -0
  25. package/src/components/Textarea/__tests__/Textarea.test.tsx +29 -0
  26. package/src/components/Toast/Toast.module.css +3 -3
  27. package/src/components/Toggle/Toggle.module.css +1 -1
  28. package/src/index.ts +7 -1
  29. package/src/stories/Checkbox.stories.tsx +45 -0
  30. package/src/stories/DesignSystemOverview.stories.tsx +64 -6
  31. package/src/stories/DragHandleRow.stories.tsx +41 -0
  32. package/src/stories/FormField.stories.tsx +8 -8
  33. package/src/stories/MobilePatterns.stories.tsx +4 -4
  34. package/src/stories/Textarea.stories.tsx +51 -0
  35. package/src/stories/TokenGallery.stories.tsx +2 -0
  36. package/src/tokens/semantic.css +6 -0
@@ -0,0 +1,97 @@
1
+ .root {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: var(--sv-space-2);
5
+ cursor: pointer;
6
+ }
7
+
8
+ /* The visual box */
9
+ .box {
10
+ position: relative;
11
+ flex-shrink: 0;
12
+ width: 18px;
13
+ height: 18px;
14
+ border-radius: var(--sv-radius-sm);
15
+ border: 1.5px solid var(--sv-color-border-strong);
16
+ background: var(--sv-color-surface);
17
+ transition:
18
+ background-color 0.15s ease,
19
+ border-color 0.15s ease;
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ }
24
+
25
+ .checked {
26
+ background-color: var(--sv-color-accent);
27
+ border-color: var(--sv-color-accent);
28
+ color: var(--sv-white);
29
+ }
30
+
31
+ /* Hide the native input visually but keep it accessible */
32
+ .input {
33
+ position: absolute;
34
+ inset: 0;
35
+ opacity: 0;
36
+ margin: 0;
37
+ width: 100%;
38
+ height: 100%;
39
+ cursor: pointer;
40
+ }
41
+
42
+ .input:disabled {
43
+ cursor: not-allowed;
44
+ }
45
+
46
+ .input:focus-visible + .tick,
47
+ .input:focus-visible {
48
+ outline: none;
49
+ }
50
+
51
+ .box:has(.input:focus-visible) {
52
+ outline: 2px solid var(--sv-color-focus-ring);
53
+ outline-offset: 2px;
54
+ }
55
+
56
+ .tick {
57
+ width: 10px;
58
+ height: 8px;
59
+ pointer-events: none;
60
+ }
61
+
62
+ /* Label */
63
+ .label {
64
+ font-size: var(--sv-font-size-sm);
65
+ color: var(--sv-color-text-primary);
66
+ cursor: pointer;
67
+ user-select: none;
68
+ transition: color 0.15s ease;
69
+ /* Strike-through animates via clip-path on a pseudo-element */
70
+ position: relative;
71
+ }
72
+
73
+ .label::after {
74
+ content: '';
75
+ position: absolute;
76
+ left: 0;
77
+ top: 50%;
78
+ width: 100%;
79
+ height: 1.5px;
80
+ background: var(--sv-color-text-muted);
81
+ transform: scaleX(0);
82
+ transform-origin: left center;
83
+ transition: transform 0.2s ease;
84
+ }
85
+
86
+ .struck {
87
+ color: var(--sv-color-text-muted);
88
+ }
89
+
90
+ .struck::after {
91
+ transform: scaleX(1);
92
+ }
93
+
94
+ .disabled {
95
+ opacity: 0.5;
96
+ cursor: not-allowed;
97
+ }
@@ -0,0 +1,51 @@
1
+ .row {
2
+ display: flex;
3
+ align-items: flex-start;
4
+ gap: var(--sv-space-1);
5
+ position: relative;
6
+ }
7
+
8
+ .row:not(:hover) .handle,
9
+ .row:not(:focus-within) .handle {
10
+ opacity: 0;
11
+ }
12
+
13
+ .handle {
14
+ flex-shrink: 0;
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: center;
18
+ width: 24px;
19
+ height: 24px;
20
+ margin-top: 2px;
21
+ border: none;
22
+ background: none;
23
+ padding: 0;
24
+ border-radius: var(--sv-radius-sm);
25
+ color: var(--sv-color-text-muted);
26
+ cursor: grab;
27
+ opacity: 1;
28
+ transition: opacity 0.1s ease;
29
+ }
30
+
31
+ .handle:focus-visible {
32
+ outline: 2px solid var(--sv-color-focus-ring);
33
+ outline-offset: 2px;
34
+ }
35
+
36
+ .handle:active {
37
+ cursor: grabbing;
38
+ }
39
+
40
+ .icon {
41
+ pointer-events: none;
42
+ }
43
+
44
+ .content {
45
+ flex: 1;
46
+ min-width: 0;
47
+ }
48
+
49
+ .dragging {
50
+ opacity: 0.5;
51
+ }
@@ -2,6 +2,11 @@
2
2
  display: flex;
3
3
  flex-direction: column;
4
4
  gap: var(--sv-space-1);
5
+ /* Every local .field/.fieldGroup class this component replaced set this
6
+ explicitly. Without it, a parent with align-items: flex-start (needed so
7
+ submit buttons don't stretch full-width) shrink-wraps this wrapper to its
8
+ content's intrinsic width instead of filling the available space. */
9
+ width: 100%;
5
10
  }
6
11
 
7
12
  .label {
@@ -14,6 +14,16 @@
14
14
  box-shadow 0.15s ease;
15
15
  }
16
16
 
17
+ /* iOS Safari (tab and standalone PWA) auto-zooms the page whenever a focused
18
+ input's font-size is below 16px. The viewport intentionally allows pinch-zoom
19
+ (no maximum-scale — an accessibility requirement), so the only way to stop the
20
+ focus-zoom is to render the field at >=16px on touch devices. */
21
+ @media (pointer: coarse) {
22
+ .input {
23
+ font-size: var(--sv-font-size-md);
24
+ }
25
+ }
26
+
17
27
  .input::placeholder {
18
28
  color: var(--sv-color-text-muted);
19
29
  }
@@ -33,6 +33,15 @@
33
33
  border-radius: var(--sv-radius-sm);
34
34
  }
35
35
 
36
+ /* iOS Safari zooms the page when a <select> under 16px receives focus, exactly
37
+ as it does for text inputs. Lift the default size to >=16px on touch devices;
38
+ the compact `.sm` variant is for dense desktop tables and is left untouched. */
39
+ @media (pointer: coarse) {
40
+ .select {
41
+ font-size: var(--sv-font-size-md);
42
+ }
43
+ }
44
+
36
45
  .select:focus-visible {
37
46
  outline: none;
38
47
  border-color: var(--sv-color-accent);
@@ -0,0 +1,39 @@
1
+ .textarea {
2
+ display: block;
3
+ width: 100%;
4
+ padding: var(--sv-space-2) var(--sv-space-3);
5
+ font-family: var(--sv-font-family);
6
+ font-size: var(--sv-font-size-sm);
7
+ line-height: 1.5;
8
+ color: var(--sv-color-text-primary);
9
+ background-color: var(--sv-color-surface);
10
+ border: 1px solid var(--sv-color-border-strong);
11
+ border-radius: var(--sv-radius-md);
12
+ resize: vertical;
13
+ transition:
14
+ border-color 0.15s ease,
15
+ box-shadow 0.15s ease;
16
+ }
17
+
18
+ .textarea::placeholder {
19
+ color: var(--sv-color-text-muted);
20
+ }
21
+
22
+ .textarea:focus-visible {
23
+ outline: none;
24
+ border-color: var(--sv-color-accent);
25
+ box-shadow: 0 0 0 2px var(--sv-color-focus-ring);
26
+ }
27
+
28
+ .textarea:read-only {
29
+ background-color: var(--sv-color-surface-sunken);
30
+ color: var(--sv-color-text-muted);
31
+ border-color: var(--sv-color-border);
32
+ cursor: default;
33
+ }
34
+
35
+ .textarea:disabled {
36
+ opacity: 0.5;
37
+ cursor: not-allowed;
38
+ resize: none;
39
+ }
@@ -80,7 +80,7 @@
80
80
 
81
81
  .message {
82
82
  font-size: var(--sv-font-size-sm);
83
- color: var(--sv-color-text-secondary);
83
+ color: var(--sv-color-text-muted);
84
84
  margin-top: var(--sv-space-1);
85
85
  line-height: 1.4;
86
86
  }
@@ -91,9 +91,9 @@
91
91
  border: none;
92
92
  padding: 0;
93
93
  cursor: pointer;
94
- color: var(--sv-color-text-tertiary);
94
+ color: var(--sv-color-text-subtle);
95
95
  line-height: 1;
96
- font-size: var(--sv-font-size-base);
96
+ font-size: var(--sv-font-size-md);
97
97
  }
98
98
 
99
99
  .close:hover {
@@ -40,7 +40,7 @@
40
40
  background: var(--sv-white);
41
41
  transition: transform 0.15s ease;
42
42
  left: 2px;
43
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
43
+ box-shadow: var(--sv-shadow-control);
44
44
  }
45
45
 
46
46
  .on .thumb {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, ButtonHTMLAttributes, SelectHTMLAttributes, ReactElement, InputHTMLAttributes, HTMLAttributes } from 'react';
2
+ import { ReactNode, ButtonHTMLAttributes, SelectHTMLAttributes, ReactElement, InputHTMLAttributes, TextareaHTMLAttributes, HTMLAttributes } from 'react';
3
3
 
4
4
  type BadgeVariant = 'role' | 'status' | 'mono';
5
5
  type BadgeStatus = 'active' | 'enabled' | 'deactivated' | 'failed' | 'invited' | 'pending' | 'neutral';
@@ -121,6 +121,12 @@ type SelectProps = Omit<SelectHTMLAttributes<HTMLSelectElement>, 'size'> & {
121
121
  * Uses the native picker on mobile — no custom dropdown, maximum a11y coverage.
122
122
  *
123
123
  * RSC-safe: presentational, no hooks, forwards all native select props.
124
+ *
125
+ * `className` sizes the outer box (the wrapper), not the `<select>` itself —
126
+ * the chevron is absolutely positioned against the wrapper, so a className
127
+ * that constrains only the `<select>` (e.g. `max-width`) shrinks the select
128
+ * while the wrapper (and thus the chevron) stays full width, leaving the
129
+ * chevron stranded past the visible box.
124
130
  */
125
131
  declare function Select({ className, size, children, ...rest }: SelectProps): react.JSX.Element;
126
132
 
@@ -184,6 +190,15 @@ type InputProps = InputHTMLAttributes<HTMLInputElement>;
184
190
  */
185
191
  declare function Input({ type, className, ...rest }: InputProps): react.JSX.Element;
186
192
 
193
+ type TextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement>;
194
+ /**
195
+ * Textarea — the primitive multi-line text field. Presentational and
196
+ * RSC-safe: it forwards all native textarea props to the underlying
197
+ * `<textarea>`. Styling references `--sv-*` tokens via CSS Modules; there
198
+ * are no hardcoded values.
199
+ */
200
+ declare function Textarea({ className, rows, ...rest }: TextareaProps): react.JSX.Element;
201
+
187
202
  type DialogSize = 'sm' | 'md' | 'lg' | 'full';
188
203
  interface DialogProps {
189
204
  /** Whether the dialog is shown. When false, nothing renders. */
@@ -262,16 +277,32 @@ interface CardProps extends HTMLAttributes<HTMLElement> {
262
277
  }
263
278
  declare function Card({ as: Tag, interactive, padding, className, children, ...rest }: CardProps): react.JSX.Element;
264
279
 
280
+ /** Props to spread onto the field's actual form control. */
281
+ interface FormFieldRenderProps {
282
+ id: string;
283
+ 'aria-describedby'?: string;
284
+ 'aria-invalid'?: boolean;
285
+ required?: boolean;
286
+ }
265
287
  interface FormFieldProps {
266
288
  label: string;
267
289
  hint?: string;
268
290
  error?: string;
269
- htmlFor?: string;
291
+ /** Explicit control id. Auto-generated via `useId()` when omitted. */
292
+ id?: string;
270
293
  required?: boolean;
271
- children: ReactNode;
272
294
  className?: string;
295
+ /** Render prop — receives the props that must be spread onto the control
296
+ * so the label, hint, and error stay associated with it. */
297
+ children: (field: FormFieldRenderProps) => ReactNode;
273
298
  }
274
- declare function FormField({ label, hint, error, htmlFor, required, children, className, }: FormFieldProps): react.JSX.Element;
299
+ /**
300
+ * FormField — accessible label + hint/error wrapper for a single form control.
301
+ * Wires `htmlFor`/`id` and `aria-describedby`/`aria-invalid` onto the control
302
+ * itself (via the render-prop `field` object), not a surrounding element, so
303
+ * screen readers reliably announce the hint or error as part of the control.
304
+ */
305
+ declare function FormField({ label, hint, error, id, required, className, children, }: FormFieldProps): react.JSX.Element;
275
306
 
276
307
  interface PageHeaderProps {
277
308
  title: string;
@@ -324,4 +355,26 @@ interface TooltipProps {
324
355
  }
325
356
  declare function Tooltip({ content, children, side }: TooltipProps): react.JSX.Element;
326
357
 
327
- export { Avatar, type AvatarProps, Badge, type BadgeProps, type BadgeStatus, type BadgeVariant, type BannerVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Card, type CardProps, Dialog, type DialogProps, type DialogSize, Drawer, type DrawerProps, EmptyState, type EmptyStateProps, FormField, type FormFieldProps, Icon, type IconName, type IconProps, Input, type InputProps, type NavTabItem, NavTabs, type NavTabsProps, PageHeader, type PageHeaderProps, Popover, type PopoverProps, SegmentedControl, type SegmentedControlProps, type SegmentedOption, Select, type SelectProps, Spinner, type SpinnerProps, SystemBanner, type SystemBannerProps, type TabItem, Tabs, type TabsProps, type ToastContextValue, type ToastItem, ToastProvider, Toggle, type ToggleProps, Tooltip, type TooltipProps, useToast };
358
+ interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'onChange'> {
359
+ checked: boolean;
360
+ onChange: (checked: boolean) => void;
361
+ label: string;
362
+ /** Renders the label with a strike-through when checked. */
363
+ strikeThrough?: boolean;
364
+ disabled?: boolean;
365
+ }
366
+ declare function Checkbox({ checked, onChange, label, strikeThrough, disabled, className, id, ...rest }: CheckboxProps): react.JSX.Element;
367
+
368
+ interface DragHandleRowProps extends HTMLAttributes<HTMLDivElement> {
369
+ children: ReactNode;
370
+ /** Pass drag-listener and drag-control props from dnd-kit here. */
371
+ handleProps?: HTMLAttributes<HTMLButtonElement>;
372
+ isDragging?: boolean;
373
+ }
374
+ /**
375
+ * A row wrapper that surfaces a drag handle on the left.
376
+ * Consumers attach dnd-kit's useSortable listeners to `handleProps`.
377
+ */
378
+ declare function DragHandleRow({ children, handleProps, isDragging, className, ...rest }: DragHandleRowProps): react.JSX.Element;
379
+
380
+ export { Avatar, type AvatarProps, Badge, type BadgeProps, type BadgeStatus, type BadgeVariant, type BannerVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Card, type CardProps, Checkbox, type CheckboxProps, Dialog, type DialogProps, type DialogSize, DragHandleRow, type DragHandleRowProps, Drawer, type DrawerProps, EmptyState, type EmptyStateProps, FormField, type FormFieldProps, type FormFieldRenderProps, Icon, type IconName, type IconProps, Input, type InputProps, type NavTabItem, NavTabs, type NavTabsProps, PageHeader, type PageHeaderProps, Popover, type PopoverProps, SegmentedControl, type SegmentedControlProps, type SegmentedOption, Select, type SelectProps, Spinner, type SpinnerProps, SystemBanner, type SystemBannerProps, type TabItem, Tabs, type TabsProps, Textarea, type TextareaProps, type ToastContextValue, type ToastItem, ToastProvider, Toggle, type ToggleProps, Tooltip, type TooltipProps, useToast };