ng-blatui 1.31.0 → 1.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -86,6 +86,8 @@ const BUI_DEFAULT_LABELS = {
86
86
  sonnerDismiss: 'Dismiss notification',
87
87
  themeCustomizer: 'Theme customizer',
88
88
  treeTableToggle: 'Toggle row',
89
+ treeTableCopy: 'Copy as tree',
90
+ treeTableCopied: 'Copied',
89
91
  videoPlay: 'Play video',
90
92
  };
91
93
  /**
@@ -1093,7 +1095,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
1093
1095
  }]
1094
1096
  }], propDecorators: { checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], iconOn: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOn", required: false }] }], iconOff: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOff", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
1095
1097
 
1096
- const TRIGGER = 'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&>svg]:text-muted-foreground [&>svg]:pointer-events-none [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:translate-y-0.5 [&>svg]:transition-transform [&>svg]:duration-200 [&[aria-expanded=true]>svg]:rotate-180';
1098
+ const TRIGGER = 'flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-default disabled:opacity-50 [&>svg]:pointer-events-none [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:translate-y-0.5 [&>svg]:text-muted-foreground [&>svg]:transition-transform [&>svg]:duration-200 [&[aria-expanded=true]>svg]:rotate-180';
1097
1099
  class BuiAccordion {
1098
1100
  userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
1099
1101
  computedClass = computed(() => cn('w-full', this.userClass()), /* @ts-ignore */
@@ -3772,7 +3774,9 @@ class BuiPopover {
3772
3774
  .withPositions([POPOVER_POSITIONS[side], POPOVER_POSITIONS[POPOVER_FALLBACK[side]]]);
3773
3775
  const overlayReference = this.overlay.create({
3774
3776
  positionStrategy,
3775
- scrollStrategy: this.overlay.scrollStrategies.reposition(),
3777
+ // Close (not reposition) on scroll, so the popover doesn't hang detached over the page
3778
+ // once its trigger scrolls out of view.
3779
+ scrollStrategy: this.overlay.scrollStrategies.close(),
3776
3780
  });
3777
3781
  overlayReference.attach(new TemplatePortal(this.content(), this.viewContainerRef));
3778
3782
  overlayReference.outsidePointerEvents().subscribe((event) => {
@@ -5366,8 +5370,15 @@ class BuiDropdownMenu {
5366
5370
  userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
5367
5371
  computedClass = computed(() => cn('absolute z-50 mt-1 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md', this.userClass()), /* @ts-ignore */
5368
5372
  ...(ngDevMode ? [{ debugName: "computedClass" }] : /* istanbul ignore next */ []));
5373
+ // The menu is absolutely anchored to its trigger; once the page scrolls it would otherwise
5374
+ // drift across the layout (e.g. over a sticky navbar). Closing on scroll matches native menus.
5375
+ onScroll() {
5376
+ if (this.menu.visible()) {
5377
+ this.menu.close();
5378
+ }
5379
+ }
5369
5380
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiDropdownMenu, deps: [], target: i0.ɵɵFactoryTarget.Directive });
5370
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: BuiDropdownMenu, isStandalone: true, selector: "[buiDropdownMenu]", inputs: { userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-slot": "dropdown-menu" }, properties: { "hidden": "!menu.visible()", "class": "computedClass()" } }, ngImport: i0 });
5381
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: BuiDropdownMenu, isStandalone: true, selector: "[buiDropdownMenu]", inputs: { userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-slot": "dropdown-menu" }, listeners: { "window:scroll": "onScroll()" }, properties: { "hidden": "!menu.visible()", "class": "computedClass()" } }, ngImport: i0 });
5371
5382
  }
5372
5383
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiDropdownMenu, decorators: [{
5373
5384
  type: Directive,
@@ -5377,6 +5388,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
5377
5388
  'data-slot': 'dropdown-menu',
5378
5389
  '[hidden]': '!menu.visible()',
5379
5390
  '[class]': 'computedClass()',
5391
+ '(window:scroll)': 'onScroll()',
5380
5392
  },
5381
5393
  }]
5382
5394
  }], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
@@ -5428,11 +5440,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
5428
5440
  * `ngMenuTrigger` opening a `ngMenu` (style the menus with `buiDropdownMenu` /
5429
5441
  * `buiDropdownMenuItem`). `MenuBar` is re-exported from `ng-blatui`.
5430
5442
  *
5443
+ * Wrap each trigger + its menu in a `relative` element so the menu anchors under its own
5444
+ * trigger (otherwise the absolutely-positioned panel lands over the bar itself).
5445
+ *
5431
5446
  * ```html
5432
5447
  * <div ngMenuBar buiMenubar>
5433
- * <button ngMenuTrigger [menu]="file" buiMenubarTrigger>File</button>
5434
- * <div ngMenu #file="ngMenu" buiDropdownMenu>
5435
- * <div ngMenuItem value="new" buiDropdownMenuItem>New</div>
5448
+ * <div class="relative">
5449
+ * <button ngMenuTrigger [menu]="file" buiMenubarTrigger>File</button>
5450
+ * <div ngMenu #file="ngMenu" buiDropdownMenu>
5451
+ * <div ngMenuItem value="new" buiDropdownMenuItem>New</div>
5452
+ * </div>
5436
5453
  * </div>
5437
5454
  * </div>
5438
5455
  * ```
@@ -6838,11 +6855,24 @@ class BuiCombobox {
6838
6855
  [disabled]="disabled()"
6839
6856
  [readonly]="!searchable()"
6840
6857
  [class.cursor-pointer]="!searchable()"
6841
- class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
6858
+ class="flex h-9 w-full rounded-md border border-input bg-transparent py-2 ps-3 pe-9 text-sm shadow-xs outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
6842
6859
  (input)="onInput($event)"
6843
6860
  (focus)="openList()"
6844
6861
  (keydown)="onKeydown($event)"
6845
6862
  />
6863
+ <svg
6864
+ viewBox="0 0 24 24"
6865
+ fill="none"
6866
+ stroke="currentColor"
6867
+ stroke-width="2"
6868
+ stroke-linecap="round"
6869
+ stroke-linejoin="round"
6870
+ aria-hidden="true"
6871
+ class="pointer-events-none absolute end-3 top-[1.125rem] size-4 -translate-y-1/2 text-muted-foreground transition-transform"
6872
+ [class.rotate-180]="open()"
6873
+ >
6874
+ <path d="m6 9 6 6 6-6" />
6875
+ </svg>
6846
6876
  @if (open() && filtered().length > 0) {
6847
6877
  <ul
6848
6878
  [id]="listId"
@@ -6922,11 +6952,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
6922
6952
  [disabled]="disabled()"
6923
6953
  [readonly]="!searchable()"
6924
6954
  [class.cursor-pointer]="!searchable()"
6925
- class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
6955
+ class="flex h-9 w-full rounded-md border border-input bg-transparent py-2 ps-3 pe-9 text-sm shadow-xs outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
6926
6956
  (input)="onInput($event)"
6927
6957
  (focus)="openList()"
6928
6958
  (keydown)="onKeydown($event)"
6929
6959
  />
6960
+ <svg
6961
+ viewBox="0 0 24 24"
6962
+ fill="none"
6963
+ stroke="currentColor"
6964
+ stroke-width="2"
6965
+ stroke-linecap="round"
6966
+ stroke-linejoin="round"
6967
+ aria-hidden="true"
6968
+ class="pointer-events-none absolute end-3 top-[1.125rem] size-4 -translate-y-1/2 text-muted-foreground transition-transform"
6969
+ [class.rotate-180]="open()"
6970
+ >
6971
+ <path d="m6 9 6 6 6-6" />
6972
+ </svg>
6930
6973
  @if (open() && filtered().length > 0) {
6931
6974
  <ul
6932
6975
  [id]="listId"
@@ -11283,6 +11326,12 @@ class BuiConfetti {
11283
11326
  /** Horizontal spread factor of the burst. */
11284
11327
  spread = input(70, /* @ts-ignore */
11285
11328
  ...(ngDevMode ? [{ debugName: "spread" }] : /* istanbul ignore next */ []));
11329
+ /** Direction the particles fly out from the origin. */
11330
+ direction = input('down', /* @ts-ignore */
11331
+ ...(ngDevMode ? [{ debugName: "direction" }] : /* istanbul ignore next */ []));
11332
+ /** Rain confetti across the whole viewport instead of bursting from the trigger. */
11333
+ fullscreen = input(false, /* @ts-ignore */
11334
+ ...(ngDevMode ? [{ debugName: "fullscreen" }] : /* istanbul ignore next */ []));
11286
11335
  /** Particle colours; defaults to the built-in palette. */
11287
11336
  colors = input(null, /* @ts-ignore */
11288
11337
  ...(ngDevMode ? [{ debugName: "colors" }] : /* istanbul ignore next */ []));
@@ -11293,18 +11342,25 @@ class BuiConfetti {
11293
11342
  seq = 0;
11294
11343
  computedClass = computed(() => cn('relative inline-block', this.userClass()), /* @ts-ignore */
11295
11344
  ...(ngDevMode ? [{ debugName: "computedClass" }] : /* istanbul ignore next */ []));
11345
+ // Fullscreen rains over a fixed, viewport-covering layer; otherwise particles burst from the
11346
+ // trigger's centre (a zero-size anchor at 50%/50%).
11347
+ layerClass = computed(() => this.fullscreen()
11348
+ ? 'pointer-events-none fixed inset-0 z-[100]'
11349
+ : 'pointer-events-none absolute top-1/2 left-1/2 size-0', /* @ts-ignore */
11350
+ ...(ngDevMode ? [{ debugName: "layerClass" }] : /* istanbul ignore next */ []));
11296
11351
  fire() {
11297
11352
  const colors = this.colors() ?? PALETTE$1;
11353
+ const isFullscreen = this.fullscreen();
11354
+ const direction = this.direction();
11298
11355
  const batch = Array.from({ length: this.count() }, () => {
11299
- const dx = (Math.random() - 0.5) * this.spread() * 4;
11300
- const dy = 80 + Math.random() * 120;
11301
- const rot = Math.random() * 720 - 360;
11356
+ const rot = (Math.random() * 720 - 360).toFixed(0);
11302
11357
  const color = colors[Math.floor(Math.random() * colors.length)];
11303
- const delay = (Math.random() * 0.1).toFixed(2);
11358
+ const delay = (Math.random() * 0.15).toFixed(2);
11304
11359
  this.seq += 1;
11360
+ const motion = isFullscreen ? this.rain() : this.burst(direction);
11305
11361
  return {
11306
11362
  id: this.seq,
11307
- style: `--dx:${dx.toFixed(0)}px;--dy:${dy.toFixed(0)}px;--rot:${rot.toFixed(0)}deg;background:${color};animation-delay:${delay}s`,
11363
+ style: `${motion}--rot:${rot}deg;background:${color};animation-delay:${delay}s`,
11308
11364
  };
11309
11365
  });
11310
11366
  this.particles.set(batch);
@@ -11313,7 +11369,49 @@ class BuiConfetti {
11313
11369
  }
11314
11370
  this.timer = setTimeout(() => {
11315
11371
  this.particles.set([]);
11316
- }, 1200);
11372
+ }, isFullscreen ? 2600 : 1200);
11373
+ }
11374
+ /** Translation vars for a burst from the origin, by direction. */
11375
+ burst(direction) {
11376
+ const spread = this.spread();
11377
+ const rand = Math.random;
11378
+ let dx;
11379
+ let dy;
11380
+ switch (direction) {
11381
+ case 'up': {
11382
+ dx = (rand() - 0.5) * spread * 4;
11383
+ dy = -(80 + rand() * 120);
11384
+ break;
11385
+ }
11386
+ case 'left': {
11387
+ dx = -(100 + rand() * spread * 3);
11388
+ dy = (rand() - 0.5) * spread * 2;
11389
+ break;
11390
+ }
11391
+ case 'right': {
11392
+ dx = 100 + rand() * spread * 3;
11393
+ dy = (rand() - 0.5) * spread * 2;
11394
+ break;
11395
+ }
11396
+ case 'radial': {
11397
+ const angle = rand() * Math.PI * 2;
11398
+ const distance = 80 + rand() * 120;
11399
+ dx = Math.cos(angle) * distance;
11400
+ dy = Math.sin(angle) * distance;
11401
+ break;
11402
+ }
11403
+ default: {
11404
+ dx = (rand() - 0.5) * spread * 4;
11405
+ dy = 80 + rand() * 120;
11406
+ }
11407
+ }
11408
+ return `--dx:${dx.toFixed(0)}px;--dy:${dy.toFixed(0)}px;`;
11409
+ }
11410
+ /** Position + fall vars for a particle raining from the top of the viewport. */
11411
+ rain() {
11412
+ const startX = (Math.random() * 100).toFixed(2);
11413
+ const drift = ((Math.random() - 0.5) * 160).toFixed(0);
11414
+ return `left:${startX}vw;top:-5vh;--dx:${drift}px;--dy:110vh;animation-duration:2.4s;`;
11317
11415
  }
11318
11416
  ngOnDestroy() {
11319
11417
  if (this.timer) {
@@ -11321,7 +11419,7 @@ class BuiConfetti {
11321
11419
  }
11322
11420
  }
11323
11421
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiConfetti, deps: [], target: i0.ɵɵFactoryTarget.Component });
11324
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: BuiConfetti, isStandalone: true, selector: "bui-confetti", inputs: { count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, spread: { classPropertyName: "spread", publicName: "spread", isSignal: true, isRequired: false, transformFunction: null }, colors: { classPropertyName: "colors", publicName: "colors", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-slot": "confetti" }, properties: { "class": "computedClass()" } }, ngImport: i0, template: `
11422
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: BuiConfetti, isStandalone: true, selector: "bui-confetti", inputs: { count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, spread: { classPropertyName: "spread", publicName: "spread", isSignal: true, isRequired: false, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, fullscreen: { classPropertyName: "fullscreen", publicName: "fullscreen", isSignal: true, isRequired: false, transformFunction: null }, colors: { classPropertyName: "colors", publicName: "colors", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-slot": "confetti" }, properties: { "class": "computedClass()" } }, ngImport: i0, template: `
11325
11423
  <span (click)="fire()" (keydown.enter)="fire()" (keydown.space)="fire()">
11326
11424
  <ng-content>
11327
11425
  <button
@@ -11332,7 +11430,7 @@ class BuiConfetti {
11332
11430
  </button>
11333
11431
  </ng-content>
11334
11432
  </span>
11335
- <div class="pointer-events-none absolute top-1/2 left-1/2 size-0" aria-hidden="true">
11433
+ <div [class]="layerClass()" aria-hidden="true">
11336
11434
  @for (particle of particles(); track particle.id) {
11337
11435
  <span
11338
11436
  class="bui-confetti-particle absolute size-2 rounded-sm"
@@ -11355,7 +11453,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
11355
11453
  </button>
11356
11454
  </ng-content>
11357
11455
  </span>
11358
- <div class="pointer-events-none absolute top-1/2 left-1/2 size-0" aria-hidden="true">
11456
+ <div [class]="layerClass()" aria-hidden="true">
11359
11457
  @for (particle of particles(); track particle.id) {
11360
11458
  <span
11361
11459
  class="bui-confetti-particle absolute size-2 rounded-sm"
@@ -11364,7 +11462,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
11364
11462
  }
11365
11463
  </div>
11366
11464
  `, styles: ["@keyframes bui-confetti-fall{to{transform:translate(var(--dx),var(--dy)) rotate(var(--rot));opacity:0}}.bui-confetti-particle{animation:bui-confetti-fall 1s ease-out forwards}@media(prefers-reduced-motion:reduce){.bui-confetti-particle{animation:none;opacity:0}}\n"] }]
11367
- }], propDecorators: { count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], spread: [{ type: i0.Input, args: [{ isSignal: true, alias: "spread", required: false }] }], colors: [{ type: i0.Input, args: [{ isSignal: true, alias: "colors", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
11465
+ }], propDecorators: { count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], spread: [{ type: i0.Input, args: [{ isSignal: true, alias: "spread", required: false }] }], direction: [{ type: i0.Input, args: [{ isSignal: true, alias: "direction", required: false }] }], fullscreen: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullscreen", required: false }] }], colors: [{ type: i0.Input, args: [{ isSignal: true, alias: "colors", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
11368
11466
 
11369
11467
  /** A step indicator. The `value` (1-based) marks the active step. Use `li[buiStepperItem]`. */
11370
11468
  class BuiStepper {
@@ -12839,7 +12937,7 @@ class BuiAudioPlayer {
12839
12937
  ...(ngDevMode ? [{ debugName: "duration" }] : /* istanbul ignore next */ []));
12840
12938
  muted = signal(false, /* @ts-ignore */
12841
12939
  ...(ngDevMode ? [{ debugName: "muted" }] : /* istanbul ignore next */ []));
12842
- computedClass = computed(() => cn('flex items-center gap-3 rounded-lg border bg-card p-3', this.userClass()), /* @ts-ignore */
12940
+ computedClass = computed(() => cn('flex min-w-0 items-center gap-2 rounded-lg border bg-card p-2 sm:gap-3 sm:p-3', this.userClass()), /* @ts-ignore */
12843
12941
  ...(ngDevMode ? [{ debugName: "computedClass" }] : /* istanbul ignore next */ []));
12844
12942
  toggle() {
12845
12943
  const element = this.a()?.nativeElement;
@@ -12930,7 +13028,7 @@ class BuiAudioPlayer {
12930
13028
  step="0.1"
12931
13029
  [max]="duration() || 0"
12932
13030
  [value]="current()"
12933
- class="h-1 flex-1 accent-primary"
13031
+ class="h-1 min-w-0 flex-1 accent-primary"
12934
13032
  [attr.aria-label]="seekText()"
12935
13033
  (input)="seek($event)"
12936
13034
  />
@@ -13002,7 +13100,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
13002
13100
  step="0.1"
13003
13101
  [max]="duration() || 0"
13004
13102
  [value]="current()"
13005
- class="h-1 flex-1 accent-primary"
13103
+ class="h-1 min-w-0 flex-1 accent-primary"
13006
13104
  [attr.aria-label]="seekText()"
13007
13105
  (input)="seek($event)"
13008
13106
  />
@@ -13666,12 +13764,20 @@ class BuiNotificationCenter {
13666
13764
  unreadText = buiLabel('notificationCenterUnread', this.unreadLabel);
13667
13765
  markAllReadText = buiLabel('notificationCenterMarkAllRead', this.markAllReadLabel);
13668
13766
  emptyText = buiLabel('notificationCenterEmpty', this.emptyLabel);
13767
+ host = inject(ElementRef);
13669
13768
  markedRead = signal(new Set(), /* @ts-ignore */
13670
13769
  ...(ngDevMode ? [{ debugName: "markedRead" }] : /* istanbul ignore next */ []));
13671
13770
  unread = computed(() => this.notifications().filter((_, index) => !this.isRead(index)).length, /* @ts-ignore */
13672
13771
  ...(ngDevMode ? [{ debugName: "unread" }] : /* istanbul ignore next */ []));
13673
13772
  computedClass = computed(() => cn('relative inline-block', this.userClass()), /* @ts-ignore */
13674
13773
  ...(ngDevMode ? [{ debugName: "computedClass" }] : /* istanbul ignore next */ []));
13774
+ // Close when a click lands outside the bell + panel. The trigger's own click is contained in
13775
+ // the host, so it toggles open without this handler immediately closing it again.
13776
+ onDocumentClick(event) {
13777
+ if (this.open() && !this.host.nativeElement.contains(event.target)) {
13778
+ this.open.set(false);
13779
+ }
13780
+ }
13675
13781
  isRead(index) {
13676
13782
  return this.markedRead().has(index) || (this.notifications()[index]?.read ?? false);
13677
13783
  }
@@ -13679,7 +13785,7 @@ class BuiNotificationCenter {
13679
13785
  this.markedRead.set(new Set(this.notifications().map((_, index) => index)));
13680
13786
  }
13681
13787
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiNotificationCenter, deps: [], target: i0.ɵɵFactoryTarget.Component });
13682
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: BuiNotificationCenter, isStandalone: true, selector: "bui-notification-center", inputs: { notifications: { classPropertyName: "notifications", publicName: "notifications", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, unreadLabel: { classPropertyName: "unreadLabel", publicName: "unreadLabel", isSignal: true, isRequired: false, transformFunction: null }, markAllReadLabel: { classPropertyName: "markAllReadLabel", publicName: "markAllReadLabel", isSignal: true, isRequired: false, transformFunction: null }, emptyLabel: { classPropertyName: "emptyLabel", publicName: "emptyLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { attributes: { "data-slot": "notification-center" }, properties: { "class": "computedClass()" } }, ngImport: i0, template: `
13788
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: BuiNotificationCenter, isStandalone: true, selector: "bui-notification-center", inputs: { notifications: { classPropertyName: "notifications", publicName: "notifications", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, unreadLabel: { classPropertyName: "unreadLabel", publicName: "unreadLabel", isSignal: true, isRequired: false, transformFunction: null }, markAllReadLabel: { classPropertyName: "markAllReadLabel", publicName: "markAllReadLabel", isSignal: true, isRequired: false, transformFunction: null }, emptyLabel: { classPropertyName: "emptyLabel", publicName: "emptyLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { attributes: { "data-slot": "notification-center" }, listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "open.set(false)", "window:scroll": "open.set(false)" }, properties: { "class": "computedClass()" } }, ngImport: i0, template: `
13683
13789
  <button
13684
13790
  type="button"
13685
13791
  class="relative inline-flex size-9 items-center justify-center rounded-md border border-input hover:bg-accent"
@@ -13754,7 +13860,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
13754
13860
  type: Component,
13755
13861
  args: [{
13756
13862
  selector: 'bui-notification-center',
13757
- host: { 'data-slot': 'notification-center', '[class]': 'computedClass()' },
13863
+ host: {
13864
+ 'data-slot': 'notification-center',
13865
+ '[class]': 'computedClass()',
13866
+ '(document:click)': 'onDocumentClick($event)',
13867
+ '(document:keydown.escape)': 'open.set(false)',
13868
+ '(window:scroll)': 'open.set(false)',
13869
+ },
13758
13870
  template: `
13759
13871
  <button
13760
13872
  type="button"
@@ -14586,7 +14698,21 @@ class BuiTreeTable {
14586
14698
  /** Accessible label for the expand/collapse toggle button. */
14587
14699
  toggleLabel = input(/* @ts-ignore */
14588
14700
  ...(ngDevMode ? [undefined, { debugName: "toggleLabel" }] : /* istanbul ignore next */ []));
14701
+ /** Show a button that copies the full hierarchy as a markdown ASCII tree. */
14702
+ copyable = input(false, /* @ts-ignore */
14703
+ ...(ngDevMode ? [{ debugName: "copyable" }] : /* istanbul ignore next */ []));
14704
+ /** Label override for the copy button (idle state). */
14705
+ copyLabel = input(/* @ts-ignore */
14706
+ ...(ngDevMode ? [undefined, { debugName: "copyLabel" }] : /* istanbul ignore next */ []));
14707
+ /** Label override for the copy button after copying. */
14708
+ copiedLabel = input(/* @ts-ignore */
14709
+ ...(ngDevMode ? [undefined, { debugName: "copiedLabel" }] : /* istanbul ignore next */ []));
14589
14710
  toggleText = buiLabel('treeTableToggle', this.toggleLabel);
14711
+ copyText = buiLabel('treeTableCopy', this.copyLabel);
14712
+ copiedText = buiLabel('treeTableCopied', this.copiedLabel);
14713
+ copied = signal(false, /* @ts-ignore */
14714
+ ...(ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []));
14715
+ copiedTimer;
14590
14716
  overrides = signal(new Map(), /* @ts-ignore */
14591
14717
  ...(ngDevMode ? [{ debugName: "overrides" }] : /* istanbul ignore next */ []));
14592
14718
  defaults = computed(() => {
@@ -14631,6 +14757,47 @@ class BuiTreeTable {
14631
14757
  next.set(path, !this.isOpen(path));
14632
14758
  this.overrides.set(next);
14633
14759
  }
14760
+ /**
14761
+ * Render the full row hierarchy as a markdown ASCII tree (`├──`/`└──`), using the first
14762
+ * column's value as each node's label and a trailing `/` for rows that have children.
14763
+ */
14764
+ toMarkdownTree() {
14765
+ const key = this.columns()[0]?.key ?? '';
14766
+ const lines = [];
14767
+ const render = (rows, prefix, isRoot) => {
14768
+ for (const [index, row] of rows.entries()) {
14769
+ const children = row.children ?? [];
14770
+ const hasChildren = children.length > 0;
14771
+ const label = this.cell(row, key) + (hasChildren ? '/' : '');
14772
+ if (isRoot) {
14773
+ lines.push(label);
14774
+ if (hasChildren) {
14775
+ render(children, '', false);
14776
+ }
14777
+ continue;
14778
+ }
14779
+ const isLast = index === rows.length - 1;
14780
+ lines.push(`${prefix}${isLast ? '└── ' : '├── '}${label}`);
14781
+ if (hasChildren) {
14782
+ const childIndent = isLast ? ' '.repeat(4) : `│${' '.repeat(3)}`;
14783
+ render(children, prefix + childIndent, false);
14784
+ }
14785
+ }
14786
+ };
14787
+ render(this.rows(), '', true);
14788
+ return lines.join('\n');
14789
+ }
14790
+ copyTree() {
14791
+ void navigator.clipboard.writeText(this.toMarkdownTree());
14792
+ this.copied.set(true);
14793
+ clearTimeout(this.copiedTimer);
14794
+ this.copiedTimer = setTimeout(() => {
14795
+ this.copied.set(false);
14796
+ }, 1500);
14797
+ }
14798
+ ngOnDestroy() {
14799
+ clearTimeout(this.copiedTimer);
14800
+ }
14634
14801
  alignClass(align) {
14635
14802
  return ALIGN$2[align ?? 'left'];
14636
14803
  }
@@ -14645,7 +14812,47 @@ class BuiTreeTable {
14645
14812
  return '';
14646
14813
  }
14647
14814
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiTreeTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
14648
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: BuiTreeTable, isStandalone: true, selector: "bui-tree-table", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, toggleLabel: { classPropertyName: "toggleLabel", publicName: "toggleLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-slot": "tree-table" }, properties: { "class": "computedClass()" } }, ngImport: i0, template: `
14815
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: BuiTreeTable, isStandalone: true, selector: "bui-tree-table", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, toggleLabel: { classPropertyName: "toggleLabel", publicName: "toggleLabel", isSignal: true, isRequired: false, transformFunction: null }, copyable: { classPropertyName: "copyable", publicName: "copyable", isSignal: true, isRequired: false, transformFunction: null }, copyLabel: { classPropertyName: "copyLabel", publicName: "copyLabel", isSignal: true, isRequired: false, transformFunction: null }, copiedLabel: { classPropertyName: "copiedLabel", publicName: "copiedLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-slot": "tree-table" }, properties: { "class": "computedClass()" } }, ngImport: i0, template: `
14816
+ @if (copyable()) {
14817
+ <div class="mb-2 flex justify-end">
14818
+ <button
14819
+ type="button"
14820
+ class="inline-flex items-center gap-1.5 rounded-md border border-input px-2.5 py-1 text-xs font-medium hover:bg-accent"
14821
+ (click)="copyTree()"
14822
+ >
14823
+ @if (copied()) {
14824
+ <svg
14825
+ class="size-3.5 text-emerald-500"
14826
+ viewBox="0 0 24 24"
14827
+ fill="none"
14828
+ stroke="currentColor"
14829
+ stroke-width="2"
14830
+ stroke-linecap="round"
14831
+ stroke-linejoin="round"
14832
+ aria-hidden="true"
14833
+ >
14834
+ <path d="M20 6 9 17l-5-5" />
14835
+ </svg>
14836
+ {{ copiedText() }}
14837
+ } @else {
14838
+ <svg
14839
+ class="size-3.5"
14840
+ viewBox="0 0 24 24"
14841
+ fill="none"
14842
+ stroke="currentColor"
14843
+ stroke-width="2"
14844
+ stroke-linecap="round"
14845
+ stroke-linejoin="round"
14846
+ aria-hidden="true"
14847
+ >
14848
+ <rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
14849
+ <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
14850
+ </svg>
14851
+ {{ copyText() }}
14852
+ }
14853
+ </button>
14854
+ </div>
14855
+ }
14649
14856
  <table class="w-full text-sm">
14650
14857
  <thead>
14651
14858
  <tr class="border-b">
@@ -14708,6 +14915,46 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
14708
14915
  selector: 'bui-tree-table',
14709
14916
  host: { 'data-slot': 'tree-table', '[class]': 'computedClass()' },
14710
14917
  template: `
14918
+ @if (copyable()) {
14919
+ <div class="mb-2 flex justify-end">
14920
+ <button
14921
+ type="button"
14922
+ class="inline-flex items-center gap-1.5 rounded-md border border-input px-2.5 py-1 text-xs font-medium hover:bg-accent"
14923
+ (click)="copyTree()"
14924
+ >
14925
+ @if (copied()) {
14926
+ <svg
14927
+ class="size-3.5 text-emerald-500"
14928
+ viewBox="0 0 24 24"
14929
+ fill="none"
14930
+ stroke="currentColor"
14931
+ stroke-width="2"
14932
+ stroke-linecap="round"
14933
+ stroke-linejoin="round"
14934
+ aria-hidden="true"
14935
+ >
14936
+ <path d="M20 6 9 17l-5-5" />
14937
+ </svg>
14938
+ {{ copiedText() }}
14939
+ } @else {
14940
+ <svg
14941
+ class="size-3.5"
14942
+ viewBox="0 0 24 24"
14943
+ fill="none"
14944
+ stroke="currentColor"
14945
+ stroke-width="2"
14946
+ stroke-linecap="round"
14947
+ stroke-linejoin="round"
14948
+ aria-hidden="true"
14949
+ >
14950
+ <rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
14951
+ <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
14952
+ </svg>
14953
+ {{ copyText() }}
14954
+ }
14955
+ </button>
14956
+ </div>
14957
+ }
14711
14958
  <table class="w-full text-sm">
14712
14959
  <thead>
14713
14960
  <tr class="border-b">
@@ -14764,7 +15011,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
14764
15011
  </table>
14765
15012
  `,
14766
15013
  }]
14767
- }], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], toggleLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleLabel", required: false }] }] } });
15014
+ }], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], toggleLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleLabel", required: false }] }], copyable: [{ type: i0.Input, args: [{ isSignal: true, alias: "copyable", required: false }] }], copyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "copyLabel", required: false }] }], copiedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "copiedLabel", required: false }] }] } });
14768
15015
 
14769
15016
  // HTML is escaped first, then a safe subset of markdown is applied, so user input can't inject markup.
14770
15017
  function renderMarkdown(markdown) {
@@ -16068,7 +16315,7 @@ class BuiContextMenu {
16068
16315
  this.open.set(false);
16069
16316
  }
16070
16317
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiContextMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
16071
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: BuiContextMenu, isStandalone: true, selector: "bui-context-menu", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selected" }, host: { attributes: { "data-slot": "context-menu" }, listeners: { "contextmenu": "onContext($event)", "document:click": "open.set(false)", "document:keydown.escape": "open.set(false)" }, properties: { "class": "computedClass()" } }, ngImport: i0, template: `
16318
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: BuiContextMenu, isStandalone: true, selector: "bui-context-menu", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selected" }, host: { attributes: { "data-slot": "context-menu" }, listeners: { "contextmenu": "onContext($event)", "document:click": "open.set(false)", "document:keydown.escape": "open.set(false)", "window:scroll": "open.set(false)" }, properties: { "class": "computedClass()" } }, ngImport: i0, template: `
16072
16319
  <ng-content />
16073
16320
  @if (open()) {
16074
16321
  <div
@@ -16181,6 +16428,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
16181
16428
  '(contextmenu)': 'onContext($event)',
16182
16429
  '(document:click)': 'open.set(false)',
16183
16430
  '(document:keydown.escape)': 'open.set(false)',
16431
+ // The menu is fixed to viewport coordinates captured at open time; close it on scroll
16432
+ // instead of letting it hang at a now-wrong position.
16433
+ '(window:scroll)': 'open.set(false)',
16184
16434
  },
16185
16435
  template: `
16186
16436
  <ng-content />
@@ -18325,36 +18575,38 @@ class BuiNavigationMenu {
18325
18575
  </svg>
18326
18576
  </button>
18327
18577
  @if (active() === i) {
18328
- <div
18329
- class="absolute start-0 top-full z-50 mt-1.5 w-64 rounded-lg border bg-popover p-2 shadow-md"
18330
- >
18331
- @if (item.featured; as featured) {
18332
- <a
18333
- [href]="href(featured.href)"
18334
- class="mb-1 block rounded-md bg-accent/50 p-3 hover:bg-accent"
18335
- >
18336
- <span class="text-sm font-semibold">{{ featured.label }}</span>
18337
- @if (featured.description) {
18338
- <span class="mt-0.5 block text-xs text-muted-foreground">{{
18339
- featured.description
18340
- }}</span>
18341
- }
18342
- </a>
18343
- }
18344
- <ul class="space-y-1">
18345
- @for (link of item.links; track link.label) {
18346
- <li>
18347
- <a [href]="href(link.href)" class="block rounded-md p-2 hover:bg-accent">
18348
- <span class="text-sm font-medium">{{ link.label }}</span>
18349
- @if (link.description) {
18350
- <span class="block text-xs text-muted-foreground">{{
18351
- link.description
18352
- }}</span>
18353
- }
18354
- </a>
18355
- </li>
18578
+ <!-- pt-1.5 (not mt-1.5) keeps the gap below the trigger hoverable, so moving the
18579
+ cursor onto the panel doesn't leave the menu and snap it shut. -->
18580
+ <div class="absolute start-0 top-full z-50 w-64 pt-1.5">
18581
+ <div class="rounded-lg border bg-popover p-2 shadow-md">
18582
+ @if (item.featured; as featured) {
18583
+ <a
18584
+ [href]="href(featured.href)"
18585
+ class="mb-1 block rounded-md bg-accent/50 p-3 hover:bg-accent"
18586
+ >
18587
+ <span class="text-sm font-semibold">{{ featured.label }}</span>
18588
+ @if (featured.description) {
18589
+ <span class="mt-0.5 block text-xs text-muted-foreground">{{
18590
+ featured.description
18591
+ }}</span>
18592
+ }
18593
+ </a>
18356
18594
  }
18357
- </ul>
18595
+ <ul class="space-y-1">
18596
+ @for (link of item.links; track link.label) {
18597
+ <li>
18598
+ <a [href]="href(link.href)" class="block rounded-md p-2 hover:bg-accent">
18599
+ <span class="text-sm font-medium">{{ link.label }}</span>
18600
+ @if (link.description) {
18601
+ <span class="block text-xs text-muted-foreground">{{
18602
+ link.description
18603
+ }}</span>
18604
+ }
18605
+ </a>
18606
+ </li>
18607
+ }
18608
+ </ul>
18609
+ </div>
18358
18610
  </div>
18359
18611
  }
18360
18612
  } @else {
@@ -18405,36 +18657,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
18405
18657
  </svg>
18406
18658
  </button>
18407
18659
  @if (active() === i) {
18408
- <div
18409
- class="absolute start-0 top-full z-50 mt-1.5 w-64 rounded-lg border bg-popover p-2 shadow-md"
18410
- >
18411
- @if (item.featured; as featured) {
18412
- <a
18413
- [href]="href(featured.href)"
18414
- class="mb-1 block rounded-md bg-accent/50 p-3 hover:bg-accent"
18415
- >
18416
- <span class="text-sm font-semibold">{{ featured.label }}</span>
18417
- @if (featured.description) {
18418
- <span class="mt-0.5 block text-xs text-muted-foreground">{{
18419
- featured.description
18420
- }}</span>
18421
- }
18422
- </a>
18423
- }
18424
- <ul class="space-y-1">
18425
- @for (link of item.links; track link.label) {
18426
- <li>
18427
- <a [href]="href(link.href)" class="block rounded-md p-2 hover:bg-accent">
18428
- <span class="text-sm font-medium">{{ link.label }}</span>
18429
- @if (link.description) {
18430
- <span class="block text-xs text-muted-foreground">{{
18431
- link.description
18432
- }}</span>
18433
- }
18434
- </a>
18435
- </li>
18660
+ <!-- pt-1.5 (not mt-1.5) keeps the gap below the trigger hoverable, so moving the
18661
+ cursor onto the panel doesn't leave the menu and snap it shut. -->
18662
+ <div class="absolute start-0 top-full z-50 w-64 pt-1.5">
18663
+ <div class="rounded-lg border bg-popover p-2 shadow-md">
18664
+ @if (item.featured; as featured) {
18665
+ <a
18666
+ [href]="href(featured.href)"
18667
+ class="mb-1 block rounded-md bg-accent/50 p-3 hover:bg-accent"
18668
+ >
18669
+ <span class="text-sm font-semibold">{{ featured.label }}</span>
18670
+ @if (featured.description) {
18671
+ <span class="mt-0.5 block text-xs text-muted-foreground">{{
18672
+ featured.description
18673
+ }}</span>
18674
+ }
18675
+ </a>
18436
18676
  }
18437
- </ul>
18677
+ <ul class="space-y-1">
18678
+ @for (link of item.links; track link.label) {
18679
+ <li>
18680
+ <a [href]="href(link.href)" class="block rounded-md p-2 hover:bg-accent">
18681
+ <span class="text-sm font-medium">{{ link.label }}</span>
18682
+ @if (link.description) {
18683
+ <span class="block text-xs text-muted-foreground">{{
18684
+ link.description
18685
+ }}</span>
18686
+ }
18687
+ </a>
18688
+ </li>
18689
+ }
18690
+ </ul>
18691
+ </div>
18438
18692
  </div>
18439
18693
  }
18440
18694
  } @else {
@@ -19209,9 +19463,16 @@ class BuiRichTextEditor {
19209
19463
  }
19210
19464
  sync() {
19211
19465
  const element = this.editor()?.nativeElement;
19212
- if (element) {
19213
- this.value.set(element.innerHTML);
19466
+ if (!element) {
19467
+ return;
19468
+ }
19469
+ // Browsers leave residual markup (e.g. `<p><br></p>`) after the user clears
19470
+ // all text, which keeps `:empty` from matching so the placeholder never
19471
+ // reappears. Normalise truly-empty content so the placeholder can show again.
19472
+ if (element.textContent === '' && !element.querySelector('img, hr')) {
19473
+ element.innerHTML = '';
19214
19474
  }
19475
+ this.value.set(element.innerHTML);
19215
19476
  }
19216
19477
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiRichTextEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
19217
19478
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: BuiRichTextEditor, isStandalone: true, selector: "bui-rich-text-editor", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, formattingLabel: { classPropertyName: "formattingLabel", publicName: "formattingLabel", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { attributes: { "data-slot": "rich-text-editor" }, properties: { "class": "computedClass()" } }, viewQueries: [{ propertyName: "editor", first: true, predicate: ["editor"], descendants: true, isSignal: true }], ngImport: i0, template: `
@@ -19240,7 +19501,7 @@ class BuiRichTextEditor {
19240
19501
  [attr.aria-label]="ariaLabel()"
19241
19502
  [attr.data-placeholder]="placeholder()"
19242
19503
  contenteditable="true"
19243
- class="min-h-32 p-3 text-sm text-foreground text-muted-foreground/60 outline-none empty:before:content-[attr(data-placeholder)]"
19504
+ class="min-h-32 p-3 text-sm text-foreground outline-none empty:before:text-muted-foreground/60 empty:before:content-[attr(data-placeholder)]"
19244
19505
  (input)="sync()"
19245
19506
  ></div>
19246
19507
  `, isInline: true });
@@ -19276,7 +19537,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
19276
19537
  [attr.aria-label]="ariaLabel()"
19277
19538
  [attr.data-placeholder]="placeholder()"
19278
19539
  contenteditable="true"
19279
- class="min-h-32 p-3 text-sm text-foreground text-muted-foreground/60 outline-none empty:before:content-[attr(data-placeholder)]"
19540
+ class="min-h-32 p-3 text-sm text-foreground outline-none empty:before:text-muted-foreground/60 empty:before:content-[attr(data-placeholder)]"
19280
19541
  (input)="sync()"
19281
19542
  ></div>
19282
19543
  `,
@@ -19379,6 +19640,11 @@ class BuiOnboardingTour {
19379
19640
  prev() {
19380
19641
  this.step.set(Math.max(0, this.step() - 1));
19381
19642
  }
19643
+ onViewportChange() {
19644
+ if (this.open()) {
19645
+ this.measure();
19646
+ }
19647
+ }
19382
19648
  finish() {
19383
19649
  this.open.set(false);
19384
19650
  this.step.set(0);
@@ -19395,7 +19661,7 @@ class BuiOnboardingTour {
19395
19661
  }
19396
19662
  }
19397
19663
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiOnboardingTour, deps: [], target: i0.ɵɵFactoryTarget.Component });
19398
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: BuiOnboardingTour, isStandalone: true, selector: "bui-onboarding-tour", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, steps: { classPropertyName: "steps", publicName: "steps", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { attributes: { "data-slot": "onboarding-tour" }, listeners: { "document:keydown.escape": "finish()" } }, ngImport: i0, template: `
19664
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: BuiOnboardingTour, isStandalone: true, selector: "bui-onboarding-tour", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, steps: { classPropertyName: "steps", publicName: "steps", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { attributes: { "data-slot": "onboarding-tour" }, listeners: { "document:keydown.escape": "finish()", "window:scroll": "onViewportChange()", "window:resize": "onViewportChange()" } }, ngImport: i0, template: `
19399
19665
  @if (open() && current()) {
19400
19666
  <div
19401
19667
  class="pointer-events-none fixed z-[60] rounded-md ring-2 ring-primary transition-all"
@@ -19448,7 +19714,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
19448
19714
  type: Component,
19449
19715
  args: [{
19450
19716
  selector: 'bui-onboarding-tour',
19451
- host: { 'data-slot': 'onboarding-tour', '(document:keydown.escape)': 'finish()' },
19717
+ host: {
19718
+ 'data-slot': 'onboarding-tour',
19719
+ '(document:keydown.escape)': 'finish()',
19720
+ // The spotlight/card use viewport (fixed) coordinates; re-measure on scroll & resize so the
19721
+ // highlight tracks its target instead of drifting out of alignment.
19722
+ '(window:scroll)': 'onViewportChange()',
19723
+ '(window:resize)': 'onViewportChange()',
19724
+ },
19452
19725
  template: `
19453
19726
  @if (open() && current()) {
19454
19727
  <div