@besaitech/ng-design-system 0.0.72 → 0.0.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -273,13 +273,16 @@ import { SdsRichTextEditor, SDS_RICH_TEXT_LABELS } from '@besaitech/ng-design-sy
273
273
 
274
274
  | Component | Selector | Notes |
275
275
  | --- | --- | --- |
276
- | `SdsRichTextEditor` | `<sds-rich-text-editor>` | CVA over an HTML string (`''` when empty). `[tools]` (headings, bold, italic, underline, strike, lists, quote, link, divider, clear, undo/redo) is the toolbar AND the schema; `rows` / `maxRows`, `maxLength` + counter, `label`, `hint`, `error`, `invalid`, `required`, `readonly`, `disabled`, `appearance` (default·filled), `contentLanguage` (spell-check language); `focus()`, `insertText(text)` |
276
+ | `SdsRichTextEditor` | `<sds-rich-text-editor>` | CVA over an HTML string (`''` when empty). `[tools]` (headings, bold, italic, underline, strike, lists, quote, link, table, divider, clear, undo/redo) is the toolbar AND the schema; `rows` / `maxRows`, `maxLength` + counter, `label`, `hint`, `error`, `invalid`, `required`, `readonly`, `disabled`, `appearance` (default·filled), `contentLanguage` (spell-check language); `focus()`, `insertText(text)` |
277
277
  | `SdsLocalizedRichTextEditor` | `<sds-localized-rich-text-editor>` | CVA over an `SdsLocalizedValue` (`{ fr: '<p>…</p>', de: … }`), like `sds-localized-input`: `languages`, `currentLanguage`, `requiredLanguages`, switcher at the end of the toolbar with filled / required / missing dots, empty translations pruned. One editor per language underneath — undo, caret and spell-check stay per language. Same rich-text inputs as above (`maxLength` is per language) |
278
278
  | `SdsRichTextToolbarEnd` | `[sdsRichTextToolbarEnd]` | Marks content projected at the end of the toolbar (kept in `readonly`): a language switcher, "insert variable" buttons calling `editor.insertText(…)` |
279
279
 
280
280
  The emitted HTML is clean by construction: no inline styles, nothing outside the enabled tools, and links
281
281
  limited to http(s), mailto, tel, site-relative paths and anchors (`sdsSafeHref`) with
282
- `rel="noopener noreferrer nofollow"` — still sanitise stored HTML when you display it. ⌘/Ctrl+K opens the
282
+ `rel="noopener noreferrer nofollow"` — still sanitise stored HTML when you display it. Tables are saved bare
283
+ (no `<colgroup>`, no inline widths). When loaded HTML holds content the tools cannot keep (`<img>`, `<iframe>`, a
284
+ table without the `table` tool…), a warning under the field names it before anyone edits — saving an edit would
285
+ drop it. ⌘/Ctrl+K opens the
283
286
  link field and is not passed on to an app-wide ⌘K palette. Strings are English by default: translate them
284
287
  per instance with `[labels]` (a `Partial<SdsRichTextLabels>`, follows a live language switch) or app-wide with
285
288
  `{ provide: SDS_RICH_TEXT_LABELS, useValue: { bold: 'Gras', … } }`.
package/charts/index.d.ts CHANGED
@@ -1293,7 +1293,7 @@ declare class SdsKpi {
1293
1293
  readonly trendLabels: _angular_core.InputSignal<readonly string[] | null>;
1294
1294
  readonly trendVariant: _angular_core.InputSignal<SdsSparklineVariant>;
1295
1295
  readonly trendColor: _angular_core.InputSignal<string | null>;
1296
- readonly icon: _angular_core.InputSignal<"x" | "search" | "bold" | "link" | "filter" | "menu" | "table" | "copy" | "square" | "list" | "chart-column" | "chart-line" | "chart-bar" | "chart-area" | "chart-pie" | "chart-scatter" | "grid-3x3" | "activity" | "gauge" | "trending-up" | "trending-down" | "clipboard-list" | "credit-card" | "external-link" | "languages" | "megaphone" | "monitor" | "store" | "user-round" | "home" | "user-plus" | "user-check" | "hand" | "users" | "bike" | "building-2" | "package" | "package-plus" | "package-open" | "rotate-ccw" | "bar-chart-3" | "log-out" | "panel-left-close" | "panel-left-open" | "panel-left" | "calendar" | "chevron-right" | "chevron-left" | "chevron-down" | "chevron-up" | "plus" | "minus" | "chevrons-left" | "chevrons-right" | "check" | "alert-circle" | "info" | "type" | "tag" | "ticket" | "square-check" | "circle-dot" | "circle-user" | "layout-grid" | "panels-top-left" | "panel-top" | "rows-3" | "book-open" | "palette" | "bell" | "phone" | "settings" | "align-left" | "toggle-left" | "arrow-up" | "arrow-down" | "chevrons-up-down" | "chevrons-up" | "chevrons-down" | "star" | "message-square" | "qr-code" | "eye" | "eye-off" | "user" | "check-circle" | "arrows-clockwise" | "file-text" | "file-check" | "hash" | "map-pin" | "inbox" | "arrow-right" | "refresh-cw" | "loader-circle" | "locate-fixed" | "locate-off" | "bug" | "scroll-text" | "moon" | "sun" | "search-x" | "command" | "sparkles" | "triangle-alert" | "octagon-x" | "ban" | "ellipsis" | "ellipsis-vertical" | "grip-vertical" | "upload" | "image" | "image-off" | "utensils" | "zoom-in" | "zoom-out" | "trash-2" | "clock" | "pencil" | "circle-help" | "stock" | "allergen-cereals-gluten" | "allergen-crustaceans" | "allergen-eggs" | "allergen-fish" | "allergen-peanuts" | "allergen-soybeans" | "allergen-milk" | "allergen-nuts" | "allergen-celery" | "allergen-mustard" | "allergen-sesame" | "allergen-sulphites" | "allergen-lupin" | "allergen-molluscs" | "italic" | "underline" | "strikethrough" | "heading-2" | "heading-3" | "pilcrow" | "list-ordered" | "text-quote" | "unlink" | "undo-2" | "redo-2" | "remove-formatting" | null>;
1296
+ readonly icon: _angular_core.InputSignal<"x" | "search" | "bold" | "link" | "filter" | "menu" | "table" | "copy" | "square" | "list" | "chart-column" | "chart-line" | "chart-bar" | "chart-area" | "chart-pie" | "chart-scatter" | "grid-3x3" | "activity" | "gauge" | "trending-up" | "trending-down" | "clipboard-list" | "credit-card" | "external-link" | "languages" | "megaphone" | "monitor" | "store" | "user-round" | "home" | "user-plus" | "user-check" | "hand" | "users" | "bike" | "building-2" | "package" | "package-plus" | "package-open" | "rotate-ccw" | "bar-chart-3" | "log-out" | "panel-left-close" | "panel-left-open" | "panel-left" | "calendar" | "chevron-right" | "chevron-left" | "chevron-down" | "chevron-up" | "plus" | "minus" | "chevrons-left" | "chevrons-right" | "check" | "alert-circle" | "info" | "type" | "tag" | "ticket" | "square-check" | "circle-dot" | "circle-user" | "layout-grid" | "panels-top-left" | "panel-top" | "rows-3" | "book-open" | "palette" | "bell" | "phone" | "settings" | "printer" | "align-left" | "toggle-left" | "arrow-up" | "arrow-down" | "chevrons-up-down" | "chevrons-up" | "chevrons-down" | "star" | "message-square" | "qr-code" | "eye" | "eye-off" | "user" | "check-circle" | "arrows-clockwise" | "file-text" | "file-check" | "hash" | "map-pin" | "inbox" | "arrow-right" | "refresh-cw" | "loader-circle" | "locate-fixed" | "locate-off" | "bug" | "scroll-text" | "moon" | "sun" | "search-x" | "command" | "sparkles" | "triangle-alert" | "octagon-x" | "ban" | "ellipsis" | "ellipsis-vertical" | "grip-vertical" | "upload" | "image" | "image-off" | "utensils" | "zoom-in" | "zoom-out" | "trash-2" | "clock" | "pencil" | "circle-help" | "stock" | "allergen-cereals-gluten" | "allergen-crustaceans" | "allergen-eggs" | "allergen-fish" | "allergen-peanuts" | "allergen-soybeans" | "allergen-milk" | "allergen-nuts" | "allergen-celery" | "allergen-mustard" | "allergen-sesame" | "allergen-sulphites" | "allergen-lupin" | "allergen-molluscs" | "italic" | "underline" | "strikethrough" | "heading-2" | "heading-3" | "pilcrow" | "list-ordered" | "text-quote" | "unlink" | "undo-2" | "redo-2" | "remove-formatting" | "between-horizontal-end" | "between-vertical-end" | "grid-2x2-x" | null>;
1297
1297
  /** Shown under the value when there is no change to report. */
1298
1298
  readonly hint: _angular_core.InputSignal<string>;
1299
1299
  readonly size: _angular_core.InputSignal<SdsKpiSize>;
@@ -5,6 +5,7 @@ import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
5
5
  import { SdsIcon, SdsButton } from '@besaitech/ng-design-system';
6
6
  import { Editor } from '@tiptap/core';
7
7
  import { Placeholder, CharacterCount } from '@tiptap/extensions';
8
+ import { TableKit } from '@tiptap/extension-table';
8
9
  import StarterKit from '@tiptap/starter-kit';
9
10
 
10
11
  /** The toolbar shown when `[tools]` is not set. */
@@ -19,6 +20,7 @@ const SDS_RICH_TEXT_DEFAULT_TOOLS = [
19
20
  'orderedList',
20
21
  'blockquote',
21
22
  'link',
23
+ 'table',
22
24
  'clearFormatting',
23
25
  'undo',
24
26
  'redo',
@@ -36,6 +38,7 @@ const SDS_RICH_TEXT_LABELS_EN = {
36
38
  orderedList: 'Numbered list',
37
39
  blockquote: 'Quote',
38
40
  link: 'Link',
41
+ table: 'Table',
39
42
  horizontalRule: 'Divider',
40
43
  clearFormatting: 'Clear formatting',
41
44
  undo: 'Undo',
@@ -46,6 +49,14 @@ const SDS_RICH_TEXT_LABELS_EN = {
46
49
  linkRemove: 'Remove link',
47
50
  linkCancel: 'Cancel',
48
51
  linkInvalid: 'Enter a web address (https://…), an e-mail address or a path starting with /.',
52
+ tableTools: 'Table',
53
+ tableAddRow: 'Row below',
54
+ tableAddColumn: 'Column right',
55
+ tableDeleteRow: 'Delete row',
56
+ tableDeleteColumn: 'Delete column',
57
+ tableHeaderRow: 'Header row',
58
+ tableDelete: 'Delete table',
59
+ unsupportedContent: 'This text contains elements this editor cannot keep: {elements}. Changing it will remove them.',
49
60
  countLimit: '{count} of {max} characters used',
50
61
  count: '{count} characters',
51
62
  };
@@ -103,11 +114,44 @@ const TOOL_DEFS = [
103
114
  { tool: 'orderedList', icon: 'list-ordered', group: 2, toggle: true, shortcut: 'Mod-Shift-7' },
104
115
  { tool: 'blockquote', icon: 'text-quote', group: 2, toggle: true, shortcut: 'Mod-Shift-B' },
105
116
  { tool: 'link', icon: 'link', group: 3, toggle: true, shortcut: 'Mod-K' },
117
+ { tool: 'table', icon: 'table', group: 3, toggle: false },
106
118
  { tool: 'horizontalRule', icon: 'minus', group: 3, toggle: false },
107
119
  { tool: 'clearFormatting', icon: 'remove-formatting', group: 4, toggle: false },
108
120
  { tool: 'undo', icon: 'undo-2', group: 5, toggle: false, shortcut: 'Mod-Z' },
109
121
  { tool: 'redo', icon: 'redo-2', group: 5, toggle: false, shortcut: 'Mod-Shift-Z' },
110
122
  ];
123
+ const TABLE_ACTIONS = [
124
+ { action: 'addRow', label: 'tableAddRow', icon: 'between-horizontal-end' },
125
+ { action: 'addColumn', label: 'tableAddColumn', icon: 'between-vertical-end' },
126
+ { action: 'deleteRow', label: 'tableDeleteRow', icon: 'minus' },
127
+ { action: 'deleteColumn', label: 'tableDeleteColumn', icon: 'minus' },
128
+ { action: 'headerRow', label: 'tableHeaderRow', icon: null },
129
+ { action: 'delete', label: 'tableDelete', icon: 'grid-2x2-x' },
130
+ ];
131
+ /**
132
+ * Elements whose CONTENT (not just formatting) is lost when the schema cannot hold them — pictures, embeds,
133
+ * tables, form controls… Loaded HTML holding one of them that the editor drops triggers the warning.
134
+ */
135
+ const LOSSY_TAGS = new Set([
136
+ 'img',
137
+ 'picture',
138
+ 'figure',
139
+ 'svg',
140
+ 'canvas',
141
+ 'video',
142
+ 'audio',
143
+ 'iframe',
144
+ 'embed',
145
+ 'object',
146
+ 'table',
147
+ 'hr',
148
+ 'form',
149
+ 'input',
150
+ 'select',
151
+ 'textarea',
152
+ 'button',
153
+ 'math',
154
+ ]);
111
155
  /** One line of the editable area, for `[rows]` → min-height. */
112
156
  const LINE_EM = 1.6;
113
157
  const FRAME_BASE = 'overflow-hidden rounded-control border text-ink transition-colors';
@@ -212,6 +256,11 @@ class SdsRichTextEditor {
212
256
  /** Formatting active at the selection, per tool (refreshed on every transaction). */
213
257
  activeTools = signal(new Set(), ...(ngDevMode ? [{ debugName: "activeTools" }] : []));
214
258
  history = signal({ undo: false, redo: false }, ...(ngDevMode ? [{ debugName: "history" }] : []));
259
+ /** The first row of the table holding the caret is a header row. */
260
+ headerRow = signal(false, ...(ngDevMode ? [{ debugName: "headerRow" }] : []));
261
+ /** Elements of the loaded HTML the enabled tools could not keep (see {@link LOSSY_TAGS}). */
262
+ lostElements = signal([], ...(ngDevMode ? [{ debugName: "lostElements" }] : []));
263
+ tableActions = TABLE_ACTIONS;
215
264
  characters = signal(0, ...(ngDevMode ? [{ debugName: "characters" }] : []));
216
265
  rovingTool = signal(null, ...(ngDevMode ? [{ debugName: "rovingTool" }] : []));
217
266
  linkOpen = signal(false, ...(ngDevMode ? [{ debugName: "linkOpen" }] : []));
@@ -238,6 +287,9 @@ class SdsRichTextEditor {
238
287
  return [...byGroup.entries()].sort(([a], [b]) => a - b).map(([, defs]) => defs);
239
288
  }, ...(ngDevMode ? [{ debugName: "groups" }] : []));
240
289
  hasCounter = computed(() => this.showCount() || this.maxLength() > 0, ...(ngDevMode ? [{ debugName: "hasCounter" }] : []));
290
+ lostLabel = computed(() => this.l().unsupportedContent.replace('{elements}', this.lostElements()
291
+ .map((tag) => `<${tag}>`)
292
+ .join(', ')), ...(ngDevMode ? [{ debugName: "lostLabel" }] : []));
241
293
  atLimit = computed(() => this.maxLength() > 0 && this.characters() >= this.maxLength(), ...(ngDevMode ? [{ debugName: "atLimit" }] : []));
242
294
  countLabel = computed(() => (this.maxLength() ? this.l().countLimit : this.l().count)
243
295
  .replace('{count}', String(this.characters()))
@@ -271,6 +323,8 @@ class SdsRichTextEditor {
271
323
  ids.push(`${this.fieldId}-hint`);
272
324
  if (this.hasCounter())
273
325
  ids.push(`${this.fieldId}-count`);
326
+ if (this.lostElements().length)
327
+ ids.push(`${this.fieldId}-lost`);
274
328
  return ids.join(' ');
275
329
  }, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
276
330
  /** Attributes of the contenteditable document (ProseMirror's own element). */
@@ -370,6 +424,8 @@ class SdsRichTextEditor {
370
424
  .run();
371
425
  this.refresh(editor);
372
426
  }
427
+ if (editor)
428
+ this.detectLoss(this.html, editor);
373
429
  }
374
430
  registerOnChange(fn) {
375
431
  this.onChange = fn;
@@ -389,6 +445,9 @@ class SdsRichTextEditor {
389
445
  return this.history().undo;
390
446
  if (tool === 'redo')
391
447
  return this.history().redo;
448
+ // no table inside a table
449
+ if (tool === 'table')
450
+ return !this.isOn('table');
392
451
  return true;
393
452
  }
394
453
  buttonClass(def) {
@@ -454,6 +513,9 @@ class SdsRichTextEditor {
454
513
  case 'horizontalRule':
455
514
  chain.setHorizontalRule().run();
456
515
  break;
516
+ case 'table':
517
+ chain.insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
518
+ break;
457
519
  case 'clearFormatting':
458
520
  chain.unsetAllMarks().clearNodes().run();
459
521
  break;
@@ -497,6 +559,41 @@ class SdsRichTextEditor {
497
559
  editor.commands.focus('end');
498
560
  }
499
561
  }
562
+ tableButtonClass(action) {
563
+ const base = 'inline-flex h-7 items-center gap-1 rounded-md px-2 text-xs font-medium transition-colors ' +
564
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500';
565
+ if (action === 'headerRow' && this.headerRow())
566
+ return `${base} ${TOOL_BUTTON_ON}`;
567
+ if (action === 'delete')
568
+ return `${base} text-danger hover:bg-danger/10`;
569
+ return `${base} text-ink-soft hover:bg-muted hover:text-ink`;
570
+ }
571
+ runTable(action) {
572
+ const editor = this.editor;
573
+ if (!editor || !this.interactive())
574
+ return;
575
+ const chain = editor.chain().focus();
576
+ switch (action) {
577
+ case 'addRow':
578
+ chain.addRowAfter().run();
579
+ break;
580
+ case 'addColumn':
581
+ chain.addColumnAfter().run();
582
+ break;
583
+ case 'deleteRow':
584
+ chain.deleteRow().run();
585
+ break;
586
+ case 'deleteColumn':
587
+ chain.deleteColumn().run();
588
+ break;
589
+ case 'headerRow':
590
+ chain.toggleHeaderRow().run();
591
+ break;
592
+ case 'delete':
593
+ chain.deleteTable().run();
594
+ break;
595
+ }
596
+ }
500
597
  // --- link -------------------------------------------------------------------
501
598
  openLink() {
502
599
  const editor = this.editor;
@@ -582,6 +679,7 @@ class SdsRichTextEditor {
582
679
  });
583
680
  this.editor = editor;
584
681
  this.refresh(editor);
682
+ this.detectLoss(content, editor);
585
683
  this.ready.set(true);
586
684
  }
587
685
  extensions() {
@@ -615,6 +713,7 @@ class SdsRichTextEditor {
615
713
  }
616
714
  : false,
617
715
  }),
716
+ ...(tools.has('table') ? [TableKit.configure({ table: { resizable: false } })] : []),
618
717
  Placeholder.configure({ placeholder: () => this.placeholder() }),
619
718
  CharacterCount.configure({ limit: this.maxLength() > 0 ? this.maxLength() : null }),
620
719
  ];
@@ -642,11 +741,29 @@ class SdsRichTextEditor {
642
741
  return true;
643
742
  }
644
743
  /**
645
- * The control's value for the editor's document: '' when it is empty, and without the empty paragraphs
646
- * the editor keeps after a final list / quote / divider so the caret can leave it (Tiptap's trailing node).
744
+ * The control's value for the editor's document: '' when it is empty, and without the editor's own
745
+ * scaffolding — the empty paragraphs kept after a final list / quote / table so the caret can leave it
746
+ * (Tiptap's trailing node), and the table layout hints (`<colgroup>`, `style="min-width…"`, the default
747
+ * `colspan="1" rowspan="1"`) that columns no one resized do not need.
647
748
  */
648
749
  currentHtml(editor) {
649
- return editor.isEmpty ? '' : editor.getHTML().replace(/(?:<p><\/p>)+$/, '');
750
+ if (isBlank(editor))
751
+ return '';
752
+ return editor
753
+ .getHTML()
754
+ .replace(/<colgroup>[\s\S]*?<\/colgroup>/g, '')
755
+ .replace(/<table style="[^"]*">/g, '<table>')
756
+ .replace(/ (?:colspan|rowspan)="1"/g, '')
757
+ .replace(/(?:<p><\/p>)+$/, '');
758
+ }
759
+ /** Flags the elements of `source` whose content the schema could not keep (shown under the field). */
760
+ detectLoss(source, editor) {
761
+ if (!source || typeof DOMParser === 'undefined') {
762
+ this.lostElements.set([]);
763
+ return;
764
+ }
765
+ const kept = tagsOf(editor.getHTML());
766
+ this.lostElements.set([...tagsOf(source)].filter((tag) => LOSSY_TAGS.has(tag) && !kept.has(tag)));
650
767
  }
651
768
  emit(editor) {
652
769
  const html = this.currentHtml(editor);
@@ -672,7 +789,9 @@ class SdsRichTextEditor {
672
789
  check('orderedList', 'orderedList');
673
790
  check('blockquote', 'blockquote');
674
791
  check('link', 'link');
792
+ check('table', 'table');
675
793
  this.activeTools.set(active);
794
+ this.headerRow.set(active.has('table') && firstRowIsHeader(editor));
676
795
  this.history.set({ undo: editor.can().undo(), redo: editor.can().redo() });
677
796
  this.characters.set(editor.storage.characterCount.characters());
678
797
  }
@@ -781,6 +900,32 @@ class SdsRichTextEditor {
781
900
  </div>
782
901
  }
783
902
 
903
+ @if (isOn('table') && interactive()) {
904
+ <div
905
+ role="toolbar"
906
+ [attr.aria-label]="l().tableTools"
907
+ class="flex flex-wrap items-center gap-0.5 border-b border-border bg-surface px-1.5 py-1"
908
+ >
909
+ @for (t of tableActions; track t.action) {
910
+ <button
911
+ type="button"
912
+ [attr.data-table-action]="t.action"
913
+ [attr.aria-pressed]="
914
+ t.action === 'headerRow' ? (headerRow() ? 'true' : 'false') : null
915
+ "
916
+ [class]="tableButtonClass(t.action)"
917
+ (mousedown)="$event.preventDefault()"
918
+ (click)="runTable(t.action)"
919
+ >
920
+ @if (t.icon) {
921
+ <sds-icon [name]="t.icon" [size]="14" />
922
+ }
923
+ {{ l()[t.label] }}
924
+ </button>
925
+ }
926
+ </div>
927
+ }
928
+
784
929
  <div
785
930
  #content
786
931
  class="sds-rte"
@@ -790,6 +935,16 @@ class SdsRichTextEditor {
790
935
  ></div>
791
936
  </div>
792
937
 
938
+ @if (lostElements().length) {
939
+ <p
940
+ [id]="fieldId + '-lost'"
941
+ class="mt-1.5 flex items-start gap-1.5 rounded-control border border-warning/40 bg-warning/10 px-2.5 py-1.5 text-xs text-ink"
942
+ >
943
+ <sds-icon name="triangle-alert" [size]="14" class="mt-px shrink-0 text-warning" />
944
+ <span>{{ lostLabel() }}</span>
945
+ </p>
946
+ }
947
+
793
948
  @if (hasCounter()) {
794
949
  <div class="mt-1.5 flex items-start gap-2 text-xs">
795
950
  @if (error()) {
@@ -814,7 +969,7 @@ class SdsRichTextEditor {
814
969
  } @else if (hint()) {
815
970
  <p [id]="fieldId + '-hint'" class="mt-1.5 text-xs text-ink-faint">{{ hint() }}</p>
816
971
  }
817
- `, isInline: true, styles: [".sds-rte-content{outline:none;padding:.625rem .875rem;color:var(--color-ink);font-size:.875rem;line-height:1.6;overflow-wrap:anywhere;white-space:pre-wrap}.sds-rte-content>:first-child{margin-top:0}.sds-rte-content>:last-child{margin-bottom:0}.sds-rte-content p,.sds-rte-content ul,.sds-rte-content ol,.sds-rte-content blockquote{margin:0 0 .75em}.sds-rte-content h2{font-size:1.25em;font-weight:600;line-height:1.3;margin:1.1em 0 .5em}.sds-rte-content h3{font-size:1.1em;font-weight:600;line-height:1.35;margin:1em 0 .4em}.sds-rte-content ul{list-style:disc;padding-left:1.5em}.sds-rte-content ol{list-style:decimal;padding-left:1.5em}.sds-rte-content li>p{margin:0}.sds-rte-content li+li{margin-top:.25em}.sds-rte-content li>ul,.sds-rte-content li>ol{margin:.25em 0 0}.sds-rte-content blockquote{border-left:3px solid var(--color-border);padding-left:.875em;color:var(--color-ink-soft)}.sds-rte-content a{color:var(--color-brand-700, var(--color-accent-blue));text-decoration:underline;text-underline-offset:2px}.dark .sds-rte-content a{color:var(--sds-dark-on-tint, var(--color-ink))}.sds-rte-content hr{border:0;border-top:1px solid var(--color-border);margin:1em 0}.sds-rte-content hr.ProseMirror-selectednode{outline:2px solid var(--color-brand-500, var(--color-accent-blue));outline-offset:2px}.sds-rte-content p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:var(--color-ink-faint);float:left;height:0;pointer-events:none}.sds-rte-content[contenteditable=false]{cursor:default}\n"], dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth", "filled"] }, { kind: "component", type: SdsButton, selector: "sds-button", inputs: ["variant", "size", "shape", "type", "disabled", "loading", "block", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
972
+ `, isInline: true, styles: [".sds-rte-content{outline:none;padding:.625rem .875rem;color:var(--color-ink);font-size:.875rem;line-height:1.6;overflow-wrap:anywhere;white-space:pre-wrap}.sds-rte-content>:first-child{margin-top:0}.sds-rte-content>:last-child{margin-bottom:0}.sds-rte-content p,.sds-rte-content ul,.sds-rte-content ol,.sds-rte-content blockquote{margin:0 0 .75em}.sds-rte-content h2{font-size:1.25em;font-weight:600;line-height:1.3;margin:1.1em 0 .5em}.sds-rte-content h3{font-size:1.1em;font-weight:600;line-height:1.35;margin:1em 0 .4em}.sds-rte-content ul{list-style:disc;padding-left:1.5em}.sds-rte-content ol{list-style:decimal;padding-left:1.5em}.sds-rte-content li>p{margin:0}.sds-rte-content li+li{margin-top:.25em}.sds-rte-content li>ul,.sds-rte-content li>ol{margin:.25em 0 0}.sds-rte-content blockquote{border-left:3px solid var(--color-border);padding-left:.875em;color:var(--color-ink-soft)}.sds-rte-content a{color:var(--color-brand-700, var(--color-accent-blue));text-decoration:underline;text-underline-offset:2px}.dark .sds-rte-content a{color:var(--sds-dark-on-tint, var(--color-ink))}.sds-rte{overflow-x:auto}.sds-rte-content table{border-collapse:collapse;width:100%;margin:0 0 .75em}.sds-rte-content th,.sds-rte-content td{border:1px solid var(--color-border);padding:.375em .5em;vertical-align:top;text-align:left;min-width:4em}.sds-rte-content th{background:var(--color-muted);font-weight:600}.sds-rte-content th>p,.sds-rte-content td>p{margin:0}.sds-rte-content .selectedCell{background:color-mix(in oklab,var(--color-brand-500, var(--color-accent-blue)) 14%,transparent)}.sds-rte-content hr{border:0;border-top:1px solid var(--color-border);margin:1em 0}.sds-rte-content hr.ProseMirror-selectednode{outline:2px solid var(--color-brand-500, var(--color-accent-blue));outline-offset:2px}.sds-rte-content p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:var(--color-ink-faint);float:left;height:0;pointer-events:none}.sds-rte-content[contenteditable=false]{cursor:default}\n"], dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth", "filled"] }, { kind: "component", type: SdsButton, selector: "sds-button", inputs: ["variant", "size", "shape", "type", "disabled", "loading", "block", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
818
973
  }
819
974
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsRichTextEditor, decorators: [{
820
975
  type: Component,
@@ -922,6 +1077,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
922
1077
  </div>
923
1078
  }
924
1079
 
1080
+ @if (isOn('table') && interactive()) {
1081
+ <div
1082
+ role="toolbar"
1083
+ [attr.aria-label]="l().tableTools"
1084
+ class="flex flex-wrap items-center gap-0.5 border-b border-border bg-surface px-1.5 py-1"
1085
+ >
1086
+ @for (t of tableActions; track t.action) {
1087
+ <button
1088
+ type="button"
1089
+ [attr.data-table-action]="t.action"
1090
+ [attr.aria-pressed]="
1091
+ t.action === 'headerRow' ? (headerRow() ? 'true' : 'false') : null
1092
+ "
1093
+ [class]="tableButtonClass(t.action)"
1094
+ (mousedown)="$event.preventDefault()"
1095
+ (click)="runTable(t.action)"
1096
+ >
1097
+ @if (t.icon) {
1098
+ <sds-icon [name]="t.icon" [size]="14" />
1099
+ }
1100
+ {{ l()[t.label] }}
1101
+ </button>
1102
+ }
1103
+ </div>
1104
+ }
1105
+
925
1106
  <div
926
1107
  #content
927
1108
  class="sds-rte"
@@ -931,6 +1112,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
931
1112
  ></div>
932
1113
  </div>
933
1114
 
1115
+ @if (lostElements().length) {
1116
+ <p
1117
+ [id]="fieldId + '-lost'"
1118
+ class="mt-1.5 flex items-start gap-1.5 rounded-control border border-warning/40 bg-warning/10 px-2.5 py-1.5 text-xs text-ink"
1119
+ >
1120
+ <sds-icon name="triangle-alert" [size]="14" class="mt-px shrink-0 text-warning" />
1121
+ <span>{{ lostLabel() }}</span>
1122
+ </p>
1123
+ }
1124
+
934
1125
  @if (hasCounter()) {
935
1126
  <div class="mt-1.5 flex items-start gap-2 text-xs">
936
1127
  @if (error()) {
@@ -955,8 +1146,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
955
1146
  } @else if (hint()) {
956
1147
  <p [id]="fieldId + '-hint'" class="mt-1.5 text-xs text-ink-faint">{{ hint() }}</p>
957
1148
  }
958
- `, styles: [".sds-rte-content{outline:none;padding:.625rem .875rem;color:var(--color-ink);font-size:.875rem;line-height:1.6;overflow-wrap:anywhere;white-space:pre-wrap}.sds-rte-content>:first-child{margin-top:0}.sds-rte-content>:last-child{margin-bottom:0}.sds-rte-content p,.sds-rte-content ul,.sds-rte-content ol,.sds-rte-content blockquote{margin:0 0 .75em}.sds-rte-content h2{font-size:1.25em;font-weight:600;line-height:1.3;margin:1.1em 0 .5em}.sds-rte-content h3{font-size:1.1em;font-weight:600;line-height:1.35;margin:1em 0 .4em}.sds-rte-content ul{list-style:disc;padding-left:1.5em}.sds-rte-content ol{list-style:decimal;padding-left:1.5em}.sds-rte-content li>p{margin:0}.sds-rte-content li+li{margin-top:.25em}.sds-rte-content li>ul,.sds-rte-content li>ol{margin:.25em 0 0}.sds-rte-content blockquote{border-left:3px solid var(--color-border);padding-left:.875em;color:var(--color-ink-soft)}.sds-rte-content a{color:var(--color-brand-700, var(--color-accent-blue));text-decoration:underline;text-underline-offset:2px}.dark .sds-rte-content a{color:var(--sds-dark-on-tint, var(--color-ink))}.sds-rte-content hr{border:0;border-top:1px solid var(--color-border);margin:1em 0}.sds-rte-content hr.ProseMirror-selectednode{outline:2px solid var(--color-brand-500, var(--color-accent-blue));outline-offset:2px}.sds-rte-content p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:var(--color-ink-faint);float:left;height:0;pointer-events:none}.sds-rte-content[contenteditable=false]{cursor:default}\n"] }]
1149
+ `, styles: [".sds-rte-content{outline:none;padding:.625rem .875rem;color:var(--color-ink);font-size:.875rem;line-height:1.6;overflow-wrap:anywhere;white-space:pre-wrap}.sds-rte-content>:first-child{margin-top:0}.sds-rte-content>:last-child{margin-bottom:0}.sds-rte-content p,.sds-rte-content ul,.sds-rte-content ol,.sds-rte-content blockquote{margin:0 0 .75em}.sds-rte-content h2{font-size:1.25em;font-weight:600;line-height:1.3;margin:1.1em 0 .5em}.sds-rte-content h3{font-size:1.1em;font-weight:600;line-height:1.35;margin:1em 0 .4em}.sds-rte-content ul{list-style:disc;padding-left:1.5em}.sds-rte-content ol{list-style:decimal;padding-left:1.5em}.sds-rte-content li>p{margin:0}.sds-rte-content li+li{margin-top:.25em}.sds-rte-content li>ul,.sds-rte-content li>ol{margin:.25em 0 0}.sds-rte-content blockquote{border-left:3px solid var(--color-border);padding-left:.875em;color:var(--color-ink-soft)}.sds-rte-content a{color:var(--color-brand-700, var(--color-accent-blue));text-decoration:underline;text-underline-offset:2px}.dark .sds-rte-content a{color:var(--sds-dark-on-tint, var(--color-ink))}.sds-rte{overflow-x:auto}.sds-rte-content table{border-collapse:collapse;width:100%;margin:0 0 .75em}.sds-rte-content th,.sds-rte-content td{border:1px solid var(--color-border);padding:.375em .5em;vertical-align:top;text-align:left;min-width:4em}.sds-rte-content th{background:var(--color-muted);font-weight:600}.sds-rte-content th>p,.sds-rte-content td>p{margin:0}.sds-rte-content .selectedCell{background:color-mix(in oklab,var(--color-brand-500, var(--color-accent-blue)) 14%,transparent)}.sds-rte-content hr{border:0;border-top:1px solid var(--color-border);margin:1em 0}.sds-rte-content hr.ProseMirror-selectednode{outline:2px solid var(--color-brand-500, var(--color-accent-blue));outline-offset:2px}.sds-rte-content p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:var(--color-ink-faint);float:left;height:0;pointer-events:none}.sds-rte-content[contenteditable=false]{cursor:default}\n"] }]
959
1150
  }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], contentLanguage: [{ type: i0.Input, args: [{ isSignal: true, alias: "contentLanguage", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], tools: [{ type: i0.Input, args: [{ isSignal: true, alias: "tools", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], maxRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRows", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], showCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCount", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], contentHost: [{ type: i0.ViewChild, args: ['content', { isSignal: true }] }], linkInput: [{ type: i0.ViewChild, args: ['linkInput', { isSignal: true }] }], toolButtons: [{ type: i0.ViewChildren, args: ['toolButton', { isSignal: true }] }], toolbarEnd: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsRichTextToolbarEnd), { isSignal: true }] }] } });
1151
+ /** The tag names (lower case) of every element in an HTML fragment. */
1152
+ function tagsOf(html) {
1153
+ const body = new DOMParser().parseFromString(html, 'text/html').body;
1154
+ return new Set(Array.from(body.querySelectorAll('*'), (el) => el.tagName.toLowerCase()));
1155
+ }
1156
+ /** Whether the table around the selection starts with a row of header cells. */
1157
+ function firstRowIsHeader(editor) {
1158
+ const { $from } = editor.state.selection;
1159
+ for (let depth = $from.depth; depth > 0; depth--) {
1160
+ const node = $from.node(depth);
1161
+ if (node.type.name === 'table') {
1162
+ return node.firstChild?.firstChild?.type.name === 'tableHeader';
1163
+ }
1164
+ }
1165
+ return false;
1166
+ }
1167
+ /**
1168
+ * A document holding nothing but one empty paragraph. (Tiptap's `isEmpty` only looks at the text, so a fresh
1169
+ * empty table or a lone divider would count as empty and be dropped from the value.)
1170
+ */
1171
+ function isBlank(editor) {
1172
+ const doc = editor.state.doc;
1173
+ return (doc.childCount === 0 ||
1174
+ (doc.childCount === 1 &&
1175
+ doc.firstChild.type.name === 'paragraph' &&
1176
+ doc.firstChild.content.size === 0));
1177
+ }
960
1178
 
961
1179
  let nextId = 0;
962
1180
  const TAB_BASE = 'flex items-center gap-1 rounded px-1.5 py-1 text-[11px] font-semibold uppercase leading-none transition-colors ' +