@adia-ai/web-components 0.8.47 → 0.8.50

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 (68) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/MIGRATION.md +136 -0
  3. package/components/card/card.css +25 -2
  4. package/components/chart/chart.a2ui.json +1 -1
  5. package/components/chart/chart.class.js +41 -10
  6. package/components/chart/chart.d.ts +1 -1
  7. package/components/chart/chart.yaml +6 -3
  8. package/components/input/input.a2ui.json +2 -2
  9. package/components/input/input.css +45 -16
  10. package/components/input/input.yaml +11 -9
  11. package/components/link/link.css +3 -2
  12. package/components/richtext/richtext.css +2 -2
  13. package/components/search/search.a2ui.json +1 -1
  14. package/components/search/search.class.js +43 -0
  15. package/components/search/search.css +28 -0
  16. package/components/search/search.yaml +4 -1
  17. package/components/select/select.class.js +4 -1
  18. package/components/select/select.css +13 -0
  19. package/components/stat/stat.a2ui.json +30 -3
  20. package/components/stat/stat.css +88 -0
  21. package/components/stat/stat.d.ts +2 -2
  22. package/components/stat/stat.yaml +99 -6
  23. package/components/table/table.a2ui.json +5 -2
  24. package/components/table/table.class.js +36 -1
  25. package/components/table/table.d.ts +2 -2
  26. package/components/table/table.yaml +17 -1
  27. package/components/table-footer/table-footer.a2ui.json +20 -2
  28. package/components/table-footer/table-footer.class.js +94 -8
  29. package/components/table-footer/table-footer.d.ts +6 -2
  30. package/components/table-footer/table-footer.yaml +57 -6
  31. package/components/table-toolbar/table-toolbar.a2ui.json +28 -2
  32. package/components/table-toolbar/table-toolbar.class.js +69 -2
  33. package/components/table-toolbar/table-toolbar.css +36 -2
  34. package/components/table-toolbar/table-toolbar.d.ts +8 -2
  35. package/components/table-toolbar/table-toolbar.examples.md +6 -4
  36. package/components/table-toolbar/table-toolbar.yaml +140 -26
  37. package/components/theme-provider/theme-provider.a2ui.json +3 -5
  38. package/components/theme-provider/theme-provider.class.js +9 -30
  39. package/components/theme-provider/theme-provider.d.ts +3 -5
  40. package/components/theme-provider/theme-provider.yaml +4 -8
  41. package/core/element.js +16 -3
  42. package/custom-elements.json +52 -10
  43. package/dist/host.min.css +1 -1
  44. package/dist/host.sheet.js +1 -1
  45. package/dist/theme-provider.min.js +8 -8
  46. package/dist/themes.min.css +1 -1
  47. package/dist/themes.sheet.js +1 -1
  48. package/dist/web-components.min.css +1 -1
  49. package/dist/web-components.min.js +70 -69
  50. package/dist/web-components.sheet.js +1 -1
  51. package/index.css +1 -2
  52. package/package.json +1 -1
  53. package/styles/README.md +1 -1
  54. package/styles/api/sizing.css +1 -1
  55. package/styles/colors/material-static.css +34 -12
  56. package/styles/host.css +1 -4
  57. package/styles/index.css +2 -2
  58. package/styles/scale.css +19 -26
  59. package/styles/themes.css +2678 -2
  60. package/styles/type/elements.css +1 -0
  61. package/styles/type/roles.css +3 -3
  62. package/styles/typography.css +6 -5
  63. package/dist/prose.min.css +0 -1
  64. package/dist/prose.sheet.js +0 -11
  65. package/dist/verse.min.css +0 -1
  66. package/dist/verse.sheet.js +0 -11
  67. package/styles/prose.css +0 -211
  68. package/styles/verse.css +0 -151
package/CHANGELOG.md CHANGED
@@ -1,7 +1,57 @@
1
1
  # Changelog — @adia-ai/web-components
2
2
 
3
+ ## [0.8.50] — 2026-08-23
4
+
5
+ ### Added
6
+ - **`stat-ui` KPI tile layout — full-bleed chart band + overlaid delta chip (gh#1835, PR #1920, ADR-0083).** New `[bleed]` (with the existing `[band]`) composition on the chart slot lets the chart reach the card's edges — a right-column bleed alone, a three-edge bottom band together with `band` — with the value/label/change stack overlaying it. New public CSS custom properties: `--stat-change-inset`, `--stat-change-size`, and the six `--stat-change-chip-*` tokens (`bg`, `fg`, `font-size`, `pad-x`, `pad-y`, `radius`). (Entry added at cut time — PR #1920 merged without one.)
7
+
8
+ ### Removed
9
+ - **BREAKING: `[prose]`/`[verse]` deprecated attribute sheets removed (gh#1885, closes gh#1822).** `styles/prose.css` and `styles/verse.css` — deprecated since v0.8.11 in favor of the ancestor `[scale]` register — are deleted outright, along with the `[prose]`/`[verse]` HTML attributes, `<theme-provider scale="prose"|"verse">`'s legacy alias mapping, and the a2ui catalog's deprecated `prose` `DynamicBoolean` prop (`common_types.json`). No deprecation alias — use `scale="content-md"` / `scale="ui-sm"` (byte-identical rendering). Migration guide: `.claude/docs/MIGRATION GUIDE.md` § Shipped in `0.8.50`. Ships in this PATCH cut per Kim ruling of 2026-08-23 PDT, held-items entry dated 2026-08-24 UTC (recorded invariant-7/IDR-0004 deviation).
10
+
11
+ ### Changed
12
+ - **BREAKING: `input-ui`/`search-ui` fill-default sizing posture, `[inline]` hugs (ADR-0077, Steps 1-2, gh#1856).** A form control dropped into a flex toolbar row with no explicit width/flex collapsed to its content/floor width instead of filling the row's remaining space, because `input-ui`'s 20ch legibility floor (gh#781) applied unconditionally on the plain `:scope` — fighting any container that wanted to size it (same root-cause class as gh#1633's select-ui/combobox-ui collapse). `input-ui`'s `:scope` now fills (`width: 100%`) by default; `--input-min-width` moves to `:scope[inline]`, which also flips to inline-level display + `fit-content` per ADR-0037 §2 hug mechanics. `search-ui` gets the same fill-default posture on its own host and forwards its `[inline]` attribute onto its internal `<input-ui>` (light-DOM composition, so `:scope[inline]` can't reach across that boundary) via a hand-rolled `observedAttributes`/`attributeChangedCallback`. `select-ui`/`combobox-ui`/`date-range-picker-ui` remain on the old posture, scheduled as later plan waves (`docs/ops/plan/plan-form-control-sizing.md`). See `.claude/docs/MIGRATION GUIDE.md`.
13
+
14
+ ### Maintenance
15
+ - **`components/` touched in this release window** (38 file(s), e.g. `chart/chart.class.js`) — carried by the entries above.
16
+ - **`core/` touched in this release window** (2 file(s), e.g. `core/element.js`) — carried by the entries above.
17
+ - **`dist/` bundles rebuilt** in this cut's window (8 file(s)) — regenerated from the source changes described above, not independent edits.
18
+
19
+ ## [0.8.48] — 2026-08-23
20
+
21
+ ### Added
22
+ - **`table-ui`/`table-footer-ui`/`table-toolbar-ui` range grammar — `open-total`, `range-of`, `range-noun` (gh#1877), ADR-0082 Amendment.** Server-side pagination's range summary ("1–20 of 200") gets three composable authoring knobs: `open-total` drops the known-total suffix for unbounded/streamed counts, `range-of` swaps the connector word, `range-noun` supplies the counted-thing label — wired through `components/table/table.css`, `components/table-footer/table-footer.js`, and `components/table-toolbar/table-toolbar.js` (#1890).
23
+ - **`table-toolbar-ui[title-always]` opt-out for the scope-reconciliation title hide (gh#1879).** The `:has([data-scope]:not(:empty)) [data-title]` reconciliation rule (landed in 0.8.47) drops the title whenever a scope chip is present; `title-always` opts a toolbar out so the title survives alongside scope chips (#1880).
24
+
25
+ ### Changed
26
+ - **`link-ui`/`anchor-ui`-bearing surfaces get hover tokens, transitions, and a new `--link-fg-disabled` (gh#1881).** Every anchor-bearing component now declares `--link-fg-hover`/`--link-fg-active` plus a `color`/`text-decoration-color` transition instead of relying on inherited defaults; `--link-fg-disabled` fills the missing disabled-state role (#1887).
27
+ - **`[prose]`/`[verse]` live usages swept to `scale=` (gh#1822).** Component demos and yaml examples using the deprecated `[prose]`/`[verse]` attribute pair migrated to the `scale=` contract ratified in an earlier cut (#1874).
28
+
29
+ ### Fixed
30
+ - **`card-ui` preserves footer top inset in content-sized cards (gh#1866).** A content-sized (non-fixed-height) card's footer lost its top inset because the inset was conditioned on a fixed-height layout branch that content-sized cards never take; the inset now applies unconditionally (`components/card/card.css`) (#1875).
31
+ - **A2UI runtime stops stamping synthetic `.id` onto elements with no authored id.** `resolve-node.js`'s synthetic-id path was firing for every unauthored element, polluting the DOM with runtime-only ids that leaked into snapshot tests and event targeting; now only elements that need an id for event wiring get one (#1872).
32
+ - **A2UI runtime reflects `JS_PROPS` as JSON attributes alongside property writes (gh#1858).** `prop-apply.js` wrote JS property values but skipped the matching attribute reflection for `JS_PROPS`-classified props, so attribute-based selectors and SSR snapshots saw a stale value; both now update together (#1870).
33
+ - **`themes.css`/`material-static.css` theme-scoped semantic alias resolution under descendant `[theme]` scopes (gh#1863).** A `--a-*` semantic alias resolved against the wrong `[theme]` ancestor when a themed subtree nested inside another themed subtree; two new gates guard the resolution order (#1873).
34
+
35
+ ### Docs
36
+ - **`site/components/table-toolbar` canonical sample composes the toolbar as a `card-ui` header (#1884).** Replaces the prior bare-toolbar sample with the pattern most consumers actually use.
37
+ - **`components/chart/chart.yaml` `ratio=` comment repaired** — the per-type wave landed in an earlier cut; the yaml comment still described the old single-ratio contract (#1871).
38
+
39
+ ### Maintenance
40
+ - **`dist/` bundles rebuilt** in this cut's window — regenerated from the source changes described above, not independent edits.
41
+ - **`styles/` touched in this release window** (`colors/material-static.css`, `themes.css`) — carried by the theme-scoped alias fix above (gh#1863, #1873).
42
+
3
43
  ## [0.8.47] — 2026-08-22
4
44
 
45
+ ### Changed
46
+ - **`nav-group-ui`/`nav-item-ui`/`nav-ui` token simplification (gh#1740, gh#459, gh#1736).** `nav-group-ui` header text settles on one unconditional NEUTRAL-HIGH (`--nav-group-fg-active` deleted; `--nav-group-fg-selected` kept for the popover's current-option row); `nav-ui[data-nav-label]` moves from `--a-fg-muted` to plain NEUTRAL with retuned padding (top 16 / sides 4 / bottom 8). Dev server: `vite.config.js` gets an explicit `server.fs.allow`/`deny` — fixes vite's default `**/.git/**` deny blocking its own `/@vite/client` after resolving pnpm symlinks into a bootstrapped worktree's shared store (#1853).
47
+
48
+ ### Fixed
49
+ - **`material-static.css`'s identical-leg `light-dark()` roles dropped; stale `themes.css` dead-code claim corrected (gh#1852).** ~224 solid-fill/hover/active/on-*/alpha-family `--md-sys-color-*` roles wrapped in a no-op `light-dark()` (both legs identical) — unwrapped. The header's second claim, that `themes.css` ships a dead divergent neutral ramp, was a false premise on investigation (a themed ancestor's override wins via ordinary custom-property inheritance, verified in Playwright) — corrected in place rather than deleting live theming infrastructure (#1854).
50
+ - **`table-toolbar-ui[size]` prop, overflow-trigger cascade tie, and three min-width floor fights.** New `size` prop wired via ambient CSS inheritance; the `[data-toolbar-overflow]` cascade tie with `menu-ui`'s base rule (redundant "More" trigger re-render) fixed; `:has([data-scope]:not(:empty)) [data-title]` reconciliation drops a duplicate title/count cluster; toolbar/input/select/menu min-width floors reconciled (#1855).
51
+ - **`core/anchor.js` popover-anchor reuse guard (all 12 consumers).** A trigger reused as the anchor for a second popover had its anchor-name clobbered by the first popover's async close-cleanup, landing the second popover at the viewport origin — fixed with an ownership guard in the cleanup closure (284 consumer tests green) (#1855).
52
+
53
+ ### Docs
54
+ - **`site/components/table-toolbar` side-by-side docs demos.** `[data-preview-split]` opt-in pairs a demo's live render with its authored HTML side by side; table-toolbar visual goldens re-baselined after the verified layout fixes (#1855).
5
55
 
6
56
  ### Maintenance
7
57
  - **`components/` touched in this release window** (12 file(s), e.g. `nav-group/nav-group.css`) — carried by the entries above.
package/MIGRATION.md CHANGED
@@ -90,6 +90,10 @@ Version cuts and migration scope:
90
90
 
91
91
  - **Shipped in `0.8.43` (three 0.8.41-era deprecated components removed — `toggle-group-ui`/`toggle-option-ui`, `progress-row-ui`, `datetime-picker-ui`, gh#1617)** — **BREAKING.** Each shipped its `status: deprecated` soft-deprecation additively in `0.8.41` (its own entry below); all three are now removed outright per the same operator ruling (gh#1562, IDR-0004 deviation). None of the three tags register as custom elements anymore. Migrate: `<toggle-group-ui value>` + `<toggle-option-ui>` children → `<segmented-ui multiple value>` + `<segment-ui>` children (`<toggle-group-ui single>` → plain `<segmented-ui>`, no `[multiple]`); `<progress-row-ui label value meta>` → `<progress-ui label value meta>` (mechanical tag rename); `<datetime-picker-ui>` → `<calendar-picker-ui precision="minute">`. See [§ Shipped in `0.8.43` — three 0.8.41-era deprecated components removed](#shipped-in-0843--three-0841-era-deprecated-components-removed-toggle-group-uitoggle-option-ui-progress-row-ui-datetime-picker-ui-gh1617).
92
92
 
93
+ - **Shipped in `0.8.50` (Kim ruling of 2026-08-23 PDT (held-items entry dated 2026-08-24 UTC) — invariant-7/IDR-0004 recorded deviation, ships in a PATCH cut)** — **BREAKING:** the `[prose]`/`[verse]` deprecated attribute sheets (`styles/prose.css`, `styles/verse.css`, deprecated since `0.8.11`) are removed outright, with no deprecation alias — `[prose]`/`[verse]` and `<theme-provider scale="prose"|"verse">` no longer resolve. Use `scale="content-md"` / `scale="ui-sm"` instead (byte-identical rendering). The a2ui catalog's deprecated `prose` `DynamicBoolean` prop is also dropped from `common_types.json`. (gh#1885, closing gh#1822.) See [§ Shipped in `0.8.50` — `[prose]`/`[verse]` sheets removed](#shipped-in-0850--proseverse-sheets-removed-gh1885-gh1822).
94
+
95
+ - **Shipped in `0.8.50` (`input-ui`/`search-ui` fill-default sizing posture, ADR-0077, gh#1856)** — **BREAKING, no deprecation alias.** A form control dropped into a flex toolbar row with no explicit width/flex used to collapse to its content/floor width instead of filling the row's remaining space (visible as clipped search placeholder text in consumer admin surfaces), because `input-ui`'s 20ch legibility floor (gh#781) applied unconditionally on the plain `:scope`. `<input-ui>`'s `:scope` now fills (`width: 100%`) by default; the old floor (`--input-min-width`) moves to `:scope[inline]`, which also switches to inline-level display + `fit-content` per ADR-0037 §2 hug mechanics. `<search-ui>` gets the same fill-default posture and forwards `[inline]` onto its internal `<input-ui>`. Migration: any consumer relying on the old shrink-wrap default (e.g. an unconstrained `<input-ui>`/`<search-ui>` inline among text, expecting it to hug its content) must add `[inline]` explicitly. `select-ui`/`combobox-ui`/`date-range-picker-ui` are unaffected — same old posture, later plan waves. See [§ Shipped in `0.8.50` — input-ui/search-ui fill-default sizing posture](#shipped-in-0850--input-uisearch-ui-fill-default-sizing-posture-adr-0077-gh1856).
96
+
93
97
  Live demos and per-component docs at [ui-kit.exe.xyz/site/](https://ui-kit.exe.xyz/site/). The `adia-ui-migration` skill (in `.agents/skills/`) automates most of the mechanical sweeps in this guide.
94
98
 
95
99
  ---
@@ -1887,6 +1891,138 @@ npm run check
1887
1891
 
1888
1892
  ---
1889
1893
 
1894
+ ## Shipped in `0.8.50` — `[prose]`/`[verse]` sheets removed (gh#1885, gh#1822)
1895
+
1896
+ **BREAKING, no deprecation alias.** `styles/prose.css` and `styles/verse.css`
1897
+ — deprecated since `0.8.11` in favor of the ancestor `[scale]` register — are
1898
+ removed outright (gh#1885, closing gh#1822's open question). The `[prose]`/
1899
+ `[verse]` HTML attributes and `<theme-provider scale="verse">`/
1900
+ `scale="prose"`'s legacy aliases no longer resolve to anything. All live
1901
+ in-repo usage had already migrated to `scale="content-md"`/`scale="ui-sm"`
1902
+ in PR #1874; this cut deletes the byte-identical fallback sheets those
1903
+ attributes depended on.
1904
+
1905
+ ### `[prose]` → `scale="content-md"` (`prose` → `content-md`)
1906
+
1907
+ `[prose]` was the long-form/reading register — byte-identical to
1908
+ `scale="content-md"`.
1909
+
1910
+ **Audit:**
1911
+ ```bash
1912
+ git grep -nE '\[prose\]|<[a-z-]+ prose\b|prose="'
1913
+ ```
1914
+
1915
+ **Sweep:**
1916
+ ```bash
1917
+ git grep -lz -E '\bprose\b' -- '*.html' '*.js' | while IFS= read -r -d '' f; do
1918
+ perl -i -pe 's/\bprose(=""|="true")?\b(?!-)/scale="content-md"/g if /<[a-z-]+[^>]*\bprose\b/' "$f"
1919
+ done
1920
+ # Boolean-attribute rewrites are structural, not string-substitutable in
1921
+ # general — review each hit; the sweep above only rewrites the exact
1922
+ # `<tag prose>` / `<tag prose="">` shape. A theme-provider scale="prose"
1923
+ # call site is a plain value rename instead:
1924
+ git grep -lz 'scale="prose"' | while IFS= read -r -d '' f; do
1925
+ perl -i -pe 's/scale="prose"/scale="content-md"/g' "$f"
1926
+ done
1927
+ ```
1928
+
1929
+ ### `[verse]` → `scale="ui-sm"` (`verse` → `ui-sm`)
1930
+
1931
+ `[verse]` was the dense/compact-product-UI register — byte-identical to
1932
+ `scale="ui-sm"`.
1933
+
1934
+ **Audit:**
1935
+ ```bash
1936
+ git grep -nE '\[verse\]|<[a-z-]+ verse\b|verse="'
1937
+ ```
1938
+
1939
+ **Sweep:**
1940
+ ```bash
1941
+ git grep -lz -E '\bverse\b' -- '*.html' '*.js' | while IFS= read -r -d '' f; do
1942
+ perl -i -pe 's/\bverse(=""|="true")?\b(?!-)/scale="ui-sm"/g if /<[a-z-]+[^>]*\bverse\b/' "$f"
1943
+ done
1944
+ git grep -lz 'scale="verse"' | while IFS= read -r -d '' f; do
1945
+ perl -i -pe 's/scale="verse"/scale="ui-sm"/g' "$f"
1946
+ done
1947
+ ```
1948
+
1949
+ ### The a2ui catalog `prose` prop is gone too
1950
+
1951
+ `common_types.json`'s deprecated `prose` `DynamicBoolean` prop (mapped onto
1952
+ the legacy `[prose]` attribute) is removed from `ComponentCommon`. Generated
1953
+ a2ui documents carrying `"prose": true` on any node no longer validate —
1954
+ regenerate via the composer, or hand-migrate to an ancestor
1955
+ `{"component": "ThemeProvider", "scale": "content-md"}` wrapper.
1956
+
1957
+ ### Verify
1958
+
1959
+ ```bash
1960
+ git grep -nE '\[prose\]|\[verse\]|prose-ui|verse-ui|prose\.css|verse\.css' \
1961
+ -- ':!CHANGELOG.md' ':!**/CHANGELOG*.md' ':!docs/ops/releases/**' \
1962
+ ':!.claude/ops/**' ':!**/HANDOFF*.md' ':!**/BUILD.md' \
1963
+ ':!.claude/docs/specs/cascade-layer-architecture.md' \
1964
+ ':!.claude/docs/MIGRATION GUIDE.md' ':!packages/web-components/MIGRATION.md'
1965
+ node scripts/build/components.mjs --verify
1966
+ npm run check
1967
+ ```
1968
+
1969
+ ---
1970
+
1971
+ ## Shipped in `0.8.50` — `input-ui`/`search-ui` fill-default sizing posture (ADR-0077, gh#1856)
1972
+
1973
+ **BREAKING, no deprecation alias.** A form control dropped into a flex
1974
+ toolbar row with no explicit width/flex collapsed to its content/floor
1975
+ width instead of filling the row's remaining space (visible as clipped
1976
+ search placeholder text in consumer admin surfaces), because `input-ui`'s
1977
+ 20ch legibility floor (gh#781) applied unconditionally on the plain
1978
+ `:scope`, fighting any container that wanted to size it. Same root-cause
1979
+ class as gh#1633's select-ui/combobox-ui collapse.
1980
+
1981
+ Implements Steps 1-2 of `docs/ops/plan/plan-form-control-sizing.md` against
1982
+ the ratified `docs/ops/adr/adr-0077-form-control-sizing-posture.md` /
1983
+ `spec-form-control-sizing-posture`: form controls fill their container by
1984
+ default; `[inline]` opts into the old shrink-wrap/hug behavior, where the
1985
+ legibility floor now lives instead of on every state.
1986
+
1987
+ ### What changed
1988
+
1989
+ - **`input-ui`** — `:scope` fills (`width: 100%`) by default; `--input-min-width`
1990
+ moves to `:scope[inline]`, which also flips to inline-level display +
1991
+ `fit-content` per ADR-0037 §2 hug mechanics.
1992
+ - **`search-ui`** — same fill-default posture on its own host. Forwards its
1993
+ `[inline]` attribute onto the internal `<input-ui>` (light-DOM
1994
+ composition — CSS `:scope[inline]` can't reach across that boundary) via
1995
+ a hand-rolled `observedAttributes`/`attributeChangedCallback`.
1996
+ - **Unaffected:** `select-ui`/`combobox-ui`/`date-range-picker-ui` remain on
1997
+ the old posture — scheduled as later `plan-form-control-sizing.md` waves.
1998
+
1999
+ ### Migration
2000
+
2001
+ Any consumer relying on the previous shrink-wrap default — an unconstrained
2002
+ `<input-ui>`/`<search-ui>` placed inline among text or in a layout that
2003
+ expected it to hug its content rather than stretch — must add `[inline]`
2004
+ explicitly:
2005
+
2006
+ ```html
2007
+ <!-- before (0.8.48 and earlier): hugged its content by default -->
2008
+ <input-ui placeholder="Search…"></input-ui>
2009
+
2010
+ <!-- after: fills its container by default; opt into the old hug -->
2011
+ <input-ui inline placeholder="Search…"></input-ui>
2012
+ ```
2013
+
2014
+ A control already given an explicit width, or already inside a container
2015
+ that constrains it (a `card-ui`, a fixed-width `field-ui`), needs no change.
2016
+
2017
+ ### Verify
2018
+
2019
+ ```bash
2020
+ node scripts/build/components.mjs --verify
2021
+ npm run check
2022
+ ```
2023
+
2024
+ ---
2025
+
1890
2026
  ## Migrating to `@adia-ai/*@v0.8.23` (2026-07-30)
1891
2027
 
1892
2028
  - **Removed: the `--a-font` alias** (pre-ADR-0035 name, DEPRECATED since the 0.8.x line). Any surviving `font-family: var(--a-font)` goes invalid-at-computed-value-time → `inherit` → UA **serif** — the visible symptom is Times-faced labels on an otherwise-correct page.
@@ -76,8 +76,30 @@
76
76
  display: flex;
77
77
  flex-direction: column;
78
78
  }
79
+ /* Explicit (not left to the base `& > footer` rule further down) so this
80
+ value is the one that wins regardless of declaration order — kept
81
+ alongside the bottom-pin spacer it replaces, gh#1866. */
79
82
  :scope > footer {
80
- margin-top: auto;
83
+ margin-block-start: var(--card-inset);
84
+ }
85
+
86
+ /* Bottom-pin (gh#559), preserved WITHOUT sacrificing the footer's own
87
+ top inset (gh#1866). The old approach put `margin-top: auto` directly
88
+ on the footer: that absorbs FREE SPACE in an equal-height grid/flex
89
+ row (the pin), but a content-sized card has none to absorb, so the
90
+ auto margin resolved to 0 and the footer lost its --card-inset gap
91
+ entirely — every OTHER region kept its inset, only the footer didn't.
92
+ Moving the auto margin to the region immediately before the footer
93
+ keeps both behaviors distinct: that region still soaks up any
94
+ leftover height (still bottom-pinning the footer in an equal-height
95
+ card), while the footer's own margin-block-start above stays the
96
+ ordinary --card-inset in every card, content-sized included.
97
+ `:has(+ footer)` (not `:last-of-type`) so exactly the ONE region
98
+ directly adjacent to the footer gets the spacer — `:last-of-type` would
99
+ match the last header AND the last section independently when both are
100
+ present, splitting the auto margin across two elements instead of one. */
101
+ :scope:has(> footer) > :is(header, section):has(+ footer) {
102
+ margin-block-end: auto;
81
103
  }
82
104
 
83
105
  /* Flex containers never collapse adjacent margins the way block flow
@@ -87,7 +109,8 @@
87
109
  single inset block flow would have collapsed them to. The leading
88
110
  region's own margin-bottom already supplies that inset; zero the
89
111
  trailing region's margin-top. footer is excluded — its margin-top is
90
- handled above (bottom-pin `auto`) or by [divider] below. */
112
+ handled above (its own explicit --card-inset, gh#1866) or by
113
+ [divider] below. */
91
114
  :scope:has(> footer) > :is(header, section) + :is(header, section) {
92
115
  margin-block-start: 0;
93
116
  }
@@ -118,7 +118,7 @@
118
118
  "default": false
119
119
  },
120
120
  "ratio": {
121
- "description": "Chart 2.0 aspect-ratio bucket (ADR-0074). Unset (default) auto-snaps to the nearest of the three studied ratios by comparing the chart's own live box aspect against two midpoint boundaries (5:4 between 3:2/1:1, 4:5 between 1:1/2:3) — never interpolated; the resolved bucket reflects onto the host as `data-ratio-resolved`. Setting `ratio` explicitly pins that bucket regardless of the box's actual aspect, overriding the auto-snap. Orthogonal to `size` (overall scale) and `type`/`color` (ADR-0064's family axis) — this attribute lands the grammar + snap plumbing only; per-type studied renderings per bucket are later waves (gh#1624 plan steps 2-4).",
121
+ "description": "Chart 2.0 aspect-ratio bucket (ADR-0074). Unset (default) auto-snaps to the nearest of the three studied ratios by comparing the chart's own live box aspect against two midpoint boundaries (5:4 between 3:2/1:1, 4:5 between 1:1/2:3) — never interpolated; the resolved bucket reflects onto the host as `data-ratio-resolved`. Setting `ratio` explicitly pins that bucket regardless of the box's actual aspect, overriding the auto-snap. Orthogonal to `size` (overall scale) and `type`/`color` (ADR-0064's family axis) — per-type studied renderings per bucket shipped across Waves A-C (gh#1624 plan steps 2-4: #1658 bar/line/area/pie/donut/sparkline, #1659 radar/scatter/ radial-bar/gauge/segments/stacked-bar/grouped-bar/multi-line, #1662 funnel/treemap/sankey/composed) — all 18 types have dedicated per-ratio render paths.",
122
122
  "type": "string",
123
123
  "enum": [
124
124
  "3:2",
@@ -569,7 +569,14 @@ export class UIChart extends UIElement {
569
569
  if (!this.#data.length) return;
570
570
  const rw = Math.round(w);
571
571
  const rh = Math.round(h);
572
- if (rw === this.#lastW && (!this.hasAttribute('resize') || rh === this.#lastH)) return;
572
+ // gh#1901 #dims() now trusts a measured clientHeight for EVERY
573
+ // chart (not only `resize`-attributed ones, see #dims() above), so
574
+ // a height-only change (e.g. `--stat-chart-band-height` changing,
575
+ // or the box's own CSS height rule flipping via a media query)
576
+ // must re-render regardless of the `resize` attribute — gating
577
+ // height changes behind `resize` froze the viewBox on exactly
578
+ // that class of change.
579
+ if (rw === this.#lastW && rh === this.#lastH) return;
573
580
  this.#lastW = rw;
574
581
  this.#lastH = rh;
575
582
  if (this.#resizeRaf) return;
@@ -614,18 +621,42 @@ export class UIChart extends UIElement {
614
621
  const n = this.#data.length || 1;
615
622
 
616
623
  const width = Math.max(containerW, 120);
617
- // When element has explicit height (resize handle, CSS height, inline style),
618
- // use it. Otherwise derive from aspect ratio. Safe because resize sets
619
- // overflow:auto which prevents content from pushing height.
620
- const hasExplicitHeight = this.hasAttribute('resize') || this.style.height;
621
- // ADR-0074 — an explicit `ratio` PINS the box's own aspect, overriding
622
- // whatever height the container/explicit-height path would otherwise
623
- // give (REQ-R-002: "regardless of the container's actual aspect
624
- // ratio"). Unset falls through to the pre-existing behavior unchanged.
624
+ // gh#1901 trust a measured clientHeight above the noise floor
625
+ // regardless of WHERE the height came from (inline style, the
626
+ // `resize` handle, or a plain CSS rule like
627
+ // `stat-ui[band] [slot="chart"] { height: … }`). Checking only
628
+ // `this.style.height`/`resize` missed the CSS-rule case entirely
629
+ // an author can size the box from a stylesheet just as validly as
630
+ // from an inline style, and #dims() has no way to tell those apart:
631
+ // `getComputedStyle(this).height` resolves to a used-value px length
632
+ // for ANY laid-out box (an explicit `height:`, an `aspect-ratio:`
633
+ // box, OR a plain `align-items:stretch` grid/flex item with no
634
+ // declared size at all) — there is no computed-style read that
635
+ // reports "an author declared this" versus "layout resolved this".
636
+ //
637
+ // KNOWN TRADE-OFF (gh#1901 review) — because of that, a chart-ui
638
+ // dropped bare into a generic grid/flex row also gets stretched to
639
+ // match a taller sibling's track/row, and that stretched height is
640
+ // trusted the same as a deliberate `height:`/`aspect-ratio:` rule
641
+ // (see chart.test.js's "ambient grid-stretch height" fixture, which
642
+ // asserts and documents this rather than hiding it). This is the
643
+ // same default a plain `<img>`/`<video>` gets in that layout — no
644
+ // intrinsic size fills whatever the stretched box gives it — and it
645
+ // is consistent with the intentional stretch-to-bleed already used by
646
+ // `stat-ui[bleed] [slot="chart"]` (`align-self:stretch; height:auto`,
647
+ // stat.css), which relies on exactly this same measured-height path.
648
+ // A narrower heuristic (e.g. gating on a specific computed-style
649
+ // signal) would have to reject that intentional case too, since
650
+ // mechanically it is indistinguishable from the accidental one.
651
+ // Below the 40px floor there's nothing to trust yet (pre-layout/
652
+ // empty box), so fall back to the aspect-ratio guess; once real
653
+ // content has rendered once, this stays self-consistent on every
654
+ // re-measure (a fallback-derived height reproduces the same value
655
+ // next time).
625
656
  const pinnedRatio = CHART_RATIOS[this.ratio];
626
657
  let height = pinnedRatio
627
658
  ? Math.round(width / pinnedRatio)
628
- : hasExplicitHeight && containerH > 40
659
+ : containerH > 40
629
660
  ? containerH
630
661
  : Math.round(width / aspect.ratio);
631
662
 
@@ -40,7 +40,7 @@ export class UIChart extends UIElement {
40
40
  noGrid: boolean;
41
41
  /** Hide value labels. Canonical spelling (ADR-0063, gh#1563) — the deprecated `hideValues` dual-read shim was cut in 0.8.43 (gh#1617). */
42
42
  noValues: boolean;
43
- /** Chart 2.0 aspect-ratio bucket (ADR-0074). Unset (default) auto-snaps to the nearest of the three studied ratios by comparing the chart's own live box aspect against two midpoint boundaries (5:4 between 3:2/1:1, 4:5 between 1:1/2:3) — never interpolated; the resolved bucket reflects onto the host as `data-ratio-resolved`. Setting `ratio` explicitly pins that bucket regardless of the box's actual aspect, overriding the auto-snap. Orthogonal to `size` (overall scale) and `type`/`color` (ADR-0064's family axis) — this attribute lands the grammar + snap plumbing only; per-type studied renderings per bucket are later waves (gh#1624 plan steps 2-4). */
43
+ /** Chart 2.0 aspect-ratio bucket (ADR-0074). Unset (default) auto-snaps to the nearest of the three studied ratios by comparing the chart's own live box aspect against two midpoint boundaries (5:4 between 3:2/1:1, 4:5 between 1:1/2:3) — never interpolated; the resolved bucket reflects onto the host as `data-ratio-resolved`. Setting `ratio` explicitly pins that bucket regardless of the box's actual aspect, overriding the auto-snap. Orthogonal to `size` (overall scale) and `type`/`color` (ADR-0064's family axis) — per-type studied renderings per bucket shipped across Waves A-C (gh#1624 plan steps 2-4: #1658 bar/line/area/pie/donut/sparkline, #1659 radar/scatter/ radial-bar/gauge/segments/stacked-bar/grouped-bar/multi-line, #1662 funnel/treemap/sankey/composed) — all 18 types have dedicated per-ratio render paths. */
44
44
  ratio: '3:2' | '1:1' | '2:3';
45
45
  /** Multi-line only. Names one series key (matching a `y` key) to render at full strength — area fill + full-opacity line. Every other series drops its area fill and renders line-only at --chart-deemphasized-opacity. Empty (default) applies full treatment to every series, unchanged from pre-gh#561 behavior. */
46
46
  seriesEmphasis: string;
@@ -105,9 +105,12 @@ props:
105
105
  bucket reflects onto the host as `data-ratio-resolved`. Setting
106
106
  `ratio` explicitly pins that bucket regardless of the box's actual
107
107
  aspect, overriding the auto-snap. Orthogonal to `size` (overall
108
- scale) and `type`/`color` (ADR-0064's family axis) — this attribute
109
- lands the grammar + snap plumbing only; per-type studied renderings
110
- per bucket are later waves (gh#1624 plan steps 2-4).
108
+ scale) and `type`/`color` (ADR-0064's family axis) — per-type studied
109
+ renderings per bucket shipped across Waves A-C (gh#1624 plan steps
110
+ 2-4: #1658 bar/line/area/pie/donut/sparkline, #1659 radar/scatter/
111
+ radial-bar/gauge/segments/stacked-bar/grouped-bar/multi-line, #1662
112
+ funnel/treemap/sankey/composed) — all 18 types have dedicated
113
+ per-ratio render paths.
111
114
  type: string
112
115
  default: ""
113
116
  enum:
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://adiaui.dev/a2ui/v0_9/components/Input.json",
4
4
  "title": "Input",
5
- "description": "Text input field with contenteditable surface. Supports prefix/suffix icons, label, form participation, and a `type=\"number\"` mode that renders [+]/[-] stepper buttons, numeric input filtering, and ARIA spinbutton semantics — no native `<input type=\"number\">` under the hood. Password type uses a native `<input>` (only path that still wraps native, for `-webkit-text-security` disc masking). Has a 20ch minimum width (`--input-min-width`, mirrors the native `<input>` baseline) so a bare, placeholder-only instance stays usable inside a flex row (row-ui) with no explicit sizing explicit `width`/`flex` or CSS can size the host above the floor; values below the floor remain clamped unless `--input-min-width` is lowered.",
5
+ "description": "Text input field with contenteditable surface. Supports prefix/suffix icons, label, form participation, and a `type=\"number\"` mode that renders [+]/[-] stepper buttons, numeric input filtering, and ARIA spinbutton semantics — no native `<input type=\"number\">` under the hood. Password type uses a native `<input>` (only path that still wraps native, for `-webkit-text-security` disc masking). Fills its container's available inline width by default (ADR-0077 / spec-form-control-sizing-posture) in a flex row (row-ui) or grid track it tracks the container all the way down, with no host floor. `[inline]` opts into the old shrink-wrap behavior and is where the 20ch minimum width (`--input-min-width`, mirrors the native `<input>` baseline) survives, guarding a bare, placeholder-only instance from collapsing to near-zero as a hugging flex item.",
6
6
  "type": "object",
7
7
  "allOf": [
8
8
  {
@@ -370,7 +370,7 @@
370
370
  "description": "Override line height"
371
371
  },
372
372
  "--input-min-width": {
373
- "description": "Override the minimum-width floor (20ch, mirrors the native `<input>` baseline). Keeps an empty, placeholder-only instance legible when placed directly in a flex row (row-ui) with no explicit width/flex sizing — the placeholder renders via an out-of-flow ::before that contributes no intrinsic width of its own, so without this floor the host collapses to near-zero as a flex item. Explicit sizing (width/flex) can grow the host above the floor but does not bypass it — set to 0 or another lower value to permit a narrower explicit size."
373
+ "description": "Override the minimum-width floor (20ch, mirrors the native `<input>` baseline), applied only in the `[inline]` hug state (ADR-0077 / spec-form-control-sizing-posture REQ-F-001/REQ-F-002). Keeps an empty, placeholder-only `[inline]` instance legible — the placeholder renders via an out-of-flow ::before that contributes no intrinsic width of its own, so without this floor a hugging instance collapses to near-zero. The default (non-`[inline]`) fill state carries no floor at all — the container owns the width all the way down. Set to 0 or another lower value to permit a narrower `[inline]` instance."
374
374
  },
375
375
  "--input-padding": {
376
376
  "description": "Override base padding (density-scaled)"
@@ -32,10 +32,14 @@ input-ui:not([disabled]) [slot="field"]:hover [slot="suffix"] {
32
32
  --input-affix-fg: var(--a-ui-text-placeholder);
33
33
  --input-field-gap: var(--a-space-1);
34
34
  --input-label-min-width: 8ch;
35
- /* Native-<input> baseline (historically ~20 characters). Floors the
36
- host so an empty, placeholder-only instance stays legible as a
37
- flex item see the :scope min-width rule below for the collapse
38
- mechanism this guards against. */
35
+ /* Native-<input> baseline (historically ~20 characters). ADR-0077 /
36
+ spec-form-control-sizing-posture (REQ-F-001/REQ-F-002) moved this
37
+ floor from the plain :scope to :scope[inline] the default
38
+ (fill) posture carries no host floor at all; the container owns
39
+ the width all the way down. The floor survives ONLY in the hug
40
+ state, where it guards an empty, placeholder-only instance from
41
+ collapsing to near-zero (gh#781/gh#1633). See :scope[inline]
42
+ below for the rule that now consumes this token. */
39
43
  --input-min-width: 20ch;
40
44
 
41
45
  /* ── Transitions ── */
@@ -63,18 +67,43 @@ input-ui:not([disabled]) [slot="field"]:hover [slot="suffix"] {
63
67
  /* ── Base ── */
64
68
  box-sizing: border-box;
65
69
  display: block;
66
- /* `display: block` gives no width once this host becomes a flex item
67
- (row-ui, or any display:flex parent) flex items size from
68
- content/flex-basis, not display type. The one child that could
69
- drive content-based sizing, [slot="text"], is EMPTY when the
70
- input has no value; its placeholder renders via an out-of-flow
71
- ::before (deliberately, for caret positioning see the pseudo
72
- below) that contributes zero intrinsic width. Without a floor,
73
- an empty placeholder-only input-ui collapses to near-zero in any
74
- flex context that doesn't otherwise force a width (gh#781).
75
- min-width, not width, so explicit width/flex can size the host
76
- above the floor; values below the floor remain clamped. Override
77
- via --input-min-width (e.g. 0 to opt back out) or size explicitly. */
70
+ /* ADR-0077 / spec-form-control-sizing-posture REQ-P-001 (fill, the
71
+ default): `display: block` alone gives no width once this host
72
+ becomes a flex item (row-ui, or any display:flex parent) flex
73
+ items size from content/flex-basis, not display type, and the one
74
+ child that could drive content-based sizing, [slot="text"], is
75
+ EMPTY when the input has no value (its placeholder renders via an
76
+ out-of-flow ::before deliberately, for caret positioning, see
77
+ the pseudo below that contributes zero intrinsic width). So the
78
+ host declares the fill explicitly with `width: 100%`: in block
79
+ flow this is a no-op (width:auto already fills ADR-0037's
80
+ confirmed full-width default), in flex parents it over-constrains
81
+ the line and lets the shrink algorithm split real space among
82
+ fill-state siblings (REQ-P-002), and in grid parents it's
83
+ redundant-but-harmless against the track's own default stretch.
84
+ This is a deliberate SCOPE-EXTENT exception: the family contract
85
+ (REQ-R-001) requires it, the same mechanism field-ui (bug-34) and
86
+ otp-input-ui (bug-37) already ship. No host floor survives here —
87
+ the container owns the width all the way down (REQ-F-001); the
88
+ legibility floor moves to the hug state below. */
89
+ width: 100%;
90
+ }
91
+
92
+ /* ADR-0077 / spec-form-control-sizing-posture REQ-P-003 (hug, via
93
+ [inline]): the explicit opt-in for the old shrink-to-content
94
+ behavior. ADR-0037 §2's display-flip mechanics (block → inline-block)
95
+ plus `width: fit-content` — fit-content (not the absence of the fill
96
+ rule) because a grid item is blockified/stretch-aligned by default,
97
+ so a bare `width: auto` [inline] host would still render at track
98
+ width. The gh#781/gh#1633 legibility floor moves here from the plain
99
+ :scope (REQ-F-001/REQ-F-002): it guards an empty, placeholder-only
100
+ instance from collapsing to near-zero, exactly as it always has —
101
+ just scoped to the state where the control, not the container, owns
102
+ its size. Override --input-min-width to 0 to opt a hug instance back
103
+ into full collapse. */
104
+ :scope[inline] {
105
+ display: inline-block;
106
+ width: fit-content;
78
107
  min-width: var(--input-min-width);
79
108
  }
80
109
 
@@ -8,9 +8,10 @@ version: 1
8
8
  description: Text input field with contenteditable surface. Supports prefix/suffix icons, label, form participation, and a
9
9
  `type="number"` mode that renders [+]/[-] stepper buttons, numeric input filtering, and ARIA spinbutton semantics — no native
10
10
  `<input type="number">` under the hood. Password type uses a native `<input>` (only path that still wraps native, for `-webkit-text-security`
11
- disc masking). Has a 20ch minimum width (`--input-min-width`, mirrors the native `<input>` baseline) so a bare,
12
- placeholder-only instance stays usable inside a flex row (row-ui) with no explicit sizing explicit `width`/`flex`
13
- or CSS can size the host above the floor; values below the floor remain clamped unless `--input-min-width` is lowered.
11
+ disc masking). Fills its container's available inline width by default (ADR-0077 / spec-form-control-sizing-posture)
12
+ in a flex row (row-ui) or grid track it tracks the container all the way down, with no host floor. `[inline]` opts into
13
+ the old shrink-wrap behavior and is where the 20ch minimum width (`--input-min-width`, mirrors the native `<input>`
14
+ baseline) survives, guarding a bare, placeholder-only instance from collapsing to near-zero as a hugging flex item.
14
15
  # Per ADR-0027 — primitives that programmatically create other primitives
15
16
  # do NOT auto-import them. Consumer (or demo shell) must explicitly import.
16
17
  composes:
@@ -238,12 +239,13 @@ tokens:
238
239
  description: Override line height
239
240
  --input-min-width:
240
241
  description: Override the minimum-width floor (20ch, mirrors the native `<input>`
241
- baseline). Keeps an empty, placeholder-only instance legible when placed directly
242
- in a flex row (row-ui) with no explicit width/flex sizing the placeholder renders
243
- via an out-of-flow ::before that contributes no intrinsic width of its own, so
244
- without this floor the host collapses to near-zero as a flex item. Explicit sizing
245
- (width/flex) can grow the host above the floor but does not bypass it set to 0
246
- or another lower value to permit a narrower explicit size.
242
+ baseline), applied only in the `[inline]` hug state (ADR-0077 / spec-form-control-sizing-posture
243
+ REQ-F-001/REQ-F-002). Keeps an empty, placeholder-only `[inline]` instance legible
244
+ the placeholder renders via an out-of-flow ::before that contributes no intrinsic
245
+ width of its own, so without this floor a hugging instance collapses to near-zero.
246
+ The default (non-`[inline]`) fill state carries no floor at allthe container owns
247
+ the width all the way down. Set to 0 or another lower value to permit a narrower
248
+ `[inline]` instance.
247
249
  --input-padding:
248
250
  description: Override base padding (density-scaled)
249
251
  --input-placeholder-color:
@@ -10,6 +10,7 @@
10
10
  --link-fg: var(--a-link);
11
11
  --link-fg-hover: var(--a-link-hover);
12
12
  --link-fg-visited: var(--a-link-visited);
13
+ --link-fg-disabled: var(--md-sys-color-neutral-on-surface-disabled);
13
14
  --link-underline-offset: 2px;
14
15
  --link-focus-ring: var(--a-focus-ring);
15
16
  }
@@ -89,9 +90,9 @@
89
90
  /* ── Disabled ── */
90
91
 
91
92
  :scope[disabled] > a {
92
- color: var(--md-sys-color-neutral-on-surface-disabled);
93
+ color: var(--link-fg-disabled);
93
94
  cursor: not-allowed;
94
- text-decoration-color: var(--md-sys-color-neutral-on-surface-disabled);
95
+ text-decoration-color: var(--link-fg-disabled);
95
96
  pointer-events: none;
96
97
  }
97
98
  }
@@ -21,8 +21,8 @@
21
21
  page and inverted inside cards. Same tier as --a-bg-muted/hover. */
22
22
  --richtext-code-bg: var(--md-sys-color-neutral-container-low);
23
23
  --richtext-border: var(--md-sys-color-neutral-outline-variant);
24
- --richtext-link: var(--a-primary-bg);
25
- --richtext-link-hover: var(--md-sys-color-primary);
24
+ --richtext-link: var(--a-link);
25
+ --richtext-link-hover: var(--a-link-hover);
26
26
 
27
27
  /* ── Typography ── */
28
28
  --richtext-font: var(--a-font-family);
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://adiaui.dev/a2ui/v0_9/components/Search.json",
4
4
  "title": "Search",
5
- "description": "Search input with magnifying-glass icon, clear button, and debounced search event.",
5
+ "description": "Search input with magnifying-glass icon, clear button, and debounced search event. Fills its container's available inline width by default (ADR-0077 / spec-form-control-sizing-posture) — in a flex row (row-ui) or grid track it tracks the container all the way down. `[inline]` opts into the old shrink-wrap behavior and forwards onto the internal input-ui so its 20ch legibility floor applies.",
6
6
  "type": "object",
7
7
  "allOf": [
8
8
  {