@sovereignfs/ui 0.33.1 → 0.40.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 (54) hide show
  1. package/README.md +1 -1
  2. package/dist/CheckableListRow.module.css +95 -0
  3. package/dist/FileDropzone.module.css +63 -0
  4. package/dist/IconPicker.module.css +63 -0
  5. package/dist/QuantityStepper.module.css +67 -0
  6. package/dist/SuggestionInput.module.css +103 -0
  7. package/dist/index.d.ts +196 -3
  8. package/dist/index.js +1175 -185
  9. package/package.json +1 -1
  10. package/src/components/CheckableListRow/CheckableListRow.module.css +95 -0
  11. package/src/components/CheckableListRow/CheckableListRow.stories.tsx +99 -0
  12. package/src/components/CheckableListRow/CheckableListRow.tsx +89 -0
  13. package/src/components/FileDropzone/FileDropzone.module.css +63 -0
  14. package/src/components/FileDropzone/FileDropzone.tsx +109 -0
  15. package/src/components/FileDropzone/__tests__/FileDropzone.test.tsx +67 -0
  16. package/src/components/Icon/icons/apple.tsx +21 -0
  17. package/src/components/Icon/icons/banana.tsx +21 -0
  18. package/src/components/Icon/icons/beef.tsx +22 -0
  19. package/src/components/Icon/icons/beer.tsx +24 -0
  20. package/src/components/Icon/icons/candy.tsx +24 -0
  21. package/src/components/Icon/icons/carrot.tsx +22 -0
  22. package/src/components/Icon/icons/coffee.tsx +23 -0
  23. package/src/components/Icon/icons/cookie.tsx +25 -0
  24. package/src/components/Icon/icons/croissant.tsx +24 -0
  25. package/src/components/Icon/icons/cup-soda.tsx +23 -0
  26. package/src/components/Icon/icons/drumstick.tsx +21 -0
  27. package/src/components/Icon/icons/egg.tsx +20 -0
  28. package/src/components/Icon/icons/external-link.tsx +22 -0
  29. package/src/components/Icon/icons/file.tsx +21 -0
  30. package/src/components/Icon/icons/fish.tsx +25 -0
  31. package/src/components/Icon/icons/index.ts +93 -1
  32. package/src/components/Icon/icons/milk.tsx +22 -0
  33. package/src/components/Icon/icons/pizza.tsx +24 -0
  34. package/src/components/Icon/icons/salad.tsx +24 -0
  35. package/src/components/Icon/icons/shopping-basket.tsx +26 -0
  36. package/src/components/Icon/icons/snowflake.tsx +31 -0
  37. package/src/components/Icon/icons/spray-can.tsx +29 -0
  38. package/src/components/Icon/icons/upload.tsx +22 -0
  39. package/src/components/Icon/icons/wine.tsx +23 -0
  40. package/src/components/IconPicker/IconPicker.module.css +63 -0
  41. package/src/components/IconPicker/IconPicker.stories.tsx +84 -0
  42. package/src/components/IconPicker/IconPicker.tsx +90 -0
  43. package/src/components/QuantityStepper/QuantityStepper.module.css +67 -0
  44. package/src/components/QuantityStepper/QuantityStepper.stories.tsx +79 -0
  45. package/src/components/QuantityStepper/QuantityStepper.tsx +107 -0
  46. package/src/components/SuggestionInput/SuggestionInput.module.css +103 -0
  47. package/src/components/SuggestionInput/SuggestionInput.stories.tsx +144 -0
  48. package/src/components/SuggestionInput/SuggestionInput.tsx +207 -0
  49. package/src/hooks/index.ts +2 -0
  50. package/src/hooks/useCommitOnEnterOrBlur.ts +39 -0
  51. package/src/index.ts +21 -2
  52. package/src/stories/DesignSystemOverview.stories.tsx +134 -1
  53. package/src/stories/FileDropzone.stories.tsx +69 -0
  54. package/src/stories/InteractionHooks.stories.tsx +55 -1
package/README.md CHANGED
@@ -19,7 +19,7 @@ npm install @sovereignfs/ui
19
19
  import { Button, Input, Card, Dialog, Drawer } from '@sovereignfs/ui';
20
20
  ```
21
21
 
22
- Available components: `Avatar`, `Badge`, `Button`, `Card`, `Dialog`, `Drawer`, `EmptyState`, `FormField`, `Icon`, `Input`, `NavTabs`, `PageHeader`, `Popover`, `SegmentedControl`, `Select`, `Spinner`, `SystemBanner`, `Tabs`, `Toast` / `useToast`, `Toggle`, `Tooltip`.
22
+ Available components and hooks: `Avatar`, `Badge`, `Button`, `Calendar`, `Card`, `CheckableListRow`, `Checkbox`, `CodeTextarea`, `ConfirmDialog`, `DatePicker`, `Dialog`, `DragHandleRow`, `Drawer`, `EmptyState`, `FormField`, `Icon`, `IconPicker`, `Input`, `Menu`, `NavTabs`, `OverlayHeader`, `PageHeader`, `Popover`, `QuantityStepper`, `SegmentedControl`, `Select`, `Sheet`, `Spinner`, `SplitPane`, `StatusBadge`, `SuggestionInput`, `SystemBanner`, `Tabs`, `TagInput`, `Textarea`, `Toast` / `useToast`, `Toggle`, `Tooltip`, `useDoubleTapHandler`, `useIsMobile`, `useLongPress`, and `useSingleOrDoubleTap`.
23
23
 
24
24
  ### Tokens
25
25
 
@@ -0,0 +1,95 @@
1
+ .row {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: var(--sv-space-3);
5
+ width: 100%;
6
+ }
7
+
8
+ .tapTarget {
9
+ display: flex;
10
+ align-items: center;
11
+ gap: var(--sv-space-2);
12
+ flex: 1;
13
+ min-width: 0;
14
+ min-height: var(--sv-touch-target-min);
15
+ cursor: pointer;
16
+ }
17
+
18
+ .tapTarget:focus-visible {
19
+ outline: 2px solid var(--sv-color-focus-ring);
20
+ outline-offset: 2px;
21
+ border-radius: var(--sv-radius-sm);
22
+ }
23
+
24
+ /* Visual box — matches Checkbox.module.css's .box/.checked/.tick. */
25
+ .box {
26
+ --checkbox-box-size: 18px;
27
+ flex-shrink: 0;
28
+ width: var(--checkbox-box-size);
29
+ height: var(--checkbox-box-size);
30
+ border-radius: var(--sv-radius-sm);
31
+ border: 1.5px solid var(--sv-color-border-strong);
32
+ background: var(--sv-color-surface);
33
+ transition:
34
+ background-color 0.15s ease,
35
+ border-color 0.15s ease;
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ }
40
+
41
+ .checked {
42
+ background-color: var(--sv-color-accent);
43
+ border-color: var(--sv-color-accent);
44
+ color: var(--sv-white);
45
+ }
46
+
47
+ .tick {
48
+ width: 10px;
49
+ height: 8px;
50
+ pointer-events: none;
51
+ }
52
+
53
+ .icon {
54
+ display: flex;
55
+ align-items: center;
56
+ flex-shrink: 0;
57
+ color: var(--sv-color-text-muted);
58
+ }
59
+
60
+ .label {
61
+ flex: 1;
62
+ min-width: 0;
63
+ font-size: var(--sv-font-size-sm);
64
+ color: var(--sv-color-text-primary);
65
+ overflow: hidden;
66
+ text-overflow: ellipsis;
67
+ white-space: nowrap;
68
+ transition: color 0.15s ease;
69
+ position: relative;
70
+ }
71
+
72
+ .label::after {
73
+ content: '';
74
+ position: absolute;
75
+ left: 0;
76
+ top: 50%;
77
+ width: 100%;
78
+ height: 1.5px;
79
+ background: var(--sv-color-text-muted);
80
+ transform: scaleX(0);
81
+ transform-origin: left center;
82
+ transition: transform 0.2s ease;
83
+ }
84
+
85
+ .struck {
86
+ color: var(--sv-color-text-muted);
87
+ }
88
+
89
+ .struck::after {
90
+ transform: scaleX(1);
91
+ }
92
+
93
+ .trailing {
94
+ flex-shrink: 0;
95
+ }
@@ -0,0 +1,63 @@
1
+ .dropzone {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: var(--sv-space-4);
5
+ width: 100%;
6
+ box-sizing: border-box;
7
+ padding: var(--sv-space-4) var(--sv-space-5);
8
+ border: 1.5px dashed var(--sv-color-border);
9
+ border-radius: var(--sv-radius-md);
10
+ background-color: var(--sv-color-surface-raised);
11
+ cursor: pointer;
12
+ transition:
13
+ background-color 0.12s ease,
14
+ border-color 0.12s ease;
15
+ }
16
+
17
+ @media (hover: hover) {
18
+ .dropzone:hover {
19
+ background-color: var(--sv-color-surface-sunken);
20
+ border-color: var(--sv-color-border-strong);
21
+ }
22
+ }
23
+
24
+ .dropzoneActive {
25
+ background-color: var(--sv-color-surface-sunken);
26
+ border-color: var(--sv-color-border-strong);
27
+ }
28
+
29
+ .dropzoneDisabled {
30
+ opacity: 0.5;
31
+ cursor: not-allowed;
32
+ }
33
+
34
+ .icon {
35
+ display: inline-flex;
36
+ flex-shrink: 0;
37
+ color: var(--sv-color-text-muted);
38
+ }
39
+
40
+ .text {
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: 2px;
44
+ min-width: 0;
45
+ }
46
+
47
+ .title {
48
+ font-size: var(--sv-font-size-sm);
49
+ font-weight: var(--sv-font-weight-medium);
50
+ color: var(--sv-color-text-primary);
51
+ overflow: hidden;
52
+ text-overflow: ellipsis;
53
+ white-space: nowrap;
54
+ }
55
+
56
+ .hint {
57
+ font-size: var(--sv-font-size-xs);
58
+ color: var(--sv-color-text-muted);
59
+ }
60
+
61
+ .hiddenInput {
62
+ display: none;
63
+ }
@@ -0,0 +1,63 @@
1
+ .trigger {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: var(--sv-space-2);
5
+ padding: var(--sv-space-2);
6
+ border: 1px solid var(--sv-color-border-strong);
7
+ border-radius: var(--sv-radius-md);
8
+ background: var(--sv-color-surface);
9
+ color: var(--sv-color-text-primary);
10
+ cursor: pointer;
11
+ }
12
+
13
+ .trigger:hover {
14
+ background: var(--sv-color-surface-sunken);
15
+ }
16
+
17
+ .trigger:disabled {
18
+ opacity: 0.5;
19
+ cursor: not-allowed;
20
+ }
21
+
22
+ .placeholder {
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ width: var(--sv-icon-size-md);
27
+ height: var(--sv-icon-size-md);
28
+ color: var(--sv-color-text-subtle);
29
+ font-size: var(--sv-font-size-sm);
30
+ }
31
+
32
+ .triggerLabel {
33
+ font-size: var(--sv-font-size-sm);
34
+ color: var(--sv-color-text-primary);
35
+ }
36
+
37
+ .grid {
38
+ display: grid;
39
+ grid-template-columns: repeat(5, 1fr);
40
+ gap: var(--sv-space-1);
41
+ padding: var(--sv-space-2);
42
+ }
43
+
44
+ .option {
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ width: var(--sv-touch-target-min);
49
+ height: var(--sv-touch-target-min);
50
+ border: none;
51
+ border-radius: var(--sv-radius-sm);
52
+ background: transparent;
53
+ color: var(--sv-color-text-primary);
54
+ cursor: pointer;
55
+ }
56
+
57
+ .option:hover {
58
+ background: var(--sv-color-surface-sunken);
59
+ }
60
+
61
+ .optionSelected {
62
+ background: var(--sv-color-accent-subtle);
63
+ }
@@ -0,0 +1,67 @@
1
+ .root {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: var(--sv-space-1);
5
+ padding: var(--sv-space-1);
6
+ border: 1px solid var(--sv-color-border-strong);
7
+ border-radius: var(--sv-radius-full);
8
+ background: var(--sv-color-surface-sunken);
9
+ }
10
+
11
+ .button {
12
+ all: unset;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ width: var(--sv-space-6);
17
+ height: var(--sv-space-6);
18
+ border-radius: var(--sv-radius-full);
19
+ color: var(--sv-color-text-primary);
20
+ font-size: var(--sv-font-size-md);
21
+ line-height: 1;
22
+ cursor: pointer;
23
+ }
24
+
25
+ .button:hover:not(:disabled) {
26
+ background: var(--sv-color-surface);
27
+ }
28
+
29
+ .button:disabled {
30
+ color: var(--sv-color-text-subtle);
31
+ cursor: not-allowed;
32
+ }
33
+
34
+ .input {
35
+ all: unset;
36
+ width: 3ch;
37
+ text-align: center;
38
+ font-family: var(--sv-font-family);
39
+ font-size: var(--sv-font-size-sm);
40
+ color: var(--sv-color-text-primary);
41
+ /* Hide the native spinner — the +/- buttons are the stepper affordance,
42
+ a second one from the browser would be redundant and often looks
43
+ inconsistent across browsers anyway. */
44
+ -moz-appearance: textfield;
45
+ }
46
+
47
+ .input::-webkit-outer-spin-button,
48
+ .input::-webkit-inner-spin-button {
49
+ -webkit-appearance: none;
50
+ margin: 0;
51
+ }
52
+
53
+ .input:focus-visible {
54
+ outline: none;
55
+ box-shadow: 0 0 0 2px var(--sv-color-focus-ring);
56
+ border-radius: var(--sv-radius-sm);
57
+ }
58
+
59
+ .input:disabled {
60
+ color: var(--sv-color-text-muted);
61
+ }
62
+
63
+ .unit {
64
+ font-size: var(--sv-font-size-xs);
65
+ color: var(--sv-color-text-subtle);
66
+ white-space: nowrap;
67
+ }
@@ -0,0 +1,103 @@
1
+ .input {
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
+ transition:
13
+ border-color 0.15s ease,
14
+ box-shadow 0.15s ease;
15
+ }
16
+
17
+ /* iOS Safari auto-zooms on focused inputs under 16px — see Input.module.css
18
+ for the full rationale; kept in sync here since this isn't built on Input. */
19
+ @media (pointer: coarse) {
20
+ .input {
21
+ font-size: var(--sv-font-size-md);
22
+ }
23
+ }
24
+
25
+ .input::placeholder {
26
+ color: var(--sv-color-text-muted);
27
+ }
28
+
29
+ .input:focus-visible {
30
+ outline: none;
31
+ border-color: var(--sv-color-accent);
32
+ box-shadow: 0 0 0 2px var(--sv-color-focus-ring);
33
+ }
34
+
35
+ .input:disabled {
36
+ opacity: 0.5;
37
+ cursor: not-allowed;
38
+ }
39
+
40
+ .list {
41
+ list-style: none;
42
+ margin: 0;
43
+ padding: var(--sv-space-1);
44
+ max-height: 320px;
45
+ overflow-y: auto;
46
+ }
47
+
48
+ .status {
49
+ padding: var(--sv-space-2) var(--sv-space-3);
50
+ font-size: var(--sv-font-size-sm);
51
+ color: var(--sv-color-text-muted);
52
+ }
53
+
54
+ .option {
55
+ display: flex;
56
+ align-items: center;
57
+ gap: var(--sv-space-2);
58
+ width: 100%;
59
+ padding: var(--sv-space-2) var(--sv-space-3);
60
+ border: none;
61
+ border-radius: var(--sv-radius-sm);
62
+ background: transparent;
63
+ font-family: var(--sv-font-family);
64
+ font-size: var(--sv-font-size-sm);
65
+ color: var(--sv-color-text-primary);
66
+ text-align: left;
67
+ cursor: pointer;
68
+ }
69
+
70
+ .option:hover,
71
+ .optionActive {
72
+ background: var(--sv-color-surface-sunken);
73
+ }
74
+
75
+ .optionIcon {
76
+ display: flex;
77
+ align-items: center;
78
+ flex-shrink: 0;
79
+ }
80
+
81
+ .optionLabel {
82
+ flex: 1;
83
+ min-width: 0;
84
+ overflow: hidden;
85
+ text-overflow: ellipsis;
86
+ white-space: nowrap;
87
+ }
88
+
89
+ .optionMeta {
90
+ flex-shrink: 0;
91
+ font-size: var(--sv-font-size-xs);
92
+ color: var(--sv-color-text-subtle);
93
+ }
94
+
95
+ .optionCreate {
96
+ font-style: italic;
97
+ color: var(--sv-color-text-muted);
98
+ }
99
+
100
+ .divider {
101
+ margin: var(--sv-space-1) var(--sv-space-2);
102
+ border-top: 1px solid var(--sv-color-border);
103
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, ButtonHTMLAttributes, SelectHTMLAttributes, ReactElement, CSSProperties, InputHTMLAttributes, TextareaHTMLAttributes, HTMLAttributes, PointerEvent, MouseEvent } from 'react';
2
+ import { ReactNode, ButtonHTMLAttributes, SelectHTMLAttributes, ReactElement, CSSProperties, InputHTMLAttributes, TextareaHTMLAttributes, HTMLAttributes, PointerEvent, MouseEvent, KeyboardEvent } from 'react';
3
3
 
4
4
  type BadgeVariant = 'role' | 'status' | 'mono';
5
5
  type BadgeStatus = 'active' | 'enabled' | 'deactivated' | 'failed' | 'invited' | 'pending' | 'neutral';
@@ -18,7 +18,7 @@ interface BadgeProps {
18
18
  */
19
19
  declare function Badge({ variant, status, children }: BadgeProps): react.JSX.Element;
20
20
 
21
- type IconName = 'house' | 'settings' | 'log-out' | 'chevron-right' | 'chevron-left' | 'chevron-down' | 'chevron-up' | 'x' | 'check' | 'plus' | 'trash-2' | 'pencil' | 'rotate-ccw' | 'search' | 'user' | 'shield' | 'lock' | 'eye' | 'eye-off' | 'mail' | 'bell' | 'activity' | 'package' | 'grid-2x2' | 'info' | 'alert-triangle' | 'calendar' | 'sliders-horizontal' | 'ellipsis-vertical';
21
+ type IconName = 'house' | 'settings' | 'log-out' | 'chevron-right' | 'chevron-left' | 'chevron-down' | 'chevron-up' | 'x' | 'check' | 'plus' | 'trash-2' | 'pencil' | 'rotate-ccw' | 'search' | 'user' | 'shield' | 'lock' | 'eye' | 'eye-off' | 'mail' | 'bell' | 'activity' | 'package' | 'grid-2x2' | 'info' | 'alert-triangle' | 'calendar' | 'sliders-horizontal' | 'ellipsis-vertical' | 'file' | 'upload' | 'external-link' | 'banana' | 'apple' | 'carrot' | 'egg' | 'beef' | 'fish' | 'coffee' | 'wine' | 'beer' | 'cookie' | 'pizza' | 'candy' | 'salad' | 'milk' | 'drumstick' | 'croissant' | 'cup-soda' | 'spray-can' | 'snowflake' | 'shopping-basket';
22
22
 
23
23
  /** Decorative icon — visually meaningful but described by surrounding text. */
24
24
  type DecorativeProps = {
@@ -701,6 +701,45 @@ interface DragHandleRowProps extends HTMLAttributes<HTMLDivElement> {
701
701
  */
702
702
  declare function DragHandleRow({ children, handleProps, isDragging, className, ...rest }: DragHandleRowProps): react.JSX.Element;
703
703
 
704
+ interface FileDropzoneProps {
705
+ /** Native `<input type="file">` `name` attribute — set this to submit the
706
+ * file as part of a native `<form action={...}>` (Server Action) request. */
707
+ name?: string;
708
+ /** Native `<input type="file">` `accept` attribute, e.g. `".zip,application/zip"`. */
709
+ accept?: string;
710
+ /** Icon shown beside the label — a file-type glyph. Defaults to a generic file icon. */
711
+ icon?: ReactNode;
712
+ /** Primary label — e.g. "Choose a ZIP file", or the selected file's name. */
713
+ label: string;
714
+ /** Secondary hint — e.g. "or drag and drop here", or the selected file's size. */
715
+ hint?: string;
716
+ /** Called with the picked or dropped file, or `null` if the input is cleared. */
717
+ onFileSelect: (file: File | null) => void;
718
+ /** Accessible name for the control — describe what will be uploaded. */
719
+ ariaLabel: string;
720
+ disabled?: boolean;
721
+ className?: string;
722
+ }
723
+ /**
724
+ * A styled drag-and-drop file picker: a dashed-border dropzone wrapping a
725
+ * visually-hidden native `<input type="file">`, so the native picker still
726
+ * opens on click and the field participates in normal form semantics. A
727
+ * dropped file is written into the input's own `FileList` (via `DataTransfer`)
728
+ * so `name` + native `<form action>` submission sees it exactly like a
729
+ * click-picked file — not just the `onFileSelect` callback.
730
+ *
731
+ * Caller owns the selected-file state and passes `label`/`hint` accordingly
732
+ * (e.g. swap to the picked file's name/size) — this component is otherwise
733
+ * uncontrolled and stateless beyond its own drag-hover visual.
734
+ *
735
+ * Do not pass `required` through to a wrapping form's validation via this
736
+ * field — `display: none` on the hidden input does not exempt it from HTML5
737
+ * constraint validation, so a `required` hidden input can block submission
738
+ * with no visible field for the user to focus. Validate `onFileSelect`'s
739
+ * result in application code instead.
740
+ */
741
+ declare function FileDropzone({ name, accept, icon, label, hint, onFileSelect, ariaLabel, disabled, className, }: FileDropzoneProps): react.JSX.Element;
742
+
704
743
  interface CalendarProps {
705
744
  /** Selected date, or `null` for no selection. */
706
745
  value: Date | null;
@@ -747,6 +786,132 @@ interface DatePickerProps {
747
786
  */
748
787
  declare function DatePicker({ value, onChange, minDate, maxDate, placeholder, 'aria-label': ariaLabel, disabled, }: DatePickerProps): react.JSX.Element;
749
788
 
789
+ interface SuggestionOption {
790
+ id: string;
791
+ label: string;
792
+ icon?: ReactNode;
793
+ /** Secondary text shown right-aligned on the row, e.g. "bought 4× recently". */
794
+ meta?: string;
795
+ }
796
+ interface SuggestionInputProps {
797
+ value: string;
798
+ onChange: (value: string) => void;
799
+ options: SuggestionOption[];
800
+ onSelect: (option: SuggestionOption) => void;
801
+ placeholder?: string;
802
+ 'aria-label': string;
803
+ id?: string;
804
+ disabled?: boolean;
805
+ /** Shown in place of the options list while a suggestion fetch is in flight. */
806
+ loading?: boolean;
807
+ /**
808
+ * Label for a trailing, always-selectable row that commits the raw typed
809
+ * text instead of a matched option — e.g. `(v) => \`Add "${v}" as a new item\`}`.
810
+ * Shown whenever `value` is non-empty and `onCreate` is provided, even
811
+ * when `options` is empty, so the input is never a dead end.
812
+ */
813
+ createLabel?: (value: string) => string;
814
+ onCreate?: (value: string) => void;
815
+ className?: string;
816
+ }
817
+ /**
818
+ * SuggestionInput — a text field with an anchored, keyboard-navigable list
819
+ * of async suggestions (a combobox), plus an optional trailing "create new"
820
+ * row so free text is always a valid commit, not just a matched option.
821
+ *
822
+ * Fully controlled: the caller owns `value`/`onChange` and supplies
823
+ * `options` (e.g. debounced server-fetched results) — this component only
824
+ * handles open/close, keyboard navigation, and selection. Built on `Popover`
825
+ * for positioning (`width: 'trigger'`, so the panel always matches the
826
+ * input's own width).
827
+ */
828
+ declare function SuggestionInput({ value, onChange, options, onSelect, placeholder, 'aria-label': ariaLabel, id, disabled, loading, createLabel, onCreate, className, }: SuggestionInputProps): react.JSX.Element;
829
+
830
+ interface IconPickerProps {
831
+ value: IconName | null;
832
+ onChange: (icon: IconName) => void;
833
+ /** Icons offered in the grid, in display order. */
834
+ options: readonly IconName[];
835
+ 'aria-label': string;
836
+ /** Shown next to the trigger's icon preview, e.g. the item's name. */
837
+ triggerLabel?: string;
838
+ disabled?: boolean;
839
+ className?: string;
840
+ }
841
+ /**
842
+ * IconPicker — a trigger button showing the current icon that opens a
843
+ * `Popover` grid of selectable icons. Built for cases with a curated,
844
+ * bounded icon set (e.g. Sovereign Shopper's grocery category icons,
845
+ * SHP-05) rather than the full design-system icon library.
846
+ *
847
+ * Selecting an icon both closes the picker and calls `onChange` — a
848
+ * consumer's `onChange` never needs to close the picker itself.
849
+ */
850
+ declare function IconPicker({ value, onChange, options, 'aria-label': ariaLabel, triggerLabel, disabled, className, }: IconPickerProps): react.JSX.Element;
851
+
852
+ interface QuantityStepperProps {
853
+ value: number;
854
+ onChange: (value: number) => void;
855
+ /** Smallest selectable value. Default `0`. */
856
+ min?: number;
857
+ /** Largest selectable value. Default unbounded. */
858
+ max?: number;
859
+ /** Amount +/- adjusts by. Default `1` — pass e.g. `0.5` for fractional
860
+ * quantities (kg, L). */
861
+ step?: number;
862
+ /** Read-only suffix shown after the number, e.g. "pcs" or "kg" — for
863
+ * compact contexts (a list row) where a separate unit selector doesn't
864
+ * fit. Editable unit selection (a `<Select>` of units) lives outside
865
+ * this component. */
866
+ unit?: string;
867
+ 'aria-label': string;
868
+ disabled?: boolean;
869
+ id?: string;
870
+ className?: string;
871
+ }
872
+ /**
873
+ * QuantityStepper — a numeric input with +/- buttons and an optional
874
+ * read-only unit suffix. Built for grocery/inventory-style quantities
875
+ * (Sovereign Shopper, SHP-06) — supports fractional `step` values since
876
+ * quantities like "1.5 kg" are common, unlike a typical integer counter.
877
+ *
878
+ * The middle field is a real `<input type="number">` so keyboard entry,
879
+ * paste, and the browser's native numeric affordances all work — the
880
+ * buttons are a convenience on top, not the only way to change the value.
881
+ */
882
+ declare function QuantityStepper({ value, onChange, min, max, step, unit, 'aria-label': ariaLabel, disabled, id, className, }: QuantityStepperProps): react.JSX.Element;
883
+
884
+ interface CheckableListRowProps {
885
+ checked: boolean;
886
+ onCheckedChange: (checked: boolean) => void;
887
+ label: string;
888
+ /** Leading icon, e.g. a per-item `<Icon>`. */
889
+ icon?: ReactNode;
890
+ /** Trailing content, e.g. a quantity, a `QuantityStepper`, or a badge. */
891
+ trailing?: ReactNode;
892
+ disabled?: boolean;
893
+ className?: string;
894
+ }
895
+ /**
896
+ * CheckableListRow — a whole-row tap target that toggles a checked state,
897
+ * with strike-through on the label when checked. Built for "tap the row to
898
+ * mark it done" lists (Sovereign Shopper's tap-to-buy, SHP-07) where the
899
+ * checkbox itself is too small a target on mobile and the row already has
900
+ * nothing else competing for the tap.
901
+ *
902
+ * Renders as `role="checkbox"` on the row itself rather than composing
903
+ * `Checkbox`'s native `<input>` — nesting a real checkbox input inside the
904
+ * row's own click handler would mean two overlapping interactive elements
905
+ * (invalid HTML, and the input's own click would double-fire the row's
906
+ * toggle). The visual box matches `Checkbox`'s styling for consistency.
907
+ *
908
+ * `trailing` content (e.g. a `QuantityStepper`) is rendered outside the
909
+ * `role="checkbox"` element so its own interactive controls (+/- buttons)
910
+ * don't sit inside another interactive element either — clicking those
911
+ * doesn't toggle the row.
912
+ */
913
+ declare function CheckableListRow({ checked, onCheckedChange, label, icon, trailing, disabled, className, }: CheckableListRowProps): react.JSX.Element;
914
+
750
915
  interface UseLongPressOptions {
751
916
  /** Called once the press has been held for `delay` without exceeding `moveTolerance`. */
752
917
  onLongPress: () => void;
@@ -853,4 +1018,32 @@ declare const MOBILE_BREAKPOINT_PX = 768;
853
1018
  */
854
1019
  declare function useIsMobile(breakpointPx?: number): boolean;
855
1020
 
856
- export { Avatar, type AvatarProps, Badge, type BadgeProps, type BadgeStatus, type BadgeVariant, type BannerVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Calendar, type CalendarProps, Card, type CardProps, Checkbox, type CheckboxProps, CodeTextarea, type CodeTextareaProps, ConfirmDialog, type ConfirmDialogProps, DatePicker, type DatePickerProps, 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 LongPressHandlers, MOBILE_BREAKPOINT_PX, Menu, type MenuEntry, type MenuItem, type MenuLabel, type MenuProps, type MenuSeparator, type NavTabItem, NavTabs, type NavTabsProps, OverlayHeader, type OverlayHeaderProps, PageHeader, type PageHeaderProps, Popover, type PopoverProps, SegmentedControl, type SegmentedControlProps, type SegmentedOption, Select, type SelectProps, Sheet, type SheetProps, Spinner, type SpinnerProps, SplitPane, type SplitPaneProps, StatusBadge, type StatusBadgeProps, type StatusBadgeStatus, SystemBanner, type SystemBannerProps, type TabItem, Tabs, type TabsProps, TagInput, type TagInputProps, Textarea, type TextareaProps, type ToastContextValue, type ToastItem, ToastProvider, Toggle, type ToggleProps, Tooltip, type TooltipProps, type UseLongPressOptions, useDoubleTapHandler, useIsMobile, useLongPress, useOverlaySecondRow, useSingleOrDoubleTap, useToast };
1021
+ interface CommitOnEnterOrBlurHandlers {
1022
+ onKeyDown: (e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
1023
+ onBlur: () => void;
1024
+ }
1025
+ /**
1026
+ * useCommitOnEnterOrBlur — spreads onto a quick-entry input/textarea so
1027
+ * Enter and losing focus both commit through the same callback.
1028
+ *
1029
+ * iOS Safari adds its own "Previous / Next / Done" toolbar above the
1030
+ * software keyboard whenever more than one focusable field is nearby (a
1031
+ * Sheet with several fields, a form, etc.) — there is no supported way to
1032
+ * suppress it, since it's WebKit's own field-detection heuristic, not
1033
+ * something the page controls. Tapping that toolbar's Done/checkmark only
1034
+ * ever fires a native `blur`; it is not a form submit and dispatches no
1035
+ * keydown, so a field that commits only on Enter silently discards whatever
1036
+ * was typed the moment a user dismisses the keyboard that way instead of
1037
+ * pressing the on-screen Return key — the two dismissal paths look
1038
+ * identical to the user but produce different outcomes.
1039
+ *
1040
+ * `onCommit` is called unconditionally on both Enter and blur — it owns any
1041
+ * empty-value no-op itself, matching how every call site's own commit
1042
+ * function already guards against an empty value before doing anything.
1043
+ * Callers needing more key handling (e.g. Escape-to-cancel) compose their
1044
+ * own `onKeyDown` around the returned one rather than this hook growing
1045
+ * options for every caller's own key bindings.
1046
+ */
1047
+ declare function useCommitOnEnterOrBlur(onCommit: () => void): CommitOnEnterOrBlurHandlers;
1048
+
1049
+ export { Avatar, type AvatarProps, Badge, type BadgeProps, type BadgeStatus, type BadgeVariant, type BannerVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Calendar, type CalendarProps, Card, type CardProps, CheckableListRow, type CheckableListRowProps, Checkbox, type CheckboxProps, CodeTextarea, type CodeTextareaProps, type CommitOnEnterOrBlurHandlers, ConfirmDialog, type ConfirmDialogProps, DatePicker, type DatePickerProps, Dialog, type DialogProps, type DialogSize, DragHandleRow, type DragHandleRowProps, Drawer, type DrawerProps, EmptyState, type EmptyStateProps, FileDropzone, type FileDropzoneProps, FormField, type FormFieldProps, type FormFieldRenderProps, Icon, type IconName, IconPicker, type IconPickerProps, type IconProps, Input, type InputProps, type LongPressHandlers, MOBILE_BREAKPOINT_PX, Menu, type MenuEntry, type MenuItem, type MenuLabel, type MenuProps, type MenuSeparator, type NavTabItem, NavTabs, type NavTabsProps, OverlayHeader, type OverlayHeaderProps, PageHeader, type PageHeaderProps, Popover, type PopoverProps, QuantityStepper, type QuantityStepperProps, SegmentedControl, type SegmentedControlProps, type SegmentedOption, Select, type SelectProps, Sheet, type SheetProps, Spinner, type SpinnerProps, SplitPane, type SplitPaneProps, StatusBadge, type StatusBadgeProps, type StatusBadgeStatus, SuggestionInput, type SuggestionInputProps, type SuggestionOption, SystemBanner, type SystemBannerProps, type TabItem, Tabs, type TabsProps, TagInput, type TagInputProps, Textarea, type TextareaProps, type ToastContextValue, type ToastItem, ToastProvider, Toggle, type ToggleProps, Tooltip, type TooltipProps, type UseLongPressOptions, useCommitOnEnterOrBlur, useDoubleTapHandler, useIsMobile, useLongPress, useOverlaySecondRow, useSingleOrDoubleTap, useToast };