@actuate-media/cms-admin 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +8 -5
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/layout/primitives.test.d.ts +2 -0
- package/dist/__tests__/layout/primitives.test.d.ts.map +1 -0
- package/dist/__tests__/layout/primitives.test.js +34 -0
- package/dist/__tests__/layout/primitives.test.js.map +1 -0
- package/dist/__tests__/lib/cv.test.d.ts +2 -0
- package/dist/__tests__/lib/cv.test.d.ts.map +1 -0
- package/dist/__tests__/lib/cv.test.js +66 -0
- package/dist/__tests__/lib/cv.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/assets/actuate-logo.d.ts +36 -0
- package/dist/assets/actuate-logo.d.ts.map +1 -0
- package/dist/assets/actuate-logo.js +15 -0
- package/dist/assets/actuate-logo.js.map +1 -0
- package/dist/components/Breadcrumbs.js +2 -2
- package/dist/components/CommandPalette.js +10 -10
- package/dist/components/ContentOverviewChart.js +3 -3
- package/dist/components/ErrorBoundary.js +1 -1
- package/dist/components/FocalPointPicker.js +2 -2
- package/dist/components/FolderTree.js +20 -20
- package/dist/components/LivePreview.js +3 -3
- package/dist/components/LocaleSwitcher.js +1 -1
- package/dist/components/MediaPickerModal.js +4 -4
- package/dist/components/PresenceIndicator.js +1 -1
- package/dist/components/SEOConfigPanel.d.ts +2 -0
- package/dist/components/SEOConfigPanel.d.ts.map +1 -0
- package/dist/components/SEOConfigPanel.js +174 -0
- package/dist/components/SEOConfigPanel.js.map +1 -0
- package/dist/components/SEOPanel.js +9 -9
- package/dist/components/SEOPerformance.js +2 -2
- package/dist/components/SchedulePublishDialog.d.ts +18 -0
- package/dist/components/SchedulePublishDialog.d.ts.map +1 -0
- package/dist/components/SchedulePublishDialog.js +106 -0
- package/dist/components/SchedulePublishDialog.js.map +1 -0
- package/dist/components/SharePreviewLinkDialog.d.ts +17 -0
- package/dist/components/SharePreviewLinkDialog.d.ts.map +1 -0
- package/dist/components/SharePreviewLinkDialog.js +83 -0
- package/dist/components/SharePreviewLinkDialog.js.map +1 -0
- package/dist/components/TipTapEditor.js +5 -5
- package/dist/components/VersionHistory.js +2 -2
- package/dist/components/ui/Badge.d.ts +33 -3
- package/dist/components/ui/Badge.d.ts.map +1 -1
- package/dist/components/ui/Badge.js +42 -8
- package/dist/components/ui/Badge.js.map +1 -1
- package/dist/components/ui/Button.d.ts +19 -8
- package/dist/components/ui/Button.d.ts.map +1 -1
- package/dist/components/ui/Button.js +35 -14
- package/dist/components/ui/Button.js.map +1 -1
- package/dist/components/ui/Card.d.ts +26 -0
- package/dist/components/ui/Card.d.ts.map +1 -0
- package/dist/components/ui/Card.js +45 -0
- package/dist/components/ui/Card.js.map +1 -0
- package/dist/components/ui/DataTable.js +1 -1
- package/dist/components/ui/Input.d.ts +15 -0
- package/dist/components/ui/Input.d.ts.map +1 -0
- package/dist/components/ui/Input.js +23 -0
- package/dist/components/ui/Input.js.map +1 -0
- package/dist/components/ui/SearchInput.js +1 -1
- package/dist/components/ui/Select.d.ts +16 -0
- package/dist/components/ui/Select.d.ts.map +1 -0
- package/dist/components/ui/Select.js +25 -0
- package/dist/components/ui/Select.js.map +1 -0
- package/dist/components/ui/Toast.js +1 -1
- package/dist/components/ui/index.d.ts +10 -4
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +5 -2
- package/dist/components/ui/index.js.map +1 -1
- package/dist/fields/BlockBuilderField.js +3 -3
- package/dist/fields/DateField.js +1 -1
- package/dist/fields/RelationshipField.js +3 -3
- package/dist/fields/TextField.js +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/Header.js +1 -1
- package/dist/layout/Layout.d.ts +14 -0
- package/dist/layout/Layout.d.ts.map +1 -1
- package/dist/layout/Layout.js +17 -11
- package/dist/layout/Layout.js.map +1 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +21 -11
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/layout/primitives/AdminShell.d.ts +43 -0
- package/dist/layout/primitives/AdminShell.d.ts.map +1 -0
- package/dist/layout/primitives/AdminShell.js +51 -0
- package/dist/layout/primitives/AdminShell.js.map +1 -0
- package/dist/layout/primitives/Box.d.ts +19 -0
- package/dist/layout/primitives/Box.d.ts.map +1 -0
- package/dist/layout/primitives/Box.js +12 -0
- package/dist/layout/primitives/Box.js.map +1 -0
- package/dist/layout/primitives/Cluster.d.ts +27 -0
- package/dist/layout/primitives/Cluster.d.ts.map +1 -0
- package/dist/layout/primitives/Cluster.js +37 -0
- package/dist/layout/primitives/Cluster.js.map +1 -0
- package/dist/layout/primitives/Grid.d.ts +45 -0
- package/dist/layout/primitives/Grid.d.ts.map +1 -0
- package/dist/layout/primitives/Grid.js +59 -0
- package/dist/layout/primitives/Grid.js.map +1 -0
- package/dist/layout/primitives/PageContainer.d.ts +36 -0
- package/dist/layout/primitives/PageContainer.d.ts.map +1 -0
- package/dist/layout/primitives/PageContainer.js +41 -0
- package/dist/layout/primitives/PageContainer.js.map +1 -0
- package/dist/layout/primitives/Split.d.ts +34 -0
- package/dist/layout/primitives/Split.d.ts.map +1 -0
- package/dist/layout/primitives/Split.js +27 -0
- package/dist/layout/primitives/Split.js.map +1 -0
- package/dist/layout/primitives/Stack.d.ts +23 -0
- package/dist/layout/primitives/Stack.d.ts.map +1 -0
- package/dist/layout/primitives/Stack.js +34 -0
- package/dist/layout/primitives/Stack.js.map +1 -0
- package/dist/layout/primitives/index.d.ts +30 -0
- package/dist/layout/primitives/index.d.ts.map +1 -0
- package/dist/layout/primitives/index.js +22 -0
- package/dist/layout/primitives/index.js.map +1 -0
- package/dist/layout/primitives/tokens.d.ts +48 -0
- package/dist/layout/primitives/tokens.d.ts.map +1 -0
- package/dist/layout/primitives/tokens.js +54 -0
- package/dist/layout/primitives/tokens.js.map +1 -0
- package/dist/lib/cv.d.ts +53 -0
- package/dist/lib/cv.d.ts.map +1 -0
- package/dist/lib/cv.js +39 -0
- package/dist/lib/cv.js.map +1 -0
- package/dist/views/ApiKeys.d.ts.map +1 -1
- package/dist/views/ApiKeys.js +13 -11
- package/dist/views/ApiKeys.js.map +1 -1
- package/dist/views/CollectionList.js +8 -8
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +333 -78
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/DocumentEdit.d.ts.map +1 -1
- package/dist/views/DocumentEdit.js +17 -5
- package/dist/views/DocumentEdit.js.map +1 -1
- package/dist/views/ForgotPassword.js +2 -2
- package/dist/views/FormEditor.js +5 -5
- package/dist/views/FormSubmissions.js +6 -6
- package/dist/views/Forms.js +2 -2
- package/dist/views/Login.d.ts +16 -1
- package/dist/views/Login.d.ts.map +1 -1
- package/dist/views/Login.js +17 -7
- package/dist/views/Login.js.map +1 -1
- package/dist/views/MediaBrowser.js +16 -16
- package/dist/views/PageEditor.js +2 -2
- package/dist/views/Pages.js +10 -10
- package/dist/views/PostEditor.js +2 -2
- package/dist/views/Posts.js +4 -4
- package/dist/views/Redirects.js +4 -4
- package/dist/views/ResetPassword.js +2 -2
- package/dist/views/SEO.js +6 -6
- package/dist/views/ScriptTagEditor.js +4 -4
- package/dist/views/ScriptTags.js +2 -2
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +9 -8
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/SetupWizard.js +2 -2
- package/dist/views/Users.js +4 -4
- package/dist/views/page-builder/AIBlockAssist.js +1 -1
- package/dist/views/page-builder/AIGenerateDialog.js +10 -10
- package/dist/views/page-builder/BlockEditor.js +10 -10
- package/dist/views/page-builder/BlockPicker.js +4 -4
- package/dist/views/page-builder/BottomBar.js +1 -1
- package/dist/views/page-builder/BuilderToolbar.js +2 -2
- package/dist/views/page-builder/ContextPanel.js +2 -2
- package/dist/views/page-builder/DesignScore.js +9 -9
- package/dist/views/page-builder/NodeSettings.js +8 -8
- package/dist/views/page-builder/PageBuilder.js +3 -3
- package/dist/views/page-builder/PageSettings.js +1 -1
- package/dist/views/page-builder/PageTemplates.js +2 -2
- package/dist/views/page-builder/SEOPanel.js +13 -13
- package/dist/views/page-builder/SavedSections.js +5 -5
- package/dist/views/page-builder/TemplatePicker.js +2 -2
- package/dist/views/page-builder/block-renderers/CTAPreview.js +5 -5
- package/dist/views/page-builder/block-renderers/CardsPreview.js +1 -1
- package/dist/views/page-builder/block-renderers/CodePreview.js +1 -1
- package/dist/views/page-builder/block-renderers/FAQPreview.js +3 -3
- package/dist/views/page-builder/block-renderers/FallbackPreview.js +1 -1
- package/dist/views/page-builder/block-renderers/FormPreview.js +3 -3
- package/dist/views/page-builder/block-renderers/GalleryPreview.js +5 -5
- package/dist/views/page-builder/block-renderers/HeroPreview.js +3 -3
- package/dist/views/page-builder/block-renderers/ImagePreview.js +3 -3
- package/dist/views/page-builder/block-renderers/TextPreview.js +3 -3
- package/dist/views/page-builder/block-renderers/VideoPreview.js +4 -4
- package/dist/views/page-builder/canvas/BlockRenderer.js +1 -1
- package/dist/views/page-builder/canvas/BuilderCanvas.js +3 -3
- package/dist/views/page-builder/canvas/ColumnRenderer.js +2 -2
- package/dist/views/page-builder/canvas/ContainerRenderer.js +2 -2
- package/dist/views/page-builder/canvas/RowRenderer.js +2 -2
- package/dist/views/page-builder/canvas/SectionRenderer.js +2 -2
- package/package.json +6 -2
- package/src/AdminRoot.tsx +21 -11
- package/src/__tests__/layout/primitives.test.ts +37 -0
- package/src/__tests__/lib/cv.test.ts +74 -0
- package/src/assets/actuate-logo.tsx +72 -0
- package/src/components/Breadcrumbs.tsx +6 -6
- package/src/components/CommandPalette.tsx +34 -34
- package/src/components/ContentOverviewChart.tsx +3 -3
- package/src/components/ErrorBoundary.tsx +3 -3
- package/src/components/FocalPointPicker.tsx +4 -4
- package/src/components/FolderTree.tsx +38 -38
- package/src/components/LivePreview.tsx +16 -16
- package/src/components/LocaleSwitcher.tsx +7 -7
- package/src/components/MediaPickerModal.tsx +21 -21
- package/src/components/PresenceIndicator.tsx +2 -2
- package/src/components/SEOConfigPanel.tsx +582 -0
- package/src/components/SEOPanel.tsx +46 -46
- package/src/components/SEOPerformance.tsx +21 -21
- package/src/components/SchedulePublishDialog.tsx +241 -0
- package/src/components/SharePreviewLinkDialog.tsx +227 -0
- package/src/components/TipTapEditor.tsx +33 -33
- package/src/components/VersionHistory.tsx +16 -16
- package/src/components/ui/Badge.tsx +66 -14
- package/src/components/ui/Button.tsx +70 -33
- package/src/components/ui/Card.tsx +101 -0
- package/src/components/ui/DataTable.tsx +1 -1
- package/src/components/ui/Input.tsx +35 -0
- package/src/components/ui/SearchInput.tsx +4 -4
- package/src/components/ui/Select.tsx +56 -0
- package/src/components/ui/Toast.tsx +1 -1
- package/src/components/ui/index.ts +18 -4
- package/src/fields/BlockBuilderField.tsx +3 -3
- package/src/fields/DateField.tsx +1 -1
- package/src/fields/RelationshipField.tsx +10 -10
- package/src/fields/TextField.tsx +1 -1
- package/src/index.ts +32 -0
- package/src/layout/Header.tsx +28 -28
- package/src/layout/Layout.tsx +39 -46
- package/src/layout/Sidebar.tsx +37 -64
- package/src/layout/primitives/AdminShell.tsx +118 -0
- package/src/layout/primitives/Box.tsx +30 -0
- package/src/layout/primitives/Cluster.tsx +74 -0
- package/src/layout/primitives/Grid.tsx +120 -0
- package/src/layout/primitives/PageContainer.tsx +96 -0
- package/src/layout/primitives/Split.tsx +73 -0
- package/src/layout/primitives/Stack.tsx +67 -0
- package/src/layout/primitives/index.ts +36 -0
- package/src/layout/primitives/tokens.ts +76 -0
- package/src/lib/cv.ts +96 -0
- package/src/styles/build-input.css +1 -1
- package/src/views/ApiKeys.tsx +57 -57
- package/src/views/CollectionList.tsx +30 -30
- package/src/views/Dashboard.tsx +737 -186
- package/src/views/DocumentEdit.tsx +90 -10
- package/src/views/ForgotPassword.tsx +18 -18
- package/src/views/FormEditor.tsx +75 -75
- package/src/views/FormSubmissions.tsx +76 -76
- package/src/views/Forms.tsx +27 -27
- package/src/views/Login.tsx +65 -25
- package/src/views/MediaBrowser.tsx +127 -127
- package/src/views/PageEditor.tsx +25 -25
- package/src/views/Pages.tsx +59 -59
- package/src/views/PostEditor.tsx +37 -37
- package/src/views/Posts.tsx +48 -48
- package/src/views/Redirects.tsx +21 -21
- package/src/views/ResetPassword.tsx +28 -28
- package/src/views/SEO.tsx +144 -144
- package/src/views/ScriptTagEditor.tsx +24 -24
- package/src/views/ScriptTags.tsx +10 -10
- package/src/views/Settings.tsx +88 -80
- package/src/views/SetupWizard.tsx +28 -28
- package/src/views/Users.tsx +20 -20
- package/src/views/page-builder/AIBlockAssist.tsx +1 -1
- package/src/views/page-builder/AIGenerateDialog.tsx +63 -63
- package/src/views/page-builder/BlockEditor.tsx +26 -26
- package/src/views/page-builder/BlockPicker.tsx +22 -22
- package/src/views/page-builder/BottomBar.tsx +8 -8
- package/src/views/page-builder/BuilderToolbar.tsx +17 -17
- package/src/views/page-builder/ContextPanel.tsx +3 -3
- package/src/views/page-builder/DesignScore.tsx +21 -21
- package/src/views/page-builder/NodeSettings.tsx +27 -27
- package/src/views/page-builder/PageBuilder.tsx +11 -11
- package/src/views/page-builder/PageSettings.tsx +4 -4
- package/src/views/page-builder/PageTemplates.tsx +18 -18
- package/src/views/page-builder/SEOPanel.tsx +53 -53
- package/src/views/page-builder/SavedSections.tsx +37 -37
- package/src/views/page-builder/TemplatePicker.tsx +17 -17
- package/src/views/page-builder/block-renderers/CTAPreview.tsx +13 -13
- package/src/views/page-builder/block-renderers/CardsPreview.tsx +5 -5
- package/src/views/page-builder/block-renderers/CodePreview.tsx +6 -6
- package/src/views/page-builder/block-renderers/FAQPreview.tsx +13 -13
- package/src/views/page-builder/block-renderers/FallbackPreview.tsx +3 -3
- package/src/views/page-builder/block-renderers/FormPreview.tsx +20 -20
- package/src/views/page-builder/block-renderers/GalleryPreview.tsx +8 -8
- package/src/views/page-builder/block-renderers/HeroPreview.tsx +16 -16
- package/src/views/page-builder/block-renderers/ImagePreview.tsx +4 -4
- package/src/views/page-builder/block-renderers/TextPreview.tsx +14 -14
- package/src/views/page-builder/block-renderers/VideoPreview.tsx +12 -12
- package/src/views/page-builder/canvas/BlockRenderer.tsx +4 -4
- package/src/views/page-builder/canvas/BuilderCanvas.tsx +6 -6
- package/src/views/page-builder/canvas/ColumnRenderer.tsx +3 -3
- package/src/views/page-builder/canvas/ContainerRenderer.tsx +2 -2
- package/src/views/page-builder/canvas/RowRenderer.tsx +2 -2
- package/src/views/page-builder/canvas/SectionRenderer.tsx +2 -2
package/src/layout/Header.tsx
CHANGED
|
@@ -24,100 +24,100 @@ export function Header({ onToggleSidebar, session, onNavigate }: HeaderProps) {
|
|
|
24
24
|
|
|
25
25
|
return (
|
|
26
26
|
<>
|
|
27
|
-
<header className="
|
|
27
|
+
<header className="border-border bg-background flex h-14 items-center justify-between gap-4 border-b px-4">
|
|
28
28
|
<button
|
|
29
29
|
onClick={onToggleSidebar}
|
|
30
|
-
className="
|
|
30
|
+
className="hover:bg-accent rounded-lg p-2 transition-colors md:hidden"
|
|
31
31
|
aria-label="Toggle sidebar"
|
|
32
32
|
>
|
|
33
|
-
<Menu className="
|
|
33
|
+
<Menu className="text-foreground h-5 w-5" strokeWidth={2} />
|
|
34
34
|
</button>
|
|
35
35
|
|
|
36
|
-
<div className="flex items-center
|
|
37
|
-
<span className="text-lg font-semibold
|
|
36
|
+
<div className="flex items-center md:hidden">
|
|
37
|
+
<span className="text-foreground text-lg font-semibold">Actuate</span>
|
|
38
38
|
</div>
|
|
39
39
|
|
|
40
40
|
<div className="flex-1" />
|
|
41
41
|
|
|
42
42
|
<div className="flex items-center gap-2 sm:gap-3">
|
|
43
|
-
<div className="hidden md:block
|
|
44
|
-
<Search className="
|
|
43
|
+
<div className="relative hidden md:block">
|
|
44
|
+
<Search className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
|
45
45
|
<input
|
|
46
46
|
type="text"
|
|
47
47
|
placeholder="Search... (⌘K)"
|
|
48
48
|
value={searchQuery}
|
|
49
49
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
50
50
|
onFocus={() => setShowCommandPalette(true)}
|
|
51
|
-
className="
|
|
51
|
+
className="border-border bg-input-background text-foreground focus:ring-ring w-64 rounded-lg border py-1.5 pr-3 pl-9 text-sm focus:border-transparent focus:ring-2 focus:outline-none"
|
|
52
52
|
/>
|
|
53
53
|
</div>
|
|
54
54
|
|
|
55
55
|
<button
|
|
56
56
|
onClick={() => setShowCommandPalette(true)}
|
|
57
|
-
className="
|
|
57
|
+
className="hover:bg-accent rounded-lg p-2 transition-colors md:hidden"
|
|
58
58
|
aria-label="Search"
|
|
59
59
|
>
|
|
60
|
-
<Search className="
|
|
60
|
+
<Search className="text-muted-foreground h-5 w-5" />
|
|
61
61
|
</button>
|
|
62
62
|
|
|
63
63
|
<LocaleSwitcher />
|
|
64
64
|
|
|
65
65
|
<button
|
|
66
66
|
onClick={toggleTheme}
|
|
67
|
-
className="
|
|
67
|
+
className="hover:bg-accent rounded-lg p-2 transition-colors"
|
|
68
68
|
aria-label="Toggle theme"
|
|
69
69
|
>
|
|
70
70
|
{resolvedTheme === 'dark' ? (
|
|
71
|
-
<Sun className="
|
|
71
|
+
<Sun className="text-muted-foreground h-5 w-5" />
|
|
72
72
|
) : (
|
|
73
|
-
<Moon className="
|
|
73
|
+
<Moon className="text-muted-foreground h-5 w-5" />
|
|
74
74
|
)}
|
|
75
75
|
</button>
|
|
76
76
|
|
|
77
77
|
<button
|
|
78
|
-
className="
|
|
78
|
+
className="hover:bg-accent relative rounded-lg p-2 transition-colors"
|
|
79
79
|
aria-label="Notifications"
|
|
80
80
|
>
|
|
81
|
-
<Bell className="
|
|
82
|
-
<span className="absolute top-1.5 right-1.5
|
|
81
|
+
<Bell className="text-muted-foreground h-5 w-5" />
|
|
82
|
+
<span className="bg-destructive absolute top-1.5 right-1.5 h-2 w-2 rounded-full" />
|
|
83
83
|
</button>
|
|
84
84
|
|
|
85
85
|
<DropdownMenu.Root>
|
|
86
86
|
<DropdownMenu.Trigger asChild>
|
|
87
|
-
<button className="flex items-center gap-2 p-1.5
|
|
88
|
-
<div className="
|
|
89
|
-
<span className="text-
|
|
87
|
+
<button className="hover:bg-accent flex items-center gap-2 rounded-lg p-1.5 transition-colors">
|
|
88
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-linear-to-br from-blue-500 to-purple-600">
|
|
89
|
+
<span className="text-sm font-medium text-white">
|
|
90
90
|
{session?.user?.name?.charAt(0)?.toUpperCase() ?? 'A'}
|
|
91
91
|
</span>
|
|
92
92
|
</div>
|
|
93
|
-
<ChevronDown className="
|
|
93
|
+
<ChevronDown className="text-muted-foreground hidden h-4 w-4 sm:block" />
|
|
94
94
|
</button>
|
|
95
95
|
</DropdownMenu.Trigger>
|
|
96
96
|
|
|
97
97
|
<DropdownMenu.Portal>
|
|
98
98
|
<DropdownMenu.Content
|
|
99
|
-
className="
|
|
99
|
+
className="bg-popover text-popover-foreground border-border z-50 min-w-[200px] rounded-lg border p-1 shadow-lg"
|
|
100
100
|
align="end"
|
|
101
101
|
sideOffset={5}
|
|
102
102
|
>
|
|
103
|
-
<div className="px-3 py-2
|
|
103
|
+
<div className="border-border border-b px-3 py-2">
|
|
104
104
|
<p className="text-sm font-medium">{session?.user?.name ?? 'Admin User'}</p>
|
|
105
|
-
<p className="text-
|
|
105
|
+
<p className="text-muted-foreground text-xs">
|
|
106
106
|
{session?.user?.email ?? 'admin@example.com'}
|
|
107
107
|
</p>
|
|
108
108
|
</div>
|
|
109
|
-
<DropdownMenu.Item className="flex items-center gap-2 px-3 py-2 text-sm
|
|
110
|
-
<User className="
|
|
109
|
+
<DropdownMenu.Item className="hover:bg-accent flex cursor-pointer items-center gap-2 rounded px-3 py-2 text-sm outline-none">
|
|
110
|
+
<User className="h-4 w-4" />
|
|
111
111
|
Profile
|
|
112
112
|
</DropdownMenu.Item>
|
|
113
113
|
<DropdownMenu.Item
|
|
114
|
-
className="flex items-center gap-2 px-3 py-2 text-sm
|
|
114
|
+
className="hover:bg-accent flex cursor-pointer items-center gap-2 rounded px-3 py-2 text-sm outline-none"
|
|
115
115
|
onSelect={() => onNavigate('/settings')}
|
|
116
116
|
>
|
|
117
117
|
Settings
|
|
118
118
|
</DropdownMenu.Item>
|
|
119
|
-
<DropdownMenu.Separator className="
|
|
120
|
-
<DropdownMenu.Item className="flex items-center gap-2 px-3 py-2 text-sm
|
|
119
|
+
<DropdownMenu.Separator className="bg-border my-1 h-px" />
|
|
120
|
+
<DropdownMenu.Item className="text-destructive hover:bg-accent flex cursor-pointer items-center gap-2 rounded px-3 py-2 text-sm outline-none">
|
|
121
121
|
Logout
|
|
122
122
|
</DropdownMenu.Item>
|
|
123
123
|
</DropdownMenu.Content>
|
package/src/layout/Layout.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { useState, useEffect, type ReactNode } from 'react'
|
|
|
4
4
|
import { Sidebar } from './Sidebar.js'
|
|
5
5
|
import { Header } from './Header.js'
|
|
6
6
|
import { Breadcrumbs } from '../components/Breadcrumbs.js'
|
|
7
|
+
import { AdminShell } from './primitives/AdminShell.js'
|
|
7
8
|
import { Toaster } from 'sonner'
|
|
8
9
|
|
|
9
10
|
export interface LayoutProps {
|
|
@@ -14,6 +15,20 @@ export interface LayoutProps {
|
|
|
14
15
|
children: ReactNode
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Layout — thin shell that wires Sidebar/Header/Breadcrumbs into the
|
|
20
|
+
* `<AdminShell>` primitive.
|
|
21
|
+
*
|
|
22
|
+
* `AdminShell` (in `./primitives/AdminShell.tsx`) owns the actual layout
|
|
23
|
+
* algorithm (CSS Grid on desktop, fixed slide-in overlay on mobile,
|
|
24
|
+
* JS-driven `matchMedia` breakpoint) so that every layout decision lives
|
|
25
|
+
* in one place. Per-product chrome (Sidebar, Header, Breadcrumbs)
|
|
26
|
+
* composes on top of it.
|
|
27
|
+
*
|
|
28
|
+
* DO NOT re-implement layout logic in this file — extend `AdminShell`
|
|
29
|
+
* instead. The previous flex + `fixed`↔`static` toggle caused recurring
|
|
30
|
+
* sidebar-overlap bugs; the grid-based primitive is the contract.
|
|
31
|
+
*/
|
|
17
32
|
export function Layout({ config, session, currentPath, onNavigate, children }: LayoutProps) {
|
|
18
33
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
|
19
34
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
|
|
@@ -22,54 +37,32 @@ export function Layout({ config, session, currentPath, onNavigate, children }: L
|
|
|
22
37
|
setMobileSidebarOpen(false)
|
|
23
38
|
}, [currentPath])
|
|
24
39
|
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
const handleResize = () => {
|
|
27
|
-
if (window.innerWidth < 1024) {
|
|
28
|
-
setMobileSidebarOpen(false)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
window.addEventListener('resize', handleResize)
|
|
32
|
-
return () => window.removeEventListener('resize', handleResize)
|
|
33
|
-
}, [])
|
|
34
|
-
|
|
35
40
|
return (
|
|
36
|
-
|
|
41
|
+
<>
|
|
37
42
|
<Toaster position="bottom-right" />
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
43
|
+
<AdminShell
|
|
44
|
+
mobileSidebarOpen={mobileSidebarOpen}
|
|
45
|
+
onMobileSidebarClose={() => setMobileSidebarOpen(false)}
|
|
46
|
+
sidebar={
|
|
47
|
+
<Sidebar
|
|
48
|
+
collapsed={sidebarCollapsed}
|
|
49
|
+
onToggleCollapse={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|
50
|
+
currentPath={currentPath}
|
|
51
|
+
onNavigate={onNavigate}
|
|
52
|
+
config={config}
|
|
53
|
+
/>
|
|
54
|
+
}
|
|
55
|
+
header={
|
|
56
|
+
<Header
|
|
57
|
+
onToggleSidebar={() => setMobileSidebarOpen(!mobileSidebarOpen)}
|
|
58
|
+
session={session}
|
|
59
|
+
onNavigate={onNavigate}
|
|
60
|
+
/>
|
|
61
|
+
}
|
|
62
|
+
breadcrumbs={<Breadcrumbs currentPath={currentPath} onNavigate={onNavigate} />}
|
|
50
63
|
>
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
currentPath={currentPath}
|
|
55
|
-
onNavigate={onNavigate}
|
|
56
|
-
config={config}
|
|
57
|
-
/>
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
<div className="flex-1 flex flex-col overflow-hidden">
|
|
61
|
-
<Header
|
|
62
|
-
onToggleSidebar={() => setMobileSidebarOpen(!mobileSidebarOpen)}
|
|
63
|
-
session={session}
|
|
64
|
-
onNavigate={onNavigate}
|
|
65
|
-
/>
|
|
66
|
-
|
|
67
|
-
<Breadcrumbs currentPath={currentPath} onNavigate={onNavigate} />
|
|
68
|
-
|
|
69
|
-
<main className="flex-1 overflow-auto bg-background">
|
|
70
|
-
<div className="h-full">{children}</div>
|
|
71
|
-
</main>
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
64
|
+
<div className="h-full">{children}</div>
|
|
65
|
+
</AdminShell>
|
|
66
|
+
</>
|
|
74
67
|
)
|
|
75
68
|
}
|
package/src/layout/Sidebar.tsx
CHANGED
|
@@ -25,8 +25,13 @@ import {
|
|
|
25
25
|
KeyRound,
|
|
26
26
|
} from 'lucide-react'
|
|
27
27
|
import type { LucideIcon } from 'lucide-react'
|
|
28
|
+
import { ActuateBrandLogo } from '../assets/actuate-logo.js'
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Compact mark used in the collapsed sidebar — just the "C" symbol from the
|
|
32
|
+
* full lockup, drawn as its own simple SVG so it stays crisp at 32×32.
|
|
33
|
+
*/
|
|
34
|
+
function ActuateMark({ className }: { className?: string }) {
|
|
30
35
|
return (
|
|
31
36
|
<svg
|
|
32
37
|
viewBox="0 0 40 44"
|
|
@@ -35,53 +40,21 @@ function ActuateLogo({ className }: { className?: string }) {
|
|
|
35
40
|
className={className}
|
|
36
41
|
aria-hidden="true"
|
|
37
42
|
>
|
|
38
|
-
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
<rect x="
|
|
42
|
-
<rect x="18" y="20" width="4" height="22" rx="1" fill="#E8646A" />
|
|
43
|
-
<rect x="25" y="20" width="4" height="22" rx="1" fill="#E8646A" />
|
|
43
|
+
<polygon points="20,2 6,18 12,18 20,8 28,18 34,18" fill="#F05E65" />
|
|
44
|
+
<rect x="11" y="20" width="4" height="22" rx="1" fill="#F05E65" />
|
|
45
|
+
<rect x="18" y="20" width="4" height="22" rx="1" fill="#F05E65" />
|
|
46
|
+
<rect x="25" y="20" width="4" height="22" rx="1" fill="#F05E65" />
|
|
44
47
|
</svg>
|
|
45
48
|
)
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Full Actuate Media lockup. Inline SVG with a transparent background so it
|
|
53
|
+
* sits naturally on whatever surface the sidebar uses (light or dark theme,
|
|
54
|
+
* custom branding background, etc.).
|
|
55
|
+
*/
|
|
48
56
|
function ActuateWordmark({ className }: { className?: string }) {
|
|
49
|
-
return
|
|
50
|
-
<svg
|
|
51
|
-
viewBox="0 0 170 44"
|
|
52
|
-
fill="none"
|
|
53
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
54
|
-
className={className}
|
|
55
|
-
aria-hidden="true"
|
|
56
|
-
>
|
|
57
|
-
<polygon points="20,2 6,18 12,18 20,8 28,18 34,18" fill="#E8646A" />
|
|
58
|
-
<rect x="11" y="20" width="4" height="22" rx="1" fill="#E8646A" />
|
|
59
|
-
<rect x="18" y="20" width="4" height="22" rx="1" fill="#E8646A" />
|
|
60
|
-
<rect x="25" y="20" width="4" height="22" rx="1" fill="#E8646A" />
|
|
61
|
-
<text
|
|
62
|
-
x="44"
|
|
63
|
-
y="25"
|
|
64
|
-
fontFamily="system-ui, sans-serif"
|
|
65
|
-
fontSize="17"
|
|
66
|
-
fontWeight="600"
|
|
67
|
-
letterSpacing="1.5"
|
|
68
|
-
fill="#E8646A"
|
|
69
|
-
>
|
|
70
|
-
ACTUATE
|
|
71
|
-
</text>
|
|
72
|
-
<text
|
|
73
|
-
x="44"
|
|
74
|
-
y="40"
|
|
75
|
-
fontFamily="system-ui, sans-serif"
|
|
76
|
-
fontSize="10"
|
|
77
|
-
fontWeight="500"
|
|
78
|
-
letterSpacing="4"
|
|
79
|
-
fill="#9CA3AF"
|
|
80
|
-
>
|
|
81
|
-
MEDIA
|
|
82
|
-
</text>
|
|
83
|
-
</svg>
|
|
84
|
-
)
|
|
57
|
+
return <ActuateBrandLogo className={className} />
|
|
85
58
|
}
|
|
86
59
|
|
|
87
60
|
const ICON_MAP: Record<string, LucideIcon> = {
|
|
@@ -104,21 +77,21 @@ function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean })
|
|
|
104
77
|
|
|
105
78
|
if (collapsed) {
|
|
106
79
|
if (customLogo) {
|
|
107
|
-
return <img src={customLogo} alt={brandName ?? 'Admin'} className="
|
|
80
|
+
return <img src={customLogo} alt={brandName ?? 'Admin'} className="h-8 w-8 object-contain" />
|
|
108
81
|
}
|
|
109
|
-
return <
|
|
82
|
+
return <ActuateMark className="h-8 w-8" />
|
|
110
83
|
}
|
|
111
84
|
|
|
112
85
|
if (customLogo) {
|
|
113
86
|
return (
|
|
114
|
-
<div className="flex items-center gap-2.5
|
|
87
|
+
<div className="flex min-w-0 items-center gap-2.5">
|
|
115
88
|
<img
|
|
116
89
|
src={customLogo}
|
|
117
90
|
alt={brandName ?? 'Admin'}
|
|
118
|
-
className="h-8 w-auto object-contain
|
|
91
|
+
className="h-8 w-auto shrink-0 object-contain"
|
|
119
92
|
/>
|
|
120
93
|
{brandName && (
|
|
121
|
-
<span className="text-sm font-semibold
|
|
94
|
+
<span className="text-sidebar-foreground truncate text-sm font-semibold">
|
|
122
95
|
{brandName}
|
|
123
96
|
</span>
|
|
124
97
|
)}
|
|
@@ -128,14 +101,14 @@ function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean })
|
|
|
128
101
|
|
|
129
102
|
if (brandName) {
|
|
130
103
|
return (
|
|
131
|
-
<div className="flex items-center gap-2.5
|
|
132
|
-
<
|
|
133
|
-
<span className="text-sm font-semibold
|
|
104
|
+
<div className="flex min-w-0 items-center gap-2.5">
|
|
105
|
+
<ActuateMark className="h-7 w-7 shrink-0" />
|
|
106
|
+
<span className="text-sidebar-foreground truncate text-sm font-semibold">{brandName}</span>
|
|
134
107
|
</div>
|
|
135
108
|
)
|
|
136
109
|
}
|
|
137
110
|
|
|
138
|
-
return <ActuateWordmark className="h-
|
|
111
|
+
return <ActuateWordmark className="h-9" />
|
|
139
112
|
}
|
|
140
113
|
|
|
141
114
|
const defaultNavItems = [
|
|
@@ -169,12 +142,12 @@ export function Sidebar({
|
|
|
169
142
|
|
|
170
143
|
return (
|
|
171
144
|
<aside
|
|
172
|
-
className={`
|
|
145
|
+
className={`bg-sidebar border-sidebar-border h-full border-r transition-all duration-200 ${
|
|
173
146
|
collapsed ? 'w-20' : 'w-64'
|
|
174
147
|
}`}
|
|
175
148
|
>
|
|
176
149
|
<div
|
|
177
|
-
className={`flex
|
|
150
|
+
className={`border-sidebar-border flex h-14 items-center border-b px-4 ${
|
|
178
151
|
collapsed ? 'justify-center' : 'justify-between'
|
|
179
152
|
}`}
|
|
180
153
|
>
|
|
@@ -182,18 +155,18 @@ export function Sidebar({
|
|
|
182
155
|
{collapsed && <BrandLogo config={config} collapsed={true} />}
|
|
183
156
|
<button
|
|
184
157
|
onClick={onToggleCollapse}
|
|
185
|
-
className="
|
|
158
|
+
className="hover:bg-sidebar-accent hidden shrink-0 rounded-lg p-2 transition-colors md:block"
|
|
186
159
|
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
187
160
|
>
|
|
188
161
|
{collapsed ? (
|
|
189
|
-
<ChevronRight className="
|
|
162
|
+
<ChevronRight className="text-sidebar-foreground h-4 w-4" />
|
|
190
163
|
) : (
|
|
191
|
-
<ChevronLeft className="
|
|
164
|
+
<ChevronLeft className="text-sidebar-foreground h-4 w-4" />
|
|
192
165
|
)}
|
|
193
166
|
</button>
|
|
194
167
|
</div>
|
|
195
168
|
|
|
196
|
-
<nav className="
|
|
169
|
+
<nav className="space-y-1 p-3">
|
|
197
170
|
{navItems.map((item, idx) => {
|
|
198
171
|
const Icon = item.icon
|
|
199
172
|
const isActive =
|
|
@@ -205,27 +178,27 @@ export function Sidebar({
|
|
|
205
178
|
return (
|
|
206
179
|
<div key={item.path}>
|
|
207
180
|
{showGroupLabel && !collapsed && (
|
|
208
|
-
<div className="pt-3 pb-1
|
|
209
|
-
<span className="text-[10px] font-semibold
|
|
181
|
+
<div className="px-3 pt-3 pb-1">
|
|
182
|
+
<span className="text-sidebar-foreground/50 text-[10px] font-semibold tracking-wider uppercase">
|
|
210
183
|
{item.group}
|
|
211
184
|
</span>
|
|
212
185
|
</div>
|
|
213
186
|
)}
|
|
214
187
|
{showGroupLabel && collapsed && (
|
|
215
|
-
<div className="pt-2 pb-1
|
|
216
|
-
<span className="w-4 border-t
|
|
188
|
+
<div className="flex justify-center pt-2 pb-1">
|
|
189
|
+
<span className="border-sidebar-foreground/20 w-4 border-t" />
|
|
217
190
|
</div>
|
|
218
191
|
)}
|
|
219
192
|
<button
|
|
220
193
|
onClick={() => onNavigate(item.path)}
|
|
221
|
-
className={`flex items-center gap-3 px-3 py-2.5
|
|
194
|
+
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left transition-colors ${
|
|
222
195
|
isActive
|
|
223
196
|
? 'bg-sidebar-accent text-sidebar-primary'
|
|
224
197
|
: 'text-sidebar-foreground hover:bg-sidebar-accent'
|
|
225
198
|
} ${collapsed ? 'justify-center' : ''}`}
|
|
226
199
|
title={collapsed ? item.label : ''}
|
|
227
200
|
>
|
|
228
|
-
<Icon className="
|
|
201
|
+
<Icon className="h-5 w-5 shrink-0" />
|
|
229
202
|
{!collapsed && <span className="text-sm font-medium">{item.label}</span>}
|
|
230
203
|
</button>
|
|
231
204
|
</div>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
export interface AdminShellProps {
|
|
5
|
+
/** Sidebar/navigation slot. Rendered in the left column on desktop. */
|
|
6
|
+
sidebar: ReactNode
|
|
7
|
+
/** Top header slot. Rendered above the main content. */
|
|
8
|
+
header?: ReactNode
|
|
9
|
+
/** Optional breadcrumb / sub-header slot rendered below the header. */
|
|
10
|
+
breadcrumbs?: ReactNode
|
|
11
|
+
/** Main content. Scrolls independently of the sidebar. */
|
|
12
|
+
children: ReactNode
|
|
13
|
+
/**
|
|
14
|
+
* Whether the mobile sidebar overlay is currently open. Controlled.
|
|
15
|
+
* AdminShell renders the backdrop + slide transform; the consumer owns
|
|
16
|
+
* the open state and the hamburger button that toggles it.
|
|
17
|
+
*/
|
|
18
|
+
mobileSidebarOpen?: boolean
|
|
19
|
+
onMobileSidebarClose?: () => void
|
|
20
|
+
/**
|
|
21
|
+
* Breakpoint above which the sidebar docks beside the content instead
|
|
22
|
+
* of overlaying on top of it. Mirrors Tailwind's `md` (768px) default.
|
|
23
|
+
*/
|
|
24
|
+
desktopBreakpoint?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The canonical admin chrome. Owns the entire viewport, splits into
|
|
29
|
+
* `sidebar | (header + breadcrumbs + content)`, and handles the mobile
|
|
30
|
+
* overlay transition.
|
|
31
|
+
*
|
|
32
|
+
* Implementation notes:
|
|
33
|
+
* - Desktop layout is CSS Grid with `gridTemplateColumns: 'auto minmax(0, 1fr)'`.
|
|
34
|
+
* The `minmax(0, 1fr)` is load-bearing: it lets the content shrink
|
|
35
|
+
* below its intrinsic width when the sidebar takes its share, which
|
|
36
|
+
* is the one thing flex-based shells fail at.
|
|
37
|
+
* - Mobile layout is a single block with the sidebar absolutely
|
|
38
|
+
* positioned and slid in via `transform: translateX(…)`. We avoid
|
|
39
|
+
* the brittle `fixed` ↔ `static` toggle that caused recurring sidebar
|
|
40
|
+
* overlap bugs in the old layout.
|
|
41
|
+
* - The desktop/mobile decision is made in JS via `matchMedia` so the
|
|
42
|
+
* layout is independent of Tailwind's `md:` utility compilation. If
|
|
43
|
+
* the CSS bundle is stale or partially loaded the layout still works.
|
|
44
|
+
*/
|
|
45
|
+
export function AdminShell({
|
|
46
|
+
sidebar,
|
|
47
|
+
header,
|
|
48
|
+
breadcrumbs,
|
|
49
|
+
children,
|
|
50
|
+
mobileSidebarOpen = false,
|
|
51
|
+
onMobileSidebarClose,
|
|
52
|
+
desktopBreakpoint = 768,
|
|
53
|
+
}: AdminShellProps) {
|
|
54
|
+
const [isDesktop, setIsDesktop] = useState(false)
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (typeof window === 'undefined') return
|
|
58
|
+
const mq = window.matchMedia(`(min-width: ${desktopBreakpoint}px)`)
|
|
59
|
+
const handler = () => setIsDesktop(mq.matches)
|
|
60
|
+
handler()
|
|
61
|
+
mq.addEventListener('change', handler)
|
|
62
|
+
return () => mq.removeEventListener('change', handler)
|
|
63
|
+
}, [desktopBreakpoint])
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
className="bg-background text-foreground h-screen overflow-hidden"
|
|
68
|
+
style={
|
|
69
|
+
isDesktop
|
|
70
|
+
? { display: 'grid', gridTemplateColumns: 'auto minmax(0, 1fr)' }
|
|
71
|
+
: { display: 'block', position: 'relative' }
|
|
72
|
+
}
|
|
73
|
+
>
|
|
74
|
+
{!isDesktop && mobileSidebarOpen && (
|
|
75
|
+
<div
|
|
76
|
+
aria-hidden
|
|
77
|
+
className="fixed inset-0 z-40 bg-black/30 backdrop-blur-sm"
|
|
78
|
+
onClick={onMobileSidebarClose}
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
<div
|
|
83
|
+
className="z-50"
|
|
84
|
+
style={
|
|
85
|
+
isDesktop
|
|
86
|
+
? {
|
|
87
|
+
gridColumn: '1 / 2',
|
|
88
|
+
height: '100vh',
|
|
89
|
+
overflow: 'hidden',
|
|
90
|
+
}
|
|
91
|
+
: {
|
|
92
|
+
position: 'fixed',
|
|
93
|
+
top: 0,
|
|
94
|
+
bottom: 0,
|
|
95
|
+
left: 0,
|
|
96
|
+
transform: mobileSidebarOpen ? 'translateX(0)' : 'translateX(-100%)',
|
|
97
|
+
transition: 'transform 300ms ease',
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
>
|
|
101
|
+
{sidebar}
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div
|
|
105
|
+
className="flex flex-col overflow-hidden"
|
|
106
|
+
style={
|
|
107
|
+
isDesktop
|
|
108
|
+
? { gridColumn: '2 / 3', height: '100vh', minWidth: 0 }
|
|
109
|
+
: { height: '100vh', minWidth: 0 }
|
|
110
|
+
}
|
|
111
|
+
>
|
|
112
|
+
{header}
|
|
113
|
+
{breadcrumbs}
|
|
114
|
+
<main className="flex-1 overflow-y-auto">{children}</main>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { forwardRef } from 'react'
|
|
2
|
+
import type { HTMLAttributes, ReactNode, ElementType } from 'react'
|
|
3
|
+
|
|
4
|
+
export interface BoxProps extends Omit<HTMLAttributes<HTMLElement>, 'children'> {
|
|
5
|
+
/**
|
|
6
|
+
* The element to render. Defaults to `div`. Use semantic elements (`<section>`,
|
|
7
|
+
* `<article>`, etc.) for accessibility — Box is the lowest-level escape
|
|
8
|
+
* hatch and exists primarily so views don't reach for raw `<div>` with
|
|
9
|
+
* five Tailwind utilities.
|
|
10
|
+
*/
|
|
11
|
+
as?: ElementType
|
|
12
|
+
children?: ReactNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The lowest-level primitive — a forwardRef'd div (by default) that exists
|
|
17
|
+
* so consumers always have a stable, semantically-tagged container to
|
|
18
|
+
* compose around. Prefer Box over a bare `<div>` to keep the design system
|
|
19
|
+
* audit clean.
|
|
20
|
+
*/
|
|
21
|
+
export const Box = forwardRef<HTMLElement, BoxProps>(function Box(
|
|
22
|
+
{ as: Component = 'div', children, ...rest },
|
|
23
|
+
ref,
|
|
24
|
+
) {
|
|
25
|
+
return (
|
|
26
|
+
<Component ref={ref} {...rest}>
|
|
27
|
+
{children}
|
|
28
|
+
</Component>
|
|
29
|
+
)
|
|
30
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode, ElementType } from 'react'
|
|
2
|
+
import type { SpaceToken } from './tokens.js'
|
|
3
|
+
|
|
4
|
+
export type ClusterAlign = 'start' | 'center' | 'end' | 'baseline' | 'stretch'
|
|
5
|
+
export type ClusterJustify = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'
|
|
6
|
+
|
|
7
|
+
export interface ClusterProps extends HTMLAttributes<HTMLElement> {
|
|
8
|
+
/** Gap between children. Maps to Tailwind `gap-{space}`. */
|
|
9
|
+
space?: SpaceToken
|
|
10
|
+
/** Render as a different element. Defaults to `div`. */
|
|
11
|
+
as?: ElementType
|
|
12
|
+
/** Align items on the cross axis. */
|
|
13
|
+
align?: ClusterAlign
|
|
14
|
+
/** Distribute items along the main axis. */
|
|
15
|
+
justify?: ClusterJustify
|
|
16
|
+
/** Reverse the order of children. */
|
|
17
|
+
reverse?: boolean
|
|
18
|
+
/** Disable wrapping (default: true, items wrap on overflow). */
|
|
19
|
+
noWrap?: boolean
|
|
20
|
+
children?: ReactNode
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ALIGN_CLASS: Record<ClusterAlign, string> = {
|
|
24
|
+
start: 'items-start',
|
|
25
|
+
center: 'items-center',
|
|
26
|
+
end: 'items-end',
|
|
27
|
+
baseline: 'items-baseline',
|
|
28
|
+
stretch: 'items-stretch',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const JUSTIFY_CLASS: Record<ClusterJustify, string> = {
|
|
32
|
+
start: 'justify-start',
|
|
33
|
+
center: 'justify-center',
|
|
34
|
+
end: 'justify-end',
|
|
35
|
+
between: 'justify-between',
|
|
36
|
+
around: 'justify-around',
|
|
37
|
+
evenly: 'justify-evenly',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Horizontal cluster of items with a consistent gap, wrapping by default.
|
|
42
|
+
* The canonical use case is action toolbars (`<Cluster justify="end">`),
|
|
43
|
+
* badge groups, breadcrumbs, etc. Never reach for `flex flex-wrap gap-2`
|
|
44
|
+
* directly — use Cluster.
|
|
45
|
+
*/
|
|
46
|
+
export function Cluster({
|
|
47
|
+
space = '2',
|
|
48
|
+
as: Component = 'div',
|
|
49
|
+
align = 'center',
|
|
50
|
+
justify = 'start',
|
|
51
|
+
reverse,
|
|
52
|
+
noWrap,
|
|
53
|
+
className = '',
|
|
54
|
+
children,
|
|
55
|
+
...rest
|
|
56
|
+
}: ClusterProps) {
|
|
57
|
+
const classes = [
|
|
58
|
+
'flex',
|
|
59
|
+
reverse ? 'flex-row-reverse' : 'flex-row',
|
|
60
|
+
noWrap ? 'flex-nowrap' : 'flex-wrap',
|
|
61
|
+
`gap-${space}`,
|
|
62
|
+
ALIGN_CLASS[align],
|
|
63
|
+
JUSTIFY_CLASS[justify],
|
|
64
|
+
className,
|
|
65
|
+
]
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.join(' ')
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Component className={classes} {...rest}>
|
|
71
|
+
{children}
|
|
72
|
+
</Component>
|
|
73
|
+
)
|
|
74
|
+
}
|