@actuate-media/cms-admin 0.14.1 → 0.15.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/actuate-admin.css +1 -1
- package/dist/layout/Header.d.ts +14 -1
- package/dist/layout/Header.d.ts.map +1 -1
- package/dist/layout/Header.js +10 -3
- package/dist/layout/Header.js.map +1 -1
- package/dist/layout/Layout.d.ts.map +1 -1
- package/dist/layout/Layout.js +50 -2
- package/dist/layout/Layout.js.map +1 -1
- package/dist/layout/Sidebar.d.ts +7 -0
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +121 -46
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +136 -62
- package/dist/views/Dashboard.js.map +1 -1
- package/package.json +2 -2
- package/src/layout/Header.tsx +90 -43
- package/src/layout/Layout.tsx +110 -1
- package/src/layout/Sidebar.tsx +218 -98
- package/src/views/Dashboard.tsx +258 -131
package/src/layout/Sidebar.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
3
4
|
import {
|
|
4
5
|
LayoutDashboard,
|
|
5
6
|
FileText,
|
|
@@ -7,7 +8,6 @@ import {
|
|
|
7
8
|
Image,
|
|
8
9
|
Settings,
|
|
9
10
|
ChevronLeft,
|
|
10
|
-
ChevronRight,
|
|
11
11
|
ClipboardList,
|
|
12
12
|
Users,
|
|
13
13
|
Search as SearchIcon,
|
|
@@ -20,9 +20,10 @@ import {
|
|
|
20
20
|
PanelBottom,
|
|
21
21
|
Layers,
|
|
22
22
|
Code2,
|
|
23
|
-
LayoutTemplate,
|
|
24
23
|
Library,
|
|
25
24
|
KeyRound,
|
|
25
|
+
PanelLeftClose,
|
|
26
|
+
PanelLeftOpen,
|
|
26
27
|
} from 'lucide-react'
|
|
27
28
|
import type { LucideIcon } from 'lucide-react'
|
|
28
29
|
import { ActuateBrandLogo } from '../assets/actuate-logo.js'
|
|
@@ -48,11 +49,6 @@ function ActuateMark({ className }: { className?: string }) {
|
|
|
48
49
|
)
|
|
49
50
|
}
|
|
50
51
|
|
|
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
|
-
*/
|
|
56
52
|
function ActuateWordmark({ className }: { className?: string }) {
|
|
57
53
|
return <ActuateBrandLogo className={className} />
|
|
58
54
|
}
|
|
@@ -91,9 +87,7 @@ function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean })
|
|
|
91
87
|
className="h-8 w-auto shrink-0 object-contain"
|
|
92
88
|
/>
|
|
93
89
|
{brandName && (
|
|
94
|
-
<span className="text-sidebar-foreground truncate text-sm font-
|
|
95
|
-
{brandName}
|
|
96
|
-
</span>
|
|
90
|
+
<span className="text-sidebar-foreground truncate text-sm font-medium">{brandName}</span>
|
|
97
91
|
)}
|
|
98
92
|
</div>
|
|
99
93
|
)
|
|
@@ -103,7 +97,7 @@ function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean })
|
|
|
103
97
|
return (
|
|
104
98
|
<div className="flex min-w-0 items-center gap-2.5">
|
|
105
99
|
<ActuateMark className="h-7 w-7 shrink-0" />
|
|
106
|
-
<span className="text-sidebar-foreground truncate text-sm font-
|
|
100
|
+
<span className="text-sidebar-foreground truncate text-sm font-medium">{brandName}</span>
|
|
107
101
|
</div>
|
|
108
102
|
)
|
|
109
103
|
}
|
|
@@ -111,16 +105,33 @@ function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean })
|
|
|
111
105
|
return <ActuateWordmark className="h-9" />
|
|
112
106
|
}
|
|
113
107
|
|
|
114
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Nav tree for the default (config-less) install. Mirrors the reference
|
|
110
|
+
* dashboard exactly: Dashboard / Posts / Pages (with Saved Sections +
|
|
111
|
+
* Templates as nested children), then a CONTENT group (Media / Forms /
|
|
112
|
+
* SEO / Script Tags), then a SETTINGS group (Users / API Keys /
|
|
113
|
+
* Settings). The group ordering is meaningful — the renderer emits a
|
|
114
|
+
* section header the first time it sees a new group.
|
|
115
|
+
*/
|
|
116
|
+
const defaultNavItems: NavItem[] = [
|
|
115
117
|
{ path: '/', label: 'Dashboard', icon: LayoutDashboard },
|
|
116
|
-
{ path: '/pages', label: 'Pages', icon: File },
|
|
117
118
|
{ path: '/posts', label: 'Posts', icon: FileText },
|
|
118
|
-
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
{
|
|
120
|
+
path: '/pages',
|
|
121
|
+
label: 'Pages',
|
|
122
|
+
icon: File,
|
|
123
|
+
children: [
|
|
124
|
+
{ path: '/saved-sections', label: 'Saved Sections', icon: Library },
|
|
125
|
+
{ path: '/page-templates', label: 'Templates', icon: Layers },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
{ path: '/media', label: 'Media', icon: Image, group: 'Content' },
|
|
129
|
+
{ path: '/forms', label: 'Forms', icon: ClipboardList, group: 'Content' },
|
|
130
|
+
{ path: '/seo', label: 'SEO', icon: SearchIcon, group: 'Content' },
|
|
131
|
+
{ path: '/script-tags', label: 'Script Tags', icon: Code2, group: 'Content' },
|
|
132
|
+
{ path: '/users', label: 'Users', icon: Users, group: 'Settings' },
|
|
133
|
+
{ path: '/api-keys', label: 'API Keys', icon: KeyRound, group: 'Settings' },
|
|
134
|
+
{ path: '/settings', label: 'Settings', icon: Settings, group: 'Settings' },
|
|
124
135
|
]
|
|
125
136
|
|
|
126
137
|
export interface SidebarProps {
|
|
@@ -142,73 +153,140 @@ export function Sidebar({
|
|
|
142
153
|
|
|
143
154
|
return (
|
|
144
155
|
<aside
|
|
145
|
-
className={`bg-sidebar border-sidebar-border h-full border-r transition-all duration-200 ${
|
|
156
|
+
className={`bg-sidebar border-sidebar-border flex h-full flex-col border-r transition-all duration-200 ${
|
|
146
157
|
collapsed ? 'w-20' : 'w-64'
|
|
147
158
|
}`}
|
|
148
159
|
>
|
|
160
|
+
{/* Brand header — no toggle button here anymore; it now lives in the footer. */}
|
|
149
161
|
<div
|
|
150
|
-
className={`border-sidebar-border flex h-14 items-center border-b px-4 ${
|
|
151
|
-
collapsed ? 'justify-center' : '
|
|
162
|
+
className={`border-sidebar-border flex h-14 shrink-0 items-center border-b px-4 ${
|
|
163
|
+
collapsed ? 'justify-center' : ''
|
|
152
164
|
}`}
|
|
153
165
|
>
|
|
154
|
-
|
|
155
|
-
|
|
166
|
+
<BrandLogo config={config} collapsed={collapsed} />
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<nav className="flex-1 space-y-1 overflow-y-auto p-3" aria-label="Primary">
|
|
170
|
+
{renderNavTree(navItems, { collapsed, currentPath, onNavigate })}
|
|
171
|
+
</nav>
|
|
172
|
+
|
|
173
|
+
{/* Footer collapse control. Sits at the bottom of the sidebar so it
|
|
174
|
+
mirrors the reference design and stays out of the way during
|
|
175
|
+
everyday nav. Hidden below md: the mobile sheet has its own
|
|
176
|
+
dismiss affordance in <AdminShell>. */}
|
|
177
|
+
<div className="border-sidebar-border hidden shrink-0 border-t p-3 md:block">
|
|
156
178
|
<button
|
|
157
179
|
onClick={onToggleCollapse}
|
|
158
|
-
className=
|
|
180
|
+
className={`text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-sm transition-colors ${
|
|
181
|
+
collapsed ? 'justify-center' : ''
|
|
182
|
+
}`}
|
|
159
183
|
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
184
|
+
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
160
185
|
>
|
|
161
186
|
{collapsed ? (
|
|
162
|
-
<
|
|
187
|
+
<PanelLeftOpen className="h-4 w-4 shrink-0" aria-hidden />
|
|
163
188
|
) : (
|
|
164
|
-
|
|
189
|
+
<>
|
|
190
|
+
<PanelLeftClose className="h-4 w-4 shrink-0" aria-hidden />
|
|
191
|
+
<span className="font-medium">Collapse sidebar</span>
|
|
192
|
+
</>
|
|
165
193
|
)}
|
|
166
194
|
</button>
|
|
167
195
|
</div>
|
|
168
|
-
|
|
169
|
-
<nav className="space-y-1 p-3">
|
|
170
|
-
{navItems.map((item, idx) => {
|
|
171
|
-
const Icon = item.icon
|
|
172
|
-
const isActive =
|
|
173
|
-
currentPath === item.path || (item.path !== '/' && currentPath.startsWith(item.path))
|
|
174
|
-
|
|
175
|
-
const prevGroup = idx > 0 ? navItems[idx - 1]?.group : undefined
|
|
176
|
-
const showGroupLabel = item.group && item.group !== prevGroup
|
|
177
|
-
|
|
178
|
-
return (
|
|
179
|
-
<div key={item.path}>
|
|
180
|
-
{showGroupLabel && !collapsed && (
|
|
181
|
-
<div className="px-3 pt-3 pb-1">
|
|
182
|
-
<span className="text-sidebar-foreground/50 text-[10px] font-semibold tracking-wider uppercase">
|
|
183
|
-
{item.group}
|
|
184
|
-
</span>
|
|
185
|
-
</div>
|
|
186
|
-
)}
|
|
187
|
-
{showGroupLabel && collapsed && (
|
|
188
|
-
<div className="flex justify-center pt-2 pb-1">
|
|
189
|
-
<span className="border-sidebar-foreground/20 w-4 border-t" />
|
|
190
|
-
</div>
|
|
191
|
-
)}
|
|
192
|
-
<button
|
|
193
|
-
onClick={() => onNavigate(item.path)}
|
|
194
|
-
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left transition-colors ${
|
|
195
|
-
isActive
|
|
196
|
-
? 'bg-sidebar-accent text-sidebar-primary'
|
|
197
|
-
: 'text-sidebar-foreground hover:bg-sidebar-accent'
|
|
198
|
-
} ${collapsed ? 'justify-center' : ''}`}
|
|
199
|
-
title={collapsed ? item.label : ''}
|
|
200
|
-
>
|
|
201
|
-
<Icon className="h-5 w-5 shrink-0" />
|
|
202
|
-
{!collapsed && <span className="text-sm font-medium">{item.label}</span>}
|
|
203
|
-
</button>
|
|
204
|
-
</div>
|
|
205
|
-
)
|
|
206
|
-
})}
|
|
207
|
-
</nav>
|
|
208
196
|
</aside>
|
|
209
197
|
)
|
|
210
198
|
}
|
|
211
199
|
|
|
200
|
+
// ─── Rendering ──────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
interface NavRenderContext {
|
|
203
|
+
collapsed: boolean
|
|
204
|
+
currentPath: string
|
|
205
|
+
onNavigate: (path: string) => void
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isPathActive(currentPath: string, itemPath: string): boolean {
|
|
209
|
+
if (itemPath === '/') return currentPath === '/'
|
|
210
|
+
// Use boundary match so /pages doesn't match /pages-something. Exact, or
|
|
211
|
+
// followed by a `/` separator.
|
|
212
|
+
return currentPath === itemPath || currentPath.startsWith(itemPath + '/')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function renderNavTree(items: NavItem[], ctx: NavRenderContext): ReactNode {
|
|
216
|
+
const nodes: ReactNode[] = []
|
|
217
|
+
let lastGroup: string | undefined = undefined
|
|
218
|
+
|
|
219
|
+
for (const item of items) {
|
|
220
|
+
if (item.group && item.group !== lastGroup) {
|
|
221
|
+
nodes.push(
|
|
222
|
+
ctx.collapsed ? (
|
|
223
|
+
<div key={`g-${item.group}`} className="flex justify-center pt-3 pb-1">
|
|
224
|
+
<span className="border-sidebar-foreground/20 w-4 border-t" aria-hidden />
|
|
225
|
+
</div>
|
|
226
|
+
) : (
|
|
227
|
+
<div key={`g-${item.group}`} className="px-3 pt-4 pb-1">
|
|
228
|
+
<span className="text-sidebar-foreground/55 text-[10px] font-semibold tracking-wider uppercase">
|
|
229
|
+
{item.group}
|
|
230
|
+
</span>
|
|
231
|
+
</div>
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
lastGroup = item.group
|
|
235
|
+
} else if (!item.group) {
|
|
236
|
+
lastGroup = undefined
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
nodes.push(<NavLink key={item.path} item={item} ctx={ctx} />)
|
|
240
|
+
if (item.children && item.children.length > 0 && !ctx.collapsed) {
|
|
241
|
+
for (const child of item.children) {
|
|
242
|
+
nodes.push(<NavLink key={child.path} item={child} ctx={ctx} nested />)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return nodes
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function NavLink({
|
|
251
|
+
item,
|
|
252
|
+
ctx,
|
|
253
|
+
nested = false,
|
|
254
|
+
}: {
|
|
255
|
+
item: NavItem
|
|
256
|
+
ctx: NavRenderContext
|
|
257
|
+
nested?: boolean
|
|
258
|
+
}) {
|
|
259
|
+
const Icon = item.icon
|
|
260
|
+
const isActive = isPathActive(ctx.currentPath, item.path)
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<button
|
|
264
|
+
onClick={() => ctx.onNavigate(item.path)}
|
|
265
|
+
className={`flex w-full items-center gap-3 rounded-lg text-left transition-colors ${
|
|
266
|
+
nested ? 'py-1.5 pr-3 pl-9' : 'px-3 py-2.5'
|
|
267
|
+
} ${
|
|
268
|
+
isActive
|
|
269
|
+
? 'bg-sidebar-accent text-sidebar-primary'
|
|
270
|
+
: 'text-sidebar-foreground hover:bg-sidebar-accent'
|
|
271
|
+
} ${ctx.collapsed ? 'justify-center px-3' : ''}`}
|
|
272
|
+
title={ctx.collapsed ? item.label : ''}
|
|
273
|
+
aria-current={isActive ? 'page' : undefined}
|
|
274
|
+
>
|
|
275
|
+
{!nested && <Icon className="h-5 w-5 shrink-0" aria-hidden />}
|
|
276
|
+
{nested && !ctx.collapsed && (
|
|
277
|
+
<Icon className="text-sidebar-foreground/60 h-4 w-4 shrink-0" aria-hidden />
|
|
278
|
+
)}
|
|
279
|
+
{!ctx.collapsed && (
|
|
280
|
+
<span className={`truncate font-medium ${nested ? 'text-[13px]' : 'text-sm'}`}>
|
|
281
|
+
{item.label}
|
|
282
|
+
</span>
|
|
283
|
+
)}
|
|
284
|
+
</button>
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ─── Config-driven nav builder ──────────────────────────────────────────────
|
|
289
|
+
|
|
212
290
|
function resolveIcon(collection: any): LucideIcon {
|
|
213
291
|
const mapped = collection.admin?.icon ? ICON_MAP[collection.admin.icon] : undefined
|
|
214
292
|
if (mapped) return mapped
|
|
@@ -219,28 +297,42 @@ export interface NavItem {
|
|
|
219
297
|
path: string
|
|
220
298
|
label: string
|
|
221
299
|
icon: LucideIcon
|
|
300
|
+
/**
|
|
301
|
+
* Section header. Items sharing the same `group` are grouped together
|
|
302
|
+
* under a header rendered on first encounter. Items without a `group`
|
|
303
|
+
* render at the top of the nav in document order.
|
|
304
|
+
*/
|
|
222
305
|
group?: string
|
|
306
|
+
/** Indented children rendered immediately after this item. */
|
|
307
|
+
children?: NavItem[]
|
|
223
308
|
}
|
|
224
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Build the sidebar nav tree from the CMS config. If no collections are
|
|
312
|
+
* configured, returns the curated `defaultNavItems` tree. When
|
|
313
|
+
* collections ARE configured, page-type collections inherit the
|
|
314
|
+
* Pages / Saved Sections / Templates child block, and every static
|
|
315
|
+
* destination (Media, Forms, SEO, Script Tags, Users, API Keys,
|
|
316
|
+
* Settings) is anchored under the right Content / Settings group.
|
|
317
|
+
*/
|
|
225
318
|
function buildNavItems(config: any): NavItem[] {
|
|
226
319
|
if (!config?.collections) return defaultNavItems
|
|
227
320
|
|
|
228
321
|
const raw = config.collections
|
|
229
322
|
const collectionsList: any[] = Array.isArray(raw) ? raw : Object.values(raw)
|
|
230
|
-
|
|
231
323
|
const visible = collectionsList.filter((c) => !c.admin?.hidden)
|
|
232
324
|
|
|
233
|
-
const
|
|
325
|
+
const pages = visible.filter((c) => c.type === 'page' && !c.admin?.group)
|
|
326
|
+
const posts = visible.filter((c) => c.type === 'post' && !c.admin?.group)
|
|
327
|
+
const otherUngrouped = visible.filter(
|
|
328
|
+
(c) => c.type !== 'page' && c.type !== 'post' && !c.admin?.group,
|
|
329
|
+
)
|
|
234
330
|
const grouped = visible.filter((c) => c.admin?.group)
|
|
235
331
|
|
|
236
|
-
const pages = ungrouped.filter((c) => c.type === 'page')
|
|
237
|
-
const posts = ungrouped.filter((c) => c.type === 'post')
|
|
238
|
-
const other = ungrouped.filter((c) => c.type !== 'page' && c.type !== 'post')
|
|
239
|
-
const sortedUngrouped = [...pages, ...posts, ...other]
|
|
240
|
-
|
|
241
332
|
const items: NavItem[] = [{ path: '/', label: 'Dashboard', icon: LayoutDashboard }]
|
|
242
333
|
|
|
243
|
-
|
|
334
|
+
// Posts-style collections come first (mirrors the reference layout).
|
|
335
|
+
for (const collection of posts) {
|
|
244
336
|
items.push({
|
|
245
337
|
label: collection.labels?.plural ?? collection.slug,
|
|
246
338
|
path: `/${collection.slug}`,
|
|
@@ -248,34 +340,62 @@ function buildNavItems(config: any): NavItem[] {
|
|
|
248
340
|
})
|
|
249
341
|
}
|
|
250
342
|
|
|
251
|
-
|
|
343
|
+
// Pages-style collections get the nested Saved Sections + Templates
|
|
344
|
+
// tree. We attach the children to the *first* page-type collection
|
|
345
|
+
// so we don't end up with three "Saved Sections" links if a project
|
|
346
|
+
// configures multiple page collections — admins editing custom
|
|
347
|
+
// page-style content still navigate to those secondary surfaces via
|
|
348
|
+
// the primary Pages parent.
|
|
349
|
+
pages.forEach((collection, idx) => {
|
|
350
|
+
const item: NavItem = {
|
|
351
|
+
label: collection.labels?.plural ?? collection.slug,
|
|
352
|
+
path: `/${collection.slug}`,
|
|
353
|
+
icon: resolveIcon(collection),
|
|
354
|
+
}
|
|
355
|
+
if (idx === 0) {
|
|
356
|
+
item.children = [
|
|
357
|
+
{ path: '/saved-sections', label: 'Saved Sections', icon: Library },
|
|
358
|
+
{ path: '/page-templates', label: 'Templates', icon: Layers },
|
|
359
|
+
]
|
|
360
|
+
}
|
|
361
|
+
items.push(item)
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
for (const collection of otherUngrouped) {
|
|
365
|
+
items.push({
|
|
366
|
+
label: collection.labels?.plural ?? collection.slug,
|
|
367
|
+
path: `/${collection.slug}`,
|
|
368
|
+
icon: resolveIcon(collection),
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Config-supplied groups land before our standard Content/Settings
|
|
373
|
+
// groups so admin-defined sections take precedence.
|
|
374
|
+
const groupBuckets = new Map<string, NavItem[]>()
|
|
252
375
|
for (const col of grouped) {
|
|
253
376
|
const group = col.admin.group as string
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
icon: resolveIcon(collection),
|
|
263
|
-
group: groupName,
|
|
264
|
-
})
|
|
265
|
-
}
|
|
377
|
+
const bucket = groupBuckets.get(group) ?? []
|
|
378
|
+
bucket.push({
|
|
379
|
+
label: col.labels?.plural ?? col.slug,
|
|
380
|
+
path: `/${col.slug}`,
|
|
381
|
+
icon: resolveIcon(col),
|
|
382
|
+
group,
|
|
383
|
+
})
|
|
384
|
+
groupBuckets.set(group, bucket)
|
|
266
385
|
}
|
|
386
|
+
for (const bucket of groupBuckets.values()) items.push(...bucket)
|
|
267
387
|
|
|
388
|
+
// Standard Content + Settings destinations. These are always
|
|
389
|
+
// present, regardless of which collections the admin configures,
|
|
390
|
+
// because they map to CMS features (not collections).
|
|
268
391
|
items.push(
|
|
269
|
-
{ path: '/
|
|
270
|
-
{ path: '/
|
|
271
|
-
{ path: '/
|
|
272
|
-
{ path: '/
|
|
273
|
-
{ path: '/
|
|
274
|
-
{ path: '/
|
|
275
|
-
{ path: '/
|
|
276
|
-
{ path: '/users', label: 'Users', icon: Users },
|
|
277
|
-
{ path: '/api-keys', label: 'API Keys', icon: KeyRound },
|
|
278
|
-
{ path: '/settings', label: 'Settings', icon: Settings },
|
|
392
|
+
{ path: '/media', label: 'Media', icon: Image, group: 'Content' },
|
|
393
|
+
{ path: '/forms', label: 'Forms', icon: ClipboardList, group: 'Content' },
|
|
394
|
+
{ path: '/seo', label: 'SEO', icon: SearchIcon, group: 'Content' },
|
|
395
|
+
{ path: '/script-tags', label: 'Script Tags', icon: Code2, group: 'Content' },
|
|
396
|
+
{ path: '/users', label: 'Users', icon: Users, group: 'Settings' },
|
|
397
|
+
{ path: '/api-keys', label: 'API Keys', icon: KeyRound, group: 'Settings' },
|
|
398
|
+
{ path: '/settings', label: 'Settings', icon: Settings, group: 'Settings' },
|
|
279
399
|
)
|
|
280
400
|
|
|
281
401
|
return items
|