@dyrected/admin 2.4.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/App.d.ts +1 -0
- package/dist/admin.css +2 -0
- package/dist/components/auth/auth-gate.d.ts +13 -0
- package/dist/components/error-boundary.d.ts +16 -0
- package/dist/components/forms/field-renderer.d.ts +22 -0
- package/dist/components/forms/fields/block-builder.d.ts +9 -0
- package/dist/components/forms/fields/date-picker.d.ts +8 -0
- package/dist/components/forms/fields/json-editor.d.ts +8 -0
- package/dist/components/forms/fields/media-picker.d.ts +12 -0
- package/dist/components/forms/fields/multi-select.d.ts +19 -0
- package/dist/components/forms/fields/radio-field.d.ts +8 -0
- package/dist/components/forms/fields/relationship-picker.d.ts +10 -0
- package/dist/components/forms/fields/rich-text-editor.d.ts +9 -0
- package/dist/components/forms/fields/select-field.d.ts +8 -0
- package/dist/components/forms/fields/switch-field.d.ts +6 -0
- package/dist/components/forms/fields/text-area-field.d.ts +8 -0
- package/dist/components/forms/fields/text-field.d.ts +8 -0
- package/dist/components/forms/form-engine.d.ts +14 -0
- package/dist/components/forms/form-field-renderer.d.ts +20 -0
- package/dist/components/forms/utils.d.ts +11 -0
- package/dist/components/layout/admin-shell.d.ts +5 -0
- package/dist/components/layout/branding-provider.d.ts +4 -0
- package/dist/components/live-preview/LivePreviewPane.d.ts +7 -0
- package/dist/components/media/focal-point-picker.d.ts +12 -0
- package/dist/components/media/media-card.d.ts +8 -0
- package/dist/components/media/media-grid.d.ts +8 -0
- package/dist/components/media/media-library-dialog.d.ts +11 -0
- package/dist/components/ui/aspect-ratio.d.ts +3 -0
- package/dist/components/ui/badge.d.ts +9 -0
- package/dist/components/ui/button.d.ts +11 -0
- package/dist/components/ui/calendar.d.ts +8 -0
- package/dist/components/ui/card.d.ts +8 -0
- package/dist/components/ui/checkbox.d.ts +4 -0
- package/dist/components/ui/command.d.ts +80 -0
- package/dist/components/ui/data-table.d.ts +14 -0
- package/dist/components/ui/dialog.d.ts +19 -0
- package/dist/components/ui/dropdown-menu.d.ts +27 -0
- package/dist/components/ui/form.d.ts +23 -0
- package/dist/components/ui/input.d.ts +3 -0
- package/dist/components/ui/label.d.ts +5 -0
- package/dist/components/ui/page-header.d.ts +10 -0
- package/dist/components/ui/pagination.d.ts +11 -0
- package/dist/components/ui/popover.d.ts +6 -0
- package/dist/components/ui/progress.d.ts +4 -0
- package/dist/components/ui/radio-group.d.ts +5 -0
- package/dist/components/ui/render-cell.d.ts +8 -0
- package/dist/components/ui/scroll-area.d.ts +5 -0
- package/dist/components/ui/select.d.ts +13 -0
- package/dist/components/ui/separator.d.ts +4 -0
- package/dist/components/ui/sheet.d.ts +25 -0
- package/dist/components/ui/sidebar.d.ts +65 -0
- package/dist/components/ui/skeleton.d.ts +2 -0
- package/dist/components/ui/sonner.d.ts +4 -0
- package/dist/components/ui/switch.d.ts +4 -0
- package/dist/components/ui/table.d.ts +10 -0
- package/dist/components/ui/tabs.d.ts +7 -0
- package/dist/components/ui/textarea.d.ts +3 -0
- package/dist/components/ui/toggle.d.ts +12 -0
- package/dist/components/ui/tooltip.d.ts +7 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-preferences.d.ts +6 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.mjs +69091 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/main.d.ts +0 -0
- package/dist/pages/auth/first-user-page.d.ts +4 -0
- package/dist/pages/auth/login-page.d.ts +4 -0
- package/dist/pages/collections/edit-page.d.ts +1 -0
- package/dist/pages/collections/list-page.d.ts +5 -0
- package/dist/pages/dashboard/dashboard.d.ts +1 -0
- package/dist/pages/globals/editor-page.d.ts +1 -0
- package/dist/pages/media/media-page.d.ts +4 -0
- package/dist/pages/setup/setup-prompt.d.ts +6 -0
- package/dist/providers/dyrected-provider.d.ts +29 -0
- package/dist/providers/query-provider.d.ts +3 -0
- package/package.json +6 -3
- package/CHANGELOG.md +0 -153
- package/components.json +0 -17
- package/eslint.config.js +0 -22
- package/index.html +0 -13
- package/postcss.config.js +0 -6
- package/scripts/prefix-tailwind-precision.py +0 -98
- package/scripts/prefix-tailwind.py +0 -67
- package/src/App.css +0 -184
- package/src/App.tsx +0 -25
- package/src/assets/dyrected.svg +0 -155
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +0 -1
- package/src/assets/vite.svg +0 -1
- package/src/components/auth/auth-gate.tsx +0 -64
- package/src/components/error-boundary.tsx +0 -45
- package/src/components/forms/field-renderer.tsx +0 -111
- package/src/components/forms/fields/block-builder.tsx +0 -213
- package/src/components/forms/fields/date-picker.tsx +0 -60
- package/src/components/forms/fields/json-editor.tsx +0 -62
- package/src/components/forms/fields/media-picker.tsx +0 -286
- package/src/components/forms/fields/multi-select.tsx +0 -145
- package/src/components/forms/fields/radio-field.tsx +0 -51
- package/src/components/forms/fields/relationship-picker.tsx +0 -143
- package/src/components/forms/fields/rich-text-editor.tsx +0 -224
- package/src/components/forms/fields/select-field.tsx +0 -35
- package/src/components/forms/fields/switch-field.tsx +0 -16
- package/src/components/forms/fields/text-area-field.tsx +0 -15
- package/src/components/forms/fields/text-field.tsx +0 -24
- package/src/components/forms/form-engine.tsx +0 -87
- package/src/components/forms/form-field-renderer.tsx +0 -269
- package/src/components/forms/utils.ts +0 -97
- package/src/components/layout/admin-shell.tsx +0 -479
- package/src/components/layout/branding-provider.tsx +0 -112
- package/src/components/live-preview/LivePreviewPane.tsx +0 -128
- package/src/components/media/focal-point-picker.tsx +0 -66
- package/src/components/media/media-card.tsx +0 -44
- package/src/components/media/media-grid.tsx +0 -32
- package/src/components/media/media-library-dialog.tsx +0 -465
- package/src/components/ui/aspect-ratio.tsx +0 -7
- package/src/components/ui/badge.tsx +0 -36
- package/src/components/ui/button.tsx +0 -56
- package/src/components/ui/calendar.tsx +0 -214
- package/src/components/ui/card.tsx +0 -79
- package/src/components/ui/checkbox.tsx +0 -28
- package/src/components/ui/command.tsx +0 -151
- package/src/components/ui/data-table.tsx +0 -219
- package/src/components/ui/dialog.tsx +0 -122
- package/src/components/ui/dropdown-menu.tsx +0 -200
- package/src/components/ui/form.tsx +0 -178
- package/src/components/ui/input.tsx +0 -24
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/page-header.tsx +0 -30
- package/src/components/ui/pagination.tsx +0 -57
- package/src/components/ui/popover.tsx +0 -29
- package/src/components/ui/progress.tsx +0 -26
- package/src/components/ui/radio-group.tsx +0 -42
- package/src/components/ui/render-cell.tsx +0 -110
- package/src/components/ui/scroll-area.tsx +0 -46
- package/src/components/ui/select.tsx +0 -160
- package/src/components/ui/separator.tsx +0 -29
- package/src/components/ui/sheet.tsx +0 -140
- package/src/components/ui/sidebar.tsx +0 -771
- package/src/components/ui/skeleton.tsx +0 -15
- package/src/components/ui/sonner.tsx +0 -27
- package/src/components/ui/switch.tsx +0 -27
- package/src/components/ui/table.tsx +0 -117
- package/src/components/ui/tabs.tsx +0 -53
- package/src/components/ui/textarea.tsx +0 -22
- package/src/components/ui/toggle.tsx +0 -43
- package/src/components/ui/tooltip.tsx +0 -28
- package/src/hooks/use-mobile.tsx +0 -19
- package/src/hooks/use-preferences.ts +0 -56
- package/src/index.css +0 -111
- package/src/index.tsx +0 -198
- package/src/lib/utils.ts +0 -36
- package/src/main.tsx +0 -10
- package/src/pages/auth/first-user-page.tsx +0 -115
- package/src/pages/auth/login-page.tsx +0 -91
- package/src/pages/collections/edit-page.tsx +0 -280
- package/src/pages/collections/list-page.tsx +0 -343
- package/src/pages/dashboard/dashboard.tsx +0 -150
- package/src/pages/globals/editor-page.tsx +0 -122
- package/src/pages/media/media-page.tsx +0 -564
- package/src/pages/setup/setup-prompt.tsx +0 -181
- package/src/providers/dyrected-provider.tsx +0 -122
- package/src/providers/query-provider.tsx +0 -19
- package/src/types/jexl.d.ts +0 -11
- package/tailwind.config.ts +0 -103
- package/tsconfig.app.json +0 -28
- package/tsconfig.json +0 -12
- package/tsconfig.node.json +0 -25
- package/vite.config.ts +0 -39
- /package/{public → dist}/favicon.svg +0 -0
- /package/{public → dist}/icons.svg +0 -0
|
@@ -1,479 +0,0 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import { useState, useEffect } from "react"
|
|
3
|
-
import { useQuery } from "@tanstack/react-query"
|
|
4
|
-
import { Link, useLocation } from "react-router-dom"
|
|
5
|
-
import {
|
|
6
|
-
Database,
|
|
7
|
-
Image as ImageIcon,
|
|
8
|
-
Settings,
|
|
9
|
-
LogOut,
|
|
10
|
-
Menu,
|
|
11
|
-
X,
|
|
12
|
-
ChevronRight,
|
|
13
|
-
ChevronDown,
|
|
14
|
-
PanelLeftClose,
|
|
15
|
-
PanelLeftOpen,
|
|
16
|
-
Sparkles,
|
|
17
|
-
Lock,
|
|
18
|
-
Shield,
|
|
19
|
-
Share2,
|
|
20
|
-
LayoutDashboard,
|
|
21
|
-
Users,
|
|
22
|
-
} from "lucide-react"
|
|
23
|
-
import { useDyrected } from "../../providers/dyrected-provider"
|
|
24
|
-
import { cn, getMediaUrl } from "../../lib/utils"
|
|
25
|
-
import { BrandingProvider } from "./branding-provider"
|
|
26
|
-
import logo from "@/assets/dyrected.svg"
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Single nav item
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
function NavItem({
|
|
32
|
-
to,
|
|
33
|
-
icon: Icon,
|
|
34
|
-
label,
|
|
35
|
-
active,
|
|
36
|
-
collapsed,
|
|
37
|
-
onClick,
|
|
38
|
-
}: {
|
|
39
|
-
to: string
|
|
40
|
-
icon: React.ElementType
|
|
41
|
-
label: React.ReactNode
|
|
42
|
-
active: boolean
|
|
43
|
-
collapsed: boolean
|
|
44
|
-
onClick?: () => void
|
|
45
|
-
}) {
|
|
46
|
-
return (
|
|
47
|
-
<Link
|
|
48
|
-
to={to}
|
|
49
|
-
onClick={onClick}
|
|
50
|
-
className={cn(
|
|
51
|
-
"dy-group dy-flex dy-items-center dy-gap-3 dy-rounded-md dy-px-3 dy-py-2 dy-text-[13px] dy-font-medium dy-transition-all dy-duration-150",
|
|
52
|
-
collapsed ? "dy-justify-center dy-px-2" : "",
|
|
53
|
-
active
|
|
54
|
-
? "dy-bg-primary dy-text-primary-foreground"
|
|
55
|
-
: "dy-text-muted-foreground hover:dy-bg-accent hover:dy-text-foreground"
|
|
56
|
-
)}
|
|
57
|
-
>
|
|
58
|
-
<Icon
|
|
59
|
-
className={cn(
|
|
60
|
-
"dy-shrink-0 dy-transition-colors",
|
|
61
|
-
collapsed ? "dy-h-[17px] dy-w-[17px]" : "dy-h-[15px] dy-w-[15px]",
|
|
62
|
-
active ? "dy-text-background" : "dy-text-muted-foreground dy-group-hover:dy-text-foreground"
|
|
63
|
-
)}
|
|
64
|
-
/>
|
|
65
|
-
{!collapsed && <span className="dy-truncate">{label}</span>}
|
|
66
|
-
{!collapsed && active && (
|
|
67
|
-
<ChevronRight className="dy-ml-auto dy-h-3.5 dy-w-3.5 dy-opacity-50 dy-shrink-0" />
|
|
68
|
-
)}
|
|
69
|
-
</Link>
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// Nav Group (Collapsible)
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
function NavGroup({
|
|
77
|
-
label,
|
|
78
|
-
children,
|
|
79
|
-
collapsed,
|
|
80
|
-
defaultExpanded = true,
|
|
81
|
-
}: {
|
|
82
|
-
label: string
|
|
83
|
-
children: React.ReactNode
|
|
84
|
-
collapsed: boolean
|
|
85
|
-
defaultExpanded?: boolean
|
|
86
|
-
}) {
|
|
87
|
-
const [expanded, setExpanded] = useState(defaultExpanded)
|
|
88
|
-
|
|
89
|
-
if (collapsed) {
|
|
90
|
-
return (
|
|
91
|
-
<div className="dy-space-y-1">
|
|
92
|
-
<div className="dy-my-2 dy-mx-3 dy-h-px dy-bg-border" />
|
|
93
|
-
{children}
|
|
94
|
-
</div>
|
|
95
|
-
)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<div className="dy-space-y-1">
|
|
100
|
-
<button
|
|
101
|
-
onClick={() => setExpanded(!expanded)}
|
|
102
|
-
className="dy-flex dy-w-full dy-items-center dy-justify-between dy-px-3 dy-mt-4 dy-mb-1 dy-group"
|
|
103
|
-
>
|
|
104
|
-
<span className="dy-text-[10px] dy-font-semibold dy-uppercase dy-tracking-widest dy-text-muted-foreground/40 dy-group-hover:dy-text-muted-foreground/60 dy-transition-colors">
|
|
105
|
-
{label}
|
|
106
|
-
</span>
|
|
107
|
-
{expanded ? (
|
|
108
|
-
<ChevronDown className="dy-h-3 dy-w-3 dy-text-muted-foreground/30 dy-group-hover:dy-text-muted-foreground/50" />
|
|
109
|
-
) : (
|
|
110
|
-
<ChevronRight className="dy-h-3 dy-w-3 dy-text-muted-foreground/30 dy-group-hover:dy-text-muted-foreground/50" />
|
|
111
|
-
)}
|
|
112
|
-
</button>
|
|
113
|
-
<div className={cn("dy-space-y-0.5 dy-overflow-hidden dy-transition-all dy-duration-200", expanded ? "dy-max-h-[1000px] dy-opacity-100" : "dy-max-h-0 dy-opacity-0")}>
|
|
114
|
-
{children}
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
// Sidebar inner content (shared)
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
function SidebarInner({
|
|
124
|
-
schemas,
|
|
125
|
-
isLoading,
|
|
126
|
-
location,
|
|
127
|
-
logout,
|
|
128
|
-
isEmbedded,
|
|
129
|
-
collapsed,
|
|
130
|
-
onToggleCollapse,
|
|
131
|
-
onNavigate,
|
|
132
|
-
}: {
|
|
133
|
-
schemas: any
|
|
134
|
-
isLoading: boolean
|
|
135
|
-
location: ReturnType<typeof useLocation>
|
|
136
|
-
logout: () => void
|
|
137
|
-
isEmbedded: boolean
|
|
138
|
-
collapsed: boolean
|
|
139
|
-
onToggleCollapse?: () => void
|
|
140
|
-
onNavigate?: () => void
|
|
141
|
-
}) {
|
|
142
|
-
const { client } = useDyrected()
|
|
143
|
-
const collections = schemas?.collections?.filter((c: any) => !c?.admin?.hidden && !c?.slug.startsWith('platform_')) ?? []
|
|
144
|
-
const globals = schemas?.globals?.filter((g: any) => !g?.admin?.hidden && !g?.slug.startsWith('platform_')) ?? []
|
|
145
|
-
const uploadCollections = collections.filter((c: any) => c.upload)
|
|
146
|
-
|
|
147
|
-
const groupLabel = (text: string) =>
|
|
148
|
-
!collapsed ? (
|
|
149
|
-
<p className="dy-px-3 dy-mb-1.5 dy-text-[10px] dy-font-semibold dy-uppercase dy-tracking-widest dy-text-muted-foreground/50">
|
|
150
|
-
{text}
|
|
151
|
-
</p>
|
|
152
|
-
) : (
|
|
153
|
-
<div className="dy-my-2 dy-mx-3 dy-h-px dy-bg-border" />
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
const branding = schemas?.admin?.branding;
|
|
157
|
-
|
|
158
|
-
return (
|
|
159
|
-
<div className="dy-flex dy-flex-col dy-min-h-screen dy-admin-ui">
|
|
160
|
-
{/* Logo */}
|
|
161
|
-
<div
|
|
162
|
-
className={cn(
|
|
163
|
-
"dy-flex dy-items-center dy-h-14 dy-shrink-0 dy-transition-all",
|
|
164
|
-
collapsed ? "dy-justify-center dy-px-2" : "dy-gap-2.5 dy-px-4"
|
|
165
|
-
)}
|
|
166
|
-
>
|
|
167
|
-
<div>
|
|
168
|
-
{!isEmbedded && (
|
|
169
|
-
<>
|
|
170
|
-
{branding?.logo || branding?.logoMark ? (
|
|
171
|
-
<div className="dy-h-7 dy-w-7 dy-flex dy-items-center dy-justify-center dy-shrink-0">
|
|
172
|
-
<img
|
|
173
|
-
src={getMediaUrl(collapsed ? (branding.logoMark || branding.logo) : (branding.logo || branding.logoMark), client?.getBaseUrl() || "")}
|
|
174
|
-
alt="Logo"
|
|
175
|
-
className="dy-max-h-full dy-max-w-full dy-object-contain"
|
|
176
|
-
/>
|
|
177
|
-
</div>
|
|
178
|
-
) : (
|
|
179
|
-
<div className="dy-h-7 dy-w-auto dy-flex dy-items-center dy-justify-center dy-shrink-0">
|
|
180
|
-
<img src={logo} alt="Dyrected" className="dy-h-8 dy-w-auto" />
|
|
181
|
-
</div>
|
|
182
|
-
)}
|
|
183
|
-
{!collapsed && (
|
|
184
|
-
<span className="dy-font-serif dy-text-lg dy-tracking-tight dy-text-foreground dy-flex-1 dy-truncate">
|
|
185
|
-
{branding?.titleSuffix?.replace(/^- /, '') || ''}
|
|
186
|
-
</span>
|
|
187
|
-
)}
|
|
188
|
-
</>
|
|
189
|
-
)}
|
|
190
|
-
</div>
|
|
191
|
-
{/* Desktop Toggle - Only visible on desktop since mobile uses overlay */}
|
|
192
|
-
|
|
193
|
-
</div>
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
{/* Nav */}
|
|
197
|
-
<nav className="dy-flex-1 dy-overflow-y-auto dy-py-4 dy-px-2 dy-space-y-4">
|
|
198
|
-
<div>
|
|
199
|
-
<NavItem
|
|
200
|
-
to="/"
|
|
201
|
-
icon={LayoutDashboard}
|
|
202
|
-
label="Dashboard"
|
|
203
|
-
active={location.pathname === "/" || location.pathname === ""}
|
|
204
|
-
collapsed={collapsed}
|
|
205
|
-
onClick={onNavigate}
|
|
206
|
-
/>
|
|
207
|
-
</div>
|
|
208
|
-
|
|
209
|
-
{uploadCollections.length > 0 && (
|
|
210
|
-
<div>
|
|
211
|
-
{groupLabel("Media")}
|
|
212
|
-
{uploadCollections.map((col: any) => (
|
|
213
|
-
<NavItem
|
|
214
|
-
key={col.slug}
|
|
215
|
-
to={`/collections/${col.slug}`}
|
|
216
|
-
icon={ImageIcon}
|
|
217
|
-
label={col.labels?.plural ?? col.label ?? col.slug}
|
|
218
|
-
active={location.pathname.startsWith(`/collections/${col.slug}`)}
|
|
219
|
-
collapsed={collapsed}
|
|
220
|
-
onClick={onNavigate}
|
|
221
|
-
/>
|
|
222
|
-
))}
|
|
223
|
-
</div>
|
|
224
|
-
)}
|
|
225
|
-
|
|
226
|
-
{(isLoading || collections.filter((c: any) => !c.upload).length > 0) && (
|
|
227
|
-
<div>
|
|
228
|
-
{isLoading ? (
|
|
229
|
-
<div className="dy-space-y-1 dy-px-1">
|
|
230
|
-
{[1, 2, 3].map((i) => (
|
|
231
|
-
<div key={i} className={cn("dy-h-8 dy-rounded-md dy-bg-muted/60 dy-animate-pulse", collapsed ? "dy-mx-1" : "dy-mx-2")} />
|
|
232
|
-
))}
|
|
233
|
-
</div>
|
|
234
|
-
) : (() => {
|
|
235
|
-
const nonUpload = collections.filter((col: any) => !col.upload)
|
|
236
|
-
const groups = new Map<string, any[]>()
|
|
237
|
-
const ungrouped: any[] = []
|
|
238
|
-
|
|
239
|
-
nonUpload.forEach((col: any) => {
|
|
240
|
-
let g = col.admin?.group
|
|
241
|
-
if (!g && col.auth) g = "System"
|
|
242
|
-
|
|
243
|
-
if (g) {
|
|
244
|
-
if (!groups.has(g)) groups.set(g, [])
|
|
245
|
-
groups.get(g)!.push(col)
|
|
246
|
-
} else {
|
|
247
|
-
ungrouped.push(col)
|
|
248
|
-
}
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
const renderCollectionItem = (col: any) => {
|
|
252
|
-
const isReadOnly = col.access?.read && !col.access?.create && !col.access?.update && !col.access?.delete
|
|
253
|
-
const navLabel = (
|
|
254
|
-
<div className="dy-flex dy-items-center dy-gap-1.5 dy-min-w-0">
|
|
255
|
-
<span className="dy-truncate">{col.labels?.plural ?? col.label ?? col.slug}</span>
|
|
256
|
-
{!collapsed && (
|
|
257
|
-
<div className="dy-flex dy-gap-1 dy-shrink-0">
|
|
258
|
-
{col.auth && <Shield className="dy-h-4 dy-w-4 dy-text-primary/70" />}
|
|
259
|
-
{col.shared && <Share2 className="dy-h-4 dy-w-4 dy-text-purple-500/70" />}
|
|
260
|
-
{isReadOnly && <Lock className="dy-h-4 dy-w-4 dy-text-muted-foreground/40" />}
|
|
261
|
-
</div>
|
|
262
|
-
)}
|
|
263
|
-
</div>
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
return (
|
|
267
|
-
<NavItem
|
|
268
|
-
key={col.slug}
|
|
269
|
-
to={`/collections/${col.slug}`}
|
|
270
|
-
icon={col.auth ? Users : Database}
|
|
271
|
-
label={navLabel}
|
|
272
|
-
active={location.pathname.startsWith(`/collections/${col.slug}`)}
|
|
273
|
-
collapsed={collapsed}
|
|
274
|
-
onClick={onNavigate}
|
|
275
|
-
/>
|
|
276
|
-
)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return (
|
|
280
|
-
<div className="dy-space-y-1">
|
|
281
|
-
{/* Grouped sections */}
|
|
282
|
-
{Array.from(groups.entries()).map(([groupName, cols]) => (
|
|
283
|
-
<NavGroup key={groupName} label={groupName} collapsed={collapsed} defaultExpanded={true}>
|
|
284
|
-
{cols.map(col => renderCollectionItem(col))}
|
|
285
|
-
</NavGroup>
|
|
286
|
-
))}
|
|
287
|
-
|
|
288
|
-
{/* Ungrouped */}
|
|
289
|
-
{ungrouped.length > 0 && (
|
|
290
|
-
<NavGroup label="Collections" collapsed={collapsed} defaultExpanded={true}>
|
|
291
|
-
{ungrouped.map(col => renderCollectionItem(col))}
|
|
292
|
-
</NavGroup>
|
|
293
|
-
)}
|
|
294
|
-
</div>
|
|
295
|
-
)
|
|
296
|
-
})()}
|
|
297
|
-
</div>
|
|
298
|
-
)}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
{globals.length > 0 && (
|
|
302
|
-
<div>
|
|
303
|
-
{groupLabel("Configuration")}
|
|
304
|
-
<div className="dy-space-y-0.5">
|
|
305
|
-
{globals.map((glob: any) => (
|
|
306
|
-
<NavItem
|
|
307
|
-
key={glob.slug}
|
|
308
|
-
to={`/globals/${glob.slug}`}
|
|
309
|
-
icon={Settings}
|
|
310
|
-
label={glob.label ?? glob.slug}
|
|
311
|
-
active={location.pathname === `/globals/${glob.slug}`}
|
|
312
|
-
collapsed={collapsed}
|
|
313
|
-
onClick={onNavigate}
|
|
314
|
-
/>
|
|
315
|
-
))}
|
|
316
|
-
</div>
|
|
317
|
-
</div>
|
|
318
|
-
)}
|
|
319
|
-
</nav>
|
|
320
|
-
|
|
321
|
-
{/* Footer */}
|
|
322
|
-
<div className="dy-border-t dy-border-border dy-px-2 dy-py-3 dy-shrink-0 dy-space-y-0.5">
|
|
323
|
-
{/* Integration guide — always visible so embedded users can access the prompt */}
|
|
324
|
-
<NavItem
|
|
325
|
-
to="/setup"
|
|
326
|
-
icon={Sparkles}
|
|
327
|
-
label="Integration Guide"
|
|
328
|
-
active={location.pathname === "/setup"}
|
|
329
|
-
collapsed={collapsed}
|
|
330
|
-
onClick={onNavigate}
|
|
331
|
-
/>
|
|
332
|
-
|
|
333
|
-
{!isEmbedded && (
|
|
334
|
-
<button
|
|
335
|
-
onClick={logout}
|
|
336
|
-
title={collapsed ? "Logout" : undefined}
|
|
337
|
-
className={cn(
|
|
338
|
-
"dy-flex dy-w-full dy-items-center dy-gap-3 dy-rounded-md dy-px-3 dy-py-2 dy-text-[13px] dy-font-medium dy-text-muted-foreground dy-transition-colors hover:dy-bg-destructive/10 hover:dy-text-destructive",
|
|
339
|
-
collapsed ? "dy-justify-center dy-px-2" : ""
|
|
340
|
-
)}
|
|
341
|
-
>
|
|
342
|
-
<LogOut className="dy-h-[15px] dy-w-[15px] dy-shrink-0" />
|
|
343
|
-
{!collapsed && <span>Logout</span>}
|
|
344
|
-
</button>
|
|
345
|
-
)}
|
|
346
|
-
</div>
|
|
347
|
-
|
|
348
|
-
{/* Desktop Collapse Toggle at Bottom */}
|
|
349
|
-
{onToggleCollapse && !isEmbedded && (
|
|
350
|
-
<div className="dy-mt-auto dy-p-4 dy-border-t dy-border-border/40">
|
|
351
|
-
<button
|
|
352
|
-
onClick={onToggleCollapse}
|
|
353
|
-
className={cn(
|
|
354
|
-
"dy-w-full dy-flex dy-items-center dy-gap-3 dy-p-2.5 dy-rounded-xl dy-text-muted-foreground/60 hover:dy-text-foreground hover:dy-bg-accent/50 dy-transition-all dy-group/btn",
|
|
355
|
-
collapsed ? "dy-justify-center" : "dy-px-3"
|
|
356
|
-
)}
|
|
357
|
-
title={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
358
|
-
>
|
|
359
|
-
{collapsed ? (
|
|
360
|
-
<PanelLeftOpen className="dy-h-5 dy-w-5" />
|
|
361
|
-
) : (
|
|
362
|
-
<>
|
|
363
|
-
<PanelLeftClose className="dy-h-5 dy-w-5 dy-group-hover/btn:dy--translate-x-0.5 dy-transition-transform" />
|
|
364
|
-
<span className="dy-text-sm dy-font-medium dy-text-[13px]">Collapse Sidebar</span>
|
|
365
|
-
</>
|
|
366
|
-
)}
|
|
367
|
-
</button>
|
|
368
|
-
</div>
|
|
369
|
-
)}
|
|
370
|
-
</div>
|
|
371
|
-
)
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// ---------------------------------------------------------------------------
|
|
375
|
-
// AdminShell
|
|
376
|
-
// ---------------------------------------------------------------------------
|
|
377
|
-
export function AdminShell({
|
|
378
|
-
children,
|
|
379
|
-
isEmbedded = false,
|
|
380
|
-
}: {
|
|
381
|
-
children: React.ReactNode
|
|
382
|
-
isEmbedded?: boolean
|
|
383
|
-
}) {
|
|
384
|
-
const { client, logout } = useDyrected()
|
|
385
|
-
const location = useLocation()
|
|
386
|
-
|
|
387
|
-
// Desktop: collapsed state (sidebar still sits in the layout)
|
|
388
|
-
const [collapsed, setCollapsed] = useState(false)
|
|
389
|
-
// Mobile: open/close overlay
|
|
390
|
-
const [mobileOpen, setMobileOpen] = useState(false)
|
|
391
|
-
|
|
392
|
-
// Close mobile sidebar on navigation
|
|
393
|
-
useEffect(() => {
|
|
394
|
-
setMobileOpen(false)
|
|
395
|
-
}, [location.pathname])
|
|
396
|
-
|
|
397
|
-
// Lock scroll on mobile when open
|
|
398
|
-
useEffect(() => {
|
|
399
|
-
document.body.style.overflow = mobileOpen ? "hidden" : ""
|
|
400
|
-
return () => { document.body.style.overflow = "" }
|
|
401
|
-
}, [mobileOpen])
|
|
402
|
-
|
|
403
|
-
const { data: schemas, isLoading } = useQuery({
|
|
404
|
-
queryKey: ["schemas"],
|
|
405
|
-
queryFn: async () => {
|
|
406
|
-
if (!client) return null
|
|
407
|
-
return client.getSchemas()
|
|
408
|
-
},
|
|
409
|
-
enabled: !!client,
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
return (
|
|
413
|
-
<BrandingProvider>
|
|
414
|
-
<div className={cn("dy-flex dy-w-full dy-relative", isEmbedded ? "dy-h-full dy-min-h-[600px]" : "dy-min-h-screen")}>
|
|
415
|
-
{/* ... existing sidebar and main content ... */}
|
|
416
|
-
<aside
|
|
417
|
-
className={cn(
|
|
418
|
-
"dy-hidden md:dy-flex dy-flex-col dy-shrink-0 dy-h-full dy-border-r dy-border-border dy-bg-background dy-transition-all dy-duration-300 dy-overflow-hidden",
|
|
419
|
-
collapsed ? "dy-w-[56px]" : "dy-w-[220px]"
|
|
420
|
-
)}
|
|
421
|
-
>
|
|
422
|
-
<SidebarInner
|
|
423
|
-
schemas={schemas}
|
|
424
|
-
isLoading={isLoading}
|
|
425
|
-
location={location}
|
|
426
|
-
logout={logout}
|
|
427
|
-
isEmbedded={isEmbedded}
|
|
428
|
-
collapsed={collapsed}
|
|
429
|
-
onToggleCollapse={() => setCollapsed((v) => !v)}
|
|
430
|
-
/>
|
|
431
|
-
</aside>
|
|
432
|
-
|
|
433
|
-
{mobileOpen && (
|
|
434
|
-
<div
|
|
435
|
-
className="dy-fixed dy-inset-0 dy-z-30 dy-bg-black/30 md:dy-hidden"
|
|
436
|
-
onClick={() => setMobileOpen(false)}
|
|
437
|
-
/>
|
|
438
|
-
)}
|
|
439
|
-
<aside
|
|
440
|
-
className={cn(
|
|
441
|
-
"dy-fixed dy-top-0 dy-left-0 dy-z-40 dy-h-full dy-w-[220px] dy-flex dy-flex-col dy-border-r dy-border-border dy-bg-background dy-transition-transform dy-duration-300 dy-ease-in-out md:dy-hidden",
|
|
442
|
-
mobileOpen ? "dy-translate-x-0" : "dy--translate-x-full"
|
|
443
|
-
)}
|
|
444
|
-
>
|
|
445
|
-
<button
|
|
446
|
-
onClick={() => setMobileOpen(false)}
|
|
447
|
-
className="dy-absolute dy-top-3.5 dy-right-3 dy-p-1.5 dy-rounded-md dy-text-muted-foreground hover:dy-bg-muted dy-transition-colors"
|
|
448
|
-
>
|
|
449
|
-
<X className="dy-h-4 dy-w-4" />
|
|
450
|
-
</button>
|
|
451
|
-
<SidebarInner
|
|
452
|
-
schemas={schemas}
|
|
453
|
-
isLoading={isLoading}
|
|
454
|
-
location={location}
|
|
455
|
-
logout={logout}
|
|
456
|
-
isEmbedded={isEmbedded}
|
|
457
|
-
collapsed={false}
|
|
458
|
-
onNavigate={() => setMobileOpen(false)}
|
|
459
|
-
/>
|
|
460
|
-
</aside>
|
|
461
|
-
|
|
462
|
-
<main className="dy-flex-1 dy-min-w-0 dy-overflow-auto dy-flex dy-flex-col dy-relative dy-bg-background/50">
|
|
463
|
-
{/* Mobile Floating Toggle */}
|
|
464
|
-
<button
|
|
465
|
-
onClick={() => setMobileOpen(true)}
|
|
466
|
-
className="md:dy-hidden dy-fixed dy-bottom-8 dy-right-8 dy-z-50 dy-h-14 dy-w-14 dy-rounded-full dy-bg-gradient-to-br dy-from-primary dy-to-primary/80 dy-text-primary-foreground dy-shadow-[0_8px_30px_rgb(0,0,0,0.3)] dy-flex dy-items-center dy-justify-center dy-transition-all active:dy-scale-90 hover:dy-scale-105 dy-border dy-border-white/20"
|
|
467
|
-
aria-label="Open menu"
|
|
468
|
-
>
|
|
469
|
-
<Menu className="dy-h-6 dy-w-6" />
|
|
470
|
-
</button>
|
|
471
|
-
|
|
472
|
-
<div className="dy-flex-1 dy-py-6 dy-px-4 lg:dy-py-10 lg:dy-px-6">
|
|
473
|
-
{children}
|
|
474
|
-
</div>
|
|
475
|
-
</main>
|
|
476
|
-
</div>
|
|
477
|
-
</BrandingProvider>
|
|
478
|
-
)
|
|
479
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import React, { useMemo } from "react";
|
|
2
|
-
import { useDyrected } from "../../providers/dyrected-provider";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Converts a color value (hex, named, or raw HSL string) to the raw HSL
|
|
6
|
-
* triplet expected by our CSS variables, e.g. "38 92% 50%".
|
|
7
|
-
*/
|
|
8
|
-
function toRawHsl(color: string): string {
|
|
9
|
-
if (!color) return "38 92% 50%"; // Default amber
|
|
10
|
-
|
|
11
|
-
if (color.startsWith("#")) {
|
|
12
|
-
let r = 0, g = 0, b = 0;
|
|
13
|
-
if (color.length === 4) {
|
|
14
|
-
r = parseInt(color[1] + color[1], 16);
|
|
15
|
-
g = parseInt(color[2] + color[2], 16);
|
|
16
|
-
b = parseInt(color[3] + color[3], 16);
|
|
17
|
-
} else if (color.length === 7) {
|
|
18
|
-
r = parseInt(color.substring(1, 3), 16);
|
|
19
|
-
g = parseInt(color.substring(3, 5), 16);
|
|
20
|
-
b = parseInt(color.substring(5, 7), 16);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
r /= 255; g /= 255; b /= 255;
|
|
24
|
-
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
25
|
-
let h = 0, s = 0;
|
|
26
|
-
const l = (max + min) / 2;
|
|
27
|
-
|
|
28
|
-
if (max !== min) {
|
|
29
|
-
const d = max - min;
|
|
30
|
-
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
31
|
-
switch (max) {
|
|
32
|
-
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
|
33
|
-
case g: h = (b - r) / d + 2; break;
|
|
34
|
-
case b: h = (r - g) / d + 4; break;
|
|
35
|
-
}
|
|
36
|
-
h /= 6;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const named: Record<string, string> = {
|
|
43
|
-
amber: "38 92% 50%",
|
|
44
|
-
green: "142 76% 36%",
|
|
45
|
-
blue: "217 91% 60%",
|
|
46
|
-
red: "0 84% 60%",
|
|
47
|
-
purple: "262 83% 58%",
|
|
48
|
-
orange: "24 95% 53%",
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
return named[color.toLowerCase()] || color;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Determines the best foreground color for text rendered on top of a given
|
|
56
|
-
* primary background. Warm, saturated colours (yellows, ambers) are
|
|
57
|
-
* perceptually bright even at moderate lightness and need dark text.
|
|
58
|
-
*/
|
|
59
|
-
function primaryForeground(hsl: string): string {
|
|
60
|
-
const parts = hsl.split(" ");
|
|
61
|
-
if (parts.length < 3) return "60 3% 6%";
|
|
62
|
-
|
|
63
|
-
const hue = parseFloat(parts[0]);
|
|
64
|
-
const saturation = parseFloat(parts[1]);
|
|
65
|
-
const lightness = parseFloat(parts[2]);
|
|
66
|
-
|
|
67
|
-
// Warm saturated colours (yellow → lime, hue 20–150°) at moderate lightness
|
|
68
|
-
// are visually bright and need the dark near-black foreground.
|
|
69
|
-
const isWarmBright =
|
|
70
|
-
saturation > 50 &&
|
|
71
|
-
lightness > 38 &&
|
|
72
|
-
hue >= 20 &&
|
|
73
|
-
hue <= 150;
|
|
74
|
-
|
|
75
|
-
if (isWarmBright) return "60 3% 6%"; // warm near-black
|
|
76
|
-
|
|
77
|
-
// Cool dark colours need white text; very pale tints need dark text.
|
|
78
|
-
return lightness < 55 ? "0 0% 100%" : "60 3% 6%";
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function BrandingProvider({ children }: { children: React.ReactNode }) {
|
|
82
|
-
const { schemas } = useDyrected();
|
|
83
|
-
const branding = schemas?.admin?.branding;
|
|
84
|
-
|
|
85
|
-
const styleTag = useMemo(() => {
|
|
86
|
-
const hsl = toRawHsl(branding?.primaryColor || "38 92% 50%");
|
|
87
|
-
const fg = primaryForeground(hsl);
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<style dangerouslySetInnerHTML={{ __html: `
|
|
91
|
-
.admin-ui {
|
|
92
|
-
--primary: ${hsl};
|
|
93
|
-
--primary-foreground: ${fg};
|
|
94
|
-
--sidebar-primary: ${hsl};
|
|
95
|
-
--sidebar-primary-foreground: ${fg};
|
|
96
|
-
--sidebar-accent-foreground: ${hsl};
|
|
97
|
-
--sidebar-ring: ${hsl};
|
|
98
|
-
--ring: ${hsl} / 0.15;
|
|
99
|
-
${branding?.fontSans ? `--font-sans: ${branding.fontSans};` : ""}
|
|
100
|
-
${branding?.fontSerif ? `--font-serif: ${branding.fontSerif};` : ""}
|
|
101
|
-
}
|
|
102
|
-
`}} />
|
|
103
|
-
);
|
|
104
|
-
}, [branding?.primaryColor, branding?.fontSans, branding?.fontSerif]);
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<>
|
|
108
|
-
{styleTag}
|
|
109
|
-
{children}
|
|
110
|
-
</>
|
|
111
|
-
);
|
|
112
|
-
}
|