ng-blatui 1.30.0 → 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  /**
@@ -174,8 +176,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
174
176
  }]
175
177
  }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
176
178
 
177
- const BASE$2 = 'inline-flex items-center justify-center rounded-md border font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden';
178
- const SIZES$6 = {
179
+ const BASE$1 = 'inline-flex items-center justify-center rounded-md border font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden';
180
+ const SIZES$4 = {
179
181
  sm: 'px-1.5 py-px text-[0.625rem]',
180
182
  default: 'px-2 py-0.5 text-xs',
181
183
  lg: 'px-3 py-1 text-sm [&>svg]:size-3.5',
@@ -252,7 +254,7 @@ class BuiBadge {
252
254
  computedClass = computed(() => {
253
255
  const tone = this.tone();
254
256
  const toneOrVariant = tone === null ? brandClass(this.variant()) : TONES$2[tone][intensityFor(this.variant())];
255
- return cn(BASE$2, SIZES$6[this.size()], toneOrVariant, this.userClass());
257
+ return cn(BASE$1, SIZES$4[this.size()], toneOrVariant, this.userClass());
256
258
  }, /* @ts-ignore */
257
259
  ...(ngDevMode ? [{ debugName: "computedClass" }] : /* istanbul ignore next */ []));
258
260
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiBadge, deps: [], target: i0.ɵɵFactoryTarget.Directive });
@@ -558,16 +560,36 @@ const TEXTAREA_SIZES = {
558
560
  default: 'min-h-16 px-3 py-2 text-base md:text-sm',
559
561
  lg: 'min-h-20 px-4 py-2.5 text-base',
560
562
  };
561
- /** Applies BlatUI textarea styling to a native `<textarea>` (auto-grows via field-sizing). */
563
+ // Vertical padding (top + bottom) baked into each size preset, used to compute the `maxRows`
564
+ // height cap. Mirrors the `py-*` utilities in TEXTAREA_SIZES.
565
+ const TEXTAREA_PADDING_Y = {
566
+ sm: '0.75rem',
567
+ default: '1rem',
568
+ lg: '1.25rem',
569
+ };
570
+ /**
571
+ * Applies BlatUI textarea styling to a native `<textarea>`. Auto-grows with its content via CSS
572
+ * `field-sizing-content` (no JS). Set `[maxRows]` to cap the growth — past it the field scrolls.
573
+ */
562
574
  class BuiTextarea {
563
575
  /** Size preset controlling minimum height, padding and text size. */
564
576
  size = input('default', /* @ts-ignore */
565
577
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
578
+ /** Maximum number of rows before the textarea scrolls instead of growing (null = unlimited). */
579
+ maxRows = input(null, /* @ts-ignore */
580
+ ...(ngDevMode ? [{ debugName: "maxRows" }] : /* istanbul ignore next */ []));
566
581
  userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
567
582
  computedClass = computed(() => cn(TEXTAREA_BASE, TEXTAREA_SIZES[this.size()], this.userClass()), /* @ts-ignore */
568
583
  ...(ngDevMode ? [{ debugName: "computedClass" }] : /* istanbul ignore next */ []));
584
+ // Cap the auto-grow at `maxRows` lines using the `lh` unit (one line-height) plus the size's
585
+ // vertical padding and the 1px borders — purely declarative, so it stays SSR-safe.
586
+ maxHeight = computed(() => {
587
+ const rows = this.maxRows();
588
+ return rows === null ? null : `calc(${rows} * 1lh + ${TEXTAREA_PADDING_Y[this.size()]} + 2px)`;
589
+ }, /* @ts-ignore */
590
+ ...(ngDevMode ? [{ debugName: "maxHeight" }] : /* istanbul ignore next */ []));
569
591
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiTextarea, deps: [], target: i0.ɵɵFactoryTarget.Directive });
570
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: BuiTextarea, isStandalone: true, selector: "textarea[buiTextarea]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-slot": "textarea" }, properties: { "attr.data-size": "size()", "class": "computedClass()" } }, ngImport: i0 });
592
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: BuiTextarea, isStandalone: true, selector: "textarea[buiTextarea]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, maxRows: { classPropertyName: "maxRows", publicName: "maxRows", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-slot": "textarea" }, properties: { "attr.data-size": "size()", "class": "computedClass()", "style.max-height": "maxHeight()", "style.overflow-y": "maxRows() === null ? null : 'auto'" } }, ngImport: i0 });
571
593
  }
572
594
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiTextarea, decorators: [{
573
595
  type: Directive,
@@ -577,9 +599,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
577
599
  'data-slot': 'textarea',
578
600
  '[attr.data-size]': 'size()',
579
601
  '[class]': 'computedClass()',
602
+ '[style.max-height]': 'maxHeight()',
603
+ '[style.overflow-y]': "maxRows() === null ? null : 'auto'",
580
604
  },
581
605
  }]
582
- }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
606
+ }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], maxRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRows", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
583
607
 
584
608
  /**
585
609
  * BlatUI linear progress bar. Exposes the full `progressbar` ARIA contract
@@ -1071,7 +1095,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
1071
1095
  }]
1072
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 }] }] } });
1073
1097
 
1074
- 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';
1075
1099
  class BuiAccordion {
1076
1100
  userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
1077
1101
  computedClass = computed(() => cn('w-full', this.userClass()), /* @ts-ignore */
@@ -2720,7 +2744,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
2720
2744
  }]
2721
2745
  }], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
2722
2746
 
2723
- const SIZES$5 = {
2747
+ const SIZES$3 = {
2724
2748
  sm: 'max-w-3xl',
2725
2749
  md: 'max-w-5xl',
2726
2750
  lg: 'max-w-6xl',
@@ -2734,7 +2758,7 @@ class BuiContainer {
2734
2758
  size = input('lg', /* @ts-ignore */
2735
2759
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2736
2760
  userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
2737
- computedClass = computed(() => cn('mx-auto w-full px-4 sm:px-6 lg:px-8', SIZES$5[this.size()], this.userClass()), /* @ts-ignore */
2761
+ computedClass = computed(() => cn('mx-auto w-full px-4 sm:px-6 lg:px-8', SIZES$3[this.size()], this.userClass()), /* @ts-ignore */
2738
2762
  ...(ngDevMode ? [{ debugName: "computedClass" }] : /* istanbul ignore next */ []));
2739
2763
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiContainer, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2740
2764
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: BuiContainer, isStandalone: true, selector: "[buiContainer]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-slot": "container" }, properties: { "class": "computedClass()" } }, ngImport: i0 });
@@ -3477,7 +3501,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
3477
3501
  }]
3478
3502
  }], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
3479
3503
 
3480
- const SIZES$4 = {
3504
+ const SIZES$2 = {
3481
3505
  sm: { box: 'size-6', overlap: '-ms-2', ring: 'ring-1', text: 'text-xs' },
3482
3506
  default: { box: 'size-8', overlap: '-ms-2.5', ring: 'ring-2', text: 'text-sm' },
3483
3507
  lg: { box: 'size-12', overlap: '-ms-3', ring: 'ring-2', text: 'text-base' },
@@ -3512,11 +3536,11 @@ class BuiAvatarGroup {
3512
3536
  ...(ngDevMode ? [{ debugName: "computedClass" }] : /* istanbul ignore next */ []));
3513
3537
  toInitials = initials;
3514
3538
  itemClass(index) {
3515
- const size = SIZES$4[this.size()];
3539
+ const size = SIZES$2[this.size()];
3516
3540
  return cn(size.box, size.ring, 'ring-background', index > 0 && size.overlap);
3517
3541
  }
3518
3542
  moreClass() {
3519
- const size = SIZES$4[this.size()];
3543
+ const size = SIZES$2[this.size()];
3520
3544
  return cn('relative z-10 flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-foreground', size.box, size.ring, 'ring-background', size.text, this.shown().length > 0 && size.overlap);
3521
3545
  }
3522
3546
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiAvatarGroup, deps: [], target: i0.ɵɵFactoryTarget.Component });
@@ -3750,7 +3774,9 @@ class BuiPopover {
3750
3774
  .withPositions([POPOVER_POSITIONS[side], POPOVER_POSITIONS[POPOVER_FALLBACK[side]]]);
3751
3775
  const overlayReference = this.overlay.create({
3752
3776
  positionStrategy,
3753
- 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(),
3754
3780
  });
3755
3781
  overlayReference.attach(new TemplatePortal(this.content(), this.viewContainerRef));
3756
3782
  overlayReference.outsidePointerEvents().subscribe((event) => {
@@ -5024,7 +5050,13 @@ const BTN_SIZE$1 = {
5024
5050
  lg: 'w-9 [&_svg]:size-4',
5025
5051
  };
5026
5052
  const FIELD_WIDTH = { sm: 'w-9', default: 'w-10', lg: 'w-12' };
5027
- /** A compact − [n] + quantity stepper (`role="group"` with a `spinbutton` field). */
5053
+ /**
5054
+ * @deprecated Use `<bui-number-input [min]="1" size="sm">` instead — it is the same numeric
5055
+ * stepper, so this duplicate was dropped from the registry and docs. Still exported as a thin
5056
+ * compatibility shim; existing usages keep working.
5057
+ *
5058
+ * A compact − [n] + quantity stepper (`role="group"` with a `spinbutton` field).
5059
+ */
5028
5060
  class BuiQuantitySelector {
5029
5061
  /** Current quantity. Two-way bindable with `[(value)]`. */
5030
5062
  value = model(1, /* @ts-ignore */
@@ -5314,79 +5346,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
5314
5346
  }]
5315
5347
  }], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
5316
5348
 
5317
- const BASE$1 = 'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 flex w-full resize-none overflow-hidden rounded-md border bg-transparent shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50';
5318
- const SIZES$3 = {
5319
- sm: 'min-h-14 px-2.5 py-1.5 text-sm',
5320
- default: 'min-h-16 px-3 py-2 text-base md:text-sm',
5321
- lg: 'min-h-20 px-4 py-2.5 text-base',
5322
- };
5323
5349
  /**
5324
- * A textarea that grows with its content. SSR-safe the height is measured only in the
5325
- * browser (via `afterNextRender` and the `input` event).
5350
+ * @deprecated Use `<textarea buiTextarea [maxRows]="…">` instead`buiTextarea` already auto-grows
5351
+ * (CSS `field-sizing-content`) and now supports `maxRows`. Kept as a thin alias so existing
5352
+ * `buiAutosizeTextarea` markup keeps working; it inherits every input from {@link BuiTextarea}.
5353
+ *
5354
+ * A textarea that grows with its content, capped at `maxRows`.
5326
5355
  */
5327
- class BuiAutosizeTextarea {
5328
- /** Size preset controlling minimum height, padding and text size. */
5329
- size = input('default', /* @ts-ignore */
5330
- ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
5331
- /** Maximum number of rows before the textarea scrolls instead of growing (null = unlimited). */
5332
- maxRows = input(null, /* @ts-ignore */
5333
- ...(ngDevMode ? [{ debugName: "maxRows" }] : /* istanbul ignore next */ []));
5334
- userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
5335
- host = inject(ElementRef);
5336
- computedClass = computed(() => cn(BASE$1, SIZES$3[this.size()], this.userClass()), /* @ts-ignore */
5337
- ...(ngDevMode ? [{ debugName: "computedClass" }] : /* istanbul ignore next */ []));
5338
- constructor() {
5339
- afterNextRender(() => {
5340
- this.resize();
5341
- });
5342
- }
5343
- resize() {
5344
- const element = this.host.nativeElement;
5345
- element.style.height = 'auto';
5346
- let target = element.scrollHeight;
5347
- const maxRows = this.maxRows();
5348
- if (maxRows !== null) {
5349
- const styles = getComputedStyle(element);
5350
- let lineHeight = Number.parseFloat(styles.lineHeight);
5351
- if (Number.isNaN(lineHeight)) {
5352
- lineHeight = Number.parseFloat(styles.fontSize) * 1.2;
5353
- }
5354
- const padding = Number.parseFloat(styles.paddingTop) + Number.parseFloat(styles.paddingBottom);
5355
- const border = Number.parseFloat(styles.borderTopWidth) + Number.parseFloat(styles.borderBottomWidth);
5356
- const cap = lineHeight * maxRows + padding + border;
5357
- if (target > cap) {
5358
- target = cap;
5359
- element.style.overflowY = 'auto';
5360
- }
5361
- else {
5362
- element.style.overflowY = 'hidden';
5363
- }
5364
- }
5365
- element.style.height = `${target}px`;
5366
- }
5367
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiAutosizeTextarea, deps: [], target: i0.ɵɵFactoryTarget.Directive });
5368
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: BuiAutosizeTextarea, isStandalone: true, selector: "textarea[buiAutosizeTextarea]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, maxRows: { classPropertyName: "maxRows", publicName: "maxRows", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-slot": "autosize-textarea" }, listeners: { "input": "resize()" }, properties: { "attr.data-size": "size()", "class": "computedClass()" } }, ngImport: i0 });
5356
+ class BuiAutosizeTextarea extends BuiTextarea {
5357
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiAutosizeTextarea, deps: null, target: i0.ɵɵFactoryTarget.Directive });
5358
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: BuiAutosizeTextarea, isStandalone: true, selector: "textarea[buiAutosizeTextarea]", host: { attributes: { "data-slot": "autosize-textarea" } }, usesInheritance: true, ngImport: i0 });
5369
5359
  }
5370
5360
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiAutosizeTextarea, decorators: [{
5371
5361
  type: Directive,
5372
5362
  args: [{
5373
5363
  selector: 'textarea[buiAutosizeTextarea]',
5374
- host: {
5375
- 'data-slot': 'autosize-textarea',
5376
- '[attr.data-size]': 'size()',
5377
- '[class]': 'computedClass()',
5378
- '(input)': 'resize()',
5379
- },
5364
+ host: { 'data-slot': 'autosize-textarea' },
5380
5365
  }]
5381
- }], ctorParameters: () => [], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], maxRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRows", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
5366
+ }] });
5382
5367
 
5383
5368
  class BuiDropdownMenu {
5384
5369
  menu = inject(Menu);
5385
5370
  userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
5386
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 */
5387
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
+ }
5388
5380
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiDropdownMenu, deps: [], target: i0.ɵɵFactoryTarget.Directive });
5389
- 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 });
5390
5382
  }
5391
5383
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiDropdownMenu, decorators: [{
5392
5384
  type: Directive,
@@ -5396,6 +5388,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
5396
5388
  'data-slot': 'dropdown-menu',
5397
5389
  '[hidden]': '!menu.visible()',
5398
5390
  '[class]': 'computedClass()',
5391
+ '(window:scroll)': 'onScroll()',
5399
5392
  },
5400
5393
  }]
5401
5394
  }], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
@@ -5447,11 +5440,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
5447
5440
  * `ngMenuTrigger` opening a `ngMenu` (style the menus with `buiDropdownMenu` /
5448
5441
  * `buiDropdownMenuItem`). `MenuBar` is re-exported from `ng-blatui`.
5449
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
+ *
5450
5446
  * ```html
5451
5447
  * <div ngMenuBar buiMenubar>
5452
- * <button ngMenuTrigger [menu]="file" buiMenubarTrigger>File</button>
5453
- * <div ngMenu #file="ngMenu" buiDropdownMenu>
5454
- * <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>
5455
5453
  * </div>
5456
5454
  * </div>
5457
5455
  * ```
@@ -6857,11 +6855,24 @@ class BuiCombobox {
6857
6855
  [disabled]="disabled()"
6858
6856
  [readonly]="!searchable()"
6859
6857
  [class.cursor-pointer]="!searchable()"
6860
- 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"
6861
6859
  (input)="onInput($event)"
6862
6860
  (focus)="openList()"
6863
6861
  (keydown)="onKeydown($event)"
6864
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>
6865
6876
  @if (open() && filtered().length > 0) {
6866
6877
  <ul
6867
6878
  [id]="listId"
@@ -6941,11 +6952,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
6941
6952
  [disabled]="disabled()"
6942
6953
  [readonly]="!searchable()"
6943
6954
  [class.cursor-pointer]="!searchable()"
6944
- 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"
6945
6956
  (input)="onInput($event)"
6946
6957
  (focus)="openList()"
6947
6958
  (keydown)="onKeydown($event)"
6948
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>
6949
6973
  @if (open() && filtered().length > 0) {
6950
6974
  <ul
6951
6975
  [id]="listId"
@@ -7431,7 +7455,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
7431
7455
  }]
7432
7456
  }], propDecorators: { author: [{ type: i0.Input, args: [{ isSignal: true, alias: "author", required: false }] }], role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], avatar: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatar", required: false }] }], cite: [{ type: i0.Input, args: [{ isSignal: true, alias: "cite", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
7433
7457
 
7434
- const SIZES$2 = { sm: 'text-sm', default: 'text-base', lg: 'text-2xl' };
7458
+ const SIZES$1 = { sm: 'text-sm', default: 'text-base', lg: 'text-2xl' };
7435
7459
  const COMPARE_SIZES = { sm: 'text-xs', default: 'text-sm', lg: 'text-base' };
7436
7460
  /** A price display with optional compare-at (strikethrough) and a discount badge. */
7437
7461
  class BuiPrice {
@@ -7463,7 +7487,7 @@ class BuiPrice {
7463
7487
  : 0;
7464
7488
  }, /* @ts-ignore */
7465
7489
  ...(ngDevMode ? [{ debugName: "discount" }] : /* istanbul ignore next */ []));
7466
- currentClass = computed(() => cn(SIZES$2[this.size()], this.onSale() ? 'text-emerald-700 dark:text-emerald-400' : 'text-foreground'), /* @ts-ignore */
7490
+ currentClass = computed(() => cn(SIZES$1[this.size()], this.onSale() ? 'text-emerald-700 dark:text-emerald-400' : 'text-foreground'), /* @ts-ignore */
7467
7491
  ...(ngDevMode ? [{ debugName: "currentClass" }] : /* istanbul ignore next */ []));
7468
7492
  compareClass = computed(() => cn(COMPARE_SIZES[this.size()], 'text-muted-foreground'), /* @ts-ignore */
7469
7493
  ...(ngDevMode ? [{ debugName: "compareClass" }] : /* istanbul ignore next */ []));
@@ -7626,7 +7650,7 @@ const COLORS = {
7626
7650
  offline: 'bg-gray-400',
7627
7651
  };
7628
7652
  const LABELS = { online: 'Online', away: 'Away', busy: 'Busy', offline: 'Offline' };
7629
- const SIZES$1 = { sm: 'size-2', default: 'size-2.5', lg: 'size-3' };
7653
+ const SIZES = { sm: 'size-2', default: 'size-2.5', lg: 'size-3' };
7630
7654
  /** A small status dot (online/away/busy/offline). Status is never colour-only — the label is always present. */
7631
7655
  class BuiPresence {
7632
7656
  /** Presence state, which sets the dot color and default label. */
@@ -7647,7 +7671,7 @@ class BuiPresence {
7647
7671
  userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
7648
7672
  colorClass = computed(() => COLORS[this.status()], /* @ts-ignore */
7649
7673
  ...(ngDevMode ? [{ debugName: "colorClass" }] : /* istanbul ignore next */ []));
7650
- sizeClass = computed(() => SIZES$1[this.size()], /* @ts-ignore */
7674
+ sizeClass = computed(() => SIZES[this.size()], /* @ts-ignore */
7651
7675
  ...(ngDevMode ? [{ debugName: "sizeClass" }] : /* istanbul ignore next */ []));
7652
7676
  labelText = computed(() => this.label() || LABELS[this.status()], /* @ts-ignore */
7653
7677
  ...(ngDevMode ? [{ debugName: "labelText" }] : /* istanbul ignore next */ []));
@@ -8563,7 +8587,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
8563
8587
  }]
8564
8588
  }], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], area: [{ type: i0.Input, args: [{ isSignal: true, alias: "area", required: false }] }], strokeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "strokeWidth", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
8565
8589
 
8566
- const SIZES = { default: 'h-9 px-4 py-2', sm: 'h-8 gap-1.5 px-3', lg: 'h-10 px-6' };
8567
8590
  /** A stateful add-to-cart button: idle → adding → added → idle. Emits `triggered` on click. */
8568
8591
  class BuiAddToCart {
8569
8592
  /** Button text shown in the idle and adding states. */
@@ -8581,9 +8604,9 @@ class BuiAddToCart {
8581
8604
  state = signal('idle', /* @ts-ignore */
8582
8605
  ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
8583
8606
  timers = [];
8584
- btnClass = computed(() => cn('inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-all outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-90', SIZES[this.size()], this.state() === 'added'
8585
- ? 'bg-emerald-600 text-white'
8586
- : 'bg-primary text-primary-foreground hover:bg-primary/90', this.userClass()), /* @ts-ignore */
8607
+ // Composes the button's styling (single source of truth via `buttonVariants`); the busy/added
8608
+ // states only recolor on top — tailwind-merge drops the conflicting base background.
8609
+ btnClass = computed(() => cn(buttonVariants({ size: this.size() }), 'disabled:opacity-90', this.state() === 'added' ? 'bg-emerald-600 text-white hover:bg-emerald-600' : '', this.userClass()), /* @ts-ignore */
8587
8610
  ...(ngDevMode ? [{ debugName: "btnClass" }] : /* istanbul ignore next */ []));
8588
8611
  add() {
8589
8612
  if (this.state() !== 'idle') {
@@ -11303,6 +11326,12 @@ class BuiConfetti {
11303
11326
  /** Horizontal spread factor of the burst. */
11304
11327
  spread = input(70, /* @ts-ignore */
11305
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 */ []));
11306
11335
  /** Particle colours; defaults to the built-in palette. */
11307
11336
  colors = input(null, /* @ts-ignore */
11308
11337
  ...(ngDevMode ? [{ debugName: "colors" }] : /* istanbul ignore next */ []));
@@ -11313,18 +11342,25 @@ class BuiConfetti {
11313
11342
  seq = 0;
11314
11343
  computedClass = computed(() => cn('relative inline-block', this.userClass()), /* @ts-ignore */
11315
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 */ []));
11316
11351
  fire() {
11317
11352
  const colors = this.colors() ?? PALETTE$1;
11353
+ const isFullscreen = this.fullscreen();
11354
+ const direction = this.direction();
11318
11355
  const batch = Array.from({ length: this.count() }, () => {
11319
- const dx = (Math.random() - 0.5) * this.spread() * 4;
11320
- const dy = 80 + Math.random() * 120;
11321
- const rot = Math.random() * 720 - 360;
11356
+ const rot = (Math.random() * 720 - 360).toFixed(0);
11322
11357
  const color = colors[Math.floor(Math.random() * colors.length)];
11323
- const delay = (Math.random() * 0.1).toFixed(2);
11358
+ const delay = (Math.random() * 0.15).toFixed(2);
11324
11359
  this.seq += 1;
11360
+ const motion = isFullscreen ? this.rain() : this.burst(direction);
11325
11361
  return {
11326
11362
  id: this.seq,
11327
- 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`,
11328
11364
  };
11329
11365
  });
11330
11366
  this.particles.set(batch);
@@ -11333,7 +11369,49 @@ class BuiConfetti {
11333
11369
  }
11334
11370
  this.timer = setTimeout(() => {
11335
11371
  this.particles.set([]);
11336
- }, 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;`;
11337
11415
  }
11338
11416
  ngOnDestroy() {
11339
11417
  if (this.timer) {
@@ -11341,7 +11419,7 @@ class BuiConfetti {
11341
11419
  }
11342
11420
  }
11343
11421
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiConfetti, deps: [], target: i0.ɵɵFactoryTarget.Component });
11344
- 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: `
11345
11423
  <span (click)="fire()" (keydown.enter)="fire()" (keydown.space)="fire()">
11346
11424
  <ng-content>
11347
11425
  <button
@@ -11352,7 +11430,7 @@ class BuiConfetti {
11352
11430
  </button>
11353
11431
  </ng-content>
11354
11432
  </span>
11355
- <div class="pointer-events-none absolute top-1/2 left-1/2 size-0" aria-hidden="true">
11433
+ <div [class]="layerClass()" aria-hidden="true">
11356
11434
  @for (particle of particles(); track particle.id) {
11357
11435
  <span
11358
11436
  class="bui-confetti-particle absolute size-2 rounded-sm"
@@ -11375,7 +11453,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
11375
11453
  </button>
11376
11454
  </ng-content>
11377
11455
  </span>
11378
- <div class="pointer-events-none absolute top-1/2 left-1/2 size-0" aria-hidden="true">
11456
+ <div [class]="layerClass()" aria-hidden="true">
11379
11457
  @for (particle of particles(); track particle.id) {
11380
11458
  <span
11381
11459
  class="bui-confetti-particle absolute size-2 rounded-sm"
@@ -11384,7 +11462,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
11384
11462
  }
11385
11463
  </div>
11386
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"] }]
11387
- }], 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 }] }] } });
11388
11466
 
11389
11467
  /** A step indicator. The `value` (1-based) marks the active step. Use `li[buiStepperItem]`. */
11390
11468
  class BuiStepper {
@@ -12859,7 +12937,7 @@ class BuiAudioPlayer {
12859
12937
  ...(ngDevMode ? [{ debugName: "duration" }] : /* istanbul ignore next */ []));
12860
12938
  muted = signal(false, /* @ts-ignore */
12861
12939
  ...(ngDevMode ? [{ debugName: "muted" }] : /* istanbul ignore next */ []));
12862
- 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 */
12863
12941
  ...(ngDevMode ? [{ debugName: "computedClass" }] : /* istanbul ignore next */ []));
12864
12942
  toggle() {
12865
12943
  const element = this.a()?.nativeElement;
@@ -12950,7 +13028,7 @@ class BuiAudioPlayer {
12950
13028
  step="0.1"
12951
13029
  [max]="duration() || 0"
12952
13030
  [value]="current()"
12953
- class="h-1 flex-1 accent-primary"
13031
+ class="h-1 min-w-0 flex-1 accent-primary"
12954
13032
  [attr.aria-label]="seekText()"
12955
13033
  (input)="seek($event)"
12956
13034
  />
@@ -13022,7 +13100,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
13022
13100
  step="0.1"
13023
13101
  [max]="duration() || 0"
13024
13102
  [value]="current()"
13025
- class="h-1 flex-1 accent-primary"
13103
+ class="h-1 min-w-0 flex-1 accent-primary"
13026
13104
  [attr.aria-label]="seekText()"
13027
13105
  (input)="seek($event)"
13028
13106
  />
@@ -13686,12 +13764,20 @@ class BuiNotificationCenter {
13686
13764
  unreadText = buiLabel('notificationCenterUnread', this.unreadLabel);
13687
13765
  markAllReadText = buiLabel('notificationCenterMarkAllRead', this.markAllReadLabel);
13688
13766
  emptyText = buiLabel('notificationCenterEmpty', this.emptyLabel);
13767
+ host = inject(ElementRef);
13689
13768
  markedRead = signal(new Set(), /* @ts-ignore */
13690
13769
  ...(ngDevMode ? [{ debugName: "markedRead" }] : /* istanbul ignore next */ []));
13691
13770
  unread = computed(() => this.notifications().filter((_, index) => !this.isRead(index)).length, /* @ts-ignore */
13692
13771
  ...(ngDevMode ? [{ debugName: "unread" }] : /* istanbul ignore next */ []));
13693
13772
  computedClass = computed(() => cn('relative inline-block', this.userClass()), /* @ts-ignore */
13694
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
+ }
13695
13781
  isRead(index) {
13696
13782
  return this.markedRead().has(index) || (this.notifications()[index]?.read ?? false);
13697
13783
  }
@@ -13699,7 +13785,7 @@ class BuiNotificationCenter {
13699
13785
  this.markedRead.set(new Set(this.notifications().map((_, index) => index)));
13700
13786
  }
13701
13787
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiNotificationCenter, deps: [], target: i0.ɵɵFactoryTarget.Component });
13702
- 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: `
13703
13789
  <button
13704
13790
  type="button"
13705
13791
  class="relative inline-flex size-9 items-center justify-center rounded-md border border-input hover:bg-accent"
@@ -13774,7 +13860,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
13774
13860
  type: Component,
13775
13861
  args: [{
13776
13862
  selector: 'bui-notification-center',
13777
- 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
+ },
13778
13870
  template: `
13779
13871
  <button
13780
13872
  type="button"
@@ -14606,7 +14698,21 @@ class BuiTreeTable {
14606
14698
  /** Accessible label for the expand/collapse toggle button. */
14607
14699
  toggleLabel = input(/* @ts-ignore */
14608
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 */ []));
14609
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;
14610
14716
  overrides = signal(new Map(), /* @ts-ignore */
14611
14717
  ...(ngDevMode ? [{ debugName: "overrides" }] : /* istanbul ignore next */ []));
14612
14718
  defaults = computed(() => {
@@ -14651,6 +14757,47 @@ class BuiTreeTable {
14651
14757
  next.set(path, !this.isOpen(path));
14652
14758
  this.overrides.set(next);
14653
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
+ }
14654
14801
  alignClass(align) {
14655
14802
  return ALIGN$2[align ?? 'left'];
14656
14803
  }
@@ -14665,7 +14812,47 @@ class BuiTreeTable {
14665
14812
  return '';
14666
14813
  }
14667
14814
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiTreeTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
14668
- 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
+ }
14669
14856
  <table class="w-full text-sm">
14670
14857
  <thead>
14671
14858
  <tr class="border-b">
@@ -14728,6 +14915,46 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
14728
14915
  selector: 'bui-tree-table',
14729
14916
  host: { 'data-slot': 'tree-table', '[class]': 'computedClass()' },
14730
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
+ }
14731
14958
  <table class="w-full text-sm">
14732
14959
  <thead>
14733
14960
  <tr class="border-b">
@@ -14784,7 +15011,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
14784
15011
  </table>
14785
15012
  `,
14786
15013
  }]
14787
- }], 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 }] }] } });
14788
15015
 
14789
15016
  // HTML is escaped first, then a safe subset of markdown is applied, so user input can't inject markup.
14790
15017
  function renderMarkdown(markdown) {
@@ -16088,7 +16315,7 @@ class BuiContextMenu {
16088
16315
  this.open.set(false);
16089
16316
  }
16090
16317
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiContextMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
16091
- 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: `
16092
16319
  <ng-content />
16093
16320
  @if (open()) {
16094
16321
  <div
@@ -16201,6 +16428,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
16201
16428
  '(contextmenu)': 'onContext($event)',
16202
16429
  '(document:click)': 'open.set(false)',
16203
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)',
16204
16434
  },
16205
16435
  template: `
16206
16436
  <ng-content />
@@ -18345,36 +18575,38 @@ class BuiNavigationMenu {
18345
18575
  </svg>
18346
18576
  </button>
18347
18577
  @if (active() === i) {
18348
- <div
18349
- class="absolute start-0 top-full z-50 mt-1.5 w-64 rounded-lg border bg-popover p-2 shadow-md"
18350
- >
18351
- @if (item.featured; as featured) {
18352
- <a
18353
- [href]="href(featured.href)"
18354
- class="mb-1 block rounded-md bg-accent/50 p-3 hover:bg-accent"
18355
- >
18356
- <span class="text-sm font-semibold">{{ featured.label }}</span>
18357
- @if (featured.description) {
18358
- <span class="mt-0.5 block text-xs text-muted-foreground">{{
18359
- featured.description
18360
- }}</span>
18361
- }
18362
- </a>
18363
- }
18364
- <ul class="space-y-1">
18365
- @for (link of item.links; track link.label) {
18366
- <li>
18367
- <a [href]="href(link.href)" class="block rounded-md p-2 hover:bg-accent">
18368
- <span class="text-sm font-medium">{{ link.label }}</span>
18369
- @if (link.description) {
18370
- <span class="block text-xs text-muted-foreground">{{
18371
- link.description
18372
- }}</span>
18373
- }
18374
- </a>
18375
- </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>
18376
18594
  }
18377
- </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>
18378
18610
  </div>
18379
18611
  }
18380
18612
  } @else {
@@ -18425,36 +18657,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
18425
18657
  </svg>
18426
18658
  </button>
18427
18659
  @if (active() === i) {
18428
- <div
18429
- class="absolute start-0 top-full z-50 mt-1.5 w-64 rounded-lg border bg-popover p-2 shadow-md"
18430
- >
18431
- @if (item.featured; as featured) {
18432
- <a
18433
- [href]="href(featured.href)"
18434
- class="mb-1 block rounded-md bg-accent/50 p-3 hover:bg-accent"
18435
- >
18436
- <span class="text-sm font-semibold">{{ featured.label }}</span>
18437
- @if (featured.description) {
18438
- <span class="mt-0.5 block text-xs text-muted-foreground">{{
18439
- featured.description
18440
- }}</span>
18441
- }
18442
- </a>
18443
- }
18444
- <ul class="space-y-1">
18445
- @for (link of item.links; track link.label) {
18446
- <li>
18447
- <a [href]="href(link.href)" class="block rounded-md p-2 hover:bg-accent">
18448
- <span class="text-sm font-medium">{{ link.label }}</span>
18449
- @if (link.description) {
18450
- <span class="block text-xs text-muted-foreground">{{
18451
- link.description
18452
- }}</span>
18453
- }
18454
- </a>
18455
- </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>
18456
18676
  }
18457
- </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>
18458
18692
  </div>
18459
18693
  }
18460
18694
  } @else {
@@ -18556,11 +18790,11 @@ class BuiMentionInput {
18556
18790
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiMentionInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
18557
18791
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: BuiMentionInput, isStandalone: true, selector: "bui-mention-input", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, mentions: { classPropertyName: "mentions", publicName: "mentions", isSignal: true, isRequired: false, transformFunction: null }, trigger: { classPropertyName: "trigger", publicName: "trigger", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", 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": "mention-input" }, properties: { "class": "computedClass()" } }, ngImport: i0, template: `
18558
18792
  <textarea
18793
+ buiTextarea
18559
18794
  [value]="value()"
18560
18795
  [placeholder]="placeholder()"
18561
18796
  [rows]="rows()"
18562
18797
  [attr.name]="name() || null"
18563
- class="w-full resize-y rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
18564
18798
  (input)="onInput($event)"
18565
18799
  (keydown)="onKeydown($event)"
18566
18800
  ></textarea>
@@ -18583,20 +18817,21 @@ class BuiMentionInput {
18583
18817
  }
18584
18818
  </ul>
18585
18819
  }
18586
- `, isInline: true });
18820
+ `, isInline: true, dependencies: [{ kind: "directive", type: BuiTextarea, selector: "textarea[buiTextarea]", inputs: ["size", "maxRows", "class"] }] });
18587
18821
  }
18588
18822
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiMentionInput, decorators: [{
18589
18823
  type: Component,
18590
18824
  args: [{
18591
18825
  selector: 'bui-mention-input',
18592
18826
  host: { 'data-slot': 'mention-input', '[class]': 'computedClass()' },
18827
+ imports: [BuiTextarea],
18593
18828
  template: `
18594
18829
  <textarea
18830
+ buiTextarea
18595
18831
  [value]="value()"
18596
18832
  [placeholder]="placeholder()"
18597
18833
  [rows]="rows()"
18598
18834
  [attr.name]="name() || null"
18599
- class="w-full resize-y rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
18600
18835
  (input)="onInput($event)"
18601
18836
  (keydown)="onKeydown($event)"
18602
18837
  ></textarea>
@@ -19398,6 +19633,11 @@ class BuiOnboardingTour {
19398
19633
  prev() {
19399
19634
  this.step.set(Math.max(0, this.step() - 1));
19400
19635
  }
19636
+ onViewportChange() {
19637
+ if (this.open()) {
19638
+ this.measure();
19639
+ }
19640
+ }
19401
19641
  finish() {
19402
19642
  this.open.set(false);
19403
19643
  this.step.set(0);
@@ -19414,7 +19654,7 @@ class BuiOnboardingTour {
19414
19654
  }
19415
19655
  }
19416
19656
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: BuiOnboardingTour, deps: [], target: i0.ɵɵFactoryTarget.Component });
19417
- 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: `
19657
+ 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: `
19418
19658
  @if (open() && current()) {
19419
19659
  <div
19420
19660
  class="pointer-events-none fixed z-[60] rounded-md ring-2 ring-primary transition-all"
@@ -19467,7 +19707,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
19467
19707
  type: Component,
19468
19708
  args: [{
19469
19709
  selector: 'bui-onboarding-tour',
19470
- host: { 'data-slot': 'onboarding-tour', '(document:keydown.escape)': 'finish()' },
19710
+ host: {
19711
+ 'data-slot': 'onboarding-tour',
19712
+ '(document:keydown.escape)': 'finish()',
19713
+ // The spotlight/card use viewport (fixed) coordinates; re-measure on scroll & resize so the
19714
+ // highlight tracks its target instead of drifting out of alignment.
19715
+ '(window:scroll)': 'onViewportChange()',
19716
+ '(window:resize)': 'onViewportChange()',
19717
+ },
19471
19718
  template: `
19472
19719
  @if (open() && current()) {
19473
19720
  <div