@aceshooting/lyra-ui 3.8.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +3 -2
  2. package/custom-elements.json +1264 -97
  3. package/dist/components/activity-feed/activity-feed.class.d.ts +13 -1
  4. package/dist/components/activity-feed/activity-feed.class.d.ts.map +1 -1
  5. package/dist/components/activity-feed/activity-feed.class.js +10 -2
  6. package/dist/components/activity-feed/activity-feed.class.js.map +1 -1
  7. package/dist/components/graph/graph-canvas.d.ts +3 -0
  8. package/dist/components/graph/graph-canvas.d.ts.map +1 -1
  9. package/dist/components/graph/graph-canvas.js +5 -0
  10. package/dist/components/graph/graph-canvas.js.map +1 -1
  11. package/dist/components/graph/graph.class.d.ts +14 -0
  12. package/dist/components/graph/graph.class.d.ts.map +1 -1
  13. package/dist/components/graph/graph.class.js +28 -0
  14. package/dist/components/graph/graph.class.js.map +1 -1
  15. package/dist/components/graph/graph.styles.d.ts.map +1 -1
  16. package/dist/components/graph/graph.styles.js +6 -0
  17. package/dist/components/graph/graph.styles.js.map +1 -1
  18. package/dist/components/input/input.class.d.ts +3 -1
  19. package/dist/components/input/input.class.d.ts.map +1 -1
  20. package/dist/components/input/input.class.js +2 -0
  21. package/dist/components/input/input.class.js.map +1 -1
  22. package/dist/components/markdown/markdown-core.class.d.ts +334 -0
  23. package/dist/components/markdown/markdown-core.class.d.ts.map +1 -0
  24. package/dist/components/markdown/markdown-core.class.js +999 -0
  25. package/dist/components/markdown/markdown-core.class.js.map +1 -0
  26. package/dist/components/markdown/markdown-core.d.ts +2 -0
  27. package/dist/components/markdown/markdown-core.d.ts.map +1 -0
  28. package/dist/components/markdown/markdown-core.js +5 -0
  29. package/dist/components/markdown/markdown-core.js.map +1 -0
  30. package/dist/components/thread-list/thread-list.class.d.ts +10 -0
  31. package/dist/components/thread-list/thread-list.class.d.ts.map +1 -1
  32. package/dist/components/thread-list/thread-list.class.js +9 -1
  33. package/dist/components/thread-list/thread-list.class.js.map +1 -1
  34. package/dist/components/usage-badge/usage-badge.class.d.ts +10 -1
  35. package/dist/components/usage-badge/usage-badge.class.d.ts.map +1 -1
  36. package/dist/components/usage-badge/usage-badge.class.js +9 -0
  37. package/dist/components/usage-badge/usage-badge.class.js.map +1 -1
  38. package/dist/internal/root-registration-allowlist.d.ts +1 -1
  39. package/dist/internal/root-registration-allowlist.d.ts.map +1 -1
  40. package/dist/internal/root-registration-allowlist.js +1 -0
  41. package/dist/internal/root-registration-allowlist.js.map +1 -1
  42. package/dist/lyra.d.ts +3 -0
  43. package/dist/lyra.d.ts.map +1 -1
  44. package/dist/lyra.js +2 -0
  45. package/dist/lyra.js.map +1 -1
  46. package/llms-full.txt +83 -9
  47. package/llms.txt +2 -1
  48. package/package.json +31 -31
  49. package/vscode-css-data.json +7 -0
  50. package/vscode-html-data.json +106 -6
  51. package/web-types.json +129 -7
package/web-types.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/web-types",
3
3
  "name": "@aceshooting/lyra-ui",
4
- "version": "3.8.0",
4
+ "version": "3.9.0",
5
5
  "description-markup": "markdown",
6
6
  "contributions": {
7
7
  "html": {
@@ -74,7 +74,7 @@
74
74
  },
75
75
  {
76
76
  "name": "lyra-activity-feed",
77
- "description": "`<lyra-activity-feed>` — an append-only streaming log of granular agent actions (\"Searching the\nweb…\", \"Read src/index.ts\"), collapsing to a localized \"Completed N steps\" summary once the run\nis over. Entries never change state once added (a step whose status mutates in place belongs to\n`<lyra-task-list>` instead). Implements the shared follow (stick-to-bottom) contract: `follow`\nis a component-managed, host-assignable property, released on user scroll-up and re-engaged at\nthe bottom, firing `lyra-follow-change` on every transition (mount excluded). At/above\n`virtualizeThreshold` entries, the body renders through an internal `<lyra-virtual-list>`\ninstead of a plain keyed list — same list semantics either way, keyed by `id`.\n\n---\n\n**CSS Shadow Parts**\n\n| Name | Description |\n| --- | --- |\n| `base` | The outer container. |\n| `body` | The scrollable region containing the entries (or the internal virtual-list). |\n| `entry` | One entry row; carries `data-tone`. |\n| `entry-icon` | The literal `icon` hint, or a tone dot when unset. |\n| `entry-text` | The entry's `text`. |\n| `entry-timestamp` | The formatted timestamp, only rendered while `showTimestamps` and a valid `timestamp` is set. |\n| `header` | The clickable header (`<button>`). |\n| `status-dot` | The decorative mode indicator dot; pulses while `mode=\"live\"`. |\n| `summary` | The header's one-line ticker (`live`) or completed-count summary (`post-hoc`). |\n| `toggle` | The chevron indicator inside the header. |\n\n---\n\n**CSS Custom Properties**\n\n- `--lyra-activity-feed-max-height` (default: `16rem`) — Cap on how tall the expanded body grows before it scrolls internally (non-virtualized mode); also sizes the internal virtual-list.",
77
+ "description": "`<lyra-activity-feed>` — an append-only streaming log of granular agent actions (\"Searching the\nweb…\", \"Read src/index.ts\"), collapsing to a localized \"Completed N steps\" summary once the run\nis over. Entries never change state once added (a step whose status mutates in place belongs to\n`<lyra-task-list>` instead). Implements the shared follow (stick-to-bottom) contract: `follow`\nis a component-managed, host-assignable property, released on user scroll-up and re-engaged at\nthe bottom, firing `lyra-follow-change` on every transition (mount excluded). At/above\n`virtualizeThreshold` entries, the body renders through an internal `<lyra-virtual-list>`\ninstead of a plain keyed list — same list semantics either way, keyed by `id`.\n\nEach entry's `text` renders as plain text by default; a host needing richer per-entry content\n(rendered markdown, a trailing tool-call chip list, etc.) sets `renderText` to fully replace it,\nidentically whether or not the feed is currently virtualized.\n\n---\n\n**CSS Shadow Parts**\n\n| Name | Description |\n| --- | --- |\n| `base` | The outer container. |\n| `body` | The scrollable region containing the entries (or the internal virtual-list). |\n| `entry` | One entry row; carries `data-tone`. |\n| `entry-icon` | The literal `icon` hint, or a tone dot when unset. |\n| `entry-text` | The entry's `text`. Not rendered while `renderText` is set — its returned content replaces this part entirely. |\n| `entry-timestamp` | The formatted timestamp, only rendered while `showTimestamps` and a valid `timestamp` is set. |\n| `header` | The clickable header (`<button>`). |\n| `status-dot` | The decorative mode indicator dot; pulses while `mode=\"live\"`. |\n| `summary` | The header's one-line ticker (`live`) or completed-count summary (`post-hoc`). |\n| `toggle` | The chevron indicator inside the header. |\n\n---\n\n**CSS Custom Properties**\n\n- `--lyra-activity-feed-max-height` (default: `16rem`) — Cap on how tall the expanded body grows before it scrolls internally (non-virtualized mode); also sizes the internal virtual-list.",
78
78
  "attributes": [
79
79
  {
80
80
  "name": "expanded",
@@ -6390,7 +6390,7 @@
6390
6390
  },
6391
6391
  {
6392
6392
  "name": "lyra-graph",
6393
- "description": "`<lyra-graph>` — a force-directed node-link diagram with pan/zoom/drag.\nRequires the optional peer deps `d3-force`/`d3-drag`/`d3-zoom`/`d3-selection`\n(lazy-loaded; a consumer who never uses this component pays zero d3 cost).\n\nSet `seed` for a deterministic layout: node initial positions become\nreproducible (keyed by node id) and the settle happens synchronously\ninstead of animating, like `prefers-reduced-motion`. `seed` only takes\neffect on the update that first populates `nodes`/`links` (or a later\nupdate that adds genuinely new node ids) — willUpdate() only reads it from\ninside rebuildSimulation(), which itself only ever assigns x/y to nodes\nthat don't already have a settled position, so changing `seed` on an\nalready-rendered graph is a no-op; nothing re-derives already-positioned\nnodes' x/y from the new value.\n\n`hiddenTypes` filters nodes/links by `GraphNode.type` without discarding position state --\n`lastPositionById` remembers every node's last settled x/y across a hide/show round-trip, so\ntoggling a type off and back on restores each node where it was instead of re-randomizing it.\n\n`communities` draws one translucent convex-hull blob per entry, behind links/nodes -- a hull's\nmembership is the union of its own `memberIds` and every node whose `communityId` matches its\n`id`. A community with no currently-visible members (all its nodes hidden by `hiddenTypes`, or\nsimply empty) renders no hull.\n\n`layout=\"layered\"` swaps the d3-force simulation for a deterministic layered layout (see\n`src/internal/layered-layout.ts`) -- node drag is disabled in that mode, and `chargeStrength` is\na documented no-op.\n\n`renderer=\"canvas\"` swaps the per-node/per-link SVG DOM for a single DPR-aware `<canvas>` --\nevery event/method/property behaves identically to `renderer=\"svg\"` (the default), with hit-\ntesting resolved via an offscreen color-picking canvas instead of DOM event targets. The\ndocumented trade-offs: no `::part(node)`/`::part(link)` styling (pixels, not elements -- theme\nvia cssprops instead), no native SVG `<title>` tooltip (replaced by `part=\"tooltip\"`), and a\ndrawn focus ring instead of a CSS one. Keyboard roving/announcements are preserved through an\noffscreen `part=\"cursor-item\"` button per node/link/hull, driving the identical roving-tabindex\nlogic as `renderer=\"svg\"`.\n\n---\n\n**CSS Shadow Parts**\n\n| Name | Description |\n| --- | --- |\n| `arrowhead` | The marker used by directed graph links. |\n| `base` | The graph wrapper. |\n| `canvas` | The single canvas surface (`renderer=\"canvas\"` only). |\n| `community-label` | A hull's label text. |\n| `cursor-item` | An offscreen keyboard-roving item (`renderer=\"canvas\"`'s a11y virtual cursor). |\n| `cursor-items` | The container of offscreen keyboard-roving items (`renderer=\"canvas\"` only). |\n| `data-list` | A visually hidden list alternative for graph data. |\n| `empty` | The empty-state message, shown when `nodes` is empty. |\n| `expand-indicator` | The \"+\" badge rendered on a node with `expandable: true`. |\n| `focus-halo` | The persistent ring tracking `focusId`'s node. |\n| `hull` | A community hull (behind links/nodes; role=\"button\"). |\n| `label` | A node label. |\n| `link` | A graph link. |\n| `link-label` | A drawn edge label (only rendered when `showEdgeLabels` is set). |\n| `live-region` | The current graph item announcement. |\n| `node` | A graph node. |\n| `svg` | The graph SVG. |\n| `tooltip` | The hover tooltip (`renderer=\"canvas\"` only; the SVG `<title>` replacement). |\n\n---\n\n**CSS Custom Properties**\n\n- `--lyra-node-fill` (default: `var(--lyra-color-brand)`) — Default node fill, overridden per-node by `GraphNode.color`.\n- `--lyra-link-color` (default: `var(--lyra-color-border)`) — Default link stroke, overridden per-link by a link's own `color`.\n- `--lyra-graph-cat-1..8` — Ordered categorical fallback palette for a typed node with no `GraphNodeType.color`, assigned by the type's index in `nodeTypes` (wraps every 8 entries). Declared centrally in `tokens.styles.ts` so `<lyra-graph>` and any future `<lyra-graph-legend>`- style component resolve the identical default.\n- `--lyra-graph-edge-label-halo` (default: `var(--lyra-color-surface)`) — Legibility halo (`stroke`) behind a drawn edge label, painted under the fill via `paint-order: stroke`.\n- `--lyra-graph-focus-halo-color` (default: `var(--lyra-color-brand)`) — `focus-halo` stroke color.\n- `--lyra-graph-selected-color` (default: `var(--lyra-color-success)`) — Selected node/link stroke.\n- `--lyra-graph-hull-fill` (default: `var(--lyra-color-brand)`) — Hull fill/stroke color.\n- `--lyra-graph-hull-opacity` (default: `0.12`) — Hull element opacity (composites fill+stroke as one group, avoiding a double-opacity seam at the fill/stroke boundary).",
6393
+ "description": "`<lyra-graph>` — a force-directed node-link diagram with pan/zoom/drag.\nRequires the optional peer deps `d3-force`/`d3-drag`/`d3-zoom`/`d3-selection`\n(lazy-loaded; a consumer who never uses this component pays zero d3 cost).\n\nSet `seed` for a deterministic layout: node initial positions become\nreproducible (keyed by node id) and the settle happens synchronously\ninstead of animating, like `prefers-reduced-motion`. `seed` only takes\neffect on the update that first populates `nodes`/`links` (or a later\nupdate that adds genuinely new node ids) — willUpdate() only reads it from\ninside rebuildSimulation(), which itself only ever assigns x/y to nodes\nthat don't already have a settled position, so changing `seed` on an\nalready-rendered graph is a no-op; nothing re-derives already-positioned\nnodes' x/y from the new value.\n\n`hiddenTypes` filters nodes/links by `GraphNode.type` without discarding position state --\n`lastPositionById` remembers every node's last settled x/y across a hide/show round-trip, so\ntoggling a type off and back on restores each node where it was instead of re-randomizing it.\n\n`communities` draws one translucent convex-hull blob per entry, behind links/nodes -- a hull's\nmembership is the union of its own `memberIds` and every node whose `communityId` matches its\n`id`. A community with no currently-visible members (all its nodes hidden by `hiddenTypes`, or\nsimply empty) renders no hull.\n\n`layout=\"layered\"` swaps the d3-force simulation for a deterministic layered layout (see\n`src/internal/layered-layout.ts`) -- node drag is disabled in that mode, and `chargeStrength` is\na documented no-op.\n\n`renderer=\"canvas\"` swaps the per-node/per-link SVG DOM for a single DPR-aware `<canvas>` --\nevery event/method/property behaves identically to `renderer=\"svg\"` (the default), with hit-\ntesting resolved via an offscreen color-picking canvas instead of DOM event targets. The\ndocumented trade-offs: no `::part(node)`/`::part(link)` styling (pixels, not elements -- theme\nvia cssprops instead), no native SVG `<title>` tooltip (replaced by `part=\"tooltip\"`), and a\ndrawn focus ring instead of a CSS one. Keyboard roving/announcements are preserved through an\noffscreen `part=\"cursor-item\"` button per node/link/hull, driving the identical roving-tabindex\nlogic as `renderer=\"svg\"`.\n\n---\n\n**CSS Shadow Parts**\n\n| Name | Description |\n| --- | --- |\n| `arrowhead` | The marker used by directed graph links. |\n| `base` | The graph wrapper. |\n| `canvas` | The single canvas surface (`renderer=\"canvas\"` only). |\n| `community-label` | A hull's label text. |\n| `cursor-item` | An offscreen keyboard-roving item (`renderer=\"canvas\"`'s a11y virtual cursor). |\n| `cursor-items` | The container of offscreen keyboard-roving items (`renderer=\"canvas\"` only). |\n| `data-list` | A visually hidden list alternative for graph data. |\n| `empty` | The empty-state message, shown when `nodes` is empty. |\n| `expand-indicator` | The \"+\" badge rendered on a node with `expandable: true`. |\n| `focus-halo` | The persistent ring tracking `focusId`'s node. |\n| `hull` | A community hull (behind links/nodes; role=\"button\"). |\n| `label` | A node label. |\n| `link` | A graph link. |\n| `link-label` | A drawn edge label (only rendered when `showEdgeLabels` is set). |\n| `live-region` | The current graph item announcement. |\n| `node` | A graph node. |\n| `svg` | The graph SVG. |\n| `tooltip` | The hover tooltip (`renderer=\"canvas\"` only; the SVG `<title>` replacement). |\n\n---\n\n**CSS Custom Properties**\n\n- `--lyra-node-fill` (default: `var(--lyra-color-brand)`) — Default node fill, overridden per-node by `GraphNode.color`.\n- `--lyra-link-color` (default: `var(--lyra-color-border)`) — Default link stroke, overridden per-link by a link's own `color`.\n- `--lyra-graph-cat-1..8` — Ordered categorical fallback palette for a typed node with no `GraphNodeType.color`, assigned by the type's index in `nodeTypes` (wraps every 8 entries). Declared centrally in `tokens.styles.ts` so `<lyra-graph>` and any future `<lyra-graph-legend>`- style component resolve the identical default.\n- `--lyra-graph-edge-label-halo` (default: `var(--lyra-color-surface)`) — Legibility halo (`stroke`) behind a drawn edge label, painted under the fill via `paint-order: stroke`.\n- `--lyra-graph-focus-halo-color` (default: `var(--lyra-color-brand)`) — `focus-halo` stroke color.\n- `--lyra-graph-selected-color` (default: `var(--lyra-color-success)`) — Selected node/link stroke.\n- `--lyra-graph-dimmed-opacity` (default: `1`) — Opacity applied to a node/link when `dimmedNodeIds`/`dimmedLinkIds` includes its id (both SVG and canvas renderers). `1` (the default) is a no-op -- an unset host sees no visual change.\n- `--lyra-graph-hull-fill` (default: `var(--lyra-color-brand)`) — Hull fill/stroke color.\n- `--lyra-graph-hull-opacity` (default: `0.12`) — Hull element opacity (composites fill+stroke as one group, avoiding a double-opacity seam at the fill/stroke boundary).",
6394
6394
  "attributes": [
6395
6395
  {
6396
6396
  "name": "aria-label",
@@ -7149,7 +7149,7 @@
7149
7149
  },
7150
7150
  {
7151
7151
  "name": "lyra-input",
7152
- "description": "`<lyra-input>` — a single-line plain-text input primitive, the `lyra-*` equivalent of a plain\n`wa-input`, form-associated via the `FormAssociated` mixin (same shape as `<lyra-textarea>`).\n\nShips the same opt-in `label`/`hint`/`errorText` form-control chrome as `<lyra-textarea>`/\n`<lyra-select>` (props + matching named slots + `form-control`/`form-control-label`/`hint`/`error`\nparts) — left unset, the chrome stays hidden. `size` uses the same `xs`–`xl` scale as\n`<lyra-select>`/`<lyra-combobox>`. `type=\"password\"` always renders a\n`password-toggle` eye-icon button that flips the internal native input between\n`type=\"password\"`/`type=\"text\"` and tracks `passwordVisible`. `type=\"email\"`/`type=\"number\"`\n(with `min`/`max`/`step`) delegate constraint validation to the internal native `<input>`'s own\nbrowser-computed `validity`, bridged into this element's `ElementInternals` by `updateValidity()`.\n\nA host `aria-label` is forwarded to the internal textbox via the typed `accessibleLabel` property;\nexternal `aria-labelledby`/`aria-describedby` idrefs are not copied across the shadow boundary.\n\n---\n\n**Slots**\n\n| Name | Description |\n| --- | --- |\n| `error` | Custom error content. |\n| `hint` | Custom hint content. |\n| `label` | Custom label content. |\n\n---\n\n**CSS Shadow Parts**\n\n| Name | Description |\n| --- | --- |\n| `error` | The error message. |\n| `form-control` | The outer wrapper around label, input, error and hint. |\n| `form-control-label` | The `<label>` element. |\n| `hint` | The hint message. |\n| `input` | The native `<input>` element. |\n| `input-wrapper` | The row wrapping the native input and the password-toggle button. |\n| `password-toggle` | The show/hide-password button, present only when `type=\"password\"`. |",
7152
+ "description": "`<lyra-input>` — a single-line plain-text input primitive, the `lyra-*` equivalent of a plain\n`wa-input`, form-associated via the `FormAssociated` mixin (same shape as `<lyra-textarea>`).\n\nShips the same opt-in `label`/`hint`/`errorText` form-control chrome as `<lyra-textarea>`/\n`<lyra-select>` (props + matching named slots + `form-control`/`form-control-label`/`hint`/`error`\nparts) — left unset, the chrome stays hidden. `size` uses the same `xs`–`xl` scale as\n`<lyra-select>`/`<lyra-combobox>`. `type=\"password\"` always renders a\n`password-toggle` eye-icon button that flips the internal native input between\n`type=\"password\"`/`type=\"text\"` and tracks `passwordVisible`. `type=\"email\"`/`type=\"number\"`\n(with `min`/`max`/`step`) delegate constraint validation to the internal native `<input>`'s own\nbrowser-computed `validity`, bridged into this element's `ElementInternals` by `updateValidity()`.\n`type=\"search\"`/`type=\"time\"` forward straight through to the native input with no additional\nchrome or validation, the same as `type=\"text\"`.\n\nA host `aria-label` is forwarded to the internal textbox via the typed `accessibleLabel` property;\nexternal `aria-labelledby`/`aria-describedby` idrefs are not copied across the shadow boundary.\n\n---\n\n**Slots**\n\n| Name | Description |\n| --- | --- |\n| `error` | Custom error content. |\n| `hint` | Custom hint content. |\n| `label` | Custom label content. |\n\n---\n\n**CSS Shadow Parts**\n\n| Name | Description |\n| --- | --- |\n| `error` | The error message. |\n| `form-control` | The outer wrapper around label, input, error and hint. |\n| `form-control-label` | The `<label>` element. |\n| `hint` | The hint message. |\n| `input` | The native `<input>` element. |\n| `input-wrapper` | The row wrapping the native input and the password-toggle button. |\n| `password-toggle` | The show/hide-password button, present only when `type=\"password\"`. |",
7153
7153
  "attributes": [
7154
7154
  {
7155
7155
  "name": "aria-label",
@@ -8101,6 +8101,124 @@
8101
8101
  }
8102
8102
  ]
8103
8103
  },
8104
+ {
8105
+ "name": "lyra-markdown-core",
8106
+ "description": "`<lyra-markdown-core>` — a build-lean variant of `<lyra-markdown>` for a consumer whose\n`languages` map already covers every language it will ever render. Every other capability (GFM\ntables, fenced code blocks, links, blockquotes, heading anchors, text-quote highlights, math) is\nidentical to `<lyra-markdown>` -- only fenced-code-block highlighting differs: this component's\nown module never textually contains a call to (or import of) `loadShikiHighlighter` (the\n~200-language default dynamic-import table `<lyra-markdown>` calls unconditionally unless its\n*runtime* `languagesOnly` flag is `true` -- a flag on that same module a bundler can't prove\nalways-true, so the unconditional call stays in the build output regardless). A consumer\nimporting this entry point instead of `markdown.js` gets a genuinely shiki-full-table-free\nbuild. A fenced block whose language isn't a key in `languages` always renders the plain-text\nfallback -- there is no default/full-table highlighter here to fall back to, mirroring\n`<lyra-code-block-core>`'s identical contract for the sibling component.\n\nBuilt on the optional peer dependencies `marked` (parsing) and `dompurify` (sanitizing), both\nlazy-loaded via `markdown-loader.ts` on first connect.\n\nRendering never ships unsanitized or broken markup silently:\n- If `marked` fails to load, or throws while parsing malformed input, the\n component falls back to plain text (`white-space: pre-wrap`, no HTML\n parsing at all) and fires `lyra-render-error`.\n- If `sanitize` is `true` (the default) and `dompurify` fails to load, the\n component *also* falls back to plain text + `lyra-render-error` — it\n never renders marked's raw HTML output when sanitization was requested\n (or defaulted to) but is unavailable, even though `marked` itself loaded\n fine.\n- If `sanitize` is explicitly `false`, marked's raw output renders as-is\n regardless of whether `dompurify` is installed — the consumer opted out\n of sanitization, so `dompurify`'s absence is irrelevant to that path.\n\nThat same plain-text fallback rendering (`data-fallback` on the `content`\npart) is also, unconditionally and by default, a brief *transient* state on\nevery connect, not just a failure path: `connectedCallback()`'s dynamic\n`import()` of `marked`/`dompurify` (see `markdown-loader.ts`) is\nasynchronous, so the very first paint of any `<lyra-markdown>` on a page\nshows plain text for at least one microtask — even when both peers are\nalready installed and load without error — until that import resolves and\na second render replaces it with the real Markdown output. Set\n`eager-load` to skip that window once the shared dependency cache is\nalready warm; see that property's doc for exactly what \"warm\" requires.\n\n`heading`/`code`/`blockquote`/`table`/`link`/`image` tokens are rendered\nthrough a `marked` renderer override that injects `part=\"...\"` attributes\ndirectly into the produced HTML — a single pass, not a second DOM walk\nafter insertion.\n\nFenced code blocks are syntax-highlighted via the same fine-grained `shiki/core` recipe\n`<lyra-code-block-core>` uses (`highlightCode`, default `true` — gated by whether a fenced\nblock's language is a key in `languages`, since there is no default highlighter here to gate on\n\"is shiki installed at all\"). The very first render of any content is always plain (identical to\n`<lyra-markdown>`'s own output); highlighting arrives as an asynchronous upgrade one render\nlater, once the fine-grained highlighter resolves. No highlighting is attempted while\n`streaming` is `true` — it applies once a stream settles, so there is no added per-chunk cost\nwhile content is still arriving.\n\nWhen `heading-anchors` is set, every rendered heading's slug (computed via the shared\nGitHub-slugger-style `Slugger`) is stamped as its `id`; `getHeadingTree()` computes that same\noutline on every parse regardless of `heading-anchors`, so a host can build a table of contents\neven while ids aren't in the DOM yet. `scrollToAnchor()` (from the adopted `DocumentAnchorTarget`\nmixin) resolves `fragment` anchors against that outline and `text-quote` anchors via\n`internal/text-quote.ts`'s shared scope/resolve helpers; `highlights` re-resolve by quote after\nevery render (never by node identity), so a highlight painted before a `streaming` update\nfinishes still finds its quote once the matching text arrives. Highlight painting uses\n`internal/text-highlights.ts`'s `acquireHighlightHandle()` -- the CSS Custom Highlight API where\nthe browser supports it (no DOM mutation at all), a `<mark>`-wrap fallback otherwise.\n\n`math` renders `$...$`/`$$...$$` TeX as MathML via the optional `katex` peer's\n`renderToString(tex, { output: 'mathml' })` -- MathML Core renders natively and accessibly in\nevergreen browsers with no extra stylesheet or webfont needing to cross the shadow boundary. A\nmissing `katex` peer renders the literal, unparsed TeX source (delimiters included) and fires one\n`lyra-render-error`.\n\n---\n\n**CSS Shadow Parts**\n\n| Name | Description |\n| --- | --- |\n| `blockquote` | Every rendered `<blockquote>`. |\n| `code-block` | Every rendered fenced/indented `<pre>`. |\n| `content` | The wrapper around the rendered (or plain-text fallback) output. |\n| `heading` | Every rendered `<h1>`–`<h6>` (shifted by `heading-offset`). |\n| `img` | Every rendered `<img>`. |\n| `inline-code` | Every rendered inline `<code>` span (backtick spans, not fenced blocks). |\n| `link` | Every rendered `<a>`. |\n| `list` | Every rendered `<ul>`/`<ol>`. |\n| `math` | A rendered inline or block math span (`data-display=\"inline\"\\|\"block\"`). |\n| `paragraph` | Every rendered `<p>`. |\n| `table` | Every rendered `<table>`. |",
8107
+ "attributes": [
8108
+ {
8109
+ "name": "content",
8110
+ "description": "The Markdown source to render.\n\nType: `string` \nDefault: `''`",
8111
+ "value": {
8112
+ "type": [
8113
+ "string"
8114
+ ]
8115
+ }
8116
+ },
8117
+ {
8118
+ "name": "eager-load",
8119
+ "description": "When `true`, `connectedCallback()` skips awaiting `loadMarkdownDeps()`'s\ndynamic `import()` if the shared `marked`/`dompurify` module cache (see\n`markdown-loader.ts`'s `getMarkdownDepsIfLoaded()`) has *already*\nresolved — e.g. because an earlier `<lyra-markdown>` instance on the\npage already finished loading, or the consumer primed the cache\ndirectly by calling `loadMarkdownDeps()` themselves at startup — and\nrenders synchronously instead. When the cache isn't warm yet (most\nnotably: the very first `<lyra-markdown>` ever connected on a page,\nsince nothing has called `loadMarkdownDeps()` before it), this still\nfalls back to the normal async path — a dynamic `import()` can't be\nmade synchronous, so this is a fast path for the common \"already warm\"\ncase, not a hard guarantee. `false` (the default) is byte-identical to\ntoday: always the async `import()`, fallback-text window included.\n\nType: `boolean` \nDefault: `false`",
8120
+ "value": {
8121
+ "type": [
8122
+ "boolean"
8123
+ ]
8124
+ }
8125
+ },
8126
+ {
8127
+ "name": "escape-html",
8128
+ "description": "When `true`, overrides marked's `html` renderer hook to emit the HTML-escaped source text\ninstead of passing raw/sanitized markup through -- for a consumer rendering arbitrary\nalready-written content (e.g. a historical chat/agent transcript full of code/XML/HTML\nsnippets) where a stray angle bracket should render as visible text, not a real DOM element.\nStill lets GFM tables/lists/etc. render normally -- only raw embedded HTML is affected.\n`false` (the default) reproduces today's exact `marked`-default (sanitized-when-`sanitize`)\npassthrough behavior.\n\nType: `boolean` \nDefault: `false`",
8129
+ "value": {
8130
+ "type": [
8131
+ "boolean"
8132
+ ]
8133
+ }
8134
+ },
8135
+ {
8136
+ "name": "gfm",
8137
+ "description": "Enable GitHub-flavored Markdown (tables, strikethrough, autolinks, task lists).\n\nType: `boolean` \nDefault: `true`",
8138
+ "value": {
8139
+ "type": [
8140
+ "boolean"
8141
+ ]
8142
+ }
8143
+ },
8144
+ {
8145
+ "name": "heading-anchors",
8146
+ "description": "Stamps a computed slug as `id` on every rendered heading. `getHeadingTree()` computes the\nsame slugs regardless of this property -- it only controls whether the `id` attribute is\nemitted into the rendered DOM. `false` (the default) preserves today's exact output.\n\nWhen `sanitize` is also on (the default), a slug whose *value* collides with a real\n`document` property name (e.g. a heading literally titled \"Title\", \"Location\", or \"Forms\"\nslugs to `title`/`location`/`forms`) has its `id` silently stripped by DOMPurify's DOM-\nclobbering protection (`SANITIZE_DOM`) -- `getHeadingTree()` still reports that heading's slug\neither way, but `scrollToAnchor({ kind: 'fragment', id })` still resolves it correctly even\nwithout a DOM `id` present, via its own position-based fallback lookup.\n\nType: `boolean` \nDefault: `false`",
8147
+ "value": {
8148
+ "type": [
8149
+ "boolean"
8150
+ ]
8151
+ }
8152
+ },
8153
+ {
8154
+ "name": "heading-offset",
8155
+ "description": "Added to every rendered heading's source `token.depth` before emitting\n`<h${depth}>` — e.g. `heading-offset=\"2\"` renders a source `#` as\n`<h3>` and a source `##` as `<h4>`. The result is clamped to `[1, 6]`\n(a source `######` with a positive offset stays at `<h6>` rather than\noverflowing past the HTML heading levels; the floor at `1` is\ndefensive, since this property is meant to be additive-only). `0`\n(the default) preserves today's exact `<h${token.depth}>` output.\n\nType: `number` \nDefault: `0`",
8156
+ "value": {
8157
+ "type": [
8158
+ "number"
8159
+ ]
8160
+ }
8161
+ },
8162
+ {
8163
+ "name": "highlight-code",
8164
+ "description": "Syntax-highlights fenced code blocks via the same optional `shiki` peer `<lyra-code-block>`\nuses. `true` (the default) upgrades every fenced block from plain `<pre><code>` once the peer\nis available -- a pure upgrade, not a behavior change gated on opt-in, since it's itself gated\ntransparently by whether `shiki` is installed at all (an app that never installs it sees\nbyte-identical output to today). Set `false` to keep plain output even when `shiki` is\ninstalled. No effect while `streaming` is `true` -- see that property's own doc.\n\nType: `boolean` \nDefault: `true`",
8165
+ "value": {
8166
+ "type": [
8167
+ "boolean"
8168
+ ]
8169
+ }
8170
+ },
8171
+ {
8172
+ "name": "internal-link-prefix",
8173
+ "description": "When set, a rendered link whose `href` starts with this prefix is\ntreated as internal — its click is intercepted and reported via\n`lyra-link-click` instead of navigating. Empty (the default) means\nevery link is treated as external.\n\nType: `string` \nDefault: `''`",
8174
+ "value": {
8175
+ "type": [
8176
+ "string"
8177
+ ]
8178
+ }
8179
+ },
8180
+ {
8181
+ "name": "link-target",
8182
+ "description": "`target` applied to every rendered `<a>`, with `rel=\"noopener\nnoreferrer\"` always added alongside it whenever a `target` is emitted.\n`'_blank'` (the default) preserves today's exact output. Set to `null`\n(or the empty string, e.g. via the `link-target=\"\"` attribute) to omit\n`target`/`rel` entirely, so rendered links open in the same tab.\n\nType: `string | null` \nDefault: `'_blank'`"
8183
+ },
8184
+ {
8185
+ "name": "locale",
8186
+ "description": "Optional locale override. Otherwise the nearest `locale`/`lang` ancestor is used.\n\nType: `string` \nDefault: `''`",
8187
+ "value": {
8188
+ "type": [
8189
+ "string"
8190
+ ]
8191
+ }
8192
+ },
8193
+ {
8194
+ "name": "math",
8195
+ "description": "Renders `$...$`/`$$...$$` TeX via the optional `katex` peer, as MathML. `false` (the\ndefault) renders `$...$` literally, unparsed -- today's exact output.\n\nType: `boolean` \nDefault: `false`",
8196
+ "value": {
8197
+ "type": [
8198
+ "boolean"
8199
+ ]
8200
+ }
8201
+ },
8202
+ {
8203
+ "name": "sanitize",
8204
+ "description": "Sanitize marked's HTML output with DOMPurify before rendering. See the\nclass doc for what happens when this is `true` (the default) but the\n`dompurify` peer isn't installed.\n\nType: `boolean` \nDefault: `true`",
8205
+ "value": {
8206
+ "type": [
8207
+ "boolean"
8208
+ ]
8209
+ }
8210
+ },
8211
+ {
8212
+ "name": "streaming",
8213
+ "description": "Signals that `content` is still arriving incrementally. Content changes\ncontinue to render immediately; while this is `true`, the host remains\n`aria-busy=\"true\"` so assistive technology knows the rendered document\nis not final. Set it back to `false` with the final content update.\nReflects so a consumer can also target `lyra-markdown[streaming]`.\n\nType: `boolean` \nDefault: `false`",
8214
+ "value": {
8215
+ "type": [
8216
+ "boolean"
8217
+ ]
8218
+ }
8219
+ }
8220
+ ]
8221
+ },
8104
8222
  {
8105
8223
  "name": "lyra-media-card",
8106
8224
  "description": "`<lyra-media-card>` — a lightweight inline preview for one already-sent,\nalready-available attachment inside a rendered chat message body (e.g.\nplugged into `<lyra-chat-message>`'s `attachments` slot, or embedded\ndirectly by a markdown/message renderer). Distinct from two other,\nsimilarly-named components in this family: `<lyra-document-preview>` is a\nfuller viewer with header chrome and an async server-side-conversion\nstate machine (`status=\"converting\"`), and `<lyra-attachment-chip>` is a\npre-send queued-file chip with upload progress. This component has\nneither concern — it only ever shows a `src` that's already final.\n\n**Format dispatch.** `kind` selects `'image'` (a plain `<img>`), `'video'`\n(a plain `<video controls>`), or `'file'` (an icon + filename chip with a\ndownload/open affordance). Leave `kind` unset to auto-detect from\n`mime-type` (`image/*` / `video/*` / anything else -> `'file'`);\nexplicitly setting `kind` always wins over detection.\n\n**Safe-URL checking.** `src` is validated before it's ever assigned to an\n`<img>`/`<video>` `src` or an `<a href>` — only `http:`/`https:`/`blob:`\n(plus `data:` for a *media* `src` only) or a scheme-relative/relative URL\nwith no scheme at all pass; anything else (`javascript:`, `vbscript:`,\nand similarly suspicious schemes) is rejected. `safeMediaSrc()` and\n`safeLinkHref()` share a platform-URL-based validator; their sink-specific\nallowlists explain why `data:` gets two different answers depending on\nwhere it is used. An `image`/`video` `kind` whose `src` fails the\nmedia-src check falls back to the generic file-chip rendering — this is\nthe \"plain preview unavailable state\" a dangerous URL degrades to, rather\nthan ever reaching a real media/anchor sink. That fallback also re-checks\n`src` against the stricter href allowlist for its own download affordance,\nbut since the href allowlist is a strict subset of the media-src allowlist,\nthat re-check can only ever change the outcome for a direct/auto-detected\n`kind=\"file\"` `src` — an `image`/`video` `src` that already failed the\nwider media-src check necessarily fails the narrower href check too, so\nthe fallback for those two kinds is always the plain, unclickable `span`.\n\n**The `video` case renders its open affordance separately from `base`.**\n`image`/`file` wrap their *entire* card in one native `<button>`/`<a>` —\nsafe because an `<img>` and a plain icon+text chip have no interactive\ncontent of their own to conflict with. A `<video controls>` element is\nitself interactive content (its own play/seek/volume controls), and HTML\nforbids nesting interactive content inside a `<button>`/`<a>` — doing so\nanyway would also make every click on the video's own controls bubble up\nand spuriously fire `lyra-open`. So for `kind=\"video\"`, `base` is a plain,\nnon-interactive wrapper around `[part=\"media\"]`, and a small separate\n`[part=\"open-button\"]` (not one of this component's originally-scoped\nparts, added as the \"explicit view/open affordance\" the class is free to\nprovide) is the thing that actually fires `lyra-open`.\n\n**Navigation.** This component never navigates on its own for `image`/\n`video` — activating the card only fires `lyra-open`; a host decides what\n\"open\" means (a lightbox, a new tab, whatever). The `file`-chip case is\nthe one exception: when `src` passes the (stricter) href safety check, the\nchip is a real `<a href download>` so a bare drop-in still does something\nuseful — but `lyra-open` fires first and is `cancelable`; a host that\ncalls `preventDefault()` on it suppresses that default download/open so it\ncan substitute its own handling instead.\n\n**Accessible action name.** The host `aria-label` maps to\n`accessibleLabel` and overrides the filename/alt/per-kind action name on\nwhichever internal element is actionable for the resolved kind. The name\nis therefore applied directly to the button or link across the shadow\nboundary; it does not replace the image alt text or the video control's\nown label.\n\n---\n\n**CSS Shadow Parts**\n\n| Name | Description |\n| --- | --- |\n| `base` | The root interactive/container element. A `<button>` for `kind=\"image\"`, a plain wrapper `<div>` for `kind=\"video\"`, and either an `<a>` (when `src` passes the href safety check) or a plain `<span>` (otherwise) for the `file`-chip fallback. |\n| `file-icon` | The generic file glyph, shown only in the file-chip fallback. |\n| `filename` | The filename text, shown only in the file-chip fallback. |\n| `media` | The `<img>` or `<video>` element. |\n| `open-button` | The explicit \"open\" affordance rendered next to `[part=\"media\"]` for `kind=\"video\"` only — see the class doc. |",
@@ -12943,7 +13061,7 @@
12943
13061
  },
12944
13062
  {
12945
13063
  "name": "lyra-thread-list",
12946
- "description": "`<lyra-thread-list>` — the conversation sidebar: a grouped, searchable list of chat sessions with\npin/archive/delete/rename affordances. *Data mode* (non-empty `threads`, or empty `threads` with\nnothing slotted) renders every row as a `<lyra-conversation-item>` inside an internal\n`<lyra-virtual-list>` — virtualized by construction, scroll position and per-row state survive a\n`threads` replacement; zero rows renders the built-in empty state. *Slotted mode* (empty `threads`\n*and* real slotted content) renders host-supplied `<lyra-conversation-item>`s from the default slot\nas-is: no grouping, virtualization, or row actions in that mode — those are data-mode-only by design\n(shadow DOM cannot inject group headers between slotted children).\n\nNo thread CRUD or persistence: every mutation (`lyra-thread-pin`/`-archive`/`-delete`/`-rename`) is\na controlled event carrying the *requested* new state — the host mutates `threads`.\n\n---\n\n**Slots**\n\n| Name | Description |\n| --- | --- |\n| (default) | Slotted mode only: host-supplied `lyra-conversation-item`s, rendered in order. Each top-level assigned element that doesn't already carry an explicit `role` is given `role=\"listitem\"`, since `[part=\"list\"]` is `role=\"list\"` in this mode and `lyra-conversation-item` deliberately doesn't self-apply that role (see its own class doc). |\n| `empty` | Replaces the built-in empty state. |\n\n---\n\n**CSS Shadow Parts**\n\n| Name | Description |\n| --- | --- |\n| `base` | The root. |\n| `empty` | The empty/no-matches state. |\n| `group-header` | Exported from the internal `lyra-virtual-list`'s `group` part (data mode, `grouping=\"date\"`). |\n| `list` | The list region. |\n| `pin-glyph` | The small pin indicator shown in a pinned row's `meta` slot (data mode). |\n| `row` | Exported from the internal `lyra-virtual-list`'s `row` part (data mode). |\n| `row-action` | A built-in pin/archive/delete icon button (data mode, when `rowActions` includes it). |\n| `search` | The search field wrapper. |\n| `search-input` | The `<input type=\"search\">`. |",
13064
+ "description": "`<lyra-thread-list>` — the conversation sidebar: a grouped, searchable list of chat sessions with\npin/archive/delete/rename affordances. *Data mode* (non-empty `threads`, or empty `threads` with\nnothing slotted) renders every row as a `<lyra-conversation-item>` inside an internal\n`<lyra-virtual-list>` — virtualized by construction, scroll position and per-row state survive a\n`threads` replacement; zero rows renders the built-in empty state. *Slotted mode* (empty `threads`\n*and* real slotted content) renders host-supplied `<lyra-conversation-item>`s from the default slot\nas-is: no grouping, virtualization, or row actions in that mode — those are data-mode-only by design\n(shadow DOM cannot inject group headers between slotted children).\n\nNo thread CRUD or persistence: every mutation (`lyra-thread-pin`/`-archive`/`-delete`/`-rename`) is\na controlled event carrying the *requested* new state — the host mutates `threads`.\n\nData mode: a host needing content with no home in `lyra-conversation-item`'s own\n`title`/`excerpt`/`meta`/`actions` surface (e.g. a leading purpose icon — the item has no default\nslot to receive one) sets `wrapRow` to wrap the already-built row.\n\n---\n\n**Slots**\n\n| Name | Description |\n| --- | --- |\n| (default) | Slotted mode only: host-supplied `lyra-conversation-item`s, rendered in order. Each top-level assigned element that doesn't already carry an explicit `role` is given `role=\"listitem\"`, since `[part=\"list\"]` is `role=\"list\"` in this mode and `lyra-conversation-item` deliberately doesn't self-apply that role (see its own class doc). |\n| `empty` | Replaces the built-in empty state. |\n\n---\n\n**CSS Shadow Parts**\n\n| Name | Description |\n| --- | --- |\n| `base` | The root. |\n| `empty` | The empty/no-matches state. |\n| `group-header` | Exported from the internal `lyra-virtual-list`'s `group` part (data mode, `grouping=\"date\"`). |\n| `list` | The list region. |\n| `pin-glyph` | The small pin indicator shown in a pinned row's `meta` slot (data mode). |\n| `row` | Exported from the internal `lyra-virtual-list`'s `row` part (data mode). |\n| `row-action` | A built-in pin/archive/delete icon button (data mode, when `rowActions` includes it). |\n| `search` | The search field wrapper. |\n| `search-input` | The `<input type=\"search\">`. |",
12947
13065
  "attributes": [
12948
13066
  {
12949
13067
  "name": "active-id",
@@ -14267,7 +14385,7 @@
14267
14385
  },
14268
14386
  {
14269
14387
  "name": "lyra-usage-badge",
14270
- "description": "`<lyra-usage-badge>` — a compact, static resource strip for one message or run: tokens in/out,\ncost, latency, with a hover/focus tooltip breakdown. Purely formatting — this component computes\nno counts, rates, or prices; every segment is independently optional, and with nothing set,\nnothing renders at all (not even a focusable/interactive shell).\n\nThe tooltip reuses `<lyra-tool-call-chip>`'s hover/focus/Escape/`aria-describedby` contract\nwholesale: hover and focus are tracked as independent \"keep it open\" reasons, so releasing one\nmodality while the other still holds doesn't close it.\n\n---\n\n**Slots**\n\n| Name | Description |\n| --- | --- |\n| (default) | Extra rows appended below the built-in tooltip breakdown (e.g. cache-read tokens). The visible strip itself is prop-driven only. |\n\n---\n\n**CSS Shadow Parts**\n\n| Name | Description |\n| --- | --- |\n| `base` | The root inline strip (a focusable non-button `role=\"group\"`, only while at least one segment or the default slot has content). |\n| `cost` | The verbatim `costText`. Only rendered when set. |\n| `latency` | The formatted `latencyMs`. Only rendered when it's a finite number. |\n| `tokens-in` | The `'{count} in'` segment. Only rendered when `tokensIn` is a finite number. |\n| `tokens-out` | The `'{count} out'` segment. Only rendered when `tokensOut` is a finite number. |\n| `tooltip` | The floating detail breakdown, only meaningful while open. |",
14388
+ "description": "`<lyra-usage-badge>` — a compact, static resource strip for one message or run: tokens in/out,\ncost, latency, with a hover/focus tooltip breakdown. Purely formatting — this component computes\nno counts, rates, or prices; every segment is independently optional, and with nothing set,\nnothing renders at all (not even a focusable/interactive shell).\n\nThe tooltip reuses `<lyra-tool-call-chip>`'s hover/focus/Escape/`aria-describedby` contract\nwholesale: hover and focus are tracked as independent \"keep it open\" reasons, so releasing one\nmodality while the other still holds doesn't close it.\n\nThe built-in latency formatting has no minutes/hours tier (`'{ms}ms'`, or one-decimal seconds\nabove 1000ms) — a host whose latencies commonly exceed a minute sets `formatLatency` to render\nits own scale instead, in both the visible strip and the tooltip row.\n\n---\n\n**Slots**\n\n| Name | Description |\n| --- | --- |\n| (default) | Extra rows appended below the built-in tooltip breakdown (e.g. cache-read tokens). The visible strip itself is prop-driven only. |\n\n---\n\n**CSS Shadow Parts**\n\n| Name | Description |\n| --- | --- |\n| `base` | The root inline strip (a focusable non-button `role=\"group\"`, only while at least one segment or the default slot has content). |\n| `cost` | The verbatim `costText`. Only rendered when set. |\n| `latency` | The formatted `latencyMs`. Only rendered when it's a finite number. |\n| `tokens-in` | The `'{count} in'` segment. Only rendered when `tokensIn` is a finite number. |\n| `tokens-out` | The `'{count} out'` segment. Only rendered when `tokensOut` is a finite number. |\n| `tooltip` | The floating detail breakdown, only meaningful while open. |",
14271
14389
  "attributes": [
14272
14390
  {
14273
14391
  "name": "compact",
@@ -14289,7 +14407,7 @@
14289
14407
  },
14290
14408
  {
14291
14409
  "name": "latency-ms",
14292
- "description": "Latency in milliseconds, formatted with the shared duration algorithm.\n\nType: `number | undefined`"
14410
+ "description": "Latency in milliseconds, formatted with the shared duration algorithm (or `formatLatency`,\nwhen set).\n\nType: `number | undefined`"
14293
14411
  },
14294
14412
  {
14295
14413
  "name": "locale",
@@ -15048,6 +15166,10 @@
15048
15166
  "name": "--lyra-graph-cat-1..8",
15049
15167
  "description": "**`<lyra-graph>`** — Ordered categorical fallback palette for a typed node with no `GraphNodeType.color`, assigned by the type's index in `nodeTypes` (wraps every 8 entries). Declared centrally in `tokens.styles.ts` so `<lyra-graph>` and any future `<lyra-graph-legend>`- style component resolve the identical default."
15050
15168
  },
15169
+ {
15170
+ "name": "--lyra-graph-dimmed-opacity",
15171
+ "description": "**`<lyra-graph>`** (default: `1`) — Opacity applied to a node/link when `dimmedNodeIds`/`dimmedLinkIds` includes its id (both SVG and canvas renderers). `1` (the default) is a no-op -- an unset host sees no visual change."
15172
+ },
15051
15173
  {
15052
15174
  "name": "--lyra-graph-edge-label-halo",
15053
15175
  "description": "**`<lyra-graph>`** (default: `var(--lyra-color-surface)`) — Legibility halo (`stroke`) behind a drawn edge label, painted under the fill via `paint-order: stroke`."