@dyrected/admin 2.4.0 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/dist/App.d.ts +1 -0
  2. package/dist/admin.css +2 -0
  3. package/dist/components/auth/auth-gate.d.ts +13 -0
  4. package/dist/components/error-boundary.d.ts +16 -0
  5. package/dist/components/forms/field-renderer.d.ts +22 -0
  6. package/dist/components/forms/fields/block-builder.d.ts +9 -0
  7. package/dist/components/forms/fields/date-picker.d.ts +8 -0
  8. package/dist/components/forms/fields/json-editor.d.ts +8 -0
  9. package/dist/components/forms/fields/media-picker.d.ts +12 -0
  10. package/dist/components/forms/fields/multi-select.d.ts +19 -0
  11. package/dist/components/forms/fields/radio-field.d.ts +8 -0
  12. package/dist/components/forms/fields/relationship-picker.d.ts +10 -0
  13. package/dist/components/forms/fields/rich-text-editor.d.ts +9 -0
  14. package/dist/components/forms/fields/select-field.d.ts +8 -0
  15. package/dist/components/forms/fields/switch-field.d.ts +6 -0
  16. package/dist/components/forms/fields/text-area-field.d.ts +8 -0
  17. package/dist/components/forms/fields/text-field.d.ts +8 -0
  18. package/dist/components/forms/form-engine.d.ts +14 -0
  19. package/dist/components/forms/form-field-renderer.d.ts +20 -0
  20. package/dist/components/forms/utils.d.ts +11 -0
  21. package/dist/components/layout/admin-shell.d.ts +5 -0
  22. package/dist/components/layout/branding-provider.d.ts +4 -0
  23. package/dist/components/live-preview/LivePreviewPane.d.ts +7 -0
  24. package/dist/components/media/focal-point-picker.d.ts +12 -0
  25. package/dist/components/media/media-card.d.ts +8 -0
  26. package/dist/components/media/media-grid.d.ts +8 -0
  27. package/dist/components/media/media-library-dialog.d.ts +11 -0
  28. package/dist/components/ui/aspect-ratio.d.ts +3 -0
  29. package/dist/components/ui/badge.d.ts +9 -0
  30. package/dist/components/ui/button.d.ts +11 -0
  31. package/dist/components/ui/calendar.d.ts +8 -0
  32. package/dist/components/ui/card.d.ts +8 -0
  33. package/dist/components/ui/checkbox.d.ts +4 -0
  34. package/dist/components/ui/command.d.ts +80 -0
  35. package/dist/components/ui/data-table.d.ts +14 -0
  36. package/dist/components/ui/dialog.d.ts +19 -0
  37. package/dist/components/ui/dropdown-menu.d.ts +27 -0
  38. package/dist/components/ui/form.d.ts +23 -0
  39. package/dist/components/ui/input.d.ts +3 -0
  40. package/dist/components/ui/label.d.ts +5 -0
  41. package/dist/components/ui/page-header.d.ts +10 -0
  42. package/dist/components/ui/pagination.d.ts +11 -0
  43. package/dist/components/ui/popover.d.ts +6 -0
  44. package/dist/components/ui/progress.d.ts +4 -0
  45. package/dist/components/ui/radio-group.d.ts +5 -0
  46. package/dist/components/ui/render-cell.d.ts +8 -0
  47. package/dist/components/ui/scroll-area.d.ts +5 -0
  48. package/dist/components/ui/select.d.ts +13 -0
  49. package/dist/components/ui/separator.d.ts +4 -0
  50. package/dist/components/ui/sheet.d.ts +25 -0
  51. package/dist/components/ui/sidebar.d.ts +65 -0
  52. package/dist/components/ui/skeleton.d.ts +2 -0
  53. package/dist/components/ui/sonner.d.ts +4 -0
  54. package/dist/components/ui/switch.d.ts +4 -0
  55. package/dist/components/ui/table.d.ts +10 -0
  56. package/dist/components/ui/tabs.d.ts +7 -0
  57. package/dist/components/ui/textarea.d.ts +3 -0
  58. package/dist/components/ui/toggle.d.ts +12 -0
  59. package/dist/components/ui/tooltip.d.ts +7 -0
  60. package/dist/hooks/use-mobile.d.ts +1 -0
  61. package/dist/hooks/use-preferences.d.ts +6 -0
  62. package/dist/index.d.ts +38 -0
  63. package/dist/index.mjs +69091 -0
  64. package/dist/lib/utils.d.ts +3 -0
  65. package/dist/main.d.ts +0 -0
  66. package/dist/pages/auth/first-user-page.d.ts +4 -0
  67. package/dist/pages/auth/login-page.d.ts +4 -0
  68. package/dist/pages/collections/edit-page.d.ts +1 -0
  69. package/dist/pages/collections/list-page.d.ts +5 -0
  70. package/dist/pages/dashboard/dashboard.d.ts +1 -0
  71. package/dist/pages/globals/editor-page.d.ts +1 -0
  72. package/dist/pages/media/media-page.d.ts +4 -0
  73. package/dist/pages/setup/setup-prompt.d.ts +6 -0
  74. package/dist/providers/dyrected-provider.d.ts +29 -0
  75. package/dist/providers/query-provider.d.ts +3 -0
  76. package/package.json +6 -3
  77. package/CHANGELOG.md +0 -153
  78. package/components.json +0 -17
  79. package/eslint.config.js +0 -22
  80. package/index.html +0 -13
  81. package/postcss.config.js +0 -6
  82. package/scripts/prefix-tailwind-precision.py +0 -98
  83. package/scripts/prefix-tailwind.py +0 -67
  84. package/src/App.css +0 -184
  85. package/src/App.tsx +0 -25
  86. package/src/assets/dyrected.svg +0 -155
  87. package/src/assets/hero.png +0 -0
  88. package/src/assets/react.svg +0 -1
  89. package/src/assets/vite.svg +0 -1
  90. package/src/components/auth/auth-gate.tsx +0 -64
  91. package/src/components/error-boundary.tsx +0 -45
  92. package/src/components/forms/field-renderer.tsx +0 -111
  93. package/src/components/forms/fields/block-builder.tsx +0 -213
  94. package/src/components/forms/fields/date-picker.tsx +0 -60
  95. package/src/components/forms/fields/json-editor.tsx +0 -62
  96. package/src/components/forms/fields/media-picker.tsx +0 -286
  97. package/src/components/forms/fields/multi-select.tsx +0 -145
  98. package/src/components/forms/fields/radio-field.tsx +0 -51
  99. package/src/components/forms/fields/relationship-picker.tsx +0 -143
  100. package/src/components/forms/fields/rich-text-editor.tsx +0 -224
  101. package/src/components/forms/fields/select-field.tsx +0 -35
  102. package/src/components/forms/fields/switch-field.tsx +0 -16
  103. package/src/components/forms/fields/text-area-field.tsx +0 -15
  104. package/src/components/forms/fields/text-field.tsx +0 -24
  105. package/src/components/forms/form-engine.tsx +0 -87
  106. package/src/components/forms/form-field-renderer.tsx +0 -269
  107. package/src/components/forms/utils.ts +0 -97
  108. package/src/components/layout/admin-shell.tsx +0 -479
  109. package/src/components/layout/branding-provider.tsx +0 -112
  110. package/src/components/live-preview/LivePreviewPane.tsx +0 -128
  111. package/src/components/media/focal-point-picker.tsx +0 -66
  112. package/src/components/media/media-card.tsx +0 -44
  113. package/src/components/media/media-grid.tsx +0 -32
  114. package/src/components/media/media-library-dialog.tsx +0 -465
  115. package/src/components/ui/aspect-ratio.tsx +0 -7
  116. package/src/components/ui/badge.tsx +0 -36
  117. package/src/components/ui/button.tsx +0 -56
  118. package/src/components/ui/calendar.tsx +0 -214
  119. package/src/components/ui/card.tsx +0 -79
  120. package/src/components/ui/checkbox.tsx +0 -28
  121. package/src/components/ui/command.tsx +0 -151
  122. package/src/components/ui/data-table.tsx +0 -219
  123. package/src/components/ui/dialog.tsx +0 -122
  124. package/src/components/ui/dropdown-menu.tsx +0 -200
  125. package/src/components/ui/form.tsx +0 -178
  126. package/src/components/ui/input.tsx +0 -24
  127. package/src/components/ui/label.tsx +0 -24
  128. package/src/components/ui/page-header.tsx +0 -30
  129. package/src/components/ui/pagination.tsx +0 -57
  130. package/src/components/ui/popover.tsx +0 -29
  131. package/src/components/ui/progress.tsx +0 -26
  132. package/src/components/ui/radio-group.tsx +0 -42
  133. package/src/components/ui/render-cell.tsx +0 -110
  134. package/src/components/ui/scroll-area.tsx +0 -46
  135. package/src/components/ui/select.tsx +0 -160
  136. package/src/components/ui/separator.tsx +0 -29
  137. package/src/components/ui/sheet.tsx +0 -140
  138. package/src/components/ui/sidebar.tsx +0 -771
  139. package/src/components/ui/skeleton.tsx +0 -15
  140. package/src/components/ui/sonner.tsx +0 -27
  141. package/src/components/ui/switch.tsx +0 -27
  142. package/src/components/ui/table.tsx +0 -117
  143. package/src/components/ui/tabs.tsx +0 -53
  144. package/src/components/ui/textarea.tsx +0 -22
  145. package/src/components/ui/toggle.tsx +0 -43
  146. package/src/components/ui/tooltip.tsx +0 -28
  147. package/src/hooks/use-mobile.tsx +0 -19
  148. package/src/hooks/use-preferences.ts +0 -56
  149. package/src/index.css +0 -111
  150. package/src/index.tsx +0 -198
  151. package/src/lib/utils.ts +0 -36
  152. package/src/main.tsx +0 -10
  153. package/src/pages/auth/first-user-page.tsx +0 -115
  154. package/src/pages/auth/login-page.tsx +0 -91
  155. package/src/pages/collections/edit-page.tsx +0 -280
  156. package/src/pages/collections/list-page.tsx +0 -343
  157. package/src/pages/dashboard/dashboard.tsx +0 -150
  158. package/src/pages/globals/editor-page.tsx +0 -122
  159. package/src/pages/media/media-page.tsx +0 -564
  160. package/src/pages/setup/setup-prompt.tsx +0 -181
  161. package/src/providers/dyrected-provider.tsx +0 -122
  162. package/src/providers/query-provider.tsx +0 -19
  163. package/src/types/jexl.d.ts +0 -11
  164. package/tailwind.config.ts +0 -103
  165. package/tsconfig.app.json +0 -28
  166. package/tsconfig.json +0 -12
  167. package/tsconfig.node.json +0 -25
  168. package/vite.config.ts +0 -39
  169. /package/{public → dist}/favicon.svg +0 -0
  170. /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
- }