@adia-ai/web-components 0.7.26 → 0.7.28
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 +38 -6
- package/components/chart/chart.a2ui.json +1 -1
- package/components/chart/chart.yaml +2 -0
- package/components/kbd/kbd.a2ui.json +4 -0
- package/components/kbd/kbd.d.ts +2 -0
- package/components/kbd/kbd.yaml +5 -0
- package/components/nav/nav.class.js +7 -0
- package/components/richtext/richtext.a2ui.json +5 -0
- package/components/richtext/richtext.css +20 -0
- package/components/richtext/richtext.d.ts +2 -0
- package/components/richtext/richtext.yaml +10 -0
- package/components/table/cell-types.js +31 -2
- package/components/table/table.a2ui.json +1 -1
- package/components/table/table.test.js +37 -0
- package/components/table/table.yaml +2 -0
- package/core/markdown.js +29 -4
- package/core/markdown.test.js +47 -0
- package/core/provider.js +17 -0
- package/dist/host.min.css +1 -1
- package/dist/host.sheet.js +1 -1
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +64 -64
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -1
- package/styles/README.md +1 -1
- package/styles/resets.css +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,36 @@
|
|
|
1
1
|
# Changelog — @adia-ai/web-components
|
|
2
|
+
## [0.7.28] — 2026-07-14
|
|
3
|
+
|
|
4
|
+
### Added
|
|
5
|
+
- **`table-ui` (`components/table/cell-types.js`) — `markdown` cell type: inline `` `code` ``/**bold**/*italic*/`[link](href)` rendered as real elements, not a plain string.** TKT-0008 (site-a2ui re-verification, direction 1): docs reference tables (props/API tables) mix prose with multiple inline `<code>` spans per cell — content the `{columns, data}` JSON contract previously couldn't carry, forcing a preserved-HTML fallback that dropped the table from the A2UI graph entirely. Renders via `core/markdown.js`'s `inline()` (now exported) — the exact encode/decode pairing the docs-transpiler's own `convertInline()` already produces elsewhere in the pipeline. `[title]` carries the tag-stripped plain text for the truncation tooltip. New unit suite in `table.test.js` (3 cases).
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **`core/markdown.js` — `inline()` is now exported.** Was module-private; reused by `cell-types.js`'s new `markdown` cell type as the client-side half of the docs-transpiler's `convertInline()` contract. No behavior change to existing callers.
|
|
9
|
+
|
|
10
|
+
### Fixed — TKT-0010, corpus admission-validation content gaps
|
|
11
|
+
- **`kbd.yaml` — declares a `textContent` prop.** Every other text-bearing leaf (`Text`, `Code`) already declares one; `Kbd` was missing it, so the transpiler's unconditional `textContent` extraction for leaf types produced a value the catalog schema rejected as an unevaluated property on every harvested `<kbd-ui>` chunk (12 instances). Purely a schema declaration gap — no behavior change (the transpiler already extracted the value correctly).
|
|
12
|
+
- **`chart.yaml`/`table.yaml` — `data` prop declares `items: {type: object}`.** Opts these two dynamic array props into `@adia-ai/a2ui-corpus`'s new `DynamicObjectList` schema (array-of-object) instead of the default `DynamicStringList` (array-of-string) — the wrong shape for chart/table row records. Paired with a transpiler fix in `@adia-ai/a2ui-compose` (the attribute value was never actually parsed); see that package's CHANGELOG.
|
|
13
|
+
|
|
14
|
+
### Fixed — GH #224, SVG zero-viewport silent non-render
|
|
15
|
+
- **`styles/resets.css` — `svg` gets its own reset rule, dropping `max-inline-size`/`block-size` instead of sharing `img`/`picture`/`video`/`canvas`'s `max-inline-size: 100%; block-size: auto` rule.** Per the SVG spec, a zero-size viewport — exactly what `max-inline-size: 100%` computes to inside a zero-intrinsic-size ancestor (the common `width:0; height:0; transform:scale(s)` pattern for a CSS-scaled canvas/scene) — makes the element **not render at all**, not "render at 0 visual size": silent, no console warning. `img`/`picture`/`video`/`canvas` don't share this failure mode. Components that embed SVG (`icon-ui`, etc.) already size it explicitly in their own scoped CSS and are unaffected either way (verified: no component CSS relies on the global rule's sizing behavior). Verified via the full web-modules test suite + a manual repro check against the reporting issue's own reproduction case (no dedicated new test — a global CSS reset rule).
|
|
16
|
+
|
|
17
|
+
### Maintenance
|
|
18
|
+
- **`dist/` CDN bundles rebuilt** (`host.min.css`, `host.sheet.js`, `theme-provider.min.js`, `web-components.min.{css,js}`, `web-components.sheet.js`) to inline the `resets.css` fix above.
|
|
19
|
+
|
|
20
|
+
## [0.7.27] — 2026-07-12
|
|
21
|
+
|
|
22
|
+
Fixes harvested from the /site → A2UI docs migration's live operator reports (TKT-0002…0006) — each was a real framework defect the migration surfaced, not a docs-only issue — plus the router race that three separate flake classes traced back to.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **`richtext-ui` (`components/richtext/`) — `flush` boolean prop: paragraph-grade embedding mode.** Removes the article chrome (the `[data-richtext-body]` `space-8`/`space-6` padding, the 45rem reading measure, the component's own canvas background, and the trailing paragraph margin) so rendered markdown sits flush in a host column like a plain text block. Use it when richtext carries a single paragraph or fragment inside an already-measured layout (doc pages, cards); leave it off for standalone articles and chat transcripts, which keep the article treatment unchanged. Motivation: the docs transpiler rides richtext for every paragraph with inline code marks — 1,672 instances across 275 pages each carried article insets (TKT-0004 B).
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **`router-ui` (`core/provider.js`) — `#loadContent` claims a monotonic navigation token BEFORE any await.** A navigation that started earlier but fetched its content slowly could resume after a faster follow-up navigation had fully rendered, and — because nothing sequenced completion against navigation order — legitimately clobber the newer route's content while `data-route-path` still named the new route. Reproduced deterministically (2s-delayed boot fetch: old code showed `path=button` with the Dashboard DOM). The token is re-checked after every await (content fetch, templateResolver, setup import); a superseded call neither writes `innerHTML` nor emits `route-loaded`. This was the root mechanism behind the site-a2ui migration's "no-artifact despite a valid artifact" residue and its batch-sweep flakes.
|
|
29
|
+
- **`nav-ui` (`components/nav/`) — collapsed-mode flyouts no longer re-open after selecting an item (TKT-0005).** The flyout popover is appended inside its `nav-group-ui`, so an option click bubbled to `nav-ui`'s delegated click handler, whose collapsed branch called `group.showPopover()` for any click landing inside the group — instantly re-opening the flyout the option's own handler had just closed. The collapsed branch now ignores clicks originating inside `[popover]`, mirroring the header guard the expanded branch always had.
|
|
30
|
+
- **`core/markdown.js` — inline code spans survive emphasis/link regexes.** `inline()` now extracts backtick code spans into placeholders before applying the bold/italic/link transforms and restores them verbatim after — code spans containing `*`, `_`, `[`, or `]` no longer render mangled. New unit suite (`core/markdown.test.js`, 6 cases).
|
|
31
|
+
|
|
32
|
+
### Maintenance
|
|
33
|
+
- `dist/` CDN bundles regenerated (`web-components.min.css`, `web-components.min.js`, `theme-provider.min.js`) carrying the richtext `flush` rules + the router/nav/markdown fixes.
|
|
2
34
|
|
|
3
35
|
## [0.7.26] — 2026-07-04
|
|
4
36
|
|
|
@@ -411,7 +443,7 @@ Upstream consumer findings from `adia-pay` against installed `0.7.23` (F1–F6).
|
|
|
411
443
|
|
|
412
444
|
### Changed — `slot="caret"` disclosure-indicator convention + FEEDBACK-89 (ADR-0036)
|
|
413
445
|
|
|
414
|
-
- **`components/tree/` (`tree-item-ui`) + `components/pane/` (`pane-ui`)** — renamed the expand/collapse indicator slot `slot="chevron"` → **`slot="caret"`** (and tokens `--*-chevron-*` → `--*-caret-*`, e.g. `--tree-caret-size`, `--pane-caret-fg`), standardizing on the slot name already used by `accordion-item-ui` / `nav-group-ui` / `select-ui` and matching the Phosphor `caret-*` icon vocabulary. **No backward-compat alias** (hard rename; the caret is auto-stamped, so blast radius is small). See
|
|
446
|
+
- **`components/tree/` (`tree-item-ui`) + `components/pane/` (`pane-ui`)** — renamed the expand/collapse indicator slot `slot="chevron"` → **`slot="caret"`** (and tokens `--*-chevron-*` → `--*-caret-*`, e.g. `--tree-caret-size`, `--pane-caret-fg`), standardizing on the slot name already used by `accordion-item-ui` / `nav-group-ui` / `select-ui` and matching the Phosphor `caret-*` icon vocabulary. **No backward-compat alias** (hard rename; the caret is auto-stamped, so blast radius is small). See `ADR-0036`.
|
|
415
447
|
- **`tree-item-ui` adopt-or-stamp (FEEDBACK-89)** — `#stamp()` now adopts a consumer's declarative `slot="caret"` and `slot="actions"` children into the auto-stamped row instead of leaving them as siblings below it. A `<button-ui slot="actions">` (the natural way to add a per-row action — add / rename / overflow) now lands inline in the styled, hover-revealed actions area. The host `#onClick` already excludes `[slot="actions"] *` from row selection, so adoption is click-safe. +3 unit tests.
|
|
416
448
|
- **`dist/` CDN bundles** (`web-components.min.css` + `web-components.min.js`) regenerated to carry the renamed `--*-caret-*` tokens + caret stamp logic.
|
|
417
449
|
|
|
@@ -3234,7 +3266,7 @@ Companion to the corpus simplification arc in `@adia-ai/a2ui-{runtime,compose,co
|
|
|
3234
3266
|
|
|
3235
3267
|
Catalog (`packages/a2ui/corpus/catalog-a2ui_0_9.json`) regenerated to pick up every yaml addition. `verify:traits` clean at 56/56; `components --verify` clean at 125 files.
|
|
3236
3268
|
|
|
3237
|
-
See root [CHANGELOG.md `[Unreleased]`](../../CHANGELOG.md) for the cross-cutting arc narrative +
|
|
3269
|
+
See root [CHANGELOG.md `[Unreleased]`](../../CHANGELOG.md) for the cross-cutting arc narrative + `.claude/docs/journal/2026/05/2026-05-12.md` §§ 43 / 44 / 50 / 51 for per-§ details.
|
|
3238
3270
|
## [0.4.3] - 2026-05-11
|
|
3239
3271
|
|
|
3240
3272
|
### Added
|
|
@@ -3250,7 +3282,7 @@ See root [CHANGELOG.md `[Unreleased]`](../../CHANGELOG.md) for the cross-cutting
|
|
|
3250
3282
|
|
|
3251
3283
|
### Lockstep
|
|
3252
3284
|
|
|
3253
|
-
9-package coordinated PATCH cut to v0.4.3 (per [`.claude/docs/specs/package-architecture.md` § 15](../../.claude/docs/specs/package-architecture.md#15-versioning-policy)). Internal `@adia-ai/*` dep ranges stay at `^0.4.0` (patch-cut asymmetry — `^0.4.0` covers `0.4.x` under semver). Ratifies
|
|
3285
|
+
9-package coordinated PATCH cut to v0.4.3 (per [`.claude/docs/specs/package-architecture.md` § 15](../../.claude/docs/specs/package-architecture.md#15-versioning-policy)). Internal `@adia-ai/*` dep ranges stay at `^0.4.0` (patch-cut asymmetry — `^0.4.0` covers `0.4.x` under semver). Ratifies `ADR-0027` — cross-primitive composition imports are the consumer's responsibility; the primitive's `.js` does NOT side-effect-import composed primitives. Source files touched: `components/input/input.{js,css,yaml,a2ui.json}`. See root [CHANGELOG.md `## [0.4.3]`](../../CHANGELOG.md) for the cut narrative.
|
|
3254
3286
|
## [0.4.2] - 2026-05-11
|
|
3255
3287
|
|
|
3256
3288
|
### Changed
|
|
@@ -3287,7 +3319,7 @@ See root [CHANGELOG.md `[Unreleased]`](../../CHANGELOG.md) for the cross-cutting
|
|
|
3287
3319
|
|
|
3288
3320
|
### Lockstep
|
|
3289
3321
|
|
|
3290
|
-
9-package coordinated PATCH cut to v0.4.2 (per [`.claude/docs/specs/package-architecture.md` § 15](../../.claude/docs/specs/package-architecture.md#15-versioning-policy)). Internal `@adia-ai/*` dep ranges stay at `^0.4.0` (patch-cut asymmetry — `^0.4.0` covers `0.4.x` under semver). ADR-0025 (
|
|
3322
|
+
9-package coordinated PATCH cut to v0.4.2 (per [`.claude/docs/specs/package-architecture.md` § 15](../../.claude/docs/specs/package-architecture.md#15-versioning-policy)). Internal `@adia-ai/*` dep ranges stay at `^0.4.0` (patch-cut asymmetry — `^0.4.0` covers `0.4.x` under semver). ADR-0025 (`.brain/adrs/0025-no-native-form-controls.md`) ratified — codifies the "no native or suppressed-native interactive widgets in primitives" principle that the `<input-ui type="number">` rewrite implements; `password` remains the documented exception (physically requires `<input type="password">` for `-webkit-text-security`). See root [CHANGELOG.md `## [0.4.2]`](../../CHANGELOG.md) for the cut narrative.
|
|
3291
3323
|
## [0.4.1] - 2026-05-10
|
|
3292
3324
|
|
|
3293
3325
|
### Ride-along (no source changes)
|
|
@@ -3738,7 +3770,7 @@ edge instead of leaving the column reading as dead width. Per-row
|
|
|
3738
3770
|
|
|
3739
3771
|
### Added — Trait test coverage at 100% + `<traits-host>` wrapper (2026-05-02)
|
|
3740
3772
|
|
|
3741
|
-
Follow-up landing on top of the v0.2.1 trait-system work — closes the two follow-up items from
|
|
3773
|
+
Follow-up landing on top of the v0.2.1 trait-system work — closes the two follow-up items from `ADR-0018`.
|
|
3742
3774
|
|
|
3743
3775
|
- **31 new per-trait behavior test files.** Every trait in `traits/` now has a sibling `.test.js`. Total trait-suite footprint: **44 files / 169 tests** (was 12 / 68). Coverage: **42/42 = 100%** (41 trait files + `traits-host`).
|
|
3744
3776
|
- **`verify:traits` CI gate flipped from soft-warning to hard-fail.** `npm run verify:traits` exits non-zero if any trait file lacks a sibling `.test.js`, with the offending names listed in the failure message. Verified via mutation test: deleting `active-state.test.js` immediately fails the gate; restoring it returns to clean.
|
|
@@ -3758,7 +3790,7 @@ Follow-up landing on top of the v0.2.1 trait-system work — closes the two foll
|
|
|
3758
3790
|
|
|
3759
3791
|
### Added — Trait system source-of-truth + declarative `traits=` attribute (2026-05-02)
|
|
3760
3792
|
|
|
3761
|
-
Rolls up under
|
|
3793
|
+
Rolls up under `ADR-0018`. Consumer-visible additions and one core change to UIElement's lifecycle.
|
|
3762
3794
|
|
|
3763
3795
|
- **`defineTrait()` schema extended.** Now requires `category` (one of nine canonical values, frozen in `define.js`) and `description` (≥ 11 chars). All 40 trait files updated to declare both inline. Build-time throw on missing fields.
|
|
3764
3796
|
- **New `getTrait(name)` export from `traits/define.js`.** Returns the trait factory for a kebab-case name, or null if not yet imported. `getTraitSchema(name)` now reads from the same registry (was a separate map).
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"data": {
|
|
57
57
|
"description": "JS property (set programmatically — `el.data = [...]`). An array of plain objects; each object's keys are named by the `x` and `y` attributes — e.g. `<chart-ui x=\"month\" y=\"revenue\">` consumes `[{month:'Jan', revenue:3200}, {month:'Feb', revenue:4100}]`. The Chart.js `{labels, datasets}` envelope is NOT chart-ui's API — passing it (or any non-array value) renders an empty chart. May also be supplied declaratively as a JSON-array `data=\"[…]\"` attribute, hydrated once at connect. Custom accessor on the element class, not a reflected attribute.",
|
|
58
|
-
"$ref": "common_types.json#/$defs/
|
|
58
|
+
"$ref": "common_types.json#/$defs/DynamicObjectList"
|
|
59
59
|
},
|
|
60
60
|
"format": {
|
|
61
61
|
"description": "Number-format mode applied to axis labels + value overlays + donut total + gauge value + treemap value + funnel value + internal tooltip. `abbr` is the legacy 1.2K / 3M format; `decimal` fixes 2 decimals; `currency` prefixes via `--chart-currency-prefix` token (default \"$\"); `percent` multiplies × 100 and adds a % suffix.",
|
|
@@ -122,6 +122,8 @@ props:
|
|
|
122
122
|
hydrated once at connect. Custom accessor on the element class, not a
|
|
123
123
|
reflected attribute.
|
|
124
124
|
type: array
|
|
125
|
+
items:
|
|
126
|
+
type: object
|
|
125
127
|
default: []
|
|
126
128
|
dynamic: true # JS-set collection prop with a custom setter — deliberately not in static properties
|
|
127
129
|
events:
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"lg"
|
|
26
26
|
],
|
|
27
27
|
"default": ""
|
|
28
|
+
},
|
|
29
|
+
"textContent": {
|
|
30
|
+
"description": "Key label text, extracted from innerHTML (e.g. \"Ctrl+K\"). The main payload field for Kbd components extracted from HTML — mirrors Text/Code's textContent prop.",
|
|
31
|
+
"$ref": "common_types.json#/$defs/DynamicString"
|
|
28
32
|
}
|
|
29
33
|
},
|
|
30
34
|
"required": [
|
package/components/kbd/kbd.d.ts
CHANGED
|
@@ -15,4 +15,6 @@ import { UIElement } from '../../core/element.js';
|
|
|
15
15
|
export class UIKbd extends UIElement {
|
|
16
16
|
/** Sizing scale: sm, md (default), lg. */
|
|
17
17
|
size: 'sm' | 'md' | 'lg';
|
|
18
|
+
/** Key label text, extracted from innerHTML (e.g. "Ctrl+K"). The main payload field for Kbd components extracted from HTML — mirrors Text/Code's textContent prop. */
|
|
19
|
+
textContent: string;
|
|
18
20
|
}
|
package/components/kbd/kbd.yaml
CHANGED
|
@@ -19,6 +19,11 @@ props:
|
|
|
19
19
|
- md
|
|
20
20
|
- lg
|
|
21
21
|
reflect: true
|
|
22
|
+
textContent:
|
|
23
|
+
description: Key label text, extracted from innerHTML (e.g. "Ctrl+K"). The main payload field for Kbd components extracted from HTML — mirrors Text/Code's textContent prop.
|
|
24
|
+
type: string
|
|
25
|
+
default: ''
|
|
26
|
+
dynamic: true
|
|
22
27
|
events: {}
|
|
23
28
|
slots:
|
|
24
29
|
default:
|
|
@@ -121,6 +121,13 @@ export class UINav extends UIElement {
|
|
|
121
121
|
if (group && this.contains(group)) {
|
|
122
122
|
const isCollapsed = this.collapsed || this.getBoundingClientRect().width <= 96;
|
|
123
123
|
if (isCollapsed) {
|
|
124
|
+
// The flyout popover is appended INSIDE the group, so an option
|
|
125
|
+
// click bubbles here after the option's own handler has selected
|
|
126
|
+
// and hidePopover()'d — re-showing would instantly reopen the
|
|
127
|
+
// flyout the user just dismissed (TKT-0005, 2026-07-12). The
|
|
128
|
+
// expanded branch below guards on the header; this is the
|
|
129
|
+
// collapsed-branch equivalent.
|
|
130
|
+
if (e.target.closest('[popover]')) return;
|
|
124
131
|
group.showPopover?.();
|
|
125
132
|
} else {
|
|
126
133
|
const header = group.querySelector(':scope > [slot="header"]');
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
"component": {
|
|
17
17
|
"const": "RichText"
|
|
18
18
|
},
|
|
19
|
+
"flush": {
|
|
20
|
+
"description": "Paragraph-grade embedding mode — removes the article chrome (body padding and the 45rem reading measure, plus the trailing paragraph margin) so the rendered markdown sits flush in a host column like a plain text block. Use when richtext carries a single paragraph or fragment inside an already-measured layout (doc pages, cards); leave off for standalone articles and chat transcripts.",
|
|
21
|
+
"type": "boolean",
|
|
22
|
+
"default": false
|
|
23
|
+
},
|
|
19
24
|
"markdown": {
|
|
20
25
|
"description": "Component property: markdown.",
|
|
21
26
|
"type": "string",
|
|
@@ -98,6 +98,26 @@
|
|
|
98
98
|
padding: var(--richtext-py) var(--richtext-px);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/* [flush] — paragraph-grade embedding (yaml `flush` prop): drop the
|
|
102
|
+
article chrome (padding, 45rem measure, own canvas background) so the
|
|
103
|
+
markdown sits in a host column like a plain text block. The trailing
|
|
104
|
+
child keeps no bottom margin — rhythm belongs to the host layout's
|
|
105
|
+
gap, matching text-ui paragraphs in the doc column (TKT-0004: every
|
|
106
|
+
code-bearing doc paragraph rides richtext). */
|
|
107
|
+
:scope[flush] {
|
|
108
|
+
background: transparent;
|
|
109
|
+
overflow-y: visible;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
:scope[flush] [data-richtext-body] {
|
|
113
|
+
max-width: none;
|
|
114
|
+
padding: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
:scope[flush] [data-richtext-body] > :last-child {
|
|
118
|
+
margin-bottom: 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
101
121
|
/* ── Headings ── */
|
|
102
122
|
|
|
103
123
|
[data-richtext-body] h1 {
|
|
@@ -21,6 +21,8 @@ editable>.
|
|
|
21
21
|
import { UIElement } from '../../core/element.js';
|
|
22
22
|
|
|
23
23
|
export class UIRichText extends UIElement {
|
|
24
|
+
/** Paragraph-grade embedding mode — removes the article chrome (body padding and the 45rem reading measure, plus the trailing paragraph margin) so the rendered markdown sits flush in a host column like a plain text block. Use when richtext carries a single paragraph or fragment inside an already-measured layout (doc pages, cards); leave off for standalone articles and chat transcripts. */
|
|
25
|
+
flush: boolean;
|
|
24
26
|
/** Component property: markdown. */
|
|
25
27
|
markdown: string;
|
|
26
28
|
/** Source URL (image, iframe, etc.). */
|
|
@@ -23,6 +23,16 @@ props:
|
|
|
23
23
|
description: Source URL (image, iframe, etc.).
|
|
24
24
|
type: string
|
|
25
25
|
default: ''
|
|
26
|
+
flush:
|
|
27
|
+
description: >-
|
|
28
|
+
Paragraph-grade embedding mode — removes the article chrome (body
|
|
29
|
+
padding and the 45rem reading measure, plus the trailing paragraph
|
|
30
|
+
margin) so the rendered markdown sits flush in a host column like a
|
|
31
|
+
plain text block. Use when richtext carries a single paragraph or
|
|
32
|
+
fragment inside an already-measured layout (doc pages, cards); leave
|
|
33
|
+
off for standalone articles and chat transcripts.
|
|
34
|
+
type: boolean
|
|
35
|
+
default: false
|
|
26
36
|
events: {}
|
|
27
37
|
slots:
|
|
28
38
|
default:
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// cell-types.js - Built-in cell type registry for table-ui
|
|
2
2
|
|
|
3
|
+
import { inline } from '../../core/markdown.js';
|
|
4
|
+
|
|
3
5
|
const cellTypes = {};
|
|
4
6
|
|
|
5
7
|
function registerCellType(name, config) {
|
|
@@ -233,7 +235,34 @@ registerCellType('link', {
|
|
|
233
235
|
sortFn: 'alphanumeric',
|
|
234
236
|
});
|
|
235
237
|
|
|
236
|
-
// 11.
|
|
238
|
+
// 11. markdown — TKT-0008: cells with inline `code`/**bold**/*italic*/
|
|
239
|
+
// [link](href) content the docs-transpiler couldn't express as a plain
|
|
240
|
+
// string (table.yaml {columns,data} contract carries plain values; this
|
|
241
|
+
// is the one escape hatch for INLINE-safe rich text, reusing the same
|
|
242
|
+
// inline() converter core/markdown.js already round-trips against
|
|
243
|
+
// scripts/build/lib/docs-transpiler.mjs's convertInline() — the two are
|
|
244
|
+
// paired encode/decode halves of one contract, not independent parsers).
|
|
245
|
+
// Block-level content (nested tables, component tags, colspan/rowspan)
|
|
246
|
+
// stays out of scope; those still fall back to the preserved-HTML Code
|
|
247
|
+
// panel synthesizeTable already emits.
|
|
248
|
+
registerCellType('markdown', {
|
|
249
|
+
render(value, row, cell) {
|
|
250
|
+
let s = cell.firstElementChild;
|
|
251
|
+
if (!s || s.tagName !== 'SPAN') {
|
|
252
|
+
cell.replaceChildren();
|
|
253
|
+
s = cell.appendChild(document.createElement('span'));
|
|
254
|
+
}
|
|
255
|
+
s.innerHTML = inline(String(value ?? ''));
|
|
256
|
+
cell.title = s.textContent;
|
|
257
|
+
},
|
|
258
|
+
format(value) {
|
|
259
|
+
return String(value ?? '');
|
|
260
|
+
},
|
|
261
|
+
align: 'left',
|
|
262
|
+
sortFn: 'alphanumeric',
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// 12. progress
|
|
237
266
|
registerCellType('progress', {
|
|
238
267
|
render(value, row, cell) {
|
|
239
268
|
let wrapper = cell.querySelector('row-ui');
|
|
@@ -262,7 +291,7 @@ registerCellType('progress', {
|
|
|
262
291
|
sortFn: 'numeric',
|
|
263
292
|
});
|
|
264
293
|
|
|
265
|
-
//
|
|
294
|
+
// 13. actions
|
|
266
295
|
registerCellType('actions', {
|
|
267
296
|
render(value, row, cell, meta) {
|
|
268
297
|
const actions = meta?.actions || [];
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"data": {
|
|
24
24
|
"description": "Row records. Array of plain objects keyed to columns[].key.",
|
|
25
|
-
"$ref": "common_types.json#/$defs/
|
|
25
|
+
"$ref": "common_types.json#/$defs/DynamicObjectList"
|
|
26
26
|
},
|
|
27
27
|
"density": {
|
|
28
28
|
"description": "Controls cell padding and row height. 'compact' for dense data, 'standard' for default spacing, 'comfortable' for spacious rows.",
|
|
@@ -389,3 +389,40 @@ describe('table-ui — F6 per-row expand suppression (rowExpandable)', () => {
|
|
|
389
389
|
expect(el.expanded).toContain(1);
|
|
390
390
|
});
|
|
391
391
|
});
|
|
392
|
+
|
|
393
|
+
describe('table-ui — "markdown" cell type (TKT-0008)', () => {
|
|
394
|
+
it('renders inline `code`/**bold**/*italic*/[link](href) as real elements, not literal syntax', async () => {
|
|
395
|
+
const el = mount('<table-ui></table-ui>');
|
|
396
|
+
el.columns = [{ key: 'desc', label: 'Description', type: 'markdown' }];
|
|
397
|
+
el.data = [{ desc: 'Semantic status: `new` | `updated` | **beta** or *deprecated*, see [docs](/site/components/badge).' }];
|
|
398
|
+
await raf();
|
|
399
|
+
|
|
400
|
+
const cell = el.querySelector('[role="gridcell"][data-key="desc"]');
|
|
401
|
+
expect([...cell.querySelectorAll('code')].map((c) => c.textContent)).toEqual(['new', 'updated']);
|
|
402
|
+
expect(cell.querySelector('strong').textContent).toBe('beta');
|
|
403
|
+
expect(cell.querySelector('em').textContent).toBe('deprecated');
|
|
404
|
+
expect(cell.querySelector('a').getAttribute('href')).toBe('/site/components/badge');
|
|
405
|
+
expect(cell.textContent).not.toMatch(/`|\*\*|\[docs\]/); // no literal markdown syntax leaked through
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('sets [title] to the plain-text (tag-stripped) version, for the truncation tooltip', async () => {
|
|
409
|
+
const el = mount('<table-ui></table-ui>');
|
|
410
|
+
el.columns = [{ key: 'prop', label: 'Prop', type: 'markdown' }];
|
|
411
|
+
el.data = [{ prop: '`variant`' }];
|
|
412
|
+
await raf();
|
|
413
|
+
|
|
414
|
+
const cell = el.querySelector('[role="gridcell"][data-key="prop"]');
|
|
415
|
+
expect(cell.title).toBe('variant');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('a plain value with no markdown syntax renders as ordinary text (no stray elements)', async () => {
|
|
419
|
+
const el = mount('<table-ui></table-ui>');
|
|
420
|
+
el.columns = [{ key: 'desc', label: 'Description', type: 'markdown' }];
|
|
421
|
+
el.data = [{ desc: 'Plain sentence, no special characters.' }];
|
|
422
|
+
await raf();
|
|
423
|
+
|
|
424
|
+
const cell = el.querySelector('[role="gridcell"][data-key="desc"]');
|
|
425
|
+
expect(cell.querySelector('code, strong, em, a')).toBeNull();
|
|
426
|
+
expect(cell.textContent).toBe('Plain sentence, no special characters.');
|
|
427
|
+
});
|
|
428
|
+
});
|
|
@@ -27,6 +27,8 @@ props:
|
|
|
27
27
|
data:
|
|
28
28
|
description: Row records. Array of plain objects keyed to columns[].key.
|
|
29
29
|
type: array
|
|
30
|
+
items:
|
|
31
|
+
type: object
|
|
30
32
|
default: []
|
|
31
33
|
dynamic: true # JS-set collection prop with a custom setter — deliberately not in static properties
|
|
32
34
|
density:
|
package/core/markdown.js
CHANGED
|
@@ -86,16 +86,41 @@ export function renderMarkdown(src) {
|
|
|
86
86
|
return out.join('\n');
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// Placeholder marker for protected code-span content — \0 can't occur in
|
|
90
|
+
// authored markdown, so it round-trips unambiguously through the regex pass.
|
|
91
|
+
const CODE_PH = (n) => `\0CODE${n}\0`;
|
|
92
|
+
const CODE_PH_RE = /\0CODE(\d+)\0/g;
|
|
93
|
+
|
|
94
|
+
export function inline(text) {
|
|
95
|
+
const escaped = esc(text);
|
|
96
|
+
|
|
97
|
+
// 1. Extract inline-code spans FIRST and protect their (already-escaped,
|
|
98
|
+
// HTML-safe) raw content behind a placeholder — otherwise a code span
|
|
99
|
+
// containing `*`/`_` (e.g. `--a-chrome-*`, `foo_bar`) gets its content
|
|
100
|
+
// mangled by the emphasis regexes below before the code regex ever
|
|
101
|
+
// sees it (root cause of the inline-flow gap — see
|
|
102
|
+
// site-a2ui/FINDINGS.md "No inline-flow construct").
|
|
103
|
+
const codeSpans = [];
|
|
104
|
+
let out = escaped.replace(/`([^`]+)`/g, (_, code) => {
|
|
105
|
+
const i = codeSpans.length;
|
|
106
|
+
codeSpans.push(code);
|
|
107
|
+
return CODE_PH(i);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// 2. Now it's safe to apply emphasis + links to the remaining (non-code)
|
|
111
|
+
// text — code-span content is inert placeholder text at this point.
|
|
112
|
+
out = out
|
|
91
113
|
// Bold **text** or __text__
|
|
92
114
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
93
115
|
.replace(/__(.+?)__/g, '<strong>$1</strong>')
|
|
94
116
|
// Italic *text* or _text_
|
|
95
117
|
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
96
118
|
.replace(/_(.+?)_/g, '<em>$1</em>')
|
|
97
|
-
// Inline code `code`
|
|
98
|
-
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
99
119
|
// Links [text](url)
|
|
100
120
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
121
|
+
|
|
122
|
+
// 3. Restore the protected code spans verbatim.
|
|
123
|
+
out = out.replace(CODE_PH_RE, (_, i) => `<code>${codeSpans[Number(i)]}</code>`);
|
|
124
|
+
|
|
125
|
+
return out;
|
|
101
126
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { renderMarkdown } from './markdown.js';
|
|
3
|
+
|
|
4
|
+
// ── inline() protect-verbatim-regions regression (site-a2ui/FINDINGS.md
|
|
5
|
+
// "No inline-flow construct" — root cause: emphasis regexes ran BEFORE
|
|
6
|
+
// code-span extraction, so any code span containing `*`/`_` mangled) ──
|
|
7
|
+
|
|
8
|
+
describe('renderMarkdown — inline code spans survive markdown-unsafe content', () => {
|
|
9
|
+
it('code span containing an asterisk is not treated as emphasis', () => {
|
|
10
|
+
const html = renderMarkdown('Use `--a-chrome-*` for chrome tokens.');
|
|
11
|
+
expect(html).toContain('<code>--a-chrome-*</code>');
|
|
12
|
+
expect(html).not.toContain('<em>');
|
|
13
|
+
expect(html).not.toContain('<strong>');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('code span containing underscores is not treated as emphasis', () => {
|
|
17
|
+
const html = renderMarkdown('Call `foo_bar_baz()` to run it.');
|
|
18
|
+
expect(html).toContain('<code>foo_bar_baz()</code>');
|
|
19
|
+
expect(html).not.toContain('<em>');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('code span containing double-asterisks is not treated as bold', () => {
|
|
23
|
+
const html = renderMarkdown('The pattern `**not-bold**` is literal.');
|
|
24
|
+
expect(html).toContain('<code>**not-bold**</code>');
|
|
25
|
+
expect(html).not.toContain('<strong>');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('bold, italic, code, and link combine correctly within one run', () => {
|
|
29
|
+
const html = renderMarkdown('This is **bold**, this is *italic*, this is `code_with_*` and this is [a link](https://example.com).');
|
|
30
|
+
expect(html).toContain('<strong>bold</strong>');
|
|
31
|
+
expect(html).toContain('<em>italic</em>');
|
|
32
|
+
expect(html).toContain('<code>code_with_*</code>');
|
|
33
|
+
expect(html).toContain('<a href="https://example.com">a link</a>');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('adjacent code spans do not bleed into each other', () => {
|
|
37
|
+
const html = renderMarkdown('First `a_b` then `c*d` then plain _italic_ text.');
|
|
38
|
+
expect(html).toContain('<code>a_b</code>');
|
|
39
|
+
expect(html).toContain('<code>c*d</code>');
|
|
40
|
+
expect(html).toContain('<em>italic</em>');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('HTML-unsafe characters inside a code span are still escaped', () => {
|
|
44
|
+
const html = renderMarkdown('Use `<script>` tags carefully.');
|
|
45
|
+
expect(html).toContain('<code><script></code>');
|
|
46
|
+
});
|
|
47
|
+
});
|
package/core/provider.js
CHANGED
|
@@ -165,6 +165,19 @@ export class UIRouter extends UIProvider {
|
|
|
165
165
|
#cache = new Map();
|
|
166
166
|
#ownCtrl = null;
|
|
167
167
|
#lastPath = null;
|
|
168
|
+
// Monotonic navigation token — claimed at #loadContent ENTRY, before any
|
|
169
|
+
// await, so completion order can never impersonate navigation order. A
|
|
170
|
+
// call that awaits (content fetch, templateResolver) and finds a newer
|
|
171
|
+
// token on resume is superseded: it must neither write innerHTML nor
|
|
172
|
+
// emit route-loaded. Incident (2026-07-12, prod deploy verify run
|
|
173
|
+
// 29199534931): the boot route's SLOW content fetch resumed AFTER a
|
|
174
|
+
// fast follow-up navigation had fully rendered — downstream guards that
|
|
175
|
+
// sequence at templateResolver entry saw the late resumer as "newest"
|
|
176
|
+
// and let it clobber the newer route's content (data-route-path said
|
|
177
|
+
// the new route; the DOM showed the old one). Same class as the
|
|
178
|
+
// "no-artifact despite a valid artifact" and batch-sweep flakes in
|
|
179
|
+
// site-a2ui/FINDINGS.md — the cancellation belongs HERE, at the source.
|
|
180
|
+
#navSeq = 0;
|
|
168
181
|
|
|
169
182
|
get #ctrl() { return this.#ownCtrl || this.controller; }
|
|
170
183
|
|
|
@@ -214,6 +227,7 @@ export class UIRouter extends UIProvider {
|
|
|
214
227
|
}
|
|
215
228
|
|
|
216
229
|
async #loadContent(route, ctrl) {
|
|
230
|
+
const nav = ++this.#navSeq; // claim BEFORE any await — see #navSeq
|
|
217
231
|
let html;
|
|
218
232
|
if (this.#cache.has(route.content)) {
|
|
219
233
|
html = this.#cache.get(route.content);
|
|
@@ -228,10 +242,12 @@ export class UIRouter extends UIProvider {
|
|
|
228
242
|
return;
|
|
229
243
|
}
|
|
230
244
|
}
|
|
245
|
+
if (nav !== this.#navSeq) return; // superseded while fetching content
|
|
231
246
|
|
|
232
247
|
// Template resolver hook — lets the site wrap content in a template before injection
|
|
233
248
|
if (this.templateResolver) {
|
|
234
249
|
html = await this.templateResolver(html, route);
|
|
250
|
+
if (nav !== this.#navSeq) return; // superseded while resolving
|
|
235
251
|
}
|
|
236
252
|
|
|
237
253
|
this.innerHTML = html;
|
|
@@ -267,6 +283,7 @@ export class UIRouter extends UIProvider {
|
|
|
267
283
|
if (mod.default) mod.default(this);
|
|
268
284
|
} catch { /* setup module is optional */ }
|
|
269
285
|
}
|
|
286
|
+
if (nav !== this.#navSeq) return; // superseded during setup import — no phantom route-loaded
|
|
270
287
|
|
|
271
288
|
const state = ctrl.getState();
|
|
272
289
|
this.emit('route-loaded', {
|