@bitforgehq/ui 0.4.0 → 0.5.0-next.3

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.
Files changed (80) hide show
  1. package/CHANGELOG.md +303 -0
  2. package/README.md +12 -12
  3. package/ai/MANIFEST.index.json +65 -2
  4. package/ai/MANIFEST.json +2221 -737
  5. package/ai/REFERENCE.index.json +161 -0
  6. package/ai/REFERENCE.json +1336 -27
  7. package/ai/UI_GUIDE.md +61 -20
  8. package/ai/components/app-frame.md +1 -1
  9. package/ai/components/chat-composer.md +357 -0
  10. package/ai/components/chat-message.md +251 -0
  11. package/ai/components/chat-thread.md +521 -0
  12. package/ai/components/checkbox.md +7 -0
  13. package/ai/components/combobox.md +12 -0
  14. package/ai/components/date-input.md +9 -0
  15. package/ai/components/datetime-input.md +2 -0
  16. package/ai/components/field.md +10 -0
  17. package/ai/components/file-input.md +498 -0
  18. package/ai/components/header.md +1 -1
  19. package/ai/components/image-input.md +368 -0
  20. package/ai/components/image-viewer.md +309 -0
  21. package/ai/components/media-card.md +478 -0
  22. package/ai/components/menu-button.md +2 -0
  23. package/ai/components/menu.md +10 -0
  24. package/ai/components/multi-combobox.md +7 -0
  25. package/ai/components/nav-group.md +10 -3
  26. package/ai/components/nav-item.md +11 -2
  27. package/ai/components/number-input.md +2 -7
  28. package/ai/components/popover.md +13 -0
  29. package/ai/components/radio-group.md +6 -0
  30. package/ai/components/segmented.md +14 -2
  31. package/ai/components/select.md +100 -42
  32. package/ai/components/sidebar-toggle.md +16 -9
  33. package/ai/components/sidebar.md +41 -13
  34. package/ai/components/tab-group.md +18 -2
  35. package/ai/components/table.md +17 -2
  36. package/ai/components/theme-menu-button.md +1 -0
  37. package/ai/components/time-input.md +2 -0
  38. package/ai/components/toolbar.md +7 -18
  39. package/ai/reference/bf-chat-composer-harness.md +64 -0
  40. package/ai/reference/bf-chat-message-role.md +27 -0
  41. package/ai/reference/bf-chat-message-status.md +28 -0
  42. package/ai/reference/bf-fade-in-directive.md +18 -0
  43. package/ai/reference/bf-file-input-harness.md +67 -0
  44. package/ai/reference/bf-file-input-variant.md +27 -0
  45. package/ai/reference/bf-image-input-harness.md +67 -0
  46. package/ai/reference/bf-image-input-shape.md +27 -0
  47. package/ai/reference/bf-image-viewer-actions-context.md +29 -0
  48. package/ai/reference/bf-image-viewer-harness.md +87 -0
  49. package/ai/reference/bf-image-viewer-image.md +25 -0
  50. package/ai/reference/bf-image-viewer-options.md +33 -0
  51. package/ai/reference/bf-image-viewer-origin.md +23 -0
  52. package/ai/reference/bf-image-viewer-ref.md +55 -0
  53. package/ai/reference/bf-image-viewer-service.md +32 -0
  54. package/ai/reference/bf-labels.md +105 -75
  55. package/ai/reference/bf-media-card-aspect.md +28 -0
  56. package/ai/reference/bf-media-card-harness.md +66 -0
  57. package/ai/reference/bf-motion-list-directive.md +22 -0
  58. package/ai/reference/bf-prose-directive.md +16 -0
  59. package/ai/reference/bf-rejected-file-reason.md +28 -0
  60. package/ai/reference/bf-rejected-file.md +28 -0
  61. package/ai/reference/bf-select-harness.md +14 -5
  62. package/ai/reference/bf-select-option.md +2 -0
  63. package/ai/reference/bf-size.md +3 -0
  64. package/ai/reference/bf_table_sort_select.md +23 -0
  65. package/ai/reference/provide-bf-labels.md +10 -3
  66. package/ai/reference/provide-bf-select.md +20 -0
  67. package/bin/bf-ui.js +1 -0
  68. package/fesm2022/bitforgehq-ui-overlay.mjs +2649 -606
  69. package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
  70. package/fesm2022/bitforgehq-ui-testing.mjs +356 -10
  71. package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
  72. package/fesm2022/bitforgehq-ui.mjs +7531 -3989
  73. package/fesm2022/bitforgehq-ui.mjs.map +1 -1
  74. package/llms-full.txt +4151 -130
  75. package/llms.txt +31 -1
  76. package/package.json +1 -1
  77. package/theme.css +60 -0
  78. package/types/bitforgehq-ui-overlay.d.ts +684 -7
  79. package/types/bitforgehq-ui-testing.d.ts +434 -6
  80. package/types/bitforgehq-ui.d.ts +1473 -182
@@ -1,12 +1,12 @@
1
1
  import * as i0 from "@angular/core";
2
- import { Component, DOCUMENT, DestroyRef, Directive, ElementRef, Injectable, InjectionToken, Injector, SimpleChange, TemplateRef, ViewContainerRef, afterNextRender, afterRenderEffect, booleanAttribute, computed, effect, inject, input, inputBinding, linkedSignal, makeEnvironmentProviders, model, numberAttribute, output, signal, viewChild } from "@angular/core";
2
+ import { Component, DOCUMENT, DestroyRef, Directive, ElementRef, EnvironmentInjector, Injectable, InjectionToken, Injector, SimpleChange, TemplateRef, ViewContainerRef, afterNextRender, afterRenderEffect, booleanAttribute, computed, effect, inject, input, inputBinding, linkedSignal, makeEnvironmentProviders, model, numberAttribute, output, signal, viewChild } from "@angular/core";
3
3
  import { FocusTrapFactory, LiveAnnouncer } from "@angular/cdk/a11y";
4
- import { Overlay, OverlayConfig } from "@angular/cdk/overlay";
4
+ import { OverlayConfig, createBlockScrollStrategy, createFlexibleConnectedPositionStrategy, createGlobalPositionStrategy, createNoopScrollStrategy, createOverlayRef, createRepositionScrollStrategy } from "@angular/cdk/overlay";
5
5
  import { ComponentPortal, TemplatePortal } from "@angular/cdk/portal";
6
6
  import { Location, NgTemplateOutlet } from "@angular/common";
7
- import { BF_DEFAULTS, BF_LABELS, BfButtonComponent, BfCalendarComponent, BfFieldComponent, BfIconButtonComponent, BfSpinnerComponent, bfFieldControlClass, ɵBF_BUTTON_CLASSES, ɵBF_ICON_BUTTON_CLASSES, ɵBF_NAV, ɵFIELD_BOX, ɵFIELD_PADDING_END, ɵFIELD_PADDING_START, ɵPILL_HIT, ɵdaysInMonth, ɵfieldDescribedBy, ɵfieldErrorMessageId, ɵfieldErrorText, ɵfieldShowsError, ɵformatIsoDate, ɵlocalIsoDate, ɵparseIsoDate, ɵuniqueId } from "@bitforgehq/ui";
7
+ import { BF_DEFAULTS, BF_LABELS, BF_TABLE_SORT_SELECT, BfButtonComponent, BfCalendarComponent, BfFadeInDirective, BfFieldComponent, BfIconButtonComponent, BfSpinnerComponent, BfTextareaComponent, bfFieldControlClass, ɵBF_BUTTON_CLASSES, ɵBF_ICON_BUTTON_CLASSES, ɵBF_NAV, ɵBfNativeSelectComponent, ɵFIELD_BOX, ɵFIELD_PADDING_END, ɵFIELD_PADDING_START, ɵPILL_HIT, ɵchoiceIndex, ɵchoiceKey, ɵdaysInMonth, ɵfieldDescribedBy, ɵfieldErrorMessageId, ɵfieldErrorText, ɵfieldShowsError, ɵformatIsoDate, ɵlocalIsoDate, ɵparseIsoDate, ɵuniqueId } from "@bitforgehq/ui";
8
8
  import { BfIconComponent } from "@bitforgehq/ui/icons";
9
- import { X } from "lucide";
9
+ import { ChevronLeft, ChevronRight, X } from "lucide";
10
10
  import { Directionality } from "@angular/cdk/bidi";
11
11
  import * as i1 from "@angular/cdk/menu";
12
12
  import { CDK_MENU, CdkMenu, CdkMenuGroup, CdkMenuItem, CdkMenuItemCheckbox, CdkMenuItemRadio, CdkMenuTrigger, CdkTargetMenuAim } from "@angular/cdk/menu";
@@ -437,7 +437,6 @@ function fromOtherPane(event, pane) {
437
437
  return false;
438
438
  }
439
439
  var BfOverlayService = class BfOverlayService {
440
- overlay = inject(Overlay);
441
440
  injector = inject(Injector);
442
441
  document = inject(DOCUMENT);
443
442
  stack = inject(BfOverlayStack);
@@ -452,9 +451,9 @@ var BfOverlayService = class BfOverlayService {
452
451
  const previouslyFocused = this.document.activeElement;
453
452
  const hasBackdrop = modal && (options.backdrop ?? true);
454
453
  const position = options.position ?? "center";
455
- const overlayRef = this.overlay.create(new OverlayConfig({
454
+ const overlayRef = createOverlayRef(this.injector, new OverlayConfig({
456
455
  positionStrategy: this.positionStrategy(position),
457
- scrollStrategy: !modal || !this.documentScrolls ? this.overlay.scrollStrategies.noop() : this.overlay.scrollStrategies.block(),
456
+ scrollStrategy: !modal || !this.documentScrolls ? createNoopScrollStrategy() : createBlockScrollStrategy(this.injector),
458
457
  hasBackdrop,
459
458
  backdropClass: options.backdropClass ?? "bf-overlay-backdrop",
460
459
  panelClass: ["bf-overlay-panel", ...options.panelClass ?? []],
@@ -600,7 +599,7 @@ var BfOverlayService = class BfOverlayService {
600
599
  if (options.ariaDescribedBy) panel.setAttribute("aria-describedby", options.ariaDescribedBy);
601
600
  }
602
601
  positionStrategy(position) {
603
- const global = this.overlay.position().global();
602
+ const global = createGlobalPositionStrategy(this.injector);
604
603
  switch (position) {
605
604
  case "bottom": return global.centerHorizontally().bottom("0");
606
605
  case "top": return global.centerHorizontally().top("0");
@@ -1691,7 +1690,6 @@ const INSET = {
1691
1690
  endRtl: "calc(24px + var(--spacing-safe-left, 0px))"
1692
1691
  };
1693
1692
  var BfDesktopToastService = class BfDesktopToastService {
1694
- overlay = inject(Overlay);
1695
1693
  injector = inject(Injector);
1696
1694
  document = inject(DOCUMENT);
1697
1695
  config = inject(BF_DESKTOP_TOAST_CONFIG);
@@ -1761,9 +1759,9 @@ var BfDesktopToastService = class BfDesktopToastService {
1761
1759
  }
1762
1760
  ensurePane(injector) {
1763
1761
  if (this.pane) return this.pane;
1764
- const overlayRef = this.overlay.create({
1762
+ const overlayRef = createOverlayRef(this.injector, {
1765
1763
  positionStrategy: this.positionStrategy(),
1766
- scrollStrategy: this.overlay.scrollStrategies.noop(),
1764
+ scrollStrategy: createNoopScrollStrategy(),
1767
1765
  hasBackdrop: false,
1768
1766
  panelClass: "bf-desktop-toast-pane"
1769
1767
  });
@@ -1817,7 +1815,7 @@ var BfDesktopToastService = class BfDesktopToastService {
1817
1815
  return this.config.position.startsWith("top");
1818
1816
  }
1819
1817
  positionStrategy() {
1820
- const global = this.overlay.position().global();
1818
+ const global = createGlobalPositionStrategy(this.injector);
1821
1819
  const vertical = this.anchoredTop() ? global.top(INSET.top) : global.bottom(INSET.bottom);
1822
1820
  return this.config.position.endsWith("center") ? vertical.centerHorizontally() : vertical.end(this.directionality.value === "rtl" ? INSET.endRtl : INSET.endLtr);
1823
1821
  }
@@ -1848,6 +1846,96 @@ i0.ɵɵngDeclareClassMetadata({
1848
1846
  }],
1849
1847
  ctorParameters: () => []
1850
1848
  });
1849
+ const ANCHORED_ENTER_EASING = "cubic-bezier(0.16, 1, 0.3, 1)";
1850
+ const ANCHORED_EXIT_EASING = "cubic-bezier(0.4, 0, 1, 1)";
1851
+ const ANCHORED_SCALE = .96;
1852
+ const OPEN = {
1853
+ opacity: 1,
1854
+ transform: "none"
1855
+ };
1856
+ function hiddenFrame(panel, anchor) {
1857
+ if (prefersReducedMotion(panel.ownerDocument.defaultView)) return { opacity: 0 };
1858
+ const p = panel.getBoundingClientRect();
1859
+ const a = anchor.getBoundingClientRect();
1860
+ let x = 0;
1861
+ let y = 0;
1862
+ let originX = null;
1863
+ let originY = "top";
1864
+ if (p.top >= a.bottom - 1) y = -4;
1865
+ else if (p.bottom <= a.top + 1) {
1866
+ y = 4;
1867
+ originY = "bottom";
1868
+ } else if (p.left >= a.right - 1) {
1869
+ x = -4;
1870
+ originX = "left";
1871
+ } else {
1872
+ x = 4;
1873
+ originX = "right";
1874
+ }
1875
+ originX ??= Math.abs(p.left - a.left) <= Math.abs(p.right - a.right) ? "left" : "right";
1876
+ panel.style.transformOrigin = `${originX} ${originY}`;
1877
+ return {
1878
+ opacity: 0,
1879
+ transform: `translate3d(${x}px, ${y}px, 0) scale(${ANCHORED_SCALE})`
1880
+ };
1881
+ }
1882
+ function animateAnchoredIn(panel, anchor) {
1883
+ return animateOrSettle(panel, [hiddenFrame(panel, anchor), OPEN], {
1884
+ duration: 180,
1885
+ easing: ANCHORED_ENTER_EASING,
1886
+ fill: "backwards"
1887
+ });
1888
+ }
1889
+ function animateAnchoredOut(panel, anchor) {
1890
+ return animateOrSettle(panel, [hiddenFrame(panel, anchor)], {
1891
+ duration: 130,
1892
+ easing: ANCHORED_EXIT_EASING,
1893
+ fill: "forwards"
1894
+ });
1895
+ }
1896
+ var AnchoredMotion = class {
1897
+ leaving = null;
1898
+ enter(ref, anchor, view) {
1899
+ view?.detectChanges();
1900
+ ref.updatePosition();
1901
+ animateAnchoredIn(ref.overlayElement, anchor);
1902
+ }
1903
+ leave(ref, anchor, freeze) {
1904
+ this.flush();
1905
+ const pane = ref.overlayElement;
1906
+ pane.setAttribute("inert", "");
1907
+ pane.style.pointerEvents = "none";
1908
+ freeze?.detach();
1909
+ const animation = animateAnchoredOut(pane, anchor);
1910
+ if (animation.playState === "finished" || animation.effect === null) {
1911
+ ref.dispose();
1912
+ return;
1913
+ }
1914
+ const entry = {
1915
+ ref,
1916
+ animation
1917
+ };
1918
+ this.leaving = entry;
1919
+ const done = () => {
1920
+ if (this.leaving === entry) {
1921
+ this.leaving = null;
1922
+ ref.dispose();
1923
+ }
1924
+ };
1925
+ animation.finished.then(done, done);
1926
+ }
1927
+ flush() {
1928
+ const entry = this.leaving;
1929
+ if (entry) {
1930
+ this.leaving = null;
1931
+ entry.animation.cancel();
1932
+ entry.ref.dispose();
1933
+ }
1934
+ }
1935
+ isLeaving() {
1936
+ return this.leaving !== null;
1937
+ }
1938
+ };
1851
1939
  const BELOW_START = {
1852
1940
  originX: "start",
1853
1941
  originY: "bottom",
@@ -1965,11 +2053,11 @@ function sidePositions(placement, direction = "ltr") {
1965
2053
  offsetX: -position.offsetX
1966
2054
  } : position);
1967
2055
  }
1968
- function createAnchoredOverlay(overlay, anchor, options) {
1969
- const strategy = overlay.position().flexibleConnectedTo(anchor).withPositions(options.positions).withFlexibleDimensions(false).withPush(false).withViewportMargin(8);
1970
- const ref = overlay.create({
2056
+ function createAnchoredOverlay(injector, anchor, options) {
2057
+ const strategy = createFlexibleConnectedPositionStrategy(injector, anchor).withPositions(options.positions).withFlexibleDimensions(false).withPush(false).withViewportMargin(8);
2058
+ const ref = createOverlayRef(injector, {
1971
2059
  positionStrategy: strategy,
1972
- scrollStrategy: overlay.scrollStrategies.reposition(),
2060
+ scrollStrategy: createRepositionScrollStrategy(injector),
1973
2061
  direction: options.direction,
1974
2062
  hasBackdrop: false,
1975
2063
  panelClass: options.panelClass
@@ -2014,6 +2102,9 @@ var BfMenuTriggerDirective = class BfMenuTriggerDirective {
2014
2102
  previous = positions;
2015
2103
  });
2016
2104
  }
2105
+ anchorElement() {
2106
+ return this.el;
2107
+ }
2017
2108
  labelId() {
2018
2109
  if (!this.el.id) this.el.id = ɵuniqueId("bf-menu-trigger");
2019
2110
  return this.el.id;
@@ -2092,6 +2183,12 @@ var BfMenuComponent = class BfMenuComponent {
2092
2183
  resolvedSize = computed(() => this.size() ?? this.defaults.controlSize, ...ngDevMode ? [{ debugName: "resolvedSize" }] : /* istanbul ignore next */ []);
2093
2184
  triggerId = this.trigger?.labelId() ?? null;
2094
2185
  labelledBy = computed(() => this.ariaLabelledby() || (this.ariaLabel() ? null : this.triggerId), ...ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []);
2186
+ constructor() {
2187
+ const trigger = this.trigger;
2188
+ if (!trigger) return;
2189
+ const host = inject(ElementRef).nativeElement;
2190
+ afterNextRender({ read: () => animateAnchoredIn(host, trigger.anchorElement()) });
2191
+ }
2095
2192
  static ɵfac = i0.ɵɵngDeclareFactory({
2096
2193
  minVersion: "12.0.0",
2097
2194
  version: "22.1.7",
@@ -2160,6 +2257,7 @@ i0.ɵɵngDeclareClassMetadata({
2160
2257
  template: "<ng-content />"
2161
2258
  }]
2162
2259
  }],
2260
+ ctorParameters: () => [],
2163
2261
  propDecorators: {
2164
2262
  size: [{
2165
2263
  type: i0.Input,
@@ -2508,7 +2606,7 @@ i0.ɵɵngDeclareClassMetadata({
2508
2606
  }]
2509
2607
  }]
2510
2608
  });
2511
- const BUTTON = ɵBF_BUTTON_CLASSES;
2609
+ const BUTTON$1 = ɵBF_BUTTON_CLASSES;
2512
2610
  const ICON$1 = ɵBF_ICON_BUTTON_CLASSES;
2513
2611
  const CARET_PADDING = {
2514
2612
  sm: "ps-3.5 pe-3",
@@ -2548,14 +2646,14 @@ var BfMenuButtonComponent = class BfMenuButtonComponent {
2548
2646
  hit
2549
2647
  ].filter(Boolean).join(" ");
2550
2648
  }
2551
- const padding = variant === "text" ? BUTTON.textPadding[size] : CARET_PADDING[size];
2552
- const pressed = this.disabled() ? "" : BUTTON.pressed[variant];
2649
+ const padding = variant === "text" ? BUTTON$1.textPadding[size] : CARET_PADDING[size];
2650
+ const pressed = this.disabled() ? "" : BUTTON$1.pressed[variant];
2553
2651
  return [
2554
- BUTTON.base,
2652
+ BUTTON$1.base,
2555
2653
  "cursor-pointer",
2556
- BUTTON.variant[variant],
2654
+ BUTTON$1.variant[variant],
2557
2655
  pressed,
2558
- BUTTON.size[size],
2656
+ BUTTON$1.size[size],
2559
2657
  padding,
2560
2658
  hit
2561
2659
  ].filter(Boolean).join(" ");
@@ -3089,7 +3187,7 @@ const SHOW_DELAY = 500;
3089
3187
  const HIDE_GRACE = 100;
3090
3188
  const FADE = 120;
3091
3189
  var BfTooltipDirective = class BfTooltipDirective {
3092
- overlay = inject(Overlay);
3190
+ environment = inject(EnvironmentInjector);
3093
3191
  document = inject(DOCUMENT);
3094
3192
  injector = inject(Injector);
3095
3193
  dir = inject(Directionality, { optional: true });
@@ -3112,7 +3210,7 @@ var BfTooltipDirective = class BfTooltipDirective {
3112
3210
  this.clearTimers();
3113
3211
  if (this.anchored || !this.bfTooltip() || this.coarsePointer()) return;
3114
3212
  const direction = this.dir?.value ?? "ltr";
3115
- const anchored = createAnchoredOverlay(this.overlay, this.host, {
3213
+ const anchored = createAnchoredOverlay(this.environment, this.host, {
3116
3214
  positions: sidePositions(this.bfTooltipPlacement(), direction),
3117
3215
  direction,
3118
3216
  panelClass: ["bf-tooltip-panel"]
@@ -3408,175 +3506,301 @@ i0.ɵɵngDeclareClassMetadata({
3408
3506
  }]
3409
3507
  }] }
3410
3508
  });
3411
- const BF_SIDE_PANEL_DATA = new InjectionToken("BF_SIDE_PANEL_DATA");
3412
- const BF_SIDE_PANEL_CONTEXT = new InjectionToken("BF_SIDE_PANEL_CONTEXT");
3413
- var BfSidePanelRef = class {
3414
- overlayRef;
3415
- instance;
3416
- id;
3417
- afterClosed;
3418
- closed;
3419
- constructor(overlayRef, instance = () => null) {
3420
- this.overlayRef = overlayRef;
3421
- this.instance = instance;
3422
- this.id = overlayRef.id;
3423
- this.afterClosed = overlayRef.afterClosed;
3424
- this.closed = overlayRef.closed;
3425
- }
3426
- get componentInstance() {
3427
- return this.instance();
3428
- }
3429
- close(result) {
3430
- this.overlayRef.close(result);
3431
- }
3509
+ const BUTTON = ɵBF_BUTTON_CLASSES;
3510
+ const VARIANT = {
3511
+ send: "primary",
3512
+ stop: "tonal"
3432
3513
  };
3433
- const HOST_BASE = "relative flex h-full flex-col border-s border-border bg-surface text-text shadow-floating";
3434
- const WIDTH = {
3435
- sm: "w-[min(22.5rem,100vw)]",
3436
- md: "w-[min(30rem,100vw)]",
3437
- lg: "w-[min(40rem,100vw)]"
3514
+ const SQUARE = {
3515
+ sm: "has-[[data-icon]>*]:w-8 has-[[data-icon]>*]:px-0",
3516
+ md: "has-[[data-icon]>*]:w-10 has-[[data-icon]>*]:px-0",
3517
+ lg: "has-[[data-icon]>*]:w-12 has-[[data-icon]>*]:px-0"
3438
3518
  };
3439
- var BfSidePanelComponent = class BfSidePanelComponent {
3519
+ const FIELD_HEIGHT = {
3520
+ sm: "[&_textarea]:min-h-8",
3521
+ md: "[&_textarea]:min-h-10",
3522
+ lg: "[&_textarea]:min-h-12 [&_textarea]:py-[11px]"
3523
+ };
3524
+ const BLOCKED = "cursor-not-allowed bg-surface-sunken text-text-disabled";
3525
+ var BfChatComposerComponent = class BfChatComposerComponent {
3526
+ defaults = inject(BF_DEFAULTS);
3440
3527
  labels = inject(BF_LABELS);
3441
- context = inject(BF_SIDE_PANEL_CONTEXT, { optional: true });
3442
- ref = inject(BfSidePanelRef, { optional: true });
3443
- title = input("", ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
3444
- closeLabel = input("", ...ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []);
3445
- overflowing = signal(false, ...ngDevMode ? [{ debugName: "overflowing" }] : /* istanbul ignore next */ []);
3446
- titleId = this.context?.titleId ?? ɵuniqueId("bf-side-panel-title");
3447
- closeIcon = X;
3448
- width = this.context?.width ?? "md";
3449
- hostClasses = computed(() => `${HOST_BASE} ${WIDTH[this.width]}`, ...ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []);
3450
- closeName = computed(() => this.closeLabel() || this.labels.close, ...ngDevMode ? [{ debugName: "closeName" }] : /* istanbul ignore next */ []);
3451
- body = viewChild.required("body", ...ngDevMode ? [{ debugName: "body" }] : /* istanbul ignore next */ []);
3452
- content = viewChild.required("content", ...ngDevMode ? [{ debugName: "content" }] : /* istanbul ignore next */ []);
3453
- constructor() {
3454
- let observer = null;
3455
- afterNextRender(() => {
3456
- const body = this.body().nativeElement;
3457
- const measure = () => this.overflowing.set(body.scrollHeight > body.clientHeight);
3458
- measure();
3459
- if (typeof ResizeObserver === "function") {
3460
- observer = new ResizeObserver(measure);
3461
- observer.observe(body);
3462
- observer.observe(this.content().nativeElement);
3463
- }
3464
- });
3465
- inject(DestroyRef).onDestroy(() => observer?.disconnect());
3528
+ value = model("", ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
3529
+ running = input(false, {
3530
+ ...ngDevMode ? { debugName: "running" } : /* istanbul ignore next */ {},
3531
+ transform: booleanAttribute
3532
+ });
3533
+ disabled = input(false, {
3534
+ ...ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {},
3535
+ transform: booleanAttribute
3536
+ });
3537
+ disabledReason = input("", ...ngDevMode ? [{ debugName: "disabledReason" }] : /* istanbul ignore next */ []);
3538
+ label = input("", ...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
3539
+ ariaLabel = input("", ...ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []);
3540
+ ariaLabelledby = input("", ...ngDevMode ? [{ debugName: "ariaLabelledby" }] : /* istanbul ignore next */ []);
3541
+ placeholder = input("", ...ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []);
3542
+ maxRows = input(8, ...ngDevMode ? [{ debugName: "maxRows" }] : /* istanbul ignore next */ []);
3543
+ maxLength = input(void 0, ...ngDevMode ? [{ debugName: "maxLength" }] : /* istanbul ignore next */ []);
3544
+ name = input("", ...ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []);
3545
+ size = input(void 0, ...ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []);
3546
+ changed = output();
3547
+ sent = output();
3548
+ stopped = output();
3549
+ field = viewChild.required(BfTextareaComponent, ...ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []);
3550
+ resolvedSize = computed(() => this.size() ?? this.defaults.fieldSize, ...ngDevMode ? [{ debugName: "resolvedSize" }] : /* istanbul ignore next */ []);
3551
+ fieldClasses = computed(() => `min-w-0 flex-1 ${FIELD_HEIGHT[this.resolvedSize()]}`, ...ngDevMode ? [{ debugName: "fieldClasses" }] : /* istanbul ignore next */ []);
3552
+ action = computed(() => this.running() ? "stop" : "send", ...ngDevMode ? [{ debugName: "action" }] : /* istanbul ignore next */ []);
3553
+ blocked = computed(() => this.disabled() && this.disabledReason() !== "", ...ngDevMode ? [{ debugName: "blocked" }] : /* istanbul ignore next */ []);
3554
+ nativeDisabled = computed(() => !this.blocked() && (this.disabled() || this.action() === "send" && this.value().trim() === ""), ...ngDevMode ? [{ debugName: "nativeDisabled" }] : /* istanbul ignore next */ []);
3555
+ buttonName = computed(() => this.action() === "stop" ? this.labels.chatStop : this.labels.chatSend, ...ngDevMode ? [{ debugName: "buttonName" }] : /* istanbul ignore next */ []);
3556
+ fieldName = computed(() => this.ariaLabel() || (this.ariaLabelledby() ? "" : this.labels.chatComposer), ...ngDevMode ? [{ debugName: "fieldName" }] : /* istanbul ignore next */ []);
3557
+ fieldPlaceholder = computed(() => this.disabled() && this.disabledReason() ? this.disabledReason() : this.placeholder(), ...ngDevMode ? [{ debugName: "fieldPlaceholder" }] : /* istanbul ignore next */ []);
3558
+ buttonClasses = computed(() => {
3559
+ const size = this.resolvedSize();
3560
+ const variant = VARIANT[this.action()];
3561
+ return [
3562
+ BUTTON.base,
3563
+ this.blocked() ? BLOCKED : `${BUTTON.variant[variant]} ${BUTTON.pressed[variant]} cursor-pointer`,
3564
+ BUTTON.size[size],
3565
+ BUTTON.padding[size],
3566
+ SQUARE[size],
3567
+ ɵPILL_HIT[size].plain
3568
+ ].join(" ");
3569
+ }, ...ngDevMode ? [{ debugName: "buttonClasses" }] : /* istanbul ignore next */ []);
3570
+ send() {
3571
+ const text = this.value().trim();
3572
+ if (this.disabled() || this.running() || text === "") return;
3573
+ this.sent.emit(text);
3574
+ this.value.set("");
3575
+ this.field().focus();
3576
+ }
3577
+ focus() {
3578
+ this.field().focus();
3579
+ }
3580
+ onChanged(text) {
3581
+ this.value.set(text);
3582
+ this.changed.emit(text);
3583
+ }
3584
+ onButton() {
3585
+ if (this.blocked()) return;
3586
+ if (this.action() === "stop") {
3587
+ this.stopped.emit();
3588
+ return;
3589
+ }
3590
+ this.send();
3466
3591
  }
3467
- close() {
3468
- this.ref?.close();
3592
+ onKeydown(event) {
3593
+ const composing = event.isComposing || event.keyCode === 229;
3594
+ if (event.key !== "Enter" || event.shiftKey || composing) return;
3595
+ event.preventDefault();
3596
+ this.send();
3469
3597
  }
3470
3598
  static ɵfac = i0.ɵɵngDeclareFactory({
3471
3599
  minVersion: "12.0.0",
3472
3600
  version: "22.1.7",
3473
3601
  ngImport: i0,
3474
- type: BfSidePanelComponent,
3602
+ type: BfChatComposerComponent,
3475
3603
  deps: [],
3476
3604
  target: i0.ɵɵFactoryTarget.Component
3477
3605
  });
3478
3606
  static ɵcmp = i0.ɵɵngDeclareComponent({
3479
- minVersion: "17.0.0",
3607
+ minVersion: "17.2.0",
3480
3608
  version: "22.1.7",
3481
- type: BfSidePanelComponent,
3609
+ type: BfChatComposerComponent,
3482
3610
  isStandalone: true,
3483
- selector: "bf-side-panel",
3611
+ selector: "bf-chat-composer",
3484
3612
  inputs: {
3485
- title: {
3486
- classPropertyName: "title",
3487
- publicName: "title",
3613
+ value: {
3614
+ classPropertyName: "value",
3615
+ publicName: "value",
3488
3616
  isSignal: true,
3489
3617
  isRequired: false,
3490
3618
  transformFunction: null
3491
3619
  },
3492
- closeLabel: {
3493
- classPropertyName: "closeLabel",
3494
- publicName: "closeLabel",
3620
+ running: {
3621
+ classPropertyName: "running",
3622
+ publicName: "running",
3623
+ isSignal: true,
3624
+ isRequired: false,
3625
+ transformFunction: null
3626
+ },
3627
+ disabled: {
3628
+ classPropertyName: "disabled",
3629
+ publicName: "disabled",
3630
+ isSignal: true,
3631
+ isRequired: false,
3632
+ transformFunction: null
3633
+ },
3634
+ disabledReason: {
3635
+ classPropertyName: "disabledReason",
3636
+ publicName: "disabledReason",
3637
+ isSignal: true,
3638
+ isRequired: false,
3639
+ transformFunction: null
3640
+ },
3641
+ label: {
3642
+ classPropertyName: "label",
3643
+ publicName: "label",
3644
+ isSignal: true,
3645
+ isRequired: false,
3646
+ transformFunction: null
3647
+ },
3648
+ ariaLabel: {
3649
+ classPropertyName: "ariaLabel",
3650
+ publicName: "ariaLabel",
3651
+ isSignal: true,
3652
+ isRequired: false,
3653
+ transformFunction: null
3654
+ },
3655
+ ariaLabelledby: {
3656
+ classPropertyName: "ariaLabelledby",
3657
+ publicName: "ariaLabelledby",
3658
+ isSignal: true,
3659
+ isRequired: false,
3660
+ transformFunction: null
3661
+ },
3662
+ placeholder: {
3663
+ classPropertyName: "placeholder",
3664
+ publicName: "placeholder",
3665
+ isSignal: true,
3666
+ isRequired: false,
3667
+ transformFunction: null
3668
+ },
3669
+ maxRows: {
3670
+ classPropertyName: "maxRows",
3671
+ publicName: "maxRows",
3672
+ isSignal: true,
3673
+ isRequired: false,
3674
+ transformFunction: null
3675
+ },
3676
+ maxLength: {
3677
+ classPropertyName: "maxLength",
3678
+ publicName: "maxLength",
3679
+ isSignal: true,
3680
+ isRequired: false,
3681
+ transformFunction: null
3682
+ },
3683
+ name: {
3684
+ classPropertyName: "name",
3685
+ publicName: "name",
3686
+ isSignal: true,
3687
+ isRequired: false,
3688
+ transformFunction: null
3689
+ },
3690
+ size: {
3691
+ classPropertyName: "size",
3692
+ publicName: "size",
3495
3693
  isSignal: true,
3496
3694
  isRequired: false,
3497
3695
  transformFunction: null
3498
3696
  }
3499
3697
  },
3500
- host: { properties: {
3501
- "class": "hostClasses()",
3502
- "attr.data-bf-side-panel-id": "titleId"
3503
- } },
3698
+ outputs: {
3699
+ value: "valueChange",
3700
+ changed: "changed",
3701
+ sent: "sent",
3702
+ stopped: "stopped"
3703
+ },
3704
+ host: {
3705
+ properties: { "attr.data-running": "running() ? '' : null" },
3706
+ classAttribute: "flex items-end gap-2"
3707
+ },
3504
3708
  viewQueries: [{
3505
- propertyName: "body",
3506
- first: true,
3507
- predicate: ["body"],
3508
- descendants: true,
3509
- isSignal: true
3510
- }, {
3511
- propertyName: "content",
3709
+ propertyName: "field",
3512
3710
  first: true,
3513
- predicate: ["content"],
3711
+ predicate: BfTextareaComponent,
3514
3712
  descendants: true,
3515
3713
  isSignal: true
3516
3714
  }],
3517
3715
  ngImport: i0,
3518
3716
  template: `
3519
- <div
3520
- data-side-panel-header
3521
- class="border-border flex min-h-16 min-w-0 shrink-0 items-center gap-3 border-b py-4 ps-6 pe-14"
3522
- >
3523
- @if (title()) {
3524
- <h2 [id]="titleId" class="text-heading min-w-0 flex-1 wrap-anywhere">{{ title() }}</h2>
3525
- }
3526
- <ng-content select="[bfSlot=header]" />
3527
- </div>
3528
- <div
3529
- #body
3530
- data-side-panel-body
3531
- class="focus-visible:ring-ring min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 py-4 focus-visible:ring-4 focus-visible:outline-hidden focus-visible:ring-inset"
3532
- [attr.tabindex]="overflowing() ? 0 : null"
3533
- [attr.role]="overflowing() && title() ? 'region' : null"
3534
- [attr.aria-labelledby]="overflowing() && title() ? titleId : null"
3535
- >
3536
- <div #content class="flex flex-col gap-4"><ng-content /></div>
3717
+ <div data-start class="flex shrink-0 items-center gap-1 empty:hidden">
3718
+ <ng-content select="[bfSlot=start]" />
3537
3719
  </div>
3538
- <!-- A projected wrapper (<div bfSlot="footer">) lays out its buttons like the row itself. -->
3539
- <div
3540
- data-side-panel-footer
3541
- class="border-border flex shrink-0 flex-wrap justify-end gap-2 border-t px-6 py-4 *:flex *:min-w-0 *:flex-wrap *:justify-end *:gap-2 empty:hidden"
3542
- >
3543
- <ng-content select="[bfSlot=footer]" />
3720
+ <bf-textarea
3721
+ [class]="fieldClasses()"
3722
+ autoResize
3723
+ [rows]="1"
3724
+ [maxRows]="maxRows()"
3725
+ [size]="resolvedSize()"
3726
+ [label]="label()"
3727
+ [ariaLabel]="fieldName()"
3728
+ [ariaLabelledby]="ariaLabelledby()"
3729
+ [placeholder]="fieldPlaceholder()"
3730
+ [disabled]="disabled()"
3731
+ [maxLength]="maxLength()"
3732
+ [name]="name()"
3733
+ [value]="value()"
3734
+ (changed)="onChanged($event)"
3735
+ (keydown)="onKeydown($event)"
3736
+ />
3737
+ <div data-end class="flex shrink-0 items-center gap-1 empty:hidden">
3738
+ <ng-content select="[bfSlot=end]" />
3544
3739
  </div>
3545
- <!-- Last in DOM order so first-tabbable focus lands on the content, not on the ×. -->
3546
- <bf-icon-button
3547
- data-side-panel-close
3548
- class="absolute end-3 top-4"
3549
- variant="text"
3550
- size="sm"
3551
- [label]="closeName()"
3552
- (clicked)="close()"
3740
+ <button
3741
+ type="button"
3742
+ data-action
3743
+ [attr.data-mode]="action()"
3744
+ [class]="buttonClasses()"
3745
+ [disabled]="nativeDisabled()"
3746
+ [attr.aria-disabled]="blocked() ? 'true' : null"
3747
+ [bfTooltip]="blocked() ? disabledReason() : ''"
3748
+ (click)="onButton()"
3553
3749
  >
3554
- <bf-icon [icon]="closeIcon" />
3555
- </bf-icon-button>
3750
+ <!-- display: contents, so a projected icon is the button's own flex item. -->
3751
+ <span
3752
+ [class]="action() === 'send' ? 'contents' : 'hidden'"
3753
+ [attr.data-icon]="action() === 'send' ? '' : null"
3754
+ ><ng-content select="[bfSlot=send-icon]"
3755
+ /></span>
3756
+ <span
3757
+ [class]="action() === 'stop' ? 'contents' : 'hidden'"
3758
+ [attr.data-icon]="action() === 'stop' ? '' : null"
3759
+ ><ng-content select="[bfSlot=stop-icon]"
3760
+ /></span>
3761
+ <span data-text class="[[data-icon]:has(>*)~&]:sr-only">{{ buttonName() }}</span>
3762
+ </button>
3556
3763
  `,
3557
3764
  isInline: true,
3558
3765
  dependencies: [{
3559
3766
  kind: "component",
3560
- type: BfIconButtonComponent,
3561
- selector: "bf-icon-button, a[bfIconButton]",
3767
+ type: BfTextareaComponent,
3768
+ selector: "bf-textarea",
3562
3769
  inputs: [
3563
- "variant",
3770
+ "value",
3564
3771
  "size",
3565
- "type",
3772
+ "label",
3773
+ "ariaLabel",
3774
+ "ariaLabelledby",
3775
+ "placeholder",
3776
+ "helpText",
3777
+ "errorMessage",
3566
3778
  "disabled",
3567
- "label"
3568
- ],
3569
- outputs: ["clicked"]
3570
- }, {
3571
- kind: "component",
3572
- type: BfIconComponent,
3573
- selector: "bf-icon",
3574
- inputs: [
3575
- "icon",
3576
- "size",
3577
- "label",
3578
- "filled"
3779
+ "readonly",
3780
+ "required",
3781
+ "invalid",
3782
+ "touched",
3783
+ "errors",
3784
+ "name",
3785
+ "id",
3786
+ "rows",
3787
+ "autoResize",
3788
+ "maxRows",
3789
+ "maxLength",
3790
+ "minLength"
3791
+ ],
3792
+ outputs: [
3793
+ "valueChange",
3794
+ "changed",
3795
+ "focused",
3796
+ "blurred",
3797
+ "touch"
3579
3798
  ]
3799
+ }, {
3800
+ kind: "directive",
3801
+ type: BfTooltipDirective,
3802
+ selector: "[bfTooltip]",
3803
+ inputs: ["bfTooltip", "bfTooltipPlacement"]
3580
3804
  }]
3581
3805
  });
3582
3806
  };
@@ -3584,119 +3808,473 @@ i0.ɵɵngDeclareClassMetadata({
3584
3808
  minVersion: "12.0.0",
3585
3809
  version: "22.1.7",
3586
3810
  ngImport: i0,
3587
- type: BfSidePanelComponent,
3811
+ type: BfChatComposerComponent,
3588
3812
  decorators: [{
3589
3813
  type: Component,
3590
3814
  args: [{
3591
- selector: "bf-side-panel",
3592
- imports: [BfIconButtonComponent, BfIconComponent],
3815
+ selector: "bf-chat-composer",
3816
+ imports: [BfTextareaComponent, BfTooltipDirective],
3593
3817
  host: {
3594
- "[class]": "hostClasses()",
3595
- "[attr.data-bf-side-panel-id]": "titleId"
3818
+ class: "flex items-end gap-2",
3819
+ "[attr.data-running]": "running() ? '' : null"
3596
3820
  },
3597
3821
  template: `
3598
- <div
3599
- data-side-panel-header
3600
- class="border-border flex min-h-16 min-w-0 shrink-0 items-center gap-3 border-b py-4 ps-6 pe-14"
3601
- >
3602
- @if (title()) {
3603
- <h2 [id]="titleId" class="text-heading min-w-0 flex-1 wrap-anywhere">{{ title() }}</h2>
3604
- }
3605
- <ng-content select="[bfSlot=header]" />
3606
- </div>
3607
- <div
3608
- #body
3609
- data-side-panel-body
3610
- class="focus-visible:ring-ring min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 py-4 focus-visible:ring-4 focus-visible:outline-hidden focus-visible:ring-inset"
3611
- [attr.tabindex]="overflowing() ? 0 : null"
3612
- [attr.role]="overflowing() && title() ? 'region' : null"
3613
- [attr.aria-labelledby]="overflowing() && title() ? titleId : null"
3614
- >
3615
- <div #content class="flex flex-col gap-4"><ng-content /></div>
3822
+ <div data-start class="flex shrink-0 items-center gap-1 empty:hidden">
3823
+ <ng-content select="[bfSlot=start]" />
3616
3824
  </div>
3617
- <!-- A projected wrapper (<div bfSlot="footer">) lays out its buttons like the row itself. -->
3618
- <div
3619
- data-side-panel-footer
3620
- class="border-border flex shrink-0 flex-wrap justify-end gap-2 border-t px-6 py-4 *:flex *:min-w-0 *:flex-wrap *:justify-end *:gap-2 empty:hidden"
3621
- >
3622
- <ng-content select="[bfSlot=footer]" />
3825
+ <bf-textarea
3826
+ [class]="fieldClasses()"
3827
+ autoResize
3828
+ [rows]="1"
3829
+ [maxRows]="maxRows()"
3830
+ [size]="resolvedSize()"
3831
+ [label]="label()"
3832
+ [ariaLabel]="fieldName()"
3833
+ [ariaLabelledby]="ariaLabelledby()"
3834
+ [placeholder]="fieldPlaceholder()"
3835
+ [disabled]="disabled()"
3836
+ [maxLength]="maxLength()"
3837
+ [name]="name()"
3838
+ [value]="value()"
3839
+ (changed)="onChanged($event)"
3840
+ (keydown)="onKeydown($event)"
3841
+ />
3842
+ <div data-end class="flex shrink-0 items-center gap-1 empty:hidden">
3843
+ <ng-content select="[bfSlot=end]" />
3623
3844
  </div>
3624
- <!-- Last in DOM order so first-tabbable focus lands on the content, not on the ×. -->
3625
- <bf-icon-button
3626
- data-side-panel-close
3627
- class="absolute end-3 top-4"
3628
- variant="text"
3629
- size="sm"
3630
- [label]="closeName()"
3631
- (clicked)="close()"
3845
+ <button
3846
+ type="button"
3847
+ data-action
3848
+ [attr.data-mode]="action()"
3849
+ [class]="buttonClasses()"
3850
+ [disabled]="nativeDisabled()"
3851
+ [attr.aria-disabled]="blocked() ? 'true' : null"
3852
+ [bfTooltip]="blocked() ? disabledReason() : ''"
3853
+ (click)="onButton()"
3632
3854
  >
3633
- <bf-icon [icon]="closeIcon" />
3634
- </bf-icon-button>
3855
+ <!-- display: contents, so a projected icon is the button's own flex item. -->
3856
+ <span
3857
+ [class]="action() === 'send' ? 'contents' : 'hidden'"
3858
+ [attr.data-icon]="action() === 'send' ? '' : null"
3859
+ ><ng-content select="[bfSlot=send-icon]"
3860
+ /></span>
3861
+ <span
3862
+ [class]="action() === 'stop' ? 'contents' : 'hidden'"
3863
+ [attr.data-icon]="action() === 'stop' ? '' : null"
3864
+ ><ng-content select="[bfSlot=stop-icon]"
3865
+ /></span>
3866
+ <span data-text class="[[data-icon]:has(>*)~&]:sr-only">{{ buttonName() }}</span>
3867
+ </button>
3635
3868
  `
3636
3869
  }]
3637
3870
  }],
3638
- ctorParameters: () => [],
3639
3871
  propDecorators: {
3640
- title: [{
3872
+ value: [{
3641
3873
  type: i0.Input,
3642
3874
  args: [{
3643
3875
  isSignal: true,
3644
- alias: "title",
3876
+ alias: "value",
3645
3877
  required: false
3646
3878
  }]
3879
+ }, {
3880
+ type: i0.Output,
3881
+ args: ["valueChange"]
3647
3882
  }],
3648
- closeLabel: [{
3883
+ running: [{
3649
3884
  type: i0.Input,
3650
3885
  args: [{
3651
3886
  isSignal: true,
3652
- alias: "closeLabel",
3887
+ alias: "running",
3653
3888
  required: false
3654
3889
  }]
3655
3890
  }],
3656
- body: [{
3657
- type: i0.ViewChild,
3658
- args: ["body", { isSignal: true }]
3891
+ disabled: [{
3892
+ type: i0.Input,
3893
+ args: [{
3894
+ isSignal: true,
3895
+ alias: "disabled",
3896
+ required: false
3897
+ }]
3659
3898
  }],
3660
- content: [{
3899
+ disabledReason: [{
3900
+ type: i0.Input,
3901
+ args: [{
3902
+ isSignal: true,
3903
+ alias: "disabledReason",
3904
+ required: false
3905
+ }]
3906
+ }],
3907
+ label: [{
3908
+ type: i0.Input,
3909
+ args: [{
3910
+ isSignal: true,
3911
+ alias: "label",
3912
+ required: false
3913
+ }]
3914
+ }],
3915
+ ariaLabel: [{
3916
+ type: i0.Input,
3917
+ args: [{
3918
+ isSignal: true,
3919
+ alias: "ariaLabel",
3920
+ required: false
3921
+ }]
3922
+ }],
3923
+ ariaLabelledby: [{
3924
+ type: i0.Input,
3925
+ args: [{
3926
+ isSignal: true,
3927
+ alias: "ariaLabelledby",
3928
+ required: false
3929
+ }]
3930
+ }],
3931
+ placeholder: [{
3932
+ type: i0.Input,
3933
+ args: [{
3934
+ isSignal: true,
3935
+ alias: "placeholder",
3936
+ required: false
3937
+ }]
3938
+ }],
3939
+ maxRows: [{
3940
+ type: i0.Input,
3941
+ args: [{
3942
+ isSignal: true,
3943
+ alias: "maxRows",
3944
+ required: false
3945
+ }]
3946
+ }],
3947
+ maxLength: [{
3948
+ type: i0.Input,
3949
+ args: [{
3950
+ isSignal: true,
3951
+ alias: "maxLength",
3952
+ required: false
3953
+ }]
3954
+ }],
3955
+ name: [{
3956
+ type: i0.Input,
3957
+ args: [{
3958
+ isSignal: true,
3959
+ alias: "name",
3960
+ required: false
3961
+ }]
3962
+ }],
3963
+ size: [{
3964
+ type: i0.Input,
3965
+ args: [{
3966
+ isSignal: true,
3967
+ alias: "size",
3968
+ required: false
3969
+ }]
3970
+ }],
3971
+ changed: [{
3972
+ type: i0.Output,
3973
+ args: ["changed"]
3974
+ }],
3975
+ sent: [{
3976
+ type: i0.Output,
3977
+ args: ["sent"]
3978
+ }],
3979
+ stopped: [{
3980
+ type: i0.Output,
3981
+ args: ["stopped"]
3982
+ }],
3983
+ field: [{
3661
3984
  type: i0.ViewChild,
3662
- args: ["content", { isSignal: true }]
3985
+ args: [i0.forwardRef(() => BfTextareaComponent), { isSignal: true }]
3663
3986
  }]
3664
3987
  }
3665
3988
  });
3666
- const BF_SIDE_PANEL_TEMPLATE = new InjectionToken("BF_SIDE_PANEL_TEMPLATE");
3667
- var BfSidePanelTemplateHostComponent = class BfSidePanelTemplateHostComponent {
3668
- view = inject(BF_SIDE_PANEL_TEMPLATE);
3669
- injector = inject(Injector);
3989
+ const BF_SIDE_PANEL_DATA = new InjectionToken("BF_SIDE_PANEL_DATA");
3990
+ const BF_SIDE_PANEL_CONTEXT = new InjectionToken("BF_SIDE_PANEL_CONTEXT");
3991
+ var BfSidePanelRef = class {
3992
+ overlayRef;
3993
+ instance;
3994
+ id;
3995
+ afterClosed;
3996
+ closed;
3997
+ constructor(overlayRef, instance = () => null) {
3998
+ this.overlayRef = overlayRef;
3999
+ this.instance = instance;
4000
+ this.id = overlayRef.id;
4001
+ this.afterClosed = overlayRef.afterClosed;
4002
+ this.closed = overlayRef.closed;
4003
+ }
4004
+ get componentInstance() {
4005
+ return this.instance();
4006
+ }
4007
+ close(result) {
4008
+ this.overlayRef.close(result);
4009
+ }
4010
+ };
4011
+ const HOST_BASE = "relative flex h-full flex-col border-s border-border bg-surface text-text shadow-floating";
4012
+ const WIDTH = {
4013
+ sm: "w-[min(22.5rem,100vw)]",
4014
+ md: "w-[min(30rem,100vw)]",
4015
+ lg: "w-[min(40rem,100vw)]"
4016
+ };
4017
+ var BfSidePanelComponent = class BfSidePanelComponent {
4018
+ labels = inject(BF_LABELS);
4019
+ context = inject(BF_SIDE_PANEL_CONTEXT, { optional: true });
4020
+ ref = inject(BfSidePanelRef, { optional: true });
4021
+ title = input("", ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
4022
+ closeLabel = input("", ...ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []);
4023
+ overflowing = signal(false, ...ngDevMode ? [{ debugName: "overflowing" }] : /* istanbul ignore next */ []);
4024
+ titleId = this.context?.titleId ?? ɵuniqueId("bf-side-panel-title");
4025
+ closeIcon = X;
4026
+ width = this.context?.width ?? "md";
4027
+ hostClasses = computed(() => `${HOST_BASE} ${WIDTH[this.width]}`, ...ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []);
4028
+ closeName = computed(() => this.closeLabel() || this.labels.close, ...ngDevMode ? [{ debugName: "closeName" }] : /* istanbul ignore next */ []);
4029
+ body = viewChild.required("body", ...ngDevMode ? [{ debugName: "body" }] : /* istanbul ignore next */ []);
4030
+ content = viewChild.required("content", ...ngDevMode ? [{ debugName: "content" }] : /* istanbul ignore next */ []);
4031
+ constructor() {
4032
+ let observer = null;
4033
+ afterNextRender(() => {
4034
+ const body = this.body().nativeElement;
4035
+ const measure = () => this.overflowing.set(body.scrollHeight > body.clientHeight);
4036
+ measure();
4037
+ if (typeof ResizeObserver === "function") {
4038
+ observer = new ResizeObserver(measure);
4039
+ observer.observe(body);
4040
+ observer.observe(this.content().nativeElement);
4041
+ }
4042
+ });
4043
+ inject(DestroyRef).onDestroy(() => observer?.disconnect());
4044
+ }
4045
+ close() {
4046
+ this.ref?.close();
4047
+ }
3670
4048
  static ɵfac = i0.ɵɵngDeclareFactory({
3671
4049
  minVersion: "12.0.0",
3672
4050
  version: "22.1.7",
3673
4051
  ngImport: i0,
3674
- type: BfSidePanelTemplateHostComponent,
4052
+ type: BfSidePanelComponent,
3675
4053
  deps: [],
3676
4054
  target: i0.ɵɵFactoryTarget.Component
3677
4055
  });
3678
4056
  static ɵcmp = i0.ɵɵngDeclareComponent({
3679
- minVersion: "14.0.0",
4057
+ minVersion: "17.0.0",
3680
4058
  version: "22.1.7",
3681
- type: BfSidePanelTemplateHostComponent,
4059
+ type: BfSidePanelComponent,
3682
4060
  isStandalone: true,
3683
- selector: "bf-side-panel-template-host",
3684
- host: { styleAttribute: "display: contents" },
3685
- ngImport: i0,
3686
- template: `<ng-container
3687
- [ngTemplateOutlet]="view.template"
3688
- [ngTemplateOutletContext]="view.context"
3689
- [ngTemplateOutletInjector]="injector"
3690
- />`,
3691
- isInline: true,
3692
- dependencies: [{
3693
- kind: "directive",
3694
- type: NgTemplateOutlet,
3695
- selector: "[ngTemplateOutlet]",
3696
- inputs: [
3697
- "ngTemplateOutletContext",
3698
- "ngTemplateOutlet",
3699
- "ngTemplateOutletInjector"
4061
+ selector: "bf-side-panel",
4062
+ inputs: {
4063
+ title: {
4064
+ classPropertyName: "title",
4065
+ publicName: "title",
4066
+ isSignal: true,
4067
+ isRequired: false,
4068
+ transformFunction: null
4069
+ },
4070
+ closeLabel: {
4071
+ classPropertyName: "closeLabel",
4072
+ publicName: "closeLabel",
4073
+ isSignal: true,
4074
+ isRequired: false,
4075
+ transformFunction: null
4076
+ }
4077
+ },
4078
+ host: { properties: {
4079
+ "class": "hostClasses()",
4080
+ "attr.data-bf-side-panel-id": "titleId"
4081
+ } },
4082
+ viewQueries: [{
4083
+ propertyName: "body",
4084
+ first: true,
4085
+ predicate: ["body"],
4086
+ descendants: true,
4087
+ isSignal: true
4088
+ }, {
4089
+ propertyName: "content",
4090
+ first: true,
4091
+ predicate: ["content"],
4092
+ descendants: true,
4093
+ isSignal: true
4094
+ }],
4095
+ ngImport: i0,
4096
+ template: `
4097
+ <div
4098
+ data-side-panel-header
4099
+ class="border-border flex min-h-16 min-w-0 shrink-0 items-center gap-3 border-b py-4 ps-6 pe-14"
4100
+ >
4101
+ @if (title()) {
4102
+ <h2 [id]="titleId" class="text-heading min-w-0 flex-1 wrap-anywhere">{{ title() }}</h2>
4103
+ }
4104
+ <ng-content select="[bfSlot=header]" />
4105
+ </div>
4106
+ <div
4107
+ #body
4108
+ data-side-panel-body
4109
+ class="focus-visible:ring-ring min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 py-4 focus-visible:ring-4 focus-visible:outline-hidden focus-visible:ring-inset"
4110
+ [attr.tabindex]="overflowing() ? 0 : null"
4111
+ [attr.role]="overflowing() && title() ? 'region' : null"
4112
+ [attr.aria-labelledby]="overflowing() && title() ? titleId : null"
4113
+ >
4114
+ <div #content class="flex flex-col gap-4"><ng-content /></div>
4115
+ </div>
4116
+ <!-- A projected wrapper (<div bfSlot="footer">) lays out its buttons like the row itself. -->
4117
+ <div
4118
+ data-side-panel-footer
4119
+ class="border-border flex shrink-0 flex-wrap justify-end gap-2 border-t px-6 py-4 *:flex *:min-w-0 *:flex-wrap *:justify-end *:gap-2 empty:hidden"
4120
+ >
4121
+ <ng-content select="[bfSlot=footer]" />
4122
+ </div>
4123
+ <!-- Last in DOM order so first-tabbable focus lands on the content, not on the ×. -->
4124
+ <bf-icon-button
4125
+ data-side-panel-close
4126
+ class="absolute end-3 top-4"
4127
+ variant="text"
4128
+ size="sm"
4129
+ [label]="closeName()"
4130
+ (clicked)="close()"
4131
+ >
4132
+ <bf-icon [icon]="closeIcon" />
4133
+ </bf-icon-button>
4134
+ `,
4135
+ isInline: true,
4136
+ dependencies: [{
4137
+ kind: "component",
4138
+ type: BfIconButtonComponent,
4139
+ selector: "bf-icon-button, a[bfIconButton]",
4140
+ inputs: [
4141
+ "variant",
4142
+ "size",
4143
+ "type",
4144
+ "disabled",
4145
+ "label"
4146
+ ],
4147
+ outputs: ["clicked"]
4148
+ }, {
4149
+ kind: "component",
4150
+ type: BfIconComponent,
4151
+ selector: "bf-icon",
4152
+ inputs: [
4153
+ "icon",
4154
+ "size",
4155
+ "label",
4156
+ "filled"
4157
+ ]
4158
+ }]
4159
+ });
4160
+ };
4161
+ i0.ɵɵngDeclareClassMetadata({
4162
+ minVersion: "12.0.0",
4163
+ version: "22.1.7",
4164
+ ngImport: i0,
4165
+ type: BfSidePanelComponent,
4166
+ decorators: [{
4167
+ type: Component,
4168
+ args: [{
4169
+ selector: "bf-side-panel",
4170
+ imports: [BfIconButtonComponent, BfIconComponent],
4171
+ host: {
4172
+ "[class]": "hostClasses()",
4173
+ "[attr.data-bf-side-panel-id]": "titleId"
4174
+ },
4175
+ template: `
4176
+ <div
4177
+ data-side-panel-header
4178
+ class="border-border flex min-h-16 min-w-0 shrink-0 items-center gap-3 border-b py-4 ps-6 pe-14"
4179
+ >
4180
+ @if (title()) {
4181
+ <h2 [id]="titleId" class="text-heading min-w-0 flex-1 wrap-anywhere">{{ title() }}</h2>
4182
+ }
4183
+ <ng-content select="[bfSlot=header]" />
4184
+ </div>
4185
+ <div
4186
+ #body
4187
+ data-side-panel-body
4188
+ class="focus-visible:ring-ring min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 py-4 focus-visible:ring-4 focus-visible:outline-hidden focus-visible:ring-inset"
4189
+ [attr.tabindex]="overflowing() ? 0 : null"
4190
+ [attr.role]="overflowing() && title() ? 'region' : null"
4191
+ [attr.aria-labelledby]="overflowing() && title() ? titleId : null"
4192
+ >
4193
+ <div #content class="flex flex-col gap-4"><ng-content /></div>
4194
+ </div>
4195
+ <!-- A projected wrapper (<div bfSlot="footer">) lays out its buttons like the row itself. -->
4196
+ <div
4197
+ data-side-panel-footer
4198
+ class="border-border flex shrink-0 flex-wrap justify-end gap-2 border-t px-6 py-4 *:flex *:min-w-0 *:flex-wrap *:justify-end *:gap-2 empty:hidden"
4199
+ >
4200
+ <ng-content select="[bfSlot=footer]" />
4201
+ </div>
4202
+ <!-- Last in DOM order so first-tabbable focus lands on the content, not on the ×. -->
4203
+ <bf-icon-button
4204
+ data-side-panel-close
4205
+ class="absolute end-3 top-4"
4206
+ variant="text"
4207
+ size="sm"
4208
+ [label]="closeName()"
4209
+ (clicked)="close()"
4210
+ >
4211
+ <bf-icon [icon]="closeIcon" />
4212
+ </bf-icon-button>
4213
+ `
4214
+ }]
4215
+ }],
4216
+ ctorParameters: () => [],
4217
+ propDecorators: {
4218
+ title: [{
4219
+ type: i0.Input,
4220
+ args: [{
4221
+ isSignal: true,
4222
+ alias: "title",
4223
+ required: false
4224
+ }]
4225
+ }],
4226
+ closeLabel: [{
4227
+ type: i0.Input,
4228
+ args: [{
4229
+ isSignal: true,
4230
+ alias: "closeLabel",
4231
+ required: false
4232
+ }]
4233
+ }],
4234
+ body: [{
4235
+ type: i0.ViewChild,
4236
+ args: ["body", { isSignal: true }]
4237
+ }],
4238
+ content: [{
4239
+ type: i0.ViewChild,
4240
+ args: ["content", { isSignal: true }]
4241
+ }]
4242
+ }
4243
+ });
4244
+ const BF_SIDE_PANEL_TEMPLATE = new InjectionToken("BF_SIDE_PANEL_TEMPLATE");
4245
+ var BfSidePanelTemplateHostComponent = class BfSidePanelTemplateHostComponent {
4246
+ view = inject(BF_SIDE_PANEL_TEMPLATE);
4247
+ injector = inject(Injector);
4248
+ static ɵfac = i0.ɵɵngDeclareFactory({
4249
+ minVersion: "12.0.0",
4250
+ version: "22.1.7",
4251
+ ngImport: i0,
4252
+ type: BfSidePanelTemplateHostComponent,
4253
+ deps: [],
4254
+ target: i0.ɵɵFactoryTarget.Component
4255
+ });
4256
+ static ɵcmp = i0.ɵɵngDeclareComponent({
4257
+ minVersion: "14.0.0",
4258
+ version: "22.1.7",
4259
+ type: BfSidePanelTemplateHostComponent,
4260
+ isStandalone: true,
4261
+ selector: "bf-side-panel-template-host",
4262
+ host: { styleAttribute: "display: contents" },
4263
+ ngImport: i0,
4264
+ template: `<ng-container
4265
+ [ngTemplateOutlet]="view.template"
4266
+ [ngTemplateOutletContext]="view.context"
4267
+ [ngTemplateOutletInjector]="injector"
4268
+ />`,
4269
+ isInline: true,
4270
+ dependencies: [{
4271
+ kind: "directive",
4272
+ type: NgTemplateOutlet,
4273
+ selector: "[ngTemplateOutlet]",
4274
+ inputs: [
4275
+ "ngTemplateOutletContext",
4276
+ "ngTemplateOutlet",
4277
+ "ngTemplateOutletInjector"
3700
4278
  ]
3701
4279
  }]
3702
4280
  });
@@ -3825,7 +4403,7 @@ i0.ɵɵngDeclareClassMetadata({
3825
4403
  }]
3826
4404
  });
3827
4405
  var BfPopoverTriggerDirective = class BfPopoverTriggerDirective {
3828
- overlay = inject(Overlay);
4406
+ environment = inject(EnvironmentInjector);
3829
4407
  document = inject(DOCUMENT);
3830
4408
  injector = inject(Injector);
3831
4409
  viewContainer = inject(ViewContainerRef);
@@ -3841,6 +4419,7 @@ var BfPopoverTriggerDirective = class BfPopoverTriggerDirective {
3841
4419
  isOpen = this.openState.asReadonly();
3842
4420
  panelId = ɵuniqueId("bf-popover");
3843
4421
  anchored = null;
4422
+ motion = new AnchoredMotion();
3844
4423
  subscriptions = [];
3845
4424
  stopNavigation = null;
3846
4425
  stopHeading = null;
@@ -3853,18 +4432,22 @@ var BfPopoverTriggerDirective = class BfPopoverTriggerDirective {
3853
4432
  if (open) control.setAttribute("aria-controls", this.panelId);
3854
4433
  else control.removeAttribute("aria-controls");
3855
4434
  } });
3856
- inject(DestroyRef).onDestroy(() => this.teardown());
4435
+ inject(DestroyRef).onDestroy(() => {
4436
+ this.teardown(false);
4437
+ this.motion.flush();
4438
+ });
3857
4439
  }
3858
4440
  open() {
3859
4441
  if (this.anchored) return;
4442
+ this.motion.flush();
3860
4443
  const direction = this.dir?.value ?? "ltr";
3861
- const anchored = createAnchoredOverlay(this.overlay, this.host, {
4444
+ const anchored = createAnchoredOverlay(this.environment, this.host, {
3862
4445
  positions: blockPositions(this.bfPopoverPlacement()),
3863
4446
  direction,
3864
4447
  panelClass: ["bf-popover-panel"]
3865
4448
  });
3866
4449
  this.anchored = anchored;
3867
- anchored.ref.attach(new TemplatePortal(this.bfPopoverTriggerFor(), this.viewContainer, void 0, this.injector));
4450
+ const view = anchored.ref.attach(new TemplatePortal(this.bfPopoverTriggerFor(), this.viewContainer, void 0, this.injector));
3868
4451
  const pane = anchored.ref.overlayElement;
3869
4452
  this.subscriptions = [anchored.ref.keydownEvents().subscribe((event) => {
3870
4453
  if (event.key === "Escape") {
@@ -3879,6 +4462,7 @@ var BfPopoverTriggerDirective = class BfPopoverTriggerDirective {
3879
4462
  pane.addEventListener("keydown", (event) => this.tabbed(event, pane));
3880
4463
  this.stopNavigation = watchPathChange(this.location, () => this.close(false));
3881
4464
  this.openState.set(true);
4465
+ this.motion.enter(anchored.ref, this.host, view);
3882
4466
  const panel = pane.querySelector("bf-popover") ?? pane;
3883
4467
  this.name(panel);
3884
4468
  (firstTabbable(panel) ?? panel).focus({ preventScroll: true });
@@ -3886,7 +4470,7 @@ var BfPopoverTriggerDirective = class BfPopoverTriggerDirective {
3886
4470
  }
3887
4471
  close(restoreFocus = true) {
3888
4472
  if (!this.anchored) return;
3889
- this.teardown();
4473
+ this.teardown(true);
3890
4474
  this.openState.set(false);
3891
4475
  if (restoreFocus) this.control().focus({ preventScroll: true });
3892
4476
  this.bfPopoverClosed.emit();
@@ -3917,15 +4501,17 @@ var BfPopoverTriggerDirective = class BfPopoverTriggerDirective {
3917
4501
  panel.setAttribute("aria-labelledby", this.labelId());
3918
4502
  this.stopHeading = watchPanelHeading(panel);
3919
4503
  }
3920
- teardown() {
4504
+ teardown(animate) {
3921
4505
  this.stopHeading?.();
3922
4506
  this.stopHeading = null;
3923
4507
  this.subscriptions.forEach((s) => s.unsubscribe());
3924
4508
  this.subscriptions = [];
3925
4509
  this.stopNavigation?.();
3926
4510
  this.stopNavigation = null;
3927
- this.anchored?.ref.dispose();
4511
+ const ref = this.anchored?.ref;
3928
4512
  this.anchored = null;
4513
+ if (ref && animate) this.motion.leave(ref, this.host);
4514
+ else ref?.dispose();
3929
4515
  }
3930
4516
  static ɵfac = i0.ɵɵngDeclareFactory({
3931
4517
  minVersion: "12.0.0",
@@ -4091,7 +4677,7 @@ function labelContains(option, query) {
4091
4677
  }
4092
4678
  var ComboboxCore = class {
4093
4679
  host;
4094
- overlay = inject(Overlay);
4680
+ environment = inject(EnvironmentInjector);
4095
4681
  injector = inject(Injector);
4096
4682
  viewContainer = inject(ViewContainerRef);
4097
4683
  dir = inject(Directionality, { optional: true });
@@ -4141,10 +4727,15 @@ var ComboboxCore = class {
4141
4727
  }, ...ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []);
4142
4728
  controls = computed(() => this.isOpen() && this.items().length > 0 ? this.listboxId : null, ...ngDevMode ? [{ debugName: "controls" }] : /* istanbul ignore next */ []);
4143
4729
  anchored = null;
4730
+ panelView = null;
4731
+ motion = new AnchoredMotion();
4144
4732
  subscriptions = [];
4145
4733
  constructor(host) {
4146
4734
  this.host = host;
4147
- inject(DestroyRef).onDestroy(() => this.teardown());
4735
+ inject(DestroyRef).onDestroy(() => {
4736
+ this.teardown(false);
4737
+ this.motion.flush();
4738
+ });
4148
4739
  }
4149
4740
  setQuery(query) {
4150
4741
  const before = this.queryState() ?? "";
@@ -4158,15 +4749,18 @@ var ComboboxCore = class {
4158
4749
  }
4159
4750
  open() {
4160
4751
  if (this.anchored) return;
4752
+ this.motion.flush();
4161
4753
  const anchor = this.host.anchor();
4162
- const anchored = createAnchoredOverlay(this.overlay, anchor, {
4754
+ const anchored = createAnchoredOverlay(this.environment, anchor, {
4163
4755
  positions: blockPositions("below-start"),
4164
4756
  direction: this.dir?.value ?? "ltr",
4165
4757
  panelClass: ["bf-combobox-panel"]
4166
4758
  });
4167
4759
  this.anchored = anchored;
4168
4760
  anchored.ref.updateSize({ width: anchor.getBoundingClientRect().width || void 0 });
4169
- anchored.ref.attach(new ComponentPortal(this.host.panel, this.viewContainer, this.injector)).setInput("core", this);
4761
+ const panel = anchored.ref.attach(new ComponentPortal(this.host.panel, this.viewContainer, this.injector));
4762
+ panel.setInput("core", this);
4763
+ this.panelView = panel.changeDetectorRef;
4170
4764
  this.subscriptions = [anchored.ref.keydownEvents().subscribe((event) => {
4171
4765
  if (event.key === "Escape") {
4172
4766
  event.preventDefault();
@@ -4176,10 +4770,11 @@ var ComboboxCore = class {
4176
4770
  if (!(event.target instanceof Node && anchor.contains(event.target))) this.close();
4177
4771
  })];
4178
4772
  this.openState.set(true);
4773
+ this.motion.enter(anchored.ref, anchor, panel.changeDetectorRef);
4179
4774
  }
4180
4775
  close() {
4181
4776
  if (!this.anchored) return;
4182
- this.teardown();
4777
+ this.teardown(true);
4183
4778
  this.openState.set(false);
4184
4779
  this.activeIndex.set(null);
4185
4780
  }
@@ -4238,11 +4833,15 @@ var ComboboxCore = class {
4238
4833
  firstEnabled(items) {
4239
4834
  return items.find((item) => !item.option.disabled);
4240
4835
  }
4241
- teardown() {
4836
+ teardown(animate) {
4242
4837
  this.subscriptions.forEach((s) => s.unsubscribe());
4243
4838
  this.subscriptions = [];
4244
- this.anchored?.ref.dispose();
4839
+ const ref = this.anchored?.ref;
4840
+ const view = this.panelView;
4245
4841
  this.anchored = null;
4842
+ this.panelView = null;
4843
+ if (ref && animate) this.motion.leave(ref, this.host.anchor(), view ?? void 0);
4844
+ else ref?.dispose();
4246
4845
  }
4247
4846
  };
4248
4847
  function sameValue(compare, optionValue, value) {
@@ -4260,7 +4859,7 @@ const COMBOBOX_CHEVRON_INSET = {
4260
4859
  md: "after:end-4",
4261
4860
  lg: "after:end-4.5"
4262
4861
  };
4263
- const PANEL = "block bg-surface text-text border border-border rounded-card shadow-floating p-1 max-h-[min(18rem,calc(100dvh-2rem))] overflow-y-auto overscroll-contain";
4862
+ const PANEL = "block w-full min-w-0 bg-surface text-text border border-border rounded-card shadow-floating p-1 max-h-[min(18rem,calc(100dvh-2rem))] overflow-y-auto overscroll-contain";
4264
4863
  const OPTION$1 = "flex w-full items-start gap-2 rounded-field px-2 text-start cursor-pointer select-none data-active:bg-surface-muted aria-disabled:cursor-not-allowed aria-disabled:text-text-disabled before:mt-[0.3em] before:h-[11px] before:w-[6px] before:mx-[5px] before:shrink-0 before:rotate-45 before:border-current before:content-[''] aria-selected:before:border-r-2 aria-selected:before:border-b-2 aria-selected:text-primary-text forced-colors:data-active:forced-color-adjust-none forced-colors:data-active:bg-[Highlight] forced-colors:data-active:text-[HighlightText] forced-colors:aria-disabled:text-[GrayText]";
4265
4864
  const SIZE = {
4266
4865
  sm: "min-h-8 py-1.5 text-label-sm pointer-coarse:min-h-hit",
@@ -4472,10 +5071,664 @@ i0.ɵɵngDeclareClassMetadata({
4472
5071
  alias: "core",
4473
5072
  required: true
4474
5073
  }]
4475
- }] }
5074
+ }] }
5075
+ });
5076
+ var BfComboboxComponent = class BfComboboxComponent {
5077
+ value = model(null, ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
5078
+ options = input.required(...ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []);
5079
+ compareWith = input(Object.is, ...ngDevMode ? [{ debugName: "compareWith" }] : /* istanbul ignore next */ []);
5080
+ filter = input(labelContains, ...ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []);
5081
+ size = input(void 0, ...ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []);
5082
+ label = input("", ...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
5083
+ ariaLabel = input("", ...ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []);
5084
+ ariaLabelledby = input("", ...ngDevMode ? [{ debugName: "ariaLabelledby" }] : /* istanbul ignore next */ []);
5085
+ placeholder = input("", ...ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []);
5086
+ helpText = input("", ...ngDevMode ? [{ debugName: "helpText" }] : /* istanbul ignore next */ []);
5087
+ errorMessage = input("", ...ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []);
5088
+ emptyLabel = input("", ...ngDevMode ? [{ debugName: "emptyLabel" }] : /* istanbul ignore next */ []);
5089
+ loading = input(false, {
5090
+ ...ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {},
5091
+ transform: booleanAttribute
5092
+ });
5093
+ disabled = input(false, {
5094
+ ...ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {},
5095
+ transform: booleanAttribute
5096
+ });
5097
+ readonly = input(false, {
5098
+ ...ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {},
5099
+ transform: booleanAttribute
5100
+ });
5101
+ required = input(false, {
5102
+ ...ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {},
5103
+ transform: booleanAttribute
5104
+ });
5105
+ invalid = input(false, {
5106
+ ...ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {},
5107
+ transform: booleanAttribute
5108
+ });
5109
+ touched = input(false, {
5110
+ ...ngDevMode ? { debugName: "touched" } : /* istanbul ignore next */ {},
5111
+ transform: booleanAttribute
5112
+ });
5113
+ errors = input([], ...ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []);
5114
+ name = input("", ...ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []);
5115
+ id = input(ɵuniqueId("bf-combobox"), ...ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []);
5116
+ changed = output();
5117
+ queried = output();
5118
+ focused = output();
5119
+ blurred = output();
5120
+ touch = output();
5121
+ el = viewChild.required("el", ...ngDevMode ? [{ debugName: "el" }] : /* istanbul ignore next */ []);
5122
+ anchorEl = viewChild.required("anchor", ...ngDevMode ? [{ debugName: "anchorEl" }] : /* istanbul ignore next */ []);
5123
+ defaults = inject(BF_DEFAULTS);
5124
+ labels = inject(BF_LABELS);
5125
+ pickedLabel = signal(null, ...ngDevMode ? [{ debugName: "pickedLabel" }] : /* istanbul ignore next */ []);
5126
+ resolvedSize = computed(() => this.size() ?? this.defaults.fieldSize, ...ngDevMode ? [{ debugName: "resolvedSize" }] : /* istanbul ignore next */ []);
5127
+ core = new ComboboxCore({
5128
+ options: this.options,
5129
+ filter: this.filter,
5130
+ isSelected: (option) => sameValue(this.compareWith(), option.value, this.value()),
5131
+ pick: (item) => this.pick(item),
5132
+ anchor: () => this.anchorEl().nativeElement,
5133
+ panel: BfComboboxListboxComponent,
5134
+ listboxLabel: computed(() => this.label() || this.ariaLabel() || null),
5135
+ listboxLabelledby: computed(() => this.label() ? null : this.ariaLabelledby() || null),
5136
+ multiple: false,
5137
+ loading: this.loading,
5138
+ emptyText: computed(() => this.emptyLabel() || this.labels.comboboxEmpty),
5139
+ size: this.resolvedSize,
5140
+ queried: (query) => this.queried.emit(query)
5141
+ });
5142
+ valueLabel = computed(() => {
5143
+ const value = this.value();
5144
+ if (value === null) return "";
5145
+ const compare = this.compareWith();
5146
+ const option = this.options().find((o) => compare(o.value, value));
5147
+ const picked = this.pickedLabel();
5148
+ return option?.label ?? (picked && compare(picked.value, value) ? picked.label : "");
5149
+ }, ...ngDevMode ? [{ debugName: "valueLabel" }] : /* istanbul ignore next */ []);
5150
+ text = computed(() => this.core.query() ?? this.valueLabel(), ...ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []);
5151
+ errorText = computed(() => ɵfieldErrorText(this.errorMessage(), this.touched(), this.errors(), this.labels.validation), ...ngDevMode ? [{ debugName: "errorText" }] : /* istanbul ignore next */ []);
5152
+ showError = computed(() => ɵfieldShowsError(this.errorText(), this.invalid(), this.touched()), ...ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []);
5153
+ describedBy = computed(() => ɵfieldDescribedBy(this.id(), this.errorText(), this.helpText()), ...ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []);
5154
+ errorMessageId = computed(() => ɵfieldErrorMessageId(this.id(), this.errorText(), this.showError()), ...ngDevMode ? [{ debugName: "errorMessageId" }] : /* istanbul ignore next */ []);
5155
+ ariaLabelAttr = computed(() => this.label() ? null : this.ariaLabel() || null, ...ngDevMode ? [{ debugName: "ariaLabelAttr" }] : /* istanbul ignore next */ []);
5156
+ ariaLabelledbyAttr = computed(() => this.label() ? null : this.ariaLabelledby() || null, ...ngDevMode ? [{ debugName: "ariaLabelledbyAttr" }] : /* istanbul ignore next */ []);
5157
+ wrapperClasses = computed(() => `${COMBOBOX_CHEVRON} ${COMBOBOX_CHEVRON_INSET[this.resolvedSize()]}`, ...ngDevMode ? [{ debugName: "wrapperClasses" }] : /* istanbul ignore next */ []);
5158
+ classes = computed(() => [
5159
+ COMBOBOX_FIELD_BASE,
5160
+ ɵFIELD_BOX[this.resolvedSize()],
5161
+ ɵFIELD_PADDING_START[this.resolvedSize()].text,
5162
+ COMBOBOX_PADDING_END[this.resolvedSize()],
5163
+ this.showError() ? "border-danger-border hover:border-danger" : "border-border-strong hover:border-text-muted"
5164
+ ].join(" "), ...ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []);
5165
+ focus(options) {
5166
+ this.el().nativeElement.focus(options);
5167
+ }
5168
+ onInput(el) {
5169
+ if (this.readonly()) return;
5170
+ this.core.typed(el.value);
5171
+ }
5172
+ onClick() {
5173
+ if (!this.readonly() && !this.disabled()) this.core.open();
5174
+ }
5175
+ onKeydown(event) {
5176
+ if (this.readonly()) return;
5177
+ if (this.core.keydown(event)) return;
5178
+ if (event.key === "Escape" && !this.core.isOpen() && this.core.query() !== null) this.resetQuery(null);
5179
+ }
5180
+ onBlur(event) {
5181
+ this.core.close();
5182
+ const query = this.core.query();
5183
+ if (query !== null) {
5184
+ const typed = foldText(query.trim());
5185
+ const match = this.options().find((o) => !o.disabled && foldText(o.label) === typed);
5186
+ if (!typed && this.value() !== null) this.set(null);
5187
+ else if (typed && match && !sameValue(this.compareWith(), match.value, this.value())) this.set(match.value, match.label);
5188
+ this.resetQuery(null);
5189
+ }
5190
+ this.blurred.emit(event);
5191
+ this.touch.emit();
5192
+ }
5193
+ pick(item) {
5194
+ if (!sameValue(this.compareWith(), item.option.value, this.value())) this.set(item.option.value, item.option.label);
5195
+ this.resetQuery(null);
5196
+ this.core.close();
5197
+ }
5198
+ set(value, label = "") {
5199
+ this.pickedLabel.set(value === null ? null : {
5200
+ value,
5201
+ label
5202
+ });
5203
+ this.value.set(value);
5204
+ this.changed.emit(value);
5205
+ }
5206
+ resetQuery(query) {
5207
+ this.core.setQuery(query);
5208
+ this.el().nativeElement.value = this.text();
5209
+ }
5210
+ static ɵfac = i0.ɵɵngDeclareFactory({
5211
+ minVersion: "12.0.0",
5212
+ version: "22.1.7",
5213
+ ngImport: i0,
5214
+ type: BfComboboxComponent,
5215
+ deps: [],
5216
+ target: i0.ɵɵFactoryTarget.Component
5217
+ });
5218
+ static ɵcmp = i0.ɵɵngDeclareComponent({
5219
+ minVersion: "17.2.0",
5220
+ version: "22.1.7",
5221
+ type: BfComboboxComponent,
5222
+ isStandalone: true,
5223
+ selector: "bf-combobox",
5224
+ inputs: {
5225
+ value: {
5226
+ classPropertyName: "value",
5227
+ publicName: "value",
5228
+ isSignal: true,
5229
+ isRequired: false,
5230
+ transformFunction: null
5231
+ },
5232
+ options: {
5233
+ classPropertyName: "options",
5234
+ publicName: "options",
5235
+ isSignal: true,
5236
+ isRequired: true,
5237
+ transformFunction: null
5238
+ },
5239
+ compareWith: {
5240
+ classPropertyName: "compareWith",
5241
+ publicName: "compareWith",
5242
+ isSignal: true,
5243
+ isRequired: false,
5244
+ transformFunction: null
5245
+ },
5246
+ filter: {
5247
+ classPropertyName: "filter",
5248
+ publicName: "filter",
5249
+ isSignal: true,
5250
+ isRequired: false,
5251
+ transformFunction: null
5252
+ },
5253
+ size: {
5254
+ classPropertyName: "size",
5255
+ publicName: "size",
5256
+ isSignal: true,
5257
+ isRequired: false,
5258
+ transformFunction: null
5259
+ },
5260
+ label: {
5261
+ classPropertyName: "label",
5262
+ publicName: "label",
5263
+ isSignal: true,
5264
+ isRequired: false,
5265
+ transformFunction: null
5266
+ },
5267
+ ariaLabel: {
5268
+ classPropertyName: "ariaLabel",
5269
+ publicName: "ariaLabel",
5270
+ isSignal: true,
5271
+ isRequired: false,
5272
+ transformFunction: null
5273
+ },
5274
+ ariaLabelledby: {
5275
+ classPropertyName: "ariaLabelledby",
5276
+ publicName: "ariaLabelledby",
5277
+ isSignal: true,
5278
+ isRequired: false,
5279
+ transformFunction: null
5280
+ },
5281
+ placeholder: {
5282
+ classPropertyName: "placeholder",
5283
+ publicName: "placeholder",
5284
+ isSignal: true,
5285
+ isRequired: false,
5286
+ transformFunction: null
5287
+ },
5288
+ helpText: {
5289
+ classPropertyName: "helpText",
5290
+ publicName: "helpText",
5291
+ isSignal: true,
5292
+ isRequired: false,
5293
+ transformFunction: null
5294
+ },
5295
+ errorMessage: {
5296
+ classPropertyName: "errorMessage",
5297
+ publicName: "errorMessage",
5298
+ isSignal: true,
5299
+ isRequired: false,
5300
+ transformFunction: null
5301
+ },
5302
+ emptyLabel: {
5303
+ classPropertyName: "emptyLabel",
5304
+ publicName: "emptyLabel",
5305
+ isSignal: true,
5306
+ isRequired: false,
5307
+ transformFunction: null
5308
+ },
5309
+ loading: {
5310
+ classPropertyName: "loading",
5311
+ publicName: "loading",
5312
+ isSignal: true,
5313
+ isRequired: false,
5314
+ transformFunction: null
5315
+ },
5316
+ disabled: {
5317
+ classPropertyName: "disabled",
5318
+ publicName: "disabled",
5319
+ isSignal: true,
5320
+ isRequired: false,
5321
+ transformFunction: null
5322
+ },
5323
+ readonly: {
5324
+ classPropertyName: "readonly",
5325
+ publicName: "readonly",
5326
+ isSignal: true,
5327
+ isRequired: false,
5328
+ transformFunction: null
5329
+ },
5330
+ required: {
5331
+ classPropertyName: "required",
5332
+ publicName: "required",
5333
+ isSignal: true,
5334
+ isRequired: false,
5335
+ transformFunction: null
5336
+ },
5337
+ invalid: {
5338
+ classPropertyName: "invalid",
5339
+ publicName: "invalid",
5340
+ isSignal: true,
5341
+ isRequired: false,
5342
+ transformFunction: null
5343
+ },
5344
+ touched: {
5345
+ classPropertyName: "touched",
5346
+ publicName: "touched",
5347
+ isSignal: true,
5348
+ isRequired: false,
5349
+ transformFunction: null
5350
+ },
5351
+ errors: {
5352
+ classPropertyName: "errors",
5353
+ publicName: "errors",
5354
+ isSignal: true,
5355
+ isRequired: false,
5356
+ transformFunction: null
5357
+ },
5358
+ name: {
5359
+ classPropertyName: "name",
5360
+ publicName: "name",
5361
+ isSignal: true,
5362
+ isRequired: false,
5363
+ transformFunction: null
5364
+ },
5365
+ id: {
5366
+ classPropertyName: "id",
5367
+ publicName: "id",
5368
+ isSignal: true,
5369
+ isRequired: false,
5370
+ transformFunction: null
5371
+ }
5372
+ },
5373
+ outputs: {
5374
+ value: "valueChange",
5375
+ changed: "changed",
5376
+ queried: "queried",
5377
+ focused: "focused",
5378
+ blurred: "blurred",
5379
+ touch: "touch"
5380
+ },
5381
+ host: {
5382
+ properties: { "attr.id": "null" },
5383
+ classAttribute: "block"
5384
+ },
5385
+ viewQueries: [{
5386
+ propertyName: "el",
5387
+ first: true,
5388
+ predicate: ["el"],
5389
+ descendants: true,
5390
+ isSignal: true
5391
+ }, {
5392
+ propertyName: "anchorEl",
5393
+ first: true,
5394
+ predicate: ["anchor"],
5395
+ descendants: true,
5396
+ isSignal: true
5397
+ }],
5398
+ ngImport: i0,
5399
+ template: `
5400
+ <bf-field
5401
+ [label]="label()"
5402
+ [helpText]="helpText()"
5403
+ [error]="errorText()"
5404
+ [required]="required()"
5405
+ [disabled]="disabled()"
5406
+ [controlId]="id()"
5407
+ >
5408
+ <div #anchor [class]="wrapperClasses()">
5409
+ <input
5410
+ #el
5411
+ type="text"
5412
+ role="combobox"
5413
+ autocomplete="off"
5414
+ aria-autocomplete="list"
5415
+ [id]="id()"
5416
+ [attr.name]="name() || null"
5417
+ [value]="text()"
5418
+ [placeholder]="placeholder()"
5419
+ [disabled]="disabled()"
5420
+ [readOnly]="readonly()"
5421
+ [required]="required()"
5422
+ [attr.aria-expanded]="core.isOpen() ? 'true' : 'false'"
5423
+ [attr.aria-controls]="core.controls()"
5424
+ [attr.aria-activedescendant]="core.active()?.id ?? null"
5425
+ [attr.aria-label]="ariaLabelAttr()"
5426
+ [attr.aria-labelledby]="ariaLabelledbyAttr()"
5427
+ [attr.aria-invalid]="showError() ? 'true' : null"
5428
+ [attr.aria-describedby]="describedBy()"
5429
+ [attr.aria-errormessage]="errorMessageId()"
5430
+ [class]="classes()"
5431
+ (input)="onInput(el)"
5432
+ (keydown)="onKeydown($event)"
5433
+ (click)="onClick()"
5434
+ (focus)="focused.emit($event)"
5435
+ (blur)="onBlur($event)"
5436
+ />
5437
+ </div>
5438
+ </bf-field>
5439
+ `,
5440
+ isInline: true,
5441
+ dependencies: [{
5442
+ kind: "component",
5443
+ type: BfFieldComponent,
5444
+ selector: "bf-field",
5445
+ inputs: [
5446
+ "label",
5447
+ "helpText",
5448
+ "error",
5449
+ "required",
5450
+ "disabled",
5451
+ "controlId"
5452
+ ]
5453
+ }]
5454
+ });
5455
+ };
5456
+ i0.ɵɵngDeclareClassMetadata({
5457
+ minVersion: "12.0.0",
5458
+ version: "22.1.7",
5459
+ ngImport: i0,
5460
+ type: BfComboboxComponent,
5461
+ decorators: [{
5462
+ type: Component,
5463
+ args: [{
5464
+ selector: "bf-combobox",
5465
+ imports: [BfFieldComponent],
5466
+ host: {
5467
+ class: "block",
5468
+ "[attr.id]": "null"
5469
+ },
5470
+ template: `
5471
+ <bf-field
5472
+ [label]="label()"
5473
+ [helpText]="helpText()"
5474
+ [error]="errorText()"
5475
+ [required]="required()"
5476
+ [disabled]="disabled()"
5477
+ [controlId]="id()"
5478
+ >
5479
+ <div #anchor [class]="wrapperClasses()">
5480
+ <input
5481
+ #el
5482
+ type="text"
5483
+ role="combobox"
5484
+ autocomplete="off"
5485
+ aria-autocomplete="list"
5486
+ [id]="id()"
5487
+ [attr.name]="name() || null"
5488
+ [value]="text()"
5489
+ [placeholder]="placeholder()"
5490
+ [disabled]="disabled()"
5491
+ [readOnly]="readonly()"
5492
+ [required]="required()"
5493
+ [attr.aria-expanded]="core.isOpen() ? 'true' : 'false'"
5494
+ [attr.aria-controls]="core.controls()"
5495
+ [attr.aria-activedescendant]="core.active()?.id ?? null"
5496
+ [attr.aria-label]="ariaLabelAttr()"
5497
+ [attr.aria-labelledby]="ariaLabelledbyAttr()"
5498
+ [attr.aria-invalid]="showError() ? 'true' : null"
5499
+ [attr.aria-describedby]="describedBy()"
5500
+ [attr.aria-errormessage]="errorMessageId()"
5501
+ [class]="classes()"
5502
+ (input)="onInput(el)"
5503
+ (keydown)="onKeydown($event)"
5504
+ (click)="onClick()"
5505
+ (focus)="focused.emit($event)"
5506
+ (blur)="onBlur($event)"
5507
+ />
5508
+ </div>
5509
+ </bf-field>
5510
+ `
5511
+ }]
5512
+ }],
5513
+ propDecorators: {
5514
+ value: [{
5515
+ type: i0.Input,
5516
+ args: [{
5517
+ isSignal: true,
5518
+ alias: "value",
5519
+ required: false
5520
+ }]
5521
+ }, {
5522
+ type: i0.Output,
5523
+ args: ["valueChange"]
5524
+ }],
5525
+ options: [{
5526
+ type: i0.Input,
5527
+ args: [{
5528
+ isSignal: true,
5529
+ alias: "options",
5530
+ required: true
5531
+ }]
5532
+ }],
5533
+ compareWith: [{
5534
+ type: i0.Input,
5535
+ args: [{
5536
+ isSignal: true,
5537
+ alias: "compareWith",
5538
+ required: false
5539
+ }]
5540
+ }],
5541
+ filter: [{
5542
+ type: i0.Input,
5543
+ args: [{
5544
+ isSignal: true,
5545
+ alias: "filter",
5546
+ required: false
5547
+ }]
5548
+ }],
5549
+ size: [{
5550
+ type: i0.Input,
5551
+ args: [{
5552
+ isSignal: true,
5553
+ alias: "size",
5554
+ required: false
5555
+ }]
5556
+ }],
5557
+ label: [{
5558
+ type: i0.Input,
5559
+ args: [{
5560
+ isSignal: true,
5561
+ alias: "label",
5562
+ required: false
5563
+ }]
5564
+ }],
5565
+ ariaLabel: [{
5566
+ type: i0.Input,
5567
+ args: [{
5568
+ isSignal: true,
5569
+ alias: "ariaLabel",
5570
+ required: false
5571
+ }]
5572
+ }],
5573
+ ariaLabelledby: [{
5574
+ type: i0.Input,
5575
+ args: [{
5576
+ isSignal: true,
5577
+ alias: "ariaLabelledby",
5578
+ required: false
5579
+ }]
5580
+ }],
5581
+ placeholder: [{
5582
+ type: i0.Input,
5583
+ args: [{
5584
+ isSignal: true,
5585
+ alias: "placeholder",
5586
+ required: false
5587
+ }]
5588
+ }],
5589
+ helpText: [{
5590
+ type: i0.Input,
5591
+ args: [{
5592
+ isSignal: true,
5593
+ alias: "helpText",
5594
+ required: false
5595
+ }]
5596
+ }],
5597
+ errorMessage: [{
5598
+ type: i0.Input,
5599
+ args: [{
5600
+ isSignal: true,
5601
+ alias: "errorMessage",
5602
+ required: false
5603
+ }]
5604
+ }],
5605
+ emptyLabel: [{
5606
+ type: i0.Input,
5607
+ args: [{
5608
+ isSignal: true,
5609
+ alias: "emptyLabel",
5610
+ required: false
5611
+ }]
5612
+ }],
5613
+ loading: [{
5614
+ type: i0.Input,
5615
+ args: [{
5616
+ isSignal: true,
5617
+ alias: "loading",
5618
+ required: false
5619
+ }]
5620
+ }],
5621
+ disabled: [{
5622
+ type: i0.Input,
5623
+ args: [{
5624
+ isSignal: true,
5625
+ alias: "disabled",
5626
+ required: false
5627
+ }]
5628
+ }],
5629
+ readonly: [{
5630
+ type: i0.Input,
5631
+ args: [{
5632
+ isSignal: true,
5633
+ alias: "readonly",
5634
+ required: false
5635
+ }]
5636
+ }],
5637
+ required: [{
5638
+ type: i0.Input,
5639
+ args: [{
5640
+ isSignal: true,
5641
+ alias: "required",
5642
+ required: false
5643
+ }]
5644
+ }],
5645
+ invalid: [{
5646
+ type: i0.Input,
5647
+ args: [{
5648
+ isSignal: true,
5649
+ alias: "invalid",
5650
+ required: false
5651
+ }]
5652
+ }],
5653
+ touched: [{
5654
+ type: i0.Input,
5655
+ args: [{
5656
+ isSignal: true,
5657
+ alias: "touched",
5658
+ required: false
5659
+ }]
5660
+ }],
5661
+ errors: [{
5662
+ type: i0.Input,
5663
+ args: [{
5664
+ isSignal: true,
5665
+ alias: "errors",
5666
+ required: false
5667
+ }]
5668
+ }],
5669
+ name: [{
5670
+ type: i0.Input,
5671
+ args: [{
5672
+ isSignal: true,
5673
+ alias: "name",
5674
+ required: false
5675
+ }]
5676
+ }],
5677
+ id: [{
5678
+ type: i0.Input,
5679
+ args: [{
5680
+ isSignal: true,
5681
+ alias: "id",
5682
+ required: false
5683
+ }]
5684
+ }],
5685
+ changed: [{
5686
+ type: i0.Output,
5687
+ args: ["changed"]
5688
+ }],
5689
+ queried: [{
5690
+ type: i0.Output,
5691
+ args: ["queried"]
5692
+ }],
5693
+ focused: [{
5694
+ type: i0.Output,
5695
+ args: ["focused"]
5696
+ }],
5697
+ blurred: [{
5698
+ type: i0.Output,
5699
+ args: ["blurred"]
5700
+ }],
5701
+ touch: [{
5702
+ type: i0.Output,
5703
+ args: ["touch"]
5704
+ }],
5705
+ el: [{
5706
+ type: i0.ViewChild,
5707
+ args: ["el", { isSignal: true }]
5708
+ }],
5709
+ anchorEl: [{
5710
+ type: i0.ViewChild,
5711
+ args: ["anchor", { isSignal: true }]
5712
+ }]
5713
+ }
4476
5714
  });
4477
- var BfComboboxComponent = class BfComboboxComponent {
4478
- value = model(null, ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
5715
+ const BOX = "flex w-full flex-wrap items-center gap-1 rounded-field border py-1 transition-colors focus-within:border-primary focus-within:ring-2 focus-within:ring-ring";
5716
+ const BOX_SIZE = {
5717
+ sm: "min-h-8 text-label-xs",
5718
+ md: "min-h-10 text-label-sm",
5719
+ lg: "min-h-12 text-label"
5720
+ };
5721
+ const MODE$1 = {
5722
+ editable: "cursor-text bg-surface text-text",
5723
+ readonly: "cursor-text bg-surface-muted text-text",
5724
+ disabled: "cursor-not-allowed border-surface-sunken bg-surface-muted text-text-disabled"
5725
+ };
5726
+ const INPUT = "h-6 min-w-16 flex-1 border-0 p-0 shadow-none focus:ring-0 focus:shadow-none bg-transparent outline-hidden placeholder:text-text-muted disabled:cursor-not-allowed";
5727
+ const TAG = "inline-flex h-6 max-w-full min-w-0 items-center rounded-control bg-primary-soft ps-2 text-label-xs font-bold text-primary-strong";
5728
+ const REMOVE = "relative inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-control hover:bg-primary-soft-hover active:bg-primary-soft-pressed focus-visible:outline-hidden focus-visible:ring-4 focus-visible:ring-ring pointer-coarse:after:absolute pointer-coarse:after:-inset-2.5 pointer-coarse:after:content-['']";
5729
+ const CROSS = "relative block size-2.5 before:absolute before:inset-x-0 before:top-1/2 before:h-[1.5px] before:-translate-y-1/2 before:rotate-45 before:bg-current before:content-[''] after:absolute after:inset-x-0 after:top-1/2 after:h-[1.5px] after:-translate-y-1/2 after:-rotate-45 after:bg-current after:content-['']";
5730
+ var BfMultiComboboxComponent = class BfMultiComboboxComponent {
5731
+ value = model([], ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
4479
5732
  options = input.required(...ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []);
4480
5733
  compareWith = input(Object.is, ...ngDevMode ? [{ debugName: "compareWith" }] : /* istanbul ignore next */ []);
4481
5734
  filter = input(labelContains, ...ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []);
@@ -4513,7 +5766,7 @@ var BfComboboxComponent = class BfComboboxComponent {
4513
5766
  });
4514
5767
  errors = input([], ...ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []);
4515
5768
  name = input("", ...ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []);
4516
- id = input(ɵuniqueId("bf-combobox"), ...ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []);
5769
+ id = input(ɵuniqueId("bf-multi-combobox"), ...ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []);
4517
5770
  changed = output();
4518
5771
  queried = output();
4519
5772
  focused = output();
@@ -4523,105 +5776,137 @@ var BfComboboxComponent = class BfComboboxComponent {
4523
5776
  anchorEl = viewChild.required("anchor", ...ngDevMode ? [{ debugName: "anchorEl" }] : /* istanbul ignore next */ []);
4524
5777
  defaults = inject(BF_DEFAULTS);
4525
5778
  labels = inject(BF_LABELS);
4526
- pickedLabel = signal(null, ...ngDevMode ? [{ debugName: "pickedLabel" }] : /* istanbul ignore next */ []);
5779
+ pickedLabels = signal([], ...ngDevMode ? [{ debugName: "pickedLabels" }] : /* istanbul ignore next */ []);
4527
5780
  resolvedSize = computed(() => this.size() ?? this.defaults.fieldSize, ...ngDevMode ? [{ debugName: "resolvedSize" }] : /* istanbul ignore next */ []);
4528
5781
  core = new ComboboxCore({
4529
5782
  options: this.options,
4530
5783
  filter: this.filter,
4531
- isSelected: (option) => sameValue(this.compareWith(), option.value, this.value()),
4532
- pick: (item) => this.pick(item),
5784
+ isSelected: (option) => this.indexOf(option.value) >= 0,
5785
+ pick: (item) => this.toggle(item),
4533
5786
  anchor: () => this.anchorEl().nativeElement,
4534
5787
  panel: BfComboboxListboxComponent,
4535
5788
  listboxLabel: computed(() => this.label() || this.ariaLabel() || null),
4536
5789
  listboxLabelledby: computed(() => this.label() ? null : this.ariaLabelledby() || null),
4537
- multiple: false,
5790
+ multiple: true,
4538
5791
  loading: this.loading,
4539
5792
  emptyText: computed(() => this.emptyLabel() || this.labels.comboboxEmpty),
4540
5793
  size: this.resolvedSize,
4541
5794
  queried: (query) => this.queried.emit(query)
4542
5795
  });
4543
- valueLabel = computed(() => {
4544
- const value = this.value();
4545
- if (value === null) return "";
5796
+ tags = computed(() => {
4546
5797
  const compare = this.compareWith();
4547
- const option = this.options().find((o) => compare(o.value, value));
4548
- const picked = this.pickedLabel();
4549
- return option?.label ?? (picked && compare(picked.value, value) ? picked.label : "");
4550
- }, ...ngDevMode ? [{ debugName: "valueLabel" }] : /* istanbul ignore next */ []);
4551
- text = computed(() => this.core.query() ?? this.valueLabel(), ...ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []);
5798
+ return this.value().map((value) => {
5799
+ return {
5800
+ value,
5801
+ label: this.options().find((o) => compare(o.value, value))?.label ?? this.pickedLabels().find((t) => compare(t.value, value))?.label ?? String(value)
5802
+ };
5803
+ });
5804
+ }, ...ngDevMode ? [{ debugName: "tags" }] : /* istanbul ignore next */ []);
5805
+ editable = computed(() => !this.disabled() && !this.readonly(), ...ngDevMode ? [{ debugName: "editable" }] : /* istanbul ignore next */ []);
4552
5806
  errorText = computed(() => ɵfieldErrorText(this.errorMessage(), this.touched(), this.errors(), this.labels.validation), ...ngDevMode ? [{ debugName: "errorText" }] : /* istanbul ignore next */ []);
4553
5807
  showError = computed(() => ɵfieldShowsError(this.errorText(), this.invalid(), this.touched()), ...ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []);
4554
5808
  describedBy = computed(() => ɵfieldDescribedBy(this.id(), this.errorText(), this.helpText()), ...ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []);
4555
5809
  errorMessageId = computed(() => ɵfieldErrorMessageId(this.id(), this.errorText(), this.showError()), ...ngDevMode ? [{ debugName: "errorMessageId" }] : /* istanbul ignore next */ []);
4556
5810
  ariaLabelAttr = computed(() => this.label() ? null : this.ariaLabel() || null, ...ngDevMode ? [{ debugName: "ariaLabelAttr" }] : /* istanbul ignore next */ []);
4557
5811
  ariaLabelledbyAttr = computed(() => this.label() ? null : this.ariaLabelledby() || null, ...ngDevMode ? [{ debugName: "ariaLabelledbyAttr" }] : /* istanbul ignore next */ []);
4558
- wrapperClasses = computed(() => `${COMBOBOX_CHEVRON} ${COMBOBOX_CHEVRON_INSET[this.resolvedSize()]}`, ...ngDevMode ? [{ debugName: "wrapperClasses" }] : /* istanbul ignore next */ []);
4559
- classes = computed(() => [
4560
- COMBOBOX_FIELD_BASE,
4561
- ɵFIELD_BOX[this.resolvedSize()],
4562
- ɵFIELD_PADDING_START[this.resolvedSize()].text,
4563
- COMBOBOX_PADDING_END[this.resolvedSize()],
4564
- this.showError() ? "border-danger-border hover:border-danger" : "border-border-strong hover:border-text-muted"
4565
- ].join(" "), ...ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []);
5812
+ boxClasses = computed(() => {
5813
+ const size = this.resolvedSize();
5814
+ const mode = this.disabled() ? "disabled" : this.readonly() ? "readonly" : "editable";
5815
+ return [
5816
+ BOX,
5817
+ BOX_SIZE[size],
5818
+ ɵFIELD_PADDING_START[size].text,
5819
+ COMBOBOX_PADDING_END[size],
5820
+ COMBOBOX_CHEVRON,
5821
+ COMBOBOX_CHEVRON_INSET[size],
5822
+ MODE$1[mode],
5823
+ mode === "disabled" ? "" : this.showError() ? "border-danger-border hover:not-focus-within:border-danger" : "border-border-strong hover:not-focus-within:border-text-muted"
5824
+ ].filter(Boolean).join(" ");
5825
+ }, ...ngDevMode ? [{ debugName: "boxClasses" }] : /* istanbul ignore next */ []);
5826
+ inputClasses = INPUT;
5827
+ tagClasses = TAG;
5828
+ removeClasses = REMOVE;
5829
+ crossClasses = CROSS;
5830
+ constructor() {
5831
+ afterRenderEffect({ write: () => {
5832
+ this.tags();
5833
+ this.core.reposition();
5834
+ } });
5835
+ }
4566
5836
  focus(options) {
4567
5837
  this.el().nativeElement.focus(options);
4568
5838
  }
4569
5839
  onInput(el) {
4570
- if (this.readonly()) return;
4571
- this.core.typed(el.value);
5840
+ if (this.editable()) this.core.typed(el.value);
4572
5841
  }
4573
5842
  onClick() {
4574
- if (!this.readonly() && !this.disabled()) this.core.open();
5843
+ if (this.editable()) this.core.open();
5844
+ }
5845
+ onBoxMousedown(event) {
5846
+ const target = event.target;
5847
+ if (!(target instanceof Element) || target.closest("button, input") || this.disabled()) return;
5848
+ event.preventDefault();
5849
+ this.focus();
5850
+ this.onClick();
4575
5851
  }
4576
5852
  onKeydown(event) {
4577
- if (this.readonly()) return;
4578
- if (this.core.keydown(event)) return;
4579
- if (event.key === "Escape" && !this.core.isOpen() && this.core.query() !== null) this.resetQuery(null);
5853
+ if (!this.editable() || this.core.keydown(event)) return;
5854
+ const query = this.core.query() ?? "";
5855
+ if (event.key === "Backspace" && query === "" && this.value().length) {
5856
+ event.preventDefault();
5857
+ this.remove(this.value().length - 1);
5858
+ } else if (event.key === "Escape" && !this.core.isOpen() && query !== "") this.resetQuery(null);
4580
5859
  }
4581
5860
  onBlur(event) {
4582
5861
  this.core.close();
4583
- const query = this.core.query();
4584
- if (query !== null) {
4585
- const typed = foldText(query.trim());
4586
- const match = this.options().find((o) => !o.disabled && foldText(o.label) === typed);
4587
- if (!typed && this.value() !== null) this.set(null);
4588
- else if (typed && match && !sameValue(this.compareWith(), match.value, this.value())) this.set(match.value, match.label);
4589
- this.resetQuery(null);
4590
- }
5862
+ this.resetQuery(null);
4591
5863
  this.blurred.emit(event);
4592
5864
  this.touch.emit();
4593
5865
  }
4594
- pick(item) {
4595
- if (!sameValue(this.compareWith(), item.option.value, this.value())) this.set(item.option.value, item.option.label);
4596
- this.resetQuery(null);
4597
- this.core.close();
5866
+ remove(index) {
5867
+ const next = this.value().filter((_, i) => i !== index);
5868
+ this.value.set(next);
5869
+ this.changed.emit(next);
5870
+ this.focus();
4598
5871
  }
4599
- set(value, label = "") {
4600
- this.pickedLabel.set(value === null ? null : {
4601
- value,
4602
- label
4603
- });
4604
- this.value.set(value);
4605
- this.changed.emit(value);
5872
+ toggle(item) {
5873
+ const at = this.indexOf(item.option.value);
5874
+ let next;
5875
+ if (at >= 0) next = this.value().filter((_, i) => i !== at);
5876
+ else {
5877
+ next = [...this.value(), item.option.value];
5878
+ const tag = {
5879
+ value: item.option.value,
5880
+ label: item.option.label
5881
+ };
5882
+ this.pickedLabels.update((tags) => [...tags, tag]);
5883
+ }
5884
+ this.value.set(next);
5885
+ this.changed.emit(next);
5886
+ this.resetQuery("");
5887
+ }
5888
+ indexOf(value) {
5889
+ const compare = this.compareWith();
5890
+ return this.value().findIndex((v) => compare(value, v));
4606
5891
  }
4607
5892
  resetQuery(query) {
4608
5893
  this.core.setQuery(query);
4609
- this.el().nativeElement.value = this.text();
5894
+ this.el().nativeElement.value = "";
4610
5895
  }
4611
5896
  static ɵfac = i0.ɵɵngDeclareFactory({
4612
5897
  minVersion: "12.0.0",
4613
5898
  version: "22.1.7",
4614
5899
  ngImport: i0,
4615
- type: BfComboboxComponent,
5900
+ type: BfMultiComboboxComponent,
4616
5901
  deps: [],
4617
5902
  target: i0.ɵɵFactoryTarget.Component
4618
5903
  });
4619
5904
  static ɵcmp = i0.ɵɵngDeclareComponent({
4620
- minVersion: "17.2.0",
5905
+ minVersion: "17.0.0",
4621
5906
  version: "22.1.7",
4622
- type: BfComboboxComponent,
5907
+ type: BfMultiComboboxComponent,
4623
5908
  isStandalone: true,
4624
- selector: "bf-combobox",
5909
+ selector: "bf-multi-combobox",
4625
5910
  inputs: {
4626
5911
  value: {
4627
5912
  classPropertyName: "value",
@@ -4806,7 +6091,25 @@ var BfComboboxComponent = class BfComboboxComponent {
4806
6091
  [disabled]="disabled()"
4807
6092
  [controlId]="id()"
4808
6093
  >
4809
- <div #anchor [class]="wrapperClasses()">
6094
+ <!-- A press on the box outside the tags puts the caret in the input. -->
6095
+ <div #anchor [class]="boxClasses()" (mousedown)="onBoxMousedown($event)">
6096
+ @for (tag of tags(); track $index) {
6097
+ <span data-chip [class]="tagClasses">
6098
+ <span data-label class="min-w-0 truncate">{{ tag.label }}</span>
6099
+ @if (editable()) {
6100
+ <button
6101
+ type="button"
6102
+ [class]="removeClasses"
6103
+ [attr.aria-label]="labels.comboboxRemove(tag.label)"
6104
+ (click)="remove($index)"
6105
+ >
6106
+ <span aria-hidden="true" [class]="crossClasses"></span>
6107
+ </button>
6108
+ } @else {
6109
+ <span class="pe-2"></span>
6110
+ }
6111
+ </span>
6112
+ }
4810
6113
  <input
4811
6114
  #el
4812
6115
  type="text"
@@ -4815,11 +6118,11 @@ var BfComboboxComponent = class BfComboboxComponent {
4815
6118
  aria-autocomplete="list"
4816
6119
  [id]="id()"
4817
6120
  [attr.name]="name() || null"
4818
- [value]="text()"
4819
- [placeholder]="placeholder()"
6121
+ [value]="core.query() ?? ''"
6122
+ [placeholder]="tags().length ? '' : placeholder()"
4820
6123
  [disabled]="disabled()"
4821
6124
  [readOnly]="readonly()"
4822
- [required]="required()"
6125
+ [required]="required() && !tags().length"
4823
6126
  [attr.aria-expanded]="core.isOpen() ? 'true' : 'false'"
4824
6127
  [attr.aria-controls]="core.controls()"
4825
6128
  [attr.aria-activedescendant]="core.active()?.id ?? null"
@@ -4828,7 +6131,7 @@ var BfComboboxComponent = class BfComboboxComponent {
4828
6131
  [attr.aria-invalid]="showError() ? 'true' : null"
4829
6132
  [attr.aria-describedby]="describedBy()"
4830
6133
  [attr.aria-errormessage]="errorMessageId()"
4831
- [class]="classes()"
6134
+ [class]="inputClasses"
4832
6135
  (input)="onInput(el)"
4833
6136
  (keydown)="onKeydown($event)"
4834
6137
  (click)="onClick()"
@@ -4858,11 +6161,11 @@ i0.ɵɵngDeclareClassMetadata({
4858
6161
  minVersion: "12.0.0",
4859
6162
  version: "22.1.7",
4860
6163
  ngImport: i0,
4861
- type: BfComboboxComponent,
6164
+ type: BfMultiComboboxComponent,
4862
6165
  decorators: [{
4863
6166
  type: Component,
4864
6167
  args: [{
4865
- selector: "bf-combobox",
6168
+ selector: "bf-multi-combobox",
4866
6169
  imports: [BfFieldComponent],
4867
6170
  host: {
4868
6171
  class: "block",
@@ -4877,7 +6180,25 @@ i0.ɵɵngDeclareClassMetadata({
4877
6180
  [disabled]="disabled()"
4878
6181
  [controlId]="id()"
4879
6182
  >
4880
- <div #anchor [class]="wrapperClasses()">
6183
+ <!-- A press on the box outside the tags puts the caret in the input. -->
6184
+ <div #anchor [class]="boxClasses()" (mousedown)="onBoxMousedown($event)">
6185
+ @for (tag of tags(); track $index) {
6186
+ <span data-chip [class]="tagClasses">
6187
+ <span data-label class="min-w-0 truncate">{{ tag.label }}</span>
6188
+ @if (editable()) {
6189
+ <button
6190
+ type="button"
6191
+ [class]="removeClasses"
6192
+ [attr.aria-label]="labels.comboboxRemove(tag.label)"
6193
+ (click)="remove($index)"
6194
+ >
6195
+ <span aria-hidden="true" [class]="crossClasses"></span>
6196
+ </button>
6197
+ } @else {
6198
+ <span class="pe-2"></span>
6199
+ }
6200
+ </span>
6201
+ }
4881
6202
  <input
4882
6203
  #el
4883
6204
  type="text"
@@ -4886,11 +6207,11 @@ i0.ɵɵngDeclareClassMetadata({
4886
6207
  aria-autocomplete="list"
4887
6208
  [id]="id()"
4888
6209
  [attr.name]="name() || null"
4889
- [value]="text()"
4890
- [placeholder]="placeholder()"
6210
+ [value]="core.query() ?? ''"
6211
+ [placeholder]="tags().length ? '' : placeholder()"
4891
6212
  [disabled]="disabled()"
4892
6213
  [readOnly]="readonly()"
4893
- [required]="required()"
6214
+ [required]="required() && !tags().length"
4894
6215
  [attr.aria-expanded]="core.isOpen() ? 'true' : 'false'"
4895
6216
  [attr.aria-controls]="core.controls()"
4896
6217
  [attr.aria-activedescendant]="core.active()?.id ?? null"
@@ -4899,7 +6220,7 @@ i0.ɵɵngDeclareClassMetadata({
4899
6220
  [attr.aria-invalid]="showError() ? 'true' : null"
4900
6221
  [attr.aria-describedby]="describedBy()"
4901
6222
  [attr.aria-errormessage]="errorMessageId()"
4902
- [class]="classes()"
6223
+ [class]="inputClasses"
4903
6224
  (input)="onInput(el)"
4904
6225
  (keydown)="onKeydown($event)"
4905
6226
  (click)="onClick()"
@@ -4911,6 +6232,7 @@ i0.ɵɵngDeclareClassMetadata({
4911
6232
  `
4912
6233
  }]
4913
6234
  }],
6235
+ ctorParameters: () => [],
4914
6236
  propDecorators: {
4915
6237
  value: [{
4916
6238
  type: i0.Input,
@@ -5113,26 +6435,26 @@ i0.ɵɵngDeclareClassMetadata({
5113
6435
  }]
5114
6436
  }
5115
6437
  });
5116
- const BOX = "flex w-full flex-wrap items-center gap-1 rounded-field border py-1 transition-colors focus-within:border-primary focus-within:ring-2 focus-within:ring-ring";
5117
- const BOX_SIZE = {
5118
- sm: "min-h-8 text-label-xs",
5119
- md: "min-h-10 text-label-sm",
5120
- lg: "min-h-12 text-label"
5121
- };
6438
+ const BASE = "flex w-full items-center rounded-field border text-text select-none transition-colors focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-ring aria-disabled:cursor-not-allowed aria-disabled:border-surface-sunken aria-disabled:bg-surface-muted aria-disabled:text-text-disabled forced-colors:aria-disabled:border-[GrayText] forced-colors:aria-disabled:text-[GrayText]";
5122
6439
  const MODE = {
5123
- editable: "cursor-text bg-surface text-text",
5124
- readonly: "cursor-text bg-surface-muted text-text",
5125
- disabled: "cursor-not-allowed border-surface-sunken bg-surface-muted text-text-disabled"
6440
+ editable: "cursor-pointer bg-surface",
6441
+ readonly: "cursor-default bg-surface-muted"
5126
6442
  };
5127
- const INPUT = "h-6 min-w-16 flex-1 bg-transparent outline-hidden placeholder:text-text-muted disabled:cursor-not-allowed";
5128
- const TAG = "inline-flex h-6 max-w-full min-w-0 items-center rounded-control bg-primary-soft ps-2 text-label-xs font-bold text-primary-strong";
5129
- const REMOVE = "relative inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-control hover:bg-primary-soft-hover active:bg-primary-soft-pressed focus-visible:outline-hidden focus-visible:ring-4 focus-visible:ring-ring pointer-coarse:after:absolute pointer-coarse:after:-inset-2.5 pointer-coarse:after:content-['']";
5130
- const CROSS = "relative block size-2.5 before:absolute before:inset-x-0 before:top-1/2 before:h-[1.5px] before:-translate-y-1/2 before:rotate-45 before:bg-current before:content-[''] after:absolute after:inset-x-0 after:top-1/2 after:h-[1.5px] after:-translate-y-1/2 after:-rotate-45 after:bg-current after:content-['']";
5131
- var BfMultiComboboxComponent = class BfMultiComboboxComponent {
5132
- value = model([], ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
6443
+ const SCROLL_KEYS = /* @__PURE__ */ new Set([
6444
+ " ",
6445
+ "ArrowUp",
6446
+ "ArrowDown",
6447
+ "Home",
6448
+ "End",
6449
+ "PageUp",
6450
+ "PageDown"
6451
+ ]);
6452
+ const PAGE = 10;
6453
+ const TYPEAHEAD_MS = 500;
6454
+ var BfSelectComponent = class BfSelectComponent {
6455
+ value = model(null, ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
5133
6456
  options = input.required(...ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []);
5134
6457
  compareWith = input(Object.is, ...ngDevMode ? [{ debugName: "compareWith" }] : /* istanbul ignore next */ []);
5135
- filter = input(labelContains, ...ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []);
5136
6458
  size = input(void 0, ...ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []);
5137
6459
  label = input("", ...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
5138
6460
  ariaLabel = input("", ...ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []);
@@ -5140,11 +6462,6 @@ var BfMultiComboboxComponent = class BfMultiComboboxComponent {
5140
6462
  placeholder = input("", ...ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []);
5141
6463
  helpText = input("", ...ngDevMode ? [{ debugName: "helpText" }] : /* istanbul ignore next */ []);
5142
6464
  errorMessage = input("", ...ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []);
5143
- emptyLabel = input("", ...ngDevMode ? [{ debugName: "emptyLabel" }] : /* istanbul ignore next */ []);
5144
- loading = input(false, {
5145
- ...ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {},
5146
- transform: booleanAttribute
5147
- });
5148
6465
  disabled = input(false, {
5149
6466
  ...ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {},
5150
6467
  transform: booleanAttribute
@@ -5167,147 +6484,190 @@ var BfMultiComboboxComponent = class BfMultiComboboxComponent {
5167
6484
  });
5168
6485
  errors = input([], ...ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []);
5169
6486
  name = input("", ...ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []);
5170
- id = input(ɵuniqueId("bf-multi-combobox"), ...ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []);
6487
+ id = input(ɵuniqueId("bf-select"), ...ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []);
5171
6488
  changed = output();
5172
- queried = output();
5173
6489
  focused = output();
5174
6490
  blurred = output();
5175
6491
  touch = output();
5176
- el = viewChild.required("el", ...ngDevMode ? [{ debugName: "el" }] : /* istanbul ignore next */ []);
5177
- anchorEl = viewChild.required("anchor", ...ngDevMode ? [{ debugName: "anchorEl" }] : /* istanbul ignore next */ []);
6492
+ fieldEl = viewChild("field", ...ngDevMode ? [{ debugName: "fieldEl" }] : /* istanbul ignore next */ []);
6493
+ anchorEl = viewChild("anchor", ...ngDevMode ? [{ debugName: "anchorEl" }] : /* istanbul ignore next */ []);
6494
+ nativeSelect = viewChild("native", ...ngDevMode ? [{ debugName: "nativeSelect" }] : /* istanbul ignore next */ []);
5178
6495
  defaults = inject(BF_DEFAULTS);
5179
6496
  labels = inject(BF_LABELS);
5180
- pickedLabels = signal([], ...ngDevMode ? [{ debugName: "pickedLabels" }] : /* istanbul ignore next */ []);
6497
+ document = inject(DOCUMENT);
6498
+ coarse = signal(false, ...ngDevMode ? [{ debugName: "coarse" }] : /* istanbul ignore next */ []);
5181
6499
  resolvedSize = computed(() => this.size() ?? this.defaults.fieldSize, ...ngDevMode ? [{ debugName: "resolvedSize" }] : /* istanbul ignore next */ []);
6500
+ selectedIndex = computed(() => ɵchoiceIndex(this.options(), this.value(), this.compareWith()), ...ngDevMode ? [{ debugName: "selectedIndex" }] : /* istanbul ignore next */ []);
6501
+ selectedLabel = computed(() => this.options()[this.selectedIndex()]?.label ?? null, ...ngDevMode ? [{ debugName: "selectedLabel" }] : /* istanbul ignore next */ []);
6502
+ formKey = computed(() => {
6503
+ const index = this.selectedIndex();
6504
+ const option = this.options()[index];
6505
+ return option ? ɵchoiceKey(option.value, index) : "";
6506
+ }, ...ngDevMode ? [{ debugName: "formKey" }] : /* istanbul ignore next */ []);
6507
+ listOptions = computed(() => this.options().map(({ value, label, disabled }) => ({
6508
+ value,
6509
+ label,
6510
+ disabled
6511
+ })), ...ngDevMode ? [{ debugName: "listOptions" }] : /* istanbul ignore next */ []);
5182
6512
  core = new ComboboxCore({
5183
- options: this.options,
5184
- filter: this.filter,
5185
- isSelected: (option) => this.indexOf(option.value) >= 0,
5186
- pick: (item) => this.toggle(item),
6513
+ options: this.listOptions,
6514
+ filter: signal(null),
6515
+ isSelected: (option) => this.listOptions()[this.selectedIndex()] === option,
6516
+ pick: (item) => this.pick(item),
5187
6517
  anchor: () => this.anchorEl().nativeElement,
5188
6518
  panel: BfComboboxListboxComponent,
5189
6519
  listboxLabel: computed(() => this.label() || this.ariaLabel() || null),
5190
6520
  listboxLabelledby: computed(() => this.label() ? null : this.ariaLabelledby() || null),
5191
- multiple: true,
5192
- loading: this.loading,
5193
- emptyText: computed(() => this.emptyLabel() || this.labels.comboboxEmpty),
6521
+ multiple: false,
6522
+ loading: signal(false),
6523
+ emptyText: signal(""),
5194
6524
  size: this.resolvedSize,
5195
- queried: (query) => this.queried.emit(query)
6525
+ queried: () => void 0
5196
6526
  });
5197
- tags = computed(() => {
5198
- const compare = this.compareWith();
5199
- return this.value().map((value) => {
5200
- return {
5201
- value,
5202
- label: this.options().find((o) => compare(o.value, value))?.label ?? this.pickedLabels().find((t) => compare(t.value, value))?.label ?? String(value)
5203
- };
5204
- });
5205
- }, ...ngDevMode ? [{ debugName: "tags" }] : /* istanbul ignore next */ []);
5206
- editable = computed(() => !this.disabled() && !this.readonly(), ...ngDevMode ? [{ debugName: "editable" }] : /* istanbul ignore next */ []);
5207
6527
  errorText = computed(() => ɵfieldErrorText(this.errorMessage(), this.touched(), this.errors(), this.labels.validation), ...ngDevMode ? [{ debugName: "errorText" }] : /* istanbul ignore next */ []);
5208
6528
  showError = computed(() => ɵfieldShowsError(this.errorText(), this.invalid(), this.touched()), ...ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []);
5209
6529
  describedBy = computed(() => ɵfieldDescribedBy(this.id(), this.errorText(), this.helpText()), ...ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []);
5210
6530
  errorMessageId = computed(() => ɵfieldErrorMessageId(this.id(), this.errorText(), this.showError()), ...ngDevMode ? [{ debugName: "errorMessageId" }] : /* istanbul ignore next */ []);
5211
6531
  ariaLabelAttr = computed(() => this.label() ? null : this.ariaLabel() || null, ...ngDevMode ? [{ debugName: "ariaLabelAttr" }] : /* istanbul ignore next */ []);
5212
6532
  ariaLabelledbyAttr = computed(() => this.label() ? null : this.ariaLabelledby() || null, ...ngDevMode ? [{ debugName: "ariaLabelledbyAttr" }] : /* istanbul ignore next */ []);
5213
- boxClasses = computed(() => {
5214
- const size = this.resolvedSize();
5215
- const mode = this.disabled() ? "disabled" : this.readonly() ? "readonly" : "editable";
5216
- return [
5217
- BOX,
5218
- BOX_SIZE[size],
5219
- ɵFIELD_PADDING_START[size].text,
5220
- COMBOBOX_PADDING_END[size],
5221
- COMBOBOX_CHEVRON,
5222
- COMBOBOX_CHEVRON_INSET[size],
5223
- MODE[mode],
5224
- mode === "disabled" ? "" : this.showError() ? "border-danger-border hover:not-focus-within:border-danger" : "border-border-strong hover:not-focus-within:border-text-muted"
5225
- ].filter(Boolean).join(" ");
5226
- }, ...ngDevMode ? [{ debugName: "boxClasses" }] : /* istanbul ignore next */ []);
5227
- inputClasses = INPUT;
5228
- tagClasses = TAG;
5229
- removeClasses = REMOVE;
5230
- crossClasses = CROSS;
6533
+ wrapperClasses = computed(() => `${COMBOBOX_CHEVRON} ${COMBOBOX_CHEVRON_INSET[this.resolvedSize()]}`, ...ngDevMode ? [{ debugName: "wrapperClasses" }] : /* istanbul ignore next */ []);
6534
+ classes = computed(() => [
6535
+ BASE,
6536
+ ɵFIELD_BOX[this.resolvedSize()],
6537
+ ɵFIELD_PADDING_START[this.resolvedSize()].text,
6538
+ COMBOBOX_PADDING_END[this.resolvedSize()],
6539
+ MODE[this.readonly() ? "readonly" : "editable"],
6540
+ this.showError() ? "border-danger-border hover:border-danger" : "border-border-strong hover:border-text-muted",
6541
+ this.selectedIndex() === -1 ? "text-text-muted" : ""
6542
+ ].filter(Boolean).join(" "), ...ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []);
6543
+ typeahead = "";
6544
+ typedAt = 0;
5231
6545
  constructor() {
5232
- afterRenderEffect({ write: () => {
5233
- this.tags();
5234
- this.core.reposition();
5235
- } });
6546
+ const media = this.document.defaultView?.matchMedia?.("(pointer: coarse)");
6547
+ if (media) {
6548
+ this.coarse.set(media.matches);
6549
+ const onChange = (event) => this.coarse.set(event.matches);
6550
+ media.addEventListener?.("change", onChange);
6551
+ inject(DestroyRef).onDestroy(() => media.removeEventListener?.("change", onChange));
6552
+ }
6553
+ effect(() => {
6554
+ if (this.coarse() || this.disabled() || this.readonly()) this.core.close();
6555
+ });
5236
6556
  }
5237
6557
  focus(options) {
5238
- this.el().nativeElement.focus(options);
6558
+ const native = this.nativeSelect();
6559
+ if (native) native.focus(options);
6560
+ else this.fieldEl()?.nativeElement.focus(options);
5239
6561
  }
5240
- onInput(el) {
5241
- if (this.editable()) this.core.typed(el.value);
6562
+ onHostClick(event) {
6563
+ if (!this.coarse() && !this.disabled() && event.target instanceof Element && event.target.closest("label")) this.focus();
5242
6564
  }
5243
6565
  onClick() {
5244
- if (this.editable()) this.core.open();
5245
- }
5246
- onBoxMousedown(event) {
5247
- const target = event.target;
5248
- if (!(target instanceof Element) || target.closest("button, input") || this.disabled()) return;
5249
- event.preventDefault();
5250
- this.focus();
5251
- this.onClick();
6566
+ if (this.core.isOpen()) this.core.close();
6567
+ else this.openList();
5252
6568
  }
5253
6569
  onKeydown(event) {
5254
- if (!this.editable() || this.core.keydown(event)) return;
5255
- const query = this.core.query() ?? "";
5256
- if (event.key === "Backspace" && query === "" && this.value().length) {
6570
+ if (this.disabled()) return;
6571
+ if (this.readonly()) {
6572
+ if (SCROLL_KEYS.has(event.key)) event.preventDefault();
6573
+ return;
6574
+ }
6575
+ const key = event.key;
6576
+ if (key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey && (key !== " " || this.typing())) {
6577
+ this.search(key);
5257
6578
  event.preventDefault();
5258
- this.remove(this.value().length - 1);
5259
- } else if (event.key === "Escape" && !this.core.isOpen() && query !== "") this.resetQuery(null);
6579
+ return;
6580
+ }
6581
+ if (!this.core.isOpen()) {
6582
+ if (key === "Enter" || key === " " || key === "ArrowDown" || key === "ArrowUp") {
6583
+ this.openList();
6584
+ event.preventDefault();
6585
+ }
6586
+ return;
6587
+ }
6588
+ switch (key) {
6589
+ case " ":
6590
+ this.pickActive();
6591
+ event.preventDefault();
6592
+ return;
6593
+ case "Tab":
6594
+ this.pickActive();
6595
+ this.core.close();
6596
+ return;
6597
+ case "PageUp":
6598
+ case "PageDown":
6599
+ this.page(key === "PageDown" ? PAGE : -10);
6600
+ event.preventDefault();
6601
+ return;
6602
+ default: this.core.keydown(event);
6603
+ }
5260
6604
  }
5261
6605
  onBlur(event) {
5262
6606
  this.core.close();
5263
- this.resetQuery(null);
5264
6607
  this.blurred.emit(event);
5265
6608
  this.touch.emit();
5266
6609
  }
5267
- remove(index) {
5268
- const next = this.value().filter((_, i) => i !== index);
5269
- this.value.set(next);
5270
- this.changed.emit(next);
5271
- this.focus();
6610
+ openList() {
6611
+ if (this.disabled() || this.readonly() || this.core.isOpen()) return;
6612
+ const items = this.enabledItems();
6613
+ if (!items.length) return;
6614
+ this.core.open();
6615
+ const selected = items.find((item) => this.core.host.isSelected(item.option));
6616
+ this.core.activate(selected ?? items[0]);
5272
6617
  }
5273
- toggle(item) {
5274
- const at = this.indexOf(item.option.value);
5275
- let next;
5276
- if (at >= 0) next = this.value().filter((_, i) => i !== at);
5277
- else {
5278
- next = [...this.value(), item.option.value];
5279
- const tag = {
5280
- value: item.option.value,
5281
- label: item.option.label
5282
- };
5283
- this.pickedLabels.update((tags) => [...tags, tag]);
5284
- }
5285
- this.value.set(next);
5286
- this.changed.emit(next);
5287
- this.resetQuery("");
6618
+ pick(item) {
6619
+ if (this.listOptions().indexOf(item.option) !== this.selectedIndex()) {
6620
+ this.value.set(item.option.value);
6621
+ this.changed.emit(item.option.value);
6622
+ }
6623
+ this.core.close();
5288
6624
  }
5289
- indexOf(value) {
5290
- const compare = this.compareWith();
5291
- return this.value().findIndex((v) => compare(value, v));
6625
+ pickActive() {
6626
+ const active = this.core.active();
6627
+ if (active) this.pick(active);
6628
+ }
6629
+ page(step) {
6630
+ const items = this.enabledItems();
6631
+ const at = items.findIndex((item) => item.index === this.core.active()?.index);
6632
+ const item = items[Math.min(Math.max((at < 0 ? 0 : at) + step, 0), items.length - 1)];
6633
+ if (item) this.core.activate(item);
6634
+ }
6635
+ typing() {
6636
+ return this.typeahead !== "" && Date.now() - this.typedAt <= TYPEAHEAD_MS;
6637
+ }
6638
+ search(key) {
6639
+ this.typeahead = (this.typing() ? this.typeahead : "") + key;
6640
+ this.typedAt = Date.now();
6641
+ const typed = foldText(this.typeahead);
6642
+ const repeated = [...typed].every((char) => char === typed[0]);
6643
+ const query = repeated ? typed[0] : typed;
6644
+ const items = this.enabledItems();
6645
+ const from = this.core.isOpen() ? this.core.active() : items.find((item) => this.core.host.isSelected(item.option));
6646
+ const at = from ? items.findIndex((item) => item.index === from.index) : -1;
6647
+ const start = at < 0 ? 0 : repeated ? at + 1 : at;
6648
+ const match = [...items.slice(start), ...items.slice(0, start)].find((item) => foldText(item.option.label).startsWith(query));
6649
+ if (match) {
6650
+ this.openList();
6651
+ this.core.activate(match);
6652
+ }
5292
6653
  }
5293
- resetQuery(query) {
5294
- this.core.setQuery(query);
5295
- this.el().nativeElement.value = "";
6654
+ enabledItems() {
6655
+ return this.core.items().filter((item) => !item.option.disabled);
5296
6656
  }
5297
6657
  static ɵfac = i0.ɵɵngDeclareFactory({
5298
6658
  minVersion: "12.0.0",
5299
6659
  version: "22.1.7",
5300
6660
  ngImport: i0,
5301
- type: BfMultiComboboxComponent,
6661
+ type: BfSelectComponent,
5302
6662
  deps: [],
5303
6663
  target: i0.ɵɵFactoryTarget.Component
5304
6664
  });
5305
6665
  static ɵcmp = i0.ɵɵngDeclareComponent({
5306
6666
  minVersion: "17.0.0",
5307
6667
  version: "22.1.7",
5308
- type: BfMultiComboboxComponent,
6668
+ type: BfSelectComponent,
5309
6669
  isStandalone: true,
5310
- selector: "bf-multi-combobox",
6670
+ selector: "bf-select",
5311
6671
  inputs: {
5312
6672
  value: {
5313
6673
  classPropertyName: "value",
@@ -5330,13 +6690,6 @@ var BfMultiComboboxComponent = class BfMultiComboboxComponent {
5330
6690
  isRequired: false,
5331
6691
  transformFunction: null
5332
6692
  },
5333
- filter: {
5334
- classPropertyName: "filter",
5335
- publicName: "filter",
5336
- isSignal: true,
5337
- isRequired: false,
5338
- transformFunction: null
5339
- },
5340
6693
  size: {
5341
6694
  classPropertyName: "size",
5342
6695
  publicName: "size",
@@ -5386,20 +6739,6 @@ var BfMultiComboboxComponent = class BfMultiComboboxComponent {
5386
6739
  isRequired: false,
5387
6740
  transformFunction: null
5388
6741
  },
5389
- emptyLabel: {
5390
- classPropertyName: "emptyLabel",
5391
- publicName: "emptyLabel",
5392
- isSignal: true,
5393
- isRequired: false,
5394
- transformFunction: null
5395
- },
5396
- loading: {
5397
- classPropertyName: "loading",
5398
- publicName: "loading",
5399
- isSignal: true,
5400
- isRequired: false,
5401
- transformFunction: null
5402
- },
5403
6742
  disabled: {
5404
6743
  classPropertyName: "disabled",
5405
6744
  publicName: "disabled",
@@ -5460,87 +6799,116 @@ var BfMultiComboboxComponent = class BfMultiComboboxComponent {
5460
6799
  outputs: {
5461
6800
  value: "valueChange",
5462
6801
  changed: "changed",
5463
- queried: "queried",
5464
6802
  focused: "focused",
5465
6803
  blurred: "blurred",
5466
6804
  touch: "touch"
5467
6805
  },
5468
6806
  host: {
6807
+ listeners: { "click": "onHostClick($event)" },
5469
6808
  properties: { "attr.id": "null" },
5470
6809
  classAttribute: "block"
5471
6810
  },
5472
- viewQueries: [{
5473
- propertyName: "el",
5474
- first: true,
5475
- predicate: ["el"],
5476
- descendants: true,
5477
- isSignal: true
5478
- }, {
5479
- propertyName: "anchorEl",
5480
- first: true,
5481
- predicate: ["anchor"],
5482
- descendants: true,
5483
- isSignal: true
5484
- }],
6811
+ viewQueries: [
6812
+ {
6813
+ propertyName: "fieldEl",
6814
+ first: true,
6815
+ predicate: ["field"],
6816
+ descendants: true,
6817
+ isSignal: true
6818
+ },
6819
+ {
6820
+ propertyName: "anchorEl",
6821
+ first: true,
6822
+ predicate: ["anchor"],
6823
+ descendants: true,
6824
+ isSignal: true
6825
+ },
6826
+ {
6827
+ propertyName: "nativeSelect",
6828
+ first: true,
6829
+ predicate: ["native"],
6830
+ descendants: true,
6831
+ isSignal: true
6832
+ }
6833
+ ],
5485
6834
  ngImport: i0,
5486
6835
  template: `
5487
- <bf-field
5488
- [label]="label()"
5489
- [helpText]="helpText()"
5490
- [error]="errorText()"
5491
- [required]="required()"
5492
- [disabled]="disabled()"
5493
- [controlId]="id()"
5494
- >
5495
- <!-- A press on the box outside the tags puts the caret in the input. -->
5496
- <div #anchor [class]="boxClasses()" (mousedown)="onBoxMousedown($event)">
5497
- @for (tag of tags(); track $index) {
5498
- <span data-chip [class]="tagClasses">
5499
- <span data-label class="min-w-0 truncate">{{ tag.label }}</span>
5500
- @if (editable()) {
5501
- <button
5502
- type="button"
5503
- [class]="removeClasses"
5504
- [attr.aria-label]="labels.comboboxRemove(tag.label)"
5505
- (click)="remove($index)"
5506
- >
5507
- <span aria-hidden="true" [class]="crossClasses"></span>
5508
- </button>
5509
- } @else {
5510
- <span class="pe-2"></span>
5511
- }
5512
- </span>
6836
+ @if (coarse()) {
6837
+ <bf-native-select
6838
+ #native
6839
+ [options]="options()"
6840
+ [compareWith]="compareWith()"
6841
+ [size]="size()"
6842
+ [label]="label()"
6843
+ [ariaLabel]="ariaLabel()"
6844
+ [ariaLabelledby]="ariaLabelledby()"
6845
+ [placeholder]="placeholder()"
6846
+ [helpText]="helpText()"
6847
+ [errorMessage]="errorMessage()"
6848
+ [disabled]="disabled()"
6849
+ [readonly]="readonly()"
6850
+ [required]="required()"
6851
+ [invalid]="invalid()"
6852
+ [touched]="touched()"
6853
+ [errors]="errors()"
6854
+ [name]="name()"
6855
+ [id]="id()"
6856
+ [(value)]="value"
6857
+ (changed)="changed.emit($event)"
6858
+ (focused)="focused.emit($event)"
6859
+ (blurred)="blurred.emit($event)"
6860
+ (touch)="touch.emit()"
6861
+ />
6862
+ } @else {
6863
+ <bf-field
6864
+ #box
6865
+ [label]="label()"
6866
+ [helpText]="helpText()"
6867
+ [error]="errorText()"
6868
+ [required]="required()"
6869
+ [disabled]="disabled()"
6870
+ [controlId]="id()"
6871
+ >
6872
+ <div #anchor [class]="wrapperClasses()">
6873
+ <div
6874
+ #field
6875
+ role="combobox"
6876
+ aria-haspopup="listbox"
6877
+ [id]="id()"
6878
+ [attr.tabindex]="disabled() ? null : 0"
6879
+ [attr.aria-disabled]="disabled() ? 'true' : null"
6880
+ [attr.aria-readonly]="readonly() ? 'true' : null"
6881
+ [attr.aria-required]="required() ? 'true' : null"
6882
+ [attr.aria-expanded]="core.isOpen() ? 'true' : 'false'"
6883
+ [attr.aria-controls]="core.controls()"
6884
+ [attr.aria-activedescendant]="core.active()?.id ?? null"
6885
+ [attr.aria-label]="ariaLabelAttr()"
6886
+ [attr.aria-labelledby]="label() ? box.labelId() : ariaLabelledbyAttr()"
6887
+ [attr.aria-invalid]="showError() ? 'true' : null"
6888
+ [attr.aria-describedby]="describedBy()"
6889
+ [attr.aria-errormessage]="errorMessageId()"
6890
+ [attr.data-placeholder]="selectedIndex() === -1 ? '' : null"
6891
+ [class]="classes()"
6892
+ (click)="onClick()"
6893
+ (keydown)="onKeydown($event)"
6894
+ (focus)="focused.emit($event)"
6895
+ (blur)="onBlur($event)"
6896
+ >
6897
+ <span class="min-w-0 truncate">{{ selectedLabel() ?? placeholder() }}</span>
6898
+ </div>
6899
+ </div>
6900
+ @if (name()) {
6901
+ <input type="hidden" [name]="name()" [value]="formKey()" [disabled]="disabled()" />
5513
6902
  }
5514
- <input
5515
- #el
5516
- type="text"
5517
- role="combobox"
5518
- autocomplete="off"
5519
- aria-autocomplete="list"
5520
- [id]="id()"
5521
- [attr.name]="name() || null"
5522
- [value]="core.query() ?? ''"
5523
- [placeholder]="tags().length ? '' : placeholder()"
5524
- [disabled]="disabled()"
5525
- [readOnly]="readonly()"
5526
- [required]="required() && !tags().length"
5527
- [attr.aria-expanded]="core.isOpen() ? 'true' : 'false'"
5528
- [attr.aria-controls]="core.controls()"
5529
- [attr.aria-activedescendant]="core.active()?.id ?? null"
5530
- [attr.aria-label]="ariaLabelAttr()"
5531
- [attr.aria-labelledby]="ariaLabelledbyAttr()"
5532
- [attr.aria-invalid]="showError() ? 'true' : null"
5533
- [attr.aria-describedby]="describedBy()"
5534
- [attr.aria-errormessage]="errorMessageId()"
5535
- [class]="inputClasses"
5536
- (input)="onInput(el)"
5537
- (keydown)="onKeydown($event)"
5538
- (click)="onClick()"
5539
- (focus)="focused.emit($event)"
5540
- (blur)="onBlur($event)"
5541
- />
5542
- </div>
5543
- </bf-field>
6903
+ <!-- The option labels for BfSelectHarness, readable while the list cannot open (disabled,
6904
+ read-only); hidden, so never rendered or announced. -->
6905
+ <span hidden data-bf-select-options>
6906
+ @for (option of options(); track $index) {
6907
+ <span [attr.data-disabled]="option.disabled ? '' : null">{{ option.label }}</span>
6908
+ }
6909
+ </span>
6910
+ </bf-field>
6911
+ }
5544
6912
  `,
5545
6913
  isInline: true,
5546
6914
  dependencies: [{
@@ -5555,6 +6923,37 @@ var BfMultiComboboxComponent = class BfMultiComboboxComponent {
5555
6923
  "disabled",
5556
6924
  "controlId"
5557
6925
  ]
6926
+ }, {
6927
+ kind: "component",
6928
+ type: ɵBfNativeSelectComponent,
6929
+ selector: "bf-native-select",
6930
+ inputs: [
6931
+ "value",
6932
+ "options",
6933
+ "compareWith",
6934
+ "size",
6935
+ "label",
6936
+ "ariaLabel",
6937
+ "ariaLabelledby",
6938
+ "placeholder",
6939
+ "helpText",
6940
+ "errorMessage",
6941
+ "disabled",
6942
+ "readonly",
6943
+ "required",
6944
+ "invalid",
6945
+ "touched",
6946
+ "errors",
6947
+ "name",
6948
+ "id"
6949
+ ],
6950
+ outputs: [
6951
+ "valueChange",
6952
+ "changed",
6953
+ "focused",
6954
+ "blurred",
6955
+ "touch"
6956
+ ]
5558
6957
  }]
5559
6958
  });
5560
6959
  };
@@ -5562,74 +6961,94 @@ i0.ɵɵngDeclareClassMetadata({
5562
6961
  minVersion: "12.0.0",
5563
6962
  version: "22.1.7",
5564
6963
  ngImport: i0,
5565
- type: BfMultiComboboxComponent,
6964
+ type: BfSelectComponent,
5566
6965
  decorators: [{
5567
6966
  type: Component,
5568
6967
  args: [{
5569
- selector: "bf-multi-combobox",
5570
- imports: [BfFieldComponent],
6968
+ selector: "bf-select",
6969
+ imports: [BfFieldComponent, ɵBfNativeSelectComponent],
5571
6970
  host: {
5572
6971
  class: "block",
5573
- "[attr.id]": "null"
6972
+ "[attr.id]": "null",
6973
+ "(click)": "onHostClick($event)"
5574
6974
  },
5575
6975
  template: `
5576
- <bf-field
5577
- [label]="label()"
5578
- [helpText]="helpText()"
5579
- [error]="errorText()"
5580
- [required]="required()"
5581
- [disabled]="disabled()"
5582
- [controlId]="id()"
5583
- >
5584
- <!-- A press on the box outside the tags puts the caret in the input. -->
5585
- <div #anchor [class]="boxClasses()" (mousedown)="onBoxMousedown($event)">
5586
- @for (tag of tags(); track $index) {
5587
- <span data-chip [class]="tagClasses">
5588
- <span data-label class="min-w-0 truncate">{{ tag.label }}</span>
5589
- @if (editable()) {
5590
- <button
5591
- type="button"
5592
- [class]="removeClasses"
5593
- [attr.aria-label]="labels.comboboxRemove(tag.label)"
5594
- (click)="remove($index)"
5595
- >
5596
- <span aria-hidden="true" [class]="crossClasses"></span>
5597
- </button>
5598
- } @else {
5599
- <span class="pe-2"></span>
5600
- }
5601
- </span>
6976
+ @if (coarse()) {
6977
+ <bf-native-select
6978
+ #native
6979
+ [options]="options()"
6980
+ [compareWith]="compareWith()"
6981
+ [size]="size()"
6982
+ [label]="label()"
6983
+ [ariaLabel]="ariaLabel()"
6984
+ [ariaLabelledby]="ariaLabelledby()"
6985
+ [placeholder]="placeholder()"
6986
+ [helpText]="helpText()"
6987
+ [errorMessage]="errorMessage()"
6988
+ [disabled]="disabled()"
6989
+ [readonly]="readonly()"
6990
+ [required]="required()"
6991
+ [invalid]="invalid()"
6992
+ [touched]="touched()"
6993
+ [errors]="errors()"
6994
+ [name]="name()"
6995
+ [id]="id()"
6996
+ [(value)]="value"
6997
+ (changed)="changed.emit($event)"
6998
+ (focused)="focused.emit($event)"
6999
+ (blurred)="blurred.emit($event)"
7000
+ (touch)="touch.emit()"
7001
+ />
7002
+ } @else {
7003
+ <bf-field
7004
+ #box
7005
+ [label]="label()"
7006
+ [helpText]="helpText()"
7007
+ [error]="errorText()"
7008
+ [required]="required()"
7009
+ [disabled]="disabled()"
7010
+ [controlId]="id()"
7011
+ >
7012
+ <div #anchor [class]="wrapperClasses()">
7013
+ <div
7014
+ #field
7015
+ role="combobox"
7016
+ aria-haspopup="listbox"
7017
+ [id]="id()"
7018
+ [attr.tabindex]="disabled() ? null : 0"
7019
+ [attr.aria-disabled]="disabled() ? 'true' : null"
7020
+ [attr.aria-readonly]="readonly() ? 'true' : null"
7021
+ [attr.aria-required]="required() ? 'true' : null"
7022
+ [attr.aria-expanded]="core.isOpen() ? 'true' : 'false'"
7023
+ [attr.aria-controls]="core.controls()"
7024
+ [attr.aria-activedescendant]="core.active()?.id ?? null"
7025
+ [attr.aria-label]="ariaLabelAttr()"
7026
+ [attr.aria-labelledby]="label() ? box.labelId() : ariaLabelledbyAttr()"
7027
+ [attr.aria-invalid]="showError() ? 'true' : null"
7028
+ [attr.aria-describedby]="describedBy()"
7029
+ [attr.aria-errormessage]="errorMessageId()"
7030
+ [attr.data-placeholder]="selectedIndex() === -1 ? '' : null"
7031
+ [class]="classes()"
7032
+ (click)="onClick()"
7033
+ (keydown)="onKeydown($event)"
7034
+ (focus)="focused.emit($event)"
7035
+ (blur)="onBlur($event)"
7036
+ >
7037
+ <span class="min-w-0 truncate">{{ selectedLabel() ?? placeholder() }}</span>
7038
+ </div>
7039
+ </div>
7040
+ @if (name()) {
7041
+ <input type="hidden" [name]="name()" [value]="formKey()" [disabled]="disabled()" />
5602
7042
  }
5603
- <input
5604
- #el
5605
- type="text"
5606
- role="combobox"
5607
- autocomplete="off"
5608
- aria-autocomplete="list"
5609
- [id]="id()"
5610
- [attr.name]="name() || null"
5611
- [value]="core.query() ?? ''"
5612
- [placeholder]="tags().length ? '' : placeholder()"
5613
- [disabled]="disabled()"
5614
- [readOnly]="readonly()"
5615
- [required]="required() && !tags().length"
5616
- [attr.aria-expanded]="core.isOpen() ? 'true' : 'false'"
5617
- [attr.aria-controls]="core.controls()"
5618
- [attr.aria-activedescendant]="core.active()?.id ?? null"
5619
- [attr.aria-label]="ariaLabelAttr()"
5620
- [attr.aria-labelledby]="ariaLabelledbyAttr()"
5621
- [attr.aria-invalid]="showError() ? 'true' : null"
5622
- [attr.aria-describedby]="describedBy()"
5623
- [attr.aria-errormessage]="errorMessageId()"
5624
- [class]="inputClasses"
5625
- (input)="onInput(el)"
5626
- (keydown)="onKeydown($event)"
5627
- (click)="onClick()"
5628
- (focus)="focused.emit($event)"
5629
- (blur)="onBlur($event)"
5630
- />
5631
- </div>
5632
- </bf-field>
7043
+ <!-- The option labels for BfSelectHarness, readable while the list cannot open (disabled,
7044
+ read-only); hidden, so never rendered or announced. -->
7045
+ <span hidden data-bf-select-options>
7046
+ @for (option of options(); track $index) {
7047
+ <span [attr.data-disabled]="option.disabled ? '' : null">{{ option.label }}</span>
7048
+ }
7049
+ </span>
7050
+ </bf-field>
7051
+ }
5633
7052
  `
5634
7053
  }]
5635
7054
  }],
@@ -5662,14 +7081,6 @@ i0.ɵɵngDeclareClassMetadata({
5662
7081
  required: false
5663
7082
  }]
5664
7083
  }],
5665
- filter: [{
5666
- type: i0.Input,
5667
- args: [{
5668
- isSignal: true,
5669
- alias: "filter",
5670
- required: false
5671
- }]
5672
- }],
5673
7084
  size: [{
5674
7085
  type: i0.Input,
5675
7086
  args: [{
@@ -5726,22 +7137,6 @@ i0.ɵɵngDeclareClassMetadata({
5726
7137
  required: false
5727
7138
  }]
5728
7139
  }],
5729
- emptyLabel: [{
5730
- type: i0.Input,
5731
- args: [{
5732
- isSignal: true,
5733
- alias: "emptyLabel",
5734
- required: false
5735
- }]
5736
- }],
5737
- loading: [{
5738
- type: i0.Input,
5739
- args: [{
5740
- isSignal: true,
5741
- alias: "loading",
5742
- required: false
5743
- }]
5744
- }],
5745
7140
  disabled: [{
5746
7141
  type: i0.Input,
5747
7142
  args: [{
@@ -5810,10 +7205,6 @@ i0.ɵɵngDeclareClassMetadata({
5810
7205
  type: i0.Output,
5811
7206
  args: ["changed"]
5812
7207
  }],
5813
- queried: [{
5814
- type: i0.Output,
5815
- args: ["queried"]
5816
- }],
5817
7208
  focused: [{
5818
7209
  type: i0.Output,
5819
7210
  args: ["focused"]
@@ -5826,16 +7217,26 @@ i0.ɵɵngDeclareClassMetadata({
5826
7217
  type: i0.Output,
5827
7218
  args: ["touch"]
5828
7219
  }],
5829
- el: [{
7220
+ fieldEl: [{
5830
7221
  type: i0.ViewChild,
5831
- args: ["el", { isSignal: true }]
7222
+ args: ["field", { isSignal: true }]
5832
7223
  }],
5833
7224
  anchorEl: [{
5834
7225
  type: i0.ViewChild,
5835
- args: ["anchor", { isSignal: true }]
7226
+ args: ["anchor", { isSignal: true }]
7227
+ }],
7228
+ nativeSelect: [{
7229
+ type: i0.ViewChild,
7230
+ args: ["native", { isSignal: true }]
5836
7231
  }]
5837
7232
  }
5838
7233
  });
7234
+ function provideBfSelect() {
7235
+ return makeEnvironmentProviders([{
7236
+ provide: BF_TABLE_SORT_SELECT,
7237
+ useValue: BfSelectComponent
7238
+ }]);
7239
+ }
5839
7240
  const DATE_KINDS = [
5840
7241
  "day",
5841
7242
  "month",
@@ -6171,7 +7572,7 @@ function localDate(iso) {
6171
7572
  return date;
6172
7573
  }
6173
7574
  var BfDateTimeFieldComponent = class BfDateTimeFieldComponent {
6174
- overlay = inject(Overlay);
7575
+ environment = inject(EnvironmentInjector);
6175
7576
  document = inject(DOCUMENT);
6176
7577
  injector = inject(Injector);
6177
7578
  viewContainer = inject(ViewContainerRef);
@@ -6379,6 +7780,7 @@ var BfDateTimeFieldComponent = class BfDateTimeFieldComponent {
6379
7780
  }, ...ngDevMode ? [{ debugName: "timeOptionViews" }] : /* istanbul ignore next */ []);
6380
7781
  activeOptionId = computed(() => this.timeOptionViews().find((option) => option.active)?.id ?? null, ...ngDevMode ? [{ debugName: "activeOptionId" }] : /* istanbul ignore next */ []);
6381
7782
  anchored = null;
7783
+ motion = new AnchoredMotion();
6382
7784
  subscriptions = [];
6383
7785
  stopNavigation = null;
6384
7786
  constructor() {
@@ -6389,7 +7791,10 @@ var BfDateTimeFieldComponent = class BfDateTimeFieldComponent {
6389
7791
  media.addEventListener?.("change", onChange);
6390
7792
  inject(DestroyRef).onDestroy(() => media.removeEventListener?.("change", onChange));
6391
7793
  }
6392
- inject(DestroyRef).onDestroy(() => this.teardown());
7794
+ inject(DestroyRef).onDestroy(() => {
7795
+ this.teardown(false);
7796
+ this.motion.flush();
7797
+ });
6393
7798
  }
6394
7799
  focus(options) {
6395
7800
  const native = this.hostEl.querySelector("input");
@@ -6404,15 +7809,16 @@ var BfDateTimeFieldComponent = class BfDateTimeFieldComponent {
6404
7809
  openPicker() {
6405
7810
  const box = this.box()?.nativeElement;
6406
7811
  if (this.anchored || !box || !this.picker() || this.disabled() || this.readonly()) return;
7812
+ this.motion.flush();
6407
7813
  const direction = getComputedStyle(box).direction === "rtl" ? "rtl" : "ltr";
6408
- const anchored = createAnchoredOverlay(this.overlay, box, {
7814
+ const anchored = createAnchoredOverlay(this.environment, box, {
6409
7815
  positions: blockPositions("below-start"),
6410
7816
  direction,
6411
7817
  panelClass: ["bf-date-time-panel"]
6412
7818
  });
6413
7819
  this.anchored = anchored;
6414
7820
  this.activeTime.set(this.initialTime());
6415
- anchored.ref.attach(new TemplatePortal(this.panel(), this.viewContainer, void 0, this.injector));
7821
+ const view = anchored.ref.attach(new TemplatePortal(this.panel(), this.viewContainer, void 0, this.injector));
6416
7822
  const pane = anchored.ref.overlayElement;
6417
7823
  this.subscriptions = [anchored.ref.keydownEvents().subscribe((event) => {
6418
7824
  if (event.key === "Escape") {
@@ -6428,11 +7834,12 @@ var BfDateTimeFieldComponent = class BfDateTimeFieldComponent {
6428
7834
  pane.addEventListener("keydown", (event) => this.tabbed(event, pane));
6429
7835
  this.stopNavigation = watchPathChange(this.location, () => this.closePicker(false));
6430
7836
  this.pickerOpen.set(true);
7837
+ this.motion.enter(anchored.ref, box, view);
6431
7838
  afterNextRender(() => this.focusPanel(pane), { injector: this.injector });
6432
7839
  }
6433
7840
  closePicker(restoreFocus = true) {
6434
7841
  if (!this.anchored) return;
6435
- this.teardown();
7842
+ this.teardown(true);
6436
7843
  this.pickerOpen.set(false);
6437
7844
  if (restoreFocus) this.trigger()?.nativeElement.focus({ preventScroll: true });
6438
7845
  else if (!this.hostEl.contains(this.document.activeElement)) this.markLeft();
@@ -6710,13 +8117,16 @@ var BfDateTimeFieldComponent = class BfDateTimeFieldComponent {
6710
8117
  if (event.shiftKey) event.preventDefault();
6711
8118
  this.closePicker(true);
6712
8119
  }
6713
- teardown() {
8120
+ teardown(animate) {
6714
8121
  this.subscriptions.forEach((s) => s.unsubscribe());
6715
8122
  this.subscriptions = [];
6716
8123
  this.stopNavigation?.();
6717
8124
  this.stopNavigation = null;
6718
- this.anchored?.ref.dispose();
8125
+ const ref = this.anchored?.ref;
6719
8126
  this.anchored = null;
8127
+ const box = this.box()?.nativeElement;
8128
+ if (ref && animate && box) this.motion.leave(ref, box);
8129
+ else ref?.dispose();
6720
8130
  }
6721
8131
  static ɵfac = i0.ɵɵngDeclareFactory({
6722
8132
  minVersion: "12.0.0",
@@ -9198,6 +10608,639 @@ i0.ɵɵngDeclareClassMetadata({
9198
10608
  }]
9199
10609
  }
9200
10610
  });
9201
- export { BF_DESKTOP_TOAST_DEFAULTS, BF_DIALOG_DATA, BF_OVERLAY_DATA, BF_OVERLAY_HOST, BF_SIDE_PANEL_DATA, BfComboboxComponent, BfComponentOverlayRef, BfDateInputComponent, BfDateTimeInputComponent, BfDesktopToastComponent, BfDesktopToastService, BfDialogComponent, BfDialogRef, BfDialogService, BfMenuButtonComponent, BfMenuComponent, BfMenuDividerDirective, BfMenuGroupDirective, BfMenuItemCheckboxDirective, BfMenuItemDirective, BfMenuItemRadioDirective, BfMenuTriggerDirective, BfMultiComboboxComponent, BfOverlayRef, BfOverlayService, BfOverlayStack, BfPopoverComponent, BfPopoverTriggerDirective, BfSidePanelComponent, BfSidePanelRef, BfSidePanelService, BfSidebarToggleComponent, BfThemeMenuButtonComponent, BfTimeInputComponent, BfToastRef, BfTooltipComponent, BfTooltipDirective, OVERLAY_DURATION, OVERLAY_EASING, OVERLAY_REDUCED_DURATION, fadeDown, fadeIn, fadeOut, fadeUp, prefersReducedMotion, provideBfDesktopToast, scaleIn, slideDown, slideInFromEnd, slideOutToEnd, slideUp, ɵBfToastQueue, labelPanelByHeading as ɵlabelPanelByHeading, watchPanelHeading as ɵwatchPanelHeading };
10611
+ function onScreen(rect, view) {
10612
+ if (!view || rect.width <= 0 || rect.height <= 0) return false;
10613
+ return rect.bottom > 0 && rect.right > 0 && rect.top < view.innerHeight && rect.left < view.innerWidth;
10614
+ }
10615
+ function zoomedFrame(frame, origin, radius) {
10616
+ if (frame.width <= 0 || frame.height <= 0 || origin.width <= 0 || origin.height <= 0) return null;
10617
+ const scale = Math.max(origin.width / frame.width, origin.height / frame.height);
10618
+ const dx = origin.left + origin.width / 2 - (frame.left + frame.width / 2);
10619
+ const dy = origin.top + origin.height / 2 - (frame.top + frame.height / 2);
10620
+ const insetX = Math.max(0, (frame.width - origin.width / scale) / 2);
10621
+ const insetY = Math.max(0, (frame.height - origin.height / scale) / 2);
10622
+ const round = radius > 0 ? radius / scale : 0;
10623
+ return {
10624
+ transform: `translate(${dx}px, ${dy}px) scale(${scale})`,
10625
+ clipPath: `inset(${insetY}px ${insetX}px round ${round}px)`
10626
+ };
10627
+ }
10628
+ const OPEN_FRAME = {
10629
+ transform: "none",
10630
+ clipPath: "inset(0px 0px round 0px)"
10631
+ };
10632
+ function fade(el, entering, duration, easing) {
10633
+ return animateOrSettle(el, entering ? [{ opacity: 0 }, { opacity: 1 }] : [{ opacity: 1 }, { opacity: 0 }], {
10634
+ duration,
10635
+ easing,
10636
+ fill: entering ? "backwards" : "forwards"
10637
+ });
10638
+ }
10639
+ function playViewerMotion(parts, origin, entering, reduced) {
10640
+ if (reduced) return fade(parts.host, entering, 150, "linear");
10641
+ const view = parts.host.ownerDocument.defaultView;
10642
+ const originRect = origin?.isConnected ? origin.getBoundingClientRect() : null;
10643
+ const zoomed = origin && originRect && onScreen(originRect, view) ? zoomedFrame(parts.frame.getBoundingClientRect(), originRect, radiusOf(origin, view)) : null;
10644
+ if (!zoomed) return fade(parts.host, entering, 260, OVERLAY_EASING);
10645
+ for (const el of parts.chrome) fade(el, entering, 260, OVERLAY_EASING);
10646
+ return animateOrSettle(parts.frame, entering ? [zoomed, OPEN_FRAME] : [OPEN_FRAME, zoomed], {
10647
+ duration: 260,
10648
+ easing: OVERLAY_EASING,
10649
+ fill: entering ? "backwards" : "forwards"
10650
+ });
10651
+ }
10652
+ function radiusOf(origin, view) {
10653
+ const value = view?.getComputedStyle(origin).borderTopLeftRadius ?? "";
10654
+ const parsed = Number.parseFloat(value);
10655
+ return Number.isFinite(parsed) ? parsed : 0;
10656
+ }
10657
+ var BfImageViewerRef = class {
10658
+ overlayRef;
10659
+ id;
10660
+ images;
10661
+ afterClosed;
10662
+ closed;
10663
+ current;
10664
+ index;
10665
+ constructor(overlayRef, images, start) {
10666
+ this.overlayRef = overlayRef;
10667
+ this.id = overlayRef.id;
10668
+ this.images = images;
10669
+ this.current = signal(start, ...ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []);
10670
+ this.index = this.current.asReadonly();
10671
+ this.afterClosed = overlayRef.afterClosed.then(() => void 0);
10672
+ this.closed = overlayRef.closed;
10673
+ }
10674
+ next() {
10675
+ this.goTo(this.current() + 1);
10676
+ }
10677
+ previous() {
10678
+ this.goTo(this.current() - 1);
10679
+ }
10680
+ goTo(index) {
10681
+ const count = this.images.length;
10682
+ if (count === 0 || !Number.isFinite(index)) return;
10683
+ this.current.set((Math.trunc(index) % count + count) % count);
10684
+ }
10685
+ close() {
10686
+ this.overlayRef.close();
10687
+ }
10688
+ };
10689
+ const BF_IMAGE_VIEWER_CONTEXT = new InjectionToken("BF_IMAGE_VIEWER_CONTEXT");
10690
+ const KEEP_OPEN = "[data-frame], button, a, input, select, textarea, label, [data-caption], [data-actions]";
10691
+ const FRAME = "relative max-h-full max-w-full";
10692
+ const FRAME_SIZED = "aspect-(--bf-image-viewer-ratio) w-[min(100cqw,calc(100cqh*var(--bf-image-viewer-ratio)))]";
10693
+ const NAV = "pointer-events-auto absolute top-1/2 -translate-y-1/2";
10694
+ var BfImageViewerComponent = class BfImageViewerComponent {
10695
+ labels = inject(BF_LABELS);
10696
+ ref = inject(BfImageViewerRef);
10697
+ context = inject(BF_IMAGE_VIEWER_CONTEXT);
10698
+ injector = inject(Injector);
10699
+ document = inject(DOCUMENT);
10700
+ host = inject(ElementRef);
10701
+ closeIcon = X;
10702
+ previousIcon = ChevronLeft;
10703
+ nextIcon = ChevronRight;
10704
+ previousClasses = `${NAV} start-3`;
10705
+ nextClasses = `${NAV} end-3`;
10706
+ frame = viewChild.required("frame", ...ngDevMode ? [{ debugName: "frame" }] : /* istanbul ignore next */ []);
10707
+ scrim = viewChild.required("scrim", ...ngDevMode ? [{ debugName: "scrim" }] : /* istanbul ignore next */ []);
10708
+ top = viewChild.required("top", ...ngDevMode ? [{ debugName: "top" }] : /* istanbul ignore next */ []);
10709
+ bottom = viewChild.required("bottom", ...ngDevMode ? [{ debugName: "bottom" }] : /* istanbul ignore next */ []);
10710
+ statuses = signal(/* @__PURE__ */ new Map(), ...ngDevMode ? [{ debugName: "statuses" }] : /* istanbul ignore next */ []);
10711
+ ratios = signal(/* @__PURE__ */ new Map(), ...ngDevMode ? [{ debugName: "ratios" }] : /* istanbul ignore next */ []);
10712
+ count = computed(() => this.ref.images.length, ...ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []);
10713
+ index = this.ref.index;
10714
+ image = computed(() => this.ref.images[this.index()] ?? {
10715
+ src: "",
10716
+ label: ""
10717
+ }, ...ngDevMode ? [{ debugName: "image" }] : /* istanbul ignore next */ []);
10718
+ status = computed(() => this.statuses().get(this.index()) ?? "pending", ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
10719
+ shown = computed(() => this.status() === "error" ? [] : [{
10720
+ index: this.index(),
10721
+ image: this.image()
10722
+ }], ...ngDevMode ? [{ debugName: "shown" }] : /* istanbul ignore next */ []);
10723
+ counter = computed(() => this.labels.imageViewerPosition(this.index() + 1, this.count()), ...ngDevMode ? [{ debugName: "counter" }] : /* istanbul ignore next */ []);
10724
+ stepped = linkedSignal({
10725
+ ...ngDevMode ? { debugName: "stepped" } : /* istanbul ignore next */ {},
10726
+ source: this.index,
10727
+ computation: (_, previous) => previous !== void 0
10728
+ });
10729
+ announcement = computed(() => {
10730
+ if (!this.stepped()) return "";
10731
+ const alt = this.image().label;
10732
+ return alt ? `${this.counter()}, ${alt}` : this.counter();
10733
+ }, ...ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []);
10734
+ placeholder = computed(() => {
10735
+ if (this.status() === "loaded") return null;
10736
+ const img = thumbnailImage(this.originFor(this.index()));
10737
+ if (!img) return null;
10738
+ const src = img.currentSrc ? img.currentSrc : img.getAttribute("src");
10739
+ return src === "" ? null : src;
10740
+ }, ...ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []);
10741
+ ratio = computed(() => {
10742
+ const own = this.ratios().get(this.index());
10743
+ if (own) return own;
10744
+ const img = thumbnailImage(this.originFor(this.index()));
10745
+ return img && img.naturalWidth > 0 && img.naturalHeight > 0 ? img.naturalWidth / img.naturalHeight : null;
10746
+ }, ...ngDevMode ? [{ debugName: "ratio" }] : /* istanbul ignore next */ []);
10747
+ frameClasses = computed(() => this.ratio() ? `${FRAME} ${FRAME_SIZED}` : `${FRAME} size-full`, ...ngDevMode ? [{ debugName: "frameClasses" }] : /* istanbul ignore next */ []);
10748
+ actionsContext = computed(() => ({
10749
+ $implicit: this.image(),
10750
+ index: this.index(),
10751
+ ref: this.ref
10752
+ }), ...ngDevMode ? [{ debugName: "actionsContext" }] : /* istanbul ignore next */ []);
10753
+ constructor() {
10754
+ this.context.register(this);
10755
+ }
10756
+ enter() {
10757
+ return playViewerMotion(this.parts(), this.originFor(this.index()), true, this.reduced());
10758
+ }
10759
+ exit() {
10760
+ return playViewerMotion(this.parts(), this.originFor(this.index()), false, this.reduced());
10761
+ }
10762
+ imageLoaded(event, index) {
10763
+ const img = event.target;
10764
+ if (img instanceof HTMLImageElement && img.naturalWidth > 0 && img.naturalHeight > 0) {
10765
+ const ratio = img.naturalWidth / img.naturalHeight;
10766
+ this.ratios.update((map) => new Map(map).set(index, ratio));
10767
+ }
10768
+ this.statuses.update((map) => new Map(map).set(index, "loaded"));
10769
+ }
10770
+ imageFailed(index) {
10771
+ this.statuses.update((map) => new Map(map).set(index, "error"));
10772
+ }
10773
+ keydown(event) {
10774
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;
10775
+ const target = event.target;
10776
+ if (target instanceof HTMLElement && (target.isContentEditable || target.matches("input, textarea, select"))) return;
10777
+ const rtl = this.document.defaultView?.getComputedStyle(this.host.nativeElement).direction === "rtl";
10778
+ const forward = rtl ? "ArrowLeft" : "ArrowRight";
10779
+ const backward = rtl ? "ArrowRight" : "ArrowLeft";
10780
+ switch (event.key) {
10781
+ case forward:
10782
+ this.ref.next();
10783
+ break;
10784
+ case backward:
10785
+ this.ref.previous();
10786
+ break;
10787
+ case "Home":
10788
+ this.ref.goTo(0);
10789
+ break;
10790
+ case "End":
10791
+ this.ref.goTo(this.count() - 1);
10792
+ break;
10793
+ default: return;
10794
+ }
10795
+ event.preventDefault();
10796
+ }
10797
+ clicked(event) {
10798
+ const target = event.target;
10799
+ if (target instanceof Element && !target.closest(KEEP_OPEN)) this.ref.close();
10800
+ }
10801
+ originFor(index) {
10802
+ const origin = this.context.origin;
10803
+ if (typeof origin === "function") return origin(index) ?? null;
10804
+ return index === this.context.start ? origin : null;
10805
+ }
10806
+ parts() {
10807
+ const host = this.host.nativeElement;
10808
+ const navigation = Array.from(host.querySelectorAll("bf-icon-button[data-chrome]"));
10809
+ return {
10810
+ host,
10811
+ frame: this.frame().nativeElement,
10812
+ chrome: [
10813
+ this.scrim().nativeElement,
10814
+ this.top().nativeElement,
10815
+ this.bottom().nativeElement,
10816
+ ...navigation
10817
+ ]
10818
+ };
10819
+ }
10820
+ reduced() {
10821
+ return prefersReducedMotion(this.document.defaultView);
10822
+ }
10823
+ static ɵfac = i0.ɵɵngDeclareFactory({
10824
+ minVersion: "12.0.0",
10825
+ version: "22.1.7",
10826
+ ngImport: i0,
10827
+ type: BfImageViewerComponent,
10828
+ deps: [],
10829
+ target: i0.ɵɵFactoryTarget.Component
10830
+ });
10831
+ static ɵcmp = i0.ɵɵngDeclareComponent({
10832
+ minVersion: "17.0.0",
10833
+ version: "22.1.7",
10834
+ type: BfImageViewerComponent,
10835
+ isStandalone: true,
10836
+ selector: "bf-image-viewer",
10837
+ host: {
10838
+ attributes: { "data-bf-image-viewer": "" },
10839
+ listeners: {
10840
+ "keydown": "keydown($event)",
10841
+ "click": "clicked($event)"
10842
+ },
10843
+ properties: {
10844
+ "style.--bf-image-viewer-ratio": "ratio()",
10845
+ "attr.data-index": "index()",
10846
+ "attr.data-count": "count()",
10847
+ "attr.data-label": "context.label"
10848
+ },
10849
+ classAttribute: "dark fixed inset-0 flex flex-col text-text"
10850
+ },
10851
+ viewQueries: [
10852
+ {
10853
+ propertyName: "frame",
10854
+ first: true,
10855
+ predicate: ["frame"],
10856
+ descendants: true,
10857
+ isSignal: true
10858
+ },
10859
+ {
10860
+ propertyName: "scrim",
10861
+ first: true,
10862
+ predicate: ["scrim"],
10863
+ descendants: true,
10864
+ isSignal: true
10865
+ },
10866
+ {
10867
+ propertyName: "top",
10868
+ first: true,
10869
+ predicate: ["top"],
10870
+ descendants: true,
10871
+ isSignal: true
10872
+ },
10873
+ {
10874
+ propertyName: "bottom",
10875
+ first: true,
10876
+ predicate: ["bottom"],
10877
+ descendants: true,
10878
+ isSignal: true
10879
+ }
10880
+ ],
10881
+ ngImport: i0,
10882
+ template: `
10883
+ <div #scrim data-scrim class="bg-scrim/90 absolute inset-0"></div>
10884
+ <div #top data-chrome class="relative flex shrink-0 items-center gap-3 p-3">
10885
+ <span data-counter class="text-label min-w-0 flex-1 ps-2" [class.invisible]="count() < 2">
10886
+ {{ counter() }}
10887
+ </span>
10888
+ <bf-icon-button data-close cdkFocusInitial [label]="labels.close" (clicked)="ref.close()">
10889
+ <bf-icon [icon]="closeIcon" />
10890
+ </bf-icon-button>
10891
+ </div>
10892
+ <div class="relative min-h-0 flex-1">
10893
+ <div
10894
+ data-stage
10895
+ class="[container-type:size] absolute inset-0 grid place-items-center px-3 sm:px-16"
10896
+ >
10897
+ <div #frame data-frame [attr.data-status]="status()" [class]="frameClasses()">
10898
+ @if (placeholder(); as thumb) {
10899
+ <img
10900
+ data-placeholder
10901
+ aria-hidden="true"
10902
+ alt=""
10903
+ class="absolute inset-0 size-full object-contain"
10904
+ [src]="thumb"
10905
+ />
10906
+ }
10907
+ @for (item of shown(); track item.index) {
10908
+ <img
10909
+ bfFadeIn
10910
+ data-image
10911
+ class="absolute inset-0 size-full object-contain"
10912
+ animate.leave="opacity-0 transition-opacity duration-slow ease-md motion-reduce:transition-none"
10913
+ [src]="item.image.src"
10914
+ [alt]="item.image.label"
10915
+ (load)="imageLoaded($event, item.index)"
10916
+ (error)="imageFailed(item.index)"
10917
+ />
10918
+ }
10919
+ @if (status() === 'pending') {
10920
+ <div class="absolute inset-0 grid place-items-center">
10921
+ <bf-spinner size="lg" />
10922
+ </div>
10923
+ } @else if (status() === 'error') {
10924
+ <p
10925
+ data-failed
10926
+ class="text-body text-text-secondary absolute inset-0 grid place-items-center p-4 text-center"
10927
+ >
10928
+ {{ labels.imageLoadFailed }}
10929
+ </p>
10930
+ }
10931
+ </div>
10932
+ </div>
10933
+ @if (count() > 1) {
10934
+ <bf-icon-button
10935
+ data-chrome
10936
+ data-previous
10937
+ [class]="previousClasses"
10938
+ [label]="labels.imageViewerPrevious"
10939
+ (clicked)="ref.previous()"
10940
+ >
10941
+ <bf-icon class="rtl:-scale-x-100" [icon]="previousIcon" />
10942
+ </bf-icon-button>
10943
+ <bf-icon-button
10944
+ data-chrome
10945
+ data-next
10946
+ [class]="nextClasses"
10947
+ [label]="labels.imageViewerNext"
10948
+ (clicked)="ref.next()"
10949
+ >
10950
+ <bf-icon class="rtl:-scale-x-100" [icon]="nextIcon" />
10951
+ </bf-icon-button>
10952
+ }
10953
+ </div>
10954
+ <div
10955
+ #bottom
10956
+ data-chrome
10957
+ class="relative flex shrink-0 flex-wrap items-center gap-x-4 gap-y-2 px-5 pt-2 pb-4 empty:hidden"
10958
+ >
10959
+ @if (image().caption; as caption) {
10960
+ <p data-caption class="text-body min-w-0 flex-1 wrap-anywhere">{{ caption }}</p>
10961
+ }
10962
+ @if (context.actions; as actions) {
10963
+ <div data-actions class="ms-auto flex flex-wrap items-center gap-2">
10964
+ <ng-container
10965
+ [ngTemplateOutlet]="actions"
10966
+ [ngTemplateOutletContext]="actionsContext()"
10967
+ [ngTemplateOutletInjector]="injector"
10968
+ />
10969
+ </div>
10970
+ }
10971
+ </div>
10972
+ <div data-announcement aria-live="polite" class="sr-only">{{ announcement() }}</div>
10973
+ `,
10974
+ isInline: true,
10975
+ dependencies: [
10976
+ {
10977
+ kind: "directive",
10978
+ type: NgTemplateOutlet,
10979
+ selector: "[ngTemplateOutlet]",
10980
+ inputs: [
10981
+ "ngTemplateOutletContext",
10982
+ "ngTemplateOutlet",
10983
+ "ngTemplateOutletInjector"
10984
+ ]
10985
+ },
10986
+ {
10987
+ kind: "directive",
10988
+ type: BfFadeInDirective,
10989
+ selector: "img[bfFadeIn]"
10990
+ },
10991
+ {
10992
+ kind: "component",
10993
+ type: BfIconButtonComponent,
10994
+ selector: "bf-icon-button, a[bfIconButton]",
10995
+ inputs: [
10996
+ "variant",
10997
+ "size",
10998
+ "type",
10999
+ "disabled",
11000
+ "label"
11001
+ ],
11002
+ outputs: ["clicked"]
11003
+ },
11004
+ {
11005
+ kind: "component",
11006
+ type: BfIconComponent,
11007
+ selector: "bf-icon",
11008
+ inputs: [
11009
+ "icon",
11010
+ "size",
11011
+ "label",
11012
+ "filled"
11013
+ ]
11014
+ },
11015
+ {
11016
+ kind: "component",
11017
+ type: BfSpinnerComponent,
11018
+ selector: "bf-spinner",
11019
+ inputs: [
11020
+ "size",
11021
+ "label",
11022
+ "decorative"
11023
+ ]
11024
+ }
11025
+ ]
11026
+ });
11027
+ };
11028
+ i0.ɵɵngDeclareClassMetadata({
11029
+ minVersion: "12.0.0",
11030
+ version: "22.1.7",
11031
+ ngImport: i0,
11032
+ type: BfImageViewerComponent,
11033
+ decorators: [{
11034
+ type: Component,
11035
+ args: [{
11036
+ selector: "bf-image-viewer",
11037
+ imports: [
11038
+ NgTemplateOutlet,
11039
+ BfFadeInDirective,
11040
+ BfIconButtonComponent,
11041
+ BfIconComponent,
11042
+ BfSpinnerComponent
11043
+ ],
11044
+ host: {
11045
+ class: "dark fixed inset-0 flex flex-col text-text",
11046
+ "data-bf-image-viewer": "",
11047
+ "[style.--bf-image-viewer-ratio]": "ratio()",
11048
+ "[attr.data-index]": "index()",
11049
+ "[attr.data-count]": "count()",
11050
+ "[attr.data-label]": "context.label",
11051
+ "(keydown)": "keydown($event)",
11052
+ "(click)": "clicked($event)"
11053
+ },
11054
+ template: `
11055
+ <div #scrim data-scrim class="bg-scrim/90 absolute inset-0"></div>
11056
+ <div #top data-chrome class="relative flex shrink-0 items-center gap-3 p-3">
11057
+ <span data-counter class="text-label min-w-0 flex-1 ps-2" [class.invisible]="count() < 2">
11058
+ {{ counter() }}
11059
+ </span>
11060
+ <bf-icon-button data-close cdkFocusInitial [label]="labels.close" (clicked)="ref.close()">
11061
+ <bf-icon [icon]="closeIcon" />
11062
+ </bf-icon-button>
11063
+ </div>
11064
+ <div class="relative min-h-0 flex-1">
11065
+ <div
11066
+ data-stage
11067
+ class="[container-type:size] absolute inset-0 grid place-items-center px-3 sm:px-16"
11068
+ >
11069
+ <div #frame data-frame [attr.data-status]="status()" [class]="frameClasses()">
11070
+ @if (placeholder(); as thumb) {
11071
+ <img
11072
+ data-placeholder
11073
+ aria-hidden="true"
11074
+ alt=""
11075
+ class="absolute inset-0 size-full object-contain"
11076
+ [src]="thumb"
11077
+ />
11078
+ }
11079
+ @for (item of shown(); track item.index) {
11080
+ <img
11081
+ bfFadeIn
11082
+ data-image
11083
+ class="absolute inset-0 size-full object-contain"
11084
+ animate.leave="opacity-0 transition-opacity duration-slow ease-md motion-reduce:transition-none"
11085
+ [src]="item.image.src"
11086
+ [alt]="item.image.label"
11087
+ (load)="imageLoaded($event, item.index)"
11088
+ (error)="imageFailed(item.index)"
11089
+ />
11090
+ }
11091
+ @if (status() === 'pending') {
11092
+ <div class="absolute inset-0 grid place-items-center">
11093
+ <bf-spinner size="lg" />
11094
+ </div>
11095
+ } @else if (status() === 'error') {
11096
+ <p
11097
+ data-failed
11098
+ class="text-body text-text-secondary absolute inset-0 grid place-items-center p-4 text-center"
11099
+ >
11100
+ {{ labels.imageLoadFailed }}
11101
+ </p>
11102
+ }
11103
+ </div>
11104
+ </div>
11105
+ @if (count() > 1) {
11106
+ <bf-icon-button
11107
+ data-chrome
11108
+ data-previous
11109
+ [class]="previousClasses"
11110
+ [label]="labels.imageViewerPrevious"
11111
+ (clicked)="ref.previous()"
11112
+ >
11113
+ <bf-icon class="rtl:-scale-x-100" [icon]="previousIcon" />
11114
+ </bf-icon-button>
11115
+ <bf-icon-button
11116
+ data-chrome
11117
+ data-next
11118
+ [class]="nextClasses"
11119
+ [label]="labels.imageViewerNext"
11120
+ (clicked)="ref.next()"
11121
+ >
11122
+ <bf-icon class="rtl:-scale-x-100" [icon]="nextIcon" />
11123
+ </bf-icon-button>
11124
+ }
11125
+ </div>
11126
+ <div
11127
+ #bottom
11128
+ data-chrome
11129
+ class="relative flex shrink-0 flex-wrap items-center gap-x-4 gap-y-2 px-5 pt-2 pb-4 empty:hidden"
11130
+ >
11131
+ @if (image().caption; as caption) {
11132
+ <p data-caption class="text-body min-w-0 flex-1 wrap-anywhere">{{ caption }}</p>
11133
+ }
11134
+ @if (context.actions; as actions) {
11135
+ <div data-actions class="ms-auto flex flex-wrap items-center gap-2">
11136
+ <ng-container
11137
+ [ngTemplateOutlet]="actions"
11138
+ [ngTemplateOutletContext]="actionsContext()"
11139
+ [ngTemplateOutletInjector]="injector"
11140
+ />
11141
+ </div>
11142
+ }
11143
+ </div>
11144
+ <div data-announcement aria-live="polite" class="sr-only">{{ announcement() }}</div>
11145
+ `
11146
+ }]
11147
+ }],
11148
+ ctorParameters: () => [],
11149
+ propDecorators: {
11150
+ frame: [{
11151
+ type: i0.ViewChild,
11152
+ args: ["frame", { isSignal: true }]
11153
+ }],
11154
+ scrim: [{
11155
+ type: i0.ViewChild,
11156
+ args: ["scrim", { isSignal: true }]
11157
+ }],
11158
+ top: [{
11159
+ type: i0.ViewChild,
11160
+ args: ["top", { isSignal: true }]
11161
+ }],
11162
+ bottom: [{
11163
+ type: i0.ViewChild,
11164
+ args: ["bottom", { isSignal: true }]
11165
+ }]
11166
+ }
11167
+ });
11168
+ function thumbnailImage(origin) {
11169
+ if (!origin) return null;
11170
+ return origin instanceof HTMLImageElement ? origin : origin.querySelector("img");
11171
+ }
11172
+ var BfImageViewerService = class BfImageViewerService {
11173
+ overlays = inject(BfOverlayService);
11174
+ injector = inject(Injector);
11175
+ open(options) {
11176
+ const images = [...options.images];
11177
+ if (images.length === 0) throw new Error("BfImageViewerService: open() needs at least one image");
11178
+ const requested = Math.trunc(options.index ?? 0);
11179
+ const start = Number.isFinite(requested) ? (requested % images.length + images.length) % images.length : 0;
11180
+ const injector = options.injector ?? this.injector;
11181
+ const label = options.ariaLabel?.length ? options.ariaLabel : injector.get(BF_LABELS).imageViewer;
11182
+ let viewerRef = null;
11183
+ let motion = null;
11184
+ const context = {
11185
+ start,
11186
+ label,
11187
+ origin: options.origin ?? null,
11188
+ actions: options.actions ?? null,
11189
+ register: (viewer) => {
11190
+ motion = viewer;
11191
+ }
11192
+ };
11193
+ const providers = (ref) => {
11194
+ viewerRef = new BfImageViewerRef(ref, images, start);
11195
+ return [{
11196
+ provide: BfImageViewerRef,
11197
+ useValue: viewerRef
11198
+ }, {
11199
+ provide: BF_IMAGE_VIEWER_CONTEXT,
11200
+ useValue: context
11201
+ }];
11202
+ };
11203
+ this.overlays.open(BfImageViewerComponent, {
11204
+ position: "fullscreen",
11205
+ backdrop: false,
11206
+ role: "dialog",
11207
+ ariaLabel: label,
11208
+ closeOnNavigation: options.closeOnNavigation,
11209
+ panelClass: ["bf-image-viewer-panel"],
11210
+ injector,
11211
+ providers,
11212
+ enter: (panel) => motion ? motion.enter() : fadeIn(panel),
11213
+ exit: (panel) => motion ? motion.exit() : fadeOut(panel)
11214
+ });
11215
+ if (!viewerRef) throw new Error("BfImageViewerService: the overlay core did not build the viewer providers");
11216
+ return viewerRef;
11217
+ }
11218
+ static ɵfac = i0.ɵɵngDeclareFactory({
11219
+ minVersion: "12.0.0",
11220
+ version: "22.1.7",
11221
+ ngImport: i0,
11222
+ type: BfImageViewerService,
11223
+ deps: [],
11224
+ target: i0.ɵɵFactoryTarget.Injectable
11225
+ });
11226
+ static ɵprov = i0.ɵɵngDeclareInjectable({
11227
+ minVersion: "12.0.0",
11228
+ version: "22.1.7",
11229
+ ngImport: i0,
11230
+ type: BfImageViewerService,
11231
+ providedIn: "root"
11232
+ });
11233
+ };
11234
+ i0.ɵɵngDeclareClassMetadata({
11235
+ minVersion: "12.0.0",
11236
+ version: "22.1.7",
11237
+ ngImport: i0,
11238
+ type: BfImageViewerService,
11239
+ decorators: [{
11240
+ type: Injectable,
11241
+ args: [{ providedIn: "root" }]
11242
+ }]
11243
+ });
11244
+ export { BF_DESKTOP_TOAST_DEFAULTS, BF_DIALOG_DATA, BF_OVERLAY_DATA, BF_OVERLAY_HOST, BF_SIDE_PANEL_DATA, BfChatComposerComponent, BfComboboxComponent, BfComponentOverlayRef, BfDateInputComponent, BfDateTimeInputComponent, BfDesktopToastComponent, BfDesktopToastService, BfDialogComponent, BfDialogRef, BfDialogService, BfImageViewerComponent, BfImageViewerRef, BfImageViewerService, BfMenuButtonComponent, BfMenuComponent, BfMenuDividerDirective, BfMenuGroupDirective, BfMenuItemCheckboxDirective, BfMenuItemDirective, BfMenuItemRadioDirective, BfMenuTriggerDirective, BfMultiComboboxComponent, BfOverlayRef, BfOverlayService, BfOverlayStack, BfPopoverComponent, BfPopoverTriggerDirective, BfSelectComponent, BfSidePanelComponent, BfSidePanelRef, BfSidePanelService, BfSidebarToggleComponent, BfThemeMenuButtonComponent, BfTimeInputComponent, BfToastRef, BfTooltipComponent, BfTooltipDirective, OVERLAY_DURATION, OVERLAY_EASING, OVERLAY_REDUCED_DURATION, fadeDown, fadeIn, fadeOut, fadeUp, prefersReducedMotion, provideBfDesktopToast, provideBfSelect, scaleIn, slideDown, slideInFromEnd, slideOutToEnd, slideUp, ɵBfToastQueue, labelPanelByHeading as ɵlabelPanelByHeading, watchPanelHeading as ɵwatchPanelHeading };
9202
11245
 
9203
11246
  //# sourceMappingURL=bitforgehq-ui-overlay.mjs.map