@adia-ai/web-components 0.7.11 → 0.7.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 +29 -4
- package/README.md +7 -7
- package/USAGE.md +2 -2
- package/components/button/button.css +4 -3
- package/components/frame/frame.a2ui.json +11 -1
- package/components/frame/frame.yaml +19 -1
- package/components/theme-provider/theme-provider.a2ui.json +88 -0
- package/components/theme-provider/theme-provider.class.js +134 -0
- package/components/theme-provider/theme-provider.css +18 -0
- package/components/theme-provider/theme-provider.d.ts +45 -0
- package/components/theme-provider/theme-provider.js +22 -0
- package/components/theme-provider/theme-provider.test.js +94 -0
- package/components/theme-provider/theme-provider.yaml +96 -0
- package/dist/host.min.css +1 -1
- package/dist/host.sheet.js +11 -0
- package/dist/prose.min.css +1 -0
- package/dist/prose.sheet.js +11 -0
- package/dist/themes.min.css +1 -0
- package/dist/themes.sheet.js +11 -0
- package/dist/verse.min.css +1 -0
- package/dist/verse.sheet.js +11 -0
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.sheet.js +11 -0
- package/package.json +7 -1
- package/styles/README.md +1 -1
- package/styles/colors/parameters.css +1 -1
- package/styles/colors/primitives-accent.css +1 -1
- package/styles/colors/primitives-brand.css +1 -1
- package/styles/colors/primitives-danger.css +1 -1
- package/styles/colors/primitives-info.css +1 -1
- package/styles/colors/primitives-neutral.css +1 -1
- package/styles/colors/primitives-success.css +1 -1
- package/styles/colors/primitives-warning.css +1 -1
- package/styles/colors/scrims.css +1 -1
- package/styles/colors/semantics/aliases.css +1 -1
- package/styles/colors/semantics/buckets.css +1 -1
- package/styles/colors/semantics/core.css +1 -1
- package/styles/colors/semantics/data-viz.css +1 -1
- package/styles/colors/semantics/features.css +1 -1
- package/styles/colors/surfaces.css +1 -1
- package/styles/components.css +1 -0
- package/styles/design-tokens-export.js +1 -1
- package/styles/host.css +1 -1
- package/styles/prose.css +1 -1
- package/styles/themes.css +12 -12
- package/web-components.sheet.d.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog — @adia-ai/web-components
|
|
2
2
|
|
|
3
|
+
## [0.7.13] — 2026-06-06
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **`./css/sheet` subpath export now resolves TS types.** The 0.7.12 `.sheet.js` twin (constructable-`CSSStyleSheet` mirror of `web-components.min.css`) shipped its `./css/sheet` export with `import`/`default` but no `types` — TS consumers got no types for `constructSheet()`. Added `web-components.sheet.d.ts` (`export function constructSheet(): CSSStyleSheet | null`), the `types` condition (types-first), and the `.d.ts` to `files[]` so the tarball packs it. Caught by `verify:exports-conditionals`.
|
|
7
|
+
- **`button-ui[color=warning]` hover contrast** — the warning fill's hover surface now uses `--a-warning-bg` (the -20 tint) instead of `--a-warning-strong`, whose mid-tone read muddy under the dark `--a-warning-fg`. Demo (`theme-provider.html`) card header wrapped in `<header>` for canonical structure. (dogfood-audit: warning-strong-vs-bg; check:card-structure.)
|
|
8
|
+
|
|
9
|
+
### Docs
|
|
10
|
+
- README CDN snippets refreshed to the current line: `@0.6` → `@0.7`, primitive count 95 → 122, pin example → `@adia-ai/web-components@0.7.13`.
|
|
11
|
+
|
|
12
|
+
## [0.7.12] — 2026-06-04
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **New `<theme-provider>` element — adopt the AdiaUI foundation from anywhere, no `<head>` link.** A Light-DOM, `display: contents` infra wrapper (the 122nd primitive) that adopts the foundation — design tokens + resets + page-frame + every primitive's CSS, the constructable-stylesheet twin of `web-components.min.css` (byte-identical to the CDN bundle) — into `document.adoptedStyleSheets` once, deduped, at module-eval. So SPA roots / embedded apps / dynamic mounts render fully-styled **without** a hand-wired `<link rel="stylesheet">` in `<head>`. Opt-in `theme="ocean|forest|…"` (named preset) + `scale="verse|prose"` (typographic register) adopt their layer twin **on demand** (`theme=` matches the `[theme]` hook directly; `scale=` maps onto the existing `[verse]`/`[prose]` attribute). Coexists with the render-blocking `<link>` path (same bytes) — link for multi-page / top-level surfaces (cacheable, no flash), provider for no-`<head>` contexts. Deliberately **not** in the all-primitives barrel (it carries the whole foundation) — import `@adia-ai/web-components/components/theme-provider` explicitly. Files: `components/theme-provider/{theme-provider.js,.class.js,.css,.yaml,.html,.test.js}`.
|
|
17
|
+
- **CDN `.sheet.js` twins.** `bundle-css.mjs` co-emits a constructable-`CSSStyleSheet` twin beside each foundation `.min.css` — `web-components.sheet.js`, `host.sheet.js`, and the `themes` / `verse` / `prose` register twins — from the **same minified buffer** (byte-identical to the `.min.css`, verified by `check:css-bundles-fresh`, exposed as the `./css/sheet` export). A page that links `web-components.min.css` and one that adopts `web-components.sheet.js` render identically. Powers `<theme-provider>` and any runtime style injector that can't add a `<head>` link.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **BREAKING (pre-1.0 PATCH cadence) — the token-theme attribute `data-theme` is renamed `theme`.** `<html data-theme="ocean">` → `<html theme="ocean">`; `[data-theme="…"]` selectors → `[theme="…"]`; the token-root selector `:root, theme-ui, [data-theme]` → `:root, theme-ui, [theme], theme-provider` across the 14 foundation files (so `<theme-provider>` is itself a token-root). Cleaner, and it converges with `<canvas-ui theme="…">`, which already scoped AdiaUI themes the same way. Done as a repo-wide word-boundary sweep — compound attributes (`data-theme-slug`, `data-themes-grid`, `data-themed`, …) are preserved (the sweep also touched the styles/, traits/, and patterns/ trees). **Consumers using `data-theme="…"` must switch to `theme="…"`** — see `docs/MIGRATION GUIDE.md`.
|
|
22
|
+
- **CDN bundles rebuilt** — `dist/web-components.min.{css,js}` + `dist/host.min.css` regenerated so the `[theme]` rename, `<theme-provider>` registration, and the new register twins reach `@adia-ai/web-components@0.7` CDN consumers.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **`dts-codegen` reads the authoritative tag.** It reconstructed each demo's tag as `${name}-ui`, which mangles a no-`-ui` infra tag (`<theme-provider>` is the first) — it now reads `x-adiaui.tag` from the sidecar. Behavior-neutral for all 121 `-ui` components (their `name-ui` === `tag`); correct for infra tags. File: `scripts/build/dts-codegen.mjs`.
|
|
27
|
+
|
|
3
28
|
## [0.7.11] — 2026-06-04
|
|
4
29
|
|
|
5
30
|
### Added
|
|
@@ -2986,7 +3011,7 @@ The future non-side-effect `class` subpath per component (allowing class import
|
|
|
2986
3011
|
|
|
2987
3012
|
All `change` / `input` event dispatches across form-bearing components now emit a `CustomEvent` carrying `detail: { value: this.value }` (value-semantic primitives) or `detail: { value: this.value, checked: this.checked }` (boolean-semantic — switch / check / radio / option-card). Backwards-compatible — `e.target.value` still works since the host's `value` property is updated before dispatch.
|
|
2988
3013
|
|
|
2989
|
-
Driven by
|
|
3014
|
+
Driven by consumer feedback from Design Tokens Studio (2026-05-12) — eliminates the `(e.target as any).value` pattern in TypeScript consumers.
|
|
2990
3015
|
|
|
2991
3016
|
**Value-semantic primitives swept** (`detail: { value }`):
|
|
2992
3017
|
- `<input-ui>` (input.js — 9 dispatch sites)
|
|
@@ -3042,7 +3067,7 @@ that don't surface in the export). Pure-JS OKLCH → OKLab → linear-sRGB
|
|
|
3042
3067
|
|
|
3043
3068
|
- **`styles/design-tokens-export.js`** (new ~430 lines) — extraction module:
|
|
3044
3069
|
CSSOM scanner with authoritative-selector filter (`:root, theme-ui,
|
|
3045
|
-
[
|
|
3070
|
+
[theme]` only — skipping attribute-scoped overrides that would
|
|
3046
3071
|
pollute the cssVar map under last-wins semantics), symbolic
|
|
3047
3072
|
`var()` / `light-dark()` chain walker, OKLCH color-math pipeline, four
|
|
3048
3073
|
output formatters (DTCG / hex / float-RGB / HSL-decimal). Public API:
|
|
@@ -3192,7 +3217,7 @@ Lockstep version bump only — source byte-identical to v0.3.4. Internal `@adia-
|
|
|
3192
3217
|
Lockstep version bump only — source byte-identical to v0.3.3. Internal `@adia-ai/*` dep ranges remain at `^0.3.0`. See root [CHANGELOG.md `## [0.3.4]`](../../CHANGELOG.md) for the cut narrative.
|
|
3193
3218
|
## [0.3.3] - 2026-05-07
|
|
3194
3219
|
|
|
3195
|
-
**Lockstep cut.** All 9 published `@adia-ai/*` packages now share version `0.3.3`, governed by [`docs/specs/package-architecture.md` § 15](
|
|
3220
|
+
**Lockstep cut.** All 9 published `@adia-ai/*` packages now share version `0.3.3`, governed by [`docs/specs/package-architecture.md` § 15](../../docs/specs/package-architecture.md#15-versioning-policy).
|
|
3196
3221
|
|
|
3197
3222
|
### Changed
|
|
3198
3223
|
|
|
@@ -3205,7 +3230,7 @@ Lockstep version bump only — source byte-identical to v0.3.3. Internal `@adia-
|
|
|
3205
3230
|
## [0.3.2] - 2026-05-06
|
|
3206
3231
|
|
|
3207
3232
|
**9-package lockstep patch cut to v0.3.2.** All lockstep members share
|
|
3208
|
-
one version per [`docs/specs/package-architecture.md` § 15](
|
|
3233
|
+
one version per [`docs/specs/package-architecture.md` § 15](../../docs/specs/package-architecture.md#15-versioning-policy).
|
|
3209
3234
|
Internal `@adia-ai/*` dep ranges unchanged at `^0.3.0`.
|
|
3210
3235
|
|
|
3211
3236
|
### No source changes
|
package/README.md
CHANGED
|
@@ -38,13 +38,13 @@ Since **v0.6.30**, this package ships pre-flattened + minified bundles under `di
|
|
|
38
38
|
|
|
39
39
|
```html
|
|
40
40
|
<!-- CSS: all primitives, tokens, resets (443 KB raw / ~50 KB gzipped) -->
|
|
41
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@adia-ai/web-components@0.
|
|
41
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@adia-ai/web-components@0.7/dist/web-components.min.css">
|
|
42
42
|
|
|
43
|
-
<!-- JS: registers all
|
|
44
|
-
<script type="module" src="https://cdn.jsdelivr.net/npm/@adia-ai/web-components@0.
|
|
43
|
+
<!-- JS: registers all 122 primitives (~250 KB gzipped via Brotli) -->
|
|
44
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@adia-ai/web-components@0.7/dist/web-components.min.js"></script>
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
The `@0.
|
|
47
|
+
The `@0.7` range tracks the latest `0.6.x` patch automatically (won't jump to a breaking `0.8`). For reproducible builds, pin an exact version instead — e.g. `@adia-ai/web-components@0.7.13/dist/web-components.min.css`.
|
|
48
48
|
|
|
49
49
|
For composite shells, add the corresponding bundle from `@adia-ai/web-modules` — see [its README](../web-modules/#cdn-no-bundler--codepen-marketing-pages-static-html) or the [CDN usage guide](https://ui-kit.exe.xyz/site/cdn-usage). The kitchen-sink path is `@adia-ai/web-modules/dist/everything.min.js` (all primitives + all 4 shells; ~190 KB gzipped) — one tag for CodePen demos.
|
|
50
50
|
|
|
@@ -59,7 +59,7 @@ For composite shells, add the corresponding bundle from `@adia-ai/web-modules`
|
|
|
59
59
|
ESM-only — bundlers (Vite, esbuild, webpack 5+, Rollup) resolve `import` for both `.js` and `.css`; in plain HTML use `<script type="module">` + `<link rel="stylesheet">`.
|
|
60
60
|
|
|
61
61
|
```js
|
|
62
|
-
import '@adia-ai/web-components'; // registers every *-ui tag (
|
|
62
|
+
import '@adia-ai/web-components'; // registers every *-ui tag (122 primitives)
|
|
63
63
|
import '@adia-ai/web-components/css'; // every primitive's CSS (one stylesheet)
|
|
64
64
|
```
|
|
65
65
|
|
|
@@ -336,12 +336,12 @@ generation engine consume.
|
|
|
336
336
|
## Themes, density, scale
|
|
337
337
|
|
|
338
338
|
```html
|
|
339
|
-
<div
|
|
339
|
+
<div theme="ocean" density="compact" size="sm">
|
|
340
340
|
…all descendants re-theme / re-densify / re-scale automatically…
|
|
341
341
|
</div>
|
|
342
342
|
```
|
|
343
343
|
|
|
344
|
-
- `[
|
|
344
|
+
- `[theme]` — 8 themes: `default`, `ocean`, `forest`, `sunset`,
|
|
345
345
|
`lavender`, `rose`, `slate`, `midnight`
|
|
346
346
|
- `[density]` — `compact` (0.85×) · `spacious` (1.15×)
|
|
347
347
|
- `[size]` — `sm`|`md`|`lg` shifts the entire typescale + component
|
package/USAGE.md
CHANGED
|
@@ -673,14 +673,14 @@ The same applies to `bind()` — call it once in the constructor or use a field,
|
|
|
673
673
|
AdiaUI is parametric. Three attributes drive global appearance:
|
|
674
674
|
|
|
675
675
|
```html
|
|
676
|
-
<div
|
|
676
|
+
<div theme="ocean" density="compact" size="sm">
|
|
677
677
|
<!-- all descendants re-theme / re-densify / re-scale -->
|
|
678
678
|
</div>
|
|
679
679
|
```
|
|
680
680
|
|
|
681
681
|
| Attribute | Values | Effect |
|
|
682
682
|
|---|---|---|
|
|
683
|
-
| `[
|
|
683
|
+
| `[theme]` | `default`, `ocean`, `forest`, `sunset`, `lavender`, `rose`, `slate`, `midnight` | Swaps the OKLCH color ramp |
|
|
684
684
|
| `[density]` | `compact` (0.85×), `spacious` (1.15×) | Multiplies `--a-density` token |
|
|
685
685
|
| `[size]` | `sm`, `md`, `lg` | Shifts typescale + component dimensions |
|
|
686
686
|
| `[radius]` | `sharp` (0), `rounded` (1), `round` (2) | Multiplies `--a-radius-k` token |
|
|
@@ -23,9 +23,10 @@ button-ui[variant="primary"]:not([disabled]):hover { --button-bg: var(--a-accent
|
|
|
23
23
|
button-ui[color="danger"]:not([disabled]):hover { --button-bg: var(--a-danger-strong); }
|
|
24
24
|
button-ui[color="success"]:not([disabled]):hover { --button-bg: var(--a-success-strong); }
|
|
25
25
|
button-ui[color="info"]:not([disabled]):hover { --button-bg: var(--a-info-strong); }
|
|
26
|
-
/* warning fill → -strong like the rest
|
|
27
|
-
the dark --a-warning-fg
|
|
28
|
-
|
|
26
|
+
/* warning fill → -bg (NOT -strong like the rest): warning's -strong mid-tone
|
|
27
|
+
reads muddy under the dark --a-warning-fg, so the hover surface uses the
|
|
28
|
+
-20 tint (--a-warning-bg) for clean contrast. (dogfood-audit: warning-strong-vs-bg) */
|
|
29
|
+
button-ui[color="warning"]:not([disabled]):hover { --button-bg: var(--a-warning-bg); }
|
|
29
30
|
|
|
30
31
|
/* Outline / ghost — no fill on hover; fg goes rest-color → `-strong`.
|
|
31
32
|
Ghost reads --button-fg-ghost-hover (default --a-fg-strong) so consumers
|
|
@@ -52,7 +52,17 @@
|
|
|
52
52
|
"Modal",
|
|
53
53
|
"Col"
|
|
54
54
|
],
|
|
55
|
-
"slots": {
|
|
55
|
+
"slots": {
|
|
56
|
+
"body": {
|
|
57
|
+
"description": "The scrolling region — the only one with overflow. The native `<section>` child takes this role by tag + DOM order; any element can opt in via `slot=\"body\"`. Scrolls between the pinned header and footer (requires a definite-height parent)."
|
|
58
|
+
},
|
|
59
|
+
"footer": {
|
|
60
|
+
"description": "Pinned bottom region — typically an action bar kept visible while the body scrolls. The native `<footer>` child takes this role by tag + DOM order; any element can opt in via `slot=\"footer\"`. Optional."
|
|
61
|
+
},
|
|
62
|
+
"header": {
|
|
63
|
+
"description": "Pinned top region — stays fixed while the body scrolls. The native `<header>` child takes this role by tag + DOM order; any element can opt in via `slot=\"header\"` (a CSS [slot] match in Light DOM, not native projection — ADR-0033), e.g. `<section slot=\"header\">` as a pinned rail. Optional."
|
|
64
|
+
}
|
|
65
|
+
},
|
|
56
66
|
"states": [
|
|
57
67
|
{
|
|
58
68
|
"description": "Default, ready for interaction.",
|
|
@@ -25,7 +25,25 @@ description: |
|
|
|
25
25
|
not broken).
|
|
26
26
|
props: {}
|
|
27
27
|
events: {}
|
|
28
|
-
slots:
|
|
28
|
+
slots:
|
|
29
|
+
header:
|
|
30
|
+
description: >-
|
|
31
|
+
Pinned top region — stays fixed while the body scrolls. The native
|
|
32
|
+
`<header>` child takes this role by tag + DOM order; any element can opt
|
|
33
|
+
in via `slot="header"` (a CSS [slot] match in Light DOM, not native
|
|
34
|
+
projection — ADR-0033), e.g. `<section slot="header">` as a pinned rail.
|
|
35
|
+
Optional.
|
|
36
|
+
body:
|
|
37
|
+
description: >-
|
|
38
|
+
The scrolling region — the only one with overflow. The native `<section>`
|
|
39
|
+
child takes this role by tag + DOM order; any element can opt in via
|
|
40
|
+
`slot="body"`. Scrolls between the pinned header and footer (requires a
|
|
41
|
+
definite-height parent).
|
|
42
|
+
footer:
|
|
43
|
+
description: >-
|
|
44
|
+
Pinned bottom region — typically an action bar kept visible while the body
|
|
45
|
+
scrolls. The native `<footer>` child takes this role by tag + DOM order;
|
|
46
|
+
any element can opt in via `slot="footer"`. Optional.
|
|
29
47
|
states:
|
|
30
48
|
- name: idle
|
|
31
49
|
description: Default, ready for interaction.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/ThemeProvider.json",
|
|
4
|
+
"title": "ThemeProvider",
|
|
5
|
+
"description": "Foundation-providing wrapper — adopts the AdiaUI foundation (design tokens +\nresets + page-frame + every primitive's CSS) into the document from anywhere\nin the DOM, so a surface renders fully-styled WITHOUT a hand-wired\n<link rel=\"stylesheet\"> in <head>. Layout-transparent (display: contents): the\nelement owns no box; children lay out as if it weren't there.\n\nMechanism: it imports the constructable-stylesheet twin of web-components.min.css\n(byte-identical to the CDN bundle, emitted from the same build buffer) and\nadopts it once into document.adoptedStyleSheets, deduped — adoption fires at\nmodule load, before paint. Coexists with the render-blocking <link> path; both\ndeliver the same bytes. Use a <link> (-> the CDN web-components.min.css) for\nmulti-page / top-level surfaces (cacheable across navigations, zero flash); use\n<theme-provider> for SPA roots, embedded apps, and dynamic mounts where you do\nnot control <head> (e.g. <embed-shell>, an A2UI surface, a micro-frontend).\n\nTheming: the base foundation is OS light/dark via light-dark() tokens. Two opt-in\nattributes adopt their layer on demand — theme=\"ocean|forest|slate|…\" applies a\nnamed preset (adopts the themes layer; matches the [theme] hook) and\nscale=\"verse|prose\" sets the compact / long-form typographic register (adopts the\nmatching register layer; maps onto the [verse]/[prose] attribute). Each layer is\nfetched only when its attribute is set, so a bare provider stays lean.\nOpt-in infra: NOT in the all-in-one @adia-ai/web-components barrel (it carries\nthe whole foundation); import @adia-ai/web-components/components/theme-provider\nexplicitly. Distinct from <frame-ui> (a layout skeleton, owns no CSS delivery)\nand the page shells (chrome over the same foundation).\n",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"allOf": [
|
|
8
|
+
{
|
|
9
|
+
"$ref": "common_types.json#/$defs/ComponentCommon"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"$ref": "common_types.json#/$defs/CatalogComponentCommon"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"properties": {
|
|
16
|
+
"theme": {
|
|
17
|
+
"description": "Named theme preset for the wrapped subtree (default, ocean, forest, sunset, lavender, rose, slate, midnight). Adopts the themes layer on demand + matches the [theme=\"…\"] hook.",
|
|
18
|
+
"type": "string",
|
|
19
|
+
"default": ""
|
|
20
|
+
},
|
|
21
|
+
"component": {
|
|
22
|
+
"const": "ThemeProvider"
|
|
23
|
+
},
|
|
24
|
+
"scale": {
|
|
25
|
+
"description": "Typographic / density register for the subtree — \"verse\" (compact) or \"prose\" (long-form). Adopts the matching register layer on demand; maps onto the [verse]/[prose] attribute (additive).",
|
|
26
|
+
"type": "string",
|
|
27
|
+
"default": ""
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"required": [
|
|
31
|
+
"component"
|
|
32
|
+
],
|
|
33
|
+
"unevaluatedProperties": false,
|
|
34
|
+
"x-adiaui": {
|
|
35
|
+
"anti_patterns": [],
|
|
36
|
+
"category": "layout",
|
|
37
|
+
"composes": [],
|
|
38
|
+
"events": {},
|
|
39
|
+
"examples": [
|
|
40
|
+
{
|
|
41
|
+
"description": "A client-mounted app root that self-provides the foundation (no head link).",
|
|
42
|
+
"a2ui": "[\n { \"id\": \"root\", \"component\": \"ThemeProvider\", \"children\": [\"panel\"] },\n { \"id\": \"panel\", \"component\": \"Frame\", \"children\": [\"body\", \"actions\"] },\n { \"id\": \"body\", \"component\": \"Section\", \"children\": [\"copy\"] },\n { \"id\": \"copy\", \"component\": \"Text\", \"variant\": \"body\", \"textContent\": \"Fully styled — no <head> stylesheet link.\" },\n { \"id\": \"actions\", \"component\": \"Footer\", \"children\": [\"go\"] },\n { \"id\": \"go\", \"component\": \"Button\", \"text\": \"Continue\", \"variant\": \"primary\" }\n]",
|
|
43
|
+
"name": "spa-root"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"keywords": [
|
|
47
|
+
"style",
|
|
48
|
+
"provider",
|
|
49
|
+
"foundation",
|
|
50
|
+
"tokens",
|
|
51
|
+
"reset",
|
|
52
|
+
"adopt",
|
|
53
|
+
"stylesheet",
|
|
54
|
+
"shell",
|
|
55
|
+
"embed",
|
|
56
|
+
"spa"
|
|
57
|
+
],
|
|
58
|
+
"name": "UIThemeProvider",
|
|
59
|
+
"related": [
|
|
60
|
+
"Frame",
|
|
61
|
+
"Card"
|
|
62
|
+
],
|
|
63
|
+
"slots": {},
|
|
64
|
+
"states": [
|
|
65
|
+
{
|
|
66
|
+
"description": "Default — foundation adopted into the document; children rendered.",
|
|
67
|
+
"name": "idle"
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"status": "experimental",
|
|
71
|
+
"synonyms": {
|
|
72
|
+
"foundation": [
|
|
73
|
+
"foundation",
|
|
74
|
+
"tokens",
|
|
75
|
+
"styles"
|
|
76
|
+
],
|
|
77
|
+
"provider": [
|
|
78
|
+
"provider",
|
|
79
|
+
"root",
|
|
80
|
+
"host"
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
"tag": "theme-provider",
|
|
84
|
+
"tokens": {},
|
|
85
|
+
"traits": [],
|
|
86
|
+
"version": 1
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-side-effect class export for `<theme-provider>`.
|
|
3
|
+
*
|
|
4
|
+
* Importing this file gives you the class (and adopts the foundation as a
|
|
5
|
+
* module-eval side effect) without auto-registering the tag. The auto-register
|
|
6
|
+
* path is `@adia-ai/web-components/components/theme-provider` (which imports this
|
|
7
|
+
* file + calls `defineIfFree()`).
|
|
8
|
+
*
|
|
9
|
+
* `<theme-provider>` — the foundation-providing wrapper. Adopt the AdiaUI
|
|
10
|
+
* foundation (design tokens + resets + page-frame + every primitive's CSS) into
|
|
11
|
+
* the document from anywhere in the DOM, so a surface renders fully-styled
|
|
12
|
+
* WITHOUT a hand-wired `<link rel="stylesheet">` in `<head>`. Layout-transparent
|
|
13
|
+
* (`display: contents`): the element owns no box; children lay out as if it
|
|
14
|
+
* weren't there.
|
|
15
|
+
*
|
|
16
|
+
* Mechanism: imports the constructable-stylesheet twin of `web-components.min.css`
|
|
17
|
+
* — byte-identical to the CDN bundle (emitted from the same buffer by
|
|
18
|
+
* `scripts/build/bundle-css.mjs`) — and adopts it once into
|
|
19
|
+
* `document.adoptedStyleSheets`, deduped by sheet identity. Adoption fires at
|
|
20
|
+
* module evaluation (the earliest point), so importing the element provides the
|
|
21
|
+
* foundation before the body paints in the common case.
|
|
22
|
+
*
|
|
23
|
+
* Coexists with the render-blocking `<link>` path — both deliver the same bytes.
|
|
24
|
+
* Use a `<link>` (→ the CDN `web-components.min.css`) for multi-page / top-level
|
|
25
|
+
* surfaces (cacheable across navigations, zero flash); use `<theme-provider>`
|
|
26
|
+
* for SPA roots, embedded apps, and dynamic mounts where you don't control
|
|
27
|
+
* `<head>` (e.g. `<embed-shell>`, an A2UI surface, a micro-frontend).
|
|
28
|
+
*
|
|
29
|
+
* Scope (v1): the provider supplies the FOUNDATION only — design tokens, resets,
|
|
30
|
+
* page-frame, and every primitive's CSS. OS light/dark resolves automatically
|
|
31
|
+
* (the tokens are `light-dark()`-based). Named themes (`[theme]`) and the
|
|
32
|
+
* `verse` / `prose` context registers live in OPT-IN layers (themes.css /
|
|
33
|
+
* verse.css / prose.css) that are NOT in the foundation bundle, so setting
|
|
34
|
+
* `theme` / `verse` on the wrapper does nothing until those layers are also
|
|
35
|
+
* adopted — a planned `theme=` / `verse` provider option (the token-root selector
|
|
36
|
+
* is pre-wired for it; see .brain/notes/theme-provider-design-2026-06-04.md).
|
|
37
|
+
*
|
|
38
|
+
* Deliberately NOT in the all-in-one `@adia-ai/web-components` barrel — it's
|
|
39
|
+
* opt-in infra (it carries the whole foundation). Import it explicitly:
|
|
40
|
+
* `import '@adia-ai/web-components/components/theme-provider';`
|
|
41
|
+
*
|
|
42
|
+
* @see ../../USAGE.md · .brain/notes/theme-provider-design-2026-06-04.md
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { UIElement } from '../../core/element.js';
|
|
46
|
+
import constructFoundationSheet from '../../dist/web-components.sheet.js';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Adopt the foundation sheet into the document exactly once. Idempotent across
|
|
50
|
+
* any number of `<theme-provider>` instances (dedup by sheet identity). No-ops in
|
|
51
|
+
* a non-DOM context — `constructFoundationSheet()` returns `null` on SSR/Node.
|
|
52
|
+
*/
|
|
53
|
+
function provideFoundation() {
|
|
54
|
+
const sheet = constructFoundationSheet();
|
|
55
|
+
if (!sheet) return;
|
|
56
|
+
if (!document.adoptedStyleSheets.includes(sheet)) {
|
|
57
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Opt-in context registers — themes (named `[theme]` presets), and the `verse` /
|
|
62
|
+
// `prose` `scale` registers. Each is its own constructable-sheet twin (built by
|
|
63
|
+
// scripts/build/bundle-css.mjs, byte-identical to its .min.css). Adopted on demand
|
|
64
|
+
// the first time a provider needs one, deduped by sheet identity. Dynamic-imported
|
|
65
|
+
// (static paths, so bundlers can code-split) so the element stays lean — a provider
|
|
66
|
+
// that does no theming never pulls these in.
|
|
67
|
+
const TWIN_LOADED = { themes: false, verse: false, prose: false };
|
|
68
|
+
function loadTwin(name) {
|
|
69
|
+
switch (name) {
|
|
70
|
+
case 'themes': return import('../../dist/themes.sheet.js');
|
|
71
|
+
case 'verse': return import('../../dist/verse.sheet.js');
|
|
72
|
+
case 'prose': return import('../../dist/prose.sheet.js');
|
|
73
|
+
default: return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function ensureTwin(name) {
|
|
77
|
+
if (TWIN_LOADED[name]) return;
|
|
78
|
+
const p = loadTwin(name);
|
|
79
|
+
if (!p) return;
|
|
80
|
+
TWIN_LOADED[name] = true;
|
|
81
|
+
p.then((mod) => {
|
|
82
|
+
const sheet = mod?.default?.();
|
|
83
|
+
if (sheet && !document.adoptedStyleSheets.includes(sheet)) {
|
|
84
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
|
|
85
|
+
}
|
|
86
|
+
}).catch(() => { TWIN_LOADED[name] = false; }); // allow a later retry on failure
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Adopt the base foundation as early as this module evaluates — before any element
|
|
90
|
+
// connects, before the body paints when imported from <head>. Importing the
|
|
91
|
+
// provider IS the act of providing the foundation, wherever the element lands.
|
|
92
|
+
provideFoundation();
|
|
93
|
+
|
|
94
|
+
export class UIThemeProvider extends UIElement {
|
|
95
|
+
static properties = {
|
|
96
|
+
// Named theme preset (ocean / forest / slate / …). Reflected, so the attribute
|
|
97
|
+
// — which IS the `[theme="…"]` CSS hook in themes.css — tracks the property.
|
|
98
|
+
theme: { type: String, reflect: true },
|
|
99
|
+
// Typographic/density register: "verse" (compact) | "prose" (long-form).
|
|
100
|
+
// Mapped onto the existing `[verse]` / `[prose]` attribute below (additive —
|
|
101
|
+
// the register CSS is untouched), so every register rule incl. `[size]`
|
|
102
|
+
// sub-tiers applies for free.
|
|
103
|
+
scale: { type: String, reflect: true },
|
|
104
|
+
};
|
|
105
|
+
static template = () => null;
|
|
106
|
+
|
|
107
|
+
connected() {
|
|
108
|
+
// Layout-transparent + invisible to the a11y tree (the template-engine /
|
|
109
|
+
// traits-host convention). Set imperatively so it holds even in the frame
|
|
110
|
+
// before the adopted foundation's own `display: contents` rule applies.
|
|
111
|
+
this.style.display = 'contents';
|
|
112
|
+
if (!this.hasAttribute('role')) this.setAttribute('role', 'presentation');
|
|
113
|
+
provideFoundation(); // belt-and-suspenders for late registration
|
|
114
|
+
this.#applyContext();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
updated() {
|
|
118
|
+
this.#applyContext();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Adopt the opt-in twins for the current theme/scale, and map `scale` onto the
|
|
122
|
+
// existing register attribute. Idempotent (safe to call on every update).
|
|
123
|
+
#applyContext() {
|
|
124
|
+
// `theme="ocean"` already matches `[theme="ocean"]` (the attr is on the host);
|
|
125
|
+
// adopt themes.sheet.js so those preset rules exist in the document.
|
|
126
|
+
if (this.theme) ensureTwin('themes');
|
|
127
|
+
|
|
128
|
+
const scale = this.scale;
|
|
129
|
+
this.toggleAttribute('verse', scale === 'verse');
|
|
130
|
+
this.toggleAttribute('prose', scale === 'prose');
|
|
131
|
+
if (scale === 'verse') ensureTwin('verse');
|
|
132
|
+
else if (scale === 'prose') ensureTwin('prose');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<theme-provider>` — layout-transparent foundation host.
|
|
3
|
+
*
|
|
4
|
+
* The element exists to adopt the AdiaUI foundation into the document (see
|
|
5
|
+
* theme-provider.class.js); it owns no layout. `display: contents` removes its
|
|
6
|
+
* box so children lay out as if the wrapper weren't there. The class also sets
|
|
7
|
+
* this imperatively on connect — belt-and-suspenders for the frame before this
|
|
8
|
+
* adopted rule applies.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: a `>` child combinator from an ancestor does NOT pierce a
|
|
11
|
+
* `display: contents` wrapper (FB-53) — keep `<theme-provider>` at/near the
|
|
12
|
+
* mount root, not spliced mid-layout-chain.
|
|
13
|
+
*/
|
|
14
|
+
@scope (theme-provider) {
|
|
15
|
+
:where(:scope) {
|
|
16
|
+
display: contents;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<theme-provider>` — Foundation-providing wrapper — adopts the AdiaUI foundation (design tokens +
|
|
3
|
+
resets + page-frame + every primitive's CSS) into the document from anywhere
|
|
4
|
+
in the DOM, so a surface renders fully-styled WITHOUT a hand-wired
|
|
5
|
+
<link rel="stylesheet"> in <head>. Layout-transparent (display: contents): the
|
|
6
|
+
element owns no box; children lay out as if it weren't there.
|
|
7
|
+
|
|
8
|
+
Mechanism: it imports the constructable-stylesheet twin of web-components.min.css
|
|
9
|
+
(byte-identical to the CDN bundle, emitted from the same build buffer) and
|
|
10
|
+
adopts it once into document.adoptedStyleSheets, deduped — adoption fires at
|
|
11
|
+
module load, before paint. Coexists with the render-blocking <link> path; both
|
|
12
|
+
deliver the same bytes. Use a <link> (-> the CDN web-components.min.css) for
|
|
13
|
+
multi-page / top-level surfaces (cacheable across navigations, zero flash); use
|
|
14
|
+
<theme-provider> for SPA roots, embedded apps, and dynamic mounts where you do
|
|
15
|
+
not control <head> (e.g. <embed-shell>, an A2UI surface, a micro-frontend).
|
|
16
|
+
|
|
17
|
+
Theming: the base foundation is OS light/dark via light-dark() tokens. Two opt-in
|
|
18
|
+
attributes adopt their layer on demand — theme="ocean|forest|slate|…" applies a
|
|
19
|
+
named preset (adopts the themes layer; matches the [theme] hook) and
|
|
20
|
+
scale="verse|prose" sets the compact / long-form typographic register (adopts the
|
|
21
|
+
matching register layer; maps onto the [verse]/[prose] attribute). Each layer is
|
|
22
|
+
fetched only when its attribute is set, so a bare provider stays lean.
|
|
23
|
+
Opt-in infra: NOT in the all-in-one @adia-ai/web-components barrel (it carries
|
|
24
|
+
the whole foundation); import @adia-ai/web-components/components/theme-provider
|
|
25
|
+
explicitly. Distinct from <frame-ui> (a layout skeleton, owns no CSS delivery)
|
|
26
|
+
and the page shells (chrome over the same foundation).
|
|
27
|
+
|
|
28
|
+
*
|
|
29
|
+
* @see https://ui-kit.exe.xyz/site/components/theme-provider
|
|
30
|
+
*
|
|
31
|
+
* Type declarations generated by scripts/build/dts-codegen.mjs from
|
|
32
|
+
* the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
|
|
33
|
+
* run `npm run build:components`, then `npm run codegen:dts` to
|
|
34
|
+
* regenerate; or hand-author this file fully if rich event types are
|
|
35
|
+
* needed beyond what the yaml `events:` block can express.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { UIElement } from '../../core/element.js';
|
|
39
|
+
|
|
40
|
+
export class UIThemeProvider extends UIElement {
|
|
41
|
+
/** Named theme preset for the wrapped subtree (default, ocean, forest, sunset, lavender, rose, slate, midnight). Adopts the themes layer on demand + matches the [theme="…"] hook. */
|
|
42
|
+
theme: string;
|
|
43
|
+
/** Typographic / density register for the subtree — "verse" (compact) or "prose" (long-form). Adopts the matching register layer on demand; maps onto the [verse]/[prose] attribute (additive). */
|
|
44
|
+
scale: string;
|
|
45
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<theme-provider>` — auto-registers the tag on import, and adopts the AdiaUI
|
|
3
|
+
* foundation into the document (the side effect lives in the class module).
|
|
4
|
+
*
|
|
5
|
+
* import '@adia-ai/web-components/components/theme-provider';
|
|
6
|
+
* <theme-provider> … your app … </theme-provider>
|
|
7
|
+
*
|
|
8
|
+
* Opt-in infra: NOT pulled by the `@adia-ai/web-components` barrel (it carries
|
|
9
|
+
* the full foundation CSS as a constructable sheet). For the class without
|
|
10
|
+
* auto-registering the tag, import the `class` subpath:
|
|
11
|
+
*
|
|
12
|
+
* import { UIThemeProvider } from '@adia-ai/web-components/components/theme-provider/class';
|
|
13
|
+
*
|
|
14
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { defineIfFree } from '../../core/register.js';
|
|
18
|
+
import { UIThemeProvider } from './theme-provider.class.js';
|
|
19
|
+
|
|
20
|
+
defineIfFree('theme-provider', UIThemeProvider);
|
|
21
|
+
|
|
22
|
+
export { UIThemeProvider };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <theme-provider> tests
|
|
3
|
+
*
|
|
4
|
+
* The provider adopts the AdiaUI foundation (a constructable stylesheet) into
|
|
5
|
+
* `document.adoptedStyleSheets` — once, deduped — and renders `display: contents`
|
|
6
|
+
* + `role=presentation`. The real foundation sheet (`web-components.sheet.js`,
|
|
7
|
+
* ~512 KB of LightningCSS output) is mocked here with a 1-rule stub so the test
|
|
8
|
+
* exercises the ELEMENT's contract (adopt-once, dedup, transparency), not the
|
|
9
|
+
* bundle's CSS. Byte-parity of the real sheet is covered by `check:css-bundles-fresh`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
13
|
+
|
|
14
|
+
// Stub the heavy foundation sheet — same default-export shape as the generated
|
|
15
|
+
// module: a constructSheet() returning a memoized CSSStyleSheet singleton.
|
|
16
|
+
vi.mock('../../dist/web-components.sheet.js', () => {
|
|
17
|
+
let sheet = null;
|
|
18
|
+
return {
|
|
19
|
+
default: () => {
|
|
20
|
+
if (typeof CSSStyleSheet === 'undefined') return null;
|
|
21
|
+
if (!sheet) { sheet = new CSSStyleSheet(); sheet.replaceSync('theme-provider{display:contents}'); }
|
|
22
|
+
return sheet;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Stub the opt-in twins — the element dynamic-imports these when theme=/scale= is
|
|
28
|
+
// set. We test the attribute contract (sync), not the adopted CSS. (Factories are
|
|
29
|
+
// inlined because vi.mock hoists above any shared `const`.)
|
|
30
|
+
vi.mock('../../dist/themes.sheet.js', () => ({ default: () => { const s = new CSSStyleSheet(); s.replaceSync(':root{}'); return s; } }));
|
|
31
|
+
vi.mock('../../dist/verse.sheet.js', () => ({ default: () => { const s = new CSSStyleSheet(); s.replaceSync(':root{}'); return s; } }));
|
|
32
|
+
vi.mock('../../dist/prose.sheet.js', () => ({ default: () => { const s = new CSSStyleSheet(); s.replaceSync(':root{}'); return s; } }));
|
|
33
|
+
|
|
34
|
+
import '../../core/element.js';
|
|
35
|
+
import './theme-provider.js'; // registers the tag + adopts the (mocked) foundation at module-eval
|
|
36
|
+
|
|
37
|
+
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
38
|
+
|
|
39
|
+
function mount(html) {
|
|
40
|
+
const wrap = document.createElement('div');
|
|
41
|
+
wrap.innerHTML = html;
|
|
42
|
+
document.body.appendChild(wrap);
|
|
43
|
+
return wrap.firstElementChild;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('<theme-provider>', () => {
|
|
47
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
48
|
+
|
|
49
|
+
it('adopts the foundation sheet at import (module-eval side effect)', () => {
|
|
50
|
+
expect(document.adoptedStyleSheets.length).toBeGreaterThanOrEqual(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('is display:contents + role=presentation on connect', async () => {
|
|
54
|
+
const el = mount('<theme-provider><span>hi</span></theme-provider>');
|
|
55
|
+
await tick();
|
|
56
|
+
expect(el.style.display).toBe('contents');
|
|
57
|
+
expect(el.getAttribute('role')).toBe('presentation');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('does not clobber an author-set role', async () => {
|
|
61
|
+
const el = mount('<theme-provider role="group"><span>hi</span></theme-provider>');
|
|
62
|
+
await tick();
|
|
63
|
+
expect(el.getAttribute('role')).toBe('group');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('dedupes — N instances adopt the foundation exactly once', async () => {
|
|
67
|
+
const before = document.adoptedStyleSheets.length; // singleton already adopted at import
|
|
68
|
+
mount('<theme-provider></theme-provider>');
|
|
69
|
+
mount('<theme-provider></theme-provider>');
|
|
70
|
+
mount('<theme-provider></theme-provider>');
|
|
71
|
+
await tick();
|
|
72
|
+
expect(document.adoptedStyleSheets.length).toBe(before);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('reflects theme= to the [theme] attribute hook', async () => {
|
|
76
|
+
const el = mount('<theme-provider theme="ocean"><span>x</span></theme-provider>');
|
|
77
|
+
await tick();
|
|
78
|
+
expect(el.getAttribute('theme')).toBe('ocean');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('scale="verse" maps to the [verse] register attribute', async () => {
|
|
82
|
+
const el = mount('<theme-provider scale="verse"><span>x</span></theme-provider>');
|
|
83
|
+
await tick();
|
|
84
|
+
expect(el.hasAttribute('verse')).toBe(true);
|
|
85
|
+
expect(el.hasAttribute('prose')).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('scale="prose" maps to the [prose] register attribute', async () => {
|
|
89
|
+
const el = mount('<theme-provider scale="prose"><span>x</span></theme-provider>');
|
|
90
|
+
await tick();
|
|
91
|
+
expect(el.hasAttribute('prose')).toBe(true);
|
|
92
|
+
expect(el.hasAttribute('verse')).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
});
|