@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.
Files changed (37) hide show
  1. package/README.md +30 -0
  2. package/dist/client/_astro/coverage.BzPU-EGZ.css +1 -0
  3. package/dist/server/entry.mjs +1 -1
  4. package/dist/server/{manifest_smcahUO6.mjs → manifest_BGl49hHW.mjs} +1 -1
  5. package/dist/server/pages/coverage.astro.mjs +1 -1
  6. package/dist/server/pages/editor.astro.mjs +1 -1
  7. package/dist/server/pages/graph.astro.mjs +1 -1
  8. package/dist/server/pages/impact.astro.mjs +1 -1
  9. package/dist/server/pages/index.astro.mjs +1 -1
  10. package/dist/server/pages/screen/_id_.astro.mjs +1 -1
  11. package/package.json +5 -1
  12. package/.astro/content-assets.mjs +0 -1
  13. package/.astro/content-modules.mjs +0 -1
  14. package/.astro/content.d.ts +0 -199
  15. package/.astro/types.d.ts +0 -2
  16. package/.prettierrc +0 -15
  17. package/CHANGELOG.md +0 -77
  18. package/astro.config.mjs +0 -20
  19. package/dist/client/_astro/coverage.DLKSOM4m.css +0 -1
  20. package/public/logo.svg +0 -5
  21. package/src/components/MockFormEditor.tsx +0 -1280
  22. package/src/components/MockPreview.astro +0 -811
  23. package/src/layouts/Layout.astro +0 -77
  24. package/src/pages/api/save-mock.ts +0 -182
  25. package/src/pages/coverage.astro +0 -399
  26. package/src/pages/editor.astro +0 -33
  27. package/src/pages/graph.astro +0 -368
  28. package/src/pages/impact.astro +0 -462
  29. package/src/pages/index.astro +0 -176
  30. package/src/pages/screen/[id].astro +0 -195
  31. package/src/styles/global.css +0 -904
  32. package/src/styles/mock-editor.css +0 -1351
  33. package/src/utils/impactAnalysis.ts +0 -304
  34. package/src/utils/loadCoverage.ts +0 -30
  35. package/src/utils/loadScreens.ts +0 -18
  36. package/tsconfig.json +0 -10
  37. /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
- }