@wildmason/aegis 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { CdkTrapFocus } from '@angular/cdk/a11y';
2
2
  import * as i0 from '@angular/core';
3
- import { input, output, HostListener, ChangeDetectionStrategy, Component, model, signal, computed, inject, ElementRef, Renderer2, ChangeDetectorRef, viewChild, effect, ViewEncapsulation, Input, Directive, contentChildren, forwardRef, ViewChild, Injectable, Injector, DestroyRef, TemplateRef, contentChild } from '@angular/core';
3
+ import { input, output, HostListener, ChangeDetectionStrategy, Component, model, signal, computed, inject, ElementRef, Renderer2, ChangeDetectorRef, viewChild, effect, ViewEncapsulation, Input, Directive, contentChildren, booleanAttribute, forwardRef, HostAttributeToken, linkedSignal, ViewChild, Injectable, Injector, DestroyRef, TemplateRef, contentChild } from '@angular/core';
4
4
  import { CdkConnectedOverlay, CdkOverlayOrigin, createFlexibleConnectedPositionStrategy, createOverlayRef, createRepositionScrollStrategy } from '@angular/cdk/overlay';
5
5
  import { NgTemplateOutlet } from '@angular/common';
6
6
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
@@ -14,7 +14,16 @@ import { ComponentPortal } from '@angular/cdk/portal';
14
14
  *
15
15
  * Owns the fixed backdrop, centering, keyboard/pointer dismissal AND the focus trap,
16
16
  * so individual modal components don't need to repeat this logic or add their own
17
- * HostListeners.
17
+ * HostListeners. The consumer supplies the whole panel: its surface, size, header
18
+ * and close control.
19
+ *
20
+ * ## wm-modal or wm-dialog
21
+ *
22
+ * Reach for `wm-dialog` first. It is a complete dialog — header, title wired to
23
+ * `aria-labelledby`, close button, padded scrolling body — and it is not limited to
24
+ * its `size` presets: `--wm-dialog-width` sets any width (Helm uses up to 960px).
25
+ * Use `wm-modal` when that chrome is wrong for the surface: a two-pane browser, a
26
+ * command palette, a panel with its own toolbar or its own scroll regions.
18
27
  *
19
28
  * ## Focus is trapped here, and it has to be
20
29
  *
@@ -33,8 +42,9 @@ import { ComponentPortal } from '@angular/cdk/portal';
33
42
  *
34
43
  * Usage:
35
44
  * <wm-modal [visible]="visible()" (dismiss)="close()">
36
- * <div class="p-6 wm-r-md w-80" style="background: var(--wm-bg-raised)">
37
- * <!-- modal panel content -->
45
+ * <div class="panel" role="dialog" aria-modal="true" aria-labelledby="panel-title">
46
+ * <h2 id="panel-title">Browse snapshot</h2>
47
+ * <!-- modal panel content, including a visible close control -->
38
48
  * </div>
39
49
  * </wm-modal>
40
50
  *
@@ -43,6 +53,11 @@ import { ComponentPortal } from '@angular/cdk/portal';
43
53
  *
44
54
  * The projected content is wrapped in a stop-propagation container so clicks inside
45
55
  * the panel do not bubble up to the backdrop and trigger a dismiss.
56
+ *
57
+ * The backdrop's layout lives in this component's stylesheet, so it needs no utility
58
+ * framework in the consumer. Its colour stays an inline
59
+ * `background: var(--wm-modal-backdrop)`, because consumers detect an open modal
60
+ * with `[style*="--wm-modal-backdrop"]`.
46
61
  */
47
62
  class WmModal {
48
63
  /** Whether the modal is currently open. */
@@ -51,8 +66,8 @@ class WmModal {
51
66
  dismiss = output();
52
67
  /**
53
68
  * Vertical alignment of the panel within the backdrop.
54
- * - 'center' (default): panel is vertically centered (items-center)
55
- * - 'top': panel appears near the top (items-start pt-20), for search/command overlays
69
+ * - 'center' (default): panel is centered on both axes
70
+ * - 'top': panel sits 5rem below the top edge, for search/command overlays
56
71
  */
57
72
  align = input('center', ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
58
73
  onEscape() {
@@ -60,11 +75,11 @@ class WmModal {
60
75
  this.dismiss.emit();
61
76
  }
62
77
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: WmModal, deps: [], target: i0.ɵɵFactoryTarget.Component });
63
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.23", type: WmModal, isStandalone: true, selector: "wm-modal", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: true, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismiss: "dismiss" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (visible()) {\n <div [class]=\"align() === 'top'\n ? 'fixed inset-0 flex items-start justify-center pt-20 z-50'\n : 'fixed inset-0 flex items-center justify-center z-50'\"\n style=\"background: var(--wm-modal-backdrop)\"\n (click)=\"dismiss.emit()\">\n <!-- The trap must sit on an element this template owns, inside this `@if`, so it\n has a parentNode by the time CdkTrapFocus runs ngAfterContentInit. A consumer\n cannot achieve that on projected content \u2014 see the class doc. -->\n <div (click)=\"$event.stopPropagation()\" cdkTrapFocus [cdkTrapFocusAutoCapture]=\"true\">\n <ng-content />\n </div>\n </div>\n}\n", dependencies: [{ kind: "directive", type: CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
78
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.23", type: WmModal, isStandalone: true, selector: "wm-modal", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: true, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismiss: "dismiss" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (visible()) {\n <!-- The inline background is a contract, not a leftover: consumers detect an open\n modal with `[style*=\"--wm-modal-backdrop\"]`, so it must stay on this element. -->\n <div class=\"wm-modal-backdrop\"\n [class.wm-modal-backdrop--top]=\"align() === 'top'\"\n style=\"background: var(--wm-modal-backdrop)\"\n (click)=\"dismiss.emit()\">\n <!-- The trap must sit on an element this template owns, inside this `@if`, so it\n has a parentNode by the time CdkTrapFocus runs ngAfterContentInit. A consumer\n cannot achieve that on projected content \u2014 see the class doc. -->\n <div (click)=\"$event.stopPropagation()\" cdkTrapFocus [cdkTrapFocusAutoCapture]=\"true\">\n <ng-content />\n </div>\n </div>\n}\n", styles: [".wm-modal-backdrop{position:fixed;inset:0;z-index:50;display:flex;align-items:center;justify-content:center}.wm-modal-backdrop--top{align-items:flex-start;padding-top:5rem}\n"], dependencies: [{ kind: "directive", type: CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
64
79
  }
65
80
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: WmModal, decorators: [{
66
81
  type: Component,
67
- args: [{ selector: 'wm-modal', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CdkTrapFocus], template: "@if (visible()) {\n <div [class]=\"align() === 'top'\n ? 'fixed inset-0 flex items-start justify-center pt-20 z-50'\n : 'fixed inset-0 flex items-center justify-center z-50'\"\n style=\"background: var(--wm-modal-backdrop)\"\n (click)=\"dismiss.emit()\">\n <!-- The trap must sit on an element this template owns, inside this `@if`, so it\n has a parentNode by the time CdkTrapFocus runs ngAfterContentInit. A consumer\n cannot achieve that on projected content \u2014 see the class doc. -->\n <div (click)=\"$event.stopPropagation()\" cdkTrapFocus [cdkTrapFocusAutoCapture]=\"true\">\n <ng-content />\n </div>\n </div>\n}\n" }]
82
+ args: [{ selector: 'wm-modal', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CdkTrapFocus], template: "@if (visible()) {\n <!-- The inline background is a contract, not a leftover: consumers detect an open\n modal with `[style*=\"--wm-modal-backdrop\"]`, so it must stay on this element. -->\n <div class=\"wm-modal-backdrop\"\n [class.wm-modal-backdrop--top]=\"align() === 'top'\"\n style=\"background: var(--wm-modal-backdrop)\"\n (click)=\"dismiss.emit()\">\n <!-- The trap must sit on an element this template owns, inside this `@if`, so it\n has a parentNode by the time CdkTrapFocus runs ngAfterContentInit. A consumer\n cannot achieve that on projected content \u2014 see the class doc. -->\n <div (click)=\"$event.stopPropagation()\" cdkTrapFocus [cdkTrapFocusAutoCapture]=\"true\">\n <ng-content />\n </div>\n </div>\n}\n", styles: [".wm-modal-backdrop{position:fixed;inset:0;z-index:50;display:flex;align-items:center;justify-content:center}.wm-modal-backdrop--top{align-items:flex-start;padding-top:5rem}\n"] }]
68
83
  }], propDecorators: { visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: true }] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], onEscape: [{
69
84
  type: HostListener,
70
85
  args: ['document:keydown.escape']
@@ -88,21 +103,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImpo
88
103
  * </div>
89
104
  * </wm-dialog>
90
105
  *
106
+ * Width:
107
+ * `size` picks a preset (sm 360px, md 480px, lg 640px). For any other width, set the
108
+ * `--wm-dialog-width` custom property on the host; it overrides every preset:
109
+ * <wm-dialog [(isOpen)]="open" title="Line history" style="--wm-dialog-width: 800px">
110
+ * Fluid values work too, e.g. `min(90vw, 48rem)`. Whatever the width, the panel is
111
+ * capped at `calc(100vw - 2rem)` wide and `calc(100dvh - 4rem)` tall.
112
+ *
113
+ * When the dialog chrome itself is wrong for the surface (a two-pane browser, a
114
+ * command palette, a panel with its own toolbar), use `wm-modal` with your own panel.
115
+ *
91
116
  * Accessibility:
92
117
  * - role="dialog" + aria-modal="true" on the panel
93
118
  * - aria-labelledby wired to the title element (when title is set)
94
- * - Focus is trapped inside the panel when open (cdkTrapFocus)
119
+ * - Focus is trapped inside the panel when open (cdkTrapFocus). Do not add a
120
+ * cdkTrapFocus of your own inside the body; it would nest a second trap.
95
121
  * - Escape key closes the dialog
96
122
  * - Clicking the backdrop closes the dialog
97
- * - On close, focus returns to the element that opened the dialog
98
- * (browser default when focus trap is released)
123
+ * - On close, focus returns to the element that had it when the dialog opened
124
+ * (cdkTrapFocusAutoCapture records that element and refocuses it when the
125
+ * trap is destroyed)
99
126
  */
100
127
  class WmDialog {
101
128
  /** Two-way binding for open/closed state. */
102
129
  isOpen = model(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
103
130
  /** Dialog heading text. When set, aria-labelledby is applied automatically. */
104
131
  title = input('', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
105
- /** Panel width preset. sm = 360px, md = 480px (default), lg = 640px. */
132
+ /**
133
+ * Panel width preset. sm = 360px, md = 480px (default), lg = 640px.
134
+ * `--wm-dialog-width` on the host overrides every preset.
135
+ */
106
136
  size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
107
137
  close() { this.isOpen.set(false); }
108
138
  onEscape() {
@@ -302,8 +332,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImpo
302
332
  * backdrop, focus-trapping, and ARIA attribute management.
303
333
  *
304
334
  * Slots:
305
- * - [trigger] — the anchor element (button, icon, etc.)
306
- * - [content] — the panel body (menu items, form, etc.)
335
+ * - [trigger] — the anchor element. Put the attribute on the focusable
336
+ * element itself, never on a wrapper or on a component host
337
+ * such as <wm-button>: the ARIA below goes to the element
338
+ * that carries it. For an Aegis-styled trigger, use
339
+ * `<button wmButton trigger>`. Nothing reads
340
+ * `data-popover-trigger`
341
+ * - [content] — the panel body (menu items, form, etc.). The panel carries
342
+ * the role, so the body takes no role of its own
307
343
  *
308
344
  * Usage:
309
345
  * ```html
@@ -316,16 +352,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImpo
316
352
  * align="end"
317
353
  * >
318
354
  * <button trigger (click)="open.set(!open())">Options</button>
319
- * <div content role="menu">
355
+ * <div content>
320
356
  * <button role="menuitem" (click)="doThing()">Do thing</button>
321
357
  * </div>
322
358
  * </wm-popover>
323
359
  * ```
324
360
  *
325
361
  * Accessibility:
326
- * - Trigger element automatically receives aria-haspopup, aria-expanded, aria-controls
327
- * - Panel receives the configured role and optional accessible name
362
+ * - Trigger element automatically receives aria-haspopup (from `hasPopup`),
363
+ * aria-expanded and aria-controls, plus role="button" when it has no role.
364
+ * They are rewritten on every change to `isOpen` or `hasPopup`, so the
365
+ * consumer does not bind them
366
+ * - Panel receives the configured role and optional accessible name. The
367
+ * role goes on the panel only: the host element, which wraps the trigger,
368
+ * never carries it, even when a consumer writes `role` as a static attribute
369
+ * - A `dialog` or `alertdialog` panel receives aria-modal="true" while it
370
+ * captures focus (see `trapFocusAutoCapture`)
328
371
  * - Focus is trapped inside the open panel (cdkTrapFocus)
372
+ * - A modal panel whose content has nothing tabbable (text only, or a lone
373
+ * disabled button) takes focus itself (tabindex="-1") and keeps it on Tab,
374
+ * so focus is never left on the trigger that aria-modal makes inert
329
375
  * - Escape key closes the panel
330
376
  * - Backdrop click closes the panel
331
377
  *
@@ -378,6 +424,30 @@ class WmPopover {
378
424
  * auto-restore.
379
425
  */
380
426
  trapFocusAutoCapture = input(true, ...(ngDevMode ? [{ debugName: "trapFocusAutoCapture" }] : /* istanbul ignore next */ []));
427
+ /**
428
+ * `"true"` for a `dialog` or `alertdialog` panel that captures focus, else
429
+ * no attribute.
430
+ *
431
+ * The panel opens over a click-catching backdrop, and a modal panel keeps
432
+ * focus inside itself (see `onAttach` and `onPanelKeydown`), so nothing
433
+ * outside it can be reached until it closes. A dialog that is modal must say
434
+ * so, or a screen reader keeps reading the page behind it. `aria-modal` is
435
+ * defined for those two roles only, so a menu or listbox panel gets no
436
+ * attribute. With `trapFocusAutoCapture` off, focus is meant to stay on the
437
+ * trigger outside the panel, and `aria-modal` would hide that focused
438
+ * trigger from a screen reader, so the panel is not declared modal.
439
+ */
440
+ ariaModal = computed(() => {
441
+ const role = this.role();
442
+ const modalRole = role === 'dialog' || role === 'alertdialog';
443
+ return modalRole && this.trapFocusAutoCapture() ? 'true' : null;
444
+ }, ...(ngDevMode ? [{ debugName: "ariaModal" }] : /* istanbul ignore next */ []));
445
+ /**
446
+ * `-1` on a modal panel, so the panel itself can take focus when its content
447
+ * has nothing tabbable. A non-modal panel stays unfocusable: a click on it
448
+ * must not pull focus off a typeahead trigger input.
449
+ */
450
+ panelTabIndex = computed(() => (this.ariaModal() ? -1 : null), ...(ngDevMode ? [{ debugName: "panelTabIndex" }] : /* istanbul ignore next */ []));
381
451
  /** Emitted when the panel should close (backdrop click, Escape key, CDK detach). */
382
452
  closed = output();
383
453
  renderer = inject(Renderer2);
@@ -385,6 +455,7 @@ class WmPopover {
385
455
  cdr = inject(ChangeDetectorRef);
386
456
  panelId = 'wm-popover-' + Math.random().toString(36).substring(2, 9);
387
457
  panel = viewChild('panel', ...(ngDevMode ? [{ debugName: "panel" }] : /* istanbul ignore next */ []));
458
+ trap = viewChild(CdkTrapFocus, ...(ngDevMode ? [{ debugName: "trap" }] : /* istanbul ignore next */ []));
388
459
  constructor() {
389
460
  effect(() => {
390
461
  const open = this.isOpen();
@@ -408,14 +479,26 @@ class WmPopover {
408
479
  { originX: x, originY: 'top', overlayX: x, overlayY: 'bottom', offsetY: -4 },
409
480
  ];
410
481
  }
482
+ /**
483
+ * Runs after the focus trap's own capture, which CDK defers to the next
484
+ * render. A `[wmAutoFocus]` element takes focus first. Otherwise a modal
485
+ * panel that the trap left without focus takes focus itself. The trap finds
486
+ * nothing to focus when the content has no tabbable element (text only, or a
487
+ * lone disabled button), and it then leaves focus on the trigger, which
488
+ * `aria-modal` has just made inert to a screen reader.
489
+ */
411
490
  onAttach() {
412
491
  setTimeout(() => {
413
- const panelEl = this.panel();
492
+ const panelEl = this.panel()?.nativeElement;
414
493
  if (!panelEl)
415
494
  return;
416
- const el = panelEl.nativeElement.querySelector('[wmAutoFocus]');
417
- if (!el)
495
+ const el = panelEl.querySelector('[wmAutoFocus]');
496
+ if (!el) {
497
+ if (this.ariaModal() && !panelEl.contains(panelEl.ownerDocument.activeElement)) {
498
+ panelEl.focus();
499
+ }
418
500
  return;
501
+ }
419
502
  el.focus();
420
503
  if (typeof el.showPicker === 'function') {
421
504
  const type = el.type;
@@ -428,16 +511,44 @@ class WmPopover {
428
511
  }
429
512
  }, 10);
430
513
  }
514
+ /**
515
+ * Tab and Shift+Tab while focus is on a modal panel itself, not on a control
516
+ * inside it. Focus moves to the first or the last tabbable control. With none,
517
+ * focus stays on the panel. The trap's own boundary anchors cannot do this:
518
+ * with nothing tabbable inside, the anchor keeps focus, and the next Tab
519
+ * leaves the overlay for the page behind the backdrop.
520
+ */
521
+ onPanelKeydown(event) {
522
+ if (event.key !== 'Tab' || !this.ariaModal())
523
+ return;
524
+ if (event.target !== this.panel()?.nativeElement)
525
+ return;
526
+ event.preventDefault();
527
+ const focusTrap = this.trap()?.focusTrap;
528
+ if (event.shiftKey) {
529
+ focusTrap?.focusLastTabbableElement();
530
+ }
531
+ else {
532
+ focusTrap?.focusFirstTabbableElement();
533
+ }
534
+ }
431
535
  onClose() {
432
536
  this.closed.emit();
433
537
  }
434
538
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: WmPopover, deps: [], target: i0.ɵɵFactoryTarget.Component });
435
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.23", type: WmPopover, isStandalone: true, selector: "wm-popover", inputs: { isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, hasPopup: { classPropertyName: "hasPopup", publicName: "hasPopup", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, trapFocusAutoCapture: { classPropertyName: "trapFocusAutoCapture", publicName: "trapFocusAutoCapture", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed" }, viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Trigger anchor -->\n<div cdkOverlayOrigin #origin=\"cdkOverlayOrigin\" class=\"wm-popover-trigger\">\n <ng-content select=\"[trigger]\"></ng-content>\n</div>\n\n<!-- Floating panel -->\n<ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"origin\"\n [cdkConnectedOverlayOpen]=\"isOpen()\"\n [cdkConnectedOverlayPositions]=\"positions\"\n [cdkConnectedOverlayScrollStrategy]=\"scrollStrategy\"\n [cdkConnectedOverlayHasBackdrop]=\"true\"\n cdkConnectedOverlayBackdropClass=\"cdk-overlay-transparent-backdrop\"\n (backdropClick)=\"onClose()\"\n (detach)=\"onClose()\"\n (attach)=\"onAttach()\"\n>\n <div\n #panel\n [id]=\"panelId\"\n [attr.role]=\"role()\"\n [attr.aria-label]=\"ariaLabel()\"\n class=\"wm-popover-panel\"\n [style.width]=\"width() === 'auto' ? 'auto' : width() + 'px'\"\n cdkTrapFocus\n [cdkTrapFocusAutoCapture]=\"trapFocusAutoCapture()\"\n (keydown.escape)=\"onClose()\"\n >\n <ng-content select=\"[content]\"></ng-content>\n </div>\n</ng-template>\n", styles: ["wm-popover{display:inline-flex;align-items:center}.wm-popover-trigger{display:block;width:100%}.wm-popover-panel{background:var(--wm-bg-raised);border:1px solid var(--wm-border);border-radius:var(--wm-radius-lg);box-shadow:0 4px 6px -1px #00000014,0 10px 20px -4px #00000024;overflow:hidden;outline:none}\n"], dependencies: [{ kind: "directive", type: CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation", "cdkConnectedOverlayUsePopover", "cdkConnectedOverlayMatchWidth", "cdkConnectedOverlay"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }, { kind: "directive", type: CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
539
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.23", type: WmPopover, isStandalone: true, selector: "wm-popover", inputs: { isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, hasPopup: { classPropertyName: "hasPopup", publicName: "hasPopup", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, trapFocusAutoCapture: { classPropertyName: "trapFocusAutoCapture", publicName: "trapFocusAutoCapture", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed" }, host: { properties: { "attr.role": "null" } }, viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "trap", first: true, predicate: CdkTrapFocus, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Trigger anchor -->\n<div cdkOverlayOrigin #origin=\"cdkOverlayOrigin\" class=\"wm-popover-trigger\">\n <ng-content select=\"[trigger]\"></ng-content>\n</div>\n\n<!-- Floating panel -->\n<ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"origin\"\n [cdkConnectedOverlayOpen]=\"isOpen()\"\n [cdkConnectedOverlayPositions]=\"positions\"\n [cdkConnectedOverlayScrollStrategy]=\"scrollStrategy\"\n [cdkConnectedOverlayHasBackdrop]=\"true\"\n cdkConnectedOverlayBackdropClass=\"cdk-overlay-transparent-backdrop\"\n (backdropClick)=\"onClose()\"\n (detach)=\"onClose()\"\n (attach)=\"onAttach()\"\n>\n <div\n #panel\n [id]=\"panelId\"\n [attr.role]=\"role()\"\n [attr.aria-modal]=\"ariaModal()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"panelTabIndex()\"\n class=\"wm-popover-panel\"\n [style.width]=\"width() === 'auto' ? 'auto' : width() + 'px'\"\n cdkTrapFocus\n [cdkTrapFocusAutoCapture]=\"trapFocusAutoCapture()\"\n (keydown.escape)=\"onClose()\"\n (keydown)=\"onPanelKeydown($event)\"\n >\n <ng-content select=\"[content]\"></ng-content>\n </div>\n</ng-template>\n", styles: ["wm-popover{display:inline-flex;align-items:center}.wm-popover-trigger{display:block;width:100%}.wm-popover-panel{background:var(--wm-bg-raised);border:1px solid var(--wm-border);border-radius:var(--wm-radius-lg);box-shadow:0 4px 6px -1px #00000014,0 10px 20px -4px #00000024;overflow:hidden;outline:none}\n"], dependencies: [{ kind: "directive", type: CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation", "cdkConnectedOverlayUsePopover", "cdkConnectedOverlayMatchWidth", "cdkConnectedOverlay"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }, { kind: "directive", type: CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
436
540
  }
437
541
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: WmPopover, decorators: [{
438
542
  type: Component,
439
- args: [{ selector: 'wm-popover', imports: [CdkConnectedOverlay, CdkOverlayOrigin, CdkTrapFocus], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<!-- Trigger anchor -->\n<div cdkOverlayOrigin #origin=\"cdkOverlayOrigin\" class=\"wm-popover-trigger\">\n <ng-content select=\"[trigger]\"></ng-content>\n</div>\n\n<!-- Floating panel -->\n<ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"origin\"\n [cdkConnectedOverlayOpen]=\"isOpen()\"\n [cdkConnectedOverlayPositions]=\"positions\"\n [cdkConnectedOverlayScrollStrategy]=\"scrollStrategy\"\n [cdkConnectedOverlayHasBackdrop]=\"true\"\n cdkConnectedOverlayBackdropClass=\"cdk-overlay-transparent-backdrop\"\n (backdropClick)=\"onClose()\"\n (detach)=\"onClose()\"\n (attach)=\"onAttach()\"\n>\n <div\n #panel\n [id]=\"panelId\"\n [attr.role]=\"role()\"\n [attr.aria-label]=\"ariaLabel()\"\n class=\"wm-popover-panel\"\n [style.width]=\"width() === 'auto' ? 'auto' : width() + 'px'\"\n cdkTrapFocus\n [cdkTrapFocusAutoCapture]=\"trapFocusAutoCapture()\"\n (keydown.escape)=\"onClose()\"\n >\n <ng-content select=\"[content]\"></ng-content>\n </div>\n</ng-template>\n", styles: ["wm-popover{display:inline-flex;align-items:center}.wm-popover-trigger{display:block;width:100%}.wm-popover-panel{background:var(--wm-bg-raised);border:1px solid var(--wm-border);border-radius:var(--wm-radius-lg);box-shadow:0 4px 6px -1px #00000014,0 10px 20px -4px #00000024;overflow:hidden;outline:none}\n"] }]
440
- }], ctorParameters: () => [], propDecorators: { isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hasPopup: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasPopup", required: false }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], trapFocusAutoCapture: [{ type: i0.Input, args: [{ isSignal: true, alias: "trapFocusAutoCapture", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }] } });
543
+ args: [{ selector: 'wm-popover', imports: [CdkConnectedOverlay, CdkOverlayOrigin, CdkTrapFocus], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
544
+ // A consumer writes `role="dialog"` as a plain attribute, and Angular
545
+ // copies a static attribute onto the host element as well as into the
546
+ // `role` input. The host wraps only the trigger, so that copy would put
547
+ // the trigger inside a second, unnamed dialog, or inside a menu that owns
548
+ // no menuitem. The role belongs to the panel alone.
549
+ '[attr.role]': 'null',
550
+ }, template: "<!-- Trigger anchor -->\n<div cdkOverlayOrigin #origin=\"cdkOverlayOrigin\" class=\"wm-popover-trigger\">\n <ng-content select=\"[trigger]\"></ng-content>\n</div>\n\n<!-- Floating panel -->\n<ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"origin\"\n [cdkConnectedOverlayOpen]=\"isOpen()\"\n [cdkConnectedOverlayPositions]=\"positions\"\n [cdkConnectedOverlayScrollStrategy]=\"scrollStrategy\"\n [cdkConnectedOverlayHasBackdrop]=\"true\"\n cdkConnectedOverlayBackdropClass=\"cdk-overlay-transparent-backdrop\"\n (backdropClick)=\"onClose()\"\n (detach)=\"onClose()\"\n (attach)=\"onAttach()\"\n>\n <div\n #panel\n [id]=\"panelId\"\n [attr.role]=\"role()\"\n [attr.aria-modal]=\"ariaModal()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"panelTabIndex()\"\n class=\"wm-popover-panel\"\n [style.width]=\"width() === 'auto' ? 'auto' : width() + 'px'\"\n cdkTrapFocus\n [cdkTrapFocusAutoCapture]=\"trapFocusAutoCapture()\"\n (keydown.escape)=\"onClose()\"\n (keydown)=\"onPanelKeydown($event)\"\n >\n <ng-content select=\"[content]\"></ng-content>\n </div>\n</ng-template>\n", styles: ["wm-popover{display:inline-flex;align-items:center}.wm-popover-trigger{display:block;width:100%}.wm-popover-panel{background:var(--wm-bg-raised);border:1px solid var(--wm-border);border-radius:var(--wm-radius-lg);box-shadow:0 4px 6px -1px #00000014,0 10px 20px -4px #00000024;overflow:hidden;outline:none}\n"] }]
551
+ }], ctorParameters: () => [], propDecorators: { isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hasPopup: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasPopup", required: false }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], trapFocusAutoCapture: [{ type: i0.Input, args: [{ isSignal: true, alias: "trapFocusAutoCapture", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], trap: [{ type: i0.ViewChild, args: [i0.forwardRef(() => CdkTrapFocus), { isSignal: true }] }] } });
441
552
 
442
553
  /**
443
554
  * WmAutoFocus — focuses an element when it enters the DOM.
@@ -513,21 +624,85 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImpo
513
624
  }]
514
625
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], unavailable: [{ type: i0.Input, args: [{ isSignal: true, alias: "unavailable", required: false }] }], unavailableReason: [{ type: i0.Input, args: [{ isSignal: true, alias: "unavailableReason", required: false }] }], separatorBefore: [{ type: i0.Input, args: [{ isSignal: true, alias: "separatorBefore", required: false }] }], tmpl: [{ type: i0.ViewChild, args: ['tmpl', { isSignal: true }] }] } });
515
626
 
627
+ /**
628
+ * WmTabs — an ARIA tablist of `<wm-tab>` children with a roving tab stop.
629
+ *
630
+ * Usage:
631
+ * <wm-tabs [activeValue]="tool()" ariaLabel="Navigator tools" (select)="tool.set($event)">
632
+ * <wm-tab value="graph">Graph</wm-tab>
633
+ * <wm-tab value="search">Search</wm-tab>
634
+ * </wm-tabs>
635
+ *
636
+ * Selection:
637
+ * - `activeValue` names the selected tab. `null`, or a value no tab carries,
638
+ * selects nothing: every tab reports aria-selected="false" and none paints
639
+ * as active. Use it when the stage shows something that is not any tab's
640
+ * panel, such as a document opened from a second tablist in the same row.
641
+ * - The tablist always keeps exactly one tab stop: the focused tab while
642
+ * focus is inside, otherwise the selected tab, otherwise the first tab that
643
+ * is not unavailable (the first tab when every tab is unavailable).
644
+ * - Arrow keys, Home and End move focus only. A tab is selected on click,
645
+ * Enter or Space, which emit `(select)`; the consumer sets `activeValue`.
646
+ *
647
+ * Accessibility:
648
+ * - `ariaLabel` or `ariaLabelledby` names the inner role="tablist". Name
649
+ * every tablist that shares a surface with another one, or assistive
650
+ * technology cannot tell them apart. `ariaLabelledby` wins when both are
651
+ * set. An `aria-label` attribute on the `<wm-tabs>` host names nothing: the
652
+ * host has no role, so the attribute never reaches the tablist.
653
+ * - Each tab is `id="wm-tab-{value}"` with `aria-controls="wm-panel-{value}"`.
654
+ * Give the panel that id and role="tabpanel". The ids are built from the
655
+ * value alone, so values must be unique across every tablist on the page.
656
+ */
516
657
  class WmTabs {
658
+ /**
659
+ * Value of the selected tab. `null`, or a value that matches no tab, selects
660
+ * nothing; the tablist still keeps one tab stop (see the class comment).
661
+ */
517
662
  activeValue = input.required(...(ngDevMode ? [{ debugName: "activeValue" }] : /* istanbul ignore next */ []));
518
663
  variant = input('line', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
664
+ /** Accessible name for the tablist. Ignored while `ariaLabelledby` is set. */
665
+ ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
666
+ /**
667
+ * Id of a visible element that names the tablist. Prefer it over
668
+ * `ariaLabel` when a visible caption exists, so the accessible name matches
669
+ * the visible text (WCAG 2.5.3, Label in Name).
670
+ */
671
+ ariaLabelledby = input(null, ...(ngDevMode ? [{ debugName: "ariaLabelledby" }] : /* istanbul ignore next */ []));
519
672
  select = output();
520
673
  tabs = contentChildren(WmTab, ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
521
- focusedIndex = signal(null, ...(ngDevMode ? [{ debugName: "focusedIndex" }] : /* istanbul ignore next */ []));
522
- selectTab(tab, index) {
523
- this.focusedIndex.set(index);
674
+ /**
675
+ * Value of the tab that holds focus, or null while focus is outside the
676
+ * tablist. Keyed by value, not index, so a tab added or removed before it
677
+ * cannot move the stop onto a different tab or off the end of the list.
678
+ */
679
+ focusedValue = signal(null, ...(ngDevMode ? [{ debugName: "focusedValue" }] : /* istanbul ignore next */ []));
680
+ /** Index of the one tab that is in the page's Tab order. */
681
+ tabStop = computed(() => {
682
+ const tabs = this.tabs();
683
+ const focused = this.focusedValue();
684
+ const focusedIndex = focused === null ? -1 : tabs.findIndex(t => t.value() === focused);
685
+ if (focusedIndex >= 0)
686
+ return focusedIndex;
687
+ const active = this.activeValue();
688
+ const activeIndex = active === null ? -1 : tabs.findIndex(t => t.value() === active);
689
+ if (activeIndex >= 0)
690
+ return activeIndex;
691
+ const firstAvailable = tabs.findIndex(t => !t.unavailable());
692
+ return firstAvailable >= 0 ? firstAvailable : 0;
693
+ }, ...(ngDevMode ? [{ debugName: "tabStop" }] : /* istanbul ignore next */ []));
694
+ host = inject(ElementRef);
695
+ selectTab(tab) {
696
+ this.focusedValue.set(tab.value());
524
697
  if (!tab.unavailable())
525
698
  this.select.emit(tab.value());
526
699
  }
527
700
  onBlur() {
528
701
  setTimeout(() => {
529
- if (!document.activeElement?.closest('.wm-tabs')) {
530
- this.focusedIndex.set(null);
702
+ // This tablist only. Focus that moved to a second <wm-tabs> must still
703
+ // return this one's stop to its selected tab.
704
+ if (!this.host.nativeElement.contains(document.activeElement)) {
705
+ this.focusedValue.set(null);
531
706
  }
532
707
  }, 0);
533
708
  }
@@ -535,8 +710,7 @@ class WmTabs {
535
710
  const tabs = this.tabs();
536
711
  if (tabs.length === 0)
537
712
  return;
538
- const activeIndex = tabs.findIndex(t => t.value() === this.activeValue());
539
- const currentIndex = this.focusedIndex() ?? (activeIndex >= 0 ? activeIndex : 0);
713
+ const currentIndex = this.tabStop();
540
714
  let nextIndex = null;
541
715
  switch (event.key) {
542
716
  case 'ArrowRight':
@@ -556,19 +730,43 @@ class WmTabs {
556
730
  }
557
731
  if (nextIndex !== null) {
558
732
  event.preventDefault();
559
- this.focusedIndex.set(nextIndex);
733
+ this.focusedValue.set(tabs[nextIndex].value());
560
734
  const btns = container.querySelectorAll('[role="tab"]');
561
735
  btns[nextIndex]?.focus();
562
736
  }
563
737
  }
564
738
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: WmTabs, deps: [], target: i0.ɵɵFactoryTarget.Component });
565
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.23", type: WmTabs, isStandalone: true, selector: "wm-tabs", inputs: { activeValue: { classPropertyName: "activeValue", publicName: "activeValue", isSignal: true, isRequired: true, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select" }, queries: [{ propertyName: "tabs", predicate: WmTab, isSignal: true }], ngImport: i0, template: "<div\n class=\"wm-tabs\"\n [class.wm-tabs--line]=\"variant() === 'line'\"\n [class.wm-tabs--pill]=\"variant() === 'pill'\"\n role=\"tablist\"\n #container\n (keydown)=\"onKeyDown($event, container)\"\n>\n @for (tab of tabs(); track tab.value(); let i = $index) {\n @if (tab.separatorBefore()) {\n <span\n class=\"wm-tab-separator\"\n role=\"separator\"\n aria-orientation=\"vertical\"\n ></span>\n }\n <button\n type=\"button\"\n role=\"tab\"\n class=\"wm-tab\"\n [id]=\"'wm-tab-' + tab.value()\"\n [attr.aria-selected]=\"activeValue() === tab.value()\"\n [attr.aria-controls]=\"'wm-panel-' + tab.value()\"\n [attr.aria-disabled]=\"tab.unavailable() ? true : null\"\n [attr.aria-description]=\"tab.unavailable() ? tab.unavailableReason() : null\"\n [attr.title]=\"tab.unavailable() ? tab.unavailableReason() : null\"\n [tabIndex]=\"(focusedIndex() === null ? activeValue() === tab.value() : focusedIndex() === i) ? 0 : -1\"\n [class.is-active]=\"activeValue() === tab.value()\"\n [class.is-unavailable]=\"tab.unavailable()\"\n (click)=\"selectTab(tab, i)\"\n (focus)=\"focusedIndex.set(i)\"\n (blur)=\"onBlur()\"\n >\n <ng-container [ngTemplateOutlet]=\"tab.tmpl()\" />\n </button>\n }\n</div>\n", styles: [".wm-tab{display:inline-flex;align-items:center;gap:6px;font-size:.8125rem;font-weight:600;background:transparent;border:none;cursor:pointer;white-space:nowrap;transition:color var(--wm-duration-fast) var(--wm-ease-standard),background var(--wm-duration-fast) var(--wm-ease-standard),border-color var(--wm-duration-fast) var(--wm-ease-standard)}.wm-tab:focus-visible{outline:2px solid var(--wm-color-accent);outline-offset:-2px;border-radius:var(--wm-radius-sm)}.wm-tab.is-unavailable{cursor:not-allowed;color:color-mix(in srgb,var(--wm-text-muted) 52%,transparent);box-shadow:none}.wm-tab.is-unavailable:hover{background:transparent;color:color-mix(in srgb,var(--wm-text-muted) 52%,transparent)}.wm-tab-separator{flex:0 0 1px;align-self:center;width:1px;height:1.25rem;margin:0 .375rem;background:var(--wm-border-subtle)}.wm-tabs--line{display:flex;gap:1.5rem;border-bottom:1px solid var(--wm-border-subtle)}.wm-tabs--line .wm-tab{padding:.625rem .25rem;color:var(--wm-text-muted);border-radius:0;border-bottom:2px solid transparent;margin-bottom:-1px}.wm-tabs--line .wm-tab:hover{color:var(--wm-text-secondary)}.wm-tabs--line .wm-tab.is-active{color:var(--wm-color-accent-readable);border-bottom-color:var(--wm-color-accent-readable)}.wm-tabs--pill{display:inline-flex;background:var(--wm-bg-sunken);padding:.25rem;border-radius:var(--wm-radius-md);gap:.125rem;border:1px solid var(--wm-border-subtle)}.wm-tabs--pill .wm-tab{padding:.375rem .75rem;border-radius:var(--wm-radius-sm);color:var(--wm-text-muted)}.wm-tabs--pill .wm-tab:hover{background:color-mix(in srgb,var(--wm-color-accent) 5%,transparent);color:var(--wm-text-secondary)}.wm-tabs--pill .wm-tab.is-active{background:var(--wm-bg-float);color:var(--wm-text-primary);box-shadow:var(--wm-shadow-sm)}.wm-tabs--line .wm-tab.is-unavailable,.wm-tabs--line .wm-tab.is-unavailable:hover,.wm-tabs--pill .wm-tab.is-unavailable,.wm-tabs--pill .wm-tab.is-unavailable:hover{background:transparent;color:color-mix(in srgb,var(--wm-text-muted) 52%,transparent);box-shadow:none}@media(prefers-reduced-motion:reduce){.wm-tab{transition:none}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
739
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.23", type: WmTabs, isStandalone: true, selector: "wm-tabs", inputs: { activeValue: { classPropertyName: "activeValue", publicName: "activeValue", isSignal: true, isRequired: true, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "ariaLabelledby", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select" }, queries: [{ propertyName: "tabs", predicate: WmTab, isSignal: true }], ngImport: i0, template: "<div\n class=\"wm-tabs\"\n [class.wm-tabs--line]=\"variant() === 'line'\"\n [class.wm-tabs--pill]=\"variant() === 'pill'\"\n role=\"tablist\"\n [attr.aria-label]=\"ariaLabelledby() ? null : ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledby() || null\"\n #container\n (keydown)=\"onKeyDown($event, container)\"\n>\n @for (tab of tabs(); track tab.value(); let i = $index) {\n @if (tab.separatorBefore()) {\n <span\n class=\"wm-tab-separator\"\n role=\"separator\"\n aria-orientation=\"vertical\"\n ></span>\n }\n <button\n type=\"button\"\n role=\"tab\"\n class=\"wm-tab\"\n [id]=\"'wm-tab-' + tab.value()\"\n [attr.aria-selected]=\"activeValue() === tab.value()\"\n [attr.aria-controls]=\"'wm-panel-' + tab.value()\"\n [attr.aria-disabled]=\"tab.unavailable() ? true : null\"\n [attr.aria-description]=\"tab.unavailable() ? tab.unavailableReason() : null\"\n [attr.title]=\"tab.unavailable() ? tab.unavailableReason() : null\"\n [tabIndex]=\"tabStop() === i ? 0 : -1\"\n [class.is-active]=\"activeValue() === tab.value()\"\n [class.is-unavailable]=\"tab.unavailable()\"\n (click)=\"selectTab(tab)\"\n (focus)=\"focusedValue.set(tab.value())\"\n (blur)=\"onBlur()\"\n >\n <ng-container [ngTemplateOutlet]=\"tab.tmpl()\" />\n </button>\n }\n</div>\n", styles: [".wm-tab{display:inline-flex;align-items:center;gap:6px;font-size:.8125rem;font-weight:600;background:transparent;border:none;cursor:pointer;white-space:nowrap;transition:color var(--wm-duration-fast) var(--wm-ease-standard),background var(--wm-duration-fast) var(--wm-ease-standard),border-color var(--wm-duration-fast) var(--wm-ease-standard)}.wm-tab:focus-visible{outline:2px solid var(--wm-color-accent);outline-offset:-2px;border-radius:var(--wm-radius-sm)}.wm-tab.is-unavailable{cursor:not-allowed;color:color-mix(in srgb,var(--wm-text-muted) 52%,transparent);box-shadow:none}.wm-tab.is-unavailable:hover{background:transparent;color:color-mix(in srgb,var(--wm-text-muted) 52%,transparent)}.wm-tab-separator{flex:0 0 1px;align-self:center;width:1px;height:1.25rem;margin:0 .375rem;background:var(--wm-border-subtle)}.wm-tabs--line{display:flex;gap:1.5rem;border-bottom:1px solid var(--wm-border-subtle)}.wm-tabs--line .wm-tab{padding:.625rem .25rem;color:var(--wm-text-muted);border-radius:0;border-bottom:2px solid transparent;margin-bottom:-1px}.wm-tabs--line .wm-tab:hover{color:var(--wm-text-secondary)}.wm-tabs--line .wm-tab.is-active{color:var(--wm-color-accent-readable);border-bottom-color:var(--wm-color-accent-readable)}.wm-tabs--pill{display:inline-flex;background:var(--wm-bg-sunken);padding:.25rem;border-radius:var(--wm-radius-md);gap:.125rem;border:1px solid var(--wm-border-subtle)}.wm-tabs--pill .wm-tab{padding:.375rem .75rem;border-radius:var(--wm-radius-sm);color:var(--wm-text-muted)}.wm-tabs--pill .wm-tab:hover{background:color-mix(in srgb,var(--wm-color-accent) 5%,transparent);color:var(--wm-text-secondary)}.wm-tabs--pill .wm-tab.is-active{background:var(--wm-bg-float);color:var(--wm-text-primary);box-shadow:var(--wm-shadow-sm)}.wm-tabs--line .wm-tab.is-unavailable,.wm-tabs--line .wm-tab.is-unavailable:hover,.wm-tabs--pill .wm-tab.is-unavailable,.wm-tabs--pill .wm-tab.is-unavailable:hover{background:transparent;color:color-mix(in srgb,var(--wm-text-muted) 52%,transparent);box-shadow:none}@media(prefers-reduced-motion:reduce){.wm-tab{transition:none}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
566
740
  }
567
741
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: WmTabs, decorators: [{
568
742
  type: Component,
569
- args: [{ selector: 'wm-tabs', imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"wm-tabs\"\n [class.wm-tabs--line]=\"variant() === 'line'\"\n [class.wm-tabs--pill]=\"variant() === 'pill'\"\n role=\"tablist\"\n #container\n (keydown)=\"onKeyDown($event, container)\"\n>\n @for (tab of tabs(); track tab.value(); let i = $index) {\n @if (tab.separatorBefore()) {\n <span\n class=\"wm-tab-separator\"\n role=\"separator\"\n aria-orientation=\"vertical\"\n ></span>\n }\n <button\n type=\"button\"\n role=\"tab\"\n class=\"wm-tab\"\n [id]=\"'wm-tab-' + tab.value()\"\n [attr.aria-selected]=\"activeValue() === tab.value()\"\n [attr.aria-controls]=\"'wm-panel-' + tab.value()\"\n [attr.aria-disabled]=\"tab.unavailable() ? true : null\"\n [attr.aria-description]=\"tab.unavailable() ? tab.unavailableReason() : null\"\n [attr.title]=\"tab.unavailable() ? tab.unavailableReason() : null\"\n [tabIndex]=\"(focusedIndex() === null ? activeValue() === tab.value() : focusedIndex() === i) ? 0 : -1\"\n [class.is-active]=\"activeValue() === tab.value()\"\n [class.is-unavailable]=\"tab.unavailable()\"\n (click)=\"selectTab(tab, i)\"\n (focus)=\"focusedIndex.set(i)\"\n (blur)=\"onBlur()\"\n >\n <ng-container [ngTemplateOutlet]=\"tab.tmpl()\" />\n </button>\n }\n</div>\n", styles: [".wm-tab{display:inline-flex;align-items:center;gap:6px;font-size:.8125rem;font-weight:600;background:transparent;border:none;cursor:pointer;white-space:nowrap;transition:color var(--wm-duration-fast) var(--wm-ease-standard),background var(--wm-duration-fast) var(--wm-ease-standard),border-color var(--wm-duration-fast) var(--wm-ease-standard)}.wm-tab:focus-visible{outline:2px solid var(--wm-color-accent);outline-offset:-2px;border-radius:var(--wm-radius-sm)}.wm-tab.is-unavailable{cursor:not-allowed;color:color-mix(in srgb,var(--wm-text-muted) 52%,transparent);box-shadow:none}.wm-tab.is-unavailable:hover{background:transparent;color:color-mix(in srgb,var(--wm-text-muted) 52%,transparent)}.wm-tab-separator{flex:0 0 1px;align-self:center;width:1px;height:1.25rem;margin:0 .375rem;background:var(--wm-border-subtle)}.wm-tabs--line{display:flex;gap:1.5rem;border-bottom:1px solid var(--wm-border-subtle)}.wm-tabs--line .wm-tab{padding:.625rem .25rem;color:var(--wm-text-muted);border-radius:0;border-bottom:2px solid transparent;margin-bottom:-1px}.wm-tabs--line .wm-tab:hover{color:var(--wm-text-secondary)}.wm-tabs--line .wm-tab.is-active{color:var(--wm-color-accent-readable);border-bottom-color:var(--wm-color-accent-readable)}.wm-tabs--pill{display:inline-flex;background:var(--wm-bg-sunken);padding:.25rem;border-radius:var(--wm-radius-md);gap:.125rem;border:1px solid var(--wm-border-subtle)}.wm-tabs--pill .wm-tab{padding:.375rem .75rem;border-radius:var(--wm-radius-sm);color:var(--wm-text-muted)}.wm-tabs--pill .wm-tab:hover{background:color-mix(in srgb,var(--wm-color-accent) 5%,transparent);color:var(--wm-text-secondary)}.wm-tabs--pill .wm-tab.is-active{background:var(--wm-bg-float);color:var(--wm-text-primary);box-shadow:var(--wm-shadow-sm)}.wm-tabs--line .wm-tab.is-unavailable,.wm-tabs--line .wm-tab.is-unavailable:hover,.wm-tabs--pill .wm-tab.is-unavailable,.wm-tabs--pill .wm-tab.is-unavailable:hover{background:transparent;color:color-mix(in srgb,var(--wm-text-muted) 52%,transparent);box-shadow:none}@media(prefers-reduced-motion:reduce){.wm-tab{transition:none}}\n"] }]
570
- }], propDecorators: { activeValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeValue", required: true }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], select: [{ type: i0.Output, args: ["select"] }], tabs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => WmTab), { isSignal: true }] }] } });
743
+ args: [{ selector: 'wm-tabs', imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"wm-tabs\"\n [class.wm-tabs--line]=\"variant() === 'line'\"\n [class.wm-tabs--pill]=\"variant() === 'pill'\"\n role=\"tablist\"\n [attr.aria-label]=\"ariaLabelledby() ? null : ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledby() || null\"\n #container\n (keydown)=\"onKeyDown($event, container)\"\n>\n @for (tab of tabs(); track tab.value(); let i = $index) {\n @if (tab.separatorBefore()) {\n <span\n class=\"wm-tab-separator\"\n role=\"separator\"\n aria-orientation=\"vertical\"\n ></span>\n }\n <button\n type=\"button\"\n role=\"tab\"\n class=\"wm-tab\"\n [id]=\"'wm-tab-' + tab.value()\"\n [attr.aria-selected]=\"activeValue() === tab.value()\"\n [attr.aria-controls]=\"'wm-panel-' + tab.value()\"\n [attr.aria-disabled]=\"tab.unavailable() ? true : null\"\n [attr.aria-description]=\"tab.unavailable() ? tab.unavailableReason() : null\"\n [attr.title]=\"tab.unavailable() ? tab.unavailableReason() : null\"\n [tabIndex]=\"tabStop() === i ? 0 : -1\"\n [class.is-active]=\"activeValue() === tab.value()\"\n [class.is-unavailable]=\"tab.unavailable()\"\n (click)=\"selectTab(tab)\"\n (focus)=\"focusedValue.set(tab.value())\"\n (blur)=\"onBlur()\"\n >\n <ng-container [ngTemplateOutlet]=\"tab.tmpl()\" />\n </button>\n }\n</div>\n", styles: [".wm-tab{display:inline-flex;align-items:center;gap:6px;font-size:.8125rem;font-weight:600;background:transparent;border:none;cursor:pointer;white-space:nowrap;transition:color var(--wm-duration-fast) var(--wm-ease-standard),background var(--wm-duration-fast) var(--wm-ease-standard),border-color var(--wm-duration-fast) var(--wm-ease-standard)}.wm-tab:focus-visible{outline:2px solid var(--wm-color-accent);outline-offset:-2px;border-radius:var(--wm-radius-sm)}.wm-tab.is-unavailable{cursor:not-allowed;color:color-mix(in srgb,var(--wm-text-muted) 52%,transparent);box-shadow:none}.wm-tab.is-unavailable:hover{background:transparent;color:color-mix(in srgb,var(--wm-text-muted) 52%,transparent)}.wm-tab-separator{flex:0 0 1px;align-self:center;width:1px;height:1.25rem;margin:0 .375rem;background:var(--wm-border-subtle)}.wm-tabs--line{display:flex;gap:1.5rem;border-bottom:1px solid var(--wm-border-subtle)}.wm-tabs--line .wm-tab{padding:.625rem .25rem;color:var(--wm-text-muted);border-radius:0;border-bottom:2px solid transparent;margin-bottom:-1px}.wm-tabs--line .wm-tab:hover{color:var(--wm-text-secondary)}.wm-tabs--line .wm-tab.is-active{color:var(--wm-color-accent-readable);border-bottom-color:var(--wm-color-accent-readable)}.wm-tabs--pill{display:inline-flex;background:var(--wm-bg-sunken);padding:.25rem;border-radius:var(--wm-radius-md);gap:.125rem;border:1px solid var(--wm-border-subtle)}.wm-tabs--pill .wm-tab{padding:.375rem .75rem;border-radius:var(--wm-radius-sm);color:var(--wm-text-muted)}.wm-tabs--pill .wm-tab:hover{background:color-mix(in srgb,var(--wm-color-accent) 5%,transparent);color:var(--wm-text-secondary)}.wm-tabs--pill .wm-tab.is-active{background:var(--wm-bg-float);color:var(--wm-text-primary);box-shadow:var(--wm-shadow-sm)}.wm-tabs--line .wm-tab.is-unavailable,.wm-tabs--line .wm-tab.is-unavailable:hover,.wm-tabs--pill .wm-tab.is-unavailable,.wm-tabs--pill .wm-tab.is-unavailable:hover{background:transparent;color:color-mix(in srgb,var(--wm-text-muted) 52%,transparent);box-shadow:none}@media(prefers-reduced-motion:reduce){.wm-tab{transition:none}}\n"] }]
744
+ }], propDecorators: { activeValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeValue", required: true }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledby", required: false }] }], select: [{ type: i0.Output, args: ["select"] }], tabs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => WmTab), { isSignal: true }] }] } });
571
745
 
746
+ /**
747
+ * The class set for one button. `<wm-button>` puts it on its inner `<button>`
748
+ * and `[wmButton]` puts it on the host, so the two forms cannot drift apart.
749
+ */
750
+ function wmButtonClass(variant, size, iconOnly) {
751
+ const base = `wm-btn wm-btn--${variant} wm-btn--${size}`;
752
+ return iconOnly ? `${base} btn-icon-only` : base;
753
+ }
754
+ /**
755
+ * WmButton — an Aegis button as an element: `<wm-button variant="ghost">`.
756
+ *
757
+ * The host wraps a real `<button>` and passes it four inputs: `disabled`,
758
+ * `type`, `ariaLabel` (as `aria-label`) and `btnTitle` (as `title`). Nothing
759
+ * else crosses. An attribute, class or directive written on `<wm-button>` stays on
760
+ * the host, which has no role and cannot take focus. When something must reach
761
+ * the element a screen reader and the keyboard use (ARIA state such as
762
+ * `aria-pressed` or `aria-expanded`, `cdkFocusInitial`, an `id` another element
763
+ * points at, a popover `trigger`, `[wmTooltip]`, a class that changes the
764
+ * button's own box), write a native element with `[wmButton]` instead:
765
+ *
766
+ * ```html
767
+ * <button wmButton variant="ghost" [attr.aria-pressed]="wrap()">Wrap</button>
768
+ * ```
769
+ */
572
770
  class WmButton {
573
771
  variant = input('primary', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
574
772
  size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
@@ -580,10 +778,7 @@ class WmButton {
580
778
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
581
779
  /** Tooltip text forwarded to the inner <button>. Named btnTitle to avoid native attribute conflict. */
582
780
  btnTitle = input('', ...(ngDevMode ? [{ debugName: "btnTitle" }] : /* istanbul ignore next */ []));
583
- btnClass = computed(() => {
584
- const base = `wm-btn wm-btn--${this.variant()} wm-btn--${this.size()}`;
585
- return this.iconOnly() ? `${base} btn-icon-only` : base;
586
- }, ...(ngDevMode ? [{ debugName: "btnClass" }] : /* istanbul ignore next */ []));
781
+ btnClass = computed(() => wmButtonClass(this.variant(), this.size(), this.iconOnly()), ...(ngDevMode ? [{ debugName: "btnClass" }] : /* istanbul ignore next */ []));
587
782
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: WmButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
588
783
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.23", type: WmButton, isStandalone: true, selector: "wm-button", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, iconOnly: { classPropertyName: "iconOnly", publicName: "iconOnly", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, btnTitle: { classPropertyName: "btnTitle", publicName: "btnTitle", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<button\n [class]=\"btnClass()\"\n [disabled]=\"disabled()\"\n [type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.title]=\"btnTitle() || null\"\n>\n <ng-content />\n</button>\n", styles: [":host{display:inline-flex}\n"] });
589
784
  }
@@ -592,6 +787,61 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImpo
592
787
  args: [{ selector: 'wm-button', template: "<button\n [class]=\"btnClass()\"\n [disabled]=\"disabled()\"\n [type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.title]=\"btnTitle() || null\"\n>\n <ng-content />\n</button>\n", styles: [":host{display:inline-flex}\n"] }]
593
788
  }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], iconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOnly", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], btnTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "btnTitle", required: false }] }] } });
594
789
 
790
+ /**
791
+ * WmButtonDirective — the Aegis button classes on a native `<button>` or `<a>`.
792
+ *
793
+ * It renders exactly the class set `<wm-button>` puts on its inner button, but
794
+ * on the element the consumer writes. That element is the focusable one, so
795
+ * every attribute and directive on it reaches the right place with no
796
+ * forwarding list: ARIA state, `cdkFocusInitial`, `id`, `form`, a popover
797
+ * `trigger`, `[wmTooltip]`, test ids and consumer classes.
798
+ *
799
+ * Use `<wm-button>` by default. Use this when one of those has to land on the
800
+ * button itself (see `WmButton` for what the component forwards).
801
+ *
802
+ * - `variant`, `size` and `iconOnly` mean what they mean on `<wm-button>`.
803
+ * - Classes the consumer writes, static or bound, are kept.
804
+ * - A `<button>` with no `type` gets `type="button"`, the `<wm-button>`
805
+ * default, so it does not submit a surrounding form. A written or bound
806
+ * `type` wins. An `<a>` is left alone.
807
+ * - Everything else is native: write `disabled`, `aria-label` and `title`
808
+ * directly. An `<a>` cannot be disabled.
809
+ *
810
+ * @example
811
+ * <button wmButton variant="ghost" size="sm" [attr.aria-pressed]="wrap()" (click)="toggleWrap()">
812
+ * Wrap lines
813
+ * </button>
814
+ * <button wmButton variant="secondary" cdkFocusInitial (click)="cancel()">Keep editing</button>
815
+ * <a wmButton variant="ghost" href="/back">Back</a>
816
+ */
817
+ class WmButtonDirective {
818
+ variant = input('primary', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
819
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
820
+ /** Makes the button square. An icon-only button still needs an `aria-label`. */
821
+ iconOnly = input(false, { ...(ngDevMode ? { debugName: "iconOnly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
822
+ hostClass = computed(() => wmButtonClass(this.variant(), this.size(), this.iconOnly()), ...(ngDevMode ? [{ debugName: "hostClass" }] : /* istanbul ignore next */ []));
823
+ constructor() {
824
+ // Static attributes are on the element before a directive is constructed,
825
+ // and a `[type]` or `[attr.type]` binding runs after, so this sets a default
826
+ // that anything the consumer writes overrides.
827
+ const element = inject(ElementRef).nativeElement;
828
+ if (element.tagName === 'BUTTON' && !element.hasAttribute('type')) {
829
+ element.setAttribute('type', 'button');
830
+ }
831
+ }
832
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: WmButtonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
833
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.23", type: WmButtonDirective, isStandalone: true, selector: "button[wmButton], a[wmButton]", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, iconOnly: { classPropertyName: "iconOnly", publicName: "iconOnly", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClass()" } }, ngImport: i0 });
834
+ }
835
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: WmButtonDirective, decorators: [{
836
+ type: Directive,
837
+ args: [{
838
+ selector: 'button[wmButton], a[wmButton]',
839
+ host: {
840
+ '[class]': 'hostClass()',
841
+ },
842
+ }]
843
+ }], ctorParameters: () => [], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], iconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOnly", required: false }] }] } });
844
+
595
845
  /**
596
846
  * WmCheckbox — Boolean selection control with optional indeterminate state.
597
847
  *
@@ -783,6 +1033,631 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImpo
783
1033
  args: [{ selector: 'wm-toggle', template: "<button\n type=\"button\"\n role=\"switch\"\n class=\"wm-toggle\"\n [class.is-on]=\"checked()\"\n [class.is-disabled]=\"disabled()\"\n [attr.aria-checked]=\"checked()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-disabled]=\"disabled() ? true : null\"\n [disabled]=\"disabled() || null\"\n (click)=\"toggle()\"\n>\n <span class=\"wm-toggle-track\" aria-hidden=\"true\">\n <span class=\"wm-toggle-thumb\"></span>\n </span>\n</button>\n", styles: [":host{display:inline-flex}.wm-toggle{display:inline-flex;align-items:center;background:none;border:none;padding:2px;margin:-2px;cursor:pointer;border-radius:var(--wm-radius-full);outline:none}.wm-toggle:focus-visible .wm-toggle-track{outline:2px solid var(--wm-color-accent);outline-offset:2px}.wm-toggle.is-disabled{opacity:.45;cursor:not-allowed;pointer-events:none}.wm-toggle-track{position:relative;width:40px;height:22px;background:var(--wm-border-input);border-radius:var(--wm-radius-full);transition:background var(--wm-transition-fast);flex-shrink:0}.wm-toggle.is-on .wm-toggle-track{background:var(--wm-color-accent)}.wm-toggle-thumb{position:absolute;top:3px;left:3px;width:16px;height:16px;background:#fff;border-radius:var(--wm-radius-full);box-shadow:0 1px 3px #00000040;transition:transform var(--wm-transition-fast)}.wm-toggle.is-on .wm-toggle-thumb{transform:translate(18px)}@media(prefers-reduced-motion:reduce){.wm-toggle-track,.wm-toggle-thumb{transition:none}}\n"] }]
784
1034
  }], propDecorators: { checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
785
1035
 
1036
+ /**
1037
+ * Upper bound on generated marks. A numeric interval that would exceed it draws
1038
+ * NOTHING rather than a truncated grid or a frozen tab: an empty rail is
1039
+ * obviously wrong to whoever set it, where a silently shortened grid is not.
1040
+ */
1041
+ const MAX_TICKS = 1000;
1042
+ /**
1043
+ * Upper bound on reserved readout strings. Only labelled stops reach this, so
1044
+ * it is a guard against a pathological grid rather than a limit anyone meets.
1045
+ */
1046
+ const MAX_VALUE_CANDIDATES = 64;
1047
+ /**
1048
+ * WmSlider — Continuous value selection along a bounded range.
1049
+ *
1050
+ * Built on a native `<input type="range">`. The native control is the whole
1051
+ * interaction: pointer drag, touch, arrow keys, Home/End, Page Up/Page Down,
1052
+ * RTL mirroring and the implicit `role="slider"` with `aria-valuenow` all come
1053
+ * from the browser rather than from hand-written handlers. Aegis styles the
1054
+ * track and thumb through the range pseudo-elements, so there is no invisible
1055
+ * overlay input and no drag maths to get wrong.
1056
+ *
1057
+ * When to use:
1058
+ * - A bounded value where the approximate position matters more than the exact
1059
+ * number (volume, opacity, zoom, a brightness or threshold setting)
1060
+ * - A value the user wants to sweep through and preview as it changes
1061
+ *
1062
+ * When NOT to use:
1063
+ * - An exact number the user knows before they start typing it → <wm-number-input>
1064
+ * - Fewer than about five discrete choices → <wm-toggle-group> or <wm-select>
1065
+ * - An unbounded value — a slider has to have a min and a max
1066
+ *
1067
+ * Value pipeline: every value, whether it arrives from a two-way binding, a
1068
+ * form, or a drag, is clamped into min..max and snapped onto the step grid
1069
+ * measured FROM MIN. The snapped value is written back, so the binding, the
1070
+ * form control and the thumb never disagree.
1071
+ *
1072
+ * Accessibility:
1073
+ * - The native input is the focusable control and carries the slider role.
1074
+ * - `ariaLabel` is required unless visible surrounding text names the control.
1075
+ * - `formatValue` also drives `aria-valuetext`, so a screen reader announces
1076
+ * "40 percent" rather than "40" when the raw number means nothing alone.
1077
+ * - Ticks and the value readout are decoration; the native value is the truth.
1078
+ *
1079
+ * Form integration:
1080
+ * <!-- Template-driven -->
1081
+ * <wm-slider [(ngModel)]="volume" [min]="0" [max]="100" />
1082
+ *
1083
+ * <!-- Reactive -->
1084
+ * <wm-slider formControlName="opacity" [min]="0" [max]="1" [step]="0.05" />
1085
+ *
1086
+ * <!-- Two-way signal binding, no forms -->
1087
+ * <wm-slider [(value)]="volume" ariaLabel="Volume" />
1088
+ */
1089
+ class WmSlider {
1090
+ /** Two-way bindable value. Always reads back clamped and snapped to the step grid. */
1091
+ value = model(0, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1092
+ /** Lower bound, inclusive. */
1093
+ min = input(0, ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
1094
+ /** Upper bound, inclusive. Values never exceed it, even when it is off the step grid. */
1095
+ max = input(100, ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
1096
+ /** Grid spacing, measured from `min`. Use 0 for a continuous slider. */
1097
+ step = input(1, ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
1098
+ /** Disables the control from the template. Reactive forms set this independently. */
1099
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
1100
+ /** Accessible name. Required unless visible text already names the control. */
1101
+ ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
1102
+ /** Shows a live readout beside the rail. */
1103
+ showValue = input(false, ...(ngDevMode ? [{ debugName: "showValue" }] : /* istanbul ignore next */ []));
1104
+ /**
1105
+ * Renders the value for humans. Also sets `aria-valuetext`, so pass one
1106
+ * whenever the bare number would not be understood on its own.
1107
+ */
1108
+ formatValue = input(null, ...(ngDevMode ? [{ debugName: "formatValue" }] : /* istanbul ignore next */ []));
1109
+ /**
1110
+ * How the track is drawn. `rail` is one continuous bar. `segmented` breaks it
1111
+ * into blocks bounded by the grid stops, and the block holding the value fills
1112
+ * partially — so a segmented track does not have to mean one block per step,
1113
+ * and survives a dense range that one-block-per-step cannot.
1114
+ */
1115
+ track = input('rail', ...(ngDevMode ? [{ debugName: "track" }] : /* istanbul ignore next */ []));
1116
+ /**
1117
+ * The grid, in one of three shapes:
1118
+ * - a **number** — a stop every N, measured from `min`
1119
+ * - an **array** of {@link WmSliderTick} — explicit stops, with labels and levels
1120
+ * - a **{@link WmSliderTickFn}** — computed from the live range
1121
+ *
1122
+ * Whatever the shape, the result is clamped to the range, sorted, and
1123
+ * de-duplicated. The same resolved grid draws the marks AND bounds the blocks
1124
+ * of a segmented track, so the two renderings cannot disagree.
1125
+ *
1126
+ * The grid is deliberately independent of `step`: a 0–100 slider stepping by 1
1127
+ * carries 101 reachable values and should still draw about 11 marks.
1128
+ */
1129
+ ticks = input([], ...(ngDevMode ? [{ debugName: "ticks" }] : /* istanbul ignore next */ []));
1130
+ /** Set by reactive forms through setDisabledState, independent of the input. */
1131
+ formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "formDisabled" }] : /* istanbul ignore next */ []));
1132
+ isDisabled = computed(() => this.disabled() || this.formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
1133
+ /** Position of the thumb across the span, 0..1, as a CSS-ready string. */
1134
+ fraction = computed(() => trim(this.fractionOf(this.value())), ...(ngDevMode ? [{ debugName: "fraction" }] : /* istanbul ignore next */ []));
1135
+ displayValue = computed(() => {
1136
+ const format = this.formatValue();
1137
+ return format ? format(this.value()) : String(this.value());
1138
+ }, ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
1139
+ /**
1140
+ * Every string the readout can show at a grid stop, de-duplicated.
1141
+ *
1142
+ * The shell is `1fr auto`, so a readout column sized by the CURRENT value
1143
+ * takes its width from the rail and gives it back on the next value — the bar
1144
+ * moves while you drag it. Rendering these hidden in the readout's own grid
1145
+ * cell makes the browser reserve the widest of them once, with no measuring.
1146
+ *
1147
+ * Only LABELLED stops contribute. An unlabelled stop is a position rather
1148
+ * than a word, and a numeric readout is already bounded by tabular numerals
1149
+ * and the two endpoints — so a 101-mark grid reserves two slots, not 101.
1150
+ */
1151
+ valueCandidates = computed(() => {
1152
+ const format = this.formatValue();
1153
+ const render = (value) => (format ? format(value) : String(value));
1154
+ const values = [
1155
+ this.min(),
1156
+ this.max(),
1157
+ ...this.resolvedTicks()
1158
+ .filter(tick => tick.label !== undefined)
1159
+ .map(tick => tick.value),
1160
+ ];
1161
+ const out = new Set();
1162
+ for (const value of values) {
1163
+ out.add(render(value));
1164
+ if (out.size >= MAX_VALUE_CANDIDATES)
1165
+ break;
1166
+ }
1167
+ return [...out];
1168
+ }, ...(ngDevMode ? [{ debugName: "valueCandidates" }] : /* istanbul ignore next */ []));
1169
+ /** Null unless a formatter is set — a valuetext echoing valuenow is noise. */
1170
+ valueText = computed(() => {
1171
+ const format = this.formatValue();
1172
+ return format ? format(this.value()) : null;
1173
+ }, ...(ngDevMode ? [{ debugName: "valueText" }] : /* istanbul ignore next */ []));
1174
+ /** The declared grid, before it is measured against the range. */
1175
+ declaredTicks = computed(() => {
1176
+ const raw = this.ticks();
1177
+ if (typeof raw === 'number') {
1178
+ return gridEvery(this.min(), this.max(), raw);
1179
+ }
1180
+ if (typeof raw === 'function') {
1181
+ return raw({ min: this.min(), max: this.max(), step: this.step() }) ?? [];
1182
+ }
1183
+ return raw ?? [];
1184
+ }, ...(ngDevMode ? [{ debugName: "declaredTicks" }] : /* istanbul ignore next */ []));
1185
+ resolvedTicks = computed(() => {
1186
+ const min = this.min();
1187
+ const max = this.max();
1188
+ const current = this.value();
1189
+ const seen = new Set();
1190
+ return this.declaredTicks()
1191
+ .filter(tick => tick.value >= min && tick.value <= max)
1192
+ // A duplicate value is not cosmetic — two marks at one value collide on
1193
+ // the @for track key and Angular throws. First declaration wins, so an
1194
+ // explicit label is never dropped in favour of a bare repeat.
1195
+ .filter(tick => {
1196
+ if (seen.has(tick.value))
1197
+ return false;
1198
+ seen.add(tick.value);
1199
+ return true;
1200
+ })
1201
+ .sort((a, b) => a.value - b.value)
1202
+ .map(tick => {
1203
+ const fraction = this.fractionOf(tick.value);
1204
+ return {
1205
+ ...tick,
1206
+ level: tick.level ?? 'major',
1207
+ fraction,
1208
+ left: `calc(var(--wm-slider-thumb) / 2 + ${trim(fraction)} * (100% - var(--wm-slider-thumb)))`,
1209
+ passed: tick.value <= current,
1210
+ };
1211
+ });
1212
+ }, ...(ngDevMode ? [{ debugName: "resolvedTicks" }] : /* istanbul ignore next */ []));
1213
+ /**
1214
+ * Blocks of a segmented track. The boundaries are the grid stops plus the two
1215
+ * ends of the range, so the blocks are the intervals BETWEEN stops — which is
1216
+ * what a block can honestly represent. Each one is sized by its own span, so
1217
+ * an uneven grid produces uneven blocks rather than lying about the scale.
1218
+ */
1219
+ segments = computed(() => {
1220
+ if (this.track() !== 'segmented')
1221
+ return [];
1222
+ const min = this.min();
1223
+ const max = this.max();
1224
+ if (max <= min)
1225
+ return [];
1226
+ const current = this.value();
1227
+ const bounds = [...new Set([min, ...this.resolvedTicks().map(t => t.value), max])].sort((a, b) => a - b);
1228
+ const out = [];
1229
+ for (let i = 0; i < bounds.length - 1; i++) {
1230
+ const from = bounds[i];
1231
+ const to = bounds[i + 1];
1232
+ const span = to - from;
1233
+ if (span <= 0)
1234
+ continue;
1235
+ const fill = Math.min(Math.max((current - from) / span, 0), 1);
1236
+ out.push({
1237
+ from,
1238
+ to,
1239
+ span,
1240
+ fill,
1241
+ passed: fill >= 1,
1242
+ current: fill > 0 && fill < 1,
1243
+ });
1244
+ }
1245
+ return out;
1246
+ }, ...(ngDevMode ? [{ debugName: "segments" }] : /* istanbul ignore next */ []));
1247
+ hasTickLabels = computed(() => this.resolvedTicks().some(tick => tick.label !== undefined), ...(ngDevMode ? [{ debugName: "hasTickLabels" }] : /* istanbul ignore next */ []));
1248
+ onChangeFn = () => { };
1249
+ onTouchedFn = () => { };
1250
+ constructor() {
1251
+ // Normalisation runs on the value itself rather than on a derived signal,
1252
+ // so a caller that binds [(value)] or a form control gets the corrected
1253
+ // number back instead of holding one the thumb does not sit on.
1254
+ effect(() => {
1255
+ const resolved = this.resolve(this.value());
1256
+ if (resolved !== this.value()) {
1257
+ this.value.set(resolved);
1258
+ this.onChangeFn(resolved);
1259
+ }
1260
+ });
1261
+ }
1262
+ writeValue(value) {
1263
+ this.value.set(this.resolve(value ?? this.min()));
1264
+ }
1265
+ registerOnChange(fn) {
1266
+ this.onChangeFn = fn;
1267
+ }
1268
+ registerOnTouched(fn) {
1269
+ this.onTouchedFn = fn;
1270
+ }
1271
+ setDisabledState(isDisabled) {
1272
+ this.formDisabled.set(isDisabled);
1273
+ }
1274
+ onRangeInput(event) {
1275
+ const resolved = this.resolve(Number(event.target.value));
1276
+ this.value.set(resolved);
1277
+ this.onChangeFn(resolved);
1278
+ }
1279
+ /** Clamps into min..max, then snaps onto the step grid measured from min. */
1280
+ resolve(raw) {
1281
+ const min = this.min();
1282
+ const max = this.max();
1283
+ if (!Number.isFinite(raw))
1284
+ return min;
1285
+ if (max <= min)
1286
+ return min;
1287
+ const clamped = Math.min(Math.max(raw, min), max);
1288
+ const step = this.step();
1289
+ if (!Number.isFinite(step) || step <= 0)
1290
+ return clamped;
1291
+ // Rounding through a fixed number of decimals keeps a 0.1 grid on 0.3
1292
+ // rather than 0.30000000000000004, which would then render and compare
1293
+ // as a different number than the one the caller asked for.
1294
+ const decimals = decimalsOf(step) + decimalsOf(min);
1295
+ const snapped = round(min + Math.round((clamped - min) / step) * step, decimals);
1296
+ // Snapping up can overshoot a max that is not a whole number of steps
1297
+ // above min; the last reachable stop is one step back.
1298
+ return snapped > max ? round(snapped - step, decimals) : snapped;
1299
+ }
1300
+ fractionOf(value) {
1301
+ const span = this.max() - this.min();
1302
+ if (span <= 0)
1303
+ return 0;
1304
+ return (value - this.min()) / span;
1305
+ }
1306
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: WmSlider, deps: [], target: i0.ɵɵFactoryTarget.Component });
1307
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.23", type: WmSlider, isStandalone: true, selector: "wm-slider", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, showValue: { classPropertyName: "showValue", publicName: "showValue", isSignal: true, isRequired: false, transformFunction: null }, formatValue: { classPropertyName: "formatValue", publicName: "formatValue", isSignal: true, isRequired: false, transformFunction: null }, track: { classPropertyName: "track", publicName: "track", isSignal: true, isRequired: false, transformFunction: null }, ticks: { classPropertyName: "ticks", publicName: "ticks", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, providers: [
1308
+ {
1309
+ provide: NG_VALUE_ACCESSOR,
1310
+ useExisting: forwardRef(() => WmSlider),
1311
+ multi: true,
1312
+ },
1313
+ ], ngImport: i0, template: "<!--\n The native <input type=\"range\"> is the control, not a decoration over one:\n its track and thumb are the styled surfaces. --wm-slider-p carries the thumb\n position to the CSS so the filled part of the track, every tick and every\n block are placed from the same number the thumb is.\n-->\n<div\n class=\"wm-slider\"\n [attr.data-track]=\"track()\"\n [class.is-disabled]=\"isDisabled()\"\n [class.has-ticks]=\"resolvedTicks().length > 0\"\n [style.--wm-slider-p]=\"fraction()\"\n>\n <div class=\"wm-slider-rail\">\n @if (segments().length > 0) {\n <!--\n Blocks sit under the transparent native track. They are bounded by the\n same resolved grid the marks come from, so a block and the mark on its\n edge can never disagree about where a value falls.\n -->\n <div class=\"wm-slider-segments\" aria-hidden=\"true\">\n @for (segment of segments(); track segment.from) {\n <span\n class=\"wm-slider-seg\"\n [class.is-passed]=\"segment.passed\"\n [class.is-current]=\"segment.current\"\n [style.flexGrow]=\"segment.span\"\n [style.--wm-slider-seg-p]=\"segment.fill\"\n ></span>\n }\n </div>\n }\n\n @if (resolvedTicks().length > 0) {\n <div class=\"wm-slider-ticks\" aria-hidden=\"true\">\n @for (tick of resolvedTicks(); track tick.value) {\n <span\n class=\"wm-slider-tick\"\n [class.wm-slider-tick--minor]=\"tick.level === 'minor'\"\n [class.is-passed]=\"tick.passed\"\n [style.left]=\"tick.left\"\n ></span>\n }\n </div>\n }\n\n <input\n class=\"wm-slider-input\"\n type=\"range\"\n [value]=\"value()\"\n [min]=\"min()\"\n [max]=\"max()\"\n [step]=\"step() > 0 ? step() : 'any'\"\n [disabled]=\"isDisabled() || null\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-valuetext]=\"valueText()\"\n (input)=\"onRangeInput($event)\"\n (blur)=\"onTouchedFn()\"\n />\n </div>\n\n @if (showValue()) {\n <!--\n The sizers stack on the output in one grid cell, so the column reserves\n the widest string the readout can hold and stops stealing width from the\n rail every time the value changes.\n -->\n <div class=\"wm-slider-value-cell\">\n <output class=\"wm-slider-value\">{{ displayValue() }}</output>\n @for (candidate of valueCandidates(); track candidate) {\n <span class=\"wm-slider-value-sizer\" aria-hidden=\"true\">{{ candidate }}</span>\n }\n </div>\n }\n\n @if (hasTickLabels()) {\n <div class=\"wm-slider-tick-labels\" aria-hidden=\"true\">\n @for (tick of resolvedTicks(); track tick.value) {\n @if (tick.label !== undefined) {\n <span class=\"wm-slider-tick-label\" [style.left]=\"tick.left\">{{ tick.label }}</span>\n }\n }\n </div>\n }\n</div>\n", styles: [":host{display:block}.wm-slider{--wm-slider-thumb: 16px;--wm-slider-track: 4px;--wm-slider-gap: 3px;--wm-slider-p: 0;--wm-slider-stop: calc( var(--wm-slider-thumb) / 2 + var(--wm-slider-p) * (100% - var(--wm-slider-thumb)) );--wm-slider-fill: linear-gradient( to right, var(--wm-color-accent) 0 var(--wm-slider-stop), var(--wm-border-input) var(--wm-slider-stop) 100% );display:grid;grid-template-columns:1fr auto;align-items:center;column-gap:.75rem;row-gap:.375rem}.wm-slider-rail{position:relative;min-width:0}.wm-slider.has-ticks .wm-slider-rail{margin-bottom:10px}.wm-slider-input{-webkit-appearance:none;appearance:none;display:block;width:100%;height:var(--wm-slider-thumb);margin:0;padding:0;background:transparent;cursor:pointer;position:relative;z-index:1}.wm-slider-input:focus{outline:none}.wm-slider-input::-webkit-slider-runnable-track{height:var(--wm-slider-track);border-radius:var(--wm-radius-full);background:var(--wm-slider-fill)}.wm-slider-input::-webkit-slider-thumb{-webkit-appearance:none;width:var(--wm-slider-thumb);height:var(--wm-slider-thumb);margin-top:calc((var(--wm-slider-track) - var(--wm-slider-thumb)) / 2);border-radius:var(--wm-radius-full);background:var(--wm-color-accent);border:2px solid var(--wm-bg-base);box-shadow:var(--wm-shadow-sm);transition:background var(--wm-transition-fast),transform var(--wm-transition-fast)}.wm-slider-input:hover::-webkit-slider-thumb{background:var(--wm-color-accent-hover);transform:scale(1.12)}.wm-slider-input:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 2px var(--wm-bg-base),0 0 0 4px var(--wm-color-accent)}.wm-slider-input::-moz-range-track{height:var(--wm-slider-track);border-radius:var(--wm-radius-full);background:var(--wm-slider-fill)}.wm-slider-input::-moz-range-thumb{width:var(--wm-slider-thumb);height:var(--wm-slider-thumb);box-sizing:border-box;border-radius:var(--wm-radius-full);background:var(--wm-color-accent);border:2px solid var(--wm-bg-base);box-shadow:var(--wm-shadow-sm);transition:background var(--wm-transition-fast),transform var(--wm-transition-fast)}.wm-slider-input:hover::-moz-range-thumb{background:var(--wm-color-accent-hover);transform:scale(1.12)}.wm-slider-input:focus-visible::-moz-range-thumb{box-shadow:0 0 0 2px var(--wm-bg-base),0 0 0 4px var(--wm-color-accent)}.wm-slider-ticks{position:absolute;inset-inline:0;top:100%;margin-top:2px;height:6px;pointer-events:none}.wm-slider-tick{position:absolute;top:0;width:2px;height:6px;margin-left:-1px;border-radius:1px;background:var(--wm-border-input);transition:background var(--wm-transition-fast)}.wm-slider-tick.is-passed{background:var(--wm-color-accent)}.wm-slider-tick--minor{height:3px;background:var(--wm-border-subtle)}.wm-slider-tick--minor.is-passed{background:color-mix(in srgb,var(--wm-color-accent) 55%,transparent)}.wm-slider[data-track=segmented]{--wm-slider-fill: none}.wm-slider-segments{position:absolute;inset:0 calc(var(--wm-slider-thumb) / 2);display:flex;align-items:center;gap:var(--wm-slider-gap);pointer-events:none}.wm-slider-seg{flex-basis:0;min-width:0;height:var(--wm-slider-track);border-radius:1px;background:var(--wm-border-input);transition:background var(--wm-transition-fast)}.wm-slider-seg.is-passed{background:var(--wm-color-accent)}.wm-slider-seg.is-current{background-image:linear-gradient(var(--wm-color-accent) 0 0);background-repeat:no-repeat;background-size:calc(var(--wm-slider-seg-p, 0) * 100%) 100%}.wm-slider-tick-labels{position:relative;grid-column:1;height:1rem}.wm-slider-tick-label{position:absolute;top:0;transform:translate(-50%);font-size:.75rem;color:var(--wm-text-muted);white-space:nowrap}.wm-slider-value-cell{grid-area:1 / 2;display:grid;justify-items:end}.wm-slider-value,.wm-slider-value-sizer{grid-area:1 / 1;font-family:var(--wm-font-mono);font-size:.8125rem;font-variant-numeric:tabular-nums;white-space:nowrap}.wm-slider-value{color:var(--wm-text-secondary);min-width:4ch;text-align:right}.wm-slider-value-sizer{visibility:hidden;pointer-events:none}.wm-slider.is-disabled{opacity:.5}.wm-slider-input:disabled{cursor:not-allowed}@media(prefers-reduced-motion:reduce){.wm-slider-input::-webkit-slider-thumb,.wm-slider-input::-moz-range-thumb,.wm-slider-tick{transition:none}.wm-slider-input:hover::-webkit-slider-thumb,.wm-slider-input:hover::-moz-range-thumb{transform:none}}\n"] });
1314
+ }
1315
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: WmSlider, decorators: [{
1316
+ type: Component,
1317
+ args: [{ selector: 'wm-slider', providers: [
1318
+ {
1319
+ provide: NG_VALUE_ACCESSOR,
1320
+ useExisting: forwardRef(() => WmSlider),
1321
+ multi: true,
1322
+ },
1323
+ ], template: "<!--\n The native <input type=\"range\"> is the control, not a decoration over one:\n its track and thumb are the styled surfaces. --wm-slider-p carries the thumb\n position to the CSS so the filled part of the track, every tick and every\n block are placed from the same number the thumb is.\n-->\n<div\n class=\"wm-slider\"\n [attr.data-track]=\"track()\"\n [class.is-disabled]=\"isDisabled()\"\n [class.has-ticks]=\"resolvedTicks().length > 0\"\n [style.--wm-slider-p]=\"fraction()\"\n>\n <div class=\"wm-slider-rail\">\n @if (segments().length > 0) {\n <!--\n Blocks sit under the transparent native track. They are bounded by the\n same resolved grid the marks come from, so a block and the mark on its\n edge can never disagree about where a value falls.\n -->\n <div class=\"wm-slider-segments\" aria-hidden=\"true\">\n @for (segment of segments(); track segment.from) {\n <span\n class=\"wm-slider-seg\"\n [class.is-passed]=\"segment.passed\"\n [class.is-current]=\"segment.current\"\n [style.flexGrow]=\"segment.span\"\n [style.--wm-slider-seg-p]=\"segment.fill\"\n ></span>\n }\n </div>\n }\n\n @if (resolvedTicks().length > 0) {\n <div class=\"wm-slider-ticks\" aria-hidden=\"true\">\n @for (tick of resolvedTicks(); track tick.value) {\n <span\n class=\"wm-slider-tick\"\n [class.wm-slider-tick--minor]=\"tick.level === 'minor'\"\n [class.is-passed]=\"tick.passed\"\n [style.left]=\"tick.left\"\n ></span>\n }\n </div>\n }\n\n <input\n class=\"wm-slider-input\"\n type=\"range\"\n [value]=\"value()\"\n [min]=\"min()\"\n [max]=\"max()\"\n [step]=\"step() > 0 ? step() : 'any'\"\n [disabled]=\"isDisabled() || null\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-valuetext]=\"valueText()\"\n (input)=\"onRangeInput($event)\"\n (blur)=\"onTouchedFn()\"\n />\n </div>\n\n @if (showValue()) {\n <!--\n The sizers stack on the output in one grid cell, so the column reserves\n the widest string the readout can hold and stops stealing width from the\n rail every time the value changes.\n -->\n <div class=\"wm-slider-value-cell\">\n <output class=\"wm-slider-value\">{{ displayValue() }}</output>\n @for (candidate of valueCandidates(); track candidate) {\n <span class=\"wm-slider-value-sizer\" aria-hidden=\"true\">{{ candidate }}</span>\n }\n </div>\n }\n\n @if (hasTickLabels()) {\n <div class=\"wm-slider-tick-labels\" aria-hidden=\"true\">\n @for (tick of resolvedTicks(); track tick.value) {\n @if (tick.label !== undefined) {\n <span class=\"wm-slider-tick-label\" [style.left]=\"tick.left\">{{ tick.label }}</span>\n }\n }\n </div>\n }\n</div>\n", styles: [":host{display:block}.wm-slider{--wm-slider-thumb: 16px;--wm-slider-track: 4px;--wm-slider-gap: 3px;--wm-slider-p: 0;--wm-slider-stop: calc( var(--wm-slider-thumb) / 2 + var(--wm-slider-p) * (100% - var(--wm-slider-thumb)) );--wm-slider-fill: linear-gradient( to right, var(--wm-color-accent) 0 var(--wm-slider-stop), var(--wm-border-input) var(--wm-slider-stop) 100% );display:grid;grid-template-columns:1fr auto;align-items:center;column-gap:.75rem;row-gap:.375rem}.wm-slider-rail{position:relative;min-width:0}.wm-slider.has-ticks .wm-slider-rail{margin-bottom:10px}.wm-slider-input{-webkit-appearance:none;appearance:none;display:block;width:100%;height:var(--wm-slider-thumb);margin:0;padding:0;background:transparent;cursor:pointer;position:relative;z-index:1}.wm-slider-input:focus{outline:none}.wm-slider-input::-webkit-slider-runnable-track{height:var(--wm-slider-track);border-radius:var(--wm-radius-full);background:var(--wm-slider-fill)}.wm-slider-input::-webkit-slider-thumb{-webkit-appearance:none;width:var(--wm-slider-thumb);height:var(--wm-slider-thumb);margin-top:calc((var(--wm-slider-track) - var(--wm-slider-thumb)) / 2);border-radius:var(--wm-radius-full);background:var(--wm-color-accent);border:2px solid var(--wm-bg-base);box-shadow:var(--wm-shadow-sm);transition:background var(--wm-transition-fast),transform var(--wm-transition-fast)}.wm-slider-input:hover::-webkit-slider-thumb{background:var(--wm-color-accent-hover);transform:scale(1.12)}.wm-slider-input:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 2px var(--wm-bg-base),0 0 0 4px var(--wm-color-accent)}.wm-slider-input::-moz-range-track{height:var(--wm-slider-track);border-radius:var(--wm-radius-full);background:var(--wm-slider-fill)}.wm-slider-input::-moz-range-thumb{width:var(--wm-slider-thumb);height:var(--wm-slider-thumb);box-sizing:border-box;border-radius:var(--wm-radius-full);background:var(--wm-color-accent);border:2px solid var(--wm-bg-base);box-shadow:var(--wm-shadow-sm);transition:background var(--wm-transition-fast),transform var(--wm-transition-fast)}.wm-slider-input:hover::-moz-range-thumb{background:var(--wm-color-accent-hover);transform:scale(1.12)}.wm-slider-input:focus-visible::-moz-range-thumb{box-shadow:0 0 0 2px var(--wm-bg-base),0 0 0 4px var(--wm-color-accent)}.wm-slider-ticks{position:absolute;inset-inline:0;top:100%;margin-top:2px;height:6px;pointer-events:none}.wm-slider-tick{position:absolute;top:0;width:2px;height:6px;margin-left:-1px;border-radius:1px;background:var(--wm-border-input);transition:background var(--wm-transition-fast)}.wm-slider-tick.is-passed{background:var(--wm-color-accent)}.wm-slider-tick--minor{height:3px;background:var(--wm-border-subtle)}.wm-slider-tick--minor.is-passed{background:color-mix(in srgb,var(--wm-color-accent) 55%,transparent)}.wm-slider[data-track=segmented]{--wm-slider-fill: none}.wm-slider-segments{position:absolute;inset:0 calc(var(--wm-slider-thumb) / 2);display:flex;align-items:center;gap:var(--wm-slider-gap);pointer-events:none}.wm-slider-seg{flex-basis:0;min-width:0;height:var(--wm-slider-track);border-radius:1px;background:var(--wm-border-input);transition:background var(--wm-transition-fast)}.wm-slider-seg.is-passed{background:var(--wm-color-accent)}.wm-slider-seg.is-current{background-image:linear-gradient(var(--wm-color-accent) 0 0);background-repeat:no-repeat;background-size:calc(var(--wm-slider-seg-p, 0) * 100%) 100%}.wm-slider-tick-labels{position:relative;grid-column:1;height:1rem}.wm-slider-tick-label{position:absolute;top:0;transform:translate(-50%);font-size:.75rem;color:var(--wm-text-muted);white-space:nowrap}.wm-slider-value-cell{grid-area:1 / 2;display:grid;justify-items:end}.wm-slider-value,.wm-slider-value-sizer{grid-area:1 / 1;font-family:var(--wm-font-mono);font-size:.8125rem;font-variant-numeric:tabular-nums;white-space:nowrap}.wm-slider-value{color:var(--wm-text-secondary);min-width:4ch;text-align:right}.wm-slider-value-sizer{visibility:hidden;pointer-events:none}.wm-slider.is-disabled{opacity:.5}.wm-slider-input:disabled{cursor:not-allowed}@media(prefers-reduced-motion:reduce){.wm-slider-input::-webkit-slider-thumb,.wm-slider-input::-moz-range-thumb,.wm-slider-tick{transition:none}.wm-slider-input:hover::-webkit-slider-thumb,.wm-slider-input:hover::-moz-range-thumb{transform:none}}\n"] }]
1324
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], showValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "showValue", required: false }] }], formatValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "formatValue", required: false }] }], track: [{ type: i0.Input, args: [{ isSignal: true, alias: "track", required: false }] }], ticks: [{ type: i0.Input, args: [{ isSignal: true, alias: "ticks", required: false }] }] } });
1325
+ /**
1326
+ * A stop every `interval`, starting at `min` and never passing `max`. Returns
1327
+ * nothing for an interval that is not positive, or one that would draw more
1328
+ * than {@link MAX_TICKS} marks.
1329
+ */
1330
+ function gridEvery(min, max, interval) {
1331
+ if (!Number.isFinite(interval) || interval <= 0)
1332
+ return [];
1333
+ const span = max - min;
1334
+ if (!Number.isFinite(span) || span <= 0)
1335
+ return [];
1336
+ const count = Math.floor(span / interval + 1e-9);
1337
+ if (count + 1 > MAX_TICKS)
1338
+ return [];
1339
+ const decimals = decimalsOf(interval) + decimalsOf(min);
1340
+ const out = [];
1341
+ for (let i = 0; i <= count; i++) {
1342
+ out.push({ value: round(min + i * interval, decimals) });
1343
+ }
1344
+ return out;
1345
+ }
1346
+ /** Decimal places in a number's plain decimal form, capped to what f64 can hold. */
1347
+ function decimalsOf(value) {
1348
+ const text = String(value);
1349
+ if (text.includes('e') || text.includes('E'))
1350
+ return 10;
1351
+ const dot = text.indexOf('.');
1352
+ return dot === -1 ? 0 : Math.min(text.length - dot - 1, 10);
1353
+ }
1354
+ function round(value, decimals) {
1355
+ return Number(value.toFixed(Math.min(decimals, 10)));
1356
+ }
1357
+ /** A CSS-ready fraction: enough precision to be exact, no trailing noise. */
1358
+ function trim(fraction) {
1359
+ return String(round(fraction, 6));
1360
+ }
1361
+
1362
+ /**
1363
+ * WmToggleGroupItem — one choice in a `<wm-toggle-group>`.
1364
+ *
1365
+ * The element itself is the `role="radio"`, so anything written on it lands
1366
+ * on the control a screen reader and the keyboard use: `aria-label` for an
1367
+ * icon-only item, `[wmTooltip]`, `data-testid`, a consumer class.
1368
+ */
1369
+ class WmToggleGroupItem {
1370
+ /**
1371
+ * The value the group takes, and emits, when this item is checked. Give
1372
+ * every item one.
1373
+ *
1374
+ * Not `input.required`: the group reads every item's value to place the tab
1375
+ * stop, and an item rendered by `@for` has its host bindings run before the
1376
+ * next item's inputs are set. A required input throws NG0950 on that read;
1377
+ * this one reads `undefined`, the group places the stop without it, and the
1378
+ * binding settles when the next item's value arrives in the same pass.
1379
+ */
1380
+ value = input(undefined, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1381
+ /** Takes this item out of the focus order and out of selection. */
1382
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
1383
+ group = inject(WmToggleGroup);
1384
+ /** @internal The item's own element, which carries role="radio". */
1385
+ element = inject(ElementRef).nativeElement;
1386
+ checked = computed(() => this.group.isChecked(this), ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
1387
+ /** @internal Disabled by its own input or by the group. */
1388
+ isDisabled = computed(() => this.disabled() || this.group.isDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
1389
+ /**
1390
+ * 0 for the one tab stop, -1 for the rest. A disabled item gets no tabindex
1391
+ * at all: -1 would still let a pointer press focus it.
1392
+ */
1393
+ tabIndex = computed(() => {
1394
+ if (this.isDisabled())
1395
+ return null;
1396
+ return this.group.tabStop() === this ? 0 : -1;
1397
+ }, ...(ngDevMode ? [{ debugName: "tabIndex" }] : /* istanbul ignore next */ []));
1398
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: WmToggleGroupItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
1399
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.23", type: WmToggleGroupItem, isStandalone: true, selector: "wm-toggle-group-item", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "radio" }, listeners: { "click": "group.select(this)" }, properties: { "class.is-checked": "checked()", "class.is-disabled": "isDisabled()", "attr.aria-checked": "checked()", "attr.aria-disabled": "isDisabled() || null", "attr.tabindex": "tabIndex()" }, classAttribute: "wm-toggle-group-item" }, ngImport: i0, template: '<ng-content />', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1400
+ }
1401
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: WmToggleGroupItem, decorators: [{
1402
+ type: Component,
1403
+ args: [{
1404
+ selector: 'wm-toggle-group-item',
1405
+ template: '<ng-content />',
1406
+ changeDetection: ChangeDetectionStrategy.OnPush,
1407
+ host: {
1408
+ role: 'radio',
1409
+ class: 'wm-toggle-group-item',
1410
+ '[class.is-checked]': 'checked()',
1411
+ '[class.is-disabled]': 'isDisabled()',
1412
+ '[attr.aria-checked]': 'checked()',
1413
+ '[attr.aria-disabled]': 'isDisabled() || null',
1414
+ '[attr.tabindex]': 'tabIndex()',
1415
+ '(click)': 'group.select(this)',
1416
+ },
1417
+ }]
1418
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
1419
+ /**
1420
+ * WmToggleGroup — a single-select choice among a few options, drawn as joined
1421
+ * segments or as separate chips.
1422
+ *
1423
+ * Usage:
1424
+ * <wm-toggle-group [(value)]="strategy" ariaLabel="Pull strategy">
1425
+ * <wm-toggle-group-item value="merge">Merge</wm-toggle-group-item>
1426
+ * <wm-toggle-group-item value="rebase">Rebase</wm-toggle-group-item>
1427
+ * </wm-toggle-group>
1428
+ *
1429
+ * When to use:
1430
+ * - Two to about five mutually exclusive options that should all stay visible:
1431
+ * a setting (pull strategy, tab size, config scope), a view mode, a verdict
1432
+ * - A single-select filter row → `appearance="chip"`
1433
+ *
1434
+ * When NOT to use:
1435
+ * - Choosing which panel shows → `<wm-tabs>`. A tab controls a panel; a toggle
1436
+ * group sets a value
1437
+ * - An on/off setting → `<wm-toggle>` or a checkbox
1438
+ * - Several options that can be on together → checkboxes
1439
+ * - More options than fit on one line → `<wm-select>`
1440
+ *
1441
+ * Selection:
1442
+ * - `value` names the checked item. `null`, or a value no item carries,
1443
+ * checks nothing; the group still keeps one tab stop.
1444
+ * - `(valueChange)` fires once per user choice and only then: never for a
1445
+ * value written from outside, never for the item that is already checked,
1446
+ * and never with null. A two-way binding to a signal that cannot hold null
1447
+ * therefore type-checks.
1448
+ * - Items compare by `Object.is`, so a value keeps its type: `[value]="4"` on
1449
+ * an item emits the number 4, not the string "4".
1450
+ *
1451
+ * Keyboard (the WAI-ARIA APG radio group):
1452
+ * - Tab enters the group on one stop: the focused item while focus is inside,
1453
+ * otherwise the checked item, otherwise the first item that is not disabled.
1454
+ * - Arrow keys move focus AND check, wrapping at both ends and stepping over
1455
+ * disabled items. Left and Right swap in a right-to-left layout; Up and Down
1456
+ * follow document order. `Home` and `End` go to the first and last items.
1457
+ * - `Space` checks the focused item. `Enter` does not, as on a native radio.
1458
+ *
1459
+ * Accessibility:
1460
+ * - The `<wm-toggle-group>` element is the `role="radiogroup"` and each item
1461
+ * is a `role="radio"` with `aria-checked`. Name the group with `ariaLabel`,
1462
+ * or `ariaLabelledby` when a visible caption exists; `ariaLabelledby` wins.
1463
+ * An `aria-label` written straight on the element also works, because the
1464
+ * element carries the role.
1465
+ * - An icon-only item needs its own `aria-label`, written on the item.
1466
+ * - A disabled item keeps `aria-disabled="true"` and leaves the focus order.
1467
+ * A disabled group does the same for every item.
1468
+ *
1469
+ * Form integration: a ControlValueAccessor, so `formControlName`,
1470
+ * `[formControl]` and `[(ngModel)]` all work. The control is marked touched
1471
+ * when focus leaves the group.
1472
+ */
1473
+ class WmToggleGroup {
1474
+ /** Value of the checked item. `null`, or a value no item carries, checks nothing. */
1475
+ value = input(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1476
+ /** The value of the item the user checked. Never null; see the class comment. */
1477
+ valueChange = output();
1478
+ /** Accessible name for the radiogroup. Ignored while `ariaLabelledby` is set. */
1479
+ ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
1480
+ /**
1481
+ * Id of a visible element that names the group. Prefer it over `ariaLabel`
1482
+ * when a visible caption exists, so the accessible name matches the visible
1483
+ * text (WCAG 2.5.3, Label in Name).
1484
+ */
1485
+ ariaLabelledby = input(null, ...(ngDevMode ? [{ debugName: "ariaLabelledby" }] : /* istanbul ignore next */ []));
1486
+ /**
1487
+ * `segmented` joins the items in one sunken track, for a setting or a view
1488
+ * mode. `chip` spaces them as separate rounded chips that wrap, for a filter
1489
+ * row or a verdict.
1490
+ */
1491
+ appearance = input('segmented', ...(ngDevMode ? [{ debugName: "appearance" }] : /* istanbul ignore next */ []));
1492
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
1493
+ /** Disables every item. Reactive forms set this independently. */
1494
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
1495
+ items = contentChildren(WmToggleGroupItem, { ...(ngDevMode ? { debugName: "items" } : /* istanbul ignore next */ {}), descendants: true });
1496
+ host = inject(ElementRef).nativeElement;
1497
+ // An aria-label or aria-labelledby attribute written on the element is kept
1498
+ // when the matching input is unset. The element carries the role, so the
1499
+ // attribute is a valid way to name it, and the host binding below would
1500
+ // otherwise erase it on the first change detection.
1501
+ staticLabel = inject(new HostAttributeToken('aria-label'), { optional: true });
1502
+ staticLabelledby = inject(new HostAttributeToken('aria-labelledby'), {
1503
+ optional: true,
1504
+ });
1505
+ accessibleLabelledby = computed(() => this.ariaLabelledby() || this.staticLabelledby || null, ...(ngDevMode ? [{ debugName: "accessibleLabelledby" }] : /* istanbul ignore next */ []));
1506
+ accessibleLabel = computed(() => this.accessibleLabelledby() ? null : this.ariaLabel() || this.staticLabel || null, ...(ngDevMode ? [{ debugName: "accessibleLabel" }] : /* istanbul ignore next */ []));
1507
+ /** The checked value. Follows `value`, and moves at once on a user choice or a form write. */
1508
+ checkedValue = linkedSignal(() => this.value(), ...(ngDevMode ? [{ debugName: "checkedValue" }] : /* istanbul ignore next */ []));
1509
+ /** Set by reactive forms through setDisabledState, independent of the input. */
1510
+ formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "formDisabled" }] : /* istanbul ignore next */ []));
1511
+ /** @internal */
1512
+ isDisabled = computed(() => this.disabled() || this.formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
1513
+ /**
1514
+ * The item that holds focus, or null while focus is outside the group. Kept
1515
+ * as the item itself rather than an index, so an item added or removed
1516
+ * before it cannot move the stop onto another item.
1517
+ */
1518
+ focusedItem = signal(null, ...(ngDevMode ? [{ debugName: "focusedItem" }] : /* istanbul ignore next */ []));
1519
+ /** @internal The one item in the page's Tab order, or null when none can take focus. */
1520
+ tabStop = computed(() => {
1521
+ if (this.isDisabled())
1522
+ return null;
1523
+ const items = this.items();
1524
+ const focused = this.focusedItem();
1525
+ if (focused && items.includes(focused) && !focused.disabled())
1526
+ return focused;
1527
+ const checked = items.find((item) => !item.disabled() && this.isChecked(item));
1528
+ return checked ?? items.find((item) => !item.disabled()) ?? null;
1529
+ }, ...(ngDevMode ? [{ debugName: "tabStop" }] : /* istanbul ignore next */ []));
1530
+ onChange = () => { };
1531
+ onTouched = () => { };
1532
+ /** @internal */
1533
+ isChecked(item) {
1534
+ const checked = this.checkedValue();
1535
+ return checked !== null && Object.is(item.value(), checked);
1536
+ }
1537
+ /** @internal Checks `item` on a user action. */
1538
+ select(item) {
1539
+ if (item.isDisabled())
1540
+ return;
1541
+ const value = item.value();
1542
+ if (Object.is(this.checkedValue(), value))
1543
+ return;
1544
+ this.checkedValue.set(value);
1545
+ this.onChange(value);
1546
+ this.valueChange.emit(value);
1547
+ }
1548
+ onKeydown(event) {
1549
+ if (this.isDisabled() || event.altKey || event.ctrlKey || event.metaKey)
1550
+ return;
1551
+ const items = this.items();
1552
+ const current = items.find((item) => item.element.contains(event.target));
1553
+ if (!current)
1554
+ return;
1555
+ if (event.key === ' ') {
1556
+ event.preventDefault();
1557
+ this.select(current);
1558
+ return;
1559
+ }
1560
+ const rtl = getComputedStyle(this.host).direction === 'rtl';
1561
+ let target;
1562
+ switch (event.key) {
1563
+ case 'ArrowDown':
1564
+ target = this.step(items, current, 1);
1565
+ break;
1566
+ case 'ArrowUp':
1567
+ target = this.step(items, current, -1);
1568
+ break;
1569
+ case 'ArrowRight':
1570
+ target = this.step(items, current, rtl ? -1 : 1);
1571
+ break;
1572
+ case 'ArrowLeft':
1573
+ target = this.step(items, current, rtl ? 1 : -1);
1574
+ break;
1575
+ case 'Home':
1576
+ target = items.find((item) => !item.disabled());
1577
+ break;
1578
+ case 'End': {
1579
+ const enabled = items.filter((item) => !item.disabled());
1580
+ target = enabled[enabled.length - 1];
1581
+ break;
1582
+ }
1583
+ default:
1584
+ return;
1585
+ }
1586
+ event.preventDefault();
1587
+ if (!target)
1588
+ return;
1589
+ this.focusedItem.set(target);
1590
+ target.element.focus();
1591
+ this.select(target);
1592
+ }
1593
+ onFocusIn(event) {
1594
+ const item = this.items().find((candidate) => candidate.element.contains(event.target));
1595
+ if (item)
1596
+ this.focusedItem.set(item);
1597
+ }
1598
+ onFocusOut(event) {
1599
+ const next = event.relatedTarget;
1600
+ if (next && this.host.contains(next))
1601
+ return;
1602
+ this.focusedItem.set(null);
1603
+ this.onTouched();
1604
+ }
1605
+ /** The next item in `direction` that is not disabled, wrapping at both ends. */
1606
+ step(items, from, direction) {
1607
+ const count = items.length;
1608
+ const start = items.indexOf(from);
1609
+ for (let offset = 1; offset <= count; offset += 1) {
1610
+ const candidate = items[(((start + direction * offset) % count) + count) % count];
1611
+ if (!candidate.disabled())
1612
+ return candidate;
1613
+ }
1614
+ return undefined;
1615
+ }
1616
+ // ── ControlValueAccessor ──────────────────────────────────────────────────
1617
+ writeValue(value) {
1618
+ this.checkedValue.set(value ?? null);
1619
+ }
1620
+ registerOnChange(fn) {
1621
+ this.onChange = fn;
1622
+ }
1623
+ registerOnTouched(fn) {
1624
+ this.onTouched = fn;
1625
+ }
1626
+ setDisabledState(isDisabled) {
1627
+ this.formDisabled.set(isDisabled);
1628
+ }
1629
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: WmToggleGroup, deps: [], target: i0.ɵɵFactoryTarget.Component });
1630
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.23", type: WmToggleGroup, isStandalone: true, selector: "wm-toggle-group", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "ariaLabelledby", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { attributes: { "role": "radiogroup" }, listeners: { "keydown": "onKeydown($event)", "focusin": "onFocusIn($event)", "focusout": "onFocusOut($event)" }, properties: { "class.wm-toggle-group--segmented": "appearance() === 'segmented'", "class.wm-toggle-group--chip": "appearance() === 'chip'", "class.wm-toggle-group--sm": "size() === 'sm'", "attr.aria-label": "accessibleLabel()", "attr.aria-labelledby": "accessibleLabelledby()", "attr.aria-disabled": "isDisabled() || null" }, classAttribute: "wm-toggle-group" }, providers: [
1631
+ {
1632
+ provide: NG_VALUE_ACCESSOR,
1633
+ useExisting: forwardRef(() => WmToggleGroup),
1634
+ multi: true,
1635
+ },
1636
+ ], queries: [{ propertyName: "items", predicate: WmToggleGroupItem, descendants: true, isSignal: true }], ngImport: i0, template: '<ng-content />', isInline: true, styles: [".wm-toggle-group{display:inline-flex;align-items:stretch;max-width:100%}.wm-toggle-group-item{display:inline-flex;align-items:center;justify-content:center;gap:6px;box-sizing:border-box;min-height:28px;padding:0 12px;border:1px solid transparent;background-color:transparent;color:var(--wm-text-secondary);font-family:inherit;font-size:.8125rem;font-weight:600;line-height:1;white-space:nowrap;cursor:pointer;-webkit-user-select:none;user-select:none;outline:none;transition:background-color var(--wm-transition-fast),border-color var(--wm-transition-fast),color var(--wm-transition-fast)}.wm-toggle-group-item svg{flex-shrink:0;width:14px;height:14px}.wm-toggle-group-item:focus-visible{outline:2px solid var(--wm-color-accent-readable);outline-offset:2px}.wm-toggle-group--segmented{gap:2px;padding:3px;border:1px solid var(--wm-border-subtle);border-radius:var(--wm-radius-md);background-color:var(--wm-bg-sunken)}.wm-toggle-group--segmented .wm-toggle-group-item{border-radius:var(--wm-radius-sm)}.wm-toggle-group--segmented .wm-toggle-group-item:hover:not(.is-checked):not(.is-disabled){background-color:var(--wm-bg-float);color:var(--wm-text-primary)}.wm-toggle-group--chip{display:flex;flex-wrap:wrap;gap:6px}.wm-toggle-group--chip .wm-toggle-group-item{border-color:var(--wm-border-input);border-radius:var(--wm-radius-full)}.wm-toggle-group--chip .wm-toggle-group-item:hover:not(.is-checked):not(.is-disabled){background-color:var(--wm-bg-sunken);border-color:var(--wm-border-hover);color:var(--wm-text-primary)}.wm-toggle-group-item.is-checked{background-color:var(--wm-color-accent);border-color:var(--wm-color-accent-readable);color:var(--wm-color-accent-text)}.wm-toggle-group-item.is-checked:hover:not(.is-disabled){background-color:var(--wm-color-accent-hover)}.wm-toggle-group-item.is-disabled{opacity:.5;cursor:not-allowed}.wm-toggle-group--sm .wm-toggle-group-item{min-height:24px;padding:0 8px;font-size:.75rem}.wm-toggle-group--sm.wm-toggle-group--segmented{padding:2px;border-radius:var(--wm-radius-sm)}.wm-toggle-group--sm.wm-toggle-group--segmented .wm-toggle-group-item{border-radius:var(--wm-radius-xs)}@media(forced-colors:active){.wm-toggle-group-item{border-color:ButtonText}.wm-toggle-group-item.is-checked{forced-color-adjust:none;background-color:SelectedItem;border-color:SelectedItem;color:SelectedItemText}.wm-toggle-group-item.is-disabled{border-color:GrayText;color:GrayText}}@media(prefers-reduced-motion:reduce){.wm-toggle-group-item{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1637
+ }
1638
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImport: i0, type: WmToggleGroup, decorators: [{
1639
+ type: Component,
1640
+ args: [{ selector: 'wm-toggle-group', template: '<ng-content />', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1641
+ {
1642
+ provide: NG_VALUE_ACCESSOR,
1643
+ useExisting: forwardRef(() => WmToggleGroup),
1644
+ multi: true,
1645
+ },
1646
+ ], host: {
1647
+ role: 'radiogroup',
1648
+ class: 'wm-toggle-group',
1649
+ '[class.wm-toggle-group--segmented]': "appearance() === 'segmented'",
1650
+ '[class.wm-toggle-group--chip]': "appearance() === 'chip'",
1651
+ '[class.wm-toggle-group--sm]': "size() === 'sm'",
1652
+ '[attr.aria-label]': 'accessibleLabel()',
1653
+ '[attr.aria-labelledby]': 'accessibleLabelledby()',
1654
+ '[attr.aria-disabled]': 'isDisabled() || null',
1655
+ '(keydown)': 'onKeydown($event)',
1656
+ '(focusin)': 'onFocusIn($event)',
1657
+ '(focusout)': 'onFocusOut($event)',
1658
+ }, styles: [".wm-toggle-group{display:inline-flex;align-items:stretch;max-width:100%}.wm-toggle-group-item{display:inline-flex;align-items:center;justify-content:center;gap:6px;box-sizing:border-box;min-height:28px;padding:0 12px;border:1px solid transparent;background-color:transparent;color:var(--wm-text-secondary);font-family:inherit;font-size:.8125rem;font-weight:600;line-height:1;white-space:nowrap;cursor:pointer;-webkit-user-select:none;user-select:none;outline:none;transition:background-color var(--wm-transition-fast),border-color var(--wm-transition-fast),color var(--wm-transition-fast)}.wm-toggle-group-item svg{flex-shrink:0;width:14px;height:14px}.wm-toggle-group-item:focus-visible{outline:2px solid var(--wm-color-accent-readable);outline-offset:2px}.wm-toggle-group--segmented{gap:2px;padding:3px;border:1px solid var(--wm-border-subtle);border-radius:var(--wm-radius-md);background-color:var(--wm-bg-sunken)}.wm-toggle-group--segmented .wm-toggle-group-item{border-radius:var(--wm-radius-sm)}.wm-toggle-group--segmented .wm-toggle-group-item:hover:not(.is-checked):not(.is-disabled){background-color:var(--wm-bg-float);color:var(--wm-text-primary)}.wm-toggle-group--chip{display:flex;flex-wrap:wrap;gap:6px}.wm-toggle-group--chip .wm-toggle-group-item{border-color:var(--wm-border-input);border-radius:var(--wm-radius-full)}.wm-toggle-group--chip .wm-toggle-group-item:hover:not(.is-checked):not(.is-disabled){background-color:var(--wm-bg-sunken);border-color:var(--wm-border-hover);color:var(--wm-text-primary)}.wm-toggle-group-item.is-checked{background-color:var(--wm-color-accent);border-color:var(--wm-color-accent-readable);color:var(--wm-color-accent-text)}.wm-toggle-group-item.is-checked:hover:not(.is-disabled){background-color:var(--wm-color-accent-hover)}.wm-toggle-group-item.is-disabled{opacity:.5;cursor:not-allowed}.wm-toggle-group--sm .wm-toggle-group-item{min-height:24px;padding:0 8px;font-size:.75rem}.wm-toggle-group--sm.wm-toggle-group--segmented{padding:2px;border-radius:var(--wm-radius-sm)}.wm-toggle-group--sm.wm-toggle-group--segmented .wm-toggle-group-item{border-radius:var(--wm-radius-xs)}@media(forced-colors:active){.wm-toggle-group-item{border-color:ButtonText}.wm-toggle-group-item.is-checked{forced-color-adjust:none;background-color:SelectedItem;border-color:SelectedItem;color:SelectedItemText}.wm-toggle-group-item.is-disabled{border-color:GrayText;color:GrayText}}@media(prefers-reduced-motion:reduce){.wm-toggle-group-item{transition:none}}\n"] }]
1659
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledby", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], items: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => WmToggleGroupItem), { ...{ descendants: true }, isSignal: true }] }] } });
1660
+
786
1661
  /**
787
1662
  * GhostField — transparent-until-hover wrapper for inline-editable fields.
788
1663
  *
@@ -2610,5 +3485,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.23", ngImpo
2610
3485
  * Generated bundle index. Do not edit.
2611
3486
  */
2612
3487
 
2613
- export { Checkbox, GhostField, NumberInput, PageHeader, Toggle, WmAnchoredScrollStrategy, WmAutoFocus, WmButton, WmCombobox, WmDialog, WmModal, WmPopover, WmRichTooltip, WmRichTooltipContent, WmRichTooltipTrigger, WmSelect, WmTab, WmTabs, WmToastContainer, WmToastService, WmTooltip, onViewportChange };
3488
+ export { Checkbox, GhostField, NumberInput, PageHeader, Toggle, WmAnchoredScrollStrategy, WmAutoFocus, WmButton, WmButtonDirective, WmCombobox, WmDialog, WmModal, WmPopover, WmRichTooltip, WmRichTooltipContent, WmRichTooltipTrigger, WmSelect, WmSlider, WmTab, WmTabs, WmToastContainer, WmToastService, WmToggleGroup, WmToggleGroupItem, WmTooltip, onViewportChange };
2614
3489
  //# sourceMappingURL=wildmason-aegis.mjs.map