@sovereignfs/ui 0.37.0 → 0.41.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 (29) hide show
  1. package/dist/BalanceChip.module.css +32 -0
  2. package/dist/FileDropzone.module.css +63 -0
  3. package/dist/MemberMultiSelect.module.css +27 -0
  4. package/dist/index.d.ts +151 -3
  5. package/dist/index.js +630 -370
  6. package/package.json +1 -1
  7. package/src/components/BalanceChip/BalanceChip.module.css +32 -0
  8. package/src/components/BalanceChip/BalanceChip.stories.tsx +45 -0
  9. package/src/components/BalanceChip/BalanceChip.tsx +33 -0
  10. package/src/components/CurrencyInput/CurrencyInput.stories.tsx +50 -0
  11. package/src/components/CurrencyInput/CurrencyInput.tsx +60 -0
  12. package/src/components/FileDropzone/FileDropzone.module.css +63 -0
  13. package/src/components/FileDropzone/FileDropzone.tsx +109 -0
  14. package/src/components/FileDropzone/__tests__/FileDropzone.test.tsx +67 -0
  15. package/src/components/Icon/icons/external-link.tsx +22 -0
  16. package/src/components/Icon/icons/file.tsx +21 -0
  17. package/src/components/Icon/icons/index.ts +12 -0
  18. package/src/components/Icon/icons/upload.tsx +22 -0
  19. package/src/components/MemberMultiSelect/MemberMultiSelect.module.css +27 -0
  20. package/src/components/MemberMultiSelect/MemberMultiSelect.stories.tsx +74 -0
  21. package/src/components/MemberMultiSelect/MemberMultiSelect.tsx +67 -0
  22. package/src/components/SplitMethodSelector/SplitMethodSelector.stories.tsx +33 -0
  23. package/src/components/SplitMethodSelector/SplitMethodSelector.tsx +43 -0
  24. package/src/hooks/index.ts +2 -0
  25. package/src/hooks/useCommitOnEnterOrBlur.ts +39 -0
  26. package/src/index.ts +24 -2
  27. package/src/stories/DesignSystemOverview.stories.tsx +119 -1
  28. package/src/stories/FileDropzone.stories.tsx +69 -0
  29. package/src/stories/InteractionHooks.stories.tsx +55 -1
@@ -0,0 +1,32 @@
1
+ .chip {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ max-width: 100%;
5
+ min-height: 24px;
6
+ padding: 2px var(--sv-space-2);
7
+ border: 1px solid var(--sv-color-border);
8
+ border-radius: var(--sv-radius-full);
9
+ font-family: var(--sv-font-family);
10
+ font-size: var(--sv-font-size-xs);
11
+ font-weight: var(--sv-font-weight-semibold);
12
+ line-height: 1.2;
13
+ white-space: nowrap;
14
+ }
15
+
16
+ .owed {
17
+ color: var(--sv-color-success-text);
18
+ background: var(--sv-color-success-surface);
19
+ border-color: var(--sv-color-success-border);
20
+ }
21
+
22
+ .owes {
23
+ color: var(--sv-color-error-text);
24
+ background: var(--sv-color-error-surface);
25
+ border-color: var(--sv-color-error-border);
26
+ }
27
+
28
+ .settled {
29
+ color: var(--sv-color-text-muted);
30
+ background: var(--sv-color-surface-sunken);
31
+ border-color: var(--sv-color-border);
32
+ }
@@ -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,27 @@
1
+ .group {
2
+ display: grid;
3
+ gap: var(--sv-space-2);
4
+ }
5
+
6
+ .label {
7
+ font-size: var(--sv-font-size-xs);
8
+ font-weight: var(--sv-font-weight-semibold);
9
+ color: var(--sv-color-text-muted);
10
+ }
11
+
12
+ .row {
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: space-between;
16
+ gap: var(--sv-space-2);
17
+ }
18
+
19
+ .trailing {
20
+ flex-shrink: 0;
21
+ max-width: 8rem;
22
+ }
23
+
24
+ .hint {
25
+ font-size: var(--sv-font-size-xs);
26
+ color: var(--sv-color-text-muted);
27
+ }
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' | 'banana' | 'apple' | 'carrot' | 'egg' | 'beef' | 'fish' | 'coffee' | 'wine' | 'beer' | 'cookie' | 'pizza' | 'candy' | 'salad' | 'milk' | 'drumstick' | 'croissant' | 'cup-soda' | 'spray-can' | 'snowflake' | 'shopping-basket';
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;
@@ -873,6 +912,87 @@ interface CheckableListRowProps {
873
912
  */
874
913
  declare function CheckableListRow({ checked, onCheckedChange, label, icon, trailing, disabled, className, }: CheckableListRowProps): react.JSX.Element;
875
914
 
915
+ interface CurrencyInputProps extends Omit<InputProps, 'value' | 'onChange' | 'type' | 'inputMode'> {
916
+ /** Smallest currency unit (cents) — never a float. Null when the field is empty or unparsable. */
917
+ valueCents: number | null;
918
+ onValueChange: (cents: number | null) => void;
919
+ }
920
+ /**
921
+ * CurrencyInput — decimal amount entry that reports its value as integer
922
+ * cents, matching Sovereign's "amounts are always smallest-unit integers"
923
+ * data-model convention (never a float in application state).
924
+ *
925
+ * Keeps its own text buffer rather than deriving display text from
926
+ * `valueCents` on every keystroke — reformatting mid-type (e.g. "12." → the
927
+ * trailing decimal point) would fight the user's cursor. It only
928
+ * re-syncs from `valueCents` when the prop changes to something the current
929
+ * text doesn't already represent (an external reset/prefill), so typing
930
+ * remains uninterrupted while the parsed amount stays in lockstep with the
931
+ * parent's state.
932
+ */
933
+ declare function CurrencyInput({ valueCents, onValueChange, ...rest }: CurrencyInputProps): react.JSX.Element;
934
+
935
+ interface BalanceChipProps {
936
+ /** Smallest currency unit (cents). Positive = owed to them, negative = they owe, zero = settled. */
937
+ amountCents: number;
938
+ /** ISO 4217 currency code, e.g. "USD". */
939
+ currency: string;
940
+ className?: string;
941
+ }
942
+ /**
943
+ * BalanceChip — inline balance indicator (green = owed to them, red = they
944
+ * owe, neutral = settled up). For any plugin tracking who-owes-whom — not
945
+ * Tally-specific despite the naming; "amountCents" is a generic signed net
946
+ * balance, not tied to expense-splitting semantics.
947
+ */
948
+ declare function BalanceChip({ amountCents, currency, className }: BalanceChipProps): react.JSX.Element;
949
+
950
+ type SplitMethod = 'equal' | 'amount' | 'percentage' | 'shares';
951
+ interface SplitMethodSelectorProps {
952
+ value: SplitMethod;
953
+ onChange: (value: SplitMethod) => void;
954
+ size?: 'sm' | 'md';
955
+ 'aria-label'?: string;
956
+ }
957
+ /**
958
+ * SplitMethodSelector — the four-way equal/amount/percentage/shares picker
959
+ * shared by any plugin that splits a cost between people. A thin
960
+ * `SegmentedControl` preset: centralizes the option list and labels so every
961
+ * expense-splitting plugin presents the same four choices in the same order,
962
+ * instead of each one re-typing its own copy of this array.
963
+ */
964
+ declare function SplitMethodSelector({ value, onChange, size, 'aria-label': ariaLabel, }: SplitMethodSelectorProps): react.JSX.Element;
965
+
966
+ interface MemberMultiSelectOption {
967
+ id: string;
968
+ label: string;
969
+ }
970
+ interface MemberMultiSelectProps {
971
+ options: MemberMultiSelectOption[];
972
+ selectedIds: Set<string>;
973
+ onToggle: (id: string, checked: boolean) => void;
974
+ /** Rendered to the right of a selected row only — e.g. a per-person `CurrencyInput` or share count. */
975
+ renderTrailing?: (id: string) => ReactNode;
976
+ /** Section label above the list, e.g. "Paid by" or "Split between". */
977
+ label?: string;
978
+ /** Helper/validation text below the list, e.g. a running total. */
979
+ hint?: ReactNode;
980
+ className?: string;
981
+ }
982
+ /**
983
+ * MemberMultiSelect — checkbox list for picking any number of people from an
984
+ * already-resolved set of options (group/project members, collaborators,
985
+ * etc.). Deliberately domain-agnostic about *who* an option represents —
986
+ * instance users and guest members are both just `{id, label}` here, so
987
+ * nothing special is needed to include guests alongside instance users; the
988
+ * caller's `options` array is the only place that distinction exists.
989
+ *
990
+ * Not a directory search or an "add a new guest" flow — pair with
991
+ * `SuggestionInput` (search + add) for that; this component only selects
992
+ * among options already passed in.
993
+ */
994
+ declare function MemberMultiSelect({ options, selectedIds, onToggle, renderTrailing, label, hint, className, }: MemberMultiSelectProps): react.JSX.Element;
995
+
876
996
  interface UseLongPressOptions {
877
997
  /** Called once the press has been held for `delay` without exceeding `moveTolerance`. */
878
998
  onLongPress: () => void;
@@ -979,4 +1099,32 @@ declare const MOBILE_BREAKPOINT_PX = 768;
979
1099
  */
980
1100
  declare function useIsMobile(breakpointPx?: number): boolean;
981
1101
 
982
- 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, 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, 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, useDoubleTapHandler, useIsMobile, useLongPress, useOverlaySecondRow, useSingleOrDoubleTap, useToast };
1102
+ interface CommitOnEnterOrBlurHandlers {
1103
+ onKeyDown: (e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
1104
+ onBlur: () => void;
1105
+ }
1106
+ /**
1107
+ * useCommitOnEnterOrBlur — spreads onto a quick-entry input/textarea so
1108
+ * Enter and losing focus both commit through the same callback.
1109
+ *
1110
+ * iOS Safari adds its own "Previous / Next / Done" toolbar above the
1111
+ * software keyboard whenever more than one focusable field is nearby (a
1112
+ * Sheet with several fields, a form, etc.) — there is no supported way to
1113
+ * suppress it, since it's WebKit's own field-detection heuristic, not
1114
+ * something the page controls. Tapping that toolbar's Done/checkmark only
1115
+ * ever fires a native `blur`; it is not a form submit and dispatches no
1116
+ * keydown, so a field that commits only on Enter silently discards whatever
1117
+ * was typed the moment a user dismisses the keyboard that way instead of
1118
+ * pressing the on-screen Return key — the two dismissal paths look
1119
+ * identical to the user but produce different outcomes.
1120
+ *
1121
+ * `onCommit` is called unconditionally on both Enter and blur — it owns any
1122
+ * empty-value no-op itself, matching how every call site's own commit
1123
+ * function already guards against an empty value before doing anything.
1124
+ * Callers needing more key handling (e.g. Escape-to-cancel) compose their
1125
+ * own `onKeyDown` around the returned one rather than this hook growing
1126
+ * options for every caller's own key bindings.
1127
+ */
1128
+ declare function useCommitOnEnterOrBlur(onCommit: () => void): CommitOnEnterOrBlurHandlers;
1129
+
1130
+ export { Avatar, type AvatarProps, Badge, type BadgeProps, type BadgeStatus, type BadgeVariant, BalanceChip, type BalanceChipProps, 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, CurrencyInput, type CurrencyInputProps, 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, MemberMultiSelect, type MemberMultiSelectOption, type MemberMultiSelectProps, 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, type SplitMethod, SplitMethodSelector, type SplitMethodSelectorProps, 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 };