@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,221 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { Image, Trash2 } from 'lucide-svelte';
4
+
5
+ let {
6
+ element,
7
+ onPick,
8
+ onRemove,
9
+ onBlur
10
+ }: {
11
+ element: HTMLImageElement;
12
+ onPick: () => void;
13
+ onRemove: () => void;
14
+ onBlur: () => void;
15
+ } = $props();
16
+
17
+ let coords = $state({ top: 0, left: 0, width: 0, height: 0 });
18
+ let isElementHovered = $state(false);
19
+ let isToolbarHovered = $state(false);
20
+ let isHovered = $derived(isElementHovered || isToolbarHovered);
21
+
22
+ let currentSrc = $state(element?.src || '');
23
+ let hasImage = $derived(
24
+ currentSrc &&
25
+ !currentSrc.startsWith('data:image/svg+xml') &&
26
+ currentSrc !== 'about:blank' &&
27
+ currentSrc !== ''
28
+ );
29
+
30
+ function updateCoords() {
31
+ const rect = element.getBoundingClientRect();
32
+ coords = {
33
+ top: rect.top,
34
+ left: rect.left,
35
+ width: rect.width,
36
+ height: rect.height
37
+ };
38
+ }
39
+
40
+ function clickListener(node: HTMLButtonElement, action: () => void) {
41
+ const handler = (event: MouseEvent) => {
42
+ event.stopPropagation();
43
+ action();
44
+ };
45
+ node.addEventListener('click', handler);
46
+ return {
47
+ destroy() {
48
+ node.removeEventListener('click', handler);
49
+ }
50
+ };
51
+ }
52
+
53
+ onMount(() => {
54
+ updateCoords();
55
+
56
+ const win = element.ownerDocument.defaultView || window;
57
+ win.addEventListener('scroll', updateCoords, { passive: true });
58
+ win.addEventListener('resize', updateCoords, { passive: true });
59
+
60
+ // Track hover state of the image element
61
+ const handleMouseEnter = () => {
62
+ isElementHovered = true;
63
+ };
64
+ const handleMouseLeave = () => {
65
+ isElementHovered = false;
66
+ };
67
+
68
+ element.addEventListener('mouseenter', handleMouseEnter);
69
+ element.addEventListener('mouseleave', handleMouseLeave);
70
+
71
+ // Initialize hover state in case mouse is already over the image
72
+ isElementHovered = element.matches(':hover');
73
+
74
+ // Click outside to blur/close
75
+ const handleGlobalClick = (event: MouseEvent) => {
76
+ const target = event.target as Element;
77
+ if (!target) return;
78
+
79
+ if (!target.closest('.builder-preview-image-toolbar') && target !== element) {
80
+ onBlur();
81
+ }
82
+ };
83
+
84
+ win.document.addEventListener('click', handleGlobalClick, true);
85
+
86
+ // Observe src attribute mutations to reactively update currentSrc
87
+ const observer = new MutationObserver(() => {
88
+ currentSrc = element.src || '';
89
+ updateCoords();
90
+ });
91
+ observer.observe(element, { attributes: true, attributeFilter: ['src'] });
92
+
93
+ return () => {
94
+ win.removeEventListener('scroll', updateCoords);
95
+ win.removeEventListener('resize', updateCoords);
96
+ element.removeEventListener('mouseenter', handleMouseEnter);
97
+ element.removeEventListener('mouseleave', handleMouseLeave);
98
+ win.document.removeEventListener('click', handleGlobalClick, true);
99
+ observer.disconnect();
100
+ };
101
+ });
102
+ </script>
103
+
104
+ <div
105
+ class="builder-preview-image-toolbar"
106
+ class:visible={isHovered}
107
+ onmouseenter={() => {
108
+ isToolbarHovered = true;
109
+ }}
110
+ onmouseleave={() => {
111
+ isToolbarHovered = false;
112
+ }}
113
+ style="
114
+ position: fixed;
115
+ top: {coords.top + 8}px;
116
+ left: {coords.left + coords.width - 8}px;
117
+ transform: translate(-100%, 0);
118
+ z-index: 99999;
119
+ "
120
+ >
121
+ <button
122
+ type="button"
123
+ use:clickListener={onPick}
124
+ title="Choose image"
125
+ class="toolbar-btn"
126
+ >
127
+ <Image size={14} />
128
+ <span class="toolbar-label">Choose</span>
129
+ </button>
130
+ {#if hasImage}
131
+ <button
132
+ type="button"
133
+ use:clickListener={onRemove}
134
+ title="Remove image"
135
+ class="toolbar-btn toolbar-btn-danger"
136
+ >
137
+ <Trash2 size={14} />
138
+ <span class="toolbar-label">Remove</span>
139
+ </button>
140
+ {/if}
141
+ </div>
142
+
143
+ <style>
144
+ .builder-preview-image-toolbar {
145
+ display: flex;
146
+ align-items: center;
147
+ height: 2rem;
148
+ overflow: hidden;
149
+ border: 1px solid #d1d5db;
150
+ background-color: #fff;
151
+ font-size: 0.75rem;
152
+ line-height: 1rem;
153
+ color: #1e1c18;
154
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
155
+ opacity: 0;
156
+ pointer-events: none;
157
+ transition: opacity 0.15s ease-in-out;
158
+ }
159
+
160
+ .builder-preview-image-toolbar.visible {
161
+ opacity: 1;
162
+ pointer-events: auto;
163
+ }
164
+
165
+ :global(.dark) .builder-preview-image-toolbar {
166
+ border-color: #4b5563;
167
+ background-color: #2d2a25;
168
+ color: #f3f4f6;
169
+ }
170
+
171
+ .toolbar-btn {
172
+ display: flex;
173
+ align-items: center;
174
+ gap: 0.375rem;
175
+ height: 100%;
176
+ border: none;
177
+ border-right: 1px solid #e5e7eb;
178
+ background: transparent;
179
+ padding: 0 0.625rem;
180
+ color: inherit;
181
+ font-size: inherit;
182
+ cursor: pointer;
183
+ transition: background-color 0.15s;
184
+ }
185
+
186
+ .toolbar-btn:last-child {
187
+ border-right: none;
188
+ }
189
+
190
+ .toolbar-btn:hover {
191
+ background-color: #f3f4f6;
192
+ }
193
+
194
+ :global(.dark) .toolbar-btn {
195
+ border-right-color: #444039;
196
+ }
197
+
198
+ :global(.dark) .toolbar-btn:hover {
199
+ background-color: #444039;
200
+ }
201
+
202
+ .toolbar-btn-danger {
203
+ color: #dc2626;
204
+ }
205
+
206
+ .toolbar-btn-danger:hover {
207
+ background-color: #fef2f2;
208
+ }
209
+
210
+ :global(.dark) .toolbar-btn-danger {
211
+ color: #f87171;
212
+ }
213
+
214
+ :global(.dark) .toolbar-btn-danger:hover {
215
+ background-color: rgba(153, 27, 27, 0.35);
216
+ }
217
+
218
+ .toolbar-label {
219
+ font-weight: 500;
220
+ }
221
+ </style>
@@ -0,0 +1,246 @@
1
+ <script lang="ts">
2
+ import { onMount, tick } from 'svelte';
3
+
4
+ let {
5
+ value,
6
+ placeholder = '',
7
+ multiline = false,
8
+ inline = false,
9
+ textStyle,
10
+ autofocus = false,
11
+ initialCaretOffset = null,
12
+ initialClickCoords = null,
13
+ onChange,
14
+ onBlur
15
+ }: {
16
+ value: string;
17
+ placeholder?: string;
18
+ multiline?: boolean;
19
+ inline?: boolean;
20
+ textStyle: string;
21
+ autofocus?: boolean;
22
+ initialCaretOffset?: number | null;
23
+ initialClickCoords?: { left: number; top: number } | null;
24
+ onChange: (value: string) => void;
25
+ onBlur: () => void;
26
+ } = $props();
27
+
28
+ let element = $state<HTMLDivElement | null>(null);
29
+ let draft = $state(value);
30
+
31
+ $effect(() => {
32
+ const nextValue = value;
33
+ if (!element) {
34
+ return;
35
+ }
36
+
37
+ if (element.ownerDocument.activeElement === element) {
38
+ return;
39
+ }
40
+
41
+ draft = nextValue;
42
+ element.textContent = nextValue;
43
+ updateEmptyState();
44
+ });
45
+
46
+ onMount(() => {
47
+ if (!element) {
48
+ return;
49
+ }
50
+
51
+ element.textContent = value;
52
+ draft = value;
53
+ updateEmptyState();
54
+
55
+ if (autofocus) {
56
+ void placeInitialSelection();
57
+ }
58
+ });
59
+
60
+ function readPlainText(node: HTMLElement): string {
61
+ return node.innerText.replace(/\u00a0/g, ' ').replace(/\r\n/g, '\n');
62
+ }
63
+
64
+ function updateEmptyState(): void {
65
+ if (!element) {
66
+ return;
67
+ }
68
+
69
+ element.classList.toggle('is-editor-empty', draft.trim().length === 0);
70
+ }
71
+
72
+ function emitChange(): void {
73
+ if (!element) {
74
+ return;
75
+ }
76
+
77
+ draft = readPlainText(element);
78
+ updateEmptyState();
79
+ onChange(draft);
80
+ }
81
+
82
+ function handlePaste(event: ClipboardEvent): void {
83
+ event.preventDefault();
84
+ const text = event.clipboardData?.getData('text/plain') ?? '';
85
+ insertPlainText(text);
86
+ emitChange();
87
+ }
88
+
89
+ function handleKeydown(event: KeyboardEvent): void {
90
+ if (!multiline && event.key === 'Enter') {
91
+ event.preventDefault();
92
+ }
93
+ }
94
+
95
+ function insertPlainText(text: string): void {
96
+ if (!element) {
97
+ return;
98
+ }
99
+
100
+ const doc = element.ownerDocument;
101
+ const selection = doc.getSelection();
102
+ if (!selection || selection.rangeCount === 0) {
103
+ element.textContent = (element.textContent ?? '') + text;
104
+ return;
105
+ }
106
+
107
+ const range = selection.getRangeAt(0);
108
+ range.deleteContents();
109
+ range.insertNode(doc.createTextNode(text));
110
+ range.collapse(false);
111
+ selection.removeAllRanges();
112
+ selection.addRange(range);
113
+ }
114
+
115
+ async function placeInitialSelection(): Promise<void> {
116
+ await tick();
117
+ if (!element) {
118
+ return;
119
+ }
120
+
121
+ element.focus();
122
+
123
+ const doc = element.ownerDocument;
124
+ const selection = doc.getSelection();
125
+ if (!selection) {
126
+ return;
127
+ }
128
+
129
+ if (initialClickCoords) {
130
+ const range =
131
+ doc.caretRangeFromPoint?.(initialClickCoords.left, initialClickCoords.top) ??
132
+ (() => {
133
+ const pos = doc.caretPositionFromPoint?.(
134
+ initialClickCoords.left,
135
+ initialClickCoords.top
136
+ );
137
+ if (!pos) {
138
+ return null;
139
+ }
140
+
141
+ const nextRange = doc.createRange();
142
+ nextRange.setStart(pos.offsetNode, pos.offset);
143
+ nextRange.collapse(true);
144
+ return nextRange;
145
+ })();
146
+
147
+ if (range && element.contains(range.startContainer)) {
148
+ selection.removeAllRanges();
149
+ selection.addRange(range);
150
+ return;
151
+ }
152
+ }
153
+
154
+ if (initialCaretOffset != null) {
155
+ setCaretAtOffset(element, initialCaretOffset, selection);
156
+ }
157
+ }
158
+
159
+ function setCaretAtOffset(
160
+ root: HTMLElement,
161
+ offset: number,
162
+ selection: Selection
163
+ ): void {
164
+ const doc = root.ownerDocument;
165
+ const walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT);
166
+ let remaining = offset;
167
+
168
+ while (walker.nextNode()) {
169
+ const textNode = walker.currentNode as Text;
170
+ if (remaining <= textNode.length) {
171
+ const range = doc.createRange();
172
+ range.setStart(textNode, remaining);
173
+ range.collapse(true);
174
+ selection.removeAllRanges();
175
+ selection.addRange(range);
176
+ return;
177
+ }
178
+
179
+ remaining -= textNode.length;
180
+ }
181
+
182
+ const range = doc.createRange();
183
+ range.selectNodeContents(root);
184
+ range.collapse(false);
185
+ selection.removeAllRanges();
186
+ selection.addRange(range);
187
+ }
188
+ </script>
189
+
190
+ <div
191
+ bind:this={element}
192
+ class="builder-preview-text-editor"
193
+ class:is-editor-empty={!draft.trim()}
194
+ class:builder-preview-text-editor--inline={inline}
195
+ class:builder-preview-text-editor--multiline={multiline}
196
+ contenteditable="plaintext-only"
197
+ role="textbox"
198
+ aria-multiline={multiline}
199
+ data-placeholder={placeholder}
200
+ style={textStyle}
201
+ oninput={emitChange}
202
+ onpaste={handlePaste}
203
+ onkeydown={handleKeydown}
204
+ onblur={onBlur}
205
+ ></div>
206
+
207
+ <style>
208
+ .builder-preview-text-editor {
209
+ width: 100%;
210
+ min-height: inherit;
211
+ margin: 0;
212
+ border: 0;
213
+ background: transparent;
214
+ padding: 0;
215
+ outline: none;
216
+ box-shadow: none;
217
+ font: inherit;
218
+ line-height: inherit;
219
+ letter-spacing: inherit;
220
+ text-transform: inherit;
221
+ text-align: inherit;
222
+ white-space: inherit;
223
+ word-wrap: inherit;
224
+ overflow-wrap: inherit;
225
+ cursor: text;
226
+ }
227
+
228
+ .builder-preview-text-editor--inline {
229
+ display: inline;
230
+ width: auto;
231
+ min-width: 1ch;
232
+ vertical-align: baseline;
233
+ }
234
+
235
+ .builder-preview-text-editor--multiline {
236
+ display: block;
237
+ white-space: pre-wrap;
238
+ }
239
+
240
+ .builder-preview-text-editor.is-editor-empty::before {
241
+ content: attr(data-placeholder);
242
+ color: #9ca3af;
243
+ opacity: 0.6;
244
+ pointer-events: none;
245
+ }
246
+ </style>
@@ -0,0 +1,234 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { Editor } from '@tiptap/core';
4
+ import StarterKit from '@tiptap/starter-kit';
5
+ import type { BuilderRichTextValue } from '../core.js';
6
+
7
+ let {
8
+ value,
9
+ mode,
10
+ placeholder = '',
11
+ chrome = 'panel',
12
+ plainTextOnly = false,
13
+ hostInline = false,
14
+ autofocus = false,
15
+ initialCaretOffset = null,
16
+ initialClickCoords = null,
17
+ editorStyle = '',
18
+ onChange,
19
+ onBlur = () => {}
20
+ }: {
21
+ value: BuilderRichTextValue;
22
+ mode: BuilderRichTextValue['mode'];
23
+ placeholder?: string;
24
+ chrome?: 'panel' | 'inline';
25
+ plainTextOnly?: boolean;
26
+ hostInline?: boolean;
27
+ autofocus?: boolean;
28
+ initialCaretOffset?: number | null;
29
+ initialClickCoords?: { left: number; top: number } | null;
30
+ editorStyle?: string;
31
+ onChange: (nextValue: BuilderRichTextValue) => void;
32
+ onBlur?: () => void;
33
+ } = $props();
34
+
35
+ let element = $state<HTMLDivElement | null>(null);
36
+ let editor = $state<Editor | null>(null);
37
+ let lastSyncedHtml = $state('');
38
+
39
+ onMount(() => {
40
+ const initialHtml = getEditorContent(value.html, mode);
41
+ lastSyncedHtml = value.html;
42
+
43
+ editor = new Editor({
44
+ element: element ?? undefined,
45
+ autofocus: false,
46
+ extensions: [
47
+ StarterKit.configure({
48
+ heading: mode === 'inline' || plainTextOnly ? false : undefined,
49
+ bulletList: mode === 'inline' || plainTextOnly ? false : undefined,
50
+ orderedList: mode === 'inline' || plainTextOnly ? false : undefined,
51
+ blockquote: mode === 'inline' || plainTextOnly ? false : undefined,
52
+ codeBlock: false,
53
+ horizontalRule: mode === 'inline' || plainTextOnly ? false : undefined,
54
+ bold: plainTextOnly ? false : undefined,
55
+ italic: plainTextOnly ? false : undefined,
56
+ strike: plainTextOnly ? false : undefined,
57
+ code: plainTextOnly ? false : undefined
58
+ })
59
+ ],
60
+ content: initialHtml,
61
+ editorProps: {
62
+ attributes: {
63
+ class:
64
+ chrome === 'inline'
65
+ ? `builder-richtext-inline-editor${hostInline ? ' builder-richtext-inline-editor--host-inline' : ''}`
66
+ : mode === 'inline'
67
+ ? 'builder-richtext-panel-editor min-h-11 border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none focus:border-[#FDE047] focus:ring-1 focus:ring-[#FDE047] dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:focus:border-[#FACC15] dark:focus:ring-[#FACC15]'
68
+ : 'builder-richtext-panel-editor min-h-32 border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none focus:border-[#FDE047] focus:ring-1 focus:ring-[#FDE047] dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:focus:border-[#FACC15] dark:focus:ring-[#FACC15]',
69
+ style: editorStyle,
70
+ 'data-placeholder': placeholder
71
+ },
72
+ handleKeyDown: (_view, event) => {
73
+ if (plainTextOnly && event.key === 'Enter') {
74
+ event.preventDefault();
75
+ return true;
76
+ }
77
+
78
+ return false;
79
+ }
80
+ },
81
+ onBlur: () => {
82
+ onBlur();
83
+ },
84
+ onUpdate: ({ editor: activeEditor }) => {
85
+ const html = normalizeOutput(mode, activeEditor.getHTML());
86
+ lastSyncedHtml = html;
87
+ onChange({
88
+ kind: 'richtext',
89
+ mode,
90
+ html,
91
+ json: activeEditor.getJSON() as Record<string, unknown>
92
+ });
93
+ activeEditor.view.dom.classList.toggle('is-editor-empty', activeEditor.isEmpty);
94
+ },
95
+ onCreate: ({ editor: activeEditor }) => {
96
+ activeEditor.view.dom.classList.toggle('is-editor-empty', activeEditor.isEmpty);
97
+ if (autofocus) {
98
+ placeInitialSelection(activeEditor);
99
+ }
100
+ }
101
+ });
102
+
103
+ return () => {
104
+ editor?.destroy();
105
+ editor = null;
106
+ };
107
+ });
108
+
109
+ $effect(() => {
110
+ const nextHtml = value.html;
111
+ if (editor && !editor.isFocused && nextHtml !== lastSyncedHtml) {
112
+ lastSyncedHtml = nextHtml;
113
+ editor.commands.setContent(getEditorContent(nextHtml, mode), { emitUpdate: false });
114
+ editor.view.dom.classList.toggle('is-editor-empty', editor.isEmpty);
115
+ }
116
+ });
117
+
118
+ function placeInitialSelection(activeEditor: Editor): void {
119
+ if (initialClickCoords) {
120
+ const pos = activeEditor.view.posAtCoords(initialClickCoords)?.pos;
121
+ if (pos != null) {
122
+ activeEditor.chain().focus().setTextSelection(pos).run();
123
+ return;
124
+ }
125
+ }
126
+
127
+ if (initialCaretOffset != null && initialCaretOffset > 0) {
128
+ const docSize = activeEditor.state.doc.content.size;
129
+ const pos = Math.min(initialCaretOffset + 1, Math.max(1, docSize - 1));
130
+ activeEditor.chain().focus().setTextSelection(pos).run();
131
+ return;
132
+ }
133
+
134
+ if (hostInline && activeEditor.isEmpty) {
135
+ activeEditor.chain().focus().setTextSelection(1).run();
136
+ return;
137
+ }
138
+
139
+ activeEditor.commands.focus();
140
+ }
141
+
142
+ function getEditorContent(html: string, currentMode: BuilderRichTextValue['mode']): string {
143
+ if (!html.trim()) {
144
+ return '<p></p>';
145
+ }
146
+
147
+ if (currentMode === 'inline' && !html.trim().startsWith('<p')) {
148
+ return `<p>${html}</p>`;
149
+ }
150
+
151
+ return html;
152
+ }
153
+
154
+ function normalizeOutput(htmlMode: BuilderRichTextValue['mode'], html: string): string {
155
+ const trimmed = html.trim();
156
+ if (trimmed === '<p></p>') {
157
+ return '';
158
+ }
159
+
160
+ if (htmlMode !== 'inline') {
161
+ return trimmed;
162
+ }
163
+
164
+ const paragraphMatch = trimmed.match(/^<p>([\s\S]*)<\/p>$/);
165
+ return paragraphMatch ? paragraphMatch[1] : trimmed;
166
+ }
167
+ </script>
168
+
169
+ <div bind:this={element} class={chrome === 'inline' ? 'builder-richtext-mount' : undefined}></div>
170
+
171
+ <style>
172
+ :global(.builder-richtext-panel-editor) {
173
+ width: 100%;
174
+ }
175
+
176
+ :global(.builder-richtext-inline-editor) {
177
+ width: 100%;
178
+ min-height: inherit;
179
+ border: 0;
180
+ background: transparent;
181
+ padding: 0;
182
+ margin: 0;
183
+ outline: none;
184
+ box-shadow: none;
185
+ color: inherit;
186
+ font: inherit;
187
+ line-height: inherit;
188
+ letter-spacing: inherit;
189
+ text-transform: inherit;
190
+ text-align: inherit;
191
+ white-space: inherit;
192
+ word-wrap: inherit;
193
+ overflow-wrap: inherit;
194
+ cursor: text;
195
+ }
196
+
197
+ :global(.builder-richtext-mount) {
198
+ display: contents;
199
+ }
200
+
201
+ :global(.builder-richtext-inline-editor > *:first-child) {
202
+ margin-top: 0;
203
+ }
204
+
205
+ :global(.builder-richtext-inline-editor > *:last-child) {
206
+ margin-bottom: 0;
207
+ }
208
+
209
+ :global(.builder-richtext-inline-editor p) {
210
+ margin: 0;
211
+ padding: 0;
212
+ line-height: inherit;
213
+ }
214
+
215
+ :global(.builder-richtext-inline-editor--host-inline) {
216
+ display: block;
217
+ width: 100%;
218
+ min-height: 1lh;
219
+ vertical-align: baseline;
220
+ position: relative;
221
+ white-space: nowrap;
222
+ }
223
+
224
+ :global(.builder-richtext-inline-editor--host-inline p) {
225
+ display: block;
226
+ margin: 0;
227
+ padding: 0;
228
+ min-height: inherit;
229
+ }
230
+
231
+ :global(.builder-richtext-inline-editor--host-inline .ProseMirror-trailingBreak) {
232
+ display: none;
233
+ }
234
+ </style>