@screenbook/ui 1.1.0 → 1.1.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.
- package/README.md +30 -0
- package/dist/client/_astro/coverage.BzPU-EGZ.css +1 -0
- package/dist/server/entry.mjs +1 -1
- package/dist/server/{manifest_smcahUO6.mjs → manifest_BGl49hHW.mjs} +1 -1
- package/dist/server/pages/coverage.astro.mjs +1 -1
- package/dist/server/pages/editor.astro.mjs +1 -1
- package/dist/server/pages/graph.astro.mjs +1 -1
- package/dist/server/pages/impact.astro.mjs +1 -1
- package/dist/server/pages/index.astro.mjs +1 -1
- package/dist/server/pages/screen/_id_.astro.mjs +1 -1
- package/package.json +5 -1
- package/.astro/content-assets.mjs +0 -1
- package/.astro/content-modules.mjs +0 -1
- package/.astro/content.d.ts +0 -199
- package/.astro/types.d.ts +0 -2
- package/.prettierrc +0 -15
- package/CHANGELOG.md +0 -77
- package/astro.config.mjs +0 -20
- package/dist/client/_astro/coverage.DLKSOM4m.css +0 -1
- package/public/logo.svg +0 -5
- package/src/components/MockFormEditor.tsx +0 -1280
- package/src/components/MockPreview.astro +0 -811
- package/src/layouts/Layout.astro +0 -77
- package/src/pages/api/save-mock.ts +0 -182
- package/src/pages/coverage.astro +0 -399
- package/src/pages/editor.astro +0 -33
- package/src/pages/graph.astro +0 -368
- package/src/pages/impact.astro +0 -462
- package/src/pages/index.astro +0 -176
- package/src/pages/screen/[id].astro +0 -195
- package/src/styles/global.css +0 -904
- package/src/styles/mock-editor.css +0 -1351
- package/src/utils/impactAnalysis.ts +0 -304
- package/src/utils/loadCoverage.ts +0 -30
- package/src/utils/loadScreens.ts +0 -18
- package/tsconfig.json +0 -10
- /package/dist/server/chunks/{loadScreens_CkCqdbH2.mjs → loadScreens_B8bVK3q5.mjs} +0 -0
|
@@ -1,1280 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
MockElement,
|
|
3
|
-
MockLayout,
|
|
4
|
-
MockSection,
|
|
5
|
-
ScreenMock,
|
|
6
|
-
} from "@screenbook/core"
|
|
7
|
-
import { useCallback, useId, useRef, useState } from "react"
|
|
8
|
-
import "../styles/mock-editor.css"
|
|
9
|
-
|
|
10
|
-
interface MockFormEditorProps {
|
|
11
|
-
screenId?: string
|
|
12
|
-
screenTitle?: string
|
|
13
|
-
initialMock?: ScreenMock
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
type ElementType =
|
|
17
|
-
| "button"
|
|
18
|
-
| "input"
|
|
19
|
-
| "link"
|
|
20
|
-
| "text"
|
|
21
|
-
| "image"
|
|
22
|
-
| "list"
|
|
23
|
-
| "table"
|
|
24
|
-
|
|
25
|
-
const ELEMENT_TYPES: { value: ElementType; label: string }[] = [
|
|
26
|
-
{ value: "button", label: "Button" },
|
|
27
|
-
{ value: "input", label: "Input" },
|
|
28
|
-
{ value: "link", label: "Link" },
|
|
29
|
-
{ value: "text", label: "Text" },
|
|
30
|
-
{ value: "image", label: "Image" },
|
|
31
|
-
{ value: "list", label: "List" },
|
|
32
|
-
{ value: "table", label: "Table" },
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
// Template definitions for quick-start mock creation
|
|
36
|
-
const TEMPLATES: { id: string; label: string; sections: MockSection[] }[] = [
|
|
37
|
-
{
|
|
38
|
-
id: "basic-form",
|
|
39
|
-
label: "Basic Form",
|
|
40
|
-
sections: [
|
|
41
|
-
{
|
|
42
|
-
title: "Header",
|
|
43
|
-
layout: "horizontal",
|
|
44
|
-
elements: [{ type: "text", label: "Page Title", variant: "heading" }],
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
title: "Form",
|
|
48
|
-
elements: [
|
|
49
|
-
{ type: "input", label: "Name", placeholder: "Enter name..." },
|
|
50
|
-
{
|
|
51
|
-
type: "input",
|
|
52
|
-
label: "Email",
|
|
53
|
-
inputType: "email",
|
|
54
|
-
placeholder: "Enter email...",
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
type: "input",
|
|
58
|
-
label: "Message",
|
|
59
|
-
inputType: "textarea",
|
|
60
|
-
placeholder: "Enter message...",
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
title: "Actions",
|
|
66
|
-
layout: "horizontal",
|
|
67
|
-
elements: [
|
|
68
|
-
{ type: "link", label: "Cancel" },
|
|
69
|
-
{ type: "button", label: "Submit", variant: "primary" },
|
|
70
|
-
],
|
|
71
|
-
},
|
|
72
|
-
],
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
id: "list-view",
|
|
76
|
-
label: "List View",
|
|
77
|
-
sections: [
|
|
78
|
-
{
|
|
79
|
-
title: "Header",
|
|
80
|
-
layout: "horizontal",
|
|
81
|
-
elements: [
|
|
82
|
-
{ type: "text", label: "Items", variant: "heading" },
|
|
83
|
-
{ type: "button", label: "Add New", variant: "primary" },
|
|
84
|
-
],
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
title: "Search",
|
|
88
|
-
elements: [
|
|
89
|
-
{
|
|
90
|
-
type: "input",
|
|
91
|
-
label: "Search",
|
|
92
|
-
inputType: "search",
|
|
93
|
-
placeholder: "Search items...",
|
|
94
|
-
},
|
|
95
|
-
],
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
title: "Content",
|
|
99
|
-
elements: [{ type: "list", label: "Item List", itemCount: 5 }],
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
id: "detail-view",
|
|
105
|
-
label: "Detail View",
|
|
106
|
-
sections: [
|
|
107
|
-
{
|
|
108
|
-
title: "Header",
|
|
109
|
-
layout: "horizontal",
|
|
110
|
-
elements: [
|
|
111
|
-
{ type: "text", label: "Item Detail", variant: "heading" },
|
|
112
|
-
{ type: "button", label: "Edit", variant: "secondary" },
|
|
113
|
-
{ type: "button", label: "Delete", variant: "danger" },
|
|
114
|
-
],
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
title: "Info",
|
|
118
|
-
elements: [
|
|
119
|
-
{ type: "text", label: "Name: Example Item", variant: "body" },
|
|
120
|
-
{ type: "text", label: "Created: 2024-01-01", variant: "caption" },
|
|
121
|
-
{ type: "image", label: "Item Image", aspectRatio: "16:9" },
|
|
122
|
-
],
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
title: "Actions",
|
|
126
|
-
layout: "horizontal",
|
|
127
|
-
elements: [{ type: "link", label: "Back to List" }],
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
id: "dashboard",
|
|
133
|
-
label: "Dashboard",
|
|
134
|
-
sections: [
|
|
135
|
-
{
|
|
136
|
-
title: "Header",
|
|
137
|
-
layout: "horizontal",
|
|
138
|
-
elements: [{ type: "text", label: "Dashboard", variant: "heading" }],
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
title: "Stats",
|
|
142
|
-
layout: "horizontal",
|
|
143
|
-
elements: [
|
|
144
|
-
{ type: "text", label: "Total: 100", variant: "subheading" },
|
|
145
|
-
{ type: "text", label: "Active: 80", variant: "subheading" },
|
|
146
|
-
{ type: "text", label: "Pending: 20", variant: "subheading" },
|
|
147
|
-
],
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
title: "Data",
|
|
151
|
-
elements: [
|
|
152
|
-
{
|
|
153
|
-
type: "table",
|
|
154
|
-
label: "Recent Items",
|
|
155
|
-
columns: ["Name", "Status", "Date"],
|
|
156
|
-
rowCount: 5,
|
|
157
|
-
},
|
|
158
|
-
],
|
|
159
|
-
},
|
|
160
|
-
],
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
id: "settings",
|
|
164
|
-
label: "Settings",
|
|
165
|
-
sections: [
|
|
166
|
-
{
|
|
167
|
-
title: "Header",
|
|
168
|
-
elements: [{ type: "text", label: "Settings", variant: "heading" }],
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
title: "Profile",
|
|
172
|
-
elements: [
|
|
173
|
-
{ type: "image", label: "Avatar", aspectRatio: "1:1" },
|
|
174
|
-
{ type: "input", label: "Display Name", placeholder: "Your name" },
|
|
175
|
-
{
|
|
176
|
-
type: "input",
|
|
177
|
-
label: "Email",
|
|
178
|
-
inputType: "email",
|
|
179
|
-
placeholder: "your@email.com",
|
|
180
|
-
},
|
|
181
|
-
],
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
title: "Preferences",
|
|
185
|
-
elements: [
|
|
186
|
-
{ type: "input", label: "Language", placeholder: "Select language" },
|
|
187
|
-
{ type: "input", label: "Timezone", placeholder: "Select timezone" },
|
|
188
|
-
],
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
title: "Actions",
|
|
192
|
-
layout: "horizontal",
|
|
193
|
-
elements: [
|
|
194
|
-
{ type: "button", label: "Save Changes", variant: "primary" },
|
|
195
|
-
],
|
|
196
|
-
},
|
|
197
|
-
],
|
|
198
|
-
},
|
|
199
|
-
]
|
|
200
|
-
|
|
201
|
-
function createDefaultElement(type: ElementType): MockElement {
|
|
202
|
-
switch (type) {
|
|
203
|
-
case "button":
|
|
204
|
-
return { type: "button", label: "Button", variant: "secondary" }
|
|
205
|
-
case "input":
|
|
206
|
-
return { type: "input", label: "Input", placeholder: "Enter text..." }
|
|
207
|
-
case "link":
|
|
208
|
-
return { type: "link", label: "Link" }
|
|
209
|
-
case "text":
|
|
210
|
-
return { type: "text", label: "Text content", variant: "body" }
|
|
211
|
-
case "image":
|
|
212
|
-
return { type: "image", label: "Image", aspectRatio: "16:9" }
|
|
213
|
-
case "list":
|
|
214
|
-
return { type: "list", label: "List", itemCount: 3 }
|
|
215
|
-
case "table":
|
|
216
|
-
return {
|
|
217
|
-
type: "table",
|
|
218
|
-
label: "Table",
|
|
219
|
-
columns: ["Col 1", "Col 2", "Col 3"],
|
|
220
|
-
rowCount: 3,
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Helper to update a section at a given path (e.g., [0, 1] = sections[0].children[1])
|
|
226
|
-
function updateSectionAtPath(
|
|
227
|
-
sections: MockSection[],
|
|
228
|
-
path: number[],
|
|
229
|
-
updater: (section: MockSection) => MockSection,
|
|
230
|
-
): MockSection[] {
|
|
231
|
-
if (path.length === 0) return sections
|
|
232
|
-
|
|
233
|
-
const [index, ...rest] = path
|
|
234
|
-
|
|
235
|
-
return sections.map((section, i) => {
|
|
236
|
-
if (i !== index) return section
|
|
237
|
-
|
|
238
|
-
if (rest.length === 0) {
|
|
239
|
-
return updater(section)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
...section,
|
|
244
|
-
children: updateSectionAtPath(section.children || [], rest, updater),
|
|
245
|
-
}
|
|
246
|
-
})
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Helper to remove a section at a given path
|
|
250
|
-
function removeSectionAtPath(
|
|
251
|
-
sections: MockSection[],
|
|
252
|
-
path: number[],
|
|
253
|
-
): MockSection[] {
|
|
254
|
-
if (path.length === 0) return sections
|
|
255
|
-
|
|
256
|
-
const [index, ...rest] = path
|
|
257
|
-
|
|
258
|
-
if (rest.length === 0) {
|
|
259
|
-
return sections.filter((_, i) => i !== index)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return sections.map((section, i) => {
|
|
263
|
-
if (i !== index) return section
|
|
264
|
-
return {
|
|
265
|
-
...section,
|
|
266
|
-
children: removeSectionAtPath(section.children || [], rest),
|
|
267
|
-
}
|
|
268
|
-
})
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Helper to add a child section at a given path
|
|
272
|
-
function addChildSectionAtPath(
|
|
273
|
-
sections: MockSection[],
|
|
274
|
-
path: number[],
|
|
275
|
-
): MockSection[] {
|
|
276
|
-
return updateSectionAtPath(sections, path, (section) => ({
|
|
277
|
-
...section,
|
|
278
|
-
children: [
|
|
279
|
-
...(section.children || []),
|
|
280
|
-
{ title: "Child Section", elements: [] },
|
|
281
|
-
],
|
|
282
|
-
}))
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export function MockFormEditor({
|
|
286
|
-
screenId,
|
|
287
|
-
screenTitle,
|
|
288
|
-
initialMock,
|
|
289
|
-
}: MockFormEditorProps) {
|
|
290
|
-
const [sections, setSections] = useState<MockSection[]>(
|
|
291
|
-
initialMock?.sections || [
|
|
292
|
-
{ title: "Header", layout: "horizontal", elements: [] },
|
|
293
|
-
],
|
|
294
|
-
)
|
|
295
|
-
const [activeSection, setActiveSection] = useState(0)
|
|
296
|
-
const [saveStatus, setSaveStatus] = useState<
|
|
297
|
-
"idle" | "saving" | "saved" | "error"
|
|
298
|
-
>("idle")
|
|
299
|
-
const [saveError, setSaveError] = useState<string | null>(null)
|
|
300
|
-
const tabListRef = useRef<HTMLDivElement>(null)
|
|
301
|
-
const uniqueId = useId()
|
|
302
|
-
|
|
303
|
-
const addSection = useCallback(() => {
|
|
304
|
-
setSections((prev) => {
|
|
305
|
-
const newSections = [...prev, { title: "New Section", elements: [] }]
|
|
306
|
-
// Set active section to the newly added one
|
|
307
|
-
setActiveSection(newSections.length - 1)
|
|
308
|
-
return newSections
|
|
309
|
-
})
|
|
310
|
-
}, [])
|
|
311
|
-
|
|
312
|
-
// Handle keyboard navigation for tab list (Arrow keys)
|
|
313
|
-
const handleTabKeyDown = useCallback(
|
|
314
|
-
(e: React.KeyboardEvent, index: number) => {
|
|
315
|
-
const lastIndex = sections.length - 1
|
|
316
|
-
let newIndex = index
|
|
317
|
-
|
|
318
|
-
switch (e.key) {
|
|
319
|
-
case "ArrowDown":
|
|
320
|
-
case "ArrowRight":
|
|
321
|
-
e.preventDefault()
|
|
322
|
-
newIndex = index >= lastIndex ? 0 : index + 1
|
|
323
|
-
break
|
|
324
|
-
case "ArrowUp":
|
|
325
|
-
case "ArrowLeft":
|
|
326
|
-
e.preventDefault()
|
|
327
|
-
newIndex = index <= 0 ? lastIndex : index - 1
|
|
328
|
-
break
|
|
329
|
-
case "Home":
|
|
330
|
-
e.preventDefault()
|
|
331
|
-
newIndex = 0
|
|
332
|
-
break
|
|
333
|
-
case "End":
|
|
334
|
-
e.preventDefault()
|
|
335
|
-
newIndex = lastIndex
|
|
336
|
-
break
|
|
337
|
-
default:
|
|
338
|
-
return
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
setActiveSection(newIndex)
|
|
342
|
-
// Focus the new tab
|
|
343
|
-
const tabList = tabListRef.current
|
|
344
|
-
if (tabList) {
|
|
345
|
-
const tabs = tabList.querySelectorAll<HTMLButtonElement>('[role="tab"]')
|
|
346
|
-
tabs[newIndex]?.focus()
|
|
347
|
-
}
|
|
348
|
-
},
|
|
349
|
-
[sections.length],
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
const applyTemplate = useCallback((templateId: string) => {
|
|
353
|
-
const template = TEMPLATES.find((t) => t.id === templateId)
|
|
354
|
-
if (template) {
|
|
355
|
-
// Deep clone the template sections to avoid mutation
|
|
356
|
-
setSections(JSON.parse(JSON.stringify(template.sections)))
|
|
357
|
-
}
|
|
358
|
-
}, [])
|
|
359
|
-
|
|
360
|
-
// Path-based section operations
|
|
361
|
-
const updateSectionByPath = useCallback(
|
|
362
|
-
(path: number[], updates: Partial<MockSection>) => {
|
|
363
|
-
setSections((prev) =>
|
|
364
|
-
updateSectionAtPath(prev, path, (section) => ({
|
|
365
|
-
...section,
|
|
366
|
-
...updates,
|
|
367
|
-
})),
|
|
368
|
-
)
|
|
369
|
-
},
|
|
370
|
-
[],
|
|
371
|
-
)
|
|
372
|
-
|
|
373
|
-
const removeSectionByPath = useCallback(
|
|
374
|
-
(path: number[]) => {
|
|
375
|
-
setSections((prev) => {
|
|
376
|
-
const newSections = removeSectionAtPath(prev, path)
|
|
377
|
-
// Adjust active section if needed
|
|
378
|
-
if (path.length === 1) {
|
|
379
|
-
const removedIndex = path[0]
|
|
380
|
-
if (activeSection >= newSections.length) {
|
|
381
|
-
setActiveSection(Math.max(0, newSections.length - 1))
|
|
382
|
-
} else if (activeSection > removedIndex) {
|
|
383
|
-
setActiveSection(activeSection - 1)
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return newSections
|
|
387
|
-
})
|
|
388
|
-
},
|
|
389
|
-
[activeSection],
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
const addChildSection = useCallback((path: number[]) => {
|
|
393
|
-
setSections((prev) => addChildSectionAtPath(prev, path))
|
|
394
|
-
}, [])
|
|
395
|
-
|
|
396
|
-
const addElementByPath = useCallback((path: number[], type: ElementType) => {
|
|
397
|
-
setSections((prev) =>
|
|
398
|
-
updateSectionAtPath(prev, path, (section) => ({
|
|
399
|
-
...section,
|
|
400
|
-
elements: [...section.elements, createDefaultElement(type)],
|
|
401
|
-
})),
|
|
402
|
-
)
|
|
403
|
-
}, [])
|
|
404
|
-
|
|
405
|
-
const removeElementByPath = useCallback(
|
|
406
|
-
(path: number[], elementIndex: number) => {
|
|
407
|
-
setSections((prev) =>
|
|
408
|
-
updateSectionAtPath(prev, path, (section) => ({
|
|
409
|
-
...section,
|
|
410
|
-
elements: section.elements.filter((_, j) => j !== elementIndex),
|
|
411
|
-
})),
|
|
412
|
-
)
|
|
413
|
-
},
|
|
414
|
-
[],
|
|
415
|
-
)
|
|
416
|
-
|
|
417
|
-
const updateElementByPath = useCallback(
|
|
418
|
-
(path: number[], elementIndex: number, updates: Partial<MockElement>) => {
|
|
419
|
-
setSections((prev) =>
|
|
420
|
-
updateSectionAtPath(prev, path, (section) => ({
|
|
421
|
-
...section,
|
|
422
|
-
elements: section.elements.map((el, j) =>
|
|
423
|
-
j === elementIndex ? ({ ...el, ...updates } as MockElement) : el,
|
|
424
|
-
),
|
|
425
|
-
})),
|
|
426
|
-
)
|
|
427
|
-
},
|
|
428
|
-
[],
|
|
429
|
-
)
|
|
430
|
-
|
|
431
|
-
const exportMock = useCallback(() => {
|
|
432
|
-
const mock: ScreenMock = { sections }
|
|
433
|
-
const code = `mock: ${JSON.stringify(mock, null, 2)}`
|
|
434
|
-
console.log(code)
|
|
435
|
-
void window.navigator.clipboard.writeText(code)
|
|
436
|
-
window.alert("Copied to clipboard!")
|
|
437
|
-
}, [sections])
|
|
438
|
-
|
|
439
|
-
const saveMock = useCallback(async () => {
|
|
440
|
-
if (!screenId) {
|
|
441
|
-
window.alert(
|
|
442
|
-
"Cannot save: No screen ID. Use 'Export Code' to copy to clipboard.",
|
|
443
|
-
)
|
|
444
|
-
return
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
setSaveStatus("saving")
|
|
448
|
-
setSaveError(null)
|
|
449
|
-
|
|
450
|
-
try {
|
|
451
|
-
const response = await fetch("/api/save-mock", {
|
|
452
|
-
method: "POST",
|
|
453
|
-
headers: { "Content-Type": "application/json" },
|
|
454
|
-
body: JSON.stringify({
|
|
455
|
-
screenId,
|
|
456
|
-
mock: { sections },
|
|
457
|
-
}),
|
|
458
|
-
})
|
|
459
|
-
|
|
460
|
-
const result = (await response.json()) as { error?: string }
|
|
461
|
-
|
|
462
|
-
if (!response.ok) {
|
|
463
|
-
throw new Error(result.error || "Failed to save")
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
setSaveStatus("saved")
|
|
467
|
-
setTimeout(() => setSaveStatus("idle"), 2000)
|
|
468
|
-
} catch (error) {
|
|
469
|
-
setSaveStatus("error")
|
|
470
|
-
setSaveError(error instanceof Error ? error.message : "Unknown error")
|
|
471
|
-
}
|
|
472
|
-
}, [screenId, sections])
|
|
473
|
-
|
|
474
|
-
const currentSection = sections[activeSection]
|
|
475
|
-
|
|
476
|
-
return (
|
|
477
|
-
<div className="mock-editor">
|
|
478
|
-
{/* Skip Link for Accessibility */}
|
|
479
|
-
<a href="#main-editor" className="mock-editor__skip-link">
|
|
480
|
-
Skip to editor
|
|
481
|
-
</a>
|
|
482
|
-
|
|
483
|
-
{/* Left Sidebar - Section Navigation */}
|
|
484
|
-
<nav className="mock-editor__sidebar" aria-label="Section navigation">
|
|
485
|
-
<div className="mock-editor__sidebar-header">
|
|
486
|
-
<span className="mock-editor__title">Mock Editor</span>
|
|
487
|
-
{screenId && (
|
|
488
|
-
<div className="mock-editor__screen-info">
|
|
489
|
-
<span className="mock-editor__screen-label">SCREEN</span>
|
|
490
|
-
<span className="mock-editor__screen-badge">{screenId}</span>
|
|
491
|
-
</div>
|
|
492
|
-
)}
|
|
493
|
-
</div>
|
|
494
|
-
|
|
495
|
-
<div className="mock-editor__sidebar-content">
|
|
496
|
-
<h2
|
|
497
|
-
id={`${uniqueId}-sections-label`}
|
|
498
|
-
className="mock-editor__sections-label"
|
|
499
|
-
>
|
|
500
|
-
SECTIONS
|
|
501
|
-
</h2>
|
|
502
|
-
<div
|
|
503
|
-
ref={tabListRef}
|
|
504
|
-
role="tablist"
|
|
505
|
-
aria-labelledby={`${uniqueId}-sections-label`}
|
|
506
|
-
aria-orientation="vertical"
|
|
507
|
-
className="mock-editor__tab-list"
|
|
508
|
-
>
|
|
509
|
-
{sections.map((section, index) => (
|
|
510
|
-
<button
|
|
511
|
-
key={index}
|
|
512
|
-
type="button"
|
|
513
|
-
role="tab"
|
|
514
|
-
id={`${uniqueId}-tab-${index}`}
|
|
515
|
-
aria-selected={activeSection === index}
|
|
516
|
-
aria-controls={`${uniqueId}-panel-${index}`}
|
|
517
|
-
tabIndex={activeSection === index ? 0 : -1}
|
|
518
|
-
className={`mock-editor__section-tab ${activeSection === index ? "mock-editor__section-tab--active" : ""}`}
|
|
519
|
-
onClick={() => setActiveSection(index)}
|
|
520
|
-
onKeyDown={(e) => handleTabKeyDown(e, index)}
|
|
521
|
-
>
|
|
522
|
-
<span className="mock-editor__tab-icon">
|
|
523
|
-
{(section.title || "Section")[0].toUpperCase()}
|
|
524
|
-
</span>
|
|
525
|
-
<span className="mock-editor__tab-content">
|
|
526
|
-
<span className="mock-editor__tab-title">
|
|
527
|
-
{section.title || "Untitled"}
|
|
528
|
-
</span>
|
|
529
|
-
<span className="mock-editor__tab-count">
|
|
530
|
-
{section.elements.length} element
|
|
531
|
-
{section.elements.length !== 1 ? "s" : ""}
|
|
532
|
-
</span>
|
|
533
|
-
</span>
|
|
534
|
-
{activeSection === index && (
|
|
535
|
-
<span
|
|
536
|
-
className="mock-editor__tab-indicator"
|
|
537
|
-
aria-hidden="true"
|
|
538
|
-
/>
|
|
539
|
-
)}
|
|
540
|
-
</button>
|
|
541
|
-
))}
|
|
542
|
-
</div>
|
|
543
|
-
|
|
544
|
-
<button
|
|
545
|
-
type="button"
|
|
546
|
-
onClick={addSection}
|
|
547
|
-
className="mock-editor__add-section-btn"
|
|
548
|
-
>
|
|
549
|
-
+ Add Section
|
|
550
|
-
</button>
|
|
551
|
-
</div>
|
|
552
|
-
</nav>
|
|
553
|
-
|
|
554
|
-
{/* Center Panel - Editor */}
|
|
555
|
-
<section
|
|
556
|
-
id="main-editor"
|
|
557
|
-
className="mock-editor__main"
|
|
558
|
-
aria-label="Section editor"
|
|
559
|
-
>
|
|
560
|
-
{/* Editor Header */}
|
|
561
|
-
<header className="mock-editor__header">
|
|
562
|
-
<div className="mock-editor__header-left">
|
|
563
|
-
<label className="mock-editor__field-label">
|
|
564
|
-
<span className="mock-editor__field-label-text">
|
|
565
|
-
Section title
|
|
566
|
-
</span>
|
|
567
|
-
<input
|
|
568
|
-
type="text"
|
|
569
|
-
value={currentSection?.title || ""}
|
|
570
|
-
onChange={(e) =>
|
|
571
|
-
updateSectionByPath([activeSection], {
|
|
572
|
-
title: e.target.value,
|
|
573
|
-
})
|
|
574
|
-
}
|
|
575
|
-
placeholder="Section title"
|
|
576
|
-
className="mock-editor__section-input"
|
|
577
|
-
aria-describedby={`${uniqueId}-title-hint`}
|
|
578
|
-
/>
|
|
579
|
-
<span
|
|
580
|
-
id={`${uniqueId}-title-hint`}
|
|
581
|
-
className="mock-editor__sr-only"
|
|
582
|
-
>
|
|
583
|
-
Edit the section title
|
|
584
|
-
</span>
|
|
585
|
-
</label>
|
|
586
|
-
|
|
587
|
-
<fieldset className="mock-editor__layout-group">
|
|
588
|
-
<legend className="mock-editor__sr-only">Section layout</legend>
|
|
589
|
-
{(["horizontal", "vertical", "grid"] as const).map((layout) => (
|
|
590
|
-
<label
|
|
591
|
-
key={layout}
|
|
592
|
-
className={`mock-editor__layout-option ${currentSection?.layout === layout || (!currentSection?.layout && layout === "vertical") ? "mock-editor__layout-option--active" : ""}`}
|
|
593
|
-
>
|
|
594
|
-
<input
|
|
595
|
-
type="radio"
|
|
596
|
-
name={`${uniqueId}-layout`}
|
|
597
|
-
value={layout}
|
|
598
|
-
checked={
|
|
599
|
-
currentSection?.layout === layout ||
|
|
600
|
-
(!currentSection?.layout && layout === "vertical")
|
|
601
|
-
}
|
|
602
|
-
onChange={() =>
|
|
603
|
-
updateSectionByPath([activeSection], { layout })
|
|
604
|
-
}
|
|
605
|
-
className="mock-editor__sr-only"
|
|
606
|
-
/>
|
|
607
|
-
<span className="mock-editor__layout-label">
|
|
608
|
-
{layout.charAt(0).toUpperCase() + layout.slice(1)}
|
|
609
|
-
</span>
|
|
610
|
-
</label>
|
|
611
|
-
))}
|
|
612
|
-
</fieldset>
|
|
613
|
-
</div>
|
|
614
|
-
|
|
615
|
-
<div className="mock-editor__header-right">
|
|
616
|
-
<label
|
|
617
|
-
className="mock-editor__sr-only"
|
|
618
|
-
htmlFor={`${uniqueId}-template`}
|
|
619
|
-
>
|
|
620
|
-
Select template
|
|
621
|
-
</label>
|
|
622
|
-
<select
|
|
623
|
-
id={`${uniqueId}-template`}
|
|
624
|
-
className="mock-editor__template-select"
|
|
625
|
-
onChange={(e) => {
|
|
626
|
-
if (e.target.value) {
|
|
627
|
-
applyTemplate(e.target.value)
|
|
628
|
-
e.target.value = ""
|
|
629
|
-
}
|
|
630
|
-
}}
|
|
631
|
-
defaultValue=""
|
|
632
|
-
>
|
|
633
|
-
<option value="" disabled>
|
|
634
|
-
Templates
|
|
635
|
-
</option>
|
|
636
|
-
{TEMPLATES.map((template) => (
|
|
637
|
-
<option key={template.id} value={template.id}>
|
|
638
|
-
{template.label}
|
|
639
|
-
</option>
|
|
640
|
-
))}
|
|
641
|
-
</select>
|
|
642
|
-
|
|
643
|
-
{saveStatus === "error" && (
|
|
644
|
-
<span
|
|
645
|
-
className="mock-editor__status mock-editor__status--error"
|
|
646
|
-
role="alert"
|
|
647
|
-
>
|
|
648
|
-
{saveError}
|
|
649
|
-
</span>
|
|
650
|
-
)}
|
|
651
|
-
{saveStatus === "saved" && (
|
|
652
|
-
<output className="mock-editor__status mock-editor__status--success">
|
|
653
|
-
Saved!
|
|
654
|
-
</output>
|
|
655
|
-
)}
|
|
656
|
-
|
|
657
|
-
<button
|
|
658
|
-
type="button"
|
|
659
|
-
onClick={exportMock}
|
|
660
|
-
className="mock-editor__btn mock-editor__btn--secondary"
|
|
661
|
-
>
|
|
662
|
-
Copy Code
|
|
663
|
-
</button>
|
|
664
|
-
<button
|
|
665
|
-
type="button"
|
|
666
|
-
onClick={saveMock}
|
|
667
|
-
disabled={saveStatus === "saving" || !screenId}
|
|
668
|
-
className="mock-editor__btn mock-editor__btn--primary"
|
|
669
|
-
title={!screenId ? "Save requires a screen ID" : undefined}
|
|
670
|
-
>
|
|
671
|
-
{saveStatus === "saving" ? "Saving..." : "Save"}
|
|
672
|
-
</button>
|
|
673
|
-
</div>
|
|
674
|
-
</header>
|
|
675
|
-
|
|
676
|
-
{/* Editor Content - Active Section */}
|
|
677
|
-
<div
|
|
678
|
-
id={`${uniqueId}-panel-${activeSection}`}
|
|
679
|
-
role="tabpanel"
|
|
680
|
-
aria-labelledby={`${uniqueId}-tab-${activeSection}`}
|
|
681
|
-
className="mock-editor__editor-content"
|
|
682
|
-
>
|
|
683
|
-
{currentSection && (
|
|
684
|
-
<>
|
|
685
|
-
<h2 className="mock-editor__sr-only">
|
|
686
|
-
{currentSection.title || "Untitled"} section editor
|
|
687
|
-
</h2>
|
|
688
|
-
|
|
689
|
-
{/* Elements */}
|
|
690
|
-
<div className="mock-editor__elements">
|
|
691
|
-
{currentSection.elements.map((element, elementIndex) => (
|
|
692
|
-
<ElementEditor
|
|
693
|
-
key={elementIndex}
|
|
694
|
-
element={element}
|
|
695
|
-
onChange={(updates) =>
|
|
696
|
-
updateElementByPath(
|
|
697
|
-
[activeSection],
|
|
698
|
-
elementIndex,
|
|
699
|
-
updates,
|
|
700
|
-
)
|
|
701
|
-
}
|
|
702
|
-
onRemove={() =>
|
|
703
|
-
removeElementByPath([activeSection], elementIndex)
|
|
704
|
-
}
|
|
705
|
-
/>
|
|
706
|
-
))}
|
|
707
|
-
|
|
708
|
-
{currentSection.elements.length === 0 && (
|
|
709
|
-
<div className="mock-editor__empty-elements">
|
|
710
|
-
No elements yet. Add elements using the buttons below.
|
|
711
|
-
</div>
|
|
712
|
-
)}
|
|
713
|
-
</div>
|
|
714
|
-
|
|
715
|
-
{/* Add Element Buttons */}
|
|
716
|
-
<fieldset className="mock-editor__element-buttons">
|
|
717
|
-
<legend className="mock-editor__sr-only">Add element</legend>
|
|
718
|
-
{ELEMENT_TYPES.map((type) => (
|
|
719
|
-
<button
|
|
720
|
-
key={type.value}
|
|
721
|
-
type="button"
|
|
722
|
-
onClick={() =>
|
|
723
|
-
addElementByPath([activeSection], type.value)
|
|
724
|
-
}
|
|
725
|
-
className={`mock-editor__add-element-btn mock-editor__add-element-btn--${type.value}`}
|
|
726
|
-
>
|
|
727
|
-
+ {type.label}
|
|
728
|
-
</button>
|
|
729
|
-
))}
|
|
730
|
-
</fieldset>
|
|
731
|
-
|
|
732
|
-
{/* Child Sections */}
|
|
733
|
-
{currentSection.children &&
|
|
734
|
-
currentSection.children.length > 0 && (
|
|
735
|
-
<div className="mock-editor__children">
|
|
736
|
-
<h3 className="mock-editor__children-title">
|
|
737
|
-
Child Sections
|
|
738
|
-
</h3>
|
|
739
|
-
{currentSection.children.map((childSection, childIndex) => (
|
|
740
|
-
<SectionEditor
|
|
741
|
-
key={childIndex}
|
|
742
|
-
section={childSection}
|
|
743
|
-
path={[activeSection, childIndex]}
|
|
744
|
-
depth={1}
|
|
745
|
-
onUpdate={updateSectionByPath}
|
|
746
|
-
onRemove={removeSectionByPath}
|
|
747
|
-
onAddChild={addChildSection}
|
|
748
|
-
onAddElement={addElementByPath}
|
|
749
|
-
onRemoveElement={removeElementByPath}
|
|
750
|
-
onUpdateElement={updateElementByPath}
|
|
751
|
-
/>
|
|
752
|
-
))}
|
|
753
|
-
</div>
|
|
754
|
-
)}
|
|
755
|
-
|
|
756
|
-
<button
|
|
757
|
-
type="button"
|
|
758
|
-
onClick={() => addChildSection([activeSection])}
|
|
759
|
-
className="mock-editor__add-child-btn"
|
|
760
|
-
aria-label="Add child section to current section"
|
|
761
|
-
>
|
|
762
|
-
+ Add Child Section
|
|
763
|
-
</button>
|
|
764
|
-
</>
|
|
765
|
-
)}
|
|
766
|
-
</div>
|
|
767
|
-
</section>
|
|
768
|
-
|
|
769
|
-
{/* Right Panel - Preview */}
|
|
770
|
-
<aside className="mock-editor__preview-panel" aria-label="Live preview">
|
|
771
|
-
<div className="mock-editor__preview-header">
|
|
772
|
-
<span className="mock-editor__preview-label">LIVE PREVIEW</span>
|
|
773
|
-
<span className="mock-editor__preview-scale">100%</span>
|
|
774
|
-
</div>
|
|
775
|
-
<div className="mock-editor__preview-wrapper">
|
|
776
|
-
<MockPreview sections={sections} title={screenTitle} />
|
|
777
|
-
</div>
|
|
778
|
-
</aside>
|
|
779
|
-
</div>
|
|
780
|
-
)
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
// Section Editor Component (recursive for child sections)
|
|
784
|
-
function SectionEditor({
|
|
785
|
-
section,
|
|
786
|
-
path,
|
|
787
|
-
depth,
|
|
788
|
-
onUpdate,
|
|
789
|
-
onRemove,
|
|
790
|
-
onAddChild,
|
|
791
|
-
onAddElement,
|
|
792
|
-
onRemoveElement,
|
|
793
|
-
onUpdateElement,
|
|
794
|
-
}: {
|
|
795
|
-
section: MockSection
|
|
796
|
-
path: number[]
|
|
797
|
-
depth: number
|
|
798
|
-
onUpdate: (path: number[], updates: Partial<MockSection>) => void
|
|
799
|
-
onRemove: (path: number[]) => void
|
|
800
|
-
onAddChild: (path: number[]) => void
|
|
801
|
-
onAddElement: (path: number[], type: ElementType) => void
|
|
802
|
-
onRemoveElement: (path: number[], elementIndex: number) => void
|
|
803
|
-
onUpdateElement: (
|
|
804
|
-
path: number[],
|
|
805
|
-
elementIndex: number,
|
|
806
|
-
updates: Partial<MockElement>,
|
|
807
|
-
) => void
|
|
808
|
-
}) {
|
|
809
|
-
const sectionClass = `mock-editor__section${depth > 0 ? " mock-editor__section--child" : ""}`
|
|
810
|
-
|
|
811
|
-
return (
|
|
812
|
-
<div className={sectionClass}>
|
|
813
|
-
{/* Section Header */}
|
|
814
|
-
<div className="mock-editor__section-header">
|
|
815
|
-
<input
|
|
816
|
-
type="text"
|
|
817
|
-
value={section.title || ""}
|
|
818
|
-
onChange={(e) => onUpdate(path, { title: e.target.value })}
|
|
819
|
-
placeholder="Section title"
|
|
820
|
-
className="mock-editor__section-input"
|
|
821
|
-
/>
|
|
822
|
-
<select
|
|
823
|
-
value={section.layout || "vertical"}
|
|
824
|
-
onChange={(e) =>
|
|
825
|
-
onUpdate(path, { layout: e.target.value as MockLayout })
|
|
826
|
-
}
|
|
827
|
-
className="mock-editor__layout-select"
|
|
828
|
-
>
|
|
829
|
-
<option value="vertical">Vertical</option>
|
|
830
|
-
<option value="horizontal">Horizontal</option>
|
|
831
|
-
<option value="grid">Grid</option>
|
|
832
|
-
</select>
|
|
833
|
-
{depth > 0 && (
|
|
834
|
-
<span className="mock-editor__depth-badge">L{depth}</span>
|
|
835
|
-
)}
|
|
836
|
-
<button
|
|
837
|
-
type="button"
|
|
838
|
-
onClick={() => onRemove(path)}
|
|
839
|
-
className="mock-editor__remove-btn"
|
|
840
|
-
>
|
|
841
|
-
×
|
|
842
|
-
</button>
|
|
843
|
-
</div>
|
|
844
|
-
|
|
845
|
-
{/* Elements */}
|
|
846
|
-
<div className="mock-editor__section-body">
|
|
847
|
-
{section.elements.map((element, elementIndex) => (
|
|
848
|
-
<ElementEditor
|
|
849
|
-
key={elementIndex}
|
|
850
|
-
element={element}
|
|
851
|
-
onChange={(updates) => onUpdateElement(path, elementIndex, updates)}
|
|
852
|
-
onRemove={() => onRemoveElement(path, elementIndex)}
|
|
853
|
-
/>
|
|
854
|
-
))}
|
|
855
|
-
|
|
856
|
-
{section.elements.length === 0 && (
|
|
857
|
-
<div className="mock-editor__empty-elements">No elements yet</div>
|
|
858
|
-
)}
|
|
859
|
-
|
|
860
|
-
{/* Add Element Buttons */}
|
|
861
|
-
<div className="mock-editor__element-buttons">
|
|
862
|
-
{ELEMENT_TYPES.map((type) => (
|
|
863
|
-
<button
|
|
864
|
-
key={type.value}
|
|
865
|
-
type="button"
|
|
866
|
-
onClick={() => onAddElement(path, type.value)}
|
|
867
|
-
className={`mock-editor__add-element-btn mock-editor__add-element-btn--${type.value}`}
|
|
868
|
-
>
|
|
869
|
-
+ {type.label}
|
|
870
|
-
</button>
|
|
871
|
-
))}
|
|
872
|
-
</div>
|
|
873
|
-
</div>
|
|
874
|
-
|
|
875
|
-
{/* Add Child Section Button */}
|
|
876
|
-
<div className="mock-editor__section-footer">
|
|
877
|
-
<button
|
|
878
|
-
type="button"
|
|
879
|
-
onClick={() => onAddChild(path)}
|
|
880
|
-
className="mock-editor__add-child-btn"
|
|
881
|
-
>
|
|
882
|
-
+ Add Child Section
|
|
883
|
-
</button>
|
|
884
|
-
</div>
|
|
885
|
-
|
|
886
|
-
{/* Render Child Sections Recursively */}
|
|
887
|
-
{section.children && section.children.length > 0 && (
|
|
888
|
-
<div className="mock-editor__children">
|
|
889
|
-
{section.children.map((childSection, childIndex) => (
|
|
890
|
-
<SectionEditor
|
|
891
|
-
key={childIndex}
|
|
892
|
-
section={childSection}
|
|
893
|
-
path={[...path, childIndex]}
|
|
894
|
-
depth={depth + 1}
|
|
895
|
-
onUpdate={onUpdate}
|
|
896
|
-
onRemove={onRemove}
|
|
897
|
-
onAddChild={onAddChild}
|
|
898
|
-
onAddElement={onAddElement}
|
|
899
|
-
onRemoveElement={onRemoveElement}
|
|
900
|
-
onUpdateElement={onUpdateElement}
|
|
901
|
-
/>
|
|
902
|
-
))}
|
|
903
|
-
</div>
|
|
904
|
-
)}
|
|
905
|
-
</div>
|
|
906
|
-
)
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
// Element Editor Component
|
|
910
|
-
function ElementEditor({
|
|
911
|
-
element,
|
|
912
|
-
onChange,
|
|
913
|
-
onRemove,
|
|
914
|
-
}: {
|
|
915
|
-
element: MockElement
|
|
916
|
-
onChange: (updates: Partial<MockElement>) => void
|
|
917
|
-
onRemove: () => void
|
|
918
|
-
}) {
|
|
919
|
-
const elementClass = `mock-editor__element mock-editor__element--${element.type}`
|
|
920
|
-
const typeClass = `mock-editor__element-type mock-editor__element-type--${element.type}`
|
|
921
|
-
|
|
922
|
-
return (
|
|
923
|
-
<div className={elementClass}>
|
|
924
|
-
<div className="mock-editor__element-header">
|
|
925
|
-
<span className={typeClass}>{element.type}</span>
|
|
926
|
-
<button
|
|
927
|
-
type="button"
|
|
928
|
-
onClick={onRemove}
|
|
929
|
-
className="mock-editor__remove-btn"
|
|
930
|
-
>
|
|
931
|
-
×
|
|
932
|
-
</button>
|
|
933
|
-
</div>
|
|
934
|
-
|
|
935
|
-
<div className="mock-editor__element-fields">
|
|
936
|
-
<input
|
|
937
|
-
type="text"
|
|
938
|
-
value={element.label}
|
|
939
|
-
onChange={(e) => onChange({ label: e.target.value })}
|
|
940
|
-
placeholder="Label"
|
|
941
|
-
className="mock-editor__field-input"
|
|
942
|
-
/>
|
|
943
|
-
|
|
944
|
-
{element.type === "button" && (
|
|
945
|
-
<>
|
|
946
|
-
<select
|
|
947
|
-
value={(element as any).variant || "secondary"}
|
|
948
|
-
onChange={(e) => onChange({ variant: e.target.value as any })}
|
|
949
|
-
className="mock-editor__field-select"
|
|
950
|
-
>
|
|
951
|
-
<option value="primary">Primary</option>
|
|
952
|
-
<option value="secondary">Secondary</option>
|
|
953
|
-
<option value="danger">Danger</option>
|
|
954
|
-
</select>
|
|
955
|
-
<input
|
|
956
|
-
type="text"
|
|
957
|
-
value={(element as any).navigateTo || ""}
|
|
958
|
-
onChange={(e) =>
|
|
959
|
-
onChange({ navigateTo: e.target.value || undefined })
|
|
960
|
-
}
|
|
961
|
-
placeholder="Navigate to (screen id)"
|
|
962
|
-
className="mock-editor__field-input"
|
|
963
|
-
/>
|
|
964
|
-
</>
|
|
965
|
-
)}
|
|
966
|
-
|
|
967
|
-
{element.type === "input" && (
|
|
968
|
-
<>
|
|
969
|
-
<select
|
|
970
|
-
value={(element as any).inputType || "text"}
|
|
971
|
-
onChange={(e) => onChange({ inputType: e.target.value as any })}
|
|
972
|
-
className="mock-editor__field-select"
|
|
973
|
-
>
|
|
974
|
-
<option value="text">Text</option>
|
|
975
|
-
<option value="email">Email</option>
|
|
976
|
-
<option value="password">Password</option>
|
|
977
|
-
<option value="textarea">Textarea</option>
|
|
978
|
-
<option value="search">Search</option>
|
|
979
|
-
</select>
|
|
980
|
-
<input
|
|
981
|
-
type="text"
|
|
982
|
-
value={(element as any).placeholder || ""}
|
|
983
|
-
onChange={(e) => onChange({ placeholder: e.target.value })}
|
|
984
|
-
placeholder="Placeholder text"
|
|
985
|
-
className="mock-editor__field-input"
|
|
986
|
-
/>
|
|
987
|
-
</>
|
|
988
|
-
)}
|
|
989
|
-
|
|
990
|
-
{element.type === "text" && (
|
|
991
|
-
<select
|
|
992
|
-
value={(element as any).variant || "body"}
|
|
993
|
-
onChange={(e) => onChange({ variant: e.target.value as any })}
|
|
994
|
-
className="mock-editor__field-select"
|
|
995
|
-
>
|
|
996
|
-
<option value="heading">Heading</option>
|
|
997
|
-
<option value="subheading">Subheading</option>
|
|
998
|
-
<option value="body">Body</option>
|
|
999
|
-
<option value="caption">Caption</option>
|
|
1000
|
-
</select>
|
|
1001
|
-
)}
|
|
1002
|
-
|
|
1003
|
-
{element.type === "link" && (
|
|
1004
|
-
<input
|
|
1005
|
-
type="text"
|
|
1006
|
-
value={(element as any).navigateTo || ""}
|
|
1007
|
-
onChange={(e) =>
|
|
1008
|
-
onChange({ navigateTo: e.target.value || undefined })
|
|
1009
|
-
}
|
|
1010
|
-
placeholder="Navigate to (screen id)"
|
|
1011
|
-
className="mock-editor__field-input"
|
|
1012
|
-
/>
|
|
1013
|
-
)}
|
|
1014
|
-
|
|
1015
|
-
{element.type === "image" && (
|
|
1016
|
-
<select
|
|
1017
|
-
value={(element as any).aspectRatio || "16:9"}
|
|
1018
|
-
onChange={(e) => onChange({ aspectRatio: e.target.value })}
|
|
1019
|
-
className="mock-editor__field-select"
|
|
1020
|
-
>
|
|
1021
|
-
<option value="16:9">16:9</option>
|
|
1022
|
-
<option value="4:3">4:3</option>
|
|
1023
|
-
<option value="1:1">1:1</option>
|
|
1024
|
-
<option value="3:4">3:4</option>
|
|
1025
|
-
</select>
|
|
1026
|
-
)}
|
|
1027
|
-
|
|
1028
|
-
{element.type === "list" && (
|
|
1029
|
-
<div className="mock-editor__field-row">
|
|
1030
|
-
<input
|
|
1031
|
-
type="number"
|
|
1032
|
-
value={(element as any).itemCount || 3}
|
|
1033
|
-
onChange={(e) =>
|
|
1034
|
-
onChange({ itemCount: parseInt(e.target.value, 10) || 3 })
|
|
1035
|
-
}
|
|
1036
|
-
placeholder="Items"
|
|
1037
|
-
min={1}
|
|
1038
|
-
max={10}
|
|
1039
|
-
className="mock-editor__field-input mock-editor__field-input--small"
|
|
1040
|
-
/>
|
|
1041
|
-
<input
|
|
1042
|
-
type="text"
|
|
1043
|
-
value={(element as any).itemNavigateTo || ""}
|
|
1044
|
-
onChange={(e) =>
|
|
1045
|
-
onChange({ itemNavigateTo: e.target.value || undefined })
|
|
1046
|
-
}
|
|
1047
|
-
placeholder="Navigate to screen..."
|
|
1048
|
-
className="mock-editor__field-input"
|
|
1049
|
-
/>
|
|
1050
|
-
</div>
|
|
1051
|
-
)}
|
|
1052
|
-
|
|
1053
|
-
{element.type === "table" && (
|
|
1054
|
-
<>
|
|
1055
|
-
<input
|
|
1056
|
-
type="text"
|
|
1057
|
-
value={(element as any).columns?.join(", ") || ""}
|
|
1058
|
-
onChange={(e) =>
|
|
1059
|
-
onChange({
|
|
1060
|
-
columns: e.target.value
|
|
1061
|
-
.split(",")
|
|
1062
|
-
.map((s: string) => s.trim())
|
|
1063
|
-
.filter(Boolean),
|
|
1064
|
-
})
|
|
1065
|
-
}
|
|
1066
|
-
placeholder="Columns (comma separated)"
|
|
1067
|
-
className="mock-editor__field-input"
|
|
1068
|
-
/>
|
|
1069
|
-
<div className="mock-editor__field-row">
|
|
1070
|
-
<input
|
|
1071
|
-
type="number"
|
|
1072
|
-
value={(element as any).rowCount || 3}
|
|
1073
|
-
onChange={(e) =>
|
|
1074
|
-
onChange({ rowCount: parseInt(e.target.value, 10) || 3 })
|
|
1075
|
-
}
|
|
1076
|
-
placeholder="Rows"
|
|
1077
|
-
min={1}
|
|
1078
|
-
max={10}
|
|
1079
|
-
className="mock-editor__field-input mock-editor__field-input--small"
|
|
1080
|
-
/>
|
|
1081
|
-
<input
|
|
1082
|
-
type="text"
|
|
1083
|
-
value={(element as any).rowNavigateTo || ""}
|
|
1084
|
-
onChange={(e) =>
|
|
1085
|
-
onChange({ rowNavigateTo: e.target.value || undefined })
|
|
1086
|
-
}
|
|
1087
|
-
placeholder="Navigate to screen..."
|
|
1088
|
-
className="mock-editor__field-input"
|
|
1089
|
-
/>
|
|
1090
|
-
</div>
|
|
1091
|
-
</>
|
|
1092
|
-
)}
|
|
1093
|
-
</div>
|
|
1094
|
-
</div>
|
|
1095
|
-
)
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// Simple Preview Component (inline version of MockPreview)
|
|
1099
|
-
function MockPreview({
|
|
1100
|
-
sections,
|
|
1101
|
-
title,
|
|
1102
|
-
}: {
|
|
1103
|
-
sections: MockSection[]
|
|
1104
|
-
title?: string
|
|
1105
|
-
}) {
|
|
1106
|
-
return (
|
|
1107
|
-
<div className="mock-editor__preview">
|
|
1108
|
-
{title && <div className="mock-editor__preview-header">{title}</div>}
|
|
1109
|
-
<div className="mock-editor__preview-body">
|
|
1110
|
-
{sections.map((section, i) => (
|
|
1111
|
-
<SectionPreview key={i} section={section} depth={0} />
|
|
1112
|
-
))}
|
|
1113
|
-
{sections.length === 0 && (
|
|
1114
|
-
<div className="mock-editor__preview-empty">
|
|
1115
|
-
Add sections to see preview
|
|
1116
|
-
</div>
|
|
1117
|
-
)}
|
|
1118
|
-
</div>
|
|
1119
|
-
</div>
|
|
1120
|
-
)
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
// Section Preview Component (recursive for child sections)
|
|
1124
|
-
function SectionPreview({
|
|
1125
|
-
section,
|
|
1126
|
-
depth,
|
|
1127
|
-
}: {
|
|
1128
|
-
section: MockSection
|
|
1129
|
-
depth: number
|
|
1130
|
-
}) {
|
|
1131
|
-
const sectionClass = `mock-editor__preview-section${depth > 0 ? " mock-editor__preview-section--child" : ""}`
|
|
1132
|
-
const titleClass = `mock-editor__preview-section-title${depth > 0 ? " mock-editor__preview-section-title--child" : ""}`
|
|
1133
|
-
|
|
1134
|
-
const bodyClass = `mock-editor__preview-section-body${
|
|
1135
|
-
section.layout === "horizontal"
|
|
1136
|
-
? " mock-editor__preview-section-body--horizontal"
|
|
1137
|
-
: section.layout === "grid"
|
|
1138
|
-
? " mock-editor__preview-section-body--grid"
|
|
1139
|
-
: ""
|
|
1140
|
-
}`
|
|
1141
|
-
|
|
1142
|
-
return (
|
|
1143
|
-
<div className={sectionClass}>
|
|
1144
|
-
{section.title && <div className={titleClass}>{section.title}</div>}
|
|
1145
|
-
<div className={bodyClass}>
|
|
1146
|
-
{section.elements.map((el, j) => (
|
|
1147
|
-
<PreviewElement
|
|
1148
|
-
key={j}
|
|
1149
|
-
element={el}
|
|
1150
|
-
horizontal={section.layout === "horizontal"}
|
|
1151
|
-
/>
|
|
1152
|
-
))}
|
|
1153
|
-
{section.elements.length === 0 &&
|
|
1154
|
-
(!section.children || section.children.length === 0) && (
|
|
1155
|
-
<div className="mock-editor__preview-empty">No elements</div>
|
|
1156
|
-
)}
|
|
1157
|
-
</div>
|
|
1158
|
-
|
|
1159
|
-
{/* Render Child Sections */}
|
|
1160
|
-
{section.children && section.children.length > 0 && (
|
|
1161
|
-
<div style={{ padding: "0 14px 14px" }}>
|
|
1162
|
-
{section.children.map((child, i) => (
|
|
1163
|
-
<SectionPreview key={i} section={child} depth={depth + 1} />
|
|
1164
|
-
))}
|
|
1165
|
-
</div>
|
|
1166
|
-
)}
|
|
1167
|
-
</div>
|
|
1168
|
-
)
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
function PreviewElement({
|
|
1172
|
-
element,
|
|
1173
|
-
horizontal,
|
|
1174
|
-
}: {
|
|
1175
|
-
element: MockElement
|
|
1176
|
-
horizontal?: boolean
|
|
1177
|
-
}) {
|
|
1178
|
-
const wrapperClass = `mock-editor__preview-element${horizontal ? " mock-editor__preview-element--horizontal" : ""}`
|
|
1179
|
-
|
|
1180
|
-
if (element.type === "button") {
|
|
1181
|
-
const variant = (element as any).variant || "secondary"
|
|
1182
|
-
return (
|
|
1183
|
-
<div className={wrapperClass}>
|
|
1184
|
-
<div
|
|
1185
|
-
className={`mock-editor__preview-button mock-editor__preview-button--${variant}`}
|
|
1186
|
-
>
|
|
1187
|
-
{element.label}
|
|
1188
|
-
</div>
|
|
1189
|
-
</div>
|
|
1190
|
-
)
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
if (element.type === "input") {
|
|
1194
|
-
return (
|
|
1195
|
-
<div className={`${wrapperClass} mock-editor__preview-input`}>
|
|
1196
|
-
<span className="mock-editor__preview-input-label">
|
|
1197
|
-
{element.label}
|
|
1198
|
-
</span>
|
|
1199
|
-
<div className="mock-editor__preview-input-field">
|
|
1200
|
-
{(element as any).placeholder || "..."}
|
|
1201
|
-
</div>
|
|
1202
|
-
</div>
|
|
1203
|
-
)
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
if (element.type === "text") {
|
|
1207
|
-
const variant = (element as any).variant || "body"
|
|
1208
|
-
return (
|
|
1209
|
-
<div className={`${wrapperClass} mock-editor__preview-text--${variant}`}>
|
|
1210
|
-
{element.label}
|
|
1211
|
-
</div>
|
|
1212
|
-
)
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
if (element.type === "link") {
|
|
1216
|
-
return (
|
|
1217
|
-
<div className={`${wrapperClass} mock-editor__preview-link`}>
|
|
1218
|
-
{element.label}
|
|
1219
|
-
</div>
|
|
1220
|
-
)
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
if (element.type === "image") {
|
|
1224
|
-
const ratio = (element as any).aspectRatio || "16:9"
|
|
1225
|
-
const [w, h] = ratio.split(":").map(Number)
|
|
1226
|
-
return (
|
|
1227
|
-
<div
|
|
1228
|
-
className={`${wrapperClass} mock-editor__preview-image`}
|
|
1229
|
-
style={{ aspectRatio: `${w}/${h}` }}
|
|
1230
|
-
>
|
|
1231
|
-
{element.label}
|
|
1232
|
-
</div>
|
|
1233
|
-
)
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
if (element.type === "list") {
|
|
1237
|
-
return (
|
|
1238
|
-
<div className={`${wrapperClass} mock-editor__preview-list`}>
|
|
1239
|
-
<div className="mock-editor__preview-list-header">{element.label}</div>
|
|
1240
|
-
{Array.from({ length: (element as any).itemCount || 3 }).map((_, i) => (
|
|
1241
|
-
<div key={i} className="mock-editor__preview-list-item">
|
|
1242
|
-
<div className="mock-editor__preview-list-placeholder" />
|
|
1243
|
-
</div>
|
|
1244
|
-
))}
|
|
1245
|
-
</div>
|
|
1246
|
-
)
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
if (element.type === "table") {
|
|
1250
|
-
const columns = (element as any).columns || ["Col 1", "Col 2", "Col 3"]
|
|
1251
|
-
const rowCount = (element as any).rowCount || 3
|
|
1252
|
-
return (
|
|
1253
|
-
<div className={`${wrapperClass} mock-editor__preview-table`}>
|
|
1254
|
-
<div className="mock-editor__preview-table-header">{element.label}</div>
|
|
1255
|
-
<table>
|
|
1256
|
-
<thead>
|
|
1257
|
-
<tr>
|
|
1258
|
-
{columns.map((col: string, i: number) => (
|
|
1259
|
-
<th key={i}>{col}</th>
|
|
1260
|
-
))}
|
|
1261
|
-
</tr>
|
|
1262
|
-
</thead>
|
|
1263
|
-
<tbody>
|
|
1264
|
-
{Array.from({ length: rowCount }).map((_, rowIndex) => (
|
|
1265
|
-
<tr key={rowIndex}>
|
|
1266
|
-
{columns.map((_: string, colIndex: number) => (
|
|
1267
|
-
<td key={colIndex}>
|
|
1268
|
-
<div className="mock-editor__preview-table-placeholder" />
|
|
1269
|
-
</td>
|
|
1270
|
-
))}
|
|
1271
|
-
</tr>
|
|
1272
|
-
))}
|
|
1273
|
-
</tbody>
|
|
1274
|
-
</table>
|
|
1275
|
-
</div>
|
|
1276
|
-
)
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
return <div className={wrapperClass}>[{element.type}]</div>
|
|
1280
|
-
}
|