@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,197 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { mount, unmount } from 'svelte';
|
|
3
|
+
import { createBuilderFallbackProps, normalizeBuilderPropsForRender } from '../core.js';
|
|
4
|
+
import type { BuilderRenderDefinition } from './contracts.js';
|
|
5
|
+
|
|
6
|
+
let { definition }: { definition: BuilderRenderDefinition } = $props();
|
|
7
|
+
|
|
8
|
+
function thumbnailFrame(node: HTMLIFrameElement): { update: () => void; destroy: () => void } {
|
|
9
|
+
let renderer: Record<string, unknown> | null = null;
|
|
10
|
+
let cleanupHeadSync: (() => void) | null = null;
|
|
11
|
+
let destroyed = false;
|
|
12
|
+
const sourceDocument = node.ownerDocument;
|
|
13
|
+
|
|
14
|
+
void render();
|
|
15
|
+
|
|
16
|
+
async function render(): Promise<void> {
|
|
17
|
+
const frameDocument = node.contentDocument;
|
|
18
|
+
if (!frameDocument) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (renderer) {
|
|
23
|
+
await unmount(renderer);
|
|
24
|
+
renderer = null;
|
|
25
|
+
}
|
|
26
|
+
cleanupHeadSync?.();
|
|
27
|
+
cleanupHeadSync = null;
|
|
28
|
+
|
|
29
|
+
frameDocument.open();
|
|
30
|
+
frameDocument.write(`<!doctype html>
|
|
31
|
+
<html>
|
|
32
|
+
<head>
|
|
33
|
+
<meta charset="utf-8">
|
|
34
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
35
|
+
<base href="${escapeHtml(sourceDocument.baseURI)}">
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<div id="component-preview-stage">
|
|
39
|
+
<div id="component-preview-root"></div>
|
|
40
|
+
</div>
|
|
41
|
+
</body>
|
|
42
|
+
</html>`);
|
|
43
|
+
frameDocument.close();
|
|
44
|
+
|
|
45
|
+
await Promise.resolve();
|
|
46
|
+
if (destroyed) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setupFrameDocument(frameDocument, sourceDocument);
|
|
51
|
+
cleanupHeadSync = syncHeadAssets(frameDocument, sourceDocument);
|
|
52
|
+
|
|
53
|
+
const target = frameDocument.getElementById('component-preview-root');
|
|
54
|
+
if (!target) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
renderer = mount(definition.component, {
|
|
59
|
+
target,
|
|
60
|
+
props: normalizeBuilderPropsForRender(createBuilderFallbackProps(definition)) as Record<
|
|
61
|
+
string,
|
|
62
|
+
unknown
|
|
63
|
+
>
|
|
64
|
+
}) as Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
update() {
|
|
69
|
+
void render();
|
|
70
|
+
},
|
|
71
|
+
destroy() {
|
|
72
|
+
destroyed = true;
|
|
73
|
+
cleanupHeadSync?.();
|
|
74
|
+
cleanupHeadSync = null;
|
|
75
|
+
if (renderer) {
|
|
76
|
+
void unmount(renderer);
|
|
77
|
+
renderer = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function setupFrameDocument(frameDocument: Document, sourceDocument: Document): void {
|
|
84
|
+
const styleElement = frameDocument.createElement('style');
|
|
85
|
+
styleElement.textContent = `
|
|
86
|
+
html,
|
|
87
|
+
body,
|
|
88
|
+
#component-preview-stage {
|
|
89
|
+
margin: 0;
|
|
90
|
+
min-height: 100%;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
body {
|
|
94
|
+
background: transparent;
|
|
95
|
+
color: #1e1c18;
|
|
96
|
+
overflow: hidden;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
* {
|
|
100
|
+
scrollbar-width: none;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
*::-webkit-scrollbar {
|
|
104
|
+
display: none;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
body.dark {
|
|
108
|
+
background: transparent;
|
|
109
|
+
color: #f3f4f6;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#component-preview-stage {
|
|
113
|
+
box-sizing: border-box;
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
justify-content: center;
|
|
117
|
+
min-height: 640px;
|
|
118
|
+
padding: 32px;
|
|
119
|
+
background:
|
|
120
|
+
linear-gradient(90deg, rgba(15, 23, 42, 0.07) 1px, transparent 1px),
|
|
121
|
+
linear-gradient(0deg, rgba(15, 23, 42, 0.07) 1px, transparent 1px),
|
|
122
|
+
radial-gradient(circle at top left, rgba(253, 224, 71, 0.18), transparent 34%),
|
|
123
|
+
linear-gradient(135deg, #fafaf5 0%, #fef9c3 100%);
|
|
124
|
+
background-size:
|
|
125
|
+
24px 24px,
|
|
126
|
+
24px 24px,
|
|
127
|
+
auto,
|
|
128
|
+
auto;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.dark #component-preview-stage {
|
|
132
|
+
background:
|
|
133
|
+
linear-gradient(90deg, rgba(255, 255, 255, 0.08) 1px, transparent 1px),
|
|
134
|
+
linear-gradient(0deg, rgba(255, 255, 255, 0.08) 1px, transparent 1px),
|
|
135
|
+
radial-gradient(circle at top left, rgba(253, 224, 71, 0.24), transparent 34%),
|
|
136
|
+
linear-gradient(135deg, #12100d 0%, #2d2a25 100%);
|
|
137
|
+
background-size:
|
|
138
|
+
24px 24px,
|
|
139
|
+
24px 24px,
|
|
140
|
+
auto,
|
|
141
|
+
auto;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#component-preview-root {
|
|
145
|
+
width: 100%;
|
|
146
|
+
}
|
|
147
|
+
`;
|
|
148
|
+
frameDocument.head.append(styleElement);
|
|
149
|
+
syncThemeClass(frameDocument, sourceDocument);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function syncHeadAssets(frameDocument: Document, sourceDocument: Document): () => void {
|
|
153
|
+
for (const asset of sourceDocument.head.querySelectorAll(
|
|
154
|
+
'link[rel="stylesheet"], link[rel="preconnect"], style'
|
|
155
|
+
)) {
|
|
156
|
+
const clone = asset.cloneNode(true) as HTMLElement;
|
|
157
|
+
if (asset instanceof HTMLLinkElement && clone instanceof HTMLLinkElement && asset.href) {
|
|
158
|
+
clone.href = asset.href;
|
|
159
|
+
}
|
|
160
|
+
frameDocument.head.append(clone);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return () => {
|
|
164
|
+
for (const asset of Array.from(frameDocument.head.children)) {
|
|
165
|
+
asset.remove();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function syncThemeClass(frameDocument: Document, sourceDocument: Document): void {
|
|
171
|
+
const isDark =
|
|
172
|
+
sourceDocument.documentElement.classList.contains('dark') ||
|
|
173
|
+
sourceDocument.body.classList.contains('dark');
|
|
174
|
+
frameDocument.documentElement.classList.toggle('dark', isDark);
|
|
175
|
+
frameDocument.body.classList.toggle('dark', isDark);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function escapeHtml(value: string): string {
|
|
179
|
+
return value
|
|
180
|
+
.replaceAll('&', '&')
|
|
181
|
+
.replaceAll('"', '"')
|
|
182
|
+
.replaceAll('<', '<')
|
|
183
|
+
.replaceAll('>', '>');
|
|
184
|
+
}
|
|
185
|
+
</script>
|
|
186
|
+
|
|
187
|
+
<div
|
|
188
|
+
class="relative h-48 w-full overflow-hidden border border-gray-200 bg-[radial-gradient(circle_at_top_left,rgba(253,224,71,0.18),transparent_34%),linear-gradient(135deg,#fafaf5_0%,#fef9c3_100%)] dark:border-gray-700 dark:bg-[radial-gradient(circle_at_top_left,rgba(253,224,71,0.24),transparent_34%),linear-gradient(135deg,#12100d_0%,#2d2a25_100%)]"
|
|
189
|
+
>
|
|
190
|
+
<iframe
|
|
191
|
+
use:thumbnailFrame
|
|
192
|
+
title={`Anteprima ${definition.type}`}
|
|
193
|
+
scrolling="no"
|
|
194
|
+
class="pointer-events-none absolute top-0 left-0 h-[640px] w-full origin-top-left scale-[0.3] border-0 bg-transparent"
|
|
195
|
+
style="width: 333.333%;"
|
|
196
|
+
></iframe>
|
|
197
|
+
</div>
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { BuilderBlock } from '../core.js';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
blocks,
|
|
6
|
+
activeBlockId,
|
|
7
|
+
onSelectBlock,
|
|
8
|
+
onDeselectBlock,
|
|
9
|
+
onMoveBlock,
|
|
10
|
+
onRemoveBlock,
|
|
11
|
+
onDragStart,
|
|
12
|
+
onAllowDrop,
|
|
13
|
+
onDrop
|
|
14
|
+
}: {
|
|
15
|
+
blocks: BuilderBlock[];
|
|
16
|
+
activeBlockId: string | null;
|
|
17
|
+
onSelectBlock: (blockId: string) => void;
|
|
18
|
+
onDeselectBlock: () => void;
|
|
19
|
+
onMoveBlock: (blockId: string, direction: -1 | 1) => void;
|
|
20
|
+
onRemoveBlock: (blockId: string) => void;
|
|
21
|
+
onDragStart: (blockId: string) => void;
|
|
22
|
+
onAllowDrop: (event: DragEvent) => void;
|
|
23
|
+
onDrop: (blockId: string) => void;
|
|
24
|
+
} = $props();
|
|
25
|
+
|
|
26
|
+
let openActionBlockId = $state<string | null>(null);
|
|
27
|
+
let draggedBlockId = $state<string | null>(null);
|
|
28
|
+
let dropTargetBlockId = $state<string | null>(null);
|
|
29
|
+
|
|
30
|
+
function toggleActionMenu(blockId: string, event: MouseEvent): void {
|
|
31
|
+
event.stopPropagation();
|
|
32
|
+
openActionBlockId = openActionBlockId === blockId ? null : blockId;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function closeActionMenu(event?: MouseEvent): void {
|
|
36
|
+
event?.stopPropagation();
|
|
37
|
+
openActionBlockId = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function moveBlock(blockId: string, direction: -1 | 1): void {
|
|
41
|
+
onMoveBlock(blockId, direction);
|
|
42
|
+
closeActionMenu();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function removeBlock(blockId: string): void {
|
|
46
|
+
onRemoveBlock(blockId);
|
|
47
|
+
closeActionMenu();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function startDrag(blockId: string): void {
|
|
51
|
+
draggedBlockId = blockId;
|
|
52
|
+
dropTargetBlockId = null;
|
|
53
|
+
onDragStart(blockId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function dragOverBlock(blockId: string, event: DragEvent): void {
|
|
57
|
+
onAllowDrop(event);
|
|
58
|
+
dropTargetBlockId = blockId;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function leaveBlock(blockId: string, event: DragEvent): void {
|
|
62
|
+
if (
|
|
63
|
+
dropTargetBlockId !== blockId ||
|
|
64
|
+
(event.relatedTarget instanceof Node && event.currentTarget instanceof Node && event.currentTarget.contains(event.relatedTarget))
|
|
65
|
+
) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
dropTargetBlockId = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function dropBlock(blockId: string): void {
|
|
73
|
+
onDrop(blockId);
|
|
74
|
+
draggedBlockId = null;
|
|
75
|
+
dropTargetBlockId = null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function endDrag(): void {
|
|
79
|
+
draggedBlockId = null;
|
|
80
|
+
dropTargetBlockId = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getDropPlacement(blockId: string): 'before' | 'after' | null {
|
|
84
|
+
if (!draggedBlockId || draggedBlockId === blockId || dropTargetBlockId !== blockId) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const draggedIndex = blocks.findIndex((block) => block.id === draggedBlockId);
|
|
89
|
+
const targetIndex = blocks.findIndex((block) => block.id === blockId);
|
|
90
|
+
if (draggedIndex === -1 || targetIndex === -1) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return draggedIndex < targetIndex ? 'after' : 'before';
|
|
95
|
+
}
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<aside class="flex h-full min-h-0 w-full flex-col border-r border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900" aria-label="Page flow" onclick={(event) => { if (!(event.target as Element).closest('[data-page-flow-block]')) onDeselectBlock(); }}>
|
|
99
|
+
<div class="min-h-0 flex-1 overflow-y-auto py-2">
|
|
100
|
+
{#if blocks.length === 0}
|
|
101
|
+
<p class="text-muted mx-3 border border-dashed border-gray-300 px-4 py-6 text-sm dark:border-gray-600">
|
|
102
|
+
No briks on the page.
|
|
103
|
+
</p>
|
|
104
|
+
{/if}
|
|
105
|
+
|
|
106
|
+
<div role="list" aria-label="Page flow">
|
|
107
|
+
{#each blocks as block (block.id)}
|
|
108
|
+
{@const dropPlacement = getDropPlacement(block.id)}
|
|
109
|
+
<div
|
|
110
|
+
data-page-flow-block
|
|
111
|
+
class={activeBlockId === block.id
|
|
112
|
+
? 'relative flex items-center gap-2 border-l-4 border-[#FDE047] bg-yellow-50 px-3 py-2 text-gray-900 dark:border-[#FACC15] dark:bg-yellow-950/40 dark:text-gray-100'
|
|
113
|
+
: 'relative flex items-center gap-2 border-l-4 border-transparent px-3 py-2 text-gray-900 transition-colors hover:bg-gray-100 dark:text-gray-100 dark:hover:bg-gray-700'}
|
|
114
|
+
role="listitem"
|
|
115
|
+
draggable={true}
|
|
116
|
+
ondragstart={() => startDrag(block.id)}
|
|
117
|
+
ondragover={(event) => dragOverBlock(block.id, event)}
|
|
118
|
+
ondragleave={(event) => leaveBlock(block.id, event)}
|
|
119
|
+
ondrop={() => dropBlock(block.id)}
|
|
120
|
+
ondragend={endDrag}>
|
|
121
|
+
{#if dropPlacement === 'before'}
|
|
122
|
+
<div class="pointer-events-none absolute -top-0.5 right-2 left-2 z-30 h-0.5 bg-[#FDE047] shadow-[0_0_0_1px_rgba(253,224,71,0.15)] dark:bg-[#FACC15]"></div>
|
|
123
|
+
{:else if dropPlacement === 'after'}
|
|
124
|
+
<div class="pointer-events-none absolute right-2 -bottom-0.5 left-2 z-30 h-0.5 bg-[#FDE047] shadow-[0_0_0_1px_rgba(253,224,71,0.15)] dark:bg-[#FACC15]"></div>
|
|
125
|
+
{/if}
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
class="absolute inset-0 z-0 cursor-pointer"
|
|
129
|
+
aria-label={`Select brik ${block.type}`}
|
|
130
|
+
onclick={() => onSelectBlock(block.id)}></button>
|
|
131
|
+
<span class="pointer-events-none relative z-10 min-w-0 flex-1 truncate text-sm font-medium">
|
|
132
|
+
{block.type}
|
|
133
|
+
</span>
|
|
134
|
+
|
|
135
|
+
<div class="relative z-20 shrink-0">
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
class="flex h-7 w-7 items-center justify-center border border-transparent text-gray-700 transition-colors hover:border-gray-300 hover:bg-white dark:text-gray-200 dark:hover:border-gray-600 dark:hover:bg-gray-800"
|
|
139
|
+
aria-label={`Azioni per ${block.type}`}
|
|
140
|
+
aria-expanded={openActionBlockId === block.id}
|
|
141
|
+
onclick={(event) => toggleActionMenu(block.id, event)}>
|
|
142
|
+
<svg class="h-4 w-4 opacity-50" viewBox="0 0 16 16" aria-hidden="true">
|
|
143
|
+
<circle cx="3" cy="8" r="1.4" fill="currentColor" />
|
|
144
|
+
<circle cx="8" cy="8" r="1.4" fill="currentColor" />
|
|
145
|
+
<circle cx="13" cy="8" r="1.4" fill="currentColor" />
|
|
146
|
+
</svg>
|
|
147
|
+
</button>
|
|
148
|
+
|
|
149
|
+
{#if openActionBlockId === block.id}
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
class="fixed inset-0 z-40 cursor-default bg-black/20 dark:bg-black/40"
|
|
153
|
+
aria-label="Close actions menu"
|
|
154
|
+
onclick={(event) => closeActionMenu(event)}></button>
|
|
155
|
+
|
|
156
|
+
<div
|
|
157
|
+
class="absolute right-0 z-50 mt-1 grid min-w-48 gap-1 border border-gray-200 bg-white p-1.5 text-sm shadow-lg dark:border-gray-700 dark:bg-gray-800">
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
class="flex items-center gap-3 px-3.5 py-2 text-left text-gray-900 transition-colors hover:bg-gray-100 dark:text-gray-100 dark:hover:bg-gray-700"
|
|
161
|
+
onclick={() => moveBlock(block.id, -1)}>
|
|
162
|
+
<svg class="h-4 w-4" viewBox="0 0 16 16" aria-hidden="true">
|
|
163
|
+
<path
|
|
164
|
+
d="M8 3.25 3.75 7.5l.9.9L7.35 5.7V13h1.3V5.7l2.7 2.7.9-.9L8 3.25Z"
|
|
165
|
+
fill="currentColor" />
|
|
166
|
+
</svg>
|
|
167
|
+
Move up
|
|
168
|
+
</button>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
class="flex items-center gap-3 px-3.5 py-2 text-left text-gray-900 transition-colors hover:bg-gray-100 dark:text-gray-100 dark:hover:bg-gray-700"
|
|
172
|
+
onclick={() => moveBlock(block.id, 1)}>
|
|
173
|
+
<svg class="h-4 w-4" viewBox="0 0 16 16" aria-hidden="true">
|
|
174
|
+
<path
|
|
175
|
+
d="M8 12.75 3.75 8.5l.9-.9 2.7 2.7V3h1.3v7.3l2.7-2.7.9.9L8 12.75Z"
|
|
176
|
+
fill="currentColor" />
|
|
177
|
+
</svg>
|
|
178
|
+
Move down
|
|
179
|
+
</button>
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
class="flex items-center gap-3 px-3.5 py-2 text-left text-red-600 transition-colors hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950/30"
|
|
183
|
+
onclick={() => removeBlock(block.id)}>
|
|
184
|
+
<svg class="h-4 w-4" viewBox="0 0 16 16" aria-hidden="true">
|
|
185
|
+
<path
|
|
186
|
+
d="M6.25 2.5h3.5l.6 1.25H13V5H3V3.75h2.65l.6-1.25Zm-1.8 3.75h1.25l.35 6.25h3.9l.35-6.25h1.25l-.4 7.5H4.85l-.4-7.5Z"
|
|
187
|
+
fill="currentColor" />
|
|
188
|
+
</svg>
|
|
189
|
+
Remove
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
{/if}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
{/each}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</aside>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
placement = 'after',
|
|
4
|
+
edgeInset = false,
|
|
5
|
+
onToggle
|
|
6
|
+
}: {
|
|
7
|
+
placement?: 'before' | 'after';
|
|
8
|
+
edgeInset?: boolean;
|
|
9
|
+
onToggle: () => void;
|
|
10
|
+
} = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div
|
|
14
|
+
class={placement === 'before'
|
|
15
|
+
? edgeInset
|
|
16
|
+
? 'pointer-events-none absolute top-2 right-0 left-0 z-40 opacity-0 transition group-hover:opacity-100 focus-within:opacity-100'
|
|
17
|
+
: 'pointer-events-none absolute top-0 right-0 left-0 z-40 -translate-y-1/2 opacity-0 transition group-hover:opacity-100 focus-within:opacity-100'
|
|
18
|
+
: edgeInset
|
|
19
|
+
? 'pointer-events-none absolute right-0 bottom-2 left-0 z-40 opacity-0 transition group-hover:opacity-100 focus-within:opacity-100'
|
|
20
|
+
: 'pointer-events-none absolute right-0 bottom-0 left-0 z-40 translate-y-1/2 opacity-0 transition group-hover:opacity-100 focus-within:opacity-100'}
|
|
21
|
+
>
|
|
22
|
+
<div class="pointer-events-auto relative mx-auto flex w-max justify-center">
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
class="btn-brutal-icon relative flex h-8 w-8 items-center justify-center text-lg leading-none"
|
|
26
|
+
aria-label="Aggiungi componente"
|
|
27
|
+
onclick={(event) => {
|
|
28
|
+
event.stopPropagation();
|
|
29
|
+
onToggle();
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
+
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { HelpCircle, Trash2 } from 'lucide-svelte';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
element,
|
|
7
|
+
onPick,
|
|
8
|
+
onRemove,
|
|
9
|
+
onFocus,
|
|
10
|
+
onBlur
|
|
11
|
+
}: {
|
|
12
|
+
element: HTMLElement;
|
|
13
|
+
onPick: () => void;
|
|
14
|
+
onRemove: () => void;
|
|
15
|
+
onFocus: () => void;
|
|
16
|
+
onBlur: () => void;
|
|
17
|
+
} = $props();
|
|
18
|
+
|
|
19
|
+
let coords = $state({ top: 0, left: 0, width: 0, height: 0 });
|
|
20
|
+
let isOpen = $state(false);
|
|
21
|
+
|
|
22
|
+
let hasIcon = $derived(
|
|
23
|
+
element &&
|
|
24
|
+
!element.hasAttribute('data-builder-icon-empty') &&
|
|
25
|
+
element.innerHTML.trim() !== ''
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
function updateCoords() {
|
|
29
|
+
const rect = element.getBoundingClientRect();
|
|
30
|
+
coords = {
|
|
31
|
+
top: rect.top,
|
|
32
|
+
left: rect.left,
|
|
33
|
+
width: rect.width,
|
|
34
|
+
height: rect.height
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function clickListener(node: HTMLButtonElement, action: () => void) {
|
|
39
|
+
const handler = (event: MouseEvent) => {
|
|
40
|
+
event.stopPropagation();
|
|
41
|
+
action();
|
|
42
|
+
};
|
|
43
|
+
node.addEventListener('click', handler);
|
|
44
|
+
return {
|
|
45
|
+
destroy() {
|
|
46
|
+
node.removeEventListener('click', handler);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function close() {
|
|
52
|
+
isOpen = false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onMount(() => {
|
|
56
|
+
updateCoords();
|
|
57
|
+
|
|
58
|
+
const win = element.ownerDocument.defaultView || window;
|
|
59
|
+
win.addEventListener('scroll', updateCoords, { passive: true });
|
|
60
|
+
win.addEventListener('resize', updateCoords, { passive: true });
|
|
61
|
+
|
|
62
|
+
// Click to activate and open menu
|
|
63
|
+
const handleElementClick = (event: MouseEvent) => {
|
|
64
|
+
event.stopPropagation();
|
|
65
|
+
isOpen = true;
|
|
66
|
+
onFocus();
|
|
67
|
+
updateCoords();
|
|
68
|
+
};
|
|
69
|
+
element.addEventListener('click', handleElementClick);
|
|
70
|
+
|
|
71
|
+
// Click outside to blur/close
|
|
72
|
+
const handleGlobalClick = (event: MouseEvent) => {
|
|
73
|
+
const target = event.target as Element;
|
|
74
|
+
if (!target) return;
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
!target.closest('.builder-preview-icon-menu') &&
|
|
78
|
+
target !== element &&
|
|
79
|
+
!element.contains(target)
|
|
80
|
+
) {
|
|
81
|
+
isOpen = false;
|
|
82
|
+
onBlur();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
win.document.addEventListener('click', handleGlobalClick, true);
|
|
87
|
+
|
|
88
|
+
// Observe changes in element structure to update coordinates
|
|
89
|
+
const observer = new MutationObserver(() => {
|
|
90
|
+
updateCoords();
|
|
91
|
+
});
|
|
92
|
+
observer.observe(element, { attributes: true, childList: true, subtree: true });
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
win.removeEventListener('scroll', updateCoords);
|
|
96
|
+
win.removeEventListener('resize', updateCoords);
|
|
97
|
+
element.removeEventListener('click', handleElementClick);
|
|
98
|
+
win.document.removeEventListener('click', handleGlobalClick, true);
|
|
99
|
+
observer.disconnect();
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
{#if isOpen}
|
|
105
|
+
<div
|
|
106
|
+
class="builder-preview-icon-menu"
|
|
107
|
+
style="
|
|
108
|
+
position: fixed;
|
|
109
|
+
top: {coords.top + coords.height + 6}px;
|
|
110
|
+
left: {coords.left + coords.width / 2}px;
|
|
111
|
+
transform: translate(-50%, 0);
|
|
112
|
+
z-index: 99999;
|
|
113
|
+
"
|
|
114
|
+
>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
use:clickListener={onPick}
|
|
118
|
+
class="menu-item"
|
|
119
|
+
>
|
|
120
|
+
<HelpCircle size={14} />
|
|
121
|
+
<span>Choose Icon...</span>
|
|
122
|
+
</button>
|
|
123
|
+
{#if hasIcon}
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
use:clickListener={onRemove}
|
|
127
|
+
class="menu-item menu-item-danger"
|
|
128
|
+
>
|
|
129
|
+
<Trash2 size={14} />
|
|
130
|
+
<span>Remove Icon</span>
|
|
131
|
+
</button>
|
|
132
|
+
{/if}
|
|
133
|
+
</div>
|
|
134
|
+
{/if}
|
|
135
|
+
|
|
136
|
+
<style>
|
|
137
|
+
.builder-preview-icon-menu {
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
141
|
+
background-color: rgba(255, 255, 255, 0.95);
|
|
142
|
+
backdrop-filter: blur(12px);
|
|
143
|
+
-webkit-backdrop-filter: blur(12px);
|
|
144
|
+
border-radius: 8px;
|
|
145
|
+
padding: 4px;
|
|
146
|
+
font-size: 0.75rem;
|
|
147
|
+
color: #2d2a25;
|
|
148
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
|
149
|
+
min-width: 140px;
|
|
150
|
+
animation: popover-in 0.15s cubic-bezier(0.16, 1, 0.3, 1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@keyframes popover-in {
|
|
154
|
+
from {
|
|
155
|
+
opacity: 0;
|
|
156
|
+
transform: translate(-50%, -4px) scale(0.95);
|
|
157
|
+
}
|
|
158
|
+
to {
|
|
159
|
+
opacity: 1;
|
|
160
|
+
transform: translate(-50%, 0) scale(1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
:global(.dark) .builder-preview-icon-menu {
|
|
165
|
+
border-color: rgba(255, 255, 255, 0.08);
|
|
166
|
+
background-color: rgba(31, 41, 55, 0.95);
|
|
167
|
+
color: #f3f4f6;
|
|
168
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -4px rgba(0, 0, 0, 0.3);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.menu-item {
|
|
172
|
+
display: flex;
|
|
173
|
+
align-items: center;
|
|
174
|
+
gap: 0.5rem;
|
|
175
|
+
width: 100%;
|
|
176
|
+
border: none;
|
|
177
|
+
background: transparent;
|
|
178
|
+
padding: 6px 12px;
|
|
179
|
+
color: inherit;
|
|
180
|
+
font-size: inherit;
|
|
181
|
+
font-weight: 500;
|
|
182
|
+
text-align: left;
|
|
183
|
+
cursor: pointer;
|
|
184
|
+
border-radius: 6px;
|
|
185
|
+
transition: background-color 0.15s, color 0.15s;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.menu-item:hover {
|
|
189
|
+
background-color: #f3f4f6;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
:global(.dark) .menu-item:hover {
|
|
193
|
+
background-color: #444039;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.menu-item-danger {
|
|
197
|
+
color: #dc2626;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.menu-item-danger:hover {
|
|
201
|
+
background-color: #fef2f2;
|
|
202
|
+
color: #b91c1c;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
:global(.dark) .menu-item-danger {
|
|
206
|
+
color: #f87171;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
:global(.dark) .menu-item-danger:hover {
|
|
210
|
+
background-color: rgba(239, 68, 68, 0.15);
|
|
211
|
+
color: #fca5a5;
|
|
212
|
+
}
|
|
213
|
+
</style>
|