@actuate-media/cms-admin 0.10.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.js +1 -1
- package/dist/components/SharePreviewLinkDialog.js +1 -1
- 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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -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.js +7 -7
- 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.js +3 -3
- 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 +4 -4
- package/src/components/SharePreviewLinkDialog.tsx +1 -1
- 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 +28 -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 +9 -9
- 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/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
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode, ElementType } from 'react'
|
|
2
|
+
import type { SpaceToken } from './tokens.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Responsive column count. Pass a number for static grids, or a map to
|
|
6
|
+
* change column count at each breakpoint.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* columns={3} // 3 cols at every breakpoint
|
|
10
|
+
* columns={{ base: 1, md: 2, lg: 4 }} // responsive
|
|
11
|
+
*/
|
|
12
|
+
export type GridResponsive =
|
|
13
|
+
| number
|
|
14
|
+
| { base?: number; sm?: number; md?: number; lg?: number; xl?: number; '2xl'?: number }
|
|
15
|
+
|
|
16
|
+
export interface GridProps extends HTMLAttributes<HTMLElement> {
|
|
17
|
+
columns: GridResponsive
|
|
18
|
+
/** Gap between cells. */
|
|
19
|
+
space?: SpaceToken
|
|
20
|
+
/** Row gap (defaults to `space`). */
|
|
21
|
+
rowSpace?: SpaceToken
|
|
22
|
+
/** Render as a different element. Defaults to `div`. */
|
|
23
|
+
as?: ElementType
|
|
24
|
+
/**
|
|
25
|
+
* Use auto-fit instead of explicit column counts. When set, `minItemWidth`
|
|
26
|
+
* controls the minimum tile width before the grid reflows. Use this for
|
|
27
|
+
* card grids that should adapt to container width without specific
|
|
28
|
+
* breakpoints.
|
|
29
|
+
*/
|
|
30
|
+
autoFit?: boolean
|
|
31
|
+
/** Min tile width when `autoFit` is true. Defaults to `16rem`. */
|
|
32
|
+
minItemWidth?: string
|
|
33
|
+
children?: ReactNode
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const COL_CLASS: Record<number, string> = {
|
|
37
|
+
1: 'grid-cols-1',
|
|
38
|
+
2: 'grid-cols-2',
|
|
39
|
+
3: 'grid-cols-3',
|
|
40
|
+
4: 'grid-cols-4',
|
|
41
|
+
5: 'grid-cols-5',
|
|
42
|
+
6: 'grid-cols-6',
|
|
43
|
+
7: 'grid-cols-7',
|
|
44
|
+
8: 'grid-cols-8',
|
|
45
|
+
9: 'grid-cols-9',
|
|
46
|
+
10: 'grid-cols-10',
|
|
47
|
+
11: 'grid-cols-11',
|
|
48
|
+
12: 'grid-cols-12',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const BP_PREFIX = ['', 'sm:', 'md:', 'lg:', 'xl:', '2xl:'] as const
|
|
52
|
+
type BpKey = 'base' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
|
53
|
+
|
|
54
|
+
function columnsToClasses(columns: GridResponsive): string {
|
|
55
|
+
if (typeof columns === 'number') {
|
|
56
|
+
return COL_CLASS[columns] ?? 'grid-cols-1'
|
|
57
|
+
}
|
|
58
|
+
const order: BpKey[] = ['base', 'sm', 'md', 'lg', 'xl', '2xl']
|
|
59
|
+
return order
|
|
60
|
+
.map((bp, i) => {
|
|
61
|
+
const n = columns[bp]
|
|
62
|
+
if (!n) return ''
|
|
63
|
+
const base = COL_CLASS[n] ?? 'grid-cols-1'
|
|
64
|
+
return BP_PREFIX[i] + base
|
|
65
|
+
})
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.join(' ')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Responsive CSS grid primitive. Pass either an explicit `columns` count
|
|
72
|
+
* (per-breakpoint or static) or set `autoFit` for a `repeat(auto-fit, …)`
|
|
73
|
+
* tile grid. Both modes share the same `space` / `rowSpace` knobs so
|
|
74
|
+
* usage is consistent across views.
|
|
75
|
+
*/
|
|
76
|
+
export function Grid({
|
|
77
|
+
columns,
|
|
78
|
+
space = '4',
|
|
79
|
+
rowSpace,
|
|
80
|
+
as: Component = 'div',
|
|
81
|
+
autoFit,
|
|
82
|
+
minItemWidth = '16rem',
|
|
83
|
+
className = '',
|
|
84
|
+
style,
|
|
85
|
+
children,
|
|
86
|
+
...rest
|
|
87
|
+
}: GridProps) {
|
|
88
|
+
if (autoFit) {
|
|
89
|
+
return (
|
|
90
|
+
<Component
|
|
91
|
+
className={['grid', `gap-${space}`, rowSpace ? `gap-y-${rowSpace}` : '', className]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join(' ')}
|
|
94
|
+
style={{
|
|
95
|
+
gridTemplateColumns: `repeat(auto-fit, minmax(${minItemWidth}, 1fr))`,
|
|
96
|
+
...style,
|
|
97
|
+
}}
|
|
98
|
+
{...rest}
|
|
99
|
+
>
|
|
100
|
+
{children}
|
|
101
|
+
</Component>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const classes = [
|
|
106
|
+
'grid',
|
|
107
|
+
columnsToClasses(columns),
|
|
108
|
+
`gap-${space}`,
|
|
109
|
+
rowSpace ? `gap-y-${rowSpace}` : '',
|
|
110
|
+
className,
|
|
111
|
+
]
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
.join(' ')
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Component className={classes} style={style} {...rest}>
|
|
117
|
+
{children}
|
|
118
|
+
</Component>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
import { Stack } from './Stack.js'
|
|
3
|
+
|
|
4
|
+
export interface PageContainerProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
|
|
5
|
+
/** Page title rendered as an h1 inside the page header. */
|
|
6
|
+
title?: ReactNode
|
|
7
|
+
/** Optional sub-headline shown beneath the title. */
|
|
8
|
+
description?: ReactNode
|
|
9
|
+
/** Right-aligned action area (buttons, filters). Rendered in the header. */
|
|
10
|
+
actions?: ReactNode
|
|
11
|
+
/**
|
|
12
|
+
* Maximum width of the content. Defaults to `7xl` (Tailwind's max-w-7xl,
|
|
13
|
+
* ~1280px). Pass `'full'` for edge-to-edge content.
|
|
14
|
+
*/
|
|
15
|
+
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | '7xl' | 'full'
|
|
16
|
+
/** Horizontal padding token. Defaults to `6` (24px). */
|
|
17
|
+
paddingX?: '4' | '6' | '8'
|
|
18
|
+
/** Vertical padding token. Defaults to `6`. */
|
|
19
|
+
paddingY?: '4' | '6' | '8' | '10'
|
|
20
|
+
/** Vertical gap between the header and the body. Defaults to `6`. */
|
|
21
|
+
gap?: '4' | '6' | '8'
|
|
22
|
+
children?: ReactNode
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MAX_WIDTH_CLASS: Record<NonNullable<PageContainerProps['maxWidth']>, string> = {
|
|
26
|
+
sm: 'max-w-sm',
|
|
27
|
+
md: 'max-w-md',
|
|
28
|
+
lg: 'max-w-lg',
|
|
29
|
+
xl: 'max-w-xl',
|
|
30
|
+
'2xl': 'max-w-2xl',
|
|
31
|
+
'3xl': 'max-w-3xl',
|
|
32
|
+
'4xl': 'max-w-4xl',
|
|
33
|
+
'5xl': 'max-w-5xl',
|
|
34
|
+
'6xl': 'max-w-6xl',
|
|
35
|
+
'7xl': 'max-w-7xl',
|
|
36
|
+
full: 'max-w-none',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The single canonical wrapper for every admin page. PageContainer owns
|
|
41
|
+
* three responsibilities so individual views can stop redoing them:
|
|
42
|
+
*
|
|
43
|
+
* 1. Consistent max-width + horizontal padding (so dashboards, lists, and
|
|
44
|
+
* forms all line up visually).
|
|
45
|
+
* 2. A standard header slot with title + description + actions.
|
|
46
|
+
* 3. A standard body slot rendered as a `<Stack>` so children stack with
|
|
47
|
+
* consistent vertical rhythm.
|
|
48
|
+
*
|
|
49
|
+
* Use PageContainer at the top of every screen. Compose with `<Stack>` /
|
|
50
|
+
* `<Grid>` / `<Cluster>` inside. Never roll a per-screen wrapper.
|
|
51
|
+
*/
|
|
52
|
+
export function PageContainer({
|
|
53
|
+
title,
|
|
54
|
+
description,
|
|
55
|
+
actions,
|
|
56
|
+
maxWidth = '7xl',
|
|
57
|
+
paddingX = '6',
|
|
58
|
+
paddingY = '6',
|
|
59
|
+
gap = '6',
|
|
60
|
+
className = '',
|
|
61
|
+
children,
|
|
62
|
+
...rest
|
|
63
|
+
}: PageContainerProps) {
|
|
64
|
+
const outerClasses = [
|
|
65
|
+
'mx-auto w-full',
|
|
66
|
+
MAX_WIDTH_CLASS[maxWidth],
|
|
67
|
+
`px-${paddingX}`,
|
|
68
|
+
`py-${paddingY}`,
|
|
69
|
+
className,
|
|
70
|
+
]
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.join(' ')
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<section className={outerClasses} {...rest}>
|
|
76
|
+
<Stack space={gap}>
|
|
77
|
+
{(title || description || actions) && (
|
|
78
|
+
<header className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
|
79
|
+
{(title || description) && (
|
|
80
|
+
<div className="min-w-0">
|
|
81
|
+
{title && (
|
|
82
|
+
<h1 className="text-foreground truncate text-2xl font-semibold tracking-tight">
|
|
83
|
+
{title}
|
|
84
|
+
</h1>
|
|
85
|
+
)}
|
|
86
|
+
{description && <p className="text-muted-foreground mt-1 text-sm">{description}</p>}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
{actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
|
|
90
|
+
</header>
|
|
91
|
+
)}
|
|
92
|
+
{children}
|
|
93
|
+
</Stack>
|
|
94
|
+
</section>
|
|
95
|
+
)
|
|
96
|
+
}
|