@skra/ui 0.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.
Files changed (160) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +39 -0
  4. package/consumer-guide/README.md +84 -0
  5. package/consumer-guide/accessible-names.md +80 -0
  6. package/consumer-guide/forms-and-binding.md +149 -0
  7. package/consumer-guide/layout.md +86 -0
  8. package/consumer-guide/overlays.md +100 -0
  9. package/consumer-guide/setup-and-gotchas.md +103 -0
  10. package/consumer-guide/theming-and-encapsulation.md +115 -0
  11. package/consumer-guide/wrapping-patterns.md +90 -0
  12. package/fesm2022/skra-ui-accordion.mjs +333 -0
  13. package/fesm2022/skra-ui-accordion.mjs.map +1 -0
  14. package/fesm2022/skra-ui-alert.mjs +110 -0
  15. package/fesm2022/skra-ui-alert.mjs.map +1 -0
  16. package/fesm2022/skra-ui-avatar.mjs +83 -0
  17. package/fesm2022/skra-ui-avatar.mjs.map +1 -0
  18. package/fesm2022/skra-ui-badge.mjs +91 -0
  19. package/fesm2022/skra-ui-badge.mjs.map +1 -0
  20. package/fesm2022/skra-ui-button.mjs +67 -0
  21. package/fesm2022/skra-ui-button.mjs.map +1 -0
  22. package/fesm2022/skra-ui-checkbox.mjs +98 -0
  23. package/fesm2022/skra-ui-checkbox.mjs.map +1 -0
  24. package/fesm2022/skra-ui-chip.mjs +82 -0
  25. package/fesm2022/skra-ui-chip.mjs.map +1 -0
  26. package/fesm2022/skra-ui-combobox.mjs +335 -0
  27. package/fesm2022/skra-ui-combobox.mjs.map +1 -0
  28. package/fesm2022/skra-ui-core.mjs +529 -0
  29. package/fesm2022/skra-ui-core.mjs.map +1 -0
  30. package/fesm2022/skra-ui-counter.mjs +53 -0
  31. package/fesm2022/skra-ui-counter.mjs.map +1 -0
  32. package/fesm2022/skra-ui-datepicker.mjs +867 -0
  33. package/fesm2022/skra-ui-datepicker.mjs.map +1 -0
  34. package/fesm2022/skra-ui-dialog.mjs +133 -0
  35. package/fesm2022/skra-ui-dialog.mjs.map +1 -0
  36. package/fesm2022/skra-ui-field.mjs +293 -0
  37. package/fesm2022/skra-ui-field.mjs.map +1 -0
  38. package/fesm2022/skra-ui-icon.mjs +54 -0
  39. package/fesm2022/skra-ui-icon.mjs.map +1 -0
  40. package/fesm2022/skra-ui-input.mjs +42 -0
  41. package/fesm2022/skra-ui-input.mjs.map +1 -0
  42. package/fesm2022/skra-ui-kbd.mjs +44 -0
  43. package/fesm2022/skra-ui-kbd.mjs.map +1 -0
  44. package/fesm2022/skra-ui-layout.mjs +119 -0
  45. package/fesm2022/skra-ui-layout.mjs.map +1 -0
  46. package/fesm2022/skra-ui-link.mjs +29 -0
  47. package/fesm2022/skra-ui-link.mjs.map +1 -0
  48. package/fesm2022/skra-ui-loading-indicator.mjs +24 -0
  49. package/fesm2022/skra-ui-loading-indicator.mjs.map +1 -0
  50. package/fesm2022/skra-ui-menu.mjs +334 -0
  51. package/fesm2022/skra-ui-menu.mjs.map +1 -0
  52. package/fesm2022/skra-ui-multi-select.mjs +179 -0
  53. package/fesm2022/skra-ui-multi-select.mjs.map +1 -0
  54. package/fesm2022/skra-ui-native-select.mjs +123 -0
  55. package/fesm2022/skra-ui-native-select.mjs.map +1 -0
  56. package/fesm2022/skra-ui-popover.mjs +158 -0
  57. package/fesm2022/skra-ui-popover.mjs.map +1 -0
  58. package/fesm2022/skra-ui-progress.mjs +57 -0
  59. package/fesm2022/skra-ui-progress.mjs.map +1 -0
  60. package/fesm2022/skra-ui-radio.mjs +92 -0
  61. package/fesm2022/skra-ui-radio.mjs.map +1 -0
  62. package/fesm2022/skra-ui-rating-display.mjs +81 -0
  63. package/fesm2022/skra-ui-rating-display.mjs.map +1 -0
  64. package/fesm2022/skra-ui-select.mjs +151 -0
  65. package/fesm2022/skra-ui-select.mjs.map +1 -0
  66. package/fesm2022/skra-ui-skeleton.mjs +65 -0
  67. package/fesm2022/skra-ui-skeleton.mjs.map +1 -0
  68. package/fesm2022/skra-ui-star-rating.mjs +110 -0
  69. package/fesm2022/skra-ui-star-rating.mjs.map +1 -0
  70. package/fesm2022/skra-ui-status-dot.mjs +61 -0
  71. package/fesm2022/skra-ui-status-dot.mjs.map +1 -0
  72. package/fesm2022/skra-ui-switch.mjs +94 -0
  73. package/fesm2022/skra-ui-switch.mjs.map +1 -0
  74. package/fesm2022/skra-ui-tabs.mjs +356 -0
  75. package/fesm2022/skra-ui-tabs.mjs.map +1 -0
  76. package/fesm2022/skra-ui-textarea.mjs +81 -0
  77. package/fesm2022/skra-ui-textarea.mjs.map +1 -0
  78. package/fesm2022/skra-ui-toast.mjs +208 -0
  79. package/fesm2022/skra-ui-toast.mjs.map +1 -0
  80. package/fesm2022/skra-ui-tooltip.mjs +252 -0
  81. package/fesm2022/skra-ui-tooltip.mjs.map +1 -0
  82. package/fesm2022/skra-ui-typography.mjs +35 -0
  83. package/fesm2022/skra-ui-typography.mjs.map +1 -0
  84. package/fesm2022/skra-ui.mjs +4 -0
  85. package/fesm2022/skra-ui.mjs.map +1 -0
  86. package/package.json +175 -0
  87. package/types/skra-ui-accordion.d.ts +196 -0
  88. package/types/skra-ui-accordion.d.ts.map +1 -0
  89. package/types/skra-ui-alert.d.ts +81 -0
  90. package/types/skra-ui-alert.d.ts.map +1 -0
  91. package/types/skra-ui-avatar.d.ts +71 -0
  92. package/types/skra-ui-avatar.d.ts.map +1 -0
  93. package/types/skra-ui-badge.d.ts +73 -0
  94. package/types/skra-ui-badge.d.ts.map +1 -0
  95. package/types/skra-ui-button.d.ts +45 -0
  96. package/types/skra-ui-button.d.ts.map +1 -0
  97. package/types/skra-ui-checkbox.d.ts +52 -0
  98. package/types/skra-ui-checkbox.d.ts.map +1 -0
  99. package/types/skra-ui-chip.d.ts +60 -0
  100. package/types/skra-ui-chip.d.ts.map +1 -0
  101. package/types/skra-ui-combobox.d.ts +95 -0
  102. package/types/skra-ui-combobox.d.ts.map +1 -0
  103. package/types/skra-ui-core.d.ts +314 -0
  104. package/types/skra-ui-core.d.ts.map +1 -0
  105. package/types/skra-ui-counter.d.ts +33 -0
  106. package/types/skra-ui-counter.d.ts.map +1 -0
  107. package/types/skra-ui-datepicker.d.ts +164 -0
  108. package/types/skra-ui-datepicker.d.ts.map +1 -0
  109. package/types/skra-ui-dialog.d.ts +59 -0
  110. package/types/skra-ui-dialog.d.ts.map +1 -0
  111. package/types/skra-ui-field.d.ts +181 -0
  112. package/types/skra-ui-field.d.ts.map +1 -0
  113. package/types/skra-ui-icon.d.ts +38 -0
  114. package/types/skra-ui-icon.d.ts.map +1 -0
  115. package/types/skra-ui-input.d.ts +22 -0
  116. package/types/skra-ui-input.d.ts.map +1 -0
  117. package/types/skra-ui-kbd.d.ts +33 -0
  118. package/types/skra-ui-kbd.d.ts.map +1 -0
  119. package/types/skra-ui-layout.d.ts +86 -0
  120. package/types/skra-ui-layout.d.ts.map +1 -0
  121. package/types/skra-ui-link.d.ts +20 -0
  122. package/types/skra-ui-link.d.ts.map +1 -0
  123. package/types/skra-ui-loading-indicator.d.ts +9 -0
  124. package/types/skra-ui-loading-indicator.d.ts.map +1 -0
  125. package/types/skra-ui-menu.d.ts +116 -0
  126. package/types/skra-ui-menu.d.ts.map +1 -0
  127. package/types/skra-ui-multi-select.d.ts +47 -0
  128. package/types/skra-ui-multi-select.d.ts.map +1 -0
  129. package/types/skra-ui-native-select.d.ts +74 -0
  130. package/types/skra-ui-native-select.d.ts.map +1 -0
  131. package/types/skra-ui-popover.d.ts +57 -0
  132. package/types/skra-ui-popover.d.ts.map +1 -0
  133. package/types/skra-ui-progress.d.ts +41 -0
  134. package/types/skra-ui-progress.d.ts.map +1 -0
  135. package/types/skra-ui-radio.d.ts +54 -0
  136. package/types/skra-ui-radio.d.ts.map +1 -0
  137. package/types/skra-ui-rating-display.d.ts +56 -0
  138. package/types/skra-ui-rating-display.d.ts.map +1 -0
  139. package/types/skra-ui-select.d.ts +43 -0
  140. package/types/skra-ui-select.d.ts.map +1 -0
  141. package/types/skra-ui-skeleton.d.ts +42 -0
  142. package/types/skra-ui-skeleton.d.ts.map +1 -0
  143. package/types/skra-ui-star-rating.d.ts +71 -0
  144. package/types/skra-ui-star-rating.d.ts.map +1 -0
  145. package/types/skra-ui-status-dot.d.ts +62 -0
  146. package/types/skra-ui-status-dot.d.ts.map +1 -0
  147. package/types/skra-ui-switch.d.ts +54 -0
  148. package/types/skra-ui-switch.d.ts.map +1 -0
  149. package/types/skra-ui-tabs.d.ts +191 -0
  150. package/types/skra-ui-tabs.d.ts.map +1 -0
  151. package/types/skra-ui-textarea.d.ts +55 -0
  152. package/types/skra-ui-textarea.d.ts.map +1 -0
  153. package/types/skra-ui-toast.d.ts +140 -0
  154. package/types/skra-ui-toast.d.ts.map +1 -0
  155. package/types/skra-ui-tooltip.d.ts +90 -0
  156. package/types/skra-ui-tooltip.d.ts.map +1 -0
  157. package/types/skra-ui-typography.d.ts +17 -0
  158. package/types/skra-ui-typography.d.ts.map +1 -0
  159. package/types/skra-ui.d.ts +3 -0
  160. package/types/skra-ui.d.ts.map +1 -0
@@ -0,0 +1,529 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, Directive, input, inject, ElementRef, contentChild, computed, signal, ViewEncapsulation, ChangeDetectionStrategy, Component, booleanAttribute, contentChildren, viewChild, effect, DestroyRef, TemplateRef } from '@angular/core';
3
+ import { SkraFieldContext } from '@skra/ui/field';
4
+ import { computePosition, offset, flip, shift, autoUpdate } from '@floating-ui/dom';
5
+ import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
6
+
7
+ /**
8
+ * DI token a listbox widget provides (`useExisting`) and a `skra-option` injects. A token (not a concrete
9
+ * class) so the option file never imports a widget file — no runtime import cycle. Provided by
10
+ * `skra-select`, `skra-multi-select`, and `skra-combobox`.
11
+ */
12
+ const SKRA_LISTBOX_PANEL = new InjectionToken('SKRA_LISTBOX_PANEL');
13
+
14
+ /**
15
+ * Slot marker: the leading icon/flag of a `skra-option` — part of the option IDENTITY (ADR 0045
16
+ * Rule 4), so it flows into `getLabel()` (emoji/glyph leading appears in the default trigger for free).
17
+ */
18
+ class SkraOptionLeading {
19
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraOptionLeading, deps: [], target: i0.ɵɵFactoryTarget.Directive });
20
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.7", type: SkraOptionLeading, isStandalone: true, selector: "[skra-option-leading]", ngImport: i0 });
21
+ }
22
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraOptionLeading, decorators: [{
23
+ type: Directive,
24
+ args: [{
25
+ selector: '[skra-option-leading]',
26
+ standalone: true,
27
+ }]
28
+ }] });
29
+ /**
30
+ * Slot marker: secondary DETAIL text of a `skra-option` (e.g. a dial code, a description) — list-only
31
+ * (ADR 0045 Rule 4), EXCLUDED from `getLabel()` and the trigger summary.
32
+ */
33
+ class SkraOptionDetail {
34
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraOptionDetail, deps: [], target: i0.ɵɵFactoryTarget.Directive });
35
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.7", type: SkraOptionDetail, isStandalone: true, selector: "[skra-option-detail]", ngImport: i0 });
36
+ }
37
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraOptionDetail, decorators: [{
38
+ type: Directive,
39
+ args: [{
40
+ selector: '[skra-option-detail]',
41
+ standalone: true,
42
+ }]
43
+ }] });
44
+
45
+ let nextOptionId = 0;
46
+ /**
47
+ * SkraOption — one selectable row in a `skra-select` listbox, a Component on the custom-element host
48
+ * `skra-option` (ADR 040 content-bearing host; there is no native option element that holds rich content
49
+ * outside `<select>`). Carries `value` + rich projected content: a leading icon (`[skra-option-leading]`)
50
+ * and a primary label = the IDENTITY (→ `getLabel()` / the default trigger, ADR 0045), plus an optional
51
+ * secondary `[skra-option-detail]` that is list-only (excluded from the trigger). The selection
52
+ * indicator (trailing checkmark) is component-rendered, driven by `aria-selected` — NOT a slot; iteration 2
53
+ * swaps it for a per-row checkbox with no API change.
54
+ *
55
+ * Implements `SkraListboxKeyboardItem` so `_SkraListboxKeyboard` (CDK `ActiveDescendantKeyManager`) can
56
+ * highlight it (`setActiveStyles`/`setInactiveStyles` → `data-skra-active`) and type-ahead to it
57
+ * (`getLabel`). Disabled is native-attr-first (Menu precedent): the consumer sets `aria-disabled="true"`
58
+ * and Forge OBSERVES it — disabled options stay navigable (discoverable) but cannot be selected.
59
+ *
60
+ * Full contract: adr/specs/component-select-spec.md.
61
+ */
62
+ class SkraOption {
63
+ /** The option's value (ADR 037 — consumer-owned; compared against the select's `value()`). */
64
+ value = input.required(...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
65
+ /** Stable id, the target of the trigger's `aria-activedescendant`. Internal — never consumer-facing. */
66
+ optionId = `skra-option-${nextOptionId++}`;
67
+ el = inject(ElementRef);
68
+ select = inject(SKRA_LISTBOX_PANEL, { optional: true });
69
+ leadingSlot = contentChild(SkraOptionLeading, ...(ngDevMode ? [{ debugName: "leadingSlot" }] : /* istanbul ignore next */ []));
70
+ detailSlot = contentChild(SkraOptionDetail, ...(ngDevMode ? [{ debugName: "detailSlot" }] : /* istanbul ignore next */ []));
71
+ hasLeading = computed(() => !!this.leadingSlot(), ...(ngDevMode ? [{ debugName: "hasLeading" }] : /* istanbul ignore next */ []));
72
+ hasDetail = computed(() => !!this.detailSlot(), ...(ngDevMode ? [{ debugName: "hasDetail" }] : /* istanbul ignore next */ []));
73
+ /** Keyboard active-descendant highlight (ADR 0044 §7 — Highlightable, NOT roving focus). */
74
+ active = signal(false, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
75
+ /**
76
+ * Cardinality hint from the parent panel (ADR 0046): `false` under `skra-select` → checkmark
77
+ * indicator; `true` under `skra-multi-select` → checkbox indicator. The only thing the shared
78
+ * `skra-option` reads to swap the component-rendered indicator. No public-API change.
79
+ */
80
+ multiple = computed(() => this.select?.multiple ?? false, ...(ngDevMode ? [{ debugName: "multiple" }] : /* istanbul ignore next */ []));
81
+ /** Selection is signal-derived (ADR 0045 / ADR 037): re-derives on parent `value()`/`compareWith()` or own `value()`. */
82
+ selected = computed(() => this.select?.isSelected(this.value()) ?? false, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
83
+ constructor() {
84
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
85
+ if (!this.select) {
86
+ console.warn('[skra-option] has no <skra-select> ancestor — it is not registered with the keyboard ' +
87
+ 'controller / selection model and cannot be picked. Project it inside a <skra-select>.');
88
+ }
89
+ }
90
+ }
91
+ // ── SkraListboxKeyboardItem (driven by _SkraListboxKeyboard / CDK ActiveDescendantKeyManager) ──
92
+ setActiveStyles() {
93
+ this.active.set(true);
94
+ }
95
+ setInactiveStyles() {
96
+ this.active.set(false);
97
+ }
98
+ /**
99
+ * ADR 0045 Rule 1: the option's IDENTITY text (leading + label), EXCLUDING `[skra-option-detail]` and
100
+ * the component-rendered indicator. Own-host `textContent` walk (the `skraMenuItem.getLabel()`
101
+ * precedent) — drives the default trigger text AND type-ahead. Reading own content, NOT
102
+ * `document.querySelector` (ADR 001-clean).
103
+ */
104
+ getLabel() {
105
+ let text = '';
106
+ const skip = (el) => el.classList.contains('skra-select-option-detail') ||
107
+ el.classList.contains('skra-select-option-indicator') ||
108
+ el.hasAttribute('skra-option-detail');
109
+ const walk = (node) => {
110
+ if (node.nodeType === Node.TEXT_NODE) {
111
+ text += node.textContent ?? '';
112
+ return;
113
+ }
114
+ if (node.nodeType === Node.ELEMENT_NODE) {
115
+ const el = node;
116
+ if (skip(el))
117
+ return;
118
+ el.childNodes.forEach(walk);
119
+ }
120
+ };
121
+ this.el.nativeElement.childNodes.forEach(walk);
122
+ return text.replace(/\s+/g, ' ').trim();
123
+ }
124
+ getValue() {
125
+ return this.value();
126
+ }
127
+ /** Soft-disabled = the consumer's `aria-disabled="true"` (native-attr-first, Menu precedent); read live. */
128
+ get disabled() {
129
+ return this.el.nativeElement.getAttribute('aria-disabled') === 'true';
130
+ }
131
+ onActivate() {
132
+ if (this.disabled)
133
+ return; // soft-disabled: not selectable (APG)
134
+ this.select?.selectValue(this.value());
135
+ }
136
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraOption, deps: [], target: i0.ɵɵFactoryTarget.Component });
137
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: SkraOption, isStandalone: true, selector: "skra-option", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "role": "option" }, listeners: { "click": "onActivate()" }, properties: { "id": "optionId", "attr.aria-selected": "selected() ? \"true\" : \"false\"", "attr.data-skra-active": "active() ? \"\" : null" } }, queries: [{ propertyName: "leadingSlot", first: true, predicate: SkraOptionLeading, descendants: true, isSignal: true }, { propertyName: "detailSlot", first: true, predicate: SkraOptionDetail, descendants: true, isSignal: true }], exportAs: ["skraOption"], ngImport: i0, template: `
138
+ @if (hasLeading()) {
139
+ <span class="skra-select-option-leading"
140
+ ><ng-content select="[skra-option-leading]"></ng-content
141
+ ></span>
142
+ }
143
+ <span class="skra-select-option-label"><ng-content></ng-content></span>
144
+ @if (hasDetail()) {
145
+ <span class="skra-select-option-detail"
146
+ ><ng-content select="[skra-option-detail]"></ng-content
147
+ ></span>
148
+ }
149
+ <span
150
+ class="skra-select-option-indicator"
151
+ [class.skra-select-option-indicator--checkbox]="multiple()"
152
+ aria-hidden="true"
153
+ ></span>
154
+ `, isInline: true, styles: ["@charset \"UTF-8\";:host{display:flex;align-items:center;gap:var(--skra-comp-select-option-gap);inline-size:100%;min-block-size:var(--skra-comp-select-option-min-block-size);margin:0;box-sizing:border-box;padding-block:var(--skra-comp-select-option-padding-block);padding-inline:var(--skra-comp-select-option-padding-inline);color:var(--skra-comp-select-option-foreground);cursor:pointer;-webkit-user-select:none;user-select:none;font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);transition:background-color var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing)}.skra-select-option-leading{display:inline-flex;align-items:center;flex:none;--skra-comp-icon-size: var(--skra-comp-select-icon-size)}.skra-select-option-label{flex:1 1 auto;min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.skra-select-option-detail{flex:none;color:var(--skra-comp-select-option-detail-foreground);font-family:var(--skra-sys-typography-role-body-small-font-family);font-size:var(--skra-sys-typography-role-body-small-font-size);font-weight:var(--skra-sys-typography-role-body-small-font-weight);line-height:var(--skra-sys-typography-role-body-small-line-height);letter-spacing:var(--skra-sys-typography-role-body-small-letter-spacing)}.skra-select-option-indicator{flex:none;inline-size:1em;display:inline-flex;align-items:center;justify-content:center}:host([aria-selected=true]) .skra-select-option-indicator:not(.skra-select-option-indicator--checkbox):before{content:\"\\2713\";color:var(--skra-comp-select-indicator-color)}.skra-select-option-indicator--checkbox{inline-size:var(--skra-comp-select-option-checkbox-size);block-size:var(--skra-comp-select-option-checkbox-size);border:var(--skra-comp-select-border-width) solid var(--skra-comp-select-option-checkbox-border-color);border-radius:var(--skra-comp-select-option-checkbox-radius);box-sizing:border-box}:host([aria-selected=true]) .skra-select-option-indicator--checkbox{background-color:var(--skra-comp-select-option-checkbox-background-selected);border-color:var(--skra-comp-select-option-checkbox-background-selected)}:host([aria-selected=true]) .skra-select-option-indicator--checkbox:before{content:\"\\2713\";color:var(--skra-comp-select-option-checkbox-check-color);font-size:.75em;line-height:1}:host(:hover){background-color:var(--skra-comp-select-option-background-hover)}:host([data-skra-active]){background-color:var(--skra-comp-select-option-background-active);outline:var(--skra-comp-select-focus-ring-width) solid var(--skra-comp-select-focus-ring-color);outline-offset:calc(-1 * var(--skra-comp-select-focus-ring-width))}:host([aria-selected=true]){background-color:var(--skra-comp-select-option-background-selected);color:var(--skra-comp-select-option-foreground-selected)}:host([aria-disabled=true]){color:var(--skra-comp-select-option-foreground-disabled);cursor:default}@media(prefers-reduced-motion:reduce){:host{transition-duration:0ms}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
155
+ }
156
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraOption, decorators: [{
157
+ type: Component,
158
+ args: [{ selector: 'skra-option', standalone: true, exportAs: 'skraOption', template: `
159
+ @if (hasLeading()) {
160
+ <span class="skra-select-option-leading"
161
+ ><ng-content select="[skra-option-leading]"></ng-content
162
+ ></span>
163
+ }
164
+ <span class="skra-select-option-label"><ng-content></ng-content></span>
165
+ @if (hasDetail()) {
166
+ <span class="skra-select-option-detail"
167
+ ><ng-content select="[skra-option-detail]"></ng-content
168
+ ></span>
169
+ }
170
+ <span
171
+ class="skra-select-option-indicator"
172
+ [class.skra-select-option-indicator--checkbox]="multiple()"
173
+ aria-hidden="true"
174
+ ></span>
175
+ `, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, host: {
176
+ role: 'option',
177
+ '[id]': 'optionId',
178
+ '[attr.aria-selected]': 'selected() ? "true" : "false"',
179
+ '[attr.data-skra-active]': 'active() ? "" : null',
180
+ '(click)': 'onActivate()',
181
+ // Disabled = the consumer's native `aria-disabled="true"` (observed live) — no Forge binding.
182
+ }, styles: ["@charset \"UTF-8\";:host{display:flex;align-items:center;gap:var(--skra-comp-select-option-gap);inline-size:100%;min-block-size:var(--skra-comp-select-option-min-block-size);margin:0;box-sizing:border-box;padding-block:var(--skra-comp-select-option-padding-block);padding-inline:var(--skra-comp-select-option-padding-inline);color:var(--skra-comp-select-option-foreground);cursor:pointer;-webkit-user-select:none;user-select:none;font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);transition:background-color var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing)}.skra-select-option-leading{display:inline-flex;align-items:center;flex:none;--skra-comp-icon-size: var(--skra-comp-select-icon-size)}.skra-select-option-label{flex:1 1 auto;min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.skra-select-option-detail{flex:none;color:var(--skra-comp-select-option-detail-foreground);font-family:var(--skra-sys-typography-role-body-small-font-family);font-size:var(--skra-sys-typography-role-body-small-font-size);font-weight:var(--skra-sys-typography-role-body-small-font-weight);line-height:var(--skra-sys-typography-role-body-small-line-height);letter-spacing:var(--skra-sys-typography-role-body-small-letter-spacing)}.skra-select-option-indicator{flex:none;inline-size:1em;display:inline-flex;align-items:center;justify-content:center}:host([aria-selected=true]) .skra-select-option-indicator:not(.skra-select-option-indicator--checkbox):before{content:\"\\2713\";color:var(--skra-comp-select-indicator-color)}.skra-select-option-indicator--checkbox{inline-size:var(--skra-comp-select-option-checkbox-size);block-size:var(--skra-comp-select-option-checkbox-size);border:var(--skra-comp-select-border-width) solid var(--skra-comp-select-option-checkbox-border-color);border-radius:var(--skra-comp-select-option-checkbox-radius);box-sizing:border-box}:host([aria-selected=true]) .skra-select-option-indicator--checkbox{background-color:var(--skra-comp-select-option-checkbox-background-selected);border-color:var(--skra-comp-select-option-checkbox-background-selected)}:host([aria-selected=true]) .skra-select-option-indicator--checkbox:before{content:\"\\2713\";color:var(--skra-comp-select-option-checkbox-check-color);font-size:.75em;line-height:1}:host(:hover){background-color:var(--skra-comp-select-option-background-hover)}:host([data-skra-active]){background-color:var(--skra-comp-select-option-background-active);outline:var(--skra-comp-select-focus-ring-width) solid var(--skra-comp-select-focus-ring-color);outline-offset:calc(-1 * var(--skra-comp-select-focus-ring-width))}:host([aria-selected=true]){background-color:var(--skra-comp-select-option-background-selected);color:var(--skra-comp-select-option-foreground-selected)}:host([aria-disabled=true]){color:var(--skra-comp-select-option-foreground-disabled);cursor:default}@media(prefers-reduced-motion:reduce){:host{transition-duration:0ms}}\n"] }]
183
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], leadingSlot: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SkraOptionLeading), { isSignal: true }] }], detailSlot: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SkraOptionDetail), { isSignal: true }] }] } });
184
+
185
+ /**
186
+ * SkraSelectTrigger — the opt-in rich-trigger marker (ADR 0045 Rule 2). Place on an `<ng-template>`
187
+ * inside `<skra-select>`; the select reads it as a `TemplateRef` and stamps it in the trigger via
188
+ * `ngTemplateOutlet` with `{ $implicit: value, option: selectedOption }` context — the ONLY supported
189
+ * path for non-text (SVG/`<img>`) trigger presentation. With no trigger template the default trigger is
190
+ * the selected option's `getLabel()` text (emoji/glyph leading content appears for free).
191
+ *
192
+ * <skra-select [value]="country()" (valueChange)="country.set($event)">
193
+ * <ng-template skraSelectTrigger let-value>
194
+ * <skra-icon [innerSVG]="flagOf(value)"></skra-icon> {{ nameOf(value) }}
195
+ * </ng-template>
196
+ * <skra-option value="de"><skra-icon [innerSVG]="flagOf('de')"></skra-icon> Germany</skra-option>
197
+ * </skra-select>
198
+ */
199
+ class SkraSelectTrigger {
200
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraSelectTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
201
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.7", type: SkraSelectTrigger, isStandalone: true, selector: "[skraSelectTrigger]", ngImport: i0 });
202
+ }
203
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraSelectTrigger, decorators: [{
204
+ type: Directive,
205
+ args: [{
206
+ selector: '[skraSelectTrigger]',
207
+ standalone: true,
208
+ }]
209
+ }] });
210
+
211
+ /**
212
+ * Anchor `floating` to `reference` and keep it positioned while visible. Returns a handle whose
213
+ * `destroy()` MUST be called when the overlay hides or the owner is destroyed (no listener leak).
214
+ *
215
+ * `options` is a getter so live token values (offset) are re-read on every reposition.
216
+ */
217
+ function _attachPositioner(reference, floating, options) {
218
+ const update = () => {
219
+ const { placement, offset: offset$1 = 0, viewportPadding = 8 } = options();
220
+ void computePosition(reference, floating, {
221
+ strategy: 'fixed', // floating element renders in the top layer → viewport-relative
222
+ placement: placement,
223
+ middleware: [
224
+ offset(offset$1),
225
+ flip(),
226
+ shift({ padding: viewportPadding }),
227
+ ],
228
+ }).then(({ x, y, placement: resolved }) => {
229
+ // Geometry only (ADR 0044 §3 rule 4). Never theme/state.
230
+ floating.style.left = `${x}px`;
231
+ floating.style.top = `${y}px`;
232
+ // Resolved side (post-flip) for orientation CSS / future arrow — a data-attr, not a style.
233
+ floating.setAttribute('data-skra-placement', resolved.split('-')[0]);
234
+ });
235
+ };
236
+ // autoUpdate repositions on scroll, resize and element resize; returns its own cleanup.
237
+ const cleanup = autoUpdate(reference, floating, update);
238
+ let destroyed = false;
239
+ return {
240
+ destroy() {
241
+ if (destroyed)
242
+ return;
243
+ destroyed = true;
244
+ cleanup();
245
+ },
246
+ };
247
+ }
248
+
249
+ /**
250
+ * _SkraListboxKeyboard — the WAI-ARIA APG **Select-Only Combobox** listbox keyboard model, confined to
251
+ * ONE file. Sibling to `menu-keyboard.ts`: per ADR 0044 §7 (2026-06-07), `@angular/cdk/a11y` is confined
252
+ * to one swappable `*-keyboard.ts` controller PER APG pattern. Menu uses `FocusKeyManager` (roving REAL
253
+ * focus); the listbox uses **`ActiveDescendantKeyManager`** — focus STAYS on the combobox trigger and the
254
+ * active option is virtual (`aria-activedescendant`), the APG-canonical select-only model. Swapping the
255
+ * engine is a one-file change, exactly as `SkraPositioner` wraps `@floating-ui/dom`.
256
+ *
257
+ * Owns: Down/Up active-descendant movement, Home/End, and type-ahead. Enter/Space (select), Escape and
258
+ * light-dismiss (`popover="auto"`) stay with the select / platform — this controller never touches them.
259
+ */
260
+ class _SkraListboxKeyboard {
261
+ manager;
262
+ items = [];
263
+ changeHandler;
264
+ /** Notified whenever the active descendant changes — the select mirrors it to `aria-activedescendant`. */
265
+ onActiveChange(handler) {
266
+ this.changeHandler = handler;
267
+ }
268
+ /**
269
+ * (Re)build the manager from the current projected option set. Called from an `effect` on the select's
270
+ * signal `contentChildren` — a passed array is a static snapshot (it does not auto-track like a live
271
+ * `QueryList`), so the manager is rebuilt whenever the option set changes (the documented signal↔CDK seam).
272
+ */
273
+ setItems(items) {
274
+ this.items = [...items];
275
+ this.manager?.destroy();
276
+ this.manager = new ActiveDescendantKeyManager(this.items)
277
+ .withTypeAhead()
278
+ .withHomeAndEnd()
279
+ // Disabled options stay navigable (APG-discoverable); selection is guarded by the select.
280
+ // `() => false` also stops the manager from reading `item.disabled` to skip-during-navigation.
281
+ .skipPredicate(() => false);
282
+ this.manager.change.subscribe((index) => this.changeHandler?.(this.items[index] ?? null));
283
+ }
284
+ /** Delegate Down/Up/Home/End/type-ahead. Enter/Space/Escape are intentionally left to the select/platform. */
285
+ onKeydown(event) {
286
+ this.manager?.onKeydown(event);
287
+ }
288
+ get activeItem() {
289
+ return this.manager?.activeItem ?? null;
290
+ }
291
+ /** Seed the active descendant on open: the selected option, else the first. */
292
+ setActive(item) {
293
+ if (!this.manager)
294
+ return;
295
+ if (item)
296
+ this.manager.setActiveItem(item);
297
+ else
298
+ this.manager.setFirstItemActive();
299
+ }
300
+ /** Deactivate (close): `setActiveItem(-1)` clears `_activeItem` and the highlight. */
301
+ clearActive() {
302
+ this.manager?.setActiveItem(-1);
303
+ }
304
+ destroy() {
305
+ this.manager?.destroy();
306
+ this.manager = undefined;
307
+ }
308
+ }
309
+
310
+ let nextListboxId = 0;
311
+ /**
312
+ * _SkraListboxWidgetBase — the **trigger-shape-agnostic** core shared by every Forge listbox widget,
313
+ * per the **ADR 0046 amendment (2026-06-13)**. It owns ONLY the substrate that does not depend on what
314
+ * the trigger looks like: the `SkraFieldContext` cascade (ADR 013), the panel `(toggle)` →
315
+ * `SkraPositioner` lifecycle + active-descendant seeding, the `_SkraListboxKeyboard` wiring
316
+ * (CDK `ActiveDescendantKeyManager`, ADR 0044 §7), imperative `open()`/`close()`, teardown, and the
317
+ * abstract `SkraListboxPanel` selection seam.
318
+ *
319
+ * Trigger BEHAVIOUR lives one level down, in a per-shape base:
320
+ * - `_SkraSelectBase` adds the button + native `popovertarget` + type-ahead trigger (Select-Only
321
+ * Combobox) and the ADR 0045 `[skraSelectTrigger]` rich-trigger template.
322
+ * - `SkraCombobox` (no extra base) adds the editable `<input>` + filter-output trigger (ADR 0049).
323
+ *
324
+ * The keydown skeleton is NOT here — each trigger shape routes keys differently (the select selects on
325
+ * Space and type-aheads; the combobox types into the input and never type-aheads). Forcing one keydown
326
+ * through both shapes is the wrong abstraction.
327
+ *
328
+ * `panelTokenPrefix` makes the panel comp-token namespace overridable so each widget reads its OWN
329
+ * `--skra-comp-<prefix>-panel-*` tokens (`select` for the select family, `combobox` for the combobox).
330
+ *
331
+ * Full contracts: adr/specs/component-{select,multi-select,combobox}-spec.md.
332
+ */
333
+ class _SkraListboxWidgetBase {
334
+ /** Equality strategy seam (ADR 037 selection model): default `Object.is`; object values pass a custom comparator. */
335
+ compareWith = input(Object.is, ...(ngDevMode ? [{ debugName: "compareWith" }] : /* istanbul ignore next */ []));
336
+ /** Value-empty hint shown in the trigger (NOT the accessible name — ADR 033). */
337
+ placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
338
+ /** Placement intent; flip/shift adjust at runtime. */
339
+ placement = input('bottom', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
340
+ /** Standalone availability source (Q-1) — `SkraFieldContext` wins when present. */
341
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
342
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
343
+ options = contentChildren(SkraOption, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
344
+ triggerRef = viewChild('trigger', ...(ngDevMode ? [{ debugName: "triggerRef" }] : /* istanbul ignore next */ []));
345
+ panelRef = viewChild('panel', ...(ngDevMode ? [{ debugName: "panelRef" }] : /* istanbul ignore next */ []));
346
+ context = inject(SkraFieldContext, { optional: true });
347
+ keyboard = new _SkraListboxKeyboard();
348
+ panelId = `skra-listbox-${nextListboxId++}`;
349
+ isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
350
+ activeOptionId = signal(null, ...(ngDevMode ? [{ debugName: "activeOptionId" }] : /* istanbul ignore next */ []));
351
+ positioner;
352
+ // ── Availability / validation (SkraFieldContext, ADR 013, with standalone fallbacks — Q-1) ──
353
+ isDisabled = computed(() => this.context?.isDisabled() ?? this.disabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
354
+ isReadonly = computed(() => this.context?.isReadonly() ?? this.readonly(), ...(ngDevMode ? [{ debugName: "isReadonly" }] : /* istanbul ignore next */ []));
355
+ hasError = computed(() => this.context?.hasError() ?? false, ...(ngDevMode ? [{ debugName: "hasError" }] : /* istanbul ignore next */ []));
356
+ isRequired = computed(() => this.context?.isRequired() ?? false, ...(ngDevMode ? [{ debugName: "isRequired" }] : /* istanbul ignore next */ []));
357
+ triggerId = computed(() => this.context?.fieldId() || null, ...(ngDevMode ? [{ debugName: "triggerId" }] : /* istanbul ignore next */ []));
358
+ describedBy = computed(() => this.context?.describedByIds() ?? null, ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
359
+ constructor() {
360
+ // Feed the keyboard controller whenever the projected option set changes (signal → array rebuild).
361
+ effect(() => this.keyboard.setItems(this.options()));
362
+ // Mirror the active descendant to the trigger's aria-activedescendant.
363
+ this.keyboard.onActiveChange((item) => this.activeOptionId.set(item?.optionId ?? null));
364
+ inject(DestroyRef).onDestroy(() => this.teardown());
365
+ }
366
+ // ── Panel open / close (native toggle from the Popover API) ──
367
+ onPanelToggle(event) {
368
+ const open = event.newState === 'open';
369
+ this.isOpen.set(open);
370
+ if (open)
371
+ this.onOpen();
372
+ else
373
+ this.onClose();
374
+ }
375
+ /**
376
+ * The element the panel is positioned against AND matched in width to (Q-5). Defaults to the trigger
377
+ * — correct for the select family, whose trigger button fills the host. The combobox overrides it to
378
+ * the host box, because its trigger is the INNER input (narrower than the bordered box + chevron), so
379
+ * the panel must align to and match the box, not the input — visual parity with select.
380
+ */
381
+ positioningAnchor() {
382
+ return this.triggerRef()?.nativeElement;
383
+ }
384
+ onOpen() {
385
+ const anchor = this.positioningAnchor();
386
+ const panel = this.panelRef()?.nativeElement;
387
+ if (anchor && panel) {
388
+ // Geometry-only (ADR 0044 §3 rule 4): the panel is at least as wide as the anchor (Q-5).
389
+ panel.style.minInlineSize = `${anchor.offsetWidth}px`;
390
+ this.positioner?.destroy();
391
+ this.positioner = _attachPositioner(anchor, panel, () => ({
392
+ placement: this.placement(),
393
+ offset: this.cssLenPx(`--skra-comp-${this.panelTokenPrefix}-panel-offset`),
394
+ viewportPadding: this.cssLenPx(`--skra-comp-${this.panelTokenPrefix}-panel-viewport-padding`),
395
+ }));
396
+ }
397
+ // Seed the active descendant (single → selected; multi → first selected, else first).
398
+ this.keyboard.setActive(this.activeSeedItem());
399
+ }
400
+ onClose() {
401
+ this.positioner?.destroy();
402
+ this.positioner = undefined;
403
+ this.keyboard.clearActive();
404
+ this.activeOptionId.set(null);
405
+ }
406
+ /** Select the keyboard's active option (Enter), unless it is soft-disabled. Shared by every trigger shape. */
407
+ selectActive() {
408
+ const active = this.keyboard.activeItem;
409
+ if (active && !active.disabled)
410
+ this.selectValue(active.getValue());
411
+ }
412
+ open() {
413
+ this.panelRef()?.nativeElement.showPopover?.();
414
+ }
415
+ close() {
416
+ this.panelRef()?.nativeElement.hidePopover?.();
417
+ }
418
+ /** Read a length comp token (px/rem) off the panel computed style for the positioner. */
419
+ cssLenPx(prop) {
420
+ const el = this.panelRef()?.nativeElement;
421
+ if (!el)
422
+ return 0;
423
+ const raw = getComputedStyle(el).getPropertyValue(prop).trim();
424
+ if (raw.endsWith('px'))
425
+ return parseFloat(raw);
426
+ if (raw.endsWith('rem')) {
427
+ const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
428
+ return parseFloat(raw) * rootFontSize;
429
+ }
430
+ return parseFloat(raw) || 0;
431
+ }
432
+ teardown() {
433
+ this.keyboard.destroy();
434
+ this.positioner?.destroy();
435
+ }
436
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: _SkraListboxWidgetBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
437
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "21.2.7", type: _SkraListboxWidgetBase, isStandalone: true, inputs: { compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-skra-disabled": "isDisabled() ? \"\" : null", "attr.data-skra-readonly": "isReadonly() ? \"\" : null", "attr.data-skra-validation": "hasError() ? \"invalid\" : null" } }, queries: [{ propertyName: "options", predicate: SkraOption, isSignal: true }], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0 });
438
+ }
439
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: _SkraListboxWidgetBase, decorators: [{
440
+ type: Directive,
441
+ args: [{
442
+ host: {
443
+ // Shell state reflection for the skin (the trigger control carries the ARIA + native disabled).
444
+ '[attr.data-skra-disabled]': 'isDisabled() ? "" : null',
445
+ '[attr.data-skra-readonly]': 'isReadonly() ? "" : null',
446
+ '[attr.data-skra-validation]': 'hasError() ? "invalid" : null',
447
+ },
448
+ }]
449
+ }], ctorParameters: () => [], propDecorators: { compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], options: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SkraOption), { isSignal: true }] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }] } });
450
+
451
+ /**
452
+ * _SkraSelectBase — the SELECT-family trigger model over {@link _SkraListboxWidgetBase}: the WAI-ARIA APG
453
+ * **Select-Only Combobox** trigger. It adds the `<button>` + native `popovertarget` interaction, the
454
+ * type-ahead keydown skeleton, and the ADR 0045 `[skraSelectTrigger]` rich-trigger template. Shared by
455
+ * `skra-select` (single) and `skra-multi-select` (multi) — the cardinality split (ADR 0046) lives in
456
+ * those concretes (the `SkraListboxPanel` seam: single replaces + closes, multi toggles + stays open).
457
+ *
458
+ * The trigger-shape-agnostic substrate (field cascade, panel toggle/positioner, keyboard wiring,
459
+ * open/close, the selection seam) lives in the parent base, shared verbatim with `skra-combobox`
460
+ * (which has a different — editable input — trigger; ADR 0046 amendment 2026-06-13).
461
+ *
462
+ * Full contract: adr/specs/component-select-spec.md + adr/specs/component-multi-select-spec.md.
463
+ */
464
+ class _SkraSelectBase extends _SkraListboxWidgetBase {
465
+ /** Select-family panel token namespace — `--skra-comp-select-panel-*` (shared single + multi). */
466
+ panelTokenPrefix = 'select';
467
+ /** Opt-in rich trigger template (ADR 0045 Rule 2). Select-family only — a combobox input shows text. */
468
+ triggerTemplate = contentChild(SkraSelectTrigger, { ...(ngDevMode ? { debugName: "triggerTemplate" } : /* istanbul ignore next */ {}), read: TemplateRef });
469
+ // ── Trigger interaction (button + native popovertarget) ──
470
+ onTriggerClick(event) {
471
+ // Block the native popovertarget open when readonly (disabled is unfocusable/inert natively).
472
+ if (this.isReadonly())
473
+ event.preventDefault();
474
+ }
475
+ onTriggerKeydown(event) {
476
+ if (this.isDisabled() || this.isReadonly())
477
+ return; // never opens / never mutates
478
+ const key = event.key;
479
+ const isSpace = key === ' ' || key === 'Spacebar';
480
+ const isNav = key === 'ArrowDown' ||
481
+ key === 'ArrowUp' ||
482
+ key === 'Home' ||
483
+ key === 'End';
484
+ const isPrintable = key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
485
+ if (!this.isOpen()) {
486
+ // Single open path: suppress the popovertarget's own toggle (Enter/Space) too and open here.
487
+ if (isNav || isSpace || key === 'Enter' || isPrintable) {
488
+ event.preventDefault();
489
+ this.open();
490
+ }
491
+ return;
492
+ }
493
+ // Open:
494
+ if (key === 'Enter' || isSpace) {
495
+ // Suppress the popovertarget toggle; the concrete `selectValue` decides close (single) vs stay-open (multi).
496
+ event.preventDefault();
497
+ this.selectActive();
498
+ return;
499
+ }
500
+ if (key === 'Tab') {
501
+ this.close(); // APG: Tab closes (keep value); focus proceeds (no preventDefault)
502
+ return;
503
+ }
504
+ if (key === 'Escape')
505
+ return; // platform (popover="auto") closes; onPanelToggle handles cleanup
506
+ if (isNav || isPrintable) {
507
+ event.preventDefault();
508
+ this.keyboard.onKeydown(event); // move / type-ahead the active descendant
509
+ }
510
+ }
511
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: _SkraSelectBase, deps: null, target: i0.ɵɵFactoryTarget.Directive });
512
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "21.2.7", type: _SkraSelectBase, isStandalone: true, queries: [{ propertyName: "triggerTemplate", first: true, predicate: SkraSelectTrigger, descendants: true, read: TemplateRef, isSignal: true }], usesInheritance: true, ngImport: i0 });
513
+ }
514
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: _SkraSelectBase, decorators: [{
515
+ type: Directive
516
+ }], propDecorators: { triggerTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SkraSelectTrigger), { ...{
517
+ read: TemplateRef,
518
+ }, isSignal: true }] }] } });
519
+
520
+ // @skra/ui/core — shared internal satellites (ADR 0054). Consumed cross-entry by
521
+ // select / multi-select / combobox / datepicker / menu / popover / tooltip.
522
+ // --- Public shared surface ---
523
+
524
+ /**
525
+ * Generated bundle index. Do not edit.
526
+ */
527
+
528
+ export { SKRA_LISTBOX_PANEL, SkraOption, SkraOptionDetail, SkraOptionLeading, SkraSelectTrigger, _SkraListboxWidgetBase, _SkraSelectBase, _attachPositioner };
529
+ //# sourceMappingURL=skra-ui-core.mjs.map