@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.
- package/client/App.tsx +0 -2
- package/client/assets/alert/icon-clear.svg +4 -0
- package/client/assets/alert/icon-done.svg +3 -0
- package/client/assets/avatar-identity/photo.png +0 -0
- package/client/assets/avatar-identity/placeholder-icon.svg +8 -0
- package/client/assets/breadcrumb/icon-ellipsis.svg +5 -0
- package/client/assets/breadcrumb/icon-home.svg +3 -0
- package/client/assets/breadcrumb/icon-separator.svg +3 -0
- package/client/assets/button-v01/icon-back.svg +4 -0
- package/client/assets/button-v01/icon-check.svg +3 -0
- package/client/assets/button-v01/icon-close.svg +4 -0
- package/client/assets/button-v01/icon-delete.svg +3 -0
- package/client/assets/button-v01/icon-device.svg +3 -0
- package/client/assets/dropdown/icon-carrot-contract.svg +3 -0
- package/client/assets/dropdown/icon-carrot-expand.svg +3 -0
- package/client/assets/dropdown/icon-clear.svg +4 -0
- package/client/assets/dropdown/icon-enter.svg +3 -0
- package/client/components/icons/UtilityChangeType.tsx +21 -0
- package/client/components/ui/alert-dialog-panel.tsx +246 -0
- package/client/components/ui/alert-v02.tsx +214 -0
- package/client/components/ui/app-bar.tsx +299 -0
- package/client/components/ui/avatar-identity.tsx +122 -0
- package/client/components/ui/banner.tsx +108 -0
- package/client/components/ui/bottom-navigation.tsx +223 -0
- package/client/components/ui/breadcrumb-trail.tsx +170 -0
- package/client/components/ui/button-v01.tsx +238 -0
- package/client/components/ui/button.tsx +2 -0
- package/client/components/ui/card.tsx +108 -26
- package/client/components/ui/code-badge.tsx +62 -0
- package/client/components/ui/date-picker.tsx +188 -0
- package/client/components/ui/dropdown.tsx +448 -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 +1182 -0
- package/client/components/ui/pagination-v01.tsx +157 -0
- package/client/components/ui/password-field-v01.tsx +67 -0
- package/client/components/ui/search-field-v01.tsx +192 -0
- package/client/components/ui/section-heading.tsx +49 -0
- package/client/components/ui/sidebar.tsx +13 -8
- package/client/components/ui/stepper.tsx +148 -0
- package/client/components/ui/switch-v01.tsx +71 -0
- package/client/components/ui/system-bar.tsx +119 -0
- package/client/components/ui/table-v01.tsx +230 -0
- package/client/components/ui/tabs-v01.tsx +99 -0
- package/client/components/ui/tag-v02.tsx +113 -0
- package/client/components/ui/text-area-v01.tsx +33 -0
- package/client/components/ui/text-field-v01.tsx +108 -0
- package/client/components/ui/timeline.tsx +181 -0
- package/client/components/ui/toast-v01.tsx +206 -0
- package/client/components/ui/tooltip.tsx +264 -71
- package/client/global.css +9565 -4378
- package/client/storybook.css +125 -0
- package/client/ui/Accordion.stories.tsx +1 -1
- package/client/ui/AlertDialogPanel/AlertDialogPanel.stories.tsx +150 -0
- package/client/ui/AlertV02/AlertV02.stories.tsx +188 -0
- package/client/ui/AppBar/app-bar.stories.tsx +280 -0
- package/client/ui/AspectRatio.stories.tsx +1 -1
- package/client/ui/AvatarIdentity/AvatarIdentity.stories.tsx +77 -0
- package/client/ui/Banner/banner.stories.tsx +238 -0
- package/client/ui/BottomNavigation/bottom-navigation.stories.tsx +285 -0
- package/client/ui/BreadcrumbTrail/BreadcrumbTrail.stories.tsx +232 -0
- package/client/ui/Button.stories.tsx +1 -1
- package/client/ui/ButtonV01/ButtonV01.stories.tsx +417 -0
- package/client/ui/Calendar/Calendar.stories.tsx +1 -1
- package/client/ui/Card.stories.tsx +285 -168
- package/client/ui/Carousel/Carousel.stories.tsx +1 -1
- package/client/ui/Chart/Chart.stories.tsx +1 -1
- package/client/ui/Checkbox.stories.tsx +1 -1
- package/client/ui/Collapsible/Collapsible.stories.tsx +1 -1
- package/client/ui/Colors.stories.tsx +1 -1
- package/client/ui/Command/Command.stories.tsx +1 -1
- package/client/ui/ContextMenu/ContextMenu.stories.tsx +1 -1
- package/client/ui/DatePicker/DatePicker.stories.tsx +293 -0
- package/client/ui/Dialog.stories.tsx +1 -1
- package/client/ui/Drawer/Drawer.stories.tsx +1 -1
- package/client/ui/Dropdown/Dropdown.stories.tsx +205 -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/Form/Form.stories.tsx +1 -1
- package/client/ui/Hero.stories.tsx +1 -1
- package/client/ui/HoverCard/HoverCard.stories.tsx +1 -1
- package/client/ui/Icons.stories.tsx +1 -1
- package/client/ui/Input.stories.tsx +1 -1
- package/client/ui/InputOTP/InputOTP.stories.tsx +1 -1
- package/client/ui/Label.stories.tsx +1 -1
- package/client/ui/Layout/Center.stories.tsx +100 -0
- package/client/ui/Layout/Columns.stories.tsx +136 -0
- package/client/ui/Layout/Container.stories.tsx +104 -0
- package/client/ui/Layout/FormLayout.stories.tsx +144 -0
- package/client/ui/Layout/Grid.stories.tsx +158 -0
- package/client/ui/Layout/GridSpan.stories.tsx +75 -0
- package/client/ui/Layout/HStack.stories.tsx +128 -0
- package/client/ui/Layout/Layout.stories.tsx +287 -0
- package/client/ui/Layout/LayoutContent.stories.tsx +91 -0
- package/client/ui/Layout/LayoutFooter.stories.tsx +77 -0
- package/client/ui/Layout/LayoutHeader.stories.tsx +104 -0
- package/client/ui/Layout/LayoutPanel.stories.tsx +143 -0
- package/client/ui/Layout/Positioned.stories.tsx +113 -0
- package/client/ui/Layout/Section.stories.tsx +84 -0
- package/client/ui/Layout/ShowHide.stories.tsx +99 -0
- package/client/ui/Layout/Spacer.stories.tsx +149 -0
- package/client/ui/Layout/Stack.stories.tsx +69 -0
- package/client/ui/Layout/StackItem.stories.tsx +124 -0
- package/client/ui/Layout/VStack.stories.tsx +101 -0
- package/client/ui/Layout/controls.ts +33 -0
- package/client/ui/Layout/demo.tsx +37 -0
- package/client/ui/Menubar/Menubar.stories.tsx +1 -1
- package/client/ui/MobileNav.stories.tsx +1 -1
- package/client/ui/Navigation.stories.tsx +1 -1
- package/client/ui/NavigationMenu/NavigationMenu.stories.tsx +1 -1
- package/client/ui/PageHeader.stories.tsx +13 -13
- package/client/ui/Pagination/Pagination.stories.tsx +178 -98
- package/client/ui/Pagination/PaginationLegacy.stories.tsx +121 -0
- package/client/ui/Panel.stories.tsx +1 -1
- package/client/ui/Popover/Popover.stories.tsx +1 -1
- package/client/ui/Progress/Progress.stories.tsx +1 -1
- package/client/ui/RadioGroup/RadioGroup.stories.tsx +1 -1
- package/client/ui/Resizable/Resizable.stories.tsx +1 -1
- package/client/ui/ScrollArea/ScrollArea.stories.tsx +1 -1
- package/client/ui/SearchField/SearchField.stories.tsx +153 -0
- package/client/ui/{SearchField.stories.tsx → SearchField/SearchFieldLegacy.stories.tsx} +2 -2
- package/client/ui/SectionBadge.stories.tsx +1 -1
- package/client/ui/Select.stories.tsx +1 -1
- package/client/ui/Separator/Separator.stories.tsx +1 -1
- package/client/ui/Sheet/Sheet.stories.tsx +1 -1
- package/client/ui/Sidebar/Sidebar.stories.tsx +57 -8
- package/client/ui/Skeleton/Skeleton.stories.tsx +1 -1
- package/client/ui/Slider/Slider.stories.tsx +1 -1
- package/client/ui/StandardNavigation.stories.tsx +1 -1
- package/client/ui/Stepper/Stepper.stories.tsx +344 -0
- package/client/ui/Switch.stories.tsx +1 -1
- package/client/ui/SwitchV01/SwitchV01.stories.tsx +152 -0
- package/client/ui/SystemBar/system-bar.stories.tsx +179 -0
- package/client/ui/Table/TableV01.stories.tsx +256 -0
- package/client/ui/Tabs.stories.tsx +1 -1
- package/client/ui/TabsV01/TabsV01.stories.tsx +144 -0
- package/client/ui/Tag.stories.tsx +1 -1
- package/client/ui/TagV02/TagStatusV02.stories.tsx +132 -0
- package/client/ui/TagV02/TagV02.stories.tsx +142 -0
- package/client/ui/TextAreaV01/TextAreaV01.stories.tsx +146 -0
- package/client/ui/TextFieldV01/PasswordFieldV01.stories.tsx +161 -0
- package/client/ui/TextFieldV01/TextFieldV01.stories.tsx +160 -0
- package/client/ui/Textarea.stories.tsx +1 -1
- package/client/ui/Timeline/timeline.stories.tsx +404 -0
- package/client/ui/Toast/Toast.stories.tsx +1 -1
- package/client/ui/ToastV01/ToastV01.stories.tsx +201 -0
- package/client/ui/Toaster/Toaster.stories.tsx +1 -1
- package/client/ui/Toggle.stories.tsx +1 -1
- package/client/ui/Tooltip.stories.tsx +287 -114
- package/client/ui/Typography.stories.tsx +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +649 -13
- package/dist/index.es.js +6109 -2360
- package/package.json +1 -1
- package/client/components/ui/badge.tsx +0 -33
- package/client/components/ui/sonner.tsx +0 -26
- package/client/components/ui/toggle-group.tsx +0 -76
- package/client/ui/Alert/Alert.stories.tsx +0 -82
- package/client/ui/AlertDialog/AlertDialog.stories.tsx +0 -106
- package/client/ui/Avatar/Avatar.stories.tsx +0 -94
- package/client/ui/Badge/Badge.stories.tsx +0 -60
- package/client/ui/Breadcrumb/Breadcrumb.stories.tsx +0 -97
- package/client/ui/DropdownMenu/DropdownMenu.stories.tsx +0 -139
- package/client/ui/Sonner/Sonner.stories.tsx +0 -48
- package/client/ui/Table/Table.stories.tsx +0 -135
- 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
|
+
};
|