@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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/dist/core.d.ts +103 -0
  4. package/dist/core.d.ts.map +1 -0
  5. package/dist/core.js +758 -0
  6. package/dist/core.js.map +1 -0
  7. package/dist/editor/BuilderApp.svelte +1299 -0
  8. package/dist/editor/BuilderFieldEditor.svelte +274 -0
  9. package/dist/editor/BuilderInspector.svelte +123 -0
  10. package/dist/editor/BuilderPreviewFrame.svelte +661 -0
  11. package/dist/editor/ComponentPreviewThumbnail.svelte +197 -0
  12. package/dist/editor/PageFlowSidebar.svelte +198 -0
  13. package/dist/editor/PreviewBlockInserter.svelte +35 -0
  14. package/dist/editor/PreviewIconEditor.svelte +213 -0
  15. package/dist/editor/PreviewImageEditor.svelte +221 -0
  16. package/dist/editor/PreviewTextEditor.svelte +246 -0
  17. package/dist/editor/RichTextEditor.svelte +234 -0
  18. package/dist/editor/contracts.d.ts +57 -0
  19. package/dist/editor/contracts.d.ts.map +1 -0
  20. package/dist/editor/contracts.js +2 -0
  21. package/dist/editor/contracts.js.map +1 -0
  22. package/dist/editor/index.d.ts +3 -0
  23. package/dist/editor/index.d.ts.map +1 -0
  24. package/dist/editor/index.js +2 -0
  25. package/dist/editor/index.js.map +1 -0
  26. package/dist/editor/shortcuts.d.ts +28 -0
  27. package/dist/editor/shortcuts.d.ts.map +1 -0
  28. package/dist/editor/shortcuts.js +28 -0
  29. package/dist/editor/shortcuts.js.map +1 -0
  30. package/dist/editor-controller.d.ts +50 -0
  31. package/dist/editor-controller.d.ts.map +1 -0
  32. package/dist/editor-controller.js +157 -0
  33. package/dist/editor-controller.js.map +1 -0
  34. package/dist/index.d.ts +7 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +6 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/preview/field-edit-debug.d.ts +5 -0
  39. package/dist/preview/field-edit-debug.d.ts.map +1 -0
  40. package/dist/preview/field-edit-debug.js +36 -0
  41. package/dist/preview/field-edit-debug.js.map +1 -0
  42. package/dist/preview/interactive-content.d.ts +8 -0
  43. package/dist/preview/interactive-content.d.ts.map +1 -0
  44. package/dist/preview/interactive-content.js +62 -0
  45. package/dist/preview/interactive-content.js.map +1 -0
  46. package/dist/preview-dom.d.ts +67 -0
  47. package/dist/preview-dom.d.ts.map +1 -0
  48. package/dist/preview-dom.js +191 -0
  49. package/dist/preview-dom.js.map +1 -0
  50. package/dist/svelte/SveltePreviewRenderer.svelte +490 -0
  51. package/dist/svelte/adapter.d.ts +7 -0
  52. package/dist/svelte/adapter.d.ts.map +1 -0
  53. package/dist/svelte/adapter.js +66 -0
  54. package/dist/svelte/adapter.js.map +1 -0
  55. package/dist/svelte/index.d.ts +3 -0
  56. package/dist/svelte/index.d.ts.map +1 -0
  57. package/dist/svelte/index.js +3 -0
  58. package/dist/svelte/index.js.map +1 -0
  59. package/dist/svelte/markup-schema.d.ts +5 -0
  60. package/dist/svelte/markup-schema.d.ts.map +1 -0
  61. package/dist/svelte/markup-schema.js +177 -0
  62. package/dist/svelte/markup-schema.js.map +1 -0
  63. package/package.json +56 -0
@@ -0,0 +1,274 @@
1
+ <script lang="ts">
2
+ import BuilderFieldEditor from './BuilderFieldEditor.svelte';
3
+ import RichTextEditor from './RichTextEditor.svelte';
4
+ import {
5
+ inferBuilderFieldKind,
6
+ isRichTextValue,
7
+ type BuilderField,
8
+ type BuilderRichTextValue
9
+ } from '../core.js';
10
+
11
+ let {
12
+ fieldKey,
13
+ field,
14
+ path,
15
+ value,
16
+ onChange,
17
+ onQueueFileEdit,
18
+ onAddItem,
19
+ onRemoveItem,
20
+ onMoveItem
21
+ }: {
22
+ fieldKey: string;
23
+ field: BuilderField;
24
+ path: string;
25
+ value: unknown;
26
+ onChange: (path: string, value: unknown) => void;
27
+ onQueueFileEdit: (path: string) => void;
28
+ onAddItem: (path: string) => void;
29
+ onRemoveItem: (path: string, index: number) => void;
30
+ onMoveItem: (path: string, index: number, direction: -1 | 1) => void;
31
+ } = $props();
32
+
33
+ const fieldKind = $derived(inferBuilderFieldKind(field));
34
+ const label = $derived(field.label ?? humanizeFieldKey(fieldKey));
35
+ const objectEntries = $derived(Object.entries(field.fields ?? {}));
36
+ const arrayItems = $derived(Array.isArray(value) ? value : []);
37
+ const stringValue = $derived(typeof value === 'string' ? value : '');
38
+ const numberValue = $derived(typeof value === 'number' ? value : 0);
39
+ const booleanValue = $derived(value === true);
40
+ const richTextValue = $derived(
41
+ isRichTextValue(value)
42
+ ? value
43
+ : ({
44
+ kind: 'richtext',
45
+ mode: fieldKind === 'richtext-inline' ? 'inline' : 'block',
46
+ html: '',
47
+ json: null
48
+ } satisfies BuilderRichTextValue)
49
+ );
50
+ const multiline = $derived(
51
+ stringValue.includes('\n') ||
52
+ stringValue.length > 120 ||
53
+ /quote|subtitle|description|content/i.test(label)
54
+ );
55
+
56
+ function updateText(nextValue: string): void {
57
+ onChange(path, nextValue);
58
+ }
59
+
60
+ function updateNumber(nextValue: string): void {
61
+ const parsedValue = Number(nextValue);
62
+ onChange(path, Number.isFinite(parsedValue) ? parsedValue : 0);
63
+ }
64
+
65
+ function updateBoolean(nextValue: boolean): void {
66
+ onChange(path, nextValue);
67
+ }
68
+
69
+ function getObjectValue(source: unknown, key: string): unknown {
70
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
71
+ return undefined;
72
+ }
73
+
74
+ return (source as Record<string, unknown>)[key];
75
+ }
76
+
77
+ function getItemLabel(item: unknown, index: number): string {
78
+ if (
79
+ field.summaryField &&
80
+ item &&
81
+ typeof item === 'object' &&
82
+ !Array.isArray(item) &&
83
+ typeof (item as Record<string, unknown>)[field.summaryField] === 'string'
84
+ ) {
85
+ return (item as Record<string, string>)[field.summaryField];
86
+ }
87
+
88
+ return `${field.itemLabel ?? 'Elemento'} ${index + 1}`;
89
+ }
90
+
91
+ function humanizeFieldKey(value: string): string {
92
+ return value
93
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
94
+ .replace(/[-_]/g, ' ')
95
+ .replace(/\s+/g, ' ')
96
+ .trim()
97
+ .replace(/^\w/, (match) => match.toUpperCase());
98
+ }
99
+ </script>
100
+
101
+ {#if fieldKind === 'object' && field.fields}
102
+ <div class="space-y-3 border-t border-gray-200 pt-4 dark:border-gray-700">
103
+ <p class="text-heading text-sm font-semibold">{label}</p>
104
+
105
+ {#each objectEntries as [nestedKey, nestedField] (nestedKey)}
106
+ <BuilderFieldEditor
107
+ fieldKey={nestedKey}
108
+ field={nestedField}
109
+ path={`${path}.${nestedKey}`}
110
+ value={getObjectValue(value, nestedKey)}
111
+ {onChange}
112
+ {onQueueFileEdit}
113
+ {onAddItem}
114
+ {onRemoveItem}
115
+ {onMoveItem} />
116
+ {/each}
117
+ </div>
118
+ {:else if fieldKind === 'array' && field.item}
119
+ <div class="space-y-3 border-t border-gray-200 pt-4 dark:border-gray-700">
120
+ <div class="flex items-center justify-between gap-3">
121
+ <p class="text-heading text-sm font-semibold">{label}</p>
122
+ <button
123
+ type="button"
124
+ class="border border-gray-300 bg-white px-2 py-1 text-xs font-medium text-gray-900 transition-colors hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700"
125
+ onclick={() => onAddItem(path)}>
126
+ + Add {field.itemLabel ?? 'item'}
127
+ </button>
128
+ </div>
129
+
130
+ {#if arrayItems.length === 0}
131
+ <p class="text-muted text-sm">No items configured.</p>
132
+ {/if}
133
+
134
+ {#each arrayItems as item, index (`${path}-${index}`)}
135
+ <div class="space-y-3 border border-gray-200 bg-white p-3 dark:border-gray-700 dark:bg-gray-800">
136
+ <div class="flex items-center justify-between gap-3">
137
+ <p class="text-heading text-sm font-medium">{getItemLabel(item, index)}</p>
138
+ <div class="flex flex-wrap gap-2">
139
+ <button
140
+ type="button"
141
+ class="border border-gray-300 px-2 py-1 text-xs transition-colors hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-700"
142
+ onclick={() => onMoveItem(path, index, -1)}>
143
+ Up
144
+ </button>
145
+ <button
146
+ type="button"
147
+ class="border border-gray-300 px-2 py-1 text-xs transition-colors hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-700"
148
+ onclick={() => onMoveItem(path, index, 1)}>
149
+ Down
150
+ </button>
151
+ <button
152
+ type="button"
153
+ class="border border-red-300 px-2 py-1 text-xs text-red-600 transition-colors hover:bg-red-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-950"
154
+ onclick={() => onRemoveItem(path, index)}>
155
+ Remove
156
+ </button>
157
+ </div>
158
+ </div>
159
+
160
+ {#if field.item.fields}
161
+ {#each Object.entries(field.item.fields) as [nestedKey, nestedField] (nestedKey)}
162
+ <BuilderFieldEditor
163
+ fieldKey={nestedKey}
164
+ field={nestedField}
165
+ path={`${path}[${index}].${nestedKey}`}
166
+ value={getObjectValue(item, nestedKey)}
167
+ {onChange}
168
+ {onQueueFileEdit}
169
+ {onAddItem}
170
+ {onRemoveItem}
171
+ {onMoveItem} />
172
+ {/each}
173
+ {:else}
174
+ <BuilderFieldEditor
175
+ fieldKey={`${fieldKey}-${index}`}
176
+ field={field.item}
177
+ path={`${path}[${index}]`}
178
+ value={item}
179
+ {onChange}
180
+ {onQueueFileEdit}
181
+ {onAddItem}
182
+ {onRemoveItem}
183
+ {onMoveItem} />
184
+ {/if}
185
+ </div>
186
+ {/each}
187
+ </div>
188
+ {:else if fieldKind === 'richtext-inline' || fieldKind === 'richtext-block'}
189
+ <label class="block">
190
+ <span class="mb-1 block text-sm font-medium text-label">{label}</span>
191
+ <RichTextEditor
192
+ value={richTextValue}
193
+ mode={fieldKind === 'richtext-inline' ? 'inline' : 'block'}
194
+ onChange={(nextValue) => onChange(path, nextValue)} />
195
+ </label>
196
+ {:else if fieldKind === 'boolean'}
197
+ <label class="flex items-center gap-3 border border-gray-200 bg-gray-50 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-800">
198
+ <input
199
+ type="checkbox"
200
+ checked={booleanValue}
201
+ class="h-4 w-4 rounded border-gray-300 dark:border-gray-600"
202
+ onchange={(event) => updateBoolean(event.currentTarget.checked)} />
203
+ <span class="text-sm font-medium text-label">{label}</span>
204
+ </label>
205
+ {:else if fieldKind === 'number'}
206
+ <label class="block">
207
+ <span class="mb-1 block text-sm font-medium text-label">{label}</span>
208
+ <input
209
+ type="number"
210
+ value={numberValue}
211
+ class="block w-full border border-gray-300 bg-white px-4 py-3 text-sm text-gray-900 placeholder-gray-400 focus:border-[#FDE047] focus:outline-none focus:ring-1 focus:ring-[#FDE047] dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500 dark:focus:border-[#FACC15] dark:focus:ring-[#FACC15]"
212
+ oninput={(event) => updateNumber(event.currentTarget.value)} />
213
+ </label>
214
+ {:else if fieldKind === 'image'}
215
+ <label class="block">
216
+ <span class="mb-1 block text-sm font-medium text-label">{label}</span>
217
+ {#if stringValue}
218
+ <img
219
+ src={stringValue}
220
+ alt={label}
221
+ class="mb-2 h-24 w-24 border border-gray-200 object-cover dark:border-gray-700" />
222
+ {/if}
223
+ <div class="flex flex-wrap gap-2">
224
+ <input
225
+ type="text"
226
+ value={stringValue}
227
+ class="min-w-0 flex-1 border border-gray-300 bg-white px-4 py-3 text-sm text-gray-900 placeholder-gray-400 focus:border-[#FDE047] focus:outline-none focus:ring-1 focus:ring-[#FDE047] dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500 dark:focus:border-[#FACC15] dark:focus:ring-[#FACC15]"
228
+ oninput={(event) => updateText(event.currentTarget.value)} />
229
+ <button
230
+ type="button"
231
+ class="border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-900 transition-colors hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700"
232
+ onclick={() => onQueueFileEdit(path)}>
233
+ File
234
+ </button>
235
+ </div>
236
+ </label>
237
+ {:else if fieldKind === 'icon'}
238
+ <label class="block">
239
+ <span class="mb-1 block text-sm font-medium text-label">{label}</span>
240
+ <div class="flex flex-wrap gap-2">
241
+ <div class="min-w-0 flex-1 flex items-center justify-center h-[46px] border border-gray-300 bg-white px-4 text-sm text-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100">
242
+ {#if stringValue}
243
+ <span class="inline-block h-6 w-6 text-gray-700 dark:text-gray-300">
244
+ {@html stringValue}
245
+ </span>
246
+ {:else}
247
+ <span class="text-gray-400 dark:text-gray-500 text-xs">No icon</span>
248
+ {/if}
249
+ </div>
250
+ <button
251
+ type="button"
252
+ class="border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-900 transition-colors hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700"
253
+ onclick={() => onQueueFileEdit(path)}>
254
+ Choose
255
+ </button>
256
+ </div>
257
+ </label>
258
+ {:else}
259
+ <label class="block">
260
+ <span class="mb-1 block text-sm font-medium text-label">{label}</span>
261
+ {#if multiline}
262
+ <textarea
263
+ value={stringValue}
264
+ class="min-h-24 w-full border border-gray-300 bg-white px-4 py-3 font-mono text-sm text-gray-900 placeholder-gray-400 focus:border-[#FDE047] focus:outline-none focus:ring-1 focus:ring-[#FDE047] dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500 dark:focus:border-[#FACC15] dark:focus:ring-[#FACC15]"
265
+ oninput={(event) => updateText(event.currentTarget.value)}></textarea>
266
+ {:else}
267
+ <input
268
+ type="text"
269
+ value={stringValue}
270
+ class="block w-full border border-gray-300 bg-white px-4 py-3 text-sm text-gray-900 placeholder-gray-400 focus:border-[#FDE047] focus:outline-none focus:ring-1 focus:ring-[#FDE047] dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500 dark:focus:border-[#FACC15] dark:focus:ring-[#FACC15]"
271
+ oninput={(event) => updateText(event.currentTarget.value)} />
272
+ {/if}
273
+ </label>
274
+ {/if}
@@ -0,0 +1,123 @@
1
+ <script lang="ts">
2
+ import { getCollectionItemSummary, type BuilderBlock, type BuilderFields } from '../core.js';
3
+ import type { BuilderRenderDefinition } from './contracts.js';
4
+ import BuilderFieldEditor from './BuilderFieldEditor.svelte';
5
+
6
+ let {
7
+ title,
8
+ description,
9
+ activeBlock,
10
+ activeDefinition,
11
+ inspectorFields,
12
+ propsError,
13
+ mdsvexOutput,
14
+ copied,
15
+ onTitleChange,
16
+ onDescriptionChange,
17
+ onFieldChange,
18
+ onQueueFileEdit,
19
+ onAddItem,
20
+ onRemoveItem,
21
+ onMoveItem,
22
+ onCopyMdsvex,
23
+ onDeselectBlock
24
+ }: {
25
+ title: string;
26
+ description: string;
27
+ activeBlock: BuilderBlock | null;
28
+ activeDefinition: BuilderRenderDefinition | null;
29
+ inspectorFields: BuilderFields;
30
+ propsError: string | null;
31
+ mdsvexOutput: string;
32
+ copied: boolean;
33
+ onTitleChange: (value: string) => void;
34
+ onDescriptionChange: (value: string) => void;
35
+ onFieldChange: (block: BuilderBlock, path: string, value: unknown) => void;
36
+ onQueueFileEdit: (blockId: string, path: string) => void;
37
+ onAddItem: (block: BuilderBlock, path: string) => void;
38
+ onRemoveItem: (block: BuilderBlock, path: string, index: number) => void;
39
+ onMoveItem: (block: BuilderBlock, path: string, index: number, direction: -1 | 1) => void;
40
+ onCopyMdsvex: () => void;
41
+ onDeselectBlock: () => void;
42
+ } = $props();
43
+
44
+ const inspectorEntries = $derived(Object.entries(inspectorFields));
45
+ </script>
46
+
47
+ <aside
48
+ class="flex h-full min-h-0 w-full flex-col border-l border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900"
49
+ onclick={(event) => {
50
+ if (!(event.target as Element).closest('section')) onDeselectBlock();
51
+ }}
52
+ >
53
+ <div class="min-h-0 flex-1 overflow-y-auto">
54
+ {#if activeBlock && activeDefinition}
55
+ <section class="p-4 dark:border-gray-700">
56
+ <div class="mb-4">
57
+ <p class="text-muted text-[11px] font-semibold tracking-wide uppercase">Brik</p>
58
+ <h3 class="text-heading mt-1 text-sm font-semibold">{activeDefinition.type}</h3>
59
+ <p class="text-muted mt-1 text-xs leading-5">{activeDefinition.description}</p>
60
+ </div>
61
+
62
+ {#if activeDefinition.mode === 'markdown'}
63
+ <label class="block">
64
+ <span class="text-label mb-1 block text-sm font-medium">Contenuto markdown</span>
65
+ <textarea
66
+ value={typeof activeBlock.props.content === 'string' ? activeBlock.props.content : ''}
67
+ class="min-h-48 w-full border border-gray-300 bg-white px-4 py-3 font-mono text-sm text-gray-900 placeholder-gray-400 focus:border-[#FDE047] focus:ring-1 focus:ring-[#FDE047] focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500 dark:focus:border-[#FACC15] dark:focus:ring-[#FACC15]"
68
+ oninput={(event) => onFieldChange(activeBlock, 'content', event.currentTarget.value)}
69
+ placeholder="Scrivi markdown..."
70
+ ></textarea>
71
+ </label>
72
+ {:else if inspectorEntries.length > 0}
73
+ <div class="space-y-4">
74
+ {#each inspectorEntries as [fieldKey, fieldDefinition] (fieldKey)}
75
+ <BuilderFieldEditor
76
+ {fieldKey}
77
+ field={fieldDefinition}
78
+ path={fieldKey}
79
+ value={activeBlock.props[fieldKey]}
80
+ onChange={(path, value) => onFieldChange(activeBlock, path, value)}
81
+ onQueueFileEdit={(path) => onQueueFileEdit(activeBlock.id, path)}
82
+ onAddItem={(path) => onAddItem(activeBlock, path)}
83
+ onRemoveItem={(path, itemIndex) => onRemoveItem(activeBlock, path, itemIndex)}
84
+ onMoveItem={(path, itemIndex, direction) =>
85
+ onMoveItem(activeBlock, path, itemIndex, direction)}
86
+ />
87
+ {/each}
88
+ </div>
89
+ {:else}
90
+ <p class="text-muted text-sm">Nothing to edit here!</p>
91
+ {/if}
92
+
93
+ {#if propsError}
94
+ <p class="text-error mt-3 text-sm">{propsError}</p>
95
+ {/if}
96
+ </section>
97
+ {:else}
98
+ <section class="p-4 dark:border-gray-700">
99
+ <h3 class="text-muted mb-4 text-[11px] font-semibold tracking-wide uppercase">Page</h3>
100
+ <div class="space-y-4">
101
+ <label class="block">
102
+ <span class="text-label mb-1 block text-sm font-medium">Page Title</span>
103
+ <input
104
+ value={title}
105
+ class="block w-full border border-gray-300 bg-white px-4 py-3 text-sm text-gray-900 placeholder-gray-400 focus:border-[#FDE047] focus:ring-1 focus:ring-[#FDE047] focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500 dark:focus:border-[#FACC15] dark:focus:ring-[#FACC15]"
106
+ placeholder="Page Title"
107
+ oninput={(event) => onTitleChange(event.currentTarget.value)}
108
+ />
109
+ </label>
110
+ <label class="block">
111
+ <span class="text-label mb-1 block text-sm font-medium">Description</span>
112
+ <input
113
+ value={description}
114
+ class="block w-full border border-gray-300 bg-white px-4 py-3 text-sm text-gray-900 placeholder-gray-400 focus:border-[#FDE047] focus:ring-1 focus:ring-[#FDE047] focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500 dark:focus:border-[#FACC15] dark:focus:ring-[#FACC15]"
115
+ placeholder="Description for the frontmatter"
116
+ oninput={(event) => onDescriptionChange(event.currentTarget.value)}
117
+ />
118
+ </label>
119
+ </div>
120
+ </section>
121
+ {/if}
122
+ </div>
123
+ </aside>