@trycompai/design-system 1.0.17 → 1.0.19
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trycompai/design-system",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
4
4
|
"description": "Design system for Comp AI - shadcn-style components with Tailwind CSS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -97,7 +97,6 @@
|
|
|
97
97
|
"tailwindcss": "^4.0.0"
|
|
98
98
|
},
|
|
99
99
|
"devDependencies": {
|
|
100
|
-
"@repo/typescript-config": "workspace:*",
|
|
101
100
|
"@tailwindcss/postcss": "^4.1.10",
|
|
102
101
|
"@types/node": "^22.18.0",
|
|
103
102
|
"@types/react": "^19.2.7",
|
|
@@ -136,6 +136,16 @@ function SelectSeparator({ ...props }: Omit<SelectPrimitive.Separator.Props, 'cl
|
|
|
136
136
|
);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
function SelectEmpty({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
|
|
140
|
+
return (
|
|
141
|
+
<div
|
|
142
|
+
data-slot="select-empty"
|
|
143
|
+
className="text-muted-foreground py-6 text-center text-sm"
|
|
144
|
+
{...props}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
139
149
|
function SelectScrollUpButton({
|
|
140
150
|
...props
|
|
141
151
|
}: Omit<React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>, 'className'>) {
|
|
@@ -167,6 +177,7 @@ function SelectScrollDownButton({
|
|
|
167
177
|
export {
|
|
168
178
|
Select,
|
|
169
179
|
SelectContent,
|
|
180
|
+
SelectEmpty,
|
|
170
181
|
SelectGroup,
|
|
171
182
|
SelectItem,
|
|
172
183
|
SelectLabel,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { Command as CommandPrimitive } from 'cmdk';
|
|
4
|
+
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog';
|
|
3
5
|
import { Combobox as ComboboxPrimitive } from '@base-ui/react';
|
|
4
6
|
import { Checkmark, ChevronDown, Search } from '@carbon/icons-react';
|
|
5
7
|
import * as React from 'react';
|
|
@@ -33,39 +35,70 @@ export interface OrganizationSelectorProps {
|
|
|
33
35
|
/** Search input placeholder */
|
|
34
36
|
searchPlaceholder?: string;
|
|
35
37
|
/** Text shown when no results match search */
|
|
36
|
-
|
|
38
|
+
emptySearchText?: string;
|
|
39
|
+
/** Text shown when organizations list is empty */
|
|
40
|
+
emptyListText?: string;
|
|
37
41
|
/** Whether the selector is disabled */
|
|
38
42
|
disabled?: boolean;
|
|
43
|
+
/** Whether organizations are being loaded */
|
|
44
|
+
loading?: boolean;
|
|
45
|
+
/** Text shown while loading */
|
|
46
|
+
loadingText?: string;
|
|
47
|
+
/** Error message to display */
|
|
48
|
+
error?: string;
|
|
39
49
|
/** Size variant for the trigger */
|
|
40
50
|
size?: 'sm' | 'default';
|
|
51
|
+
/** Max width for the organization name in trigger (e.g., '150px') */
|
|
52
|
+
maxWidth?: string;
|
|
53
|
+
/** Keyboard shortcut to open (e.g., 'o' for Cmd+O). Set to null to disable. */
|
|
54
|
+
hotkey?: string | null;
|
|
55
|
+
/** Whether to open as a full-screen modal (like Cmd+K) instead of dropdown */
|
|
56
|
+
modal?: boolean;
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
// ============================================================================
|
|
44
60
|
// Internal Components
|
|
45
61
|
// ============================================================================
|
|
46
62
|
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
function LoadingSpinner() {
|
|
64
|
+
return (
|
|
65
|
+
<svg
|
|
66
|
+
className="size-4 animate-spin text-muted-foreground"
|
|
67
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
68
|
+
fill="none"
|
|
69
|
+
viewBox="0 0 24 24"
|
|
70
|
+
>
|
|
71
|
+
<circle
|
|
72
|
+
className="opacity-25"
|
|
73
|
+
cx="12"
|
|
74
|
+
cy="12"
|
|
75
|
+
r="10"
|
|
76
|
+
stroke="currentColor"
|
|
77
|
+
strokeWidth="4"
|
|
78
|
+
/>
|
|
79
|
+
<path
|
|
80
|
+
className="opacity-75"
|
|
81
|
+
fill="currentColor"
|
|
82
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
83
|
+
/>
|
|
84
|
+
</svg>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
56
87
|
|
|
57
|
-
|
|
88
|
+
function EmptyState({ icon, text }: { icon?: React.ReactNode; text: string }) {
|
|
89
|
+
return (
|
|
90
|
+
<div className="flex flex-col items-center justify-center gap-2 py-6 text-center">
|
|
91
|
+
{icon && <span className="text-muted-foreground">{icon}</span>}
|
|
92
|
+
<span className="text-muted-foreground text-sm">{text}</span>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
58
95
|
}
|
|
59
96
|
|
|
60
|
-
function OrganizationItemContent({ org }: { org: Organization }) {
|
|
97
|
+
function OrganizationItemContent({ org, maxWidth }: { org: Organization; maxWidth?: string }) {
|
|
61
98
|
return (
|
|
62
99
|
<>
|
|
63
|
-
{org.icon
|
|
64
|
-
|
|
65
|
-
) : (
|
|
66
|
-
<OrganizationColorDot color={org.color} />
|
|
67
|
-
)}
|
|
68
|
-
<span className="truncate">{org.name}</span>
|
|
100
|
+
{org.icon && <span className="shrink-0">{org.icon}</span>}
|
|
101
|
+
<span className="truncate" style={maxWidth ? { maxWidth } : undefined}>{org.name}</span>
|
|
69
102
|
</>
|
|
70
103
|
);
|
|
71
104
|
}
|
|
@@ -81,16 +114,44 @@ function OrganizationSelector({
|
|
|
81
114
|
onValueChange,
|
|
82
115
|
placeholder = 'Select organization',
|
|
83
116
|
searchPlaceholder = 'Search by name or ID...',
|
|
84
|
-
|
|
117
|
+
emptySearchText = 'No organizations found',
|
|
118
|
+
emptyListText = 'No organizations available',
|
|
85
119
|
disabled = false,
|
|
120
|
+
loading = false,
|
|
121
|
+
loadingText = 'Loading organizations...',
|
|
122
|
+
error,
|
|
86
123
|
size = 'default',
|
|
124
|
+
maxWidth = '150px',
|
|
125
|
+
hotkey = 'o',
|
|
126
|
+
modal = false,
|
|
87
127
|
}: OrganizationSelectorProps) {
|
|
88
128
|
const [internalValue, setInternalValue] = React.useState(defaultValue ?? '');
|
|
89
129
|
const [searchQuery, setSearchQuery] = React.useState('');
|
|
130
|
+
const [open, setOpen] = React.useState(false);
|
|
131
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
|
90
132
|
|
|
91
133
|
const selectedValue = value ?? internalValue;
|
|
92
134
|
const selectedOrg = organizations.find((org) => org.id === selectedValue);
|
|
93
135
|
|
|
136
|
+
// Keyboard shortcut to open (Cmd/Ctrl + hotkey)
|
|
137
|
+
React.useEffect(() => {
|
|
138
|
+
if (!hotkey || disabled || loading) return;
|
|
139
|
+
|
|
140
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
141
|
+
if (event.key.toLowerCase() === hotkey.toLowerCase() && (event.metaKey || event.ctrlKey)) {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
if (modal) {
|
|
144
|
+
setOpen(true);
|
|
145
|
+
} else {
|
|
146
|
+
triggerRef.current?.click();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
152
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
153
|
+
}, [hotkey, disabled, loading, modal]);
|
|
154
|
+
|
|
94
155
|
// Filter organizations by ID or name (case-insensitive)
|
|
95
156
|
const filteredOrganizations = React.useMemo(() => {
|
|
96
157
|
if (!searchQuery.trim()) return organizations;
|
|
@@ -110,28 +171,154 @@ function OrganizationSelector({
|
|
|
110
171
|
}
|
|
111
172
|
onValueChange?.(orgId);
|
|
112
173
|
setSearchQuery(''); // Clear search on selection
|
|
174
|
+
setOpen(false); // Close modal on selection
|
|
113
175
|
},
|
|
114
176
|
[value, onValueChange]
|
|
115
177
|
);
|
|
116
178
|
|
|
117
179
|
const triggerSizeClass = size === 'sm' ? 'h-7' : 'h-8';
|
|
118
180
|
|
|
181
|
+
// Determine content state
|
|
182
|
+
const hasError = Boolean(error);
|
|
183
|
+
const isEmpty = organizations.length === 0;
|
|
184
|
+
const hasNoResults = filteredOrganizations.length === 0 && !isEmpty;
|
|
185
|
+
|
|
186
|
+
const triggerButton = (
|
|
187
|
+
<button
|
|
188
|
+
ref={triggerRef}
|
|
189
|
+
type="button"
|
|
190
|
+
data-slot="organization-selector-trigger"
|
|
191
|
+
data-size={size}
|
|
192
|
+
disabled={disabled || loading}
|
|
193
|
+
onClick={modal ? () => setOpen(true) : undefined}
|
|
194
|
+
className={`gap-1.5 rounded-lg border border-transparent bg-transparent py-2 pr-2.5 pl-2.5 text-sm transition-colors select-none hover:bg-accent [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 ${triggerSizeClass}`}
|
|
195
|
+
>
|
|
196
|
+
{loading ? (
|
|
197
|
+
<>
|
|
198
|
+
<LoadingSpinner />
|
|
199
|
+
<span className="text-muted-foreground">Loading...</span>
|
|
200
|
+
</>
|
|
201
|
+
) : selectedOrg ? (
|
|
202
|
+
<OrganizationItemContent org={selectedOrg} maxWidth={maxWidth} />
|
|
203
|
+
) : (
|
|
204
|
+
<span className="text-muted-foreground">{placeholder}</span>
|
|
205
|
+
)}
|
|
206
|
+
{!loading && <ChevronDown className="text-muted-foreground size-3.5 shrink-0" />}
|
|
207
|
+
</button>
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// Modal mode: full-screen dialog like Cmd+K
|
|
211
|
+
if (modal) {
|
|
212
|
+
return (
|
|
213
|
+
<>
|
|
214
|
+
{triggerButton}
|
|
215
|
+
<DialogPrimitive.Root open={open} onOpenChange={setOpen}>
|
|
216
|
+
<DialogPrimitive.Portal>
|
|
217
|
+
<DialogPrimitive.Backdrop
|
|
218
|
+
data-slot="dialog-overlay"
|
|
219
|
+
className="data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50"
|
|
220
|
+
/>
|
|
221
|
+
<DialogPrimitive.Popup
|
|
222
|
+
data-slot="command-dialog-content"
|
|
223
|
+
className="data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 rounded-xl p-0 ring-1 duration-100 sm:max-w-lg group/dialog-content fixed top-1/2 left-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 overflow-hidden outline-none"
|
|
224
|
+
>
|
|
225
|
+
<div className="sr-only">
|
|
226
|
+
<DialogPrimitive.Title>Select Organization</DialogPrimitive.Title>
|
|
227
|
+
<DialogPrimitive.Description>
|
|
228
|
+
Search and select an organization from the list
|
|
229
|
+
</DialogPrimitive.Description>
|
|
230
|
+
</div>
|
|
231
|
+
<CommandPrimitive
|
|
232
|
+
data-slot="command"
|
|
233
|
+
className="bg-popover text-popover-foreground rounded-xl p-1 flex size-full flex-col overflow-hidden"
|
|
234
|
+
filter={(value, search) => {
|
|
235
|
+
const org = organizations.find((o) => o.id === value);
|
|
236
|
+
if (!org) return 0;
|
|
237
|
+
const query = search.toLowerCase();
|
|
238
|
+
if (org.id.toLowerCase().includes(query)) return 1;
|
|
239
|
+
if (org.name.toLowerCase().includes(query)) return 1;
|
|
240
|
+
return 0;
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
{/* Search Input */}
|
|
244
|
+
<div data-slot="command-input-wrapper" className="p-1 pb-0">
|
|
245
|
+
<div
|
|
246
|
+
data-slot="input-group"
|
|
247
|
+
role="group"
|
|
248
|
+
className="border-input/30 bg-input/30 h-8 rounded-lg border shadow-none transition-[color,box-shadow] has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] group/input-group relative flex w-full min-w-0 items-center outline-none"
|
|
249
|
+
>
|
|
250
|
+
<CommandPrimitive.Input
|
|
251
|
+
data-slot="command-input"
|
|
252
|
+
placeholder={searchPlaceholder}
|
|
253
|
+
className="w-full flex-1 bg-transparent px-2.5 py-1 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
|
|
254
|
+
/>
|
|
255
|
+
<div
|
|
256
|
+
data-slot="input-group-addon"
|
|
257
|
+
data-align="inline-end"
|
|
258
|
+
className="text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 pr-2 text-sm font-medium select-none"
|
|
259
|
+
>
|
|
260
|
+
<Search className="size-4 shrink-0 opacity-50" />
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{/* Organization List */}
|
|
266
|
+
<CommandPrimitive.List
|
|
267
|
+
data-slot="command-list"
|
|
268
|
+
className="no-scrollbar max-h-72 scroll-py-1 outline-none overflow-x-hidden overflow-y-auto"
|
|
269
|
+
>
|
|
270
|
+
{loading ? (
|
|
271
|
+
<EmptyState icon={<LoadingSpinner />} text={loadingText} />
|
|
272
|
+
) : hasError ? (
|
|
273
|
+
<EmptyState text={error!} />
|
|
274
|
+
) : isEmpty ? (
|
|
275
|
+
<EmptyState text={emptyListText} />
|
|
276
|
+
) : (
|
|
277
|
+
<CommandPrimitive.Group data-slot="command-group" className="text-foreground overflow-hidden p-1">
|
|
278
|
+
<CommandPrimitive.Empty data-slot="command-empty" className="py-6 text-center text-sm">
|
|
279
|
+
{emptySearchText}
|
|
280
|
+
</CommandPrimitive.Empty>
|
|
281
|
+
{organizations.map((org) => (
|
|
282
|
+
<CommandPrimitive.Item
|
|
283
|
+
key={org.id}
|
|
284
|
+
data-slot="command-item"
|
|
285
|
+
value={org.id}
|
|
286
|
+
onSelect={() => handleValueChange(org.id)}
|
|
287
|
+
className="data-[selected=true]:bg-muted data-[selected=true]:text-foreground relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
|
288
|
+
>
|
|
289
|
+
<OrganizationItemContent org={org} />
|
|
290
|
+
{selectedValue === org.id && (
|
|
291
|
+
<Checkmark className="ml-auto size-4" />
|
|
292
|
+
)}
|
|
293
|
+
</CommandPrimitive.Item>
|
|
294
|
+
))}
|
|
295
|
+
</CommandPrimitive.Group>
|
|
296
|
+
)}
|
|
297
|
+
</CommandPrimitive.List>
|
|
298
|
+
</CommandPrimitive>
|
|
299
|
+
</DialogPrimitive.Popup>
|
|
300
|
+
</DialogPrimitive.Portal>
|
|
301
|
+
</DialogPrimitive.Root>
|
|
302
|
+
</>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Dropdown mode (default): inline combobox
|
|
119
307
|
return (
|
|
120
308
|
<ComboboxPrimitive.Root value={selectedValue} onValueChange={handleValueChange}>
|
|
121
309
|
<ComboboxPrimitive.Trigger
|
|
310
|
+
ref={triggerRef}
|
|
122
311
|
data-slot="organization-selector-trigger"
|
|
123
312
|
data-size={size}
|
|
124
313
|
disabled={disabled}
|
|
125
|
-
className={`
|
|
314
|
+
className={`gap-1.5 rounded-lg border border-transparent bg-transparent py-2 pr-2.5 pl-2.5 text-sm transition-colors select-none hover:bg-accent [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 ${triggerSizeClass}`}
|
|
126
315
|
>
|
|
127
|
-
|
|
128
|
-
{selectedOrg
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
</span>
|
|
134
|
-
<ChevronDown className="text-muted-foreground size-4 shrink-0" />
|
|
316
|
+
{selectedOrg ? (
|
|
317
|
+
<OrganizationItemContent org={selectedOrg} maxWidth={maxWidth} />
|
|
318
|
+
) : (
|
|
319
|
+
<span className="text-muted-foreground">{placeholder}</span>
|
|
320
|
+
)}
|
|
321
|
+
<ChevronDown className="text-muted-foreground size-3.5 shrink-0" />
|
|
135
322
|
</ComboboxPrimitive.Trigger>
|
|
136
323
|
|
|
137
324
|
<ComboboxPrimitive.Portal>
|
|
@@ -164,10 +351,14 @@ function OrganizationSelector({
|
|
|
164
351
|
data-slot="organization-selector-list"
|
|
165
352
|
className="no-scrollbar max-h-64 scroll-py-1 overflow-y-auto p-1 overscroll-contain"
|
|
166
353
|
>
|
|
167
|
-
{
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
354
|
+
{loading ? (
|
|
355
|
+
<EmptyState icon={<LoadingSpinner />} text={loadingText} />
|
|
356
|
+
) : hasError ? (
|
|
357
|
+
<EmptyState text={error!} />
|
|
358
|
+
) : isEmpty ? (
|
|
359
|
+
<EmptyState text={emptyListText} />
|
|
360
|
+
) : hasNoResults ? (
|
|
361
|
+
<EmptyState text={emptySearchText} />
|
|
171
362
|
) : (
|
|
172
363
|
filteredOrganizations.map((org) => (
|
|
173
364
|
<ComboboxPrimitive.Item
|
|
@@ -3,6 +3,7 @@ import * as React from 'react';
|
|
|
3
3
|
|
|
4
4
|
import { cn } from '../../../lib/utils';
|
|
5
5
|
import { Stack } from '../atoms/stack';
|
|
6
|
+
import { Skeleton } from '../atoms/skeleton';
|
|
6
7
|
|
|
7
8
|
const pageLayoutVariants = cva('min-h-full bg-background text-foreground', {
|
|
8
9
|
variants: {
|
|
@@ -48,6 +49,29 @@ interface PageLayoutProps
|
|
|
48
49
|
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
49
50
|
/** Vertical gap between children. Defaults to 'lg' (gap-6). */
|
|
50
51
|
gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '0' | '1' | '2' | '3' | '4' | '6' | '8';
|
|
52
|
+
/** Whether the page is loading. Shows skeleton placeholder when true. */
|
|
53
|
+
loading?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function PageLayoutSkeleton() {
|
|
57
|
+
return (
|
|
58
|
+
<Stack gap="lg">
|
|
59
|
+
{/* Header skeleton */}
|
|
60
|
+
<div className="space-y-2">
|
|
61
|
+
<Skeleton style={{ width: '30%', height: 24 }} />
|
|
62
|
+
<Skeleton style={{ width: '50%', height: 16 }} />
|
|
63
|
+
</div>
|
|
64
|
+
{/* Content skeleton */}
|
|
65
|
+
<div className="space-y-4">
|
|
66
|
+
<Skeleton style={{ width: '100%', height: 200 }} />
|
|
67
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
68
|
+
<Skeleton style={{ width: '100%', height: 120 }} />
|
|
69
|
+
<Skeleton style={{ width: '100%', height: 120 }} />
|
|
70
|
+
<Skeleton style={{ width: '100%', height: 120 }} />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</Stack>
|
|
74
|
+
);
|
|
51
75
|
}
|
|
52
76
|
|
|
53
77
|
function PageLayout({
|
|
@@ -56,13 +80,16 @@ function PageLayout({
|
|
|
56
80
|
container = true,
|
|
57
81
|
maxWidth,
|
|
58
82
|
gap = 'lg',
|
|
83
|
+
loading = false,
|
|
59
84
|
children,
|
|
60
85
|
...props
|
|
61
86
|
}: PageLayoutProps) {
|
|
62
87
|
// For center variant, default to smaller max-width (sm) for auth-style pages
|
|
63
88
|
const resolvedMaxWidth = maxWidth ?? (variant === 'center' ? 'sm' : 'xl');
|
|
64
89
|
|
|
65
|
-
const content = (
|
|
90
|
+
const content = loading ? (
|
|
91
|
+
<PageLayoutSkeleton />
|
|
92
|
+
) : (
|
|
66
93
|
<Stack gap={gap}>
|
|
67
94
|
{children}
|
|
68
95
|
</Stack>
|
|
@@ -72,6 +99,7 @@ function PageLayout({
|
|
|
72
99
|
<div
|
|
73
100
|
data-slot="page-layout"
|
|
74
101
|
data-variant={variant}
|
|
102
|
+
data-loading={loading || undefined}
|
|
75
103
|
className={pageLayoutVariants({ variant, padding })}
|
|
76
104
|
{...props}
|
|
77
105
|
>
|