@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,81 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, computed, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
+
4
+ /**
5
+ * `<skra-rating-display>` — a static, non-interactive **fractional** rating display
6
+ * ("4.3 out of 5 stars"). The read-only counterpart to `skra-star-rating` (the
7
+ * interactive whole-star INPUT in `libs/ui/forms`); a SEPARATE atom by atom-granularity
8
+ * (opposite a11y contract `role="img"` vs `radiogroup`; float vs int; no interaction).
9
+ * Third resident of `libs/ui/data-display`, beside Avatar + StatusDot. See
10
+ * `adr/specs/component-rating-display-spec.md`.
11
+ *
12
+ * **Element, not a decorated native (ADR 012):** there is no native rating element, like
13
+ * StatusDot/Avatar. NOT a Web Component (ADR 000).
14
+ *
15
+ * **Value consumer-owned (ADR 037):** `value` is a plain `input<number>()` (float). The atom
16
+ * owns no value, no `ControlValueAccessor`, no `@angular/forms`.
17
+ *
18
+ * **Fractional fill is CSP-clean (ADR 021):** the fill is QUANTIZED to a `data-skra-fill`
19
+ * bucket reflected on the host (`{0,5,…,100}`); the SCSS maps each bucket to the fill-overlay
20
+ * width. Zero `[style.--var]`, zero inline style — a bounded value is quantizable, so no
21
+ * geometry exception is taken. State Surface all-N/A (non-focusable display; Badge/StatusDot
22
+ * precedent).
23
+ *
24
+ * **Accessible name (ADR 033 Path 3 — the StatusDot/Avatar contract):** the host carries NO
25
+ * default ARIA. The consumer declares semantics:
26
+ * - decorative (Mode A, beside a visible value like "4.3") → `aria-hidden="true"`;
27
+ * - meaningful (Mode B, the stars are the sole carrier) → `role="img"` + `aria-label`
28
+ * TOGETHER (e.g. `aria-label="Rated 4.3 out of 5"`).
29
+ * The inner star glyphs are always decorative. NO `ariaLabel` input (boundary #8) — the label
30
+ * is consumer-owned + localizable (the agnostic doctrine; Forge does not hard-code "out of N").
31
+ *
32
+ * **Look is skin-owned:** glyph colours + sizes come from the shared family surface
33
+ * (`comp/star-rating.json`). NO `intent`/`emphasis`/`color`/`variant` look-inputs.
34
+ */
35
+ class SkraRatingDisplay {
36
+ /** The rating to display — FLOAT (e.g. 4.3). Consumer-owned (ADR 037); clamped to [0, max]. */
37
+ value = input(0, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
38
+ /** Number of stars (semantic count). */
39
+ max = input(5, ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
40
+ /** ADR 035 size axis — the only variant axis. */
41
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
42
+ /** Star slots [1..max] — both layers render the same count. */
43
+ stars = computed(() => {
44
+ const n = Math.max(0, Math.floor(this.max()));
45
+ return Array.from({ length: n }, (_, i) => i + 1);
46
+ }, ...(ngDevMode ? [{ debugName: "stars" }] : /* istanbul ignore next */ []));
47
+ /**
48
+ * Fill percentage quantized to 5% buckets (`{0,5,…,100}`) — reflected as `data-skra-fill`,
49
+ * mapped to the overlay width by the SCSS. 4.3/5 → 86% → bucket 85 (visually exact-enough).
50
+ * A bounded, quantizable value is exactly what ADR 021 reflection is for — no style exception.
51
+ */
52
+ fillBucket = computed(() => {
53
+ const max = this.max();
54
+ if (max <= 0)
55
+ return 0;
56
+ const ratio = this.value() / max;
57
+ const clamped = Math.min(1, Math.max(0, ratio));
58
+ return Math.round(clamped * 20) * 5;
59
+ }, ...(ngDevMode ? [{ debugName: "fillBucket" }] : /* istanbul ignore next */ []));
60
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraRatingDisplay, deps: [], target: i0.ɵɵFactoryTarget.Component });
61
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: SkraRatingDisplay, isStandalone: true, selector: "skra-rating-display", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-skra-fill": "fillBucket()", "attr.data-skra-size": "size()" } }, ngImport: i0, template: "<!--\n Two stacked glyph layers: an empty-stars track that defines the box, and a filled-stars\n overlay clipped to the quantized fill width (the SCSS maps data-skra-fill \u2192 inline-size).\n Both layers are decorative (aria-hidden); the accessible name, when any, is on the host\n (consumer-applied role=\"img\" + aria-label \u2014 ADR 033 Path 3).\n-->\n<div class=\"skra-rating-display-track\" aria-hidden=\"true\">\n @for (star of stars(); track star) {\n <svg\n class=\"skra-rating-display-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\"\n />\n </svg>\n }\n</div>\n<div class=\"skra-rating-display-fill\" aria-hidden=\"true\">\n @for (star of stars(); track star) {\n <svg\n class=\"skra-rating-display-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\"\n />\n </svg>\n }\n</div>\n", styles: [":host{margin:0;position:relative;display:inline-flex;line-height:0;--_star-size: var(--skra-comp-star-rating-star-size-md)}:host([data-skra-size=sm]){--_star-size: var(--skra-comp-star-rating-star-size-sm)}:host([data-skra-size=lg]){--_star-size: var(--skra-comp-star-rating-star-size-lg)}.skra-rating-display-track,.skra-rating-display-fill{display:flex;gap:0;line-height:0}.skra-rating-display-fill{position:absolute;inset-block-start:0;inset-inline-start:0;block-size:100%;inline-size:0;overflow:hidden}.skra-rating-display-track .skra-rating-display-glyph{fill:var(--skra-comp-star-rating-star-color-empty)}.skra-rating-display-fill .skra-rating-display-glyph{fill:var(--skra-comp-star-rating-star-color-filled)}.skra-rating-display-glyph{flex:0 0 auto;inline-size:var(--_star-size);block-size:var(--_star-size)}:host([data-skra-fill=\"0\"]) .skra-rating-display-fill{inline-size:0%}:host([data-skra-fill=\"5\"]) .skra-rating-display-fill{inline-size:5%}:host([data-skra-fill=\"10\"]) .skra-rating-display-fill{inline-size:10%}:host([data-skra-fill=\"15\"]) .skra-rating-display-fill{inline-size:15%}:host([data-skra-fill=\"20\"]) .skra-rating-display-fill{inline-size:20%}:host([data-skra-fill=\"25\"]) .skra-rating-display-fill{inline-size:25%}:host([data-skra-fill=\"30\"]) .skra-rating-display-fill{inline-size:30%}:host([data-skra-fill=\"35\"]) .skra-rating-display-fill{inline-size:35%}:host([data-skra-fill=\"40\"]) .skra-rating-display-fill{inline-size:40%}:host([data-skra-fill=\"45\"]) .skra-rating-display-fill{inline-size:45%}:host([data-skra-fill=\"50\"]) .skra-rating-display-fill{inline-size:50%}:host([data-skra-fill=\"55\"]) .skra-rating-display-fill{inline-size:55%}:host([data-skra-fill=\"60\"]) .skra-rating-display-fill{inline-size:60%}:host([data-skra-fill=\"65\"]) .skra-rating-display-fill{inline-size:65%}:host([data-skra-fill=\"70\"]) .skra-rating-display-fill{inline-size:70%}:host([data-skra-fill=\"75\"]) .skra-rating-display-fill{inline-size:75%}:host([data-skra-fill=\"80\"]) .skra-rating-display-fill{inline-size:80%}:host([data-skra-fill=\"85\"]) .skra-rating-display-fill{inline-size:85%}:host([data-skra-fill=\"90\"]) .skra-rating-display-fill{inline-size:90%}:host([data-skra-fill=\"95\"]) .skra-rating-display-fill{inline-size:95%}:host([data-skra-fill=\"100\"]) .skra-rating-display-fill{inline-size:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
62
+ }
63
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraRatingDisplay, decorators: [{
64
+ type: Component,
65
+ args: [{ selector: 'skra-rating-display', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, host: {
66
+ // Quantized fractional fill (ADR 021 — data-attr, never [style.--var]).
67
+ '[attr.data-skra-fill]': 'fillBucket()',
68
+ // Variant Surface size axis (ADR 035) — reuses the family star-size-* tokens.
69
+ '[attr.data-skra-size]': 'size()',
70
+ }, template: "<!--\n Two stacked glyph layers: an empty-stars track that defines the box, and a filled-stars\n overlay clipped to the quantized fill width (the SCSS maps data-skra-fill \u2192 inline-size).\n Both layers are decorative (aria-hidden); the accessible name, when any, is on the host\n (consumer-applied role=\"img\" + aria-label \u2014 ADR 033 Path 3).\n-->\n<div class=\"skra-rating-display-track\" aria-hidden=\"true\">\n @for (star of stars(); track star) {\n <svg\n class=\"skra-rating-display-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\"\n />\n </svg>\n }\n</div>\n<div class=\"skra-rating-display-fill\" aria-hidden=\"true\">\n @for (star of stars(); track star) {\n <svg\n class=\"skra-rating-display-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\"\n />\n </svg>\n }\n</div>\n", styles: [":host{margin:0;position:relative;display:inline-flex;line-height:0;--_star-size: var(--skra-comp-star-rating-star-size-md)}:host([data-skra-size=sm]){--_star-size: var(--skra-comp-star-rating-star-size-sm)}:host([data-skra-size=lg]){--_star-size: var(--skra-comp-star-rating-star-size-lg)}.skra-rating-display-track,.skra-rating-display-fill{display:flex;gap:0;line-height:0}.skra-rating-display-fill{position:absolute;inset-block-start:0;inset-inline-start:0;block-size:100%;inline-size:0;overflow:hidden}.skra-rating-display-track .skra-rating-display-glyph{fill:var(--skra-comp-star-rating-star-color-empty)}.skra-rating-display-fill .skra-rating-display-glyph{fill:var(--skra-comp-star-rating-star-color-filled)}.skra-rating-display-glyph{flex:0 0 auto;inline-size:var(--_star-size);block-size:var(--_star-size)}:host([data-skra-fill=\"0\"]) .skra-rating-display-fill{inline-size:0%}:host([data-skra-fill=\"5\"]) .skra-rating-display-fill{inline-size:5%}:host([data-skra-fill=\"10\"]) .skra-rating-display-fill{inline-size:10%}:host([data-skra-fill=\"15\"]) .skra-rating-display-fill{inline-size:15%}:host([data-skra-fill=\"20\"]) .skra-rating-display-fill{inline-size:20%}:host([data-skra-fill=\"25\"]) .skra-rating-display-fill{inline-size:25%}:host([data-skra-fill=\"30\"]) .skra-rating-display-fill{inline-size:30%}:host([data-skra-fill=\"35\"]) .skra-rating-display-fill{inline-size:35%}:host([data-skra-fill=\"40\"]) .skra-rating-display-fill{inline-size:40%}:host([data-skra-fill=\"45\"]) .skra-rating-display-fill{inline-size:45%}:host([data-skra-fill=\"50\"]) .skra-rating-display-fill{inline-size:50%}:host([data-skra-fill=\"55\"]) .skra-rating-display-fill{inline-size:55%}:host([data-skra-fill=\"60\"]) .skra-rating-display-fill{inline-size:60%}:host([data-skra-fill=\"65\"]) .skra-rating-display-fill{inline-size:65%}:host([data-skra-fill=\"70\"]) .skra-rating-display-fill{inline-size:70%}:host([data-skra-fill=\"75\"]) .skra-rating-display-fill{inline-size:75%}:host([data-skra-fill=\"80\"]) .skra-rating-display-fill{inline-size:80%}:host([data-skra-fill=\"85\"]) .skra-rating-display-fill{inline-size:85%}:host([data-skra-fill=\"90\"]) .skra-rating-display-fill{inline-size:90%}:host([data-skra-fill=\"95\"]) .skra-rating-display-fill{inline-size:95%}:host([data-skra-fill=\"100\"]) .skra-rating-display-fill{inline-size:100%}\n"] }]
71
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
72
+
73
+ // Static fractional rating display — read-only counterpart to skra-star-rating
74
+ // (@skra/ui/star-rating); shares the family glyph surface (comp/star-rating.json).
75
+
76
+ /**
77
+ * Generated bundle index. Do not edit.
78
+ */
79
+
80
+ export { SkraRatingDisplay };
81
+ //# sourceMappingURL=skra-ui-rating-display.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skra-ui-rating-display.mjs","sources":["../../../../libs/ui/rating-display/src/lib/rating-display/rating-display.ts","../../../../libs/ui/rating-display/src/lib/rating-display/rating-display.html","../../../../libs/ui/rating-display/src/index.ts","../../../../libs/ui/rating-display/src/skra-ui-rating-display.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ViewEncapsulation,\n computed,\n input,\n} from '@angular/core';\n\nexport type RatingDisplaySize = 'sm' | 'md' | 'lg';\n\n/**\n * `<skra-rating-display>` — a static, non-interactive **fractional** rating display\n * (\"4.3 out of 5 stars\"). The read-only counterpart to `skra-star-rating` (the\n * interactive whole-star INPUT in `libs/ui/forms`); a SEPARATE atom by atom-granularity\n * (opposite a11y contract `role=\"img\"` vs `radiogroup`; float vs int; no interaction).\n * Third resident of `libs/ui/data-display`, beside Avatar + StatusDot. See\n * `adr/specs/component-rating-display-spec.md`.\n *\n * **Element, not a decorated native (ADR 012):** there is no native rating element, like\n * StatusDot/Avatar. NOT a Web Component (ADR 000).\n *\n * **Value consumer-owned (ADR 037):** `value` is a plain `input<number>()` (float). The atom\n * owns no value, no `ControlValueAccessor`, no `@angular/forms`.\n *\n * **Fractional fill is CSP-clean (ADR 021):** the fill is QUANTIZED to a `data-skra-fill`\n * bucket reflected on the host (`{0,5,…,100}`); the SCSS maps each bucket to the fill-overlay\n * width. Zero `[style.--var]`, zero inline style — a bounded value is quantizable, so no\n * geometry exception is taken. State Surface all-N/A (non-focusable display; Badge/StatusDot\n * precedent).\n *\n * **Accessible name (ADR 033 Path 3 — the StatusDot/Avatar contract):** the host carries NO\n * default ARIA. The consumer declares semantics:\n * - decorative (Mode A, beside a visible value like \"4.3\") → `aria-hidden=\"true\"`;\n * - meaningful (Mode B, the stars are the sole carrier) → `role=\"img\"` + `aria-label`\n * TOGETHER (e.g. `aria-label=\"Rated 4.3 out of 5\"`).\n * The inner star glyphs are always decorative. NO `ariaLabel` input (boundary #8) — the label\n * is consumer-owned + localizable (the agnostic doctrine; Forge does not hard-code \"out of N\").\n *\n * **Look is skin-owned:** glyph colours + sizes come from the shared family surface\n * (`comp/star-rating.json`). NO `intent`/`emphasis`/`color`/`variant` look-inputs.\n */\n@Component({\n selector: 'skra-rating-display',\n standalone: true,\n templateUrl: './rating-display.html',\n styleUrl: './rating-display.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n // Quantized fractional fill (ADR 021 — data-attr, never [style.--var]).\n '[attr.data-skra-fill]': 'fillBucket()',\n // Variant Surface size axis (ADR 035) — reuses the family star-size-* tokens.\n '[attr.data-skra-size]': 'size()',\n },\n})\nexport class SkraRatingDisplay {\n /** The rating to display — FLOAT (e.g. 4.3). Consumer-owned (ADR 037); clamped to [0, max]. */\n readonly value = input<number>(0);\n /** Number of stars (semantic count). */\n readonly max = input<number>(5);\n /** ADR 035 size axis — the only variant axis. */\n readonly size = input<RatingDisplaySize>('md');\n\n /** Star slots [1..max] — both layers render the same count. */\n readonly stars = computed(() => {\n const n = Math.max(0, Math.floor(this.max()));\n return Array.from({ length: n }, (_, i) => i + 1);\n });\n\n /**\n * Fill percentage quantized to 5% buckets (`{0,5,…,100}`) — reflected as `data-skra-fill`,\n * mapped to the overlay width by the SCSS. 4.3/5 → 86% → bucket 85 (visually exact-enough).\n * A bounded, quantizable value is exactly what ADR 021 reflection is for — no style exception.\n */\n readonly fillBucket = computed(() => {\n const max = this.max();\n if (max <= 0) return 0;\n const ratio = this.value() / max;\n const clamped = Math.min(1, Math.max(0, ratio));\n return Math.round(clamped * 20) * 5;\n });\n}\n","<!--\n Two stacked glyph layers: an empty-stars track that defines the box, and a filled-stars\n overlay clipped to the quantized fill width (the SCSS maps data-skra-fill → inline-size).\n Both layers are decorative (aria-hidden); the accessible name, when any, is on the host\n (consumer-applied role=\"img\" + aria-label — ADR 033 Path 3).\n-->\n<div class=\"skra-rating-display-track\" aria-hidden=\"true\">\n @for (star of stars(); track star) {\n <svg\n class=\"skra-rating-display-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\"\n />\n </svg>\n }\n</div>\n<div class=\"skra-rating-display-fill\" aria-hidden=\"true\">\n @for (star of stars(); track star) {\n <svg\n class=\"skra-rating-display-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\"\n />\n </svg>\n }\n</div>\n","// Static fractional rating display — read-only counterpart to skra-star-rating\n// (@skra/ui/star-rating); shares the family glyph surface (comp/star-rating.json).\nexport * from './lib/rating-display/rating-display';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BG;MAeU,iBAAiB,CAAA;;AAEnB,IAAA,KAAK,GAAG,KAAK,CAAS,CAAC,4EAAC;;AAExB,IAAA,GAAG,GAAG,KAAK,CAAS,CAAC,0EAAC;;AAEtB,IAAA,IAAI,GAAG,KAAK,CAAoB,IAAI,2EAAC;;AAGrC,IAAA,KAAK,GAAG,QAAQ,CAAC,MAAK;AAC7B,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAC7C,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACnD,IAAA,CAAC,4EAAC;AAEF;;;;AAIG;AACM,IAAA,UAAU,GAAG,QAAQ,CAAC,MAAK;AAClC,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;QACtB,IAAI,GAAG,IAAI,CAAC;AAAE,YAAA,OAAO,CAAC;QACtB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,GAAG;AAChC,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,GAAG,CAAC;AACrC,IAAA,CAAC,iFAAC;uGAzBS,iBAAiB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAjB,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,iBAAiB,4hBCvD9B,mmCAkCA,EAAA,MAAA,EAAA,CAAA,+yEAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;2FDqBa,iBAAiB,EAAA,UAAA,EAAA,CAAA;kBAd7B,SAAS;+BACE,qBAAqB,EAAA,UAAA,EACnB,IAAI,EAAA,eAAA,EAGC,uBAAuB,CAAC,MAAM,EAAA,aAAA,EAChC,iBAAiB,CAAC,QAAQ,EAAA,IAAA,EACnC;;AAEJ,wBAAA,uBAAuB,EAAE,cAAc;;AAEvC,wBAAA,uBAAuB,EAAE,QAAQ;AAClC,qBAAA,EAAA,QAAA,EAAA,mmCAAA,EAAA,MAAA,EAAA,CAAA,+yEAAA,CAAA,EAAA;;;AErDH;AACA;;ACDA;;AAEG;;;;"}
@@ -0,0 +1,151 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { input, output, computed, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
4
+ import { _SkraSelectBase, SKRA_LISTBOX_PANEL } from '@skra/ui/core';
5
+ export { SkraOption, SkraOptionDetail, SkraOptionLeading, SkraSelectTrigger } from '@skra/ui/core';
6
+
7
+ /**
8
+ * SkraSelect — a custom single-select listbox: the WAI-ARIA APG **Select-Only Combobox** pattern. A
9
+ * value-display **trigger** (`role="combobox"`) opens a top-layer **listbox** panel (Popover API,
10
+ * `popover="auto"`, ADR 0044) of projected `skra-option` children. The thin single-select concrete
11
+ * over `_SkraSelectBase` (ADR 0046 — cardinality split): it adds the honest `value: T` channel + the
12
+ * replace-and-close selection model; everything else (overlay/keyboard/field substrate) lives in the
13
+ * shared base, shared verbatim with `skra-multi-select`.
14
+ *
15
+ * - **Value (ADR 037):** `value` is a consumer-owned `input<T>()` + `valueChange` output — NO
16
+ * `@angular/forms`, no CVA. Selection is signal-derived; equality is a strategy seam (`Object.is`
17
+ * default + opt-in `compareWith`).
18
+ * - **Trigger content (ADR 0045):** default = the selected option's `getLabel()` text (emoji/glyph
19
+ * leading free); rich non-text triggers opt in via `<ng-template skraSelectTrigger>`.
20
+ *
21
+ * Full contract: adr/specs/component-select-spec.md.
22
+ */
23
+ class SkraSelect extends _SkraSelectBase {
24
+ /** Consumer-owned value channel (ADR 037). */
25
+ value = input(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
26
+ /** Fires ONLY on user selection — never on a programmatic `value()` change (ADR 037). */
27
+ valueChange = output();
28
+ /** Cardinality hint (ADR 0046) — single-select. */
29
+ multiple = false;
30
+ /** The option whose value matches `value()` (ADR 037 selection model) — feeds the trigger. */
31
+ selectedOption = computed(() => this.options().find((o) => this.isSelected(o.value())) ?? null, ...(ngDevMode ? [{ debugName: "selectedOption" }] : /* istanbul ignore next */ []));
32
+ triggerContext = computed(() => ({
33
+ $implicit: this.value(),
34
+ option: this.selectedOption(),
35
+ }), ...(ngDevMode ? [{ debugName: "triggerContext" }] : /* istanbul ignore next */ []));
36
+ // ── SkraListboxPanel seam ──
37
+ isSelected(value) {
38
+ const current = this.value();
39
+ return current !== undefined && this.compareWith()(current, value);
40
+ }
41
+ selectValue(value) {
42
+ this.valueChange.emit(value);
43
+ this.close(); // single-select: pick + close
44
+ }
45
+ activeSeedItem() {
46
+ return this.selectedOption();
47
+ }
48
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraSelect, deps: null, target: i0.ɵɵFactoryTarget.Component });
49
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: SkraSelect, isStandalone: true, selector: "skra-select", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, providers: [{ provide: SKRA_LISTBOX_PANEL, useExisting: SkraSelect }], exportAs: ["skraSelect"], usesInheritance: true, ngImport: i0, template: `
50
+ <button
51
+ #trigger
52
+ class="skra-select-trigger"
53
+ type="button"
54
+ role="combobox"
55
+ aria-haspopup="listbox"
56
+ [attr.id]="triggerId()"
57
+ [attr.popovertarget]="panelId"
58
+ [attr.aria-controls]="panelId"
59
+ [attr.aria-expanded]="isOpen()"
60
+ [attr.aria-activedescendant]="activeOptionId()"
61
+ [attr.aria-describedby]="describedBy()"
62
+ [attr.aria-invalid]="hasError() ? 'true' : null"
63
+ [attr.aria-required]="isRequired() ? 'true' : null"
64
+ [attr.aria-readonly]="isReadonly() ? 'true' : null"
65
+ [disabled]="isDisabled()"
66
+ (click)="onTriggerClick($event)"
67
+ (keydown)="onTriggerKeydown($event)"
68
+ >
69
+ <span class="skra-select-trigger-value">
70
+ @if (triggerTemplate(); as tpl) {
71
+ <ng-container
72
+ [ngTemplateOutlet]="tpl"
73
+ [ngTemplateOutletContext]="triggerContext()"
74
+ ></ng-container>
75
+ } @else if (selectedOption(); as opt) {
76
+ {{ opt.getLabel() }}
77
+ } @else {
78
+ <span class="skra-select-placeholder">{{ placeholder() }}</span>
79
+ }
80
+ </span>
81
+ <span class="skra-select-arrow" aria-hidden="true"></span>
82
+ </button>
83
+
84
+ <div
85
+ #panel
86
+ class="skra-select-listbox"
87
+ [id]="panelId"
88
+ role="listbox"
89
+ popover="auto"
90
+ (toggle)="onPanelToggle($event)"
91
+ >
92
+ <ng-content></ng-content>
93
+ </div>
94
+ `, isInline: true, styles: ["@charset \"UTF-8\";:host{display:block;box-sizing:border-box;margin:0}.skra-select-trigger{display:flex;align-items:center;gap:var(--skra-comp-select-gap);inline-size:100%;min-block-size:var(--skra-comp-select-min-block-size);box-sizing:border-box;margin:0;padding-block:var(--skra-comp-select-padding-block);padding-inline:var(--skra-comp-select-padding-inline);text-align:start;cursor:pointer;background:var(--skra-comp-select-background);color:var(--skra-comp-select-foreground);border-color:var(--skra-comp-select-border-color);border-width:var(--skra-comp-select-border-width);border-style:var(--skra-comp-select-border-style);border-radius:var(--skra-comp-select-radius);box-shadow:var(--skra-comp-select-shadow);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),border-color var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing)}.skra-select-trigger:hover:not(:disabled):not([aria-readonly=true]){background:var(--skra-comp-select-background-hover)}.skra-select-trigger:focus-visible{outline:var(--skra-comp-select-focus-ring-width) solid var(--skra-comp-select-focus-ring-color);outline-offset:.125rem}.skra-select-trigger:disabled{background:var(--skra-comp-select-background-disabled);color:var(--skra-comp-select-foreground-disabled);cursor:not-allowed}.skra-select-trigger[aria-readonly=true]{background:var(--skra-comp-select-background-readonly);cursor:default}.skra-select-trigger[aria-invalid=true]{border-color:var(--skra-comp-select-border-color-validation-invalid)}.skra-select-trigger-value{flex:1 1 auto;min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;--skra-comp-icon-size: var(--skra-comp-select-icon-size)}.skra-select-placeholder{color:var(--skra-comp-select-foreground-disabled)}.skra-select-arrow{flex:none;inline-size:var(--skra-comp-select-arrow-icon-size);block-size:var(--skra-comp-select-arrow-icon-size);background-color:var(--skra-comp-select-arrow-color);mask:url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M6%209l6%206%206-6'%20fill='none'%20stroke='%23000'%20stroke-width='2'/%3E%3C/svg%3E\") no-repeat center/contain;-webkit-mask:url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M6%209l6%206%206-6'%20fill='none'%20stroke='%23000'%20stroke-width='2'/%3E%3C/svg%3E\") no-repeat center/contain;transition:transform var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing)}.skra-select-trigger[aria-expanded=true] .skra-select-arrow{transform:rotate(180deg)}.skra-select-listbox{position:fixed;margin:0;inset:auto;box-sizing:border-box;padding-block:var(--skra-comp-select-panel-padding-block);padding-inline:var(--skra-comp-select-panel-padding-inline);background:var(--skra-comp-select-panel-background);color:var(--skra-comp-select-panel-foreground);border:var(--skra-comp-select-border-width) solid var(--skra-comp-select-panel-border-color);border-radius:var(--skra-comp-select-panel-radius);box-shadow:var(--skra-comp-select-panel-shadow);inline-size:max-content;max-block-size:var(--skra-comp-select-panel-max-block-size);overflow-y:auto;flex-direction:column;opacity:0;transition:opacity var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing),display var(--skra-comp-select-motion-duration) allow-discrete,overlay var(--skra-comp-select-motion-duration) allow-discrete}.skra-select-listbox:popover-open{display:flex;opacity:1}@starting-style{.skra-select-listbox:popover-open{opacity:0}}@media(prefers-reduced-motion:reduce){.skra-select-trigger,.skra-select-arrow,.skra-select-listbox{transition-duration:0ms}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
95
+ }
96
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraSelect, decorators: [{
97
+ type: Component,
98
+ args: [{ selector: 'skra-select', standalone: true, exportAs: 'skraSelect', imports: [NgTemplateOutlet], template: `
99
+ <button
100
+ #trigger
101
+ class="skra-select-trigger"
102
+ type="button"
103
+ role="combobox"
104
+ aria-haspopup="listbox"
105
+ [attr.id]="triggerId()"
106
+ [attr.popovertarget]="panelId"
107
+ [attr.aria-controls]="panelId"
108
+ [attr.aria-expanded]="isOpen()"
109
+ [attr.aria-activedescendant]="activeOptionId()"
110
+ [attr.aria-describedby]="describedBy()"
111
+ [attr.aria-invalid]="hasError() ? 'true' : null"
112
+ [attr.aria-required]="isRequired() ? 'true' : null"
113
+ [attr.aria-readonly]="isReadonly() ? 'true' : null"
114
+ [disabled]="isDisabled()"
115
+ (click)="onTriggerClick($event)"
116
+ (keydown)="onTriggerKeydown($event)"
117
+ >
118
+ <span class="skra-select-trigger-value">
119
+ @if (triggerTemplate(); as tpl) {
120
+ <ng-container
121
+ [ngTemplateOutlet]="tpl"
122
+ [ngTemplateOutletContext]="triggerContext()"
123
+ ></ng-container>
124
+ } @else if (selectedOption(); as opt) {
125
+ {{ opt.getLabel() }}
126
+ } @else {
127
+ <span class="skra-select-placeholder">{{ placeholder() }}</span>
128
+ }
129
+ </span>
130
+ <span class="skra-select-arrow" aria-hidden="true"></span>
131
+ </button>
132
+
133
+ <div
134
+ #panel
135
+ class="skra-select-listbox"
136
+ [id]="panelId"
137
+ role="listbox"
138
+ popover="auto"
139
+ (toggle)="onPanelToggle($event)"
140
+ >
141
+ <ng-content></ng-content>
142
+ </div>
143
+ `, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, providers: [{ provide: SKRA_LISTBOX_PANEL, useExisting: SkraSelect }], styles: ["@charset \"UTF-8\";:host{display:block;box-sizing:border-box;margin:0}.skra-select-trigger{display:flex;align-items:center;gap:var(--skra-comp-select-gap);inline-size:100%;min-block-size:var(--skra-comp-select-min-block-size);box-sizing:border-box;margin:0;padding-block:var(--skra-comp-select-padding-block);padding-inline:var(--skra-comp-select-padding-inline);text-align:start;cursor:pointer;background:var(--skra-comp-select-background);color:var(--skra-comp-select-foreground);border-color:var(--skra-comp-select-border-color);border-width:var(--skra-comp-select-border-width);border-style:var(--skra-comp-select-border-style);border-radius:var(--skra-comp-select-radius);box-shadow:var(--skra-comp-select-shadow);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),border-color var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing)}.skra-select-trigger:hover:not(:disabled):not([aria-readonly=true]){background:var(--skra-comp-select-background-hover)}.skra-select-trigger:focus-visible{outline:var(--skra-comp-select-focus-ring-width) solid var(--skra-comp-select-focus-ring-color);outline-offset:.125rem}.skra-select-trigger:disabled{background:var(--skra-comp-select-background-disabled);color:var(--skra-comp-select-foreground-disabled);cursor:not-allowed}.skra-select-trigger[aria-readonly=true]{background:var(--skra-comp-select-background-readonly);cursor:default}.skra-select-trigger[aria-invalid=true]{border-color:var(--skra-comp-select-border-color-validation-invalid)}.skra-select-trigger-value{flex:1 1 auto;min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;--skra-comp-icon-size: var(--skra-comp-select-icon-size)}.skra-select-placeholder{color:var(--skra-comp-select-foreground-disabled)}.skra-select-arrow{flex:none;inline-size:var(--skra-comp-select-arrow-icon-size);block-size:var(--skra-comp-select-arrow-icon-size);background-color:var(--skra-comp-select-arrow-color);mask:url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M6%209l6%206%206-6'%20fill='none'%20stroke='%23000'%20stroke-width='2'/%3E%3C/svg%3E\") no-repeat center/contain;-webkit-mask:url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20d='M6%209l6%206%206-6'%20fill='none'%20stroke='%23000'%20stroke-width='2'/%3E%3C/svg%3E\") no-repeat center/contain;transition:transform var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing)}.skra-select-trigger[aria-expanded=true] .skra-select-arrow{transform:rotate(180deg)}.skra-select-listbox{position:fixed;margin:0;inset:auto;box-sizing:border-box;padding-block:var(--skra-comp-select-panel-padding-block);padding-inline:var(--skra-comp-select-panel-padding-inline);background:var(--skra-comp-select-panel-background);color:var(--skra-comp-select-panel-foreground);border:var(--skra-comp-select-border-width) solid var(--skra-comp-select-panel-border-color);border-radius:var(--skra-comp-select-panel-radius);box-shadow:var(--skra-comp-select-panel-shadow);inline-size:max-content;max-block-size:var(--skra-comp-select-panel-max-block-size);overflow-y:auto;flex-direction:column;opacity:0;transition:opacity var(--skra-comp-select-motion-duration) var(--skra-comp-select-motion-easing),display var(--skra-comp-select-motion-duration) allow-discrete,overlay var(--skra-comp-select-motion-duration) allow-discrete}.skra-select-listbox:popover-open{display:flex;opacity:1}@starting-style{.skra-select-listbox:popover-open{opacity:0}}@media(prefers-reduced-motion:reduce){.skra-select-trigger,.skra-select-arrow,.skra-select-listbox{transition-duration:0ms}}\n"] }]
144
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
145
+
146
+ /**
147
+ * Generated bundle index. Do not edit.
148
+ */
149
+
150
+ export { SkraSelect };
151
+ //# sourceMappingURL=skra-ui-select.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skra-ui-select.mjs","sources":["../../../../libs/ui/select/src/lib/select/select.ts","../../../../libs/ui/select/src/skra-ui-select.ts"],"sourcesContent":["import { NgTemplateOutlet } from '@angular/common';\nimport {\n ChangeDetectionStrategy,\n Component,\n ViewEncapsulation,\n computed,\n input,\n output,\n} from '@angular/core';\n\nimport { _SkraSelectBase } from '@skra/ui/core';\nimport {\n SKRA_LISTBOX_PANEL,\n type SkraListboxKeyboardItem,\n} from '@skra/ui/core';\n\n/**\n * SkraSelect — a custom single-select listbox: the WAI-ARIA APG **Select-Only Combobox** pattern. A\n * value-display **trigger** (`role=\"combobox\"`) opens a top-layer **listbox** panel (Popover API,\n * `popover=\"auto\"`, ADR 0044) of projected `skra-option` children. The thin single-select concrete\n * over `_SkraSelectBase` (ADR 0046 — cardinality split): it adds the honest `value: T` channel + the\n * replace-and-close selection model; everything else (overlay/keyboard/field substrate) lives in the\n * shared base, shared verbatim with `skra-multi-select`.\n *\n * - **Value (ADR 037):** `value` is a consumer-owned `input<T>()` + `valueChange` output — NO\n * `@angular/forms`, no CVA. Selection is signal-derived; equality is a strategy seam (`Object.is`\n * default + opt-in `compareWith`).\n * - **Trigger content (ADR 0045):** default = the selected option's `getLabel()` text (emoji/glyph\n * leading free); rich non-text triggers opt in via `<ng-template skraSelectTrigger>`.\n *\n * Full contract: adr/specs/component-select-spec.md.\n */\n@Component({\n selector: 'skra-select',\n standalone: true,\n exportAs: 'skraSelect',\n imports: [NgTemplateOutlet],\n template: `\n <button\n #trigger\n class=\"skra-select-trigger\"\n type=\"button\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n [attr.id]=\"triggerId()\"\n [attr.popovertarget]=\"panelId\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"hasError() ? 'true' : null\"\n [attr.aria-required]=\"isRequired() ? 'true' : null\"\n [attr.aria-readonly]=\"isReadonly() ? 'true' : null\"\n [disabled]=\"isDisabled()\"\n (click)=\"onTriggerClick($event)\"\n (keydown)=\"onTriggerKeydown($event)\"\n >\n <span class=\"skra-select-trigger-value\">\n @if (triggerTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"triggerContext()\"\n ></ng-container>\n } @else if (selectedOption(); as opt) {\n {{ opt.getLabel() }}\n } @else {\n <span class=\"skra-select-placeholder\">{{ placeholder() }}</span>\n }\n </span>\n <span class=\"skra-select-arrow\" aria-hidden=\"true\"></span>\n </button>\n\n <div\n #panel\n class=\"skra-select-listbox\"\n [id]=\"panelId\"\n role=\"listbox\"\n popover=\"auto\"\n (toggle)=\"onPanelToggle($event)\"\n >\n <ng-content></ng-content>\n </div>\n `,\n styleUrl: './select.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n providers: [{ provide: SKRA_LISTBOX_PANEL, useExisting: SkraSelect }],\n})\nexport class SkraSelect<T = unknown> extends _SkraSelectBase<T> {\n /** Consumer-owned value channel (ADR 037). */\n readonly value = input<T>();\n /** Fires ONLY on user selection — never on a programmatic `value()` change (ADR 037). */\n readonly valueChange = output<T>();\n /** Cardinality hint (ADR 0046) — single-select. */\n readonly multiple = false;\n\n /** The option whose value matches `value()` (ADR 037 selection model) — feeds the trigger. */\n protected readonly selectedOption = computed(\n () => this.options().find((o) => this.isSelected(o.value())) ?? null,\n );\n\n protected readonly triggerContext = computed(() => ({\n $implicit: this.value(),\n option: this.selectedOption(),\n }));\n\n // ── SkraListboxPanel seam ──\n isSelected(value: unknown): boolean {\n const current = this.value();\n return current !== undefined && this.compareWith()(current, value as T);\n }\n\n selectValue(value: unknown): void {\n this.valueChange.emit(value as T);\n this.close(); // single-select: pick + close\n }\n\n protected activeSeedItem(): SkraListboxKeyboardItem | null {\n return this.selectedOption();\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,65 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, computed, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
+
4
+ /**
5
+ * `<skra-skeleton>` — a decorative content placeholder (Wave 2, `libs/ui/indicators`).
6
+ * Occupies the place of not-yet-loaded content with an animated, layout-shaped placeholder so
7
+ * the layout does not shift on load (CLS) and the user perceives that content is coming.
8
+ *
9
+ * Third resident of the loading family, with a distinct SRP: `[skraLoadingIndicator]` (ADR 028)
10
+ * is the *indeterminate* "host is busy" spinner; `SkraProgress` is the *determinate* "task is
11
+ * N% complete" bar; **SkraSkeleton is the *content placeholder*** ("the content hasn't arrived —
12
+ * here is its shape"). See `adr/specs/component-skeleton-spec.md` § SRP boundary.
13
+ *
14
+ * **A custom element, NOT a Component-on-native-host** (ADR 040): a skeleton has no semantic
15
+ * native equivalent and is purely presentational, so it follows the custom-element atom model
16
+ * (Badge/Chip/Alert precedent). The host is a layout container that renders its own inner
17
+ * `.skra-skeleton__bar` element(s) — which is why a void-host directive was rejected: the
18
+ * multi-line `text` shape needs internal structure.
19
+ *
20
+ * **Decorative a11y (ADR 033):** the host is `aria-hidden="true"` — a skeleton conveys no
21
+ * information to assistive tech (the decorative-icon exemption). The "loading" announcement
22
+ * belongs to the consumer's content **container** (`aria-busy="true"`), never to the skeleton.
23
+ *
24
+ * **Sizing is container/`em`-driven (ADR 021 — the CSP reason there are no `width`/`height`
25
+ * inputs):** `text` line bars are `em`-relative (they inherit the surrounding font-size) at full
26
+ * container width; `block`/`circle` fill a host the **consumer** sizes (e.g. via `[skraBox]`).
27
+ * The host therefore never carries an inline `style`. State Surface all-N/A (non-interactive).
28
+ */
29
+ class SkraSkeleton {
30
+ /** The kind of content faked — selects placeholder geometry, never colour/theme (not a look-input). */
31
+ shape = input('text', ...(ngDevMode ? [{ debugName: "shape" }] : /* istanbul ignore next */ []));
32
+ /** `text` only — number of line bars (multi-line paragraph). Ignored for `block`/`circle`. */
33
+ lines = input(1, ...(ngDevMode ? [{ debugName: "lines" }] : /* istanbul ignore next */ []));
34
+ /** The bars to render: `lines` bars for text (≥1), a single bar for block/circle. */
35
+ bars = computed(() => {
36
+ const count = this.shape() === 'text' ? Math.max(1, Math.floor(this.lines())) : 1;
37
+ return Array.from({ length: count }, (_, i) => i);
38
+ }, ...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
39
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
40
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: SkraSkeleton, isStandalone: true, selector: "skra-skeleton", inputs: { shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, lines: { classPropertyName: "lines", publicName: "lines", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "aria-hidden": "true" }, properties: { "attr.data-skra-shape": "shape()" } }, ngImport: i0, template: `
41
+ @for (bar of bars(); track bar) {
42
+ <span class="skra-skeleton__bar"></span>
43
+ }
44
+ `, isInline: true, styles: [":host{display:block;inline-size:100%;margin:0}:host([data-skra-shape=text]){display:flex;flex-direction:column;gap:.5em}:host([data-skra-shape=block]),:host([data-skra-shape=circle]){block-size:100%}.skra-skeleton__bar{display:block;background-color:var(--skra-comp-skeleton-background);border-width:var(--skra-comp-skeleton-border-width);border-style:var(--skra-comp-skeleton-border-style);border-color:var(--skra-comp-skeleton-border-color);border-radius:var(--skra-comp-skeleton-radius);animation:skra-skeleton-pulse 1.4s var(--skra-comp-skeleton-motion-easing) infinite}:host([data-skra-shape=text]) .skra-skeleton__bar{block-size:.7em;inline-size:100%}:host([data-skra-shape=text]) .skra-skeleton__bar:last-child:not(:only-child){inline-size:60%}:host([data-skra-shape=block]) .skra-skeleton__bar,:host([data-skra-shape=circle]) .skra-skeleton__bar{block-size:100%;inline-size:100%}:host([data-skra-shape=circle]) .skra-skeleton__bar{border-radius:50%}@keyframes skra-skeleton-pulse{0%,to{opacity:1}50%{opacity:.7}}@media(prefers-reduced-motion:reduce){.skra-skeleton__bar{animation:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
45
+ }
46
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraSkeleton, decorators: [{
47
+ type: Component,
48
+ args: [{ selector: 'skra-skeleton', standalone: true, template: `
49
+ @for (bar of bars(); track bar) {
50
+ <span class="skra-skeleton__bar"></span>
51
+ }
52
+ `, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, host: {
53
+ // Decorative — kept out of the accessibility tree (ADR 033 decorative exemption).
54
+ 'aria-hidden': 'true',
55
+ // Structural variant for CSP-safe SCSS branching (a variant, not an ADR 027 state).
56
+ '[attr.data-skra-shape]': 'shape()',
57
+ }, styles: [":host{display:block;inline-size:100%;margin:0}:host([data-skra-shape=text]){display:flex;flex-direction:column;gap:.5em}:host([data-skra-shape=block]),:host([data-skra-shape=circle]){block-size:100%}.skra-skeleton__bar{display:block;background-color:var(--skra-comp-skeleton-background);border-width:var(--skra-comp-skeleton-border-width);border-style:var(--skra-comp-skeleton-border-style);border-color:var(--skra-comp-skeleton-border-color);border-radius:var(--skra-comp-skeleton-radius);animation:skra-skeleton-pulse 1.4s var(--skra-comp-skeleton-motion-easing) infinite}:host([data-skra-shape=text]) .skra-skeleton__bar{block-size:.7em;inline-size:100%}:host([data-skra-shape=text]) .skra-skeleton__bar:last-child:not(:only-child){inline-size:60%}:host([data-skra-shape=block]) .skra-skeleton__bar,:host([data-skra-shape=circle]) .skra-skeleton__bar{block-size:100%;inline-size:100%}:host([data-skra-shape=circle]) .skra-skeleton__bar{border-radius:50%}@keyframes skra-skeleton-pulse{0%,to{opacity:1}50%{opacity:.7}}@media(prefers-reduced-motion:reduce){.skra-skeleton__bar{animation:none}}\n"] }]
58
+ }], propDecorators: { shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], lines: [{ type: i0.Input, args: [{ isSignal: true, alias: "lines", required: false }] }] } });
59
+
60
+ /**
61
+ * Generated bundle index. Do not edit.
62
+ */
63
+
64
+ export { SkraSkeleton };
65
+ //# sourceMappingURL=skra-ui-skeleton.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skra-ui-skeleton.mjs","sources":["../../../../libs/ui/skeleton/src/lib/skeleton/skeleton.ts","../../../../libs/ui/skeleton/src/skra-ui-skeleton.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ViewEncapsulation,\n computed,\n input,\n} from '@angular/core';\n\nexport type SkraSkeletonShape = 'text' | 'block' | 'circle';\n\n/**\n * `<skra-skeleton>` — a decorative content placeholder (Wave 2, `libs/ui/indicators`).\n * Occupies the place of not-yet-loaded content with an animated, layout-shaped placeholder so\n * the layout does not shift on load (CLS) and the user perceives that content is coming.\n *\n * Third resident of the loading family, with a distinct SRP: `[skraLoadingIndicator]` (ADR 028)\n * is the *indeterminate* \"host is busy\" spinner; `SkraProgress` is the *determinate* \"task is\n * N% complete\" bar; **SkraSkeleton is the *content placeholder*** (\"the content hasn't arrived —\n * here is its shape\"). See `adr/specs/component-skeleton-spec.md` § SRP boundary.\n *\n * **A custom element, NOT a Component-on-native-host** (ADR 040): a skeleton has no semantic\n * native equivalent and is purely presentational, so it follows the custom-element atom model\n * (Badge/Chip/Alert precedent). The host is a layout container that renders its own inner\n * `.skra-skeleton__bar` element(s) — which is why a void-host directive was rejected: the\n * multi-line `text` shape needs internal structure.\n *\n * **Decorative a11y (ADR 033):** the host is `aria-hidden=\"true\"` — a skeleton conveys no\n * information to assistive tech (the decorative-icon exemption). The \"loading\" announcement\n * belongs to the consumer's content **container** (`aria-busy=\"true\"`), never to the skeleton.\n *\n * **Sizing is container/`em`-driven (ADR 021 — the CSP reason there are no `width`/`height`\n * inputs):** `text` line bars are `em`-relative (they inherit the surrounding font-size) at full\n * container width; `block`/`circle` fill a host the **consumer** sizes (e.g. via `[skraBox]`).\n * The host therefore never carries an inline `style`. State Surface all-N/A (non-interactive).\n */\n@Component({\n selector: 'skra-skeleton',\n standalone: true,\n template: `\n @for (bar of bars(); track bar) {\n <span class=\"skra-skeleton__bar\"></span>\n }\n `,\n styleUrl: './skeleton.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n // Decorative — kept out of the accessibility tree (ADR 033 decorative exemption).\n 'aria-hidden': 'true',\n // Structural variant for CSP-safe SCSS branching (a variant, not an ADR 027 state).\n '[attr.data-skra-shape]': 'shape()',\n },\n})\nexport class SkraSkeleton {\n /** The kind of content faked — selects placeholder geometry, never colour/theme (not a look-input). */\n readonly shape = input<SkraSkeletonShape>('text');\n\n /** `text` only — number of line bars (multi-line paragraph). Ignored for `block`/`circle`. */\n readonly lines = input<number>(1);\n\n /** The bars to render: `lines` bars for text (≥1), a single bar for block/circle. */\n protected readonly bars = computed(() => {\n const count =\n this.shape() === 'text' ? Math.max(1, Math.floor(this.lines())) : 1;\n return Array.from({ length: count }, (_, i) => i);\n });\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;AAUA;;;;;;;;;;;;;;;;;;;;;;;;AAwBG;MAmBU,YAAY,CAAA;;AAEd,IAAA,KAAK,GAAG,KAAK,CAAoB,MAAM,4EAAC;;AAGxC,IAAA,KAAK,GAAG,KAAK,CAAS,CAAC,4EAAC;;AAGd,IAAA,IAAI,GAAG,QAAQ,CAAC,MAAK;AACtC,QAAA,MAAM,KAAK,GACT,IAAI,CAAC,KAAK,EAAE,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC;AACrE,QAAA,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;AACnD,IAAA,CAAC,2EAAC;uGAZS,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,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,eAAA,EAAA,MAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,IAAA,EAAA,EAAA,UAAA,EAAA,EAAA,aAAA,EAAA,MAAA,EAAA,EAAA,UAAA,EAAA,EAAA,sBAAA,EAAA,SAAA,EAAA,EAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EAfb;;;;AAIT,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,MAAA,EAAA,CAAA,0kCAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;2FAWU,YAAY,EAAA,UAAA,EAAA,CAAA;kBAlBxB,SAAS;+BACE,eAAe,EAAA,UAAA,EACb,IAAI,EAAA,QAAA,EACN;;;;AAIT,EAAA,CAAA,EAAA,eAAA,EAEgB,uBAAuB,CAAC,MAAM,iBAChC,iBAAiB,CAAC,QAAQ,EAAA,IAAA,EACnC;;AAEJ,wBAAA,aAAa,EAAE,MAAM;;AAErB,wBAAA,wBAAwB,EAAE,SAAS;AACpC,qBAAA,EAAA,MAAA,EAAA,CAAA,0kCAAA,CAAA,EAAA;;;ACnDH;;AAEG;;;;"}
@@ -0,0 +1,110 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, input, output, computed, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { IdGeneratorService, SkraFieldContext } from '@skra/ui/field';
4
+
5
+ /**
6
+ * `<skra-star-rating>` — a whole-star rating **input**. An APG Rating composite
7
+ * (`role="radiogroup"`) rendered as stars: internally N `(input[type=radio] + label)`
8
+ * pairs sharing one `name`, where the label IS the star glyph (inline SVG) and the
9
+ * input is sr-only but focusable.
10
+ *
11
+ * Governed by the **ADR 039 §7 note (2026-06-24)**: a composite widget MAY render
12
+ * internal native radios — this is NOT the §3.3-forbidden `<skra-radio-group>` —
13
+ * because (1) value is consumer-owned (`[value]` / `(valueChange)`, controlled-only
14
+ * per ADR 037, the Tabs `[selected]` precedent — no internal value-of-record signal),
15
+ * (2) there is no `SkraRadioGroupContext`, (3) it is a distinct APG composite (like
16
+ * `skra-select`), not a generic radio-group wrapper. Custom element justified per
17
+ * ADR 012 — there is no native single host for a rating.
18
+ *
19
+ * <skra-field [hasError]="!form.rating()">
20
+ * <label skraLabelFor>Rate your experience</label>
21
+ * <skra-star-rating [value]="form.rating()" (valueChange)="form.rating.set($event)" />
22
+ * <skra-error>Please pick a rating</skra-error>
23
+ * </skra-field>
24
+ *
25
+ * Keyboard is the **native radio group** (arrows move + select, wrap, single tab stop)
26
+ * — ZERO CDK. The fill + hover-preview are **pure CSS** (`:checked`/`:hover` sibling
27
+ * combinators) — CSP-clean (ADR 021), no `[style.--var]`, no TS for the preview. The
28
+ * committed `aria-checked` is the only thing AT observes; hover-preview is visual-only.
29
+ *
30
+ * Optional `SkraFieldContext` (ADR 013): the radiogroup is named via `aria-labelledby`
31
+ * → the field label (`SkraFieldContext.labelId`, since a radiogroup is not labelable by
32
+ * `for`/`id`); `disabled`/`required`/`readonly`/`invalid`/`describedby` cascade. Value is
33
+ * NEVER in the context (ADR 037). The `readonly` state shows a committed whole-star rating
34
+ * without allowing change ("show my 4★").
35
+ *
36
+ * Whole-star only (half-step input deferred). Fractional aggregate display ("4.3★") is the
37
+ * separate `skra-rating-display` atom (`libs/ui/data-display`).
38
+ */
39
+ class SkraStarRating {
40
+ idGen = inject(IdGeneratorService);
41
+ context = inject(SkraFieldContext, { optional: true });
42
+ /** Committed rating; 0 (or out of [1,max]) = unrated. Consumer-owned, controlled (ADR 037). */
43
+ value = input(0, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
44
+ /** Number of stars. Semantic count, not a look-input. */
45
+ max = input(5, ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
46
+ /** ADR 035 size axis — the only variant axis. */
47
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
48
+ /** Fires on USER selection only (never on programmatic [value] changes). */
49
+ valueChange = output();
50
+ fallbackName = this.idGen.nextId('rating');
51
+ /** Unique radio-group name so multiple ratings on a page don't merge. */
52
+ groupName = computed(() => this.context?.fieldId() || this.fallbackName, ...(ngDevMode ? [{ debugName: "groupName" }] : /* istanbul ignore next */ []));
53
+ isDisabled = computed(() => this.context?.isDisabled() ?? false, ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
54
+ isReadonly = computed(() => this.context?.isReadonly() ?? false, ...(ngDevMode ? [{ debugName: "isReadonly" }] : /* istanbul ignore next */ []));
55
+ /**
56
+ * Star values HIGH→LOW (`[max, …, 1]`) so the CSS sibling combinator
57
+ * `input:checked ~ label` fills the checked star and every lower one. The visual
58
+ * order is restored left-to-right via `flex-direction: row-reverse` in the SCSS.
59
+ */
60
+ stars = computed(() => {
61
+ const n = Math.max(0, Math.floor(this.max()));
62
+ return Array.from({ length: n }, (_, i) => n - i);
63
+ }, ...(ngDevMode ? [{ debugName: "stars" }] : /* istanbul ignore next */ []));
64
+ starLabel(star) {
65
+ return star === 1 ? '1 star' : `${star} stars`;
66
+ }
67
+ onSelect(star) {
68
+ // Reached only when not readonly (click/keydown are blocked below when readonly),
69
+ // and never on programmatic [value] changes — so this is user-intent only.
70
+ this.valueChange.emit(star);
71
+ }
72
+ blockWhenReadonly(event) {
73
+ if (this.isReadonly())
74
+ event.preventDefault();
75
+ }
76
+ blockKeyWhenReadonly(event) {
77
+ if (this.isReadonly() && event.key !== 'Tab')
78
+ event.preventDefault();
79
+ }
80
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraStarRating, deps: [], target: i0.ɵɵFactoryTarget.Component });
81
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: SkraStarRating, isStandalone: true, selector: "skra-star-rating", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { attributes: { "role": "radiogroup" }, listeners: { "click": "blockWhenReadonly($event)", "keydown": "blockKeyWhenReadonly($event)" }, properties: { "attr.aria-labelledby": "context?.labelId() ?? null", "attr.aria-required": "context?.isRequired() ? \"true\" : null", "attr.aria-invalid": "context?.hasError() ? \"true\" : null", "attr.aria-readonly": "context?.isReadonly() ? \"true\" : null", "attr.aria-describedby": "context?.describedByIds() ?? null", "attr.data-skra-disabled": "isDisabled() ? \"\" : null", "attr.data-skra-size": "size()", "attr.id": "context?.fieldId() ?? null" } }, ngImport: i0, template: "@for (star of stars(); track star) {\n<input\n type=\"radio\"\n class=\"skra-star-rating-input\"\n [name]=\"groupName()\"\n [id]=\"groupName() + '-' + star\"\n [value]=\"star\"\n [checked]=\"value() === star\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"starLabel(star)\"\n (change)=\"onSelect(star)\"\n/>\n<label\n class=\"skra-star-rating-star\"\n [attr.for]=\"groupName() + '-' + star\"\n aria-hidden=\"true\"\n>\n <svg\n class=\"skra-star-rating-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\"\n />\n </svg>\n</label>\n}\n", styles: [":host{margin:0;display:inline-flex;flex-direction:row-reverse;align-items:center;gap:var(--skra-comp-star-rating-gap);--_star-size: var(--skra-comp-star-rating-star-size-md)}:host([data-skra-size=sm]){--_star-size: var(--skra-comp-star-rating-star-size-sm)}:host([data-skra-size=md]){--_star-size: var(--skra-comp-star-rating-star-size-md)}:host([data-skra-size=lg]){--_star-size: var(--skra-comp-star-rating-star-size-lg)}:host([data-skra-disabled]){opacity:var(--skra-sys-opacity-disabled);cursor:not-allowed}.skra-star-rating-input{position:absolute;inline-size:1px;block-size:1px;margin:0;padding:0;opacity:0;pointer-events:none}.skra-star-rating-star{--_fill: var(--skra-comp-star-rating-star-color-empty);display:inline-flex;cursor:pointer;line-height:0;border-radius:.125rem}.skra-star-rating-glyph{inline-size:var(--_star-size);block-size:var(--_star-size);fill:var(--_fill);transition:fill var(--skra-comp-star-rating-motion-duration) var(--skra-comp-star-rating-motion-easing)}.skra-star-rating-input:checked~.skra-star-rating-star{--_fill: var(--skra-comp-star-rating-star-color-filled)}:host(:hover:not([aria-readonly=true]):not([data-skra-disabled])) .skra-star-rating-star{--_fill: var(--skra-comp-star-rating-star-color-empty)}:host(:hover:not([aria-readonly=true]):not([data-skra-disabled])) .skra-star-rating-star:hover,:host(:hover:not([aria-readonly=true]):not([data-skra-disabled])) .skra-star-rating-star:hover~.skra-star-rating-star{--_fill: var(--skra-comp-star-rating-star-color-filled)}:host([aria-readonly=true]) .skra-star-rating-star{cursor:default}.skra-star-rating-input:focus-visible+.skra-star-rating-star{outline:var(--skra-comp-star-rating-focus-ring-width) solid var(--skra-comp-star-rating-focus-ring-color);outline-offset:.125rem}@media(prefers-reduced-motion:reduce){.skra-star-rating-glyph{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
82
+ }
83
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraStarRating, decorators: [{
84
+ type: Component,
85
+ args: [{ selector: 'skra-star-rating', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, host: {
86
+ role: 'radiogroup',
87
+ // Named by the field label (Path 1) — radiogroups are not labelable by for/id.
88
+ '[attr.aria-labelledby]': 'context?.labelId() ?? null',
89
+ '[attr.aria-required]': 'context?.isRequired() ? "true" : null',
90
+ '[attr.aria-invalid]': 'context?.hasError() ? "true" : null',
91
+ '[attr.aria-readonly]': 'context?.isReadonly() ? "true" : null',
92
+ '[attr.aria-describedby]': 'context?.describedByIds() ?? null',
93
+ '[attr.data-skra-disabled]': 'isDisabled() ? "" : null',
94
+ '[attr.data-skra-size]': 'size()',
95
+ '[attr.id]': 'context?.fieldId() ?? null',
96
+ // Readonly enforcement — without it the announced aria-readonly would be a lie.
97
+ // Blocks the toggle from pointer (click) and keyboard (arrows fire change directly).
98
+ '(click)': 'blockWhenReadonly($event)',
99
+ '(keydown)': 'blockKeyWhenReadonly($event)',
100
+ }, template: "@for (star of stars(); track star) {\n<input\n type=\"radio\"\n class=\"skra-star-rating-input\"\n [name]=\"groupName()\"\n [id]=\"groupName() + '-' + star\"\n [value]=\"star\"\n [checked]=\"value() === star\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"starLabel(star)\"\n (change)=\"onSelect(star)\"\n/>\n<label\n class=\"skra-star-rating-star\"\n [attr.for]=\"groupName() + '-' + star\"\n aria-hidden=\"true\"\n>\n <svg\n class=\"skra-star-rating-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\"\n />\n </svg>\n</label>\n}\n", styles: [":host{margin:0;display:inline-flex;flex-direction:row-reverse;align-items:center;gap:var(--skra-comp-star-rating-gap);--_star-size: var(--skra-comp-star-rating-star-size-md)}:host([data-skra-size=sm]){--_star-size: var(--skra-comp-star-rating-star-size-sm)}:host([data-skra-size=md]){--_star-size: var(--skra-comp-star-rating-star-size-md)}:host([data-skra-size=lg]){--_star-size: var(--skra-comp-star-rating-star-size-lg)}:host([data-skra-disabled]){opacity:var(--skra-sys-opacity-disabled);cursor:not-allowed}.skra-star-rating-input{position:absolute;inline-size:1px;block-size:1px;margin:0;padding:0;opacity:0;pointer-events:none}.skra-star-rating-star{--_fill: var(--skra-comp-star-rating-star-color-empty);display:inline-flex;cursor:pointer;line-height:0;border-radius:.125rem}.skra-star-rating-glyph{inline-size:var(--_star-size);block-size:var(--_star-size);fill:var(--_fill);transition:fill var(--skra-comp-star-rating-motion-duration) var(--skra-comp-star-rating-motion-easing)}.skra-star-rating-input:checked~.skra-star-rating-star{--_fill: var(--skra-comp-star-rating-star-color-filled)}:host(:hover:not([aria-readonly=true]):not([data-skra-disabled])) .skra-star-rating-star{--_fill: var(--skra-comp-star-rating-star-color-empty)}:host(:hover:not([aria-readonly=true]):not([data-skra-disabled])) .skra-star-rating-star:hover,:host(:hover:not([aria-readonly=true]):not([data-skra-disabled])) .skra-star-rating-star:hover~.skra-star-rating-star{--_fill: var(--skra-comp-star-rating-star-color-filled)}:host([aria-readonly=true]) .skra-star-rating-star{cursor:default}.skra-star-rating-input:focus-visible+.skra-star-rating-star{outline:var(--skra-comp-star-rating-focus-ring-width) solid var(--skra-comp-star-rating-focus-ring-color);outline-offset:.125rem}@media(prefers-reduced-motion:reduce){.skra-star-rating-glyph{transition:none}}\n"] }]
101
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
102
+
103
+ // APG-radiogroup composite rendered as stars (ADR 039 §7).
104
+
105
+ /**
106
+ * Generated bundle index. Do not edit.
107
+ */
108
+
109
+ export { SkraStarRating };
110
+ //# sourceMappingURL=skra-ui-star-rating.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skra-ui-star-rating.mjs","sources":["../../../../libs/ui/star-rating/src/lib/star-rating/star-rating.ts","../../../../libs/ui/star-rating/src/lib/star-rating/star-rating.html","../../../../libs/ui/star-rating/src/index.ts","../../../../libs/ui/star-rating/src/skra-ui-star-rating.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ViewEncapsulation,\n computed,\n inject,\n input,\n output,\n} from '@angular/core';\nimport { SkraFieldContext } from '@skra/ui/field';\nimport { IdGeneratorService } from '@skra/ui/field';\n\nexport type StarRatingSize = 'sm' | 'md' | 'lg';\n\n/**\n * `<skra-star-rating>` — a whole-star rating **input**. An APG Rating composite\n * (`role=\"radiogroup\"`) rendered as stars: internally N `(input[type=radio] + label)`\n * pairs sharing one `name`, where the label IS the star glyph (inline SVG) and the\n * input is sr-only but focusable.\n *\n * Governed by the **ADR 039 §7 note (2026-06-24)**: a composite widget MAY render\n * internal native radios — this is NOT the §3.3-forbidden `<skra-radio-group>` —\n * because (1) value is consumer-owned (`[value]` / `(valueChange)`, controlled-only\n * per ADR 037, the Tabs `[selected]` precedent — no internal value-of-record signal),\n * (2) there is no `SkraRadioGroupContext`, (3) it is a distinct APG composite (like\n * `skra-select`), not a generic radio-group wrapper. Custom element justified per\n * ADR 012 — there is no native single host for a rating.\n *\n * <skra-field [hasError]=\"!form.rating()\">\n * <label skraLabelFor>Rate your experience</label>\n * <skra-star-rating [value]=\"form.rating()\" (valueChange)=\"form.rating.set($event)\" />\n * <skra-error>Please pick a rating</skra-error>\n * </skra-field>\n *\n * Keyboard is the **native radio group** (arrows move + select, wrap, single tab stop)\n * — ZERO CDK. The fill + hover-preview are **pure CSS** (`:checked`/`:hover` sibling\n * combinators) — CSP-clean (ADR 021), no `[style.--var]`, no TS for the preview. The\n * committed `aria-checked` is the only thing AT observes; hover-preview is visual-only.\n *\n * Optional `SkraFieldContext` (ADR 013): the radiogroup is named via `aria-labelledby`\n * → the field label (`SkraFieldContext.labelId`, since a radiogroup is not labelable by\n * `for`/`id`); `disabled`/`required`/`readonly`/`invalid`/`describedby` cascade. Value is\n * NEVER in the context (ADR 037). The `readonly` state shows a committed whole-star rating\n * without allowing change (\"show my 4★\").\n *\n * Whole-star only (half-step input deferred). Fractional aggregate display (\"4.3★\") is the\n * separate `skra-rating-display` atom (`libs/ui/data-display`).\n */\n@Component({\n selector: 'skra-star-rating',\n standalone: true,\n templateUrl: './star-rating.html',\n styleUrl: './star-rating.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.Emulated,\n host: {\n role: 'radiogroup',\n // Named by the field label (Path 1) — radiogroups are not labelable by for/id.\n '[attr.aria-labelledby]': 'context?.labelId() ?? null',\n '[attr.aria-required]': 'context?.isRequired() ? \"true\" : null',\n '[attr.aria-invalid]': 'context?.hasError() ? \"true\" : null',\n '[attr.aria-readonly]': 'context?.isReadonly() ? \"true\" : null',\n '[attr.aria-describedby]': 'context?.describedByIds() ?? null',\n '[attr.data-skra-disabled]': 'isDisabled() ? \"\" : null',\n '[attr.data-skra-size]': 'size()',\n '[attr.id]': 'context?.fieldId() ?? null',\n // Readonly enforcement — without it the announced aria-readonly would be a lie.\n // Blocks the toggle from pointer (click) and keyboard (arrows fire change directly).\n '(click)': 'blockWhenReadonly($event)',\n '(keydown)': 'blockKeyWhenReadonly($event)',\n },\n})\nexport class SkraStarRating {\n private readonly idGen = inject(IdGeneratorService);\n readonly context = inject(SkraFieldContext, { optional: true });\n\n /** Committed rating; 0 (or out of [1,max]) = unrated. Consumer-owned, controlled (ADR 037). */\n readonly value = input<number>(0);\n /** Number of stars. Semantic count, not a look-input. */\n readonly max = input<number>(5);\n /** ADR 035 size axis — the only variant axis. */\n readonly size = input<StarRatingSize>('md');\n /** Fires on USER selection only (never on programmatic [value] changes). */\n readonly valueChange = output<number>();\n\n private readonly fallbackName = this.idGen.nextId('rating');\n /** Unique radio-group name so multiple ratings on a page don't merge. */\n readonly groupName = computed(\n () => this.context?.fieldId() || this.fallbackName,\n );\n\n readonly isDisabled = computed(() => this.context?.isDisabled() ?? false);\n readonly isReadonly = computed(() => this.context?.isReadonly() ?? false);\n\n /**\n * Star values HIGH→LOW (`[max, …, 1]`) so the CSS sibling combinator\n * `input:checked ~ label` fills the checked star and every lower one. The visual\n * order is restored left-to-right via `flex-direction: row-reverse` in the SCSS.\n */\n readonly stars = computed(() => {\n const n = Math.max(0, Math.floor(this.max()));\n return Array.from({ length: n }, (_, i) => n - i);\n });\n\n protected starLabel(star: number): string {\n return star === 1 ? '1 star' : `${star} stars`;\n }\n\n protected onSelect(star: number): void {\n // Reached only when not readonly (click/keydown are blocked below when readonly),\n // and never on programmatic [value] changes — so this is user-intent only.\n this.valueChange.emit(star);\n }\n\n protected blockWhenReadonly(event: Event): void {\n if (this.isReadonly()) event.preventDefault();\n }\n\n protected blockKeyWhenReadonly(event: KeyboardEvent): void {\n if (this.isReadonly() && event.key !== 'Tab') event.preventDefault();\n }\n}\n","@for (star of stars(); track star) {\n<input\n type=\"radio\"\n class=\"skra-star-rating-input\"\n [name]=\"groupName()\"\n [id]=\"groupName() + '-' + star\"\n [value]=\"star\"\n [checked]=\"value() === star\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"starLabel(star)\"\n (change)=\"onSelect(star)\"\n/>\n<label\n class=\"skra-star-rating-star\"\n [attr.for]=\"groupName() + '-' + star\"\n aria-hidden=\"true\"\n>\n <svg\n class=\"skra-star-rating-glyph\"\n viewBox=\"0 0 24 24\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z\"\n />\n </svg>\n</label>\n}\n","// APG-radiogroup composite rendered as stars (ADR 039 §7).\r\nexport * from './lib/star-rating/star-rating';\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
+
4
+ /**
5
+ * `<skra-status-dot>` — a bare, small, non-interactive status/presence indicator (a coloured dot)
6
+ * composed beside other content: an avatar corner (online/away/busy/offline), or beside a label.
7
+ * Second resident of `libs/ui/data-display`, sibling to `<skra-avatar>` (whose corner presence-dot
8
+ * it provides). See `adr/specs/component-status-dot-spec.md`.
9
+ *
10
+ * **Element, not a decorated native (ADR 012):** there is no native `<dot>`, and a bare `<span>`
11
+ * carries no semantics and forces a colour literal. NOT a Web Component (ADR 000).
12
+ *
13
+ * **No content (the SRP line vs `skra-badge`):** the dot IS the glyph — no slots, no projected
14
+ * content, no label/count. Meaning is carried by ADJACENT text the consumer owns. The defining
15
+ * value-add over a hand-rolled span is the semantic `intent`→token-colour mapping (no hex — ADR
16
+ * 002/023) + the size axis that tracks the avatar.
17
+ *
18
+ * **Non-interactive (Avatar/Badge precedent):** no focusable node, no widget role, no keyboard, no
19
+ * outputs → State Surface all-N/A. A clickable status = `button[skraButton]` composition, not a
20
+ * variant.
21
+ *
22
+ * **Accessible name (ADR 033 Path 3 — graphic host, the SkraIcon/Avatar contract):** the host
23
+ * carries NO default ARIA. The consumer declares semantics explicitly, exactly as for
24
+ * `<skra-icon>`/`<skra-avatar>`:
25
+ * - decorative (Mode A, the ~90% case — dot beside visible/sr-only status text) → `aria-hidden="true"`;
26
+ * - meaningful (Mode B — the dot is the SOLE status carrier) → **`role="img"` + `aria-label`/
27
+ * `aria-labelledby` TOGETHER**. `role="img"` is REQUIRED: `aria-label` on the roleless host is
28
+ * ARIA-prohibited (axe `aria-prohibited-attr`, the Avatar Mode-B fix) and conveys no name.
29
+ * WCAG 1.4.1 (colour-independence) cannot be enforced by the atom — the adjacent text is the
30
+ * consumer's; the `InlineStatus`/`A11yCheck` stories carry the editorial contract. NO `ariaLabel`
31
+ * input (ADR 033 boundary #8).
32
+ *
33
+ * **Look is skin-owned:** circle-vs-square is the `radius` comp token (brutal's `radius.full: 0`
34
+ * ships a square dot automatically). The fill is the semantic `intent`→`sys.color.<intent>.base`
35
+ * (recolour at the sys level, not a per-dot token — skin philosophy); NO `emphasis` (always filled),
36
+ * NO `live` (a dot has no text to announce — the consumer's adjacent live text does), NO
37
+ * `color`/`variant`/`shape` look-inputs.
38
+ */
39
+ class SkraStatusDot {
40
+ /** Variant Surface Intent axis (ADR 035) — the fill colour (status palette subset). */
41
+ intent = input('neutral', ...(ngDevMode ? [{ debugName: "intent" }] : /* istanbul ignore next */ []));
42
+ /** Variant Surface Size axis (ADR 035) — the dot's (colored core) diameter. */
43
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
44
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraStatusDot, deps: [], target: i0.ɵɵFactoryTarget.Component });
45
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.7", type: SkraStatusDot, isStandalone: true, selector: "skra-status-dot", inputs: { intent: { classPropertyName: "intent", publicName: "intent", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-skra-intent": "intent()", "attr.data-skra-size": "size()" } }, ngImport: i0, template: '', isInline: true, styles: ["@charset \"UTF-8\";:host{display:inline-block;flex-shrink:0;box-sizing:content-box;margin:0;inline-size:var(--skra-comp-status-dot-size-md);block-size:var(--skra-comp-status-dot-size-md);background:var(--skra-comp-status-dot-background);border-color:var(--skra-comp-status-dot-border-color);border-width:var(--skra-comp-status-dot-border-width);border-style:var(--skra-comp-status-dot-border-style);border-radius:var(--skra-comp-status-dot-radius);padding-block:var(--skra-comp-status-dot-padding-block);padding-inline:var(--skra-comp-status-dot-padding-inline);box-shadow:var(--skra-comp-status-dot-shadow)}:host([data-skra-size=sm]){inline-size:var(--skra-comp-status-dot-size-sm);block-size:var(--skra-comp-status-dot-size-sm)}:host([data-skra-size=lg]){inline-size:var(--skra-comp-status-dot-size-lg);block-size:var(--skra-comp-status-dot-size-lg)}:host([data-skra-intent=info]){--skra-comp-status-dot-background: var(--skra-sys-color-info-base)}:host([data-skra-intent=success]){--skra-comp-status-dot-background: var(--skra-sys-color-success-base)}:host([data-skra-intent=warning]){--skra-comp-status-dot-background: var(--skra-sys-color-warning-base)}:host([data-skra-intent=danger]){--skra-comp-status-dot-background: var(--skra-sys-color-danger-base)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
46
+ }
47
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraStatusDot, decorators: [{
48
+ type: Component,
49
+ args: [{ selector: 'skra-status-dot', standalone: true, template: '', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, host: {
50
+ // Variant Surface reflection (ADR 035 §3.5; ADR 021 — never [style.--var]).
51
+ '[attr.data-skra-intent]': 'intent()',
52
+ '[attr.data-skra-size]': 'size()',
53
+ }, styles: ["@charset \"UTF-8\";:host{display:inline-block;flex-shrink:0;box-sizing:content-box;margin:0;inline-size:var(--skra-comp-status-dot-size-md);block-size:var(--skra-comp-status-dot-size-md);background:var(--skra-comp-status-dot-background);border-color:var(--skra-comp-status-dot-border-color);border-width:var(--skra-comp-status-dot-border-width);border-style:var(--skra-comp-status-dot-border-style);border-radius:var(--skra-comp-status-dot-radius);padding-block:var(--skra-comp-status-dot-padding-block);padding-inline:var(--skra-comp-status-dot-padding-inline);box-shadow:var(--skra-comp-status-dot-shadow)}:host([data-skra-size=sm]){inline-size:var(--skra-comp-status-dot-size-sm);block-size:var(--skra-comp-status-dot-size-sm)}:host([data-skra-size=lg]){inline-size:var(--skra-comp-status-dot-size-lg);block-size:var(--skra-comp-status-dot-size-lg)}:host([data-skra-intent=info]){--skra-comp-status-dot-background: var(--skra-sys-color-info-base)}:host([data-skra-intent=success]){--skra-comp-status-dot-background: var(--skra-sys-color-success-base)}:host([data-skra-intent=warning]){--skra-comp-status-dot-background: var(--skra-sys-color-warning-base)}:host([data-skra-intent=danger]){--skra-comp-status-dot-background: var(--skra-sys-color-danger-base)}\n"] }]
54
+ }], propDecorators: { intent: [{ type: i0.Input, args: [{ isSignal: true, alias: "intent", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
55
+
56
+ /**
57
+ * Generated bundle index. Do not edit.
58
+ */
59
+
60
+ export { SkraStatusDot };
61
+ //# sourceMappingURL=skra-ui-status-dot.mjs.map