@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,123 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, ElementRef, isDevMode, effect, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { SkraFieldContext } from '@skra/ui/field';
4
+
5
+ /**
6
+ * Wires a native `<select>` inside `<skra-field>` to SkraFieldContext (ADR 013).
7
+ * Tier 1 Foundations primitive — **single-select only**: a dropdown, or a
8
+ * single-select scrolling listbox via the native `[size]` attribute.
9
+ *
10
+ * Native `[multiple]` is deliberately NOT supported (the directive rejects it,
11
+ * see below). Native `<select multiple>` UX is not production-grade and cannot
12
+ * be fixed at the native-`<select>` tier: selection requires modifier keys or
13
+ * drag, the plain-click-toggle affordance exists only for a bare `<select
14
+ * multiple>` in some engines (and breaks with an explicit `[size]`), and the
15
+ * option markers are UA-drawn / unstylable cross-browser. Multi-select — chip
16
+ * display, search, custom option templates, and a styleable per-row checkbox —
17
+ * is wholly a Tier 2 `skra-select` (CDK listbox) concern per
18
+ * `adr/specs/0039-select-directive-spec.md` §1.1 / §4.1.
19
+ *
20
+ * <skra-field>
21
+ * <label skraLabelFor>Country</label>
22
+ * <select skraSelect [value]="country()" (change)="country.set($any($event.target).value)">
23
+ * <option value="pl">Poland</option>
24
+ * <option value="de">Germany</option>
25
+ * </select>
26
+ * </skra-field>
27
+ *
28
+ * Native `<select>` is the only web-platform widget whose dropdown renders in
29
+ * the OS / browser top-layer, above modal `<dialog>`, above Shadow DOM
30
+ * boundaries, above z-index stacking contexts. By staying native at Tier 1 we
31
+ * inherit that for free — Tier 2 custom listbox will solve the same problem
32
+ * via CDK overlay + Top Layer API.
33
+ *
34
+ * Value channel stays consumer-owned per ADR 037: bind via native `[value]` +
35
+ * `(change)`, Reactive Forms `formControlName`, Signal Forms `[formField]`,
36
+ * or `[(ngModel)]` — all work orthogonally.
37
+ *
38
+ * `readonly` is ARIA-only because HTML spec does not apply native `[readonly]`
39
+ * to `<select>` (same gotcha as `<input type="checkbox|radio">`). Unlike a text
40
+ * input, though, a native `<select>` is an *uncontrolled* widget: if the listbox
41
+ * opens and the user picks an option, the displayed value changes regardless of
42
+ * the consumer's binding — so "consumer prevents change" cannot actually hold a
43
+ * native select readonly. The directive therefore enforces readonly behaviourally
44
+ * (blocks the picker from opening / the value from changing — see
45
+ * `blockWhenReadonly` / `blockKeyWhenReadonly`), mirroring the imperative
46
+ * `el.disabled` enforcement below. Both states are made non-interactive; only
47
+ * readonly stays focusable + un-greyed and presents as the bare value. This is
48
+ * interaction integrity for the control's own state — NOT a value channel, so the
49
+ * ADR 037 no-`@angular/forms` / no-CVA boundary is untouched.
50
+ */
51
+ class SkraNativeSelect {
52
+ el = inject(ElementRef);
53
+ context = inject(SkraFieldContext);
54
+ constructor() {
55
+ const el = this.el.nativeElement;
56
+ // Reject native multi-select loudly in dev (mirrors the mandatory-context
57
+ // throw — Forge fails on contract violations rather than degrading
58
+ // silently). Dev-only: never crashes a production bundle. A `<select
59
+ // multiple>` that truly wants native UX can drop `skraSelect` and stay raw
60
+ // HTML; for a real multi-select, use the Tier 2 <skra-select>.
61
+ if (isDevMode() && el.multiple) {
62
+ throw new Error('[skraSelect] does not support the native [multiple] attribute at ' +
63
+ 'Tier 1. Native multi-select UX is not production-grade (modifier-key/' +
64
+ 'drag selection, UA-drawn unstylable markers, browser-fragile) and ' +
65
+ 'cannot be fixed at the native <select> tier. Use a single-select ' +
66
+ '<select skraSelect> (optionally with [size] for a listbox), or the ' +
67
+ 'Tier 2 <skra-select> (CDK listbox) for real multi-select. See ' +
68
+ 'adr/specs/0039-select-directive-spec.md §4.1.');
69
+ }
70
+ effect(() => {
71
+ el.disabled = this.context.isDisabled();
72
+ });
73
+ }
74
+ /**
75
+ * Readonly: suppress the pointer affordance that opens the native listbox.
76
+ * `preventDefault` on `mousedown` stops the picker from opening (and, on a
77
+ * native select, focus-on-click — Tab focus still works, which is the right
78
+ * trade-off for a value-display control).
79
+ */
80
+ blockWhenReadonly(event) {
81
+ if (this.context.isReadonly())
82
+ event.preventDefault();
83
+ }
84
+ /**
85
+ * Readonly: suppress every keyboard affordance that opens or mutates the
86
+ * select, while letting Tab through for focus traversal. Space / Enter /
87
+ * Alt+ArrowDown open the listbox; bare Arrows + first-letter typeahead change
88
+ * the value inline without opening it — all are value-changing on a native
89
+ * select, so all are blocked in readonly.
90
+ */
91
+ blockKeyWhenReadonly(event) {
92
+ if (this.context.isReadonly() && event.key !== 'Tab') {
93
+ event.preventDefault();
94
+ }
95
+ }
96
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraNativeSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
97
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.7", type: SkraNativeSelect, isStandalone: true, selector: "select[skraSelect]", host: { listeners: { "mousedown": "blockWhenReadonly($event)", "keydown": "blockKeyWhenReadonly($event)" }, properties: { "id": "context.fieldId()", "attr.aria-describedby": "context.describedByIds()", "attr.aria-invalid": "context.hasError() ? \"true\" : null", "attr.aria-required": "context.isRequired() ? \"true\" : null", "attr.aria-readonly": "context.isReadonly() ? \"true\" : null" } }, ngImport: i0, template: '<ng-content></ng-content>', isInline: true, styles: [":host{background-color:var(--skra-comp-field-background);color:var(--skra-comp-field-foreground);border-color:var(--skra-comp-field-border-color);border-width:var(--skra-comp-field-border-width);border-style:var(--skra-comp-field-border-style);border-radius:var(--skra-comp-field-radius);padding-block:var(--skra-comp-field-padding-block);padding-inline:var(--skra-comp-field-padding-inline);box-shadow:var(--skra-comp-field-shadow);font:inherit;outline:none;cursor:pointer;transition:background-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),border-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),box-shadow var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing)}:host(:not([size])){appearance:none;padding-inline-end:var(--skra-comp-field-select-arrow-padding-inline-end);background-image:var(--skra-comp-field-select-chevron-glyph);background-repeat:no-repeat;background-position:right var(--skra-comp-field-padding-inline) center;background-size:var(--skra-comp-field-select-arrow-icon-size)}:host([size]){background-image:none;cursor:default}:host(:not([size]):hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-background-hover);border-color:var(--skra-comp-field-border-color-hover)}:host(:focus-visible){border-color:var(--skra-comp-field-focus-ring-color);box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-focus-ring-color)}:host(:disabled){cursor:not-allowed;opacity:var(--skra-sys-opacity-disabled)}:host([aria-readonly=true]){cursor:default}:host([aria-invalid=true]){border-color:var(--skra-comp-field-error-color)}:host([aria-invalid=true]:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-error-color)}@media(prefers-reduced-motion:reduce){:host{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
98
+ }
99
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraNativeSelect, decorators: [{
100
+ type: Component,
101
+ args: [{ selector: 'select[skraSelect]', standalone: true, template: '<ng-content></ng-content>', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, host: {
102
+ '[id]': 'context.fieldId()',
103
+ '[attr.aria-describedby]': 'context.describedByIds()',
104
+ '[attr.aria-invalid]': 'context.hasError() ? "true" : null',
105
+ '[attr.aria-required]': 'context.isRequired() ? "true" : null',
106
+ '[attr.aria-readonly]': 'context.isReadonly() ? "true" : null',
107
+ // No `data-skra-readonly` — slice consumes `[aria-readonly="true"]`
108
+ // directly; emitting a parallel data-attr would be dead surface
109
+ // (no slice selector reads it). Matches checkbox/radio readonly pattern
110
+ // in `_form-controls.scss` (ARIA-only branch, no data-attr companion).
111
+ '(mousedown)': 'blockWhenReadonly($event)',
112
+ '(keydown)': 'blockKeyWhenReadonly($event)',
113
+ }, styles: [":host{background-color:var(--skra-comp-field-background);color:var(--skra-comp-field-foreground);border-color:var(--skra-comp-field-border-color);border-width:var(--skra-comp-field-border-width);border-style:var(--skra-comp-field-border-style);border-radius:var(--skra-comp-field-radius);padding-block:var(--skra-comp-field-padding-block);padding-inline:var(--skra-comp-field-padding-inline);box-shadow:var(--skra-comp-field-shadow);font:inherit;outline:none;cursor:pointer;transition:background-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),border-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),box-shadow var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing)}:host(:not([size])){appearance:none;padding-inline-end:var(--skra-comp-field-select-arrow-padding-inline-end);background-image:var(--skra-comp-field-select-chevron-glyph);background-repeat:no-repeat;background-position:right var(--skra-comp-field-padding-inline) center;background-size:var(--skra-comp-field-select-arrow-icon-size)}:host([size]){background-image:none;cursor:default}:host(:not([size]):hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-background-hover);border-color:var(--skra-comp-field-border-color-hover)}:host(:focus-visible){border-color:var(--skra-comp-field-focus-ring-color);box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-focus-ring-color)}:host(:disabled){cursor:not-allowed;opacity:var(--skra-sys-opacity-disabled)}:host([aria-readonly=true]){cursor:default}:host([aria-invalid=true]){border-color:var(--skra-comp-field-error-color)}:host([aria-invalid=true]:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-error-color)}@media(prefers-reduced-motion:reduce){:host{transition:none}}\n"] }]
114
+ }], ctorParameters: () => [] });
115
+
116
+ // select[skraSelect] native form-control directive (SkraNativeSelect — ADR 0054 §4.3).
117
+
118
+ /**
119
+ * Generated bundle index. Do not edit.
120
+ */
121
+
122
+ export { SkraNativeSelect };
123
+ //# sourceMappingURL=skra-ui-native-select.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skra-ui-native-select.mjs","sources":["../../../../libs/ui/native-select/src/lib/select/select.ts","../../../../libs/ui/native-select/src/index.ts","../../../../libs/ui/native-select/src/skra-ui-native-select.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n ViewEncapsulation,\n effect,\n inject,\n isDevMode,\n} from '@angular/core';\nimport { SkraFieldContext } from '@skra/ui/field';\n\n/**\n * Wires a native `<select>` inside `<skra-field>` to SkraFieldContext (ADR 013).\n * Tier 1 Foundations primitive — **single-select only**: a dropdown, or a\n * single-select scrolling listbox via the native `[size]` attribute.\n *\n * Native `[multiple]` is deliberately NOT supported (the directive rejects it,\n * see below). Native `<select multiple>` UX is not production-grade and cannot\n * be fixed at the native-`<select>` tier: selection requires modifier keys or\n * drag, the plain-click-toggle affordance exists only for a bare `<select\n * multiple>` in some engines (and breaks with an explicit `[size]`), and the\n * option markers are UA-drawn / unstylable cross-browser. Multi-select — chip\n * display, search, custom option templates, and a styleable per-row checkbox —\n * is wholly a Tier 2 `skra-select` (CDK listbox) concern per\n * `adr/specs/0039-select-directive-spec.md` §1.1 / §4.1.\n *\n * <skra-field>\n * <label skraLabelFor>Country</label>\n * <select skraSelect [value]=\"country()\" (change)=\"country.set($any($event.target).value)\">\n * <option value=\"pl\">Poland</option>\n * <option value=\"de\">Germany</option>\n * </select>\n * </skra-field>\n *\n * Native `<select>` is the only web-platform widget whose dropdown renders in\n * the OS / browser top-layer, above modal `<dialog>`, above Shadow DOM\n * boundaries, above z-index stacking contexts. By staying native at Tier 1 we\n * inherit that for free — Tier 2 custom listbox will solve the same problem\n * via CDK overlay + Top Layer API.\n *\n * Value channel stays consumer-owned per ADR 037: bind via native `[value]` +\n * `(change)`, Reactive Forms `formControlName`, Signal Forms `[formField]`,\n * or `[(ngModel)]` — all work orthogonally.\n *\n * `readonly` is ARIA-only because HTML spec does not apply native `[readonly]`\n * to `<select>` (same gotcha as `<input type=\"checkbox|radio\">`). Unlike a text\n * input, though, a native `<select>` is an *uncontrolled* widget: if the listbox\n * opens and the user picks an option, the displayed value changes regardless of\n * the consumer's binding — so \"consumer prevents change\" cannot actually hold a\n * native select readonly. The directive therefore enforces readonly behaviourally\n * (blocks the picker from opening / the value from changing — see\n * `blockWhenReadonly` / `blockKeyWhenReadonly`), mirroring the imperative\n * `el.disabled` enforcement below. Both states are made non-interactive; only\n * readonly stays focusable + un-greyed and presents as the bare value. This is\n * interaction integrity for the control's own state — NOT a value channel, so the\n * ADR 037 no-`@angular/forms` / no-CVA boundary is untouched.\n */\n@Component({\n selector: 'select[skraSelect]',\n standalone: true,\n // Content-bearing host: <ng-content> projects the consumer's\n // <option>/<optgroup> children through. `template: ''` (as used for the void\n // <input> host) would discard them — see ADR 040 §4.\n template: '<ng-content></ng-content>',\n styleUrl: './select.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n '[id]': 'context.fieldId()',\n '[attr.aria-describedby]': 'context.describedByIds()',\n '[attr.aria-invalid]': 'context.hasError() ? \"true\" : null',\n '[attr.aria-required]': 'context.isRequired() ? \"true\" : null',\n '[attr.aria-readonly]': 'context.isReadonly() ? \"true\" : null',\n // No `data-skra-readonly` — slice consumes `[aria-readonly=\"true\"]`\n // directly; emitting a parallel data-attr would be dead surface\n // (no slice selector reads it). Matches checkbox/radio readonly pattern\n // in `_form-controls.scss` (ARIA-only branch, no data-attr companion).\n '(mousedown)': 'blockWhenReadonly($event)',\n '(keydown)': 'blockKeyWhenReadonly($event)',\n },\n})\nexport class SkraNativeSelect {\n private readonly el = inject<ElementRef<HTMLSelectElement>>(ElementRef);\n readonly context = inject(SkraFieldContext);\n\n constructor() {\n const el = this.el.nativeElement;\n\n // Reject native multi-select loudly in dev (mirrors the mandatory-context\n // throw — Forge fails on contract violations rather than degrading\n // silently). Dev-only: never crashes a production bundle. A `<select\n // multiple>` that truly wants native UX can drop `skraSelect` and stay raw\n // HTML; for a real multi-select, use the Tier 2 <skra-select>.\n if (isDevMode() && el.multiple) {\n throw new Error(\n '[skraSelect] does not support the native [multiple] attribute at ' +\n 'Tier 1. Native multi-select UX is not production-grade (modifier-key/' +\n 'drag selection, UA-drawn unstylable markers, browser-fragile) and ' +\n 'cannot be fixed at the native <select> tier. Use a single-select ' +\n '<select skraSelect> (optionally with [size] for a listbox), or the ' +\n 'Tier 2 <skra-select> (CDK listbox) for real multi-select. See ' +\n 'adr/specs/0039-select-directive-spec.md §4.1.',\n );\n }\n\n effect(() => {\n el.disabled = this.context.isDisabled();\n });\n }\n\n /**\n * Readonly: suppress the pointer affordance that opens the native listbox.\n * `preventDefault` on `mousedown` stops the picker from opening (and, on a\n * native select, focus-on-click — Tab focus still works, which is the right\n * trade-off for a value-display control).\n */\n protected blockWhenReadonly(event: Event): void {\n if (this.context.isReadonly()) event.preventDefault();\n }\n\n /**\n * Readonly: suppress every keyboard affordance that opens or mutates the\n * select, while letting Tab through for focus traversal. Space / Enter /\n * Alt+ArrowDown open the listbox; bare Arrows + first-letter typeahead change\n * the value inline without opening it — all are value-changing on a native\n * select, so all are blocked in readonly.\n */\n protected blockKeyWhenReadonly(event: KeyboardEvent): void {\n if (this.context.isReadonly() && event.key !== 'Tab') {\n event.preventDefault();\n }\n }\n}\n","// select[skraSelect] native form-control directive (SkraNativeSelect — ADR 0054 §4.3).\nexport * from './lib/select/select';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAWA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CG;MAyBU,gBAAgB,CAAA;AACV,IAAA,EAAE,GAAG,MAAM,CAAgC,UAAU,CAAC;AAC9D,IAAA,OAAO,GAAG,MAAM,CAAC,gBAAgB,CAAC;AAE3C,IAAA,WAAA,GAAA;AACE,QAAA,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC,aAAa;;;;;;AAOhC,QAAA,IAAI,SAAS,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE;YAC9B,MAAM,IAAI,KAAK,CACb,mEAAmE;gBACjE,uEAAuE;gBACvE,oEAAoE;gBACpE,mEAAmE;gBACnE,qEAAqE;gBACrE,gEAAgE;AAChE,gBAAA,+CAA+C,CAClD;QACH;QAEA,MAAM,CAAC,MAAK;YACV,EAAE,CAAC,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE;AACzC,QAAA,CAAC,CAAC;IACJ;AAEA;;;;;AAKG;AACO,IAAA,iBAAiB,CAAC,KAAY,EAAA;AACtC,QAAA,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE;YAAE,KAAK,CAAC,cAAc,EAAE;IACvD;AAEA;;;;;;AAMG;AACO,IAAA,oBAAoB,CAAC,KAAoB,EAAA;AACjD,QAAA,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,EAAE;YACpD,KAAK,CAAC,cAAc,EAAE;QACxB;IACF;uGAlDW,gBAAgB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAhB,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,gBAAgB,ydAlBjB,2BAA2B,EAAA,QAAA,EAAA,IAAA,EAAA,MAAA,EAAA,CAAA,26DAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;2FAkB1B,gBAAgB,EAAA,UAAA,EAAA,CAAA;kBAxB5B,SAAS;AACE,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,oBAAoB,EAAA,UAAA,EAClB,IAAI,EAAA,QAAA,EAIN,2BAA2B,EAAA,eAAA,EAEpB,uBAAuB,CAAC,MAAM,EAAA,aAAA,EAChC,iBAAiB,CAAC,QAAQ,EAAA,IAAA,EACnC;AACJ,wBAAA,MAAM,EAAE,mBAAmB;AAC3B,wBAAA,yBAAyB,EAAE,0BAA0B;AACrD,wBAAA,qBAAqB,EAAE,oCAAoC;AAC3D,wBAAA,sBAAsB,EAAE,sCAAsC;AAC9D,wBAAA,sBAAsB,EAAE,sCAAsC;;;;;AAK9D,wBAAA,aAAa,EAAE,2BAA2B;AAC1C,wBAAA,WAAW,EAAE,8BAA8B;AAC5C,qBAAA,EAAA,MAAA,EAAA,CAAA,26DAAA,CAAA,EAAA;;;AC/EH;;ACAA;;AAEG;;;;"}
@@ -0,0 +1,158 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, inject, ElementRef, Renderer2, afterNextRender, DestroyRef, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { _attachPositioner } from '@skra/ui/core';
4
+
5
+ /**
6
+ * SkraPopover — a NON-MODAL, anchored panel of arbitrary (interactive) content, rendered in the
7
+ * browser TOP LAYER via the native Popover API (`popover="auto"`), per ADR 0044. It is the middle
8
+ * ground between Tooltip (supplementary, non-interactive, hover/focus) and the page-blocking modal
9
+ * Dialog.
10
+ *
11
+ * SHAPE (scope 2026-06-06): a Component on the PANEL with projected content — NOT a directive on the
12
+ * trigger + `TemplateRef`. The trigger is the consumer's own button linked by the native
13
+ * `popovertarget` attribute (zero Forge trigger API); toggle, light-dismiss (pointer-outside),
14
+ * Escape and invocation-order stacking are ALL platform-owned (contrast Tooltip's `popover="manual"`,
15
+ * which hand-wires Escape). The consumer MUST give `<skra-popover>` an `id` so `popovertarget` can
16
+ * reference it.
17
+ *
18
+ * <button skraButton popovertarget="user-menu">Account</button>
19
+ * <skra-popover id="user-menu"> …interactive content… </skra-popover>
20
+ *
21
+ * Forge owns only: the token-driven surface (see `popover.scss`), positioning via `SkraPositioner`
22
+ * (geometry-only inline; ADR 0044 §3 rules 4–5), and the disclosure ARIA on the trigger
23
+ * (`aria-expanded` / `aria-controls`). Focus is NOT trapped and NOT force-moved (non-modal — ADR 0044
24
+ * §3 rule 8); reachability + focus-return are native to the Popover API. Panel naming is the
25
+ * consumer's `aria-labelledby` → projected heading (ADR 033 container carve-out — no `ariaLabel`
26
+ * input). A right-click context menu is NOT a Popover concern (→ `skra-menu` + recipe; SRP + WCAG).
27
+ *
28
+ * Full contract: adr/specs/component-popover-spec.md.
29
+ */
30
+ class SkraPopover {
31
+ /** Placement intent; `flip`/`shift` adjust at runtime. */
32
+ placement = input('bottom', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
33
+ host = inject(ElementRef);
34
+ renderer = inject(Renderer2);
35
+ /** The consumer's `[popovertarget]` button, resolved once (lazily-idempotent via `resolved`). */
36
+ trigger = null;
37
+ resolved = false;
38
+ positioner;
39
+ constructor() {
40
+ // All imperative DOM wiring lives in afterNextRender — a browser-only hook, so the native listener
41
+ // and the trigger resolution never run on the server (SSR-safe by construction; O-18).
42
+ afterNextRender(() => {
43
+ // Imperative (NOT host-metadata) so a consumer `(toggle)` binding does not shadow it — see host {}.
44
+ this.host.nativeElement.addEventListener('toggle', this.onToggleNative);
45
+ // Resolve + wire the trigger after the view exists (the trigger may be a later sibling), so the
46
+ // disclosure relationship (aria-expanded="false" + aria-controls) is present before first open.
47
+ this.ensureTrigger();
48
+ });
49
+ inject(DestroyRef).onDestroy(() => this.teardown());
50
+ }
51
+ // ── Toggle (imperative native listener — see host {} note) ────────────────────
52
+ onToggleNative = (event) => {
53
+ if (event.newState === 'open')
54
+ this.onOpen();
55
+ else
56
+ this.onClose();
57
+ };
58
+ onOpen() {
59
+ const anchor = this.ensureTrigger(); // safety net if afterNextRender has not yet run
60
+ if (anchor) {
61
+ this.renderer.setAttribute(anchor, 'aria-expanded', 'true');
62
+ }
63
+ else {
64
+ return; // orphan popover (dev-warned at init); nothing to anchor to.
65
+ }
66
+ this.positioner?.destroy();
67
+ this.positioner = _attachPositioner(anchor, this.host.nativeElement, () => ({
68
+ placement: this.placement(),
69
+ offset: this.cssLenPx('--skra-comp-popover-offset'),
70
+ viewportPadding: this.cssLenPx('--skra-comp-popover-viewport-padding'),
71
+ }));
72
+ }
73
+ onClose() {
74
+ if (this.trigger) {
75
+ this.renderer.setAttribute(this.trigger, 'aria-expanded', 'false');
76
+ }
77
+ this.positioner?.destroy();
78
+ this.positioner = undefined;
79
+ }
80
+ /**
81
+ * Resolve the `[popovertarget]` trigger and establish the disclosure ARIA on it (sanctioned a11y
82
+ * wiring, ADR 013-style — not style mutation). Idempotent: runs at most once. Returns the trigger
83
+ * (or null for an orphan popover, dev-warned once).
84
+ */
85
+ ensureTrigger() {
86
+ if (this.resolved)
87
+ return this.trigger;
88
+ this.resolved = true;
89
+ const id = this.host.nativeElement.id;
90
+ if (!id) {
91
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
92
+ console.warn('[skra-popover] has no `id` — it cannot be targeted by a `popovertarget` ' +
93
+ 'trigger and the disclosure ARIA (aria-expanded/aria-controls) cannot be wired. ' +
94
+ 'Give the popover an id and point the trigger at it: ' +
95
+ '<button popovertarget="my-id">…</button> <skra-popover id="my-id">…');
96
+ }
97
+ return null;
98
+ }
99
+ // Match by attribute value rather than a built selector — no escaping needed, robust to ids
100
+ // with special characters (and `CSS.escape` is not universally available).
101
+ this.trigger =
102
+ Array.from(document.querySelectorAll('[popovertarget]')).find((el) => el.getAttribute('popovertarget') === id) ?? null;
103
+ if (!this.trigger) {
104
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
105
+ console.warn(`[skra-popover] no trigger found with [popovertarget="${id}"] — the popover ` +
106
+ 'is unreachable and has no element to carry aria-expanded/aria-controls. ' +
107
+ 'Add a focusable trigger button with the matching popovertarget.');
108
+ }
109
+ return null;
110
+ }
111
+ // Establish the disclosure relationship before the first open (APG Disclosure).
112
+ this.renderer.setAttribute(this.trigger, 'aria-controls', id);
113
+ this.renderer.setAttribute(this.trigger, 'aria-expanded', 'false');
114
+ return this.trigger;
115
+ }
116
+ /** Read a length comp token (px/rem) off the host computed style for the positioner. */
117
+ cssLenPx(prop) {
118
+ const raw = getComputedStyle(this.host.nativeElement)
119
+ .getPropertyValue(prop)
120
+ .trim();
121
+ if (raw.endsWith('px'))
122
+ return parseFloat(raw);
123
+ if (raw.endsWith('rem')) {
124
+ const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
125
+ return parseFloat(raw) * rootFontSize;
126
+ }
127
+ return parseFloat(raw) || 0;
128
+ }
129
+ teardown() {
130
+ this.host.nativeElement.removeEventListener('toggle', this.onToggleNative);
131
+ this.positioner?.destroy();
132
+ if (this.trigger) {
133
+ this.renderer.removeAttribute(this.trigger, 'aria-expanded');
134
+ this.renderer.removeAttribute(this.trigger, 'aria-controls');
135
+ }
136
+ }
137
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraPopover, deps: [], target: i0.ɵɵFactoryTarget.Component });
138
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.7", type: SkraPopover, isStandalone: true, selector: "skra-popover", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "popover": "auto" } }, exportAs: ["skraPopover"], ngImport: i0, template: '<ng-content></ng-content>', isInline: true, styles: ["@charset \"UTF-8\";:host{position:fixed;margin:0;inset:auto;box-sizing:border-box;background:var(--skra-comp-popover-background);color:var(--skra-comp-popover-foreground);border-color:var(--skra-comp-popover-border-color);border-width:var(--skra-comp-popover-border-width);border-style:var(--skra-comp-popover-border-style);border-radius:var(--skra-comp-popover-radius);padding-block:var(--skra-comp-popover-padding-block);padding-inline:var(--skra-comp-popover-padding-inline);box-shadow:var(--skra-comp-popover-shadow);inline-size:max-content;max-inline-size:var(--skra-comp-popover-max-inline-size);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);opacity:0;transition:opacity var(--skra-comp-popover-motion-duration) var(--skra-comp-popover-motion-easing),display var(--skra-comp-popover-motion-duration) allow-discrete,overlay var(--skra-comp-popover-motion-duration) allow-discrete}:host(:popover-open){display:block;opacity:1}@starting-style{:host(:popover-open){opacity:0}}@media(prefers-reduced-motion:reduce){:host{transition-duration:0ms}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
139
+ }
140
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraPopover, decorators: [{
141
+ type: Component,
142
+ args: [{ selector: 'skra-popover', standalone: true, exportAs: 'skraPopover', template: '<ng-content></ng-content>', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, host: {
143
+ // Native top layer + platform-owned toggle / light-dismiss / Escape / stacking (ADR 0044).
144
+ popover: 'auto',
145
+ // NOTE: `toggle` is wired IMPERATIVELY (addEventListener) in the constructor, NOT as a host listener.
146
+ // A consumer template binding `(toggle)` on <skra-popover> (a documented side-effect path) SHADOWS a
147
+ // host-metadata `(toggle)` listener for the same event — which would silently break positioning +
148
+ // aria-expanded. A raw `addEventListener` fires alongside the consumer's binding. (Found via the
149
+ // skra-menu browser probe, 2026-06-07; same fix applied there.)
150
+ }, styles: ["@charset \"UTF-8\";:host{position:fixed;margin:0;inset:auto;box-sizing:border-box;background:var(--skra-comp-popover-background);color:var(--skra-comp-popover-foreground);border-color:var(--skra-comp-popover-border-color);border-width:var(--skra-comp-popover-border-width);border-style:var(--skra-comp-popover-border-style);border-radius:var(--skra-comp-popover-radius);padding-block:var(--skra-comp-popover-padding-block);padding-inline:var(--skra-comp-popover-padding-inline);box-shadow:var(--skra-comp-popover-shadow);inline-size:max-content;max-inline-size:var(--skra-comp-popover-max-inline-size);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);opacity:0;transition:opacity var(--skra-comp-popover-motion-duration) var(--skra-comp-popover-motion-easing),display var(--skra-comp-popover-motion-duration) allow-discrete,overlay var(--skra-comp-popover-motion-duration) allow-discrete}:host(:popover-open){display:block;opacity:1}@starting-style{:host(:popover-open){opacity:0}}@media(prefers-reduced-motion:reduce){:host{transition-duration:0ms}}\n"] }]
151
+ }], ctorParameters: () => [], propDecorators: { placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }] } });
152
+
153
+ /**
154
+ * Generated bundle index. Do not edit.
155
+ */
156
+
157
+ export { SkraPopover };
158
+ //# sourceMappingURL=skra-ui-popover.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skra-ui-popover.mjs","sources":["../../../../libs/ui/popover/src/lib/popover/popover.ts","../../../../libs/ui/popover/src/skra-ui-popover.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n ElementRef,\n Renderer2,\n ViewEncapsulation,\n afterNextRender,\n inject,\n input,\n} from '@angular/core';\n\nimport {\n _attachPositioner,\n type SkraPlacement,\n type _PositionerHandle,\n} from '@skra/ui/core';\n\ndeclare const ngDevMode: boolean | undefined;\n\n/**\n * SkraPopover — a NON-MODAL, anchored panel of arbitrary (interactive) content, rendered in the\n * browser TOP LAYER via the native Popover API (`popover=\"auto\"`), per ADR 0044. It is the middle\n * ground between Tooltip (supplementary, non-interactive, hover/focus) and the page-blocking modal\n * Dialog.\n *\n * SHAPE (scope 2026-06-06): a Component on the PANEL with projected content — NOT a directive on the\n * trigger + `TemplateRef`. The trigger is the consumer's own button linked by the native\n * `popovertarget` attribute (zero Forge trigger API); toggle, light-dismiss (pointer-outside),\n * Escape and invocation-order stacking are ALL platform-owned (contrast Tooltip's `popover=\"manual\"`,\n * which hand-wires Escape). The consumer MUST give `<skra-popover>` an `id` so `popovertarget` can\n * reference it.\n *\n * <button skraButton popovertarget=\"user-menu\">Account</button>\n * <skra-popover id=\"user-menu\"> …interactive content… </skra-popover>\n *\n * Forge owns only: the token-driven surface (see `popover.scss`), positioning via `SkraPositioner`\n * (geometry-only inline; ADR 0044 §3 rules 4–5), and the disclosure ARIA on the trigger\n * (`aria-expanded` / `aria-controls`). Focus is NOT trapped and NOT force-moved (non-modal — ADR 0044\n * §3 rule 8); reachability + focus-return are native to the Popover API. Panel naming is the\n * consumer's `aria-labelledby` → projected heading (ADR 033 container carve-out — no `ariaLabel`\n * input). A right-click context menu is NOT a Popover concern (→ `skra-menu` + recipe; SRP + WCAG).\n *\n * Full contract: adr/specs/component-popover-spec.md.\n */\n@Component({\n selector: 'skra-popover',\n standalone: true,\n exportAs: 'skraPopover',\n template: '<ng-content></ng-content>',\n styleUrl: './popover.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n // Native top layer + platform-owned toggle / light-dismiss / Escape / stacking (ADR 0044).\n popover: 'auto',\n // NOTE: `toggle` is wired IMPERATIVELY (addEventListener) in the constructor, NOT as a host listener.\n // A consumer template binding `(toggle)` on <skra-popover> (a documented side-effect path) SHADOWS a\n // host-metadata `(toggle)` listener for the same event — which would silently break positioning +\n // aria-expanded. A raw `addEventListener` fires alongside the consumer's binding. (Found via the\n // skra-menu browser probe, 2026-06-07; same fix applied there.)\n },\n})\nexport class SkraPopover {\n /** Placement intent; `flip`/`shift` adjust at runtime. */\n readonly placement = input<SkraPlacement>('bottom');\n\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n private readonly renderer = inject(Renderer2);\n\n /** The consumer's `[popovertarget]` button, resolved once (lazily-idempotent via `resolved`). */\n private trigger: HTMLElement | null = null;\n private resolved = false;\n private positioner?: _PositionerHandle;\n\n constructor() {\n // All imperative DOM wiring lives in afterNextRender — a browser-only hook, so the native listener\n // and the trigger resolution never run on the server (SSR-safe by construction; O-18).\n afterNextRender(() => {\n // Imperative (NOT host-metadata) so a consumer `(toggle)` binding does not shadow it — see host {}.\n this.host.nativeElement.addEventListener('toggle', this.onToggleNative);\n // Resolve + wire the trigger after the view exists (the trigger may be a later sibling), so the\n // disclosure relationship (aria-expanded=\"false\" + aria-controls) is present before first open.\n this.ensureTrigger();\n });\n inject(DestroyRef).onDestroy(() => this.teardown());\n }\n\n // ── Toggle (imperative native listener — see host {} note) ────────────────────\n private readonly onToggleNative = (event: Event): void => {\n if ((event as ToggleEvent).newState === 'open') this.onOpen();\n else this.onClose();\n };\n\n private onOpen(): void {\n const anchor = this.ensureTrigger(); // safety net if afterNextRender has not yet run\n if (anchor) {\n this.renderer.setAttribute(anchor, 'aria-expanded', 'true');\n } else {\n return; // orphan popover (dev-warned at init); nothing to anchor to.\n }\n\n this.positioner?.destroy();\n this.positioner = _attachPositioner(\n anchor,\n this.host.nativeElement,\n () => ({\n placement: this.placement(),\n offset: this.cssLenPx('--skra-comp-popover-offset'),\n viewportPadding: this.cssLenPx('--skra-comp-popover-viewport-padding'),\n }),\n );\n }\n\n private onClose(): void {\n if (this.trigger) {\n this.renderer.setAttribute(this.trigger, 'aria-expanded', 'false');\n }\n this.positioner?.destroy();\n this.positioner = undefined;\n }\n\n /**\n * Resolve the `[popovertarget]` trigger and establish the disclosure ARIA on it (sanctioned a11y\n * wiring, ADR 013-style — not style mutation). Idempotent: runs at most once. Returns the trigger\n * (or null for an orphan popover, dev-warned once).\n */\n private ensureTrigger(): HTMLElement | null {\n if (this.resolved) return this.trigger;\n this.resolved = true;\n\n const id = this.host.nativeElement.id;\n if (!id) {\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\n console.warn(\n '[skra-popover] has no `id` — it cannot be targeted by a `popovertarget` ' +\n 'trigger and the disclosure ARIA (aria-expanded/aria-controls) cannot be wired. ' +\n 'Give the popover an id and point the trigger at it: ' +\n '<button popovertarget=\"my-id\">…</button> <skra-popover id=\"my-id\">…',\n );\n }\n return null;\n }\n\n // Match by attribute value rather than a built selector — no escaping needed, robust to ids\n // with special characters (and `CSS.escape` is not universally available).\n this.trigger =\n Array.from(\n document.querySelectorAll<HTMLElement>('[popovertarget]'),\n ).find((el) => el.getAttribute('popovertarget') === id) ?? null;\n if (!this.trigger) {\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\n console.warn(\n `[skra-popover] no trigger found with [popovertarget=\"${id}\"] — the popover ` +\n 'is unreachable and has no element to carry aria-expanded/aria-controls. ' +\n 'Add a focusable trigger button with the matching popovertarget.',\n );\n }\n return null;\n }\n\n // Establish the disclosure relationship before the first open (APG Disclosure).\n this.renderer.setAttribute(this.trigger, 'aria-controls', id);\n this.renderer.setAttribute(this.trigger, 'aria-expanded', 'false');\n return this.trigger;\n }\n\n /** Read a length comp token (px/rem) off the host computed style for the positioner. */\n private cssLenPx(prop: string): number {\n const raw = getComputedStyle(this.host.nativeElement)\n .getPropertyValue(prop)\n .trim();\n if (raw.endsWith('px')) return parseFloat(raw);\n if (raw.endsWith('rem')) {\n const rootFontSize =\n parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;\n return parseFloat(raw) * rootFontSize;\n }\n return parseFloat(raw) || 0;\n }\n\n private teardown(): void {\n this.host.nativeElement.removeEventListener('toggle', this.onToggleNative);\n this.positioner?.destroy();\n if (this.trigger) {\n this.renderer.removeAttribute(this.trigger, 'aria-expanded');\n this.renderer.removeAttribute(this.trigger, 'aria-controls');\n }\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
+
4
+ /**
5
+ * `progress[skraProgress]` — a determinate, horizontal task-completion bar (Wave 2,
6
+ * `libs/ui/indicators`). Shows the known progress of a finite task (file upload, wizard
7
+ * "step 3 of 5", import job) as a fill from 0 → `value` / `max`.
8
+ *
9
+ * SRP-distinct from `[skraLoadingIndicator]` (ADR 028): that primitive is the
10
+ * *indeterminate* "host is busy" spinner injected into another component's `loading()`
11
+ * slot; SkraProgress is the *determinate* "task is N% complete" standalone bar. The two
12
+ * are complementary — see `adr/specs/component-progress-spec.md` § SRP boundary.
13
+ *
14
+ * **Why a Component on the native `<progress>` host** (ADR 040), not a `<div role="progressbar">`:
15
+ * 1. ADR 021 (CSP): the fill width is a continuous `value/max` length. A custom element would
16
+ * force `[style.--progress-value]` (forbidden) or a class/`data-*`-per-percent hack. Native
17
+ * `<progress value max>` lets the BROWSER compute the fill width on its
18
+ * `::-webkit-progress-value` / `::-moz-progress-bar` pseudo — zero author inline-style.
19
+ * 2. Free a11y: the native host carries `role="progressbar"` + `aria-valuenow`/`-valuemin`/
20
+ * `-valuemax` with no author ARIA (native-attr-first, boundary #9).
21
+ *
22
+ * Non-interactive: `<progress>` is non-focusable, so the State Surface is all-N/A (no
23
+ * `disabled`/`loading`/`severity` in MVP — those are deferred / belong to other atoms).
24
+ *
25
+ * Naming (ADR 033): the consumer associates a `<label for>` / `aria-labelledby` (passed through
26
+ * to the host). No `SkraFieldContext` injection (a progress bar is a read-only display, not a
27
+ * form field); no `ariaLabel` input.
28
+ *
29
+ * `value`/`max` are plain consumer-driven inputs (ADR 037 — agnostic; wire the source from any
30
+ * signal/form strategy). Native `<progress>` clamps `value` to `[0, max]`, so no author clamp.
31
+ */
32
+ class SkraProgress {
33
+ /** Current completion, reflected to the native `value` attribute (native clamps to [0, max]). */
34
+ value = input(0, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
35
+ /** Upper bound, reflected to the native `max` attribute. Drives native `aria-valuemax`. */
36
+ max = input(100, ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
37
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraProgress, deps: [], target: i0.ɵɵFactoryTarget.Component });
38
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.7", type: SkraProgress, isStandalone: true, selector: "progress[skraProgress]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.value": "value()", "attr.max": "max()" } }, ngImport: i0, template: '', isInline: true, styles: [":host{appearance:none;-webkit-appearance:none;box-sizing:border-box;display:block;inline-size:100%;block-size:var(--skra-comp-progress-block-size);border-width:var(--skra-comp-progress-border-width);border-style:var(--skra-comp-progress-border-style);border-color:var(--skra-comp-progress-border-color);border-radius:var(--skra-comp-progress-radius);box-shadow:var(--skra-comp-progress-shadow);background-color:var(--skra-comp-progress-background);overflow:hidden;margin:0}:host::-webkit-progress-bar{background-color:var(--skra-comp-progress-background);border-radius:var(--skra-comp-progress-radius)}:host::-webkit-progress-value{background-color:var(--skra-comp-progress-fill-color);transition:inline-size var(--skra-comp-progress-motion-duration) var(--skra-comp-progress-motion-easing)}:host::-moz-progress-bar{background-color:var(--skra-comp-progress-fill-color)}@media(prefers-reduced-motion:reduce){:host::-webkit-progress-value{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
39
+ }
40
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraProgress, decorators: [{
41
+ type: Component,
42
+ args: [{ selector: 'progress[skraProgress]', standalone: true, template: '', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, host: {
43
+ // The data contract — reflected to the NATIVE attributes. The browser derives
44
+ // role="progressbar" + aria-valuenow/-valuemin/-valuemax + the fill width from these.
45
+ // No [style.*] (ADR 021 — the fill is native, never an inline style) and no data-skra-*
46
+ // (State Surface all-N/A).
47
+ '[attr.value]': 'value()',
48
+ '[attr.max]': 'max()',
49
+ }, styles: [":host{appearance:none;-webkit-appearance:none;box-sizing:border-box;display:block;inline-size:100%;block-size:var(--skra-comp-progress-block-size);border-width:var(--skra-comp-progress-border-width);border-style:var(--skra-comp-progress-border-style);border-color:var(--skra-comp-progress-border-color);border-radius:var(--skra-comp-progress-radius);box-shadow:var(--skra-comp-progress-shadow);background-color:var(--skra-comp-progress-background);overflow:hidden;margin:0}:host::-webkit-progress-bar{background-color:var(--skra-comp-progress-background);border-radius:var(--skra-comp-progress-radius)}:host::-webkit-progress-value{background-color:var(--skra-comp-progress-fill-color);transition:inline-size var(--skra-comp-progress-motion-duration) var(--skra-comp-progress-motion-easing)}:host::-moz-progress-bar{background-color:var(--skra-comp-progress-fill-color)}@media(prefers-reduced-motion:reduce){:host::-webkit-progress-value{transition:none}}\n"] }]
50
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }] } });
51
+
52
+ /**
53
+ * Generated bundle index. Do not edit.
54
+ */
55
+
56
+ export { SkraProgress };
57
+ //# sourceMappingURL=skra-ui-progress.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skra-ui-progress.mjs","sources":["../../../../libs/ui/progress/src/lib/progress/progress.ts","../../../../libs/ui/progress/src/skra-ui-progress.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ViewEncapsulation,\n input,\n} from '@angular/core';\n\n/**\n * `progress[skraProgress]` — a determinate, horizontal task-completion bar (Wave 2,\n * `libs/ui/indicators`). Shows the known progress of a finite task (file upload, wizard\n * \"step 3 of 5\", import job) as a fill from 0 → `value` / `max`.\n *\n * SRP-distinct from `[skraLoadingIndicator]` (ADR 028): that primitive is the\n * *indeterminate* \"host is busy\" spinner injected into another component's `loading()`\n * slot; SkraProgress is the *determinate* \"task is N% complete\" standalone bar. The two\n * are complementary — see `adr/specs/component-progress-spec.md` § SRP boundary.\n *\n * **Why a Component on the native `<progress>` host** (ADR 040), not a `<div role=\"progressbar\">`:\n * 1. ADR 021 (CSP): the fill width is a continuous `value/max` length. A custom element would\n * force `[style.--progress-value]` (forbidden) or a class/`data-*`-per-percent hack. Native\n * `<progress value max>` lets the BROWSER compute the fill width on its\n * `::-webkit-progress-value` / `::-moz-progress-bar` pseudo — zero author inline-style.\n * 2. Free a11y: the native host carries `role=\"progressbar\"` + `aria-valuenow`/`-valuemin`/\n * `-valuemax` with no author ARIA (native-attr-first, boundary #9).\n *\n * Non-interactive: `<progress>` is non-focusable, so the State Surface is all-N/A (no\n * `disabled`/`loading`/`severity` in MVP — those are deferred / belong to other atoms).\n *\n * Naming (ADR 033): the consumer associates a `<label for>` / `aria-labelledby` (passed through\n * to the host). No `SkraFieldContext` injection (a progress bar is a read-only display, not a\n * form field); no `ariaLabel` input.\n *\n * `value`/`max` are plain consumer-driven inputs (ADR 037 — agnostic; wire the source from any\n * signal/form strategy). Native `<progress>` clamps `value` to `[0, max]`, so no author clamp.\n */\n@Component({\n selector: 'progress[skraProgress]',\n standalone: true,\n template: '',\n styleUrl: './progress.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n // The data contract — reflected to the NATIVE attributes. The browser derives\n // role=\"progressbar\" + aria-valuenow/-valuemin/-valuemax + the fill width from these.\n // No [style.*] (ADR 021 — the fill is native, never an inline style) and no data-skra-*\n // (State Surface all-N/A).\n '[attr.value]': 'value()',\n '[attr.max]': 'max()',\n },\n})\nexport class SkraProgress {\n /** Current completion, reflected to the native `value` attribute (native clamps to [0, max]). */\n readonly value = input<number>(0);\n\n /** Upper bound, reflected to the native `max` attribute. Drives native `aria-valuemax`. */\n readonly max = input<number>(100);\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;AAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BG;MAiBU,YAAY,CAAA;;AAEd,IAAA,KAAK,GAAG,KAAK,CAAS,CAAC,4EAAC;;AAGxB,IAAA,GAAG,GAAG,KAAK,CAAS,GAAG,0EAAC;uGALtB,YAAY,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAZ,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,YAAY,gZAbb,EAAE,EAAA,QAAA,EAAA,IAAA,EAAA,MAAA,EAAA,CAAA,+7BAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;2FAaD,YAAY,EAAA,UAAA,EAAA,CAAA;kBAhBxB,SAAS;AACE,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,wBAAwB,EAAA,UAAA,EACtB,IAAI,EAAA,QAAA,EACN,EAAE,EAAA,eAAA,EAEK,uBAAuB,CAAC,MAAM,EAAA,aAAA,EAChC,iBAAiB,CAAC,QAAQ,EAAA,IAAA,EACnC;;;;;AAKJ,wBAAA,cAAc,EAAE,SAAS;AACzB,wBAAA,YAAY,EAAE,OAAO;AACtB,qBAAA,EAAA,MAAA,EAAA,CAAA,+7BAAA,CAAA,EAAA;;;ACjDH;;AAEG;;;;"}
@@ -0,0 +1,92 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, ElementRef, effect, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { SkraFieldContext } from '@skra/ui/field';
4
+
5
+ /**
6
+ * Wires a native `<input type="radio">` to SkraFieldContext when inside
7
+ * `<skra-field>`, or stands alone with no cascade. Per ADR 039/040, Forge ships
8
+ * no `<skra-radio>` / `<skra-radio-group>` wrappers — this is a Component-on-
9
+ * the-native-host (`<input>` is void → `template: ''`, ADR 040 §2) carrying its
10
+ * own `radio.scss` via Emulated encapsulation. Radios are
11
+ * grouped via native `<fieldset skraFieldset>` + `<legend skraLegend>`,
12
+ * coordination happens through the shared `name` attribute (HTML-native), and
13
+ * one-of-N value selection is consumer-owned per ADR 037.
14
+ *
15
+ * <fieldset skraFieldset>
16
+ * <legend skraLegend>Choose plan</legend>
17
+ * <skra-field>
18
+ * <input type="radio" skraRadio name="plan" value="free"
19
+ * [checked]="plan() === 'free'" (change)="plan.set('free')" />
20
+ * <label skraLabelFor>Free</label>
21
+ * </skra-field>
22
+ * <skra-field>
23
+ * <input type="radio" skraRadio name="plan" value="pro"
24
+ * [checked]="plan() === 'pro'" (change)="plan.set('pro')" />
25
+ * <label skraLabelFor>Pro</label>
26
+ * </skra-field>
27
+ * </fieldset>
28
+ *
29
+ * Toggle state (`checked`) is NOT reflected as `data-skra-*` — slice CSS uses
30
+ * the native `:checked` pseudo-class. Browser handles `aria-checked` natively.
31
+ */
32
+ class SkraRadio {
33
+ el = inject(ElementRef);
34
+ context = inject(SkraFieldContext, { optional: true });
35
+ constructor() {
36
+ if (this.context) {
37
+ const ctx = this.context;
38
+ const el = this.el.nativeElement;
39
+ effect(() => {
40
+ el.id = ctx.fieldId();
41
+ });
42
+ effect(() => {
43
+ el.disabled = ctx.isDisabled();
44
+ });
45
+ }
46
+ }
47
+ /**
48
+ * Readonly enforcement — native radio has no `[readonly]`, so without this the
49
+ * `aria-readonly` we announce would be a lie (the user could still change the
50
+ * selection). `preventDefault` on `click` blocks the selection change from both
51
+ * pointer and Space/Enter (keyboard activation dispatches a click on form
52
+ * controls). Mirrors the `<select skraSelect>` readonly guard.
53
+ */
54
+ blockWhenReadonly(event) {
55
+ if (this.context?.isReadonly())
56
+ event.preventDefault();
57
+ }
58
+ /**
59
+ * Arrow keys move selection between radios in a `name` group WITHOUT a click,
60
+ * so they need a separate guard. Block every key except `Tab` (focus traversal)
61
+ * when readonly.
62
+ */
63
+ blockKeyWhenReadonly(event) {
64
+ if (this.context?.isReadonly() && event.key !== 'Tab') {
65
+ event.preventDefault();
66
+ }
67
+ }
68
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraRadio, deps: [], target: i0.ɵɵFactoryTarget.Component });
69
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.7", type: SkraRadio, isStandalone: true, selector: "input[type=\"radio\"][skraRadio]", host: { listeners: { "click": "blockWhenReadonly($event)", "keydown": "blockKeyWhenReadonly($event)" }, properties: { "attr.data-skra-disabled": "context?.isDisabled() ? \"\" : null", "attr.aria-invalid": "context?.hasError() ? \"true\" : null", "attr.aria-readonly": "context?.isReadonly() ? \"true\" : null", "attr.aria-describedby": "context?.describedByIds() ?? null" } }, ngImport: i0, template: '', isInline: true, styles: [":host{appearance:none;flex-shrink:0;cursor:pointer;outline:none;background-repeat:no-repeat;background-position:center;inline-size:var(--skra-comp-field-radio-size);block-size:var(--skra-comp-field-radio-size);border-width:var(--skra-comp-field-radio-border-width);border-style:solid;border-color:var(--skra-comp-field-radio-border-color);border-radius:50%;background-color:var(--skra-comp-field-radio-background);transition:background-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),border-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),box-shadow var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing)}:host(:checked){background-color:var(--skra-comp-field-radio-checked-background);border-color:var(--skra-comp-field-radio-checked-background);background-image:radial-gradient(circle,var(--skra-comp-field-radio-dot-color) 0,var(--skra-comp-field-radio-dot-color) calc(var(--skra-comp-field-radio-dot-size) / 2),transparent calc(var(--skra-comp-field-radio-dot-size) / 2 + .5px))}:host(:hover:not(:disabled):not([aria-readonly=true])){border-color:var(--skra-comp-field-radio-border-color-hover)}:host(:checked:hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-radio-checked-background-hover);border-color:var(--skra-comp-field-radio-checked-background-hover)}:host(:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-focus-ring-color)}:host(:disabled),:host([data-skra-disabled]){cursor:not-allowed;opacity:var(--skra-sys-opacity-disabled)}:host([aria-readonly=true]){cursor:default;background-color:var(--skra-comp-field-background-readonly);border-color:var(--skra-comp-field-border-color-readonly)}:host(:checked[aria-readonly=true]){background-color:var(--skra-comp-field-radio-checked-background-readonly);border-color:var(--skra-comp-field-radio-checked-background-readonly)}:host([aria-invalid=true]){border-color:var(--skra-comp-field-error-color)}:host([aria-invalid=true]:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-error-color)}@media(prefers-reduced-motion:reduce){:host{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
70
+ }
71
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraRadio, decorators: [{
72
+ type: Component,
73
+ args: [{ selector: 'input[type="radio"][skraRadio]', standalone: true, template: '', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, host: {
74
+ '[attr.data-skra-disabled]': 'context?.isDisabled() ? "" : null',
75
+ '[attr.aria-invalid]': 'context?.hasError() ? "true" : null',
76
+ // Readonly is ARIA-only (native `readonly` doesn't apply to radios per HTML
77
+ // spec). No `data-skra-readonly` — dead surface (styling keys off
78
+ // `[aria-readonly]`), removed 2026-05-31 to match select's Q-3 ARIA-only
79
+ // pattern (Batch 5 cleanroom review).
80
+ '[attr.aria-readonly]': 'context?.isReadonly() ? "true" : null',
81
+ '[attr.aria-describedby]': 'context?.describedByIds() ?? null',
82
+ '(click)': 'blockWhenReadonly($event)',
83
+ '(keydown)': 'blockKeyWhenReadonly($event)',
84
+ }, styles: [":host{appearance:none;flex-shrink:0;cursor:pointer;outline:none;background-repeat:no-repeat;background-position:center;inline-size:var(--skra-comp-field-radio-size);block-size:var(--skra-comp-field-radio-size);border-width:var(--skra-comp-field-radio-border-width);border-style:solid;border-color:var(--skra-comp-field-radio-border-color);border-radius:50%;background-color:var(--skra-comp-field-radio-background);transition:background-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),border-color var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing),box-shadow var(--skra-comp-field-motion-duration) var(--skra-comp-field-motion-easing)}:host(:checked){background-color:var(--skra-comp-field-radio-checked-background);border-color:var(--skra-comp-field-radio-checked-background);background-image:radial-gradient(circle,var(--skra-comp-field-radio-dot-color) 0,var(--skra-comp-field-radio-dot-color) calc(var(--skra-comp-field-radio-dot-size) / 2),transparent calc(var(--skra-comp-field-radio-dot-size) / 2 + .5px))}:host(:hover:not(:disabled):not([aria-readonly=true])){border-color:var(--skra-comp-field-radio-border-color-hover)}:host(:checked:hover:not(:disabled):not([aria-readonly=true])){background-color:var(--skra-comp-field-radio-checked-background-hover);border-color:var(--skra-comp-field-radio-checked-background-hover)}:host(:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-focus-ring-color)}:host(:disabled),:host([data-skra-disabled]){cursor:not-allowed;opacity:var(--skra-sys-opacity-disabled)}:host([aria-readonly=true]){cursor:default;background-color:var(--skra-comp-field-background-readonly);border-color:var(--skra-comp-field-border-color-readonly)}:host(:checked[aria-readonly=true]){background-color:var(--skra-comp-field-radio-checked-background-readonly);border-color:var(--skra-comp-field-radio-checked-background-readonly)}:host([aria-invalid=true]){border-color:var(--skra-comp-field-error-color)}:host([aria-invalid=true]:focus-visible){box-shadow:0 0 0 var(--skra-comp-field-focus-ring-width) var(--skra-comp-field-error-color)}@media(prefers-reduced-motion:reduce){:host{transition:none}}\n"] }]
85
+ }], ctorParameters: () => [] });
86
+
87
+ /**
88
+ * Generated bundle index. Do not edit.
89
+ */
90
+
91
+ export { SkraRadio };
92
+ //# sourceMappingURL=skra-ui-radio.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skra-ui-radio.mjs","sources":["../../../../libs/ui/radio/src/lib/radio/radio.ts","../../../../libs/ui/radio/src/skra-ui-radio.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n ViewEncapsulation,\n effect,\n inject,\n} from '@angular/core';\nimport { SkraFieldContext } from '@skra/ui/field';\n\n/**\n * Wires a native `<input type=\"radio\">` to SkraFieldContext when inside\n * `<skra-field>`, or stands alone with no cascade. Per ADR 039/040, Forge ships\n * no `<skra-radio>` / `<skra-radio-group>` wrappers — this is a Component-on-\n * the-native-host (`<input>` is void → `template: ''`, ADR 040 §2) carrying its\n * own `radio.scss` via Emulated encapsulation. Radios are\n * grouped via native `<fieldset skraFieldset>` + `<legend skraLegend>`,\n * coordination happens through the shared `name` attribute (HTML-native), and\n * one-of-N value selection is consumer-owned per ADR 037.\n *\n * <fieldset skraFieldset>\n * <legend skraLegend>Choose plan</legend>\n * <skra-field>\n * <input type=\"radio\" skraRadio name=\"plan\" value=\"free\"\n * [checked]=\"plan() === 'free'\" (change)=\"plan.set('free')\" />\n * <label skraLabelFor>Free</label>\n * </skra-field>\n * <skra-field>\n * <input type=\"radio\" skraRadio name=\"plan\" value=\"pro\"\n * [checked]=\"plan() === 'pro'\" (change)=\"plan.set('pro')\" />\n * <label skraLabelFor>Pro</label>\n * </skra-field>\n * </fieldset>\n *\n * Toggle state (`checked`) is NOT reflected as `data-skra-*` — slice CSS uses\n * the native `:checked` pseudo-class. Browser handles `aria-checked` natively.\n */\n@Component({\n selector: 'input[type=\"radio\"][skraRadio]',\n standalone: true,\n // Void host — no <ng-content>. See ADR 040 §2.\n template: '',\n styleUrl: './radio.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n '[attr.data-skra-disabled]': 'context?.isDisabled() ? \"\" : null',\n '[attr.aria-invalid]': 'context?.hasError() ? \"true\" : null',\n // Readonly is ARIA-only (native `readonly` doesn't apply to radios per HTML\n // spec). No `data-skra-readonly` — dead surface (styling keys off\n // `[aria-readonly]`), removed 2026-05-31 to match select's Q-3 ARIA-only\n // pattern (Batch 5 cleanroom review).\n '[attr.aria-readonly]': 'context?.isReadonly() ? \"true\" : null',\n '[attr.aria-describedby]': 'context?.describedByIds() ?? null',\n '(click)': 'blockWhenReadonly($event)',\n '(keydown)': 'blockKeyWhenReadonly($event)',\n },\n})\nexport class SkraRadio {\n private readonly el = inject<ElementRef<HTMLInputElement>>(ElementRef);\n readonly context = inject(SkraFieldContext, { optional: true });\n\n constructor() {\n if (this.context) {\n const ctx = this.context;\n const el = this.el.nativeElement;\n effect(() => {\n el.id = ctx.fieldId();\n });\n effect(() => {\n el.disabled = ctx.isDisabled();\n });\n }\n }\n\n /**\n * Readonly enforcement — native radio has no `[readonly]`, so without this the\n * `aria-readonly` we announce would be a lie (the user could still change the\n * selection). `preventDefault` on `click` blocks the selection change from both\n * pointer and Space/Enter (keyboard activation dispatches a click on form\n * controls). Mirrors the `<select skraSelect>` readonly guard.\n */\n protected blockWhenReadonly(event: Event): void {\n if (this.context?.isReadonly()) event.preventDefault();\n }\n\n /**\n * Arrow keys move selection between radios in a `name` group WITHOUT a click,\n * so they need a separate guard. Block every key except `Tab` (focus traversal)\n * when readonly.\n */\n protected blockKeyWhenReadonly(event: KeyboardEvent): void {\n if (this.context?.isReadonly() && event.key !== 'Tab') {\n event.preventDefault();\n }\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BG;MAsBU,SAAS,CAAA;AACH,IAAA,EAAE,GAAG,MAAM,CAA+B,UAAU,CAAC;IAC7D,OAAO,GAAG,MAAM,CAAC,gBAAgB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAE/D,IAAA,WAAA,GAAA;AACE,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE;AAChB,YAAA,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO;AACxB,YAAA,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC,aAAa;YAChC,MAAM,CAAC,MAAK;AACV,gBAAA,EAAE,CAAC,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE;AACvB,YAAA,CAAC,CAAC;YACF,MAAM,CAAC,MAAK;AACV,gBAAA,EAAE,CAAC,QAAQ,GAAG,GAAG,CAAC,UAAU,EAAE;AAChC,YAAA,CAAC,CAAC;QACJ;IACF;AAEA;;;;;;AAMG;AACO,IAAA,iBAAiB,CAAC,KAAY,EAAA;AACtC,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE;YAAE,KAAK,CAAC,cAAc,EAAE;IACxD;AAEA;;;;AAIG;AACO,IAAA,oBAAoB,CAAC,KAAoB,EAAA;AACjD,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,EAAE;YACrD,KAAK,CAAC,cAAc,EAAE;QACxB;IACF;uGArCW,SAAS,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAT,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,SAAS,qdAjBV,EAAE,EAAA,QAAA,EAAA,IAAA,EAAA,MAAA,EAAA,CAAA,mrEAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;2FAiBD,SAAS,EAAA,UAAA,EAAA,CAAA;kBArBrB,SAAS;AACE,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,gCAAgC,EAAA,UAAA,EAC9B,IAAI,EAAA,QAAA,EAEN,EAAE,EAAA,eAAA,EAEK,uBAAuB,CAAC,MAAM,EAAA,aAAA,EAChC,iBAAiB,CAAC,QAAQ,EAAA,IAAA,EACnC;AACJ,wBAAA,2BAA2B,EAAE,mCAAmC;AAChE,wBAAA,qBAAqB,EAAE,qCAAqC;;;;;AAK5D,wBAAA,sBAAsB,EAAE,uCAAuC;AAC/D,wBAAA,yBAAyB,EAAE,mCAAmC;AAC9D,wBAAA,SAAS,EAAE,2BAA2B;AACtC,wBAAA,WAAW,EAAE,8BAA8B;AAC5C,qBAAA,EAAA,MAAA,EAAA,CAAA,mrEAAA,CAAA,EAAA;;;ACxDH;;AAEG;;;;"}