@brixter/brix-builder 0.0.1
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/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/core.d.ts +103 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +758 -0
- package/dist/core.js.map +1 -0
- package/dist/editor/BuilderApp.svelte +1299 -0
- package/dist/editor/BuilderFieldEditor.svelte +274 -0
- package/dist/editor/BuilderInspector.svelte +123 -0
- package/dist/editor/BuilderPreviewFrame.svelte +661 -0
- package/dist/editor/ComponentPreviewThumbnail.svelte +197 -0
- package/dist/editor/PageFlowSidebar.svelte +198 -0
- package/dist/editor/PreviewBlockInserter.svelte +35 -0
- package/dist/editor/PreviewIconEditor.svelte +213 -0
- package/dist/editor/PreviewImageEditor.svelte +221 -0
- package/dist/editor/PreviewTextEditor.svelte +246 -0
- package/dist/editor/RichTextEditor.svelte +234 -0
- package/dist/editor/contracts.d.ts +57 -0
- package/dist/editor/contracts.d.ts.map +1 -0
- package/dist/editor/contracts.js +2 -0
- package/dist/editor/contracts.js.map +1 -0
- package/dist/editor/index.d.ts +3 -0
- package/dist/editor/index.d.ts.map +1 -0
- package/dist/editor/index.js +2 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/editor/shortcuts.d.ts +28 -0
- package/dist/editor/shortcuts.d.ts.map +1 -0
- package/dist/editor/shortcuts.js +28 -0
- package/dist/editor/shortcuts.js.map +1 -0
- package/dist/editor-controller.d.ts +50 -0
- package/dist/editor-controller.d.ts.map +1 -0
- package/dist/editor-controller.js +157 -0
- package/dist/editor-controller.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/preview/field-edit-debug.d.ts +5 -0
- package/dist/preview/field-edit-debug.d.ts.map +1 -0
- package/dist/preview/field-edit-debug.js +36 -0
- package/dist/preview/field-edit-debug.js.map +1 -0
- package/dist/preview/interactive-content.d.ts +8 -0
- package/dist/preview/interactive-content.d.ts.map +1 -0
- package/dist/preview/interactive-content.js +62 -0
- package/dist/preview/interactive-content.js.map +1 -0
- package/dist/preview-dom.d.ts +67 -0
- package/dist/preview-dom.d.ts.map +1 -0
- package/dist/preview-dom.js +191 -0
- package/dist/preview-dom.js.map +1 -0
- package/dist/svelte/SveltePreviewRenderer.svelte +490 -0
- package/dist/svelte/adapter.d.ts +7 -0
- package/dist/svelte/adapter.d.ts.map +1 -0
- package/dist/svelte/adapter.js +66 -0
- package/dist/svelte/adapter.js.map +1 -0
- package/dist/svelte/index.d.ts +3 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +3 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/svelte/markup-schema.d.ts +5 -0
- package/dist/svelte/markup-schema.d.ts.map +1 -0
- package/dist/svelte/markup-schema.js +177 -0
- package/dist/svelte/markup-schema.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,1299 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
addBlock as addBlockToState,
|
|
4
|
+
addItem as addItemToState,
|
|
5
|
+
applyFileToPendingEdit,
|
|
6
|
+
clearPendingFileEdit,
|
|
7
|
+
closeReorderModal as closeReorderModalInState,
|
|
8
|
+
createEditorControllerState,
|
|
9
|
+
getActiveReorderContext,
|
|
10
|
+
getBuilderDefinition,
|
|
11
|
+
handleBlockDragStart,
|
|
12
|
+
handleBlockDrop,
|
|
13
|
+
handleCollectionItemDragStart as startCollectionItemDrag,
|
|
14
|
+
handleCollectionItemDrop as dropCollectionItem,
|
|
15
|
+
moveBlock as moveBlockInState,
|
|
16
|
+
moveItem as moveItemInState,
|
|
17
|
+
openReorderModal as openReorderModalInState,
|
|
18
|
+
queueFileEdit as queueFileEditInState,
|
|
19
|
+
removeBlock as removeBlockFromState,
|
|
20
|
+
removeItem as removeItemFromState,
|
|
21
|
+
setBlockError,
|
|
22
|
+
updatePropAtPath
|
|
23
|
+
} from '../editor-controller.js';
|
|
24
|
+
import {
|
|
25
|
+
createInspectorFieldsFromFields,
|
|
26
|
+
getCollectionItemSummary,
|
|
27
|
+
normalizeBuilderPropsForRender,
|
|
28
|
+
parseBrixYamlDocument,
|
|
29
|
+
serializeToBrixYaml,
|
|
30
|
+
serializeToMdsvex,
|
|
31
|
+
getFieldByPath,
|
|
32
|
+
inferBuilderFieldKind,
|
|
33
|
+
type BuilderBlock,
|
|
34
|
+
type BuilderDocument,
|
|
35
|
+
type BuilderPreviewBinding,
|
|
36
|
+
type BuilderRichTextValue
|
|
37
|
+
} from '../core.js';
|
|
38
|
+
import { attachPreviewEditableFields } from '../preview/enhance-editable-fields.js';
|
|
39
|
+
import { describeFieldElement, logFieldEditEvent } from '../preview/field-edit-debug.js';
|
|
40
|
+
import {
|
|
41
|
+
attachPreviewContainer,
|
|
42
|
+
materializeFieldPath,
|
|
43
|
+
resolvePreviewBindingAtPoint,
|
|
44
|
+
type PreviewCollectionOverlay,
|
|
45
|
+
type PreviewOverlay
|
|
46
|
+
} from '../preview-dom.js';
|
|
47
|
+
import type {
|
|
48
|
+
BuilderAppPreviewProps,
|
|
49
|
+
BuilderRenderDefinition,
|
|
50
|
+
PreviewFieldEdit
|
|
51
|
+
} from './contracts.js';
|
|
52
|
+
import BuilderInspector from './BuilderInspector.svelte';
|
|
53
|
+
import BuilderPreviewFrame from './BuilderPreviewFrame.svelte';
|
|
54
|
+
import ComponentPreviewThumbnail from './ComponentPreviewThumbnail.svelte';
|
|
55
|
+
import PageFlowSidebar from './PageFlowSidebar.svelte';
|
|
56
|
+
import { matchesShortcut, SHORTCUTS } from './shortcuts.js';
|
|
57
|
+
|
|
58
|
+
let {
|
|
59
|
+
definitions,
|
|
60
|
+
initialDocument,
|
|
61
|
+
initialBrixYaml,
|
|
62
|
+
chrome = 'standalone',
|
|
63
|
+
onBrixYamlChange,
|
|
64
|
+
pageFlowOpen = $bindable(true),
|
|
65
|
+
inspectorOpen = $bindable(true),
|
|
66
|
+
activeBlockId = $bindable<string | null>(null),
|
|
67
|
+
onpickImage,
|
|
68
|
+
onpickIcon,
|
|
69
|
+
previewMode = $bindable(false),
|
|
70
|
+
viewportSize = $bindable<'desktop' | 'tablet' | 'mobile'>('desktop')
|
|
71
|
+
}: {
|
|
72
|
+
definitions: BuilderRenderDefinition[];
|
|
73
|
+
initialDocument?: BuilderDocument;
|
|
74
|
+
initialBrixYaml?: string;
|
|
75
|
+
chrome?: 'standalone' | 'embedded';
|
|
76
|
+
onBrixYamlChange?: (value: string) => void;
|
|
77
|
+
pageFlowOpen?: boolean;
|
|
78
|
+
inspectorOpen?: boolean;
|
|
79
|
+
activeBlockId?: string | null;
|
|
80
|
+
onpickImage?: (callback: (imageUrl: string) => void) => void;
|
|
81
|
+
onpickIcon?: (callback: (iconSvg: string) => void) => void;
|
|
82
|
+
previewMode?: boolean;
|
|
83
|
+
viewportSize?: 'desktop' | 'tablet' | 'mobile';
|
|
84
|
+
} = $props();
|
|
85
|
+
|
|
86
|
+
let controller = $state<ReturnType<typeof createEditorControllerState> | null>(null);
|
|
87
|
+
let previewOverlays = $state<Record<string, PreviewOverlay[]>>({});
|
|
88
|
+
let previewCollectionOverlays = $state<Record<string, PreviewCollectionOverlay[]>>({});
|
|
89
|
+
let initialized = $state(false);
|
|
90
|
+
let activeFieldEdit = $state<PreviewFieldEdit | null>(null);
|
|
91
|
+
let inserterModal = $state<{ blockId: string; placement: 'before' | 'after' } | null>(null);
|
|
92
|
+
let pageFlowShortcutModifier = $state<'command' | 'control'>('command');
|
|
93
|
+
const previewBlockElements = new Map<string, HTMLElement>();
|
|
94
|
+
const pageFlowShortcutKey = SHORTCUTS.togglePageFlow.key;
|
|
95
|
+
const inspectorShortcutKey = SHORTCUTS.toggleInspector.key;
|
|
96
|
+
const previewShortcutKey = SHORTCUTS.togglePreview.key;
|
|
97
|
+
|
|
98
|
+
let pageFlowWidth = $state(280);
|
|
99
|
+
let inspectorWidth = $state(320);
|
|
100
|
+
let resizing: 'pageFlow' | 'inspector' | null = $state(null);
|
|
101
|
+
let resizeStartX = 0;
|
|
102
|
+
let resizeStartWidth = 0;
|
|
103
|
+
|
|
104
|
+
function startResize(side: 'pageFlow' | 'inspector', event: MouseEvent): void {
|
|
105
|
+
resizing = side;
|
|
106
|
+
resizeStartX = event.clientX;
|
|
107
|
+
resizeStartWidth = side === 'pageFlow' ? pageFlowWidth : inspectorWidth;
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function handleResizeMove(event: MouseEvent): void {
|
|
112
|
+
if (!resizing) return;
|
|
113
|
+
const dx = event.clientX - resizeStartX;
|
|
114
|
+
if (resizing === 'pageFlow') {
|
|
115
|
+
pageFlowWidth = Math.max(160, Math.min(480, resizeStartWidth + dx));
|
|
116
|
+
} else {
|
|
117
|
+
inspectorWidth = Math.max(200, Math.min(560, resizeStartWidth - dx));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function stopResize(): void {
|
|
122
|
+
resizing = null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
$effect(() => {
|
|
126
|
+
if (!controller) {
|
|
127
|
+
const hydratedDocument =
|
|
128
|
+
initialDocument ??
|
|
129
|
+
(initialBrixYaml ? parseBrixYamlDocument(initialBrixYaml, definitions) : undefined);
|
|
130
|
+
controller = createEditorControllerState(definitions, hydratedDocument);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
$effect(() => {
|
|
135
|
+
if (typeof navigator !== 'undefined') {
|
|
136
|
+
pageFlowShortcutModifier = /Mac|iPhone|iPad|iPod/.test(navigator.platform)
|
|
137
|
+
? 'command'
|
|
138
|
+
: 'control';
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
$effect(() => {
|
|
143
|
+
const blocks = controller?.document.blocks ?? [];
|
|
144
|
+
if (blocks.length === 0) {
|
|
145
|
+
activeBlockId = null;
|
|
146
|
+
initialized = false;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!initialized) {
|
|
151
|
+
activeBlockId = blocks[0]?.id ?? null;
|
|
152
|
+
initialized = true;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (activeBlockId && !blocks.some((block) => block.id === activeBlockId)) {
|
|
157
|
+
activeBlockId = null;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const mdsvexOutput = $derived(
|
|
162
|
+
serializeToMdsvex(
|
|
163
|
+
controller?.document ?? { title: '', description: '', blocks: [] },
|
|
164
|
+
definitions
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
const brixYamlOutput = $derived(
|
|
168
|
+
serializeToBrixYaml(
|
|
169
|
+
controller?.document ?? { title: '', description: '', blocks: [] },
|
|
170
|
+
definitions
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
const activeReorderContext = $derived.by(() =>
|
|
174
|
+
controller ? getActiveReorderContext(controller, definitions) : null
|
|
175
|
+
);
|
|
176
|
+
const activeBlock = $derived(
|
|
177
|
+
controller?.document.blocks.find((block) => block.id === activeBlockId) ?? null
|
|
178
|
+
);
|
|
179
|
+
const activeDefinition = $derived(
|
|
180
|
+
activeBlock ? getBuilderDefinition(activeBlock.type, definitions) : null
|
|
181
|
+
);
|
|
182
|
+
const inspectorFields = $derived(
|
|
183
|
+
activeDefinition ? createInspectorFieldsFromFields(activeDefinition.fields) : {}
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
function addBlock(type: string): void {
|
|
187
|
+
if (!controller) return;
|
|
188
|
+
const block = addBlockToState(controller, definitions, type);
|
|
189
|
+
activeBlockId = block.id;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function addBlockAfter(blockId: string, type: string): void {
|
|
193
|
+
if (!controller) return;
|
|
194
|
+
|
|
195
|
+
const targetIndex = controller.document.blocks.findIndex((block) => block.id === blockId);
|
|
196
|
+
const block = addBlockToState(controller, definitions, type);
|
|
197
|
+
|
|
198
|
+
if (targetIndex !== -1) {
|
|
199
|
+
controller.document.blocks = [
|
|
200
|
+
...controller.document.blocks.slice(0, targetIndex + 1),
|
|
201
|
+
block,
|
|
202
|
+
...controller.document.blocks
|
|
203
|
+
.slice(targetIndex + 1)
|
|
204
|
+
.filter((entry) => entry.id !== block.id)
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
activeBlockId = block.id;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function addBlockBefore(blockId: string, type: string): void {
|
|
212
|
+
if (!controller) return;
|
|
213
|
+
|
|
214
|
+
const targetIndex = controller.document.blocks.findIndex((block) => block.id === blockId);
|
|
215
|
+
const block = addBlockToState(controller, definitions, type);
|
|
216
|
+
|
|
217
|
+
if (targetIndex !== -1) {
|
|
218
|
+
controller.document.blocks = [
|
|
219
|
+
...controller.document.blocks.slice(0, targetIndex),
|
|
220
|
+
block,
|
|
221
|
+
...controller.document.blocks.slice(targetIndex).filter((entry) => entry.id !== block.id)
|
|
222
|
+
];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
activeBlockId = block.id;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function removeBlock(blockId: string): void {
|
|
229
|
+
if (!controller) return;
|
|
230
|
+
removeBlockFromState(controller, blockId);
|
|
231
|
+
if (activeFieldEdit?.blockId === blockId) {
|
|
232
|
+
closeFieldEdit();
|
|
233
|
+
}
|
|
234
|
+
if (activeBlockId === blockId) {
|
|
235
|
+
activeBlockId = controller.document.blocks[0]?.id ?? null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function moveBlock(blockId: string, direction: -1 | 1): void {
|
|
240
|
+
if (!controller) return;
|
|
241
|
+
moveBlockInState(controller, blockId, direction);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function handleDragStart(blockId: string): void {
|
|
245
|
+
if (!controller) return;
|
|
246
|
+
handleBlockDragStart(controller, blockId);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function allowDrop(event: DragEvent): void {
|
|
250
|
+
event.preventDefault();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function handleDrop(targetBlockId: string): void {
|
|
254
|
+
if (!controller) return;
|
|
255
|
+
const droppedBlockId = controller.draggedBlockId;
|
|
256
|
+
handleBlockDrop(controller, targetBlockId);
|
|
257
|
+
if (droppedBlockId) {
|
|
258
|
+
selectBlock(droppedBlockId, { forceScroll: true });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function togglePageFlow(): void {
|
|
263
|
+
pageFlowOpen = !pageFlowOpen;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function toggleInspector(): void {
|
|
267
|
+
inspectorOpen = !inspectorOpen;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function togglePreview(): void {
|
|
271
|
+
previewMode = !previewMode;
|
|
272
|
+
if (previewMode) {
|
|
273
|
+
deselectBlock();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function handleWindowKeydown(event: KeyboardEvent): void {
|
|
278
|
+
if (matchesShortcut(event, SHORTCUTS.closeModal) && inserterModal) {
|
|
279
|
+
closeInserterModal();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (matchesShortcut(event, SHORTCUTS.togglePageFlow)) {
|
|
283
|
+
event.preventDefault();
|
|
284
|
+
togglePageFlow();
|
|
285
|
+
}
|
|
286
|
+
if (matchesShortcut(event, SHORTCUTS.toggleInspector)) {
|
|
287
|
+
event.preventDefault();
|
|
288
|
+
toggleInspector();
|
|
289
|
+
}
|
|
290
|
+
if (matchesShortcut(event, SHORTCUTS.togglePreview)) {
|
|
291
|
+
event.preventDefault();
|
|
292
|
+
togglePreview();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function updateFieldValue(block: BuilderBlock, path: string, value: unknown): void {
|
|
297
|
+
if (!controller) return;
|
|
298
|
+
updatePropAtPath(controller, block, path, value);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function queueFileEdit(blockId: string, path: string): void {
|
|
302
|
+
console.log('queueFileEdit called in BuilderApp:', blockId, path, 'onpickImage is:', !!onpickImage);
|
|
303
|
+
if (!controller) return;
|
|
304
|
+
|
|
305
|
+
const block = controller.document.blocks.find((b) => b.id === blockId);
|
|
306
|
+
const definition = block ? getBuilderDefinition(block.type, definitions) : null;
|
|
307
|
+
const field = definition ? getFieldByPath(definition.fields, path) : null;
|
|
308
|
+
const kind = field ? inferBuilderFieldKind(field) : null;
|
|
309
|
+
|
|
310
|
+
if (kind === 'icon') {
|
|
311
|
+
queueFileEditInState(controller, blockId, path);
|
|
312
|
+
if (onpickIcon) {
|
|
313
|
+
onpickIcon((iconSvg) => {
|
|
314
|
+
if (!controller) return;
|
|
315
|
+
const updatedBlock = applyFileToPendingEdit(controller, iconSvg);
|
|
316
|
+
closeFieldEdit();
|
|
317
|
+
if (!updatedBlock) {
|
|
318
|
+
clearPendingFileEdit(controller);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
} else {
|
|
322
|
+
console.warn('onpickIcon is not defined in BuilderApp');
|
|
323
|
+
clearPendingFileEdit(controller);
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
queueFileEditInState(controller, blockId, path);
|
|
329
|
+
if (onpickImage) {
|
|
330
|
+
console.log('calling onpickImage from BuilderApp...');
|
|
331
|
+
onpickImage((imageUrl) => {
|
|
332
|
+
console.log('onpickImage callback received image URL:', imageUrl);
|
|
333
|
+
if (!controller) return;
|
|
334
|
+
const updatedBlock = applyFileToPendingEdit(controller, imageUrl);
|
|
335
|
+
closeFieldEdit();
|
|
336
|
+
if (!updatedBlock) {
|
|
337
|
+
clearPendingFileEdit(controller);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
} else {
|
|
341
|
+
console.log('onpickImage is not defined in BuilderApp, opening native file picker...');
|
|
342
|
+
openFilePicker();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function openFilePicker(): void {
|
|
347
|
+
const input = document.createElement('input');
|
|
348
|
+
input.type = 'file';
|
|
349
|
+
input.accept = 'image/*';
|
|
350
|
+
input.style.display = 'none';
|
|
351
|
+
input.addEventListener('change', handleFileSelection, { once: true });
|
|
352
|
+
document.body.append(input);
|
|
353
|
+
input.click();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function scrollPreviewToBlock(blockId: string): void {
|
|
357
|
+
const element = previewBlockElements.get(blockId);
|
|
358
|
+
if (!element) return;
|
|
359
|
+
|
|
360
|
+
requestAnimationFrame(() => {
|
|
361
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' });
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function selectBlock(blockId: string, options: { forceScroll?: boolean } = {}): void {
|
|
366
|
+
const selectionChanged = activeBlockId !== blockId;
|
|
367
|
+
activeBlockId = blockId;
|
|
368
|
+
if (selectionChanged || options.forceScroll) {
|
|
369
|
+
scrollPreviewToBlock(blockId);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function handlePreviewClick(block: BuilderBlock, event: Event): void {
|
|
374
|
+
if (!controller) return;
|
|
375
|
+
selectBlock(block.id);
|
|
376
|
+
|
|
377
|
+
const definition = getBuilderDefinition(block.type, definitions);
|
|
378
|
+
const container = event.currentTarget;
|
|
379
|
+
|
|
380
|
+
if (!isHTMLElement(container)) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
logFieldEditEvent('block-click', 'start', {
|
|
385
|
+
blockId: block.id,
|
|
386
|
+
eventType: event.type,
|
|
387
|
+
clientX: event instanceof MouseEvent ? event.clientX : null,
|
|
388
|
+
clientY: event instanceof MouseEvent ? event.clientY : null,
|
|
389
|
+
target: describeFieldElement(event.target instanceof Element ? event.target : null)
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const resolvedBinding = resolvePreviewBindingAtPoint<BuilderPreviewBinding>({
|
|
393
|
+
bindings: definition.previewBindings,
|
|
394
|
+
container,
|
|
395
|
+
target: event.target,
|
|
396
|
+
clientX: event instanceof MouseEvent ? event.clientX : undefined,
|
|
397
|
+
clientY: event instanceof MouseEvent ? event.clientY : undefined
|
|
398
|
+
});
|
|
399
|
+
if (!resolvedBinding) {
|
|
400
|
+
logFieldEditEvent('block-click', 'no binding — closing field edit', { blockId: block.id });
|
|
401
|
+
closeFieldEdit();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
logFieldEditEvent('block-click', 'binding resolved', {
|
|
406
|
+
blockId: block.id,
|
|
407
|
+
path: resolvedBinding.path,
|
|
408
|
+
bindingType: resolvedBinding.binding.type,
|
|
409
|
+
...describeFieldElement(resolvedBinding.matchedElement)
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
event.preventDefault();
|
|
413
|
+
|
|
414
|
+
if (resolvedBinding.binding.type === 'image' || resolvedBinding.binding.type === 'icon') {
|
|
415
|
+
const matchedElement = resolvedBinding.matchedElement as HTMLElement;
|
|
416
|
+
const rawPath = matchedElement.getAttribute('data-builder-field');
|
|
417
|
+
const path =
|
|
418
|
+
rawPath != null
|
|
419
|
+
? (materializeFieldPath(rawPath, container, matchedElement) ?? resolvedBinding.path)
|
|
420
|
+
: resolvedBinding.path;
|
|
421
|
+
|
|
422
|
+
activeFieldEdit = {
|
|
423
|
+
blockId: block.id,
|
|
424
|
+
path,
|
|
425
|
+
caretOffset: null
|
|
426
|
+
};
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (resolvedBinding.binding.type === 'richtext' || resolvedBinding.binding.type === 'text') {
|
|
431
|
+
const matchedElement = resolvedBinding.matchedElement as HTMLElement;
|
|
432
|
+
const rawPath = matchedElement.getAttribute('data-builder-field');
|
|
433
|
+
const path =
|
|
434
|
+
rawPath != null
|
|
435
|
+
? (materializeFieldPath(rawPath, container, matchedElement) ?? resolvedBinding.path)
|
|
436
|
+
: resolvedBinding.path;
|
|
437
|
+
|
|
438
|
+
activeFieldEdit = {
|
|
439
|
+
blockId: block.id,
|
|
440
|
+
path,
|
|
441
|
+
caretOffset: getClickCaretOffset(matchedElement, event)
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function handlePreviewKeydown(block: BuilderBlock, event: KeyboardEvent): void {
|
|
447
|
+
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (isEditableKeyboardTarget(event.target)) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
event.preventDefault();
|
|
456
|
+
handlePreviewClick(block, event);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function handleFileSelection(event: Event): Promise<void> {
|
|
460
|
+
if (!controller) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const target = event.currentTarget;
|
|
465
|
+
const pending = controller.pendingFileEdit;
|
|
466
|
+
if (!(target instanceof HTMLInputElement) || !pending) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const [file] = Array.from(target.files ?? []);
|
|
471
|
+
if (!file) {
|
|
472
|
+
clearPendingFileEdit(controller);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
const dataUrl = await readFileAsDataUrl(file);
|
|
478
|
+
const updatedBlock = applyFileToPendingEdit(controller, dataUrl);
|
|
479
|
+
closeFieldEdit();
|
|
480
|
+
if (!updatedBlock) {
|
|
481
|
+
clearPendingFileEdit(controller);
|
|
482
|
+
}
|
|
483
|
+
} catch (error) {
|
|
484
|
+
setBlockError(
|
|
485
|
+
controller,
|
|
486
|
+
pending.blockId,
|
|
487
|
+
error instanceof Error ? error.message : 'Impossibile leggere il file selezionato.'
|
|
488
|
+
);
|
|
489
|
+
clearPendingFileEdit(controller);
|
|
490
|
+
} finally {
|
|
491
|
+
target.value = '';
|
|
492
|
+
target.remove();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function readFileAsDataUrl(file: File): Promise<string> {
|
|
497
|
+
return new Promise((resolve, reject) => {
|
|
498
|
+
const reader = new FileReader();
|
|
499
|
+
reader.onload = () => {
|
|
500
|
+
if (typeof reader.result === 'string') {
|
|
501
|
+
resolve(reader.result);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
reject(new Error('Il file selezionato non puo essere convertito in data URL.'));
|
|
506
|
+
};
|
|
507
|
+
reader.onerror = () => reject(reader.error ?? new Error('Errore di lettura file.'));
|
|
508
|
+
reader.readAsDataURL(file);
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function addItem(block: BuilderBlock, collectionPath: string): void {
|
|
513
|
+
if (!controller) return;
|
|
514
|
+
addItemToState(controller, definitions, block, collectionPath);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function removeItem(block: BuilderBlock, collectionPath: string, index: number): void {
|
|
518
|
+
if (!controller) return;
|
|
519
|
+
removeItemFromState(controller, definitions, block, collectionPath, index);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function moveItem(
|
|
523
|
+
block: BuilderBlock,
|
|
524
|
+
collectionPath: string,
|
|
525
|
+
index: number,
|
|
526
|
+
direction: -1 | 1
|
|
527
|
+
): void {
|
|
528
|
+
if (!controller) return;
|
|
529
|
+
moveItemInState(controller, definitions, block, collectionPath, index, direction);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function openReorderModal(blockId: string, collectionPath: string): void {
|
|
533
|
+
if (!controller) return;
|
|
534
|
+
openReorderModalInState(controller, blockId, collectionPath);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function closeReorderModal(): void {
|
|
538
|
+
if (!controller) return;
|
|
539
|
+
closeReorderModalInState(controller);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function openInserterModal(blockId: string, placement: 'before' | 'after'): void {
|
|
543
|
+
inserterModal = { blockId, placement };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function closeInserterModal(): void {
|
|
547
|
+
inserterModal = null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function insertFromModal(type: string): void {
|
|
551
|
+
if (!inserterModal) return;
|
|
552
|
+
if (inserterModal.placement === 'before') {
|
|
553
|
+
addBlockBefore(inserterModal.blockId, type);
|
|
554
|
+
} else {
|
|
555
|
+
addBlockAfter(inserterModal.blockId, type);
|
|
556
|
+
}
|
|
557
|
+
inserterModal = null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function closeFieldEdit(): void {
|
|
561
|
+
activeFieldEdit = null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function deselectBlock(): void {
|
|
565
|
+
activeFieldEdit = null;
|
|
566
|
+
activeBlockId = null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function updatePreviewRichText(
|
|
570
|
+
block: BuilderBlock,
|
|
571
|
+
path: string,
|
|
572
|
+
value: BuilderRichTextValue
|
|
573
|
+
): void {
|
|
574
|
+
updateFieldValue(block, path, value);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function updatePreviewText(block: BuilderBlock, path: string, value: string): void {
|
|
578
|
+
updateFieldValue(block, path, value);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function updateDocumentTitle(value: string): void {
|
|
582
|
+
if (!controller) return;
|
|
583
|
+
controller.document.title = value;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function updateDocumentDescription(value: string): void {
|
|
587
|
+
if (!controller) return;
|
|
588
|
+
controller.document.description = value;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
$effect(() => {
|
|
592
|
+
onBrixYamlChange?.(brixYamlOutput);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
function getClickCaretOffset(element: Element, event: Event): number | null {
|
|
596
|
+
if (!(event instanceof MouseEvent) || !isHTMLElement(element)) {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const doc = element.ownerDocument;
|
|
601
|
+
const range =
|
|
602
|
+
doc.caretRangeFromPoint?.(event.clientX, event.clientY) ??
|
|
603
|
+
(() => {
|
|
604
|
+
const pos = doc.caretPositionFromPoint?.(event.clientX, event.clientY);
|
|
605
|
+
if (!pos) {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const nextRange = doc.createRange();
|
|
610
|
+
nextRange.setStart(pos.offsetNode, pos.offset);
|
|
611
|
+
nextRange.collapse(true);
|
|
612
|
+
return nextRange;
|
|
613
|
+
})();
|
|
614
|
+
|
|
615
|
+
if (!range || !element.contains(range.startContainer)) {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const preRange = doc.createRange();
|
|
620
|
+
preRange.selectNodeContents(element);
|
|
621
|
+
preRange.setEnd(range.startContainer, range.startOffset);
|
|
622
|
+
return preRange.toString().length;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function isElement(value: unknown): value is Element {
|
|
626
|
+
return typeof value === 'object' && value !== null && (value as Node).nodeType === 1;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function isHTMLElement(value: unknown): value is HTMLElement {
|
|
630
|
+
if (!isElement(value)) {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const view = value.ownerDocument.defaultView;
|
|
635
|
+
return view ? value instanceof view.HTMLElement : value instanceof HTMLElement;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function isEditableKeyboardTarget(target: EventTarget | null): boolean {
|
|
639
|
+
if (!isElement(target)) {
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return Boolean(
|
|
644
|
+
target.closest(
|
|
645
|
+
'.builder-preview-field-editor, .ProseMirror, .builder-preview-text-editor, input, textarea, [contenteditable="true"]'
|
|
646
|
+
)
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function handleCollectionItemDragStart(
|
|
651
|
+
blockId: string,
|
|
652
|
+
collectionPath: string,
|
|
653
|
+
index: number
|
|
654
|
+
): void {
|
|
655
|
+
if (!controller) return;
|
|
656
|
+
startCollectionItemDrag(controller, blockId, collectionPath, index);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function handleCollectionItemDrop(targetIndex: number): void {
|
|
660
|
+
if (!controller) return;
|
|
661
|
+
dropCollectionItem(controller, definitions, targetIndex);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function previewContainer(
|
|
665
|
+
node: HTMLElement,
|
|
666
|
+
params: {
|
|
667
|
+
block: BuilderBlock;
|
|
668
|
+
definition: BuilderRenderDefinition;
|
|
669
|
+
editing: import('./contracts.js').PreviewEditingContext;
|
|
670
|
+
}
|
|
671
|
+
): {
|
|
672
|
+
update: (nextParams: {
|
|
673
|
+
block: BuilderBlock;
|
|
674
|
+
definition: BuilderRenderDefinition;
|
|
675
|
+
editing: import('./contracts.js').PreviewEditingContext;
|
|
676
|
+
}) => void;
|
|
677
|
+
destroy: () => void;
|
|
678
|
+
} {
|
|
679
|
+
let blockId = params.block.id;
|
|
680
|
+
previewBlockElements.set(blockId, node);
|
|
681
|
+
|
|
682
|
+
const onOverlaysChange = (blockId: string, overlays: PreviewOverlay[]) => {
|
|
683
|
+
if (overlays.length === 0) {
|
|
684
|
+
delete previewOverlays[blockId];
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
previewOverlays[blockId] = overlays;
|
|
689
|
+
};
|
|
690
|
+
const onCollectionOverlaysChange = (blockId: string, overlays: PreviewCollectionOverlay[]) => {
|
|
691
|
+
if (overlays.length === 0) {
|
|
692
|
+
delete previewCollectionOverlays[blockId];
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
previewCollectionOverlays[blockId] = overlays;
|
|
697
|
+
};
|
|
698
|
+
const editableFields = attachPreviewEditableFields(node, {
|
|
699
|
+
block: params.block,
|
|
700
|
+
definition: params.definition,
|
|
701
|
+
previewProps: params.editing.previewProps,
|
|
702
|
+
active: params.editing.active,
|
|
703
|
+
focusPath: params.editing.focusPath,
|
|
704
|
+
caretOffset: params.editing.caretOffset,
|
|
705
|
+
onUpdateRichText: (path, value) => updatePreviewRichText(params.block, path, value),
|
|
706
|
+
onUpdateText: (path, value) => updatePreviewText(params.block, path, value),
|
|
707
|
+
onQueueFileEdit: (path) => queueFileEdit(params.block.id, path),
|
|
708
|
+
onCloseFieldEdit: closeFieldEdit,
|
|
709
|
+
onFocusField: (path, caretOffset) => {
|
|
710
|
+
activeFieldEdit = { blockId: params.block.id, path, caretOffset };
|
|
711
|
+
selectBlock(params.block.id, { forceScroll: false });
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
const action = attachPreviewContainer(node, {
|
|
715
|
+
block: params.block,
|
|
716
|
+
definition: params.definition,
|
|
717
|
+
onOverlaysChange,
|
|
718
|
+
onCollectionOverlaysChange
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
update(nextParams) {
|
|
723
|
+
if (blockId !== nextParams.block.id) {
|
|
724
|
+
previewBlockElements.delete(blockId);
|
|
725
|
+
blockId = nextParams.block.id;
|
|
726
|
+
previewBlockElements.set(blockId, node);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
editableFields.update({
|
|
730
|
+
block: nextParams.block,
|
|
731
|
+
definition: nextParams.definition,
|
|
732
|
+
previewProps: nextParams.editing.previewProps,
|
|
733
|
+
active: nextParams.editing.active,
|
|
734
|
+
focusPath: nextParams.editing.focusPath,
|
|
735
|
+
caretOffset: nextParams.editing.caretOffset,
|
|
736
|
+
onUpdateRichText: (path, value) => updatePreviewRichText(nextParams.block, path, value),
|
|
737
|
+
onUpdateText: (path, value) => updatePreviewText(nextParams.block, path, value),
|
|
738
|
+
onQueueFileEdit: (path) => queueFileEdit(nextParams.block.id, path),
|
|
739
|
+
onCloseFieldEdit: closeFieldEdit,
|
|
740
|
+
onFocusField: (path, caretOffset) => {
|
|
741
|
+
activeFieldEdit = { blockId: nextParams.block.id, path, caretOffset };
|
|
742
|
+
selectBlock(nextParams.block.id, { forceScroll: false });
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
action.update({
|
|
746
|
+
block: nextParams.block,
|
|
747
|
+
definition: nextParams.definition,
|
|
748
|
+
onOverlaysChange,
|
|
749
|
+
onCollectionOverlaysChange
|
|
750
|
+
});
|
|
751
|
+
},
|
|
752
|
+
destroy() {
|
|
753
|
+
previewBlockElements.delete(blockId);
|
|
754
|
+
editableFields.destroy();
|
|
755
|
+
action.destroy();
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function copyMdsvex(): Promise<void> {
|
|
761
|
+
if (!controller) return;
|
|
762
|
+
await navigator.clipboard.writeText(mdsvexOutput);
|
|
763
|
+
controller.copied = true;
|
|
764
|
+
setTimeout(() => {
|
|
765
|
+
if (controller) {
|
|
766
|
+
controller.copied = false;
|
|
767
|
+
}
|
|
768
|
+
}, 1500);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const previewProps = $derived<BuilderAppPreviewProps>({
|
|
772
|
+
definitions,
|
|
773
|
+
blocks: controller?.document.blocks ?? [],
|
|
774
|
+
propsErrors: controller?.propsErrors ?? {},
|
|
775
|
+
previewOverlays,
|
|
776
|
+
previewCollectionOverlays,
|
|
777
|
+
activeBlockId,
|
|
778
|
+
activeFieldEdit,
|
|
779
|
+
previewContainer,
|
|
780
|
+
onPreviewClick: handlePreviewClick,
|
|
781
|
+
onPreviewKeydown: handlePreviewKeydown,
|
|
782
|
+
onSelectBlock: selectBlock,
|
|
783
|
+
onCloseFieldEdit: closeFieldEdit,
|
|
784
|
+
onUpdateRichText: updatePreviewRichText,
|
|
785
|
+
onUpdateText: updatePreviewText,
|
|
786
|
+
onQueueFileEdit: queueFileEdit,
|
|
787
|
+
onAddBlockBefore: addBlockBefore,
|
|
788
|
+
onAddBlockAfter: addBlockAfter,
|
|
789
|
+
onAddItem: addItem,
|
|
790
|
+
onRemoveItem: removeItem,
|
|
791
|
+
onMoveItem: moveItem,
|
|
792
|
+
onOpenReorderModal: openReorderModal,
|
|
793
|
+
onOpenInserterModal: openInserterModal,
|
|
794
|
+
onDeselectBlock: deselectBlock,
|
|
795
|
+
previewMode,
|
|
796
|
+
viewportSize
|
|
797
|
+
});
|
|
798
|
+
</script>
|
|
799
|
+
|
|
800
|
+
<svelte:window
|
|
801
|
+
onkeydown={handleWindowKeydown}
|
|
802
|
+
onmousemove={handleResizeMove}
|
|
803
|
+
onmouseup={stopResize}
|
|
804
|
+
/>
|
|
805
|
+
|
|
806
|
+
<svelte:head>
|
|
807
|
+
<title>Brixter Builder</title>
|
|
808
|
+
<meta
|
|
809
|
+
name="description"
|
|
810
|
+
content="Brixter visual editor for briks, pages, and optional mdsvex export."
|
|
811
|
+
/>
|
|
812
|
+
</svelte:head>
|
|
813
|
+
|
|
814
|
+
<div
|
|
815
|
+
class={chrome === 'standalone'
|
|
816
|
+
? 'builder-app flex h-screen flex-col overflow-hidden bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100'
|
|
817
|
+
: 'builder-app flex h-full min-h-0 flex-col overflow-hidden bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100'}
|
|
818
|
+
>
|
|
819
|
+
{#if chrome === 'standalone'}
|
|
820
|
+
<header
|
|
821
|
+
class="flex h-[60px] shrink-0 items-center justify-between border-b border-gray-200 bg-white px-3 dark:border-gray-700 dark:bg-gray-900"
|
|
822
|
+
onclick={(event) => { if (!(event.target as Element).closest('button, input, a')) deselectBlock(); }}
|
|
823
|
+
>
|
|
824
|
+
<div class="flex items-center gap-2">
|
|
825
|
+
<div
|
|
826
|
+
class="flex h-9 w-9 items-center justify-center bg-gray-900 text-sm font-semibold text-white dark:bg-gray-100 dark:text-gray-900"
|
|
827
|
+
>
|
|
828
|
+
B
|
|
829
|
+
</div>
|
|
830
|
+
<button
|
|
831
|
+
type="button"
|
|
832
|
+
class={pageFlowOpen
|
|
833
|
+
? 'btn-brutal-icon group relative flex h-9 w-9 items-center justify-center'
|
|
834
|
+
: 'group relative flex h-9 w-9 items-center justify-center border border-gray-300 bg-white text-gray-900 transition-colors hover:border-accent hover:bg-accent-hover hover:text-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:hover:border-accent dark:hover:bg-accent-hover dark:hover:text-gray-900'}
|
|
835
|
+
aria-label={pageFlowOpen ? 'Chiudi Page flow' : 'Apri Page flow'}
|
|
836
|
+
aria-pressed={pageFlowOpen}
|
|
837
|
+
onclick={togglePageFlow}
|
|
838
|
+
>
|
|
839
|
+
<svg class="h-4 w-4" viewBox="0 0 16 16" aria-hidden="true">
|
|
840
|
+
<path
|
|
841
|
+
d="M3 3.5h10v1.25H3V3.5Zm0 3.875h10v1.25H3v-1.25Zm0 3.875h10v1.25H3v-1.25Z"
|
|
842
|
+
fill="currentColor"
|
|
843
|
+
/>
|
|
844
|
+
</svg>
|
|
845
|
+
<span
|
|
846
|
+
class="pointer-events-none absolute top-full left-0 z-50 mt-2 flex flex-col items-start gap-1.5 border border-gray-300 bg-white px-3 py-2 text-xs whitespace-nowrap text-gray-900 opacity-0 shadow-lg transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
|
|
847
|
+
>
|
|
848
|
+
<span class="font-semibold">Page flow</span>
|
|
849
|
+
<span class="flex items-center gap-1 text-gray-500 dark:text-gray-400">
|
|
850
|
+
<span
|
|
851
|
+
class="inline-flex h-5 items-center gap-1 border-2 border-gray-200 bg-gray-50 px-1.5 text-[11px] font-medium text-gray-700 dark:border-gray-500 dark:bg-gray-800 dark:text-gray-200"
|
|
852
|
+
>
|
|
853
|
+
{#if pageFlowShortcutModifier === 'command'}
|
|
854
|
+
<svg class="h-3 w-3" viewBox="0 0 16 16" aria-hidden="true">
|
|
855
|
+
<path
|
|
856
|
+
d="M5 2.25A2.75 2.75 0 0 0 2.25 5v.75H5V2.25Zm1.25 3.5h3.5v-3.5h-3.5v3.5Zm4.75 0h2.75V5A2.75 2.75 0 0 0 11 2.25h-.75v3.5ZM9.75 7h-3.5v2h3.5V7ZM5 7H2.25v2H5V7Zm5.25 0v2h3.5V7h-3.5ZM5 10.25H2.25V11A2.75 2.75 0 0 0 5 13.75h.75v-3.5H5Zm1.25 0v3.5h3.5v-3.5h-3.5Zm4 0v3.5H11A2.75 2.75 0 0 0 13.75 11v-.75h-3.5Z"
|
|
857
|
+
fill="currentColor"
|
|
858
|
+
/>
|
|
859
|
+
</svg>
|
|
860
|
+
{:else}
|
|
861
|
+
Ctrl
|
|
862
|
+
{/if}
|
|
863
|
+
</span>
|
|
864
|
+
<span class="text-[11px] font-semibold text-gray-400 dark:text-gray-500">+</span>
|
|
865
|
+
<span
|
|
866
|
+
class="inline-flex h-5 items-center border-2 border-gray-200 bg-gray-50 px-1.5 text-[11px] font-medium text-gray-700 dark:border-gray-500 dark:bg-gray-800 dark:text-gray-200"
|
|
867
|
+
>
|
|
868
|
+
{pageFlowShortcutKey}
|
|
869
|
+
</span>
|
|
870
|
+
</span>
|
|
871
|
+
</span>
|
|
872
|
+
</button>
|
|
873
|
+
<button
|
|
874
|
+
type="button"
|
|
875
|
+
class="btn-brutal-icon flex h-9 w-9 items-center justify-center text-xl leading-none"
|
|
876
|
+
onclick={() => definitions[0] && addBlock(definitions[0].type)}
|
|
877
|
+
aria-label="Aggiungi brik"
|
|
878
|
+
>
|
|
879
|
+
+
|
|
880
|
+
</button>
|
|
881
|
+
<div class="ml-2 h-6 w-px bg-gray-200 dark:bg-gray-700"></div>
|
|
882
|
+
<p class="text-sm font-medium">Brixter Builder</p>
|
|
883
|
+
</div>
|
|
884
|
+
|
|
885
|
+
<!-- Center Device Selection -->
|
|
886
|
+
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 inline-flex items-center gap-0.5 p-0.5 bg-gray-100 dark:bg-gray-800/80 z-10 border-2 border-gray-200/50 dark:border-gray-700/50 shadow-inner">
|
|
887
|
+
<button
|
|
888
|
+
type="button"
|
|
889
|
+
class="flex h-8 w-8 items-center justify-center cursor-pointer transition-all duration-150 {viewportSize === 'desktop'
|
|
890
|
+
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-white font-medium'
|
|
891
|
+
: 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 bg-transparent'}"
|
|
892
|
+
onclick={() => viewportSize = 'desktop'}
|
|
893
|
+
title="Desktop (100%)"
|
|
894
|
+
>
|
|
895
|
+
<svg class="h-4.5 w-4.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>
|
|
896
|
+
</button>
|
|
897
|
+
<button
|
|
898
|
+
type="button"
|
|
899
|
+
class="flex h-8 w-8 items-center justify-center cursor-pointer transition-all duration-150 {viewportSize === 'tablet'
|
|
900
|
+
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-white font-medium'
|
|
901
|
+
: 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 bg-transparent'}"
|
|
902
|
+
onclick={() => viewportSize = 'tablet'}
|
|
903
|
+
title="Tablet (768px)"
|
|
904
|
+
>
|
|
905
|
+
<svg class="h-4.5 w-4.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"/><line x1="18" x2="18.01" y1="12" y2="12"/></svg>
|
|
906
|
+
</button>
|
|
907
|
+
<button
|
|
908
|
+
type="button"
|
|
909
|
+
class="flex h-8 w-8 items-center justify-center cursor-pointer transition-all duration-150 {viewportSize === 'mobile'
|
|
910
|
+
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-white font-medium'
|
|
911
|
+
: 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 bg-transparent'}"
|
|
912
|
+
onclick={() => viewportSize = 'mobile'}
|
|
913
|
+
title="Mobile (375px)"
|
|
914
|
+
>
|
|
915
|
+
<svg class="h-4.5 w-4.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"/><line x1="12" x2="12.01" y1="17" y2="17"/></svg>
|
|
916
|
+
</button>
|
|
917
|
+
</div>
|
|
918
|
+
|
|
919
|
+
<div class="flex items-center gap-2">
|
|
920
|
+
<!-- Mode Selector -->
|
|
921
|
+
<div class="group relative inline-flex items-center gap-0.5 p-0.5 bg-gray-100 dark:bg-gray-800/80 border-2 border-gray-200/50 dark:border-gray-700/50 shadow-inner">
|
|
922
|
+
<button
|
|
923
|
+
type="button"
|
|
924
|
+
class="inline-flex h-8 cursor-pointer items-center gap-1.5 px-3 text-xs font-medium transition-all duration-150 {!previewMode
|
|
925
|
+
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-white'
|
|
926
|
+
: 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 bg-transparent'}"
|
|
927
|
+
onclick={() => previewMode = false}
|
|
928
|
+
>
|
|
929
|
+
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>
|
|
930
|
+
<span>Editor</span>
|
|
931
|
+
</button>
|
|
932
|
+
<button
|
|
933
|
+
type="button"
|
|
934
|
+
class="inline-flex h-8 cursor-pointer items-center gap-1.5 px-3 text-xs font-medium transition-all duration-150 {previewMode
|
|
935
|
+
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-white'
|
|
936
|
+
: 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 bg-transparent'}"
|
|
937
|
+
onclick={() => {
|
|
938
|
+
previewMode = true;
|
|
939
|
+
deselectBlock();
|
|
940
|
+
}}
|
|
941
|
+
>
|
|
942
|
+
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
943
|
+
<span>Preview</span>
|
|
944
|
+
</button>
|
|
945
|
+
<span
|
|
946
|
+
class="pointer-events-none absolute top-full right-0 z-50 mt-2 flex flex-col items-start gap-1.5 border border-gray-300 bg-white px-3 py-2 text-xs whitespace-nowrap text-gray-900 opacity-0 shadow-lg transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
|
|
947
|
+
>
|
|
948
|
+
<span class="font-semibold">Preview</span>
|
|
949
|
+
<span class="flex items-center gap-1 text-gray-500 dark:text-gray-400">
|
|
950
|
+
<span
|
|
951
|
+
class="inline-flex h-5 items-center gap-1 border-2 border-gray-200 bg-gray-50 px-1.5 text-[11px] font-medium text-gray-700 dark:border-gray-500 dark:bg-gray-800 dark:text-gray-200"
|
|
952
|
+
>
|
|
953
|
+
{#if pageFlowShortcutModifier === 'command'}
|
|
954
|
+
<svg class="h-3 w-3" viewBox="0 0 16 16" aria-hidden="true">
|
|
955
|
+
<path
|
|
956
|
+
d="M5 2.25A2.75 2.75 0 0 0 2.25 5v.75H5V2.25Zm1.25 3.5h3.5v-3.5h-3.5v3.5Zm4.75 0h2.75V5A2.75 2.75 0 0 0 11 2.25h-.75v3.5ZM9.75 7h-3.5v2h3.5V7ZM5 7H2.25v2H5V7Zm5.25 0v2h3.5V7h-3.5ZM5 10.25H2.25V11A2.75 2.75 0 0 0 5 13.75h.75v-3.5H5Zm1.25 0v3.5h3.5v-3.5h-3.5Zm4 0v3.5H11A2.75 2.75 0 0 0 13.75 11v-.75h-3.5Z"
|
|
957
|
+
fill="currentColor"
|
|
958
|
+
/>
|
|
959
|
+
</svg>
|
|
960
|
+
{:else}
|
|
961
|
+
Ctrl
|
|
962
|
+
{/if}
|
|
963
|
+
</span>
|
|
964
|
+
<span class="text-[11px] font-semibold text-gray-400 dark:text-gray-500">+</span>
|
|
965
|
+
<span
|
|
966
|
+
class="inline-flex h-5 items-center border-2 border-gray-200 bg-gray-50 px-1.5 text-[11px] font-medium uppercase text-gray-700 dark:border-gray-500 dark:bg-gray-800 dark:text-gray-200"
|
|
967
|
+
>
|
|
968
|
+
{previewShortcutKey}
|
|
969
|
+
</span>
|
|
970
|
+
</span>
|
|
971
|
+
</span>
|
|
972
|
+
</div>
|
|
973
|
+
|
|
974
|
+
<button
|
|
975
|
+
type="button"
|
|
976
|
+
class="btn-brutal-flat cursor-pointer px-3 py-1.5 text-xs font-medium"
|
|
977
|
+
onclick={copyMdsvex}
|
|
978
|
+
>
|
|
979
|
+
{controller?.copied ? 'Copiato' : 'Copia export'}
|
|
980
|
+
</button>
|
|
981
|
+
<button
|
|
982
|
+
type="button"
|
|
983
|
+
class={inspectorOpen
|
|
984
|
+
? 'btn-brutal-icon group relative flex h-9 w-9 items-center justify-center'
|
|
985
|
+
: 'group relative flex h-9 w-9 items-center justify-center border border-gray-300 bg-white text-gray-900 transition-colors hover:border-accent hover:bg-accent-hover hover:text-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:hover:border-accent dark:hover:bg-accent-hover dark:hover:text-gray-900'}
|
|
986
|
+
aria-label={inspectorOpen ? 'Chiudi Inspector' : 'Apri Inspector'}
|
|
987
|
+
aria-pressed={inspectorOpen}
|
|
988
|
+
onclick={toggleInspector}
|
|
989
|
+
>
|
|
990
|
+
<svg class="h-4 w-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
991
|
+
<rect x="2" y="2" width="12" height="12" rx="1.5" stroke="currentColor" stroke-width="1.25"/>
|
|
992
|
+
<path d="M10.5 2.5v11" stroke="currentColor" stroke-width="1.25"/>
|
|
993
|
+
</svg>
|
|
994
|
+
<span
|
|
995
|
+
class="pointer-events-none absolute top-full right-0 z-50 mt-2 flex flex-col items-start gap-1.5 border border-gray-300 bg-white px-3 py-2 text-xs whitespace-nowrap text-gray-900 opacity-0 shadow-lg transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
|
|
996
|
+
>
|
|
997
|
+
<span class="font-semibold">Inspector</span>
|
|
998
|
+
<span class="flex items-center gap-1 text-gray-500 dark:text-gray-400">
|
|
999
|
+
<span
|
|
1000
|
+
class="inline-flex h-5 items-center gap-1 border-2 border-gray-200 bg-gray-50 px-1.5 text-[11px] font-medium text-gray-700 dark:border-gray-500 dark:bg-gray-800 dark:text-gray-200"
|
|
1001
|
+
>
|
|
1002
|
+
{#if pageFlowShortcutModifier === 'command'}
|
|
1003
|
+
<svg class="h-3 w-3" viewBox="0 0 16 16" aria-hidden="true">
|
|
1004
|
+
<path
|
|
1005
|
+
d="M5 2.25A2.75 2.75 0 0 0 2.25 5v.75H5V2.25Zm1.25 3.5h3.5v-3.5h-3.5v3.5Zm4.75 0h2.75V5A2.75 2.75 0 0 0 11 2.25h-.75v3.5ZM9.75 7h-3.5v2h3.5V7ZM5 7H2.25v2H5V7Zm5.25 0v2h3.5V7h-3.5ZM5 10.25H2.25V11A2.75 2.75 0 0 0 5 13.75h.75v-3.5H5Zm1.25 0v3.5h3.5v-3.5h-3.5Zm4 0v3.5H11A2.75 2.75 0 0 0 13.75 11v-.75h-3.5Z"
|
|
1006
|
+
fill="currentColor"
|
|
1007
|
+
/>
|
|
1008
|
+
</svg>
|
|
1009
|
+
{:else}
|
|
1010
|
+
Ctrl
|
|
1011
|
+
{/if}
|
|
1012
|
+
</span>
|
|
1013
|
+
<span class="text-[11px] font-semibold text-gray-400 dark:text-gray-500">+</span>
|
|
1014
|
+
<span class="inline-flex h-5 items-center border-2 border-gray-200 bg-gray-50 px-1.5 text-[11px] font-medium text-gray-700 dark:border-gray-500 dark:bg-gray-800 dark:text-gray-200">Shift</span>
|
|
1015
|
+
<span class="text-[11px] font-semibold text-gray-400 dark:text-gray-500">+</span>
|
|
1016
|
+
<span
|
|
1017
|
+
class="inline-flex h-5 items-center border-2 border-gray-200 bg-gray-50 px-1.5 text-[11px] font-medium text-gray-700 dark:border-gray-500 dark:bg-gray-800 dark:text-gray-200"
|
|
1018
|
+
>
|
|
1019
|
+
{inspectorShortcutKey}
|
|
1020
|
+
</span>
|
|
1021
|
+
</span>
|
|
1022
|
+
</span>
|
|
1023
|
+
</button>
|
|
1024
|
+
</div>
|
|
1025
|
+
</header>
|
|
1026
|
+
{/if}
|
|
1027
|
+
|
|
1028
|
+
{#if resizing}
|
|
1029
|
+
<div class="fixed inset-0 z-50 cursor-col-resize" />
|
|
1030
|
+
{/if}
|
|
1031
|
+
|
|
1032
|
+
<div class="flex min-h-0 flex-1 overflow-hidden">
|
|
1033
|
+
{#if pageFlowOpen && !previewMode}
|
|
1034
|
+
<div class="relative shrink-0" style="width: {pageFlowWidth}px">
|
|
1035
|
+
<PageFlowSidebar
|
|
1036
|
+
blocks={controller?.document.blocks ?? []}
|
|
1037
|
+
{activeBlockId}
|
|
1038
|
+
onSelectBlock={selectBlock}
|
|
1039
|
+
onDeselectBlock={deselectBlock}
|
|
1040
|
+
onMoveBlock={moveBlock}
|
|
1041
|
+
onRemoveBlock={removeBlock}
|
|
1042
|
+
onDragStart={handleDragStart}
|
|
1043
|
+
onAllowDrop={allowDrop}
|
|
1044
|
+
onDrop={handleDrop}
|
|
1045
|
+
/>
|
|
1046
|
+
<div
|
|
1047
|
+
class="absolute top-0 right-0 z-10 h-full w-1 cursor-col-resize transition-colors hover:bg-[#FDE047]/30 dark:hover:bg-[#FACC15]/30"
|
|
1048
|
+
onmousedown={(e) => startResize('pageFlow', e)}
|
|
1049
|
+
/>
|
|
1050
|
+
</div>
|
|
1051
|
+
{/if}
|
|
1052
|
+
|
|
1053
|
+
<main class="min-w-0 flex-1 overflow-hidden bg-white dark:bg-[#12100d]">
|
|
1054
|
+
<div class="h-full min-h-0 w-full">
|
|
1055
|
+
<BuilderPreviewFrame {...previewProps} onKeydown={handleWindowKeydown} />
|
|
1056
|
+
</div>
|
|
1057
|
+
</main>
|
|
1058
|
+
|
|
1059
|
+
{#if inspectorOpen && !previewMode}
|
|
1060
|
+
<div class="relative shrink-0" style="width: {inspectorWidth}px">
|
|
1061
|
+
<BuilderInspector
|
|
1062
|
+
title={controller?.document.title ?? ''}
|
|
1063
|
+
description={controller?.document.description ?? ''}
|
|
1064
|
+
{activeBlock}
|
|
1065
|
+
{activeDefinition}
|
|
1066
|
+
{inspectorFields}
|
|
1067
|
+
propsError={activeBlock ? (controller?.propsErrors[activeBlock.id] ?? null) : null}
|
|
1068
|
+
{mdsvexOutput}
|
|
1069
|
+
copied={controller?.copied ?? false}
|
|
1070
|
+
onTitleChange={updateDocumentTitle}
|
|
1071
|
+
onDescriptionChange={updateDocumentDescription}
|
|
1072
|
+
onFieldChange={updateFieldValue}
|
|
1073
|
+
onQueueFileEdit={queueFileEdit}
|
|
1074
|
+
onAddItem={addItem}
|
|
1075
|
+
onRemoveItem={removeItem}
|
|
1076
|
+
onMoveItem={moveItem}
|
|
1077
|
+
onCopyMdsvex={copyMdsvex}
|
|
1078
|
+
onDeselectBlock={deselectBlock}
|
|
1079
|
+
/>
|
|
1080
|
+
<div
|
|
1081
|
+
class="absolute top-0 left-0 z-10 h-full w-1 cursor-col-resize transition-colors hover:bg-[#FDE047]/30 dark:hover:bg-[#FACC15]/30"
|
|
1082
|
+
onmousedown={(e) => startResize('inspector', e)}
|
|
1083
|
+
/>
|
|
1084
|
+
</div>
|
|
1085
|
+
{/if}
|
|
1086
|
+
</div>
|
|
1087
|
+
</div>
|
|
1088
|
+
|
|
1089
|
+
{#if inserterModal}
|
|
1090
|
+
<div class="fixed inset-0 z-50 flex items-center justify-center p-6">
|
|
1091
|
+
<button
|
|
1092
|
+
type="button"
|
|
1093
|
+
class="absolute inset-0 bg-black/45"
|
|
1094
|
+
aria-label="Close component selector"
|
|
1095
|
+
onclick={closeInserterModal}
|
|
1096
|
+
></button>
|
|
1097
|
+
<div
|
|
1098
|
+
class="relative flex max-h-[min(760px,calc(100vh-3rem))] w-full max-w-5xl flex-col border border-gray-300 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-900"
|
|
1099
|
+
role="dialog"
|
|
1100
|
+
aria-modal="true"
|
|
1101
|
+
aria-label="Choose component to add"
|
|
1102
|
+
tabindex="-1"
|
|
1103
|
+
onclick={(event) => event.stopPropagation()}
|
|
1104
|
+
onkeydown={(event) => {
|
|
1105
|
+
if (event.key === 'Escape') {
|
|
1106
|
+
closeInserterModal();
|
|
1107
|
+
}
|
|
1108
|
+
}}
|
|
1109
|
+
>
|
|
1110
|
+
<div
|
|
1111
|
+
class="flex shrink-0 items-start justify-between gap-4 border-b border-gray-200 p-5 dark:border-gray-700"
|
|
1112
|
+
>
|
|
1113
|
+
<div>
|
|
1114
|
+
<h2 class="text-heading text-lg font-semibold">Add component</h2>
|
|
1115
|
+
<p class="text-muted mt-1 text-sm">
|
|
1116
|
+
Choose the component to insert {inserterModal.placement === 'before' ? 'before' : 'after'} this
|
|
1117
|
+
section.
|
|
1118
|
+
</p>
|
|
1119
|
+
</div>
|
|
1120
|
+
<button
|
|
1121
|
+
type="button"
|
|
1122
|
+
class="flex h-9 w-9 items-center justify-center border border-gray-300 text-xl leading-none text-gray-700 transition-colors hover:border-accent hover:bg-accent-hover hover:text-gray-900 dark:border-gray-700 dark:text-gray-200 dark:hover:border-accent dark:hover:bg-accent-hover"
|
|
1123
|
+
aria-label="Close"
|
|
1124
|
+
onclick={closeInserterModal}
|
|
1125
|
+
>
|
|
1126
|
+
×
|
|
1127
|
+
</button>
|
|
1128
|
+
</div>
|
|
1129
|
+
|
|
1130
|
+
<div class="min-h-0 flex-1 overflow-y-auto p-5">
|
|
1131
|
+
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
1132
|
+
{#each definitions as definition (definition.type)}
|
|
1133
|
+
<button
|
|
1134
|
+
type="button"
|
|
1135
|
+
class="group overflow-hidden border border-gray-300 bg-white text-left transition-colors hover:border-[#FDE047] hover:bg-yellow-50/50 focus:border-[#FDE047] focus:ring-2 focus:ring-[#FDE047]/30 focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:hover:border-[#FACC15] dark:hover:bg-yellow-950/40 dark:focus:border-[#FACC15] dark:focus:ring-[#FACC15]/30"
|
|
1136
|
+
onclick={() => insertFromModal(definition.type)}
|
|
1137
|
+
>
|
|
1138
|
+
<ComponentPreviewThumbnail {definition} />
|
|
1139
|
+
<div class="border-t border-gray-200 p-4 dark:border-gray-700">
|
|
1140
|
+
<p class="text-heading text-sm font-semibold">{definition.type}</p>
|
|
1141
|
+
<p class="text-muted mt-1 line-clamp-2 text-xs leading-5">
|
|
1142
|
+
{definition.description}
|
|
1143
|
+
</p>
|
|
1144
|
+
</div>
|
|
1145
|
+
</button>
|
|
1146
|
+
{/each}
|
|
1147
|
+
</div>
|
|
1148
|
+
</div>
|
|
1149
|
+
</div>
|
|
1150
|
+
</div>
|
|
1151
|
+
{/if}
|
|
1152
|
+
|
|
1153
|
+
{#if activeReorderContext}
|
|
1154
|
+
<div class="fixed inset-0 z-50 flex items-center justify-center p-6">
|
|
1155
|
+
<button
|
|
1156
|
+
type="button"
|
|
1157
|
+
class="absolute inset-0 bg-black/40"
|
|
1158
|
+
aria-label="Chiudi modale riordino"
|
|
1159
|
+
onclick={closeReorderModal}
|
|
1160
|
+
></button>
|
|
1161
|
+
<div
|
|
1162
|
+
class="relative w-full max-w-3xl border border-gray-300 bg-white p-6 shadow-2xl dark:border-gray-700 dark:bg-gray-900"
|
|
1163
|
+
role="dialog"
|
|
1164
|
+
aria-modal="true"
|
|
1165
|
+
aria-label={`Riordina ${activeReorderContext.collection.label}`}
|
|
1166
|
+
tabindex="0"
|
|
1167
|
+
>
|
|
1168
|
+
<div class="flex flex-wrap items-start justify-between gap-4">
|
|
1169
|
+
<div>
|
|
1170
|
+
<h2 class="font-display text-heading text-2xl">
|
|
1171
|
+
Riordina {activeReorderContext.collection.label}
|
|
1172
|
+
</h2>
|
|
1173
|
+
<p class="text-muted mt-1 text-sm">
|
|
1174
|
+
Trascina gli elementi in una lista lineare per aggiornare l'ordine della collection.
|
|
1175
|
+
</p>
|
|
1176
|
+
</div>
|
|
1177
|
+
|
|
1178
|
+
<button
|
|
1179
|
+
type="button"
|
|
1180
|
+
class="border border-gray-300 px-4 py-2 text-sm font-medium text-gray-900 transition-colors hover:bg-gray-100 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-700"
|
|
1181
|
+
onclick={closeReorderModal}
|
|
1182
|
+
>
|
|
1183
|
+
Chiudi
|
|
1184
|
+
</button>
|
|
1185
|
+
</div>
|
|
1186
|
+
|
|
1187
|
+
<div class="mt-6 space-y-3" role="list">
|
|
1188
|
+
{#each activeReorderContext.items as item, itemIndex}
|
|
1189
|
+
<div
|
|
1190
|
+
class="flex cursor-move flex-wrap items-center justify-between gap-4 border-2 border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-800"
|
|
1191
|
+
role="listitem"
|
|
1192
|
+
draggable={true}
|
|
1193
|
+
ondragstart={() =>
|
|
1194
|
+
handleCollectionItemDragStart(
|
|
1195
|
+
activeReorderContext.block.id,
|
|
1196
|
+
activeReorderContext.collection.path,
|
|
1197
|
+
itemIndex
|
|
1198
|
+
)}
|
|
1199
|
+
ondragover={allowDrop}
|
|
1200
|
+
ondrop={() => handleCollectionItemDrop(itemIndex)}
|
|
1201
|
+
>
|
|
1202
|
+
<div class="flex min-w-0 items-center gap-4">
|
|
1203
|
+
<div
|
|
1204
|
+
class="text-muted flex h-12 w-12 items-center justify-center border border-gray-300 bg-white text-xs font-semibold dark:border-gray-700 dark:bg-gray-900"
|
|
1205
|
+
>
|
|
1206
|
+
{itemIndex + 1}
|
|
1207
|
+
</div>
|
|
1208
|
+
|
|
1209
|
+
<div class="min-w-0">
|
|
1210
|
+
<p class="text-heading truncate text-sm font-medium">
|
|
1211
|
+
{getCollectionItemSummary(item, activeReorderContext.collection, itemIndex)}
|
|
1212
|
+
</p>
|
|
1213
|
+
<p class="text-muted truncate text-xs">
|
|
1214
|
+
{activeReorderContext.collection.path}[{itemIndex}]
|
|
1215
|
+
</p>
|
|
1216
|
+
</div>
|
|
1217
|
+
</div>
|
|
1218
|
+
|
|
1219
|
+
<div class="flex flex-wrap items-center gap-2 text-sm">
|
|
1220
|
+
<button
|
|
1221
|
+
type="button"
|
|
1222
|
+
class="border border-gray-300 px-3 py-1.5 transition-colors hover:bg-gray-100 dark:border-gray-700 dark:hover:bg-gray-700"
|
|
1223
|
+
onclick={() =>
|
|
1224
|
+
moveItem(
|
|
1225
|
+
activeReorderContext.block,
|
|
1226
|
+
activeReorderContext.collection.path,
|
|
1227
|
+
itemIndex,
|
|
1228
|
+
-1
|
|
1229
|
+
)}
|
|
1230
|
+
>
|
|
1231
|
+
Su
|
|
1232
|
+
</button>
|
|
1233
|
+
<button
|
|
1234
|
+
type="button"
|
|
1235
|
+
class="border border-gray-300 px-3 py-1.5 transition-colors hover:bg-gray-100 dark:border-gray-700 dark:hover:bg-gray-700"
|
|
1236
|
+
onclick={() =>
|
|
1237
|
+
moveItem(
|
|
1238
|
+
activeReorderContext.block,
|
|
1239
|
+
activeReorderContext.collection.path,
|
|
1240
|
+
itemIndex,
|
|
1241
|
+
1
|
|
1242
|
+
)}
|
|
1243
|
+
>
|
|
1244
|
+
Giu
|
|
1245
|
+
</button>
|
|
1246
|
+
</div>
|
|
1247
|
+
</div>
|
|
1248
|
+
{/each}
|
|
1249
|
+
</div>
|
|
1250
|
+
</div>
|
|
1251
|
+
</div>
|
|
1252
|
+
{/if}
|
|
1253
|
+
|
|
1254
|
+
<style>
|
|
1255
|
+
:global(.builder-app *) {
|
|
1256
|
+
scrollbar-color: #cbd5e1 transparent;
|
|
1257
|
+
scrollbar-width: thin;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
:global(.builder-app *::-webkit-scrollbar) {
|
|
1261
|
+
width: 10px;
|
|
1262
|
+
height: 10px;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
:global(.builder-app *::-webkit-scrollbar-track) {
|
|
1266
|
+
background: transparent;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
:global(.builder-app *::-webkit-scrollbar-thumb) {
|
|
1270
|
+
min-height: 40px;
|
|
1271
|
+
border: 3px solid transparent;
|
|
1272
|
+
border-radius: 999px;
|
|
1273
|
+
background: #cbd5e1;
|
|
1274
|
+
background-clip: padding-box;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
:global(.builder-app *::-webkit-scrollbar-thumb:hover) {
|
|
1278
|
+
background: #fde047;
|
|
1279
|
+
background-clip: padding-box;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
:global(.builder-app *::-webkit-scrollbar-corner) {
|
|
1283
|
+
background: transparent;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
:global(.dark .builder-app *) {
|
|
1287
|
+
scrollbar-color: #475569 transparent;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
:global(.dark .builder-app *::-webkit-scrollbar-thumb) {
|
|
1291
|
+
background: #475569;
|
|
1292
|
+
background-clip: padding-box;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
:global(.dark .builder-app *::-webkit-scrollbar-thumb:hover) {
|
|
1296
|
+
background: #facc15;
|
|
1297
|
+
background-clip: padding-box;
|
|
1298
|
+
}
|
|
1299
|
+
</style>
|