@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.
Files changed (30) hide show
  1. package/client/components/ui/app-bar.tsx +299 -0
  2. package/client/components/ui/banner.tsx +108 -0
  3. package/client/components/ui/bottom-navigation.tsx +223 -0
  4. package/client/components/ui/card.tsx +108 -26
  5. package/client/components/ui/date-picker.tsx +188 -0
  6. package/client/components/ui/empty-state.tsx +197 -0
  7. package/client/components/ui/file-upload.tsx +214 -0
  8. package/client/components/ui/footer.tsx +179 -0
  9. package/client/components/ui/layout.tsx +381 -0
  10. package/client/components/ui/sidebar.tsx +11 -5
  11. package/client/components/ui/stepper.tsx +148 -0
  12. package/client/components/ui/system-bar.tsx +119 -0
  13. package/client/components/ui/timeline.tsx +181 -0
  14. package/client/global.css +2304 -41
  15. package/client/ui/AppBar/app-bar.stories.tsx +280 -0
  16. package/client/ui/Banner/banner.stories.tsx +238 -0
  17. package/client/ui/BottomNavigation/bottom-navigation.stories.tsx +285 -0
  18. package/client/ui/Card.stories.tsx +285 -168
  19. package/client/ui/DatePicker/DatePicker.stories.tsx +293 -0
  20. package/client/ui/EmptyState/empty-state.stories.tsx +251 -0
  21. package/client/ui/FileUpload/FileUpload.stories.tsx +218 -0
  22. package/client/ui/Footer/footer.stories.tsx +194 -0
  23. package/client/ui/Layout.stories.tsx +1481 -0
  24. package/client/ui/Stepper/Stepper.stories.tsx +344 -0
  25. package/client/ui/SystemBar/system-bar.stories.tsx +179 -0
  26. package/client/ui/Timeline/timeline.stories.tsx +404 -0
  27. package/dist/index.cjs.js +1 -1
  28. package/dist/index.d.ts +219 -0
  29. package/dist/index.es.js +1504 -99
  30. 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
- >(({ 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}
@@ -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 };