@glissade/backend-dom 0.23.0-pre.1 → 0.23.0-pre.2

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/README.md CHANGED
@@ -136,11 +136,38 @@ base bundle is absent or a different version (never a cryptic `undefined`).
136
136
  The backend only ever manages its **own root** subtree — your overlay/foreign DOM
137
137
  in the host element is left untouched.
138
138
 
139
- ## Status: Stage S2 (forward render)
140
-
141
- Today this is a **forward renderer**: each `render()` rebuilds the tree. That is
142
- right for **playback preview, structural snapshots, and a11y reads**. The
143
- **retained-DOM reconciler** (reuse + patch the same element per `data-node-id`
144
- across frames required so an in-progress inline-edit caret, selection, focus,
145
- and event listeners survive a re-render) is **Stage S3**, a follow-up. See
146
- `docs/design/dom-backend.md`.
139
+ ## Accessibility & theming (S4)
140
+
141
+ The DOM tier exists for editing + a11y, so it keeps the **real text** readable by
142
+ assistive tech and hides the decorative geometry:
143
+
144
+ - **Shape `<svg>` islands and `<img>` elements are `aria-hidden`** a screen
145
+ reader reads the Text divs (the meaningful content), not the paths.
146
+ - **`ariaLabel`** names the whole graphic: the root gets `role="figure"` +
147
+ `aria-label` (a labeled region whose readable text stays exposed — unlike
148
+ `role="img"`, which would hide the text). Unset ⇒ a generic container.
149
+ - **Focus order** is the host/editor's to manage — every node carries
150
+ `data-node-id`, so an editor decides which nodes are focusable (`tabindex`)
151
+ rather than the backend imposing a tab order on every shape.
152
+
153
+ ```js
154
+ const backend = new DomBackend(stage, {
155
+ ariaLabel: 'Episode 1 cold open',
156
+ cssColorVars: true, // emit colors as var(--gs-c-…, color) for re-theming
157
+ });
158
+ ```
159
+
160
+ **CSS-variable theming** (`cssColorVars`): solid fills/text colors emit as
161
+ `var(--gs-c-<ident>, <color>)`, so a host can re-theme (light/dark, brand) by
162
+ overriding the `--gs-c-*` variables in CSS — the browser re-paints with **no
163
+ re-render**. Off by default (literal colors; byte-stable for existing consumers).
164
+ Gradient stops are not yet varied (a follow-up).
165
+
166
+ ## Stages
167
+
168
+ S2 (forward render) + **S3** (the retained-DOM reconciler — reuse + patch per
169
+ `data-node-id` so inline-edit caret / selection / focus / listeners survive a
170
+ re-render) + the 0.22 structural hardening (shape-sized SVG islands, rounded-rect
171
+ fill, movement-boundary reconcile, `onReflow` font re-wrap) + **S4** (a11y +
172
+ CSS-native theming, above) have all shipped. The real-browser visual-smoke gate
173
+ runs in the consumer canaries' standing harnesses. See `docs/design/dom-backend.md`.
package/dist/index.d.ts CHANGED
@@ -13,6 +13,21 @@ interface DomBackendOptions {
13
13
  * draw loop. No-op where `document.fonts` is absent (e.g. jsdom).
14
14
  */
15
15
  onReflow?: () => void;
16
+ /**
17
+ * S4 a11y: an accessible name for the whole rendered graphic. When set, the
18
+ * root gets `role="figure"` + `aria-label` (a labeled region whose readable
19
+ * text stays in the a11y tree). Decorative geometry (the SVG shape islands +
20
+ * images) is always `aria-hidden`, so a screen reader reads the Text, not the
21
+ * paths. Unset ⇒ the root carries no role (a generic container).
22
+ */
23
+ ariaLabel?: string;
24
+ /**
25
+ * S4 theming: emit solid colors as CSS custom properties — `var(--gs-c-<ident>,
26
+ * <color>)` — so a host can re-theme (light/dark, brand) by overriding the
27
+ * `--gs-c-*` variables in CSS, **without a re-render** (the browser re-paints).
28
+ * Default off ⇒ literal colors (byte-stable for existing consumers).
29
+ */
30
+ cssColorVars?: boolean;
16
31
  }
17
32
  /**
18
33
  * A DOM/SVG `RenderBackend`. Construct with a host element (renders into it) or a
package/dist/index.js CHANGED
@@ -158,6 +158,8 @@ var DomBackend = class {
158
158
  #owned = /* @__PURE__ */ new WeakMap();
159
159
  #ids = [];
160
160
  #onReflow;
161
+ /** S4: wrap solid colors in `var(--gs-c-…, color)` so a host re-themes via CSS. */
162
+ #cssColorVars;
161
163
  #measureSpan = null;
162
164
  #warnedMeasure = false;
163
165
  #warnedMesh = false;
@@ -172,8 +174,13 @@ var DomBackend = class {
172
174
  this.root.setAttribute("data-gs-dom", "");
173
175
  this.root.style.position = "relative";
174
176
  this.root.style.overflow = "hidden";
177
+ if (opts.ariaLabel !== void 0) {
178
+ this.root.setAttribute("role", "figure");
179
+ this.root.setAttribute("aria-label", opts.ariaLabel);
180
+ }
175
181
  if (this.#host) this.#host.appendChild(this.root);
176
182
  this.#onReflow = opts.onReflow;
183
+ this.#cssColorVars = opts.cssColorVars ?? false;
177
184
  this.#wireFontReflow();
178
185
  }
179
186
  /**
@@ -236,6 +243,7 @@ var DomBackend = class {
236
243
  const svg = doc.createElementNS(SVG_NS, "svg");
237
244
  svg.setAttribute("width", "0");
238
245
  svg.setAttribute("height", "0");
246
+ svg.setAttribute("aria-hidden", "true");
239
247
  svg.style.position = "absolute";
240
248
  svg.style.left = "0";
241
249
  svg.style.top = "0";
@@ -397,6 +405,8 @@ var DomBackend = class {
397
405
  const img = doc.createElement("img");
398
406
  img.style.position = "absolute";
399
407
  img.style.objectFit = "fill";
408
+ img.setAttribute("alt", "");
409
+ img.setAttribute("aria-hidden", "true");
400
410
  return {
401
411
  op: "drawImage",
402
412
  el: img,
@@ -716,9 +726,19 @@ var DomBackend = class {
716
726
  if (a && typeof a === "object" && "src" in a && typeof a.src === "string") return a.src;
717
727
  }
718
728
  #solid(paint) {
719
- if (paint.kind === "color") return paint.color;
720
- if (paint.kind === "mesh") return paint.bg ?? paint.points[0]?.color ?? "#000";
721
- return paint.stops[0]?.color ?? "#000";
729
+ const color = paint.kind === "color" ? paint.color : paint.kind === "mesh" ? paint.bg ?? paint.points[0]?.color ?? "#000" : paint.stops[0]?.color ?? "#000";
730
+ return this.#themeColor(color);
731
+ }
732
+ /**
733
+ * S4 theming: when `cssColorVars` is on, wrap a literal color in a CSS custom
734
+ * property `var(--gs-c-<ident>, <color>)` so a host re-themes by overriding the
735
+ * `--gs-c-*` vars in CSS — no re-render (the browser re-paints). Off ⇒ the
736
+ * literal color (byte-stable). The ident is the color with non-ident runs
737
+ * collapsed to '-' (`'#89b4fa'` → `89b4fa`, `'rgb(1,2,3)'` → `rgb-1-2-3`).
738
+ */
739
+ #themeColor(color) {
740
+ if (!this.#cssColorVars) return color;
741
+ return `var(--gs-c-${color.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "")}, ${color})`;
722
742
  }
723
743
  /** A signature of the gradient paint — a `<defs>` subtree is rebuilt only when
724
744
  * this changes (kind / coords / stops), avoiding per-frame churn. */
@@ -731,7 +751,7 @@ var DomBackend = class {
731
751
  #resolvePaint(paint, o, scope, key) {
732
752
  if (paint.kind === "color") {
733
753
  this.#setAttr(o.path, o, "dataApprox", "data-approx", void 0);
734
- return paint.color;
754
+ return this.#themeColor(paint.color);
735
755
  }
736
756
  if (paint.kind === "mesh") {
737
757
  this.#setAttr(o.path, o, "dataApprox", "data-approx", "true");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/backend-dom",
3
- "version": "0.23.0-pre.1",
3
+ "version": "0.23.0-pre.2",
4
4
  "description": "glissade DOM render backend: DisplayList -> HTML/SVG elements. A preview / non-parity realtime tier (accessibility, selectable text, CSS-native embedding) — NOT a Skia-export twin.",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -18,8 +18,8 @@
18
18
  "dist"
19
19
  ],
20
20
  "dependencies": {
21
- "@glissade/scene": "0.23.0-pre.1",
22
- "@glissade/core": "0.23.0-pre.1"
21
+ "@glissade/core": "0.23.0-pre.2",
22
+ "@glissade/scene": "0.23.0-pre.2"
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",