@syscore/ui-library 1.25.0 → 2.0.0-beta-publish-workflow-beta.1

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 (168) hide show
  1. package/client/App.tsx +0 -2
  2. package/client/assets/alert/icon-clear.svg +4 -0
  3. package/client/assets/alert/icon-done.svg +3 -0
  4. package/client/assets/avatar-identity/photo.png +0 -0
  5. package/client/assets/avatar-identity/placeholder-icon.svg +8 -0
  6. package/client/assets/breadcrumb/icon-ellipsis.svg +5 -0
  7. package/client/assets/breadcrumb/icon-home.svg +3 -0
  8. package/client/assets/breadcrumb/icon-separator.svg +3 -0
  9. package/client/assets/button-v01/icon-back.svg +4 -0
  10. package/client/assets/button-v01/icon-check.svg +3 -0
  11. package/client/assets/button-v01/icon-close.svg +4 -0
  12. package/client/assets/button-v01/icon-delete.svg +3 -0
  13. package/client/assets/button-v01/icon-device.svg +3 -0
  14. package/client/assets/dropdown/icon-carrot-contract.svg +3 -0
  15. package/client/assets/dropdown/icon-carrot-expand.svg +3 -0
  16. package/client/assets/dropdown/icon-clear.svg +4 -0
  17. package/client/assets/dropdown/icon-enter.svg +3 -0
  18. package/client/components/icons/UtilityChangeType.tsx +21 -0
  19. package/client/components/ui/alert-dialog-panel.tsx +246 -0
  20. package/client/components/ui/alert-v02.tsx +214 -0
  21. package/client/components/ui/app-bar.tsx +299 -0
  22. package/client/components/ui/avatar-identity.tsx +122 -0
  23. package/client/components/ui/banner.tsx +108 -0
  24. package/client/components/ui/bottom-navigation.tsx +223 -0
  25. package/client/components/ui/breadcrumb-trail.tsx +170 -0
  26. package/client/components/ui/button-v01.tsx +238 -0
  27. package/client/components/ui/button.tsx +2 -0
  28. package/client/components/ui/card.tsx +108 -26
  29. package/client/components/ui/code-badge.tsx +62 -0
  30. package/client/components/ui/date-picker.tsx +188 -0
  31. package/client/components/ui/dropdown.tsx +448 -0
  32. package/client/components/ui/empty-state.tsx +197 -0
  33. package/client/components/ui/file-upload.tsx +214 -0
  34. package/client/components/ui/footer.tsx +179 -0
  35. package/client/components/ui/layout.tsx +1182 -0
  36. package/client/components/ui/pagination-v01.tsx +157 -0
  37. package/client/components/ui/password-field-v01.tsx +67 -0
  38. package/client/components/ui/search-field-v01.tsx +192 -0
  39. package/client/components/ui/section-heading.tsx +49 -0
  40. package/client/components/ui/sidebar.tsx +13 -8
  41. package/client/components/ui/stepper.tsx +148 -0
  42. package/client/components/ui/switch-v01.tsx +71 -0
  43. package/client/components/ui/system-bar.tsx +119 -0
  44. package/client/components/ui/table-v01.tsx +230 -0
  45. package/client/components/ui/tabs-v01.tsx +99 -0
  46. package/client/components/ui/tag-v02.tsx +113 -0
  47. package/client/components/ui/text-area-v01.tsx +33 -0
  48. package/client/components/ui/text-field-v01.tsx +108 -0
  49. package/client/components/ui/timeline.tsx +181 -0
  50. package/client/components/ui/toast-v01.tsx +206 -0
  51. package/client/components/ui/tooltip.tsx +264 -71
  52. package/client/global.css +9565 -4378
  53. package/client/storybook.css +125 -0
  54. package/client/ui/Accordion.stories.tsx +1 -1
  55. package/client/ui/AlertDialogPanel/AlertDialogPanel.stories.tsx +150 -0
  56. package/client/ui/AlertV02/AlertV02.stories.tsx +188 -0
  57. package/client/ui/AppBar/app-bar.stories.tsx +280 -0
  58. package/client/ui/AspectRatio.stories.tsx +1 -1
  59. package/client/ui/AvatarIdentity/AvatarIdentity.stories.tsx +77 -0
  60. package/client/ui/Banner/banner.stories.tsx +238 -0
  61. package/client/ui/BottomNavigation/bottom-navigation.stories.tsx +285 -0
  62. package/client/ui/BreadcrumbTrail/BreadcrumbTrail.stories.tsx +232 -0
  63. package/client/ui/Button.stories.tsx +1 -1
  64. package/client/ui/ButtonV01/ButtonV01.stories.tsx +417 -0
  65. package/client/ui/Calendar/Calendar.stories.tsx +1 -1
  66. package/client/ui/Card.stories.tsx +285 -168
  67. package/client/ui/Carousel/Carousel.stories.tsx +1 -1
  68. package/client/ui/Chart/Chart.stories.tsx +1 -1
  69. package/client/ui/Checkbox.stories.tsx +1 -1
  70. package/client/ui/Collapsible/Collapsible.stories.tsx +1 -1
  71. package/client/ui/Colors.stories.tsx +1 -1
  72. package/client/ui/Command/Command.stories.tsx +1 -1
  73. package/client/ui/ContextMenu/ContextMenu.stories.tsx +1 -1
  74. package/client/ui/DatePicker/DatePicker.stories.tsx +293 -0
  75. package/client/ui/Dialog.stories.tsx +1 -1
  76. package/client/ui/Drawer/Drawer.stories.tsx +1 -1
  77. package/client/ui/Dropdown/Dropdown.stories.tsx +205 -0
  78. package/client/ui/EmptyState/empty-state.stories.tsx +251 -0
  79. package/client/ui/FileUpload/FileUpload.stories.tsx +218 -0
  80. package/client/ui/Footer/footer.stories.tsx +194 -0
  81. package/client/ui/Form/Form.stories.tsx +1 -1
  82. package/client/ui/Hero.stories.tsx +1 -1
  83. package/client/ui/HoverCard/HoverCard.stories.tsx +1 -1
  84. package/client/ui/Icons.stories.tsx +1 -1
  85. package/client/ui/Input.stories.tsx +1 -1
  86. package/client/ui/InputOTP/InputOTP.stories.tsx +1 -1
  87. package/client/ui/Label.stories.tsx +1 -1
  88. package/client/ui/Layout/Center.stories.tsx +100 -0
  89. package/client/ui/Layout/Columns.stories.tsx +136 -0
  90. package/client/ui/Layout/Container.stories.tsx +104 -0
  91. package/client/ui/Layout/FormLayout.stories.tsx +144 -0
  92. package/client/ui/Layout/Grid.stories.tsx +158 -0
  93. package/client/ui/Layout/GridSpan.stories.tsx +75 -0
  94. package/client/ui/Layout/HStack.stories.tsx +128 -0
  95. package/client/ui/Layout/Layout.stories.tsx +287 -0
  96. package/client/ui/Layout/LayoutContent.stories.tsx +91 -0
  97. package/client/ui/Layout/LayoutFooter.stories.tsx +77 -0
  98. package/client/ui/Layout/LayoutHeader.stories.tsx +104 -0
  99. package/client/ui/Layout/LayoutPanel.stories.tsx +143 -0
  100. package/client/ui/Layout/Positioned.stories.tsx +113 -0
  101. package/client/ui/Layout/Section.stories.tsx +84 -0
  102. package/client/ui/Layout/ShowHide.stories.tsx +99 -0
  103. package/client/ui/Layout/Spacer.stories.tsx +149 -0
  104. package/client/ui/Layout/Stack.stories.tsx +69 -0
  105. package/client/ui/Layout/StackItem.stories.tsx +124 -0
  106. package/client/ui/Layout/VStack.stories.tsx +101 -0
  107. package/client/ui/Layout/controls.ts +33 -0
  108. package/client/ui/Layout/demo.tsx +37 -0
  109. package/client/ui/Menubar/Menubar.stories.tsx +1 -1
  110. package/client/ui/MobileNav.stories.tsx +1 -1
  111. package/client/ui/Navigation.stories.tsx +1 -1
  112. package/client/ui/NavigationMenu/NavigationMenu.stories.tsx +1 -1
  113. package/client/ui/PageHeader.stories.tsx +13 -13
  114. package/client/ui/Pagination/Pagination.stories.tsx +178 -98
  115. package/client/ui/Pagination/PaginationLegacy.stories.tsx +121 -0
  116. package/client/ui/Panel.stories.tsx +1 -1
  117. package/client/ui/Popover/Popover.stories.tsx +1 -1
  118. package/client/ui/Progress/Progress.stories.tsx +1 -1
  119. package/client/ui/RadioGroup/RadioGroup.stories.tsx +1 -1
  120. package/client/ui/Resizable/Resizable.stories.tsx +1 -1
  121. package/client/ui/ScrollArea/ScrollArea.stories.tsx +1 -1
  122. package/client/ui/SearchField/SearchField.stories.tsx +153 -0
  123. package/client/ui/{SearchField.stories.tsx → SearchField/SearchFieldLegacy.stories.tsx} +2 -2
  124. package/client/ui/SectionBadge.stories.tsx +1 -1
  125. package/client/ui/Select.stories.tsx +1 -1
  126. package/client/ui/Separator/Separator.stories.tsx +1 -1
  127. package/client/ui/Sheet/Sheet.stories.tsx +1 -1
  128. package/client/ui/Sidebar/Sidebar.stories.tsx +57 -8
  129. package/client/ui/Skeleton/Skeleton.stories.tsx +1 -1
  130. package/client/ui/Slider/Slider.stories.tsx +1 -1
  131. package/client/ui/StandardNavigation.stories.tsx +1 -1
  132. package/client/ui/Stepper/Stepper.stories.tsx +344 -0
  133. package/client/ui/Switch.stories.tsx +1 -1
  134. package/client/ui/SwitchV01/SwitchV01.stories.tsx +152 -0
  135. package/client/ui/SystemBar/system-bar.stories.tsx +179 -0
  136. package/client/ui/Table/TableV01.stories.tsx +256 -0
  137. package/client/ui/Tabs.stories.tsx +1 -1
  138. package/client/ui/TabsV01/TabsV01.stories.tsx +144 -0
  139. package/client/ui/Tag.stories.tsx +1 -1
  140. package/client/ui/TagV02/TagStatusV02.stories.tsx +132 -0
  141. package/client/ui/TagV02/TagV02.stories.tsx +142 -0
  142. package/client/ui/TextAreaV01/TextAreaV01.stories.tsx +146 -0
  143. package/client/ui/TextFieldV01/PasswordFieldV01.stories.tsx +161 -0
  144. package/client/ui/TextFieldV01/TextFieldV01.stories.tsx +160 -0
  145. package/client/ui/Textarea.stories.tsx +1 -1
  146. package/client/ui/Timeline/timeline.stories.tsx +404 -0
  147. package/client/ui/Toast/Toast.stories.tsx +1 -1
  148. package/client/ui/ToastV01/ToastV01.stories.tsx +201 -0
  149. package/client/ui/Toaster/Toaster.stories.tsx +1 -1
  150. package/client/ui/Toggle.stories.tsx +1 -1
  151. package/client/ui/Tooltip.stories.tsx +287 -114
  152. package/client/ui/Typography.stories.tsx +1 -1
  153. package/dist/index.cjs.js +1 -1
  154. package/dist/index.d.ts +649 -13
  155. package/dist/index.es.js +6109 -2360
  156. package/package.json +1 -1
  157. package/client/components/ui/badge.tsx +0 -33
  158. package/client/components/ui/sonner.tsx +0 -26
  159. package/client/components/ui/toggle-group.tsx +0 -76
  160. package/client/ui/Alert/Alert.stories.tsx +0 -82
  161. package/client/ui/AlertDialog/AlertDialog.stories.tsx +0 -106
  162. package/client/ui/Avatar/Avatar.stories.tsx +0 -94
  163. package/client/ui/Badge/Badge.stories.tsx +0 -60
  164. package/client/ui/Breadcrumb/Breadcrumb.stories.tsx +0 -97
  165. package/client/ui/DropdownMenu/DropdownMenu.stories.tsx +0 -139
  166. package/client/ui/Sonner/Sonner.stories.tsx +0 -48
  167. package/client/ui/Table/Table.stories.tsx +0 -135
  168. package/client/ui/ToggleGroup/ToggleGroup.stories.tsx +0 -88
@@ -0,0 +1,157 @@
1
+ import * as React from "react";
2
+ import { MoreHorizontal } from "lucide-react";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ export type PaginationV01State = "default" | "active" | "disabled";
7
+
8
+ /**
9
+ * Figma draws the chevrons as filled paths on a 12x12 box (node 12223:4804 /
10
+ * 12223:4842), not as stroked icons — lucide's stroked chevron reads noticeably
11
+ * thinner at this size. Inlined verbatim; colour comes from `currentColor`,
12
+ * which the CSS sets independently of the label.
13
+ */
14
+ const ChevronLeftFilled = (props: React.SVGProps<SVGSVGElement>) => (
15
+ <svg viewBox="0 0 12 12" fill="none" aria-hidden {...props}>
16
+ <path
17
+ d="M7.18279 1.18276C7.42687 0.938681 7.82348 0.938681 8.06755 1.18276C8.31159 1.42684 8.31162 1.82346 8.06755 2.06752L4.50994 5.62514L8.06755 9.18276C8.31159 9.42684 8.31162 9.82346 8.06755 10.0675C7.82349 10.3116 7.42687 10.3116 7.18279 10.0675L3.18279 6.06752C2.93871 5.82345 2.93871 5.42684 3.18279 5.18276L7.18279 1.18276Z"
18
+ fill="currentColor"
19
+ />
20
+ </svg>
21
+ );
22
+
23
+ const ChevronRightFilled = (props: React.SVGProps<SVGSVGElement>) => (
24
+ <svg viewBox="0 0 12 12" fill="none" aria-hidden {...props}>
25
+ <path
26
+ d="M4.06774 10.0677C3.8238 10.3116 3.42708 10.3113 3.18297 10.0677C2.9389 9.82361 2.9389 9.427 3.18297 9.18292L6.74059 5.62531L3.18297 2.06769C2.9389 1.82361 2.9389 1.427 3.18297 1.18292C3.42705 0.938845 3.82366 0.938845 4.06774 1.18292L8.06774 5.18292C8.31139 5.42704 8.31167 5.82375 8.06774 6.06769L4.06774 10.0677Z"
27
+ fill="currentColor"
28
+ />
29
+ </svg>
30
+ );
31
+
32
+ const PaginationV01 = ({
33
+ className,
34
+ children,
35
+ ...props
36
+ }: React.ComponentProps<"nav">) => (
37
+ <nav
38
+ role="navigation"
39
+ aria-label="Pagination"
40
+ className={cn("pagination-v01", className)}
41
+ {...props}
42
+ >
43
+ {children}
44
+ </nav>
45
+ );
46
+ PaginationV01.displayName = "PaginationV01";
47
+
48
+ interface ControlProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
49
+ state?: PaginationV01State;
50
+ }
51
+
52
+ const PaginationV01Previous = React.forwardRef<HTMLButtonElement, ControlProps>(
53
+ (
54
+ { className, state = "default", disabled, children = "Previous", ...props },
55
+ ref,
56
+ ) => {
57
+ const isDisabled = disabled || state === "disabled";
58
+ return (
59
+ <button
60
+ ref={ref}
61
+ type="button"
62
+ data-state={state}
63
+ disabled={isDisabled}
64
+ aria-label="Go to previous page"
65
+ className={cn("pagination-v01-control font-mazzardSoft", className)}
66
+ {...props}
67
+ >
68
+ <ChevronLeftFilled className="pagination-v01-icon" />
69
+ <span className="pagination-v01-label body-small">{children}</span>
70
+ </button>
71
+ );
72
+ },
73
+ );
74
+ PaginationV01Previous.displayName = "PaginationV01Previous";
75
+
76
+ const PaginationV01Next = React.forwardRef<HTMLButtonElement, ControlProps>(
77
+ (
78
+ { className, state = "default", disabled, children = "Next", ...props },
79
+ ref,
80
+ ) => {
81
+ const isDisabled = disabled || state === "disabled";
82
+ return (
83
+ <button
84
+ ref={ref}
85
+ type="button"
86
+ data-state={state}
87
+ disabled={isDisabled}
88
+ aria-label="Go to next page"
89
+ className={cn("pagination-v01-control font-mazzardSoft", className)}
90
+ {...props}
91
+ >
92
+ <span className="pagination-v01-label body-small">{children}</span>
93
+ <ChevronRightFilled className="pagination-v01-icon" />
94
+ </button>
95
+ );
96
+ },
97
+ );
98
+ PaginationV01Next.displayName = "PaginationV01Next";
99
+
100
+ interface PageProps extends ControlProps {
101
+ current?: boolean;
102
+ }
103
+
104
+ const PaginationV01Page = React.forwardRef<HTMLButtonElement, PageProps>(
105
+ (
106
+ { className, state = "default", current, disabled, children, ...props },
107
+ ref,
108
+ ) => {
109
+ const resolved: PaginationV01State = current ? "active" : state;
110
+ const isDisabled = disabled || resolved === "disabled";
111
+ const isCurrent = resolved === "active";
112
+ return (
113
+ <button
114
+ ref={ref}
115
+ type="button"
116
+ data-state={resolved}
117
+ disabled={isDisabled}
118
+ aria-current={isCurrent ? "page" : undefined}
119
+ aria-label={typeof children === "string" ? `Page ${children}` : "Page"}
120
+ className={cn("pagination-v01-page", className)}
121
+ {...props}
122
+ >
123
+ {children}
124
+ </button>
125
+ );
126
+ },
127
+ );
128
+ PaginationV01Page.displayName = "PaginationV01Page";
129
+
130
+ interface EllipsisProps extends React.HTMLAttributes<HTMLSpanElement> {
131
+ state?: PaginationV01State;
132
+ }
133
+
134
+ const PaginationV01Ellipsis = ({
135
+ className,
136
+ state = "default",
137
+ ...props
138
+ }: EllipsisProps) => (
139
+ <span
140
+ aria-hidden
141
+ data-state={state}
142
+ className={cn("pagination-v01-ellipsis", className)}
143
+ {...props}
144
+ >
145
+ <MoreHorizontal className="pagination-v01-icon" strokeWidth={4} />
146
+ <span className="sr-only">More pages</span>
147
+ </span>
148
+ );
149
+ PaginationV01Ellipsis.displayName = "PaginationV01Ellipsis";
150
+
151
+ export {
152
+ PaginationV01,
153
+ PaginationV01Previous,
154
+ PaginationV01Next,
155
+ PaginationV01Page,
156
+ PaginationV01Ellipsis,
157
+ };
@@ -0,0 +1,67 @@
1
+ import * as React from "react";
2
+ import { Eye, EyeOff } from "lucide-react";
3
+
4
+ import {
5
+ TextFieldV01,
6
+ type TextFieldV01Props,
7
+ } from "@/components/ui/text-field-v01";
8
+
9
+ export interface PasswordFieldV01Props
10
+ extends Omit<TextFieldV01Props, "type" | "action"> {
11
+ visible?: boolean;
12
+ defaultVisible?: boolean;
13
+ onVisibleChange?: (visible: boolean) => void;
14
+ }
15
+
16
+ const PasswordFieldV01 = React.forwardRef<
17
+ HTMLInputElement,
18
+ PasswordFieldV01Props
19
+ >(
20
+ (
21
+ {
22
+ visible: controlledVisible,
23
+ defaultVisible = false,
24
+ onVisibleChange,
25
+ disabled,
26
+ ...props
27
+ },
28
+ ref,
29
+ ) => {
30
+ const isControlled = controlledVisible !== undefined;
31
+ const [internal, setInternal] = React.useState(defaultVisible);
32
+ const visible = isControlled ? controlledVisible : internal;
33
+
34
+ const toggle = () => {
35
+ if (!isControlled) setInternal(!visible);
36
+ onVisibleChange?.(!visible);
37
+ };
38
+
39
+ return (
40
+ <TextFieldV01
41
+ ref={ref}
42
+ type={visible ? "text" : "password"}
43
+ disabled={disabled}
44
+ action={
45
+ <button
46
+ type="button"
47
+ className="text-field-v01-action"
48
+ onClick={toggle}
49
+ disabled={disabled}
50
+ aria-label={visible ? "Hide password" : "Show password"}
51
+ aria-pressed={visible}
52
+ >
53
+ {visible ? (
54
+ <Eye size={16} strokeWidth={1.5} aria-hidden />
55
+ ) : (
56
+ <EyeOff size={16} strokeWidth={1.5} aria-hidden />
57
+ )}
58
+ </button>
59
+ }
60
+ {...props}
61
+ />
62
+ );
63
+ },
64
+ );
65
+ PasswordFieldV01.displayName = "PasswordFieldV01";
66
+
67
+ export { PasswordFieldV01 };
@@ -0,0 +1,192 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ /**
6
+ * Search field — built from the Figma component set `Search field`
7
+ * (node 12008:2262, "Search Field -v0.1").
8
+ *
9
+ * Figma models it as a single `State` property: Default, Focus, Active, Filled,
10
+ * Disable. Those map onto real interaction here — focus and value derive the
11
+ * state — and `state` can also be passed to pin one for specs and stories.
12
+ *
13
+ * Spacing and radius come from `--space-*` / `--radius-*`. The sizes Figma
14
+ * specifies that are not on a scale live as `--search-field-*` variables.
15
+ */
16
+ type SearchFieldV01State =
17
+ | "default"
18
+ | "focus"
19
+ | "active"
20
+ | "filled"
21
+ | "disabled";
22
+
23
+ /**
24
+ * Figma's icons are stroked at 1.5 on a 16 viewBox — lucide's own 24 viewBox
25
+ * renders a different weight at this size, so the paths are inlined verbatim.
26
+ * All three take `currentColor`.
27
+ */
28
+ const iconProps = {
29
+ fill: "none",
30
+ strokeWidth: 1.5,
31
+ strokeLinecap: "round" as const,
32
+ strokeLinejoin: "round" as const,
33
+ "aria-hidden": true,
34
+ };
35
+
36
+ const SearchIcon = (props: React.SVGProps<SVGSVGElement>) => (
37
+ <svg viewBox="0 0 16 16" {...iconProps} {...props}>
38
+ <path
39
+ d="M15 15L10.3333 10.3333M11.8889 6.44444C11.8889 9.45133 9.45133 11.8889 6.44444 11.8889C3.43756 11.8889 1 9.45133 1 6.44444C1 3.43756 3.43756 1 6.44444 1C9.45133 1 11.8889 3.43756 11.8889 6.44444Z"
40
+ stroke="currentColor"
41
+ />
42
+ </svg>
43
+ );
44
+
45
+ const EnterIcon = (props: React.SVGProps<SVGSVGElement>) => (
46
+ <svg viewBox="0 0 16 16" {...iconProps} {...props}>
47
+ <path d="M9.16667 13.6L15 8L9.16667 2.4M15 8L1 8" stroke="currentColor" />
48
+ </svg>
49
+ );
50
+
51
+ const ClearIcon = (props: React.SVGProps<SVGSVGElement>) => (
52
+ <svg viewBox="0 0 11.1 11.1" {...iconProps} {...props}>
53
+ <path d="M10.35 0.75L0.75 10.35" stroke="currentColor" />
54
+ <path d="M0.75 0.75L10.35 10.35" stroke="currentColor" />
55
+ </svg>
56
+ );
57
+
58
+ interface SearchFieldV01Props
59
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
60
+ /**
61
+ * Figma `State`. Omit to let focus and value drive it:
62
+ * empty + blurred = `default`, empty + focused = `focus`,
63
+ * has value + focused = `active`, has value + blurred = `filled`.
64
+ */
65
+ state?: SearchFieldV01State;
66
+ /**
67
+ * Inline typeahead remainder rendered after the value — Figma's `Active`
68
+ * state ("Entry" + "typeahead"). Also shows the Enter affordance.
69
+ */
70
+ suggestion?: string;
71
+ /** Renders the clear button once there is a value (Figma `Filled`). */
72
+ onClear?: () => void;
73
+ /** Class for the outer box; `className` goes to the input. */
74
+ containerClassName?: string;
75
+ }
76
+
77
+ const SearchFieldV01 = React.forwardRef<HTMLInputElement, SearchFieldV01Props>(
78
+ (
79
+ {
80
+ className,
81
+ containerClassName,
82
+ state,
83
+ suggestion,
84
+ onClear,
85
+ placeholder = "Placeholder",
86
+ disabled,
87
+ value,
88
+ defaultValue,
89
+ onFocus,
90
+ onBlur,
91
+ onChange,
92
+ ...props
93
+ },
94
+ ref,
95
+ ) => {
96
+ const [focused, setFocused] = React.useState(false);
97
+ const [internalValue, setInternalValue] = React.useState(
98
+ defaultValue != null ? String(defaultValue) : "",
99
+ );
100
+
101
+ const currentValue = value != null ? String(value) : internalValue;
102
+ const hasValue = currentValue.length > 0;
103
+
104
+ const resolved: SearchFieldV01State =
105
+ state ??
106
+ (disabled
107
+ ? "disabled"
108
+ : focused
109
+ ? hasValue
110
+ ? "active"
111
+ : "focus"
112
+ : hasValue
113
+ ? "filled"
114
+ : "default");
115
+
116
+ // Figma drops the search icon as soon as the field is in use.
117
+ const showSearchIcon = resolved === "default" || resolved === "disabled";
118
+ const showTypeahead = resolved === "active" && Boolean(suggestion);
119
+ const showEnter = resolved === "active";
120
+ const showClear = resolved === "filled" && hasValue && Boolean(onClear);
121
+
122
+ return (
123
+ <div
124
+ data-state={resolved}
125
+ className={cn(
126
+ "search-field-v01",
127
+ showTypeahead && "search-field-v01--typeahead",
128
+ containerClassName,
129
+ )}
130
+ >
131
+ {showSearchIcon && (
132
+ <span className="search-field-v01-icon-box">
133
+ <SearchIcon className="search-field-v01-icon" />
134
+ </span>
135
+ )}
136
+
137
+ <span className="search-field-v01-field">
138
+ <input
139
+ ref={ref}
140
+ type="search"
141
+ role="searchbox"
142
+ className={cn("search-field-v01-input", className)}
143
+ placeholder={placeholder}
144
+ disabled={disabled || resolved === "disabled"}
145
+ value={value != null ? value : internalValue}
146
+ onFocus={(event) => {
147
+ setFocused(true);
148
+ onFocus?.(event);
149
+ }}
150
+ onBlur={(event) => {
151
+ setFocused(false);
152
+ onBlur?.(event);
153
+ }}
154
+ onChange={(event) => {
155
+ if (value == null) setInternalValue(event.target.value);
156
+ onChange?.(event);
157
+ }}
158
+ {...props}
159
+ />
160
+ {showTypeahead && (
161
+ <span className="search-field-v01-suggestion" aria-hidden="true">
162
+ {suggestion}
163
+ </span>
164
+ )}
165
+ </span>
166
+
167
+ {(showEnter || showClear) && (
168
+ <span className="search-field-v01-actions">
169
+ {showClear ? (
170
+ <button
171
+ type="button"
172
+ className="search-field-v01-clear"
173
+ aria-label="Clear search"
174
+ onClick={onClear}
175
+ >
176
+ <ClearIcon className="search-field-v01-clear-glyph" />
177
+ </button>
178
+ ) : (
179
+ <span className="search-field-v01-enter-box">
180
+ <EnterIcon className="search-field-v01-icon" />
181
+ </span>
182
+ )}
183
+ </span>
184
+ )}
185
+ </div>
186
+ );
187
+ },
188
+ );
189
+ SearchFieldV01.displayName = "SearchFieldV01";
190
+
191
+ export { SearchFieldV01 };
192
+ export type { SearchFieldV01Props, SearchFieldV01State };
@@ -0,0 +1,49 @@
1
+ import * as React from "react";
2
+ import { Text } from "@syscore/ui-library";
3
+ import { Stack } from "./layout";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // SectionHeading — title + subtitle pair (RULE #-4B / ui-spacing-and-density.md)
7
+ //
8
+ // Owns the gap (floor gap-3, defaults to the gap-4 target) AND the line-height on
9
+ // each line, via DS_PENDING_CSS combined selectors that beat the DS typography
10
+ // class on specificity. No page should stack two Text lines and hand-roll this
11
+ // itself -- that's exactly how the heading/subtitle overlap bug shipped.
12
+ // ---------------------------------------------------------------------------
13
+ interface SectionHeadingProps {
14
+ title: React.ReactNode;
15
+ subtitle?: React.ReactNode;
16
+ titleAs?: React.ElementType;
17
+ titleVariant?: string;
18
+ subtitleVariant?: string;
19
+ gap?: number;
20
+ }
21
+
22
+ const SectionHeading = React.forwardRef<HTMLElement, SectionHeadingProps>(
23
+ (
24
+ {
25
+ title,
26
+ subtitle,
27
+ titleAs,
28
+ titleVariant = "heading-xxsmall",
29
+ subtitleVariant = "body-base",
30
+ gap = 4,
31
+ },
32
+ ref,
33
+ ) => (
34
+ <Stack ref={ref} gap={gap}>
35
+ <Text as={titleAs} variant={titleVariant} className="section-heading__title">
36
+ {title}
37
+ </Text>
38
+ {subtitle ? (
39
+ <Text variant={subtitleVariant} className="section-heading__subtitle">
40
+ {subtitle}
41
+ </Text>
42
+ ) : null}
43
+ </Stack>
44
+ ),
45
+ );
46
+ SectionHeading.displayName = "SectionHeading";
47
+
48
+ export { SectionHeading };
49
+ export type { SectionHeadingProps };
@@ -281,8 +281,12 @@ SidebarTrigger.displayName = "SidebarTrigger";
281
281
 
282
282
  const SidebarRail = React.forwardRef<
283
283
  HTMLButtonElement,
284
- React.ComponentProps<"button">
285
- >(({ className, ...props }, ref) => {
284
+ React.ComponentProps<"button"> & {
285
+ side?: "left" | "right";
286
+ state?: "expanded" | "collapsed";
287
+ collapsible?: "offcanvas" | "icon" | "none";
288
+ }
289
+ >(({ className, side, state, collapsible, ...props }, ref) => {
286
290
  const { toggleSidebar } = useSidebar();
287
291
 
288
292
  return (
@@ -308,8 +312,11 @@ SidebarRail.displayName = "SidebarRail";
308
312
 
309
313
  const SidebarInset = React.forwardRef<
310
314
  HTMLDivElement,
311
- React.ComponentProps<"main">
312
- >(({ className, ...props }, ref) => {
315
+ React.ComponentProps<"main"> & {
316
+ state?: "expanded" | "collapsed";
317
+ variant?: "sidebar" | "floating" | "inset";
318
+ }
319
+ >(({ className, state, variant, ...props }, ref) => {
313
320
  return (
314
321
  <main
315
322
  ref={ref}
@@ -566,11 +573,10 @@ const SidebarMenuButton = React.forwardRef<
566
573
  }
567
574
 
568
575
  return (
569
- <Tooltip>
576
+ <Tooltip trigger="hover">
570
577
  <TooltipTrigger asChild>{button}</TooltipTrigger>
571
578
  <TooltipContent
572
- side="right"
573
- align="center"
579
+ direction="left"
574
580
  hidden={state !== "collapsed" || isMobile}
575
581
  {...tooltip}
576
582
  />
@@ -707,7 +713,6 @@ const SidebarMenuSubButton = React.forwardRef<
707
713
  size === "md" && "sidebar-menu-sub-button--size-md",
708
714
  className,
709
715
  )}
710
- data-size={size}
711
716
  {...props}
712
717
  />
713
718
  );
@@ -0,0 +1,148 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Types
6
+ // ---------------------------------------------------------------------------
7
+ export interface StepItem {
8
+ title: string;
9
+ description?: string;
10
+ }
11
+
12
+ type StepStatus = "complete" | "current" | "upcoming";
13
+ type StepperOrientation = "horizontal" | "vertical";
14
+ type StepperVariant = "default" | "outlined";
15
+ type StepperSize = "sm" | "md" | "lg";
16
+
17
+ export interface StepperProps {
18
+ steps: StepItem[];
19
+ currentStep: number; // 0-indexed
20
+ orientation?: StepperOrientation;
21
+ variant?: StepperVariant;
22
+ size?: StepperSize;
23
+ onStepClick?: (index: number) => void;
24
+ className?: string;
25
+ }
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Helpers
29
+ // ---------------------------------------------------------------------------
30
+ function getStatus(index: number, currentStep: number): StepStatus {
31
+ if (index < currentStep) return "complete";
32
+ if (index === currentStep) return "current";
33
+ return "upcoming";
34
+ }
35
+
36
+ const CheckIcon = () => (
37
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
38
+ <path
39
+ d="M2.5 7L5.5 10L11.5 4"
40
+ stroke="currentColor"
41
+ strokeWidth="2"
42
+ strokeLinecap="round"
43
+ strokeLinejoin="round"
44
+ />
45
+ </svg>
46
+ );
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Stepper
50
+ // ---------------------------------------------------------------------------
51
+ const Stepper = React.forwardRef<HTMLDivElement, StepperProps>(
52
+ (
53
+ {
54
+ steps,
55
+ currentStep,
56
+ orientation = "horizontal",
57
+ variant = "default",
58
+ size = "md",
59
+ onStepClick,
60
+ className,
61
+ },
62
+ ref,
63
+ ) => {
64
+ return (
65
+ <div
66
+ ref={ref}
67
+ role="list"
68
+ aria-label="Progress steps"
69
+ className={cn(
70
+ "stepper",
71
+ `stepper--${orientation}`,
72
+ `stepper--${size}`,
73
+ className,
74
+ )}
75
+ >
76
+ {steps.map((step, index) => {
77
+ const status = getStatus(index, currentStep);
78
+ const isClickable = !!onStepClick && status === "complete";
79
+ const isLast = index === steps.length - 1;
80
+
81
+ return (
82
+ <div
83
+ key={index}
84
+ role="listitem"
85
+ aria-current={status === "current" ? "step" : undefined}
86
+ className={cn(
87
+ "stepper-item",
88
+ `stepper-item--${status}`,
89
+ orientation === "horizontal" && !isLast && "stepper-item--with-connector",
90
+ )}
91
+ >
92
+ {/* Indicator + connector row */}
93
+ <div className="stepper-indicator-row">
94
+ {/* Circle */}
95
+ <button
96
+ type="button"
97
+ disabled={!isClickable}
98
+ onClick={() => isClickable && onStepClick(index)}
99
+ className={cn(
100
+ "stepper-indicator",
101
+ `stepper-indicator--${status}`,
102
+ `stepper-indicator--${variant}`,
103
+ isClickable && "stepper-indicator--clickable",
104
+ )}
105
+ aria-label={`Step ${index + 1}: ${step.title} (${status})`}
106
+ >
107
+ {status === "complete" ? (
108
+ <CheckIcon />
109
+ ) : (
110
+ <span className="stepper-indicator-number">{index + 1}</span>
111
+ )}
112
+ </button>
113
+
114
+ {/* Connector line — horizontal: after indicator, vertical: below */}
115
+ {!isLast && (
116
+ <div
117
+ className={cn(
118
+ "stepper-connector",
119
+ `stepper-connector--${orientation}`,
120
+ index < currentStep && "stepper-connector--complete",
121
+ )}
122
+ aria-hidden="true"
123
+ />
124
+ )}
125
+ </div>
126
+
127
+ {/* Label */}
128
+ <div className="stepper-label">
129
+ <span className={cn("stepper-title", `stepper-title--${status}`)}>
130
+ {step.title}
131
+ </span>
132
+ {step.description && (
133
+ <span className={cn("stepper-description", `stepper-description--${status}`)}>
134
+ {step.description}
135
+ </span>
136
+ )}
137
+ </div>
138
+ </div>
139
+ );
140
+ })}
141
+ </div>
142
+ );
143
+ },
144
+ );
145
+
146
+ Stepper.displayName = "Stepper";
147
+
148
+ export { Stepper };