@object-ui/layout 3.3.0 → 3.3.2
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/CHANGELOG.md +19 -0
- package/README.md +21 -1
- package/dist/index.js +731 -716
- package/dist/index.umd.cjs +3 -3
- package/dist/packages/layout/src/NavigationRenderer.d.ts +2 -1
- package/package.json +42 -7
- package/.turbo/turbo-build.log +0 -38
- package/src/AppSchemaRenderer.tsx +0 -480
- package/src/AppShell.tsx +0 -149
- package/src/NavigationRenderer.tsx +0 -746
- package/src/Page.tsx +0 -39
- package/src/PageCard.tsx +0 -12
- package/src/PageHeader.tsx +0 -35
- package/src/ResponsiveGrid.tsx +0 -118
- package/src/SidebarNav.tsx +0 -164
- package/src/__tests__/AppSchemaRenderer.test.tsx +0 -408
- package/src/__tests__/NavigationRenderer.test.tsx +0 -562
- package/src/index.ts +0 -96
- package/src/stories/AppShell.stories.tsx +0 -110
- package/src/stories/ResponsiveGrid.stories.tsx +0 -110
- package/src/stories/SidebarNav.stories.tsx +0 -223
- package/tsconfig.json +0 -9
- package/vite.config.ts +0 -39
package/src/Page.tsx
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// packages/layout/src/Page.tsx
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { SchemaRenderer } from '@object-ui/react';
|
|
4
|
-
import { PageSchema, SchemaNode } from '@object-ui/types';
|
|
5
|
-
import { PageHeader } from './PageHeader';
|
|
6
|
-
import { cn } from '@object-ui/components';
|
|
7
|
-
|
|
8
|
-
// Helper to ensure children is always an array
|
|
9
|
-
const getChildren = (children?: SchemaNode[] | SchemaNode): SchemaNode[] => {
|
|
10
|
-
if (!children) return [];
|
|
11
|
-
if (Array.isArray(children)) return children;
|
|
12
|
-
return [children];
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export function Page({ schema, className, style, id, ...props }: { schema: PageSchema; className?: string; style?: React.CSSProperties; id?: string } & any) {
|
|
16
|
-
const children = getChildren(schema.children);
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<div
|
|
20
|
-
id={id || schema.id}
|
|
21
|
-
className={cn("flex flex-col h-full space-y-4", className)}
|
|
22
|
-
style={style}
|
|
23
|
-
>
|
|
24
|
-
<PageHeader
|
|
25
|
-
title={schema.title}
|
|
26
|
-
description={schema.description}
|
|
27
|
-
/>
|
|
28
|
-
<div className="flex-1 overflow-auto">
|
|
29
|
-
{children.map((child: any, index: number) => (
|
|
30
|
-
<SchemaRenderer
|
|
31
|
-
key={child?.id || index}
|
|
32
|
-
schema={child}
|
|
33
|
-
{...props}
|
|
34
|
-
/>
|
|
35
|
-
))}
|
|
36
|
-
</div>
|
|
37
|
-
</div>
|
|
38
|
-
);
|
|
39
|
-
}
|
package/src/PageCard.tsx
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { cn } from '@object-ui/components';
|
|
3
|
-
|
|
4
|
-
export function PageCard({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
5
|
-
return (
|
|
6
|
-
<div className={cn("rounded-xl border bg-card text-card-foreground shadow", className)} {...props}>
|
|
7
|
-
<div className="p-6">
|
|
8
|
-
{children}
|
|
9
|
-
</div>
|
|
10
|
-
</div>
|
|
11
|
-
);
|
|
12
|
-
}
|
package/src/PageHeader.tsx
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { cn } from '@object-ui/components';
|
|
3
|
-
|
|
4
|
-
export interface PageHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
-
title: string;
|
|
6
|
-
description?: string;
|
|
7
|
-
action?: React.ReactNode;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function PageHeader({
|
|
11
|
-
title,
|
|
12
|
-
description,
|
|
13
|
-
action,
|
|
14
|
-
className,
|
|
15
|
-
children,
|
|
16
|
-
...props
|
|
17
|
-
}: PageHeaderProps) {
|
|
18
|
-
return (
|
|
19
|
-
<div className={cn("flex flex-col gap-4 pb-4 md:pb-8", className)} {...props}>
|
|
20
|
-
<div className="flex items-center justify-between gap-4">
|
|
21
|
-
<div className="flex flex-col gap-1">
|
|
22
|
-
<h1 className="text-2xl font-bold tracking-tight md:text-3xl">{title}</h1>
|
|
23
|
-
{description && <p className="text-sm text-muted-foreground">{description}</p>}
|
|
24
|
-
</div>
|
|
25
|
-
{/* Render children (actions) in the top-right slot if available */}
|
|
26
|
-
{(action || children) && (
|
|
27
|
-
<div className="flex items-center gap-2">
|
|
28
|
-
{action}
|
|
29
|
-
{children}
|
|
30
|
-
</div>
|
|
31
|
-
)}
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
);
|
|
35
|
-
}
|
package/src/ResponsiveGrid.tsx
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import React from 'react';
|
|
10
|
-
import { cn } from '@object-ui/components';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Spec-aligned breakpoint column map (mirrors @objectstack/spec BreakpointColumnMapSchema).
|
|
14
|
-
* Maps breakpoint names to grid column counts (1-12).
|
|
15
|
-
*/
|
|
16
|
-
export interface BreakpointColumnMap {
|
|
17
|
-
xs?: number;
|
|
18
|
-
sm?: number;
|
|
19
|
-
md?: number;
|
|
20
|
-
lg?: number;
|
|
21
|
-
xl?: number;
|
|
22
|
-
'2xl'?: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Spec-aligned breakpoint order map (mirrors @objectstack/spec BreakpointOrderMapSchema).
|
|
27
|
-
* Maps breakpoint names to display order numbers.
|
|
28
|
-
*/
|
|
29
|
-
export interface BreakpointOrderMap {
|
|
30
|
-
xs?: number;
|
|
31
|
-
sm?: number;
|
|
32
|
-
md?: number;
|
|
33
|
-
lg?: number;
|
|
34
|
-
xl?: number;
|
|
35
|
-
'2xl'?: number;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface ResponsiveGridProps {
|
|
39
|
-
/** Grid column map per breakpoint */
|
|
40
|
-
columns?: BreakpointColumnMap;
|
|
41
|
-
/** Gap between grid items */
|
|
42
|
-
gap?: number | string;
|
|
43
|
-
/** Additional class names */
|
|
44
|
-
className?: string;
|
|
45
|
-
/** Children */
|
|
46
|
-
children: React.ReactNode;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Tailwind class mapping for grid columns at each breakpoint.
|
|
51
|
-
* Uses standard Tailwind grid-cols utilities for CSS-only responsiveness.
|
|
52
|
-
*/
|
|
53
|
-
const COLS_CLASSES: Record<string, Record<number, string>> = {
|
|
54
|
-
xs: { 1: 'grid-cols-1', 2: 'grid-cols-2', 3: 'grid-cols-3', 4: 'grid-cols-4', 6: 'grid-cols-6', 12: 'grid-cols-12' },
|
|
55
|
-
sm: { 1: 'sm:grid-cols-1', 2: 'sm:grid-cols-2', 3: 'sm:grid-cols-3', 4: 'sm:grid-cols-4', 6: 'sm:grid-cols-6', 12: 'sm:grid-cols-12' },
|
|
56
|
-
md: { 1: 'md:grid-cols-1', 2: 'md:grid-cols-2', 3: 'md:grid-cols-3', 4: 'md:grid-cols-4', 6: 'md:grid-cols-6', 12: 'md:grid-cols-12' },
|
|
57
|
-
lg: { 1: 'lg:grid-cols-1', 2: 'lg:grid-cols-2', 3: 'lg:grid-cols-3', 4: 'lg:grid-cols-4', 6: 'lg:grid-cols-6', 12: 'lg:grid-cols-12' },
|
|
58
|
-
xl: { 1: 'xl:grid-cols-1', 2: 'xl:grid-cols-2', 3: 'xl:grid-cols-3', 4: 'xl:grid-cols-4', 6: 'xl:grid-cols-6', 12: 'xl:grid-cols-12' },
|
|
59
|
-
'2xl': { 1: '2xl:grid-cols-1', 2: '2xl:grid-cols-2', 3: '2xl:grid-cols-3', 4: '2xl:grid-cols-4', 6: '2xl:grid-cols-6', 12: '2xl:grid-cols-12' },
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Resolve a BreakpointColumnMap into Tailwind CSS grid classes.
|
|
64
|
-
*/
|
|
65
|
-
function resolveColumnClasses(columns?: BreakpointColumnMap): string {
|
|
66
|
-
if (!columns) return 'grid-cols-1';
|
|
67
|
-
|
|
68
|
-
const classes: string[] = [];
|
|
69
|
-
for (const [bp, cols] of Object.entries(columns)) {
|
|
70
|
-
const bpClasses = COLS_CLASSES[bp];
|
|
71
|
-
if (bpClasses && cols) {
|
|
72
|
-
// Use closest supported column count
|
|
73
|
-
const supported = Object.keys(bpClasses).map(Number);
|
|
74
|
-
const closest = supported.reduce((prev, curr) =>
|
|
75
|
-
Math.abs(curr - cols) < Math.abs(prev - cols) ? curr : prev
|
|
76
|
-
);
|
|
77
|
-
classes.push(bpClasses[closest]);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return classes.join(' ') || 'grid-cols-1';
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const GAP_CLASSES: Record<number, string> = {
|
|
85
|
-
0: 'gap-0', 1: 'gap-1', 2: 'gap-2', 3: 'gap-3', 4: 'gap-4',
|
|
86
|
-
5: 'gap-5', 6: 'gap-6', 8: 'gap-8',
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* ResponsiveGrid — A layout component that consumes @objectstack/spec
|
|
91
|
-
* BreakpointColumnMapSchema for responsive grid layouts.
|
|
92
|
-
*
|
|
93
|
-
* Uses pure Tailwind CSS classes for responsive behavior (no JS resize listeners).
|
|
94
|
-
*
|
|
95
|
-
* @example
|
|
96
|
-
* ```tsx
|
|
97
|
-
* <ResponsiveGrid columns={{ xs: 1, sm: 2, lg: 3 }} gap={4}>
|
|
98
|
-
* <Card>Item 1</Card>
|
|
99
|
-
* <Card>Item 2</Card>
|
|
100
|
-
* <Card>Item 3</Card>
|
|
101
|
-
* </ResponsiveGrid>
|
|
102
|
-
* ```
|
|
103
|
-
*/
|
|
104
|
-
export const ResponsiveGrid: React.FC<ResponsiveGridProps> = ({
|
|
105
|
-
columns,
|
|
106
|
-
gap = 4,
|
|
107
|
-
className,
|
|
108
|
-
children,
|
|
109
|
-
}) => {
|
|
110
|
-
const colClasses = resolveColumnClasses(columns);
|
|
111
|
-
const gapClass = typeof gap === 'number' ? (GAP_CLASSES[gap] || `gap-${gap}`) : '';
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<div className={cn('grid', colClasses, gapClass, className)}>
|
|
115
|
-
{children}
|
|
116
|
-
</div>
|
|
117
|
-
);
|
|
118
|
-
};
|
package/src/SidebarNav.tsx
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { NavLink, useLocation } from 'react-router-dom';
|
|
3
|
-
import {
|
|
4
|
-
Sidebar,
|
|
5
|
-
SidebarContent,
|
|
6
|
-
SidebarGroup,
|
|
7
|
-
SidebarGroupLabel,
|
|
8
|
-
SidebarGroupContent,
|
|
9
|
-
SidebarMenu,
|
|
10
|
-
SidebarMenuItem,
|
|
11
|
-
SidebarMenuButton,
|
|
12
|
-
SidebarMenuSub,
|
|
13
|
-
SidebarMenuSubItem,
|
|
14
|
-
SidebarMenuSubButton,
|
|
15
|
-
Badge,
|
|
16
|
-
Input,
|
|
17
|
-
Collapsible,
|
|
18
|
-
CollapsibleTrigger,
|
|
19
|
-
CollapsibleContent,
|
|
20
|
-
} from '@object-ui/components';
|
|
21
|
-
import { ChevronRight, Search } from 'lucide-react';
|
|
22
|
-
|
|
23
|
-
export interface NavItem {
|
|
24
|
-
title: string;
|
|
25
|
-
href: string;
|
|
26
|
-
icon?: React.ComponentType<{ className?: string }>;
|
|
27
|
-
badge?: string | number;
|
|
28
|
-
badgeVariant?: 'default' | 'destructive' | 'outline';
|
|
29
|
-
children?: NavItem[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface NavGroup {
|
|
33
|
-
label: string;
|
|
34
|
-
items: NavItem[];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface SidebarNavProps {
|
|
38
|
-
items: NavItem[] | NavGroup[];
|
|
39
|
-
title?: string;
|
|
40
|
-
className?: string;
|
|
41
|
-
collapsible?: "offcanvas" | "icon" | "none";
|
|
42
|
-
searchEnabled?: boolean;
|
|
43
|
-
searchPlaceholder?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function isNavGroup(item: NavItem | NavGroup): item is NavGroup {
|
|
47
|
-
return 'items' in item && !('href' in item);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function NavItemRenderer({ item, pathname }: { item: NavItem; pathname: string }) {
|
|
51
|
-
if (item.children && item.children.length > 0) {
|
|
52
|
-
return (
|
|
53
|
-
<Collapsible asChild defaultOpen className="group/collapsible">
|
|
54
|
-
<SidebarMenuItem>
|
|
55
|
-
<CollapsibleTrigger asChild>
|
|
56
|
-
<SidebarMenuButton tooltip={item.title}>
|
|
57
|
-
{item.icon && <item.icon />}
|
|
58
|
-
<span>{item.title}</span>
|
|
59
|
-
{item.badge != null && (
|
|
60
|
-
<Badge variant={item.badgeVariant || 'default'} className="ml-auto mr-1 h-5 min-w-5 px-1 text-xs">
|
|
61
|
-
{item.badge}
|
|
62
|
-
</Badge>
|
|
63
|
-
)}
|
|
64
|
-
<ChevronRight className="ml-auto h-4 w-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
|
65
|
-
</SidebarMenuButton>
|
|
66
|
-
</CollapsibleTrigger>
|
|
67
|
-
<CollapsibleContent>
|
|
68
|
-
<SidebarMenuSub>
|
|
69
|
-
{item.children.map((child) => (
|
|
70
|
-
<SidebarMenuSubItem key={child.href}>
|
|
71
|
-
<SidebarMenuSubButton asChild isActive={pathname === child.href}>
|
|
72
|
-
<NavLink to={child.href}>
|
|
73
|
-
{child.icon && <child.icon />}
|
|
74
|
-
<span>{child.title}</span>
|
|
75
|
-
{child.badge != null && (
|
|
76
|
-
<Badge variant={child.badgeVariant || 'default'} className="ml-auto h-5 min-w-5 px-1 text-xs">
|
|
77
|
-
{child.badge}
|
|
78
|
-
</Badge>
|
|
79
|
-
)}
|
|
80
|
-
</NavLink>
|
|
81
|
-
</SidebarMenuSubButton>
|
|
82
|
-
</SidebarMenuSubItem>
|
|
83
|
-
))}
|
|
84
|
-
</SidebarMenuSub>
|
|
85
|
-
</CollapsibleContent>
|
|
86
|
-
</SidebarMenuItem>
|
|
87
|
-
</Collapsible>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<SidebarMenuItem>
|
|
93
|
-
<SidebarMenuButton asChild isActive={pathname === item.href} tooltip={item.title}>
|
|
94
|
-
<NavLink to={item.href}>
|
|
95
|
-
{item.icon && <item.icon />}
|
|
96
|
-
<span>{item.title}</span>
|
|
97
|
-
{item.badge != null && (
|
|
98
|
-
<Badge variant={item.badgeVariant || 'default'} className="ml-auto h-5 min-w-5 px-1 text-xs">
|
|
99
|
-
{item.badge}
|
|
100
|
-
</Badge>
|
|
101
|
-
)}
|
|
102
|
-
</NavLink>
|
|
103
|
-
</SidebarMenuButton>
|
|
104
|
-
</SidebarMenuItem>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function SidebarNav({ items, title = "Application", className, collapsible = "icon", searchEnabled = false, searchPlaceholder = "Search..." }: SidebarNavProps) {
|
|
109
|
-
const location = useLocation();
|
|
110
|
-
const [search, setSearch] = React.useState('');
|
|
111
|
-
|
|
112
|
-
const flatItems: Array<{ groupLabel?: string; items: NavItem[] }> = React.useMemo(() => {
|
|
113
|
-
if (items.length === 0) return [];
|
|
114
|
-
if (isNavGroup(items[0])) {
|
|
115
|
-
return (items as NavGroup[]).map(g => ({ groupLabel: g.label, items: g.items }));
|
|
116
|
-
}
|
|
117
|
-
return [{ items: items as NavItem[] }];
|
|
118
|
-
}, [items]);
|
|
119
|
-
|
|
120
|
-
const filteredGroups = React.useMemo(() => {
|
|
121
|
-
if (!search) return flatItems;
|
|
122
|
-
const lowerSearch = search.toLowerCase();
|
|
123
|
-
return flatItems.map(group => ({
|
|
124
|
-
...group,
|
|
125
|
-
items: group.items.filter(item =>
|
|
126
|
-
item.title.toLowerCase().includes(lowerSearch) ||
|
|
127
|
-
item.children?.some(child => child.title.toLowerCase().includes(lowerSearch))
|
|
128
|
-
),
|
|
129
|
-
})).filter(group => group.items.length > 0);
|
|
130
|
-
}, [flatItems, search]);
|
|
131
|
-
|
|
132
|
-
return (
|
|
133
|
-
<Sidebar className={className} collapsible={collapsible}>
|
|
134
|
-
<SidebarContent>
|
|
135
|
-
{searchEnabled && (
|
|
136
|
-
<div className="px-3 py-2">
|
|
137
|
-
<div className="relative">
|
|
138
|
-
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
139
|
-
<Input
|
|
140
|
-
type="search"
|
|
141
|
-
placeholder={searchPlaceholder}
|
|
142
|
-
value={search}
|
|
143
|
-
onChange={(e) => setSearch(e.target.value)}
|
|
144
|
-
className="pl-8 h-9"
|
|
145
|
-
/>
|
|
146
|
-
</div>
|
|
147
|
-
</div>
|
|
148
|
-
)}
|
|
149
|
-
{filteredGroups.map((group, gIdx) => (
|
|
150
|
-
<SidebarGroup key={group.groupLabel || gIdx}>
|
|
151
|
-
<SidebarGroupLabel>{group.groupLabel || title}</SidebarGroupLabel>
|
|
152
|
-
<SidebarGroupContent>
|
|
153
|
-
<SidebarMenu>
|
|
154
|
-
{group.items.map((item) => (
|
|
155
|
-
<NavItemRenderer key={item.href} item={item} pathname={location.pathname} />
|
|
156
|
-
))}
|
|
157
|
-
</SidebarMenu>
|
|
158
|
-
</SidebarGroupContent>
|
|
159
|
-
</SidebarGroup>
|
|
160
|
-
))}
|
|
161
|
-
</SidebarContent>
|
|
162
|
-
</Sidebar>
|
|
163
|
-
);
|
|
164
|
-
}
|