@mk-kit/ui 0.34.0

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 +115 -0
  3. package/block-editor/README.md +254 -0
  4. package/fesm2022/mk-kit-ui-block-editor.mjs +2158 -0
  5. package/fesm2022/mk-kit-ui-block-editor.mjs.map +1 -0
  6. package/fesm2022/mk-kit-ui-button.mjs +81 -0
  7. package/fesm2022/mk-kit-ui-button.mjs.map +1 -0
  8. package/fesm2022/mk-kit-ui-checkbox.mjs +136 -0
  9. package/fesm2022/mk-kit-ui-checkbox.mjs.map +1 -0
  10. package/fesm2022/mk-kit-ui-chip.mjs +122 -0
  11. package/fesm2022/mk-kit-ui-chip.mjs.map +1 -0
  12. package/fesm2022/mk-kit-ui-context-menu.mjs +144 -0
  13. package/fesm2022/mk-kit-ui-context-menu.mjs.map +1 -0
  14. package/fesm2022/mk-kit-ui-core.mjs +1576 -0
  15. package/fesm2022/mk-kit-ui-core.mjs.map +1 -0
  16. package/fesm2022/mk-kit-ui-data.mjs +6055 -0
  17. package/fesm2022/mk-kit-ui-data.mjs.map +1 -0
  18. package/fesm2022/mk-kit-ui-datetime.mjs +3409 -0
  19. package/fesm2022/mk-kit-ui-datetime.mjs.map +1 -0
  20. package/fesm2022/mk-kit-ui-directives.mjs +1779 -0
  21. package/fesm2022/mk-kit-ui-directives.mjs.map +1 -0
  22. package/fesm2022/mk-kit-ui-dnd.mjs +1073 -0
  23. package/fesm2022/mk-kit-ui-dnd.mjs.map +1 -0
  24. package/fesm2022/mk-kit-ui-feedback.mjs +2426 -0
  25. package/fesm2022/mk-kit-ui-feedback.mjs.map +1 -0
  26. package/fesm2022/mk-kit-ui-forms.mjs +9208 -0
  27. package/fesm2022/mk-kit-ui-forms.mjs.map +1 -0
  28. package/fesm2022/mk-kit-ui-icon.mjs +470 -0
  29. package/fesm2022/mk-kit-ui-icon.mjs.map +1 -0
  30. package/fesm2022/mk-kit-ui-media.mjs +896 -0
  31. package/fesm2022/mk-kit-ui-media.mjs.map +1 -0
  32. package/fesm2022/mk-kit-ui-navigation.mjs +2542 -0
  33. package/fesm2022/mk-kit-ui-navigation.mjs.map +1 -0
  34. package/fesm2022/mk-kit-ui-rich-text.mjs +565 -0
  35. package/fesm2022/mk-kit-ui-rich-text.mjs.map +1 -0
  36. package/fesm2022/mk-kit-ui-table.mjs +1378 -0
  37. package/fesm2022/mk-kit-ui-table.mjs.map +1 -0
  38. package/fesm2022/mk-kit-ui.mjs +32 -0
  39. package/fesm2022/mk-kit-ui.mjs.map +1 -0
  40. package/package.json +130 -0
  41. package/schematics/collection.json +10 -0
  42. package/schematics/ng-add/index.js +113 -0
  43. package/schematics/ng-add/schema.json +22 -0
  44. package/schematics/package.json +3 -0
  45. package/styles/mk-kit.css +750 -0
  46. package/types/mk-kit-ui-block-editor.d.ts +292 -0
  47. package/types/mk-kit-ui-button.d.ts +40 -0
  48. package/types/mk-kit-ui-checkbox.d.ts +62 -0
  49. package/types/mk-kit-ui-chip.d.ts +59 -0
  50. package/types/mk-kit-ui-context-menu.d.ts +57 -0
  51. package/types/mk-kit-ui-core.d.ts +1105 -0
  52. package/types/mk-kit-ui-data.d.ts +2580 -0
  53. package/types/mk-kit-ui-datetime.d.ts +1171 -0
  54. package/types/mk-kit-ui-directives.d.ts +807 -0
  55. package/types/mk-kit-ui-dnd.d.ts +423 -0
  56. package/types/mk-kit-ui-feedback.d.ts +1270 -0
  57. package/types/mk-kit-ui-forms.d.ts +3586 -0
  58. package/types/mk-kit-ui-icon.d.ts +108 -0
  59. package/types/mk-kit-ui-media.d.ts +549 -0
  60. package/types/mk-kit-ui-navigation.d.ts +1169 -0
  61. package/types/mk-kit-ui-rich-text.d.ts +187 -0
  62. package/types/mk-kit-ui-table.d.ts +739 -0
  63. package/types/mk-kit-ui.d.ts +17 -0
@@ -0,0 +1,2158 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, signal, computed, Injectable, input, booleanAttribute, output, ChangeDetectionStrategy, Component, viewChild, DOCUMENT, ElementRef, DestroyRef, model, forwardRef } from '@angular/core';
3
+ import { NG_VALUE_ACCESSOR } from '@angular/forms';
4
+ import * as i1 from '@mk-kit/ui/core';
5
+ import { MK_I18N, mkUniqueId } from '@mk-kit/ui/core';
6
+ import { MkRichTextEngine, sanitizeInlineHtml } from '@mk-kit/ui/rich-text';
7
+ export { sanitizeInlineHtml } from '@mk-kit/ui/rich-text';
8
+ import { DomSanitizer } from '@angular/platform-browser';
9
+
10
+ /**
11
+ * Block document model for the mk-kit block editor.
12
+ *
13
+ * The model is deliberately small and 100% JSON-serialisable so a document can
14
+ * be persisted to a database, sent over the wire, and re-hydrated without any
15
+ * class instances or functions. All editor state lives in these plain objects.
16
+ */
17
+ /** Current document schema version. */
18
+ const MK_BLOCK_DOCUMENT_VERSION = 1;
19
+ /** An empty, valid document. */
20
+ function mkEmptyDocument() {
21
+ return { version: MK_BLOCK_DOCUMENT_VERSION, blocks: [] };
22
+ }
23
+ let blockCounter = 0;
24
+ /**
25
+ * Generates a reasonably unique, JSON-safe block id. Combines a monotonic
26
+ * counter with a short random suffix so ids are unique even across documents
27
+ * merged at runtime.
28
+ */
29
+ function mkBlockId(prefix = 'blk') {
30
+ const rand = Math.random().toString(36).slice(2, 8);
31
+ return `${prefix}-${++blockCounter}-${rand}`;
32
+ }
33
+ /** Deep-clones a block (and its subtree), assigning fresh ids. */
34
+ function mkCloneBlock(block) {
35
+ return {
36
+ id: mkBlockId(block.type),
37
+ type: block.type,
38
+ data: structuredCloneSafe(block.data),
39
+ children: block.children?.map(mkCloneBlock),
40
+ };
41
+ }
42
+ /** Structured clone with a JSON fallback for older runtimes. */
43
+ function structuredCloneSafe(value) {
44
+ try {
45
+ return structuredClone(value);
46
+ }
47
+ catch {
48
+ return JSON.parse(JSON.stringify(value ?? null));
49
+ }
50
+ }
51
+
52
+ /** Convenience factory: builds a `create()`-ready block with fresh id. */
53
+ function block$1(type, data = {}, children) {
54
+ return { id: mkBlockId(type), type, data, ...(children ? { children } : {}) };
55
+ }
56
+ /**
57
+ * The default palette of built-in block definitions. Grouped for the inserter.
58
+ * Consumers can spread this and append custom definitions, or replace it.
59
+ *
60
+ * The English `label`/`group`/`description` text here is only a base: the
61
+ * editor resolves the built-in types' display text from the `blockEditor`
62
+ * i18n strings (see `provideMkI18n`) before merging custom definitions.
63
+ */
64
+ const MK_DEFAULT_BLOCKS = [
65
+ {
66
+ type: 'paragraph',
67
+ label: 'Paragraph',
68
+ icon: '¶',
69
+ group: 'Text',
70
+ description: 'Rich text with inline formatting.',
71
+ keywords: ['text', 'body', 'p'],
72
+ transforms: ['heading', 'quote', 'code'],
73
+ create: () => block$1('paragraph', { html: '' }),
74
+ },
75
+ {
76
+ type: 'heading',
77
+ label: 'Heading',
78
+ icon: 'H',
79
+ group: 'Text',
80
+ description: 'A section title (H1–H4).',
81
+ keywords: ['title', 'h1', 'h2', 'h3'],
82
+ transforms: ['paragraph'],
83
+ create: () => block$1('heading', { html: '', level: 2 }),
84
+ },
85
+ {
86
+ type: 'list',
87
+ label: 'List',
88
+ icon: '•',
89
+ group: 'Text',
90
+ description: 'Bulleted or numbered list.',
91
+ keywords: ['ul', 'ol', 'bullet', 'numbered'],
92
+ create: () => block$1('list', { ordered: false, items: [''] }),
93
+ },
94
+ {
95
+ type: 'quote',
96
+ label: 'Quote',
97
+ icon: '“',
98
+ group: 'Text',
99
+ description: 'A blockquote with optional citation.',
100
+ keywords: ['blockquote', 'citation'],
101
+ transforms: ['paragraph'],
102
+ create: () => block$1('quote', { html: '', citation: '' }),
103
+ },
104
+ {
105
+ type: 'code',
106
+ label: 'Code',
107
+ icon: '</>',
108
+ group: 'Text',
109
+ description: 'Preformatted, monospace code.',
110
+ keywords: ['pre', 'snippet', 'monospace'],
111
+ create: () => block$1('code', { code: '', language: '' }),
112
+ },
113
+ {
114
+ type: 'image',
115
+ label: 'Image',
116
+ icon: '🖼',
117
+ group: 'Media',
118
+ description: 'Upload or link an image.',
119
+ keywords: ['photo', 'picture', 'img', 'upload'],
120
+ create: () => block$1('image', { src: '', alt: '', caption: '', align: 'center', width: 100 }),
121
+ },
122
+ {
123
+ type: 'embed',
124
+ label: 'Embed',
125
+ icon: '▶',
126
+ group: 'Media',
127
+ description: 'YouTube, Vimeo or any URL.',
128
+ keywords: ['youtube', 'vimeo', 'video', 'iframe', 'url'],
129
+ create: () => block$1('embed', { url: '' }),
130
+ },
131
+ {
132
+ type: 'button',
133
+ label: 'Button',
134
+ icon: '⬚',
135
+ group: 'Media',
136
+ description: 'A call-to-action link button.',
137
+ keywords: ['cta', 'link', 'action'],
138
+ create: () => block$1('button', { label: 'Click me', href: '', tone: 'primary', variant: 'solid', align: 'left' }),
139
+ },
140
+ {
141
+ type: 'divider',
142
+ label: 'Divider',
143
+ icon: '—',
144
+ group: 'Layout',
145
+ description: 'A horizontal separator.',
146
+ keywords: ['hr', 'separator', 'rule'],
147
+ create: () => block$1('divider', {}),
148
+ },
149
+ {
150
+ type: 'columns',
151
+ label: 'Columns',
152
+ icon: '▥',
153
+ group: 'Layout',
154
+ description: 'A responsive multi-column layout.',
155
+ keywords: ['grid', 'layout', 'row', 'column'],
156
+ allowedInColumns: false,
157
+ create: () => block$1('columns', { count: 2, ratio: '50-50', gap: 4, align: 'stretch', justify: 'start' }, [block$1('column', {}, []), block$1('column', {}, [])]),
158
+ },
159
+ ];
160
+ /**
161
+ * Multi-provider injection token for adding block definitions app-wide. Each
162
+ * provided array is merged over {@link MK_DEFAULT_BLOCKS} (by `type`). The
163
+ * editor's `blocks` input takes final precedence.
164
+ *
165
+ * ```ts
166
+ * providers: [
167
+ * { provide: MK_BLOCK_DEFINITIONS, multi: true, useValue: [myCalloutBlock] },
168
+ * ]
169
+ * ```
170
+ */
171
+ const MK_BLOCK_DEFINITIONS = new InjectionToken('MK_BLOCK_DEFINITIONS');
172
+ /** App-wide upload handler token (overridable by the editor's input). */
173
+ const MK_BLOCK_UPLOAD_HANDLER = new InjectionToken('MK_BLOCK_UPLOAD_HANDLER');
174
+ /** Default embed providers: YouTube + Vimeo. */
175
+ const MK_DEFAULT_EMBED_PROVIDERS = [
176
+ {
177
+ name: 'YouTube',
178
+ test: /(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{11})/i,
179
+ aspectRatio: 16 / 9,
180
+ toEmbedUrl(url) {
181
+ const m = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{11})/i);
182
+ return m ? `https://www.youtube-nocookie.com/embed/${m[1]}` : null;
183
+ },
184
+ },
185
+ {
186
+ name: 'Vimeo',
187
+ test: /vimeo\.com\/(?:video\/)?(\d+)/i,
188
+ aspectRatio: 16 / 9,
189
+ toEmbedUrl(url) {
190
+ const m = url.match(/vimeo\.com\/(?:video\/)?(\d+)/i);
191
+ return m ? `https://player.vimeo.com/video/${m[1]}` : null;
192
+ },
193
+ },
194
+ ];
195
+ /** App-wide embed providers token (merged with the editor's input). */
196
+ const MK_BLOCK_EMBED_PROVIDERS = new InjectionToken('MK_BLOCK_EMBED_PROVIDERS');
197
+ /**
198
+ * Merges default definitions with app-token definitions and per-editor input,
199
+ * de-duplicating by `type` (later sources win).
200
+ */
201
+ function mkMergeBlockDefinitions(defaults, ...sources) {
202
+ const byType = new Map();
203
+ for (const def of defaults)
204
+ byType.set(def.type, def);
205
+ for (const source of sources) {
206
+ if (!source)
207
+ continue;
208
+ for (const def of source)
209
+ byType.set(def.type, def);
210
+ }
211
+ return [...byType.values()];
212
+ }
213
+
214
+ /**
215
+ * Shared, editor-scoped context threaded to every nested `mk-block-list` and
216
+ * block-editor component. Provided by {@link MkBlockEditor}, so deeply nested
217
+ * blocks (e.g. inside columns) read config and coordinate focus without every
218
+ * level re-declaring inputs.
219
+ *
220
+ * The config fields are plain `Signal` references that {@link MkBlockEditor}
221
+ * connects to its input-derived signals in its constructor — i.e. BEFORE any
222
+ * child component can inject this context — so consumers always track the live
223
+ * source with no mirroring effects. The initial values here only serve
224
+ * contexts created without an editor (e.g. component tests).
225
+ */
226
+ class MkBlockEditorContext {
227
+ announcer;
228
+ i18n = inject(MK_I18N);
229
+ /** Active block palette (defaults merged with app/editor definitions). */
230
+ definitions = signal([], /* @ts-ignore */
231
+ ...(ngDevMode ? [{ debugName: "definitions" }] : /* istanbul ignore next */ []));
232
+ /** Read-only mode: content visible, editing chrome hidden. */
233
+ readonly = signal(false, /* @ts-ignore */
234
+ ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
235
+ /** Disabled mode (form-level). */
236
+ disabled = signal(false, /* @ts-ignore */
237
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
238
+ /** Placeholder for empty text blocks. */
239
+ placeholder = signal(this.i18n.blockEditor.emptyBlockPlaceholder, /* @ts-ignore */
240
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
241
+ /** Effective upload handler (input beats token beats data-URL fallback). */
242
+ uploadHandler = signal(null, /* @ts-ignore */
243
+ ...(ngDevMode ? [{ debugName: "uploadHandler" }] : /* istanbul ignore next */ []));
244
+ /** Effective embed providers (defaults + token + input). */
245
+ embedProviders = signal([], /* @ts-ignore */
246
+ ...(ngDevMode ? [{ debugName: "embedProviders" }] : /* istanbul ignore next */ []));
247
+ /** Root editor host, used to locate editables for focus coordination. */
248
+ hostRef = null;
249
+ /**
250
+ * `type → definition` lookup, rebuilt only when the registry array changes.
251
+ * `definitionFor` is called several times per block per change-detection
252
+ * pass, so an `array.find` scan here was O(blocks × definitions) per CD.
253
+ */
254
+ definitionMap = computed(() => {
255
+ const map = new Map();
256
+ for (const def of this.definitions())
257
+ map.set(def.type, def);
258
+ return map;
259
+ }, /* @ts-ignore */
260
+ ...(ngDevMode ? [{ debugName: "definitionMap" }] : /* istanbul ignore next */ []));
261
+ constructor(announcer) {
262
+ this.announcer = announcer;
263
+ }
264
+ /** Returns the definition for a block type, if registered. */
265
+ definitionFor(type) {
266
+ return this.definitionMap().get(type);
267
+ }
268
+ /** Human label for a block type (falls back to the raw type). */
269
+ labelFor(type) {
270
+ return this.definitionFor(type)?.label ?? type;
271
+ }
272
+ /** Announce a status change to assistive tech. */
273
+ announce(message) {
274
+ this.announcer.announce(message, 'polite');
275
+ }
276
+ /**
277
+ * Move focus to a block's primary editable after the next render. Queries the
278
+ * live DOM by `data-block-id` so it works regardless of nesting depth.
279
+ */
280
+ focusBlock(id, position = 'end') {
281
+ const root = this.hostRef?.nativeElement;
282
+ if (!root || typeof window === 'undefined')
283
+ return;
284
+ setTimeout(() => {
285
+ const wrapper = root.querySelector(`[data-block-id="${cssEscape(id)}"]`);
286
+ const editable = wrapper?.querySelector('[data-mk-editable]');
287
+ const target = editable ?? wrapper?.querySelector('[data-mk-focus]');
288
+ if (!target)
289
+ return;
290
+ target.focus();
291
+ if (editable)
292
+ placeCaret(editable, position);
293
+ });
294
+ }
295
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockEditorContext, deps: [{ token: i1.MkLiveAnnouncer }], target: i0.ɵɵFactoryTarget.Injectable });
296
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockEditorContext });
297
+ }
298
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockEditorContext, decorators: [{
299
+ type: Injectable
300
+ }], ctorParameters: () => [{ type: i1.MkLiveAnnouncer }] });
301
+ /** Escapes an id for use in a CSS attribute selector. */
302
+ function cssEscape(value) {
303
+ return value.replace(/["\\]/g, '\\$&');
304
+ }
305
+ /** Places the caret at the start or end of a contenteditable element. */
306
+ function placeCaret(el, position) {
307
+ const win = el.ownerDocument.defaultView;
308
+ const selection = win?.getSelection();
309
+ if (!selection)
310
+ return;
311
+ const range = el.ownerDocument.createRange();
312
+ range.selectNodeContents(el);
313
+ range.collapse(position === 'start');
314
+ selection.removeAllRanges();
315
+ selection.addRange(range);
316
+ }
317
+
318
+ /**
319
+ * Paragraph block — rich text stored as sanitised HTML in `block.data.html`,
320
+ * with the floating inline toolbar provided by {@link MkRichTextEngine}.
321
+ */
322
+ class MkParagraphBlock {
323
+ i18n = inject(MK_I18N);
324
+ block = input.required(/* @ts-ignore */
325
+ ...(ngDevMode ? [{ debugName: "block" }] : /* istanbul ignore next */ []));
326
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
327
+ placeholder = input(this.i18n.blockEditor.emptyBlockPlaceholder, /* @ts-ignore */
328
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
329
+ blockChange = output();
330
+ splitAt = output();
331
+ removeEmpty = output();
332
+ arrowOut = output();
333
+ onContent(html) {
334
+ const b = this.block();
335
+ this.blockChange.emit({ ...b, data: { ...b.data, html } });
336
+ }
337
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkParagraphBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
338
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.7", type: MkParagraphBlock, isStandalone: true, selector: "mk-paragraph-block", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blockChange: "blockChange", splitAt: "splitAt", removeEmpty: "removeEmpty", arrowOut: "arrowOut" }, ngImport: i0, template: `
339
+ <mk-rich-text-engine
340
+ [html]="block().data['html'] ?? ''"
341
+ [placeholder]="placeholder()"
342
+ [disabled]="readonly()"
343
+ [ariaLabel]="i18n.blockEditor.blockParagraph"
344
+ (contentChange)="onContent($event)"
345
+ (splitAt)="splitAt.emit($event)"
346
+ (removeEmpty)="removeEmpty.emit()"
347
+ (arrowOut)="arrowOut.emit($event)"
348
+ />
349
+ `, isInline: true, dependencies: [{ kind: "component", type: MkRichTextEngine, selector: "mk-rich-text-engine", inputs: ["html", "placeholder", "disabled", "multiline", "splitOnEnter", "minHeight", "ariaLabel", "labelledBy", "describedBy", "invalid"], outputs: ["contentChange", "splitAt", "removeEmpty", "arrowOut", "focusChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
350
+ }
351
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkParagraphBlock, decorators: [{
352
+ type: Component,
353
+ args: [{
354
+ selector: 'mk-paragraph-block',
355
+ imports: [MkRichTextEngine],
356
+ changeDetection: ChangeDetectionStrategy.OnPush,
357
+ template: `
358
+ <mk-rich-text-engine
359
+ [html]="block().data['html'] ?? ''"
360
+ [placeholder]="placeholder()"
361
+ [disabled]="readonly()"
362
+ [ariaLabel]="i18n.blockEditor.blockParagraph"
363
+ (contentChange)="onContent($event)"
364
+ (splitAt)="splitAt.emit($event)"
365
+ (removeEmpty)="removeEmpty.emit()"
366
+ (arrowOut)="arrowOut.emit($event)"
367
+ />
368
+ `,
369
+ }]
370
+ }], propDecorators: { block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], blockChange: [{ type: i0.Output, args: ["blockChange"] }], splitAt: [{ type: i0.Output, args: ["splitAt"] }], removeEmpty: [{ type: i0.Output, args: ["removeEmpty"] }], arrowOut: [{ type: i0.Output, args: ["arrowOut"] }] } });
371
+
372
+ const LEVELS = [1, 2, 3, 4];
373
+ /**
374
+ * Heading block — the same rich text as a paragraph but rendered at a
375
+ * configurable level (H1–H4), chosen from an inline settings row.
376
+ */
377
+ class MkHeadingBlock {
378
+ i18n = inject(MK_I18N);
379
+ block = input.required(/* @ts-ignore */
380
+ ...(ngDevMode ? [{ debugName: "block" }] : /* istanbul ignore next */ []));
381
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
382
+ blockChange = output();
383
+ splitAt = output();
384
+ removeEmpty = output();
385
+ arrowOut = output();
386
+ levels = LEVELS;
387
+ level = computed(() => {
388
+ const l = this.block().data['level'];
389
+ return LEVELS.includes(l) ? l : 2;
390
+ }, /* @ts-ignore */
391
+ ...(ngDevMode ? [{ debugName: "level" }] : /* istanbul ignore next */ []));
392
+ setLevel(level) {
393
+ const b = this.block();
394
+ this.blockChange.emit({ ...b, data: { ...b.data, level } });
395
+ }
396
+ onContent(html) {
397
+ const b = this.block();
398
+ this.blockChange.emit({ ...b, data: { ...b.data, html } });
399
+ }
400
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHeadingBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
401
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkHeadingBlock, isStandalone: true, selector: "mk-heading-block", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blockChange: "blockChange", splitAt: "splitAt", removeEmpty: "removeEmpty", arrowOut: "arrowOut" }, ngImport: i0, template: `
402
+ @if (!readonly()) {
403
+ <div class="mk-heading-block__levels" role="group" [attr.aria-label]="i18n.blockEditor.headingLevelGroup">
404
+ @for (lvl of levels; track lvl) {
405
+ <button
406
+ type="button"
407
+ class="mk-heading-block__level"
408
+ [class.mk-heading-block__level--active]="level() === lvl"
409
+ [attr.aria-pressed]="level() === lvl"
410
+ (click)="setLevel(lvl)"
411
+ >
412
+ H{{ lvl }}
413
+ </button>
414
+ }
415
+ </div>
416
+ }
417
+ <mk-rich-text-engine
418
+ class="mk-heading-block__text"
419
+ [class]="'mk-heading-block__text--h' + level()"
420
+ [html]="block().data['html'] ?? ''"
421
+ [placeholder]="i18n.blockEditor.headingPlaceholder(level())"
422
+ [disabled]="readonly()"
423
+ [multiline]="false"
424
+ [ariaLabel]="i18n.blockEditor.headingLevel(level())"
425
+ (contentChange)="onContent($event)"
426
+ (splitAt)="splitAt.emit($event)"
427
+ (removeEmpty)="removeEmpty.emit()"
428
+ (arrowOut)="arrowOut.emit($event)"
429
+ />
430
+ `, isInline: true, styles: [":host{display:block}.mk-heading-block__levels{display:inline-flex;gap:2px;margin-bottom:var(--mk-space-1);padding:2px;border-radius:var(--mk-radius-sm);background:var(--mk-surface-2)}.mk-heading-block__level{min-width:30px;height:24px;border:none;border-radius:var(--mk-radius-xs);background:transparent;color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-semibold);cursor:pointer}.mk-heading-block__level:hover{background:var(--mk-hover-overlay)}.mk-heading-block__level--active{background:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-heading-block__level:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-heading-block__text--h1{font-size:var(--mk-font-size-3xl);font-weight:var(--mk-font-weight-bold)}.mk-heading-block__text--h2{font-size:var(--mk-font-size-2xl);font-weight:var(--mk-font-weight-bold)}.mk-heading-block__text--h3{font-size:var(--mk-font-size-xl);font-weight:var(--mk-font-weight-semibold)}.mk-heading-block__text--h4{font-size:var(--mk-font-size-lg);font-weight:var(--mk-font-weight-semibold)}\n"], dependencies: [{ kind: "component", type: MkRichTextEngine, selector: "mk-rich-text-engine", inputs: ["html", "placeholder", "disabled", "multiline", "splitOnEnter", "minHeight", "ariaLabel", "labelledBy", "describedBy", "invalid"], outputs: ["contentChange", "splitAt", "removeEmpty", "arrowOut", "focusChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
431
+ }
432
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHeadingBlock, decorators: [{
433
+ type: Component,
434
+ args: [{ selector: 'mk-heading-block', imports: [MkRichTextEngine], changeDetection: ChangeDetectionStrategy.OnPush, template: `
435
+ @if (!readonly()) {
436
+ <div class="mk-heading-block__levels" role="group" [attr.aria-label]="i18n.blockEditor.headingLevelGroup">
437
+ @for (lvl of levels; track lvl) {
438
+ <button
439
+ type="button"
440
+ class="mk-heading-block__level"
441
+ [class.mk-heading-block__level--active]="level() === lvl"
442
+ [attr.aria-pressed]="level() === lvl"
443
+ (click)="setLevel(lvl)"
444
+ >
445
+ H{{ lvl }}
446
+ </button>
447
+ }
448
+ </div>
449
+ }
450
+ <mk-rich-text-engine
451
+ class="mk-heading-block__text"
452
+ [class]="'mk-heading-block__text--h' + level()"
453
+ [html]="block().data['html'] ?? ''"
454
+ [placeholder]="i18n.blockEditor.headingPlaceholder(level())"
455
+ [disabled]="readonly()"
456
+ [multiline]="false"
457
+ [ariaLabel]="i18n.blockEditor.headingLevel(level())"
458
+ (contentChange)="onContent($event)"
459
+ (splitAt)="splitAt.emit($event)"
460
+ (removeEmpty)="removeEmpty.emit()"
461
+ (arrowOut)="arrowOut.emit($event)"
462
+ />
463
+ `, styles: [":host{display:block}.mk-heading-block__levels{display:inline-flex;gap:2px;margin-bottom:var(--mk-space-1);padding:2px;border-radius:var(--mk-radius-sm);background:var(--mk-surface-2)}.mk-heading-block__level{min-width:30px;height:24px;border:none;border-radius:var(--mk-radius-xs);background:transparent;color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-semibold);cursor:pointer}.mk-heading-block__level:hover{background:var(--mk-hover-overlay)}.mk-heading-block__level--active{background:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-heading-block__level:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-heading-block__text--h1{font-size:var(--mk-font-size-3xl);font-weight:var(--mk-font-weight-bold)}.mk-heading-block__text--h2{font-size:var(--mk-font-size-2xl);font-weight:var(--mk-font-weight-bold)}.mk-heading-block__text--h3{font-size:var(--mk-font-size-xl);font-weight:var(--mk-font-weight-semibold)}.mk-heading-block__text--h4{font-size:var(--mk-font-size-lg);font-weight:var(--mk-font-weight-semibold)}\n"] }]
464
+ }], propDecorators: { block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], blockChange: [{ type: i0.Output, args: ["blockChange"] }], splitAt: [{ type: i0.Output, args: ["splitAt"] }], removeEmpty: [{ type: i0.Output, args: ["removeEmpty"] }], arrowOut: [{ type: i0.Output, args: ["arrowOut"] }] } });
465
+
466
+ /** Quote block — a blockquote with rich text and an optional citation line. */
467
+ class MkQuoteBlock {
468
+ i18n = inject(MK_I18N);
469
+ block = input.required(/* @ts-ignore */
470
+ ...(ngDevMode ? [{ debugName: "block" }] : /* istanbul ignore next */ []));
471
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
472
+ blockChange = output();
473
+ splitAt = output();
474
+ removeEmpty = output();
475
+ arrowOut = output();
476
+ onContent(html) {
477
+ const b = this.block();
478
+ this.blockChange.emit({ ...b, data: { ...b.data, html } });
479
+ }
480
+ onCite(event) {
481
+ const b = this.block();
482
+ const citation = event.target.value;
483
+ this.blockChange.emit({ ...b, data: { ...b.data, citation } });
484
+ }
485
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkQuoteBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
486
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkQuoteBlock, isStandalone: true, selector: "mk-quote-block", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blockChange: "blockChange", splitAt: "splitAt", removeEmpty: "removeEmpty", arrowOut: "arrowOut" }, ngImport: i0, template: `
487
+ <blockquote class="mk-quote-block__quote">
488
+ <mk-rich-text-engine
489
+ [html]="block().data['html'] ?? ''"
490
+ [placeholder]="i18n.blockEditor.blockQuote"
491
+ [disabled]="readonly()"
492
+ [ariaLabel]="i18n.blockEditor.quoteText"
493
+ (contentChange)="onContent($event)"
494
+ (splitAt)="splitAt.emit($event)"
495
+ (removeEmpty)="removeEmpty.emit()"
496
+ (arrowOut)="arrowOut.emit($event)"
497
+ />
498
+ </blockquote>
499
+ @if (!readonly() || block().data['citation']) {
500
+ <input
501
+ class="mk-quote-block__cite"
502
+ type="text"
503
+ [value]="block().data['citation'] ?? ''"
504
+ [disabled]="readonly()"
505
+ [placeholder]="i18n.blockEditor.addCitation"
506
+ [attr.aria-label]="i18n.blockEditor.citation"
507
+ (input)="onCite($event)"
508
+ />
509
+ }
510
+ `, isInline: true, styles: [":host{display:block}.mk-quote-block__quote{margin:0;padding-inline-start:var(--mk-space-4);border-inline-start:4px solid var(--mk-primary);font-size:var(--mk-font-size-lg);font-style:italic;color:var(--mk-text)}.mk-quote-block__cite{margin-top:var(--mk-space-2);margin-inline-start:var(--mk-space-4);width:calc(100% - var(--mk-space-4));border:none;background:transparent;color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-sm)}.mk-quote-block__cite:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);border-radius:var(--mk-radius-xs)}\n"], dependencies: [{ kind: "component", type: MkRichTextEngine, selector: "mk-rich-text-engine", inputs: ["html", "placeholder", "disabled", "multiline", "splitOnEnter", "minHeight", "ariaLabel", "labelledBy", "describedBy", "invalid"], outputs: ["contentChange", "splitAt", "removeEmpty", "arrowOut", "focusChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
511
+ }
512
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkQuoteBlock, decorators: [{
513
+ type: Component,
514
+ args: [{ selector: 'mk-quote-block', imports: [MkRichTextEngine], changeDetection: ChangeDetectionStrategy.OnPush, template: `
515
+ <blockquote class="mk-quote-block__quote">
516
+ <mk-rich-text-engine
517
+ [html]="block().data['html'] ?? ''"
518
+ [placeholder]="i18n.blockEditor.blockQuote"
519
+ [disabled]="readonly()"
520
+ [ariaLabel]="i18n.blockEditor.quoteText"
521
+ (contentChange)="onContent($event)"
522
+ (splitAt)="splitAt.emit($event)"
523
+ (removeEmpty)="removeEmpty.emit()"
524
+ (arrowOut)="arrowOut.emit($event)"
525
+ />
526
+ </blockquote>
527
+ @if (!readonly() || block().data['citation']) {
528
+ <input
529
+ class="mk-quote-block__cite"
530
+ type="text"
531
+ [value]="block().data['citation'] ?? ''"
532
+ [disabled]="readonly()"
533
+ [placeholder]="i18n.blockEditor.addCitation"
534
+ [attr.aria-label]="i18n.blockEditor.citation"
535
+ (input)="onCite($event)"
536
+ />
537
+ }
538
+ `, styles: [":host{display:block}.mk-quote-block__quote{margin:0;padding-inline-start:var(--mk-space-4);border-inline-start:4px solid var(--mk-primary);font-size:var(--mk-font-size-lg);font-style:italic;color:var(--mk-text)}.mk-quote-block__cite{margin-top:var(--mk-space-2);margin-inline-start:var(--mk-space-4);width:calc(100% - var(--mk-space-4));border:none;background:transparent;color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-sm)}.mk-quote-block__cite:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);border-radius:var(--mk-radius-xs)}\n"] }]
539
+ }], propDecorators: { block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], blockChange: [{ type: i0.Output, args: ["blockChange"] }], splitAt: [{ type: i0.Output, args: ["splitAt"] }], removeEmpty: [{ type: i0.Output, args: ["removeEmpty"] }], arrowOut: [{ type: i0.Output, args: ["arrowOut"] }] } });
540
+
541
+ /** List block — a bulleted or numbered list of rich-text items. */
542
+ class MkListBlock {
543
+ i18n = inject(MK_I18N);
544
+ block = input.required(/* @ts-ignore */
545
+ ...(ngDevMode ? [{ debugName: "block" }] : /* istanbul ignore next */ []));
546
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
547
+ blockChange = output();
548
+ ordered = computed(() => !!this.block().data['ordered'], /* @ts-ignore */
549
+ ...(ngDevMode ? [{ debugName: "ordered" }] : /* istanbul ignore next */ []));
550
+ items = computed(() => {
551
+ const items = this.block().data['items'];
552
+ return Array.isArray(items) && items.length ? items : [''];
553
+ }, /* @ts-ignore */
554
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
555
+ setOrdered(ordered) {
556
+ const b = this.block();
557
+ this.blockChange.emit({ ...b, data: { ...b.data, ordered } });
558
+ }
559
+ onItem(index, html) {
560
+ const items = [...this.items()];
561
+ items[index] = html;
562
+ this.emitItems(items);
563
+ }
564
+ onSplit(index, split) {
565
+ const items = [...this.items()];
566
+ items[index] = split.before;
567
+ items.splice(index + 1, 0, split.after);
568
+ this.emitItems(items);
569
+ }
570
+ onRemove(index) {
571
+ const items = [...this.items()];
572
+ if (items.length <= 1)
573
+ return;
574
+ items.splice(index, 1);
575
+ this.emitItems(items);
576
+ }
577
+ emitItems(items) {
578
+ const b = this.block();
579
+ this.blockChange.emit({ ...b, data: { ...b.data, items } });
580
+ }
581
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkListBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
582
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkListBlock, isStandalone: true, selector: "mk-list-block", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blockChange: "blockChange" }, ngImport: i0, template: `
583
+ @if (!readonly()) {
584
+ <div class="mk-list-block__opts" role="group" [attr.aria-label]="i18n.blockEditor.listStyle">
585
+ <button
586
+ type="button"
587
+ class="mk-list-block__opt"
588
+ [class.mk-list-block__opt--active]="!ordered()"
589
+ [attr.aria-pressed]="!ordered()"
590
+ (click)="setOrdered(false)"
591
+ >
592
+ • {{ i18n.blockEditor.bulleted }}
593
+ </button>
594
+ <button
595
+ type="button"
596
+ class="mk-list-block__opt"
597
+ [class.mk-list-block__opt--active]="ordered()"
598
+ [attr.aria-pressed]="ordered()"
599
+ (click)="setOrdered(true)"
600
+ >
601
+ 1. {{ i18n.blockEditor.numbered }}
602
+ </button>
603
+ </div>
604
+ }
605
+ @if (ordered()) {
606
+ <ol class="mk-list-block__list">
607
+ @for (item of items(); track $index) {
608
+ <li class="mk-list-block__item">
609
+ <mk-rich-text-engine
610
+ [html]="item"
611
+ [placeholder]="i18n.blockEditor.listItem"
612
+ [disabled]="readonly()"
613
+ [multiline]="false"
614
+ [ariaLabel]="i18n.blockEditor.listItem"
615
+ (contentChange)="onItem($index, $event)"
616
+ (splitAt)="onSplit($index, $event)"
617
+ (removeEmpty)="onRemove($index)"
618
+ />
619
+ </li>
620
+ }
621
+ </ol>
622
+ } @else {
623
+ <ul class="mk-list-block__list">
624
+ @for (item of items(); track $index) {
625
+ <li class="mk-list-block__item">
626
+ <mk-rich-text-engine
627
+ [html]="item"
628
+ [placeholder]="i18n.blockEditor.listItem"
629
+ [disabled]="readonly()"
630
+ [multiline]="false"
631
+ [ariaLabel]="i18n.blockEditor.listItem"
632
+ (contentChange)="onItem($index, $event)"
633
+ (splitAt)="onSplit($index, $event)"
634
+ (removeEmpty)="onRemove($index)"
635
+ />
636
+ </li>
637
+ }
638
+ </ul>
639
+ }
640
+ `, isInline: true, styles: [":host{display:block}.mk-list-block__opts{display:inline-flex;gap:2px;margin-bottom:var(--mk-space-2);padding:2px;border-radius:var(--mk-radius-sm);background:var(--mk-surface-2)}.mk-list-block__opt{height:24px;padding:0 var(--mk-space-2);border:none;border-radius:var(--mk-radius-xs);background:transparent;color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-medium);cursor:pointer}.mk-list-block__opt:hover{background:var(--mk-hover-overlay)}.mk-list-block__opt--active{background:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-list-block__opt:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-list-block__list{margin:0;padding-inline-start:var(--mk-space-6);color:var(--mk-text)}.mk-list-block__item{margin:var(--mk-space-1) 0}\n"], dependencies: [{ kind: "component", type: MkRichTextEngine, selector: "mk-rich-text-engine", inputs: ["html", "placeholder", "disabled", "multiline", "splitOnEnter", "minHeight", "ariaLabel", "labelledBy", "describedBy", "invalid"], outputs: ["contentChange", "splitAt", "removeEmpty", "arrowOut", "focusChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
641
+ }
642
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkListBlock, decorators: [{
643
+ type: Component,
644
+ args: [{ selector: 'mk-list-block', imports: [MkRichTextEngine], changeDetection: ChangeDetectionStrategy.OnPush, template: `
645
+ @if (!readonly()) {
646
+ <div class="mk-list-block__opts" role="group" [attr.aria-label]="i18n.blockEditor.listStyle">
647
+ <button
648
+ type="button"
649
+ class="mk-list-block__opt"
650
+ [class.mk-list-block__opt--active]="!ordered()"
651
+ [attr.aria-pressed]="!ordered()"
652
+ (click)="setOrdered(false)"
653
+ >
654
+ • {{ i18n.blockEditor.bulleted }}
655
+ </button>
656
+ <button
657
+ type="button"
658
+ class="mk-list-block__opt"
659
+ [class.mk-list-block__opt--active]="ordered()"
660
+ [attr.aria-pressed]="ordered()"
661
+ (click)="setOrdered(true)"
662
+ >
663
+ 1. {{ i18n.blockEditor.numbered }}
664
+ </button>
665
+ </div>
666
+ }
667
+ @if (ordered()) {
668
+ <ol class="mk-list-block__list">
669
+ @for (item of items(); track $index) {
670
+ <li class="mk-list-block__item">
671
+ <mk-rich-text-engine
672
+ [html]="item"
673
+ [placeholder]="i18n.blockEditor.listItem"
674
+ [disabled]="readonly()"
675
+ [multiline]="false"
676
+ [ariaLabel]="i18n.blockEditor.listItem"
677
+ (contentChange)="onItem($index, $event)"
678
+ (splitAt)="onSplit($index, $event)"
679
+ (removeEmpty)="onRemove($index)"
680
+ />
681
+ </li>
682
+ }
683
+ </ol>
684
+ } @else {
685
+ <ul class="mk-list-block__list">
686
+ @for (item of items(); track $index) {
687
+ <li class="mk-list-block__item">
688
+ <mk-rich-text-engine
689
+ [html]="item"
690
+ [placeholder]="i18n.blockEditor.listItem"
691
+ [disabled]="readonly()"
692
+ [multiline]="false"
693
+ [ariaLabel]="i18n.blockEditor.listItem"
694
+ (contentChange)="onItem($index, $event)"
695
+ (splitAt)="onSplit($index, $event)"
696
+ (removeEmpty)="onRemove($index)"
697
+ />
698
+ </li>
699
+ }
700
+ </ul>
701
+ }
702
+ `, styles: [":host{display:block}.mk-list-block__opts{display:inline-flex;gap:2px;margin-bottom:var(--mk-space-2);padding:2px;border-radius:var(--mk-radius-sm);background:var(--mk-surface-2)}.mk-list-block__opt{height:24px;padding:0 var(--mk-space-2);border:none;border-radius:var(--mk-radius-xs);background:transparent;color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-medium);cursor:pointer}.mk-list-block__opt:hover{background:var(--mk-hover-overlay)}.mk-list-block__opt--active{background:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-list-block__opt:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-list-block__list{margin:0;padding-inline-start:var(--mk-space-6);color:var(--mk-text)}.mk-list-block__item{margin:var(--mk-space-1) 0}\n"] }]
703
+ }], propDecorators: { block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], blockChange: [{ type: i0.Output, args: ["blockChange"] }] } });
704
+
705
+ /**
706
+ * Code block — a plain, monospace, preformatted text area. Content is stored as
707
+ * raw text (never executed, never interpreted as HTML) in `block.data.code`.
708
+ */
709
+ class MkCodeBlock {
710
+ i18n = inject(MK_I18N);
711
+ block = input.required(/* @ts-ignore */
712
+ ...(ngDevMode ? [{ debugName: "block" }] : /* istanbul ignore next */ []));
713
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
714
+ blockChange = output();
715
+ rows() {
716
+ const lines = String(this.block().data['code'] ?? '').split('\n').length;
717
+ return Math.max(3, Math.min(30, lines + 1));
718
+ }
719
+ onCode(event) {
720
+ const b = this.block();
721
+ const code = event.target.value;
722
+ this.blockChange.emit({ ...b, data: { ...b.data, code } });
723
+ }
724
+ onLang(event) {
725
+ const b = this.block();
726
+ const language = event.target.value;
727
+ this.blockChange.emit({ ...b, data: { ...b.data, language } });
728
+ }
729
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkCodeBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
730
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkCodeBlock, isStandalone: true, selector: "mk-code-block", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blockChange: "blockChange" }, ngImport: i0, template: `
731
+ @if (!readonly()) {
732
+ <input
733
+ class="mk-code-block__lang"
734
+ type="text"
735
+ [value]="block().data['language'] ?? ''"
736
+ [placeholder]="i18n.blockEditor.codeLanguagePlaceholder"
737
+ [attr.aria-label]="i18n.blockEditor.codeLanguage"
738
+ (input)="onLang($event)"
739
+ />
740
+ }
741
+ <textarea
742
+ class="mk-code-block__area"
743
+ data-mk-focus
744
+ spellcheck="false"
745
+ autocapitalize="off"
746
+ autocomplete="off"
747
+ [value]="block().data['code'] ?? ''"
748
+ [readOnly]="readonly()"
749
+ [rows]="rows()"
750
+ [placeholder]="i18n.blockEditor.enterCode"
751
+ [attr.aria-label]="i18n.blockEditor.blockCode"
752
+ (input)="onCode($event)"
753
+ ></textarea>
754
+ `, isInline: true, styles: [":host{display:block}.mk-code-block__lang{display:block;margin-bottom:var(--mk-space-1);padding:var(--mk-space-1) var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text-muted);font-family:var(--mk-font-mono);font-size:var(--mk-font-size-xs)}.mk-code-block__area{display:block;width:100%;min-height:4em;padding:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-code-bg);color:var(--mk-text);font-family:var(--mk-font-mono);font-size:var(--mk-font-size-sm);line-height:var(--mk-line-height-normal);resize:vertical;tab-size:2}.mk-code-block__area:focus-visible,.mk-code-block__lang:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
755
+ }
756
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkCodeBlock, decorators: [{
757
+ type: Component,
758
+ args: [{ selector: 'mk-code-block', changeDetection: ChangeDetectionStrategy.OnPush, template: `
759
+ @if (!readonly()) {
760
+ <input
761
+ class="mk-code-block__lang"
762
+ type="text"
763
+ [value]="block().data['language'] ?? ''"
764
+ [placeholder]="i18n.blockEditor.codeLanguagePlaceholder"
765
+ [attr.aria-label]="i18n.blockEditor.codeLanguage"
766
+ (input)="onLang($event)"
767
+ />
768
+ }
769
+ <textarea
770
+ class="mk-code-block__area"
771
+ data-mk-focus
772
+ spellcheck="false"
773
+ autocapitalize="off"
774
+ autocomplete="off"
775
+ [value]="block().data['code'] ?? ''"
776
+ [readOnly]="readonly()"
777
+ [rows]="rows()"
778
+ [placeholder]="i18n.blockEditor.enterCode"
779
+ [attr.aria-label]="i18n.blockEditor.blockCode"
780
+ (input)="onCode($event)"
781
+ ></textarea>
782
+ `, styles: [":host{display:block}.mk-code-block__lang{display:block;margin-bottom:var(--mk-space-1);padding:var(--mk-space-1) var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text-muted);font-family:var(--mk-font-mono);font-size:var(--mk-font-size-xs)}.mk-code-block__area{display:block;width:100%;min-height:4em;padding:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-code-bg);color:var(--mk-text);font-family:var(--mk-font-mono);font-size:var(--mk-font-size-sm);line-height:var(--mk-line-height-normal);resize:vertical;tab-size:2}.mk-code-block__area:focus-visible,.mk-code-block__lang:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"] }]
783
+ }], propDecorators: { block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], blockChange: [{ type: i0.Output, args: ["blockChange"] }] } });
784
+
785
+ /**
786
+ * Image block — upload (file picker or drag-drop) or paste a URL. If an
787
+ * `uploadHandler` is configured (editor input or {@link MK_BLOCK_UPLOAD_HANDLER}
788
+ * token) it is used to persist the file and get back a URL; otherwise the file
789
+ * is read as an inline `data:` URL via `FileReader`. Supports alt text,
790
+ * caption, width and alignment.
791
+ */
792
+ class MkImageBlock {
793
+ ctx = inject(MkBlockEditorContext);
794
+ fileInput = viewChild('fileInput', /* @ts-ignore */
795
+ ...(ngDevMode ? [{ debugName: "fileInput" }] : /* istanbul ignore next */ []));
796
+ i18n = inject(MK_I18N);
797
+ block = input.required(/* @ts-ignore */
798
+ ...(ngDevMode ? [{ debugName: "block" }] : /* istanbul ignore next */ []));
799
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
800
+ blockChange = output();
801
+ aligns = ['left', 'center', 'right'];
802
+ uploading = signal(false, /* @ts-ignore */
803
+ ...(ngDevMode ? [{ debugName: "uploading" }] : /* istanbul ignore next */ []));
804
+ error = signal(null, /* @ts-ignore */
805
+ ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
806
+ dragActive = signal(false, /* @ts-ignore */
807
+ ...(ngDevMode ? [{ debugName: "dragActive" }] : /* istanbul ignore next */ []));
808
+ src() {
809
+ return this.block().data['src'] ?? '';
810
+ }
811
+ /** Localised caption for an alignment option. */
812
+ alignLabel(align) {
813
+ const s = this.i18n.blockEditor;
814
+ return { left: s.alignLeft, center: s.alignCenter, right: s.alignRight }[align];
815
+ }
816
+ openPicker() {
817
+ this.fileInput()?.nativeElement.click();
818
+ }
819
+ onFileSelected(event) {
820
+ const input = event.target;
821
+ const file = input.files?.[0];
822
+ if (file)
823
+ this.handleFile(file);
824
+ input.value = '';
825
+ }
826
+ onDrop(event) {
827
+ event.preventDefault();
828
+ this.dragActive.set(false);
829
+ if (this.readonly())
830
+ return;
831
+ const file = event.dataTransfer?.files?.[0];
832
+ if (file)
833
+ this.handleFile(file);
834
+ }
835
+ onDragOver(event) {
836
+ if (this.readonly())
837
+ return;
838
+ event.preventDefault();
839
+ this.dragActive.set(true);
840
+ }
841
+ onDragLeave() {
842
+ this.dragActive.set(false);
843
+ }
844
+ onUrl(event) {
845
+ const value = event.target.value.trim();
846
+ this.patch({ src: value });
847
+ }
848
+ onAlt(event) {
849
+ this.patch({ alt: event.target.value });
850
+ }
851
+ onCaption(event) {
852
+ this.patch({ caption: event.target.value });
853
+ }
854
+ setAlign(align) {
855
+ this.patch({ align });
856
+ }
857
+ onWidth(event) {
858
+ this.patch({ width: Number(event.target.value) });
859
+ }
860
+ clear() {
861
+ this.patch({ src: '' });
862
+ }
863
+ async handleFile(file) {
864
+ if (!file.type.startsWith('image/')) {
865
+ this.error.set(this.i18n.blockEditor.notAnImage);
866
+ return;
867
+ }
868
+ this.error.set(null);
869
+ this.uploading.set(true);
870
+ try {
871
+ const handler = this.ctx.uploadHandler();
872
+ const url = handler ? await handler(file) : await this.readAsDataUrl(file);
873
+ this.patch({ src: url, alt: this.block().data['alt'] || file.name });
874
+ this.ctx.announce(this.i18n.blockEditor.imageAdded);
875
+ }
876
+ catch {
877
+ this.error.set(this.i18n.blockEditor.uploadFailed);
878
+ }
879
+ finally {
880
+ this.uploading.set(false);
881
+ }
882
+ }
883
+ readAsDataUrl(file) {
884
+ return new Promise((resolve, reject) => {
885
+ const reader = new FileReader();
886
+ reader.onload = () => resolve(String(reader.result));
887
+ reader.onerror = () => reject(reader.error);
888
+ reader.readAsDataURL(file);
889
+ });
890
+ }
891
+ patch(data) {
892
+ const b = this.block();
893
+ this.blockChange.emit({ ...b, data: { ...b.data, ...data } });
894
+ }
895
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkImageBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
896
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkImageBlock, isStandalone: true, selector: "mk-image-block", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blockChange: "blockChange" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<input\n #fileInput\n type=\"file\"\n accept=\"image/*\"\n class=\"mk-image-block__file\"\n hidden\n (change)=\"onFileSelected($event)\"\n/>\n\n@if (src()) {\n <figure\n class=\"mk-image-block__figure\"\n [class.mk-image-block__figure--left]=\"block().data['align'] === 'left'\"\n [class.mk-image-block__figure--right]=\"block().data['align'] === 'right'\"\n >\n <img\n class=\"mk-image-block__img\"\n [src]=\"src()\"\n [alt]=\"block().data['alt'] ?? ''\"\n [style.width.%]=\"block().data['width'] || 100\"\n />\n @if (block().data['caption'] || !readonly()) {\n <figcaption class=\"mk-image-block__caption\">{{ block().data['caption'] }}</figcaption>\n }\n </figure>\n\n @if (!readonly()) {\n <div class=\"mk-image-block__settings\">\n <label class=\"mk-image-block__field\">\n <span>{{ i18n.blockEditor.altText }}</span>\n <input type=\"text\" [value]=\"block().data['alt'] ?? ''\" (input)=\"onAlt($event)\" />\n </label>\n <label class=\"mk-image-block__field\">\n <span>{{ i18n.blockEditor.caption }}</span>\n <input type=\"text\" [value]=\"block().data['caption'] ?? ''\" (input)=\"onCaption($event)\" />\n </label>\n <div class=\"mk-image-block__field\">\n <span id=\"mk-img-align\">{{ i18n.blockEditor.alignment }}</span>\n <div class=\"mk-image-block__aligns\" role=\"group\" aria-labelledby=\"mk-img-align\">\n @for (a of aligns; track a) {\n <button\n type=\"button\"\n class=\"mk-image-block__align\"\n [class.mk-image-block__align--active]=\"(block().data['align'] ?? 'center') === a\"\n [attr.aria-pressed]=\"(block().data['align'] ?? 'center') === a\"\n (click)=\"setAlign(a)\"\n >\n {{ alignLabel(a) }}\n </button>\n }\n </div>\n </div>\n <label class=\"mk-image-block__field\">\n <span>{{ i18n.blockEditor.imageWidth(block().data['width'] || 100) }}</span>\n <input\n type=\"range\"\n min=\"20\"\n max=\"100\"\n step=\"5\"\n [value]=\"block().data['width'] || 100\"\n (input)=\"onWidth($event)\"\n />\n </label>\n <button type=\"button\" class=\"mk-image-block__replace\" (click)=\"clear()\">\n {{ i18n.blockEditor.replaceImage }}\n </button>\n </div>\n }\n} @else {\n <div\n class=\"mk-image-block__drop\"\n [class.mk-image-block__drop--active]=\"dragActive()\"\n (drop)=\"onDrop($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave()\"\n >\n @if (uploading()) {\n <p class=\"mk-image-block__hint\">{{ i18n.blockEditor.uploading }}</p>\n } @else {\n <p class=\"mk-image-block__icon\" aria-hidden=\"true\">\uD83D\uDDBC</p>\n <p class=\"mk-image-block__hint\">{{ i18n.blockEditor.dropImagePrompt }}</p>\n <button\n type=\"button\"\n class=\"mk-image-block__upload\"\n data-mk-focus\n [disabled]=\"readonly()\"\n (click)=\"openPicker()\"\n >\n {{ i18n.blockEditor.chooseFile }}\n </button>\n <label class=\"mk-image-block__url\">\n <span class=\"mk-visually-hidden\">{{ i18n.blockEditor.imageUrl }}</span>\n <input\n type=\"url\"\n [placeholder]=\"i18n.blockEditor.pasteImageUrl\"\n [disabled]=\"readonly()\"\n (change)=\"onUrl($event)\"\n />\n </label>\n }\n @if (error()) {\n <p class=\"mk-image-block__error\" role=\"alert\">{{ error() }}</p>\n }\n </div>\n}\n", styles: [":host{display:block}.mk-image-block__figure{margin:0;text-align:center}.mk-image-block__figure--left{text-align:left}.mk-image-block__figure--right{text-align:right}.mk-image-block__img{max-width:100%;height:auto;border-radius:var(--mk-radius-md);vertical-align:middle}.mk-image-block__caption{margin-top:var(--mk-space-2);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}.mk-image-block__drop{display:flex;flex-direction:column;align-items:center;gap:var(--mk-space-2);padding:var(--mk-space-8) var(--mk-space-4);border:var(--mk-border-width-strong) dashed var(--mk-border-strong);border-radius:var(--mk-radius-lg);background:var(--mk-surface-2);text-align:center;transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-image-block__drop--active{border-color:var(--mk-primary);background:var(--mk-primary-subtle)}.mk-image-block__icon{margin:0;font-size:var(--mk-font-size-3xl)}.mk-image-block__hint{margin:0;color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}.mk-image-block__upload{height:var(--mk-control-height-sm);padding:0 var(--mk-space-4);border:var(--mk-border-width) solid var(--mk-border-strong);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text);font:inherit;font-weight:var(--mk-font-weight-medium);cursor:pointer}.mk-image-block__upload:hover{background:var(--mk-surface-3)}.mk-image-block__url input{width:min(320px,80vw);height:var(--mk-control-height-sm);padding:0 var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text);font:inherit}.mk-image-block__error{margin:0;color:var(--mk-danger-text);font-size:var(--mk-font-size-sm)}.mk-image-block__settings{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:var(--mk-space-3);margin-top:var(--mk-space-3);padding:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border-subtle);border-radius:var(--mk-radius-md);background:var(--mk-surface-2)}.mk-image-block__field{display:flex;flex-direction:column;gap:var(--mk-space-1);font-size:var(--mk-font-size-xs);color:var(--mk-text-muted)}.mk-image-block__field input[type=text],.mk-image-block__field input[type=url]{height:var(--mk-control-height-sm);padding:0 var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text);font:inherit}.mk-image-block__aligns{display:inline-flex;gap:2px}.mk-image-block__align{flex:1;height:var(--mk-control-height-sm);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-xs);text-transform:capitalize;cursor:pointer}.mk-image-block__align--active{background:var(--mk-primary);border-color:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-image-block__replace{align-self:end;height:var(--mk-control-height-sm);border:none;border-radius:var(--mk-radius-sm);background:transparent;color:var(--mk-danger-text);font:inherit;font-size:var(--mk-font-size-sm);cursor:pointer}button:focus-visible,input:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
897
+ }
898
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkImageBlock, decorators: [{
899
+ type: Component,
900
+ args: [{ selector: 'mk-image-block', changeDetection: ChangeDetectionStrategy.OnPush, template: "<input\n #fileInput\n type=\"file\"\n accept=\"image/*\"\n class=\"mk-image-block__file\"\n hidden\n (change)=\"onFileSelected($event)\"\n/>\n\n@if (src()) {\n <figure\n class=\"mk-image-block__figure\"\n [class.mk-image-block__figure--left]=\"block().data['align'] === 'left'\"\n [class.mk-image-block__figure--right]=\"block().data['align'] === 'right'\"\n >\n <img\n class=\"mk-image-block__img\"\n [src]=\"src()\"\n [alt]=\"block().data['alt'] ?? ''\"\n [style.width.%]=\"block().data['width'] || 100\"\n />\n @if (block().data['caption'] || !readonly()) {\n <figcaption class=\"mk-image-block__caption\">{{ block().data['caption'] }}</figcaption>\n }\n </figure>\n\n @if (!readonly()) {\n <div class=\"mk-image-block__settings\">\n <label class=\"mk-image-block__field\">\n <span>{{ i18n.blockEditor.altText }}</span>\n <input type=\"text\" [value]=\"block().data['alt'] ?? ''\" (input)=\"onAlt($event)\" />\n </label>\n <label class=\"mk-image-block__field\">\n <span>{{ i18n.blockEditor.caption }}</span>\n <input type=\"text\" [value]=\"block().data['caption'] ?? ''\" (input)=\"onCaption($event)\" />\n </label>\n <div class=\"mk-image-block__field\">\n <span id=\"mk-img-align\">{{ i18n.blockEditor.alignment }}</span>\n <div class=\"mk-image-block__aligns\" role=\"group\" aria-labelledby=\"mk-img-align\">\n @for (a of aligns; track a) {\n <button\n type=\"button\"\n class=\"mk-image-block__align\"\n [class.mk-image-block__align--active]=\"(block().data['align'] ?? 'center') === a\"\n [attr.aria-pressed]=\"(block().data['align'] ?? 'center') === a\"\n (click)=\"setAlign(a)\"\n >\n {{ alignLabel(a) }}\n </button>\n }\n </div>\n </div>\n <label class=\"mk-image-block__field\">\n <span>{{ i18n.blockEditor.imageWidth(block().data['width'] || 100) }}</span>\n <input\n type=\"range\"\n min=\"20\"\n max=\"100\"\n step=\"5\"\n [value]=\"block().data['width'] || 100\"\n (input)=\"onWidth($event)\"\n />\n </label>\n <button type=\"button\" class=\"mk-image-block__replace\" (click)=\"clear()\">\n {{ i18n.blockEditor.replaceImage }}\n </button>\n </div>\n }\n} @else {\n <div\n class=\"mk-image-block__drop\"\n [class.mk-image-block__drop--active]=\"dragActive()\"\n (drop)=\"onDrop($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave()\"\n >\n @if (uploading()) {\n <p class=\"mk-image-block__hint\">{{ i18n.blockEditor.uploading }}</p>\n } @else {\n <p class=\"mk-image-block__icon\" aria-hidden=\"true\">\uD83D\uDDBC</p>\n <p class=\"mk-image-block__hint\">{{ i18n.blockEditor.dropImagePrompt }}</p>\n <button\n type=\"button\"\n class=\"mk-image-block__upload\"\n data-mk-focus\n [disabled]=\"readonly()\"\n (click)=\"openPicker()\"\n >\n {{ i18n.blockEditor.chooseFile }}\n </button>\n <label class=\"mk-image-block__url\">\n <span class=\"mk-visually-hidden\">{{ i18n.blockEditor.imageUrl }}</span>\n <input\n type=\"url\"\n [placeholder]=\"i18n.blockEditor.pasteImageUrl\"\n [disabled]=\"readonly()\"\n (change)=\"onUrl($event)\"\n />\n </label>\n }\n @if (error()) {\n <p class=\"mk-image-block__error\" role=\"alert\">{{ error() }}</p>\n }\n </div>\n}\n", styles: [":host{display:block}.mk-image-block__figure{margin:0;text-align:center}.mk-image-block__figure--left{text-align:left}.mk-image-block__figure--right{text-align:right}.mk-image-block__img{max-width:100%;height:auto;border-radius:var(--mk-radius-md);vertical-align:middle}.mk-image-block__caption{margin-top:var(--mk-space-2);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}.mk-image-block__drop{display:flex;flex-direction:column;align-items:center;gap:var(--mk-space-2);padding:var(--mk-space-8) var(--mk-space-4);border:var(--mk-border-width-strong) dashed var(--mk-border-strong);border-radius:var(--mk-radius-lg);background:var(--mk-surface-2);text-align:center;transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-image-block__drop--active{border-color:var(--mk-primary);background:var(--mk-primary-subtle)}.mk-image-block__icon{margin:0;font-size:var(--mk-font-size-3xl)}.mk-image-block__hint{margin:0;color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}.mk-image-block__upload{height:var(--mk-control-height-sm);padding:0 var(--mk-space-4);border:var(--mk-border-width) solid var(--mk-border-strong);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text);font:inherit;font-weight:var(--mk-font-weight-medium);cursor:pointer}.mk-image-block__upload:hover{background:var(--mk-surface-3)}.mk-image-block__url input{width:min(320px,80vw);height:var(--mk-control-height-sm);padding:0 var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text);font:inherit}.mk-image-block__error{margin:0;color:var(--mk-danger-text);font-size:var(--mk-font-size-sm)}.mk-image-block__settings{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:var(--mk-space-3);margin-top:var(--mk-space-3);padding:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border-subtle);border-radius:var(--mk-radius-md);background:var(--mk-surface-2)}.mk-image-block__field{display:flex;flex-direction:column;gap:var(--mk-space-1);font-size:var(--mk-font-size-xs);color:var(--mk-text-muted)}.mk-image-block__field input[type=text],.mk-image-block__field input[type=url]{height:var(--mk-control-height-sm);padding:0 var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text);font:inherit}.mk-image-block__aligns{display:inline-flex;gap:2px}.mk-image-block__align{flex:1;height:var(--mk-control-height-sm);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-xs);text-transform:capitalize;cursor:pointer}.mk-image-block__align--active{background:var(--mk-primary);border-color:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-image-block__replace{align-self:end;height:var(--mk-control-height-sm);border:none;border-radius:var(--mk-radius-sm);background:transparent;color:var(--mk-danger-text);font:inherit;font-size:var(--mk-font-size-sm);cursor:pointer}button:focus-visible,input:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}\n"] }]
901
+ }], propDecorators: { fileInput: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }], block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], blockChange: [{ type: i0.Output, args: ["blockChange"] }] } });
902
+
903
+ /** Escapes text for safe placement in element content. */
904
+ function mkEscapeHtml(value) {
905
+ return String(value ?? '')
906
+ .replace(/&/g, '&amp;')
907
+ .replace(/</g, '&lt;')
908
+ .replace(/>/g, '&gt;');
909
+ }
910
+ /** Escapes a value for safe placement in a double-quoted attribute. */
911
+ function mkEscapeAttr(value) {
912
+ return mkEscapeHtml(value).replace(/"/g, '&quot;');
913
+ }
914
+ /** True for URL schemes safe to place in `href`/`src`. */
915
+ function mkIsSafeUrl(url) {
916
+ const trimmed = String(url ?? '').trim();
917
+ if (trimmed === '')
918
+ return false;
919
+ // Allow relative, http(s), mailto, tel and data:image.
920
+ if (/^(https?:|mailto:|tel:)/i.test(trimmed))
921
+ return true;
922
+ if (/^data:image\//i.test(trimmed))
923
+ return true;
924
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed))
925
+ return false; // any other scheme
926
+ return true; // relative / anchor / protocol-relative-ish
927
+ }
928
+ /** Maps a ratio preset like `66-33` to a CSS `grid-template-columns` value. */
929
+ function mkRatioToColumns(ratio, count) {
930
+ const parts = String(ratio ?? '')
931
+ .split('-')
932
+ .map((n) => parseFloat(n))
933
+ .filter((n) => !isNaN(n) && n > 0);
934
+ if (parts.length >= 2)
935
+ return parts.map((n) => `${n}fr`).join(' ');
936
+ return `repeat(${Math.max(1, count || 1)}, 1fr)`;
937
+ }
938
+ const HEADING_LEVELS = new Set([1, 2, 3, 4]);
939
+ /** Serialises one block to a semantic HTML string. */
940
+ function blockToHtml(b) {
941
+ switch (b.type) {
942
+ case 'paragraph':
943
+ return `<p>${sanitizeInlineHtml(b.data['html'] ?? '')}</p>`;
944
+ case 'heading': {
945
+ const level = HEADING_LEVELS.has(b.data['level']) ? b.data['level'] : 2;
946
+ return `<h${level}>${sanitizeInlineHtml(b.data['html'] ?? '')}</h${level}>`;
947
+ }
948
+ case 'list': {
949
+ const ordered = !!b.data['ordered'];
950
+ const tag = ordered ? 'ol' : 'ul';
951
+ const items = Array.isArray(b.data['items']) ? b.data['items'] : [];
952
+ const li = items.map((it) => `<li>${sanitizeInlineHtml(it ?? '')}</li>`).join('');
953
+ return `<${tag}>${li}</${tag}>`;
954
+ }
955
+ case 'quote': {
956
+ const cite = String(b.data['citation'] ?? '').trim();
957
+ const citeHtml = cite ? `<cite>${mkEscapeHtml(cite)}</cite>` : '';
958
+ return `<blockquote><p>${sanitizeInlineHtml(b.data['html'] ?? '')}</p>${citeHtml}</blockquote>`;
959
+ }
960
+ case 'code': {
961
+ const code = mkEscapeHtml(b.data['code'] ?? '');
962
+ const lang = String(b.data['language'] ?? '').trim();
963
+ const cls = lang ? ` class="language-${mkEscapeAttr(lang)}"` : '';
964
+ return `<pre><code${cls}>${code}</code></pre>`;
965
+ }
966
+ case 'image': {
967
+ const src = String(b.data['src'] ?? '');
968
+ if (!mkIsSafeUrl(src) || !src)
969
+ return '';
970
+ const align = String(b.data['align'] ?? 'center');
971
+ const width = Number(b.data['width']) || 100;
972
+ const alt = mkEscapeAttr(b.data['alt'] ?? '');
973
+ const caption = String(b.data['caption'] ?? '').trim();
974
+ const figStyle = `text-align:${align === 'left' ? 'left' : align === 'right' ? 'right' : 'center'};margin:1rem 0;`;
975
+ const img = `<img src="${mkEscapeAttr(src)}" alt="${alt}" style="max-width:${Math.min(100, Math.max(10, width))}%;height:auto;border-radius:.5rem;" loading="lazy">`;
976
+ const cap = caption ? `<figcaption style="font-size:.875rem;opacity:.75;margin-top:.5rem;">${mkEscapeHtml(caption)}</figcaption>` : '';
977
+ return `<figure style="${figStyle}">${img}${cap}</figure>`;
978
+ }
979
+ case 'button': {
980
+ const href = String(b.data['href'] ?? '');
981
+ const label = mkEscapeHtml(b.data['label'] ?? 'Button');
982
+ const align = String(b.data['align'] ?? 'left');
983
+ const safe = mkIsSafeUrl(href) && href ? mkEscapeAttr(href) : '#';
984
+ const wrapStyle = `text-align:${align};margin:1rem 0;`;
985
+ const btnStyle = 'display:inline-block;padding:.6rem 1.1rem;border-radius:.5rem;text-decoration:none;font-weight:600;';
986
+ return `<div style="${wrapStyle}"><a href="${safe}" class="mk-cta mk-cta--${mkEscapeAttr(b.data['tone'] ?? 'primary')}" style="${btnStyle}" rel="noopener noreferrer">${label}</a></div>`;
987
+ }
988
+ case 'divider':
989
+ return '<hr>';
990
+ case 'embed': {
991
+ const url = String(b.data['url'] ?? '');
992
+ const embedUrl = String(b.data['embedUrl'] ?? '');
993
+ const ratio = Number(b.data['aspectRatio']) || 16 / 9;
994
+ if (embedUrl && mkIsSafeUrl(embedUrl)) {
995
+ const pad = (100 / ratio).toFixed(3);
996
+ return (`<div class="mk-embed" style="position:relative;width:100%;padding-top:${pad}%;margin:1rem 0;">` +
997
+ `<iframe src="${mkEscapeAttr(embedUrl)}" title="Embedded content" loading="lazy" ` +
998
+ `sandbox="allow-scripts allow-same-origin allow-presentation allow-popups" ` +
999
+ `referrerpolicy="strict-origin-when-cross-origin" allowfullscreen ` +
1000
+ `style="position:absolute;inset:0;width:100%;height:100%;border:0;border-radius:.5rem;"></iframe></div>`);
1001
+ }
1002
+ if (mkIsSafeUrl(url) && url) {
1003
+ return `<p class="mk-embed-fallback"><a href="${mkEscapeAttr(url)}" rel="noopener noreferrer" target="_blank">${mkEscapeHtml(url)}</a></p>`;
1004
+ }
1005
+ return '';
1006
+ }
1007
+ case 'columns': {
1008
+ const count = Number(b.data['count']) || (b.children?.length ?? 2);
1009
+ const cols = mkRatioToColumns(b.data['ratio'] ?? '', count);
1010
+ const gap = (Number(b.data['gap']) || 4) * 0.25;
1011
+ const align = String(b.data['align'] ?? 'stretch');
1012
+ const justify = String(b.data['justify'] ?? 'start');
1013
+ const justifyMap = {
1014
+ start: 'flex-start', center: 'center', end: 'flex-end', between: 'space-between',
1015
+ };
1016
+ const style = `display:grid;grid-template-columns:${cols};gap:${gap}rem;align-items:${align};` +
1017
+ `justify-content:${justifyMap[justify] ?? 'flex-start'};margin:1rem 0;`;
1018
+ const columns = (b.children ?? [])
1019
+ .map((col) => `<div class="mk-column">${(col.children ?? []).map(blockToHtml).join('')}</div>`)
1020
+ .join('');
1021
+ return `<div class="mk-columns" style="${style}">${columns}</div>`;
1022
+ }
1023
+ case 'column':
1024
+ return (b.children ?? []).map(blockToHtml).join('');
1025
+ default:
1026
+ return '';
1027
+ }
1028
+ }
1029
+ /**
1030
+ * Serialises a document to clean, semantic, self-contained HTML — the "rich
1031
+ * HTML content" you persist and publish for a blog post or content page.
1032
+ * Layout blocks become responsive grid wrappers with inline styles.
1033
+ */
1034
+ function mkBlocksToHtml(doc) {
1035
+ if (!doc?.blocks?.length)
1036
+ return '';
1037
+ return doc.blocks.map(blockToHtml).join('\n');
1038
+ }
1039
+ /** Strips all tags, collapsing a block to its text content. */
1040
+ function stripTags(html) {
1041
+ return String(html ?? '')
1042
+ .replace(/<[^>]*>/g, ' ')
1043
+ .replace(/&nbsp;/g, ' ')
1044
+ .replace(/&amp;/g, '&')
1045
+ .replace(/&lt;/g, '<')
1046
+ .replace(/&gt;/g, '>')
1047
+ .replace(/\s+/g, ' ')
1048
+ .trim();
1049
+ }
1050
+ /** Collects the plain-text content of a block subtree. */
1051
+ function blockToText(b) {
1052
+ switch (b.type) {
1053
+ case 'paragraph':
1054
+ case 'heading':
1055
+ case 'quote':
1056
+ return stripTags(b.data['html'] ?? '');
1057
+ case 'list':
1058
+ return (Array.isArray(b.data['items']) ? b.data['items'] : [])
1059
+ .map((i) => stripTags(i))
1060
+ .filter(Boolean)
1061
+ .join(' · ');
1062
+ case 'code':
1063
+ return String(b.data['code'] ?? '').trim();
1064
+ case 'image':
1065
+ return String(b.data['caption'] ?? b.data['alt'] ?? '').trim();
1066
+ case 'button':
1067
+ return String(b.data['label'] ?? '').trim();
1068
+ case 'embed':
1069
+ return String(b.data['url'] ?? '').trim();
1070
+ case 'columns':
1071
+ case 'column':
1072
+ return (b.children ?? [])
1073
+ .map((c) => (c.type === 'column' ? blockToText(c) : blockToText(c)))
1074
+ .filter(Boolean)
1075
+ .join(' ');
1076
+ default:
1077
+ return '';
1078
+ }
1079
+ }
1080
+ /**
1081
+ * Serialises a document to plain text — handy for previews, excerpts, search
1082
+ * indexes and meta descriptions.
1083
+ */
1084
+ function mkBlocksToText(doc) {
1085
+ if (!doc?.blocks?.length)
1086
+ return '';
1087
+ return doc.blocks
1088
+ .map(blockToText)
1089
+ .filter((t) => t.length > 0)
1090
+ .join('\n\n');
1091
+ }
1092
+
1093
+ /**
1094
+ * Embed block — paste a URL; matching allow-listed providers (YouTube, Vimeo,
1095
+ * plus any supplied via `embedProviders`) render a SANDBOXED iframe. The only
1096
+ * value passed to `DomSanitizer.bypassSecurityTrustResourceUrl` is the
1097
+ * provider-transformed embed URL (never the raw pasted string). Non-matching
1098
+ * URLs fall back to a safe link card — raw HTML is never injected.
1099
+ */
1100
+ class MkEmbedBlock {
1101
+ ctx = inject(MkBlockEditorContext);
1102
+ sanitizer = inject(DomSanitizer);
1103
+ i18n = inject(MK_I18N);
1104
+ block = input.required(/* @ts-ignore */
1105
+ ...(ngDevMode ? [{ debugName: "block" }] : /* istanbul ignore next */ []));
1106
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1107
+ blockChange = output();
1108
+ embedUrl = computed(() => this.block().data['embedUrl'] ?? '', /* @ts-ignore */
1109
+ ...(ngDevMode ? [{ debugName: "embedUrl" }] : /* istanbul ignore next */ []));
1110
+ url = computed(() => this.block().data['url'] ?? '', /* @ts-ignore */
1111
+ ...(ngDevMode ? [{ debugName: "url" }] : /* istanbul ignore next */ []));
1112
+ aspectRatio = computed(() => Number(this.block().data['aspectRatio']) || 16 / 9, /* @ts-ignore */
1113
+ ...(ngDevMode ? [{ debugName: "aspectRatio" }] : /* istanbul ignore next */ []));
1114
+ providerName = computed(() => this.block().data['provider'] ?? '', /* @ts-ignore */
1115
+ ...(ngDevMode ? [{ debugName: "providerName" }] : /* istanbul ignore next */ []));
1116
+ paddingTop = computed(() => `${100 / this.aspectRatio()}%`, /* @ts-ignore */
1117
+ ...(ngDevMode ? [{ debugName: "paddingTop" }] : /* istanbul ignore next */ []));
1118
+ /** Only ever wraps a provider-transformed URL — see the class doc. */
1119
+ safeSrc = computed(() => {
1120
+ const src = this.embedUrl();
1121
+ if (!src || !mkIsSafeUrl(src) || !/^https:\/\//i.test(src))
1122
+ return null;
1123
+ return this.sanitizer.bypassSecurityTrustResourceUrl(src);
1124
+ }, /* @ts-ignore */
1125
+ ...(ngDevMode ? [{ debugName: "safeSrc" }] : /* istanbul ignore next */ []));
1126
+ hasFallback = computed(() => !this.embedUrl() && !!this.url() && mkIsSafeUrl(this.url()), /* @ts-ignore */
1127
+ ...(ngDevMode ? [{ debugName: "hasFallback" }] : /* istanbul ignore next */ []));
1128
+ onUrl(event) {
1129
+ const raw = event.target.value.trim();
1130
+ this.resolve(raw);
1131
+ }
1132
+ resolve(raw) {
1133
+ const b = this.block();
1134
+ if (!raw) {
1135
+ this.blockChange.emit({ ...b, data: { ...b.data, url: '', embedUrl: '', provider: '' } });
1136
+ return;
1137
+ }
1138
+ for (const provider of this.ctx.embedProviders()) {
1139
+ if (provider.test.test(raw)) {
1140
+ const embedUrl = provider.toEmbedUrl(raw);
1141
+ if (embedUrl) {
1142
+ this.blockChange.emit({
1143
+ ...b,
1144
+ data: {
1145
+ ...b.data,
1146
+ url: raw,
1147
+ embedUrl,
1148
+ provider: provider.name,
1149
+ aspectRatio: provider.aspectRatio ?? 16 / 9,
1150
+ },
1151
+ });
1152
+ this.ctx.announce(this.i18n.blockEditor.embedAdded(provider.name));
1153
+ return;
1154
+ }
1155
+ }
1156
+ }
1157
+ // No provider matched — keep the URL for the safe link-card fallback.
1158
+ this.blockChange.emit({ ...b, data: { ...b.data, url: raw, embedUrl: '', provider: '' } });
1159
+ }
1160
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkEmbedBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
1161
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkEmbedBlock, isStandalone: true, selector: "mk-embed-block", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blockChange: "blockChange" }, ngImport: i0, template: "@if (safeSrc()) {\n <div class=\"mk-embed-block__frame\" [style.padding-top]=\"paddingTop()\">\n <iframe\n class=\"mk-embed-block__iframe\"\n [src]=\"safeSrc()\"\n [title]=\"i18n.blockEditor.embedTitle(providerName())\"\n loading=\"lazy\"\n referrerpolicy=\"strict-origin-when-cross-origin\"\n sandbox=\"allow-scripts allow-same-origin allow-presentation allow-popups\"\n allowfullscreen\n ></iframe>\n </div>\n} @else if (hasFallback()) {\n <a class=\"mk-embed-block__card\" [href]=\"url()\" target=\"_blank\" rel=\"noopener noreferrer\">\n <span class=\"mk-embed-block__card-icon\" aria-hidden=\"true\">\uD83D\uDD17</span>\n <span class=\"mk-embed-block__card-body\">\n <span class=\"mk-embed-block__card-title\">{{ i18n.blockEditor.externalContent }}</span>\n <span class=\"mk-embed-block__card-url\">{{ url() }}</span>\n </span>\n </a>\n}\n\n@if (!readonly()) {\n <label class=\"mk-embed-block__input\">\n <span class=\"mk-visually-hidden\">{{ i18n.blockEditor.embedUrl }}</span>\n <input\n type=\"url\"\n data-mk-focus\n [value]=\"url()\"\n [placeholder]=\"i18n.blockEditor.pasteEmbedUrl\"\n (change)=\"onUrl($event)\"\n />\n </label>\n @if (url() && !safeSrc() && !hasFallback()) {\n <p class=\"mk-embed-block__note\">{{ i18n.blockEditor.embedFallbackNote }}</p>\n }\n}\n", styles: [":host{display:block}.mk-embed-block__frame{position:relative;width:100%;overflow:hidden;border-radius:var(--mk-radius-md);background:var(--mk-surface-3)}.mk-embed-block__iframe{position:absolute;inset:0;width:100%;height:100%;border:0}.mk-embed-block__card{display:flex;align-items:center;gap:var(--mk-space-3);padding:var(--mk-space-3) var(--mk-space-4);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface-2);color:var(--mk-text);text-decoration:none}.mk-embed-block__card:hover{border-color:var(--mk-border-strong);background:var(--mk-surface-3)}.mk-embed-block__card-icon{font-size:var(--mk-font-size-xl)}.mk-embed-block__card-body{display:flex;flex-direction:column;min-width:0}.mk-embed-block__card-title{font-weight:var(--mk-font-weight-medium)}.mk-embed-block__card-url{color:var(--mk-text-muted);font-size:var(--mk-font-size-sm);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mk-embed-block__input{display:block;margin-top:var(--mk-space-2)}.mk-embed-block__input input{width:100%;height:var(--mk-control-height-sm);padding:0 var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text);font:inherit}.mk-embed-block__input input:focus-visible,.mk-embed-block__card:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-embed-block__note{margin:var(--mk-space-1) 0 0;color:var(--mk-text-subtle);font-size:var(--mk-font-size-xs)}.mk-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1162
+ }
1163
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkEmbedBlock, decorators: [{
1164
+ type: Component,
1165
+ args: [{ selector: 'mk-embed-block', changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (safeSrc()) {\n <div class=\"mk-embed-block__frame\" [style.padding-top]=\"paddingTop()\">\n <iframe\n class=\"mk-embed-block__iframe\"\n [src]=\"safeSrc()\"\n [title]=\"i18n.blockEditor.embedTitle(providerName())\"\n loading=\"lazy\"\n referrerpolicy=\"strict-origin-when-cross-origin\"\n sandbox=\"allow-scripts allow-same-origin allow-presentation allow-popups\"\n allowfullscreen\n ></iframe>\n </div>\n} @else if (hasFallback()) {\n <a class=\"mk-embed-block__card\" [href]=\"url()\" target=\"_blank\" rel=\"noopener noreferrer\">\n <span class=\"mk-embed-block__card-icon\" aria-hidden=\"true\">\uD83D\uDD17</span>\n <span class=\"mk-embed-block__card-body\">\n <span class=\"mk-embed-block__card-title\">{{ i18n.blockEditor.externalContent }}</span>\n <span class=\"mk-embed-block__card-url\">{{ url() }}</span>\n </span>\n </a>\n}\n\n@if (!readonly()) {\n <label class=\"mk-embed-block__input\">\n <span class=\"mk-visually-hidden\">{{ i18n.blockEditor.embedUrl }}</span>\n <input\n type=\"url\"\n data-mk-focus\n [value]=\"url()\"\n [placeholder]=\"i18n.blockEditor.pasteEmbedUrl\"\n (change)=\"onUrl($event)\"\n />\n </label>\n @if (url() && !safeSrc() && !hasFallback()) {\n <p class=\"mk-embed-block__note\">{{ i18n.blockEditor.embedFallbackNote }}</p>\n }\n}\n", styles: [":host{display:block}.mk-embed-block__frame{position:relative;width:100%;overflow:hidden;border-radius:var(--mk-radius-md);background:var(--mk-surface-3)}.mk-embed-block__iframe{position:absolute;inset:0;width:100%;height:100%;border:0}.mk-embed-block__card{display:flex;align-items:center;gap:var(--mk-space-3);padding:var(--mk-space-3) var(--mk-space-4);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface-2);color:var(--mk-text);text-decoration:none}.mk-embed-block__card:hover{border-color:var(--mk-border-strong);background:var(--mk-surface-3)}.mk-embed-block__card-icon{font-size:var(--mk-font-size-xl)}.mk-embed-block__card-body{display:flex;flex-direction:column;min-width:0}.mk-embed-block__card-title{font-weight:var(--mk-font-weight-medium)}.mk-embed-block__card-url{color:var(--mk-text-muted);font-size:var(--mk-font-size-sm);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mk-embed-block__input{display:block;margin-top:var(--mk-space-2)}.mk-embed-block__input input{width:100%;height:var(--mk-control-height-sm);padding:0 var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text);font:inherit}.mk-embed-block__input input:focus-visible,.mk-embed-block__card:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-embed-block__note{margin:var(--mk-space-1) 0 0;color:var(--mk-text-subtle);font-size:var(--mk-font-size-xs)}.mk-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}\n"] }]
1166
+ }], propDecorators: { block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], blockChange: [{ type: i0.Output, args: ["blockChange"] }] } });
1167
+
1168
+ const TONES = ['primary', 'neutral', 'success', 'warning', 'danger', 'info'];
1169
+ /**
1170
+ * Button/CTA block — a themed call-to-action link with configurable label,
1171
+ * href, tone, variant and alignment. Styling mirrors the mk-kit button tokens.
1172
+ */
1173
+ class MkButtonBlock {
1174
+ i18n = inject(MK_I18N);
1175
+ block = input.required(/* @ts-ignore */
1176
+ ...(ngDevMode ? [{ debugName: "block" }] : /* istanbul ignore next */ []));
1177
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1178
+ blockChange = output();
1179
+ tones = TONES;
1180
+ aligns = ['left', 'center', 'right'];
1181
+ variants = ['solid', 'soft', 'outline'];
1182
+ data(key) {
1183
+ return this.block().data[key];
1184
+ }
1185
+ /** Localised captions for the tone / variant / alignment option buttons. */
1186
+ toneLabel(tone) {
1187
+ const s = this.i18n.blockEditor;
1188
+ const map = {
1189
+ primary: s.tonePrimary,
1190
+ neutral: s.toneNeutral,
1191
+ success: s.toneSuccess,
1192
+ warning: s.toneWarning,
1193
+ danger: s.toneDanger,
1194
+ info: s.toneInfo,
1195
+ };
1196
+ return map[tone];
1197
+ }
1198
+ variantLabel(variant) {
1199
+ const s = this.i18n.blockEditor;
1200
+ const map = {
1201
+ solid: s.variantSolid,
1202
+ soft: s.variantSoft,
1203
+ outline: s.variantOutline,
1204
+ };
1205
+ return map[variant] ?? variant;
1206
+ }
1207
+ alignLabel(align) {
1208
+ const s = this.i18n.blockEditor;
1209
+ return { left: s.alignLeft, center: s.alignCenter, right: s.alignRight }[align];
1210
+ }
1211
+ onLabel(event) {
1212
+ this.patch({ label: event.target.value });
1213
+ }
1214
+ onHref(event) {
1215
+ this.patch({ href: event.target.value.trim() });
1216
+ }
1217
+ setTone(tone) {
1218
+ this.patch({ tone });
1219
+ }
1220
+ setVariant(variant) {
1221
+ this.patch({ variant });
1222
+ }
1223
+ setAlign(align) {
1224
+ this.patch({ align });
1225
+ }
1226
+ patch(data) {
1227
+ const b = this.block();
1228
+ this.blockChange.emit({ ...b, data: { ...b.data, ...data } });
1229
+ }
1230
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkButtonBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
1231
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkButtonBlock, isStandalone: true, selector: "mk-button-block", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blockChange: "blockChange" }, ngImport: i0, template: "<div\n class=\"mk-button-block__preview\"\n [class.mk-button-block__preview--center]=\"(data('align') ?? 'left') === 'center'\"\n [class.mk-button-block__preview--right]=\"(data('align') ?? 'left') === 'right'\"\n>\n <a\n class=\"mk-cta\"\n [class.mk-cta--soft]=\"data('variant') === 'soft'\"\n [class.mk-cta--outline]=\"data('variant') === 'outline'\"\n [attr.data-tone]=\"data('tone') ?? 'primary'\"\n [attr.href]=\"readonly() ? (data('href') || null) : null\"\n [attr.target]=\"readonly() ? '_blank' : null\"\n rel=\"noopener noreferrer\"\n data-mk-focus\n >\n {{ data('label') || i18n.blockEditor.blockButton }}\n </a>\n</div>\n\n@if (!readonly()) {\n <div class=\"mk-button-block__settings\">\n <label class=\"mk-button-block__field\">\n <span>{{ i18n.blockEditor.buttonLabel }}</span>\n <input type=\"text\" [value]=\"data('label') ?? ''\" (input)=\"onLabel($event)\" />\n </label>\n <label class=\"mk-button-block__field\">\n <span>{{ i18n.blockEditor.buttonLink }}</span>\n <input type=\"url\" [value]=\"data('href') ?? ''\" placeholder=\"https://\u2026\" (input)=\"onHref($event)\" />\n </label>\n\n <div class=\"mk-button-block__field\">\n <span id=\"mk-cta-tone\">{{ i18n.blockEditor.buttonTone }}</span>\n <div class=\"mk-button-block__row\" role=\"group\" aria-labelledby=\"mk-cta-tone\">\n @for (t of tones; track t) {\n <button\n type=\"button\"\n class=\"mk-button-block__swatch\"\n [attr.data-tone]=\"t\"\n [class.mk-button-block__swatch--active]=\"(data('tone') ?? 'primary') === t\"\n [attr.aria-pressed]=\"(data('tone') ?? 'primary') === t\"\n [attr.aria-label]=\"toneLabel(t)\"\n [title]=\"toneLabel(t)\"\n (click)=\"setTone(t)\"\n ></button>\n }\n </div>\n </div>\n\n <div class=\"mk-button-block__field\">\n <span id=\"mk-cta-variant\">{{ i18n.blockEditor.buttonVariant }}</span>\n <div class=\"mk-button-block__row\" role=\"group\" aria-labelledby=\"mk-cta-variant\">\n @for (v of variants; track v) {\n <button\n type=\"button\"\n class=\"mk-button-block__pill\"\n [class.mk-button-block__pill--active]=\"(data('variant') ?? 'solid') === v\"\n [attr.aria-pressed]=\"(data('variant') ?? 'solid') === v\"\n (click)=\"setVariant(v)\"\n >\n {{ variantLabel(v) }}\n </button>\n }\n </div>\n </div>\n\n <div class=\"mk-button-block__field\">\n <span id=\"mk-cta-align\">{{ i18n.blockEditor.alignment }}</span>\n <div class=\"mk-button-block__row\" role=\"group\" aria-labelledby=\"mk-cta-align\">\n @for (a of aligns; track a) {\n <button\n type=\"button\"\n class=\"mk-button-block__pill\"\n [class.mk-button-block__pill--active]=\"(data('align') ?? 'left') === a\"\n [attr.aria-pressed]=\"(data('align') ?? 'left') === a\"\n (click)=\"setAlign(a)\"\n >\n {{ alignLabel(a) }}\n </button>\n }\n </div>\n </div>\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block}.mk-button-block__preview{text-align:start}.mk-button-block__preview--center{text-align:center}.mk-button-block__preview--right{text-align:right}.mk-cta{--_main: var(--mk-primary);--_hover: var(--mk-primary-hover);--_contrast: var(--mk-primary-contrast);--_subtle: var(--mk-primary-subtle);--_subtle-text: var(--mk-primary-subtle-text);display:inline-flex;align-items:center;justify-content:center;height:var(--mk-control-height-md);padding:0 var(--mk-space-5);border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);background:var(--_main);color:var(--_contrast);font-weight:var(--mk-font-weight-medium);text-decoration:none;cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-cta:hover{background:var(--_hover)}.mk-cta[data-tone=success]{--_main: var(--mk-success);--_hover: var(--mk-success-hover);--_contrast: var(--mk-success-contrast);--_subtle: var(--mk-success-subtle);--_subtle-text: var(--mk-success-subtle-text)}.mk-cta[data-tone=warning]{--_main: var(--mk-warning);--_hover: var(--mk-warning-hover);--_contrast: var(--mk-warning-contrast);--_subtle: var(--mk-warning-subtle);--_subtle-text: var(--mk-warning-subtle-text)}.mk-cta[data-tone=danger]{--_main: var(--mk-danger);--_hover: var(--mk-danger-hover);--_contrast: var(--mk-danger-contrast);--_subtle: var(--mk-danger-subtle);--_subtle-text: var(--mk-danger-subtle-text)}.mk-cta[data-tone=info]{--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text)}.mk-cta[data-tone=neutral]{--_main: var(--mk-surface-inverse);--_hover: var(--mk-surface-inverse);--_contrast: var(--mk-text-inverse);--_subtle: var(--mk-neutral-subtle);--_subtle-text: var(--mk-neutral-subtle-text)}.mk-cta--soft{background:var(--_subtle);color:var(--_subtle-text)}.mk-cta--soft:hover{background-image:linear-gradient(var(--mk-hover-overlay),var(--mk-hover-overlay))}.mk-cta--outline{background:transparent;color:var(--_subtle-text);border-color:color-mix(in srgb,var(--_main) 42%,var(--mk-border-strong))}.mk-cta--outline:hover{background:var(--_subtle)}.mk-cta:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-button-block__settings{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--mk-space-3);margin-top:var(--mk-space-3);padding:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border-subtle);border-radius:var(--mk-radius-md);background:var(--mk-surface-2)}.mk-button-block__field{display:flex;flex-direction:column;gap:var(--mk-space-1);color:var(--mk-text-muted);font-size:var(--mk-font-size-xs)}.mk-button-block__field input{height:var(--mk-control-height-sm);padding:0 var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text);font:inherit}.mk-button-block__row{display:flex;flex-wrap:wrap;gap:var(--mk-space-1)}.mk-button-block__swatch{width:24px;height:24px;border:2px solid transparent;border-radius:var(--mk-radius-circle);background:var(--mk-primary);cursor:pointer}.mk-button-block__swatch[data-tone=success]{background:var(--mk-success)}.mk-button-block__swatch[data-tone=warning]{background:var(--mk-warning)}.mk-button-block__swatch[data-tone=danger]{background:var(--mk-danger)}.mk-button-block__swatch[data-tone=info]{background:var(--mk-info)}.mk-button-block__swatch[data-tone=neutral]{background:var(--mk-surface-inverse)}.mk-button-block__swatch--active{border-color:var(--mk-text)}.mk-button-block__pill{height:var(--mk-control-height-sm);padding:0 var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-xs);text-transform:capitalize;cursor:pointer}.mk-button-block__pill--active{background:var(--mk-primary);border-color:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-button-block__swatch:focus-visible,.mk-button-block__pill:focus-visible,.mk-button-block__field input:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1232
+ }
1233
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkButtonBlock, decorators: [{
1234
+ type: Component,
1235
+ args: [{ selector: 'mk-button-block', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"mk-button-block__preview\"\n [class.mk-button-block__preview--center]=\"(data('align') ?? 'left') === 'center'\"\n [class.mk-button-block__preview--right]=\"(data('align') ?? 'left') === 'right'\"\n>\n <a\n class=\"mk-cta\"\n [class.mk-cta--soft]=\"data('variant') === 'soft'\"\n [class.mk-cta--outline]=\"data('variant') === 'outline'\"\n [attr.data-tone]=\"data('tone') ?? 'primary'\"\n [attr.href]=\"readonly() ? (data('href') || null) : null\"\n [attr.target]=\"readonly() ? '_blank' : null\"\n rel=\"noopener noreferrer\"\n data-mk-focus\n >\n {{ data('label') || i18n.blockEditor.blockButton }}\n </a>\n</div>\n\n@if (!readonly()) {\n <div class=\"mk-button-block__settings\">\n <label class=\"mk-button-block__field\">\n <span>{{ i18n.blockEditor.buttonLabel }}</span>\n <input type=\"text\" [value]=\"data('label') ?? ''\" (input)=\"onLabel($event)\" />\n </label>\n <label class=\"mk-button-block__field\">\n <span>{{ i18n.blockEditor.buttonLink }}</span>\n <input type=\"url\" [value]=\"data('href') ?? ''\" placeholder=\"https://\u2026\" (input)=\"onHref($event)\" />\n </label>\n\n <div class=\"mk-button-block__field\">\n <span id=\"mk-cta-tone\">{{ i18n.blockEditor.buttonTone }}</span>\n <div class=\"mk-button-block__row\" role=\"group\" aria-labelledby=\"mk-cta-tone\">\n @for (t of tones; track t) {\n <button\n type=\"button\"\n class=\"mk-button-block__swatch\"\n [attr.data-tone]=\"t\"\n [class.mk-button-block__swatch--active]=\"(data('tone') ?? 'primary') === t\"\n [attr.aria-pressed]=\"(data('tone') ?? 'primary') === t\"\n [attr.aria-label]=\"toneLabel(t)\"\n [title]=\"toneLabel(t)\"\n (click)=\"setTone(t)\"\n ></button>\n }\n </div>\n </div>\n\n <div class=\"mk-button-block__field\">\n <span id=\"mk-cta-variant\">{{ i18n.blockEditor.buttonVariant }}</span>\n <div class=\"mk-button-block__row\" role=\"group\" aria-labelledby=\"mk-cta-variant\">\n @for (v of variants; track v) {\n <button\n type=\"button\"\n class=\"mk-button-block__pill\"\n [class.mk-button-block__pill--active]=\"(data('variant') ?? 'solid') === v\"\n [attr.aria-pressed]=\"(data('variant') ?? 'solid') === v\"\n (click)=\"setVariant(v)\"\n >\n {{ variantLabel(v) }}\n </button>\n }\n </div>\n </div>\n\n <div class=\"mk-button-block__field\">\n <span id=\"mk-cta-align\">{{ i18n.blockEditor.alignment }}</span>\n <div class=\"mk-button-block__row\" role=\"group\" aria-labelledby=\"mk-cta-align\">\n @for (a of aligns; track a) {\n <button\n type=\"button\"\n class=\"mk-button-block__pill\"\n [class.mk-button-block__pill--active]=\"(data('align') ?? 'left') === a\"\n [attr.aria-pressed]=\"(data('align') ?? 'left') === a\"\n (click)=\"setAlign(a)\"\n >\n {{ alignLabel(a) }}\n </button>\n }\n </div>\n </div>\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block}.mk-button-block__preview{text-align:start}.mk-button-block__preview--center{text-align:center}.mk-button-block__preview--right{text-align:right}.mk-cta{--_main: var(--mk-primary);--_hover: var(--mk-primary-hover);--_contrast: var(--mk-primary-contrast);--_subtle: var(--mk-primary-subtle);--_subtle-text: var(--mk-primary-subtle-text);display:inline-flex;align-items:center;justify-content:center;height:var(--mk-control-height-md);padding:0 var(--mk-space-5);border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);background:var(--_main);color:var(--_contrast);font-weight:var(--mk-font-weight-medium);text-decoration:none;cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-cta:hover{background:var(--_hover)}.mk-cta[data-tone=success]{--_main: var(--mk-success);--_hover: var(--mk-success-hover);--_contrast: var(--mk-success-contrast);--_subtle: var(--mk-success-subtle);--_subtle-text: var(--mk-success-subtle-text)}.mk-cta[data-tone=warning]{--_main: var(--mk-warning);--_hover: var(--mk-warning-hover);--_contrast: var(--mk-warning-contrast);--_subtle: var(--mk-warning-subtle);--_subtle-text: var(--mk-warning-subtle-text)}.mk-cta[data-tone=danger]{--_main: var(--mk-danger);--_hover: var(--mk-danger-hover);--_contrast: var(--mk-danger-contrast);--_subtle: var(--mk-danger-subtle);--_subtle-text: var(--mk-danger-subtle-text)}.mk-cta[data-tone=info]{--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text)}.mk-cta[data-tone=neutral]{--_main: var(--mk-surface-inverse);--_hover: var(--mk-surface-inverse);--_contrast: var(--mk-text-inverse);--_subtle: var(--mk-neutral-subtle);--_subtle-text: var(--mk-neutral-subtle-text)}.mk-cta--soft{background:var(--_subtle);color:var(--_subtle-text)}.mk-cta--soft:hover{background-image:linear-gradient(var(--mk-hover-overlay),var(--mk-hover-overlay))}.mk-cta--outline{background:transparent;color:var(--_subtle-text);border-color:color-mix(in srgb,var(--_main) 42%,var(--mk-border-strong))}.mk-cta--outline:hover{background:var(--_subtle)}.mk-cta:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-button-block__settings{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--mk-space-3);margin-top:var(--mk-space-3);padding:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border-subtle);border-radius:var(--mk-radius-md);background:var(--mk-surface-2)}.mk-button-block__field{display:flex;flex-direction:column;gap:var(--mk-space-1);color:var(--mk-text-muted);font-size:var(--mk-font-size-xs)}.mk-button-block__field input{height:var(--mk-control-height-sm);padding:0 var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text);font:inherit}.mk-button-block__row{display:flex;flex-wrap:wrap;gap:var(--mk-space-1)}.mk-button-block__swatch{width:24px;height:24px;border:2px solid transparent;border-radius:var(--mk-radius-circle);background:var(--mk-primary);cursor:pointer}.mk-button-block__swatch[data-tone=success]{background:var(--mk-success)}.mk-button-block__swatch[data-tone=warning]{background:var(--mk-warning)}.mk-button-block__swatch[data-tone=danger]{background:var(--mk-danger)}.mk-button-block__swatch[data-tone=info]{background:var(--mk-info)}.mk-button-block__swatch[data-tone=neutral]{background:var(--mk-surface-inverse)}.mk-button-block__swatch--active{border-color:var(--mk-text)}.mk-button-block__pill{height:var(--mk-control-height-sm);padding:0 var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-xs);text-transform:capitalize;cursor:pointer}.mk-button-block__pill--active{background:var(--mk-primary);border-color:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-button-block__swatch:focus-visible,.mk-button-block__pill:focus-visible,.mk-button-block__field input:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"] }]
1236
+ }], propDecorators: { block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], blockChange: [{ type: i0.Output, args: ["blockChange"] }] } });
1237
+
1238
+ /**
1239
+ * Shares ONE capture-phase document `pointerdown` listener between every
1240
+ * mounted {@link MkBlockInserter} (a block list renders an inserter per block,
1241
+ * so per-instance listeners made document dispatch O(blocks)). The listener is
1242
+ * attached lazily with the first registration and detached when the last
1243
+ * callback unregisters. No-ops on the server (SSR).
1244
+ */
1245
+ class MkDocumentPointerdownRegistry {
1246
+ document = inject(DOCUMENT);
1247
+ callbacks = new Set();
1248
+ listener = (event) => {
1249
+ // Copy so a callback closing (and unregistering) mid-dispatch is safe.
1250
+ for (const callback of [...this.callbacks])
1251
+ callback(event);
1252
+ };
1253
+ /** Registers a callback; returns an unregister function. */
1254
+ register(callback) {
1255
+ if (typeof window === 'undefined')
1256
+ return () => { };
1257
+ if (this.callbacks.size === 0) {
1258
+ this.document.addEventListener('pointerdown', this.listener, true);
1259
+ }
1260
+ this.callbacks.add(callback);
1261
+ return () => {
1262
+ if (!this.callbacks.delete(callback))
1263
+ return;
1264
+ if (this.callbacks.size === 0) {
1265
+ this.document.removeEventListener('pointerdown', this.listener, true);
1266
+ }
1267
+ };
1268
+ }
1269
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDocumentPointerdownRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1270
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDocumentPointerdownRegistry, providedIn: 'root' });
1271
+ }
1272
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDocumentPointerdownRegistry, decorators: [{
1273
+ type: Injectable,
1274
+ args: [{ providedIn: 'root' }]
1275
+ }] });
1276
+ /**
1277
+ * Block inserter — a "+ Add block" control that opens a filterable, keyboard
1278
+ * accessible palette of the configured blocks, grouped by `group`. Renders
1279
+ * either as a full button or a slim between-blocks divider trigger.
1280
+ *
1281
+ * Keyboard: type to filter, ArrowUp/Down to move, Enter to insert, Esc to
1282
+ * close. Implements the combobox → listbox pattern.
1283
+ */
1284
+ class MkBlockInserter {
1285
+ host = inject(ElementRef);
1286
+ searchRef = viewChild('search', /* @ts-ignore */
1287
+ ...(ngDevMode ? [{ debugName: "searchRef" }] : /* istanbul ignore next */ []));
1288
+ i18n = inject(MK_I18N);
1289
+ focusTimer = null;
1290
+ constructor() {
1291
+ // One shared document listener for all inserters (instead of N host
1292
+ // `(document:pointerdown)` bindings — one per rendered inserter).
1293
+ const unregister = inject(MkDocumentPointerdownRegistry).register((event) => this.onDocumentPointerdown(event));
1294
+ inject(DestroyRef).onDestroy(unregister);
1295
+ }
1296
+ ngOnDestroy() {
1297
+ if (this.focusTimer !== null) {
1298
+ clearTimeout(this.focusTimer);
1299
+ this.focusTimer = null;
1300
+ }
1301
+ }
1302
+ /** The blocks to offer. */
1303
+ definitions = input([], /* @ts-ignore */
1304
+ ...(ngDevMode ? [{ debugName: "definitions" }] : /* istanbul ignore next */ []));
1305
+ /** Trigger presentation: `button` (default) or `slim` between-blocks rule. */
1306
+ variant = input('button', /* @ts-ignore */
1307
+ ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
1308
+ /** Trigger label. */
1309
+ label = input(this.i18n.blockEditor.addBlock, /* @ts-ignore */
1310
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1311
+ /** Disable the trigger. */
1312
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1313
+ /** When true, only blocks allowed inside columns are shown. */
1314
+ columnsOnly = input(false, { ...(ngDevMode ? { debugName: "columnsOnly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1315
+ /** Emitted with the chosen definition. */
1316
+ pick = output();
1317
+ open = signal(false, /* @ts-ignore */
1318
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
1319
+ query = signal('', /* @ts-ignore */
1320
+ ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
1321
+ activeIndex = signal(0, /* @ts-ignore */
1322
+ ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
1323
+ panelId = mkUniqueId('mk-inserter-panel');
1324
+ listId = mkUniqueId('mk-inserter-list');
1325
+ available = computed(() => this.definitions().filter((d) => (this.columnsOnly() ? d.allowedInColumns !== false : true)), /* @ts-ignore */
1326
+ ...(ngDevMode ? [{ debugName: "available" }] : /* istanbul ignore next */ []));
1327
+ /** Flat, filtered list in display order (used for keyboard navigation). */
1328
+ filtered = computed(() => {
1329
+ const q = this.query().trim().toLowerCase();
1330
+ if (!q)
1331
+ return this.available();
1332
+ return this.available().filter((d) => {
1333
+ const hay = [d.label, d.type, d.group ?? '', ...(d.keywords ?? [])].join(' ').toLowerCase();
1334
+ return hay.includes(q);
1335
+ });
1336
+ }, /* @ts-ignore */
1337
+ ...(ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []));
1338
+ /**
1339
+ * Filtered list grouped by `group` for rendering, with each option carrying
1340
+ * its flat index (single pass — the template previously resolved each
1341
+ * option's index with three `indexOf` scans, O(n²) per render).
1342
+ */
1343
+ groups = computed(() => {
1344
+ const map = new Map();
1345
+ this.filtered().forEach((def, index) => {
1346
+ const key = def.group ?? this.i18n.blockEditor.blocks;
1347
+ (map.get(key) ?? map.set(key, []).get(key)).push({ def, index });
1348
+ });
1349
+ return [...map.entries()].map(([name, items]) => ({ name, items }));
1350
+ }, /* @ts-ignore */
1351
+ ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
1352
+ optionId(index) {
1353
+ return `${this.listId}-opt-${index}`;
1354
+ }
1355
+ toggle() {
1356
+ if (this.disabled())
1357
+ return;
1358
+ this.open() ? this.close() : this.openPanel();
1359
+ }
1360
+ openPanel() {
1361
+ this.open.set(true);
1362
+ this.query.set('');
1363
+ this.activeIndex.set(0);
1364
+ if (this.focusTimer !== null)
1365
+ clearTimeout(this.focusTimer);
1366
+ this.focusTimer = setTimeout(() => {
1367
+ this.focusTimer = null;
1368
+ this.searchRef()?.nativeElement.focus();
1369
+ });
1370
+ }
1371
+ close() {
1372
+ if (!this.open())
1373
+ return;
1374
+ this.open.set(false);
1375
+ }
1376
+ onQuery(event) {
1377
+ this.query.set(event.target.value);
1378
+ this.activeIndex.set(0);
1379
+ }
1380
+ onKeydown(event) {
1381
+ const list = this.filtered();
1382
+ switch (event.key) {
1383
+ case 'ArrowDown':
1384
+ event.preventDefault();
1385
+ this.activeIndex.update((i) => (list.length ? (i + 1) % list.length : 0));
1386
+ break;
1387
+ case 'ArrowUp':
1388
+ event.preventDefault();
1389
+ this.activeIndex.update((i) => (list.length ? (i - 1 + list.length) % list.length : 0));
1390
+ break;
1391
+ case 'Enter':
1392
+ event.preventDefault();
1393
+ if (list[this.activeIndex()])
1394
+ this.choose(list[this.activeIndex()]);
1395
+ break;
1396
+ case 'Escape':
1397
+ event.preventDefault();
1398
+ this.close();
1399
+ break;
1400
+ }
1401
+ }
1402
+ choose(def) {
1403
+ this.pick.emit(def);
1404
+ this.close();
1405
+ }
1406
+ onDocumentPointerdown(event) {
1407
+ if (!this.open())
1408
+ return;
1409
+ if (!this.host.nativeElement.contains(event.target))
1410
+ this.close();
1411
+ }
1412
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockInserter, deps: [], target: i0.ɵɵFactoryTarget.Component });
1413
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkBlockInserter, isStandalone: true, selector: "mk-block-inserter", inputs: { definitions: { classPropertyName: "definitions", publicName: "definitions", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, columnsOnly: { classPropertyName: "columnsOnly", publicName: "columnsOnly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pick: "pick" }, host: { properties: { "class.mk-block-inserter--slim": "variant() === 'slim'" }, classAttribute: "mk-block-inserter" }, viewQueries: [{ propertyName: "searchRef", first: true, predicate: ["search"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (variant() === 'slim') {\n <button\n type=\"button\"\n class=\"mk-block-inserter__slim-trigger\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"i18n.blockEditor.insertBlockHere\"\n (click)=\"toggle()\"\n >\n <span class=\"mk-block-inserter__plus\" aria-hidden=\"true\">\uFF0B</span>\n </button>\n} @else {\n <button\n type=\"button\"\n class=\"mk-block-inserter__trigger\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [disabled]=\"disabled()\"\n (click)=\"toggle()\"\n >\n <span class=\"mk-block-inserter__plus\" aria-hidden=\"true\">\uFF0B</span>\n {{ label() }}\n </button>\n}\n\n@if (open()) {\n <div class=\"mk-block-inserter__panel\" [id]=\"panelId\" role=\"dialog\" [attr.aria-label]=\"i18n.blockEditor.blockInserter\">\n <input\n #search\n type=\"text\"\n class=\"mk-block-inserter__search\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"true\"\n [attr.aria-controls]=\"listId\"\n [attr.aria-activedescendant]=\"filtered().length ? optionId(activeIndex()) : null\"\n [placeholder]=\"i18n.blockEditor.searchBlocks\"\n [value]=\"query()\"\n (input)=\"onQuery($event)\"\n (keydown)=\"onKeydown($event)\"\n />\n\n <div class=\"mk-block-inserter__list\" [id]=\"listId\" role=\"listbox\" [attr.aria-label]=\"i18n.blockEditor.blocks\">\n @for (group of groups(); track group.name) {\n <div class=\"mk-block-inserter__group\" role=\"group\" [attr.aria-label]=\"group.name\">\n <div class=\"mk-block-inserter__group-label\">{{ group.name }}</div>\n @for (item of group.items; track item.def.type) {\n <button\n type=\"button\"\n class=\"mk-block-inserter__option\"\n [class.mk-block-inserter__option--active]=\"item.index === activeIndex()\"\n [id]=\"optionId(item.index)\"\n role=\"option\"\n [attr.aria-selected]=\"item.index === activeIndex()\"\n (mouseenter)=\"activeIndex.set(item.index)\"\n (click)=\"choose(item.def)\"\n >\n <span class=\"mk-block-inserter__icon\" aria-hidden=\"true\">{{ item.def.icon ?? '\u25A2' }}</span>\n <span class=\"mk-block-inserter__text\">\n <span class=\"mk-block-inserter__name\">{{ item.def.label }}</span>\n @if (item.def.description) {\n <span class=\"mk-block-inserter__desc\">{{ item.def.description }}</span>\n }\n </span>\n </button>\n }\n </div>\n }\n @if (!filtered().length) {\n <p class=\"mk-block-inserter__empty\">{{ i18n.blockEditor.noBlocksMatch(query()) }}</p>\n }\n </div>\n </div>\n}\n", styles: [":host{position:relative;display:block}.mk-block-inserter__trigger{display:inline-flex;align-items:center;gap:var(--mk-space-2);height:var(--mk-control-height-md);padding:0 var(--mk-space-4);border:var(--mk-border-width) dashed var(--mk-border-strong);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text-muted);font:inherit;font-weight:var(--mk-font-weight-medium);cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard),border-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block-inserter__trigger:hover{color:var(--mk-primary);border-color:var(--mk-primary)}.mk-block-inserter--slim{height:0;overflow:visible}.mk-block-inserter__slim-trigger{display:flex;align-items:center;justify-content:center;width:100%;height:var(--mk-space-4);margin:calc(var(--mk-space-1) * -1) 0;border:none;background:transparent;color:var(--mk-primary);cursor:pointer;opacity:0;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block-inserter__slim-trigger:before{content:\"\";position:absolute;left:0;right:0;height:var(--mk-border-width-strong);background:var(--mk-primary);border-radius:var(--mk-radius-pill)}.mk-block-inserter--slim:hover .mk-block-inserter__slim-trigger,.mk-block-inserter__slim-trigger:focus-visible{opacity:1}.mk-block-inserter__slim-trigger .mk-block-inserter__plus{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:var(--mk-radius-circle);background:var(--mk-primary);color:var(--mk-primary-contrast);font-size:var(--mk-font-size-xs)}.mk-block-inserter__trigger:focus-visible,.mk-block-inserter__slim-trigger:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-block-inserter__panel{position:absolute;z-index:var(--mk-z-menu);top:calc(100% + var(--mk-space-2));inset-inline-start:0;width:min(320px,90vw);max-height:380px;display:flex;flex-direction:column;border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);background:var(--mk-surface);box-shadow:var(--mk-shadow-lg);overflow:hidden}.mk-block-inserter__search{margin:var(--mk-space-2);height:var(--mk-control-height-sm);padding:0 var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface-2);color:var(--mk-text);font:inherit}.mk-block-inserter__search:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-block-inserter__list{overflow-y:auto;padding:0 var(--mk-space-1) var(--mk-space-2)}.mk-block-inserter__group-label{padding:var(--mk-space-2) var(--mk-space-2) var(--mk-space-1);color:var(--mk-text-subtle);font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-semibold);text-transform:uppercase;letter-spacing:.04em}.mk-block-inserter__option{display:flex;align-items:center;gap:var(--mk-space-3);width:100%;padding:var(--mk-space-2);border:none;border-radius:var(--mk-radius-md);background:transparent;color:var(--mk-text);font:inherit;text-align:start;cursor:pointer}.mk-block-inserter__option--active{background:var(--mk-selected-bg);color:var(--mk-selected-text)}.mk-block-inserter__icon{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;flex:none;border-radius:var(--mk-radius-sm);background:var(--mk-surface-3);font-size:var(--mk-font-size-md)}.mk-block-inserter__text{display:flex;flex-direction:column;min-width:0}.mk-block-inserter__name{font-weight:var(--mk-font-weight-medium)}.mk-block-inserter__desc{color:var(--mk-text-subtle);font-size:var(--mk-font-size-xs)}.mk-block-inserter__option--active .mk-block-inserter__desc{color:inherit}.mk-block-inserter__empty{padding:var(--mk-space-4);margin:0;color:var(--mk-text-muted);font-size:var(--mk-font-size-sm);text-align:center}@media(prefers-reduced-motion:reduce){.mk-block-inserter__slim-trigger{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1414
+ }
1415
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockInserter, decorators: [{
1416
+ type: Component,
1417
+ args: [{ selector: 'mk-block-inserter', changeDetection: ChangeDetectionStrategy.OnPush, host: {
1418
+ class: 'mk-block-inserter',
1419
+ '[class.mk-block-inserter--slim]': "variant() === 'slim'",
1420
+ }, template: "@if (variant() === 'slim') {\n <button\n type=\"button\"\n class=\"mk-block-inserter__slim-trigger\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"i18n.blockEditor.insertBlockHere\"\n (click)=\"toggle()\"\n >\n <span class=\"mk-block-inserter__plus\" aria-hidden=\"true\">\uFF0B</span>\n </button>\n} @else {\n <button\n type=\"button\"\n class=\"mk-block-inserter__trigger\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [disabled]=\"disabled()\"\n (click)=\"toggle()\"\n >\n <span class=\"mk-block-inserter__plus\" aria-hidden=\"true\">\uFF0B</span>\n {{ label() }}\n </button>\n}\n\n@if (open()) {\n <div class=\"mk-block-inserter__panel\" [id]=\"panelId\" role=\"dialog\" [attr.aria-label]=\"i18n.blockEditor.blockInserter\">\n <input\n #search\n type=\"text\"\n class=\"mk-block-inserter__search\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"true\"\n [attr.aria-controls]=\"listId\"\n [attr.aria-activedescendant]=\"filtered().length ? optionId(activeIndex()) : null\"\n [placeholder]=\"i18n.blockEditor.searchBlocks\"\n [value]=\"query()\"\n (input)=\"onQuery($event)\"\n (keydown)=\"onKeydown($event)\"\n />\n\n <div class=\"mk-block-inserter__list\" [id]=\"listId\" role=\"listbox\" [attr.aria-label]=\"i18n.blockEditor.blocks\">\n @for (group of groups(); track group.name) {\n <div class=\"mk-block-inserter__group\" role=\"group\" [attr.aria-label]=\"group.name\">\n <div class=\"mk-block-inserter__group-label\">{{ group.name }}</div>\n @for (item of group.items; track item.def.type) {\n <button\n type=\"button\"\n class=\"mk-block-inserter__option\"\n [class.mk-block-inserter__option--active]=\"item.index === activeIndex()\"\n [id]=\"optionId(item.index)\"\n role=\"option\"\n [attr.aria-selected]=\"item.index === activeIndex()\"\n (mouseenter)=\"activeIndex.set(item.index)\"\n (click)=\"choose(item.def)\"\n >\n <span class=\"mk-block-inserter__icon\" aria-hidden=\"true\">{{ item.def.icon ?? '\u25A2' }}</span>\n <span class=\"mk-block-inserter__text\">\n <span class=\"mk-block-inserter__name\">{{ item.def.label }}</span>\n @if (item.def.description) {\n <span class=\"mk-block-inserter__desc\">{{ item.def.description }}</span>\n }\n </span>\n </button>\n }\n </div>\n }\n @if (!filtered().length) {\n <p class=\"mk-block-inserter__empty\">{{ i18n.blockEditor.noBlocksMatch(query()) }}</p>\n }\n </div>\n </div>\n}\n", styles: [":host{position:relative;display:block}.mk-block-inserter__trigger{display:inline-flex;align-items:center;gap:var(--mk-space-2);height:var(--mk-control-height-md);padding:0 var(--mk-space-4);border:var(--mk-border-width) dashed var(--mk-border-strong);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text-muted);font:inherit;font-weight:var(--mk-font-weight-medium);cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard),border-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block-inserter__trigger:hover{color:var(--mk-primary);border-color:var(--mk-primary)}.mk-block-inserter--slim{height:0;overflow:visible}.mk-block-inserter__slim-trigger{display:flex;align-items:center;justify-content:center;width:100%;height:var(--mk-space-4);margin:calc(var(--mk-space-1) * -1) 0;border:none;background:transparent;color:var(--mk-primary);cursor:pointer;opacity:0;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block-inserter__slim-trigger:before{content:\"\";position:absolute;left:0;right:0;height:var(--mk-border-width-strong);background:var(--mk-primary);border-radius:var(--mk-radius-pill)}.mk-block-inserter--slim:hover .mk-block-inserter__slim-trigger,.mk-block-inserter__slim-trigger:focus-visible{opacity:1}.mk-block-inserter__slim-trigger .mk-block-inserter__plus{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:var(--mk-radius-circle);background:var(--mk-primary);color:var(--mk-primary-contrast);font-size:var(--mk-font-size-xs)}.mk-block-inserter__trigger:focus-visible,.mk-block-inserter__slim-trigger:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-block-inserter__panel{position:absolute;z-index:var(--mk-z-menu);top:calc(100% + var(--mk-space-2));inset-inline-start:0;width:min(320px,90vw);max-height:380px;display:flex;flex-direction:column;border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);background:var(--mk-surface);box-shadow:var(--mk-shadow-lg);overflow:hidden}.mk-block-inserter__search{margin:var(--mk-space-2);height:var(--mk-control-height-sm);padding:0 var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface-2);color:var(--mk-text);font:inherit}.mk-block-inserter__search:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-block-inserter__list{overflow-y:auto;padding:0 var(--mk-space-1) var(--mk-space-2)}.mk-block-inserter__group-label{padding:var(--mk-space-2) var(--mk-space-2) var(--mk-space-1);color:var(--mk-text-subtle);font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-semibold);text-transform:uppercase;letter-spacing:.04em}.mk-block-inserter__option{display:flex;align-items:center;gap:var(--mk-space-3);width:100%;padding:var(--mk-space-2);border:none;border-radius:var(--mk-radius-md);background:transparent;color:var(--mk-text);font:inherit;text-align:start;cursor:pointer}.mk-block-inserter__option--active{background:var(--mk-selected-bg);color:var(--mk-selected-text)}.mk-block-inserter__icon{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;flex:none;border-radius:var(--mk-radius-sm);background:var(--mk-surface-3);font-size:var(--mk-font-size-md)}.mk-block-inserter__text{display:flex;flex-direction:column;min-width:0}.mk-block-inserter__name{font-weight:var(--mk-font-weight-medium)}.mk-block-inserter__desc{color:var(--mk-text-subtle);font-size:var(--mk-font-size-xs)}.mk-block-inserter__option--active .mk-block-inserter__desc{color:inherit}.mk-block-inserter__empty{padding:var(--mk-space-4);margin:0;color:var(--mk-text-muted);font-size:var(--mk-font-size-sm);text-align:center}@media(prefers-reduced-motion:reduce){.mk-block-inserter__slim-trigger{transition:none}}\n"] }]
1421
+ }], ctorParameters: () => [], propDecorators: { searchRef: [{ type: i0.ViewChild, args: ['search', { isSignal: true }] }], definitions: [{ type: i0.Input, args: [{ isSignal: true, alias: "definitions", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], columnsOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnsOnly", required: false }] }], pick: [{ type: i0.Output, args: ["pick"] }] } });
1422
+
1423
+ /**
1424
+ * Column ratio presets offered per column count. An empty label means "equal
1425
+ * widths" and is rendered with the localised `ratioEqual` caption.
1426
+ */
1427
+ const RATIO_PRESETS = {
1428
+ 2: [
1429
+ { value: '50-50', label: '50 / 50' },
1430
+ { value: '66-33', label: '66 / 33' },
1431
+ { value: '33-66', label: '33 / 66' },
1432
+ { value: '75-25', label: '75 / 25' },
1433
+ { value: '25-75', label: '25 / 75' },
1434
+ ],
1435
+ 3: [
1436
+ { value: '33-33-33', label: '' },
1437
+ { value: '50-25-25', label: '50 / 25 / 25' },
1438
+ { value: '25-50-25', label: '25 / 50 / 25' },
1439
+ ],
1440
+ 4: [{ value: '25-25-25-25', label: '' }],
1441
+ };
1442
+ /**
1443
+ * Recursive block list — renders an ordered array of blocks, each wrapped with
1444
+ * move/duplicate/delete/transform chrome and the type-specific editor, with
1445
+ * inserters between. Layout (columns) blocks recurse into nested
1446
+ * `mk-block-list`s. Owns all structural operations for its own array and emits
1447
+ * the new array via `blocksChange`; nested lists bubble their changes upward.
1448
+ */
1449
+ class MkBlockList {
1450
+ ctx = inject(MkBlockEditorContext);
1451
+ i18n = inject(MK_I18N);
1452
+ /** The blocks to render. */
1453
+ blocks = input.required(/* @ts-ignore */
1454
+ ...(ngDevMode ? [{ debugName: "blocks" }] : /* istanbul ignore next */ []));
1455
+ /** Nesting depth (0 = top level). Used to forbid columns-in-columns. */
1456
+ depth = input(0, /* @ts-ignore */
1457
+ ...(ngDevMode ? [{ debugName: "depth" }] : /* istanbul ignore next */ []));
1458
+ /** Restrict the inserter to column-safe blocks. */
1459
+ inColumn = input(false, { ...(ngDevMode ? { debugName: "inColumn" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1460
+ /** Emitted with the full, updated array on any structural or content change. */
1461
+ blocksChange = output();
1462
+ /** Which block's toolbar menu is open (by index), or -1. */
1463
+ menuOpen = signal(-1, /* @ts-ignore */
1464
+ ...(ngDevMode ? [{ debugName: "menuOpen" }] : /* istanbul ignore next */ []));
1465
+ ratioPresets = RATIO_PRESETS;
1466
+ definition(type) {
1467
+ return this.ctx.definitionFor(type);
1468
+ }
1469
+ // --- content updates ------------------------------------------------------
1470
+ update(index, block) {
1471
+ const next = [...this.blocks()];
1472
+ next[index] = block;
1473
+ this.blocksChange.emit(next);
1474
+ }
1475
+ onColumnChange(blockIndex, colIndex, children) {
1476
+ const block = this.blocks()[blockIndex];
1477
+ const columns = [...(block.children ?? [])];
1478
+ columns[colIndex] = { ...columns[colIndex], children };
1479
+ this.update(blockIndex, { ...block, children: columns });
1480
+ }
1481
+ // --- structural ops -------------------------------------------------------
1482
+ insertAt(index, def) {
1483
+ const block = def.create();
1484
+ const next = [...this.blocks()];
1485
+ next.splice(index, 0, block);
1486
+ this.blocksChange.emit(next);
1487
+ this.ctx.announce(this.i18n.blockEditor.blockAdded(def.label));
1488
+ this.ctx.focusBlock(block.id, 'start');
1489
+ }
1490
+ duplicate(index) {
1491
+ const clone = mkCloneBlock(this.blocks()[index]);
1492
+ const next = [...this.blocks()];
1493
+ next.splice(index + 1, 0, clone);
1494
+ this.blocksChange.emit(next);
1495
+ this.menuOpen.set(-1);
1496
+ this.ctx.announce(this.i18n.blockEditor.blockDuplicated);
1497
+ }
1498
+ remove(index) {
1499
+ const removed = this.blocks()[index];
1500
+ const next = this.blocks().filter((_, i) => i !== index);
1501
+ this.blocksChange.emit(next);
1502
+ this.menuOpen.set(-1);
1503
+ this.ctx.announce(this.i18n.blockEditor.blockDeleted(this.ctx.labelFor(removed.type)));
1504
+ const focusTarget = next[index - 1] ?? next[index];
1505
+ if (focusTarget)
1506
+ this.ctx.focusBlock(focusTarget.id, 'end');
1507
+ }
1508
+ move(index, delta) {
1509
+ const target = index + delta;
1510
+ if (target < 0 || target >= this.blocks().length)
1511
+ return;
1512
+ const next = [...this.blocks()];
1513
+ [next[index], next[target]] = [next[target], next[index]];
1514
+ this.blocksChange.emit(next);
1515
+ this.ctx.announce(delta < 0 ? this.i18n.blockEditor.blockMovedUp : this.i18n.blockEditor.blockMovedDown);
1516
+ }
1517
+ transform(index, type) {
1518
+ const def = this.definition(type);
1519
+ if (!def)
1520
+ return;
1521
+ const source = this.blocks()[index];
1522
+ const created = def.create();
1523
+ // Carry rich-text HTML across text-type transforms where it makes sense.
1524
+ const carriesHtml = 'html' in created.data && typeof source.data['html'] === 'string';
1525
+ const data = carriesHtml ? { ...created.data, html: source.data['html'] } : created.data;
1526
+ const next = [...this.blocks()];
1527
+ next[index] = { ...created, id: source.id, data };
1528
+ this.blocksChange.emit(next);
1529
+ this.menuOpen.set(-1);
1530
+ this.ctx.announce(this.i18n.blockEditor.turnedInto(def.label));
1531
+ }
1532
+ // --- text-block keyboard intents -----------------------------------------
1533
+ onSplit(index, split) {
1534
+ const current = this.blocks()[index];
1535
+ const paragraphDef = this.definition('paragraph');
1536
+ const created = paragraphDef ? paragraphDef.create() : this.fallbackParagraph();
1537
+ const newBlock = { ...created, data: { ...created.data, html: split.after } };
1538
+ const next = [...this.blocks()];
1539
+ next[index] = { ...current, data: { ...current.data, html: split.before } };
1540
+ next.splice(index + 1, 0, newBlock);
1541
+ this.blocksChange.emit(next);
1542
+ this.ctx.focusBlock(newBlock.id, 'start');
1543
+ }
1544
+ onMergeBack(index) {
1545
+ if (index <= 0)
1546
+ return;
1547
+ const prev = this.blocks()[index - 1];
1548
+ this.remove(index);
1549
+ this.ctx.focusBlock(prev.id, 'end');
1550
+ }
1551
+ onArrowOut(index, dir) {
1552
+ const target = dir === 'up' ? index - 1 : index + 1;
1553
+ const block = this.blocks()[target];
1554
+ if (block)
1555
+ this.ctx.focusBlock(block.id, dir === 'up' ? 'end' : 'start');
1556
+ }
1557
+ // --- columns helpers ------------------------------------------------------
1558
+ setColumnCount(index, count) {
1559
+ const block = this.blocks()[index];
1560
+ const current = [...(block.children ?? [])];
1561
+ while (current.length < count)
1562
+ current.push({ id: mkBlockId('column'), type: 'column', data: {}, children: [] });
1563
+ current.length = count;
1564
+ const presets = RATIO_PRESETS[count] ?? RATIO_PRESETS[2];
1565
+ this.update(index, {
1566
+ ...block,
1567
+ data: { ...block.data, count, ratio: presets[0].value },
1568
+ children: current,
1569
+ });
1570
+ }
1571
+ setColumnData(index, patch) {
1572
+ const block = this.blocks()[index];
1573
+ this.update(index, { ...block, data: { ...block.data, ...patch } });
1574
+ }
1575
+ onRatioChange(index, event) {
1576
+ this.setColumnData(index, { ratio: event.target.value });
1577
+ }
1578
+ onGapChange(index, event) {
1579
+ this.setColumnData(index, { gap: Number(event.target.value) });
1580
+ }
1581
+ onAlignChange(index, event) {
1582
+ this.setColumnData(index, { align: event.target.value });
1583
+ }
1584
+ onJustifyChange(index, event) {
1585
+ this.setColumnData(index, { justify: event.target.value });
1586
+ }
1587
+ gridColumns(block) {
1588
+ const ratio = String(block.data['ratio'] ?? '');
1589
+ const parts = ratio.split('-').map((n) => parseFloat(n)).filter((n) => n > 0);
1590
+ if (parts.length >= 2)
1591
+ return parts.map((n) => `${n}fr`).join(' ');
1592
+ return `repeat(${block.children?.length ?? 2}, 1fr)`;
1593
+ }
1594
+ gridGap(block) {
1595
+ const g = Math.max(0, Math.min(8, Number(block.data['gap']) || 4));
1596
+ return `var(--mk-space-${g})`;
1597
+ }
1598
+ justifyValue(block) {
1599
+ const map = {
1600
+ start: 'flex-start', center: 'center', end: 'flex-end', between: 'space-between',
1601
+ };
1602
+ return map[String(block.data['justify'] ?? 'start')] ?? 'flex-start';
1603
+ }
1604
+ // --- toolbar menu ---------------------------------------------------------
1605
+ toggleMenu(index) {
1606
+ this.menuOpen.update((i) => (i === index ? -1 : index));
1607
+ }
1608
+ /** Escape inside the options popup closes it and restores the toggle's focus. */
1609
+ closeMenu(trigger, event) {
1610
+ event.preventDefault();
1611
+ event.stopPropagation();
1612
+ this.menuOpen.set(-1);
1613
+ trigger.focus();
1614
+ }
1615
+ transformsFor(type) {
1616
+ return this.definition(type)?.transforms ?? [];
1617
+ }
1618
+ fallbackParagraph() {
1619
+ return { id: mkBlockId('paragraph'), type: 'paragraph', data: { html: '' } };
1620
+ }
1621
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockList, deps: [], target: i0.ɵɵFactoryTarget.Component });
1622
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkBlockList, isStandalone: true, selector: "mk-block-list", inputs: { blocks: { classPropertyName: "blocks", publicName: "blocks", isSignal: true, isRequired: true, transformFunction: null }, depth: { classPropertyName: "depth", publicName: "depth", isSignal: true, isRequired: false, transformFunction: null }, inColumn: { classPropertyName: "inColumn", publicName: "inColumn", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blocksChange: "blocksChange" }, host: { classAttribute: "mk-block-list" }, ngImport: i0, template: "@let readonly = ctx.readonly() || ctx.disabled();\n\n@for (block of blocks(); track block.id; let i = $index; let last = $last) {\n @if (!readonly) {\n <mk-block-inserter\n variant=\"slim\"\n [definitions]=\"ctx.definitions()\"\n [columnsOnly]=\"inColumn()\"\n (pick)=\"insertAt(i, $event)\"\n />\n }\n\n <div class=\"mk-block\" [attr.data-block-id]=\"block.id\" [class.mk-block--readonly]=\"readonly\">\n @if (!readonly) {\n <div class=\"mk-block__gutter\" aria-hidden=\"false\">\n <button\n type=\"button\"\n class=\"mk-block__move\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"i18n.blockEditor.moveBlockUp\"\n (click)=\"move(i, -1)\"\n >\n \u25B2\n </button>\n <span class=\"mk-block__handle\" [title]=\"i18n.blockEditor.dragHandle\" aria-hidden=\"true\">\u283F</span>\n <button\n type=\"button\"\n class=\"mk-block__move\"\n [disabled]=\"last\"\n [attr.aria-label]=\"i18n.blockEditor.moveBlockDown\"\n (click)=\"move(i, 1)\"\n >\n \u25BC\n </button>\n </div>\n\n <div class=\"mk-block__bar\">\n <span class=\"mk-block__type\">\n <span class=\"mk-block__type-icon\" aria-hidden=\"true\">{{ definition(block.type)?.icon }}</span>\n {{ definition(block.type)?.label ?? block.type }}\n </span>\n <div class=\"mk-block__actions\">\n <button\n #menuTrigger\n type=\"button\"\n class=\"mk-block__action\"\n [attr.aria-expanded]=\"menuOpen() === i\"\n [attr.aria-controls]=\"'mk-block-menu-' + block.id\"\n [attr.aria-label]=\"i18n.blockEditor.blockOptions\"\n (click)=\"toggleMenu(i)\"\n >\n \u22EF\n </button>\n @if (menuOpen() === i) {\n <div\n class=\"mk-block__menu\"\n [id]=\"'mk-block-menu-' + block.id\"\n (keydown.escape)=\"closeMenu(menuTrigger, $event)\"\n >\n <button type=\"button\" (click)=\"duplicate(i)\">{{ i18n.blockEditor.duplicate }}</button>\n @for (t of transformsFor(block.type); track t) {\n <button type=\"button\" (click)=\"transform(i, t)\">\n {{ i18n.blockEditor.turnInto(definition(t)?.label ?? t) }}\n </button>\n }\n <button type=\"button\" class=\"mk-block__menu-danger\" (click)=\"remove(i)\">\n {{ i18n.blockEditor.remove }}\n </button>\n </div>\n }\n </div>\n </div>\n }\n\n <div class=\"mk-block__body\">\n @switch (block.type) {\n @case ('paragraph') {\n <mk-paragraph-block\n [block]=\"block\"\n [readonly]=\"readonly\"\n [placeholder]=\"ctx.placeholder()\"\n (blockChange)=\"update(i, $event)\"\n (splitAt)=\"onSplit(i, $event)\"\n (removeEmpty)=\"onMergeBack(i)\"\n (arrowOut)=\"onArrowOut(i, $event)\"\n />\n }\n @case ('heading') {\n <mk-heading-block\n [block]=\"block\"\n [readonly]=\"readonly\"\n (blockChange)=\"update(i, $event)\"\n (splitAt)=\"onSplit(i, $event)\"\n (removeEmpty)=\"onMergeBack(i)\"\n (arrowOut)=\"onArrowOut(i, $event)\"\n />\n }\n @case ('quote') {\n <mk-quote-block\n [block]=\"block\"\n [readonly]=\"readonly\"\n (blockChange)=\"update(i, $event)\"\n (splitAt)=\"onSplit(i, $event)\"\n (removeEmpty)=\"onMergeBack(i)\"\n (arrowOut)=\"onArrowOut(i, $event)\"\n />\n }\n @case ('list') {\n <mk-list-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('code') {\n <mk-code-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('image') {\n <mk-image-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('embed') {\n <mk-embed-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('button') {\n <mk-button-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('divider') {\n <hr class=\"mk-block__divider\" data-mk-focus tabindex=\"-1\" />\n }\n @case ('columns') {\n @if (!readonly) {\n <div class=\"mk-columns-settings\" role=\"group\" [attr.aria-label]=\"i18n.blockEditor.columnSettings\">\n <label>\n <span>{{ i18n.blockEditor.columns }}</span>\n <span class=\"mk-columns-settings__counts\">\n @for (n of [2, 3, 4]; track n) {\n <button\n type=\"button\"\n [class.mk-columns-settings__count--active]=\"(block.data['count'] || 2) === n\"\n [attr.aria-pressed]=\"(block.data['count'] || 2) === n\"\n (click)=\"setColumnCount(i, n)\"\n >\n {{ n }}\n </button>\n }\n </span>\n </label>\n <label>\n <span>{{ i18n.blockEditor.ratio }}</span>\n <select [value]=\"block.data['ratio']\" (change)=\"onRatioChange(i, $event)\">\n @for (preset of ratioPresets[block.data['count'] || 2]; track preset.value) {\n <option [value]=\"preset.value\">{{ preset.label || i18n.blockEditor.ratioEqual }}</option>\n }\n </select>\n </label>\n <label>\n <span>{{ i18n.blockEditor.gap }}</span>\n <input\n type=\"range\"\n min=\"0\"\n max=\"8\"\n [value]=\"block.data['gap'] ?? 4\"\n (input)=\"onGapChange(i, $event)\"\n />\n </label>\n <label>\n <span>{{ i18n.blockEditor.align }}</span>\n <select [value]=\"block.data['align'] ?? 'stretch'\" (change)=\"onAlignChange(i, $event)\">\n <option value=\"stretch\">{{ i18n.blockEditor.alignStretch }}</option>\n <option value=\"start\">{{ i18n.blockEditor.alignTop }}</option>\n <option value=\"center\">{{ i18n.blockEditor.alignMiddle }}</option>\n <option value=\"end\">{{ i18n.blockEditor.alignBottom }}</option>\n </select>\n </label>\n <label>\n <span>{{ i18n.blockEditor.justify }}</span>\n <select [value]=\"block.data['justify'] ?? 'start'\" (change)=\"onJustifyChange(i, $event)\">\n <option value=\"start\">{{ i18n.blockEditor.justifyStart }}</option>\n <option value=\"center\">{{ i18n.blockEditor.justifyCenter }}</option>\n <option value=\"end\">{{ i18n.blockEditor.justifyEnd }}</option>\n <option value=\"between\">{{ i18n.blockEditor.justifyBetween }}</option>\n </select>\n </label>\n </div>\n }\n <div\n class=\"mk-columns\"\n [style.grid-template-columns]=\"gridColumns(block)\"\n [style.gap]=\"gridGap(block)\"\n [style.align-items]=\"block.data['align'] ?? 'stretch'\"\n [style.justify-content]=\"justifyValue(block)\"\n >\n @for (col of block.children ?? []; track col.id; let c = $index) {\n <div class=\"mk-column\">\n <mk-block-list\n [blocks]=\"col.children ?? []\"\n [depth]=\"depth() + 1\"\n [inColumn]=\"true\"\n (blocksChange)=\"onColumnChange(i, c, $event)\"\n />\n </div>\n }\n </div>\n }\n @default {\n <p class=\"mk-block__unknown\">{{ i18n.blockEditor.unknownBlock(block.type) }}</p>\n }\n }\n </div>\n </div>\n}\n\n@if (!readonly) {\n <mk-block-inserter\n class=\"mk-block-list__add\"\n [definitions]=\"ctx.definitions()\"\n [columnsOnly]=\"inColumn()\"\n [label]=\"blocks().length ? i18n.blockEditor.addBlock : i18n.blockEditor.addFirstBlock\"\n (pick)=\"insertAt(blocks().length, $event)\"\n />\n}\n", styles: [":host{display:block}.mk-block{position:relative;padding:var(--mk-space-2);padding-inline-start:var(--mk-space-8);border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block--readonly{padding-inline-start:var(--mk-space-2)}.mk-block:hover,.mk-block:focus-within{border-color:var(--mk-border-subtle);background:var(--mk-surface-2)}.mk-block__gutter{position:absolute;top:var(--mk-space-2);inset-inline-start:0;display:flex;flex-direction:column;align-items:center;gap:2px;opacity:0;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block:hover .mk-block__gutter,.mk-block:focus-within .mk-block__gutter{opacity:1}.mk-block__move{width:22px;height:18px;border:none;border-radius:var(--mk-radius-xs);background:transparent;color:var(--mk-text-muted);font-size:var(--mk-font-size-xs);line-height:1;cursor:pointer}.mk-block__move:hover:not(:disabled){background:var(--mk-hover-overlay);color:var(--mk-text)}.mk-block__move:disabled{opacity:.3;cursor:not-allowed}.mk-block__handle{color:var(--mk-text-subtle);font-size:var(--mk-font-size-sm);cursor:grab}.mk-block__bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--mk-space-1);opacity:0;height:20px;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block:hover .mk-block__bar,.mk-block:focus-within .mk-block__bar{opacity:1}.mk-block__type{display:inline-flex;align-items:center;gap:var(--mk-space-1);color:var(--mk-text-subtle);font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-medium)}.mk-block__type-icon{font-size:var(--mk-font-size-sm)}.mk-block__actions{position:relative}.mk-block__action{width:24px;height:20px;border:none;border-radius:var(--mk-radius-xs);background:transparent;color:var(--mk-text-muted);cursor:pointer}.mk-block__action:hover{background:var(--mk-hover-overlay);color:var(--mk-text)}.mk-block__menu{position:absolute;z-index:var(--mk-z-menu);top:calc(100% + var(--mk-space-1));inset-inline-end:0;display:flex;flex-direction:column;min-width:180px;padding:var(--mk-space-1);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface);box-shadow:var(--mk-shadow-lg)}.mk-block__menu button{padding:var(--mk-space-2) var(--mk-space-3);border:none;border-radius:var(--mk-radius-sm);background:transparent;color:var(--mk-text);font:inherit;font-size:var(--mk-font-size-sm);text-align:start;cursor:pointer}.mk-block__menu button:hover{background:var(--mk-hover-overlay)}.mk-block__menu-danger{color:var(--mk-danger-text)}.mk-block__divider{height:0;margin:var(--mk-space-3) 0;border:none;border-top:var(--mk-border-width-strong) solid var(--mk-border-strong)}.mk-block__unknown{padding:var(--mk-space-3);border:var(--mk-border-width) dashed var(--mk-border);border-radius:var(--mk-radius-md);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}.mk-columns{display:grid;margin:var(--mk-space-2) 0}.mk-column{min-width:0;padding:var(--mk-space-1);border:var(--mk-border-width) dashed var(--mk-border-subtle);border-radius:var(--mk-radius-sm)}.mk-columns-settings{display:flex;flex-wrap:wrap;gap:var(--mk-space-3);margin-bottom:var(--mk-space-2);padding:var(--mk-space-2) var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border-subtle);border-radius:var(--mk-radius-md);background:var(--mk-surface-2)}.mk-columns-settings label{display:flex;flex-direction:column;gap:var(--mk-space-1);color:var(--mk-text-muted);font-size:var(--mk-font-size-xs)}.mk-columns-settings select,.mk-columns-settings input[type=range]{height:var(--mk-control-height-sm);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text);font:inherit;font-size:var(--mk-font-size-xs)}.mk-columns-settings__counts{display:inline-flex;gap:2px}.mk-columns-settings__counts button{width:28px;height:var(--mk-control-height-sm);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text-muted);font:inherit;cursor:pointer}.mk-columns-settings__count--active{background:var(--mk-primary);border-color:var(--mk-primary);color:var(--mk-primary-contrast)}@media(max-width:640px){.mk-columns{grid-template-columns:1fr!important}}button:focus-visible,select:focus-visible,input:focus-visible,[tabindex]:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}@media(prefers-reduced-motion:reduce){.mk-block,.mk-block__gutter,.mk-block__bar{transition:none}}\n"], dependencies: [{ kind: "component", type: MkBlockList, selector: "mk-block-list", inputs: ["blocks", "depth", "inColumn"], outputs: ["blocksChange"] }, { kind: "component", type: MkParagraphBlock, selector: "mk-paragraph-block", inputs: ["block", "readonly", "placeholder"], outputs: ["blockChange", "splitAt", "removeEmpty", "arrowOut"] }, { kind: "component", type: MkHeadingBlock, selector: "mk-heading-block", inputs: ["block", "readonly"], outputs: ["blockChange", "splitAt", "removeEmpty", "arrowOut"] }, { kind: "component", type: MkQuoteBlock, selector: "mk-quote-block", inputs: ["block", "readonly"], outputs: ["blockChange", "splitAt", "removeEmpty", "arrowOut"] }, { kind: "component", type: MkListBlock, selector: "mk-list-block", inputs: ["block", "readonly"], outputs: ["blockChange"] }, { kind: "component", type: MkCodeBlock, selector: "mk-code-block", inputs: ["block", "readonly"], outputs: ["blockChange"] }, { kind: "component", type: MkImageBlock, selector: "mk-image-block", inputs: ["block", "readonly"], outputs: ["blockChange"] }, { kind: "component", type: MkEmbedBlock, selector: "mk-embed-block", inputs: ["block", "readonly"], outputs: ["blockChange"] }, { kind: "component", type: MkButtonBlock, selector: "mk-button-block", inputs: ["block", "readonly"], outputs: ["blockChange"] }, { kind: "component", type: MkBlockInserter, selector: "mk-block-inserter", inputs: ["definitions", "variant", "label", "disabled", "columnsOnly"], outputs: ["pick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1623
+ }
1624
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockList, decorators: [{
1625
+ type: Component,
1626
+ args: [{ selector: 'mk-block-list', imports: [
1627
+ MkParagraphBlock,
1628
+ MkHeadingBlock,
1629
+ MkQuoteBlock,
1630
+ MkListBlock,
1631
+ MkCodeBlock,
1632
+ MkImageBlock,
1633
+ MkEmbedBlock,
1634
+ MkButtonBlock,
1635
+ MkBlockInserter,
1636
+ ], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'mk-block-list' }, template: "@let readonly = ctx.readonly() || ctx.disabled();\n\n@for (block of blocks(); track block.id; let i = $index; let last = $last) {\n @if (!readonly) {\n <mk-block-inserter\n variant=\"slim\"\n [definitions]=\"ctx.definitions()\"\n [columnsOnly]=\"inColumn()\"\n (pick)=\"insertAt(i, $event)\"\n />\n }\n\n <div class=\"mk-block\" [attr.data-block-id]=\"block.id\" [class.mk-block--readonly]=\"readonly\">\n @if (!readonly) {\n <div class=\"mk-block__gutter\" aria-hidden=\"false\">\n <button\n type=\"button\"\n class=\"mk-block__move\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"i18n.blockEditor.moveBlockUp\"\n (click)=\"move(i, -1)\"\n >\n \u25B2\n </button>\n <span class=\"mk-block__handle\" [title]=\"i18n.blockEditor.dragHandle\" aria-hidden=\"true\">\u283F</span>\n <button\n type=\"button\"\n class=\"mk-block__move\"\n [disabled]=\"last\"\n [attr.aria-label]=\"i18n.blockEditor.moveBlockDown\"\n (click)=\"move(i, 1)\"\n >\n \u25BC\n </button>\n </div>\n\n <div class=\"mk-block__bar\">\n <span class=\"mk-block__type\">\n <span class=\"mk-block__type-icon\" aria-hidden=\"true\">{{ definition(block.type)?.icon }}</span>\n {{ definition(block.type)?.label ?? block.type }}\n </span>\n <div class=\"mk-block__actions\">\n <button\n #menuTrigger\n type=\"button\"\n class=\"mk-block__action\"\n [attr.aria-expanded]=\"menuOpen() === i\"\n [attr.aria-controls]=\"'mk-block-menu-' + block.id\"\n [attr.aria-label]=\"i18n.blockEditor.blockOptions\"\n (click)=\"toggleMenu(i)\"\n >\n \u22EF\n </button>\n @if (menuOpen() === i) {\n <div\n class=\"mk-block__menu\"\n [id]=\"'mk-block-menu-' + block.id\"\n (keydown.escape)=\"closeMenu(menuTrigger, $event)\"\n >\n <button type=\"button\" (click)=\"duplicate(i)\">{{ i18n.blockEditor.duplicate }}</button>\n @for (t of transformsFor(block.type); track t) {\n <button type=\"button\" (click)=\"transform(i, t)\">\n {{ i18n.blockEditor.turnInto(definition(t)?.label ?? t) }}\n </button>\n }\n <button type=\"button\" class=\"mk-block__menu-danger\" (click)=\"remove(i)\">\n {{ i18n.blockEditor.remove }}\n </button>\n </div>\n }\n </div>\n </div>\n }\n\n <div class=\"mk-block__body\">\n @switch (block.type) {\n @case ('paragraph') {\n <mk-paragraph-block\n [block]=\"block\"\n [readonly]=\"readonly\"\n [placeholder]=\"ctx.placeholder()\"\n (blockChange)=\"update(i, $event)\"\n (splitAt)=\"onSplit(i, $event)\"\n (removeEmpty)=\"onMergeBack(i)\"\n (arrowOut)=\"onArrowOut(i, $event)\"\n />\n }\n @case ('heading') {\n <mk-heading-block\n [block]=\"block\"\n [readonly]=\"readonly\"\n (blockChange)=\"update(i, $event)\"\n (splitAt)=\"onSplit(i, $event)\"\n (removeEmpty)=\"onMergeBack(i)\"\n (arrowOut)=\"onArrowOut(i, $event)\"\n />\n }\n @case ('quote') {\n <mk-quote-block\n [block]=\"block\"\n [readonly]=\"readonly\"\n (blockChange)=\"update(i, $event)\"\n (splitAt)=\"onSplit(i, $event)\"\n (removeEmpty)=\"onMergeBack(i)\"\n (arrowOut)=\"onArrowOut(i, $event)\"\n />\n }\n @case ('list') {\n <mk-list-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('code') {\n <mk-code-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('image') {\n <mk-image-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('embed') {\n <mk-embed-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('button') {\n <mk-button-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('divider') {\n <hr class=\"mk-block__divider\" data-mk-focus tabindex=\"-1\" />\n }\n @case ('columns') {\n @if (!readonly) {\n <div class=\"mk-columns-settings\" role=\"group\" [attr.aria-label]=\"i18n.blockEditor.columnSettings\">\n <label>\n <span>{{ i18n.blockEditor.columns }}</span>\n <span class=\"mk-columns-settings__counts\">\n @for (n of [2, 3, 4]; track n) {\n <button\n type=\"button\"\n [class.mk-columns-settings__count--active]=\"(block.data['count'] || 2) === n\"\n [attr.aria-pressed]=\"(block.data['count'] || 2) === n\"\n (click)=\"setColumnCount(i, n)\"\n >\n {{ n }}\n </button>\n }\n </span>\n </label>\n <label>\n <span>{{ i18n.blockEditor.ratio }}</span>\n <select [value]=\"block.data['ratio']\" (change)=\"onRatioChange(i, $event)\">\n @for (preset of ratioPresets[block.data['count'] || 2]; track preset.value) {\n <option [value]=\"preset.value\">{{ preset.label || i18n.blockEditor.ratioEqual }}</option>\n }\n </select>\n </label>\n <label>\n <span>{{ i18n.blockEditor.gap }}</span>\n <input\n type=\"range\"\n min=\"0\"\n max=\"8\"\n [value]=\"block.data['gap'] ?? 4\"\n (input)=\"onGapChange(i, $event)\"\n />\n </label>\n <label>\n <span>{{ i18n.blockEditor.align }}</span>\n <select [value]=\"block.data['align'] ?? 'stretch'\" (change)=\"onAlignChange(i, $event)\">\n <option value=\"stretch\">{{ i18n.blockEditor.alignStretch }}</option>\n <option value=\"start\">{{ i18n.blockEditor.alignTop }}</option>\n <option value=\"center\">{{ i18n.blockEditor.alignMiddle }}</option>\n <option value=\"end\">{{ i18n.blockEditor.alignBottom }}</option>\n </select>\n </label>\n <label>\n <span>{{ i18n.blockEditor.justify }}</span>\n <select [value]=\"block.data['justify'] ?? 'start'\" (change)=\"onJustifyChange(i, $event)\">\n <option value=\"start\">{{ i18n.blockEditor.justifyStart }}</option>\n <option value=\"center\">{{ i18n.blockEditor.justifyCenter }}</option>\n <option value=\"end\">{{ i18n.blockEditor.justifyEnd }}</option>\n <option value=\"between\">{{ i18n.blockEditor.justifyBetween }}</option>\n </select>\n </label>\n </div>\n }\n <div\n class=\"mk-columns\"\n [style.grid-template-columns]=\"gridColumns(block)\"\n [style.gap]=\"gridGap(block)\"\n [style.align-items]=\"block.data['align'] ?? 'stretch'\"\n [style.justify-content]=\"justifyValue(block)\"\n >\n @for (col of block.children ?? []; track col.id; let c = $index) {\n <div class=\"mk-column\">\n <mk-block-list\n [blocks]=\"col.children ?? []\"\n [depth]=\"depth() + 1\"\n [inColumn]=\"true\"\n (blocksChange)=\"onColumnChange(i, c, $event)\"\n />\n </div>\n }\n </div>\n }\n @default {\n <p class=\"mk-block__unknown\">{{ i18n.blockEditor.unknownBlock(block.type) }}</p>\n }\n }\n </div>\n </div>\n}\n\n@if (!readonly) {\n <mk-block-inserter\n class=\"mk-block-list__add\"\n [definitions]=\"ctx.definitions()\"\n [columnsOnly]=\"inColumn()\"\n [label]=\"blocks().length ? i18n.blockEditor.addBlock : i18n.blockEditor.addFirstBlock\"\n (pick)=\"insertAt(blocks().length, $event)\"\n />\n}\n", styles: [":host{display:block}.mk-block{position:relative;padding:var(--mk-space-2);padding-inline-start:var(--mk-space-8);border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block--readonly{padding-inline-start:var(--mk-space-2)}.mk-block:hover,.mk-block:focus-within{border-color:var(--mk-border-subtle);background:var(--mk-surface-2)}.mk-block__gutter{position:absolute;top:var(--mk-space-2);inset-inline-start:0;display:flex;flex-direction:column;align-items:center;gap:2px;opacity:0;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block:hover .mk-block__gutter,.mk-block:focus-within .mk-block__gutter{opacity:1}.mk-block__move{width:22px;height:18px;border:none;border-radius:var(--mk-radius-xs);background:transparent;color:var(--mk-text-muted);font-size:var(--mk-font-size-xs);line-height:1;cursor:pointer}.mk-block__move:hover:not(:disabled){background:var(--mk-hover-overlay);color:var(--mk-text)}.mk-block__move:disabled{opacity:.3;cursor:not-allowed}.mk-block__handle{color:var(--mk-text-subtle);font-size:var(--mk-font-size-sm);cursor:grab}.mk-block__bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--mk-space-1);opacity:0;height:20px;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block:hover .mk-block__bar,.mk-block:focus-within .mk-block__bar{opacity:1}.mk-block__type{display:inline-flex;align-items:center;gap:var(--mk-space-1);color:var(--mk-text-subtle);font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-medium)}.mk-block__type-icon{font-size:var(--mk-font-size-sm)}.mk-block__actions{position:relative}.mk-block__action{width:24px;height:20px;border:none;border-radius:var(--mk-radius-xs);background:transparent;color:var(--mk-text-muted);cursor:pointer}.mk-block__action:hover{background:var(--mk-hover-overlay);color:var(--mk-text)}.mk-block__menu{position:absolute;z-index:var(--mk-z-menu);top:calc(100% + var(--mk-space-1));inset-inline-end:0;display:flex;flex-direction:column;min-width:180px;padding:var(--mk-space-1);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface);box-shadow:var(--mk-shadow-lg)}.mk-block__menu button{padding:var(--mk-space-2) var(--mk-space-3);border:none;border-radius:var(--mk-radius-sm);background:transparent;color:var(--mk-text);font:inherit;font-size:var(--mk-font-size-sm);text-align:start;cursor:pointer}.mk-block__menu button:hover{background:var(--mk-hover-overlay)}.mk-block__menu-danger{color:var(--mk-danger-text)}.mk-block__divider{height:0;margin:var(--mk-space-3) 0;border:none;border-top:var(--mk-border-width-strong) solid var(--mk-border-strong)}.mk-block__unknown{padding:var(--mk-space-3);border:var(--mk-border-width) dashed var(--mk-border);border-radius:var(--mk-radius-md);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}.mk-columns{display:grid;margin:var(--mk-space-2) 0}.mk-column{min-width:0;padding:var(--mk-space-1);border:var(--mk-border-width) dashed var(--mk-border-subtle);border-radius:var(--mk-radius-sm)}.mk-columns-settings{display:flex;flex-wrap:wrap;gap:var(--mk-space-3);margin-bottom:var(--mk-space-2);padding:var(--mk-space-2) var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border-subtle);border-radius:var(--mk-radius-md);background:var(--mk-surface-2)}.mk-columns-settings label{display:flex;flex-direction:column;gap:var(--mk-space-1);color:var(--mk-text-muted);font-size:var(--mk-font-size-xs)}.mk-columns-settings select,.mk-columns-settings input[type=range]{height:var(--mk-control-height-sm);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text);font:inherit;font-size:var(--mk-font-size-xs)}.mk-columns-settings__counts{display:inline-flex;gap:2px}.mk-columns-settings__counts button{width:28px;height:var(--mk-control-height-sm);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text-muted);font:inherit;cursor:pointer}.mk-columns-settings__count--active{background:var(--mk-primary);border-color:var(--mk-primary);color:var(--mk-primary-contrast)}@media(max-width:640px){.mk-columns{grid-template-columns:1fr!important}}button:focus-visible,select:focus-visible,input:focus-visible,[tabindex]:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}@media(prefers-reduced-motion:reduce){.mk-block,.mk-block__gutter,.mk-block__bar{transition:none}}\n"] }]
1637
+ }], propDecorators: { blocks: [{ type: i0.Input, args: [{ isSignal: true, alias: "blocks", required: true }] }], depth: [{ type: i0.Input, args: [{ isSignal: true, alias: "depth", required: false }] }], inColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "inColumn", required: false }] }], blocksChange: [{ type: i0.Output, args: ["blocksChange"] }] } });
1638
+
1639
+ /**
1640
+ * HTML → {@link MkBlockDocument} parser — the inverse of {@link mkBlocksToHtml}.
1641
+ *
1642
+ * Lets the block editor back a **string-typed richtext field** (e.g. a CMS
1643
+ * `richtext` column that stores an HTML string): seed the editor from stored
1644
+ * HTML, and serialise back to HTML on change. Best-effort and forgiving — any
1645
+ * HTML parses, with unrecognised block-level wrappers recursed into and loose
1646
+ * inline runs grouped into paragraphs. Round-trips the blocks `mkBlocksToHtml`
1647
+ * emits (paragraph, heading, list, quote, code, image, divider, button, embed,
1648
+ * columns).
1649
+ *
1650
+ * Uses the DOM (`DOMParser`) — available in browsers and jsdom. In a pure Node
1651
+ * context without a DOM it falls back to a single sanitised paragraph.
1652
+ */
1653
+ const HEADING_LEVEL = {
1654
+ h1: 1, h2: 2, h3: 3, h4: 4, h5: 4, h6: 4,
1655
+ };
1656
+ /** Block-level tags handled as their own block (others recurse or group). */
1657
+ const BLOCK_TAGS = new Set([
1658
+ 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'blockquote', 'pre',
1659
+ 'figure', 'img', 'hr', 'div', 'section', 'article', 'table',
1660
+ ]);
1661
+ function block(type, data, children) {
1662
+ const b = { id: mkBlockId(type), type, data };
1663
+ if (children)
1664
+ b.children = children;
1665
+ return b;
1666
+ }
1667
+ function parseRoot(html) {
1668
+ if (typeof DOMParser !== 'undefined') {
1669
+ return new DOMParser().parseFromString(html, 'text/html').body;
1670
+ }
1671
+ if (typeof document !== 'undefined') {
1672
+ const tpl = document.createElement('template');
1673
+ tpl.innerHTML = html;
1674
+ return tpl.content;
1675
+ }
1676
+ return null;
1677
+ }
1678
+ const TEXT_NODE = 3;
1679
+ const ELEMENT_NODE = 1;
1680
+ function isBlockElement(node) {
1681
+ return (node.nodeType === ELEMENT_NODE &&
1682
+ BLOCK_TAGS.has(node.tagName.toLowerCase()));
1683
+ }
1684
+ /** HTML for one node in an inline run (encoded text, element markup). */
1685
+ function nodeHtml(node) {
1686
+ if (node.nodeType === TEXT_NODE)
1687
+ return mkEscapeHtml(node.textContent ?? '');
1688
+ if (node.nodeType === ELEMENT_NODE)
1689
+ return node.outerHTML;
1690
+ return '';
1691
+ }
1692
+ function style(el, prop) {
1693
+ return el.style?.getPropertyValue(prop)?.trim() ?? '';
1694
+ }
1695
+ /** Walk a parent's children into blocks, grouping loose inline runs. */
1696
+ function collectBlocks(root, out) {
1697
+ let inline = [];
1698
+ const flush = () => {
1699
+ if (!inline.length)
1700
+ return;
1701
+ const html = inline.map(nodeHtml).join('').trim();
1702
+ if (html)
1703
+ out.push(block('paragraph', { html: sanitizeInlineHtml(html) }));
1704
+ inline = [];
1705
+ };
1706
+ for (const node of Array.from(root.childNodes)) {
1707
+ if (isBlockElement(node)) {
1708
+ flush();
1709
+ appendElement(node, out);
1710
+ }
1711
+ else if (node.nodeType === TEXT_NODE && !node.textContent?.trim()) {
1712
+ // ignore whitespace between blocks
1713
+ }
1714
+ else {
1715
+ inline.push(node);
1716
+ }
1717
+ }
1718
+ flush();
1719
+ }
1720
+ /** Map a block-level element to one or more blocks (appended to `out`). */
1721
+ function appendElement(el, out) {
1722
+ const tag = el.tagName.toLowerCase();
1723
+ if (tag === 'p') {
1724
+ out.push(block('paragraph', { html: sanitizeInlineHtml(el.innerHTML) }));
1725
+ return;
1726
+ }
1727
+ if (tag in HEADING_LEVEL) {
1728
+ out.push(block('heading', {
1729
+ level: HEADING_LEVEL[tag],
1730
+ html: sanitizeInlineHtml(el.innerHTML),
1731
+ }));
1732
+ return;
1733
+ }
1734
+ if (tag === 'ul' || tag === 'ol') {
1735
+ const items = Array.from(el.querySelectorAll(':scope > li')).map((li) => sanitizeInlineHtml(li.innerHTML));
1736
+ out.push(block('list', { ordered: tag === 'ol', items: items.length ? items : [''] }));
1737
+ return;
1738
+ }
1739
+ if (tag === 'blockquote') {
1740
+ const cite = el.querySelector('cite');
1741
+ const p = el.querySelector('p');
1742
+ const body = p ? p.innerHTML : el.innerHTML;
1743
+ out.push(block('quote', {
1744
+ html: sanitizeInlineHtml(body),
1745
+ citation: cite?.textContent?.trim() ?? '',
1746
+ }));
1747
+ return;
1748
+ }
1749
+ if (tag === 'pre') {
1750
+ const codeEl = el.querySelector('code') ?? el;
1751
+ const lang = (codeEl.className.match(/language-([\w-]+)/) ?? [])[1] ?? '';
1752
+ out.push(block('code', { code: codeEl.textContent ?? '', language: lang }));
1753
+ return;
1754
+ }
1755
+ if (tag === 'figure' || tag === 'img') {
1756
+ const img = tag === 'img' ? el : el.querySelector('img');
1757
+ if (img) {
1758
+ const width = parseInt(style(img, 'max-width'), 10);
1759
+ const align = style(el, 'text-align') === 'left'
1760
+ ? 'left'
1761
+ : style(el, 'text-align') === 'right'
1762
+ ? 'right'
1763
+ : 'center';
1764
+ out.push(block('image', {
1765
+ src: img.getAttribute('src') ?? '',
1766
+ alt: img.getAttribute('alt') ?? '',
1767
+ caption: el.querySelector('figcaption')?.textContent?.trim() ?? '',
1768
+ align,
1769
+ width: Number.isFinite(width) && width > 0 ? width : 100,
1770
+ }));
1771
+ }
1772
+ return;
1773
+ }
1774
+ if (tag === 'hr') {
1775
+ out.push(block('divider', {}));
1776
+ return;
1777
+ }
1778
+ // <div> / generic containers: recognise the wrappers mkBlocksToHtml emits,
1779
+ // otherwise recurse into the children.
1780
+ if (el.classList.contains('mk-columns')) {
1781
+ const columns = Array.from(el.querySelectorAll(':scope > .mk-column')).map((colEl) => {
1782
+ const children = [];
1783
+ collectBlocks(colEl, children);
1784
+ return block('column', {}, children);
1785
+ });
1786
+ out.push(block('columns', { count: columns.length || 2 }, columns));
1787
+ return;
1788
+ }
1789
+ const cta = el.querySelector(':scope > a.mk-cta, a.mk-cta');
1790
+ if (cta) {
1791
+ const tone = (cta.className.match(/mk-cta--([\w-]+)/) ?? [])[1] ?? 'primary';
1792
+ out.push(block('button', {
1793
+ label: cta.textContent?.trim() ?? 'Button',
1794
+ href: cta.getAttribute('href') ?? '',
1795
+ align: style(el, 'text-align') || 'left',
1796
+ tone,
1797
+ }));
1798
+ return;
1799
+ }
1800
+ if (el.classList.contains('mk-embed')) {
1801
+ const iframe = el.querySelector('iframe');
1802
+ out.push(block('embed', {
1803
+ url: '',
1804
+ embedUrl: iframe?.getAttribute('src') ?? '',
1805
+ aspectRatio: 16 / 9,
1806
+ }));
1807
+ return;
1808
+ }
1809
+ // Unknown container — recurse into its children.
1810
+ collectBlocks(el, out);
1811
+ }
1812
+ /**
1813
+ * Parse an HTML string into a {@link MkBlockDocument}. Empty / whitespace input
1814
+ * yields an empty document.
1815
+ */
1816
+ function mkHtmlToBlocks(html) {
1817
+ const source = String(html ?? '');
1818
+ if (!source.trim())
1819
+ return mkEmptyDocument();
1820
+ const root = parseRoot(source);
1821
+ if (!root) {
1822
+ // No DOM (server) — degrade to a single sanitised paragraph.
1823
+ return {
1824
+ version: MK_BLOCK_DOCUMENT_VERSION,
1825
+ blocks: [block('paragraph', { html: sanitizeInlineHtml(source) })],
1826
+ };
1827
+ }
1828
+ const blocks = [];
1829
+ collectBlocks(root, blocks);
1830
+ return { version: MK_BLOCK_DOCUMENT_VERSION, blocks };
1831
+ }
1832
+
1833
+ /**
1834
+ * Block editor — a configurable, Gutenberg-style block content editor. Renders
1835
+ * an ordered document of blocks (paragraph, heading, image, columns, embed,
1836
+ * list, quote, button, divider, code) with a filterable inserter, per-block
1837
+ * chrome (move/duplicate/transform/delete) and inline rich-text formatting.
1838
+ *
1839
+ * The palette is fully customisable: pass definitions via the `blocks` input or
1840
+ * the {@link MK_BLOCK_DEFINITIONS} token (mirrors `registerBlockType`). Both are
1841
+ * merged over {@link MK_DEFAULT_BLOCKS}.
1842
+ *
1843
+ * Implements `ControlValueAccessor` AND a two-way `value` model, so it works
1844
+ * with `[(ngModel)]`, reactive forms and `[(value)]`.
1845
+ *
1846
+ * ```html
1847
+ * <mk-block-editor [(value)]="doc" [uploadHandler]="upload" />
1848
+ * ```
1849
+ */
1850
+ class MkBlockEditor {
1851
+ i18n = inject(MK_I18N);
1852
+ ctx = inject(MkBlockEditorContext);
1853
+ hostRef = inject(ElementRef);
1854
+ tokenDefinitions = inject(MK_BLOCK_DEFINITIONS, { optional: true });
1855
+ tokenUpload = inject(MK_BLOCK_UPLOAD_HANDLER, { optional: true });
1856
+ tokenEmbeds = inject(MK_BLOCK_EMBED_PROVIDERS, { optional: true });
1857
+ /** Two-way document value (always an `MkBlockDocument`, regardless of format). */
1858
+ value = model(mkEmptyDocument(), /* @ts-ignore */
1859
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1860
+ /**
1861
+ * Form/CVA exchange format. `document` (default) reads/writes an
1862
+ * `MkBlockDocument`; `html` reads/writes an **HTML string** — so the editor
1863
+ * can back a string-typed richtext field (`[(ngModel)]="htmlString"`). The
1864
+ * two-way `value` model stays an `MkBlockDocument` either way.
1865
+ */
1866
+ valueFormat = input('document', /* @ts-ignore */
1867
+ ...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
1868
+ /** Custom / extended block definitions (merged over the defaults + token). */
1869
+ blocks = input(null, /* @ts-ignore */
1870
+ ...(ngDevMode ? [{ debugName: "blocks" }] : /* istanbul ignore next */ []));
1871
+ /** Placeholder for empty text blocks. */
1872
+ placeholder = input(this.i18n.blockEditor.emptyBlockPlaceholder, /* @ts-ignore */
1873
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
1874
+ /** Accessible label for the editor region. */
1875
+ ariaLabel = input(this.i18n.blockEditor.editorLabel, /* @ts-ignore */
1876
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
1877
+ /** Read-only: renders content without editing chrome. */
1878
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1879
+ /** Disabled (form-level). */
1880
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1881
+ /** Force the invalid visual + `aria-invalid` when used standalone. */
1882
+ invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1883
+ /** Async upload handler (overrides the {@link MK_BLOCK_UPLOAD_HANDLER} token). */
1884
+ uploadHandler = input(null, /* @ts-ignore */
1885
+ ...(ngDevMode ? [{ debugName: "uploadHandler" }] : /* istanbul ignore next */ []));
1886
+ /** Extra embed providers (merged with defaults + token). */
1887
+ embedProviders = input(null, /* @ts-ignore */
1888
+ ...(ngDevMode ? [{ debugName: "embedProviders" }] : /* istanbul ignore next */ []));
1889
+ /** Fires on any document change (in addition to the `value` model). */
1890
+ change = output();
1891
+ /** Fires the serialised HTML on any change — convenient in `html` mode. */
1892
+ htmlChange = output();
1893
+ cvaDisabled = signal(false, /* @ts-ignore */
1894
+ ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
1895
+ /** Invalid visual, forced by the `invalid` input. */
1896
+ isInvalid = computed(() => this.invalid(), /* @ts-ignore */
1897
+ ...(ngDevMode ? [{ debugName: "isInvalid" }] : /* istanbul ignore next */ []));
1898
+ onChange = () => { };
1899
+ onTouched = () => { };
1900
+ /** Effective, merged block definitions (defaults localised via MK_I18N). */
1901
+ definitions = computed(() => mkMergeBlockDefinitions(localizeDefaultBlocks(this.i18n.blockEditor), ...(this.tokenDefinitions ?? []), this.blocks()), /* @ts-ignore */
1902
+ ...(ngDevMode ? [{ debugName: "definitions" }] : /* istanbul ignore next */ []));
1903
+ topBlocks = computed(() => this.value()?.blocks ?? [], /* @ts-ignore */
1904
+ ...(ngDevMode ? [{ debugName: "topBlocks" }] : /* istanbul ignore next */ []));
1905
+ constructor() {
1906
+ this.ctx.hostRef = this.hostRef;
1907
+ // Connect the shared context straight to the input-derived signals (pull
1908
+ // based; no per-input mirror effects). Must happen before any child
1909
+ // renders, which the constructor guarantees: the context is provided by
1910
+ // this component, so children inject it only after we are constructed.
1911
+ this.ctx.definitions = this.definitions;
1912
+ this.ctx.readonly = this.readonly;
1913
+ this.ctx.disabled = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
1914
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
1915
+ this.ctx.placeholder = this.placeholder;
1916
+ this.ctx.uploadHandler = computed(() => this.uploadHandler() ?? this.tokenUpload ?? null, /* @ts-ignore */
1917
+ ...(ngDevMode ? [{ debugName: "uploadHandler" }] : /* istanbul ignore next */ []));
1918
+ this.ctx.embedProviders = computed(() => [
1919
+ ...MK_DEFAULT_EMBED_PROVIDERS,
1920
+ ...(this.tokenEmbeds?.flat() ?? []),
1921
+ ...(this.embedProviders() ?? []),
1922
+ ], /* @ts-ignore */
1923
+ ...(ngDevMode ? [{ debugName: "embedProviders" }] : /* istanbul ignore next */ []));
1924
+ }
1925
+ onBlocksChange(blocks) {
1926
+ const next = {
1927
+ version: this.value()?.version ?? MK_BLOCK_DOCUMENT_VERSION,
1928
+ blocks,
1929
+ };
1930
+ this.value.set(next);
1931
+ // Serialising the whole document on every keystroke is expensive — do it
1932
+ // only when something actually consumes the HTML: `html` value format, or
1933
+ // a subscriber on `htmlChange`.
1934
+ const wantsHtml = this.valueFormat() === 'html' || hasObservers(this.htmlChange);
1935
+ const html = wantsHtml ? mkBlocksToHtml(next) : null;
1936
+ this.onChange(this.valueFormat() === 'html' ? html : next);
1937
+ this.onTouched();
1938
+ this.change.emit(next);
1939
+ if (html !== null)
1940
+ this.htmlChange.emit(html);
1941
+ }
1942
+ /**
1943
+ * Marks the control touched once focus leaves it entirely, so a form that
1944
+ * gates its errors on `touched` behaves the same here as on a native input.
1945
+ */
1946
+ onFocusOut(event) {
1947
+ const next = event.relatedTarget;
1948
+ if (next && this.hostRef.nativeElement.contains(next))
1949
+ return;
1950
+ this.onTouched();
1951
+ }
1952
+ // --- ControlValueAccessor -------------------------------------------------
1953
+ writeValue(value) {
1954
+ if (this.valueFormat() === 'html') {
1955
+ this.value.set(typeof value === 'string'
1956
+ ? mkHtmlToBlocks(value)
1957
+ : (value ?? mkEmptyDocument()));
1958
+ }
1959
+ else {
1960
+ this.value.set(value ?? mkEmptyDocument());
1961
+ }
1962
+ }
1963
+ registerOnChange(fn) {
1964
+ this.onChange = fn;
1965
+ }
1966
+ registerOnTouched(fn) {
1967
+ this.onTouched = fn;
1968
+ }
1969
+ setDisabledState(isDisabled) {
1970
+ this.cvaDisabled.set(isDisabled);
1971
+ }
1972
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
1973
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.7", type: MkBlockEditor, isStandalone: true, selector: "mk-block-editor", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, blocks: { classPropertyName: "blocks", publicName: "blocks", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, uploadHandler: { classPropertyName: "uploadHandler", publicName: "uploadHandler", isSignal: true, isRequired: false, transformFunction: null }, embedProviders: { classPropertyName: "embedProviders", publicName: "embedProviders", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", change: "change", htmlChange: "htmlChange" }, host: { attributes: { "role": "group" }, listeners: { "focusout": "onFocusOut($event)" }, properties: { "attr.aria-label": "ariaLabel()", "class.mk-block-editor--readonly": "readonly()", "class.mk-block-editor--invalid": "isInvalid()", "attr.aria-invalid": "isInvalid() || null", "class.mk-block-editor--disabled": "disabled() || cvaDisabled()" }, classAttribute: "mk-block-editor" }, providers: [
1974
+ MkBlockEditorContext,
1975
+ {
1976
+ provide: NG_VALUE_ACCESSOR,
1977
+ useExisting: forwardRef(() => MkBlockEditor),
1978
+ multi: true,
1979
+ },
1980
+ ], ngImport: i0, template: "<mk-block-list [blocks]=\"topBlocks()\" [depth]=\"0\" (blocksChange)=\"onBlocksChange($event)\" />\n", styles: [":host{display:block;padding:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);background:var(--mk-surface);color:var(--mk-text);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-relaxed)}:host(.mk-block-editor--disabled){opacity:.6;pointer-events:none}:host(.mk-block-editor--readonly){border-style:dashed}:host(.mk-block-editor--invalid){border-color:var(--mk-danger)}\n"], dependencies: [{ kind: "component", type: MkBlockList, selector: "mk-block-list", inputs: ["blocks", "depth", "inColumn"], outputs: ["blocksChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1981
+ }
1982
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockEditor, decorators: [{
1983
+ type: Component,
1984
+ args: [{ selector: 'mk-block-editor', imports: [MkBlockList], changeDetection: ChangeDetectionStrategy.OnPush, host: {
1985
+ class: 'mk-block-editor',
1986
+ role: 'group',
1987
+ '[attr.aria-label]': 'ariaLabel()',
1988
+ '[class.mk-block-editor--readonly]': 'readonly()',
1989
+ '[class.mk-block-editor--invalid]': 'isInvalid()',
1990
+ '[attr.aria-invalid]': 'isInvalid() || null',
1991
+ '[class.mk-block-editor--disabled]': 'disabled() || cvaDisabled()',
1992
+ '(focusout)': 'onFocusOut($event)',
1993
+ }, providers: [
1994
+ MkBlockEditorContext,
1995
+ {
1996
+ provide: NG_VALUE_ACCESSOR,
1997
+ useExisting: forwardRef(() => MkBlockEditor),
1998
+ multi: true,
1999
+ },
2000
+ ], template: "<mk-block-list [blocks]=\"topBlocks()\" [depth]=\"0\" (blocksChange)=\"onBlocksChange($event)\" />\n", styles: [":host{display:block;padding:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);background:var(--mk-surface);color:var(--mk-text);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-relaxed)}:host(.mk-block-editor--disabled){opacity:.6;pointer-events:none}:host(.mk-block-editor--readonly){border-style:dashed}:host(.mk-block-editor--invalid){border-color:var(--mk-danger)}\n"] }]
2001
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], blocks: [{ type: i0.Input, args: [{ isSignal: true, alias: "blocks", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], uploadHandler: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadHandler", required: false }] }], embedProviders: [{ type: i0.Input, args: [{ isSignal: true, alias: "embedProviders", required: false }] }], change: [{ type: i0.Output, args: ["change"] }], htmlChange: [{ type: i0.Output, args: ["htmlChange"] }] } });
2002
+ /**
2003
+ * Whether an `output()` emitter currently has any subscribers (template
2004
+ * `(htmlChange)` bindings and programmatic `.subscribe()` both register one).
2005
+ * Reads `OutputEmitterRef`'s internal `listeners` array — not public API, so
2006
+ * if the field ever disappears in a future Angular this reports `true` and we
2007
+ * fall back to always serialising (the documented pre-optimisation behaviour).
2008
+ */
2009
+ function hasObservers(ref) {
2010
+ const listeners = ref.listeners;
2011
+ if (listeners === undefined)
2012
+ return true;
2013
+ return (listeners?.length ?? 0) > 0;
2014
+ }
2015
+ /**
2016
+ * Returns {@link MK_DEFAULT_BLOCKS} with the built-in labels, descriptions and
2017
+ * group headings resolved from the active i18n strings (custom definitions
2018
+ * merged on top keep their own text). The Button block's `create()` is wrapped
2019
+ * so a fresh block starts with the localised default label.
2020
+ */
2021
+ function localizeDefaultBlocks(s) {
2022
+ const text = {
2023
+ paragraph: { label: s.blockParagraph, description: s.blockParagraphDesc },
2024
+ heading: { label: s.blockHeading, description: s.blockHeadingDesc },
2025
+ list: { label: s.blockList, description: s.blockListDesc },
2026
+ quote: { label: s.blockQuote, description: s.blockQuoteDesc },
2027
+ code: { label: s.blockCode, description: s.blockCodeDesc },
2028
+ image: { label: s.blockImage, description: s.blockImageDesc },
2029
+ embed: { label: s.blockEmbed, description: s.blockEmbedDesc },
2030
+ button: { label: s.blockButton, description: s.blockButtonDesc },
2031
+ divider: { label: s.blockDivider, description: s.blockDividerDesc },
2032
+ columns: { label: s.blockColumns, description: s.blockColumnsDesc },
2033
+ };
2034
+ const groups = {
2035
+ Text: s.groupText,
2036
+ Media: s.groupMedia,
2037
+ Layout: s.groupLayout,
2038
+ };
2039
+ return MK_DEFAULT_BLOCKS.map((def) => {
2040
+ const t = text[def.type];
2041
+ const localized = {
2042
+ ...def,
2043
+ label: t?.label ?? def.label,
2044
+ description: t?.description ?? def.description,
2045
+ group: groups[def.group ?? ''] ?? def.group,
2046
+ };
2047
+ if (def.type === 'button') {
2048
+ localized.create = () => {
2049
+ const b = def.create();
2050
+ return { ...b, data: { ...b.data, label: s.buttonDefaultLabel } };
2051
+ };
2052
+ }
2053
+ return localized;
2054
+ });
2055
+ }
2056
+
2057
+ /**
2058
+ * Block renderer — read-only, themed display of a {@link MkBlockDocument} for
2059
+ * published content pages and blog posts. Reuses the same layout CSS as the
2060
+ * editor so authored columns look identical when published.
2061
+ *
2062
+ * SECURITY MODEL: rich-text HTML is passed through {@link sanitizeInlineHtml}
2063
+ * and then bound with `[innerHTML]`, so Angular's `DomSanitizer` sanitises it
2064
+ * again at render time (never bypassed for author HTML). The ONLY bypass is for
2065
+ * a stored embed's `embedUrl`, which came from an allow-listed provider.
2066
+ */
2067
+ class MkBlockRenderer {
2068
+ i18n = inject(MK_I18N);
2069
+ sanitizer = inject(DomSanitizer);
2070
+ safeSrcCache = new Map();
2071
+ /** The document to render. */
2072
+ value = input(null, /* @ts-ignore */
2073
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
2074
+ /** Internal: a block subtree (used when recursing into columns). */
2075
+ blocks = input(null, /* @ts-ignore */
2076
+ ...(ngDevMode ? [{ debugName: "blocks" }] : /* istanbul ignore next */ []));
2077
+ items = computed(() => this.blocks() ?? this.value()?.blocks ?? [], /* @ts-ignore */
2078
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
2079
+ /** Sanitised rich-text HTML for `[innerHTML]` binding. */
2080
+ safeHtml(html) {
2081
+ return sanitizeInlineHtml(String(html ?? ''));
2082
+ }
2083
+ listItems(block) {
2084
+ const items = block.data['items'];
2085
+ return Array.isArray(items) ? items : [];
2086
+ }
2087
+ headingLevel(block) {
2088
+ const l = block.data['level'];
2089
+ return [1, 2, 3, 4].includes(l) ? l : 2;
2090
+ }
2091
+ isImageSafe(block) {
2092
+ const src = String(block.data['src'] ?? '');
2093
+ return !!src && mkIsSafeUrl(src);
2094
+ }
2095
+ buttonHref(block) {
2096
+ const href = String(block.data['href'] ?? '');
2097
+ return mkIsSafeUrl(href) && href ? href : '#';
2098
+ }
2099
+ paddingTop(block) {
2100
+ const ratio = Number(block.data['aspectRatio']) || 16 / 9;
2101
+ return `${100 / ratio}%`;
2102
+ }
2103
+ /** Trusts only a provider-transformed https embed URL — see class doc. */
2104
+ embedSrc(block) {
2105
+ const src = String(block.data['embedUrl'] ?? '');
2106
+ if (!src || !/^https:\/\//i.test(src) || !mkIsSafeUrl(src))
2107
+ return null;
2108
+ let trusted = this.safeSrcCache.get(src);
2109
+ if (!trusted) {
2110
+ trusted = this.sanitizer.bypassSecurityTrustResourceUrl(src);
2111
+ this.safeSrcCache.set(src, trusted);
2112
+ }
2113
+ return trusted;
2114
+ }
2115
+ fallbackUrl(block) {
2116
+ const url = String(block.data['url'] ?? '');
2117
+ return mkIsSafeUrl(url) && url ? url : null;
2118
+ }
2119
+ gridColumns(block) {
2120
+ const ratio = String(block.data['ratio'] ?? '');
2121
+ const parts = ratio.split('-').map((n) => parseFloat(n)).filter((n) => n > 0);
2122
+ if (parts.length >= 2)
2123
+ return parts.map((n) => `${n}fr`).join(' ');
2124
+ return `repeat(${block.children?.length ?? 2}, 1fr)`;
2125
+ }
2126
+ gridGap(block) {
2127
+ const g = Math.max(0, Math.min(8, Number(block.data['gap']) || 4));
2128
+ return `var(--mk-space-${g})`;
2129
+ }
2130
+ justifyValue(block) {
2131
+ const map = {
2132
+ start: 'flex-start', center: 'center', end: 'flex-end', between: 'space-between',
2133
+ };
2134
+ return map[String(block.data['justify'] ?? 'start')] ?? 'flex-start';
2135
+ }
2136
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockRenderer, deps: [], target: i0.ɵɵFactoryTarget.Component });
2137
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkBlockRenderer, isStandalone: true, selector: "mk-block-renderer", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, blocks: { classPropertyName: "blocks", publicName: "blocks", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "mk-block-renderer" }, ngImport: i0, template: "@for (block of items(); track block.id) {\n @switch (block.type) {\n @case ('paragraph') {\n <p class=\"mk-r-paragraph\" [innerHTML]=\"safeHtml(block.data['html'])\"></p>\n }\n @case ('heading') {\n @switch (headingLevel(block)) {\n @case (1) { <h1 class=\"mk-r-heading\" [innerHTML]=\"safeHtml(block.data['html'])\"></h1> }\n @case (2) { <h2 class=\"mk-r-heading\" [innerHTML]=\"safeHtml(block.data['html'])\"></h2> }\n @case (3) { <h3 class=\"mk-r-heading\" [innerHTML]=\"safeHtml(block.data['html'])\"></h3> }\n @case (4) { <h4 class=\"mk-r-heading\" [innerHTML]=\"safeHtml(block.data['html'])\"></h4> }\n }\n }\n @case ('list') {\n @if (block.data['ordered']) {\n <ol class=\"mk-r-list\">\n @for (item of listItems(block); track $index) {\n <li [innerHTML]=\"safeHtml(item)\"></li>\n }\n </ol>\n } @else {\n <ul class=\"mk-r-list\">\n @for (item of listItems(block); track $index) {\n <li [innerHTML]=\"safeHtml(item)\"></li>\n }\n </ul>\n }\n }\n @case ('quote') {\n <blockquote class=\"mk-r-quote\">\n <p [innerHTML]=\"safeHtml(block.data['html'])\"></p>\n @if (block.data['citation']) {\n <cite class=\"mk-r-cite\">{{ block.data['citation'] }}</cite>\n }\n </blockquote>\n }\n @case ('code') {\n <pre class=\"mk-r-code\"><code>{{ block.data['code'] }}</code></pre>\n }\n @case ('image') {\n @if (isImageSafe(block)) {\n <figure\n class=\"mk-r-figure\"\n [class.mk-r-figure--left]=\"block.data['align'] === 'left'\"\n [class.mk-r-figure--right]=\"block.data['align'] === 'right'\"\n >\n <img\n [src]=\"block.data['src']\"\n [alt]=\"block.data['alt'] ?? ''\"\n [style.width.%]=\"block.data['width'] || 100\"\n loading=\"lazy\"\n />\n @if (block.data['caption']) {\n <figcaption>{{ block.data['caption'] }}</figcaption>\n }\n </figure>\n }\n }\n @case ('button') {\n <div\n class=\"mk-r-cta-wrap\"\n [class.mk-r-cta-wrap--center]=\"block.data['align'] === 'center'\"\n [class.mk-r-cta-wrap--right]=\"block.data['align'] === 'right'\"\n >\n <a\n class=\"mk-cta\"\n [class.mk-cta--soft]=\"block.data['variant'] === 'soft'\"\n [class.mk-cta--outline]=\"block.data['variant'] === 'outline'\"\n [attr.data-tone]=\"block.data['tone'] ?? 'primary'\"\n [href]=\"buttonHref(block)\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {{ block.data['label'] || i18n.blockEditor.blockButton }}\n </a>\n </div>\n }\n @case ('divider') {\n <hr class=\"mk-r-divider\" />\n }\n @case ('embed') {\n @if (embedSrc(block); as src) {\n <div class=\"mk-r-embed\" [style.padding-top]=\"paddingTop(block)\">\n <iframe\n [src]=\"src\"\n [title]=\"i18n.blockEditor.embeddedContent\"\n loading=\"lazy\"\n referrerpolicy=\"strict-origin-when-cross-origin\"\n sandbox=\"allow-scripts allow-same-origin allow-presentation allow-popups\"\n allowfullscreen\n ></iframe>\n </div>\n } @else if (fallbackUrl(block); as url) {\n <p class=\"mk-r-embed-fallback\">\n <a [href]=\"url\" target=\"_blank\" rel=\"noopener noreferrer\">{{ url }}</a>\n </p>\n }\n }\n @case ('columns') {\n <div\n class=\"mk-columns\"\n [style.grid-template-columns]=\"gridColumns(block)\"\n [style.gap]=\"gridGap(block)\"\n [style.align-items]=\"block.data['align'] ?? 'stretch'\"\n [style.justify-content]=\"justifyValue(block)\"\n >\n @for (col of block.children ?? []; track col.id) {\n <div class=\"mk-column\">\n <mk-block-renderer [blocks]=\"col.children ?? []\" />\n </div>\n }\n </div>\n }\n }\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;color:var(--mk-text);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-relaxed)}.mk-r-paragraph{margin:0 0 var(--mk-space-4)}.mk-r-heading{margin:var(--mk-space-6) 0 var(--mk-space-3);line-height:var(--mk-line-height-snug);color:var(--mk-text)}h1.mk-r-heading{font-size:var(--mk-font-size-3xl);font-weight:var(--mk-font-weight-bold)}h2.mk-r-heading{font-size:var(--mk-font-size-2xl);font-weight:var(--mk-font-weight-bold)}h3.mk-r-heading{font-size:var(--mk-font-size-xl);font-weight:var(--mk-font-weight-semibold)}h4.mk-r-heading{font-size:var(--mk-font-size-lg);font-weight:var(--mk-font-weight-semibold)}.mk-r-paragraph a,.mk-r-list a,.mk-r-quote a{color:var(--mk-primary);text-decoration:underline}.mk-r-paragraph code,.mk-r-list code{font-family:var(--mk-font-mono);font-size:.9em;padding:.1em .3em;border-radius:var(--mk-radius-xs);background:var(--mk-code-bg)}.mk-r-list{margin:0 0 var(--mk-space-4);padding-inline-start:var(--mk-space-6)}.mk-r-list li{margin:var(--mk-space-1) 0}.mk-r-quote{margin:0 0 var(--mk-space-4);padding-inline-start:var(--mk-space-4);border-inline-start:4px solid var(--mk-primary);font-size:var(--mk-font-size-lg);font-style:italic}.mk-r-quote p{margin:0}.mk-r-cite{display:block;margin-top:var(--mk-space-2);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm);font-style:normal}.mk-r-code{margin:0 0 var(--mk-space-4);padding:var(--mk-space-3);border-radius:var(--mk-radius-md);background:var(--mk-code-bg);color:var(--mk-text);font-family:var(--mk-font-mono);font-size:var(--mk-font-size-sm);overflow-x:auto}.mk-r-figure{margin:0 0 var(--mk-space-4);text-align:center}.mk-r-figure--left{text-align:left}.mk-r-figure--right{text-align:right}.mk-r-figure img{max-width:100%;height:auto;border-radius:var(--mk-radius-md)}.mk-r-figure figcaption{margin-top:var(--mk-space-2);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}.mk-r-divider{height:0;margin:var(--mk-space-6) 0;border:none;border-top:var(--mk-border-width-strong) solid var(--mk-border-strong)}.mk-r-cta-wrap{margin:0 0 var(--mk-space-4);text-align:start}.mk-r-cta-wrap--center{text-align:center}.mk-r-cta-wrap--right{text-align:right}.mk-cta{--_main: var(--mk-primary);--_hover: var(--mk-primary-hover);--_contrast: var(--mk-primary-contrast);--_subtle: var(--mk-primary-subtle);--_subtle-text: var(--mk-primary-subtle-text);display:inline-flex;align-items:center;justify-content:center;height:var(--mk-control-height-md);padding:0 var(--mk-space-5);border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);background:var(--_main);color:var(--_contrast);font-weight:var(--mk-font-weight-medium);text-decoration:none;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-cta:hover{background:var(--_hover)}.mk-cta[data-tone=success]{--_main: var(--mk-success);--_hover: var(--mk-success-hover);--_contrast: var(--mk-success-contrast);--_subtle: var(--mk-success-subtle);--_subtle-text: var(--mk-success-subtle-text)}.mk-cta[data-tone=warning]{--_main: var(--mk-warning);--_hover: var(--mk-warning-hover);--_contrast: var(--mk-warning-contrast);--_subtle: var(--mk-warning-subtle);--_subtle-text: var(--mk-warning-subtle-text)}.mk-cta[data-tone=danger]{--_main: var(--mk-danger);--_hover: var(--mk-danger-hover);--_contrast: var(--mk-danger-contrast);--_subtle: var(--mk-danger-subtle);--_subtle-text: var(--mk-danger-subtle-text)}.mk-cta[data-tone=info]{--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text)}.mk-cta[data-tone=neutral]{--_main: var(--mk-surface-inverse);--_hover: var(--mk-surface-inverse);--_contrast: var(--mk-text-inverse);--_subtle: var(--mk-neutral-subtle);--_subtle-text: var(--mk-neutral-subtle-text)}.mk-cta--soft{background:var(--_subtle);color:var(--_subtle-text)}.mk-cta--outline{background:transparent;color:var(--_subtle-text);border-color:color-mix(in srgb,var(--_main) 42%,var(--mk-border-strong))}.mk-cta:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-r-embed{position:relative;width:100%;margin:0 0 var(--mk-space-4);overflow:hidden;border-radius:var(--mk-radius-md);background:var(--mk-surface-3)}.mk-r-embed iframe{position:absolute;inset:0;width:100%;height:100%;border:0}.mk-r-embed-fallback a{color:var(--mk-primary)}.mk-columns{display:grid;margin:0 0 var(--mk-space-4)}.mk-column{min-width:0}@media(max-width:640px){.mk-columns{grid-template-columns:1fr!important}}\n"], dependencies: [{ kind: "component", type: MkBlockRenderer, selector: "mk-block-renderer", inputs: ["value", "blocks"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2138
+ }
2139
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockRenderer, decorators: [{
2140
+ type: Component,
2141
+ args: [{ selector: 'mk-block-renderer', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'mk-block-renderer' }, template: "@for (block of items(); track block.id) {\n @switch (block.type) {\n @case ('paragraph') {\n <p class=\"mk-r-paragraph\" [innerHTML]=\"safeHtml(block.data['html'])\"></p>\n }\n @case ('heading') {\n @switch (headingLevel(block)) {\n @case (1) { <h1 class=\"mk-r-heading\" [innerHTML]=\"safeHtml(block.data['html'])\"></h1> }\n @case (2) { <h2 class=\"mk-r-heading\" [innerHTML]=\"safeHtml(block.data['html'])\"></h2> }\n @case (3) { <h3 class=\"mk-r-heading\" [innerHTML]=\"safeHtml(block.data['html'])\"></h3> }\n @case (4) { <h4 class=\"mk-r-heading\" [innerHTML]=\"safeHtml(block.data['html'])\"></h4> }\n }\n }\n @case ('list') {\n @if (block.data['ordered']) {\n <ol class=\"mk-r-list\">\n @for (item of listItems(block); track $index) {\n <li [innerHTML]=\"safeHtml(item)\"></li>\n }\n </ol>\n } @else {\n <ul class=\"mk-r-list\">\n @for (item of listItems(block); track $index) {\n <li [innerHTML]=\"safeHtml(item)\"></li>\n }\n </ul>\n }\n }\n @case ('quote') {\n <blockquote class=\"mk-r-quote\">\n <p [innerHTML]=\"safeHtml(block.data['html'])\"></p>\n @if (block.data['citation']) {\n <cite class=\"mk-r-cite\">{{ block.data['citation'] }}</cite>\n }\n </blockquote>\n }\n @case ('code') {\n <pre class=\"mk-r-code\"><code>{{ block.data['code'] }}</code></pre>\n }\n @case ('image') {\n @if (isImageSafe(block)) {\n <figure\n class=\"mk-r-figure\"\n [class.mk-r-figure--left]=\"block.data['align'] === 'left'\"\n [class.mk-r-figure--right]=\"block.data['align'] === 'right'\"\n >\n <img\n [src]=\"block.data['src']\"\n [alt]=\"block.data['alt'] ?? ''\"\n [style.width.%]=\"block.data['width'] || 100\"\n loading=\"lazy\"\n />\n @if (block.data['caption']) {\n <figcaption>{{ block.data['caption'] }}</figcaption>\n }\n </figure>\n }\n }\n @case ('button') {\n <div\n class=\"mk-r-cta-wrap\"\n [class.mk-r-cta-wrap--center]=\"block.data['align'] === 'center'\"\n [class.mk-r-cta-wrap--right]=\"block.data['align'] === 'right'\"\n >\n <a\n class=\"mk-cta\"\n [class.mk-cta--soft]=\"block.data['variant'] === 'soft'\"\n [class.mk-cta--outline]=\"block.data['variant'] === 'outline'\"\n [attr.data-tone]=\"block.data['tone'] ?? 'primary'\"\n [href]=\"buttonHref(block)\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {{ block.data['label'] || i18n.blockEditor.blockButton }}\n </a>\n </div>\n }\n @case ('divider') {\n <hr class=\"mk-r-divider\" />\n }\n @case ('embed') {\n @if (embedSrc(block); as src) {\n <div class=\"mk-r-embed\" [style.padding-top]=\"paddingTop(block)\">\n <iframe\n [src]=\"src\"\n [title]=\"i18n.blockEditor.embeddedContent\"\n loading=\"lazy\"\n referrerpolicy=\"strict-origin-when-cross-origin\"\n sandbox=\"allow-scripts allow-same-origin allow-presentation allow-popups\"\n allowfullscreen\n ></iframe>\n </div>\n } @else if (fallbackUrl(block); as url) {\n <p class=\"mk-r-embed-fallback\">\n <a [href]=\"url\" target=\"_blank\" rel=\"noopener noreferrer\">{{ url }}</a>\n </p>\n }\n }\n @case ('columns') {\n <div\n class=\"mk-columns\"\n [style.grid-template-columns]=\"gridColumns(block)\"\n [style.gap]=\"gridGap(block)\"\n [style.align-items]=\"block.data['align'] ?? 'stretch'\"\n [style.justify-content]=\"justifyValue(block)\"\n >\n @for (col of block.children ?? []; track col.id) {\n <div class=\"mk-column\">\n <mk-block-renderer [blocks]=\"col.children ?? []\" />\n </div>\n }\n </div>\n }\n }\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;color:var(--mk-text);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-relaxed)}.mk-r-paragraph{margin:0 0 var(--mk-space-4)}.mk-r-heading{margin:var(--mk-space-6) 0 var(--mk-space-3);line-height:var(--mk-line-height-snug);color:var(--mk-text)}h1.mk-r-heading{font-size:var(--mk-font-size-3xl);font-weight:var(--mk-font-weight-bold)}h2.mk-r-heading{font-size:var(--mk-font-size-2xl);font-weight:var(--mk-font-weight-bold)}h3.mk-r-heading{font-size:var(--mk-font-size-xl);font-weight:var(--mk-font-weight-semibold)}h4.mk-r-heading{font-size:var(--mk-font-size-lg);font-weight:var(--mk-font-weight-semibold)}.mk-r-paragraph a,.mk-r-list a,.mk-r-quote a{color:var(--mk-primary);text-decoration:underline}.mk-r-paragraph code,.mk-r-list code{font-family:var(--mk-font-mono);font-size:.9em;padding:.1em .3em;border-radius:var(--mk-radius-xs);background:var(--mk-code-bg)}.mk-r-list{margin:0 0 var(--mk-space-4);padding-inline-start:var(--mk-space-6)}.mk-r-list li{margin:var(--mk-space-1) 0}.mk-r-quote{margin:0 0 var(--mk-space-4);padding-inline-start:var(--mk-space-4);border-inline-start:4px solid var(--mk-primary);font-size:var(--mk-font-size-lg);font-style:italic}.mk-r-quote p{margin:0}.mk-r-cite{display:block;margin-top:var(--mk-space-2);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm);font-style:normal}.mk-r-code{margin:0 0 var(--mk-space-4);padding:var(--mk-space-3);border-radius:var(--mk-radius-md);background:var(--mk-code-bg);color:var(--mk-text);font-family:var(--mk-font-mono);font-size:var(--mk-font-size-sm);overflow-x:auto}.mk-r-figure{margin:0 0 var(--mk-space-4);text-align:center}.mk-r-figure--left{text-align:left}.mk-r-figure--right{text-align:right}.mk-r-figure img{max-width:100%;height:auto;border-radius:var(--mk-radius-md)}.mk-r-figure figcaption{margin-top:var(--mk-space-2);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}.mk-r-divider{height:0;margin:var(--mk-space-6) 0;border:none;border-top:var(--mk-border-width-strong) solid var(--mk-border-strong)}.mk-r-cta-wrap{margin:0 0 var(--mk-space-4);text-align:start}.mk-r-cta-wrap--center{text-align:center}.mk-r-cta-wrap--right{text-align:right}.mk-cta{--_main: var(--mk-primary);--_hover: var(--mk-primary-hover);--_contrast: var(--mk-primary-contrast);--_subtle: var(--mk-primary-subtle);--_subtle-text: var(--mk-primary-subtle-text);display:inline-flex;align-items:center;justify-content:center;height:var(--mk-control-height-md);padding:0 var(--mk-space-5);border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);background:var(--_main);color:var(--_contrast);font-weight:var(--mk-font-weight-medium);text-decoration:none;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-cta:hover{background:var(--_hover)}.mk-cta[data-tone=success]{--_main: var(--mk-success);--_hover: var(--mk-success-hover);--_contrast: var(--mk-success-contrast);--_subtle: var(--mk-success-subtle);--_subtle-text: var(--mk-success-subtle-text)}.mk-cta[data-tone=warning]{--_main: var(--mk-warning);--_hover: var(--mk-warning-hover);--_contrast: var(--mk-warning-contrast);--_subtle: var(--mk-warning-subtle);--_subtle-text: var(--mk-warning-subtle-text)}.mk-cta[data-tone=danger]{--_main: var(--mk-danger);--_hover: var(--mk-danger-hover);--_contrast: var(--mk-danger-contrast);--_subtle: var(--mk-danger-subtle);--_subtle-text: var(--mk-danger-subtle-text)}.mk-cta[data-tone=info]{--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text)}.mk-cta[data-tone=neutral]{--_main: var(--mk-surface-inverse);--_hover: var(--mk-surface-inverse);--_contrast: var(--mk-text-inverse);--_subtle: var(--mk-neutral-subtle);--_subtle-text: var(--mk-neutral-subtle-text)}.mk-cta--soft{background:var(--_subtle);color:var(--_subtle-text)}.mk-cta--outline{background:transparent;color:var(--_subtle-text);border-color:color-mix(in srgb,var(--_main) 42%,var(--mk-border-strong))}.mk-cta:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-r-embed{position:relative;width:100%;margin:0 0 var(--mk-space-4);overflow:hidden;border-radius:var(--mk-radius-md);background:var(--mk-surface-3)}.mk-r-embed iframe{position:absolute;inset:0;width:100%;height:100%;border:0}.mk-r-embed-fallback a{color:var(--mk-primary)}.mk-columns{display:grid;margin:0 0 var(--mk-space-4)}.mk-column{min-width:0}@media(max-width:640px){.mk-columns{grid-template-columns:1fr!important}}\n"] }]
2142
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], blocks: [{ type: i0.Input, args: [{ isSignal: true, alias: "blocks", required: false }] }] } });
2143
+
2144
+ /**
2145
+ * BLOCK EDITOR group barrel for @mk-kit/ui.
2146
+ *
2147
+ * Public surface for the Gutenberg-style block content editor and its
2148
+ * read-only renderer. Built-in block edit components stay internal — consumers
2149
+ * interact through the editor, the registry and the serializers.
2150
+ */
2151
+ // Editor + renderer components
2152
+
2153
+ /**
2154
+ * Generated bundle index. Do not edit.
2155
+ */
2156
+
2157
+ export { MK_BLOCK_DEFINITIONS, MK_BLOCK_DOCUMENT_VERSION, MK_BLOCK_EMBED_PROVIDERS, MK_BLOCK_UPLOAD_HANDLER, MK_DEFAULT_BLOCKS, MK_DEFAULT_EMBED_PROVIDERS, MkBlockEditor, MkBlockRenderer, mkBlockId, mkBlocksToHtml, mkBlocksToText, mkCloneBlock, mkEmptyDocument, mkEscapeAttr, mkEscapeHtml, mkHtmlToBlocks, mkIsSafeUrl, mkMergeBlockDefinitions };
2158
+ //# sourceMappingURL=mk-kit-ui-block-editor.mjs.map