@adia-ai/web-components 0.6.7 → 0.6.9
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 +22 -0
- package/USAGE.md +215 -0
- package/components/action-list/action-list.css +1 -1
- package/components/card/card.css +43 -43
- package/components/chart/chart.css +1 -1
- package/components/chat-thread/chat-thread.css +6 -6
- package/components/code/code.css +17 -17
- package/components/command/command.css +7 -7
- package/components/grid/grid.css +6 -6
- package/components/pane/pane.css +10 -10
- package/components/stack/stack.css +1 -1
- package/core/template.js +236 -7
- package/core/template.test.js +294 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog — @adia-ai/web-components
|
|
2
2
|
|
|
3
|
+
## [0.6.9] — 2026-05-21
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **9 component CSS files — `@scope` blocks no longer use bare-combinator descendants (F-002, P1).** Pre-fix: 92 selector clauses across `action-list.css`, `card.css`, `chart.css`, `chat-thread.css`, `code.css`, `command.css`, `grid.css`, `pane.css`, `stack.css` used the `@scope { > X { } }` shape (bare `>` combinator with no left-hand selector inside `@scope`). Chromium/Firefox tolerate this; LightningCSS rejects it as "Invalid empty selector" (matches the CSS Nesting + `@scope` spec strictly). Symptom: consumers on Vite 7 (default `cssMinify: 'lightningcss'`) saw build-time failures at `card.css:152`; Vite 6 + earlier Vite 7 (esbuild minify) silently swallowed the same source. Fix: codemod-driven rewrite of all 92 clauses from `> X` to `& > X` (explicit `&` reference matches the modern CSS Nesting idiom; semantically identical; LightningCSS-clean). All 120 CSS files now pass LightningCSS minify against Vite 8 default targets (Chromium 125+, Safari 18+, Firefox 129+). Codemod source: `scripts/build/codemod-scope-bare-descendants.mjs` (committed for future audits; idempotent + `--verify` mode). Paired CI gate `check:scope-bare-descendants` blocks regression at PR time; companion `check:lightningcss-build` smoke-minifies every CSS file end-to-end against Vite 8 targets. Closes claims-ui FEEDBACK-02.
|
|
7
|
+
|
|
8
|
+
### Documentation
|
|
9
|
+
- **`USAGE.md` — new "Looking up a component's API" subsection.** Three discovery paths now documented in one place: `npx adia-ai-doc <component>` CLI (machine-readable + offline-safe), `<component>.examples.html` per-component live demo + Properties table, and `<component>.yaml` source-of-truth schema. Closes claims-ui FEEDBACK-09 (per-component opt-out attributes are documented; this subsection makes the doc-path discoverable from the top-level USAGE entrypoint).
|
|
10
|
+
|
|
11
|
+
## [0.6.8] — 2026-05-20
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **`core/template.js` — `.camelCaseProp=${expr}` property bindings now route to the canonical setter (FB-55, P1).** Pre-fix: the HTML parser lowercases attribute names inside `<template>.innerHTML` per HTML5 §13.2.5.32, so `.className=${expr}` arrived at the binding scanner as `.classname`. The binding then wrote `element['classname'] = v` — an enumerable expando — never invoking the `HTMLElement.prototype.className` setter. Classes never applied; no warning, no error. Trap affected every camelCase DOM property surface (`.className`, `.innerText`, `.tabIndex`, `.ariaLabel`, `.contentEditable`, `.readOnly`, `.maxLength`, `.minLength`, `.colSpan`, `.rowSpan`) AND every camelCase property declared via `UIElement.static properties` (80+ across primitives — `<color-picker>` `.maxChroma`, `<grid>` `.columnGap`, `<breadcrumb>` `.collapseKeepLeading`, etc.). Even the canonical `<my-panel .minL=${minL}>` example from `README.md:212` silently failed. **Fix: two-layer name resolution** — (1) `PROP_CASE_FIX` static map at `scan()` time covers built-in DOM camelCase (zero-cost lookup, no prototype walk for the common case); (2) lazy prototype-walk fallback at `applyValue()` time finds canonical camelCase property names on the element's instance + prototype chain via case-insensitive match, caching resolution on the part after first hit. Catches UIElement custom-element props installed per-instance in `installProps()` (those don't exist at `scan()` time because the element hasn't been upgraded yet). Backward-compatible: when neither layer finds a case-insensitive match, the original lowercase name is preserved (consumers writing to lowercase expandos unaffected). 10 NEW vitest cases in `core/template.test.js`'s `FB-55` describe block pin the contract. Closes RESPONSE-55 (reviewer-#A expansion to UIElement primitives explicitly covered).
|
|
15
|
+
- **`core/template.js` — nested `${html\`<svg-child/>\`}` interpolations now render in SVG namespace (FB-57, P1).** Pre-fix: each nested `html\`\`` is a separate cached template whose `tpl.innerHTML = m` is parsed at document level without SVG insertion-mode context, so `<circle>`/`<line>`/etc. arrived as `HTMLUnknownElement` (NS `http://www.w3.org/1999/xhtml`). The `<span style="display:contents">` wrapper from `wrap()` didn't fix it because `display: contents` is layout-level, not namespace-level; SVG layout is namespace-strict → present-but-invisible output, zero bounding rect, no error. **Fix: namespace-aware `mount()`** — discriminate via `container.closest('svg, math, foreignObject, annotation-xml')` at mount time; when stamping into an SVG/MathML subtree, recursively re-namespace the cloned fragment via `createElementNS`, transferring attributes via `setAttributeNS` to preserve attr-namespaces (e.g., legacy `xlink:href`). `<foreignObject>` (SVG) and `<annotation-xml encoding="text/html"|"application/xhtml+xml">` (MathML) descendants correctly REVERT to HTML namespace per HTML5 §12.2.5 foreign-content insertion mode. Implementation site: `mount()` rather than `getTemplate()` — keeps the template cache coherent (same `html\`\`` source can stamp into either context without doubling cache entries). 6 NEW vitest cases in `core/template.test.js`'s `FB-57` describe block (inline-SVG regression, nested-SVG fix, direct-stamp into SVG container, foreignObject boundary, MathML, HTML-container no-op). Closes RESPONSE-57 (Option A accepted with implementation-site refinement; Option B rejected as half-measure).
|
|
16
|
+
- **`core/template.js` — partial-interp warning text recommends `class="${expr}"` first (FB-55 #2).** The v0.5.5 §184 warn text put `.className=${expression}` first as the recommended fix for partial class interpolation; that path now works post-FB-55 fix above, but `class="${expr}"` (full-attribute interpolation) is the more discoverable form that doesn't require knowing the camelCase property name. Promoting it first. The `.classList=${{foo: true, bar: false}}` aspirational line is removed — classList is a read-only DOMTokenList accessor, no `PROP_CASE_FIX` entry can make it assignable.
|
|
17
|
+
|
|
18
|
+
### Docs
|
|
19
|
+
- **`USAGE.md` — three new template-related sections** documenting the FB-55/56/57 cluster:
|
|
20
|
+
- **§-TBD (v0.6.8) — Host element display defaults (FB-56)** — surfaces the HTML-spec inline default for custom elements + the `:host(:where(*))` zero-specificity recipe used across the in-tree primitive set (30+ flex-container primitives). Documents why `UIElement` doesn't bake a `display` rule into the host (container vs leaf vs passthrough is a consumer-layout decision). Worked symptom + diagnostic recipe ("inspect the host directly — `getComputedStyle(host).display === 'inline'`").
|
|
21
|
+
- **§-TBD (v0.6.8) — SVG composition + namespacing (FB-57)** — documents three modes (inline, nested-interpolations, imperative-`.innerHTML`) with the `<foreignObject>` boundary semantics and the MathML parallel. Migration is zero — pre-fix code that worked keeps working; pre-fix code that was silently broken (nested SVG interpolations) starts producing visible output.
|
|
22
|
+
- **§-TBD (v0.6.8) — `.camelCaseProp=${expr}` property binding (FB-55)** — explains the two-layer name resolution, lists affected camelCase property surfaces, documents backward-compatibility for genuine expando consumers, and notes the discoverability trade-off (the simpler `class="${expr}"` doesn't require knowing the camelCase property name).
|
|
23
|
+
- **USAGE.md `§-TBD (v0.6.8) — UIElement lifecycle hooks (FB-59 + FB-43/44/47/58 cluster)`** — new section documenting the canonical hook order: `adoptStyles` → `prepareParts` → **`connected()`** (line 122, inside `untracked()`) → effect first-run with **stamp at line 127** → `render()` → `updated()`. Source-cited from `core/element.js:107-142`. Resolves a cluster-wide misunderstanding about when `connected()` fires relative to the template stamp: it runs BEFORE the stamp wipes consumer-supplied light-DOM children, NOT after. Includes the "Container UIElement pattern" example showing `connected()` → capture children → arm MutationObserver, plus the defensive `if (present.length > 0) keep` cache-fallback pattern (consumer-discovered) that makes capture robust to ordering. Distinguishes `connected()` (recommended subclass hook) vs `connectedCallback()` (rarely overridden W3C hook). Closes FB-59 P3 docs ask + corrects the §B mechanism walkthrough in RESPONSE-58 (sibling followup-RESPONSE retracts the §B order claim).
|
|
24
|
+
|
|
3
25
|
## [0.6.7] — 2026-05-19
|
|
4
26
|
|
|
5
27
|
### Added
|
package/USAGE.md
CHANGED
|
@@ -59,6 +59,35 @@ import { signal, computed, effect, html, UIElement, UIFormElement } from '@adia-
|
|
|
59
59
|
import '@adia-ai/web-components/traits';
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
### Looking up a component's API
|
|
63
|
+
|
|
64
|
+
Three reliable surfaces, in order of immediacy:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 1. CLI (since v0.6.7) — prints yaml summary + live demo URL + first example
|
|
68
|
+
npx adia-ui-doc table-toolbar # full prop / slot / event surface
|
|
69
|
+
npx adia-ui-doc # bare invocation lists all 96 components
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# 2. Per-component examples (ship in npm tarball since v0.6.7)
|
|
74
|
+
cat node_modules/@adia-ai/web-components/components/table-toolbar/table-toolbar.examples.md
|
|
75
|
+
|
|
76
|
+
# Or the richer .examples.html for Properties tables + multi-section walks
|
|
77
|
+
open node_modules/@adia-ai/web-components/components/table-toolbar/table-toolbar.examples.html
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# 3. The component's yaml (authoritative — all props, defaults, descriptions)
|
|
82
|
+
cat node_modules/@adia-ai/web-components/components/table-toolbar/table-toolbar.yaml
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The yaml is the source of truth for every property, slot, event, and token.
|
|
86
|
+
The CLI reads it. The `.d.ts` files are generated from it. When this USAGE
|
|
87
|
+
guide and the yaml disagree, the yaml wins — file a doc-bug FEEDBACK.
|
|
88
|
+
|
|
89
|
+
For the live demo of every component: <https://ui-kit.exe.xyz/site/components/>.
|
|
90
|
+
|
|
62
91
|
---
|
|
63
92
|
|
|
64
93
|
## The mental model
|
|
@@ -1181,6 +1210,115 @@ Why AdiaUI doesn't implement `?attr=`: custom elements declare their reflective
|
|
|
1181
1210
|
- HTML comments containing quoted attributes (`<!-- attr="value" -->`) — same fix (v0.5.3 §155).
|
|
1182
1211
|
- Backticks inside HTML comments — see §221i above (JS-spec footgun; not a parser bug).
|
|
1183
1212
|
|
|
1213
|
+
### §-TBD (v0.6.8) — Host element display defaults (FB-56)
|
|
1214
|
+
|
|
1215
|
+
Per the HTML spec, **all custom elements default to `display: inline`** — and `UIElement` subclasses are no exception. When you use a `UIElement` subclass as a layout container (filling a flex parent, holding an SVG canvas, wrapping a fluid grid), the inline default collapses the host to zero height and any `height: 100%` descendant collapses with it.
|
|
1216
|
+
|
|
1217
|
+
**Symptom you're hitting this trap:** content renders in DevTools with the expected DOM, but its computed height is 0 because the custom-element host between it and a layout parent has `display: inline`. Inspect the host element directly — `getComputedStyle(host).display === 'inline'` confirms.
|
|
1218
|
+
|
|
1219
|
+
If you author a `UIElement` to be a layout container, set an explicit `display` on the host tag — the in-tree convention is `:host(:where(*))` so consumer-app CSS can still override without `!important`:
|
|
1220
|
+
|
|
1221
|
+
```js
|
|
1222
|
+
import { css } from '@adia-ai/web-components/core/element';
|
|
1223
|
+
|
|
1224
|
+
class MyContainer extends UIElement {
|
|
1225
|
+
static styles = css`
|
|
1226
|
+
:host(:where(*)) { /* zero-specificity wrap; consumers override at (0,0,1) */
|
|
1227
|
+
display: flex;
|
|
1228
|
+
flex-direction: column;
|
|
1229
|
+
flex: 1 1 auto;
|
|
1230
|
+
min-height: 0;
|
|
1231
|
+
}
|
|
1232
|
+
`;
|
|
1233
|
+
static template = (host) => html`<slot></slot>`;
|
|
1234
|
+
}
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
Or, if the host should be transparent to layout (its children inherit the parent's grid/flex context directly):
|
|
1238
|
+
|
|
1239
|
+
```css
|
|
1240
|
+
my-passthrough-element { display: contents; }
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
See also: the slot-routing pass-through pattern that's the natural consumer of `display: contents` (cross-referenced from §345's `.map()` vs `repeat()` discussion where `wrap()` spans use the same mechanism).
|
|
1244
|
+
|
|
1245
|
+
**`UIElement` does not bake a `display` rule into the host** because container vs leaf vs passthrough is a consumer-layout decision, not a framework one. Per primitive, a global default would risk breaking layouts where the consumer explicitly wants `inline` semantics (e.g. inline badge inside flowing text, `<icon>` rendered alongside surrounding `<text-ui>`, `<kbd-ui>` keystroke marker). The in-tree primitive set spans `inline` / `inline-block` / `block` / `flex` / `grid` / `contents` — no single default fits.
|
|
1246
|
+
|
|
1247
|
+
**In-tree convention reference:** scan `packages/web-components/components/*/<name>.css` for the `:host(:where(*))` pattern. The 30+ flex-container primitives (`<editor-shell>`, `<nav>`, `<pane>`, `<page>`, `<card>`, etc.) all use this shape; copy from the closest-fit primitive.
|
|
1248
|
+
|
|
1249
|
+
### §-TBD (v0.6.8) — SVG composition + namespacing (FB-57)
|
|
1250
|
+
|
|
1251
|
+
`html\`\`` handles SVG content in three modes, with one historical trap closed in v0.6.8.
|
|
1252
|
+
|
|
1253
|
+
**Mode 1 — inline SVG in a single template (always worked):**
|
|
1254
|
+
|
|
1255
|
+
```js
|
|
1256
|
+
html`
|
|
1257
|
+
<svg viewBox="0 0 100 100">
|
|
1258
|
+
<circle cx="50" cy="50" r="10"/>
|
|
1259
|
+
<path d="M 0 0 L 100 100"/>
|
|
1260
|
+
</svg>
|
|
1261
|
+
`
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1264
|
+
When the parser sees `<svg>` in `tpl.innerHTML`, it enters SVG insertion mode for its descendants. Children arrive as proper `SVGElement` instances.
|
|
1265
|
+
|
|
1266
|
+
**Mode 2 — nested template interpolations (now works, post-v0.6.8 FB-57 fix):**
|
|
1267
|
+
|
|
1268
|
+
```js
|
|
1269
|
+
html`
|
|
1270
|
+
<svg viewBox="0 0 100 100">
|
|
1271
|
+
${dots.map(d => html`<circle cx="${d.x}" cy="${d.y}" r="3"/>`)}
|
|
1272
|
+
</svg>
|
|
1273
|
+
`
|
|
1274
|
+
```
|
|
1275
|
+
|
|
1276
|
+
Each nested `html\`<circle/>\`` is a separate template parsed at document level (no SVG context), so pre-v0.6.8 the circles arrived as `HTMLUnknownElement` — present in the DOM but invisible to SVG layout (zero bounding rect, no stroke, no fill, no error). v0.6.8 `mount()` detects when the container is inside an SVG (or MathML) subtree and re-namespaces the cloned fragment via `createElementNS`. The natural composition pattern works.
|
|
1277
|
+
|
|
1278
|
+
**Mode 3 — imperative `.innerHTML` commit (escape hatch for very large SVG):**
|
|
1279
|
+
|
|
1280
|
+
For SVG content too large to template cleanly (thousands of nodes, dynamic shape strings):
|
|
1281
|
+
|
|
1282
|
+
```js
|
|
1283
|
+
class MyChart extends UIElement {
|
|
1284
|
+
static template = (host) => html`<svg viewBox="0 0 ${host.w} ${host.h}"></svg>`;
|
|
1285
|
+
updated() {
|
|
1286
|
+
const svg = this.querySelector('svg');
|
|
1287
|
+
svg.innerHTML = host._buildSvgString(); // SVG-namespaced via SVGSVGElement.innerHTML
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
`.innerHTML=${str}` as a template binding also works for this (the property name is already lowercase, so the FB-55 PROP_CASE_FIX entry just maps `innerhtml` → `innerHTML`, and the assignment routes through the SVGSVGElement's `innerHTML` setter which DOES enter SVG insertion mode).
|
|
1293
|
+
|
|
1294
|
+
**`<foreignObject>` boundary:** content inside `<foreignObject>` returns to HTML namespace per HTML5 §12.2.5 foreign-content insertion mode. The template engine respects this — interpolations inside `<foreignObject>` are HTML-namespaced even when the outer container is SVG. Use it as the documented escape hatch for HTML widgets embedded in SVG graphs (tooltips, popovers, foreign Cells in dataviz).
|
|
1295
|
+
|
|
1296
|
+
**MathML** uses the same mechanism: nested `${html\`<mi/>\`}` inside `<math>` gets re-namespaced to MathML NS. `<annotation-xml encoding="text/html">` is the MathML equivalent of `<foreignObject>` for HTML re-entry.
|
|
1297
|
+
|
|
1298
|
+
**Migration:** zero. Pre-v0.6.8 code that already worked (inline SVG + `.innerHTML` commits) keeps working. Code that was silently broken (nested SVG interpolations) starts producing visible output.
|
|
1299
|
+
|
|
1300
|
+
### §-TBD (v0.6.8) — `.camelCaseProp=${expr}` property binding (FB-55)
|
|
1301
|
+
|
|
1302
|
+
Pre-v0.6.8 trap: the HTML parser lowercases attribute names inside `<template>.innerHTML` per HTML5 §13.2.5.32, so `.className=${expr}` arrived at the binding scanner as `.classname`. The binding then wrote `element['classname'] = v` — a lowercase enumerable **expando** — never invoking the `HTMLElement.prototype.className` setter. CSS classes never applied; no warning, no error.
|
|
1303
|
+
|
|
1304
|
+
Same trap affected every camelCase DOM property surface: `.className`, `.innerText`, `.tabIndex`, `.ariaLabel`, `.contentEditable`, `.readOnly`, `.maxLength`, `.minLength`, `.colSpan`, `.rowSpan` — AND every camelCase property declared via `UIElement.static properties` (e.g. `<color-picker>`'s `.maxChroma`, `<grid>`'s `.columnGap`, `<breadcrumb>`'s `.collapseKeepLeading`, plus 80+ others across primitives). Even the canonical reactive-binding example from `README.md` (`<my-panel .minL=${minL}>`) silently failed.
|
|
1305
|
+
|
|
1306
|
+
v0.6.8 fix: a two-layer name resolution in `core/template.js`:
|
|
1307
|
+
|
|
1308
|
+
1. **`PROP_CASE_FIX` static map** at scan time covers built-in DOM camelCase properties (zero-cost lookup, no prototype walk for the common case).
|
|
1309
|
+
2. **Prototype-walk fallback** at `applyValue()` time finds canonical camelCase property names on the element's instance + prototype chain via case-insensitive match — caches the resolution on the part after first hit. Catches UIElement custom-element props installed per-instance in `installProps()`.
|
|
1310
|
+
|
|
1311
|
+
```js
|
|
1312
|
+
// ✅ Works post-v0.6.8 — both forms route to the canonical setter
|
|
1313
|
+
html`<span .className=${'foo bar'}>text</span>`;
|
|
1314
|
+
html`<color-picker .maxChroma=${0.3}></color-picker>`;
|
|
1315
|
+
html`<my-panel .minL=${minL}></my-panel>`;
|
|
1316
|
+
```
|
|
1317
|
+
|
|
1318
|
+
**Backward compatibility:** when neither the map nor the prototype walk finds a case-insensitive match, the original lowercase name is preserved. Consumers deliberately writing to lowercase expando properties (`.myexpando=${v}`) are unaffected.
|
|
1319
|
+
|
|
1320
|
+
**Discoverability note:** the simpler form `class="${expr}"` (full-attribute interpolation) works without needing to know the camelCase property name and is the v0.6.8 partial-interp warn-text's first recommendation. Reach for `.className=` when you specifically want the property-binding semantics (e.g., when the value carries non-string types, or for performance in tight inner loops).
|
|
1321
|
+
|
|
1184
1322
|
### §345 (v0.5.19) — `.map()` vs `repeat()` for reactive lists (FB-47)
|
|
1185
1323
|
|
|
1186
1324
|
Use **`repeat(items, keyFn, tplFn)`** for any list whose items are signal-driven and should preserve identity across re-renders. Plain `.map()` returns an `Array` of template results, which the template engine materializes via `container.replaceChildren()` on every parent update — correct, but per-item DOM is re-created from scratch.
|
|
@@ -1277,6 +1415,83 @@ Use the descendant combinator (a single space) instead, which traverses any dept
|
|
|
1277
1415
|
|
|
1278
1416
|
Same caveat applies to `:nth-child()` / `:nth-of-type()` / `:first-child` / `:last-child` against the parent — those count actual children (the wrapper-spans), not the items inside. If you need nth-item styling, key it via a data attribute on the item itself rather than relying on positional pseudo-classes against the wrapper layer.
|
|
1279
1417
|
|
|
1418
|
+
### §-TBD (v0.6.8) — UIElement lifecycle hooks (FB-59 + FB-43/44/47/58 cluster)
|
|
1419
|
+
|
|
1420
|
+
UIElement orchestrates the custom-element lifecycle and exposes two extension points: the W3C standard `connectedCallback()` (rarely overridden) and the convenience **`connected()` hook (the recommended subclass extension point)**. Same pattern for `disconnected()`.
|
|
1421
|
+
|
|
1422
|
+
#### Canonical hook order
|
|
1423
|
+
|
|
1424
|
+
For a UIElement subclass with `static template = () => html\`<x/>\`` mounted into the DOM with consumer-supplied light-DOM children:
|
|
1425
|
+
|
|
1426
|
+
1. **`adoptStyles(ctor)`** — adopted stylesheets attached to the document
|
|
1427
|
+
2. **`prepareParts(ctor)`** — `static template` markup parsed into the part registry
|
|
1428
|
+
3. **`this.connected()`** — subclass hook fires here, inside `untracked(...)`. **Light-DOM children supplied by the parent template are intact at this point.**
|
|
1429
|
+
4. **Effect first-run** — `stamp(result, this)` runs here if `ctor.template(this)` returns non-null. This is the call that **wipes consumer-supplied light-DOM children** (via `mount()` → `container.replaceChildren(f)`).
|
|
1430
|
+
5. **`this.render()`** — explicit render hook for imperative DOM updates
|
|
1431
|
+
6. **`this.updated(changedProps)`** — change-set hook for property-specific reactions
|
|
1432
|
+
|
|
1433
|
+
**The key insight for container components: `connected()` runs BEFORE the stamp.** If your container UIElement has both a non-null `static template` AND wants to capture consumer-supplied light-DOM children, do the capture in `connected()`. The wipe runs in step 4, which is after step 3.
|
|
1434
|
+
|
|
1435
|
+
Source: `packages/web-components/core/element.js:107-142` — `UIElement.connectedCallback()`. `connected()` fires at line 122 inside `untracked(...)`; the stamp runs at line 127 inside the effect registered at lines 123-137.
|
|
1436
|
+
|
|
1437
|
+
#### Container UIElement pattern (FB-43/44/47/58 cluster — the right shape)
|
|
1438
|
+
|
|
1439
|
+
When your UIElement is a container for consumer-supplied light-DOM children AND has its own internal `static template`, capture the children in `connected()`:
|
|
1440
|
+
|
|
1441
|
+
```js
|
|
1442
|
+
class DtsGraph extends UIElement {
|
|
1443
|
+
static template = () => html`<div class="dts-graph-root"></div>`;
|
|
1444
|
+
|
|
1445
|
+
// ✅ connected() runs BEFORE the stamp wipes children — capture here
|
|
1446
|
+
connected() {
|
|
1447
|
+
this._series = [...this.querySelectorAll('dts-graph-band, dts-graph-curve, …')];
|
|
1448
|
+
|
|
1449
|
+
// Optional: MutationObserver for late-arriving children (e.g. parent template
|
|
1450
|
+
// toggles `${cond ? html\`<band/>\` : ''}` and the new band lands later)
|
|
1451
|
+
this._mo = new MutationObserver(() => this._captureSeries());
|
|
1452
|
+
this._mo.observe(this, { childList: true, subtree: true });
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
_captureSeries() {
|
|
1456
|
+
const present = [...this.querySelectorAll('dts-graph-band, …')];
|
|
1457
|
+
// Defensive cache-fallback: post-stamp wipe makes `present` empty, but the
|
|
1458
|
+
// initial capture from connected() above already saved the refs. Keep them.
|
|
1459
|
+
if (present.length > 0) this._series = present;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
disconnected() {
|
|
1463
|
+
this._mo?.disconnect();
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
The `if (present.length > 0) keep` cache-fallback is the architectural pattern that makes the capture robust to either ordering. It works in any lifecycle environment with no specification dependency.
|
|
1469
|
+
|
|
1470
|
+
#### `connected()` vs `connectedCallback()` reference
|
|
1471
|
+
|
|
1472
|
+
| Hook | Who calls it | When (relative to step list above) | Use case |
|
|
1473
|
+
|---|---|---|---|
|
|
1474
|
+
| `connectedCallback()` | The browser, per W3C custom-elements spec | Orchestrates the whole sequence | Almost never override — UIElement runs the lifecycle for you |
|
|
1475
|
+
| **`connected()`** | UIElement, from within `connectedCallback()` at line 122 | Step 3 — after `prepareParts()`, BEFORE the template stamp | **Subclass setup, light-DOM capture, observer arming, signal effects** |
|
|
1476
|
+
| `render()` | The effect, after every signal change | Step 5 — after `stamp()` if the template returned non-null | Imperative DOM updates that don't fit the template engine |
|
|
1477
|
+
| `updated(changedProps)` | The effect | Step 6 — after `render()`, when properties changed | React to specific prop changes |
|
|
1478
|
+
| `disconnected()` | UIElement, from `disconnectedCallback()` | Element leaves the DOM | Teardown observers, timers, listeners |
|
|
1479
|
+
|
|
1480
|
+
#### When NOT to override `connectedCallback()` directly
|
|
1481
|
+
|
|
1482
|
+
`UIElement.connectedCallback()` orchestrates the entire lifecycle. Overriding it directly requires re-invoking `super.connectedCallback()` correctly, and you'd have to choose whether your work goes before or after super:
|
|
1483
|
+
|
|
1484
|
+
- **BEFORE super**: you miss the `prepareParts(ctor)` call that initializes the part registry. `static template` won't stamp. Other downstream initialization breaks.
|
|
1485
|
+
- **AFTER super**: your work runs after step 6 (`updated()`), which means after the stamp wiped your light-DOM children. You'd need to capture in `constructor()` instead — but `constructor()` runs before the element is in the DOM, so light-DOM children aren't appended yet.
|
|
1486
|
+
|
|
1487
|
+
`connected()` is the right hook for 99% of consumer code. It runs at exactly the right time: children present, stamp not yet run.
|
|
1488
|
+
|
|
1489
|
+
#### Why `connected()` is wrapped in `untracked()`
|
|
1490
|
+
|
|
1491
|
+
Per the comment in `element.js:112-121`: `connected()` commonly reads reactive properties via template-literal interpolation (`${this.label}` etc.) when stamping inner DOM. If the outer call path is inside another effect (e.g. a parent's render loop `appendChild`'ing this node), those reads would subscribe the outer effect to this element's signals — invisible cross-element coupling that looks like spooky re-runs on unrelated state changes. The `untracked()` wrapper prevents that subscription leak. The element's own effect (registered at lines 123-137) re-reads the same signals in its own tracking context, so reactivity within the element is unaffected.
|
|
1492
|
+
|
|
1493
|
+
This means: inside `connected()`, you CAN read signals/props for setup logic, but those reads won't establish reactive subscriptions. Use the element's own `render()` or signal effects for reactive work; use `connected()` for one-shot setup.
|
|
1494
|
+
|
|
1280
1495
|
### §221j — Typography token cheatsheet
|
|
1281
1496
|
|
|
1282
1497
|
Quick-reference for component-CSS authoring. Cross-reference [`styles/typography.css`](./styles/typography.css):
|
|
@@ -99,7 +99,7 @@ action-item-ui:hover [slot="icon"] {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/* Default slot (trailing content: kbd, badge, chevron) pushed to far right */
|
|
102
|
-
> :not([slot="icon"]):not([slot="text"]) {
|
|
102
|
+
& > :not([slot="icon"]):not([slot="text"]) {
|
|
103
103
|
flex-shrink: 0;
|
|
104
104
|
margin-inline-start: auto;
|
|
105
105
|
color: var(--action-item-icon-fg);
|
package/components/card/card.css
CHANGED
|
@@ -149,7 +149,7 @@
|
|
|
149
149
|
[padding] switches to padding mode with background.
|
|
150
150
|
Grid layout: optional icon | heading+description | optional action */
|
|
151
151
|
|
|
152
|
-
> header {
|
|
152
|
+
& > header {
|
|
153
153
|
display: block;
|
|
154
154
|
margin: var(--card-inset);
|
|
155
155
|
}
|
|
@@ -157,24 +157,24 @@
|
|
|
157
157
|
/* Activate grid layout only when a DIRECT slotted child is present.
|
|
158
158
|
Constraining to `:has(> [slot])` prevents deeply nested slots (e.g. an
|
|
159
159
|
[slot="icon"] inside an <avatar-ui>) from falsely activating the grid. */
|
|
160
|
-
> header:has(> [slot]) {
|
|
160
|
+
& > header:has(> [slot]) {
|
|
161
161
|
display: grid;
|
|
162
162
|
gap: var(--card-header-gap);
|
|
163
163
|
align-items: center;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
> header[padding] {
|
|
166
|
+
& > header[padding] {
|
|
167
167
|
margin: 0;
|
|
168
168
|
padding: var(--card-inset);
|
|
169
169
|
background: var(--card-bg-padded);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
> header[divider] {
|
|
172
|
+
& > header[divider] {
|
|
173
173
|
padding-bottom: var(--card-inset);
|
|
174
174
|
border-bottom: 1px solid var(--card-divider);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
> header[center] {
|
|
177
|
+
& > header[center] {
|
|
178
178
|
text-align: center;
|
|
179
179
|
justify-items: center;
|
|
180
180
|
}
|
|
@@ -182,16 +182,16 @@
|
|
|
182
182
|
/* Column templates — match gen-ui-kit pattern. Direct-child `:has(> …)` so
|
|
183
183
|
nested [slot="icon"] inside a composite (e.g. <avatar-ui>) can't trigger
|
|
184
184
|
the icon column by accident. */
|
|
185
|
-
> header:has(> [slot="icon"]):has(> [slot="action"]) { grid-template-columns: max-content 1fr max-content; }
|
|
186
|
-
> header:has(> [slot="icon"]):not(:has(> [slot="action"])) { grid-template-columns: max-content 1fr; }
|
|
187
|
-
> header:not(:has(> [slot="icon"])):has(> [slot="action"]) { grid-template-columns: 1fr max-content; }
|
|
188
|
-
> header:not(:has(> [slot="icon"])):not(:has(> [slot="action"])) { grid-template-columns: 1fr; }
|
|
185
|
+
& > header:has(> [slot="icon"]):has(> [slot="action"]) { grid-template-columns: max-content 1fr max-content; }
|
|
186
|
+
& > header:has(> [slot="icon"]):not(:has(> [slot="action"])) { grid-template-columns: max-content 1fr; }
|
|
187
|
+
& > header:not(:has(> [slot="icon"])):has(> [slot="action"]) { grid-template-columns: 1fr max-content; }
|
|
188
|
+
& > header:not(:has(> [slot="icon"])):not(:has(> [slot="action"])) { grid-template-columns: 1fr; }
|
|
189
189
|
|
|
190
190
|
/* Unslotted children (zettel fragment injection, etc.) — each on its own row,
|
|
191
191
|
spanning the full width, stacked ABOVE the heading/description/action grid.
|
|
192
192
|
This lets compositions inject logos, banners, etc. into a header without
|
|
193
193
|
having to know the slot vocabulary. */
|
|
194
|
-
> header > *:not([slot]):not(h1):not(h2):not(h3):not(h4):not(h5):not(h6):not(p):not(small) {
|
|
194
|
+
& > header > *:not([slot]):not(h1):not(h2):not(h3):not(h4):not(h5):not(h6):not(p):not(small) {
|
|
195
195
|
grid-column: 1 / -1;
|
|
196
196
|
justify-self: center;
|
|
197
197
|
}
|
|
@@ -200,7 +200,7 @@
|
|
|
200
200
|
Default: single row, center-aligned with heading/action.
|
|
201
201
|
When a description row exists, spans both rows and anchors to the
|
|
202
202
|
start so the icon visually aligns with the heading. */
|
|
203
|
-
> header > [slot="icon"] {
|
|
203
|
+
& > header > [slot="icon"] {
|
|
204
204
|
grid-column: 1;
|
|
205
205
|
grid-row: 1;
|
|
206
206
|
align-self: center;
|
|
@@ -209,39 +209,39 @@
|
|
|
209
209
|
justify-content: center;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
> header:has(> :is([slot="description"], p, small)) > [slot="icon"] {
|
|
212
|
+
& > header:has(> :is([slot="description"], p, small)) > [slot="icon"] {
|
|
213
213
|
grid-row: 1 / span 2;
|
|
214
214
|
align-self: start;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
/* Heading — row 1 */
|
|
218
|
-
> header > :is([slot="heading"], h1, h2, h3, h4, h5, h6),
|
|
219
|
-
> header > [slot="heading"] :is(h1, h2, h3, h4, h5, h6) {
|
|
218
|
+
& > header > :is([slot="heading"], h1, h2, h3, h4, h5, h6),
|
|
219
|
+
& > header > [slot="heading"] :is(h1, h2, h3, h4, h5, h6) {
|
|
220
220
|
grid-row: 1;
|
|
221
221
|
line-height: 1.3;
|
|
222
222
|
margin: 0;
|
|
223
223
|
}
|
|
224
224
|
/* Heading slot is a flex container — can hold title + inline badges/metadata */
|
|
225
|
-
> header > [slot="heading"] {
|
|
225
|
+
& > header > [slot="heading"] {
|
|
226
226
|
display: flex;
|
|
227
227
|
align-items: center;
|
|
228
228
|
gap: var(--card-header-gap);
|
|
229
229
|
}
|
|
230
|
-
> header:has(> [slot="icon"]) > :is([slot="heading"], h1, h2, h3, h4, h5, h6) { grid-column: 2; }
|
|
231
|
-
> header:not(:has(> [slot="icon"])) > :is([slot="heading"], h1, h2, h3, h4, h5, h6) { grid-column: 1; }
|
|
230
|
+
& > header:has(> [slot="icon"]) > :is([slot="heading"], h1, h2, h3, h4, h5, h6) { grid-column: 2; }
|
|
231
|
+
& > header:not(:has(> [slot="icon"])) > :is([slot="heading"], h1, h2, h3, h4, h5, h6) { grid-column: 1; }
|
|
232
232
|
|
|
233
233
|
/* Description — row 2 */
|
|
234
|
-
> header > :is([slot="description"], p, small) {
|
|
234
|
+
& > header > :is([slot="description"], p, small) {
|
|
235
235
|
grid-row: 2;
|
|
236
236
|
grid-column: 1 / -1;
|
|
237
237
|
line-height: 1.4;
|
|
238
238
|
margin: 0;
|
|
239
239
|
}
|
|
240
|
-
> header:has(> [slot="icon"]) > :is([slot="description"], p, small) { grid-column: 2 / -1; }
|
|
240
|
+
& > header:has(> [slot="icon"]) > :is([slot="description"], p, small) { grid-column: 2 / -1; }
|
|
241
241
|
|
|
242
242
|
/* Action — row 1, last column.
|
|
243
243
|
Flex container so it can hold badge + button + anything inline. */
|
|
244
|
-
> header [slot="action"] {
|
|
244
|
+
& > header [slot="action"] {
|
|
245
245
|
justify-self: end;
|
|
246
246
|
align-self: center;
|
|
247
247
|
grid-row: 1;
|
|
@@ -256,17 +256,17 @@
|
|
|
256
256
|
[padding] switches to padding with background.
|
|
257
257
|
[bleed] removes all spacing for edge-to-edge content. */
|
|
258
258
|
|
|
259
|
-
> section {
|
|
259
|
+
& > section {
|
|
260
260
|
margin: var(--card-inset);
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
> section[padding] {
|
|
263
|
+
& > section[padding] {
|
|
264
264
|
margin: 0;
|
|
265
265
|
padding: var(--card-inset);
|
|
266
266
|
background: var(--card-bg-padded);
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
> section[bleed] {
|
|
269
|
+
& > section[bleed] {
|
|
270
270
|
margin: 0;
|
|
271
271
|
padding: 0;
|
|
272
272
|
}
|
|
@@ -276,7 +276,7 @@
|
|
|
276
276
|
[divider] adds top border.
|
|
277
277
|
[padding] switches to padded mode. */
|
|
278
278
|
|
|
279
|
-
> footer {
|
|
279
|
+
& > footer {
|
|
280
280
|
display: block;
|
|
281
281
|
margin: var(--card-inset);
|
|
282
282
|
}
|
|
@@ -284,8 +284,8 @@
|
|
|
284
284
|
/* Activate flex layout when a direct slotted child or multiple children are
|
|
285
285
|
present. `:has(> [slot])` so nested [slot="…"] inside composites can't
|
|
286
286
|
trigger the flex row. */
|
|
287
|
-
> footer:has(> [slot]),
|
|
288
|
-
> footer:has(> :nth-child(2)) {
|
|
287
|
+
& > footer:has(> [slot]),
|
|
288
|
+
& > footer:has(> :nth-child(2)) {
|
|
289
289
|
display: flex;
|
|
290
290
|
flex-wrap: wrap;
|
|
291
291
|
align-items: center;
|
|
@@ -293,30 +293,30 @@
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
/* col-ui in footer takes full width (common for stacked footer: button + divider + social) */
|
|
296
|
-
> footer > col-ui { width: 100%; }
|
|
296
|
+
& > footer > col-ui { width: 100%; }
|
|
297
297
|
|
|
298
|
-
> footer[justify="end"] {
|
|
298
|
+
& > footer[justify="end"] {
|
|
299
299
|
justify-content: flex-end;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
-
> footer[divider] {
|
|
302
|
+
& > footer[divider] {
|
|
303
303
|
margin-top: var(--card-inset);
|
|
304
304
|
padding-top: var(--card-inset);
|
|
305
305
|
border-top: 1px solid var(--card-divider);
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
-
> footer[padding] {
|
|
308
|
+
& > footer[padding] {
|
|
309
309
|
margin: 0;
|
|
310
310
|
padding: var(--card-inset);
|
|
311
311
|
background: var(--card-bg-padded);
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
/* Footer with direct-child description + action = space-between */
|
|
315
|
-
> footer:has(> :is([slot="description"], p, small)):has(> [slot="action"]) {
|
|
315
|
+
& > footer:has(> :is([slot="description"], p, small)):has(> [slot="action"]) {
|
|
316
316
|
justify-content: space-between;
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
-
> footer :is([slot="description"], p, small) {
|
|
319
|
+
& > footer :is([slot="description"], p, small) {
|
|
320
320
|
font-size: var(--card-description-size);
|
|
321
321
|
color: var(--card-description-fg);
|
|
322
322
|
flex: 1;
|
|
@@ -327,7 +327,7 @@
|
|
|
327
327
|
/* Footer heading — trend-stat line (symmetric with header's [slot="heading"]).
|
|
328
328
|
Used for chart trend-footer patterns like "Trending up by 5.2% this month".
|
|
329
329
|
Paired with [slot="description"] for a "Jan – Mar 2024" period caption. */
|
|
330
|
-
> footer > [slot="heading"] {
|
|
330
|
+
& > footer > [slot="heading"] {
|
|
331
331
|
font-size: var(--card-font-size);
|
|
332
332
|
font-weight: var(--a-weight-medium);
|
|
333
333
|
color: var(--card-heading-fg);
|
|
@@ -339,21 +339,21 @@
|
|
|
339
339
|
/* Footer with heading + description = column stack, heading on top. The
|
|
340
340
|
existing flex-row layout becomes a flex-column when a heading is present,
|
|
341
341
|
so the trend line and period caption stack naturally. */
|
|
342
|
-
> footer:has(> [slot="heading"]) {
|
|
342
|
+
& > footer:has(> [slot="heading"]) {
|
|
343
343
|
flex-direction: column;
|
|
344
344
|
align-items: flex-start;
|
|
345
345
|
gap: var(--a-space-0-5);
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
-
> footer:has(> [slot="heading"])[justify="end"] {
|
|
348
|
+
& > footer:has(> [slot="heading"])[justify="end"] {
|
|
349
349
|
align-items: flex-end;
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
-
> footer:has(> [slot="heading"])[justify="center"] {
|
|
352
|
+
& > footer:has(> [slot="heading"])[justify="center"] {
|
|
353
353
|
align-items: center;
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
-
> footer [slot="action"] {
|
|
356
|
+
& > footer [slot="action"] {
|
|
357
357
|
margin-inline-start: auto;
|
|
358
358
|
display: flex;
|
|
359
359
|
gap: var(--card-footer-gap);
|
|
@@ -361,14 +361,14 @@
|
|
|
361
361
|
|
|
362
362
|
/* Multiple direct action-slotted children (A2UI pattern):
|
|
363
363
|
only the first pushes the group right; subsequent ones flow with gap */
|
|
364
|
-
> footer > [slot="action"] ~ [slot="action"] {
|
|
364
|
+
& > footer > [slot="action"] ~ [slot="action"] {
|
|
365
365
|
margin-inline-start: 0;
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
/* Dual-cluster footer: leading action (e.g. Delete) on the inline-start
|
|
369
369
|
edge, trailing action cluster on the inline-end. margin-inline-end:
|
|
370
370
|
auto fills the gap between the two groups. */
|
|
371
|
-
> footer > [slot="action-leading"] {
|
|
371
|
+
& > footer > [slot="action-leading"] {
|
|
372
372
|
margin-inline-end: auto;
|
|
373
373
|
display: flex;
|
|
374
374
|
gap: var(--card-footer-gap);
|
|
@@ -376,15 +376,15 @@
|
|
|
376
376
|
|
|
377
377
|
/* ═══════ Images ═══════ */
|
|
378
378
|
|
|
379
|
-
> img,
|
|
380
|
-
> [slot="media"] {
|
|
379
|
+
& > img,
|
|
380
|
+
& > [slot="media"] {
|
|
381
381
|
display: block;
|
|
382
382
|
width: 100%;
|
|
383
383
|
object-fit: cover;
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
-
> img:first-child,
|
|
387
|
-
> [slot="media"]:first-child {
|
|
386
|
+
& > img:first-child,
|
|
387
|
+
& > [slot="media"]:first-child {
|
|
388
388
|
border-radius: var(--card-radius) var(--card-radius) 0 0;
|
|
389
389
|
}
|
|
390
390
|
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
/* Empty-state slot — author places <empty-state-ui slot="empty"> inside
|
|
113
113
|
chart-ui. CSS toggles visibility on [data-has-data] (set by chart.js
|
|
114
114
|
when .data is non-empty). No data ⇒ empty-state visible; data ⇒ hidden. */
|
|
115
|
-
> [slot="empty"] { display: none; }
|
|
115
|
+
& > [slot="empty"] { display: none; }
|
|
116
116
|
:scope:not([data-has-data]) > [slot="empty"] {
|
|
117
117
|
display: flex;
|
|
118
118
|
flex: 1 1 auto;
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/* -- Header -- */
|
|
68
|
-
> header {
|
|
68
|
+
& > header {
|
|
69
69
|
display: flex;
|
|
70
70
|
align-items: center;
|
|
71
71
|
gap: var(--chat-gap);
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
border-bottom: 1px solid var(--chat-border-color);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
> header [slot="avatar"] {
|
|
76
|
+
& > header [slot="avatar"] {
|
|
77
77
|
width: var(--chat-header-avatar-size);
|
|
78
78
|
height: var(--chat-header-avatar-size);
|
|
79
79
|
border-radius: var(--chat-header-avatar-radius);
|
|
@@ -87,19 +87,19 @@
|
|
|
87
87
|
flex-shrink: 0;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
> header [slot="name"] {
|
|
90
|
+
& > header [slot="name"] {
|
|
91
91
|
font-weight: var(--chat-header-name-weight);
|
|
92
92
|
font-size: var(--chat-header-name-size);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
> header [slot="status"] {
|
|
95
|
+
& > header [slot="status"] {
|
|
96
96
|
font-size: var(--chat-header-status-size);
|
|
97
97
|
color: var(--chat-header-status-fg);
|
|
98
98
|
margin-inline-start: auto;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/* -- Messages -- */
|
|
102
|
-
> section {
|
|
102
|
+
& > section {
|
|
103
103
|
flex: 1;
|
|
104
104
|
overflow-y: auto;
|
|
105
105
|
padding: var(--chat-messages-padding);
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
/* -- Footer -- */
|
|
178
|
-
> footer {
|
|
178
|
+
& > footer {
|
|
179
179
|
min-height: var(--chat-footer-min-height);
|
|
180
180
|
display: flex;
|
|
181
181
|
align-items: center;
|