@gilav21/shadcn-angular-rte 0.1.1 → 0.2.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.
@@ -72,6 +72,494 @@ function prefersReducedMotion() {
72
72
  return globalThis.window?.matchMedia('(prefers-reduced-motion: reduce)').matches ?? false;
73
73
  }
74
74
 
75
+ /**
76
+ * The syntax tokenizer, shared by every surface that shows code.
77
+ *
78
+ * It began inside `ui-code-block` as private methods over a private pattern
79
+ * table. The rich text editor and its read-only view need exactly the same
80
+ * tokens — `rich-text-markdown.service.ts` has always stored and round-tripped
81
+ * a code block's language, and the sanitizer has always allowed the
82
+ * `language-*` and `token-*` classes — so the tokenizer moved here rather than
83
+ * being written a second time. Rendering the whole `ui-code-block` COMPONENT
84
+ * inside a document was the alternative and is wrong: it brings a copy button,
85
+ * a line-number gutter, fold chevrons and a hard-coded dark surface into prose,
86
+ * and none of that can live inside a contenteditable.
87
+ *
88
+ * Two rules the callers rely on:
89
+ *
90
+ * 1. **A line is tokenized on its own.** Highlighting never spans lines, so a
91
+ * multi-line string or block comment is coloured per line. That is what lets
92
+ * the editor re-highlight one line without re-reading the block.
93
+ * 2. **An unknown language is not a guess.** {@link languagePatternsFor}
94
+ * returns `null` for a language it does not know, and for none at all.
95
+ * `ui-code-block` is a code viewer and keeps its documented TypeScript
96
+ * fallback, but a DOCUMENT is mostly prose: a fence with no language, or one
97
+ * tagged `text`, `log` or `diff`, would otherwise be coloured by TypeScript
98
+ * rules, inventing keywords in the reader's notes.
99
+ */
100
+ /**
101
+ * Build a word-boundary keyword matcher from a list. Constructed at runtime so
102
+ * the (legitimately long) per-language keyword set is a maintainable array
103
+ * rather than one giant, hard-to-read regex literal.
104
+ */
105
+ const keywordPattern = (words) => new RegExp(String.raw `\b(${words.join('|')})\b`);
106
+ const TS_KEYWORDS = ['const', 'let', 'var', 'function', 'class', 'import', 'from', 'return', 'if', 'else', 'for', 'while', 'export', 'interface', 'type', 'public', 'private', 'protected', 'implements', 'extends', 'new', 'this', 'true', 'false', 'null', 'undefined', 'void', 'async', 'await'];
107
+ const JS_KEYWORDS = ['const', 'let', 'var', 'function', 'class', 'import', 'from', 'return', 'if', 'else', 'for', 'while', 'export', 'new', 'this', 'true', 'false', 'null', 'undefined', 'void', 'async', 'await'];
108
+ const PYTHON_KEYWORDS = ['def', 'class', 'import', 'from', 'if', 'else', 'elif', 'for', 'while', 'return', 'try', 'except', 'finally', 'with', 'as', 'pass', 'break', 'continue', 'lambda', 'yield', 'async', 'await', 'True', 'False', 'None'];
109
+ const JAVA_KEYWORDS = ['public', 'private', 'protected', 'class', 'interface', 'enum', 'extends', 'implements', 'new', 'this', 'super', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'default', 'break', 'continue', 'try', 'catch', 'finally', 'throw', 'throws', 'import', 'package', 'void', 'int', 'boolean', 'char', 'byte', 'short', 'long', 'float', 'double', 'static', 'final', 'abstract', 'synchronized', 'volatile', 'transient', 'native', 'strictfp', 'instanceof', 'null', 'true', 'false'];
110
+ const CSHARP_KEYWORDS = ['public', 'private', 'protected', 'internal', 'class', 'struct', 'record', 'interface', 'enum', 'delegate', 'event', 'void', 'int', 'string', 'bool', 'var', 'async', 'await', 'Task', 'return', 'if', 'else', 'for', 'foreach', 'while', 'do', 'switch', 'case', 'default', 'break', 'continue', 'try', 'catch', 'finally', 'throw', 'new', 'this', 'base', 'using', 'namespace', 'static', 'readonly', 'const', 'override', 'virtual', 'abstract', 'sealed', 'get', 'set', 'value'];
111
+ /** The built-in language rules, keyed by lowercase language name. */
112
+ const LANGUAGE_PATTERNS = {
113
+ typescript: [
114
+ { type: 'comment', regex: /\/\/.*/ },
115
+ { type: 'string', regex: /(["'])(?:(?=(\\?))\2.)*?\1/ },
116
+ { type: 'keyword', regex: keywordPattern(TS_KEYWORDS) },
117
+ { type: 'number', regex: /\b\d+\b/ },
118
+ { type: 'function', regex: /\b[a-zA-Z_$]\w*(?=\()/ },
119
+ ],
120
+ javascript: [
121
+ { type: 'comment', regex: /\/\/.*/ },
122
+ { type: 'string', regex: /(["'])(?:(?=(\\?))\2.)*?\1/ },
123
+ { type: 'keyword', regex: keywordPattern(JS_KEYWORDS) },
124
+ { type: 'number', regex: /\b\d+\b/ },
125
+ { type: 'function', regex: /\b[a-zA-Z_$]\w*(?=\()/ },
126
+ ],
127
+ python: [
128
+ { type: 'comment', regex: /#.*/ },
129
+ { type: 'string', regex: /(["'])(?:(?=(\\?))\2.)*?\1/ },
130
+ { type: 'decorator', regex: /@[\w.]+/ },
131
+ { type: 'keyword', regex: keywordPattern(PYTHON_KEYWORDS) },
132
+ { type: 'number', regex: /\b\d+\b/ },
133
+ { type: 'function', regex: /\b[a-zA-Z_]\w*(?=\()/ },
134
+ ],
135
+ java: [
136
+ { type: 'comment', regex: /\/\/.*/ },
137
+ { type: 'string', regex: /"(?:[^"\\]|\\.)*"/ },
138
+ { type: 'keyword', regex: keywordPattern(JAVA_KEYWORDS) },
139
+ { type: 'decorator', regex: /@\w+/ },
140
+ { type: 'number', regex: /\b\d+\b/ },
141
+ { type: 'function', regex: /\b[a-zA-Z_$]\w*(?=\()/ },
142
+ ],
143
+ html: [
144
+ { type: 'comment', regex: /<!--[\s\S]*?-->/ },
145
+ { type: 'tag', regex: /<\/?[a-z0-9-]+/i },
146
+ { type: 'attr', regex: /[a-z0-9-]{1,256}(?==)/i },
147
+ { type: 'string', regex: /"(?:[^"\\]|\\.)*"/ },
148
+ ],
149
+ xml: [
150
+ { type: 'comment', regex: /<!--[\s\S]*?-->/ },
151
+ { type: 'decorator', regex: /<\?xml[\s\S]*?\?>|<!\[CDATA\[[\s\S]*?\]\]>|<!DOCTYPE[^>]*>/i },
152
+ { type: 'string', regex: /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/ },
153
+ { type: 'tag', regex: /<\/?[a-zA-Z_][\w.\-:]{0,256}/ },
154
+ { type: 'attr', regex: /[a-zA-Z_][\w.\-:]{0,256}(?==)/ },
155
+ { type: 'keyword', regex: /&(?:[a-zA-Z]+|#\d+|#x[0-9a-fA-F]+);/ },
156
+ { type: 'number', regex: /\b\d+\b/ },
157
+ ],
158
+ css: [
159
+ { type: 'comment', regex: /\/\*[\s\S]*?\*\// },
160
+ { type: 'selector', regex: /[.#]?[a-zA-Z0-9_-]{1,256}(?=\{)/ },
161
+ { type: 'property', regex: /[a-z0-9-]{1,256}(?=:)/i },
162
+ { type: 'string', regex: /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/ },
163
+ { type: 'number', regex: /\b\d+(?:px|rem|em|%|vh|vw|s|ms|deg)?\b/ },
164
+ ],
165
+ json: [
166
+ { type: 'function', regex: /"[^"]+":/ },
167
+ { type: 'string', regex: /"(?:[^"\\]|\\.)*"/ },
168
+ { type: 'keyword', regex: /\b(true|false|null)\b/ },
169
+ { type: 'number', regex: /\b\d+\b/ },
170
+ ],
171
+ csharp: [
172
+ { type: 'comment', regex: /\/\/.*/ },
173
+ { type: 'string', regex: /"(?:[^"\\]|\\.)*"/ },
174
+ { type: 'decorator', regex: /\[[a-zA-Z]\w*\]/ },
175
+ { type: 'keyword', regex: keywordPattern(CSHARP_KEYWORDS) },
176
+ { type: 'number', regex: /\b\d+\b/ },
177
+ { type: 'function', regex: /\b[a-zA-Z_$]\w*(?=\()/ },
178
+ ],
179
+ yaml: [
180
+ { type: 'comment', regex: /#.*/ },
181
+ { type: 'string', regex: /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/ },
182
+ { type: 'attr', regex: /[a-zA-Z0-9_-]{1,256}(?=:)/ },
183
+ { type: 'keyword', regex: /\b(true|false|null|yes|no|on|off)\b/ },
184
+ { type: 'number', regex: /\b\d+(\.\d+)?\b/ },
185
+ ],
186
+ bash: [
187
+ { type: 'comment', regex: /#.*/ },
188
+ { type: 'string', regex: /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/ },
189
+ { type: 'keyword', regex: /\b(echo|ls|cd|pwd|mkdir|rm|cp|mv|touch|cat|grep|open|ssh|git|npm|node|ng|sudo|chmod|chown)\b/ },
190
+ { type: 'decorator', regex: /\$\w+/ },
191
+ ],
192
+ };
193
+ /**
194
+ * Spellings an author writes in a fence that mean a language the table already
195
+ * knows. A markdown fence carries whatever the author typed, and "ts" is far
196
+ * more common than "typescript".
197
+ */
198
+ const LANGUAGE_ALIASES = {
199
+ ts: 'typescript',
200
+ tsx: 'typescript',
201
+ js: 'javascript',
202
+ jsx: 'javascript',
203
+ mjs: 'javascript',
204
+ cjs: 'javascript',
205
+ py: 'python',
206
+ 'c#': 'csharp',
207
+ cs: 'csharp',
208
+ yml: 'yaml',
209
+ sh: 'bash',
210
+ shell: 'bash',
211
+ zsh: 'bash',
212
+ htm: 'html',
213
+ svg: 'xml',
214
+ scss: 'css',
215
+ };
216
+ /**
217
+ * The rules for a language, or `null` when it is one this module does not know.
218
+ *
219
+ * `null` is a real answer, not a failure: see rule 2 in the module comment. The
220
+ * caller decides what to do with it — `ui-code-block` substitutes TypeScript,
221
+ * a document leaves the code alone.
222
+ */
223
+ function languagePatternsFor(language) {
224
+ if (!language)
225
+ return null;
226
+ const key = language.toLowerCase();
227
+ return LANGUAGE_PATTERNS[LANGUAGE_ALIASES[key] ?? key] ?? null;
228
+ }
229
+ /**
230
+ * Tokenize ONE line. See rule 1 in the module comment: highlighting never spans
231
+ * lines, so the caller splits and calls this per line.
232
+ */
233
+ function tokenizeLine(line, patterns) {
234
+ if (!line)
235
+ return [];
236
+ const tokens = [];
237
+ let cursor = 0;
238
+ while (cursor < line.length) {
239
+ const remaining = line.slice(cursor);
240
+ const best = findBestMatch(remaining, patterns);
241
+ if (best?.index === 0) {
242
+ tokens.push({ type: best.type, text: best.text });
243
+ cursor += best.text.length;
244
+ }
245
+ else if (best) {
246
+ tokens.push({ type: 'text', text: remaining.slice(0, best.index) });
247
+ cursor += best.index;
248
+ }
249
+ else {
250
+ tokens.push({ type: 'text', text: remaining });
251
+ cursor += remaining.length;
252
+ }
253
+ }
254
+ return tokens;
255
+ }
256
+ function findBestMatch(remaining, patterns) {
257
+ let best = null;
258
+ for (const pattern of patterns) {
259
+ const match = pattern.regex.exec(remaining);
260
+ if (match && (best === null || match.index < best.index)) {
261
+ best = { type: pattern.type, text: match[0], index: match.index };
262
+ }
263
+ }
264
+ return best;
265
+ }
266
+ /**
267
+ * The class a document's highlighter marks a token with.
268
+ *
269
+ * `token` plus `token-<type>`, which `rich-text-sanitizer.service.ts` has
270
+ * allowed since before there was a highlighter: markup coloured elsewhere and
271
+ * pasted in already survived sanitization with these exact classes, so the
272
+ * colours this produces and the colours a paste brings are one set of rules.
273
+ * The colours themselves live in RICH_TEXT_PROSE_CLASSES, where the editor and
274
+ * the view share them and light and dark each get their own.
275
+ *
276
+ * A `text` run gets no class at all: it is the code's own colour, and a span
277
+ * per unstyled run would triple the node count for nothing.
278
+ */
279
+ function tokenClassFor(type) {
280
+ return type === 'text' ? null : `token token-${type}`;
281
+ }
282
+ /**
283
+ * A code block's text with its line breaks, whichever form each one takes.
284
+ *
285
+ * The rule every reader of a code block has to follow: a line break is a line
286
+ * break, whether it is a `\n` character or a `<br>` element. `textContent`
287
+ * does not follow it — a `<br>` contributes nothing — and Shift+Enter, the
288
+ * editor's key for a new line inside a block, inserts `<br>`. So a function
289
+ * typed line by line was saved as one line, and the highlighter, which refused
290
+ * any block holding an element, stopped colouring from the first new line on.
291
+ */
292
+ function readCodeLines(node) {
293
+ const lines = { text: '', breaks: [], trailingBr: false };
294
+ collectCodeLines(node, lines);
295
+ if (lines.breaks.at(-1) === 'br' && lines.text.endsWith('\n') && endsWithBr(node)) {
296
+ lines.text = lines.text.slice(0, -1);
297
+ lines.breaks.pop();
298
+ lines.trailingBr = true;
299
+ }
300
+ return lines;
301
+ }
302
+ function collectCodeLines(node, into) {
303
+ for (const child of Array.from(node.childNodes)) {
304
+ if (child.nodeType === Node.TEXT_NODE) {
305
+ const data = child.data;
306
+ into.text += data;
307
+ into.breaks.push(...Array.from(data.matchAll(/\n/g), () => 'newline'));
308
+ }
309
+ else if (child.nodeName === 'BR') {
310
+ into.text += '\n';
311
+ into.breaks.push('br');
312
+ }
313
+ else if (child.nodeType === Node.ELEMENT_NODE) {
314
+ collectCodeLines(child, into);
315
+ }
316
+ }
317
+ }
318
+ /** Whether the last thing in `node`, looking into nested elements, is a `<br>`. */
319
+ function endsWithBr(node) {
320
+ let last = node.lastChild;
321
+ while (last?.nodeType === Node.ELEMENT_NODE && last.nodeName !== 'BR')
322
+ last = last.lastChild;
323
+ return last?.nodeName === 'BR';
324
+ }
325
+ /** A code block's text with every line break as `\n`; see {@link readCodeLines}. */
326
+ function codeTextOf(node) {
327
+ return readCodeLines(node).text;
328
+ }
329
+ /**
330
+ * Elements that only STYLE the code text around them, which the paint reads
331
+ * through and does not keep.
332
+ *
333
+ * A browser writes these into a contenteditable on its own. Editing next to a
334
+ * coloured token, Chrome preserves the token's computed look as literal markup:
335
+ * `<font color="…">` with the keyword colour, `<b>` for its weight,
336
+ * `<span style="font-weight: 400">` to undo it. The bold, italic, underline and
337
+ * strike buttons write the rest. None of it can survive a save — a fence is
338
+ * text — and the colour of code is the highlighter's to decide. Refusing a
339
+ * block that held any of it switched highlighting off for the whole block the
340
+ * first time the browser did this, and it never came back.
341
+ */
342
+ const STYLE_ONLY_TAGS = new Set(['SPAN', 'FONT', 'B', 'STRONG', 'I', 'EM', 'U', 'S', 'STRIKE', 'DEL']);
343
+ /**
344
+ * Whether the paint can rebuild the block without losing anything.
345
+ *
346
+ * The paint is rebuilt from the block's lines, so it may only run where lines
347
+ * are all there is. A code block CAN hold more: the markdown writer keeps an
348
+ * image in one by writing the block in its tag form, and rebuilding that block
349
+ * deleted the image outright; a link is content too. A `<br>` is a line break
350
+ * and is rebuilt. Styling wrappers carry nothing but a look (see
351
+ * {@link STYLE_ONLY_TAGS}), and a span this module painted is exactly what the
352
+ * rebuild replaces.
353
+ */
354
+ function isTextOnlyCode(code) {
355
+ return Array.from(code.querySelectorAll('*')).every((element) => element.nodeName === 'BR' || STYLE_ONLY_TAGS.has(element.nodeName));
356
+ }
357
+ /**
358
+ * Marks a document's `<pre><code>` element up with token spans, in place.
359
+ *
360
+ * The element's own TEXT is the source of truth and is never changed — the
361
+ * children are rebuilt from `textContent`, so running this twice is the same as
362
+ * running it once and a block that was already highlighted re-highlights
363
+ * cleanly. That idempotence is what lets the editor re-run it on every quiet
364
+ * moment without the markup growing.
365
+ *
366
+ * Returns whether anything was painted, so a caller that has to restore a caret
367
+ * can skip the work when nothing moved.
368
+ */
369
+ function highlightCodeElement(code, language) {
370
+ const painted = paintedFragment(code, language);
371
+ if (!painted)
372
+ return false;
373
+ code.replaceChildren(painted);
374
+ return true;
375
+ }
376
+ /**
377
+ * The token markup for a block, built detached, or `null` when the block is not
378
+ * one to paint. Built apart from the element so a caller can compare it with
379
+ * what is already there BEFORE any node is replaced.
380
+ */
381
+ function paintedFragment(code, language) {
382
+ const patterns = languagePatternsFor(language);
383
+ if (!patterns || !isTextOnlyCode(code))
384
+ return null;
385
+ const { text, breaks, trailingBr } = readCodeLines(code);
386
+ if (text === '')
387
+ return null;
388
+ const doc = code.ownerDocument;
389
+ const painted = doc.createDocumentFragment();
390
+ const lines = text.split('\n');
391
+ lines.forEach((line, index) => {
392
+ // Every break is written back as its own node AND in the form it had.
393
+ // As `\n` inside the last token, a trailing blank line was dropped on
394
+ // every pass; converted from `<br>` to `\n`, the empty line a
395
+ // Shift+Enter had just opened collapsed and took the caret with it.
396
+ if (index > 0)
397
+ painted.appendChild(breaks[index - 1] === 'br' ? doc.createElement('br') : doc.createTextNode('\n'));
398
+ for (const token of tokenizeLine(line, patterns)) {
399
+ const className = tokenClassFor(token.type);
400
+ if (className === null) {
401
+ painted.appendChild(doc.createTextNode(token.text));
402
+ continue;
403
+ }
404
+ const span = doc.createElement('span');
405
+ span.className = className;
406
+ span.textContent = token.text;
407
+ painted.appendChild(span);
408
+ }
409
+ });
410
+ if (trailingBr)
411
+ painted.appendChild(doc.createElement('br'));
412
+ return painted;
413
+ }
414
+ /** A fragment's markup, for comparing with an element's `innerHTML`. */
415
+ function markupOf(fragment) {
416
+ const holder = fragment.ownerDocument.createElement('div');
417
+ holder.appendChild(fragment.cloneNode(true));
418
+ return holder.innerHTML;
419
+ }
420
+ /** The language a rendered `<pre><code>` carries: `data-language`, else the `language-*` class. */
421
+ function languageOfCodeElement(code) {
422
+ const attribute = code.dataset['language'];
423
+ if (attribute)
424
+ return attribute;
425
+ const fromClass = /(?:^|\s)language-([\w+#.-]+)/.exec(code.className);
426
+ return fromClass?.[1] ?? null;
427
+ }
428
+ /**
429
+ * Highlights every code block under `root` that names a language this module
430
+ * knows, and returns how many it painted.
431
+ */
432
+ function highlightCodeBlocks(root) {
433
+ let painted = 0;
434
+ for (const code of Array.from(root.querySelectorAll('pre > code'))) {
435
+ if (highlightCodeElement(code, languageOfCodeElement(code)))
436
+ painted++;
437
+ }
438
+ return painted;
439
+ }
440
+ /**
441
+ * Removes the spans the highlighter painted, in place.
442
+ *
443
+ * Only from a block whose language {@link languagePatternsFor} knows, which is
444
+ * exactly the set this module paints. Spans in a block it does NOT know came
445
+ * from the author — a paste from an already-highlighted source, which the
446
+ * sanitizer deliberately keeps — and are content, not decoration.
447
+ *
448
+ * The spans are unwrapped rather than the element flattened to its text: a code
449
+ * block may hold an image (the markdown writer keeps one by writing the block
450
+ * in its tag form), and `textContent = textContent` would delete it.
451
+ */
452
+ function stripCodeHighlighting(root) {
453
+ for (const code of Array.from(root.querySelectorAll('pre > code'))) {
454
+ if (!languagePatternsFor(languageOfCodeElement(code)))
455
+ continue;
456
+ for (const span of Array.from(code.querySelectorAll('span.token'))) {
457
+ span.replaceWith(...Array.from(span.childNodes));
458
+ }
459
+ code.normalize();
460
+ }
461
+ }
462
+ /**
463
+ * How many characters come before the boundary (`container`, `offset`) in
464
+ * `root`, counting each `<br>` as one — the newline it stands for.
465
+ *
466
+ * `Range.toString()` counts a `<br>` as nothing, so on a line opened with
467
+ * Shift+Enter the caret was remembered one character early per break above it,
468
+ * and every repaint walked it back towards the top of the block.
469
+ */
470
+ function characterOffsetIn(root, container, offset) {
471
+ if (!root.contains(container))
472
+ return null;
473
+ const measure = root.ownerDocument.createRange();
474
+ measure.setStart(root, 0);
475
+ measure.setEnd(container, offset);
476
+ return rawLengthOf(measure.cloneContents());
477
+ }
478
+ /** Text length with every `<br>` counted as one character, the trailing one included. */
479
+ function rawLengthOf(node) {
480
+ let length = 0;
481
+ for (const child of Array.from(node.childNodes)) {
482
+ if (child.nodeType === Node.TEXT_NODE)
483
+ length += child.data.length;
484
+ else if (child.nodeName === 'BR')
485
+ length += 1;
486
+ else
487
+ length += rawLengthOf(child);
488
+ }
489
+ return length;
490
+ }
491
+ function collapsedRange(doc, place) {
492
+ const range = doc.createRange();
493
+ place(range);
494
+ range.collapse(true);
495
+ return range;
496
+ }
497
+ /**
498
+ * A collapsed range `offset` characters into `root`, counted as
499
+ * {@link characterOffsetIn} counts them, or `null` when the block is empty.
500
+ */
501
+ function rangeAtCharacterOffset(root, offset) {
502
+ const doc = root.ownerDocument;
503
+ const walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
504
+ let seen = 0;
505
+ let tail = null;
506
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
507
+ if (node.nodeType === Node.TEXT_NODE) {
508
+ const text = node;
509
+ if (seen + text.data.length >= offset)
510
+ return collapsedRange(doc, (range) => range.setStart(text, offset - seen));
511
+ seen += text.data.length;
512
+ tail = text;
513
+ }
514
+ else if (node.nodeName === 'BR') {
515
+ const br = node;
516
+ if (seen >= offset)
517
+ return collapsedRange(doc, (range) => range.setStartBefore(br));
518
+ seen += 1;
519
+ tail = br;
520
+ }
521
+ }
522
+ const end = tail;
523
+ if (!end)
524
+ return null;
525
+ // Past a break with nothing after it: the empty line a Shift+Enter opened.
526
+ if (end.nodeType === Node.TEXT_NODE)
527
+ return collapsedRange(doc, (range) => range.setStart(end, end.data.length));
528
+ return collapsedRange(doc, (range) => range.setStartAfter(end));
529
+ }
530
+ /**
531
+ * Highlights one code block and puts the caret back where it was.
532
+ *
533
+ * The caret is remembered as a CHARACTER OFFSET into the block's text, not as a
534
+ * node and an index: every node inside the block is replaced, so a remembered
535
+ * node would be detached by the time it was restored — the caret would jump to
536
+ * the top of the document and the next keystroke would land there. The text
537
+ * itself does not change, so the offset always still means the same place.
538
+ *
539
+ * Does nothing when the block is already painted exactly as it would be, so a
540
+ * keystroke that changes no token does not move the selection at all. That
541
+ * comparison is made on the DETACHED paint, before any node is replaced: made
542
+ * after, as it first was, the nodes were already swapped, the "nothing changed"
543
+ * exit skipped the restore, and the browser dropped the caret to the start of
544
+ * the block -- so every second keystroke in a word landed at the front.
545
+ */
546
+ function highlightCodeElementKeepingCaret(code, language, selection) {
547
+ const painted = paintedFragment(code, language);
548
+ if (!painted || markupOf(painted) === code.innerHTML)
549
+ return false;
550
+ const range = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
551
+ const caret = range?.collapsed ? characterOffsetIn(code, range.startContainer, range.startOffset) : null;
552
+ code.replaceChildren(painted);
553
+ if (caret !== null && selection) {
554
+ const restored = rangeAtCharacterOffset(code, caret);
555
+ if (restored) {
556
+ selection.removeAllRanges();
557
+ selection.addRange(restored);
558
+ }
559
+ }
560
+ return true;
561
+ }
562
+
75
563
  /**
76
564
  * Counting and cutting text the way a reader sees it.
77
565
  */
@@ -829,6 +1317,32 @@ function firstTextNode(node) {
829
1317
  return null;
830
1318
  return doc.createTreeWalker(node, NodeFilter.SHOW_TEXT).nextNode();
831
1319
  }
1320
+ /**
1321
+ * How deep blocks nest before the markdown reader leaves the rest as text; see
1322
+ * parseToggleBlocks and buildBlockquote in rich-text-markdown.service.ts. The
1323
+ * reader counts a level for every details body, quote holding a nested quote
1324
+ * and list item's block, so a details block nested through a list or a quote
1325
+ * meets the same cap; a sub-list, read in its parent's pass, adds none. The
1326
+ * writer counts the same list items and every quote (see nestingDepthOf), which
1327
+ * is never less.
1328
+ *
1329
+ * It lives here rather than beside the reader so the EDITOR can respect the
1330
+ * same ceiling. A structure typed past it is unwrapped the next time the
1331
+ * document is read back, so building one is building something that will not
1332
+ * survive; the nested-blockquote input rule clamps to this for that reason.
1333
+ */
1334
+ const MAX_NESTING_DEPTH = 32;
1335
+ /** How many `<blockquote>` elements `node` sits inside, within `root`. */
1336
+ function blockquoteDepthOf(node, root) {
1337
+ let depth = 0;
1338
+ let at = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
1339
+ while (at && at !== root && root.contains(at)) {
1340
+ if (at.tagName === 'BLOCKQUOTE')
1341
+ depth++;
1342
+ at = at.parentElement;
1343
+ }
1344
+ return depth;
1345
+ }
832
1346
 
833
1347
  const SVG_CHECK_LENGTH = 256;
834
1348
  function isJpeg(b) {
@@ -2864,14 +3378,15 @@ function parseListLine(line) {
2864
3378
  // runs after this one, so the refusal has to live here.
2865
3379
  if (THEMATIC_BREAK.test(line))
2866
3380
  return null;
2867
- const taskMatch = new RegExp(/^(\s*)[-*+]\s+\[([ xX])\]\s*(\S.*|)$/).exec(line);
3381
+ const taskMatch = new RegExp(/^(\s*)([-*+])\s+\[([ xX])\]\s*(\S.*|)$/).exec(line);
2868
3382
  if (taskMatch) {
2869
- const checked = taskMatch[2] !== ' ';
3383
+ const checked = taskMatch[3] !== ' ';
2870
3384
  return {
2871
3385
  indent: taskMatch[1].length,
2872
3386
  type: 'task',
2873
- content: `[${checked ? 'x' : ' '}] ${taskMatch[3]}`,
3387
+ content: `[${checked ? 'x' : ' '}] ${taskMatch[4]}`,
2874
3388
  column: contentColumn(line, taskMatch[1].length + 1),
3389
+ marker: taskMatch[2],
2875
3390
  };
2876
3391
  }
2877
3392
  // The content group is OPTIONAL. Requiring \s+ then a non-space meant an
@@ -2881,27 +3396,56 @@ function parseListLine(line) {
2881
3396
  // when a user opens a bullet and clicks away, so this hit an everyday
2882
3397
  // keystroke; for <ol> the second list also renumbered from 1.
2883
3398
  // A rule (--- / *** / ___) still does not match: it has no space.
2884
- const ulMatch = new RegExp(/^([ \t]*)[-*+]([ \t].*)?$/).exec(line);
3399
+ const ulMatch = new RegExp(/^([ \t]*)([-*+])([ \t].*)?$/).exec(line);
2885
3400
  if (ulMatch) {
2886
3401
  return {
2887
3402
  indent: ulMatch[1].length,
2888
3403
  type: 'ul',
2889
- content: listItemContent(ulMatch[2]),
3404
+ content: listItemContent(ulMatch[3]),
2890
3405
  column: contentColumn(line, ulMatch[1].length + 1),
3406
+ marker: ulMatch[2],
2891
3407
  };
2892
3408
  }
2893
- const olMatch = new RegExp(/^([ \t]*)(\d{1,9})\.([ \t].*)?$/).exec(line);
3409
+ // Both ordered delimiters, "." and ")". CommonMark has always allowed the
3410
+ // second, and the writer needs it to end one ordered list before the next.
3411
+ const olMatch = new RegExp(/^([ \t]*)(\d{1,9})([.)])([ \t].*)?$/).exec(line);
2894
3412
  if (olMatch) {
2895
3413
  return {
2896
3414
  indent: olMatch[1].length,
2897
3415
  type: 'ol',
2898
- content: listItemContent(olMatch[3]),
3416
+ content: listItemContent(olMatch[4]),
2899
3417
  column: contentColumn(line, olMatch[1].length + olMatch[2].length + 1),
2900
3418
  number: Number.parseInt(olMatch[2], 10),
3419
+ marker: olMatch[3],
2901
3420
  };
2902
3421
  }
2903
3422
  return null;
2904
3423
  }
3424
+ /** The two markers each kind of list alternates between; see {@link listMarkerFor}. */
3425
+ const UL_MARKERS = ['-', '*'];
3426
+ const OL_MARKERS = ['.', ')'];
3427
+ /** The previous sibling when it is a list of the same kind, so the two would merge. */
3428
+ function adjacentSiblingList(element) {
3429
+ const previous = element.previousElementSibling;
3430
+ return previous?.tagName === element.tagName ? previous : null;
3431
+ }
3432
+ /**
3433
+ * The marker a list is written with, alternating along a run of sibling lists.
3434
+ *
3435
+ * A blank line does not end a list -- CommonMark ends one at a marker CHANGE --
3436
+ * so two `<ol>`s side by side were written `1. a` / `1. b` and read back as ONE
3437
+ * list with b renumbered to 2. Alternating the bullet character (for an ordered
3438
+ * list, the delimiter after the number) puts a boundary the reader honours
3439
+ * between every adjacent pair, and leaves a list with no list beside it written
3440
+ * exactly as before.
3441
+ */
3442
+ function listMarkerFor(listEl, type) {
3443
+ const markers = type === 'ol' ? OL_MARKERS : UL_MARKERS;
3444
+ let runs = 0;
3445
+ for (let previous = adjacentSiblingList(listEl); previous; previous = adjacentSiblingList(previous))
3446
+ runs++;
3447
+ return markers[runs % markers.length];
3448
+ }
2905
3449
  /** How far a line is indented. */
2906
3450
  function indentOf(line) {
2907
3451
  return line.length - line.trimStart().length;
@@ -2991,7 +3535,7 @@ function holdLine(line, continuation, pendingBlank) {
2991
3535
  pendingBlank.length = 0;
2992
3536
  }
2993
3537
  /** Close the list levels that a line at `indent` of kind `type` does not continue. */
2994
- function closeLevelsFor(stack, indent, type) {
3538
+ function closeLevelsFor(stack, indent, type, marker) {
2995
3539
  // Pop only DEEPER levels. Popping the current one too (`>=`) threw away the
2996
3540
  // list a sibling belongs to, so every line started a fresh one — an ordered
2997
3541
  // list renumbered from 1 on every row, and a screen reader announced
@@ -3000,8 +3544,12 @@ function closeLevelsFor(stack, indent, type) {
3000
3544
  stack.pop();
3001
3545
  }
3002
3546
  // A same-indent line of a DIFFERENT kind (bullet after numbered) is its own
3003
- // list, so that one context is replaced rather than appended.
3004
- if (stack.at(-1)?.indent === indent && stack.at(-1)?.type !== type) {
3547
+ // list, so that one context is replaced rather than appended. A different
3548
+ // MARKER of the same kind ends it just as surely -- that is CommonMark's
3549
+ // only way to put two sibling lists of one kind next to each other, and the
3550
+ // writer relies on it (see listMarkerFor).
3551
+ const open = stack.at(-1);
3552
+ if (open?.indent === indent && (open.type !== type || open.marker !== marker)) {
3005
3553
  stack.pop();
3006
3554
  }
3007
3555
  }
@@ -3012,7 +3560,7 @@ function pushListItem(stack, rootLists, line) {
3012
3560
  parent.items.push(item);
3013
3561
  return;
3014
3562
  }
3015
- const list = { type: line.type, items: [item], indent: line.indent, start: line.number ?? 1 };
3563
+ const list = { type: line.type, items: [item], indent: line.indent, start: line.number ?? 1, marker: line.marker };
3016
3564
  if (parent)
3017
3565
  parent.items.at(-1)?.parts.push(list);
3018
3566
  else
@@ -3317,8 +3865,12 @@ function touchesWord(element) {
3317
3865
  || touches(nodeBeside(element, 'nextSibling'), /^[\p{L}\p{N}]/u);
3318
3866
  }
3319
3867
  function touches(node, letter) {
3868
+ // A BLOCK sibling is not on this line and cannot run into the delimiters.
3869
+ // Counted as touching, a task row's nested <ul> flipped the bold beside it
3870
+ // to its tag form on the SECOND save -- the first save had moved the list
3871
+ // next to it -- so the document kept changing after it had been saved once.
3320
3872
  if (node?.nodeType === Node.ELEMENT_NODE)
3321
- return (node.textContent ?? '') !== '';
3873
+ return isPhrasing(node) && (node.textContent ?? '') !== '';
3322
3874
  return node?.nodeType === Node.TEXT_NODE && letter.test(node.textContent ?? '');
3323
3875
  }
3324
3876
  /**
@@ -3616,10 +4168,34 @@ function buildListContextHtml(ctx) {
3616
4168
  // half already does for parentheses. A flat [^\\]]* meant "see [1]" or
3617
4169
  // "[Draft] spec" -- everyday link and alt text -- did not match at all, so the
3618
4170
  // anchor was destroyed on save and its markdown source shown as page text.
4171
+ const TARGET_BODY = String.raw `(?:[^()]|\([^()]*\))`;
4172
+ const LINK_TEXT_BODY = String.raw `(?:[^[\]]|\[[^[\]]*\])`;
3619
4173
  const MEDIA_TARGET_PATTERN = {
3620
- image: /!\[((?:[^[\]]|\[[^[\]]*\]){0,4096})\]\(((?:[^()]|\([^()]*\))+)\)/g,
3621
- link: /\[((?:[^[\]]|\[[^[\]]*\]){1,4096})\]\(((?:[^()]|\([^()]*\)){1,4096})\)/g,
4174
+ image: new RegExp(String.raw `!\[(${LINK_TEXT_BODY}{0,4096})\]\((${TARGET_BODY}+)\)`, 'g'),
4175
+ link: new RegExp(String.raw `\[(${LINK_TEXT_BODY}{1,4096})\]\((${TARGET_BODY}{1,4096})\)`, 'g'),
3622
4176
  };
4177
+ /**
4178
+ * The writer's half of {@link TARGET_BODY}: an address the reader hands back
4179
+ * unchanged. It also refuses a backslash, which `protectEscapes` consumes
4180
+ * before any target is matched, so a plain address holding one would not
4181
+ * survive either.
4182
+ */
4183
+ const PLAIN_TARGET = /^(?:[^()\\]|\([^()\\]*\))*$/;
4184
+ /**
4185
+ * A link or image address as markdown source.
4186
+ *
4187
+ * The reader's grammar allows at most ONE level of balanced parentheses, so an
4188
+ * address holding an odd or a nested ")" was cut at that character: the link
4189
+ * pointed at the truncated prefix and the rest of the address landed on the
4190
+ * page as visible text. CommonMark's answer is the backslash escape, and
4191
+ * `protectEscapes` parks those before any target is matched, so no parenthesis
4192
+ * is left for the grammar to trip on. An address the grammar already returns
4193
+ * unchanged keeps its plain form, so ordinary URLs -- Wikipedia's
4194
+ * "X_(disambiguation)" among them -- are written exactly as before.
4195
+ */
4196
+ function markdownTarget(target) {
4197
+ return PLAIN_TARGET.test(target) ? target : target.replaceAll(/[\\()]/g, String.raw `\$&`);
4198
+ }
3623
4199
  /**
3624
4200
  * Marker standing in for a character with Markdown meaning while the emphasis
3625
4201
  * passes run. Those passes regex over the WHOLE string, attribute values
@@ -3690,6 +4266,38 @@ const INDENTED_FENCE_TOKEN = /^\s+\d{1,9}\s*$/;
3690
4266
  * list item above it, whatever block it turns out to hold.
3691
4267
  */
3692
4268
  const CONTINUATION_LINE = /^ {2,}\S/;
4269
+ /** Columns a tab advances to, per CommonMark. */
4270
+ const TAB_STOP = 4;
4271
+ /**
4272
+ * Every line's leading whitespace with its tabs advanced to the next
4273
+ * four-column tab stop.
4274
+ *
4275
+ * CommonMark measures block indentation in COLUMNS (§2.2), while every
4276
+ * indentation predicate here counts characters -- CONTINUATION_LINE, indentOf,
4277
+ * contentColumn. A tab therefore read as one column, fell short of the item's
4278
+ * continuation column, and the block escaped its list item: "- a" followed by a
4279
+ * tab-indented quote came back as a list and a separate paragraph, and the next
4280
+ * save wrote the quote marker as the literal characters "\> q". Expanding the
4281
+ * tabs once, up front, lets all of those predicates stay character-based.
4282
+ *
4283
+ * Only the leading run is rewritten: a tab inside prose is content, not
4284
+ * structure. Fenced and inline code are parked before this runs, so no tab
4285
+ * inside code is ever seen.
4286
+ */
4287
+ function expandLeadingTabs(text) {
4288
+ if (!text.includes('\t'))
4289
+ return text;
4290
+ return text.split('\n').map(expandLineIndent).join('\n');
4291
+ }
4292
+ function expandLineIndent(line) {
4293
+ const indent = /^[ \t]*/.exec(line)?.[0] ?? '';
4294
+ if (!indent.includes('\t'))
4295
+ return line;
4296
+ let column = 0;
4297
+ for (const ch of indent)
4298
+ column += ch === '\t' ? TAB_STOP - (column % TAB_STOP) : 1;
4299
+ return ' '.repeat(column) + line.slice(indent.length);
4300
+ }
3693
4301
  /** Private-use delimiters parking a raw HTML tag during the inline passes. */
3694
4302
  const RAW_TAG_OPEN = '';
3695
4303
  const RAW_TAG_CLOSE = '';
@@ -3971,6 +4579,10 @@ class RichTextMarkdownService {
3971
4579
  // -- rendering "</div>" as visible "&lt;/div&gt;".
3972
4580
  const protectedCode = [];
3973
4581
  html = this.protectCodeFences(html, protectedCode);
4582
+ // AFTER the fences are parked, so a tab inside code is never rewritten,
4583
+ // and BEFORE every block pass, all of which measure indentation in
4584
+ // characters. See expandLeadingTabs.
4585
+ html = expandLeadingTabs(html);
3974
4586
  // Inline code is lifted out with the fences and for the same reason: a
3975
4587
  // code span is inert text. parseInlineCode ran LAST, after the emphasis
3976
4588
  // and line-break passes had already rewritten its contents, so
@@ -4003,8 +4615,8 @@ class RichTextMarkdownService {
4003
4615
  // before this pass ever ran, and the break was silently dropped.
4004
4616
  html = this.parseLineBreaks(html);
4005
4617
  html = this.parseParagraphs(html, protectedTags);
4006
- html = this.parseImages(html, protectedInline, inlineSources, protectedTags);
4007
- html = this.parseLinks(html, inlineSources, protectedTags);
4618
+ html = this.parseImages(html, protectedInline, inlineSources, protectedTags, protectedEscapes);
4619
+ html = this.parseLinks(html, inlineSources, protectedTags, protectedEscapes);
4008
4620
  html = this.parseBoldItalic(html);
4009
4621
  html = this.parseStrikethrough(html);
4010
4622
  // After the emphasis passes, so the characters hidden in link and image
@@ -4469,7 +5081,7 @@ class RichTextMarkdownService {
4469
5081
  result.push(...orphans, line);
4470
5082
  continue;
4471
5083
  }
4472
- closeLevelsFor(stack, parsed.indent, parsed.type);
5084
+ closeLevelsFor(stack, parsed.indent, parsed.type, parsed.marker);
4473
5085
  flushContinuation();
4474
5086
  pushListItem(stack, rootLists, parsed);
4475
5087
  }
@@ -4734,9 +5346,17 @@ class RichTextMarkdownService {
4734
5346
  * the query string broke. A target with a space is no target; only one that
4735
5347
  * held a code span or a tag is checked for it, since a plain target with a
4736
5348
  * space has always been read as a link.
4737
- */
4738
- targetSource(target, inlineSources, tagStore) {
4739
- const typed = decodeCharacterReferences(restoreParked(restoreParked(target, INLINE_CODE_OPEN, INLINE_CODE_CLOSE, inlineSources), RAW_TAG_OPEN, RAW_TAG_CLOSE, tagStore));
5349
+ *
5350
+ * A backslash escape is resolved LAST, after the character references: it is
5351
+ * the author saying "this character, literally", so what it hands back must
5352
+ * not be read as syntax again -- `\&amp;` means those five characters, not
5353
+ * an ampersand. Resolving it here at all is what lets the writer escape a
5354
+ * parenthesis the target grammar cannot carry (see markdownTarget): the real
5355
+ * character reaches the sanitizer and the attribute, rather than the parked
5356
+ * token, which sanitizeImageSrc percent-encoded into the address.
5357
+ */
5358
+ targetSource(target, inlineSources, tagStore, escapes) {
5359
+ const typed = restoreParked(decodeCharacterReferences(restoreParked(restoreParked(target, INLINE_CODE_OPEN, INLINE_CODE_CLOSE, inlineSources), RAW_TAG_OPEN, RAW_TAG_CLOSE, tagStore)), ESCAPED_OPEN, ESCAPED_CLOSE, escapes);
4740
5360
  return holdsParkedToken(target) && /\s/.test(typed) ? null : typed;
4741
5361
  }
4742
5362
  /**
@@ -4748,12 +5368,49 @@ class RichTextMarkdownService {
4748
5368
  altText(alt, inlineStore, tagStore) {
4749
5369
  return restoreParked(resolveInlineCodeText(alt, inlineStore), RAW_TAG_OPEN, RAW_TAG_CLOSE, tagStore, (tag) => this.escapeHtml(tag));
4750
5370
  }
4751
- parseImages(html, inlineStore, inlineSources, tagStore) {
5371
+ /**
5372
+ * Park a tag this pass wrote in the raw-tag store, so no later pass can see
5373
+ * inside it.
5374
+ *
5375
+ * The tag is written UNSHIELDED because parking supersedes the shield:
5376
+ * restoreRawTags runs after unshieldUrls, so a shielded character parked in
5377
+ * here would never be given back.
5378
+ */
5379
+ parkWrittenTag(tag, tagStore) {
5380
+ const token = `${RAW_TAG_OPEN}${tagStore.length}${RAW_TAG_CLOSE}`;
5381
+ tagStore.push(tag);
5382
+ return token;
5383
+ }
5384
+ /**
5385
+ * An `<img>` this pass wrote, with its attributes escaped but not shielded.
5386
+ *
5387
+ * Its alt text is DOCUMENT TEXT, which escapeHtmlInContent has already
5388
+ * escaped, so only the quote is escaped again here -- escaping it twice
5389
+ * brought a "<" back as the characters "&lt;", one more layer per save.
5390
+ */
5391
+ imageTag(attribute, target, alt) {
5392
+ return `<img ${attribute}="${this.escapeHtml(target)}" alt="${alt.replaceAll('"', '&quot;')}">`;
5393
+ }
5394
+ /**
5395
+ * Parse images ![alt](src).
5396
+ *
5397
+ * The tag is parked, not returned live. Left in the stream, its alt
5398
+ * attribute was still document text as far as the passes that follow were
5399
+ * concerned: parseLinks read `![a [b](u) c](i.png)` as a link INSIDE the
5400
+ * attribute and wrote an `<a href="` into it, which ended the attribute and
5401
+ * put the rest of the tag on the page. Shielding only hid `* _ \` ~`, so it
5402
+ * could never have covered the bracket forms.
5403
+ */
5404
+ parseImages(html, inlineStore, inlineSources, tagStore, escapes) {
4752
5405
  return html.replaceAll(MEDIA_TARGET_PATTERN.image, (match, alt, target) => {
4753
- const src = this.targetSource(target, inlineSources, tagStore);
5406
+ const src = this.targetSource(target, inlineSources, tagStore, escapes);
4754
5407
  if (src === null)
4755
5408
  return match;
4756
5409
  const safeSrc = this.sanitizer.sanitizeImageSrc(src);
5410
+ // An alt attribute is plain text: a parked code span restored in
5411
+ // there would land as the literal string "<code>x</code>". Resolve
5412
+ // it back to the text the author typed instead.
5413
+ const plainAlt = this.altText(alt, inlineStore, tagStore);
4757
5414
  if (!safeSrc) {
4758
5415
  // A POLICY-blocked image keeps its element and alt, so the
4759
5416
  // reader sees a labelled frame rather than nothing and the block
@@ -4763,22 +5420,17 @@ class RichTextMarkdownService {
4763
5420
  const blocked = this.sanitizer.takeBlockedByPolicy();
4764
5421
  if (blocked === null)
4765
5422
  return '';
4766
- const blockedAlt = this.altText(alt, inlineStore, tagStore);
4767
- return `<img data-blocked-src="${this.attr(blocked)}" alt="${this.textAttr(blockedAlt)}">`;
5423
+ return this.parkWrittenTag(this.imageTag('data-blocked-src', blocked, plainAlt), tagStore);
4768
5424
  }
4769
- // An alt attribute is plain text: a parked code span restored in
4770
- // there would land as the literal string "<code>x</code>". Resolve
4771
- // it back to the text the author typed instead.
4772
- const plainAlt = this.altText(alt, inlineStore, tagStore);
4773
- return `<img src="${this.attr(safeSrc)}" alt="${this.textAttr(plainAlt)}">`;
5425
+ return this.parkWrittenTag(this.imageTag('src', safeSrc, plainAlt), tagStore);
4774
5426
  });
4775
5427
  }
4776
5428
  /**
4777
5429
  * Parse links [text](url).
4778
5430
  */
4779
- parseLinks(html, inlineSources, tagStore) {
5431
+ parseLinks(html, inlineSources, tagStore, escapes) {
4780
5432
  return html.replaceAll(MEDIA_TARGET_PATTERN.link, (match, text, target) => {
4781
- const url = this.targetSource(target, inlineSources, tagStore);
5433
+ const url = this.targetSource(target, inlineSources, tagStore, escapes);
4782
5434
  if (url === null)
4783
5435
  return match;
4784
5436
  const safeUrl = this.sanitizer.sanitizeUrl(url);
@@ -5070,7 +5722,7 @@ class RichTextMarkdownService {
5070
5722
  // of the link fused.
5071
5723
  if (inner.trim() === '')
5072
5724
  return `<a href="${this.escapeHtml(href)}"></a>${inner}`;
5073
- return `[${inner}](${href})`;
5725
+ return `[${inner}](${markdownTarget(href)})`;
5074
5726
  }
5075
5727
  handleImageTag(element) {
5076
5728
  const alt = element.getAttribute('alt') ?? '';
@@ -5087,7 +5739,7 @@ class RichTextMarkdownService {
5087
5739
  // the paragraph inside the attribute, backticks or asterisks were read as
5088
5740
  // syntax, and "&copy;" or "<i>" came back as a character or a tag.
5089
5741
  const text = escapeMarkdownText(alt.replaceAll(/[ \t\n\r\f]+/g, ' '));
5090
- return `![${text}](${src})`;
5742
+ return `![${text}](${markdownTarget(src)})`;
5091
5743
  }
5092
5744
  /**
5093
5745
  * Unwrap every span that is written as its text alone, so the text on either
@@ -5192,7 +5844,16 @@ class RichTextMarkdownService {
5192
5844
  // from opening and the code came back as paragraphs.
5193
5845
  const language = element.querySelector('code')?.dataset['language'] ?? '';
5194
5846
  const lang = /^[\w+#.-]+$/.test(language) ? language : '';
5195
- const codeContent = element.textContent ?? '';
5847
+ // A fence is TEXT: it cannot carry an image, and a block written from
5848
+ // textContent alone deleted one outright on the first save. The tag form
5849
+ // can carry it and reads back verbatim, which is the answer inline code
5850
+ // already gives for the same content.
5851
+ if (element.querySelector('img'))
5852
+ return this.preTagForm(element, lang, inListItem);
5853
+ // Every line break, including a <br>. textContent counts a <br> as
5854
+ // nothing, and Shift+Enter -- how a line is added inside a block --
5855
+ // inserts one, so a function typed line by line was saved on one line.
5856
+ const codeContent = codeTextOf(element);
5196
5857
  const indent = inListItem ? ' ' : '';
5197
5858
  const body = codeContent
5198
5859
  .split('\n')
@@ -5202,6 +5863,21 @@ class RichTextMarkdownService {
5202
5863
  const fence = '`'.repeat(Math.max(3, longestRun + 1));
5203
5864
  return `\n${indent}${fence}${lang}\n${body}\n${indent}${fence}\n`;
5204
5865
  }
5866
+ /**
5867
+ * A code block as its tag pair, on one markdown line.
5868
+ *
5869
+ * One line, with every newline written as a character reference, because a
5870
+ * raw newline inside the pair would let the block passes split it: the
5871
+ * `<pre>` and `</pre>` are parked separately and the lines between them are
5872
+ * read as markdown. The reference decodes to the newline when the sanitizer
5873
+ * parses the restored tag, so the code keeps its own line breaks.
5874
+ */
5875
+ preTagForm(element, language, inListItem) {
5876
+ const code = element.querySelector('code') ?? element;
5877
+ const lang = language ? ` data-language="${this.escapeHtml(language)}"` : '';
5878
+ const indent = inListItem ? ' ' : '';
5879
+ return `\n${indent}<pre><code${lang}>${this.codeTagContent(code)}</code></pre>\n`;
5880
+ }
5205
5881
  handleUlTag(element) {
5206
5882
  const result = ['\n'];
5207
5883
  const isTask = 'taskList' in element.dataset;
@@ -5393,10 +6069,11 @@ class RichTextMarkdownService {
5393
6069
  listToMarkdown(listEl, type, indent, result) {
5394
6070
  const items = Array.from(listEl.children);
5395
6071
  const first = type === 'ol' ? listStartOf(listEl) : 1;
6072
+ const marker = listMarkerFor(listEl, type);
5396
6073
  items.forEach((li, index) => {
5397
6074
  const [lead, ...rest] = itemRuns(li);
5398
6075
  const ordinal = first + index <= MAX_LIST_NUMBER ? first + index : first;
5399
- result.push(this.formatListItem(type, li, this.leadContent(li, lead, indent), indent, ordinal));
6076
+ result.push(this.formatListItem(type, li, this.leadContent(li, lead, indent), indent, ordinal, marker));
5400
6077
  // Every sub-list, in order, with the content after each where it
5401
6078
  // falls. Only the last sub-list was kept, so an item holding two --
5402
6079
  // bullets under numbers, which indenting beside an existing sub-list of
@@ -5442,15 +6119,15 @@ class RichTextMarkdownService {
5442
6119
  // text vanished from the saved markdown while the HTML looked fine.
5443
6120
  return node.nodeType === Node.TEXT_NODE ? textToMarkdown(node) : this.elementToMarkdown(node);
5444
6121
  }
5445
- formatListItem(type, li, content, indent, ordinal) {
6122
+ formatListItem(type, li, content, indent, ordinal, marker) {
5446
6123
  if (type === 'task') {
5447
6124
  const checked = li.dataset['checked'] === 'true';
5448
- return `${indent}- [${checked ? 'x' : ' '}] ${breaksAsTags(content)}\n`;
6125
+ return `${indent}${marker} [${checked ? 'x' : ' '}] ${breaksAsTags(content)}\n`;
5449
6126
  }
5450
6127
  if (type === 'ol') {
5451
- return `${indent}${ordinal}. ${content}\n`;
6128
+ return `${indent}${ordinal}${marker} ${content}\n`;
5452
6129
  }
5453
- return `${indent}- ${content}\n`;
6130
+ return `${indent}${marker} ${content}\n`;
5454
6131
  }
5455
6132
  detectNestedListType(nestedList) {
5456
6133
  if ('taskList' in nestedList.dataset)
@@ -5611,15 +6288,6 @@ const SUBTREE_INDEPENDENT_TAGS = new Set(['table', 'img', 'input', 'pre', 'ul',
5611
6288
  const MAX_TABLE_ROWSPAN = 1000;
5612
6289
  /** Widest row a table may emit, bounding paste amplification. */
5613
6290
  const MAX_TABLE_COLUMNS = 1000;
5614
- /**
5615
- * How deep blocks nest before the reader leaves the rest as text; see
5616
- * parseToggleBlocks and buildBlockquote. The reader counts a level for every
5617
- * details body, quote holding a nested quote and list item's block, so a details
5618
- * block nested through a list or a quote meets the same cap; a sub-list, read in
5619
- * its parent's pass, adds none. The writer counts the same list items and every
5620
- * quote (see nestingDepthOf), which is never less.
5621
- */
5622
- const MAX_NESTING_DEPTH = 32;
5623
6291
  /** HTML's own limit for a column span. */
5624
6292
  const MAX_COLSPAN = 1000;
5625
6293
  /** Blank-line boundary between blocks, captured so joining restores the text. */
@@ -9335,7 +10003,9 @@ const BLOCK_RULES = [
9335
10003
  { kind: 'heading3', pattern: /^#{3}$/, terminators: [' '] },
9336
10004
  { kind: 'bulletList', pattern: /^[-*]$/, terminators: [' '] },
9337
10005
  { kind: 'orderedList', pattern: /^\d{1,3}\.$/, terminators: [' '] },
9338
- { kind: 'blockquote', pattern: /^>$/, terminators: [' '] },
10006
+ // A RUN of markers, one quote each. `/^>$/` matched exactly one, so a reader
10007
+ // could load and round-trip a nested quote but an author could not type one.
10008
+ { kind: 'blockquote', pattern: /^(>+)$/, terminators: [' '] },
9339
10009
  { kind: 'taskUnchecked', pattern: /^\[]$/, terminators: [' '] },
9340
10010
  { kind: 'taskChecked', pattern: /^\[[xX]]$/, terminators: [' '] },
9341
10011
  { kind: 'horizontalRule', pattern: /^-{3}$/, terminators: [''] },
@@ -9364,9 +10034,11 @@ function matchBlockInputRule(textBeforeCaret, terminator) {
9364
10034
  if (!match)
9365
10035
  continue;
9366
10036
  const markerLength = textBeforeCaret.length + terminator.length;
9367
- return rule.kind === 'codeBlock'
9368
- ? { kind: rule.kind, markerLength, language: match[1] ?? '' }
9369
- : { kind: rule.kind, markerLength };
10037
+ if (rule.kind === 'codeBlock')
10038
+ return { kind: rule.kind, markerLength, language: match[1] ?? '' };
10039
+ if (rule.kind === 'blockquote')
10040
+ return { kind: rule.kind, markerLength, depth: match[1].length };
10041
+ return { kind: rule.kind, markerLength };
9370
10042
  }
9371
10043
  return null;
9372
10044
  }
@@ -9683,6 +10355,25 @@ const RICH_TEXT_PROSE_CLASSES = [
9683
10355
  '[&_details>:not(summary)]:px-3 [&_details>:not(summary)]:py-2',
9684
10356
  '[&_hr]:border-t [&_hr]:border-border [&_hr]:my-4',
9685
10357
  '[&_blockquote]:border-s-4 [&_blockquote]:border-border [&_blockquote]:ps-4 [&_blockquote]:py-1 [&_blockquote]:my-2 [&_blockquote]:text-muted-foreground [&_blockquote]:italic',
10358
+ // Syntax highlighting. The classes are the ones the sanitizer has always
10359
+ // allowed (`token`, `token-*`), so a document highlighted here and one
10360
+ // pasted in already highlighted colour by one set of rules — see
10361
+ // `lib/code-highlight.ts`.
10362
+ //
10363
+ // A code block sits on `bg-muted`, which is light in the light theme and
10364
+ // dark in the dark one, so every colour needs both. `ui-code-block`'s own
10365
+ // palette is not reused: it is tuned for that component's fixed
10366
+ // `bg-zinc-950` surface and is unreadable on a light page.
10367
+ '[&_.token-keyword]:text-violet-700 dark:[&_.token-keyword]:text-violet-300 [&_.token-keyword]:font-semibold',
10368
+ '[&_.token-string]:text-emerald-700 dark:[&_.token-string]:text-emerald-300',
10369
+ '[&_.token-comment]:text-muted-foreground [&_.token-comment]:italic',
10370
+ '[&_.token-number]:text-amber-700 dark:[&_.token-number]:text-amber-300',
10371
+ '[&_.token-function]:text-blue-700 dark:[&_.token-function]:text-blue-300',
10372
+ '[&_.token-decorator]:text-fuchsia-700 dark:[&_.token-decorator]:text-fuchsia-300',
10373
+ '[&_.token-tag]:text-rose-700 dark:[&_.token-tag]:text-rose-300',
10374
+ '[&_.token-selector]:text-rose-700 dark:[&_.token-selector]:text-rose-300 [&_.token-selector]:font-semibold',
10375
+ '[&_.token-attr]:text-sky-700 dark:[&_.token-attr]:text-sky-300',
10376
+ '[&_.token-property]:text-sky-700 dark:[&_.token-property]:text-sky-300',
9686
10377
  ];
9687
10378
 
9688
10379
  /**
@@ -10039,6 +10730,9 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
10039
10730
  if (!this.editorDiv)
10040
10731
  return;
10041
10732
  this.editorDiv.nativeElement.innerHTML = html;
10733
+ // Every block, because every block is new. This is the only path that
10734
+ // repaints the whole document; typing repaints the caret's block alone.
10735
+ highlightCodeBlocks(this.editorDiv.nativeElement);
10042
10736
  this.labelBlockedImages();
10043
10737
  this.selectedImageNode.set(null);
10044
10738
  this.tableCellSelected.set([]);
@@ -10139,6 +10833,11 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
10139
10833
  this.rootCommandRegistry = inject(RichTextCommandRegistry, { skipSelf: true });
10140
10834
  /** Output format: `'markdown'` converts to/from Markdown; `'html'` works with raw HTML. */
10141
10835
  this.mode = input('markdown', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
10836
+ /**
10837
+ * Colour preset for this editor and its overlays — the names `change-theme`
10838
+ * accepts. Unset, the editor follows the app's global tokens.
10839
+ */
10840
+ this.theme = input(...(ngDevMode ? [undefined, { debugName: "theme" }] : /* istanbul ignore next */ []));
10142
10841
  /** Visual border/focus style. See {@link EditorVariant}. */
10143
10842
  this.variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
10144
10843
  /** Text size preset for the editor content. See {@link EditorSize}. */
@@ -10228,6 +10927,18 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
10228
10927
  * on every keystroke.
10229
10928
  */
10230
10929
  this.findDebounceMs = input(150, ...(ngDevMode ? [{ debugName: "findDebounceMs" }] : /* istanbul ignore next */ []));
10930
+ /**
10931
+ * Milliseconds of quiet before the caret's code block is re-highlighted, so
10932
+ * a burst of keystrokes repaints once. `0` repaints synchronously on every
10933
+ * keystroke, which is what the unit suite uses.
10934
+ *
10935
+ * Highlighting is presentational only: the spans it paints never reach the
10936
+ * value (`readContentFromEditor` unwraps them) and never reach the history,
10937
+ * so this knob changes when colour catches up with typing and nothing else.
10938
+ * Set it to a large number, or highlight nothing, by giving your fences a
10939
+ * language the highlighter does not know.
10940
+ */
10941
+ this.codeHighlightDebounceMs = input(150, ...(ngDevMode ? [{ debugName: "codeHighlightDebounceMs" }] : /* istanbul ignore next */ []));
10231
10942
  /**
10232
10943
  * Hosts whose remote images and CSS backgrounds may load. Empty (the
10233
10944
  * default) means no policy: any `https://` source is permitted, which is how
@@ -10493,6 +11204,7 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
10493
11204
  .replace('{total}', String(total));
10494
11205
  }, ...(ngDevMode ? [{ debugName: "findCounterText" }] : /* istanbul ignore next */ []));
10495
11206
  this.findDebounceTimer = null;
11207
+ this.codeHighlightTimer = null;
10496
11208
  this.findOverlay = null;
10497
11209
  this.findRepaintHandle = null;
10498
11210
  this.findResizeObserver = null;
@@ -11068,7 +11780,7 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
11068
11780
  this.lastInputRule = null;
11069
11781
  const transformed = this.applyInputRules(event);
11070
11782
  this.sweepSpentCaretAnchors(div);
11071
- const html = this.sanitizer.sanitize(div.innerHTML).replaceAll('\u200B', '');
11783
+ const html = this.sanitizer.sanitize(this.undecoratedMarkup(div)).replaceAll('\u200B', '');
11072
11784
  const triggerTextContent = this.buildTriggerAwareText(div.innerHTML);
11073
11785
  const selection = this.document.getSelection();
11074
11786
  const hasSelection = !!selection && selection.rangeCount > 0;
@@ -11087,6 +11799,10 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
11087
11799
  else {
11088
11800
  this.scheduleDebouncedHistoryPush();
11089
11801
  }
11802
+ // LAST, and after the value has already been read: the repaint must
11803
+ // never be what a reader of the document sees, only what the author
11804
+ // does.
11805
+ this.scheduleCodeHighlight();
11090
11806
  }
11091
11807
  /**
11092
11808
  * Template-bound `keydown` handler on the editable area, in strict priority
@@ -14840,6 +15556,7 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
14840
15556
  closeFindReplace() {
14841
15557
  const current = this.findMatches()[this.findCurrentIndex()]?.cloneRange();
14842
15558
  this.cancelPendingFind();
15559
+ this.cancelPendingCodeHighlight();
14843
15560
  this.teardownFindOverlay();
14844
15561
  this.findReplaceVisible.set(false);
14845
15562
  this.findQuery.set('');
@@ -15482,10 +16199,30 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
15482
16199
  const editorElement = this.getEditorElement();
15483
16200
  if (!editorElement)
15484
16201
  return null;
15485
- const html = this.sanitizer.sanitize(editorElement.innerHTML).replaceAll('\u200B', '');
16202
+ const html = this.sanitizer.sanitize(this.undecoratedMarkup(editorElement)).replaceAll('\u200B', '');
15486
16203
  this.htmlContent.set(html);
15487
16204
  return html;
15488
16205
  }
16206
+ /**
16207
+ * The editable's markup with the highlighter's spans unwrapped.
16208
+ *
16209
+ * Syntax colour is DECORATION: the author typed the code, not the spans
16210
+ * around it, so they must not reach the value, the history or the markdown.
16211
+ * Left in, every keystroke inside a fence emitted a different HTML string
16212
+ * for the same code, and each undo step carried a repaint of its own.
16213
+ *
16214
+ * Read from a copy, so the live DOM \u2014 and with it the caret and the colour
16215
+ * the author is looking at \u2014 is never touched. The copy is skipped entirely
16216
+ * when the document carries no token span, which is every document with no
16217
+ * code in it.
16218
+ */
16219
+ undecoratedMarkup(editorElement) {
16220
+ if (!editorElement.querySelector('pre > code span.token'))
16221
+ return editorElement.innerHTML;
16222
+ const copy = editorElement.cloneNode(true);
16223
+ stripCodeHighlighting(copy);
16224
+ return copy.innerHTML;
16225
+ }
15489
16226
  getMentionElementsInSelection() {
15490
16227
  const editor = this.getEditorElement();
15491
16228
  const selection = this.document.getSelection();
@@ -16260,6 +16997,29 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
16260
16997
  const quoted = line ? this.quoteLines([line]) : [];
16261
16998
  return quoted[0] ?? block;
16262
16999
  }
17000
+ /**
17001
+ * Quote one block `levels` deep, or as deep as the markdown reader's
17002
+ * ceiling still allows.
17003
+ *
17004
+ * The ceiling is the READER's ({@link MAX_NESTING_DEPTH}), not one this rule
17005
+ * invents: a quote nested past it is unwrapped when the document is read
17006
+ * back, so building one here types a structure the next load throws away.
17007
+ * Quotes the block ALREADY sits in count towards it, which is also what
17008
+ * makes typing ">" inside a quote nest one level deeper instead of doing
17009
+ * nothing.
17010
+ *
17011
+ * Every level is built inside the one snapshot `tryBlockRule` took, so the
17012
+ * whole transform is a single undo step and Backspace restores all of the
17013
+ * markers the author typed, not just the last one.
17014
+ */
17015
+ quoteBlockToDepth(block, levels) {
17016
+ const editor = this.editorDiv?.nativeElement;
17017
+ const room = MAX_NESTING_DEPTH - (editor ? blockquoteDepthOf(block, editor) : 0);
17018
+ let quoted = block;
17019
+ for (let level = 0; level < Math.min(levels, room); level++)
17020
+ quoted = this.quoteBlock(quoted);
17021
+ return quoted;
17022
+ }
16263
17023
  /**
16264
17024
  * Wrap lines in one `<blockquote>` and return them as its lines.
16265
17025
  *
@@ -17181,7 +17941,7 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
17181
17941
  case 'orderedList':
17182
17942
  return this.finishBlockRule(this.wrapBlockInList(block, 'ol'));
17183
17943
  case 'blockquote':
17184
- return this.finishBlockRule(this.quoteBlock(block));
17944
+ return this.finishBlockRule(this.quoteBlockToDepth(block, match.depth ?? 1));
17185
17945
  case 'taskUnchecked':
17186
17946
  return this.buildTaskBlock(block, false);
17187
17947
  case 'taskChecked':
@@ -17839,6 +18599,55 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
17839
18599
  this.tableCellSelecting = false;
17840
18600
  this.tableCellSelectAnchor = null;
17841
18601
  }
18602
+ /**
18603
+ * Repaint the caret's code block after {@link codeHighlightDebounceMs} of
18604
+ * quiet. A debounce of 0 repaints inline.
18605
+ */
18606
+ scheduleCodeHighlight() {
18607
+ this.cancelPendingCodeHighlight();
18608
+ const delay = this.codeHighlightDebounceMs();
18609
+ if (delay <= 0) {
18610
+ this.highlightCaretCodeBlock();
18611
+ return;
18612
+ }
18613
+ this.codeHighlightTimer = setTimeout(() => {
18614
+ this.codeHighlightTimer = null;
18615
+ this.highlightCaretCodeBlock();
18616
+ }, delay);
18617
+ }
18618
+ cancelPendingCodeHighlight() {
18619
+ if (this.codeHighlightTimer !== null) {
18620
+ clearTimeout(this.codeHighlightTimer);
18621
+ this.codeHighlightTimer = null;
18622
+ }
18623
+ }
18624
+ /**
18625
+ * Repaint the one code block the caret is in.
18626
+ *
18627
+ * The caret's block alone, not the document: a repaint replaces every node
18628
+ * in the block, and doing that to a block the author is not in would detach
18629
+ * whatever else held a reference to it for no visible gain. A wholesale
18630
+ * load repaints everything, through {@link replaceEditorHtml}.
18631
+ *
18632
+ * Nothing here touches the history or the value. The spans are decoration:
18633
+ * `readContentFromEditor` unwraps them before anything reads the document,
18634
+ * so typing in a code block produces the same value it always did, and an
18635
+ * undo step never has a repaint of its own to step through.
18636
+ */
18637
+ highlightCaretCodeBlock() {
18638
+ const editor = this.editorDiv?.nativeElement;
18639
+ if (!editor || this.isDisabled() || this.readonly())
18640
+ return;
18641
+ const selection = this.document.getSelection();
18642
+ if (!selection || selection.rangeCount === 0)
18643
+ return;
18644
+ const start = selection.getRangeAt(0).startContainer;
18645
+ const node = start.nodeType === Node.TEXT_NODE ? start.parentElement : start;
18646
+ const code = node?.closest('pre > code');
18647
+ if (!code || !editor.contains(code))
18648
+ return;
18649
+ highlightCodeElementKeepingCaret(code, languageOfCodeElement(code), selection);
18650
+ }
17842
18651
  ngOnDestroy() {
17843
18652
  this.shortcutHandle?.unregister();
17844
18653
  this.shortcutHandle = null;
@@ -17853,7 +18662,7 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
17853
18662
  this.removeFloatingScrollListener();
17854
18663
  }
17855
18664
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RichTextEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
17856
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: RichTextEditorComponent, isStandalone: true, selector: "ui-rich-text-editor", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null }, toolbarItems: { classPropertyName: "toolbarItems", publicName: "toolbarItems", isSignal: true, isRequired: false, transformFunction: null }, customToolbarItems: { classPropertyName: "customToolbarItems", publicName: "customToolbarItems", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, markdownShortcuts: { classPropertyName: "markdownShortcuts", publicName: "markdownShortcuts", isSignal: true, isRequired: false, transformFunction: null }, counter: { classPropertyName: "counter", publicName: "counter", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, history: { classPropertyName: "history", publicName: "history", isSignal: true, isRequired: false, transformFunction: null }, findDebounceMs: { classPropertyName: "findDebounceMs", publicName: "findDebounceMs", isSignal: true, isRequired: false, transformFunction: null }, allowedImageHosts: { classPropertyName: "allowedImageHosts", publicName: "allowedImageHosts", isSignal: true, isRequired: false, transformFunction: null }, allowedLinkSchemes: { classPropertyName: "allowedLinkSchemes", publicName: "allowedLinkSchemes", isSignal: true, isRequired: false, transformFunction: null }, blockedImageMessage: { classPropertyName: "blockedImageMessage", publicName: "blockedImageMessage", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, dir: { classPropertyName: "dir", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { customToolbarAction: "customToolbarAction", htmlChange: "htmlChange", markdownChange: "markdownChange", wordCountChange: "wordCountChange", historyChange: "historyChange", imageBlocked: "imageBlocked", focused: "focused", blurred: "blurred" }, host: { classAttribute: "block" }, providers: [
18665
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: RichTextEditorComponent, isStandalone: true, selector: "ui-rich-text-editor", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null }, toolbarItems: { classPropertyName: "toolbarItems", publicName: "toolbarItems", isSignal: true, isRequired: false, transformFunction: null }, customToolbarItems: { classPropertyName: "customToolbarItems", publicName: "customToolbarItems", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, markdownShortcuts: { classPropertyName: "markdownShortcuts", publicName: "markdownShortcuts", isSignal: true, isRequired: false, transformFunction: null }, counter: { classPropertyName: "counter", publicName: "counter", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, history: { classPropertyName: "history", publicName: "history", isSignal: true, isRequired: false, transformFunction: null }, findDebounceMs: { classPropertyName: "findDebounceMs", publicName: "findDebounceMs", isSignal: true, isRequired: false, transformFunction: null }, codeHighlightDebounceMs: { classPropertyName: "codeHighlightDebounceMs", publicName: "codeHighlightDebounceMs", isSignal: true, isRequired: false, transformFunction: null }, allowedImageHosts: { classPropertyName: "allowedImageHosts", publicName: "allowedImageHosts", isSignal: true, isRequired: false, transformFunction: null }, allowedLinkSchemes: { classPropertyName: "allowedLinkSchemes", publicName: "allowedLinkSchemes", isSignal: true, isRequired: false, transformFunction: null }, blockedImageMessage: { classPropertyName: "blockedImageMessage", publicName: "blockedImageMessage", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, dir: { classPropertyName: "dir", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { customToolbarAction: "customToolbarAction", htmlChange: "htmlChange", markdownChange: "markdownChange", wordCountChange: "wordCountChange", historyChange: "historyChange", imageBlocked: "imageBlocked", focused: "focused", blurred: "blurred" }, host: { properties: { "attr.data-ui-theme": "theme()" }, classAttribute: "block" }, providers: [
17857
18666
  {
17858
18667
  provide: NG_VALUE_ACCESSOR,
17859
18668
  useExisting: forwardRef(() => RichTextEditorComponent),
@@ -17875,7 +18684,7 @@ class RichTextEditorComponent extends RichTextEditorAddonHost {
17875
18684
  RichTextSanitizerService,
17876
18685
  RichTextMarkdownService,
17877
18686
  provideComponentLocale(() => RichTextEditorComponent),
17878
- ], viewQueries: [{ propertyName: "editorDiv", first: true, predicate: ["editorDiv"], descendants: true }, { propertyName: "editorContainer", first: true, predicate: ["editorContainer"], descendants: true }, { propertyName: "tableContextMenuRef", first: true, predicate: ["tableContextMenuRef"], descendants: true }], usesInheritance: true, ngImport: i0, template: "@if (toolbar() === 'top' && !readonly()) {\n <ui-rich-text-toolbar\n [items]=\"toolbarItems()\"\n [activeFormats]=\"activeFormats()\"\n [textStyleAvailable]=\"textStyleAvailable()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [locale]=\"resolvedLocale()\"\n (formatCommand)=\"onFormatCommand($event)\"\n [addonSlots]=\"toolbarSlots.slots()\"\n (addonSlotClick)=\"onAddonSlotClick($event)\"\n />\n}\n\n<div #editorContainer [class]=\"editorContainerClasses()\" [dir]=\"resolvedDir()\">\n <!--\n Three of the attributes below exist for assistive tech specifically:\n\n The tab index drops to -1 when disabled. A native disabled control leaves the\n tab order entirely; a static 0 left a keyboard user stopping on an editor\n that takes focus and then does nothing.\n\n The readonly state is announced explicitly, because contenteditable false is\n a browser editing hint rather than a state screen readers reliably report on\n a textbox \u2014 without it a reader discovers the field is read-only by typing\n and seeing nothing happen.\n\n The placeholder is announced explicitly too, because the visible one is CSS\n generated content. Chromium exposes that as a plain text child, so a reader\n announces the hint as though it were the document's own text.\n -->\n <div\n #editorDiv\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [attr.contenteditable]=\"!isDisabled() && !readonly()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-readonly]=\"readonly() ? 'true' : null\"\n [class]=\"editableClasses()\"\n [attr.placeholder]=\"placeholder() || resolvedLocale().editor.placeholder\"\n [attr.aria-placeholder]=\"placeholder() || resolvedLocale().editor.placeholder\"\n [attr.aria-label]=\"ariaLabel() || resolvedLocale().editor.ariaLabel\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.data-slot]=\"'rich-text-editor'\"\n [style.min-height]=\"minHeight()\"\n [style.max-height]=\"maxHeight()\"\n role=\"textbox\"\n aria-multiline=\"true\"\n [attr.aria-expanded]=\"activeSuggestionPopup() ? 'true' : null\"\n [attr.aria-controls]=\"activeSuggestionPopup()?.controlsId ?? null\"\n [attr.aria-activedescendant]=\"activeSuggestionPopup()?.activeOptionId ?? null\"\n (input)=\"onInput($event)\"\n (beforeinput)=\"onBeforeInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (paste)=\"onPaste($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur($event)\"\n (mouseup)=\"onSelectionChange()\"\n (keyup)=\"onSelectionChange()\"\n (click)=\"onEditorClick($event)\"\n (mousemove)=\"onEditorMouseMove($event)\"\n (mousedown)=\"onEditorMouseDown($event)\"\n (touchstart)=\"onEditorTouchStart($event)\"\n (contextmenu)=\"onEditorContextMenu($event)\"\n (dragover)=\"onEditorDragOver($event)\"\n (dragleave)=\"onEditorDragLeave($event)\"\n (drop)=\"onEditorDrop($event)\"\n ></div>\n\n @if (findReplaceVisible()) {\n <div data-slot=\"rich-text-find-panel\"\n class=\"absolute top-2 end-2 z-50 bg-popover border rounded-lg shadow-lg p-3 w-80 max-w-[calc(100vw-2rem)] max-sm:w-[calc(100%-1rem)] max-sm:start-2 animate-in slide-in-from-top-2 fade-in-0\"\n tabindex=\"-1\"\n (keydown.escape)=\"closeFindReplace()\"\n (keydown)=\"onFindReplaceKeydown($event)\">\n <div class=\"flex flex-wrap items-center gap-1.5 mb-2\">\n <input\n #findInput\n data-slot=\"rich-text-find-query\"\n type=\"text\"\n [placeholder]=\"resolvedLocale().findReplace.findPlaceholder\"\n [attr.aria-label]=\"resolvedLocale().findReplace.find\"\n [attr.aria-invalid]=\"findRegexError() ? 'true' : null\"\n [value]=\"findQuery()\"\n (input)=\"onFindQueryChange($any($event.target).value)\"\n class=\"flex h-7 min-w-0 flex-1 rounded-md border border-input bg-transparent px-2 py-1 text-xs shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n />\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs font-medium transition-colors hover:bg-accent\"\n [class.bg-accent]=\"findCaseSensitive()\"\n [attr.aria-pressed]=\"findCaseSensitive()\"\n [attr.aria-label]=\"resolvedLocale().findReplace.caseSensitive\"\n [title]=\"resolvedLocale().findReplace.caseSensitive\"\n (click)=\"toggleFindCaseSensitive()\">Aa</button>\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs font-medium transition-colors hover:bg-accent\"\n [class.bg-accent]=\"findWholeWord()\"\n [attr.aria-pressed]=\"findWholeWord()\"\n [attr.aria-label]=\"resolvedLocale().findReplace.wholeWord\"\n [title]=\"resolvedLocale().findReplace.wholeWord\"\n (click)=\"toggleFindWholeWord()\">ab</button>\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs font-medium transition-colors hover:bg-accent\"\n [class.bg-accent]=\"findUseRegex()\"\n [attr.aria-pressed]=\"findUseRegex()\"\n [attr.aria-label]=\"resolvedLocale().findReplace.useRegex\"\n [title]=\"resolvedLocale().findReplace.useRegex\"\n (click)=\"toggleFindUseRegex()\">.*</button>\n </div>\n <div class=\"flex flex-wrap items-center justify-between gap-2 mb-2\">\n <span data-slot=\"rich-text-find-counter\"\n aria-live=\"polite\"\n class=\"text-xs text-muted-foreground\">{{ findCounterText() }}</span>\n <div class=\"flex gap-1\">\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs hover:bg-accent disabled:opacity-50\"\n [attr.aria-label]=\"resolvedLocale().findReplace.previous\"\n [title]=\"resolvedLocale().findReplace.previous\"\n [disabled]=\"findMatchCount() === 0\"\n (click)=\"findPrevious()\">&#x25B2;</button>\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs hover:bg-accent disabled:opacity-50\"\n [attr.aria-label]=\"resolvedLocale().findReplace.next\"\n [title]=\"resolvedLocale().findReplace.next\"\n [disabled]=\"findMatchCount() === 0\"\n (click)=\"findNext()\">&#x25BC;</button>\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs hover:bg-accent\"\n [attr.aria-label]=\"resolvedLocale().findReplace.close\"\n [title]=\"resolvedLocale().findReplace.close\"\n (click)=\"closeFindReplace()\">&#x2715;</button>\n </div>\n </div>\n @if (showReplaceRow()) {\n <div class=\"flex flex-wrap items-center gap-1.5\">\n <input\n data-slot=\"rich-text-find-replace\"\n type=\"text\"\n [placeholder]=\"resolvedLocale().findReplace.replacePlaceholder\"\n [attr.aria-label]=\"resolvedLocale().findReplace.replace\"\n [value]=\"replaceText()\"\n (input)=\"replaceText.set($any($event.target).value)\"\n class=\"flex h-7 min-w-0 flex-1 rounded-md border border-input bg-transparent px-2 py-1 text-xs shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n />\n <button type=\"button\" class=\"inline-flex min-h-7 items-center justify-center rounded-md px-2 py-1 text-xs hover:bg-accent whitespace-nowrap disabled:opacity-50\" [disabled]=\"findMatchCount() === 0\" (click)=\"replaceSingle()\">{{ resolvedLocale().findReplace.replace }}</button>\n <button type=\"button\" class=\"inline-flex min-h-7 items-center justify-center rounded-md px-2 py-1 text-xs hover:bg-accent whitespace-nowrap disabled:opacity-50\" [disabled]=\"findMatchCount() === 0\" (click)=\"replaceAll()\">{{ resolvedLocale().findReplace.replaceAll }}</button>\n </div>\n }\n </div>\n }\n\n @if (dragOver()) {\n <div class=\"absolute inset-0 pointer-events-none border-2 border-dashed border-primary/60 rounded-md bg-primary/5\"></div>\n }\n\n @if (toolbar() === 'floating' && !readonly() && showFloatingToolbar()) {\n <div \n class=\"fixed z-[9999] bg-popover border rounded-lg shadow-lg p-1\"\n [style.left.px]=\"floatingToolbarPosition().x\"\n [style.top.px]=\"floatingToolbarPosition().y\"\n >\n <ui-rich-text-toolbar\n [items]=\"['bold', 'italic', 'underline', 'separator', 'clear']\"\n [activeFormats]=\"emptyFormats\"\n [compact]=\"true\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [locale]=\"resolvedLocale()\"\n (formatCommand)=\"onFloatingFormatCommand($event)\"\n [addonSlots]=\"toolbarSlots.slots()\"\n (addonSlotClick)=\"onAddonSlotClick($event)\"\n />\n </div>\n }\n\n @if (tableContextMenuOpen()) {\n <div\n #tableContextMenuRef\n class=\"fixed z-50 min-w-[180px] max-w-[calc(100vw-2rem)] max-h-[calc(100vh-1rem)] overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95\"\n [style.left.px]=\"tableContextMenuPosition().x\"\n [style.top.px]=\"tableContextMenuPosition().y\"\n (mousedown)=\"$event.preventDefault()\"\n (contextmenu)=\"onContextMenuOverlayContextMenu($event)\"\n >\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"addTableRowAbove()\">\n {{ resolvedLocale().table.addRowAbove }}\n </button>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"addTableRowBelow()\">\n {{ resolvedLocale().table.addRowBelow }}\n </button>\n <div class=\"my-1 h-px bg-border\"></div>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"addTableColumnLeft()\">\n {{ resolvedLocale().table.addColumnLeft }}\n </button>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"addTableColumnRight()\">\n {{ resolvedLocale().table.addColumnRight }}\n </button>\n <div class=\"my-1 h-px bg-border\"></div>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"toggleTableHeaderRow()\">\n {{ resolvedLocale().table.toggleHeaderRow }}\n </button>\n <div class=\"my-1 h-px bg-border\"></div>\n @if (tableCellSelected().length >= 2) {\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"mergeCells()\">\n {{ resolvedLocale().table.mergeCells }}\n </button>\n }\n @if (canSplitCell()) {\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"splitCell()\">\n {{ resolvedLocale().table.splitCell }}\n </button>\n }\n @if (tableCellSelected().length >= 2 || canSplitCell()) {\n <div class=\"my-1 h-px bg-border\"></div>\n }\n <div class=\"px-2 py-1.5\">\n <div class=\"text-xs text-muted-foreground mb-1.5\">{{ resolvedLocale().table.borders }}</div>\n <div class=\"flex items-center gap-1\">\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.bordersAll\" (click)=\"setTableBorders('all')\">\n <svg viewBox=\"0 0 20 20\" class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <rect x=\"1\" y=\"1\" width=\"18\" height=\"18\" /><line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"19\" /><line x1=\"1\" y1=\"10\" x2=\"19\" y2=\"10\" />\n </svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.bordersNone\" (click)=\"setTableBorders('none')\">\n <svg viewBox=\"0 0 20 20\" class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1\" opacity=\"0.35\">\n <rect x=\"1\" y=\"1\" width=\"18\" height=\"18\" stroke-dasharray=\"2 2\" /><line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"19\" stroke-dasharray=\"2 2\" /><line x1=\"1\" y1=\"10\" x2=\"19\" y2=\"10\" stroke-dasharray=\"2 2\" />\n </svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.bordersOuter\" (click)=\"setTableBorders('outer')\">\n <svg viewBox=\"0 0 20 20\" class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\">\n <rect x=\"1\" y=\"1\" width=\"18\" height=\"18\" stroke-width=\"1.5\" /><line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"19\" stroke-width=\"1\" opacity=\"0.25\" stroke-dasharray=\"2 2\" /><line x1=\"1\" y1=\"10\" x2=\"19\" y2=\"10\" stroke-width=\"1\" opacity=\"0.25\" stroke-dasharray=\"2 2\" />\n </svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.bordersHorizontal\" (click)=\"setTableBorders('horizontal')\">\n <svg viewBox=\"0 0 20 20\" class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\">\n <line x1=\"1\" y1=\"1\" x2=\"19\" y2=\"1\" stroke-width=\"1.5\" /><line x1=\"1\" y1=\"10\" x2=\"19\" y2=\"10\" stroke-width=\"1.5\" /><line x1=\"1\" y1=\"19\" x2=\"19\" y2=\"19\" stroke-width=\"1.5\" />\n </svg>\n </button>\n </div>\n </div>\n <div class=\"my-1 h-px bg-border\"></div>\n <div class=\"px-2 py-1.5\">\n <div class=\"text-xs text-muted-foreground mb-1.5\">{{ resolvedLocale().table.cellAlignLeft }}</div>\n <div class=\"flex items-center gap-1\" dir=\"ltr\">\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.cellAlignLeft\" (click)=\"setCellAlignment('left')\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"21\" x2=\"3\" y1=\"6\" y2=\"6\"/><line x1=\"15\" x2=\"3\" y1=\"12\" y2=\"12\"/><line x1=\"17\" x2=\"3\" y1=\"18\" y2=\"18\"/></svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.cellAlignCenter\" (click)=\"setCellAlignment('center')\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"21\" x2=\"3\" y1=\"6\" y2=\"6\"/><line x1=\"17\" x2=\"7\" y1=\"12\" y2=\"12\"/><line x1=\"19\" x2=\"5\" y1=\"18\" y2=\"18\"/></svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.cellAlignRight\" (click)=\"setCellAlignment('right')\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"21\" x2=\"3\" y1=\"6\" y2=\"6\"/><line x1=\"21\" x2=\"9\" y1=\"12\" y2=\"12\"/><line x1=\"21\" x2=\"7\" y1=\"18\" y2=\"18\"/></svg>\n </button>\n </div>\n </div>\n <div class=\"px-2 py-1.5\">\n <div class=\"text-xs text-muted-foreground mb-1.5\">{{ resolvedLocale().table.cellColor }}</div>\n <div class=\"grid grid-cols-8 gap-0.5\">\n @for (color of tableCellColors; track color) {\n <button type=\"button\" class=\"w-4 h-4 rounded border border-border hover:scale-110 transition-transform\" [style.background-color]=\"color\" [attr.aria-label]=\"color\" (click)=\"setCellColor(color)\"></button>\n }\n </div>\n </div>\n <div class=\"my-1 h-px bg-border\"></div>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground text-destructive\" (click)=\"deleteTableRow()\">\n {{ resolvedLocale().table.deleteRow }}\n </button>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground text-destructive\" (click)=\"deleteTableColumn()\">\n {{ resolvedLocale().table.deleteColumn }}\n </button>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground text-destructive\" (click)=\"deleteTable()\">\n {{ resolvedLocale().table.deleteTable }}\n </button>\n </div>\n }\n</div>\n\n@if (showCount() || showWordCount()) {\n <!--\n A polite status region, so a screen reader hears the count change, and the\n limit is shown once one is set: enforcement used to be entirely silent,\n so hitting maxLength read as a dead keyboard with the counter still showing\n a bare number and no indication a limit existed at all. The colour change\n at the limit gives sighted users the same signal.\n -->\n <output [attr.id]=\"counterId\" class=\"flex justify-end text-xs text-muted-foreground mt-1 px-1\" aria-live=\"polite\">\n @if (showCount()) {\n <!--\n The localized phrase keeps its own {count}, and the limit follows as a\n separate parenthetical. Interpolating \"0 / 120\" INTO the slot produced\n \"0 / 120 characters\", which is wrong in English and cannot be localized\n at all -- a locale ordering it differently had no way to express it,\n because the separator was hardcoded in the template expression.\n -->\n <span [class.text-destructive]=\"atCharacterLimit()\" [class.font-medium]=\"atCharacterLimit()\">\n {{ interpolateLocale(resolvedLocale().editor.characters, { count: characterCount() }) }}@if (maxLength()) {<span class=\"ms-1\">({{ maxLength() }} max)</span>}\n </span>\n }\n @if (showWordCount()) {\n <span [class.ms-3]=\"showCount()\">{{ interpolateLocale(resolvedLocale().editor.words, { count: wordCount() }) }}</span>\n }\n </output>\n}\n", styles: ["@media print{:host{display:block;overflow:visible}:host [data-slot=rich-text-editor]{max-height:none!important;height:auto!important;overflow:visible!important;border:none}:host [role=toolbar],:host [data-slot=rich-text-floating-toolbar],:host [data-slot=rich-text-find-overlay],:host [data-slot=rich-text-outline-panel],:host [data-slot=rich-text-history-badge]{display:none!important}:host tr,:host img{break-inside:avoid}:host h1,:host h2,:host h3{break-after:avoid}}:host img[data-blocked-src]{display:inline-flex;align-items:center;justify-content:center;min-width:8rem;min-height:4.5rem;padding:calc(.75rem * var(--_d));border:1px dashed var(--border);border-radius:var(--radius);background-color:color-mix(in oklab,var(--muted) 60%,transparent);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);color:var(--muted-foreground);font-size:.75rem;line-height:1.4;text-align:center;font-style:italic}:host img[data-blocked-src]:after{content:attr(data-blocked-label)}\n"], dependencies: [{ kind: "component", type: RichTextToolbarComponent, selector: "ui-rich-text-toolbar", inputs: ["items", "activeFormats", "compact", "class", "disabled", "readonly", "textStyleAvailable", "locale", "addonSlots"], outputs: ["formatCommand", "addonSlotClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
18687
+ ], viewQueries: [{ propertyName: "editorDiv", first: true, predicate: ["editorDiv"], descendants: true }, { propertyName: "editorContainer", first: true, predicate: ["editorContainer"], descendants: true }, { propertyName: "tableContextMenuRef", first: true, predicate: ["tableContextMenuRef"], descendants: true }], usesInheritance: true, ngImport: i0, template: "@if (toolbar() === 'top' && !readonly()) {\n <ui-rich-text-toolbar\n [items]=\"toolbarItems()\"\n [activeFormats]=\"activeFormats()\"\n [textStyleAvailable]=\"textStyleAvailable()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [locale]=\"resolvedLocale()\"\n (formatCommand)=\"onFormatCommand($event)\"\n [addonSlots]=\"toolbarSlots.slots()\"\n (addonSlotClick)=\"onAddonSlotClick($event)\"\n />\n}\n\n<div #editorContainer [class]=\"editorContainerClasses()\" [dir]=\"resolvedDir()\">\n <!--\n Three of the attributes below exist for assistive tech specifically:\n\n The tab index drops to -1 when disabled. A native disabled control leaves the\n tab order entirely; a static 0 left a keyboard user stopping on an editor\n that takes focus and then does nothing.\n\n The readonly state is announced explicitly, because contenteditable false is\n a browser editing hint rather than a state screen readers reliably report on\n a textbox \u2014 without it a reader discovers the field is read-only by typing\n and seeing nothing happen.\n\n The placeholder is announced explicitly too, because the visible one is CSS\n generated content. Chromium exposes that as a plain text child, so a reader\n announces the hint as though it were the document's own text.\n -->\n <div\n #editorDiv\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [attr.contenteditable]=\"!isDisabled() && !readonly()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-readonly]=\"readonly() ? 'true' : null\"\n [class]=\"editableClasses()\"\n [attr.placeholder]=\"placeholder() || resolvedLocale().editor.placeholder\"\n [attr.aria-placeholder]=\"placeholder() || resolvedLocale().editor.placeholder\"\n [attr.aria-label]=\"ariaLabel() || resolvedLocale().editor.ariaLabel\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.data-slot]=\"'rich-text-editor'\"\n [style.min-height]=\"minHeight()\"\n [style.max-height]=\"maxHeight()\"\n role=\"textbox\"\n aria-multiline=\"true\"\n [attr.aria-expanded]=\"activeSuggestionPopup() ? 'true' : null\"\n [attr.aria-controls]=\"activeSuggestionPopup()?.controlsId ?? null\"\n [attr.aria-activedescendant]=\"activeSuggestionPopup()?.activeOptionId ?? null\"\n (input)=\"onInput($event)\"\n (beforeinput)=\"onBeforeInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (paste)=\"onPaste($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur($event)\"\n (mouseup)=\"onSelectionChange()\"\n (keyup)=\"onSelectionChange()\"\n (click)=\"onEditorClick($event)\"\n (mousemove)=\"onEditorMouseMove($event)\"\n (mousedown)=\"onEditorMouseDown($event)\"\n (touchstart)=\"onEditorTouchStart($event)\"\n (contextmenu)=\"onEditorContextMenu($event)\"\n (dragover)=\"onEditorDragOver($event)\"\n (dragleave)=\"onEditorDragLeave($event)\"\n (drop)=\"onEditorDrop($event)\"\n ></div>\n\n @if (findReplaceVisible()) {\n <div data-slot=\"rich-text-find-panel\"\n class=\"absolute top-2 end-2 z-50 bg-popover border rounded-lg shadow-lg p-3 w-80 max-w-[calc(100vw-2rem)] max-sm:w-[calc(100%-1rem)] max-sm:start-2 animate-in slide-in-from-top-2 fade-in-0\"\n tabindex=\"-1\"\n (keydown.escape)=\"closeFindReplace()\"\n (keydown)=\"onFindReplaceKeydown($event)\">\n <div class=\"flex flex-wrap items-center gap-1.5 mb-2\">\n <input\n #findInput\n data-slot=\"rich-text-find-query\"\n type=\"text\"\n [placeholder]=\"resolvedLocale().findReplace.findPlaceholder\"\n [attr.aria-label]=\"resolvedLocale().findReplace.find\"\n [attr.aria-invalid]=\"findRegexError() ? 'true' : null\"\n [value]=\"findQuery()\"\n (input)=\"onFindQueryChange($any($event.target).value)\"\n class=\"flex h-7 min-w-0 flex-1 rounded-md border border-input bg-transparent px-2 py-1 text-xs shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n />\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs font-medium transition-colors hover:bg-accent\"\n [class.bg-accent]=\"findCaseSensitive()\"\n [attr.aria-pressed]=\"findCaseSensitive()\"\n [attr.aria-label]=\"resolvedLocale().findReplace.caseSensitive\"\n [title]=\"resolvedLocale().findReplace.caseSensitive\"\n (click)=\"toggleFindCaseSensitive()\">Aa</button>\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs font-medium transition-colors hover:bg-accent\"\n [class.bg-accent]=\"findWholeWord()\"\n [attr.aria-pressed]=\"findWholeWord()\"\n [attr.aria-label]=\"resolvedLocale().findReplace.wholeWord\"\n [title]=\"resolvedLocale().findReplace.wholeWord\"\n (click)=\"toggleFindWholeWord()\">ab</button>\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs font-medium transition-colors hover:bg-accent\"\n [class.bg-accent]=\"findUseRegex()\"\n [attr.aria-pressed]=\"findUseRegex()\"\n [attr.aria-label]=\"resolvedLocale().findReplace.useRegex\"\n [title]=\"resolvedLocale().findReplace.useRegex\"\n (click)=\"toggleFindUseRegex()\">.*</button>\n </div>\n <div class=\"flex flex-wrap items-center justify-between gap-2 mb-2\">\n <span data-slot=\"rich-text-find-counter\"\n aria-live=\"polite\"\n class=\"text-xs text-muted-foreground\">{{ findCounterText() }}</span>\n <div class=\"flex gap-1\">\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs hover:bg-accent disabled:opacity-50\"\n [attr.aria-label]=\"resolvedLocale().findReplace.previous\"\n [title]=\"resolvedLocale().findReplace.previous\"\n [disabled]=\"findMatchCount() === 0\"\n (click)=\"findPrevious()\">&#x25B2;</button>\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs hover:bg-accent disabled:opacity-50\"\n [attr.aria-label]=\"resolvedLocale().findReplace.next\"\n [title]=\"resolvedLocale().findReplace.next\"\n [disabled]=\"findMatchCount() === 0\"\n (click)=\"findNext()\">&#x25BC;</button>\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs hover:bg-accent\"\n [attr.aria-label]=\"resolvedLocale().findReplace.close\"\n [title]=\"resolvedLocale().findReplace.close\"\n (click)=\"closeFindReplace()\">&#x2715;</button>\n </div>\n </div>\n @if (showReplaceRow()) {\n <div class=\"flex flex-wrap items-center gap-1.5\">\n <input\n data-slot=\"rich-text-find-replace\"\n type=\"text\"\n [placeholder]=\"resolvedLocale().findReplace.replacePlaceholder\"\n [attr.aria-label]=\"resolvedLocale().findReplace.replace\"\n [value]=\"replaceText()\"\n (input)=\"replaceText.set($any($event.target).value)\"\n class=\"flex h-7 min-w-0 flex-1 rounded-md border border-input bg-transparent px-2 py-1 text-xs shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n />\n <button type=\"button\" class=\"inline-flex min-h-7 items-center justify-center rounded-md px-2 py-1 text-xs hover:bg-accent whitespace-nowrap disabled:opacity-50\" [disabled]=\"findMatchCount() === 0\" (click)=\"replaceSingle()\">{{ resolvedLocale().findReplace.replace }}</button>\n <button type=\"button\" class=\"inline-flex min-h-7 items-center justify-center rounded-md px-2 py-1 text-xs hover:bg-accent whitespace-nowrap disabled:opacity-50\" [disabled]=\"findMatchCount() === 0\" (click)=\"replaceAll()\">{{ resolvedLocale().findReplace.replaceAll }}</button>\n </div>\n }\n </div>\n }\n\n @if (dragOver()) {\n <div class=\"absolute inset-0 pointer-events-none border-2 border-dashed border-primary/60 rounded-md bg-primary/5\"></div>\n }\n\n @if (toolbar() === 'floating' && !readonly() && showFloatingToolbar()) {\n <div \n class=\"fixed z-[9999] bg-popover border rounded-lg shadow-lg p-1\"\n [style.left.px]=\"floatingToolbarPosition().x\"\n [style.top.px]=\"floatingToolbarPosition().y\"\n >\n <ui-rich-text-toolbar\n [items]=\"['bold', 'italic', 'underline', 'separator', 'clear']\"\n [activeFormats]=\"emptyFormats\"\n [compact]=\"true\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [locale]=\"resolvedLocale()\"\n (formatCommand)=\"onFloatingFormatCommand($event)\"\n [addonSlots]=\"toolbarSlots.slots()\"\n (addonSlotClick)=\"onAddonSlotClick($event)\"\n />\n </div>\n }\n\n @if (tableContextMenuOpen()) {\n <div\n #tableContextMenuRef\n class=\"fixed z-50 min-w-[180px] max-w-[calc(100vw-2rem)] max-h-[calc(100vh-1rem)] overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95\"\n [style.left.px]=\"tableContextMenuPosition().x\"\n [style.top.px]=\"tableContextMenuPosition().y\"\n (mousedown)=\"$event.preventDefault()\"\n (contextmenu)=\"onContextMenuOverlayContextMenu($event)\"\n >\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"addTableRowAbove()\">\n {{ resolvedLocale().table.addRowAbove }}\n </button>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"addTableRowBelow()\">\n {{ resolvedLocale().table.addRowBelow }}\n </button>\n <div class=\"my-1 h-px bg-border\"></div>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"addTableColumnLeft()\">\n {{ resolvedLocale().table.addColumnLeft }}\n </button>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"addTableColumnRight()\">\n {{ resolvedLocale().table.addColumnRight }}\n </button>\n <div class=\"my-1 h-px bg-border\"></div>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"toggleTableHeaderRow()\">\n {{ resolvedLocale().table.toggleHeaderRow }}\n </button>\n <div class=\"my-1 h-px bg-border\"></div>\n @if (tableCellSelected().length >= 2) {\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"mergeCells()\">\n {{ resolvedLocale().table.mergeCells }}\n </button>\n }\n @if (canSplitCell()) {\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"splitCell()\">\n {{ resolvedLocale().table.splitCell }}\n </button>\n }\n @if (tableCellSelected().length >= 2 || canSplitCell()) {\n <div class=\"my-1 h-px bg-border\"></div>\n }\n <div class=\"px-2 py-1.5\">\n <div class=\"text-xs text-muted-foreground mb-1.5\">{{ resolvedLocale().table.borders }}</div>\n <div class=\"flex items-center gap-1\">\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.bordersAll\" (click)=\"setTableBorders('all')\">\n <svg viewBox=\"0 0 20 20\" class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <rect x=\"1\" y=\"1\" width=\"18\" height=\"18\" /><line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"19\" /><line x1=\"1\" y1=\"10\" x2=\"19\" y2=\"10\" />\n </svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.bordersNone\" (click)=\"setTableBorders('none')\">\n <svg viewBox=\"0 0 20 20\" class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1\" opacity=\"0.35\">\n <rect x=\"1\" y=\"1\" width=\"18\" height=\"18\" stroke-dasharray=\"2 2\" /><line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"19\" stroke-dasharray=\"2 2\" /><line x1=\"1\" y1=\"10\" x2=\"19\" y2=\"10\" stroke-dasharray=\"2 2\" />\n </svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.bordersOuter\" (click)=\"setTableBorders('outer')\">\n <svg viewBox=\"0 0 20 20\" class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\">\n <rect x=\"1\" y=\"1\" width=\"18\" height=\"18\" stroke-width=\"1.5\" /><line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"19\" stroke-width=\"1\" opacity=\"0.25\" stroke-dasharray=\"2 2\" /><line x1=\"1\" y1=\"10\" x2=\"19\" y2=\"10\" stroke-width=\"1\" opacity=\"0.25\" stroke-dasharray=\"2 2\" />\n </svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.bordersHorizontal\" (click)=\"setTableBorders('horizontal')\">\n <svg viewBox=\"0 0 20 20\" class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\">\n <line x1=\"1\" y1=\"1\" x2=\"19\" y2=\"1\" stroke-width=\"1.5\" /><line x1=\"1\" y1=\"10\" x2=\"19\" y2=\"10\" stroke-width=\"1.5\" /><line x1=\"1\" y1=\"19\" x2=\"19\" y2=\"19\" stroke-width=\"1.5\" />\n </svg>\n </button>\n </div>\n </div>\n <div class=\"my-1 h-px bg-border\"></div>\n <div class=\"px-2 py-1.5\">\n <div class=\"text-xs text-muted-foreground mb-1.5\">{{ resolvedLocale().table.cellAlignLeft }}</div>\n <div class=\"flex items-center gap-1\" dir=\"ltr\">\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.cellAlignLeft\" (click)=\"setCellAlignment('left')\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"21\" x2=\"3\" y1=\"6\" y2=\"6\"/><line x1=\"15\" x2=\"3\" y1=\"12\" y2=\"12\"/><line x1=\"17\" x2=\"3\" y1=\"18\" y2=\"18\"/></svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.cellAlignCenter\" (click)=\"setCellAlignment('center')\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"21\" x2=\"3\" y1=\"6\" y2=\"6\"/><line x1=\"17\" x2=\"7\" y1=\"12\" y2=\"12\"/><line x1=\"19\" x2=\"5\" y1=\"18\" y2=\"18\"/></svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.cellAlignRight\" (click)=\"setCellAlignment('right')\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"21\" x2=\"3\" y1=\"6\" y2=\"6\"/><line x1=\"21\" x2=\"9\" y1=\"12\" y2=\"12\"/><line x1=\"21\" x2=\"7\" y1=\"18\" y2=\"18\"/></svg>\n </button>\n </div>\n </div>\n <div class=\"px-2 py-1.5\">\n <div class=\"text-xs text-muted-foreground mb-1.5\">{{ resolvedLocale().table.cellColor }}</div>\n <div class=\"grid grid-cols-8 gap-0.5\">\n @for (color of tableCellColors; track color) {\n <button type=\"button\" class=\"w-4 h-4 rounded border border-border hover:scale-110 transition-transform\" [style.background-color]=\"color\" [attr.aria-label]=\"color\" (click)=\"setCellColor(color)\"></button>\n }\n </div>\n </div>\n <div class=\"my-1 h-px bg-border\"></div>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground text-destructive\" (click)=\"deleteTableRow()\">\n {{ resolvedLocale().table.deleteRow }}\n </button>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground text-destructive\" (click)=\"deleteTableColumn()\">\n {{ resolvedLocale().table.deleteColumn }}\n </button>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground text-destructive\" (click)=\"deleteTable()\">\n {{ resolvedLocale().table.deleteTable }}\n </button>\n </div>\n }\n</div>\n\n@if (showCount() || showWordCount()) {\n <!--\n A polite status region, so a screen reader hears the count change, and the\n limit is shown once one is set: enforcement used to be entirely silent,\n so hitting maxLength read as a dead keyboard with the counter still showing\n a bare number and no indication a limit existed at all. The colour change\n at the limit gives sighted users the same signal.\n -->\n <output [attr.id]=\"counterId\" class=\"flex justify-end text-xs text-muted-foreground mt-1 px-1\" aria-live=\"polite\">\n @if (showCount()) {\n <!--\n The localized phrase keeps its own {count}, and the limit follows as a\n separate parenthetical. Interpolating \"0 / 120\" INTO the slot produced\n \"0 / 120 characters\", which is wrong in English and cannot be localized\n at all -- a locale ordering it differently had no way to express it,\n because the separator was hardcoded in the template expression.\n -->\n <span [class.text-destructive]=\"atCharacterLimit()\" [class.font-medium]=\"atCharacterLimit()\">\n {{ interpolateLocale(resolvedLocale().editor.characters, { count: characterCount() }) }}@if (maxLength()) {<span class=\"ms-1\">({{ maxLength() }} max)</span>}\n </span>\n }\n @if (showWordCount()) {\n <span [class.ms-3]=\"showCount()\">{{ interpolateLocale(resolvedLocale().editor.words, { count: wordCount() }) }}</span>\n }\n </output>\n}\n", styles: ["@media print{:host{display:block;overflow:visible}:host [data-slot=rich-text-editor]{max-height:none!important;height:auto!important;overflow:visible!important;border:none}:host [role=toolbar],:host [data-slot=rich-text-floating-toolbar],:host [data-slot=rich-text-find-overlay],:host [data-slot=rich-text-outline-panel],:host [data-slot=rich-text-history-badge]{display:none!important}:host tr,:host img{break-inside:avoid}:host h1,:host h2,:host h3{break-after:avoid}}:host img[data-blocked-src]{display:inline-flex;align-items:center;justify-content:center;min-width:8rem;min-height:4.5rem;padding:calc(.75rem * var(--_d));border:1px dashed var(--border);border-radius:var(--radius);background-color:color-mix(in oklab,var(--muted) 60%,transparent);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);color:var(--muted-foreground);font-size:.75rem;line-height:1.4;text-align:center;font-style:italic}:host img[data-blocked-src]:after{content:attr(data-blocked-label)}:host([data-ui-theme=\"neutral\"]){--primary: oklch(.205 0 0);--primary-foreground: oklch(.985 0 0);--ring: oklch(.708 0 0)}:host-context(.dark)[data-ui-theme=neutral]{--primary: oklch(.985 0 0);--primary-foreground: oklch(.205 0 0);--ring: oklch(.708 0 0)}:host([data-ui-theme=\"zinc\"]){--primary: oklch(.205 0 0);--primary-foreground: oklch(.985 0 0);--ring: oklch(.708 0 0)}:host-context(.dark)[data-ui-theme=zinc]{--primary: oklch(.985 0 0);--primary-foreground: oklch(.205 0 0);--ring: oklch(.708 0 0)}:host([data-ui-theme=\"slate\"]){--primary: oklch(.208 .042 265.755);--primary-foreground: oklch(.984 .003 247.858);--ring: oklch(.704 .04 256.788)}:host-context(.dark)[data-ui-theme=slate]{--primary: oklch(.929 .013 255.508);--primary-foreground: oklch(.208 .042 265.755);--ring: oklch(.704 .04 256.788)}:host([data-ui-theme=\"stone\"]){--primary: oklch(.216 .006 56.043);--primary-foreground: oklch(.985 .001 106.423);--ring: oklch(.709 .01 56.259)}:host-context(.dark)[data-ui-theme=stone]{--primary: oklch(.923 .003 48.717);--primary-foreground: oklch(.216 .006 56.043);--ring: oklch(.553 .013 58.071)}:host([data-ui-theme=\"gray\"]){--primary: oklch(.21 .028 264.532);--primary-foreground: oklch(.985 .002 247.839);--ring: oklch(.707 .022 264.436)}:host-context(.dark)[data-ui-theme=gray]{--primary: oklch(.928 .006 264.531);--primary-foreground: oklch(.21 .028 264.532);--ring: oklch(.551 .027 264.364)}:host([data-ui-theme=\"red\"]){--primary: oklch(.577 .245 27.325);--primary-foreground: oklch(.985 0 0);--ring: oklch(.577 .245 27.325)}:host-context(.dark)[data-ui-theme=red]{--primary: oklch(.577 .245 27.325);--primary-foreground: oklch(.985 0 0);--ring: oklch(.577 .245 27.325)}:host([data-ui-theme=\"rose\"]){--primary: oklch(.645 .246 16.439);--primary-foreground: oklch(.985 0 0);--ring: oklch(.645 .246 16.439)}:host-context(.dark)[data-ui-theme=rose]{--primary: oklch(.645 .246 16.439);--primary-foreground: oklch(.985 0 0);--ring: oklch(.645 .246 16.439)}:host([data-ui-theme=\"orange\"]){--primary: oklch(.646 .222 41.116);--primary-foreground: oklch(.985 0 0);--ring: oklch(.646 .222 41.116)}:host-context(.dark)[data-ui-theme=orange]{--primary: oklch(.646 .222 41.116);--primary-foreground: oklch(.985 0 0);--ring: oklch(.646 .222 41.116)}:host([data-ui-theme=\"green\"]){--primary: oklch(.623 .214 131.655);--primary-foreground: oklch(.985 0 0);--ring: oklch(.623 .214 131.655)}:host-context(.dark)[data-ui-theme=green]{--primary: oklch(.623 .214 131.655);--primary-foreground: oklch(.985 0 0);--ring: oklch(.623 .214 131.655)}:host([data-ui-theme=\"blue\"]){--primary: oklch(.546 .245 262.881);--primary-foreground: oklch(.985 0 0);--ring: oklch(.546 .245 262.881)}:host-context(.dark)[data-ui-theme=blue]{--primary: oklch(.546 .245 262.881);--primary-foreground: oklch(.985 0 0);--ring: oklch(.546 .245 262.881)}:host([data-ui-theme=\"yellow\"]){--primary: oklch(.82 .185 84.67);--primary-foreground: oklch(.205 0 0);--ring: oklch(.82 .185 84.67)}:host-context(.dark)[data-ui-theme=yellow]{--primary: oklch(.82 .185 84.67);--primary-foreground: oklch(.205 0 0);--ring: oklch(.82 .185 84.67)}:host([data-ui-theme=\"violet\"]){--primary: oklch(.558 .288 302.321);--primary-foreground: oklch(.985 0 0);--ring: oklch(.558 .288 302.321)}:host-context(.dark)[data-ui-theme=violet]{--primary: oklch(.558 .288 302.321);--primary-foreground: oklch(.985 0 0);--ring: oklch(.558 .288 302.321)}:host([data-ui-theme=\"amber\"]){--primary: oklch(.77 .16 70);--primary-foreground: oklch(.145 0 0);--ring: oklch(.77 .16 70)}:host-context(.dark)[data-ui-theme=amber]{--primary: oklch(.77 .16 70);--primary-foreground: oklch(.145 0 0);--ring: oklch(.77 .16 70)}\n"], dependencies: [{ kind: "component", type: RichTextToolbarComponent, selector: "ui-rich-text-toolbar", inputs: ["items", "activeFormats", "compact", "class", "disabled", "readonly", "textStyleAvailable", "locale", "addonSlots"], outputs: ["formatCommand", "addonSlotClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
17879
18688
  }
17880
18689
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RichTextEditorComponent, decorators: [{
17881
18690
  type: Component,
@@ -17905,7 +18714,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
17905
18714
  provideComponentLocale(() => RichTextEditorComponent),
17906
18715
  ], host: {
17907
18716
  class: 'block',
17908
- }, template: "@if (toolbar() === 'top' && !readonly()) {\n <ui-rich-text-toolbar\n [items]=\"toolbarItems()\"\n [activeFormats]=\"activeFormats()\"\n [textStyleAvailable]=\"textStyleAvailable()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [locale]=\"resolvedLocale()\"\n (formatCommand)=\"onFormatCommand($event)\"\n [addonSlots]=\"toolbarSlots.slots()\"\n (addonSlotClick)=\"onAddonSlotClick($event)\"\n />\n}\n\n<div #editorContainer [class]=\"editorContainerClasses()\" [dir]=\"resolvedDir()\">\n <!--\n Three of the attributes below exist for assistive tech specifically:\n\n The tab index drops to -1 when disabled. A native disabled control leaves the\n tab order entirely; a static 0 left a keyboard user stopping on an editor\n that takes focus and then does nothing.\n\n The readonly state is announced explicitly, because contenteditable false is\n a browser editing hint rather than a state screen readers reliably report on\n a textbox \u2014 without it a reader discovers the field is read-only by typing\n and seeing nothing happen.\n\n The placeholder is announced explicitly too, because the visible one is CSS\n generated content. Chromium exposes that as a plain text child, so a reader\n announces the hint as though it were the document's own text.\n -->\n <div\n #editorDiv\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [attr.contenteditable]=\"!isDisabled() && !readonly()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-readonly]=\"readonly() ? 'true' : null\"\n [class]=\"editableClasses()\"\n [attr.placeholder]=\"placeholder() || resolvedLocale().editor.placeholder\"\n [attr.aria-placeholder]=\"placeholder() || resolvedLocale().editor.placeholder\"\n [attr.aria-label]=\"ariaLabel() || resolvedLocale().editor.ariaLabel\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.data-slot]=\"'rich-text-editor'\"\n [style.min-height]=\"minHeight()\"\n [style.max-height]=\"maxHeight()\"\n role=\"textbox\"\n aria-multiline=\"true\"\n [attr.aria-expanded]=\"activeSuggestionPopup() ? 'true' : null\"\n [attr.aria-controls]=\"activeSuggestionPopup()?.controlsId ?? null\"\n [attr.aria-activedescendant]=\"activeSuggestionPopup()?.activeOptionId ?? null\"\n (input)=\"onInput($event)\"\n (beforeinput)=\"onBeforeInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (paste)=\"onPaste($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur($event)\"\n (mouseup)=\"onSelectionChange()\"\n (keyup)=\"onSelectionChange()\"\n (click)=\"onEditorClick($event)\"\n (mousemove)=\"onEditorMouseMove($event)\"\n (mousedown)=\"onEditorMouseDown($event)\"\n (touchstart)=\"onEditorTouchStart($event)\"\n (contextmenu)=\"onEditorContextMenu($event)\"\n (dragover)=\"onEditorDragOver($event)\"\n (dragleave)=\"onEditorDragLeave($event)\"\n (drop)=\"onEditorDrop($event)\"\n ></div>\n\n @if (findReplaceVisible()) {\n <div data-slot=\"rich-text-find-panel\"\n class=\"absolute top-2 end-2 z-50 bg-popover border rounded-lg shadow-lg p-3 w-80 max-w-[calc(100vw-2rem)] max-sm:w-[calc(100%-1rem)] max-sm:start-2 animate-in slide-in-from-top-2 fade-in-0\"\n tabindex=\"-1\"\n (keydown.escape)=\"closeFindReplace()\"\n (keydown)=\"onFindReplaceKeydown($event)\">\n <div class=\"flex flex-wrap items-center gap-1.5 mb-2\">\n <input\n #findInput\n data-slot=\"rich-text-find-query\"\n type=\"text\"\n [placeholder]=\"resolvedLocale().findReplace.findPlaceholder\"\n [attr.aria-label]=\"resolvedLocale().findReplace.find\"\n [attr.aria-invalid]=\"findRegexError() ? 'true' : null\"\n [value]=\"findQuery()\"\n (input)=\"onFindQueryChange($any($event.target).value)\"\n class=\"flex h-7 min-w-0 flex-1 rounded-md border border-input bg-transparent px-2 py-1 text-xs shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n />\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs font-medium transition-colors hover:bg-accent\"\n [class.bg-accent]=\"findCaseSensitive()\"\n [attr.aria-pressed]=\"findCaseSensitive()\"\n [attr.aria-label]=\"resolvedLocale().findReplace.caseSensitive\"\n [title]=\"resolvedLocale().findReplace.caseSensitive\"\n (click)=\"toggleFindCaseSensitive()\">Aa</button>\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs font-medium transition-colors hover:bg-accent\"\n [class.bg-accent]=\"findWholeWord()\"\n [attr.aria-pressed]=\"findWholeWord()\"\n [attr.aria-label]=\"resolvedLocale().findReplace.wholeWord\"\n [title]=\"resolvedLocale().findReplace.wholeWord\"\n (click)=\"toggleFindWholeWord()\">ab</button>\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs font-medium transition-colors hover:bg-accent\"\n [class.bg-accent]=\"findUseRegex()\"\n [attr.aria-pressed]=\"findUseRegex()\"\n [attr.aria-label]=\"resolvedLocale().findReplace.useRegex\"\n [title]=\"resolvedLocale().findReplace.useRegex\"\n (click)=\"toggleFindUseRegex()\">.*</button>\n </div>\n <div class=\"flex flex-wrap items-center justify-between gap-2 mb-2\">\n <span data-slot=\"rich-text-find-counter\"\n aria-live=\"polite\"\n class=\"text-xs text-muted-foreground\">{{ findCounterText() }}</span>\n <div class=\"flex gap-1\">\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs hover:bg-accent disabled:opacity-50\"\n [attr.aria-label]=\"resolvedLocale().findReplace.previous\"\n [title]=\"resolvedLocale().findReplace.previous\"\n [disabled]=\"findMatchCount() === 0\"\n (click)=\"findPrevious()\">&#x25B2;</button>\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs hover:bg-accent disabled:opacity-50\"\n [attr.aria-label]=\"resolvedLocale().findReplace.next\"\n [title]=\"resolvedLocale().findReplace.next\"\n [disabled]=\"findMatchCount() === 0\"\n (click)=\"findNext()\">&#x25BC;</button>\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs hover:bg-accent\"\n [attr.aria-label]=\"resolvedLocale().findReplace.close\"\n [title]=\"resolvedLocale().findReplace.close\"\n (click)=\"closeFindReplace()\">&#x2715;</button>\n </div>\n </div>\n @if (showReplaceRow()) {\n <div class=\"flex flex-wrap items-center gap-1.5\">\n <input\n data-slot=\"rich-text-find-replace\"\n type=\"text\"\n [placeholder]=\"resolvedLocale().findReplace.replacePlaceholder\"\n [attr.aria-label]=\"resolvedLocale().findReplace.replace\"\n [value]=\"replaceText()\"\n (input)=\"replaceText.set($any($event.target).value)\"\n class=\"flex h-7 min-w-0 flex-1 rounded-md border border-input bg-transparent px-2 py-1 text-xs shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n />\n <button type=\"button\" class=\"inline-flex min-h-7 items-center justify-center rounded-md px-2 py-1 text-xs hover:bg-accent whitespace-nowrap disabled:opacity-50\" [disabled]=\"findMatchCount() === 0\" (click)=\"replaceSingle()\">{{ resolvedLocale().findReplace.replace }}</button>\n <button type=\"button\" class=\"inline-flex min-h-7 items-center justify-center rounded-md px-2 py-1 text-xs hover:bg-accent whitespace-nowrap disabled:opacity-50\" [disabled]=\"findMatchCount() === 0\" (click)=\"replaceAll()\">{{ resolvedLocale().findReplace.replaceAll }}</button>\n </div>\n }\n </div>\n }\n\n @if (dragOver()) {\n <div class=\"absolute inset-0 pointer-events-none border-2 border-dashed border-primary/60 rounded-md bg-primary/5\"></div>\n }\n\n @if (toolbar() === 'floating' && !readonly() && showFloatingToolbar()) {\n <div \n class=\"fixed z-[9999] bg-popover border rounded-lg shadow-lg p-1\"\n [style.left.px]=\"floatingToolbarPosition().x\"\n [style.top.px]=\"floatingToolbarPosition().y\"\n >\n <ui-rich-text-toolbar\n [items]=\"['bold', 'italic', 'underline', 'separator', 'clear']\"\n [activeFormats]=\"emptyFormats\"\n [compact]=\"true\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [locale]=\"resolvedLocale()\"\n (formatCommand)=\"onFloatingFormatCommand($event)\"\n [addonSlots]=\"toolbarSlots.slots()\"\n (addonSlotClick)=\"onAddonSlotClick($event)\"\n />\n </div>\n }\n\n @if (tableContextMenuOpen()) {\n <div\n #tableContextMenuRef\n class=\"fixed z-50 min-w-[180px] max-w-[calc(100vw-2rem)] max-h-[calc(100vh-1rem)] overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95\"\n [style.left.px]=\"tableContextMenuPosition().x\"\n [style.top.px]=\"tableContextMenuPosition().y\"\n (mousedown)=\"$event.preventDefault()\"\n (contextmenu)=\"onContextMenuOverlayContextMenu($event)\"\n >\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"addTableRowAbove()\">\n {{ resolvedLocale().table.addRowAbove }}\n </button>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"addTableRowBelow()\">\n {{ resolvedLocale().table.addRowBelow }}\n </button>\n <div class=\"my-1 h-px bg-border\"></div>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"addTableColumnLeft()\">\n {{ resolvedLocale().table.addColumnLeft }}\n </button>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"addTableColumnRight()\">\n {{ resolvedLocale().table.addColumnRight }}\n </button>\n <div class=\"my-1 h-px bg-border\"></div>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"toggleTableHeaderRow()\">\n {{ resolvedLocale().table.toggleHeaderRow }}\n </button>\n <div class=\"my-1 h-px bg-border\"></div>\n @if (tableCellSelected().length >= 2) {\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"mergeCells()\">\n {{ resolvedLocale().table.mergeCells }}\n </button>\n }\n @if (canSplitCell()) {\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"splitCell()\">\n {{ resolvedLocale().table.splitCell }}\n </button>\n }\n @if (tableCellSelected().length >= 2 || canSplitCell()) {\n <div class=\"my-1 h-px bg-border\"></div>\n }\n <div class=\"px-2 py-1.5\">\n <div class=\"text-xs text-muted-foreground mb-1.5\">{{ resolvedLocale().table.borders }}</div>\n <div class=\"flex items-center gap-1\">\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.bordersAll\" (click)=\"setTableBorders('all')\">\n <svg viewBox=\"0 0 20 20\" class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <rect x=\"1\" y=\"1\" width=\"18\" height=\"18\" /><line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"19\" /><line x1=\"1\" y1=\"10\" x2=\"19\" y2=\"10\" />\n </svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.bordersNone\" (click)=\"setTableBorders('none')\">\n <svg viewBox=\"0 0 20 20\" class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1\" opacity=\"0.35\">\n <rect x=\"1\" y=\"1\" width=\"18\" height=\"18\" stroke-dasharray=\"2 2\" /><line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"19\" stroke-dasharray=\"2 2\" /><line x1=\"1\" y1=\"10\" x2=\"19\" y2=\"10\" stroke-dasharray=\"2 2\" />\n </svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.bordersOuter\" (click)=\"setTableBorders('outer')\">\n <svg viewBox=\"0 0 20 20\" class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\">\n <rect x=\"1\" y=\"1\" width=\"18\" height=\"18\" stroke-width=\"1.5\" /><line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"19\" stroke-width=\"1\" opacity=\"0.25\" stroke-dasharray=\"2 2\" /><line x1=\"1\" y1=\"10\" x2=\"19\" y2=\"10\" stroke-width=\"1\" opacity=\"0.25\" stroke-dasharray=\"2 2\" />\n </svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.bordersHorizontal\" (click)=\"setTableBorders('horizontal')\">\n <svg viewBox=\"0 0 20 20\" class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\">\n <line x1=\"1\" y1=\"1\" x2=\"19\" y2=\"1\" stroke-width=\"1.5\" /><line x1=\"1\" y1=\"10\" x2=\"19\" y2=\"10\" stroke-width=\"1.5\" /><line x1=\"1\" y1=\"19\" x2=\"19\" y2=\"19\" stroke-width=\"1.5\" />\n </svg>\n </button>\n </div>\n </div>\n <div class=\"my-1 h-px bg-border\"></div>\n <div class=\"px-2 py-1.5\">\n <div class=\"text-xs text-muted-foreground mb-1.5\">{{ resolvedLocale().table.cellAlignLeft }}</div>\n <div class=\"flex items-center gap-1\" dir=\"ltr\">\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.cellAlignLeft\" (click)=\"setCellAlignment('left')\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"21\" x2=\"3\" y1=\"6\" y2=\"6\"/><line x1=\"15\" x2=\"3\" y1=\"12\" y2=\"12\"/><line x1=\"17\" x2=\"3\" y1=\"18\" y2=\"18\"/></svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.cellAlignCenter\" (click)=\"setCellAlignment('center')\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"21\" x2=\"3\" y1=\"6\" y2=\"6\"/><line x1=\"17\" x2=\"7\" y1=\"12\" y2=\"12\"/><line x1=\"19\" x2=\"5\" y1=\"18\" y2=\"18\"/></svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.cellAlignRight\" (click)=\"setCellAlignment('right')\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"21\" x2=\"3\" y1=\"6\" y2=\"6\"/><line x1=\"21\" x2=\"9\" y1=\"12\" y2=\"12\"/><line x1=\"21\" x2=\"7\" y1=\"18\" y2=\"18\"/></svg>\n </button>\n </div>\n </div>\n <div class=\"px-2 py-1.5\">\n <div class=\"text-xs text-muted-foreground mb-1.5\">{{ resolvedLocale().table.cellColor }}</div>\n <div class=\"grid grid-cols-8 gap-0.5\">\n @for (color of tableCellColors; track color) {\n <button type=\"button\" class=\"w-4 h-4 rounded border border-border hover:scale-110 transition-transform\" [style.background-color]=\"color\" [attr.aria-label]=\"color\" (click)=\"setCellColor(color)\"></button>\n }\n </div>\n </div>\n <div class=\"my-1 h-px bg-border\"></div>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground text-destructive\" (click)=\"deleteTableRow()\">\n {{ resolvedLocale().table.deleteRow }}\n </button>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground text-destructive\" (click)=\"deleteTableColumn()\">\n {{ resolvedLocale().table.deleteColumn }}\n </button>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground text-destructive\" (click)=\"deleteTable()\">\n {{ resolvedLocale().table.deleteTable }}\n </button>\n </div>\n }\n</div>\n\n@if (showCount() || showWordCount()) {\n <!--\n A polite status region, so a screen reader hears the count change, and the\n limit is shown once one is set: enforcement used to be entirely silent,\n so hitting maxLength read as a dead keyboard with the counter still showing\n a bare number and no indication a limit existed at all. The colour change\n at the limit gives sighted users the same signal.\n -->\n <output [attr.id]=\"counterId\" class=\"flex justify-end text-xs text-muted-foreground mt-1 px-1\" aria-live=\"polite\">\n @if (showCount()) {\n <!--\n The localized phrase keeps its own {count}, and the limit follows as a\n separate parenthetical. Interpolating \"0 / 120\" INTO the slot produced\n \"0 / 120 characters\", which is wrong in English and cannot be localized\n at all -- a locale ordering it differently had no way to express it,\n because the separator was hardcoded in the template expression.\n -->\n <span [class.text-destructive]=\"atCharacterLimit()\" [class.font-medium]=\"atCharacterLimit()\">\n {{ interpolateLocale(resolvedLocale().editor.characters, { count: characterCount() }) }}@if (maxLength()) {<span class=\"ms-1\">({{ maxLength() }} max)</span>}\n </span>\n }\n @if (showWordCount()) {\n <span [class.ms-3]=\"showCount()\">{{ interpolateLocale(resolvedLocale().editor.words, { count: wordCount() }) }}</span>\n }\n </output>\n}\n", styles: ["@media print{:host{display:block;overflow:visible}:host [data-slot=rich-text-editor]{max-height:none!important;height:auto!important;overflow:visible!important;border:none}:host [role=toolbar],:host [data-slot=rich-text-floating-toolbar],:host [data-slot=rich-text-find-overlay],:host [data-slot=rich-text-outline-panel],:host [data-slot=rich-text-history-badge]{display:none!important}:host tr,:host img{break-inside:avoid}:host h1,:host h2,:host h3{break-after:avoid}}:host img[data-blocked-src]{display:inline-flex;align-items:center;justify-content:center;min-width:8rem;min-height:4.5rem;padding:calc(.75rem * var(--_d));border:1px dashed var(--border);border-radius:var(--radius);background-color:color-mix(in oklab,var(--muted) 60%,transparent);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);color:var(--muted-foreground);font-size:.75rem;line-height:1.4;text-align:center;font-style:italic}:host img[data-blocked-src]:after{content:attr(data-blocked-label)}\n"] }]
18717
+ '[attr.data-ui-theme]': 'theme()',
18718
+ }, template: "@if (toolbar() === 'top' && !readonly()) {\n <ui-rich-text-toolbar\n [items]=\"toolbarItems()\"\n [activeFormats]=\"activeFormats()\"\n [textStyleAvailable]=\"textStyleAvailable()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [locale]=\"resolvedLocale()\"\n (formatCommand)=\"onFormatCommand($event)\"\n [addonSlots]=\"toolbarSlots.slots()\"\n (addonSlotClick)=\"onAddonSlotClick($event)\"\n />\n}\n\n<div #editorContainer [class]=\"editorContainerClasses()\" [dir]=\"resolvedDir()\">\n <!--\n Three of the attributes below exist for assistive tech specifically:\n\n The tab index drops to -1 when disabled. A native disabled control leaves the\n tab order entirely; a static 0 left a keyboard user stopping on an editor\n that takes focus and then does nothing.\n\n The readonly state is announced explicitly, because contenteditable false is\n a browser editing hint rather than a state screen readers reliably report on\n a textbox \u2014 without it a reader discovers the field is read-only by typing\n and seeing nothing happen.\n\n The placeholder is announced explicitly too, because the visible one is CSS\n generated content. Chromium exposes that as a plain text child, so a reader\n announces the hint as though it were the document's own text.\n -->\n <div\n #editorDiv\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [attr.contenteditable]=\"!isDisabled() && !readonly()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-readonly]=\"readonly() ? 'true' : null\"\n [class]=\"editableClasses()\"\n [attr.placeholder]=\"placeholder() || resolvedLocale().editor.placeholder\"\n [attr.aria-placeholder]=\"placeholder() || resolvedLocale().editor.placeholder\"\n [attr.aria-label]=\"ariaLabel() || resolvedLocale().editor.ariaLabel\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.data-slot]=\"'rich-text-editor'\"\n [style.min-height]=\"minHeight()\"\n [style.max-height]=\"maxHeight()\"\n role=\"textbox\"\n aria-multiline=\"true\"\n [attr.aria-expanded]=\"activeSuggestionPopup() ? 'true' : null\"\n [attr.aria-controls]=\"activeSuggestionPopup()?.controlsId ?? null\"\n [attr.aria-activedescendant]=\"activeSuggestionPopup()?.activeOptionId ?? null\"\n (input)=\"onInput($event)\"\n (beforeinput)=\"onBeforeInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (paste)=\"onPaste($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur($event)\"\n (mouseup)=\"onSelectionChange()\"\n (keyup)=\"onSelectionChange()\"\n (click)=\"onEditorClick($event)\"\n (mousemove)=\"onEditorMouseMove($event)\"\n (mousedown)=\"onEditorMouseDown($event)\"\n (touchstart)=\"onEditorTouchStart($event)\"\n (contextmenu)=\"onEditorContextMenu($event)\"\n (dragover)=\"onEditorDragOver($event)\"\n (dragleave)=\"onEditorDragLeave($event)\"\n (drop)=\"onEditorDrop($event)\"\n ></div>\n\n @if (findReplaceVisible()) {\n <div data-slot=\"rich-text-find-panel\"\n class=\"absolute top-2 end-2 z-50 bg-popover border rounded-lg shadow-lg p-3 w-80 max-w-[calc(100vw-2rem)] max-sm:w-[calc(100%-1rem)] max-sm:start-2 animate-in slide-in-from-top-2 fade-in-0\"\n tabindex=\"-1\"\n (keydown.escape)=\"closeFindReplace()\"\n (keydown)=\"onFindReplaceKeydown($event)\">\n <div class=\"flex flex-wrap items-center gap-1.5 mb-2\">\n <input\n #findInput\n data-slot=\"rich-text-find-query\"\n type=\"text\"\n [placeholder]=\"resolvedLocale().findReplace.findPlaceholder\"\n [attr.aria-label]=\"resolvedLocale().findReplace.find\"\n [attr.aria-invalid]=\"findRegexError() ? 'true' : null\"\n [value]=\"findQuery()\"\n (input)=\"onFindQueryChange($any($event.target).value)\"\n class=\"flex h-7 min-w-0 flex-1 rounded-md border border-input bg-transparent px-2 py-1 text-xs shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n />\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs font-medium transition-colors hover:bg-accent\"\n [class.bg-accent]=\"findCaseSensitive()\"\n [attr.aria-pressed]=\"findCaseSensitive()\"\n [attr.aria-label]=\"resolvedLocale().findReplace.caseSensitive\"\n [title]=\"resolvedLocale().findReplace.caseSensitive\"\n (click)=\"toggleFindCaseSensitive()\">Aa</button>\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs font-medium transition-colors hover:bg-accent\"\n [class.bg-accent]=\"findWholeWord()\"\n [attr.aria-pressed]=\"findWholeWord()\"\n [attr.aria-label]=\"resolvedLocale().findReplace.wholeWord\"\n [title]=\"resolvedLocale().findReplace.wholeWord\"\n (click)=\"toggleFindWholeWord()\">ab</button>\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs font-medium transition-colors hover:bg-accent\"\n [class.bg-accent]=\"findUseRegex()\"\n [attr.aria-pressed]=\"findUseRegex()\"\n [attr.aria-label]=\"resolvedLocale().findReplace.useRegex\"\n [title]=\"resolvedLocale().findReplace.useRegex\"\n (click)=\"toggleFindUseRegex()\">.*</button>\n </div>\n <div class=\"flex flex-wrap items-center justify-between gap-2 mb-2\">\n <span data-slot=\"rich-text-find-counter\"\n aria-live=\"polite\"\n class=\"text-xs text-muted-foreground\">{{ findCounterText() }}</span>\n <div class=\"flex gap-1\">\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs hover:bg-accent disabled:opacity-50\"\n [attr.aria-label]=\"resolvedLocale().findReplace.previous\"\n [title]=\"resolvedLocale().findReplace.previous\"\n [disabled]=\"findMatchCount() === 0\"\n (click)=\"findPrevious()\">&#x25B2;</button>\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs hover:bg-accent disabled:opacity-50\"\n [attr.aria-label]=\"resolvedLocale().findReplace.next\"\n [title]=\"resolvedLocale().findReplace.next\"\n [disabled]=\"findMatchCount() === 0\"\n (click)=\"findNext()\">&#x25BC;</button>\n <button type=\"button\"\n class=\"inline-flex min-h-7 min-w-7 items-center justify-center rounded-md p-1 text-xs hover:bg-accent\"\n [attr.aria-label]=\"resolvedLocale().findReplace.close\"\n [title]=\"resolvedLocale().findReplace.close\"\n (click)=\"closeFindReplace()\">&#x2715;</button>\n </div>\n </div>\n @if (showReplaceRow()) {\n <div class=\"flex flex-wrap items-center gap-1.5\">\n <input\n data-slot=\"rich-text-find-replace\"\n type=\"text\"\n [placeholder]=\"resolvedLocale().findReplace.replacePlaceholder\"\n [attr.aria-label]=\"resolvedLocale().findReplace.replace\"\n [value]=\"replaceText()\"\n (input)=\"replaceText.set($any($event.target).value)\"\n class=\"flex h-7 min-w-0 flex-1 rounded-md border border-input bg-transparent px-2 py-1 text-xs shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n />\n <button type=\"button\" class=\"inline-flex min-h-7 items-center justify-center rounded-md px-2 py-1 text-xs hover:bg-accent whitespace-nowrap disabled:opacity-50\" [disabled]=\"findMatchCount() === 0\" (click)=\"replaceSingle()\">{{ resolvedLocale().findReplace.replace }}</button>\n <button type=\"button\" class=\"inline-flex min-h-7 items-center justify-center rounded-md px-2 py-1 text-xs hover:bg-accent whitespace-nowrap disabled:opacity-50\" [disabled]=\"findMatchCount() === 0\" (click)=\"replaceAll()\">{{ resolvedLocale().findReplace.replaceAll }}</button>\n </div>\n }\n </div>\n }\n\n @if (dragOver()) {\n <div class=\"absolute inset-0 pointer-events-none border-2 border-dashed border-primary/60 rounded-md bg-primary/5\"></div>\n }\n\n @if (toolbar() === 'floating' && !readonly() && showFloatingToolbar()) {\n <div \n class=\"fixed z-[9999] bg-popover border rounded-lg shadow-lg p-1\"\n [style.left.px]=\"floatingToolbarPosition().x\"\n [style.top.px]=\"floatingToolbarPosition().y\"\n >\n <ui-rich-text-toolbar\n [items]=\"['bold', 'italic', 'underline', 'separator', 'clear']\"\n [activeFormats]=\"emptyFormats\"\n [compact]=\"true\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"readonly()\"\n [locale]=\"resolvedLocale()\"\n (formatCommand)=\"onFloatingFormatCommand($event)\"\n [addonSlots]=\"toolbarSlots.slots()\"\n (addonSlotClick)=\"onAddonSlotClick($event)\"\n />\n </div>\n }\n\n @if (tableContextMenuOpen()) {\n <div\n #tableContextMenuRef\n class=\"fixed z-50 min-w-[180px] max-w-[calc(100vw-2rem)] max-h-[calc(100vh-1rem)] overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95\"\n [style.left.px]=\"tableContextMenuPosition().x\"\n [style.top.px]=\"tableContextMenuPosition().y\"\n (mousedown)=\"$event.preventDefault()\"\n (contextmenu)=\"onContextMenuOverlayContextMenu($event)\"\n >\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"addTableRowAbove()\">\n {{ resolvedLocale().table.addRowAbove }}\n </button>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"addTableRowBelow()\">\n {{ resolvedLocale().table.addRowBelow }}\n </button>\n <div class=\"my-1 h-px bg-border\"></div>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"addTableColumnLeft()\">\n {{ resolvedLocale().table.addColumnLeft }}\n </button>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"addTableColumnRight()\">\n {{ resolvedLocale().table.addColumnRight }}\n </button>\n <div class=\"my-1 h-px bg-border\"></div>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"toggleTableHeaderRow()\">\n {{ resolvedLocale().table.toggleHeaderRow }}\n </button>\n <div class=\"my-1 h-px bg-border\"></div>\n @if (tableCellSelected().length >= 2) {\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"mergeCells()\">\n {{ resolvedLocale().table.mergeCells }}\n </button>\n }\n @if (canSplitCell()) {\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground\" (click)=\"splitCell()\">\n {{ resolvedLocale().table.splitCell }}\n </button>\n }\n @if (tableCellSelected().length >= 2 || canSplitCell()) {\n <div class=\"my-1 h-px bg-border\"></div>\n }\n <div class=\"px-2 py-1.5\">\n <div class=\"text-xs text-muted-foreground mb-1.5\">{{ resolvedLocale().table.borders }}</div>\n <div class=\"flex items-center gap-1\">\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.bordersAll\" (click)=\"setTableBorders('all')\">\n <svg viewBox=\"0 0 20 20\" class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <rect x=\"1\" y=\"1\" width=\"18\" height=\"18\" /><line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"19\" /><line x1=\"1\" y1=\"10\" x2=\"19\" y2=\"10\" />\n </svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.bordersNone\" (click)=\"setTableBorders('none')\">\n <svg viewBox=\"0 0 20 20\" class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1\" opacity=\"0.35\">\n <rect x=\"1\" y=\"1\" width=\"18\" height=\"18\" stroke-dasharray=\"2 2\" /><line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"19\" stroke-dasharray=\"2 2\" /><line x1=\"1\" y1=\"10\" x2=\"19\" y2=\"10\" stroke-dasharray=\"2 2\" />\n </svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.bordersOuter\" (click)=\"setTableBorders('outer')\">\n <svg viewBox=\"0 0 20 20\" class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\">\n <rect x=\"1\" y=\"1\" width=\"18\" height=\"18\" stroke-width=\"1.5\" /><line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"19\" stroke-width=\"1\" opacity=\"0.25\" stroke-dasharray=\"2 2\" /><line x1=\"1\" y1=\"10\" x2=\"19\" y2=\"10\" stroke-width=\"1\" opacity=\"0.25\" stroke-dasharray=\"2 2\" />\n </svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.bordersHorizontal\" (click)=\"setTableBorders('horizontal')\">\n <svg viewBox=\"0 0 20 20\" class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\">\n <line x1=\"1\" y1=\"1\" x2=\"19\" y2=\"1\" stroke-width=\"1.5\" /><line x1=\"1\" y1=\"10\" x2=\"19\" y2=\"10\" stroke-width=\"1.5\" /><line x1=\"1\" y1=\"19\" x2=\"19\" y2=\"19\" stroke-width=\"1.5\" />\n </svg>\n </button>\n </div>\n </div>\n <div class=\"my-1 h-px bg-border\"></div>\n <div class=\"px-2 py-1.5\">\n <div class=\"text-xs text-muted-foreground mb-1.5\">{{ resolvedLocale().table.cellAlignLeft }}</div>\n <div class=\"flex items-center gap-1\" dir=\"ltr\">\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.cellAlignLeft\" (click)=\"setCellAlignment('left')\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"21\" x2=\"3\" y1=\"6\" y2=\"6\"/><line x1=\"15\" x2=\"3\" y1=\"12\" y2=\"12\"/><line x1=\"17\" x2=\"3\" y1=\"18\" y2=\"18\"/></svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.cellAlignCenter\" (click)=\"setCellAlignment('center')\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"21\" x2=\"3\" y1=\"6\" y2=\"6\"/><line x1=\"17\" x2=\"7\" y1=\"12\" y2=\"12\"/><line x1=\"19\" x2=\"5\" y1=\"18\" y2=\"18\"/></svg>\n </button>\n <button type=\"button\" class=\"flex items-center justify-center w-7 h-7 rounded border border-transparent hover:border-border hover:bg-accent\" [title]=\"resolvedLocale().table.cellAlignRight\" (click)=\"setCellAlignment('right')\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"21\" x2=\"3\" y1=\"6\" y2=\"6\"/><line x1=\"21\" x2=\"9\" y1=\"12\" y2=\"12\"/><line x1=\"21\" x2=\"7\" y1=\"18\" y2=\"18\"/></svg>\n </button>\n </div>\n </div>\n <div class=\"px-2 py-1.5\">\n <div class=\"text-xs text-muted-foreground mb-1.5\">{{ resolvedLocale().table.cellColor }}</div>\n <div class=\"grid grid-cols-8 gap-0.5\">\n @for (color of tableCellColors; track color) {\n <button type=\"button\" class=\"w-4 h-4 rounded border border-border hover:scale-110 transition-transform\" [style.background-color]=\"color\" [attr.aria-label]=\"color\" (click)=\"setCellColor(color)\"></button>\n }\n </div>\n </div>\n <div class=\"my-1 h-px bg-border\"></div>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground text-destructive\" (click)=\"deleteTableRow()\">\n {{ resolvedLocale().table.deleteRow }}\n </button>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground text-destructive\" (click)=\"deleteTableColumn()\">\n {{ resolvedLocale().table.deleteColumn }}\n </button>\n <button type=\"button\" class=\"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground text-destructive\" (click)=\"deleteTable()\">\n {{ resolvedLocale().table.deleteTable }}\n </button>\n </div>\n }\n</div>\n\n@if (showCount() || showWordCount()) {\n <!--\n A polite status region, so a screen reader hears the count change, and the\n limit is shown once one is set: enforcement used to be entirely silent,\n so hitting maxLength read as a dead keyboard with the counter still showing\n a bare number and no indication a limit existed at all. The colour change\n at the limit gives sighted users the same signal.\n -->\n <output [attr.id]=\"counterId\" class=\"flex justify-end text-xs text-muted-foreground mt-1 px-1\" aria-live=\"polite\">\n @if (showCount()) {\n <!--\n The localized phrase keeps its own {count}, and the limit follows as a\n separate parenthetical. Interpolating \"0 / 120\" INTO the slot produced\n \"0 / 120 characters\", which is wrong in English and cannot be localized\n at all -- a locale ordering it differently had no way to express it,\n because the separator was hardcoded in the template expression.\n -->\n <span [class.text-destructive]=\"atCharacterLimit()\" [class.font-medium]=\"atCharacterLimit()\">\n {{ interpolateLocale(resolvedLocale().editor.characters, { count: characterCount() }) }}@if (maxLength()) {<span class=\"ms-1\">({{ maxLength() }} max)</span>}\n </span>\n }\n @if (showWordCount()) {\n <span [class.ms-3]=\"showCount()\">{{ interpolateLocale(resolvedLocale().editor.words, { count: wordCount() }) }}</span>\n }\n </output>\n}\n", styles: ["@media print{:host{display:block;overflow:visible}:host [data-slot=rich-text-editor]{max-height:none!important;height:auto!important;overflow:visible!important;border:none}:host [role=toolbar],:host [data-slot=rich-text-floating-toolbar],:host [data-slot=rich-text-find-overlay],:host [data-slot=rich-text-outline-panel],:host [data-slot=rich-text-history-badge]{display:none!important}:host tr,:host img{break-inside:avoid}:host h1,:host h2,:host h3{break-after:avoid}}:host img[data-blocked-src]{display:inline-flex;align-items:center;justify-content:center;min-width:8rem;min-height:4.5rem;padding:calc(.75rem * var(--_d));border:1px dashed var(--border);border-radius:var(--radius);background-color:color-mix(in oklab,var(--muted) 60%,transparent);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);color:var(--muted-foreground);font-size:.75rem;line-height:1.4;text-align:center;font-style:italic}:host img[data-blocked-src]:after{content:attr(data-blocked-label)}:host([data-ui-theme=\"neutral\"]){--primary: oklch(.205 0 0);--primary-foreground: oklch(.985 0 0);--ring: oklch(.708 0 0)}:host-context(.dark)[data-ui-theme=neutral]{--primary: oklch(.985 0 0);--primary-foreground: oklch(.205 0 0);--ring: oklch(.708 0 0)}:host([data-ui-theme=\"zinc\"]){--primary: oklch(.205 0 0);--primary-foreground: oklch(.985 0 0);--ring: oklch(.708 0 0)}:host-context(.dark)[data-ui-theme=zinc]{--primary: oklch(.985 0 0);--primary-foreground: oklch(.205 0 0);--ring: oklch(.708 0 0)}:host([data-ui-theme=\"slate\"]){--primary: oklch(.208 .042 265.755);--primary-foreground: oklch(.984 .003 247.858);--ring: oklch(.704 .04 256.788)}:host-context(.dark)[data-ui-theme=slate]{--primary: oklch(.929 .013 255.508);--primary-foreground: oklch(.208 .042 265.755);--ring: oklch(.704 .04 256.788)}:host([data-ui-theme=\"stone\"]){--primary: oklch(.216 .006 56.043);--primary-foreground: oklch(.985 .001 106.423);--ring: oklch(.709 .01 56.259)}:host-context(.dark)[data-ui-theme=stone]{--primary: oklch(.923 .003 48.717);--primary-foreground: oklch(.216 .006 56.043);--ring: oklch(.553 .013 58.071)}:host([data-ui-theme=\"gray\"]){--primary: oklch(.21 .028 264.532);--primary-foreground: oklch(.985 .002 247.839);--ring: oklch(.707 .022 264.436)}:host-context(.dark)[data-ui-theme=gray]{--primary: oklch(.928 .006 264.531);--primary-foreground: oklch(.21 .028 264.532);--ring: oklch(.551 .027 264.364)}:host([data-ui-theme=\"red\"]){--primary: oklch(.577 .245 27.325);--primary-foreground: oklch(.985 0 0);--ring: oklch(.577 .245 27.325)}:host-context(.dark)[data-ui-theme=red]{--primary: oklch(.577 .245 27.325);--primary-foreground: oklch(.985 0 0);--ring: oklch(.577 .245 27.325)}:host([data-ui-theme=\"rose\"]){--primary: oklch(.645 .246 16.439);--primary-foreground: oklch(.985 0 0);--ring: oklch(.645 .246 16.439)}:host-context(.dark)[data-ui-theme=rose]{--primary: oklch(.645 .246 16.439);--primary-foreground: oklch(.985 0 0);--ring: oklch(.645 .246 16.439)}:host([data-ui-theme=\"orange\"]){--primary: oklch(.646 .222 41.116);--primary-foreground: oklch(.985 0 0);--ring: oklch(.646 .222 41.116)}:host-context(.dark)[data-ui-theme=orange]{--primary: oklch(.646 .222 41.116);--primary-foreground: oklch(.985 0 0);--ring: oklch(.646 .222 41.116)}:host([data-ui-theme=\"green\"]){--primary: oklch(.623 .214 131.655);--primary-foreground: oklch(.985 0 0);--ring: oklch(.623 .214 131.655)}:host-context(.dark)[data-ui-theme=green]{--primary: oklch(.623 .214 131.655);--primary-foreground: oklch(.985 0 0);--ring: oklch(.623 .214 131.655)}:host([data-ui-theme=\"blue\"]){--primary: oklch(.546 .245 262.881);--primary-foreground: oklch(.985 0 0);--ring: oklch(.546 .245 262.881)}:host-context(.dark)[data-ui-theme=blue]{--primary: oklch(.546 .245 262.881);--primary-foreground: oklch(.985 0 0);--ring: oklch(.546 .245 262.881)}:host([data-ui-theme=\"yellow\"]){--primary: oklch(.82 .185 84.67);--primary-foreground: oklch(.205 0 0);--ring: oklch(.82 .185 84.67)}:host-context(.dark)[data-ui-theme=yellow]{--primary: oklch(.82 .185 84.67);--primary-foreground: oklch(.205 0 0);--ring: oklch(.82 .185 84.67)}:host([data-ui-theme=\"violet\"]){--primary: oklch(.558 .288 302.321);--primary-foreground: oklch(.985 0 0);--ring: oklch(.558 .288 302.321)}:host-context(.dark)[data-ui-theme=violet]{--primary: oklch(.558 .288 302.321);--primary-foreground: oklch(.985 0 0);--ring: oklch(.558 .288 302.321)}:host([data-ui-theme=\"amber\"]){--primary: oklch(.77 .16 70);--primary-foreground: oklch(.145 0 0);--ring: oklch(.77 .16 70)}:host-context(.dark)[data-ui-theme=amber]{--primary: oklch(.77 .16 70);--primary-foreground: oklch(.145 0 0);--ring: oklch(.77 .16 70)}\n"] }]
17909
18719
  }], ctorParameters: () => [], propDecorators: { editorDiv: [{
17910
18720
  type: ViewChild,
17911
18721
  args: ['editorDiv']
@@ -17915,7 +18725,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
17915
18725
  }], tableContextMenuRef: [{
17916
18726
  type: ViewChild,
17917
18727
  args: ['tableContextMenuRef']
17918
- }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], toolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbar", required: false }] }], toolbarItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbarItems", required: false }] }], customToolbarItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "customToolbarItems", required: false }] }], customToolbarAction: [{ type: i0.Output, args: ["customToolbarAction"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], minHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "minHeight", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], markdownShortcuts: [{ type: i0.Input, args: [{ isSignal: true, alias: "markdownShortcuts", required: false }] }], counter: [{ type: i0.Input, args: [{ isSignal: true, alias: "counter", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], history: [{ type: i0.Input, args: [{ isSignal: true, alias: "history", required: false }] }], findDebounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "findDebounceMs", required: false }] }], allowedImageHosts: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowedImageHosts", required: false }] }], allowedLinkSchemes: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowedLinkSchemes", required: false }] }], blockedImageMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "blockedImageMessage", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], dir: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], htmlChange: [{ type: i0.Output, args: ["htmlChange"] }], markdownChange: [{ type: i0.Output, args: ["markdownChange"] }], wordCountChange: [{ type: i0.Output, args: ["wordCountChange"] }], historyChange: [{ type: i0.Output, args: ["historyChange"] }], imageBlocked: [{ type: i0.Output, args: ["imageBlocked"] }], focused: [{ type: i0.Output, args: ["focused"] }], blurred: [{ type: i0.Output, args: ["blurred"] }] } });
18728
+ }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], toolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbar", required: false }] }], toolbarItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbarItems", required: false }] }], customToolbarItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "customToolbarItems", required: false }] }], customToolbarAction: [{ type: i0.Output, args: ["customToolbarAction"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], minHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "minHeight", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], markdownShortcuts: [{ type: i0.Input, args: [{ isSignal: true, alias: "markdownShortcuts", required: false }] }], counter: [{ type: i0.Input, args: [{ isSignal: true, alias: "counter", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], history: [{ type: i0.Input, args: [{ isSignal: true, alias: "history", required: false }] }], findDebounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "findDebounceMs", required: false }] }], codeHighlightDebounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "codeHighlightDebounceMs", required: false }] }], allowedImageHosts: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowedImageHosts", required: false }] }], allowedLinkSchemes: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowedLinkSchemes", required: false }] }], blockedImageMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "blockedImageMessage", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], dir: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], htmlChange: [{ type: i0.Output, args: ["htmlChange"] }], markdownChange: [{ type: i0.Output, args: ["markdownChange"] }], wordCountChange: [{ type: i0.Output, args: ["wordCountChange"] }], historyChange: [{ type: i0.Output, args: ["historyChange"] }], imageBlocked: [{ type: i0.Output, args: ["imageBlocked"] }], focused: [{ type: i0.Output, args: ["focused"] }], blurred: [{ type: i0.Output, args: ["blurred"] }] } });
17919
18729
  /** The last child element of `parent` whose tag is `tagName`, or null. */
17920
18730
  function lastChildTagged(parent, tagName) {
17921
18731
  for (let child = parent.lastElementChild; child; child = child.previousElementSibling) {
@@ -19058,6 +19868,55 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
19058
19868
  }, template: "@if (skeleton()) {\n <ui-skeleton class=\"h-9 w-24 rounded-md\" />\n} @else {\n <button\n uiRipple\n [uiRippleDisabled]=\"!ripple()\"\n [uiRippleColor]=\"rippleColor()\"\n [class]=\"classes()\"\n [disabled]=\"disabled() || loading()\"\n [type]=\"type()\"\n [attr.data-slot]=\"'button'\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\n (click)=\"clicked.emit($event)\"\n >\n @if (loading()) {\n <div class=\"absolute inset-0 flex items-center justify-center\" aria-hidden=\"true\">\n <ui-spinner size=\"xs\" />\n </div>\n }\n @if (label()) {\n {{ label() }}\n } @else {\n <ng-content />\n }\n </button>\n}\n", styles: [":host{--_d: var(--density-button, var(--density, 1))}@layer components{[data-slot=button]{height:calc(2.25rem * var(--_d));padding-inline:calc(1rem * var(--_d))}:host([data-size=\"sm\"]) [data-slot=button]{height:calc(2rem * var(--_d));padding-inline:calc(.75rem * var(--_d))}:host([data-size=\"lg\"]) [data-slot=button]{height:calc(2.5rem * var(--_d));padding-inline:calc(2rem * var(--_d))}:host([data-size=\"icon\"]) [data-slot=button]{height:calc(2.25rem * var(--_d));width:calc(2.25rem * var(--_d));padding:0}:host([data-size=\"icon-sm\"]) [data-slot=button]{height:calc(2rem * var(--_d));width:calc(2rem * var(--_d));padding:0}:host([data-size=\"icon-lg\"]) [data-slot=button]{height:calc(2.5rem * var(--_d));width:calc(2.5rem * var(--_d));padding:0}}\n"] }]
19059
19869
  }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ripple: [{ type: i0.Input, args: [{ isSignal: true, alias: "ripple", required: false }] }], rippleColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "rippleColor", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], skeleton: [{ type: i0.Input, args: [{ isSignal: true, alias: "skeleton", required: false }] }], clicked: [{ type: i0.Output, args: ["clicked"] }] } });
19060
19870
 
19871
+ /**
19872
+ * Per-instance colour presets — the same names `npx shadcn-angular change-theme`
19873
+ * accepts. A component that exposes a `theme` input reflects it as the
19874
+ * `data-ui-theme` attribute, and its own stylesheet maps each name to the
19875
+ * preset's tokens, so nothing global is involved.
19876
+ */
19877
+ const THEME_NAMES = [
19878
+ 'zinc', 'slate', 'stone', 'gray', 'neutral',
19879
+ 'red', 'rose', 'orange', 'green', 'blue', 'yellow', 'violet', 'amber',
19880
+ ];
19881
+ /** The attribute a themed host carries; overlays look it up to inherit the preset. */
19882
+ const THEME_ATTRIBUTE = 'data-ui-theme';
19883
+ /** The tokens a preset overrides — everything else stays the app's global tokens. */
19884
+ const THEME_TOKENS = ['--primary', '--primary-foreground', '--ring'];
19885
+ /**
19886
+ * Carries a themed component's preset onto an overlay rendered outside it.
19887
+ * Call it when the overlay is shown — including every re-show of a reused one.
19888
+ *
19889
+ * Custom properties inherit through the DOM tree, so anything a themed component
19890
+ * renders inside itself is themed for free, live. Two kinds of overlay are NOT
19891
+ * descendants and would silently fall back to the global tokens: one appended
19892
+ * to `document.body`, and one an addon directive creates through its
19893
+ * `ViewContainerRef`, which Angular inserts as a SIBLING of the host. Those get
19894
+ * the resolved tokens copied at show time (so a dark-mode toggle while one is
19895
+ * open is not reflected until it re-opens).
19896
+ *
19897
+ * A real descendant is left alone so it keeps inheriting live, and an overlay
19898
+ * whose origin is not themed (any more) has earlier copies cleared, so it
19899
+ * follows the global tokens.
19900
+ */
19901
+ function inheritThemeTokens(origin, overlay) {
19902
+ const themed = origin.closest(`[${THEME_ATTRIBUTE}]`);
19903
+ if (themed?.contains(overlay))
19904
+ return;
19905
+ if (!themed) {
19906
+ for (const token of THEME_TOKENS)
19907
+ overlay.style.removeProperty(token);
19908
+ delete overlay.dataset['uiTheme'];
19909
+ return;
19910
+ }
19911
+ const computed = getComputedStyle(themed);
19912
+ for (const token of THEME_TOKENS) {
19913
+ overlay.style.setProperty(token, computed.getPropertyValue(token));
19914
+ }
19915
+ // The attribute travels too, so an overlay opened FROM this overlay (a
19916
+ // context submenu, a tooltip inside a popover) still finds a themed origin.
19917
+ overlay.dataset['uiTheme'] = themed.dataset['uiTheme'];
19918
+ }
19919
+
19061
19920
  const ALERT_DIALOG = new InjectionToken('ALERT_DIALOG');
19062
19921
  class AlertDialogComponent {
19063
19922
  constructor() {
@@ -19641,6 +20500,7 @@ class ConfirmDirective {
19641
20500
  this.appRef = inject(ApplicationRef);
19642
20501
  this.injector = inject(EnvironmentInjector);
19643
20502
  this.elementInjector = inject(Injector);
20503
+ this.hostEl = inject(ElementRef);
19644
20504
  }
19645
20505
  /**
19646
20506
  * Intercept the host click and open the confirmation dialog instead.
@@ -19659,6 +20519,7 @@ class ConfirmDirective {
19659
20519
  ref.instance.cancelLabel.set(this.resolvedCancelLabel());
19660
20520
  this.appRef.attachView(ref.hostView);
19661
20521
  document.body.appendChild(ref.location.nativeElement);
20522
+ inheritThemeTokens(this.hostEl.nativeElement, ref.location.nativeElement);
19662
20523
  ref.instance.show();
19663
20524
  const confirmSub = ref.instance.confirmed.subscribe(() => {
19664
20525
  this.confirmed.emit();
@@ -20350,6 +21211,7 @@ class RichTextActionsDirective {
20350
21211
  };
20351
21212
  const occupied = target.existing ? readActions(target.existing).map((a) => a.trigger) : [];
20352
21213
  const ref = this.vcr.createComponent(RichTextActionsDialogComponent);
21214
+ inheritThemeTokens(this.vcr.element.nativeElement, ref.location.nativeElement);
20353
21215
  ref.setInput('definitions', this.uiRteActions());
20354
21216
  ref.setInput('locale', this.i18n.t());
20355
21217
  ref.setInput('context', {
@@ -20445,6 +21307,7 @@ class RichTextActionsDirective {
20445
21307
  this.popoverTarget = el;
20446
21308
  const rows = this.buildPopoverRows(el);
20447
21309
  const ref = this.vcr.createComponent(RichTextActionsPopoverComponent);
21310
+ inheritThemeTokens(this.vcr.element.nativeElement, ref.location.nativeElement);
20448
21311
  ref.setInput('actions', rows);
20449
21312
  ref.setInput('locale', this.i18n.t());
20450
21313
  ref.setInput('canAdd', rows.length < 2);
@@ -20516,6 +21379,7 @@ class RichTextActionsDirective {
20516
21379
  this.hidePopover();
20517
21380
  const target = { kind: 'text', existing: el, image: null };
20518
21381
  const ref = this.vcr.createComponent(RichTextActionsDialogComponent);
21382
+ inheritThemeTokens(this.vcr.element.nativeElement, ref.location.nativeElement);
20519
21383
  ref.setInput('definitions', this.uiRteActions());
20520
21384
  ref.setInput('locale', this.i18n.t());
20521
21385
  ref.setInput('context', {
@@ -21100,6 +21964,7 @@ function hoverCardHandlers(injector, o = {}) {
21100
21964
  body: String(event.params['body'] ?? ''),
21101
21965
  dir: directionOf(event.element),
21102
21966
  });
21967
+ inheritThemeTokens(event.element, open.host);
21103
21968
  anchorOverlay(open.host, event.element);
21104
21969
  // Grace area: keep the card open while the pointer is over it.
21105
21970
  open.host.addEventListener('mouseenter', cancelClose);
@@ -21244,6 +22109,7 @@ function openDialogHandlers(injector, o = {}) {
21244
22109
  dir: directionOf(event.element),
21245
22110
  });
21246
22111
  overlay.host.style.inset = '0';
22112
+ inheritThemeTokens(event.element, overlay.host);
21247
22113
  const onKeydown = (e) => { if (e.key === 'Escape')
21248
22114
  dismiss(); };
21249
22115
  const dismiss = () => {
@@ -22704,6 +23570,7 @@ class PopoverContentComponent {
22704
23570
  constructor() {
22705
23571
  this.popover = inject(POPOVER, { optional: true });
22706
23572
  this.document = inject(DOCUMENT);
23573
+ this.hostEl = inject(ElementRef);
22707
23574
  /** Extra classes merged onto the panel. Keep a width constraint viewport-aware (`max-w-[calc(100vw-2rem)]`), since the panel is positioned, not laid out by its parent. */
22708
23575
  this.class = input('', ...(ngDevMode ? [{ debugName: "class" }] : /* istanbul ignore next */ []));
22709
23576
  /** Alignment along the chosen {@link side}'s edge: `'start'`, `'center'` or `'end'`. May be flipped at runtime when {@link avoidCollisions} is on. */
@@ -22937,6 +23804,7 @@ class PopoverContentComponent {
22937
23804
  this.portalHost.dataset['popoverPortal'] = 'true';
22938
23805
  this.portalHost.style.cssText = 'display:contents';
22939
23806
  this.document.body.appendChild(this.portalHost);
23807
+ inheritThemeTokens(this.hostEl.nativeElement, this.portalHost);
22940
23808
  this.popover?.registerPortal(this.portalHost);
22941
23809
  }
22942
23810
  this.portalHost.appendChild(el);
@@ -26180,6 +27048,7 @@ class TooltipDirective {
26180
27048
  this.renderer.appendChild(tooltipEl, text);
26181
27049
  this.renderer.setAttribute(tooltipEl, 'class', 'fixed z-50 whitespace-nowrap rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground pointer-events-none');
26182
27050
  this.renderer.appendChild(document.body, tooltipEl);
27051
+ inheritThemeTokens(this.el.nativeElement, tooltipEl);
26183
27052
  return tooltipEl;
26184
27053
  }
26185
27054
  resolveTargetElement() {
@@ -30778,7 +31647,7 @@ class RichTextFileImportDirective {
30778
31647
  async importDocx(file) {
30779
31648
  this.assertImportableSize(file);
30780
31649
  const bytes = new Uint8Array(await file.arrayBuffer());
30781
- const { parseDocx } = await import('./gilav21-shadcn-angular-rte-docx-parser-CFx_4GC5.mjs');
31650
+ const { parseDocx } = await import('./gilav21-shadcn-angular-rte-docx-parser-CdxuIlVT.mjs');
30782
31651
  const { renderDocxForEditor } = await import('./gilav21-shadcn-angular-rte-docx-to-editor-html-Cl9eIhkC.mjs');
30783
31652
  const html = renderDocxForEditor(parseDocx(bytes));
30784
31653
  this.insertImported(html);
@@ -33133,6 +34002,7 @@ class RichTextLinksDirective {
33133
34002
  createOverlay(text, url, showRemove, rect) {
33134
34003
  this.closeOverlay();
33135
34004
  const ref = this.vcr.createComponent(RichTextLinksFormComponent);
34005
+ inheritThemeTokens(this.vcr.element.nativeElement, ref.location.nativeElement);
33136
34006
  ref.setInput('locale', this.i18n.t());
33137
34007
  ref.setInput('text', text);
33138
34008
  ref.setInput('url', url);
@@ -35493,6 +36363,7 @@ class RichTextSlashCommandsDirective {
35493
36363
  }
35494
36364
  createMenu() {
35495
36365
  const ref = this.vcr.createComponent(RichTextSlashCommandsMenuComponent);
36366
+ inheritThemeTokens(this.vcr.element.nativeElement, ref.location.nativeElement);
35496
36367
  // Set here rather than as a host binding on the component: the id never
35497
36368
  // changes, and a host binding costs a change-detection pass per cycle
35498
36369
  // that re-fires the menu's scroll-into-view effect.
@@ -37740,5 +38611,5 @@ const RTE_FULL = [
37740
38611
  * Generated bundle index. Do not edit.
37741
38612
  */
37742
38613
 
37743
- export { RichTextColorsButtonComponent as $, ACTION_ATTRS as A, RICH_TEXT_LOCALES as B, RICH_TEXT_MENTIONS_LOCALES as C, DEFAULT_COLOR_PALETTE as D, EMPTINESS_FIXTURES as E, FIND_MAX_PAINTED_RECTS as F, RICH_TEXT_OUTLINE_CONTEXT as G, HEADER_BYTES as H, IMAGE_IMPORT_ACCEPT as I, RICH_TEXT_OUTLINE_LOCALES as J, RICH_TEXT_PROSE_CLASSES as K, RICH_TEXT_SHORTCUT_DEFINITIONS as L, RICH_TEXT_SLASH_COMMANDS_LOCALES as M, RICH_TEXT_TABLES_BUTTON_CONTEXT as N, RICH_TEXT_TABLES_LOCALES as O, PDF_MIME as P, RICH_TEXT_TYPOGRAPHY_BUTTON_CONTEXT as Q, RICH_TEXT_ACTIONS_SANITIZER_RULES as R, RICH_TEXT_TYPOGRAPHY_LOCALES as S, RTE_FULL as T, RichTextActionsBindDirective as U, RichTextActionsDirective as V, RichTextAiChipComponent as W, RichTextAiDirective as X, RichTextAiPanelComponent as Y, RichTextAllowDirective as Z, RichTextAllowHost as _, ACTION_PARAMS as a, hostOf as a$, RichTextColorsDirective as a0, RichTextCommandRegistry as a1, RichTextEditorAddonHost as a2, RichTextEditorComponent as a3, RichTextEmojiButtonComponent as a4, RichTextEmojiDirective as a5, RichTextFileImportButtonComponent as a6, RichTextFileImportDirective as a7, RichTextFileImportOverlayComponent as a8, RichTextHistoryDirective as a9, addonSetting as aA, anchorOverlay as aB, applyImageAlignment as aC, applyStarterStyle as aD, assertFlatParams as aE, bindRichTextActions as aF, buildDefaultSlashCommands as aG, buildEntityInsertNode as aH, buildEntityRenderContext as aI, buildFindIndex as aJ, buildLineIndex as aK, caretPosition as aL, classifyImport as aM, compileFindRegex as aN, computeSeedStyleString as aO, containsCssUrl as aP, createImageElement as aQ, cssFunctionCalls as aR, dataUrlToFile as aS, decodeCssEscapes as aT, detectTrigger as aU, directionOf as aV, dragHasSupportedDocument as aW, extractCssUrls as aX, flattenIntoRowText as aY, hasUnsafeCssFunction as aZ, holdsNothing as a_, RichTextHistoryPanelComponent as aa, RichTextImageResizerComponent as ab, RichTextImagesButtonComponent as ac, RichTextImagesDirective as ad, RichTextImagesOverlayComponent as ae, RichTextLinksButtonComponent as af, RichTextLinksDirective as ag, RichTextLinksFormComponent as ah, RichTextMarkdownService as ai, RichTextMentionPopoverComponent as aj, RichTextMentionsDirective as ak, RichTextOutlineDirective as al, RichTextOutlinePanelComponent as am, RichTextPasteNormalizerService as an, RichTextSanitizerService as ao, RichTextSlashCommandsDirective as ap, RichTextSlashCommandsMenuComponent as aq, RichTextTablesButtonComponent as ar, RichTextTablesDirective as as, RichTextToolbarComponent as at, RichTextToolbarViewContext as au, RichTextTypographyButtonComponent as av, RichTextTypographyDirective as aw, TEXT_STYLE_OPTIONS as ax, TOOLBAR_BUTTONS as ay, TRANSPARENT_PIXEL as az, AddonSlotRegistry as b, hoverCardAction as b0, hoverCardHandlers as b1, isCombinedOnElement as b2, isHostAllowed as b3, isHostBearingUrl as b4, isInlineHoldingBlock as b5, isLineOwner as b6, isNestedList as b7, isPdfHeader as b8, isPhrasing as b9, parseImageSize as bA, placeCaretIn as bB, placeImageAtSelection as bC, positionAfterLine as bD, rangeShowsNothing as bE, readActions as bF, readFileAsDataUrl as bG, remoteHostOf as bH, removeAction as bI, reportResourceDecisions as bJ, resolveFromPage as bK, richTextHasMedia as bL, richTextMaxLength as bM, richTextMinWords as bN, richTextRequired as bO, richTextVisibleText as bP, rowRunsOf as bQ, selectTriggerRange as bR, separateListKinds as bS, simpleHash as bT, stripCssComments as bU, stripStyleIfMatches as bV, structureAround as bW, validateActionId as bX, validateActionParams as bY, writeAction as bZ, writeCombined as b_, isRichTextEmpty as ba, isSupportedDocumentFile as bb, isZipHeader as bc, labelBlockedImages as bd, lastOwnInlineNode as be, lineAbove as bf, lineBelow as bg, lineIsEmpty as bh, lineIsTextOnly as bi, lineKeepsItsElement as bj, lineOf as bk, lineOwnNodes as bl, lineTagIsFixed as bm, lineText as bn, linesBetween as bo, linesMayJoin as bp, linkedPreviewDialogAction as bq, linkedPreviewDialogHandlers as br, matchBlockInputRule as bs, matchInlineInputRule as bt, mountTopLayer as bu, nodeShowsNothing as bv, normalizeHostEntry as bw, offsetToPosition as bx, openDialogAction as by, openDialogHandlers as bz, DEFAULT_FONT_FAMILIES as c, DEFAULT_HIGHLIGHT_PALETTE as d, DEFAULT_LINK_SCHEMES as e, DEFAULT_TOOLBAR_ITEMS as f, DOCUMENT_IMPORT_ACCEPT as g, DOCX_MIME as h, isValidImageMagicBytes as i, FIND_MAX_QUERY_LENGTH as j, INLINE_RULE_LOOKBEHIND as k, PresetDialogComponent as l, PresetHoverCardComponent as m, RICH_TEXT_AI_CONTEXT as n, RICH_TEXT_AI_LOCALES as o, RICH_TEXT_COLORS_LOCALES as p, RICH_TEXT_COLOR_BUTTON_CONTEXT as q, RICH_TEXT_EMOJI_CONTEXT as r, RICH_TEXT_EMOJI_LOCALES as s, RICH_TEXT_FILE_IMPORT_BUTTON_CONTEXT as t, RICH_TEXT_FILE_IMPORT_LOCALES as u, RICH_TEXT_HISTORY_LOCALES as v, RICH_TEXT_IMAGES_BUTTON_CONTEXT as w, RICH_TEXT_IMAGES_LOCALES as x, RICH_TEXT_LINKS_BUTTON_CONTEXT as y, RICH_TEXT_LINKS_LOCALES as z };
37744
- //# sourceMappingURL=gilav21-shadcn-angular-rte-gilav21-shadcn-angular-rte-DWSBW7Gw.mjs.map
38614
+ export { RichTextAllowHost as $, ACTION_ATTRS as A, RICH_TEXT_LOCALES as B, RICH_TEXT_MENTIONS_LOCALES as C, DEFAULT_COLOR_PALETTE as D, EMPTINESS_FIXTURES as E, FIND_MAX_PAINTED_RECTS as F, RICH_TEXT_OUTLINE_CONTEXT as G, HEADER_BYTES as H, IMAGE_IMPORT_ACCEPT as I, RICH_TEXT_OUTLINE_LOCALES as J, RICH_TEXT_PROSE_CLASSES as K, RICH_TEXT_SHORTCUT_DEFINITIONS as L, MAX_NESTING_DEPTH as M, RICH_TEXT_SLASH_COMMANDS_LOCALES as N, RICH_TEXT_TABLES_BUTTON_CONTEXT as O, PDF_MIME as P, RICH_TEXT_TABLES_LOCALES as Q, RICH_TEXT_ACTIONS_SANITIZER_RULES as R, RICH_TEXT_TYPOGRAPHY_BUTTON_CONTEXT as S, RICH_TEXT_TYPOGRAPHY_LOCALES as T, RTE_FULL as U, RichTextActionsBindDirective as V, RichTextActionsDirective as W, RichTextAiChipComponent as X, RichTextAiDirective as Y, RichTextAiPanelComponent as Z, RichTextAllowDirective as _, ACTION_PARAMS as a, hasUnsafeCssFunction as a$, RichTextColorsButtonComponent as a0, RichTextColorsDirective as a1, RichTextCommandRegistry as a2, RichTextEditorAddonHost as a3, RichTextEditorComponent as a4, RichTextEmojiButtonComponent as a5, RichTextEmojiDirective as a6, RichTextFileImportButtonComponent as a7, RichTextFileImportDirective as a8, RichTextFileImportOverlayComponent as a9, TRANSPARENT_PIXEL as aA, addonSetting as aB, anchorOverlay as aC, applyImageAlignment as aD, applyStarterStyle as aE, assertFlatParams as aF, bindRichTextActions as aG, blockquoteDepthOf as aH, buildDefaultSlashCommands as aI, buildEntityInsertNode as aJ, buildEntityRenderContext as aK, buildFindIndex as aL, buildLineIndex as aM, caretPosition as aN, classifyImport as aO, compileFindRegex as aP, computeSeedStyleString as aQ, containsCssUrl as aR, createImageElement as aS, cssFunctionCalls as aT, dataUrlToFile as aU, decodeCssEscapes as aV, detectTrigger as aW, directionOf as aX, dragHasSupportedDocument as aY, extractCssUrls as aZ, flattenIntoRowText as a_, RichTextHistoryDirective as aa, RichTextHistoryPanelComponent as ab, RichTextImageResizerComponent as ac, RichTextImagesButtonComponent as ad, RichTextImagesDirective as ae, RichTextImagesOverlayComponent as af, RichTextLinksButtonComponent as ag, RichTextLinksDirective as ah, RichTextLinksFormComponent as ai, RichTextMarkdownService as aj, RichTextMentionPopoverComponent as ak, RichTextMentionsDirective as al, RichTextOutlineDirective as am, RichTextOutlinePanelComponent as an, RichTextPasteNormalizerService as ao, RichTextSanitizerService as ap, RichTextSlashCommandsDirective as aq, RichTextSlashCommandsMenuComponent as ar, RichTextTablesButtonComponent as as, RichTextTablesDirective as at, RichTextToolbarComponent as au, RichTextToolbarViewContext as av, RichTextTypographyButtonComponent as aw, RichTextTypographyDirective as ax, TEXT_STYLE_OPTIONS as ay, TOOLBAR_BUTTONS as az, AddonSlotRegistry as b, writeAction as b$, holdsNothing as b0, hostOf as b1, hoverCardAction as b2, hoverCardHandlers as b3, isCombinedOnElement as b4, isHostAllowed as b5, isHostBearingUrl as b6, isInlineHoldingBlock as b7, isLineOwner as b8, isNestedList as b9, openDialogAction as bA, openDialogHandlers as bB, parseImageSize as bC, placeCaretIn as bD, placeImageAtSelection as bE, positionAfterLine as bF, rangeShowsNothing as bG, readActions as bH, readFileAsDataUrl as bI, remoteHostOf as bJ, removeAction as bK, reportResourceDecisions as bL, resolveFromPage as bM, richTextHasMedia as bN, richTextMaxLength as bO, richTextMinWords as bP, richTextRequired as bQ, richTextVisibleText as bR, rowRunsOf as bS, selectTriggerRange as bT, separateListKinds as bU, simpleHash as bV, stripCssComments as bW, stripStyleIfMatches as bX, structureAround as bY, validateActionId as bZ, validateActionParams as b_, isPdfHeader as ba, isPhrasing as bb, isRichTextEmpty as bc, isSupportedDocumentFile as bd, isZipHeader as be, labelBlockedImages as bf, lastOwnInlineNode as bg, lineAbove as bh, lineBelow as bi, lineIsEmpty as bj, lineIsTextOnly as bk, lineKeepsItsElement as bl, lineOf as bm, lineOwnNodes as bn, lineTagIsFixed as bo, lineText as bp, linesBetween as bq, linesMayJoin as br, linkedPreviewDialogAction as bs, linkedPreviewDialogHandlers as bt, matchBlockInputRule as bu, matchInlineInputRule as bv, mountTopLayer as bw, nodeShowsNothing as bx, normalizeHostEntry as by, offsetToPosition as bz, DEFAULT_FONT_FAMILIES as c, writeCombined as c0, DEFAULT_HIGHLIGHT_PALETTE as d, DEFAULT_LINK_SCHEMES as e, DEFAULT_TOOLBAR_ITEMS as f, DOCUMENT_IMPORT_ACCEPT as g, DOCX_MIME as h, isValidImageMagicBytes as i, FIND_MAX_QUERY_LENGTH as j, INLINE_RULE_LOOKBEHIND as k, PresetDialogComponent as l, PresetHoverCardComponent as m, RICH_TEXT_AI_CONTEXT as n, RICH_TEXT_AI_LOCALES as o, RICH_TEXT_COLORS_LOCALES as p, RICH_TEXT_COLOR_BUTTON_CONTEXT as q, RICH_TEXT_EMOJI_CONTEXT as r, RICH_TEXT_EMOJI_LOCALES as s, RICH_TEXT_FILE_IMPORT_BUTTON_CONTEXT as t, RICH_TEXT_FILE_IMPORT_LOCALES as u, RICH_TEXT_HISTORY_LOCALES as v, RICH_TEXT_IMAGES_BUTTON_CONTEXT as w, RICH_TEXT_IMAGES_LOCALES as x, RICH_TEXT_LINKS_BUTTON_CONTEXT as y, RICH_TEXT_LINKS_LOCALES as z };
38615
+ //# sourceMappingURL=gilav21-shadcn-angular-rte-gilav21-shadcn-angular-rte-BFcfCqW4.mjs.map