@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,1182 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ const spacing = (n: number): string => `${n * 0.25}rem`;
5
+
6
+ const warned = new Set<string>();
7
+
8
+ /** Dev-only, deduped: flags a value that will not follow design-system changes. */
9
+ function warnOnce(message: string): void {
10
+ if (process.env.NODE_ENV === "production" || warned.has(message)) return;
11
+ warned.add(message);
12
+ console.warn(message);
13
+ }
14
+
15
+ /** Named space scale from design. Change px on `--space-*` in global.css to update apps. */
16
+ export const SPACE_SCALE = {
17
+ "3xs": 2,
18
+ "2xs": 4,
19
+ xs: 8,
20
+ sm: 12,
21
+ md: 16,
22
+ lg: 20,
23
+ xl: 24,
24
+ "2xl": 32,
25
+ "3xl": 40,
26
+ "4xl": 48,
27
+ "5xl": 64,
28
+ "6xl": 80,
29
+ "7xl": 96,
30
+ "8xl": 120,
31
+ } as const;
32
+
33
+ export type SpaceName = keyof typeof SPACE_SCALE;
34
+ export const SPACE_NAMES = Object.keys(SPACE_SCALE) as SpaceName[];
35
+ export const SPACER_SPACES = Object.values(
36
+ SPACE_SCALE,
37
+ ) as unknown as SpacerSpacePx[];
38
+ type SpacerSpacePx = (typeof SPACE_SCALE)[SpaceName];
39
+
40
+ const PX_TO_SPACE = Object.fromEntries(
41
+ SPACE_NAMES.map((name) => [SPACE_SCALE[name], name]),
42
+ ) as Record<SpacerSpacePx, SpaceName>;
43
+
44
+ /**
45
+ * Named space token. Numbers are legacy 4px units, kept only for migration —
46
+ * they resolve back to a `--space-*` token so a scale change still reaches apps.
47
+ */
48
+ export type SpaceInput = SpaceName | number;
49
+
50
+ function tokenCss(name: SpaceName): string {
51
+ return `var(--space-${name})`;
52
+ }
53
+
54
+ /**
55
+ * Resolves to `var(--space-*)` whenever possible so the scale stays the single
56
+ * source of truth. Legacy 4px units (`gap={4}`) map back onto the scale; a value
57
+ * off the scale is a frozen literal and warns in development.
58
+ */
59
+ function gapCss(value: SpaceInput | undefined): string | undefined {
60
+ if (value == null) return undefined;
61
+ if (typeof value === "string") return tokenCss(value);
62
+ if (value === 0) return "0px";
63
+ const named = PX_TO_SPACE[(value * 4) as SpacerSpacePx];
64
+ if (named) return tokenCss(named);
65
+ warnOnce(
66
+ `layout: spacing ${value} is off the space scale, so ${spacing(value)} is frozen into this app ` +
67
+ `and a design-system change will not reach it. Use a named token (gap="md").`,
68
+ );
69
+ return spacing(value);
70
+ }
71
+
72
+ const toCssSize = (v: number | string | undefined) =>
73
+ v === undefined ? undefined : typeof v === "number" ? `${v}px` : v;
74
+
75
+ type SizeValue = number | string;
76
+ type StackDirection = "vertical" | "horizontal";
77
+ type StackAlign = "start" | "center" | "end" | "stretch" | "baseline";
78
+ type StackJustify =
79
+ | "start"
80
+ | "center"
81
+ | "end"
82
+ | "between"
83
+ | "around"
84
+ | "evenly";
85
+ type StackWrap = boolean | "wrap" | "nowrap" | "wrap-reverse";
86
+ type GridCols = 1 | 2 | 3 | 4 | 6 | 12;
87
+
88
+ const isWrapped = (wrap?: StackWrap) => wrap === true || wrap === "wrap";
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Stack
92
+ // ---------------------------------------------------------------------------
93
+
94
+ interface StackProps extends React.HTMLAttributes<HTMLElement> {
95
+ as?: React.ElementType;
96
+ direction?: StackDirection;
97
+ gap?: SpaceInput;
98
+ align?: StackAlign;
99
+ justify?: StackJustify;
100
+ /** Main-axis alias used by Astryx HStack / VStack. */
101
+ hAlign?: StackJustify | StackAlign;
102
+ /** Cross-axis alias used by Astryx HStack / VStack. */
103
+ vAlign?: StackJustify | StackAlign;
104
+ wrap?: StackWrap;
105
+ padding?: SpaceInput;
106
+ width?: SizeValue;
107
+ height?: SizeValue;
108
+ maxWidth?: SizeValue;
109
+ minHeight?: SizeValue;
110
+ isScrollable?: boolean;
111
+ }
112
+
113
+ const Stack = React.forwardRef<HTMLElement, StackProps>(
114
+ (
115
+ {
116
+ as: Component = "div",
117
+ direction = "vertical",
118
+ gap = "md",
119
+ align,
120
+ justify,
121
+ hAlign,
122
+ vAlign,
123
+ wrap = false,
124
+ padding,
125
+ width,
126
+ height,
127
+ maxWidth,
128
+ minHeight,
129
+ isScrollable = false,
130
+ className,
131
+ style,
132
+ ...props
133
+ },
134
+ ref,
135
+ ) => {
136
+ const resolvedJustify =
137
+ justify ??
138
+ (direction === "horizontal"
139
+ ? (hAlign as StackJustify | undefined)
140
+ : (vAlign as StackJustify | undefined));
141
+ const resolvedAlign =
142
+ align ??
143
+ (direction === "horizontal"
144
+ ? (vAlign as StackAlign | undefined)
145
+ : (hAlign as StackAlign | undefined));
146
+
147
+ return (
148
+ <Component
149
+ ref={ref}
150
+ className={cn(
151
+ "stack",
152
+ direction === "horizontal" && "stack--horizontal",
153
+ resolvedAlign && `stack--align-${resolvedAlign}`,
154
+ resolvedJustify && `stack--justify-${resolvedJustify}`,
155
+ isWrapped(wrap) && "stack--wrap",
156
+ wrap === "wrap-reverse" && "stack--wrap-reverse",
157
+ isScrollable && "stack--scrollable",
158
+ className,
159
+ )}
160
+ style={{
161
+ gap: gapCss(gap),
162
+ padding: gapCss(padding),
163
+ width: toCssSize(width),
164
+ height: toCssSize(height),
165
+ maxWidth: toCssSize(maxWidth),
166
+ minHeight: toCssSize(minHeight),
167
+ ...style,
168
+ }}
169
+ {...props}
170
+ />
171
+ );
172
+ },
173
+ );
174
+ Stack.displayName = "Stack";
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // HStack / VStack
178
+ // ---------------------------------------------------------------------------
179
+
180
+ type HStackProps = Omit<StackProps, "direction">;
181
+ type VStackProps = Omit<StackProps, "direction">;
182
+
183
+ const HStack = React.forwardRef<HTMLElement, HStackProps>((props, ref) => (
184
+ <Stack ref={ref} direction="horizontal" {...props} />
185
+ ));
186
+ HStack.displayName = "HStack";
187
+
188
+ const VStack = React.forwardRef<HTMLElement, VStackProps>((props, ref) => (
189
+ <Stack ref={ref} direction="vertical" {...props} />
190
+ ));
191
+ VStack.displayName = "VStack";
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // StackItem
195
+ // ---------------------------------------------------------------------------
196
+
197
+ type StackItemSize = "static" | "fill";
198
+
199
+ interface StackItemProps extends React.HTMLAttributes<HTMLElement> {
200
+ as?: React.ElementType;
201
+ size?: StackItemSize;
202
+ alignSelf?: StackAlign;
203
+ isScrollable?: boolean;
204
+ }
205
+
206
+ const StackItem = React.forwardRef<HTMLElement, StackItemProps>(
207
+ (
208
+ {
209
+ as: Component = "div",
210
+ size = "static",
211
+ alignSelf,
212
+ isScrollable = false,
213
+ className,
214
+ ...props
215
+ },
216
+ ref,
217
+ ) => (
218
+ <Component
219
+ ref={ref}
220
+ className={cn(
221
+ "stack-item",
222
+ size === "fill" && "stack-item--fill",
223
+ alignSelf && `stack-item--align-${alignSelf}`,
224
+ isScrollable && "stack-item--scrollable",
225
+ className,
226
+ )}
227
+ {...props}
228
+ />
229
+ ),
230
+ );
231
+ StackItem.displayName = "StackItem";
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // Grid
235
+ // ---------------------------------------------------------------------------
236
+
237
+ type GridColumnsConfig =
238
+ | GridCols
239
+ | {
240
+ minWidth: number;
241
+ max?: number;
242
+ repeat?: "fill" | "fit";
243
+ };
244
+
245
+ interface GridProps extends React.HTMLAttributes<HTMLElement> {
246
+ as?: React.ElementType;
247
+ /** Preferred Astryx name. Number = fixed columns; object = auto-fill by min width. */
248
+ columns?: GridColumnsConfig;
249
+ /** @deprecated Use `columns`. */
250
+ cols?: GridCols;
251
+ colsSm?: GridCols;
252
+ colsMd?: GridCols;
253
+ colsLg?: GridCols;
254
+ gap?: SpaceInput;
255
+ rowGap?: SpaceInput;
256
+ columnGap?: SpaceInput;
257
+ /** @deprecated Use `columnGap`. */
258
+ colGap?: number;
259
+ align?: "start" | "center" | "end" | "stretch";
260
+ justify?: "start" | "center" | "end" | "stretch";
261
+ width?: SizeValue;
262
+ height?: SizeValue;
263
+ maxWidth?: SizeValue;
264
+ minHeight?: SizeValue;
265
+ }
266
+
267
+ const Grid = React.forwardRef<HTMLElement, GridProps>(
268
+ (
269
+ {
270
+ as: Component = "div",
271
+ columns,
272
+ cols,
273
+ colsSm,
274
+ colsMd,
275
+ colsLg,
276
+ gap = "md",
277
+ rowGap,
278
+ columnGap,
279
+ colGap,
280
+ align,
281
+ justify,
282
+ width,
283
+ height,
284
+ maxWidth,
285
+ minHeight,
286
+ className,
287
+ style,
288
+ ...props
289
+ },
290
+ ref,
291
+ ) => {
292
+ const resolvedColumnGap = columnGap ?? colGap;
293
+ const isResponsive = columns !== undefined && typeof columns === "object";
294
+ const fixedCols = typeof columns === "number" ? columns : (cols ?? 1);
295
+
296
+ let template: string | undefined;
297
+ if (isResponsive) {
298
+ const repeat = columns.repeat ?? "fill";
299
+ if (columns.max) {
300
+ template = `repeat(auto-${repeat}, minmax(max(${columns.minWidth}px, calc(100% / ${columns.max})), 1fr))`;
301
+ } else {
302
+ template = `repeat(auto-${repeat}, minmax(${columns.minWidth}px, 1fr))`;
303
+ }
304
+ }
305
+
306
+ return (
307
+ <Component
308
+ ref={ref}
309
+ className={cn(
310
+ "grid-layout",
311
+ !isResponsive && `grid-layout--cols-${fixedCols}`,
312
+ !isResponsive && colsSm && `grid-layout--sm-cols-${colsSm}`,
313
+ !isResponsive && colsMd && `grid-layout--md-cols-${colsMd}`,
314
+ !isResponsive && colsLg && `grid-layout--lg-cols-${colsLg}`,
315
+ align && `grid-layout--align-${align}`,
316
+ justify && `grid-layout--justify-${justify}`,
317
+ className,
318
+ )}
319
+ style={{
320
+ gridTemplateColumns: template,
321
+ gap:
322
+ rowGap === undefined && resolvedColumnGap === undefined
323
+ ? gapCss(gap)
324
+ : undefined,
325
+ rowGap: gapCss(rowGap),
326
+ columnGap: gapCss(resolvedColumnGap),
327
+ width: toCssSize(width),
328
+ height: toCssSize(height),
329
+ maxWidth: toCssSize(maxWidth),
330
+ minHeight: toCssSize(minHeight),
331
+ ...style,
332
+ }}
333
+ {...props}
334
+ />
335
+ );
336
+ },
337
+ );
338
+ Grid.displayName = "Grid";
339
+
340
+ // ---------------------------------------------------------------------------
341
+ // GridSpan
342
+ // ---------------------------------------------------------------------------
343
+
344
+ interface GridSpanProps extends React.HTMLAttributes<HTMLElement> {
345
+ as?: React.ElementType;
346
+ columns?: number | "full";
347
+ rows?: number;
348
+ }
349
+
350
+ const GridSpan = React.forwardRef<HTMLElement, GridSpanProps>(
351
+ (
352
+ { as: Component = "div", columns, rows, className, style, ...props },
353
+ ref,
354
+ ) => (
355
+ <Component
356
+ ref={ref}
357
+ className={cn("grid-span", className)}
358
+ style={{
359
+ gridColumn:
360
+ columns === "full"
361
+ ? "1 / -1"
362
+ : columns != null
363
+ ? `span ${columns}`
364
+ : undefined,
365
+ gridRow: rows != null ? `span ${rows}` : undefined,
366
+ ...style,
367
+ }}
368
+ {...props}
369
+ />
370
+ ),
371
+ );
372
+ GridSpan.displayName = "GridSpan";
373
+
374
+ // ---------------------------------------------------------------------------
375
+ // Columns — asymmetric tracks (compat). Prefer Layout + LayoutPanel or GridSpan.
376
+ // ---------------------------------------------------------------------------
377
+
378
+ type ColumnsPreset =
379
+ | "sidebar"
380
+ | "sidebar-right"
381
+ | "wide-sidebar"
382
+ | "narrow-sidebar"
383
+ | "split"
384
+ | "1-2"
385
+ | "2-1"
386
+ | "1-3"
387
+ | "3-1";
388
+
389
+ type ColumnsCollapse = "sm" | "md" | "lg";
390
+
391
+ interface ColumnsProps extends React.HTMLAttributes<HTMLElement> {
392
+ as?: React.ElementType;
393
+ preset?: ColumnsPreset;
394
+ /** Raw `grid-template-columns`. Overrides `preset`; prefer a preset so the value stays in the design system. */
395
+ template?: string;
396
+ gap?: SpaceInput;
397
+ /** @deprecated Use `columnGap`. */
398
+ colGap?: SpaceInput;
399
+ columnGap?: SpaceInput;
400
+ rowGap?: SpaceInput;
401
+ collapse?: ColumnsCollapse;
402
+ }
403
+
404
+ /** Sidebar widths reuse the panel scale so a Columns sidebar and a LayoutPanel match. */
405
+ const ColumnsPresetMap: Record<ColumnsPreset, string> = {
406
+ sidebar: "var(--layout-panel-width-default) 1fr",
407
+ "sidebar-right": "1fr var(--layout-panel-width-default)",
408
+ "wide-sidebar": "var(--layout-panel-width-wide) 1fr",
409
+ "narrow-sidebar": "var(--layout-panel-width-narrow) 1fr",
410
+ split: "1fr 1fr",
411
+ "1-2": "1fr 2fr",
412
+ "2-1": "2fr 1fr",
413
+ "1-3": "1fr 3fr",
414
+ "3-1": "3fr 1fr",
415
+ };
416
+
417
+ const Columns = React.forwardRef<HTMLElement, ColumnsProps>(
418
+ (
419
+ {
420
+ as: Component = "div",
421
+ preset = "split",
422
+ template,
423
+ gap = "xl",
424
+ colGap,
425
+ columnGap,
426
+ rowGap,
427
+ collapse,
428
+ className,
429
+ style,
430
+ ...props
431
+ },
432
+ ref,
433
+ ) => {
434
+ const resolvedColumnGap = columnGap ?? colGap;
435
+ return (
436
+ <Component
437
+ ref={ref}
438
+ className={cn(
439
+ "columns-layout",
440
+ collapse && `columns-layout--collapse-${collapse}`,
441
+ className,
442
+ )}
443
+ style={{
444
+ gridTemplateColumns: template ?? ColumnsPresetMap[preset],
445
+ gap:
446
+ rowGap === undefined && resolvedColumnGap === undefined
447
+ ? gapCss(gap)
448
+ : undefined,
449
+ rowGap: gapCss(rowGap),
450
+ columnGap: gapCss(resolvedColumnGap),
451
+ ...style,
452
+ }}
453
+ {...props}
454
+ />
455
+ );
456
+ },
457
+ );
458
+ Columns.displayName = "Columns";
459
+
460
+ // ---------------------------------------------------------------------------
461
+ // Center
462
+ // ---------------------------------------------------------------------------
463
+
464
+ type CenterAxis = "both" | "horizontal" | "vertical";
465
+
466
+ interface CenterProps extends React.HTMLAttributes<HTMLElement> {
467
+ as?: React.ElementType;
468
+ axis?: CenterAxis;
469
+ width?: SizeValue;
470
+ height?: SizeValue;
471
+ maxWidth?: SizeValue;
472
+ minHeight?: SizeValue;
473
+ padding?: SpaceInput;
474
+ isInline?: boolean;
475
+ }
476
+
477
+ const Center = React.forwardRef<HTMLElement, CenterProps>(
478
+ (
479
+ {
480
+ as: Component = "div",
481
+ axis = "both",
482
+ width,
483
+ height,
484
+ maxWidth,
485
+ minHeight,
486
+ padding,
487
+ isInline = false,
488
+ className,
489
+ style,
490
+ ...props
491
+ },
492
+ ref,
493
+ ) => (
494
+ <Component
495
+ ref={ref}
496
+ className={cn(
497
+ "center",
498
+ isInline && "center--inline",
499
+ `center--${axis}`,
500
+ className,
501
+ )}
502
+ style={{
503
+ width: toCssSize(width),
504
+ height: toCssSize(height),
505
+ maxWidth: toCssSize(maxWidth),
506
+ minHeight: toCssSize(minHeight),
507
+ padding: gapCss(padding),
508
+ ...style,
509
+ }}
510
+ {...props}
511
+ />
512
+ ),
513
+ );
514
+ Center.displayName = "Center";
515
+
516
+ // ---------------------------------------------------------------------------
517
+ // FormLayout
518
+ // ---------------------------------------------------------------------------
519
+
520
+ type FormLayoutDirection = "vertical" | "horizontal" | "horizontal-labels";
521
+
522
+ interface FormLayoutProps extends React.HTMLAttributes<HTMLElement> {
523
+ as?: React.ElementType;
524
+ direction?: FormLayoutDirection;
525
+ gap?: SpaceInput;
526
+ width?: SizeValue;
527
+ maxWidth?: SizeValue;
528
+ }
529
+
530
+ const FormLayout = React.forwardRef<HTMLElement, FormLayoutProps>(
531
+ (
532
+ {
533
+ as: Component = "div",
534
+ direction = "vertical",
535
+ gap,
536
+ width,
537
+ maxWidth,
538
+ className,
539
+ style,
540
+ ...props
541
+ },
542
+ ref,
543
+ ) => (
544
+ <Component
545
+ ref={ref}
546
+ className={cn(
547
+ "form-layout",
548
+ direction === "horizontal" && "form-layout--horizontal",
549
+ direction === "horizontal-labels" && "form-layout--horizontal-labels",
550
+ className,
551
+ )}
552
+ style={{
553
+ gap: gapCss(gap),
554
+ width: toCssSize(width),
555
+ maxWidth: toCssSize(maxWidth),
556
+ ...style,
557
+ }}
558
+ {...props}
559
+ />
560
+ ),
561
+ );
562
+ FormLayout.displayName = "FormLayout";
563
+
564
+ // ---------------------------------------------------------------------------
565
+ // Section
566
+ // ---------------------------------------------------------------------------
567
+
568
+ type SectionPadding = "none" | "sm" | "md" | "lg" | "xl";
569
+
570
+ interface SectionProps extends React.HTMLAttributes<HTMLElement> {
571
+ as?: React.ElementType;
572
+ padding?: SectionPadding;
573
+ maxWidth?: SizeValue;
574
+ }
575
+
576
+ const Section = React.forwardRef<HTMLElement, SectionProps>(
577
+ (
578
+ {
579
+ as: Component = "section",
580
+ padding = "lg",
581
+ maxWidth,
582
+ className,
583
+ style,
584
+ ...props
585
+ },
586
+ ref,
587
+ ) => (
588
+ <Component
589
+ ref={ref}
590
+ className={cn(
591
+ "section-layout",
592
+ padding !== "none" && `section-layout--${padding}`,
593
+ className,
594
+ )}
595
+ style={{ maxWidth: toCssSize(maxWidth), ...style }}
596
+ {...props}
597
+ />
598
+ ),
599
+ );
600
+ Section.displayName = "Section";
601
+
602
+ // ---------------------------------------------------------------------------
603
+ // Layout — page shell (header / start panel / content / end panel / footer)
604
+ // ---------------------------------------------------------------------------
605
+
606
+ type LayoutHeight = "fill" | "auto";
607
+ type LayoutPanelPlacement = "start" | "end";
608
+
609
+ /** Panel widths come from `--layout-panel-width-*`, never from app-side px. */
610
+ type LayoutPanelWidth = "narrow" | "default" | "wide";
611
+ /** Content measures come from `--layout-measure-*`, never from app-side px. */
612
+ type LayoutMeasure = "sm" | "md" | "lg" | "full";
613
+
614
+ /**
615
+ * Slots deliberately drop `className` and `style`: both let an app freeze a value
616
+ * the design system can no longer change. Every dimension is a named prop that
617
+ * resolves to a `--layout-*` variable.
618
+ */
619
+ type LayoutSlotHTMLProps<T = HTMLElement> = Omit<
620
+ React.HTMLAttributes<T>,
621
+ "className" | "style"
622
+ >;
623
+
624
+ function panelWidthVar(
625
+ width: LayoutPanelWidth | number | undefined,
626
+ ): string | undefined {
627
+ if (width == null) return undefined;
628
+ if (typeof width === "number") {
629
+ warnOnce(
630
+ `LayoutPanel: width={${width}} freezes ${width}px into this app. ` +
631
+ `Use width="narrow" | "default" | "wide" so --layout-panel-width-* stays in control.`,
632
+ );
633
+ return `${width}px`;
634
+ }
635
+ return `var(--layout-panel-width-${width})`;
636
+ }
637
+
638
+ function measureVar(
639
+ measure: LayoutMeasure | number | string | undefined,
640
+ ): string | undefined {
641
+ if (measure == null) return undefined;
642
+ if (
643
+ typeof measure !== "string" ||
644
+ !["sm", "md", "lg", "full"].includes(measure)
645
+ ) {
646
+ const frozen = toCssSize(measure as number | string);
647
+ warnOnce(
648
+ `Layout: contentWidth={${JSON.stringify(measure)}} freezes ${frozen} into this app. ` +
649
+ `Use contentWidth="sm" | "md" | "lg" | "full" so --layout-measure-* stays in control.`,
650
+ );
651
+ return frozen;
652
+ }
653
+ return `var(--layout-measure-${measure})`;
654
+ }
655
+ type LayoutSlotId = "header" | "footer" | "content" | "panel";
656
+
657
+ const LAYOUT_SLOT = Symbol.for("iwbi.layoutSlot");
658
+
659
+ type SlotComponent = React.ForwardRefExoticComponent<never> & {
660
+ [LAYOUT_SLOT]?: LayoutSlotId;
661
+ render?: { [LAYOUT_SLOT]?: LayoutSlotId };
662
+ type?: { [LAYOUT_SLOT]?: LayoutSlotId };
663
+ };
664
+
665
+ function markSlot<T>(component: T, slot: LayoutSlotId): T {
666
+ (component as T & { [LAYOUT_SLOT]: LayoutSlotId })[LAYOUT_SLOT] = slot;
667
+ return component;
668
+ }
669
+
670
+ function slotIdOf(element: React.ReactElement): LayoutSlotId | undefined {
671
+ const type = element.type;
672
+ if (typeof type === "string" || typeof type === "number") return undefined;
673
+ const tagged = type as SlotComponent;
674
+ return (
675
+ tagged[LAYOUT_SLOT] ??
676
+ tagged.render?.[LAYOUT_SLOT] ??
677
+ tagged.type?.[LAYOUT_SLOT]
678
+ );
679
+ }
680
+
681
+ interface LayoutHeaderProps extends LayoutSlotHTMLProps {
682
+ /** Kept narrow so the landmark cannot be lost by accident. */
683
+ as?: "header" | "div";
684
+ }
685
+
686
+ const LayoutHeader = markSlot(
687
+ React.forwardRef<HTMLElement, LayoutHeaderProps>(
688
+ ({ as = "header", ...props }, ref) => {
689
+ const Component = as as React.ElementType;
690
+ return <Component ref={ref} className="layout-header" {...props} />;
691
+ },
692
+ ),
693
+ "header",
694
+ );
695
+ LayoutHeader.displayName = "LayoutHeader";
696
+
697
+ interface LayoutFooterProps extends LayoutSlotHTMLProps {
698
+ /** Kept narrow so the landmark cannot be lost by accident. */
699
+ as?: "footer" | "div";
700
+ }
701
+
702
+ const LayoutFooter = markSlot(
703
+ React.forwardRef<HTMLElement, LayoutFooterProps>(
704
+ ({ as = "footer", ...props }, ref) => {
705
+ const Component = as as React.ElementType;
706
+ return <Component ref={ref} className="layout-footer" {...props} />;
707
+ },
708
+ ),
709
+ "footer",
710
+ );
711
+ LayoutFooter.displayName = "LayoutFooter";
712
+
713
+ interface LayoutContentProps extends LayoutSlotHTMLProps {
714
+ /** Kept narrow so the landmark cannot be lost by accident. */
715
+ as?: "main" | "div";
716
+ isScrollable?: boolean;
717
+ }
718
+
719
+ const LayoutContent = markSlot(
720
+ React.forwardRef<HTMLElement, LayoutContentProps>(
721
+ ({ as = "main", isScrollable = true, tabIndex, ...props }, ref) => {
722
+ const Component = as as React.ElementType;
723
+ return (
724
+ <Component
725
+ ref={ref}
726
+ className={cn(
727
+ "layout-content",
728
+ isScrollable && "layout-content--scrollable",
729
+ )}
730
+ tabIndex={tabIndex ?? (isScrollable ? 0 : undefined)}
731
+ {...props}
732
+ />
733
+ );
734
+ },
735
+ ),
736
+ "content",
737
+ );
738
+ LayoutContent.displayName = "LayoutContent";
739
+
740
+ interface LayoutPanelProps extends LayoutSlotHTMLProps {
741
+ /** Kept narrow so the landmark cannot be lost by accident. */
742
+ as?: "aside" | "nav" | "div";
743
+ placement?: LayoutPanelPlacement;
744
+ /** Named width backed by `--layout-panel-width-*`. Default `default` (17.5rem). */
745
+ width?: LayoutPanelWidth;
746
+ /** Named inset backed by `--layout-panel-padding`. Default `xl`. */
747
+ padding?: SpaceName;
748
+ isScrollable?: boolean;
749
+ }
750
+
751
+ const LayoutPanel = markSlot(
752
+ React.forwardRef<HTMLElement, LayoutPanelProps>(
753
+ (
754
+ {
755
+ as = "aside",
756
+ placement = "start",
757
+ width,
758
+ padding,
759
+ isScrollable = true,
760
+ tabIndex,
761
+ "aria-label": ariaLabel,
762
+ ...props
763
+ },
764
+ ref,
765
+ ) => {
766
+ const Component = as as React.ElementType;
767
+ return (
768
+ <Component
769
+ ref={ref}
770
+ className={cn(
771
+ "layout-panel",
772
+ `layout-panel--${placement}`,
773
+ isScrollable && "layout-panel--scrollable",
774
+ )}
775
+ style={{
776
+ ...(width != null
777
+ ? { ["--layout-panel-width" as string]: panelWidthVar(width) }
778
+ : undefined),
779
+ ...(padding != null
780
+ ? { ["--layout-panel-padding" as string]: tokenCss(padding) }
781
+ : undefined),
782
+ }}
783
+ tabIndex={tabIndex ?? (isScrollable ? 0 : undefined)}
784
+ aria-label={
785
+ ariaLabel ?? (placement === "end" ? "Details" : "Navigation")
786
+ }
787
+ {...props}
788
+ />
789
+ );
790
+ },
791
+ ),
792
+ "panel",
793
+ );
794
+ LayoutPanel.displayName = "LayoutPanel";
795
+
796
+ interface LayoutProps extends LayoutSlotHTMLProps<HTMLDivElement> {
797
+ height?: LayoutHeight;
798
+ /** Named measure backed by `--layout-measure-*`. Caps and centers the main column. */
799
+ contentWidth?: LayoutMeasure;
800
+ /** Named inset for header/footer (horizontal) and content (all sides). Default `2xl`. Panels use their own `padding`. */
801
+ padding?: SpaceName;
802
+ }
803
+
804
+ type LayoutSlots = {
805
+ header?: React.ReactNode;
806
+ footer?: React.ReactNode;
807
+ start?: React.ReactNode;
808
+ end?: React.ReactNode;
809
+ content?: React.ReactNode;
810
+ };
811
+
812
+ function collectSlots(children: React.ReactNode): LayoutSlots {
813
+ const slots: LayoutSlots = {};
814
+ const leftovers: React.ReactNode[] = [];
815
+ let sawStartPanel = false;
816
+
817
+ React.Children.forEach(children, (child) => {
818
+ if (!React.isValidElement(child)) {
819
+ leftovers.push(child);
820
+ return;
821
+ }
822
+ const slot = slotIdOf(child);
823
+ if (slot === "header" && slots.header == null) {
824
+ slots.header = child;
825
+ return;
826
+ }
827
+ if (slot === "footer" && slots.footer == null) {
828
+ slots.footer = child;
829
+ return;
830
+ }
831
+ if (slot === "content" && slots.content == null) {
832
+ slots.content = child;
833
+ return;
834
+ }
835
+ if (slot === "panel") {
836
+ const placement =
837
+ (child.props as LayoutPanelProps).placement ??
838
+ (sawStartPanel ? "end" : "start");
839
+ if (placement === "end" && slots.end == null) {
840
+ slots.end = child;
841
+ return;
842
+ }
843
+ if (slots.start == null) {
844
+ slots.start = child;
845
+ sawStartPanel = true;
846
+ return;
847
+ }
848
+ if (slots.end == null) {
849
+ slots.end = child;
850
+ return;
851
+ }
852
+ }
853
+ leftovers.push(child);
854
+ });
855
+
856
+ if (leftovers.length > 0) {
857
+ if (process.env.NODE_ENV !== "production") {
858
+ console.warn(
859
+ "Layout: child was not LayoutHeader, LayoutFooter, LayoutContent, or LayoutPanel.",
860
+ leftovers,
861
+ );
862
+ }
863
+ if (slots.content == null) {
864
+ slots.content = leftovers.length === 1 ? leftovers[0] : leftovers;
865
+ }
866
+ }
867
+
868
+ return slots;
869
+ }
870
+
871
+ const Layout = React.forwardRef<HTMLDivElement, LayoutProps>(
872
+ (
873
+ { children, height = "fill", contentWidth, padding = "2xl", ...props },
874
+ ref,
875
+ ) => {
876
+ const slots = collectSlots(children);
877
+
878
+ return (
879
+ <div
880
+ ref={ref}
881
+ className={cn(
882
+ "layout",
883
+ height === "fill" ? "layout--fill" : "layout--auto",
884
+ )}
885
+ style={{
886
+ ["--layout-padding" as string]: gapCss(padding),
887
+ ...(contentWidth != null
888
+ ? {
889
+ ["--layout-content-max-width" as string]:
890
+ measureVar(contentWidth),
891
+ }
892
+ : undefined),
893
+ }}
894
+ {...props}
895
+ >
896
+ {slots.header}
897
+ {slots.start}
898
+ {slots.content != null &&
899
+ (React.isValidElement(slots.content) &&
900
+ slotIdOf(slots.content as React.ReactElement) === "content" ? (
901
+ slots.content
902
+ ) : (
903
+ <LayoutContent>{slots.content}</LayoutContent>
904
+ ))}
905
+ {slots.end}
906
+ {slots.footer}
907
+ </div>
908
+ );
909
+ },
910
+ );
911
+ Layout.displayName = "Layout";
912
+
913
+ // ---------------------------------------------------------------------------
914
+ // Container — max-width wrapper (kept; prefer Center / Layout contentWidth)
915
+ // ---------------------------------------------------------------------------
916
+
917
+ type ContainerSize = "sm" | "md" | "lg" | "xl" | "2xl" | "full";
918
+
919
+ interface ContainerProps extends React.HTMLAttributes<HTMLElement> {
920
+ as?: React.ElementType;
921
+ size?: ContainerSize;
922
+ center?: boolean;
923
+ padded?: boolean;
924
+ }
925
+
926
+ const Container = React.forwardRef<HTMLElement, ContainerProps>(
927
+ (
928
+ {
929
+ as: Component = "div",
930
+ size = "xl",
931
+ center = true,
932
+ padded = true,
933
+ className,
934
+ ...props
935
+ },
936
+ ref,
937
+ ) => (
938
+ <Component
939
+ ref={ref}
940
+ className={cn(
941
+ "container-layout",
942
+ `container-layout--${size}`,
943
+ center && "container-layout--center",
944
+ padded && "container-layout--padded",
945
+ className,
946
+ )}
947
+ {...props}
948
+ />
949
+ ),
950
+ );
951
+ Container.displayName = "Container";
952
+
953
+ // ---------------------------------------------------------------------------
954
+ // Show / Hide
955
+ // ---------------------------------------------------------------------------
956
+
957
+ type Breakpoint = "sm" | "md" | "lg" | "xl";
958
+
959
+ interface ShowProps extends React.HTMLAttributes<HTMLDivElement> {
960
+ above?: Breakpoint;
961
+ below?: Breakpoint;
962
+ as?: React.ElementType;
963
+ }
964
+
965
+ const Show = React.forwardRef<HTMLDivElement, ShowProps>(
966
+ ({ above, below, as: Component = "div", className, ...props }, ref) => (
967
+ <Component
968
+ ref={ref}
969
+ className={cn(
970
+ above && `show-above-${above}`,
971
+ below && `show-below-${below}`,
972
+ className,
973
+ )}
974
+ {...props}
975
+ />
976
+ ),
977
+ );
978
+ Show.displayName = "Show";
979
+
980
+ interface HideProps extends React.HTMLAttributes<HTMLDivElement> {
981
+ above?: Breakpoint;
982
+ below?: Breakpoint;
983
+ as?: React.ElementType;
984
+ }
985
+
986
+ const Hide = React.forwardRef<HTMLDivElement, HideProps>(
987
+ ({ above, below, as: Component = "div", className, ...props }, ref) => (
988
+ <Component
989
+ ref={ref}
990
+ className={cn(
991
+ above && `hide-above-${above}`,
992
+ below && `hide-below-${below}`,
993
+ className,
994
+ )}
995
+ {...props}
996
+ />
997
+ ),
998
+ );
999
+ Hide.displayName = "Hide";
1000
+
1001
+ // ---------------------------------------------------------------------------
1002
+ // Positioned / Spacer
1003
+ // ---------------------------------------------------------------------------
1004
+
1005
+ type PositionType = "relative" | "absolute" | "fixed" | "sticky";
1006
+
1007
+ interface PositionedProps extends React.HTMLAttributes<HTMLElement> {
1008
+ as?: React.ElementType;
1009
+ position?: PositionType;
1010
+ top?: number | string;
1011
+ right?: number | string;
1012
+ bottom?: number | string;
1013
+ left?: number | string;
1014
+ inset?: number | string;
1015
+ zIndex?: number;
1016
+ fill?: boolean;
1017
+ }
1018
+
1019
+ const Positioned = React.forwardRef<HTMLElement, PositionedProps>(
1020
+ (
1021
+ {
1022
+ as: Component = "div",
1023
+ position = "relative",
1024
+ top,
1025
+ right,
1026
+ bottom,
1027
+ left,
1028
+ inset,
1029
+ zIndex,
1030
+ fill = false,
1031
+ className,
1032
+ style,
1033
+ ...props
1034
+ },
1035
+ ref,
1036
+ ) => {
1037
+ const resolveValue = (v: number | string | undefined) =>
1038
+ v === undefined ? undefined : typeof v === "number" ? spacing(v) : v;
1039
+
1040
+ return (
1041
+ <Component
1042
+ ref={ref}
1043
+ className={cn(`positioned--${position}`, className)}
1044
+ style={{
1045
+ top: resolveValue(top),
1046
+ right: resolveValue(right),
1047
+ bottom: resolveValue(bottom),
1048
+ left: resolveValue(left),
1049
+ inset: fill ? 0 : resolveValue(inset),
1050
+ zIndex,
1051
+ ...style,
1052
+ }}
1053
+ {...props}
1054
+ />
1055
+ );
1056
+ },
1057
+ );
1058
+ Positioned.displayName = "Positioned";
1059
+
1060
+ export type SpacerSpace = SpaceName | SpacerSpacePx;
1061
+ export type SpacerAxis = "horizontal" | "vertical";
1062
+
1063
+ function spacerTokenCss(space?: SpacerSpace, size?: number): string {
1064
+ if (typeof space === "string") return tokenCss(space);
1065
+ if (typeof space === "number") {
1066
+ const named = PX_TO_SPACE[space];
1067
+ return named ? tokenCss(named) : `${space}px`;
1068
+ }
1069
+ if (size != null) {
1070
+ const px = size * 4;
1071
+ const named = PX_TO_SPACE[px as SpacerSpacePx];
1072
+ return named ? tokenCss(named) : spacing(size);
1073
+ }
1074
+ return tokenCss("md");
1075
+ }
1076
+
1077
+ interface SpacerProps extends React.HTMLAttributes<HTMLDivElement> {
1078
+ /** Named token (`md`) or legacy px (`16`). Default `md`. */
1079
+ space?: SpacerSpace;
1080
+ /**
1081
+ * @deprecated 4px units (`size={4}` → 16px / `md`). Prefer `space="md"`.
1082
+ */
1083
+ size?: number;
1084
+ axis?: SpacerAxis;
1085
+ /** Grow to fill leftover space in a stack. Ignores `space` / `axis`. */
1086
+ flex?: boolean;
1087
+ }
1088
+
1089
+ const Spacer = ({
1090
+ space,
1091
+ size,
1092
+ axis = "vertical",
1093
+ flex = false,
1094
+ className,
1095
+ style,
1096
+ ...props
1097
+ }: SpacerProps) => {
1098
+ const token = spacerTokenCss(space, size);
1099
+ return (
1100
+ <div
1101
+ aria-hidden="true"
1102
+ className={cn(
1103
+ "spacer",
1104
+ flex
1105
+ ? "spacer--flex"
1106
+ : axis === "horizontal"
1107
+ ? "spacer--horizontal"
1108
+ : "spacer--vertical",
1109
+ className,
1110
+ )}
1111
+ style={{
1112
+ ...(flex
1113
+ ? undefined
1114
+ : axis === "vertical"
1115
+ ? { height: token, minHeight: token }
1116
+ : { width: token, minWidth: token }),
1117
+ ...style,
1118
+ }}
1119
+ {...props}
1120
+ />
1121
+ );
1122
+ };
1123
+ Spacer.displayName = "Spacer";
1124
+
1125
+ export {
1126
+ Stack,
1127
+ HStack,
1128
+ VStack,
1129
+ StackItem,
1130
+ Grid,
1131
+ GridSpan,
1132
+ Columns,
1133
+ Center,
1134
+ FormLayout,
1135
+ Section,
1136
+ Layout,
1137
+ LayoutHeader,
1138
+ LayoutFooter,
1139
+ LayoutContent,
1140
+ LayoutPanel,
1141
+ Container,
1142
+ Show,
1143
+ Hide,
1144
+ Positioned,
1145
+ Spacer,
1146
+ // Previous names — keep working until remaining stories migrate
1147
+ Layout as AppShell,
1148
+ LayoutHeader as AppShellHeader,
1149
+ LayoutPanel as AppShellSidebar,
1150
+ LayoutContent as AppShellMain,
1151
+ LayoutFooter as AppShellFooter,
1152
+ };
1153
+
1154
+ export type {
1155
+ StackProps,
1156
+ HStackProps,
1157
+ VStackProps,
1158
+ StackItemProps,
1159
+ GridProps,
1160
+ GridSpanProps,
1161
+ ColumnsProps,
1162
+ CenterProps,
1163
+ FormLayoutProps,
1164
+ SectionProps,
1165
+ LayoutProps,
1166
+ LayoutHeaderProps,
1167
+ LayoutFooterProps,
1168
+ LayoutContentProps,
1169
+ LayoutPanelProps,
1170
+ ContainerProps,
1171
+ ShowProps,
1172
+ HideProps,
1173
+ PositionedProps,
1174
+ SpacerProps,
1175
+ LayoutHeight,
1176
+ LayoutPanelWidth,
1177
+ LayoutPanelPlacement,
1178
+ LayoutMeasure,
1179
+ CenterAxis,
1180
+ FormLayoutDirection,
1181
+ LayoutProps as AppShellProps,
1182
+ };