@syscore/ui-library 1.25.0 → 2.0.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.
- package/client/components/ui/app-bar.tsx +299 -0
- package/client/components/ui/banner.tsx +108 -0
- package/client/components/ui/bottom-navigation.tsx +223 -0
- package/client/components/ui/card.tsx +108 -26
- package/client/components/ui/date-picker.tsx +188 -0
- package/client/components/ui/empty-state.tsx +197 -0
- package/client/components/ui/file-upload.tsx +214 -0
- package/client/components/ui/footer.tsx +179 -0
- package/client/components/ui/layout.tsx +381 -0
- package/client/components/ui/sidebar.tsx +11 -5
- package/client/components/ui/stepper.tsx +148 -0
- package/client/components/ui/system-bar.tsx +119 -0
- package/client/components/ui/timeline.tsx +181 -0
- package/client/global.css +2304 -41
- package/client/ui/AppBar/app-bar.stories.tsx +280 -0
- package/client/ui/Banner/banner.stories.tsx +238 -0
- package/client/ui/BottomNavigation/bottom-navigation.stories.tsx +285 -0
- package/client/ui/Card.stories.tsx +285 -168
- package/client/ui/DatePicker/DatePicker.stories.tsx +293 -0
- package/client/ui/EmptyState/empty-state.stories.tsx +251 -0
- package/client/ui/FileUpload/FileUpload.stories.tsx +218 -0
- package/client/ui/Footer/footer.stories.tsx +194 -0
- package/client/ui/Layout.stories.tsx +1481 -0
- package/client/ui/Stepper/Stepper.stories.tsx +344 -0
- package/client/ui/SystemBar/system-bar.stories.tsx +179 -0
- package/client/ui/Timeline/timeline.stories.tsx +404 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +219 -0
- package/dist/index.es.js +1504 -99
- package/package.json +1 -1
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
// Spacing scale — matches Tailwind (1 unit = 4px)
|
|
5
|
+
const spacing = (n: number): string => `${n * 0.25}rem`;
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Stack
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
type StackDirection = "vertical" | "horizontal";
|
|
11
|
+
type StackAlign = "start" | "center" | "end" | "stretch" | "baseline";
|
|
12
|
+
type StackJustify = "start" | "center" | "end" | "between" | "around" | "evenly";
|
|
13
|
+
|
|
14
|
+
interface StackProps extends React.HTMLAttributes<HTMLElement> {
|
|
15
|
+
as?: React.ElementType;
|
|
16
|
+
direction?: StackDirection;
|
|
17
|
+
gap?: number;
|
|
18
|
+
align?: StackAlign;
|
|
19
|
+
justify?: StackJustify;
|
|
20
|
+
wrap?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const Stack = React.forwardRef<HTMLElement, StackProps>(
|
|
24
|
+
({ as: Component = "div", direction = "vertical", gap = 4, align, justify, wrap = false, className, style, ...props }, ref) => (
|
|
25
|
+
<Component
|
|
26
|
+
ref={ref}
|
|
27
|
+
className={cn(
|
|
28
|
+
"stack",
|
|
29
|
+
direction === "horizontal" && "stack--horizontal",
|
|
30
|
+
align && `stack--align-${align}`,
|
|
31
|
+
justify && `stack--justify-${justify}`,
|
|
32
|
+
wrap && "stack--wrap",
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
style={{ gap: spacing(gap), ...style }}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
Stack.displayName = "Stack";
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Grid
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
type GridCols = 1 | 2 | 3 | 4 | 6 | 12;
|
|
46
|
+
|
|
47
|
+
interface GridProps extends React.HTMLAttributes<HTMLElement> {
|
|
48
|
+
as?: React.ElementType;
|
|
49
|
+
cols?: GridCols;
|
|
50
|
+
colsSm?: GridCols;
|
|
51
|
+
colsMd?: GridCols;
|
|
52
|
+
colsLg?: GridCols;
|
|
53
|
+
gap?: number;
|
|
54
|
+
rowGap?: number;
|
|
55
|
+
colGap?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const Grid = React.forwardRef<HTMLElement, GridProps>(
|
|
59
|
+
({ as: Component = "div", cols = 1, colsSm, colsMd, colsLg, gap = 4, rowGap, colGap, className, style, ...props }, ref) => (
|
|
60
|
+
<Component
|
|
61
|
+
ref={ref}
|
|
62
|
+
className={cn(
|
|
63
|
+
"grid-layout",
|
|
64
|
+
`grid-layout--cols-${cols}`,
|
|
65
|
+
colsSm && `grid-layout--sm-cols-${colsSm}`,
|
|
66
|
+
colsMd && `grid-layout--md-cols-${colsMd}`,
|
|
67
|
+
colsLg && `grid-layout--lg-cols-${colsLg}`,
|
|
68
|
+
className,
|
|
69
|
+
)}
|
|
70
|
+
style={{
|
|
71
|
+
gap: rowGap === undefined && colGap === undefined ? spacing(gap) : undefined,
|
|
72
|
+
rowGap: rowGap !== undefined ? spacing(rowGap) : undefined,
|
|
73
|
+
columnGap: colGap !== undefined ? spacing(colGap) : undefined,
|
|
74
|
+
...style,
|
|
75
|
+
}}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
Grid.displayName = "Grid";
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Columns — asymmetric two-column layout
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
type ColumnsPreset =
|
|
86
|
+
| "sidebar" // 280px | 1fr
|
|
87
|
+
| "sidebar-right" // 1fr | 280px
|
|
88
|
+
| "wide-sidebar" // 360px | 1fr
|
|
89
|
+
| "narrow-sidebar" // 200px | 1fr
|
|
90
|
+
| "split" // 1fr | 1fr
|
|
91
|
+
| "1-2" // 1fr | 2fr
|
|
92
|
+
| "2-1" // 2fr | 1fr
|
|
93
|
+
| "1-3" // 1fr | 3fr
|
|
94
|
+
| "3-1"; // 3fr | 1fr
|
|
95
|
+
|
|
96
|
+
type ColumnsCollapse = "sm" | "md" | "lg";
|
|
97
|
+
|
|
98
|
+
interface ColumnsProps extends React.HTMLAttributes<HTMLElement> {
|
|
99
|
+
as?: React.ElementType;
|
|
100
|
+
preset?: ColumnsPreset;
|
|
101
|
+
template?: string; // custom CSS grid-template-columns value
|
|
102
|
+
gap?: number;
|
|
103
|
+
colGap?: number;
|
|
104
|
+
rowGap?: number;
|
|
105
|
+
collapse?: ColumnsCollapse; // stack to single column below this breakpoint
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const ColumnsPresetMap: Record<ColumnsPreset, string> = {
|
|
109
|
+
"sidebar": "280px 1fr",
|
|
110
|
+
"sidebar-right": "1fr 280px",
|
|
111
|
+
"wide-sidebar": "360px 1fr",
|
|
112
|
+
"narrow-sidebar": "200px 1fr",
|
|
113
|
+
"split": "1fr 1fr",
|
|
114
|
+
"1-2": "1fr 2fr",
|
|
115
|
+
"2-1": "2fr 1fr",
|
|
116
|
+
"1-3": "1fr 3fr",
|
|
117
|
+
"3-1": "3fr 1fr",
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const Columns = React.forwardRef<HTMLElement, ColumnsProps>(
|
|
121
|
+
({ as: Component = "div", preset = "split", template, gap = 6, colGap, rowGap, collapse, className, style, ...props }, ref) => (
|
|
122
|
+
<Component
|
|
123
|
+
ref={ref}
|
|
124
|
+
className={cn(
|
|
125
|
+
"columns-layout",
|
|
126
|
+
collapse && `columns-layout--collapse-${collapse}`,
|
|
127
|
+
className,
|
|
128
|
+
)}
|
|
129
|
+
style={{
|
|
130
|
+
gridTemplateColumns: template ?? ColumnsPresetMap[preset],
|
|
131
|
+
gap: rowGap === undefined && colGap === undefined ? spacing(gap) : undefined,
|
|
132
|
+
rowGap: rowGap !== undefined ? spacing(rowGap) : undefined,
|
|
133
|
+
columnGap: colGap !== undefined ? spacing(colGap) : undefined,
|
|
134
|
+
...style,
|
|
135
|
+
}}
|
|
136
|
+
{...props}
|
|
137
|
+
/>
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
Columns.displayName = "Columns";
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Container
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
type ContainerSize = "sm" | "md" | "lg" | "xl" | "2xl" | "full";
|
|
146
|
+
|
|
147
|
+
interface ContainerProps extends React.HTMLAttributes<HTMLElement> {
|
|
148
|
+
as?: React.ElementType;
|
|
149
|
+
size?: ContainerSize;
|
|
150
|
+
center?: boolean;
|
|
151
|
+
padded?: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const Container = React.forwardRef<HTMLElement, ContainerProps>(
|
|
155
|
+
({ as: Component = "div", size = "xl", center = true, padded = true, className, ...props }, ref) => (
|
|
156
|
+
<Component
|
|
157
|
+
ref={ref}
|
|
158
|
+
className={cn(
|
|
159
|
+
"container-layout",
|
|
160
|
+
`container-layout--${size}`,
|
|
161
|
+
center && "container-layout--center",
|
|
162
|
+
padded && "container-layout--padded",
|
|
163
|
+
className,
|
|
164
|
+
)}
|
|
165
|
+
{...props}
|
|
166
|
+
/>
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
Container.displayName = "Container";
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Section
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
type SectionPadding = "none" | "sm" | "md" | "lg" | "xl";
|
|
175
|
+
|
|
176
|
+
interface SectionProps extends React.HTMLAttributes<HTMLElement> {
|
|
177
|
+
as?: React.ElementType;
|
|
178
|
+
padding?: SectionPadding;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const Section = React.forwardRef<HTMLElement, SectionProps>(
|
|
182
|
+
({ as: Component = "section", padding = "lg", className, ...props }, ref) => (
|
|
183
|
+
<Component
|
|
184
|
+
ref={ref}
|
|
185
|
+
className={cn("section-layout", padding !== "none" && `section-layout--${padding}`, className)}
|
|
186
|
+
{...props}
|
|
187
|
+
/>
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
Section.displayName = "Section";
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// AppShell — full page frame
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
interface AppShellProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
196
|
+
sidebarWidth?: number | string;
|
|
197
|
+
headerHeight?: number | string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const AppShell = React.forwardRef<HTMLDivElement, AppShellProps>(
|
|
201
|
+
({ sidebarWidth = "17.5rem", headerHeight = "4rem", className, style, ...props }, ref) => (
|
|
202
|
+
<div
|
|
203
|
+
ref={ref}
|
|
204
|
+
className={cn("app-shell", className)}
|
|
205
|
+
style={{
|
|
206
|
+
"--app-shell-header-height": typeof headerHeight === "number" ? `${headerHeight}px` : headerHeight,
|
|
207
|
+
"--app-shell-sidebar-width": typeof sidebarWidth === "number" ? `${sidebarWidth}px` : sidebarWidth,
|
|
208
|
+
...style,
|
|
209
|
+
} as React.CSSProperties}
|
|
210
|
+
{...props}
|
|
211
|
+
/>
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
AppShell.displayName = "AppShell";
|
|
215
|
+
|
|
216
|
+
const AppShellHeader = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(
|
|
217
|
+
({ className, ...props }, ref) => (
|
|
218
|
+
<header ref={ref} className={cn("app-shell-header", className)} {...props} />
|
|
219
|
+
),
|
|
220
|
+
);
|
|
221
|
+
AppShellHeader.displayName = "AppShellHeader";
|
|
222
|
+
|
|
223
|
+
const AppShellSidebar = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(
|
|
224
|
+
({ className, ...props }, ref) => (
|
|
225
|
+
<aside ref={ref} className={cn("app-shell-sidebar", className)} {...props} />
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
AppShellSidebar.displayName = "AppShellSidebar";
|
|
229
|
+
|
|
230
|
+
const AppShellMain = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(
|
|
231
|
+
({ className, ...props }, ref) => (
|
|
232
|
+
<main ref={ref} className={cn("app-shell-main", className)} {...props} />
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
AppShellMain.displayName = "AppShellMain";
|
|
236
|
+
|
|
237
|
+
const AppShellFooter = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(
|
|
238
|
+
({ className, ...props }, ref) => (
|
|
239
|
+
<footer ref={ref} className={cn("app-shell-footer", className)} {...props} />
|
|
240
|
+
),
|
|
241
|
+
);
|
|
242
|
+
AppShellFooter.displayName = "AppShellFooter";
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// Show / Hide — responsive visibility
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
type Breakpoint = "sm" | "md" | "lg" | "xl";
|
|
248
|
+
|
|
249
|
+
interface ShowProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
250
|
+
above?: Breakpoint;
|
|
251
|
+
below?: Breakpoint;
|
|
252
|
+
as?: React.ElementType;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const Show = React.forwardRef<HTMLDivElement, ShowProps>(
|
|
256
|
+
({ above, below, as: Component = "div", className, ...props }, ref) => (
|
|
257
|
+
<Component
|
|
258
|
+
ref={ref}
|
|
259
|
+
className={cn(
|
|
260
|
+
above && `show-above-${above}`,
|
|
261
|
+
below && `show-below-${below}`,
|
|
262
|
+
className,
|
|
263
|
+
)}
|
|
264
|
+
{...props}
|
|
265
|
+
/>
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
Show.displayName = "Show";
|
|
269
|
+
|
|
270
|
+
interface HideProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
271
|
+
above?: Breakpoint;
|
|
272
|
+
below?: Breakpoint;
|
|
273
|
+
as?: React.ElementType;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const Hide = React.forwardRef<HTMLDivElement, HideProps>(
|
|
277
|
+
({ above, below, as: Component = "div", className, ...props }, ref) => (
|
|
278
|
+
<Component
|
|
279
|
+
ref={ref}
|
|
280
|
+
className={cn(
|
|
281
|
+
above && `hide-above-${above}`,
|
|
282
|
+
below && `hide-below-${below}`,
|
|
283
|
+
className,
|
|
284
|
+
)}
|
|
285
|
+
{...props}
|
|
286
|
+
/>
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
Hide.displayName = "Hide";
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Positioned — relative/absolute/fixed/sticky wrapper
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
type PositionType = "relative" | "absolute" | "fixed" | "sticky";
|
|
295
|
+
|
|
296
|
+
interface PositionedProps extends React.HTMLAttributes<HTMLElement> {
|
|
297
|
+
as?: React.ElementType;
|
|
298
|
+
position?: PositionType;
|
|
299
|
+
top?: number | string;
|
|
300
|
+
right?: number | string;
|
|
301
|
+
bottom?: number | string;
|
|
302
|
+
left?: number | string;
|
|
303
|
+
inset?: number | string;
|
|
304
|
+
zIndex?: number;
|
|
305
|
+
fill?: boolean; // shorthand for inset: 0
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const Positioned = React.forwardRef<HTMLElement, PositionedProps>(
|
|
309
|
+
({ as: Component = "div", position = "relative", top, right, bottom, left, inset, zIndex, fill = false, className, style, ...props }, ref) => {
|
|
310
|
+
const resolveValue = (v: number | string | undefined) =>
|
|
311
|
+
v === undefined ? undefined : typeof v === "number" ? spacing(v) : v;
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<Component
|
|
315
|
+
ref={ref}
|
|
316
|
+
className={cn(`positioned--${position}`, className)}
|
|
317
|
+
style={{
|
|
318
|
+
top: resolveValue(top),
|
|
319
|
+
right: resolveValue(right),
|
|
320
|
+
bottom: resolveValue(bottom),
|
|
321
|
+
left: resolveValue(left),
|
|
322
|
+
inset: fill ? 0 : resolveValue(inset),
|
|
323
|
+
zIndex,
|
|
324
|
+
...style,
|
|
325
|
+
}}
|
|
326
|
+
{...props}
|
|
327
|
+
/>
|
|
328
|
+
);
|
|
329
|
+
},
|
|
330
|
+
);
|
|
331
|
+
Positioned.displayName = "Positioned";
|
|
332
|
+
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// Spacer
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
interface SpacerProps {
|
|
337
|
+
size?: number;
|
|
338
|
+
axis?: "horizontal" | "vertical";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const Spacer = ({ size = 4, axis = "vertical" }: SpacerProps) => (
|
|
342
|
+
<div
|
|
343
|
+
aria-hidden="true"
|
|
344
|
+
style={
|
|
345
|
+
axis === "vertical"
|
|
346
|
+
? { height: spacing(size), minHeight: spacing(size) }
|
|
347
|
+
: { width: spacing(size), minWidth: spacing(size) }
|
|
348
|
+
}
|
|
349
|
+
/>
|
|
350
|
+
);
|
|
351
|
+
Spacer.displayName = "Spacer";
|
|
352
|
+
|
|
353
|
+
export {
|
|
354
|
+
Stack,
|
|
355
|
+
Grid,
|
|
356
|
+
Columns,
|
|
357
|
+
Container,
|
|
358
|
+
Section,
|
|
359
|
+
AppShell,
|
|
360
|
+
AppShellHeader,
|
|
361
|
+
AppShellSidebar,
|
|
362
|
+
AppShellMain,
|
|
363
|
+
AppShellFooter,
|
|
364
|
+
Show,
|
|
365
|
+
Hide,
|
|
366
|
+
Positioned,
|
|
367
|
+
Spacer,
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
export type {
|
|
371
|
+
StackProps,
|
|
372
|
+
GridProps,
|
|
373
|
+
ColumnsProps,
|
|
374
|
+
ContainerProps,
|
|
375
|
+
SectionProps,
|
|
376
|
+
AppShellProps,
|
|
377
|
+
ShowProps,
|
|
378
|
+
HideProps,
|
|
379
|
+
PositionedProps,
|
|
380
|
+
SpacerProps,
|
|
381
|
+
};
|
|
@@ -281,8 +281,12 @@ SidebarTrigger.displayName = "SidebarTrigger";
|
|
|
281
281
|
|
|
282
282
|
const SidebarRail = React.forwardRef<
|
|
283
283
|
HTMLButtonElement,
|
|
284
|
-
React.ComponentProps<"button">
|
|
285
|
-
|
|
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
|
-
|
|
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}
|
|
@@ -707,7 +714,6 @@ const SidebarMenuSubButton = React.forwardRef<
|
|
|
707
714
|
size === "md" && "sidebar-menu-sub-button--size-md",
|
|
708
715
|
className,
|
|
709
716
|
)}
|
|
710
|
-
data-size={size}
|
|
711
717
|
{...props}
|
|
712
718
|
/>
|
|
713
719
|
);
|
|
@@ -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 };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
// ─── SystemBar Root ───────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export interface SystemBarProps extends React.HTMLAttributes<HTMLElement> {
|
|
7
|
+
/**
|
|
8
|
+
* Background color — any CSS color or var() token.
|
|
9
|
+
* When set, text and icons automatically switch to white for contrast.
|
|
10
|
+
*/
|
|
11
|
+
color?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Window mode: renders a desktop-style title bar with
|
|
14
|
+
* message indicator on the left and window controls on the right.
|
|
15
|
+
*/
|
|
16
|
+
window?: boolean;
|
|
17
|
+
/** Height of the bar in px (default 24 for status bar, 32 for window) */
|
|
18
|
+
height?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const SystemBar = React.forwardRef<HTMLElement, SystemBarProps>(
|
|
22
|
+
(
|
|
23
|
+
{
|
|
24
|
+
className,
|
|
25
|
+
children,
|
|
26
|
+
color,
|
|
27
|
+
window: isWindow = false,
|
|
28
|
+
height,
|
|
29
|
+
style,
|
|
30
|
+
...props
|
|
31
|
+
},
|
|
32
|
+
ref
|
|
33
|
+
) => {
|
|
34
|
+
const barHeight = height ?? (isWindow ? 32 : 24);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<header
|
|
38
|
+
ref={ref}
|
|
39
|
+
role="banner"
|
|
40
|
+
style={{
|
|
41
|
+
height: barHeight,
|
|
42
|
+
backgroundColor: color,
|
|
43
|
+
...style,
|
|
44
|
+
}}
|
|
45
|
+
className={cn(
|
|
46
|
+
"system-bar",
|
|
47
|
+
isWindow && "system-bar--window",
|
|
48
|
+
color && "system-bar--colored",
|
|
49
|
+
className
|
|
50
|
+
)}
|
|
51
|
+
{...props}
|
|
52
|
+
>
|
|
53
|
+
{children}
|
|
54
|
+
</header>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
SystemBar.displayName = "SystemBar";
|
|
59
|
+
|
|
60
|
+
// ─── SystemBarSpacer ──────────────────────────────────────────────────────────
|
|
61
|
+
// Pushes content to the right (flex: 1).
|
|
62
|
+
|
|
63
|
+
export interface SystemBarSpacerProps
|
|
64
|
+
extends React.HTMLAttributes<HTMLSpanElement> {}
|
|
65
|
+
|
|
66
|
+
const SystemBarSpacer = React.forwardRef<HTMLSpanElement, SystemBarSpacerProps>(
|
|
67
|
+
({ className, ...props }, ref) => (
|
|
68
|
+
<span
|
|
69
|
+
ref={ref}
|
|
70
|
+
className={cn("system-bar-spacer", className)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
SystemBarSpacer.displayName = "SystemBarSpacer";
|
|
76
|
+
|
|
77
|
+
// ─── SystemBarIcon ────────────────────────────────────────────────────────────
|
|
78
|
+
// Thin wrapper so icon children pick up the right size + color.
|
|
79
|
+
|
|
80
|
+
export interface SystemBarIconProps
|
|
81
|
+
extends React.HTMLAttributes<HTMLSpanElement> {}
|
|
82
|
+
|
|
83
|
+
const SystemBarIcon = React.forwardRef<HTMLSpanElement, SystemBarIconProps>(
|
|
84
|
+
({ className, ...props }, ref) => (
|
|
85
|
+
<span
|
|
86
|
+
ref={ref}
|
|
87
|
+
className={cn("system-bar-icon", className)}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
SystemBarIcon.displayName = "SystemBarIcon";
|
|
93
|
+
|
|
94
|
+
// ─── SystemBarAction ──────────────────────────────────────────────────────────
|
|
95
|
+
// Icon button used in window mode (minimise / maximise / close).
|
|
96
|
+
|
|
97
|
+
export interface SystemBarActionProps
|
|
98
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
99
|
+
"aria-label": string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const SystemBarAction = React.forwardRef<
|
|
103
|
+
HTMLButtonElement,
|
|
104
|
+
SystemBarActionProps
|
|
105
|
+
>(({ className, children, ...props }, ref) => (
|
|
106
|
+
<button
|
|
107
|
+
ref={ref}
|
|
108
|
+
type="button"
|
|
109
|
+
className={cn("system-bar-action", className)}
|
|
110
|
+
{...props}
|
|
111
|
+
>
|
|
112
|
+
{children}
|
|
113
|
+
</button>
|
|
114
|
+
));
|
|
115
|
+
SystemBarAction.displayName = "SystemBarAction";
|
|
116
|
+
|
|
117
|
+
// ─── Exports ─────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
export { SystemBar, SystemBarSpacer, SystemBarIcon, SystemBarAction };
|