@phuong-tran-redoc/document-engine-angular 0.1.4 → 0.1.6

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.
@@ -28,7 +28,7 @@ import { BubbleMenuPlugin } from '@tiptap/extension-bubble-menu';
28
28
  import * as i2 from '@angular/forms';
29
29
  import { NG_VALUE_ACCESSOR, FormsModule, FormControl, ReactiveFormsModule, FormBuilder, Validators, NonNullableFormBuilder } from '@angular/forms';
30
30
  import { FloatingMenuPlugin } from '@tiptap/extension-floating-menu';
31
- import { computePosition, offset, flip, shift, autoUpdate } from '@floating-ui/dom';
31
+ import { autoUpdate, computePosition, offset, flip, shift } from '@floating-ui/dom';
32
32
  import { throttle } from 'lodash-es';
33
33
  import { DomSanitizer } from '@angular/platform-browser';
34
34
  import { Subject, debounceTime, distinctUntilChanged, takeUntil } from 'rxjs';
@@ -883,9 +883,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
883
883
  *
884
884
  * Usage:
885
885
  * <button #trigger>Open</button>
886
- * <div [popover]="trigger" [isOpen]="isOpen">
886
+ * <div [documentEnginePopover]="trigger" [isOpen]="isOpen">
887
887
  * Content
888
888
  * </div>
889
+ *
890
+ * The host is pinned `position: fixed`, so floating-ui is asked for viewport-
891
+ * relative coordinates (`strategy: 'fixed'`). Omitting the strategy yields
892
+ * offset-parent-relative numbers, which are off by the page's scroll offset
893
+ * once written onto a fixed element.
889
894
  */
890
895
  class PopoverDirective {
891
896
  elRef = inject(ElementRef);
@@ -893,7 +898,18 @@ class PopoverDirective {
893
898
  /**
894
899
  * The trigger element reference (ElementRef or HTMLElement)
895
900
  */
901
+ documentEnginePopover;
902
+ /**
903
+ * The trigger element reference (ElementRef or HTMLElement)
904
+ *
905
+ * @deprecated Use `documentEnginePopover`. `[popover]` collides with the
906
+ * native HTML popover attribute and will be removed in the next major.
907
+ */
896
908
  popover;
909
+ /** The trigger, whichever input supplied it. */
910
+ get trigger() {
911
+ return this.documentEnginePopover ?? this.popover;
912
+ }
897
913
  /**
898
914
  * Whether the popover is open
899
915
  */
@@ -920,7 +936,8 @@ class PopoverDirective {
920
936
  this.updateVisibility();
921
937
  }
922
938
  ngOnChanges(changes) {
923
- if (this.viewInitialized && (changes['isOpen'] || changes['popover'])) {
939
+ if (this.viewInitialized &&
940
+ (changes['isOpen'] || changes['popover'] || changes['documentEnginePopover'])) {
924
941
  this.updateVisibility();
925
942
  }
926
943
  }
@@ -930,7 +947,7 @@ class PopoverDirective {
930
947
  updateVisibility() {
931
948
  if (!this.viewInitialized)
932
949
  return;
933
- if (this.isOpen && this.popover) {
950
+ if (this.isOpen && this.trigger) {
934
951
  // Wait for next tick to ensure element is rendered
935
952
  setTimeout(() => {
936
953
  this.setupFloating();
@@ -948,15 +965,16 @@ class PopoverDirective {
948
965
  }
949
966
  _setupFloatingInternal() {
950
967
  let triggerEl;
951
- if (!this.popover) {
968
+ const trigger = this.trigger;
969
+ if (!trigger) {
952
970
  if (this.retryCount < this.maxRetries) {
953
971
  this.retryCount++;
954
972
  setTimeout(() => this._setupFloatingInternal(), 10);
955
973
  }
956
974
  return;
957
975
  }
958
- if (this.popover instanceof ElementRef) {
959
- triggerEl = this.popover.nativeElement;
976
+ if (trigger instanceof ElementRef) {
977
+ triggerEl = trigger.nativeElement;
960
978
  // If ElementRef exists but nativeElement is not yet available, retry
961
979
  if (!triggerEl) {
962
980
  if (this.retryCount < this.maxRetries) {
@@ -967,7 +985,7 @@ class PopoverDirective {
967
985
  }
968
986
  }
969
987
  else {
970
- triggerEl = this.popover;
988
+ triggerEl = trigger;
971
989
  }
972
990
  const dropdownEl = this.elRef.nativeElement;
973
991
  if (!triggerEl || !dropdownEl) {
@@ -979,13 +997,8 @@ class PopoverDirective {
979
997
  return;
980
998
  }
981
999
  // Compute initial position before showing dropdown to prevent flash
982
- computePosition(triggerEl, dropdownEl, {
983
- placement: this.placement,
984
- middleware: [offset(this.offset), flip(), shift({ padding: this.padding })],
985
- })
986
- .then(({ x, y }) => {
987
- // Set position first, then make visible
988
- Object.assign(dropdownEl.style, { left: `${x}px`, top: `${y}px` });
1000
+ this.reposition(triggerEl, dropdownEl)
1001
+ .then(() => {
989
1002
  this.isPositioned = true;
990
1003
  // Trigger change detection to update visibility binding
991
1004
  this.cdr.markForCheck();
@@ -993,14 +1006,27 @@ class PopoverDirective {
993
1006
  .catch((error) => {
994
1007
  console.error('[PopoverDirective] Error computing position', error);
995
1008
  });
996
- // Use autoUpdate to automatically reposition on scroll/resize
1009
+ // Use autoUpdate to automatically reposition on scroll/resize. It must use
1010
+ // the SAME strategy as the initial call, or the first scroll undoes the fix.
997
1011
  this.cleanupAutoUpdate = autoUpdate(triggerEl, dropdownEl, () => {
998
- computePosition(triggerEl, dropdownEl, {
999
- placement: this.placement,
1000
- middleware: [offset(this.offset), flip(), shift({ padding: this.padding })],
1001
- }).then(({ x, y }) => {
1002
- Object.assign(dropdownEl.style, { left: `${x}px`, top: `${y}px` });
1003
- });
1012
+ void this.reposition(triggerEl, dropdownEl);
1013
+ });
1014
+ }
1015
+ /**
1016
+ * Position `dropdownEl` against `triggerEl`.
1017
+ *
1018
+ * `strategy: 'fixed'` is required: the host binding pins this element to
1019
+ * `position: fixed`, so the coordinates must be viewport-relative. Without it
1020
+ * floating-ui returns offset-parent-relative numbers and the panel lands the
1021
+ * page's scroll offset away from its trigger.
1022
+ */
1023
+ reposition(triggerEl, dropdownEl) {
1024
+ return computePosition(triggerEl, dropdownEl, {
1025
+ strategy: 'fixed',
1026
+ placement: this.placement,
1027
+ middleware: [offset(this.offset), flip(), shift({ padding: this.padding })],
1028
+ }).then(({ x, y }) => {
1029
+ Object.assign(dropdownEl.style, { left: `${x}px`, top: `${y}px` });
1004
1030
  });
1005
1031
  }
1006
1032
  cleanup() {
@@ -1010,19 +1036,24 @@ class PopoverDirective {
1010
1036
  }
1011
1037
  }
1012
1038
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: PopoverDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1013
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.10", type: PopoverDirective, isStandalone: true, selector: "[popover]", inputs: { popover: "popover", isOpen: "isOpen", placement: "placement", offset: "offset", padding: "padding" }, host: { properties: { "style.position": "\"fixed\"", "style.visibility": "isPositioned ? \"visible\" : \"hidden\"" } }, usesOnChanges: true, ngImport: i0 });
1039
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.10", type: PopoverDirective, isStandalone: true, selector: "[documentEnginePopover], [popover]", inputs: { documentEnginePopover: "documentEnginePopover", popover: "popover", isOpen: "isOpen", placement: "placement", offset: "offset", padding: "padding" }, host: { properties: { "style.position": "\"fixed\"", "style.visibility": "isPositioned ? \"visible\" : \"hidden\"" } }, usesOnChanges: true, ngImport: i0 });
1014
1040
  }
1015
1041
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: PopoverDirective, decorators: [{
1016
1042
  type: Directive,
1017
1043
  args: [{
1018
- selector: '[popover]',
1044
+ // `documentEnginePopover` is the supported selector. `[popover]` is kept as a
1045
+ // deprecated alias for consumers on <= 0.1.4; it collides with the platform's
1046
+ // native popover attribute and will be removed in the next major.
1047
+ selector: '[documentEnginePopover], [popover]',
1019
1048
  standalone: true,
1020
1049
  host: {
1021
1050
  '[style.position]': '"fixed"',
1022
1051
  '[style.visibility]': 'isPositioned ? "visible" : "hidden"',
1023
1052
  },
1024
1053
  }]
1025
- }], propDecorators: { popover: [{
1054
+ }], propDecorators: { documentEnginePopover: [{
1055
+ type: Input
1056
+ }], popover: [{
1026
1057
  type: Input
1027
1058
  }], isOpen: [{
1028
1059
  type: Input
@@ -1464,6 +1495,7 @@ class ColorPickerComponent {
1464
1495
  type="button"
1465
1496
  class="swatch"
1466
1497
  [style.background-color]="color.value"
1498
+ [attr.data-color]="color.value"
1467
1499
  (click)="colorSelected.emit(color)"
1468
1500
  [attr.title]="color.name"
1469
1501
  >
@@ -1543,6 +1575,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
1543
1575
  type="button"
1544
1576
  class="swatch"
1545
1577
  [style.background-color]="color.value"
1578
+ [attr.data-color]="color.value"
1546
1579
  (click)="colorSelected.emit(color)"
1547
1580
  [attr.title]="color.name"
1548
1581
  >
@@ -1728,7 +1761,7 @@ class ColorPickerViewComponent {
1728
1761
  <button type="button" documentEngineButton variant="secondary" (click)="close?.()">Close</button>
1729
1762
  </div>
1730
1763
  </div>
1731
- `, isInline: true, styles: [".color-picker-view{display:flex;flex-direction:column}.color-picker-view__title{padding:.5rem 1rem;border-bottom:1px solid var(--border, #e5e7eb);font-weight:500}.color-picker-view__actions{display:flex;gap:.5rem;justify-content:space-between;padding:.75rem;border-top:1px solid var(--border, #e5e7eb)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: ButtonDirective, selector: "button[documentEngineButton]", inputs: ["variant", "size"] }, { kind: "component", type: ColorPickerComponent, selector: "document-engine-color-picker", inputs: ["colorPalette", "usedColors", "activeColor"], outputs: ["colorSelected", "colorRemoved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1764
+ `, isInline: true, styles: [".color-picker-view{display:flex;flex-direction:column}.color-picker-view__title{padding:.5rem 1rem;border-bottom:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));font-weight:500}.color-picker-view__actions{display:flex;gap:.5rem;justify-content:space-between;padding:.75rem;border-top:1px solid var(--border, hsl(214.3, 31.8%, 91.4%))}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: ButtonDirective, selector: "button[documentEngineButton]", inputs: ["variant", "size"] }, { kind: "component", type: ColorPickerComponent, selector: "document-engine-color-picker", inputs: ["colorPalette", "usedColors", "activeColor"], outputs: ["colorSelected", "colorRemoved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1732
1765
  }
1733
1766
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: ColorPickerViewComponent, decorators: [{
1734
1767
  type: Component,
@@ -1752,7 +1785,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
1752
1785
  <button type="button" documentEngineButton variant="secondary" (click)="close?.()">Close</button>
1753
1786
  </div>
1754
1787
  </div>
1755
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".color-picker-view{display:flex;flex-direction:column}.color-picker-view__title{padding:.5rem 1rem;border-bottom:1px solid var(--border, #e5e7eb);font-weight:500}.color-picker-view__actions{display:flex;gap:.5rem;justify-content:space-between;padding:.75rem;border-top:1px solid var(--border, #e5e7eb)}\n"] }]
1788
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".color-picker-view{display:flex;flex-direction:column}.color-picker-view__title{padding:.5rem 1rem;border-bottom:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));font-weight:500}.color-picker-view__actions{display:flex;gap:.5rem;justify-content:space-between;padding:.75rem;border-top:1px solid var(--border, hsl(214.3, 31.8%, 91.4%))}\n"] }]
1756
1789
  }] });
1757
1790
 
1758
1791
  const ColorBubbleConfig = {
@@ -2515,7 +2548,7 @@ class LinkEditViewComponent {
2515
2548
  </div>
2516
2549
  </form>
2517
2550
  </div>
2518
- `, isInline: true, styles: [".link-edit-view{display:flex;flex-direction:column;min-width:320px}.link-edit-view__header{padding:.5rem 1rem;border-bottom:1px solid #e5e7eb;font-weight:500;font-size:.875rem}.link-edit-view__content{display:flex;flex-direction:column;gap:1.5rem;padding:1rem .5rem}.link-edit-view__shortcuts{display:flex;flex-wrap:wrap;gap:1rem;align-items:center;justify-content:space-between;border-top:1px solid #e5e7eb;padding:1rem .5rem}.link-edit-view__shortcuts-label{color:#6b7280;font-size:.875rem}.link-edit-view__actions{display:flex;gap:.5rem;justify-content:flex-end;padding:.5rem;border-top:1px solid #e5e7eb}.link-edit-view__field{display:flex;flex-direction:column;gap:.5rem;width:100%}.chip-set{display:flex;flex-wrap:wrap;gap:.5rem;align-items:center}.chip{display:inline-flex;align-items:center;padding:.25rem .75rem;background-color:#f3f4f6;border:1px solid #e5e7eb;border-radius:9999px;font-size:.875rem;color:#374151;cursor:pointer;transition:background-color .2s,border-color .2s}.chip:hover{background-color:#e5e7eb;border-color:#d1d5db}.chip:active{background-color:#d1d5db}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: ButtonDirective, selector: "button[documentEngineButton]", inputs: ["variant", "size"] }, { kind: "directive", type: InputDirective, selector: "input[documentEngineInput]", inputs: ["class"] }, { kind: "directive", type: LabelDirective, selector: "label[documentEngineLabel]", inputs: ["class"] }, { kind: "component", type: ErrorMessageComponent, selector: "document-engine-error-message", inputs: ["class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2551
+ `, isInline: true, styles: [".link-edit-view{display:flex;flex-direction:column;min-width:320px}.link-edit-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));font-weight:500;font-size:.875rem}.link-edit-view__content{display:flex;flex-direction:column;gap:1.5rem;padding:1rem .5rem}.link-edit-view__shortcuts{display:flex;flex-wrap:wrap;gap:1rem;align-items:center;justify-content:space-between;border-top:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));padding:1rem .5rem}.link-edit-view__shortcuts-label{color:var(--muted-foreground, hsl(215.4, 16.3%, 46.9%));font-size:.875rem}.link-edit-view__actions{display:flex;gap:.5rem;justify-content:flex-end;padding:.5rem;border-top:1px solid var(--border, hsl(214.3, 31.8%, 91.4%))}.link-edit-view__field{display:flex;flex-direction:column;gap:.5rem;width:100%}.chip-set{display:flex;flex-wrap:wrap;gap:.5rem;align-items:center}.chip{display:inline-flex;align-items:center;padding:.25rem .75rem;background-color:var(--muted, hsl(210, 40%, 96.1%));border:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));border-radius:9999px;font-size:.875rem;color:var(--foreground, hsl(222.2, 84%, 4.9%));cursor:pointer;transition:background-color .2s,border-color .2s}.chip:hover{background-color:var(--accent, hsl(210, 40%, 96.1%));color:var(--accent-foreground, hsl(222.2, 47.4%, 11.2%));border-color:var(--ring, hsl(222.2, 84%, 4.9%))}.chip:active{background-color:var(--secondary, hsl(210, 40%, 96.1%));color:var(--secondary-foreground, hsl(222.2, 47.4%, 11.2%))}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: ButtonDirective, selector: "button[documentEngineButton]", inputs: ["variant", "size"] }, { kind: "directive", type: InputDirective, selector: "input[documentEngineInput]", inputs: ["class"] }, { kind: "directive", type: LabelDirective, selector: "label[documentEngineLabel]", inputs: ["class"] }, { kind: "component", type: ErrorMessageComponent, selector: "document-engine-error-message", inputs: ["class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2519
2552
  }
2520
2553
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: LinkEditViewComponent, decorators: [{
2521
2554
  type: Component,
@@ -2585,7 +2618,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
2585
2618
  </div>
2586
2619
  </form>
2587
2620
  </div>
2588
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".link-edit-view{display:flex;flex-direction:column;min-width:320px}.link-edit-view__header{padding:.5rem 1rem;border-bottom:1px solid #e5e7eb;font-weight:500;font-size:.875rem}.link-edit-view__content{display:flex;flex-direction:column;gap:1.5rem;padding:1rem .5rem}.link-edit-view__shortcuts{display:flex;flex-wrap:wrap;gap:1rem;align-items:center;justify-content:space-between;border-top:1px solid #e5e7eb;padding:1rem .5rem}.link-edit-view__shortcuts-label{color:#6b7280;font-size:.875rem}.link-edit-view__actions{display:flex;gap:.5rem;justify-content:flex-end;padding:.5rem;border-top:1px solid #e5e7eb}.link-edit-view__field{display:flex;flex-direction:column;gap:.5rem;width:100%}.chip-set{display:flex;flex-wrap:wrap;gap:.5rem;align-items:center}.chip{display:inline-flex;align-items:center;padding:.25rem .75rem;background-color:#f3f4f6;border:1px solid #e5e7eb;border-radius:9999px;font-size:.875rem;color:#374151;cursor:pointer;transition:background-color .2s,border-color .2s}.chip:hover{background-color:#e5e7eb;border-color:#d1d5db}.chip:active{background-color:#d1d5db}\n"] }]
2621
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".link-edit-view{display:flex;flex-direction:column;min-width:320px}.link-edit-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));font-weight:500;font-size:.875rem}.link-edit-view__content{display:flex;flex-direction:column;gap:1.5rem;padding:1rem .5rem}.link-edit-view__shortcuts{display:flex;flex-wrap:wrap;gap:1rem;align-items:center;justify-content:space-between;border-top:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));padding:1rem .5rem}.link-edit-view__shortcuts-label{color:var(--muted-foreground, hsl(215.4, 16.3%, 46.9%));font-size:.875rem}.link-edit-view__actions{display:flex;gap:.5rem;justify-content:flex-end;padding:.5rem;border-top:1px solid var(--border, hsl(214.3, 31.8%, 91.4%))}.link-edit-view__field{display:flex;flex-direction:column;gap:.5rem;width:100%}.chip-set{display:flex;flex-wrap:wrap;gap:.5rem;align-items:center}.chip{display:inline-flex;align-items:center;padding:.25rem .75rem;background-color:var(--muted, hsl(210, 40%, 96.1%));border:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));border-radius:9999px;font-size:.875rem;color:var(--foreground, hsl(222.2, 84%, 4.9%));cursor:pointer;transition:background-color .2s,border-color .2s}.chip:hover{background-color:var(--accent, hsl(210, 40%, 96.1%));color:var(--accent-foreground, hsl(222.2, 47.4%, 11.2%));border-color:var(--ring, hsl(222.2, 84%, 4.9%))}.chip:active{background-color:var(--secondary, hsl(210, 40%, 96.1%));color:var(--secondary-foreground, hsl(222.2, 47.4%, 11.2%))}\n"] }]
2589
2622
  }] });
2590
2623
 
2591
2624
  /**
@@ -2656,7 +2689,7 @@ class LinkMainViewComponent {
2656
2689
  </button>
2657
2690
  </div>
2658
2691
  </div>
2659
- `, isInline: true, styles: [".link-main-view{display:flex;flex-direction:column}.link-main-view__header{padding:.5rem 1rem;border-bottom:1px solid #e5e7eb;font-weight:500;font-size:.875rem}.link-main-view__content{display:flex;gap:.5rem;padding:.75rem;align-items:center}.link-main-view__url{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px;font-size:.875rem;color:#3b82f6;text-decoration:none;transition:color .2s}.link-main-view__url:hover{color:#2563eb;text-decoration:underline}.link-main-view__button{display:inline-flex;align-items:center;gap:.25rem;white-space:nowrap;min-width:fit-content;width:auto}.link-main-view__button document-engine-icon{width:1rem;height:1rem;display:inline-flex;align-items:center;justify-content:center}.link-main-view__button span{font-size:.875rem}.link-main-view__button-icon{display:inline-flex;align-items:center;justify-content:center}.link-main-view__button-icon document-engine-icon{width:1rem;height:1rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: ButtonDirective, selector: "button[documentEngineButton]", inputs: ["variant", "size"] }, { kind: "component", type: IconComponent, selector: "document-engine-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2692
+ `, isInline: true, styles: [".link-main-view{display:flex;flex-direction:column}.link-main-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));font-weight:500;font-size:.875rem}.link-main-view__content{display:flex;gap:.5rem;padding:.75rem;align-items:center}.link-main-view__url{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px;font-size:.875rem;color:var(--primary, hsl(222.2, 47.4%, 11.2%));text-decoration:none;transition:color .2s}.link-main-view__url:hover{text-decoration:underline}.link-main-view__button{display:inline-flex;align-items:center;gap:.25rem;white-space:nowrap;min-width:fit-content;width:auto}.link-main-view__button document-engine-icon{width:1rem;height:1rem;display:inline-flex;align-items:center;justify-content:center}.link-main-view__button span{font-size:.875rem}.link-main-view__button-icon{display:inline-flex;align-items:center;justify-content:center}.link-main-view__button-icon document-engine-icon{width:1rem;height:1rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: ButtonDirective, selector: "button[documentEngineButton]", inputs: ["variant", "size"] }, { kind: "component", type: IconComponent, selector: "document-engine-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2660
2693
  }
2661
2694
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: LinkMainViewComponent, decorators: [{
2662
2695
  type: Component,
@@ -2699,9 +2732,125 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
2699
2732
  </button>
2700
2733
  </div>
2701
2734
  </div>
2702
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".link-main-view{display:flex;flex-direction:column}.link-main-view__header{padding:.5rem 1rem;border-bottom:1px solid #e5e7eb;font-weight:500;font-size:.875rem}.link-main-view__content{display:flex;gap:.5rem;padding:.75rem;align-items:center}.link-main-view__url{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px;font-size:.875rem;color:#3b82f6;text-decoration:none;transition:color .2s}.link-main-view__url:hover{color:#2563eb;text-decoration:underline}.link-main-view__button{display:inline-flex;align-items:center;gap:.25rem;white-space:nowrap;min-width:fit-content;width:auto}.link-main-view__button document-engine-icon{width:1rem;height:1rem;display:inline-flex;align-items:center;justify-content:center}.link-main-view__button span{font-size:.875rem}.link-main-view__button-icon{display:inline-flex;align-items:center;justify-content:center}.link-main-view__button-icon document-engine-icon{width:1rem;height:1rem}\n"] }]
2735
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".link-main-view{display:flex;flex-direction:column}.link-main-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));font-weight:500;font-size:.875rem}.link-main-view__content{display:flex;gap:.5rem;padding:.75rem;align-items:center}.link-main-view__url{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px;font-size:.875rem;color:var(--primary, hsl(222.2, 47.4%, 11.2%));text-decoration:none;transition:color .2s}.link-main-view__url:hover{text-decoration:underline}.link-main-view__button{display:inline-flex;align-items:center;gap:.25rem;white-space:nowrap;min-width:fit-content;width:auto}.link-main-view__button document-engine-icon{width:1rem;height:1rem;display:inline-flex;align-items:center;justify-content:center}.link-main-view__button span{font-size:.875rem}.link-main-view__button-icon{display:inline-flex;align-items:center;justify-content:center}.link-main-view__button-icon document-engine-icon{width:1rem;height:1rem}\n"] }]
2703
2736
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }] });
2704
2737
 
2738
+ /**
2739
+ * Checkbox component
2740
+ * Works with ngModel and reactive forms
2741
+ *
2742
+ * Usage:
2743
+ * <document-engine-checkbox [(ngModel)]="checked">Label text</document-engine-checkbox>
2744
+ */
2745
+ class CheckboxComponent {
2746
+ disabled = false;
2747
+ checked = false;
2748
+ checkedChange = new EventEmitter();
2749
+ hostClass = true;
2750
+ cdr = inject(ChangeDetectorRef);
2751
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
2752
+ onChange = () => { };
2753
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
2754
+ onTouched = () => { };
2755
+ onCheckboxChange(event) {
2756
+ if (this.disabled)
2757
+ return;
2758
+ const target = event.target;
2759
+ this.checked = target.checked;
2760
+ this.checkedChange.emit(this.checked);
2761
+ this.onChange(this.checked);
2762
+ this.onTouched();
2763
+ }
2764
+ // ControlValueAccessor implementation.
2765
+ //
2766
+ // The forms package calls these from outside any template binding, so on an
2767
+ // OnPush component they must mark the view dirty explicitly. Without it a
2768
+ // programmatic `setValue()` — including a bubble-menu view being re-activated
2769
+ // with different attributes on a reused instance — updates the model while the
2770
+ // rendered tick keeps showing the previous state.
2771
+ writeValue(value) {
2772
+ this.checked = value;
2773
+ this.cdr.markForCheck();
2774
+ }
2775
+ registerOnChange(fn) {
2776
+ this.onChange = fn;
2777
+ }
2778
+ registerOnTouched(fn) {
2779
+ this.onTouched = fn;
2780
+ }
2781
+ setDisabledState(isDisabled) {
2782
+ this.disabled = isDisabled;
2783
+ this.cdr.markForCheck();
2784
+ }
2785
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: CheckboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2786
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: CheckboxComponent, isStandalone: true, selector: "document-engine-checkbox", inputs: { disabled: "disabled", checked: "checked" }, outputs: { checkedChange: "checkedChange" }, host: { properties: { "class.document-engine-checkbox-container": "this.hostClass" } }, providers: [
2787
+ {
2788
+ provide: NG_VALUE_ACCESSOR,
2789
+ useExisting: forwardRef(() => CheckboxComponent),
2790
+ multi: true,
2791
+ },
2792
+ ], ngImport: i0, template: `
2793
+ <label class="document-engine-checkbox">
2794
+ <input
2795
+ type="checkbox"
2796
+ class="document-engine-checkbox__input"
2797
+ [checked]="checked"
2798
+ [disabled]="disabled"
2799
+ (change)="onCheckboxChange($event)"
2800
+ />
2801
+ <span class="document-engine-checkbox__box">
2802
+ <document-engine-icon
2803
+ *ngIf="checked"
2804
+ name="check"
2805
+ class="document-engine-checkbox__icon"
2806
+ ></document-engine-icon>
2807
+ </span>
2808
+ <span class="document-engine-checkbox__label">
2809
+ <ng-content></ng-content>
2810
+ </span>
2811
+ </label>
2812
+ `, isInline: true, styles: [".document-engine-checkbox{display:inline-flex;align-items:center;gap:.5rem;cursor:pointer;-webkit-user-select:none;user-select:none}.document-engine-checkbox__input{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.document-engine-checkbox__box{display:flex;align-items:center;justify-content:center;width:1rem;height:1rem;border:1px solid var(--input);border-radius:.25rem;background-color:transparent;transition:background-color .2s,border-color .2s}.document-engine-checkbox__input:checked~.document-engine-checkbox__box{background-color:var(--primary);border-color:var(--primary)}.document-engine-checkbox__input:focus-visible~.document-engine-checkbox__box{outline:2px solid var(--ring);outline-offset:2px}.document-engine-checkbox__input:disabled~.document-engine-checkbox__box{cursor:not-allowed;opacity:.5}.document-engine-checkbox__icon{width:.75rem;height:.75rem;color:var(--primary-foreground)}.document-engine-checkbox__label{font-size:.875rem;line-height:1;font-weight:500}.document-engine-checkbox__input:disabled~.document-engine-checkbox__label{cursor:not-allowed;opacity:.5}.document-engine-checkbox-container{display:inline-block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: IconComponent, selector: "document-engine-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2813
+ }
2814
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: CheckboxComponent, decorators: [{
2815
+ type: Component,
2816
+ args: [{ selector: 'document-engine-checkbox', standalone: true, imports: [CommonModule, IconComponent], providers: [
2817
+ {
2818
+ provide: NG_VALUE_ACCESSOR,
2819
+ useExisting: forwardRef(() => CheckboxComponent),
2820
+ multi: true,
2821
+ },
2822
+ ], template: `
2823
+ <label class="document-engine-checkbox">
2824
+ <input
2825
+ type="checkbox"
2826
+ class="document-engine-checkbox__input"
2827
+ [checked]="checked"
2828
+ [disabled]="disabled"
2829
+ (change)="onCheckboxChange($event)"
2830
+ />
2831
+ <span class="document-engine-checkbox__box">
2832
+ <document-engine-icon
2833
+ *ngIf="checked"
2834
+ name="check"
2835
+ class="document-engine-checkbox__icon"
2836
+ ></document-engine-icon>
2837
+ </span>
2838
+ <span class="document-engine-checkbox__label">
2839
+ <ng-content></ng-content>
2840
+ </span>
2841
+ </label>
2842
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".document-engine-checkbox{display:inline-flex;align-items:center;gap:.5rem;cursor:pointer;-webkit-user-select:none;user-select:none}.document-engine-checkbox__input{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.document-engine-checkbox__box{display:flex;align-items:center;justify-content:center;width:1rem;height:1rem;border:1px solid var(--input);border-radius:.25rem;background-color:transparent;transition:background-color .2s,border-color .2s}.document-engine-checkbox__input:checked~.document-engine-checkbox__box{background-color:var(--primary);border-color:var(--primary)}.document-engine-checkbox__input:focus-visible~.document-engine-checkbox__box{outline:2px solid var(--ring);outline-offset:2px}.document-engine-checkbox__input:disabled~.document-engine-checkbox__box{cursor:not-allowed;opacity:.5}.document-engine-checkbox__icon{width:.75rem;height:.75rem;color:var(--primary-foreground)}.document-engine-checkbox__label{font-size:.875rem;line-height:1;font-weight:500}.document-engine-checkbox__input:disabled~.document-engine-checkbox__label{cursor:not-allowed;opacity:.5}.document-engine-checkbox-container{display:inline-block}\n"] }]
2843
+ }], propDecorators: { disabled: [{
2844
+ type: Input
2845
+ }], checked: [{
2846
+ type: Input
2847
+ }], checkedChange: [{
2848
+ type: Output
2849
+ }], hostClass: [{
2850
+ type: HostBinding,
2851
+ args: ['class.document-engine-checkbox-container']
2852
+ }] } });
2853
+
2705
2854
  /**
2706
2855
  * Properties view for link bubble menu
2707
2856
  * Shows link properties like target and rel attributes
@@ -2765,27 +2914,30 @@ class LinkPropertiesViewComponent {
2765
2914
  <!-- Content -->
2766
2915
  <div class="link-properties-view__content">
2767
2916
  <!-- Open in new tab -->
2768
- <label class="checkbox-label">
2769
- <input type="checkbox" class="checkbox-input" [formControl]="newTabControl" />
2770
- <span class="checkbox-text">Open in new tab</span>
2771
- </label>
2917
+ <div class="link-properties-view__option">
2918
+ <document-engine-checkbox [formControl]="newTabControl">Open in new tab</document-engine-checkbox>
2919
+ </div>
2772
2920
 
2773
2921
  <!-- No follow -->
2774
- <label class="checkbox-label">
2775
- <input type="checkbox" class="checkbox-input" [formControl]="noFollowControl" />
2776
- <span class="checkbox-text"> Add <code class="inline-code">rel="nofollow"</code> </span>
2777
- </label>
2922
+ <div class="link-properties-view__option">
2923
+ <document-engine-checkbox [formControl]="noFollowControl">
2924
+ Add <code class="inline-code">rel="nofollow"</code>
2925
+ </document-engine-checkbox>
2926
+ </div>
2778
2927
 
2779
2928
  <!-- No opener -->
2780
- <label class="checkbox-label">
2781
- <input type="checkbox" class="checkbox-input" [formControl]="noOpenerControl" />
2782
- <span class="checkbox-text"> Add <code class="inline-code">rel="noopener"</code> </span>
2783
- </label>
2784
-
2785
- <label class="checkbox-label">
2786
- <input type="checkbox" class="checkbox-input" [formControl]="noReferrerControl" />
2787
- <span class="checkbox-text"> Add <code class="inline-code">rel="noreferrer"</code> </span>
2788
- </label>
2929
+ <div class="link-properties-view__option">
2930
+ <document-engine-checkbox [formControl]="noOpenerControl">
2931
+ Add <code class="inline-code">rel="noopener"</code>
2932
+ </document-engine-checkbox>
2933
+ </div>
2934
+
2935
+ <!-- No referrer -->
2936
+ <div class="link-properties-view__option">
2937
+ <document-engine-checkbox [formControl]="noReferrerControl">
2938
+ Add <code class="inline-code">rel="noreferrer"</code>
2939
+ </document-engine-checkbox>
2940
+ </div>
2789
2941
  </div>
2790
2942
 
2791
2943
  <!-- Actions -->
@@ -2795,11 +2947,11 @@ class LinkPropertiesViewComponent {
2795
2947
  <button type="submit" documentEngineButton variant="default" (click)="saveProperties()">Save</button>
2796
2948
  </div>
2797
2949
  </div>
2798
- `, isInline: true, styles: [".link-properties-view{display:flex;flex-direction:column;min-width:280px}.link-properties-view__header{padding:.5rem 1rem;border-bottom:1px solid #e5e7eb;font-weight:500;font-size:.875rem}.link-properties-view__content{display:flex;flex-direction:column;gap:.5rem;padding:1rem .5rem}.link-properties-view__actions{display:flex;gap:.5rem;justify-content:flex-end;padding:.5rem;border-top:1px solid #e5e7eb}.checkbox-label{display:flex;align-items:center;gap:.5rem;cursor:pointer;-webkit-user-select:none;user-select:none;padding:.25rem}.checkbox-input{width:1rem;height:1rem;cursor:pointer;accent-color:#3b82f6}.checkbox-text{font-size:.875rem;color:#374151}.inline-code{padding:.125rem .25rem;border-radius:.25rem;background-color:#0000000d;font-family:Courier New,monospace;font-size:.813rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: ButtonDirective, selector: "button[documentEngineButton]", inputs: ["variant", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2950
+ `, isInline: true, styles: ["@charset \"UTF-8\";.link-properties-view{display:flex;flex-direction:column;min-width:280px}.link-properties-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));font-weight:500;font-size:.875rem}.link-properties-view__content{display:flex;flex-direction:column;gap:.5rem;padding:1rem .5rem}.link-properties-view__actions{display:flex;gap:.5rem;justify-content:flex-end;padding:.5rem;border-top:1px solid var(--border, hsl(214.3, 31.8%, 91.4%))}.link-properties-view__option{display:flex;align-items:center;padding:.25rem}.inline-code{padding:.125rem .25rem;border-radius:.25rem;background-color:var(--muted, hsl(210, 40%, 96.1%));color:var(--muted-foreground, hsl(215.4, 16.3%, 46.9%));font-family:Courier New,monospace;font-size:.813rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: ButtonDirective, selector: "button[documentEngineButton]", inputs: ["variant", "size"] }, { kind: "component", type: CheckboxComponent, selector: "document-engine-checkbox", inputs: ["disabled", "checked"], outputs: ["checkedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2799
2951
  }
2800
2952
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: LinkPropertiesViewComponent, decorators: [{
2801
2953
  type: Component,
2802
- args: [{ selector: 'document-engine-link-properties-view', standalone: true, imports: [CommonModule, ReactiveFormsModule, ButtonDirective], template: `
2954
+ args: [{ selector: 'document-engine-link-properties-view', standalone: true, imports: [CommonModule, ReactiveFormsModule, ButtonDirective, CheckboxComponent], template: `
2803
2955
  <div class="link-properties-view">
2804
2956
  <!-- Title -->
2805
2957
  <!-- No referrer -->
@@ -2808,27 +2960,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
2808
2960
  <!-- Content -->
2809
2961
  <div class="link-properties-view__content">
2810
2962
  <!-- Open in new tab -->
2811
- <label class="checkbox-label">
2812
- <input type="checkbox" class="checkbox-input" [formControl]="newTabControl" />
2813
- <span class="checkbox-text">Open in new tab</span>
2814
- </label>
2963
+ <div class="link-properties-view__option">
2964
+ <document-engine-checkbox [formControl]="newTabControl">Open in new tab</document-engine-checkbox>
2965
+ </div>
2815
2966
 
2816
2967
  <!-- No follow -->
2817
- <label class="checkbox-label">
2818
- <input type="checkbox" class="checkbox-input" [formControl]="noFollowControl" />
2819
- <span class="checkbox-text"> Add <code class="inline-code">rel="nofollow"</code> </span>
2820
- </label>
2968
+ <div class="link-properties-view__option">
2969
+ <document-engine-checkbox [formControl]="noFollowControl">
2970
+ Add <code class="inline-code">rel="nofollow"</code>
2971
+ </document-engine-checkbox>
2972
+ </div>
2821
2973
 
2822
2974
  <!-- No opener -->
2823
- <label class="checkbox-label">
2824
- <input type="checkbox" class="checkbox-input" [formControl]="noOpenerControl" />
2825
- <span class="checkbox-text"> Add <code class="inline-code">rel="noopener"</code> </span>
2826
- </label>
2827
-
2828
- <label class="checkbox-label">
2829
- <input type="checkbox" class="checkbox-input" [formControl]="noReferrerControl" />
2830
- <span class="checkbox-text"> Add <code class="inline-code">rel="noreferrer"</code> </span>
2831
- </label>
2975
+ <div class="link-properties-view__option">
2976
+ <document-engine-checkbox [formControl]="noOpenerControl">
2977
+ Add <code class="inline-code">rel="noopener"</code>
2978
+ </document-engine-checkbox>
2979
+ </div>
2980
+
2981
+ <!-- No referrer -->
2982
+ <div class="link-properties-view__option">
2983
+ <document-engine-checkbox [formControl]="noReferrerControl">
2984
+ Add <code class="inline-code">rel="noreferrer"</code>
2985
+ </document-engine-checkbox>
2986
+ </div>
2832
2987
  </div>
2833
2988
 
2834
2989
  <!-- Actions -->
@@ -2838,7 +2993,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
2838
2993
  <button type="submit" documentEngineButton variant="default" (click)="saveProperties()">Save</button>
2839
2994
  </div>
2840
2995
  </div>
2841
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".link-properties-view{display:flex;flex-direction:column;min-width:280px}.link-properties-view__header{padding:.5rem 1rem;border-bottom:1px solid #e5e7eb;font-weight:500;font-size:.875rem}.link-properties-view__content{display:flex;flex-direction:column;gap:.5rem;padding:1rem .5rem}.link-properties-view__actions{display:flex;gap:.5rem;justify-content:flex-end;padding:.5rem;border-top:1px solid #e5e7eb}.checkbox-label{display:flex;align-items:center;gap:.5rem;cursor:pointer;-webkit-user-select:none;user-select:none;padding:.25rem}.checkbox-input{width:1rem;height:1rem;cursor:pointer;accent-color:#3b82f6}.checkbox-text{font-size:.875rem;color:#374151}.inline-code{padding:.125rem .25rem;border-radius:.25rem;background-color:#0000000d;font-family:Courier New,monospace;font-size:.813rem}\n"] }]
2996
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@charset \"UTF-8\";.link-properties-view{display:flex;flex-direction:column;min-width:280px}.link-properties-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));font-weight:500;font-size:.875rem}.link-properties-view__content{display:flex;flex-direction:column;gap:.5rem;padding:1rem .5rem}.link-properties-view__actions{display:flex;gap:.5rem;justify-content:flex-end;padding:.5rem;border-top:1px solid var(--border, hsl(214.3, 31.8%, 91.4%))}.link-properties-view__option{display:flex;align-items:center;padding:.25rem}.inline-code{padding:.125rem .25rem;border-radius:.25rem;background-color:var(--muted, hsl(210, 40%, 96.1%));color:var(--muted-foreground, hsl(215.4, 16.3%, 46.9%));font-family:Courier New,monospace;font-size:.813rem}\n"] }]
2842
2997
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }] });
2843
2998
 
2844
2999
  const LinkBubbleConfig = {
@@ -3578,6 +3733,46 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
3578
3733
  args: [ToggleOptionDirective, { descendants: true }]
3579
3734
  }] } });
3580
3735
 
3736
+ /**
3737
+ * Normalize what a user typed into the border-width field into a valid CSS length.
3738
+ *
3739
+ * The field is a free-text input, and `border-width: 3` is not a valid declaration —
3740
+ * the browser drops it silently, so typing a bare number used to do nothing at all
3741
+ * with no feedback. A bare number is the single most likely thing to type, and in a
3742
+ * length context it conventionally means pixels, so it is completed to `px`.
3743
+ *
3744
+ * Anything already carrying a unit is left alone (only the unit is lower-cased).
3745
+ *
3746
+ * The two failure modes are deliberately NOT the same value, because the command this
3747
+ * feeds reads `null` as "clear this field" and `undefined` as "leave it alone":
3748
+ *
3749
+ * - an **empty** field is a real instruction to clear the width, so it returns `null`;
3750
+ * - **unusable** input (`abc`, `-2px`, `50%`) returns `undefined`, so a typo leaves the
3751
+ * width the cell already had instead of wiping it. Collapsing the two would mean a
3752
+ * user with a working `1px` border who mistypes `3 pixels` loses it on Save with no
3753
+ * message — the field is free text, so mistyping it is the expected case, not a rare one.
3754
+ *
3755
+ * @returns a valid CSS border-width; `null` to clear it; `undefined` to leave it alone.
3756
+ */
3757
+ function normalizeBorderWidth(input) {
3758
+ const value = (input ?? '').trim();
3759
+ if (!value)
3760
+ return null;
3761
+ // `thin | medium | thick` are valid border-width keywords in their own right.
3762
+ if (/^(thin|medium|thick)$/i.test(value))
3763
+ return value.toLowerCase();
3764
+ // A bare number — the case this helper exists for. Border widths cannot be
3765
+ // negative, so a signed value is rejected rather than quietly clamped.
3766
+ if (/^\d+(\.\d+)?$/.test(value))
3767
+ return `${parseFloat(value)}px`;
3768
+ // Already a length. Units per CSS Values 4; `%` is not valid for border-width.
3769
+ const withUnit = /^(\d+(?:\.\d+)?)\s*(px|em|rem|pt|pc|in|cm|mm|q|ch|ex|vh|vw|vmin|vmax)$/i.exec(value);
3770
+ if (withUnit)
3771
+ return `${parseFloat(withUnit[1])}${withUnit[2].toLowerCase()}`;
3772
+ // Not a border-width at all — leave whatever the cell already has untouched.
3773
+ return undefined;
3774
+ }
3775
+
3581
3776
  /**
3582
3777
  * Cell style view for table bubble menu
3583
3778
  * Allows styling individual table cells (borders, background, alignment)
@@ -3679,10 +3874,14 @@ class TableCellStyleViewComponent {
3679
3874
  this.cdr.markForCheck();
3680
3875
  }
3681
3876
  onSave() {
3877
+ // See `normalizeBorderWidth`: a bare number is completed to `px` so the field does
3878
+ // not silently do nothing, and unusable input yields `undefined` so the command
3879
+ // leaves the width the cell already has rather than clearing it.
3880
+ const width = normalizeBorderWidth(this.borderWidth);
3682
3881
  this.editor
3683
3882
  ?.chain()
3684
3883
  .focus()
3685
- .setCellBorder({ style: this.borderStyle, color: this.borderColor, width: this.borderWidth })
3884
+ .setCellBorder({ style: this.borderStyle, color: this.borderColor, width })
3686
3885
  .setCellBackgroundColor(this.backgroundColor)
3687
3886
  .setCellTextAlign(this.textAlign)
3688
3887
  .setCellVerticalAlign(this.vAlign)
@@ -3707,6 +3906,7 @@ class TableCellStyleViewComponent {
3707
3906
  <div class="table-cell-style-view__field-label">Width</div>
3708
3907
  <input
3709
3908
  documentEngineInput
3909
+ data-testid="border-width"
3710
3910
  [(ngModel)]="borderWidth"
3711
3911
  (ngModelChange)="onBorderWidthChange()"
3712
3912
  placeholder="1px"
@@ -3716,7 +3916,7 @@ class TableCellStyleViewComponent {
3716
3916
  <div class="table-cell-style-view__field">
3717
3917
  <div class="table-cell-style-view__field-label">Style</div>
3718
3918
 
3719
- <document-engine-select [(value)]="borderStyle" variant="outline">
3919
+ <document-engine-select data-testid="border-style" [(value)]="borderStyle" variant="outline">
3720
3920
  <button documentEngineSelectOption value="solid">Solid</button>
3721
3921
  <button documentEngineSelectOption value="dashed">Dashed</button>
3722
3922
  <button documentEngineSelectOption value="dotted">Dotted</button>
@@ -3737,6 +3937,7 @@ class TableCellStyleViewComponent {
3737
3937
  size="icon"
3738
3938
  variant="ghost"
3739
3939
  class="table-cell-style-view__color-clear"
3940
+ data-testid="clear-border-color"
3740
3941
  (click)="borderColor = null"
3741
3942
  >
3742
3943
  <document-engine-icon name="close"></document-engine-icon>
@@ -3745,6 +3946,7 @@ class TableCellStyleViewComponent {
3745
3946
  <button
3746
3947
  #borderColorTrigger
3747
3948
  type="button"
3949
+ data-testid="border-color-trigger"
3748
3950
  class="table-cell-style-view__color-swatch"
3749
3951
  [style.background-color]="borderColor || 'transparent'"
3750
3952
  (click)="toggleColorPicker('border')"
@@ -3757,13 +3959,14 @@ class TableCellStyleViewComponent {
3757
3959
  <!-- Color picker for border -->
3758
3960
  <div
3759
3961
  *ngIf="showColorPicker === 'border'"
3760
- [popover]="borderColorTrigger"
3962
+ [documentEnginePopover]="borderColorTrigger"
3761
3963
  [isOpen]="showColorPicker === 'border'"
3762
3964
  class="table-cell-style-view__color-picker-dropdown"
3763
3965
  (click)="$event.stopPropagation()"
3764
3966
  >
3765
3967
  <document-engine-color-picker
3766
3968
  [colorPalette]="colorPalette"
3969
+ data-testid="border-color"
3767
3970
  [activeColor]="borderColorObj"
3768
3971
  (colorSelected)="onBorderColorSelected($event)"
3769
3972
  (colorRemoved)="onBorderColorRemoved()"
@@ -3788,6 +3991,7 @@ class TableCellStyleViewComponent {
3788
3991
  size="icon"
3789
3992
  variant="ghost"
3790
3993
  class="table-cell-style-view__color-clear"
3994
+ data-testid="clear-background-color"
3791
3995
  (click)="backgroundColor = null"
3792
3996
  >
3793
3997
  <document-engine-icon name="close"></document-engine-icon>
@@ -3796,6 +4000,7 @@ class TableCellStyleViewComponent {
3796
4000
  <button
3797
4001
  #bgColorTrigger
3798
4002
  type="button"
4003
+ data-testid="background-color-trigger"
3799
4004
  class="table-cell-style-view__color-swatch"
3800
4005
  [style.background-color]="backgroundColor || 'transparent'"
3801
4006
  (click)="toggleColorPicker('bg')"
@@ -3808,13 +4013,14 @@ class TableCellStyleViewComponent {
3808
4013
  <!-- Color picker for background -->
3809
4014
  <div
3810
4015
  *ngIf="showColorPicker === 'bg'"
3811
- [popover]="bgColorTrigger"
4016
+ [documentEnginePopover]="bgColorTrigger"
3812
4017
  [isOpen]="showColorPicker === 'bg'"
3813
4018
  class="table-cell-style-view__color-picker-dropdown"
3814
4019
  (click)="$event.stopPropagation()"
3815
4020
  >
3816
4021
  <document-engine-color-picker
3817
4022
  [colorPalette]="colorPalette"
4023
+ data-testid="background-color"
3818
4024
  [activeColor]="backgroundColorObj"
3819
4025
  (colorSelected)="onBackgroundColorSelected($event)"
3820
4026
  (colorRemoved)="onBackgroundColorRemoved()"
@@ -3861,11 +4067,11 @@ class TableCellStyleViewComponent {
3861
4067
 
3862
4068
  <!-- Actions -->
3863
4069
  <div class="table-cell-style-view__actions">
3864
- <button documentEngineButton variant="ghost" (click)="cancel()">Cancel</button>
3865
- <button documentEngineButton variant="default" (click)="onSave()">Save</button>
4070
+ <button documentEngineButton data-testid="cancel" variant="ghost" (click)="cancel()">Cancel</button>
4071
+ <button documentEngineButton data-testid="save" variant="default" (click)="onSave()">Save</button>
3866
4072
  </div>
3867
4073
  </div>
3868
- `, isInline: true, styles: [".table-cell-style-view{display:flex;flex-direction:column;width:32.5rem}.table-cell-style-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500}.table-cell-style-view__content{display:flex;flex-direction:column;gap:1rem;padding:.75rem}.table-cell-style-view__section{display:flex;flex-direction:column;gap:.5rem}.table-cell-style-view__section-label{font-weight:700;color:var(--muted-foreground)}.table-cell-style-view__border-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:.5rem}.table-cell-style-view__field{display:flex;flex-direction:column;gap:.25rem;grid-column:span 2}.table-cell-style-view__field--color{grid-column:span 4;position:relative}.table-cell-style-view__field-label{font-size:.75rem;color:var(--muted-foreground)}.table-cell-style-view__color-input{position:relative;display:flex;align-items:center;gap:.25rem}.table-cell-style-view__color-input input{flex:1;padding-right:3rem}.table-cell-style-view__color-clear{position:absolute;right:2.5rem;z-index:1}.table-cell-style-view__color-swatch{position:absolute;right:.5rem;width:1.75rem;height:1.75rem;border-radius:.25rem;border:1px solid var(--border);cursor:pointer;transition:transform .2s;overflow:hidden;z-index:2}.table-cell-style-view__color-swatch:hover{transform:scale(1.1)}.table-cell-style-view__color-checker{position:absolute;inset:0;opacity:.3;background-image:repeating-conic-gradient(#ccc 0deg 90deg,#fff 90deg 180deg);background-size:4px 4px}.table-cell-style-view__color-picker-dropdown{z-index:50;background:var(--popover);border:1px solid var(--border);border-radius:.5rem;box-shadow:var(--shadow-elevation-2);padding:.5rem}.table-cell-style-view__actions{display:flex;align-items:center;justify-content:flex-end;gap:.5rem;padding:.75rem;border-top:1px solid var(--border)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: IconComponent, selector: "document-engine-icon", inputs: ["name"] }, { kind: "directive", type: ButtonDirective, selector: "button[documentEngineButton]", inputs: ["variant", "size"] }, { kind: "directive", type: InputDirective, selector: "input[documentEngineInput]", inputs: ["class"] }, { kind: "component", type: SelectComponent, selector: "document-engine-select", inputs: ["value", "disabled", "labelMode", "variant"], outputs: ["valueChange"] }, { kind: "directive", type: SelectOptionDirective, selector: "[documentEngineSelectOption]", inputs: ["value", "label", "disabled"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ColorPickerComponent, selector: "document-engine-color-picker", inputs: ["colorPalette", "usedColors", "activeColor"], outputs: ["colorSelected", "colorRemoved"] }, { kind: "component", type: ToggleGroupComponent, selector: "document-engine-toggle-group", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: ToggleOptionDirective, selector: "button[documentEngineToggleOption]", inputs: ["value", "disabled"] }, { kind: "directive", type: PopoverDirective, selector: "[popover]", inputs: ["popover", "isOpen", "placement", "offset", "padding"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4074
+ `, isInline: true, styles: ["@charset \"UTF-8\";.table-cell-style-view{display:flex;flex-direction:column;width:32.5rem;max-inline-size:calc(100vw - 2rem)}.table-cell-style-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500}.table-cell-style-view__content{display:flex;flex-direction:column;gap:1rem;padding:.75rem}.table-cell-style-view__section{display:flex;flex-direction:column;gap:.5rem}.table-cell-style-view__section-label{font-weight:700;color:var(--muted-foreground)}.table-cell-style-view__border-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:.5rem}.table-cell-style-view__field{display:flex;flex-direction:column;gap:.25rem;grid-column:span 2}.table-cell-style-view__field--color{grid-column:span 4;position:relative}.table-cell-style-view__field-label{font-size:.75rem;color:var(--muted-foreground)}.table-cell-style-view__color-input{position:relative;display:flex;align-items:center;gap:.25rem}.table-cell-style-view__color-input input{flex:1;padding-right:3rem}.table-cell-style-view__color-clear{position:absolute;right:2.5rem;z-index:1}.table-cell-style-view__color-swatch{position:absolute;right:.5rem;width:1.75rem;height:1.75rem;border-radius:.25rem;border:1px solid var(--border);cursor:pointer;transition:transform .2s;overflow:hidden;z-index:2}.table-cell-style-view__color-swatch:hover{transform:scale(1.1)}.table-cell-style-view__color-checker{position:absolute;inset:0;opacity:.3;background-image:repeating-conic-gradient(#ccc 0deg 90deg,#fff 90deg 180deg);background-size:4px 4px}.table-cell-style-view__color-picker-dropdown{box-sizing:border-box;z-index:50;border-radius:.5rem;border:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));background-color:var(--popover, var(--card, hsl(0, 0%, 100%)));color:var(--popover-foreground, var(--card-foreground, hsl(222.2, 84%, 4.9%)));box-shadow:var(--shadow-elevation-2, 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1));padding:.5rem}.table-cell-style-view__actions{display:flex;align-items:center;justify-content:flex-end;gap:.5rem;padding:.75rem;border-top:1px solid var(--border)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: IconComponent, selector: "document-engine-icon", inputs: ["name"] }, { kind: "directive", type: ButtonDirective, selector: "button[documentEngineButton]", inputs: ["variant", "size"] }, { kind: "directive", type: InputDirective, selector: "input[documentEngineInput]", inputs: ["class"] }, { kind: "component", type: SelectComponent, selector: "document-engine-select", inputs: ["value", "disabled", "labelMode", "variant"], outputs: ["valueChange"] }, { kind: "directive", type: SelectOptionDirective, selector: "[documentEngineSelectOption]", inputs: ["value", "label", "disabled"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ColorPickerComponent, selector: "document-engine-color-picker", inputs: ["colorPalette", "usedColors", "activeColor"], outputs: ["colorSelected", "colorRemoved"] }, { kind: "component", type: ToggleGroupComponent, selector: "document-engine-toggle-group", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: ToggleOptionDirective, selector: "button[documentEngineToggleOption]", inputs: ["value", "disabled"] }, { kind: "directive", type: PopoverDirective, selector: "[documentEnginePopover], [popover]", inputs: ["documentEnginePopover", "popover", "isOpen", "placement", "offset", "padding"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3869
4075
  }
3870
4076
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TableCellStyleViewComponent, decorators: [{
3871
4077
  type: Component,
@@ -3895,6 +4101,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
3895
4101
  <div class="table-cell-style-view__field-label">Width</div>
3896
4102
  <input
3897
4103
  documentEngineInput
4104
+ data-testid="border-width"
3898
4105
  [(ngModel)]="borderWidth"
3899
4106
  (ngModelChange)="onBorderWidthChange()"
3900
4107
  placeholder="1px"
@@ -3904,7 +4111,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
3904
4111
  <div class="table-cell-style-view__field">
3905
4112
  <div class="table-cell-style-view__field-label">Style</div>
3906
4113
 
3907
- <document-engine-select [(value)]="borderStyle" variant="outline">
4114
+ <document-engine-select data-testid="border-style" [(value)]="borderStyle" variant="outline">
3908
4115
  <button documentEngineSelectOption value="solid">Solid</button>
3909
4116
  <button documentEngineSelectOption value="dashed">Dashed</button>
3910
4117
  <button documentEngineSelectOption value="dotted">Dotted</button>
@@ -3925,6 +4132,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
3925
4132
  size="icon"
3926
4133
  variant="ghost"
3927
4134
  class="table-cell-style-view__color-clear"
4135
+ data-testid="clear-border-color"
3928
4136
  (click)="borderColor = null"
3929
4137
  >
3930
4138
  <document-engine-icon name="close"></document-engine-icon>
@@ -3933,6 +4141,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
3933
4141
  <button
3934
4142
  #borderColorTrigger
3935
4143
  type="button"
4144
+ data-testid="border-color-trigger"
3936
4145
  class="table-cell-style-view__color-swatch"
3937
4146
  [style.background-color]="borderColor || 'transparent'"
3938
4147
  (click)="toggleColorPicker('border')"
@@ -3945,13 +4154,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
3945
4154
  <!-- Color picker for border -->
3946
4155
  <div
3947
4156
  *ngIf="showColorPicker === 'border'"
3948
- [popover]="borderColorTrigger"
4157
+ [documentEnginePopover]="borderColorTrigger"
3949
4158
  [isOpen]="showColorPicker === 'border'"
3950
4159
  class="table-cell-style-view__color-picker-dropdown"
3951
4160
  (click)="$event.stopPropagation()"
3952
4161
  >
3953
4162
  <document-engine-color-picker
3954
4163
  [colorPalette]="colorPalette"
4164
+ data-testid="border-color"
3955
4165
  [activeColor]="borderColorObj"
3956
4166
  (colorSelected)="onBorderColorSelected($event)"
3957
4167
  (colorRemoved)="onBorderColorRemoved()"
@@ -3976,6 +4186,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
3976
4186
  size="icon"
3977
4187
  variant="ghost"
3978
4188
  class="table-cell-style-view__color-clear"
4189
+ data-testid="clear-background-color"
3979
4190
  (click)="backgroundColor = null"
3980
4191
  >
3981
4192
  <document-engine-icon name="close"></document-engine-icon>
@@ -3984,6 +4195,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
3984
4195
  <button
3985
4196
  #bgColorTrigger
3986
4197
  type="button"
4198
+ data-testid="background-color-trigger"
3987
4199
  class="table-cell-style-view__color-swatch"
3988
4200
  [style.background-color]="backgroundColor || 'transparent'"
3989
4201
  (click)="toggleColorPicker('bg')"
@@ -3996,13 +4208,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
3996
4208
  <!-- Color picker for background -->
3997
4209
  <div
3998
4210
  *ngIf="showColorPicker === 'bg'"
3999
- [popover]="bgColorTrigger"
4211
+ [documentEnginePopover]="bgColorTrigger"
4000
4212
  [isOpen]="showColorPicker === 'bg'"
4001
4213
  class="table-cell-style-view__color-picker-dropdown"
4002
4214
  (click)="$event.stopPropagation()"
4003
4215
  >
4004
4216
  <document-engine-color-picker
4005
4217
  [colorPalette]="colorPalette"
4218
+ data-testid="background-color"
4006
4219
  [activeColor]="backgroundColorObj"
4007
4220
  (colorSelected)="onBackgroundColorSelected($event)"
4008
4221
  (colorRemoved)="onBackgroundColorRemoved()"
@@ -4049,11 +4262,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
4049
4262
 
4050
4263
  <!-- Actions -->
4051
4264
  <div class="table-cell-style-view__actions">
4052
- <button documentEngineButton variant="ghost" (click)="cancel()">Cancel</button>
4053
- <button documentEngineButton variant="default" (click)="onSave()">Save</button>
4265
+ <button documentEngineButton data-testid="cancel" variant="ghost" (click)="cancel()">Cancel</button>
4266
+ <button documentEngineButton data-testid="save" variant="default" (click)="onSave()">Save</button>
4054
4267
  </div>
4055
4268
  </div>
4056
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".table-cell-style-view{display:flex;flex-direction:column;width:32.5rem}.table-cell-style-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500}.table-cell-style-view__content{display:flex;flex-direction:column;gap:1rem;padding:.75rem}.table-cell-style-view__section{display:flex;flex-direction:column;gap:.5rem}.table-cell-style-view__section-label{font-weight:700;color:var(--muted-foreground)}.table-cell-style-view__border-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:.5rem}.table-cell-style-view__field{display:flex;flex-direction:column;gap:.25rem;grid-column:span 2}.table-cell-style-view__field--color{grid-column:span 4;position:relative}.table-cell-style-view__field-label{font-size:.75rem;color:var(--muted-foreground)}.table-cell-style-view__color-input{position:relative;display:flex;align-items:center;gap:.25rem}.table-cell-style-view__color-input input{flex:1;padding-right:3rem}.table-cell-style-view__color-clear{position:absolute;right:2.5rem;z-index:1}.table-cell-style-view__color-swatch{position:absolute;right:.5rem;width:1.75rem;height:1.75rem;border-radius:.25rem;border:1px solid var(--border);cursor:pointer;transition:transform .2s;overflow:hidden;z-index:2}.table-cell-style-view__color-swatch:hover{transform:scale(1.1)}.table-cell-style-view__color-checker{position:absolute;inset:0;opacity:.3;background-image:repeating-conic-gradient(#ccc 0deg 90deg,#fff 90deg 180deg);background-size:4px 4px}.table-cell-style-view__color-picker-dropdown{z-index:50;background:var(--popover);border:1px solid var(--border);border-radius:.5rem;box-shadow:var(--shadow-elevation-2);padding:.5rem}.table-cell-style-view__actions{display:flex;align-items:center;justify-content:flex-end;gap:.5rem;padding:.75rem;border-top:1px solid var(--border)}\n"] }]
4269
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@charset \"UTF-8\";.table-cell-style-view{display:flex;flex-direction:column;width:32.5rem;max-inline-size:calc(100vw - 2rem)}.table-cell-style-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500}.table-cell-style-view__content{display:flex;flex-direction:column;gap:1rem;padding:.75rem}.table-cell-style-view__section{display:flex;flex-direction:column;gap:.5rem}.table-cell-style-view__section-label{font-weight:700;color:var(--muted-foreground)}.table-cell-style-view__border-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:.5rem}.table-cell-style-view__field{display:flex;flex-direction:column;gap:.25rem;grid-column:span 2}.table-cell-style-view__field--color{grid-column:span 4;position:relative}.table-cell-style-view__field-label{font-size:.75rem;color:var(--muted-foreground)}.table-cell-style-view__color-input{position:relative;display:flex;align-items:center;gap:.25rem}.table-cell-style-view__color-input input{flex:1;padding-right:3rem}.table-cell-style-view__color-clear{position:absolute;right:2.5rem;z-index:1}.table-cell-style-view__color-swatch{position:absolute;right:.5rem;width:1.75rem;height:1.75rem;border-radius:.25rem;border:1px solid var(--border);cursor:pointer;transition:transform .2s;overflow:hidden;z-index:2}.table-cell-style-view__color-swatch:hover{transform:scale(1.1)}.table-cell-style-view__color-checker{position:absolute;inset:0;opacity:.3;background-image:repeating-conic-gradient(#ccc 0deg 90deg,#fff 90deg 180deg);background-size:4px 4px}.table-cell-style-view__color-picker-dropdown{box-sizing:border-box;z-index:50;border-radius:.5rem;border:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));background-color:var(--popover, var(--card, hsl(0, 0%, 100%)));color:var(--popover-foreground, var(--card-foreground, hsl(222.2, 84%, 4.9%)));box-shadow:var(--shadow-elevation-2, 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1));padding:.5rem}.table-cell-style-view__actions{display:flex;align-items:center;justify-content:flex-end;gap:.5rem;padding:.75rem;border-top:1px solid var(--border)}\n"] }]
4057
4270
  }], propDecorators: { borderColorTrigger: [{
4058
4271
  type: ViewChild,
4059
4272
  args: ['borderColorTrigger', { static: false, read: ElementRef }]
@@ -4062,113 +4275,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
4062
4275
  args: ['bgColorTrigger', { static: false, read: ElementRef }]
4063
4276
  }] } });
4064
4277
 
4065
- /**
4066
- * Checkbox component
4067
- * Works with ngModel and reactive forms
4068
- *
4069
- * Usage:
4070
- * <document-engine-checkbox [(ngModel)]="checked">Label text</document-engine-checkbox>
4071
- */
4072
- class CheckboxComponent {
4073
- disabled = false;
4074
- checked = false;
4075
- checkedChange = new EventEmitter();
4076
- hostClass = true;
4077
- // eslint-disable-next-line @typescript-eslint/no-empty-function
4078
- onChange = () => { };
4079
- // eslint-disable-next-line @typescript-eslint/no-empty-function
4080
- onTouched = () => { };
4081
- onCheckboxChange(event) {
4082
- if (this.disabled)
4083
- return;
4084
- const target = event.target;
4085
- this.checked = target.checked;
4086
- this.checkedChange.emit(this.checked);
4087
- this.onChange(this.checked);
4088
- this.onTouched();
4089
- }
4090
- // ControlValueAccessor implementation
4091
- writeValue(value) {
4092
- this.checked = value;
4093
- }
4094
- registerOnChange(fn) {
4095
- this.onChange = fn;
4096
- }
4097
- registerOnTouched(fn) {
4098
- this.onTouched = fn;
4099
- }
4100
- setDisabledState(isDisabled) {
4101
- this.disabled = isDisabled;
4102
- }
4103
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: CheckboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4104
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: CheckboxComponent, isStandalone: true, selector: "document-engine-checkbox", inputs: { disabled: "disabled", checked: "checked" }, outputs: { checkedChange: "checkedChange" }, host: { properties: { "class.document-engine-checkbox-container": "this.hostClass" } }, providers: [
4105
- {
4106
- provide: NG_VALUE_ACCESSOR,
4107
- useExisting: forwardRef(() => CheckboxComponent),
4108
- multi: true,
4109
- },
4110
- ], ngImport: i0, template: `
4111
- <label class="document-engine-checkbox">
4112
- <input
4113
- type="checkbox"
4114
- class="document-engine-checkbox__input"
4115
- [checked]="checked"
4116
- [disabled]="disabled"
4117
- (change)="onCheckboxChange($event)"
4118
- />
4119
- <span class="document-engine-checkbox__box">
4120
- <document-engine-icon
4121
- *ngIf="checked"
4122
- name="check"
4123
- class="document-engine-checkbox__icon"
4124
- ></document-engine-icon>
4125
- </span>
4126
- <span class="document-engine-checkbox__label">
4127
- <ng-content></ng-content>
4128
- </span>
4129
- </label>
4130
- `, isInline: true, styles: [".document-engine-checkbox{display:inline-flex;align-items:center;gap:.5rem;cursor:pointer;-webkit-user-select:none;user-select:none}.document-engine-checkbox__input{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.document-engine-checkbox__box{display:flex;align-items:center;justify-content:center;width:1rem;height:1rem;border:1px solid var(--input);border-radius:.25rem;background-color:transparent;transition:background-color .2s,border-color .2s}.document-engine-checkbox__input:checked~.document-engine-checkbox__box{background-color:var(--primary);border-color:var(--primary)}.document-engine-checkbox__input:focus-visible~.document-engine-checkbox__box{outline:2px solid var(--ring);outline-offset:2px}.document-engine-checkbox__input:disabled~.document-engine-checkbox__box{cursor:not-allowed;opacity:.5}.document-engine-checkbox__icon{width:.75rem;height:.75rem;color:var(--primary-foreground)}.document-engine-checkbox__label{font-size:.875rem;line-height:1;font-weight:500}.document-engine-checkbox__input:disabled~.document-engine-checkbox__label{cursor:not-allowed;opacity:.5}.document-engine-checkbox-container{display:inline-block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: IconComponent, selector: "document-engine-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4131
- }
4132
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: CheckboxComponent, decorators: [{
4133
- type: Component,
4134
- args: [{ selector: 'document-engine-checkbox', standalone: true, imports: [CommonModule, IconComponent], providers: [
4135
- {
4136
- provide: NG_VALUE_ACCESSOR,
4137
- useExisting: forwardRef(() => CheckboxComponent),
4138
- multi: true,
4139
- },
4140
- ], template: `
4141
- <label class="document-engine-checkbox">
4142
- <input
4143
- type="checkbox"
4144
- class="document-engine-checkbox__input"
4145
- [checked]="checked"
4146
- [disabled]="disabled"
4147
- (change)="onCheckboxChange($event)"
4148
- />
4149
- <span class="document-engine-checkbox__box">
4150
- <document-engine-icon
4151
- *ngIf="checked"
4152
- name="check"
4153
- class="document-engine-checkbox__icon"
4154
- ></document-engine-icon>
4155
- </span>
4156
- <span class="document-engine-checkbox__label">
4157
- <ng-content></ng-content>
4158
- </span>
4159
- </label>
4160
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".document-engine-checkbox{display:inline-flex;align-items:center;gap:.5rem;cursor:pointer;-webkit-user-select:none;user-select:none}.document-engine-checkbox__input{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.document-engine-checkbox__box{display:flex;align-items:center;justify-content:center;width:1rem;height:1rem;border:1px solid var(--input);border-radius:.25rem;background-color:transparent;transition:background-color .2s,border-color .2s}.document-engine-checkbox__input:checked~.document-engine-checkbox__box{background-color:var(--primary);border-color:var(--primary)}.document-engine-checkbox__input:focus-visible~.document-engine-checkbox__box{outline:2px solid var(--ring);outline-offset:2px}.document-engine-checkbox__input:disabled~.document-engine-checkbox__box{cursor:not-allowed;opacity:.5}.document-engine-checkbox__icon{width:.75rem;height:.75rem;color:var(--primary-foreground)}.document-engine-checkbox__label{font-size:.875rem;line-height:1;font-weight:500}.document-engine-checkbox__input:disabled~.document-engine-checkbox__label{cursor:not-allowed;opacity:.5}.document-engine-checkbox-container{display:inline-block}\n"] }]
4161
- }], propDecorators: { disabled: [{
4162
- type: Input
4163
- }], checked: [{
4164
- type: Input
4165
- }], checkedChange: [{
4166
- type: Output
4167
- }], hostClass: [{
4168
- type: HostBinding,
4169
- args: ['class.document-engine-checkbox-container']
4170
- }] } });
4171
-
4172
4278
  /**
4173
4279
  * Table creation view for bubble menu
4174
4280
  * Allows users to configure table dimensions and header row
@@ -4553,7 +4659,7 @@ class TableMainViewComponent {
4553
4659
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: TableMainViewComponent, isStandalone: true, selector: "document-engine-table-main-view", ngImport: i0, template: `
4554
4660
  <div class="table-main-view">
4555
4661
  <!-- Column actions -->
4556
- <document-engine-select [(value)]="colAction" (valueChange)="handleColAction($event)" labelMode="static">
4662
+ <document-engine-select data-testid="column-actions" [(value)]="colAction" (valueChange)="handleColAction($event)" labelMode="static">
4557
4663
  <document-engine-icon documentEngineSelectLabel name="table_column"></document-engine-icon>
4558
4664
 
4559
4665
  <button documentEngineSelectOption value="add-before">Insert column left</button>
@@ -4563,7 +4669,7 @@ class TableMainViewComponent {
4563
4669
  </document-engine-select>
4564
4670
 
4565
4671
  <!-- Row actions -->
4566
- <document-engine-select [(value)]="rowAction" (valueChange)="handleRowAction($event)" labelMode="static">
4672
+ <document-engine-select data-testid="row-actions" [(value)]="rowAction" (valueChange)="handleRowAction($event)" labelMode="static">
4567
4673
  <document-engine-icon documentEngineSelectLabel name="table_row"></document-engine-icon>
4568
4674
 
4569
4675
  <button documentEngineSelectOption value="add-before">Insert row above</button>
@@ -4573,11 +4679,15 @@ class TableMainViewComponent {
4573
4679
  </document-engine-select>
4574
4680
 
4575
4681
  <!-- Cell actions -->
4576
- <document-engine-select [(value)]="cellAction" (valueChange)="handleCellAction($event)" labelMode="static">
4682
+ <document-engine-select data-testid="cell-actions" [(value)]="cellAction" (valueChange)="handleCellAction($event)" labelMode="static">
4577
4683
  <document-engine-icon documentEngineSelectLabel name="table_merge_cell"></document-engine-icon>
4578
4684
 
4579
- <button documentEngineSelectOption value="merge" [disabled]="!canMerge">Merge cells</button>
4580
- <button documentEngineSelectOption value="split" [disabled]="!canSplit">Split cell</button>
4685
+ <button documentEngineSelectOption data-testid="merge-cells" value="merge" [disabled]="!canMerge">
4686
+ Merge cells
4687
+ </button>
4688
+ <button documentEngineSelectOption data-testid="split-cell" value="split" [disabled]="!canSplit">
4689
+ Split cell
4690
+ </button>
4581
4691
  </document-engine-select>
4582
4692
 
4583
4693
  <!-- Table style -->
@@ -4586,6 +4696,7 @@ class TableMainViewComponent {
4586
4696
  size="icon"
4587
4697
  variant="ghost"
4588
4698
  title="Table Properties"
4699
+ data-testid="table-properties"
4589
4700
  (click)="navigateTo?.('table-style')"
4590
4701
  >
4591
4702
  <document-engine-icon name="table_property"></document-engine-icon>
@@ -4597,6 +4708,7 @@ class TableMainViewComponent {
4597
4708
  size="icon"
4598
4709
  variant="ghost"
4599
4710
  title="Cell Properties"
4711
+ data-testid="cell-properties"
4600
4712
  (click)="navigateTo?.('cell-style')"
4601
4713
  >
4602
4714
  <document-engine-icon name="table_cell_property"></document-engine-icon>
@@ -4609,7 +4721,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
4609
4721
  args: [{ selector: 'document-engine-table-main-view', standalone: true, imports: [CommonModule, ButtonDirective, IconComponent, SelectComponent, SelectOptionDirective, SelectLabelDirective], template: `
4610
4722
  <div class="table-main-view">
4611
4723
  <!-- Column actions -->
4612
- <document-engine-select [(value)]="colAction" (valueChange)="handleColAction($event)" labelMode="static">
4724
+ <document-engine-select data-testid="column-actions" [(value)]="colAction" (valueChange)="handleColAction($event)" labelMode="static">
4613
4725
  <document-engine-icon documentEngineSelectLabel name="table_column"></document-engine-icon>
4614
4726
 
4615
4727
  <button documentEngineSelectOption value="add-before">Insert column left</button>
@@ -4619,7 +4731,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
4619
4731
  </document-engine-select>
4620
4732
 
4621
4733
  <!-- Row actions -->
4622
- <document-engine-select [(value)]="rowAction" (valueChange)="handleRowAction($event)" labelMode="static">
4734
+ <document-engine-select data-testid="row-actions" [(value)]="rowAction" (valueChange)="handleRowAction($event)" labelMode="static">
4623
4735
  <document-engine-icon documentEngineSelectLabel name="table_row"></document-engine-icon>
4624
4736
 
4625
4737
  <button documentEngineSelectOption value="add-before">Insert row above</button>
@@ -4629,11 +4741,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
4629
4741
  </document-engine-select>
4630
4742
 
4631
4743
  <!-- Cell actions -->
4632
- <document-engine-select [(value)]="cellAction" (valueChange)="handleCellAction($event)" labelMode="static">
4744
+ <document-engine-select data-testid="cell-actions" [(value)]="cellAction" (valueChange)="handleCellAction($event)" labelMode="static">
4633
4745
  <document-engine-icon documentEngineSelectLabel name="table_merge_cell"></document-engine-icon>
4634
4746
 
4635
- <button documentEngineSelectOption value="merge" [disabled]="!canMerge">Merge cells</button>
4636
- <button documentEngineSelectOption value="split" [disabled]="!canSplit">Split cell</button>
4747
+ <button documentEngineSelectOption data-testid="merge-cells" value="merge" [disabled]="!canMerge">
4748
+ Merge cells
4749
+ </button>
4750
+ <button documentEngineSelectOption data-testid="split-cell" value="split" [disabled]="!canSplit">
4751
+ Split cell
4752
+ </button>
4637
4753
  </document-engine-select>
4638
4754
 
4639
4755
  <!-- Table style -->
@@ -4642,6 +4758,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
4642
4758
  size="icon"
4643
4759
  variant="ghost"
4644
4760
  title="Table Properties"
4761
+ data-testid="table-properties"
4645
4762
  (click)="navigateTo?.('table-style')"
4646
4763
  >
4647
4764
  <document-engine-icon name="table_property"></document-engine-icon>
@@ -4653,6 +4770,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
4653
4770
  size="icon"
4654
4771
  variant="ghost"
4655
4772
  title="Cell Properties"
4773
+ data-testid="cell-properties"
4656
4774
  (click)="navigateTo?.('cell-style')"
4657
4775
  >
4658
4776
  <document-engine-icon name="table_cell_property"></document-engine-icon>
@@ -4759,10 +4877,14 @@ class TableStyleViewComponent {
4759
4877
  const editor = this.editor;
4760
4878
  if (!editor)
4761
4879
  return;
4880
+ // Complete a bare number to `px` before it reaches the command; `border-width: 3`
4881
+ // is invalid CSS and would be dropped without any feedback. `undefined` here means
4882
+ // the input was not a width at all, and the command leaves the current one alone.
4883
+ const width = normalizeBorderWidth(this.borderWidth);
4762
4884
  editor
4763
4885
  .chain()
4764
4886
  .focus()
4765
- .setTableBorder({ style: this.borderStyle, color: this.borderColor, width: this.borderWidth })
4887
+ .setTableBorder({ style: this.borderStyle, color: this.borderColor, width })
4766
4888
  .setTableBackgroundColor(this.tableBg)
4767
4889
  .run();
4768
4890
  this.cancel();
@@ -4786,6 +4908,7 @@ class TableStyleViewComponent {
4786
4908
 
4787
4909
  <input
4788
4910
  documentEngineInput
4911
+ data-testid="border-width"
4789
4912
  [(ngModel)]="borderWidth"
4790
4913
  (ngModelChange)="onBorderWidthChange()"
4791
4914
  placeholder="1px"
@@ -4795,7 +4918,7 @@ class TableStyleViewComponent {
4795
4918
  <div class="table-style-view__field">
4796
4919
  <div class="table-style-view__field-label">Style</div>
4797
4920
 
4798
- <document-engine-select [(value)]="borderStyle" variant="outline">
4921
+ <document-engine-select data-testid="border-style" [(value)]="borderStyle" variant="outline">
4799
4922
  <button documentEngineSelectOption value="solid">Solid</button>
4800
4923
  <button documentEngineSelectOption value="double">Double</button>
4801
4924
  <button documentEngineSelectOption value="dashed">Dashed</button>
@@ -4816,6 +4939,7 @@ class TableStyleViewComponent {
4816
4939
  size="icon"
4817
4940
  variant="ghost"
4818
4941
  class="table-style-view__color-clear"
4942
+ data-testid="clear-border-color"
4819
4943
  (click)="borderColor = null"
4820
4944
  >
4821
4945
  <document-engine-icon name="close"></document-engine-icon>
@@ -4824,6 +4948,7 @@ class TableStyleViewComponent {
4824
4948
  <button
4825
4949
  #borderColorTrigger
4826
4950
  type="button"
4951
+ data-testid="border-color-trigger"
4827
4952
  class="table-style-view__color-swatch"
4828
4953
  [style.background-color]="borderColor || 'transparent'"
4829
4954
  (click)="toggleColorPicker('border')"
@@ -4836,13 +4961,14 @@ class TableStyleViewComponent {
4836
4961
  <!-- Color picker for border -->
4837
4962
  <div
4838
4963
  *ngIf="showColorPicker === 'border'"
4839
- [popover]="borderColorTrigger"
4964
+ [documentEnginePopover]="borderColorTrigger"
4840
4965
  [isOpen]="showColorPicker === 'border'"
4841
4966
  class="table-style-view__color-picker-dropdown"
4842
4967
  (click)="$event.stopPropagation()"
4843
4968
  >
4844
4969
  <document-engine-color-picker
4845
4970
  [colorPalette]="colorPalette"
4971
+ data-testid="border-color"
4846
4972
  [activeColor]="borderColorObj"
4847
4973
  (colorSelected)="onBorderColorSelected($event)"
4848
4974
  (colorRemoved)="onBorderColorRemoved()"
@@ -4867,6 +4993,7 @@ class TableStyleViewComponent {
4867
4993
  size="icon"
4868
4994
  variant="ghost"
4869
4995
  class="table-style-view__color-clear"
4996
+ data-testid="clear-background-color"
4870
4997
  (click)="tableBg = null"
4871
4998
  >
4872
4999
  <document-engine-icon name="close"></document-engine-icon>
@@ -4875,6 +5002,7 @@ class TableStyleViewComponent {
4875
5002
  <button
4876
5003
  #bgColorTrigger
4877
5004
  type="button"
5005
+ data-testid="background-color-trigger"
4878
5006
  class="table-style-view__color-swatch"
4879
5007
  [style.background-color]="tableBg || 'transparent'"
4880
5008
  (click)="toggleColorPicker('bg')"
@@ -4887,13 +5015,14 @@ class TableStyleViewComponent {
4887
5015
  <!-- Color picker for background -->
4888
5016
  <div
4889
5017
  *ngIf="showColorPicker === 'bg'"
4890
- [popover]="bgColorTrigger"
5018
+ [documentEnginePopover]="bgColorTrigger"
4891
5019
  [isOpen]="showColorPicker === 'bg'"
4892
5020
  class="table-style-view__color-picker-dropdown"
4893
5021
  (click)="$event.stopPropagation()"
4894
5022
  >
4895
5023
  <document-engine-color-picker
4896
5024
  [colorPalette]="colorPalette"
5025
+ data-testid="background-color"
4897
5026
  [activeColor]="tableBgObj"
4898
5027
  (colorSelected)="onTableBgSelected($event)"
4899
5028
  (colorRemoved)="onTableBgRemoved()"
@@ -4905,11 +5034,11 @@ class TableStyleViewComponent {
4905
5034
 
4906
5035
  <!-- Actions -->
4907
5036
  <div class="table-style-view__actions">
4908
- <button documentEngineButton variant="ghost" (click)="cancel()">Cancel</button>
4909
- <button documentEngineButton variant="default" (click)="onSave()">Save</button>
5037
+ <button documentEngineButton data-testid="cancel" variant="ghost" (click)="cancel()">Cancel</button>
5038
+ <button documentEngineButton data-testid="save" variant="default" (click)="onSave()">Save</button>
4910
5039
  </div>
4911
5040
  </div>
4912
- `, isInline: true, styles: [".table-style-view{display:flex;flex-direction:column;width:32.5rem}.table-style-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500}.table-style-view__content{display:flex;flex-direction:column;gap:1rem;padding:.75rem}.table-style-view__section{display:flex;flex-direction:column;gap:.5rem}.table-style-view__section-label{font-weight:700;color:var(--muted-foreground)}.table-style-view__border-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:.5rem}.table-style-view__field{display:flex;flex-direction:column;gap:.25rem;grid-column:span 2}.table-style-view__field--color{grid-column:span 4;position:relative}.table-style-view__field-label{font-size:.75rem;color:var(--muted-foreground)}.table-style-view__color-input{position:relative;display:flex;align-items:center;gap:.25rem}.table-style-view__color-input input{flex:1;padding-right:3rem}.table-style-view__color-clear{position:absolute;right:2.5rem;z-index:1}.table-style-view__color-swatch{position:absolute;right:.5rem;width:1.75rem;height:1.75rem;border-radius:.25rem;border:1px solid var(--border);cursor:pointer;transition:transform .2s;overflow:hidden;z-index:2}.table-style-view__color-swatch:hover{transform:scale(1.1)}.table-style-view__color-checker{position:absolute;inset:0;opacity:.3;background-image:repeating-conic-gradient(#ccc 0deg 90deg,#fff 90deg 180deg);background-size:4px 4px}.table-style-view__color-picker-dropdown{z-index:50;background:var(--popover);border:1px solid var(--border);border-radius:.5rem;box-shadow:var(--shadow-elevation-2);padding:.5rem}.table-style-view__actions{display:flex;align-items:center;justify-content:flex-end;gap:.5rem;padding:.75rem;border-top:1px solid var(--border)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: IconComponent, selector: "document-engine-icon", inputs: ["name"] }, { kind: "directive", type: ButtonDirective, selector: "button[documentEngineButton]", inputs: ["variant", "size"] }, { kind: "directive", type: InputDirective, selector: "input[documentEngineInput]", inputs: ["class"] }, { kind: "component", type: SelectComponent, selector: "document-engine-select", inputs: ["value", "disabled", "labelMode", "variant"], outputs: ["valueChange"] }, { kind: "directive", type: SelectOptionDirective, selector: "[documentEngineSelectOption]", inputs: ["value", "label", "disabled"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ColorPickerComponent, selector: "document-engine-color-picker", inputs: ["colorPalette", "usedColors", "activeColor"], outputs: ["colorSelected", "colorRemoved"] }, { kind: "directive", type: PopoverDirective, selector: "[popover]", inputs: ["popover", "isOpen", "placement", "offset", "padding"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5041
+ `, isInline: true, styles: ["@charset \"UTF-8\";.table-style-view{display:flex;flex-direction:column;width:32.5rem;max-inline-size:calc(100vw - 2rem)}.table-style-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500}.table-style-view__content{display:flex;flex-direction:column;gap:1rem;padding:.75rem}.table-style-view__section{display:flex;flex-direction:column;gap:.5rem}.table-style-view__section-label{font-weight:700;color:var(--muted-foreground)}.table-style-view__border-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:.5rem}.table-style-view__field{display:flex;flex-direction:column;gap:.25rem;grid-column:span 2}.table-style-view__field--color{grid-column:span 4;position:relative}.table-style-view__field-label{font-size:.75rem;color:var(--muted-foreground)}.table-style-view__color-input{position:relative;display:flex;align-items:center;gap:.25rem}.table-style-view__color-input input{flex:1;padding-right:3rem}.table-style-view__color-clear{position:absolute;right:2.5rem;z-index:1}.table-style-view__color-swatch{position:absolute;right:.5rem;width:1.75rem;height:1.75rem;border-radius:.25rem;border:1px solid var(--border);cursor:pointer;transition:transform .2s;overflow:hidden;z-index:2}.table-style-view__color-swatch:hover{transform:scale(1.1)}.table-style-view__color-checker{position:absolute;inset:0;opacity:.3;background-image:repeating-conic-gradient(#ccc 0deg 90deg,#fff 90deg 180deg);background-size:4px 4px}.table-style-view__color-picker-dropdown{box-sizing:border-box;z-index:50;border-radius:.5rem;border:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));background-color:var(--popover, var(--card, hsl(0, 0%, 100%)));color:var(--popover-foreground, var(--card-foreground, hsl(222.2, 84%, 4.9%)));box-shadow:var(--shadow-elevation-2, 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1));padding:.5rem}.table-style-view__actions{display:flex;align-items:center;justify-content:flex-end;gap:.5rem;padding:.75rem;border-top:1px solid var(--border)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: IconComponent, selector: "document-engine-icon", inputs: ["name"] }, { kind: "directive", type: ButtonDirective, selector: "button[documentEngineButton]", inputs: ["variant", "size"] }, { kind: "directive", type: InputDirective, selector: "input[documentEngineInput]", inputs: ["class"] }, { kind: "component", type: SelectComponent, selector: "document-engine-select", inputs: ["value", "disabled", "labelMode", "variant"], outputs: ["valueChange"] }, { kind: "directive", type: SelectOptionDirective, selector: "[documentEngineSelectOption]", inputs: ["value", "label", "disabled"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ColorPickerComponent, selector: "document-engine-color-picker", inputs: ["colorPalette", "usedColors", "activeColor"], outputs: ["colorSelected", "colorRemoved"] }, { kind: "directive", type: PopoverDirective, selector: "[documentEnginePopover], [popover]", inputs: ["documentEnginePopover", "popover", "isOpen", "placement", "offset", "padding"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4913
5042
  }
4914
5043
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: TableStyleViewComponent, decorators: [{
4915
5044
  type: Component,
@@ -4938,6 +5067,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
4938
5067
 
4939
5068
  <input
4940
5069
  documentEngineInput
5070
+ data-testid="border-width"
4941
5071
  [(ngModel)]="borderWidth"
4942
5072
  (ngModelChange)="onBorderWidthChange()"
4943
5073
  placeholder="1px"
@@ -4947,7 +5077,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
4947
5077
  <div class="table-style-view__field">
4948
5078
  <div class="table-style-view__field-label">Style</div>
4949
5079
 
4950
- <document-engine-select [(value)]="borderStyle" variant="outline">
5080
+ <document-engine-select data-testid="border-style" [(value)]="borderStyle" variant="outline">
4951
5081
  <button documentEngineSelectOption value="solid">Solid</button>
4952
5082
  <button documentEngineSelectOption value="double">Double</button>
4953
5083
  <button documentEngineSelectOption value="dashed">Dashed</button>
@@ -4968,6 +5098,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
4968
5098
  size="icon"
4969
5099
  variant="ghost"
4970
5100
  class="table-style-view__color-clear"
5101
+ data-testid="clear-border-color"
4971
5102
  (click)="borderColor = null"
4972
5103
  >
4973
5104
  <document-engine-icon name="close"></document-engine-icon>
@@ -4976,6 +5107,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
4976
5107
  <button
4977
5108
  #borderColorTrigger
4978
5109
  type="button"
5110
+ data-testid="border-color-trigger"
4979
5111
  class="table-style-view__color-swatch"
4980
5112
  [style.background-color]="borderColor || 'transparent'"
4981
5113
  (click)="toggleColorPicker('border')"
@@ -4988,13 +5120,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
4988
5120
  <!-- Color picker for border -->
4989
5121
  <div
4990
5122
  *ngIf="showColorPicker === 'border'"
4991
- [popover]="borderColorTrigger"
5123
+ [documentEnginePopover]="borderColorTrigger"
4992
5124
  [isOpen]="showColorPicker === 'border'"
4993
5125
  class="table-style-view__color-picker-dropdown"
4994
5126
  (click)="$event.stopPropagation()"
4995
5127
  >
4996
5128
  <document-engine-color-picker
4997
5129
  [colorPalette]="colorPalette"
5130
+ data-testid="border-color"
4998
5131
  [activeColor]="borderColorObj"
4999
5132
  (colorSelected)="onBorderColorSelected($event)"
5000
5133
  (colorRemoved)="onBorderColorRemoved()"
@@ -5019,6 +5152,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
5019
5152
  size="icon"
5020
5153
  variant="ghost"
5021
5154
  class="table-style-view__color-clear"
5155
+ data-testid="clear-background-color"
5022
5156
  (click)="tableBg = null"
5023
5157
  >
5024
5158
  <document-engine-icon name="close"></document-engine-icon>
@@ -5027,6 +5161,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
5027
5161
  <button
5028
5162
  #bgColorTrigger
5029
5163
  type="button"
5164
+ data-testid="background-color-trigger"
5030
5165
  class="table-style-view__color-swatch"
5031
5166
  [style.background-color]="tableBg || 'transparent'"
5032
5167
  (click)="toggleColorPicker('bg')"
@@ -5039,13 +5174,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
5039
5174
  <!-- Color picker for background -->
5040
5175
  <div
5041
5176
  *ngIf="showColorPicker === 'bg'"
5042
- [popover]="bgColorTrigger"
5177
+ [documentEnginePopover]="bgColorTrigger"
5043
5178
  [isOpen]="showColorPicker === 'bg'"
5044
5179
  class="table-style-view__color-picker-dropdown"
5045
5180
  (click)="$event.stopPropagation()"
5046
5181
  >
5047
5182
  <document-engine-color-picker
5048
5183
  [colorPalette]="colorPalette"
5184
+ data-testid="background-color"
5049
5185
  [activeColor]="tableBgObj"
5050
5186
  (colorSelected)="onTableBgSelected($event)"
5051
5187
  (colorRemoved)="onTableBgRemoved()"
@@ -5057,11 +5193,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
5057
5193
 
5058
5194
  <!-- Actions -->
5059
5195
  <div class="table-style-view__actions">
5060
- <button documentEngineButton variant="ghost" (click)="cancel()">Cancel</button>
5061
- <button documentEngineButton variant="default" (click)="onSave()">Save</button>
5196
+ <button documentEngineButton data-testid="cancel" variant="ghost" (click)="cancel()">Cancel</button>
5197
+ <button documentEngineButton data-testid="save" variant="default" (click)="onSave()">Save</button>
5062
5198
  </div>
5063
5199
  </div>
5064
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".table-style-view{display:flex;flex-direction:column;width:32.5rem}.table-style-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500}.table-style-view__content{display:flex;flex-direction:column;gap:1rem;padding:.75rem}.table-style-view__section{display:flex;flex-direction:column;gap:.5rem}.table-style-view__section-label{font-weight:700;color:var(--muted-foreground)}.table-style-view__border-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:.5rem}.table-style-view__field{display:flex;flex-direction:column;gap:.25rem;grid-column:span 2}.table-style-view__field--color{grid-column:span 4;position:relative}.table-style-view__field-label{font-size:.75rem;color:var(--muted-foreground)}.table-style-view__color-input{position:relative;display:flex;align-items:center;gap:.25rem}.table-style-view__color-input input{flex:1;padding-right:3rem}.table-style-view__color-clear{position:absolute;right:2.5rem;z-index:1}.table-style-view__color-swatch{position:absolute;right:.5rem;width:1.75rem;height:1.75rem;border-radius:.25rem;border:1px solid var(--border);cursor:pointer;transition:transform .2s;overflow:hidden;z-index:2}.table-style-view__color-swatch:hover{transform:scale(1.1)}.table-style-view__color-checker{position:absolute;inset:0;opacity:.3;background-image:repeating-conic-gradient(#ccc 0deg 90deg,#fff 90deg 180deg);background-size:4px 4px}.table-style-view__color-picker-dropdown{z-index:50;background:var(--popover);border:1px solid var(--border);border-radius:.5rem;box-shadow:var(--shadow-elevation-2);padding:.5rem}.table-style-view__actions{display:flex;align-items:center;justify-content:flex-end;gap:.5rem;padding:.75rem;border-top:1px solid var(--border)}\n"] }]
5200
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@charset \"UTF-8\";.table-style-view{display:flex;flex-direction:column;width:32.5rem;max-inline-size:calc(100vw - 2rem)}.table-style-view__header{padding:.5rem 1rem;border-bottom:1px solid var(--border);font-weight:500}.table-style-view__content{display:flex;flex-direction:column;gap:1rem;padding:.75rem}.table-style-view__section{display:flex;flex-direction:column;gap:.5rem}.table-style-view__section-label{font-weight:700;color:var(--muted-foreground)}.table-style-view__border-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:.5rem}.table-style-view__field{display:flex;flex-direction:column;gap:.25rem;grid-column:span 2}.table-style-view__field--color{grid-column:span 4;position:relative}.table-style-view__field-label{font-size:.75rem;color:var(--muted-foreground)}.table-style-view__color-input{position:relative;display:flex;align-items:center;gap:.25rem}.table-style-view__color-input input{flex:1;padding-right:3rem}.table-style-view__color-clear{position:absolute;right:2.5rem;z-index:1}.table-style-view__color-swatch{position:absolute;right:.5rem;width:1.75rem;height:1.75rem;border-radius:.25rem;border:1px solid var(--border);cursor:pointer;transition:transform .2s;overflow:hidden;z-index:2}.table-style-view__color-swatch:hover{transform:scale(1.1)}.table-style-view__color-checker{position:absolute;inset:0;opacity:.3;background-image:repeating-conic-gradient(#ccc 0deg 90deg,#fff 90deg 180deg);background-size:4px 4px}.table-style-view__color-picker-dropdown{box-sizing:border-box;z-index:50;border-radius:.5rem;border:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));background-color:var(--popover, var(--card, hsl(0, 0%, 100%)));color:var(--popover-foreground, var(--card-foreground, hsl(222.2, 84%, 4.9%)));box-shadow:var(--shadow-elevation-2, 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1));padding:.5rem}.table-style-view__actions{display:flex;align-items:center;justify-content:flex-end;gap:.5rem;padding:.75rem;border-top:1px solid var(--border)}\n"] }]
5065
5201
  }], propDecorators: { borderColorTrigger: [{
5066
5202
  type: ViewChild,
5067
5203
  args: ['borderColorTrigger', { static: false, read: ElementRef }]
@@ -5680,7 +5816,7 @@ class EditorBubbleMenuComponent {
5680
5816
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: EditorBubbleMenuComponent, isStandalone: true, selector: "document-engine-editor-bubble-menu", inputs: { editor: "editor", config: "config" }, viewQueries: [{ propertyName: "viewContainer", first: true, predicate: ["viewContainer"], descendants: true, read: ViewContainerRef }, { propertyName: "bubbleElement", first: true, predicate: ["bubbleElement"], descendants: true }], ngImport: i0, template: `
5681
5817
  <div
5682
5818
  #bubbleElement
5683
- class="bubble-menu-wrapper rounded border bg-card text-card-foreground border-border shadow-elevation-2 z-30"
5819
+ class="bubble-menu-wrapper"
5684
5820
  tiptapBubbleMenu
5685
5821
  [editor]="editor"
5686
5822
  [props]="config.props!"
@@ -5690,18 +5826,14 @@ class EditorBubbleMenuComponent {
5690
5826
  <!-- View component will be dynamically inserted here -->
5691
5827
  <ng-container #viewContainer></ng-container>
5692
5828
  </div>
5693
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: TiptapBubbleMenuDirective, selector: "tiptap-bubble-menu[editor], [tiptapBubbleMenu][editor]", inputs: ["props", "editor", "mountTo", "shouldShow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5829
+ `, isInline: true, styles: ["@charset \"UTF-8\";.bubble-menu-wrapper{box-sizing:border-box;z-index:30;border-radius:.25rem;border:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));background-color:var(--popover, var(--card, hsl(0, 0%, 100%)));color:var(--popover-foreground, var(--card-foreground, hsl(222.2, 84%, 4.9%)));box-shadow:var(--shadow-elevation-2, 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1))}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: TiptapBubbleMenuDirective, selector: "tiptap-bubble-menu[editor], [tiptapBubbleMenu][editor]", inputs: ["props", "editor", "mountTo", "shouldShow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5694
5830
  }
5695
5831
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: EditorBubbleMenuComponent, decorators: [{
5696
5832
  type: Component,
5697
- args: [{
5698
- selector: 'document-engine-editor-bubble-menu',
5699
- standalone: true,
5700
- imports: [CommonModule, TiptapBubbleMenuDirective],
5701
- template: `
5833
+ args: [{ selector: 'document-engine-editor-bubble-menu', standalone: true, imports: [CommonModule, TiptapBubbleMenuDirective], template: `
5702
5834
  <div
5703
5835
  #bubbleElement
5704
- class="bubble-menu-wrapper rounded border bg-card text-card-foreground border-border shadow-elevation-2 z-30"
5836
+ class="bubble-menu-wrapper"
5705
5837
  tiptapBubbleMenu
5706
5838
  [editor]="editor"
5707
5839
  [props]="config.props!"
@@ -5711,9 +5843,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
5711
5843
  <!-- View component will be dynamically inserted here -->
5712
5844
  <ng-container #viewContainer></ng-container>
5713
5845
  </div>
5714
- `,
5715
- changeDetection: ChangeDetectionStrategy.OnPush,
5716
- }]
5846
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@charset \"UTF-8\";.bubble-menu-wrapper{box-sizing:border-box;z-index:30;border-radius:.25rem;border:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));background-color:var(--popover, var(--card, hsl(0, 0%, 100%)));color:var(--popover-foreground, var(--card-foreground, hsl(222.2, 84%, 4.9%)));box-shadow:var(--shadow-elevation-2, 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1))}\n"] }]
5717
5847
  }], propDecorators: { editor: [{
5718
5848
  type: Input
5719
5849
  }], config: [{
@@ -5742,6 +5872,8 @@ class ToolbarBubbleMenuComponent {
5742
5872
  currentViewRef = null;
5743
5873
  currentViewId = null;
5744
5874
  currentAttributes = {};
5875
+ cleanupAutoUpdate;
5876
+ positionTimeout;
5745
5877
  isVisible = false;
5746
5878
  ngAfterViewInit() {
5747
5879
  const bubbleElement = this.bubbleElement?.nativeElement;
@@ -5782,7 +5914,8 @@ class ToolbarBubbleMenuComponent {
5782
5914
  this.showView(initialView);
5783
5915
  // Position the bubble relative to the target element
5784
5916
  // TODO: It make the bubble jump when the bubble is opened
5785
- setTimeout(() => this.positionBubble(targetElement), 0);
5917
+ clearTimeout(this.positionTimeout);
5918
+ this.positionTimeout = setTimeout(() => this.positionBubble(targetElement), 0);
5786
5919
  // Attach outside click/escape listeners using the bubble element, not the toolbar button
5787
5920
  const bubbleEl = this.bubbleElement?.nativeElement;
5788
5921
  if (bubbleEl) {
@@ -5795,6 +5928,15 @@ class ToolbarBubbleMenuComponent {
5795
5928
  */
5796
5929
  hideBubble() {
5797
5930
  this.isVisible = false;
5931
+ // Cancel a positioning pass that has not run yet. `showBubble` defers it by a
5932
+ // tick, so a show-then-immediately-hide (or a teardown) would otherwise let it
5933
+ // fire afterwards and register a fresh `autoUpdate` — scroll/resize listeners
5934
+ // plus a Resize and an Intersection observer — on a hidden or detached
5935
+ // element, with the handle for disposing it already thrown away.
5936
+ clearTimeout(this.positionTimeout);
5937
+ this.positionTimeout = undefined;
5938
+ this.cleanupAutoUpdate?.();
5939
+ this.cleanupAutoUpdate = undefined;
5798
5940
  this.eventManager.detach(this.config.pluginKey);
5799
5941
  this.destroyCurrentView();
5800
5942
  this.cdr.markForCheck();
@@ -5899,59 +6041,67 @@ class ToolbarBubbleMenuComponent {
5899
6041
  findViewConfig(viewId) {
5900
6042
  return this.config.views.find((v) => v.id === viewId) || null;
5901
6043
  }
6044
+ /**
6045
+ * Anchor the bubble to its trigger with floating-ui, matching
6046
+ * `PopoverDirective`.
6047
+ *
6048
+ * The hand-rolled version this replaces measured the trigger exactly once, so
6049
+ * scrolling left the panel welded to the viewport while its target slid away.
6050
+ * It also had no third case: a panel that fit neither below nor above got a
6051
+ * negative `top`, putting its own header off the top edge of the screen.
6052
+ * `flip` handles the second placement and `shift` clamps the panel into the
6053
+ * viewport instead, so there is no unhandled case left.
6054
+ *
6055
+ * `strategy: 'fixed'` matches the `position: fixed` written below — asking for
6056
+ * the default `'absolute'` coordinates would offset the panel by the page's
6057
+ * scroll amount.
6058
+ */
5902
6059
  positionBubble(targetElement) {
5903
6060
  const bubble = this.bubbleElement?.nativeElement;
5904
6061
  if (!bubble)
5905
6062
  return;
5906
- const targetRect = targetElement.getBoundingClientRect();
5907
- const bubbleRect = bubble.getBoundingClientRect();
5908
- const viewportWidth = window.innerWidth;
5909
- const viewportHeight = window.innerHeight;
5910
- // Default position: below the target element, centered
5911
- let top = targetRect.bottom + 8;
5912
- let left = targetRect.left + targetRect.width / 2 - bubbleRect.width / 2;
5913
- // Adjust horizontal position to stay within viewport
5914
- if (left < 8) {
5915
- left = 8;
5916
- }
5917
- else if (left + bubbleRect.width > viewportWidth - 8) {
5918
- left = viewportWidth - bubbleRect.width - 8;
5919
- }
5920
- // If bubble would go below viewport, position it above the target
5921
- if (top + bubbleRect.height > viewportHeight - 8) {
5922
- top = targetRect.top - bubbleRect.height - 8;
5923
- }
5924
- // Apply position
5925
6063
  bubble.style.position = 'fixed';
5926
- bubble.style.top = `${top}px`;
5927
- bubble.style.left = `${left}px`;
6064
+ this.cleanupAutoUpdate?.();
6065
+ this.cleanupAutoUpdate = autoUpdate(targetElement, bubble, () => {
6066
+ computePosition(targetElement, bubble, {
6067
+ strategy: 'fixed',
6068
+ placement: 'bottom',
6069
+ middleware: [offset(8), flip({ padding: 8 }), shift({ padding: 8 })],
6070
+ })
6071
+ .then(({ x, y }) => {
6072
+ Object.assign(bubble.style, { left: `${x}px`, top: `${y}px` });
6073
+ })
6074
+ .catch((error) => {
6075
+ console.error('[ToolbarBubbleMenu] Error computing position', error);
6076
+ });
6077
+ });
5928
6078
  }
5929
6079
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: ToolbarBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5930
6080
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: ToolbarBubbleMenuComponent, isStandalone: true, selector: "document-engine-toolbar-bubble-menu", inputs: { editor: "editor", config: "config" }, viewQueries: [{ propertyName: "bubbleElement", first: true, predicate: ["bubbleElement"], descendants: true }, { propertyName: "viewContainer", first: true, predicate: ["viewContainer"], descendants: true, read: ViewContainerRef }], ngImport: i0, template: `
5931
6081
  <div
5932
6082
  #bubbleElement
5933
- class="toolbar-bubble-menu rounded border bg-card text-card-foreground border-border shadow-elevation-2 z-50"
5934
- [class.hidden]="!isVisible"
6083
+ class="toolbar-bubble-menu"
6084
+ [class.toolbar-bubble-menu--hidden]="!isVisible"
5935
6085
  [style.width]="config.width || 'auto'"
5936
6086
  [style.maxWidth]="config.maxWidth || '320px'"
5937
6087
  >
5938
6088
  <ng-container #viewContainer></ng-container>
5939
6089
  </div>
5940
- `, isInline: true, styles: [".toolbar-bubble-menu{opacity:0;transform:translateY(-10px);transition:opacity .2s ease-out,transform .2s ease-out;pointer-events:none}.toolbar-bubble-menu:not(.hidden){opacity:1;transform:translateY(0);pointer-events:auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6090
+ `, isInline: true, styles: ["@charset \"UTF-8\";.toolbar-bubble-menu{box-sizing:border-box;z-index:50;border-radius:.25rem;border:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));background-color:var(--popover, var(--card, hsl(0, 0%, 100%)));color:var(--popover-foreground, var(--card-foreground, hsl(222.2, 84%, 4.9%)));box-shadow:var(--shadow-elevation-2, 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1));opacity:1;pointer-events:auto;transition:opacity .2s ease-out}.toolbar-bubble-menu--hidden{display:none;opacity:0;pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5941
6091
  }
5942
6092
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: ToolbarBubbleMenuComponent, decorators: [{
5943
6093
  type: Component,
5944
6094
  args: [{ selector: 'document-engine-toolbar-bubble-menu', standalone: true, imports: [CommonModule], template: `
5945
6095
  <div
5946
6096
  #bubbleElement
5947
- class="toolbar-bubble-menu rounded border bg-card text-card-foreground border-border shadow-elevation-2 z-50"
5948
- [class.hidden]="!isVisible"
6097
+ class="toolbar-bubble-menu"
6098
+ [class.toolbar-bubble-menu--hidden]="!isVisible"
5949
6099
  [style.width]="config.width || 'auto'"
5950
6100
  [style.maxWidth]="config.maxWidth || '320px'"
5951
6101
  >
5952
6102
  <ng-container #viewContainer></ng-container>
5953
6103
  </div>
5954
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".toolbar-bubble-menu{opacity:0;transform:translateY(-10px);transition:opacity .2s ease-out,transform .2s ease-out;pointer-events:none}.toolbar-bubble-menu:not(.hidden){opacity:1;transform:translateY(0);pointer-events:auto}\n"] }]
6104
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@charset \"UTF-8\";.toolbar-bubble-menu{box-sizing:border-box;z-index:50;border-radius:.25rem;border:1px solid var(--border, hsl(214.3, 31.8%, 91.4%));background-color:var(--popover, var(--card, hsl(0, 0%, 100%)));color:var(--popover-foreground, var(--card-foreground, hsl(222.2, 84%, 4.9%)));box-shadow:var(--shadow-elevation-2, 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1));opacity:1;pointer-events:auto;transition:opacity .2s ease-out}.toolbar-bubble-menu--hidden{display:none;opacity:0;pointer-events:none}\n"] }]
5955
6105
  }], propDecorators: { editor: [{
5956
6106
  type: Input
5957
6107
  }], config: [{
@@ -6454,13 +6604,13 @@ class DocumentEditorComponent {
6454
6604
  return merged;
6455
6605
  }
6456
6606
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6457
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: DocumentEditorComponent, isStandalone: true, selector: "document-engine-editor", inputs: { config: "config" }, outputs: { editorReady: "editorReady" }, host: { classAttribute: "no-twp" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"document-engine-document-editor\">\n <document-engine-toolbar\n *ngIf=\"mergedConfig.showToolbar\"\n [editor]=\"editor\"\n [capabilities]=\"capabilities\"\n [config]=\"mergedConfig\"\n ></document-engine-toolbar>\n\n <div class=\"document-engine-document-editor__content\">\n <ng-content></ng-content>\n </div>\n\n <document-engine-footer\n *ngIf=\"editor && mergedConfig.showFooter\"\n [editor]=\"editor\"\n [config]=\"mergedConfig\"\n ></document-engine-footer>\n</div>\n", styles: [":is() *,:is() *:before,:is() *:after{box-sizing:border-box}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ToolbarComponent, selector: "document-engine-toolbar", inputs: ["editor", "capabilities", "config"], outputs: ["action"] }, { kind: "component", type: FooterComponent, selector: "document-engine-footer", inputs: ["editor", "config"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6607
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.10", type: DocumentEditorComponent, isStandalone: true, selector: "document-engine-editor", inputs: { config: "config" }, outputs: { editorReady: "editorReady" }, host: { classAttribute: "no-twp" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"document-engine-document-editor\">\n <document-engine-toolbar\n *ngIf=\"mergedConfig.showToolbar\"\n [editor]=\"editor\"\n [capabilities]=\"capabilities\"\n [config]=\"mergedConfig\"\n ></document-engine-toolbar>\n\n <div class=\"document-engine-document-editor__content\">\n <ng-content></ng-content>\n </div>\n\n <document-engine-footer\n *ngIf=\"editor && mergedConfig.showFooter\"\n [editor]=\"editor\"\n [config]=\"mergedConfig\"\n ></document-engine-footer>\n</div>\n", styles: [":host{display:flex;flex-direction:column;min-height:0}:is() *,:is() *:before,:is() *:after{box-sizing:border-box}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ToolbarComponent, selector: "document-engine-toolbar", inputs: ["editor", "capabilities", "config"], outputs: ["action"] }, { kind: "component", type: FooterComponent, selector: "document-engine-footer", inputs: ["editor", "config"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6458
6608
  }
6459
6609
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: DocumentEditorComponent, decorators: [{
6460
6610
  type: Component,
6461
6611
  args: [{ selector: 'document-engine-editor', imports: [CommonModule, ToolbarComponent, FooterComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: {
6462
6612
  class: 'no-twp',
6463
- }, template: "<div class=\"document-engine-document-editor\">\n <document-engine-toolbar\n *ngIf=\"mergedConfig.showToolbar\"\n [editor]=\"editor\"\n [capabilities]=\"capabilities\"\n [config]=\"mergedConfig\"\n ></document-engine-toolbar>\n\n <div class=\"document-engine-document-editor__content\">\n <ng-content></ng-content>\n </div>\n\n <document-engine-footer\n *ngIf=\"editor && mergedConfig.showFooter\"\n [editor]=\"editor\"\n [config]=\"mergedConfig\"\n ></document-engine-footer>\n</div>\n", styles: [":is() *,:is() *:before,:is() *:after{box-sizing:border-box}\n"] }]
6613
+ }, template: "<div class=\"document-engine-document-editor\">\n <document-engine-toolbar\n *ngIf=\"mergedConfig.showToolbar\"\n [editor]=\"editor\"\n [capabilities]=\"capabilities\"\n [config]=\"mergedConfig\"\n ></document-engine-toolbar>\n\n <div class=\"document-engine-document-editor__content\">\n <ng-content></ng-content>\n </div>\n\n <document-engine-footer\n *ngIf=\"editor && mergedConfig.showFooter\"\n [editor]=\"editor\"\n [config]=\"mergedConfig\"\n ></document-engine-footer>\n</div>\n", styles: [":host{display:flex;flex-direction:column;min-height:0}:is() *,:is() *:before,:is() *:after{box-sizing:border-box}\n"] }]
6464
6614
  }], propDecorators: { config: [{
6465
6615
  type: Input
6466
6616
  }], editorReady: [{