@adia-ai/web-components 0.5.12 → 0.5.13

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.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,69 @@ runtime ships in the sibling `@adia-ai/a2ui-runtime` package
11
11
 
12
12
  _No pending changes._
13
13
 
14
+ ## [0.5.13] - 2026-05-15
15
+
16
+ ### v0.5.13 §310 — `apcaContrast` soft-clamp constant fix (FEEDBACK-35; P2 correctness)
17
+
18
+ `@adia-ai/web-components/color`'s `apcaContrast()` was using `APCA_LO_CLAMP = 0.06` for the soft-clamp threshold where the canonical APCA spec (`apca-w3@0.1.9`'s `SAPC_BLACK_THRESHOLD`) calls for `0.022`. The cubic-root soft-clamp `Math.pow(threshold - Y, 1.414)` operating on the wrong threshold lifted near-black luminance too aggressively. Pre-fix `apcaContrast('#000','#fff')` returned 99.49; post-fix returns 106.04 — matches canonical reference within ±0.01 Lc across all 8 reference pairs. Consumers using `<swatch-ui auto-contrast>` (which transitively calls `pickContrastingFg → apcaContrast`) silently got wrong contrast verdicts for any swatch with `Y < 0.06`.
19
+
20
+ #### Fixed — `color/`
21
+
22
+ - `color/index.js` `apcaContrast()` soft-clamp now uses `APCA_LO_FG`/`APCA_LO_BG = 0.022` (declared correctly since v0.5.12 but never read). Dead constants (`APCA_NORM_L`, `APCA_REVERSE_FACTOR`, `APCA_NORM_FACTOR`, `APCA_LO_CLAMP`, `APCA_RESCALE`) removed; new `APCA_BLACK_CLAMP = 1.414` factored out for readability.
23
+
24
+ ### v0.5.13 §311 — `@adia-ai/web-components/color` P3 helpers (FEEDBACK-31)
25
+
26
+ `@adia-ai/web-components/color` (v0.5.12 §301) shipped sRGB-only gamut helpers; the v0.6.0 §301 plan-doc spec lines 121-126 committed both sRGB AND Display-P3. v0.5.13 §311 closes the gap.
27
+
28
+ #### Added — `color/`
29
+
30
+ - `inP3Gamut(L, C, H): boolean` — returns true if the OKLCH triple is inside Display-P3 gamut (tolerance `0.001`).
31
+ - `gamutMapChromaP3(L, C, H): number` — 8-iteration chroma bisection in P3 (matches `gamutMapChroma`'s shape).
32
+ - Internal `linearSrgbToLinearP3()` matrix sourced from CSS Color 4 § 12.3 (D65 → D65; no Bradford chromatic adaptation needed).
33
+ - `color/index.d.ts` ships matching declarations.
34
+
35
+ ### v0.5.13 §312 — `<stat-ui>` explicit exports map entries (FEEDBACK-32)
36
+
37
+ v0.5.12 §253a hand-authored `stat-ui.d.ts` on disk but the package.json wildcard `./components/*` expanded `./components/stat` → `./components/stat/stat.js` (non-existent; actual file is `stat-ui.js`). Consumer TS imports failed at the package-export layer with `TS2882` even though the file exists + Vite resolved at runtime.
38
+
39
+ #### Changed — `package.json`
40
+
41
+ - `exports` map adds 2 explicit entries: `./components/stat` + `./components/stat/stat-ui.js` both pointing at `stat-ui.{d.ts,js}`.
42
+ - **Drops at v0.6.0 §303** when the `git mv stat-ui.{js,css,d.ts} → stat.{js,css,d.ts}` rename normalizes the filename + the wildcard naturally covers `stat.{js,d.ts}`.
43
+
44
+ ### v0.5.13 §313 — `<color-input-ui>` constraint pass-through + parsed-detail forwarding (FEEDBACK-33)
45
+
46
+ `<color-input-ui>` (v0.5.12 §302) composed `<color-picker-ui>` but dropped 5 generation-constraint props (`maxChroma` / `maxL` / `minL` / `hueDriftMax` / `baseHue`) and 3 parsed-OKLCH-channel-scalars (`{l, c, h}`) from the re-emitted event detail. Consumers using L-range constraints or OKLCH-native logic (Tokens Studio's `defaults.Lmin`/`Lmax`) couldn't migrate to `<color-input-ui>` without a UX regression.
47
+
48
+ #### Added — `components/color-input/`
49
+
50
+ - `class.js` adds 5 generation-constraint props to `static properties` + sets them on inner picker at `#mount()` + diff-detect re-set on `render()` (Object.is handles NaN-equality for `baseHue`/`hueDriftMax`).
51
+ - `#commit()` re-emits with all 6 detail fields including parsed scalars `l: detail.l, c: detail.c, h: detail.h`.
52
+ - `color-input.yaml` declares the 5 props + 3 new detail fields per `change`/`input` events.
53
+ - `color-input.d.ts` `ColorInputChangeEventDetail` interface gains `l: number; c: number; h: number;` fields; class gains 5 typed prop declarations.
54
+
55
+ ### v0.5.13 §314 — `<swatch-ui>` `[slot="chrome"]` named slot (FEEDBACK-34)
56
+
57
+ `<swatch-ui>`'s `#stamp()` lifecycle wiped `innerHTML` then funneled surviving children into `#labelEl`. Consumer chrome positioned absolutely against the swatch tile (gamut badges + override/tracked dots per C1.3 dogfood) landed inside the label's text bbox where `position: absolute; top/right/bottom/left` anchored to the small label geometry instead of the tile. v0.5.13 §314 adds a `[slot="chrome"]` named slot whose children survive stamping as direct host siblings of `#tileEl`.
58
+
59
+ #### Added — `components/swatch/`
60
+
61
+ - `class.js` `#stamp()` extracts `[slot="chrome"]` children BEFORE the wipe; re-attaches them as direct host siblings IMMEDIATELY after `#tileEl`. The host's `position: relative` anchor (when `shape="block"`) lets consumer absolute-positioning anchor to the tile's geometry.
62
+ - `swatch.yaml` `slots:` block declares the `chrome` named slot + clarifies `default` slot's funnel-into-label behavior.
63
+
64
+ #### Migration
65
+
66
+ ```diff
67
+ <swatch-ui shape="block" label-position="overlay" selectable>
68
+ - <badge-ui class="dts-gamut-badge" ...>P3</badge-ui>
69
+ - <span class="dts-override-dot"></span>
70
+ + <badge-ui slot="chrome" class="dts-gamut-badge" ...>P3</badge-ui>
71
+ + <span slot="chrome" class="dts-override-dot"></span>
72
+ </swatch-ui>
73
+ ```
74
+
75
+ Single `slot="chrome"` attribute on each chrome element. Existing absolute-positioning CSS unchanged.
76
+
14
77
  ## [0.5.12] - 2026-05-15
15
78
 
16
79
  ### v0.5.12 §252 — `package.json` `files` array completion (FB-28 close-out)
package/color/index.d.ts CHANGED
@@ -87,6 +87,22 @@ export function isOklchInGamut(L: number, C: number, H: number): boolean;
87
87
  */
88
88
  export function gamutMapChroma(L: number, C: number, H: number): number;
89
89
 
90
+ // ── Display-P3 gamut (v0.5.13 §-TBD, FB-31) ──────────────────────────────
91
+
92
+ /**
93
+ * Returns true if the OKLCH triple is inside Display-P3 gamut (tolerance
94
+ * `0.001`). sRGB-linear → P3-linear conversion uses the CSS Color 4 § 12.3
95
+ * matrix (D65 → D65; no Bradford adaptation needed).
96
+ */
97
+ export function inP3Gamut(L: number, C: number, H: number): boolean;
98
+
99
+ /**
100
+ * Bisect chroma to find the largest value that keeps `(L, _, H)` in
101
+ * Display-P3 gamut. 8 bisection iterations — same shape as the sRGB
102
+ * `gamutMapChroma`. Used by P3-aware design tooling.
103
+ */
104
+ export function gamutMapChromaP3(L: number, C: number, H: number): number;
105
+
90
106
  // ── APCA (advanced perceptual contrast algorithm) ─────────────────────────
91
107
 
92
108
  /**
package/color/index.js CHANGED
@@ -154,15 +154,67 @@ export function gamutMapChroma(L, C, H) {
154
154
  return lo;
155
155
  }
156
156
 
157
+ // ── Display-P3 gamut ──────────────────────────────────────────────────────
158
+ //
159
+ // v0.5.13 §-TBD (FB-31). The v0.6.0 §301 plan-doc spec committed
160
+ // `inP3Gamut(o)` + `gamutMapOklch(o, 'p3')`; the v0.5.12 §301 ship dropped
161
+ // both (sRGB-only surface). FB-31 closes the gap with explicit P3 helpers
162
+ // matching the existing scalar-channels signature shape.
163
+ //
164
+ // sRGB-linear → Display-P3-linear matrix sourced from CSS Color 4 § 12.3
165
+ // (assumes both spaces share the D65 white point, which is true post-2018
166
+ // browser implementations; no Bradford chromatic adaptation needed).
167
+
168
+ function linearSrgbToLinearP3(r, g, b) {
169
+ return [
170
+ 0.8224621 * r + 0.1775380 * g + 0.0000000 * b,
171
+ 0.0331941 * r + 0.9668058 * g + 0.0000001 * b,
172
+ 0.0170827 * r + 0.0723974 * g + 0.9105199 * b,
173
+ ];
174
+ }
175
+
176
+ /** Returns true if the OKLCH triple is inside Display-P3 gamut (tolerance `0.001`). */
177
+ export function inP3Gamut(L, C, H) {
178
+ const { a, b } = oklchToOklab(L, C, H);
179
+ const [lr, lg, lb] = oklabToLinearSrgb(L, a, b);
180
+ const [p3r, p3g, p3b] = linearSrgbToLinearP3(lr, lg, lb);
181
+ return isInGamut(linearToSrgb(p3r), linearToSrgb(p3g), linearToSrgb(p3b));
182
+ }
183
+
184
+ /**
185
+ * Bisect chroma to find the largest value that keeps `(L, _, H)` in
186
+ * Display-P3 gamut. 8 bisection iterations — same shape as the sRGB
187
+ * `gamutMapChroma`. Used by P3-aware design tooling (Tokens Studio,
188
+ * theme designers targeting P3-display laptops + iPads).
189
+ */
190
+ export function gamutMapChromaP3(L, C, H) {
191
+ if (inP3Gamut(L, C, H)) return C;
192
+ let lo = 0, hi = C;
193
+ for (let i = 0; i < 8; i++) {
194
+ const mid = (lo + hi) / 2;
195
+ if (inP3Gamut(L, mid, H)) lo = mid;
196
+ else hi = mid;
197
+ }
198
+ return lo;
199
+ }
200
+
157
201
  // ── APCA (advanced perceptual contrast algorithm) ─────────────────────────
202
+ //
203
+ // Source of truth: Andrew Somers' SACAM specification + apca-w3@0.1.9 (the
204
+ // W3C-pinned reference impl). The soft-clamp threshold is `0.022` for both
205
+ // fg + bg luminance values (per `SAPC_BLACK_THRESHOLD` in apca-w3 source).
206
+ // The 1.414 exponent is the APCA "BLACK_CLAMP" rescale constant.
207
+ //
208
+ // v0.5.13 §-TBD (FB-35): corrected the soft-clamp constant. v0.5.12 ship
209
+ // used 0.06 (declared as APCA_LO_CLAMP) by mistake, producing Lc 99.49 for
210
+ // black-on-white where canonical is 106.04 (-6.5pt bias for any pair
211
+ // involving near-black). Constants APCA_LO_FG / APCA_LO_BG were declared
212
+ // correctly but never read; the 0.06 constant was a stray from an earlier
213
+ // draft. apcaSrgbY() unchanged.
158
214
 
159
- const APCA_NORM_L = 0.027;
160
215
  const APCA_LO_BG = 0.022;
161
216
  const APCA_LO_FG = 0.022;
162
- const APCA_REVERSE_FACTOR = 1.14;
163
- const APCA_NORM_FACTOR = 1.14;
164
- const APCA_LO_CLAMP = 0.06;
165
- const APCA_RESCALE = 1.14;
217
+ const APCA_BLACK_CLAMP = 1.414;
166
218
 
167
219
  function apcaSrgbY(rNorm, gNorm, bNorm) {
168
220
  // sRGB → relative luminance via APCA Y (different coefficients than WCAG).
@@ -201,9 +253,10 @@ export function apcaContrast(fgHex, bgHex) {
201
253
  let Yfg = apcaSrgbY(fg.r, fg.g, fg.b);
202
254
  let Ybg = apcaSrgbY(bg.r, bg.g, bg.b);
203
255
 
204
- // Soft clamp near black per APCA.
205
- if (Yfg < APCA_LO_CLAMP) Yfg += Math.pow(APCA_LO_CLAMP - Yfg, 1.414);
206
- if (Ybg < APCA_LO_CLAMP) Ybg += Math.pow(APCA_LO_CLAMP - Ybg, 1.414);
256
+ // Soft clamp near black per APCA spec — threshold 0.022 per SACAM /
257
+ // apca-w3@0.1.9 (`SAPC_BLACK_THRESHOLD`).
258
+ if (Yfg < APCA_LO_FG) Yfg += Math.pow(APCA_LO_FG - Yfg, APCA_BLACK_CLAMP);
259
+ if (Ybg < APCA_LO_BG) Ybg += Math.pow(APCA_LO_BG - Ybg, APCA_BLACK_CLAMP);
207
260
 
208
261
  let Lc;
209
262
  if (Ybg > Yfg) {
@@ -40,10 +40,20 @@ export class UIColorInput extends UIFormElement {
40
40
  static get properties() {
41
41
  return {
42
42
  ...UIFormElement.properties,
43
- value: { type: String, default: '#3b82f6', reflect: true },
44
- format: { type: String, default: 'hex', reflect: true },
45
- placement: { type: String, default: 'bottom-start', reflect: true },
46
- open: { type: Boolean, default: false, reflect: true },
43
+ value: { type: String, default: '#3b82f6', reflect: true },
44
+ format: { type: String, default: 'hex', reflect: true },
45
+ placement: { type: String, default: 'bottom-start', reflect: true },
46
+ open: { type: Boolean, default: false, reflect: true },
47
+ // Generation-constraint props forwarded to the inner <color-picker-ui>.
48
+ // v0.5.13 §-TBD (FB-33 §1) — full parity with color-picker's 5 constraint
49
+ // props (`maxChroma` / `maxL` / `minL` / `hueDriftMax` / `baseHue`) so
50
+ // <color-input-ui> adoption doesn't force a UX regression on consumers
51
+ // already using L-range / chroma / hue-drift constraints.
52
+ maxChroma: { type: Number, default: Infinity, reflect: true },
53
+ maxL: { type: Number, default: 1, reflect: true },
54
+ minL: { type: Number, default: 0, reflect: true },
55
+ hueDriftMax: { type: Number, default: NaN, reflect: true },
56
+ baseHue: { type: Number, default: NaN, reflect: true },
47
57
  };
48
58
  }
49
59
 
@@ -66,8 +76,16 @@ export class UIColorInput extends UIFormElement {
66
76
  if (this.#popover && this.#popover.open !== this.open) {
67
77
  this.#popover.open = this.open;
68
78
  }
69
- if (this.#picker && this.#picker.format !== this.format) {
70
- this.#picker.format = this.format;
79
+ if (this.#picker) {
80
+ if (this.#picker.format !== this.format) this.#picker.format = this.format;
81
+ // Forward live changes to the 5 generation-constraint props (FB-33 §1).
82
+ // Object.is handles the NaN-equality case correctly so `baseHue`/
83
+ // `hueDriftMax` don't reassign on every render.
84
+ if (!Object.is(this.#picker.maxChroma, this.maxChroma)) this.#picker.maxChroma = this.maxChroma;
85
+ if (!Object.is(this.#picker.maxL, this.maxL)) this.#picker.maxL = this.maxL;
86
+ if (!Object.is(this.#picker.minL, this.minL)) this.#picker.minL = this.minL;
87
+ if (!Object.is(this.#picker.hueDriftMax, this.hueDriftMax)) this.#picker.hueDriftMax = this.hueDriftMax;
88
+ if (!Object.is(this.#picker.baseHue, this.baseHue)) this.#picker.baseHue = this.baseHue;
71
89
  }
72
90
  if (this.#button) {
73
91
  if (this.disabled) this.#button.setAttribute('disabled', '');
@@ -115,6 +133,13 @@ export class UIColorInput extends UIFormElement {
115
133
  picker.setAttribute('slot', 'content');
116
134
  picker.setAttribute('format', this.format);
117
135
  picker.setAttribute('value', this.value);
136
+ // Set generation-constraint props directly (numeric — attribute-reflection
137
+ // would round-trip but properties cover NaN/Infinity correctly).
138
+ picker.maxChroma = this.maxChroma;
139
+ picker.maxL = this.maxL;
140
+ picker.minL = this.minL;
141
+ picker.hueDriftMax = this.hueDriftMax;
142
+ picker.baseHue = this.baseHue;
118
143
 
119
144
  popover.append(button, picker);
120
145
  this.append(popover);
@@ -162,10 +187,20 @@ export class UIColorInput extends UIFormElement {
162
187
  this.syncValue(next);
163
188
  }
164
189
  this.#syncFromValue();
190
+ // Forward the inner picker's parsed OKLCH channel scalars (v0.5.13 §-TBD,
191
+ // FB-33 §1-adjacent). Consumers writing OKLCH-native logic get the parsed
192
+ // {l, c, h} for free, matching <color-picker-ui>'s detail shape.
165
193
  this.dispatchEvent(new CustomEvent(kind, {
166
194
  bubbles: true,
167
195
  composed: true,
168
- detail: { value: next ?? this.value, hex: detail.hex, oklch: detail.oklch },
196
+ detail: {
197
+ value: next ?? this.value,
198
+ hex: detail.hex,
199
+ oklch: detail.oklch,
200
+ l: detail.l,
201
+ c: detail.c,
202
+ h: detail.h,
203
+ },
169
204
  }));
170
205
  }
171
206
 
@@ -13,6 +13,11 @@
13
13
  }
14
14
  ],
15
15
  "properties": {
16
+ "baseHue": {
17
+ "description": "Reference hue (degrees) for the hueDriftMax constraint, forwarded to inner picker. Default NaN — picker falls back to its hue at first commit.",
18
+ "type": "number",
19
+ "default": "NaN"
20
+ },
16
21
  "component": {
17
22
  "const": "ColorInput"
18
23
  },
@@ -30,6 +35,26 @@
30
35
  ],
31
36
  "default": "hex"
32
37
  },
38
+ "hueDriftMax": {
39
+ "description": "Generation constraint forwarded to inner picker — maximum allowed signed-shortest-path hue deviation (degrees) from baseHue. Default NaN (no constraint).",
40
+ "type": "number",
41
+ "default": "NaN"
42
+ },
43
+ "maxChroma": {
44
+ "description": "Generation constraint forwarded to the inner `<color-picker-ui>` (v0.5.13\n§-TBD, FB-33 §1). Clamp the OKLCH chroma channel to at most this value.\nDefault Infinity (no constraint).\n",
45
+ "type": "number",
46
+ "default": "Infinity"
47
+ },
48
+ "maxL": {
49
+ "description": "Generation constraint forwarded to inner picker — clamp OKLCH lightness to at most this value (0..1). Default 1.",
50
+ "type": "number",
51
+ "default": 1
52
+ },
53
+ "minL": {
54
+ "description": "Generation constraint forwarded to inner picker — clamp OKLCH lightness to at least this value (0..1). Default 0.",
55
+ "type": "number",
56
+ "default": 0
57
+ },
33
58
  "name": {
34
59
  "description": "Form field name.",
35
60
  "type": "string",
@@ -77,10 +102,22 @@
77
102
  "change": {
78
103
  "description": "Fired when the color is committed (pointerup / Enter / picker close). Bubbles from inner picker.",
79
104
  "detail": {
105
+ "c": {
106
+ "description": "Parsed OKLCH chroma scalar.",
107
+ "type": "number"
108
+ },
109
+ "h": {
110
+ "description": "Parsed OKLCH hue scalar (degrees, NaN ok for achromatic).",
111
+ "type": "number"
112
+ },
80
113
  "hex": {
81
114
  "description": "Hex form (`#rrggbb`).",
82
115
  "type": "string"
83
116
  },
117
+ "l": {
118
+ "description": "Parsed OKLCH lightness scalar (0..1). v0.5.13 §-TBD (FB-33 §1-adjacent).",
119
+ "type": "number"
120
+ },
84
121
  "oklch": {
85
122
  "description": "OKLCH string (`oklch(L C H)`).",
86
123
  "type": "string"
@@ -94,9 +131,18 @@
94
131
  "input": {
95
132
  "description": "Fired during continuous drag of the inner picker. Bubbles from inner picker.",
96
133
  "detail": {
134
+ "c": {
135
+ "type": "number"
136
+ },
137
+ "h": {
138
+ "type": "number"
139
+ },
97
140
  "hex": {
98
141
  "type": "string"
99
142
  },
143
+ "l": {
144
+ "type": "number"
145
+ },
100
146
  "oklch": {
101
147
  "type": "string"
102
148
  },
@@ -38,6 +38,12 @@ export interface ColorInputChangeEventDetail {
38
38
  hex: string;
39
39
  /** OKLCH string (`oklch(L C H)`, fixed precision). */
40
40
  oklch: string;
41
+ /** Parsed OKLCH lightness (0..1). v0.5.13 §-TBD (FB-33 §1-adjacent). */
42
+ l: number;
43
+ /** Parsed OKLCH chroma. */
44
+ c: number;
45
+ /** Parsed OKLCH hue (degrees, NaN ok for achromatic). */
46
+ h: number;
41
47
  }
42
48
  export type ColorInputChangeEvent = CustomEvent<ColorInputChangeEventDetail>;
43
49
  export type ColorInputInputEvent = CustomEvent<ColorInputChangeEventDetail>;
@@ -55,6 +61,16 @@ export class UIColorInput extends UIFormElement {
55
61
  placement: 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
56
62
  /** Reflects the inner popover's open state. Set programmatically to open/close without a click. */
57
63
  open: boolean;
64
+ /** Generation constraint forwarded to inner picker: clamp OKLCH chroma to at most this value. `Infinity` = unconstrained. v0.5.13 §-TBD (FB-33 §1). */
65
+ maxChroma: number;
66
+ /** Generation constraint forwarded to inner picker: clamp OKLCH lightness to at most this value (0..1). `1` = unconstrained. */
67
+ maxL: number;
68
+ /** Generation constraint forwarded to inner picker: clamp OKLCH lightness to at least this value (0..1). `0` = unconstrained. */
69
+ minL: number;
70
+ /** Generation constraint forwarded to inner picker: maximum allowed signed-shortest-path hue drift in degrees from `baseHue`. `NaN` = unconstrained. */
71
+ hueDriftMax: number;
72
+ /** Reference hue (degrees) for `hueDriftMax`, forwarded to inner picker. `NaN` = picker falls back to its hue at first commit. */
73
+ baseHue: number;
58
74
 
59
75
  /** Open the picker programmatically. */
60
76
  showPicker(): void;
@@ -61,6 +61,34 @@ props:
61
61
  type: boolean
62
62
  default: false
63
63
  reflect: true
64
+ maxChroma:
65
+ description: |
66
+ Generation constraint forwarded to the inner `<color-picker-ui>` (v0.5.13
67
+ §-TBD, FB-33 §1). Clamp the OKLCH chroma channel to at most this value.
68
+ Default Infinity (no constraint).
69
+ type: number
70
+ default: Infinity
71
+ reflect: true
72
+ maxL:
73
+ description: Generation constraint forwarded to inner picker — clamp OKLCH lightness to at most this value (0..1). Default 1.
74
+ type: number
75
+ default: 1
76
+ reflect: true
77
+ minL:
78
+ description: Generation constraint forwarded to inner picker — clamp OKLCH lightness to at least this value (0..1). Default 0.
79
+ type: number
80
+ default: 0
81
+ reflect: true
82
+ hueDriftMax:
83
+ description: Generation constraint forwarded to inner picker — maximum allowed signed-shortest-path hue deviation (degrees) from baseHue. Default NaN (no constraint).
84
+ type: number
85
+ default: NaN
86
+ reflect: true
87
+ baseHue:
88
+ description: Reference hue (degrees) for the hueDriftMax constraint, forwarded to inner picker. Default NaN — picker falls back to its hue at first commit.
89
+ type: number
90
+ default: NaN
91
+ reflect: true
64
92
  events:
65
93
  change:
66
94
  description: Fired when the color is committed (pointerup / Enter / picker close). Bubbles from inner picker.
@@ -74,6 +102,15 @@ events:
74
102
  oklch:
75
103
  type: string
76
104
  description: OKLCH string (`oklch(L C H)`).
105
+ l:
106
+ type: number
107
+ description: Parsed OKLCH lightness scalar (0..1). v0.5.13 §-TBD (FB-33 §1-adjacent).
108
+ c:
109
+ type: number
110
+ description: Parsed OKLCH chroma scalar.
111
+ h:
112
+ type: number
113
+ description: Parsed OKLCH hue scalar (degrees, NaN ok for achromatic).
77
114
  input:
78
115
  description: Fired during continuous drag of the inner picker. Bubbles from inner picker.
79
116
  detail:
@@ -84,6 +121,12 @@ events:
84
121
  type: string
85
122
  oklch:
86
123
  type: string
124
+ l:
125
+ type: number
126
+ c:
127
+ type: number
128
+ h:
129
+ type: number
87
130
  slots: {}
88
131
  states:
89
132
  - name: idle
@@ -212,8 +212,20 @@ export class UISwatch extends UIElement {
212
212
 
213
213
  #stamp() {
214
214
  if (this.#stamped) return;
215
+
216
+ // v0.5.13 §-TBD (FB-34 §1) — `[slot="chrome"]` children are sibling-of-tile
217
+ // chrome (gamut badges, override/tracked dots, custom indicators) that must
218
+ // survive stamping as direct host siblings, NOT funnel into #labelEl where
219
+ // absolute-positioning anchors against the label text bbox. Pull them out
220
+ // FIRST so the innerHTML='' wipe below doesn't lose them.
221
+ const chromeSlot = Array.from(this.children).filter(
222
+ n => n.getAttribute && n.getAttribute('slot') === 'chrome'
223
+ );
224
+ for (const n of chromeSlot) n.remove();
225
+
215
226
  // Capture pre-existing default-slot content so consumer-authored
216
227
  // children (e.g. <swatch-ui>Forecast</swatch-ui>) survive stamping.
228
+ // `[slot="chrome"]` children are already removed above, so won't appear here.
217
229
  const slotted = Array.from(this.childNodes).filter(n =>
218
230
  !(n.nodeType === 1 && n.dataset && (
219
231
  n.dataset.tile !== undefined ||
@@ -230,6 +242,14 @@ export class UISwatch extends UIElement {
230
242
  this.#tileEl.setAttribute('aria-hidden', 'true');
231
243
  this.appendChild(this.#tileEl);
232
244
 
245
+ // Chrome-slot children re-attach IMMEDIATELY after the tile so they can
246
+ // position absolutely against the host (host carries position: relative
247
+ // when shape="block" + label-position="overlay" per swatch.css). Keeping
248
+ // them sibling-of-tile (not descendant-of-tile) lets consumer CSS use
249
+ // `position: absolute; top: 2px; right: 2px` and anchor against the host's
250
+ // padding-box, which matches the tile's geometry in the block layout.
251
+ for (const n of chromeSlot) this.appendChild(n);
252
+
233
253
  // Badge container — holds one or more <span data-badge-variant="..."> children.
234
254
  // Multi-badge support added in v0.4.9 §92 (FEEDBACK-04 follow-up).
235
255
  this.#badgeEl = document.createElement('span');
@@ -149,7 +149,10 @@
149
149
  ],
150
150
  "slots": {
151
151
  "default": {
152
- "description": "Optional rich label content. When present, replaces the [label] attribute's text."
152
+ "description": "Optional rich label content. When present, replaces the [label]\nattribute's text. Consumer chrome positioned absolutely against the\nswatch tile should use `[slot=\"chrome\"]` instead — default-slot\nchildren are funneled into the label region.\n"
153
+ },
154
+ "chrome": {
155
+ "description": "§-TBD (v0.5.13, FB-34 §1). Sibling-of-tile chrome — gamut badges,\noverride/tracked dots, custom indicators — that survives stamping as\na direct host sibling rather than being funneled into the label. Pair\nwith consumer-authored CSS `position: absolute` against the host\n(the host carries `position: relative` when `shape=\"block\"`, so\n`top/right/bottom/left` offsets anchor to the tile's geometry).\nCloses the C1.3 dogfood chrome-overlay composition gap.\n"
153
156
  }
154
157
  },
155
158
  "states": [
@@ -119,7 +119,20 @@ events:
119
119
  description: The [label] attribute value.
120
120
  slots:
121
121
  default:
122
- description: Optional rich label content. When present, replaces the [label] attribute's text.
122
+ description: |
123
+ Optional rich label content. When present, replaces the [label]
124
+ attribute's text. Consumer chrome positioned absolutely against the
125
+ swatch tile should use `[slot="chrome"]` instead — default-slot
126
+ children are funneled into the label region.
127
+ chrome:
128
+ description: |
129
+ §-TBD (v0.5.13, FB-34 §1). Sibling-of-tile chrome — gamut badges,
130
+ override/tracked dots, custom indicators — that survives stamping as
131
+ a direct host sibling rather than being funneled into the label. Pair
132
+ with consumer-authored CSS `position: absolute` against the host
133
+ (the host carries `position: relative` when `shape="block"`, so
134
+ `top/right/bottom/left` offsets anchor to the tile's geometry).
135
+ Closes the C1.3 dogfood chrome-overlay composition gap.
123
136
  states:
124
137
  - name: idle
125
138
  description: Default, ready for display.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/web-components",
3
- "version": "0.5.12",
3
+ "version": "0.5.13",
4
4
  "description": "AdiaUI web components — vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-runtime.",
5
5
  "type": "module",
6
6
  "types": "./index.d.ts",
@@ -23,6 +23,14 @@
23
23
  "types": "./components/*/*.d.ts",
24
24
  "default": "./components/*/*.js"
25
25
  },
26
+ "./components/stat": {
27
+ "types": "./components/stat/stat-ui.d.ts",
28
+ "default": "./components/stat/stat-ui.js"
29
+ },
30
+ "./components/stat/stat-ui.js": {
31
+ "types": "./components/stat/stat-ui.d.ts",
32
+ "default": "./components/stat/stat-ui.js"
33
+ },
26
34
  "./components/*/class": {
27
35
  "types": "./components/*/*.d.ts",
28
36
  "default": "./components/*/class.js"