@adia-ai/web-modules 0.6.18 → 0.6.20

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
@@ -1,5 +1,55 @@
1
1
  # Changelog — @adia-ai/web-modules
2
2
 
3
+ ## [0.6.20] — 2026-05-21
4
+
5
+ ### Added — `<admin-entity-item>` shell identity-row primitive (FEEDBACK-38)
6
+
7
+ - **New CSS-only shell-tier primitive: a composable icon + label + badge identity row.** A leading `<icon-ui>` (or `<img>` avatar), a truncating `[slot="label"]`, and an optional trailing `[slot="badge"]` travel as one slottable unit — slot it whole into `[slot="heading"]` of an `<admin-topbar>` / `<admin-statusbar>` for workspace or user identity. Geometry mirrors `<select-ui>`'s trigger row (`icon` ≡ `leading`, `label` ≡ `display`, `badge` ≡ `caret`). Inside a collapsible `<admin-sidebar>` the label + badge hide when the rail collapses and the icon survives, so one markup serves both states — closing the gap consumers were filling with ad-hoc `<span>` / `<div>` wrappers. New files: `shell/admin-entity-item/admin-entity-item.{yaml,a2ui.json,html,examples.html}` + `css/admin-shell.entity-item.css`. `admin-topbar.yaml` / `admin-statusbar.yaml` gain `a2ui.rules` pointing at it.
8
+
9
+ ### Added — `admin-shell` warns when `<admin-topbar>` / `<admin-statusbar>` is missing its slot (FEEDBACK-37)
10
+
11
+ - **`<admin-shell>` now logs a one-shot `console.warn` when an `<admin-topbar>` or `<admin-statusbar>` is a direct child of `<admin-sidebar>` / `<admin-content>` without `slot="header"` / `slot="footer"`.** Both are CSS-only elements; omitting the slot lands them in the default body slot — rendering them in the content column instead of the chrome band, previously with zero diagnostics. The check runs once at connect, `WeakSet`-guarded per element. Same silent-misuse-warn discipline `drawer-ui` / `chart-ui` / `segmented-ui` received in 0.6.19. Files: `admin-shell.js`.
12
+
13
+ ### Changed — `<admin-page-header>` background rebased to `--a-canvas-0`
14
+
15
+ - **`--page-content-header-bg` token: `--a-canvas-1` → `--a-canvas-0`.** The sticky page-header band now matches the page-body content surface (`--page-content-bg`), so the header reads as a flush extension of the content area rather than as separate chrome. This is the third iteration of the page-header treatment: v0.6.14 used `--a-canvas-2` (ΔL=0.08, "elevated step over content"); v0.6.16 rebased to `--a-canvas-1` ("shell-chrome continuation"); both still read as not-quite-flush against dense dark-mode layouts. `canvas-0` settles it — fully flush. Consumers who want a raised or contrasting band override `--page-content-header-bg` at `:root`. Files: `admin-shell.tokens.css` (token value + history comment).
16
+
17
+ ### Fixed — `admin-topbar` / `admin-statusbar` inline padding aligns with sibling `<section-ui>`
18
+
19
+ - **The topbar/statusbar inline padding now matches the sibling `<section-ui>`'s, so chrome and content share one gutter.** In the content column the bars took `--page-header-px` (`a-space-3`) while the content `<section-ui>` uses `--page-content-inset` (`a-space-10`) — the topbar title and the body content were misaligned. They now use `--page-content-inset`. The shared rule also covered the sidebar context, where the sidebar's `<section-ui>` uses the tighter `--page-sidebar-px`; a new `admin-sidebar > :is(admin-topbar, admin-statusbar)` rule scopes that case so each context aligns with its own `<section-ui>`. Files: `css/admin-shell.bespoke.css`.
20
+
21
+ ### Fixed — collapsed sidebar `[slot="heading"]` hide no longer wipes composed wrappers (FEEDBACK-38 addendum)
22
+
23
+ - **`collapsed.css`'s `[slot="heading"] { display: none }` rail-cleanup rule is now scoped to leaf text headings.** The unscoped rule hid *any* `slot="heading"` child in a collapsed sidebar — including composed wrappers like `<admin-entity-item slot="heading">`, wiping their icon along with the label text. It now matches `:is(span, p, div, h1–h6)[slot="heading"]` plus `[slot="heading"]:not(:has(> [slot]))` — a heading whose children are themselves slotted (a composed unit) survives collapse and manages its own children. Backward-compatible: bare `<span slot="heading">` text labels still hide. Files: `css/admin-shell.collapsed.css`.
24
+
25
+ ### Fixed — `nav-item-ui` collapsed hit area fills the 48px rail (FEEDBACK-39)
26
+
27
+ - **In a collapsed sidebar, `nav-item-ui` now stretches to the full rail width instead of shrinking to its ~30px icon box.** Previously the centred 30px item left ~9px dead gutters on each side — clicks there hit `section-ui`, not the nav item (~37% of the rail was dead space). `collapsed.css` now sets `width: 100%` on both `nav-item-ui` and `nav-ui` — the latter is required because the collapsed section shrink-wraps its children, so the item's `width: 100%` would otherwise resolve against the 30px content box. `justify-content: center` keeps the glyph centred. Files: `css/admin-shell.collapsed.css`.
28
+
29
+ ## [0.6.19] — 2026-05-21
30
+
31
+ ### Added — `./shell/with-css` opt-in export (FEEDBACK-25)
32
+
33
+ - `import '@adia-ai/web-modules/shell/with-css'` registers the JS-backed
34
+ shell elements (`admin-shell`, `admin-sidebar`, `admin-command`) AND
35
+ mounts the shell layout CSS in one side-effect import. The default
36
+ `./shell` subpath stays CSS-free to preserve the explicit-import contract
37
+ (ADR-0030); `/with-css` is the named opt-in. New `shell/with-css.js` +
38
+ `shell/with-css.d.ts`.
39
+
40
+ ### Added — `types` condition on CSS subpath exports (FEEDBACK-26)
41
+
42
+ - The `shell` / `chat` / `editor` / `simple` / `runtime` / `theme` `.css`
43
+ subpath exports now carry a `types` condition (`css-module.d.ts` stub),
44
+ fixing TS2882 under `moduleResolution: bundler`.
45
+
46
+ ### Docs — per-cluster import contract (FEEDBACK-25)
47
+
48
+ - README documents that the per-cluster JS import registers only the
49
+ JS-backed elements — the CSS-only structural children (`admin-content`,
50
+ `admin-topbar`, `admin-page`, …) need no JS registration by design — plus
51
+ the new `/with-css` opt-in.
52
+
3
53
  ## [0.6.18] — 2026-05-21
4
54
 
5
55
  ### Added — `admin-sidebar` resize-handle diagnostic (FB-17)
package/README.md CHANGED
@@ -33,6 +33,13 @@ import '@adia-ai/web-modules/theme'; // theme-panel
33
33
  import '@adia-ai/web-modules/runtime'; // gen-root + a2ui-root
34
34
  ```
35
35
 
36
+ > The per-cluster import registers the **JS-backed** elements only. Each
37
+ > cluster also has **CSS-only** structural children (e.g. `admin-content`,
38
+ > `admin-topbar`, `admin-page` — see the [Clusters](#clusters) table) that
39
+ > need no JS registration: they are plain custom elements styled entirely
40
+ > by the cluster CSS. `customElements.get()` returning `undefined` for one
41
+ > of them is expected, not a bug.
42
+
36
43
  ## Import: CSS (since v0.4.5)
37
44
 
38
45
  > ⚠️ **Required — shell layouts depend on this CSS.** Shell JS modules
@@ -58,6 +65,27 @@ import '@adia-ai/web-modules/runtime/gen-root.css';
58
65
 
59
66
  > **For pre-v0.4.5 consumers:** if you were importing CSS via the relative `node_modules` path (`'../node_modules/@adia-ai/web-modules/editor/editor-shell/editor-shell.css'`), switch to the package-specifier form above. The relative-path form is fragile under pnpm, Yarn PnP, and npm hoisting; the new package-specifier form works under all three.
60
67
 
68
+ ## Import: JS + CSS in one line (`/with-css`)
69
+
70
+ For the `shell` cluster, the `/with-css` opt-in collapses the JS + CSS
71
+ imports into a single side-effect import:
72
+
73
+ ```js
74
+ import '@adia-ai/web-modules/shell/with-css';
75
+ ```
76
+
77
+ This registers the JS-backed shell elements (`admin-shell`,
78
+ `admin-sidebar`, `admin-command`) **and** mounts the shell layout CSS
79
+ (`admin-shell.css` bundles every sub-component style). It is the one-line
80
+ equivalent of `import '@adia-ai/web-modules/shell'` +
81
+ `import '@adia-ai/web-modules/shell/admin-shell.css'`.
82
+
83
+ The default `@adia-ai/web-modules/shell` subpath stays CSS-free to
84
+ preserve the explicit-import contract ([ADR-0030](../../.brain/adrs/0030-shell-with-css-opt-in-carve-out.md));
85
+ `/with-css` is the named opt-in. Per-element companions
86
+ (`@adia-ai/web-modules/shell/admin-shell/with-css`) also exist for
87
+ finer-grained imports.
88
+
61
89
  Each cluster carries a **shell host** (behavior-only orchestrator) plus
62
90
  a **family of bespoke shell-tier children** (cluster-namespaced custom
63
91
  elements with state-as-attribute semantics) per [ADR-0023](../../.brain/adrs/0023-bespoke-shell-tier-children.md). The bespoke vocabulary is the only recognized authoring shape since v0.4.0 ([ADR-0024](../../.brain/adrs/0024-legacy-shell-shapes-retired.md)).
@@ -0,0 +1,6 @@
1
+ // Ambient declaration so TypeScript (moduleResolution: bundler / node16)
2
+ // can resolve CSS side-effect subpath imports — e.g.
3
+ // import '@adia-ai/web-components/css';
4
+ // without a TS2882 "no type declarations for side-effect import" error.
5
+ // See FEEDBACK-26.
6
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/web-modules",
3
- "version": "0.6.18",
3
+ "version": "0.6.20",
4
4
  "description": "AdiaUI composite custom elements \u2014 shell, chat, editor, runtime clusters built from @adia-ai/web-components primitives. Subpath exports per cluster.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,6 +14,11 @@
14
14
  "import": "./shell/index.js",
15
15
  "default": "./shell/index.js"
16
16
  },
17
+ "./shell/with-css": {
18
+ "types": "./shell/with-css.d.ts",
19
+ "import": "./shell/with-css.js",
20
+ "default": "./shell/with-css.js"
21
+ },
17
22
  "./shell/*": {
18
23
  "types": "./shell/*/*.d.ts",
19
24
  "import": "./shell/*/*.js",
@@ -24,9 +29,18 @@
24
29
  "import": "./shell/*/with-css.js",
25
30
  "default": "./shell/*/with-css.js"
26
31
  },
27
- "./shell/*.css": "./shell/*/*.css",
28
- "./shell/*/*.css": "./shell/*/*.css",
29
- "./shell/*/css/*.css": "./shell/*/css/*.css",
32
+ "./shell/*.css": {
33
+ "types": "./css-module.d.ts",
34
+ "default": "./shell/*/*.css"
35
+ },
36
+ "./shell/*/*.css": {
37
+ "types": "./css-module.d.ts",
38
+ "default": "./shell/*/*.css"
39
+ },
40
+ "./shell/*/css/*.css": {
41
+ "types": "./css-module.d.ts",
42
+ "default": "./shell/*/css/*.css"
43
+ },
30
44
  "./chat": {
31
45
  "types": "./chat/index.d.ts",
32
46
  "import": "./chat/index.js",
@@ -42,9 +56,18 @@
42
56
  "import": "./chat/*/with-css.js",
43
57
  "default": "./chat/*/with-css.js"
44
58
  },
45
- "./chat/*.css": "./chat/*/*.css",
46
- "./chat/*/*.css": "./chat/*/*.css",
47
- "./chat/*/css/*.css": "./chat/*/css/*.css",
59
+ "./chat/*.css": {
60
+ "types": "./css-module.d.ts",
61
+ "default": "./chat/*/*.css"
62
+ },
63
+ "./chat/*/*.css": {
64
+ "types": "./css-module.d.ts",
65
+ "default": "./chat/*/*.css"
66
+ },
67
+ "./chat/*/css/*.css": {
68
+ "types": "./css-module.d.ts",
69
+ "default": "./chat/*/css/*.css"
70
+ },
48
71
  "./editor": {
49
72
  "types": "./editor/index.d.ts",
50
73
  "import": "./editor/index.js",
@@ -65,9 +88,18 @@
65
88
  "import": "./editor/*/with-css.js",
66
89
  "default": "./editor/*/with-css.js"
67
90
  },
68
- "./editor/*.css": "./editor/*/*.css",
69
- "./editor/*/*.css": "./editor/*/*.css",
70
- "./editor/*/css/*.css": "./editor/*/css/*.css",
91
+ "./editor/*.css": {
92
+ "types": "./css-module.d.ts",
93
+ "default": "./editor/*/*.css"
94
+ },
95
+ "./editor/*/*.css": {
96
+ "types": "./css-module.d.ts",
97
+ "default": "./editor/*/*.css"
98
+ },
99
+ "./editor/*/css/*.css": {
100
+ "types": "./css-module.d.ts",
101
+ "default": "./editor/*/css/*.css"
102
+ },
71
103
  "./simple": {
72
104
  "types": "./simple/index.d.ts",
73
105
  "import": "./simple/index.js",
@@ -83,9 +115,18 @@
83
115
  "import": "./simple/*/with-css.js",
84
116
  "default": "./simple/*/with-css.js"
85
117
  },
86
- "./simple/*.css": "./simple/*/*.css",
87
- "./simple/*/*.css": "./simple/*/*.css",
88
- "./simple/*/css/*.css": "./simple/*/css/*.css",
118
+ "./simple/*.css": {
119
+ "types": "./css-module.d.ts",
120
+ "default": "./simple/*/*.css"
121
+ },
122
+ "./simple/*/*.css": {
123
+ "types": "./css-module.d.ts",
124
+ "default": "./simple/*/*.css"
125
+ },
126
+ "./simple/*/css/*.css": {
127
+ "types": "./css-module.d.ts",
128
+ "default": "./simple/*/css/*.css"
129
+ },
89
130
  "./runtime": {
90
131
  "types": "./runtime/index.d.ts",
91
132
  "import": "./runtime/index.js",
@@ -96,9 +137,18 @@
96
137
  "import": "./runtime/*/*.js",
97
138
  "default": "./runtime/*/*.js"
98
139
  },
99
- "./runtime/*.css": "./runtime/*/*.css",
100
- "./runtime/*/*.css": "./runtime/*/*.css",
101
- "./runtime/*/css/*.css": "./runtime/*/css/*.css",
140
+ "./runtime/*.css": {
141
+ "types": "./css-module.d.ts",
142
+ "default": "./runtime/*/*.css"
143
+ },
144
+ "./runtime/*/*.css": {
145
+ "types": "./css-module.d.ts",
146
+ "default": "./runtime/*/*.css"
147
+ },
148
+ "./runtime/*/css/*.css": {
149
+ "types": "./css-module.d.ts",
150
+ "default": "./runtime/*/css/*.css"
151
+ },
102
152
  "./theme": {
103
153
  "types": "./theme/index.d.ts",
104
154
  "import": "./theme/index.js",
@@ -109,9 +159,18 @@
109
159
  "import": "./theme/*/*.js",
110
160
  "default": "./theme/*/*.js"
111
161
  },
112
- "./theme/*.css": "./theme/*/*.css",
113
- "./theme/*/*.css": "./theme/*/*.css",
114
- "./theme/*/css/*.css": "./theme/*/css/*.css",
162
+ "./theme/*.css": {
163
+ "types": "./css-module.d.ts",
164
+ "default": "./theme/*/*.css"
165
+ },
166
+ "./theme/*/*.css": {
167
+ "types": "./css-module.d.ts",
168
+ "default": "./theme/*/*.css"
169
+ },
170
+ "./theme/*/css/*.css": {
171
+ "types": "./css-module.d.ts",
172
+ "default": "./theme/*/css/*.css"
173
+ },
115
174
  "./generative": {
116
175
  "types": "./generative/index.d.ts",
117
176
  "import": "./generative/index.js",
@@ -149,6 +208,7 @@
149
208
  "!generative/**/*.examples.js",
150
209
  "!generative/**/*.html",
151
210
  "index.js",
211
+ "css-module.d.ts",
152
212
  "README.md",
153
213
  "CHANGELOG.md"
154
214
  ],
@@ -0,0 +1,79 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://adiaui.dev/a2ui/v0_9/components/AdminEntityItem.json",
4
+ "title": "AdminEntityItem",
5
+ "description": "Module-tier shell identity row. CSS-only — no behavior, no JS.\nA composable icon + label + badge unit: a leading icon (or avatar),\na truncating label, and an optional trailing badge that travel\ntogether as one slottable element.\n\nUse it wherever a shell surface shows an entity identity — workspace\nidentity in an <admin-topbar slot=\"header\">, user identity in an\n<admin-statusbar>, the leading item of a breadcrumb, a flyout header,\nor a user row. Inside a collapsible <admin-sidebar> the label and\nbadge hide when the rail collapses; the icon survives — so the same\nmarkup works expanded and collapsed without authoring two shapes.\n\nGeometry mirrors <select-ui>'s trigger row: [slot=\"icon\"] is fixed\n(like [slot=\"leading\"]), [slot=\"label\"] flexes + truncates (like\n[slot=\"display\"]), [slot=\"badge\"] is fixed trailing (like\n[slot=\"caret\"]).\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
+ "component": {
17
+ "const": "AdminEntityItem"
18
+ }
19
+ },
20
+ "required": [
21
+ "component"
22
+ ],
23
+ "unevaluatedProperties": false,
24
+ "x-adiaui": {
25
+ "anti_patterns": [],
26
+ "category": "layout",
27
+ "composes": [],
28
+ "events": {},
29
+ "examples": [],
30
+ "keywords": [
31
+ "admin-entity-item",
32
+ "entity-item",
33
+ "identity-row",
34
+ "workspace-identity",
35
+ "user-identity",
36
+ "icon-label-badge"
37
+ ],
38
+ "name": "AdminEntityItem",
39
+ "related": [
40
+ "AdminTopbar",
41
+ "AdminStatusbar",
42
+ "AdminSidebar",
43
+ "Select",
44
+ "Badge",
45
+ "Icon"
46
+ ],
47
+ "slots": {
48
+ "default": {
49
+ "description": "Default content — unslotted children flow inline. Prefer the named slots for the canonical icon + label + badge composition."
50
+ },
51
+ "badge": {
52
+ "description": "Optional trailing chip — environment (staging / beta) or role badge. Fixed width. Hidden when a collapsible sidebar collapses."
53
+ },
54
+ "icon": {
55
+ "description": "Leading visual — an <icon-ui> glyph or an <img> avatar (img is given a circular crop). Fixed width; survives sidebar collapse."
56
+ },
57
+ "label": {
58
+ "description": "Primary text. Flexes to fill the remaining width and truncates with an ellipsis. Hidden when a collapsible sidebar collapses."
59
+ }
60
+ },
61
+ "states": [
62
+ {
63
+ "description": "Default, the only state.",
64
+ "name": "idle"
65
+ }
66
+ ],
67
+ "synonyms": {
68
+ "entity-item": [
69
+ "identity-row",
70
+ "entity-row",
71
+ "identity-item"
72
+ ]
73
+ },
74
+ "tag": "admin-entity-item",
75
+ "tokens": {},
76
+ "traits": [],
77
+ "version": 1
78
+ }
79
+ }
@@ -0,0 +1,84 @@
1
+ # Edit this file; run `npm run build:components` to regenerate a2ui.json.
2
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
3
+ name: AdminEntityItem
4
+ tag: admin-entity-item
5
+ component: AdminEntityItem
6
+ category: layout
7
+ version: 1
8
+ description: |
9
+ Module-tier shell identity row. CSS-only — no behavior, no JS.
10
+ A composable icon + label + badge unit: a leading icon (or avatar),
11
+ a truncating label, and an optional trailing badge that travel
12
+ together as one slottable element.
13
+
14
+ Use it wherever a shell surface shows an entity identity — workspace
15
+ identity in an <admin-topbar slot="header">, user identity in an
16
+ <admin-statusbar>, the leading item of a breadcrumb, a flyout header,
17
+ or a user row. Inside a collapsible <admin-sidebar> the label and
18
+ badge hide when the rail collapses; the icon survives — so the same
19
+ markup works expanded and collapsed without authoring two shapes.
20
+
21
+ Geometry mirrors <select-ui>'s trigger row: [slot="icon"] is fixed
22
+ (like [slot="leading"]), [slot="label"] flexes + truncates (like
23
+ [slot="display"]), [slot="badge"] is fixed trailing (like
24
+ [slot="caret"]).
25
+
26
+ props: {}
27
+
28
+ events: {}
29
+
30
+ slots:
31
+ default:
32
+ description: >-
33
+ Default content — unslotted children flow inline. Prefer the
34
+ named slots for the canonical icon + label + badge composition.
35
+ icon:
36
+ description: >-
37
+ Leading visual — an <icon-ui> glyph or an <img> avatar (img is
38
+ given a circular crop). Fixed width; survives sidebar collapse.
39
+ label:
40
+ description: >-
41
+ Primary text. Flexes to fill the remaining width and truncates
42
+ with an ellipsis. Hidden when a collapsible sidebar collapses.
43
+ badge:
44
+ description: >-
45
+ Optional trailing chip — environment (staging / beta) or role
46
+ badge. Fixed width. Hidden when a collapsible sidebar collapses.
47
+
48
+ states:
49
+ - name: idle
50
+ description: Default, the only state.
51
+
52
+ traits: []
53
+
54
+ a2ui:
55
+ rules:
56
+ - >-
57
+ admin-entity-item is the canonical icon + label + badge identity
58
+ row for shell surfaces. Slot it whole into [slot="heading"] of an
59
+ <admin-topbar> / <admin-statusbar> so the icon + label + badge
60
+ collapse together inside a collapsible <admin-sidebar>. Do NOT
61
+ re-implement it with a bare <span> or an ad-hoc flex <div> — those
62
+ have no shared collapse boundary.
63
+ - >-
64
+ For an INTERACTIVE workspace switcher use <select-ui variant="ghost">
65
+ instead — admin-entity-item is read-only identity display.
66
+
67
+ keywords:
68
+ - admin-entity-item
69
+ - entity-item
70
+ - identity-row
71
+ - workspace-identity
72
+ - user-identity
73
+ - icon-label-badge
74
+
75
+ synonyms:
76
+ entity-item: [identity-row, entity-row, identity-item]
77
+
78
+ related:
79
+ - AdminTopbar
80
+ - AdminStatusbar
81
+ - AdminSidebar
82
+ - Select
83
+ - Badge
84
+ - Icon
@@ -13,3 +13,4 @@
13
13
  @import "./css/admin-shell.templates.css";
14
14
  @import "./css/admin-shell.helpers.css";
15
15
  @import "./css/admin-shell.bespoke.css";
16
+ @import "./css/admin-shell.entity-item.css";
@@ -40,6 +40,10 @@ class AdminShell extends UIElement {
40
40
 
41
41
  static template = () => null;
42
42
 
43
+ // FEEDBACK-37: one-shot per-element warn for <admin-topbar>/<admin-statusbar>
44
+ // mounted without their slot. WeakSet so it never pins the element from GC.
45
+ static #warnedSlots = new WeakSet();
46
+
43
47
  connected() {
44
48
  // Apply the v0.6.13 default mode if (and only if) the author did
45
49
  // not supply a `mode` attribute on the tag. `hasAttribute` returns
@@ -48,6 +52,7 @@ class AdminShell extends UIElement {
48
52
  if (!this.hasAttribute('mode')) this.mode = DEFAULT_MODE;
49
53
  this.#wireToggleButtons();
50
54
  this.#wireCommandTriggers();
55
+ this.#checkSlotContracts();
51
56
  }
52
57
 
53
58
  // No #disconnected — children own their own cleanup; the host registers
@@ -95,6 +100,33 @@ class AdminShell extends UIElement {
95
100
  if (item) nav.select(item);
96
101
  });
97
102
  }
103
+
104
+ // ── 3. Slot-contract diagnostics (FEEDBACK-37) ──
105
+ // <admin-topbar>/<admin-statusbar> are CSS-only — placed inside
106
+ // <admin-sidebar>/<admin-content> without slot="header"/"footer" they
107
+ // land in the default slot and render in the body column instead of the
108
+ // chrome band, with no diagnostic. One-shot warn per misused element.
109
+ // (admin-statusbar folded in — identical failure mode to admin-topbar.)
110
+ #checkSlotContracts() {
111
+ const want = { 'admin-topbar': 'header', 'admin-statusbar': 'footer' };
112
+ for (const parent of this.querySelectorAll('admin-sidebar, admin-content')) {
113
+ for (const child of parent.children) {
114
+ const slot = want[child.localName];
115
+ if (slot
116
+ && child.getAttribute('slot') !== slot
117
+ && !AdminShell.#warnedSlots.has(child)) {
118
+ AdminShell.#warnedSlots.add(child);
119
+ // eslint-disable-next-line no-console
120
+ console.warn(
121
+ `[admin-shell] <${child.localName}> inside <${parent.localName}> is missing ` +
122
+ `slot="${slot}" — it renders in the default body slot instead of the chrome ` +
123
+ `band. Add slot="${slot}". See ${child.localName}.yaml.`,
124
+ child,
125
+ );
126
+ }
127
+ }
128
+ }
129
+ }
98
130
  }
99
131
 
100
132
  customElements.define('admin-shell', AdminShell);
@@ -87,7 +87,12 @@ describe('admin-shell.collapsed.css — vanilla HTML fallback (v0.6.17)', () =>
87
87
  // text labels don't overflow the 48px rail when the consumer doesn't
88
88
  // use AdiaUI <nav-item-ui> primitives.
89
89
  expect(collapsedCSS).toMatch(/Vanilla-HTML fallback/);
90
- expect(collapsedCSS).toMatch(/\[slot="heading"\]\s*\{[\s\S]*?display:\s*none/);
90
+ // v0.6.20 (FEEDBACK-38 addendum): the heading-hide rule narrowed from a
91
+ // blanket `[slot="heading"]` to plain-text headings + headings without
92
+ // slotted children, so composed wrappers (<admin-entity-item slot="heading">)
93
+ // survive collapse. The rule still resolves to `display: none`.
94
+ expect(collapsedCSS).toMatch(/\[slot="heading"\][^{]*\{\s*display:\s*none/);
95
+ expect(collapsedCSS).toMatch(/\[slot="heading"\]:not\(:has\(>\s*\[slot\]\)\)/);
91
96
  expect(collapsedCSS).toMatch(/button\.nav-item/);
92
97
  expect(collapsedCSS).toMatch(/text-indent:\s*-9999px/);
93
98
  });
@@ -34,7 +34,7 @@ admin-sidebar > admin-topbar {
34
34
  display: flex;
35
35
  align-items: center;
36
36
  gap: var(--page-header-gap);
37
- padding: 0 var(--page-header-px);
37
+ padding: 0 var(--page-content-inset);
38
38
  height: var(--page-header-height);
39
39
  font-size: var(--page-header-font);
40
40
  border-bottom: var(--page-border);
@@ -48,7 +48,7 @@ admin-sidebar > admin-statusbar {
48
48
  display: flex;
49
49
  align-items: center;
50
50
  gap: var(--page-header-gap);
51
- padding: 0 var(--page-header-px);
51
+ padding: 0 var(--page-content-inset);
52
52
  height: var(--page-header-height);
53
53
  font-size: var(--page-header-font);
54
54
  color: var(--page-header-fg-muted);
@@ -56,6 +56,13 @@ admin-sidebar > admin-statusbar {
56
56
  flex-shrink: 0;
57
57
  }
58
58
 
59
+ /* Sidebar header/footer chrome takes the sidebar's tighter inline inset
60
+ (--page-sidebar-px) so it aligns with the sidebar's own <section-ui>
61
+ body — not the content column's wider --page-content-inset. */
62
+ admin-sidebar > :is(admin-topbar, admin-statusbar) {
63
+ padding-inline: var(--page-sidebar-px);
64
+ }
65
+
59
66
  /* ── admin-scroll ≡ <section> child of <main> ── */
60
67
  admin-content > admin-scroll {
61
68
  flex: 1;
@@ -71,6 +71,19 @@
71
71
  padding: 0;
72
72
  min-height: var(--nav-item-row-height);
73
73
  min-width: var(--nav-item-row-height);
74
+ /* FEEDBACK-39: fill the rail width so the whole 48px column is the hit
75
+ area — without this the item shrinks to its ~30px icon box, leaving
76
+ ~9px dead gutters on each side. justify-content keeps the glyph
77
+ centred within the now-full-width item. */
78
+ width: 100%;
79
+ }
80
+
81
+ /* FEEDBACK-39: nav-ui itself must fill the rail — the collapsed section
82
+ centres + shrink-wraps its children, so without an explicit width
83
+ nav-ui collapses to its ~30px content box and `width: 100%` on the
84
+ items would resolve against 30px, not the 48px rail. */
85
+ nav-ui {
86
+ width: 100%;
74
87
  }
75
88
 
76
89
  /* Button: icon-only mode */
@@ -111,7 +124,13 @@
111
124
  <admin-topbar slot="header">) — keeps the rail clean in collapsed mode.
112
125
  Authors who want a brand mark visible when collapsed should put an
113
126
  <icon-ui> or logo image in [slot="leading"] instead. */
114
- [slot="heading"] {
127
+ /* FEEDBACK-38 (addendum): scope the hide to leaf text headings — a bare
128
+ <span slot="heading">Brand</span> — not composed wrappers (e.g. an
129
+ <admin-entity-item slot="heading">) that carry their own internal slot
130
+ contract. `:not(:has(> [slot]))` preserves any heading element whose
131
+ children are themselves slotted, so its icon survives collapse. */
132
+ :is(span, p, div, h1, h2, h3, h4, h5, h6)[slot="heading"],
133
+ [slot="heading"]:not(:has(> [slot])) {
115
134
  display: none;
116
135
  }
117
136
 
@@ -0,0 +1,82 @@
1
+ /* ═══════════════════════════════════════════════════════════════
2
+ admin-shell — <admin-entity-item> (FEEDBACK-38)
3
+
4
+ CSS-only shell-tier primitive: a composable icon + label + badge
5
+ identity row. The leading icon, a truncating label, and an
6
+ optional trailing badge travel as one unit, so they can be
7
+ slotted whole into <admin-topbar>, <admin-statusbar>, breadcrumb
8
+ leading items, flyout headers, and user-identity rows.
9
+
10
+ Geometry mirrors <select-ui>'s trigger row:
11
+ [slot="icon"] ≡ select-ui [slot="leading"] — fixed, flex-shrink:0
12
+ [slot="label"] ≡ select-ui [slot="display"] — flexible, truncates
13
+ [slot="badge"] ≡ select-ui [slot="caret"] — fixed trailing
14
+
15
+ Collapse: inside a collapsed <admin-sidebar> (@container sidebar,
16
+ ≤96px) the label + badge hide and the icon survives. This is why
17
+ the element is a wrapper: collapsed.css's [slot="heading"] hide
18
+ rule is scoped to preserve any heading that carries slotted
19
+ children (`:not(:has(> [slot]))`, see FEEDBACK-38 addendum), so a
20
+ <admin-entity-item slot="heading"> survives collapse and manages
21
+ its own label/badge here — a bare <span slot="heading"> cannot.
22
+ ═══════════════════════════════════════════════════════════════ */
23
+
24
+ admin-entity-item {
25
+ /* ── tunable tokens ── */
26
+ --entity-item-gap: var(--a-space-2);
27
+ --entity-item-icon-size: 1rem;
28
+ --entity-item-icon-color: var(--a-fg-muted);
29
+ --entity-item-avatar-size: 1.5rem;
30
+
31
+ display: inline-flex;
32
+ align-items: center;
33
+ gap: var(--entity-item-gap);
34
+ min-width: 0;
35
+ overflow: hidden;
36
+ }
37
+
38
+ /* Leading visual — <icon-ui> glyph or <img> avatar. Never shrinks. */
39
+ admin-entity-item > [slot="icon"] {
40
+ flex-shrink: 0;
41
+ display: inline-flex;
42
+ align-items: center;
43
+ line-height: 1;
44
+ color: var(--entity-item-icon-color);
45
+ --a-icon-size: var(--entity-item-icon-size);
46
+ }
47
+
48
+ /* <img slot="icon"> → circular avatar treatment. */
49
+ admin-entity-item > img[slot="icon"] {
50
+ width: var(--entity-item-avatar-size);
51
+ height: var(--entity-item-avatar-size);
52
+ border-radius: var(--a-radius-full);
53
+ object-fit: cover;
54
+ }
55
+
56
+ /* Truncating label — takes the remaining width, ellipsises overflow. */
57
+ admin-entity-item > [slot="label"] {
58
+ flex: 1;
59
+ min-width: 0;
60
+ overflow: hidden;
61
+ text-overflow: ellipsis;
62
+ white-space: nowrap;
63
+ font-size: var(--a-ui-sm);
64
+ font-weight: var(--a-weight-medium, 500);
65
+ color: var(--a-fg);
66
+ }
67
+
68
+ /* Optional trailing badge — environment / role chip. Never shrinks. */
69
+ admin-entity-item > [slot="badge"] {
70
+ flex-shrink: 0;
71
+ }
72
+
73
+ /* ── Collapsed sidebar (icon-only rail) ──
74
+ Hide the label + badge; the icon survives. The enclosing
75
+ <admin-topbar>/<admin-statusbar> collapsed rule (justify-content:
76
+ center, in collapsed.css) centres the now-icon-only item. */
77
+ @container sidebar (max-width: 96px) {
78
+ admin-entity-item > [slot="label"],
79
+ admin-entity-item > [slot="badge"] {
80
+ display: none;
81
+ }
82
+ }
@@ -38,7 +38,7 @@
38
38
 
39
39
  /* Content area — scroll region inside main */
40
40
  --page-content-bg: var(--a-canvas-0); /* content surface (lighter than chrome) */
41
- --page-content-header-bg: var(--a-canvas-1); /* sticky page-header band — matches sidebar/topbar canvas so the shell reads as a continuous L-shape of chrome wrapping the page-body. ΔL=0.04 above body canvas-0. History: v0.6.14 used canvas-2 (ΔL=0.08) under the "elevated step over content" intent, but vision-tests against dense layouts showed the band still read as flush with body. v0.6.15 reframes the page-header as shell-chrome continuation (canvas-1 matches sidebar) rather than as an elevated step — visual cohesion over contrast. Consumers who want a stronger raised band can override this token at :root. */
41
+ --page-content-header-bg: var(--a-canvas-0); /* sticky page-header band — canvas-0, matching the page-body content surface (--page-content-bg) so the header reads as a flush extension of the content area, not as separate chrome. History: v0.6.14 used canvas-2 (ΔL=0.08, "elevated step over content"); v0.6.16 rebased to canvas-1 ("shell-chrome continuation"); both still read as not-quite-flush against dense dark-mode layouts, so the band is now canvas-0 fully flush with the body content surface. Consumers who want a raised/contrasting band override this token at :root. */
42
42
  --page-content-radius: var(--a-radius-lg); /* used by "rounded" mode */
43
43
  --page-content-inset: var(--a-space-10); /* padding for header/body/footer */
44
44
  --page-content-max-width: 1540px; /* max-width for content children */
@@ -39,6 +39,7 @@
39
39
  "AdminShell",
40
40
  "AdminContent",
41
41
  "AdminTopbar",
42
+ "AdminEntityItem",
42
43
  "Footer"
43
44
  ],
44
45
  "slots": {
@@ -50,6 +50,17 @@ a2ui:
50
50
  admin-statusbar replaces <footer-ui> at shell-tier. Same slot
51
51
  vocabulary as <admin-topbar>; visual treatment differs (the
52
52
  shell css applies a top-border instead of bottom).
53
+ - >-
54
+ When placed inside <admin-sidebar> or <admin-content>, admin-statusbar
55
+ MUST carry slot="footer". Without it the statusbar lands in the
56
+ default body slot instead of the chrome footer band. Canonical:
57
+ <admin-statusbar slot="footer">…</admin-statusbar>. admin-shell logs
58
+ a one-shot console.warn when the slot is missing.
59
+ - >-
60
+ For a user-identity row (avatar + name + role badge), slot a single
61
+ <admin-entity-item slot="heading"> rather than separate slot="icon" +
62
+ slot="heading" children — the wrapper collapses the name + badge
63
+ together inside a collapsible <admin-sidebar>, keeping the avatar.
53
64
 
54
65
  keywords:
55
66
  - admin-statusbar
@@ -65,4 +76,5 @@ related:
65
76
  - AdminShell
66
77
  - AdminContent
67
78
  - AdminTopbar
79
+ - AdminEntityItem
68
80
  - Footer
@@ -40,6 +40,7 @@
40
40
  "AdminContent",
41
41
  "AdminSidebar",
42
42
  "AdminStatusbar",
43
+ "AdminEntityItem",
43
44
  "Header"
44
45
  ],
45
46
  "slots": {
@@ -56,6 +56,18 @@ a2ui:
56
56
  admin-topbar replaces <header-ui> at shell-tier — use it for
57
57
  chrome bars inside admin-shell, admin-content, admin-sidebar.
58
58
  Use <header-ui> for primitive containers (Card / Drawer / Modal).
59
+ - >-
60
+ When placed inside <admin-sidebar> or <admin-content>, admin-topbar
61
+ MUST carry slot="header". Without it the topbar lands in the default
62
+ body slot and renders below the content instead of as the chrome
63
+ header band. Canonical: <admin-topbar slot="header">…</admin-topbar>.
64
+ admin-shell logs a one-shot console.warn when the slot is missing.
65
+ - >-
66
+ For an icon + label (+ optional badge) identity row — workspace or
67
+ product identity — slot a single <admin-entity-item slot="heading">
68
+ rather than separate slot="icon" + slot="heading" children. The
69
+ wrapper keeps the icon and label as one unit so they truncate and
70
+ collapse together inside a collapsible <admin-sidebar>.
59
71
 
60
72
  keywords:
61
73
  - admin-topbar
@@ -72,4 +84,5 @@ related:
72
84
  - AdminContent
73
85
  - AdminSidebar
74
86
  - AdminStatusbar
87
+ - AdminEntityItem
75
88
  - Header
@@ -0,0 +1,3 @@
1
+ // Opt-in `/with-css` companion for the shell cluster (FEEDBACK-25).
2
+ // Side-effect-only module — no type surface.
3
+ export {};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Opt-in `/with-css` companion for the whole shell cluster.
3
+ *
4
+ * Side-effect-only module. Importing this:
5
+ * 1. Loads `./index.js` — registers the JS-backed shell vocabulary
6
+ * (admin-shell, admin-sidebar, admin-command)
7
+ * 2. Side-effect-imports `./admin-shell/admin-shell.css` — the shell
8
+ * layout CSS. One file bundles every sub-component style (topbar,
9
+ * content, page, scroll, statusbar), so the CSS-only shell elements
10
+ * (admin-topbar, admin-content, admin-page, admin-page-header,
11
+ * admin-page-body, admin-scroll) are styled by it too.
12
+ *
13
+ * Consumer-facing entry point:
14
+ * import '@adia-ai/web-modules/shell/with-css';
15
+ *
16
+ * The default `@adia-ai/web-modules/shell` subpath preserves the
17
+ * explicit-import CSS contract (ADR-0030); this `/with-css` companion is
18
+ * the named opt-in for a one-line working shell. See FEEDBACK-25.
19
+ */
20
+ import './index.js';
21
+ import './admin-shell/admin-shell.css';