@actuate-media/cms-admin 0.67.0 → 0.68.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/CHANGELOG.md +16 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +29 -5
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/hooks/useIdleLogout.test.d.ts +2 -0
- package/dist/__tests__/hooks/useIdleLogout.test.d.ts.map +1 -0
- package/dist/__tests__/hooks/useIdleLogout.test.js +64 -0
- package/dist/__tests__/hooks/useIdleLogout.test.js.map +1 -0
- package/dist/__tests__/lib/post-types-service.test.d.ts +2 -0
- package/dist/__tests__/lib/post-types-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/post-types-service.test.js +88 -0
- package/dist/__tests__/lib/post-types-service.test.js.map +1 -0
- package/dist/__tests__/views/collection-list-nav.render.test.js +43 -5
- package/dist/__tests__/views/collection-list-nav.render.test.js.map +1 -1
- package/dist/__tests__/views/forms-list.render.test.js +25 -6
- package/dist/__tests__/views/forms-list.render.test.js.map +1 -1
- package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.js +121 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.js.map +1 -0
- package/dist/__tests__/views/page-templates.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-templates.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-templates.render.test.js +105 -0
- package/dist/__tests__/views/page-templates.render.test.js.map +1 -0
- package/dist/__tests__/views/post-type-editor.render.test.d.ts +2 -0
- package/dist/__tests__/views/post-type-editor.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/post-type-editor.render.test.js +102 -0
- package/dist/__tests__/views/post-type-editor.render.test.js.map +1 -0
- package/dist/__tests__/views/security-settings.render.test.js +85 -4
- package/dist/__tests__/views/security-settings.render.test.js.map +1 -1
- package/dist/__tests__/views/seo-content-pane.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-content-pane.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-content-pane.render.test.js +117 -0
- package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/collections/EntryDetailPane.d.ts +10 -0
- package/dist/components/collections/EntryDetailPane.d.ts.map +1 -0
- package/dist/components/collections/EntryDetailPane.js +92 -0
- package/dist/components/collections/EntryDetailPane.js.map +1 -0
- package/dist/components/forms/FieldsPanel.js +1 -1
- package/dist/components/forms/FieldsPanel.js.map +1 -1
- package/dist/components/forms/FormSchemaPane.d.ts +8 -0
- package/dist/components/forms/FormSchemaPane.d.ts.map +1 -0
- package/dist/components/forms/FormSchemaPane.js +95 -0
- package/dist/components/forms/FormSchemaPane.js.map +1 -0
- package/dist/components/seo/SeoEditorPane.d.ts +9 -0
- package/dist/components/seo/SeoEditorPane.d.ts.map +1 -0
- package/dist/components/seo/SeoEditorPane.js +175 -0
- package/dist/components/seo/SeoEditorPane.js.map +1 -0
- package/dist/components/ui/Badge.d.ts +1 -1
- package/dist/components/ui/Button.d.ts +1 -1
- package/dist/components/ui/StatusBadge.d.ts +1 -1
- package/dist/hooks/useIdleLogout.d.ts +18 -0
- package/dist/hooks/useIdleLogout.d.ts.map +1 -0
- package/dist/hooks/useIdleLogout.js +66 -0
- package/dist/hooks/useIdleLogout.js.map +1 -0
- package/dist/lib/api.d.ts +1 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +7 -1
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/post-types-service.d.ts +44 -0
- package/dist/lib/post-types-service.d.ts.map +1 -0
- package/dist/lib/post-types-service.js +83 -0
- package/dist/lib/post-types-service.js.map +1 -0
- package/dist/views/CollectionList.d.ts.map +1 -1
- package/dist/views/CollectionList.js +23 -4
- package/dist/views/CollectionList.js.map +1 -1
- package/dist/views/Forms.d.ts.map +1 -1
- package/dist/views/Forms.js +25 -21
- package/dist/views/Forms.js.map +1 -1
- package/dist/views/Posts/PostTypeEditor.d.ts +16 -0
- package/dist/views/Posts/PostTypeEditor.d.ts.map +1 -0
- package/dist/views/Posts/PostTypeEditor.js +159 -0
- package/dist/views/Posts/PostTypeEditor.js.map +1 -0
- package/dist/views/Posts/PostTypesView.d.ts +3 -2
- package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
- package/dist/views/Posts/PostTypesView.js +1 -1
- package/dist/views/Posts/PostTypesView.js.map +1 -1
- package/dist/views/page-builder/BuilderSectionsPanel.d.ts +20 -0
- package/dist/views/page-builder/BuilderSectionsPanel.d.ts.map +1 -0
- package/dist/views/page-builder/BuilderSectionsPanel.js +105 -0
- package/dist/views/page-builder/BuilderSectionsPanel.js.map +1 -0
- package/dist/views/page-builder/PageBuilder.d.ts.map +1 -1
- package/dist/views/page-builder/PageBuilder.js +51 -2
- package/dist/views/page-builder/PageBuilder.js.map +1 -1
- package/dist/views/page-builder/PageTemplates.d.ts.map +1 -1
- package/dist/views/page-builder/PageTemplates.js +113 -6
- package/dist/views/page-builder/PageTemplates.js.map +1 -1
- package/dist/views/seo/ContentTab.d.ts.map +1 -1
- package/dist/views/seo/ContentTab.js +22 -6
- package/dist/views/seo/ContentTab.js.map +1 -1
- package/dist/views/settings/AccessProtectionCard.d.ts +5 -1
- package/dist/views/settings/AccessProtectionCard.d.ts.map +1 -1
- package/dist/views/settings/AccessProtectionCard.js +13 -10
- package/dist/views/settings/AccessProtectionCard.js.map +1 -1
- package/dist/views/settings/SecurityTab.d.ts.map +1 -1
- package/dist/views/settings/SecurityTab.js +3 -3
- package/dist/views/settings/SecurityTab.js.map +1 -1
- package/dist/views/settings/SessionPolicyCard.d.ts +1 -2
- package/dist/views/settings/SessionPolicyCard.d.ts.map +1 -1
- package/dist/views/settings/SessionPolicyCard.js +12 -5
- package/dist/views/settings/SessionPolicyCard.js.map +1 -1
- package/dist/views/settings/useSecuritySettings.d.ts +13 -1
- package/dist/views/settings/useSecuritySettings.d.ts.map +1 -1
- package/dist/views/settings/useSecuritySettings.js +47 -4
- package/dist/views/settings/useSecuritySettings.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +36 -7
- package/src/__tests__/hooks/useIdleLogout.test.tsx +86 -0
- package/src/__tests__/lib/post-types-service.test.ts +112 -0
- package/src/__tests__/views/collection-list-nav.render.test.tsx +51 -5
- package/src/__tests__/views/forms-list.render.test.tsx +34 -6
- package/src/__tests__/views/page-builder-sections-panel.render.test.tsx +138 -0
- package/src/__tests__/views/page-templates.render.test.tsx +142 -0
- package/src/__tests__/views/post-type-editor.render.test.tsx +116 -0
- package/src/__tests__/views/security-settings.render.test.tsx +111 -4
- package/src/__tests__/views/seo-content-pane.render.test.tsx +142 -0
- package/src/components/collections/EntryDetailPane.tsx +182 -0
- package/src/components/forms/FieldsPanel.tsx +3 -1
- package/src/components/forms/FormSchemaPane.tsx +192 -0
- package/src/components/seo/SeoEditorPane.tsx +533 -0
- package/src/hooks/useIdleLogout.ts +77 -0
- package/src/lib/api.ts +8 -2
- package/src/lib/post-types-service.ts +129 -0
- package/src/views/CollectionList.tsx +158 -110
- package/src/views/Forms.tsx +154 -126
- package/src/views/Posts/PostTypeEditor.tsx +568 -0
- package/src/views/Posts/PostTypesView.tsx +5 -4
- package/src/views/page-builder/BuilderSectionsPanel.tsx +304 -0
- package/src/views/page-builder/PageBuilder.tsx +69 -2
- package/src/views/page-builder/PageTemplates.tsx +340 -57
- package/src/views/seo/ContentTab.tsx +113 -80
- package/src/views/settings/AccessProtectionCard.tsx +83 -53
- package/src/views/settings/SecurityTab.tsx +23 -2
- package/src/views/settings/SessionPolicyCard.tsx +45 -9
- package/src/views/settings/useSecuritySettings.ts +78 -22
- package/dist/components/forms/FormSchemaDrawer.d.ts +0 -9
- package/dist/components/forms/FormSchemaDrawer.d.ts.map +0 -1
- package/dist/components/forms/FormSchemaDrawer.js +0 -99
- package/dist/components/forms/FormSchemaDrawer.js.map +0 -1
- package/dist/components/seo/SeoEditorDrawer.d.ts +0 -8
- package/dist/components/seo/SeoEditorDrawer.d.ts.map +0 -1
- package/dist/components/seo/SeoEditorDrawer.js +0 -172
- package/dist/components/seo/SeoEditorDrawer.js.map +0 -1
- package/src/components/forms/FormSchemaDrawer.tsx +0 -192
- package/src/components/seo/SeoEditorDrawer.tsx +0 -529
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
DndContext,
|
|
6
|
+
KeyboardSensor,
|
|
7
|
+
PointerSensor,
|
|
8
|
+
closestCenter,
|
|
9
|
+
useSensor,
|
|
10
|
+
useSensors,
|
|
11
|
+
type DragEndEvent,
|
|
12
|
+
} from '@dnd-kit/core'
|
|
13
|
+
import {
|
|
14
|
+
SortableContext,
|
|
15
|
+
sortableKeyboardCoordinates,
|
|
16
|
+
useSortable,
|
|
17
|
+
verticalListSortingStrategy,
|
|
18
|
+
} from '@dnd-kit/sortable'
|
|
19
|
+
import { CSS } from '@dnd-kit/utilities'
|
|
20
|
+
import {
|
|
21
|
+
Bookmark,
|
|
22
|
+
Code,
|
|
23
|
+
FileText,
|
|
24
|
+
Grid,
|
|
25
|
+
GripVertical,
|
|
26
|
+
HelpCircle,
|
|
27
|
+
Image,
|
|
28
|
+
LayoutGrid,
|
|
29
|
+
LayoutTemplate,
|
|
30
|
+
MousePointer,
|
|
31
|
+
PanelLeftClose,
|
|
32
|
+
PanelLeftOpen,
|
|
33
|
+
Play,
|
|
34
|
+
Plus,
|
|
35
|
+
Star,
|
|
36
|
+
Type,
|
|
37
|
+
type LucideIcon,
|
|
38
|
+
} from 'lucide-react'
|
|
39
|
+
import { BlockCatalog } from '@actuate-media/cms-core/page-builder'
|
|
40
|
+
import type { BuilderNode, PageNode } from '@actuate-media/cms-core'
|
|
41
|
+
|
|
42
|
+
export interface BuilderSectionsPanelProps {
|
|
43
|
+
tree: PageNode
|
|
44
|
+
selectedNodeId: string | null
|
|
45
|
+
/** Collapsed to a slim rail. State is owned by PageBuilder (persisted). */
|
|
46
|
+
collapsed: boolean
|
|
47
|
+
onToggleCollapsed: () => void
|
|
48
|
+
onSelect: (id: string) => void
|
|
49
|
+
/** Move a top-level section to `toIndex` within the page. */
|
|
50
|
+
onReorder: (id: string, toIndex: number) => void
|
|
51
|
+
onAddSection: () => void
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface SectionEntry {
|
|
55
|
+
id: string
|
|
56
|
+
name: string
|
|
57
|
+
meta: string
|
|
58
|
+
icon: LucideIcon
|
|
59
|
+
hidden: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Same icon-name → component mapping the BlockPicker uses for the catalog. */
|
|
63
|
+
const BLOCK_ICONS: Record<string, LucideIcon> = {
|
|
64
|
+
Star,
|
|
65
|
+
Type,
|
|
66
|
+
Image,
|
|
67
|
+
LayoutGrid,
|
|
68
|
+
MousePointer,
|
|
69
|
+
Play,
|
|
70
|
+
Grid,
|
|
71
|
+
HelpCircle,
|
|
72
|
+
FileText,
|
|
73
|
+
Code,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function collectBlockTypes(node: BuilderNode, out: string[]): void {
|
|
77
|
+
if (node.type === 'block') {
|
|
78
|
+
out.push(node.settings.blockType)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if (node.type === 'savedSectionRef') return
|
|
82
|
+
for (const child of node.children) collectBlockTypes(child, out)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function humanize(type: string): string {
|
|
86
|
+
return type.charAt(0).toUpperCase() + type.slice(1)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Builder sections have no user-facing title, so each row derives its name
|
|
91
|
+
* from the section's first block (via the catalog label) and summarizes its
|
|
92
|
+
* contents as the kind label — enough for the editor to recognize and jump
|
|
93
|
+
* between sections without inventing data we don't store.
|
|
94
|
+
*/
|
|
95
|
+
function buildEntries(tree: PageNode, catalog: BlockCatalog): SectionEntry[] {
|
|
96
|
+
return tree.children.map((node, index) => {
|
|
97
|
+
if (node.type === 'savedSectionRef') {
|
|
98
|
+
return {
|
|
99
|
+
id: node.id,
|
|
100
|
+
name: 'Saved section',
|
|
101
|
+
meta: 'From the section library',
|
|
102
|
+
icon: Bookmark,
|
|
103
|
+
hidden: false,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const blockTypes: string[] = []
|
|
107
|
+
collectBlockTypes(node, blockTypes)
|
|
108
|
+
const firstType = blockTypes[0]
|
|
109
|
+
const def = firstType ? catalog.get(firstType) : undefined
|
|
110
|
+
return {
|
|
111
|
+
id: node.id,
|
|
112
|
+
name: def?.label ?? (firstType ? humanize(firstType) : `Section ${index + 1}`),
|
|
113
|
+
meta:
|
|
114
|
+
blockTypes.length === 0
|
|
115
|
+
? 'Empty section'
|
|
116
|
+
: `${blockTypes.length} block${blockTypes.length === 1 ? '' : 's'}`,
|
|
117
|
+
icon: (def && BLOCK_ICONS[def.icon]) ?? LayoutTemplate,
|
|
118
|
+
hidden: node.settings.visibility === 'hidden',
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const ICON_BUTTON_CLASS =
|
|
124
|
+
'flex h-8 w-8 items-center justify-center rounded-[7px] text-[var(--sub)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--acc-l)] hover:text-[var(--acc)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none'
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Design-parity left rail for the Page Builder (PageBuilder.html): the page's
|
|
128
|
+
* sections listed in order with drag-to-reorder, click-to-select (which
|
|
129
|
+
* highlights the section on the canvas and opens its settings in the existing
|
|
130
|
+
* right-hand context panel), and an "Add Section" affordance at the bottom.
|
|
131
|
+
*/
|
|
132
|
+
export function BuilderSectionsPanel({
|
|
133
|
+
tree,
|
|
134
|
+
selectedNodeId,
|
|
135
|
+
collapsed,
|
|
136
|
+
onToggleCollapsed,
|
|
137
|
+
onSelect,
|
|
138
|
+
onReorder,
|
|
139
|
+
onAddSection,
|
|
140
|
+
}: BuilderSectionsPanelProps) {
|
|
141
|
+
const catalog = useMemo(() => new BlockCatalog(), [])
|
|
142
|
+
const entries = useMemo(() => buildEntries(tree, catalog), [tree, catalog])
|
|
143
|
+
|
|
144
|
+
const sensors = useSensors(
|
|
145
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
146
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
function handleDragEnd(event: DragEndEvent) {
|
|
150
|
+
const { active, over } = event
|
|
151
|
+
if (!over || active.id === over.id) return
|
|
152
|
+
const ids = entries.map((e) => e.id)
|
|
153
|
+
const to = ids.indexOf(String(over.id))
|
|
154
|
+
if (to === -1 || !ids.includes(String(active.id))) return
|
|
155
|
+
onReorder(String(active.id), to)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (collapsed) {
|
|
159
|
+
return (
|
|
160
|
+
<nav
|
|
161
|
+
aria-label="Page sections"
|
|
162
|
+
className="flex w-11 shrink-0 flex-col items-center border-r border-[var(--bdr)] bg-[var(--sb)] py-2"
|
|
163
|
+
>
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
onClick={onToggleCollapsed}
|
|
167
|
+
aria-expanded={false}
|
|
168
|
+
aria-label="Expand sections panel"
|
|
169
|
+
className={ICON_BUTTON_CLASS}
|
|
170
|
+
>
|
|
171
|
+
<PanelLeftOpen size={16} aria-hidden />
|
|
172
|
+
</button>
|
|
173
|
+
</nav>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<nav
|
|
179
|
+
aria-label="Page sections"
|
|
180
|
+
className="flex w-[280px] shrink-0 flex-col overflow-hidden border-r border-[var(--bdr)] bg-[var(--sb)]"
|
|
181
|
+
>
|
|
182
|
+
<div className="flex items-center justify-between border-b border-[var(--bdr)] py-2 pr-2 pl-3.5">
|
|
183
|
+
<div className="flex items-center gap-2">
|
|
184
|
+
<h2 className="text-[13px] font-medium text-[var(--txt)]">Sections</h2>
|
|
185
|
+
<span className="rounded-[4px] bg-[var(--acc-l)] px-[7px] py-0.5 text-[11px] font-medium text-[var(--acc)] tabular-nums">
|
|
186
|
+
{entries.length}
|
|
187
|
+
</span>
|
|
188
|
+
</div>
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
onClick={onToggleCollapsed}
|
|
192
|
+
aria-expanded
|
|
193
|
+
aria-label="Collapse sections panel"
|
|
194
|
+
className={ICON_BUTTON_CLASS}
|
|
195
|
+
>
|
|
196
|
+
<PanelLeftClose size={16} aria-hidden />
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div className="flex-1 overflow-y-auto">
|
|
201
|
+
{entries.length === 0 ? (
|
|
202
|
+
<p className="px-4 py-6 text-center text-[12.5px] text-[var(--muted)]">
|
|
203
|
+
No sections yet. Add one below to start building.
|
|
204
|
+
</p>
|
|
205
|
+
) : (
|
|
206
|
+
<DndContext
|
|
207
|
+
sensors={sensors}
|
|
208
|
+
collisionDetection={closestCenter}
|
|
209
|
+
onDragEnd={handleDragEnd}
|
|
210
|
+
>
|
|
211
|
+
<SortableContext
|
|
212
|
+
items={entries.map((e) => e.id)}
|
|
213
|
+
strategy={verticalListSortingStrategy}
|
|
214
|
+
>
|
|
215
|
+
<ul aria-label="Sections in page order">
|
|
216
|
+
{entries.map((entry) => (
|
|
217
|
+
<SortableSectionRow
|
|
218
|
+
key={entry.id}
|
|
219
|
+
entry={entry}
|
|
220
|
+
selected={entry.id === selectedNodeId}
|
|
221
|
+
onSelect={onSelect}
|
|
222
|
+
/>
|
|
223
|
+
))}
|
|
224
|
+
</ul>
|
|
225
|
+
</SortableContext>
|
|
226
|
+
</DndContext>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div className="border-t border-[var(--bdr)]">
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
onClick={onAddSection}
|
|
234
|
+
className="flex w-full items-center gap-2 px-3.5 py-3 text-[13px] font-medium text-[var(--acc)] transition-colors duration-[var(--motion-fast)] hover:bg-[var(--acc-l)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none focus-visible:ring-inset"
|
|
235
|
+
>
|
|
236
|
+
<Plus size={14} aria-hidden />
|
|
237
|
+
Add Section
|
|
238
|
+
</button>
|
|
239
|
+
</div>
|
|
240
|
+
</nav>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
interface SortableSectionRowProps {
|
|
245
|
+
entry: SectionEntry
|
|
246
|
+
selected: boolean
|
|
247
|
+
onSelect: (id: string) => void
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function SortableSectionRow({ entry, selected, onSelect }: SortableSectionRowProps) {
|
|
251
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
252
|
+
id: entry.id,
|
|
253
|
+
})
|
|
254
|
+
const Icon = entry.icon
|
|
255
|
+
|
|
256
|
+
const style = {
|
|
257
|
+
transform: CSS.Transform.toString(transform),
|
|
258
|
+
transition,
|
|
259
|
+
zIndex: isDragging ? 10 : undefined,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<li ref={setNodeRef} style={style} className={isDragging ? 'shadow-[var(--shd2)]' : undefined}>
|
|
264
|
+
<div
|
|
265
|
+
className={`flex items-center gap-1.5 border-b border-[var(--bdr)] py-2 pr-2.5 pl-1.5 transition-colors duration-[var(--motion-fast)] ${
|
|
266
|
+
selected ? 'bg-[var(--acc-l)]' : 'hover:bg-[var(--acc-l)]'
|
|
267
|
+
}`}
|
|
268
|
+
>
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
aria-label={`Drag to reorder ${entry.name}`}
|
|
272
|
+
{...attributes}
|
|
273
|
+
{...listeners}
|
|
274
|
+
className="shrink-0 cursor-grab touch-none rounded p-1 text-[var(--muted)] transition-colors duration-[var(--motion-fast)] hover:text-[var(--acc)] focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none active:cursor-grabbing"
|
|
275
|
+
>
|
|
276
|
+
<GripVertical size={15} aria-hidden />
|
|
277
|
+
</button>
|
|
278
|
+
<button
|
|
279
|
+
type="button"
|
|
280
|
+
onClick={() => onSelect(entry.id)}
|
|
281
|
+
aria-current={selected ? 'true' : undefined}
|
|
282
|
+
className="flex min-w-0 flex-1 items-center gap-2.5 rounded text-left focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none"
|
|
283
|
+
>
|
|
284
|
+
<span
|
|
285
|
+
aria-hidden
|
|
286
|
+
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[7px] bg-[var(--acc-l)] text-[var(--acc)]"
|
|
287
|
+
>
|
|
288
|
+
<Icon size={14} />
|
|
289
|
+
</span>
|
|
290
|
+
<span className="flex min-w-0 flex-col">
|
|
291
|
+
<span
|
|
292
|
+
className={`truncate text-[13px] font-medium ${
|
|
293
|
+
entry.hidden ? 'text-[var(--muted)] line-through' : 'text-[var(--txt)]'
|
|
294
|
+
}`}
|
|
295
|
+
>
|
|
296
|
+
{entry.name}
|
|
297
|
+
</span>
|
|
298
|
+
<span className="truncate text-[11px] text-[var(--muted)]">{entry.meta}</span>
|
|
299
|
+
</span>
|
|
300
|
+
</button>
|
|
301
|
+
</div>
|
|
302
|
+
</li>
|
|
303
|
+
)
|
|
304
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback } from 'react'
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
4
4
|
import { Loader2, AlertTriangle } from 'lucide-react'
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { cmsApi } from '../../lib/api.js'
|
|
@@ -15,6 +15,7 @@ import type { PageNode } from '@actuate-media/cms-core'
|
|
|
15
15
|
import { BuilderToolbar } from './BuilderToolbar.js'
|
|
16
16
|
import { BottomBar } from './BottomBar.js'
|
|
17
17
|
import { BuilderCanvas } from './canvas/index.js'
|
|
18
|
+
import { BuilderSectionsPanel } from './BuilderSectionsPanel.js'
|
|
18
19
|
import { ContextPanel } from './ContextPanel.js'
|
|
19
20
|
import { AIGenerateDialog } from './AIGenerateDialog.js'
|
|
20
21
|
|
|
@@ -33,12 +34,35 @@ const DEVICE_WIDTHS = {
|
|
|
33
34
|
mobile: '375px',
|
|
34
35
|
} as const
|
|
35
36
|
|
|
37
|
+
const SECTIONS_PANEL_KEY = 'actuate-builder-sections-panel'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Left sections panel default: open on wide viewports (≥1280px), collapsed
|
|
41
|
+
* below so the canvas keeps room. An explicit user choice, persisted in
|
|
42
|
+
* localStorage, always wins over the breakpoint default.
|
|
43
|
+
*/
|
|
44
|
+
function initialSectionsCollapsed(): boolean {
|
|
45
|
+
try {
|
|
46
|
+
const stored = localStorage.getItem(SECTIONS_PANEL_KEY)
|
|
47
|
+
if (stored === 'open') return false
|
|
48
|
+
if (stored === 'collapsed') return true
|
|
49
|
+
} catch {
|
|
50
|
+
/* localStorage unavailable — fall through to the breakpoint default */
|
|
51
|
+
}
|
|
52
|
+
if (typeof window !== 'undefined') {
|
|
53
|
+
return window.innerWidth < 1280
|
|
54
|
+
}
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
export function PageBuilder({ documentId, collectionSlug, config, onNavigate }: PageBuilderProps) {
|
|
37
59
|
const [loading, setLoading] = useState(!!documentId)
|
|
38
60
|
const [loadError, setLoadError] = useState<string | null>(null)
|
|
39
61
|
const [saving, setSaving] = useState(false)
|
|
40
62
|
const [status, setStatus] = useState<DocumentStatus>('DRAFT')
|
|
41
63
|
const [aiDialogOpen, setAiDialogOpen] = useState(false)
|
|
64
|
+
const [sectionsCollapsed, setSectionsCollapsed] = useState(initialSectionsCollapsed)
|
|
65
|
+
const canvasRef = useRef<HTMLDivElement>(null)
|
|
42
66
|
|
|
43
67
|
const builder = useBuilderState()
|
|
44
68
|
const {
|
|
@@ -182,6 +206,38 @@ export function PageBuilder({ documentId, collectionSlug, config, onNavigate }:
|
|
|
182
206
|
[replaceTree],
|
|
183
207
|
)
|
|
184
208
|
|
|
209
|
+
const toggleSectionsPanel = useCallback(() => {
|
|
210
|
+
setSectionsCollapsed((prev) => {
|
|
211
|
+
const next = !prev
|
|
212
|
+
try {
|
|
213
|
+
localStorage.setItem(SECTIONS_PANEL_KEY, next ? 'collapsed' : 'open')
|
|
214
|
+
} catch {
|
|
215
|
+
/* localStorage unavailable — the choice just won't persist */
|
|
216
|
+
}
|
|
217
|
+
return next
|
|
218
|
+
})
|
|
219
|
+
}, [])
|
|
220
|
+
|
|
221
|
+
// Selecting from the left panel highlights the section on the canvas (the
|
|
222
|
+
// canvas already outlines the selected node) and opens its settings in the
|
|
223
|
+
// right-hand context panel, then scrolls the section into view.
|
|
224
|
+
const handlePanelSelect = useCallback(
|
|
225
|
+
(id: string) => {
|
|
226
|
+
selectNode(id)
|
|
227
|
+
canvasRef.current
|
|
228
|
+
?.querySelector(`[data-node-id="${CSS.escape(id)}"]`)
|
|
229
|
+
?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
230
|
+
},
|
|
231
|
+
[selectNode],
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
const handlePanelReorder = useCallback(
|
|
235
|
+
(id: string, toIndex: number) => {
|
|
236
|
+
moveNodeById(id, tree.id, toIndex)
|
|
237
|
+
},
|
|
238
|
+
[moveNodeById, tree.id],
|
|
239
|
+
)
|
|
240
|
+
|
|
185
241
|
if (loading) {
|
|
186
242
|
return (
|
|
187
243
|
<div className="bg-background flex h-full items-center justify-center">
|
|
@@ -247,8 +303,19 @@ export function PageBuilder({ documentId, collectionSlug, config, onNavigate }:
|
|
|
247
303
|
/>
|
|
248
304
|
|
|
249
305
|
<div className="flex flex-1 overflow-hidden">
|
|
306
|
+
{/* Sections panel (design parity: sections list left of the canvas) */}
|
|
307
|
+
<BuilderSectionsPanel
|
|
308
|
+
tree={tree}
|
|
309
|
+
selectedNodeId={selectedNodeId}
|
|
310
|
+
collapsed={sectionsCollapsed}
|
|
311
|
+
onToggleCollapsed={toggleSectionsPanel}
|
|
312
|
+
onSelect={handlePanelSelect}
|
|
313
|
+
onReorder={handlePanelReorder}
|
|
314
|
+
onAddSection={addSection}
|
|
315
|
+
/>
|
|
316
|
+
|
|
250
317
|
{/* Canvas area */}
|
|
251
|
-
<div className="bg-muted flex-1 overflow-auto">
|
|
318
|
+
<div ref={canvasRef} className="bg-muted flex-1 overflow-auto">
|
|
252
319
|
<div
|
|
253
320
|
className="mx-auto h-full max-w-full transition-all duration-200"
|
|
254
321
|
style={{ width: canvasWidth }}
|