Embed every shared palette behind root appearance selectors and synchronize packed SVG output with the editable document. Document the CSS-controlled palette contract and the separate export-time geometry boundary for agents and users.
- commit
- 7eea6d2be8a8c770788227bb550290899188a9db
- parent
- b3c92c7027619e10e42274ac96128f19095f2d82
- author
- Dami <dami.etoile@gmail.com>
| M | apps/ | 8 | ++++++++ |
| M | apps/ | 8 | ++++++++ |
| M | apps/ | 10 | ++++++++-- |
| M | apps/ | 6 | ++++++ |
| M | apps/ | 26 | +++++++++++++++++++++++-- |
| M | apps/ | 13 | ++++++++++--- |
| M | apps/ | 12 | ++++++++---- |
| M | apps/ | 15 | ++++++++++++++- |
| M | apps/ | 12 | +++++++----- |
| M | docs/ | 10 | ++++++++-- |
| M | services/ | 2 | +- |
| M | services/ | 4 | ++++ |
| M | shared/ | 2 | +- |
| M | shared/ | 31 | +++++++++++++++++------------- |
| M | skills/ | 13 | ++++++++++--- |
@@ -105,6 +105,14 @@ requiring publication. An open published alias uses its stable server render.
105105 An anonymous draft uses a compressed, self-contained editable SVG URL and does106106 not create server state.107107 108+Before packing an anonymous URL, the editor prepares the selected transparent109+or framed geometry and embeds self-contained palette CSS. The root110+`data-dz-appearance` value selects raw, light, dark, or automatic palette rules.111+The `/svg/{packed}` route validates the SVG and synchronizes that one value from112+the embedded `diagram.presentation.appearance`. Consumers do not need to113+simulate the palette or patch SVG paint client-side. Frame padding and the outer114+`viewBox` are prepared before packing because CSS cannot change SVG bounds.115+108116 Packed URLs contain the rendered SVG, diagram source, options, metadata, and109117 presentation settings. Anyone with the URL can inspect that content and reopen110118 it as an editable SVG. Do not use a packed URL for secrets. Diagram.zip rejects@@ -47,6 +47,14 @@ The shared appearances apply one Diagram.zip palette to supported renderer outpu
4747 - Transparent appearances omit the outer canvas.4848 - Framed appearances add a matching canvas, standard padding, and a border.4949 50+When the editor creates an anonymous packed SVG URL, it embeds self-contained51+palette rules selected by the root `data-dz-appearance`. The `/svg/{packed}`52+route synchronizes that single value from the embedded53+`diagram.presentation.appearance`. Changing the data value selects raw, light,54+dark, or automatic palette CSS without rewriting SVG paint. Transparent or55+framed bounds are prepared before packing because CSS cannot change the outer56+`viewBox`.57+5058 The editor disables appearances that the current renderer profile cannot support. Diagrams.net supports raw and framed appearances while its authored paint remains unchanged.5159 5260 Excalidraw and TikZ support every appearance. Their neutral canvas, ink, and line paint adapt while authored non-neutral paint remains renderer-defined.@@ -115,7 +115,7 @@ The [renderer catalog](https://diagram.zip/render/v1/catalog) reports every engi
115115 | Normalizer | `svg-normalizer-2` | Sanitization, annotation, or serialization behavior changes. |116116 | Profile | `neutral-svg-semantic-2` | Renderer recognition or role assignment changes. |117117 | Palette | `diagramzip-palette-2` | Materialized role colors change. |118-| Materializer | `svg-materializer-3` | Appearance rules, padding, frame, or output assembly changes. |118+| Materializer | `svg-materializer-4` | Appearance rules, padding, frame, or output assembly changes. |119119 | Renderer | Catalog version and build | Engine output or its integration changes. |120120 121121 These versions advance independently. A renderer upgrade does not require a schema change, and a palette update does not require new geometry.@@ -133,7 +133,7 @@ Canonical and materialized SVG expose their active contract through `data-dz-*`
133133 | `data-dz-engine` | Renderer engine identifier. |134134 | `data-dz-profile` | Selected normalization profile. |135135 | `data-dz-palette` | `renderer` for raw SVG, or the selected Diagram.zip palette build. |136-| `data-dz-appearance` | Materialized appearance. |136+| `data-dz-appearance` | Active palette selector and materialized geometry mode. |137137 | `data-dz-conformance` | Active capability level. |138138 | `data-dz-appearances` | Supported non-raw appearances, separated by spaces. |139139 | `data-dz-bounds` | Canonical diagram bounds as `x y width height`. |@@ -159,6 +159,12 @@ The identifiers above are examples. Inspect the artifact or catalog instead of h
159159 160160 Consumers should treat `data-dz-appearances` as an allowlist. Adding an unsupported value by hand does not create a supported artifact.161161 162+Materialized SVG embeds raw, light, dark, and automatic palette rules keyed by163+the root `data-dz-appearance`. Changing that one value can select palette CSS164+without rewriting element paint. Transparent versus framed output may have165+different bounds; materialization still prepares the outer `viewBox`, padding,166+canvas, and frame geometry.167+162168 ## Consumer guarantees163169 164170 A normalized SVG has these guarantees:@@ -133,6 +133,12 @@ assert(llmsFull.includes('TRN originates with [Michael Chu]'), 'llms-full.txt do
133133 assert(llmsFull.includes('## Public authoring contract'), 'llms-full.txt does not include the public authoring contract.')134134 assert(llmsFull.includes('Presentation appearance changes palette and canvas treatment.'), 'llms-full.txt does not separate TRN layout from appearance.')135135 assert(llmsFull.includes('Do not edit generated SVG, CSS classes, data attributes, viewBox values, or renderer implementation files.'), 'llms-full.txt does not protect the TRN output boundary.')136+assert(llmsFull.includes('`data-dz-appearance` value selects raw, light, dark, or automatic palette rules.'), 'llms-full.txt does not explain packed SVG palette selection.')137+assert(llmsFull.includes('CSS cannot change SVG bounds.'), 'llms-full.txt does not preserve the packed SVG geometry boundary.')138+139+const diagrammingSkill = await source(join(site, 'static', 'skills', 'diagramming', 'SKILL.md'))140+assert(diagrammingSkill.includes('synchronizes that single'), 'Diagramming skill does not explain packed SVG appearance synchronization.')141+assert(diagrammingSkill.includes('report a mismatch as a contract violation'), 'Diagramming skill does not tell agents how to handle a packed SVG appearance mismatch.')136142 137143 const expectedSkillIds = [...diagramSkillIds].sort()138144 const skillEntries = await readdir(skills, { withFileTypes: true })@@ -147,6 +147,14 @@ The shared appearances apply one Diagram.zip palette to supported renderer outpu
147147 - Transparent appearances omit the outer canvas.148148 - Framed appearances add a matching canvas, standard padding, and a border.149149 150+When the editor creates an anonymous packed SVG URL, it embeds self-contained151+palette rules selected by the root `data-dz-appearance`. The `/svg/{packed}`152+route synchronizes that single value from the embedded153+`diagram.presentation.appearance`. Changing the data value selects raw, light,154+dark, or automatic palette CSS without rewriting SVG paint. Transparent or155+framed bounds are prepared before packing because CSS cannot change the outer156+`viewBox`.157+150158 The editor disables appearances that the current renderer profile cannot support. Diagrams.net supports raw and framed appearances while its authored paint remains unchanged.151159 152160 Excalidraw and TikZ support every appearance. Their neutral canvas, ink, and line paint adapt while authored non-neutral paint remains renderer-defined.@@ -311,7 +319,7 @@ The [renderer catalog](https://diagram.zip/render/v1/catalog) reports every engi
311319 | Normalizer | `svg-normalizer-2` | Sanitization, annotation, or serialization behavior changes. |312320 | Profile | `neutral-svg-semantic-2` | Renderer recognition or role assignment changes. |313321 | Palette | `diagramzip-palette-2` | Materialized role colors change. |314-| Materializer | `svg-materializer-3` | Appearance rules, padding, frame, or output assembly changes. |322+| Materializer | `svg-materializer-4` | Appearance rules, padding, frame, or output assembly changes. |315323 | Renderer | Catalog version and build | Engine output or its integration changes. |316324 317325 These versions advance independently. A renderer upgrade does not require a schema change, and a palette update does not require new geometry.@@ -329,7 +337,7 @@ Canonical and materialized SVG expose their active contract through `data-dz-*`
329337 | `data-dz-engine` | Renderer engine identifier. |330338 | `data-dz-profile` | Selected normalization profile. |331339 | `data-dz-palette` | `renderer` for raw SVG, or the selected Diagram.zip palette build. |332-| `data-dz-appearance` | Materialized appearance. |340+| `data-dz-appearance` | Active palette selector and materialized geometry mode. |333341 | `data-dz-conformance` | Active capability level. |334342 | `data-dz-appearances` | Supported non-raw appearances, separated by spaces. |335343 | `data-dz-bounds` | Canonical diagram bounds as `x y width height`. |@@ -355,6 +363,12 @@ The identifiers above are examples. Inspect the artifact or catalog instead of h
355363 356364 Consumers should treat `data-dz-appearances` as an allowlist. Adding an unsupported value by hand does not create a supported artifact.357365 366+Materialized SVG embeds raw, light, dark, and automatic palette rules keyed by367+the root `data-dz-appearance`. Changing that one value can select palette CSS368+without rewriting element paint. Transparent versus framed output may have369+different bounds; materialization still prepares the outer `viewBox`, padding,370+canvas, and frame geometry.371+358372 ## Consumer guarantees359373 360374 A normalized SVG has these guarantees:@@ -487,6 +501,14 @@ requiring publication. An open published alias uses its stable server render.
487501 An anonymous draft uses a compressed, self-contained editable SVG URL and does488502 not create server state.489503 504+Before packing an anonymous URL, the editor prepares the selected transparent505+or framed geometry and embeds self-contained palette CSS. The root506+`data-dz-appearance` value selects raw, light, dark, or automatic palette rules.507+The `/svg/{packed}` route validates the SVG and synchronizes that one value from508+the embedded `diagram.presentation.appearance`. Consumers do not need to509+simulate the palette or patch SVG paint client-side. Frame padding and the outer510+`viewBox` are prepared before packing because CSS cannot change SVG bounds.511+490512 Packed URLs contain the rendered SVG, diagram source, options, metadata, and491513 presentation settings. Anyone with the URL can inspect that content and reopen492514 it as an editable SVG. Do not use a packed URL for secrets. Diagram.zip rejects@@ -48,9 +48,16 @@ the user wants server persistence or a share alias.
4848 4949 For an image link without persistence, **Copy SVG URL** or **Copy as Markdown**5050 packs the editable SVG into a self-contained URL. Published open aliases use51-their stable render URL instead. Packed links expose the embedded source and52-metadata to anyone who has the URL; use a file or alias when the packed URL is53-too large, and do not suggest public embeds for locked diagrams.51+their stable render URL instead. For an anonymous packed link, the editor52+prepares transparent or framed geometry and embeds palette CSS selected by the53+root `data-dz-appearance`. The `/svg/{packed}` route synchronizes that single54+value from the embedded `diagram.presentation.appearance`. Verify those values55+agree; report a mismatch as a contract violation, and do not simulate the56+palette or patch SVG paint client-side. CSS does not change the outer `viewBox`,57+so changing between transparent and framed geometry requires a fresh export.58+Packed links expose the embedded source and metadata to anyone who has the URL;59+use a file or alias when the packed URL is too large, and do not suggest public60+embeds for locked diagrams.5461 5562 ## Prior art and standards5663 @@ -212,10 +212,14 @@ the same appearance on stable SVG and Markdown links. `raw` uses the renderer
212212 canvas controls. A shared appearance ignores those legacy controls and213213 materializes its own palette and, for framed appearances, its own canvas.214214 215-An anonymous draft instead exports the current deterministic editable SVG and216-packs those bytes into `/svg/{compressed_editable_svg}`. This path does not217-proxy a render request and works with the canonical output from every HTTP218-renderer unit.215+An anonymous draft instead prepares the selected transparent or framed216+geometry, embeds self-contained palette CSS, and packs the deterministic217+editable SVG into `/svg/{compressed_editable_svg}`. The root218+`data-dz-appearance` is the palette selector. The route validates and sanitizes219+the packed SVG, then synchronizes that one value from the embedded220+`diagram.presentation.appearance`. It does not proxy a render request. Frame221+padding and the outer `viewBox` remain export-time geometry because CSS cannot222+change SVG bounds.219223 220224 ## Editor and share UX221225 @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
22 import { readFile } from 'node:fs/promises'33 import test from 'node:test'44 import { fileURLToPath } from 'node:url'5-import { canonicalizeSvg } from '../../../shared/svg/index.js'5+import { attachEditableDocument, canonicalizeSvg, materializeSvg } from '../../../shared/svg/index.js'66 import {77 EditableSvgError,88 editableDocumentFor,@@ -60,6 +60,18 @@ const cases = [
6060 },6161 ]6262 63+test('uses the embedded presentation as the packed SVG palette selector', async () => {64+ const state = cases[1].state65+ const rendered = await fixture('mermaid')66+ const canonical = canonicalizeSvg(rendered, state.meta, state.type, cases[1].version)67+ const light = materializeSvg(canonical, 'light-transparent')68+ const packed = attachEditableDocument(light, editableDocumentFor(state))69+70+ assert.match(packed, /data-dz-appearance=\"dark-transparent\"/)71+ assert.ok(packed.includes('data-dz-appearance^=\"light-\"'))72+ assert.ok(packed.includes('data-dz-appearance^=\"dark-\"'))73+})74+6375 test('exports and reimports a deterministic golden set of renderer SVGs', async () => {6476 for (const entry of cases) {6577 const state = entry.state@@ -73,6 +85,7 @@ test('exports and reimports a deterministic golden set of renderer SVGs', async
7385 assert.equal(first, repeated, `${entry.name} export must be deterministic`)7486 assert.equal(first, reexported, `${entry.name} import/export must be byte-stable`)7587 assert.deepEqual(imported, state, `${entry.name} state must round trip exactly`)88+ assert.ok(first.includes("data-dz-appearance=\"" + state.presentation.appearance + "\""))7689 assert.match(first, /data-dz-document="1"/)7790 assert.equal((first.match(/<metadata data-dz-kind="document"/g) ?? []).length, 1)7891 }@@ -91,13 +91,15 @@ test('materializes explicit and automatic palettes without changing diagram geom
9191 const automatic = materializeSvg(semanticCanonical, 'auto-transparent')9292 9393 for (const output of [light, dark, automatic]) {94- assert.match(output, /data-dz-materializer="svg-materializer-3"/)94+ assert.match(output, /data-dz-materializer="svg-materializer-4"/)9595 assert.match(output, /data-dz-palette="diagramzip-palette-2"/)9696 assert.match(output, /<rect x="1" y="1" width="8" height="8"/)9797 assert.doesNotMatch(output, /<(?:rect|path|polygon)[^>]*data-dz-role="canvas"/)9898 }99- assert.match(light, /--dz-ink:#0f172a/)100- assert.match(dark, /--dz-ink:#f8fafc/)99+ for (const output of [light, dark, automatic]) {100+ assert.ok(output.includes("data-dz-appearance^=\"light-\"") && output.includes("--dz-ink:#0f172a"))101+ assert.ok(output.includes("data-dz-appearance^=\"dark-\"") && output.includes("--dz-ink:#f8fafc"))102+ }101103 assert.match(automatic, /@media\(prefers-color-scheme:dark\)/)102104 })103105 @@ -189,11 +191,11 @@ test('normalizes TRN table structure and relationship zones', () => {
189191 assert.match(trn, /class="trn-zone-shape trn-branch-4"[^>]*data-dz-fill="accent-4"[^>]*data-dz-stroke="line-muted"/)190192 assert.match(trn, /class="trn-operation-label"[^>]*data-dz-fill="on-accent"/)191193 const light = materializeSvg(trn, 'light-transparent')192- assert.match(light, /:root\[data-dz-profile="trn-semantic-4"\]\{--dz-accent-1:#dbeafe;--dz-accent-2:#fef3c7;--dz-accent-3:#ede9fe;--dz-accent-4:#ccfbf1;--dz-on-accent:#0f172a;/)194+ assert.match(light, /:root\[data-dz-profile="trn-semantic-4"\]\[data-dz-appearance\^="light-"\]/)193195 const dark = materializeSvg(trn, 'dark-transparent')194196 assert.match(dark, /data-dz-appearance="dark-transparent"/)195197 assert.match(dark, /--dz-accent-4:#34d399/)196- assert.doesNotMatch(dark, /--dz-accent-1:#dbeafe/)198+ assert.match(dark, /:root\[data-dz-appearance\^="dark-"\]\{[^}]*--dz-accent-4:#34d399/)197199 })198200 199201 test('themes every Svgbob stroke without treating open curves as object surfaces', () => {@@ -461,8 +461,10 @@ difficult to compare by pixels.
461461 ## Materialization462462 463463 Canonical SVG is the reusable geometry and role representation. Materialization464-selects a root appearance value and embeds the required palette tokens and role465-rules. It applies canvas or frame geometry and serializes a self-contained SVG.464+embeds the palette tokens and role rules for raw, light, dark, and automatic465+selection. The root `data-dz-appearance` is the single CSS selector for those466+palettes. Materialization also applies canvas or frame geometry and serializes a467+self-contained SVG.466468 467469 Consumers may inline canonical SVG and change its root appearance in a trusted468470 environment. Consumers displaying SVG through an image element cannot assume@@ -474,6 +476,10 @@ and dark or transparent and framed variants reuses canonical geometry. Framed
474476 and transparent variants may have different outer view boxes because padding475477 and frame geometry are presentation, not diagram layout.476478 479+Changing the root data value is sufficient to switch palette rules within480+prepared geometry. Changing between transparent and framed bounds still481+requires materialization because CSS cannot rewrite the root `viewBox`.482+477483 ## Conformance and capability reporting478484 479485 An appearance is supported only when the active profile satisfies its@@ -398,7 +398,7 @@ describe('open aliases', () => {
398398 expect(raw.status).toBe(200)399399 expect(framed.status).toBe(200)400400 expect(framed.headers.get('X-Diagram-Appearance')).toBe('dark-framed')401- expect(framed.headers.get('X-SVG-Materializer')).toBe('svg-materializer-3')401+ expect(framed.headers.get('X-SVG-Materializer')).toBe('svg-materializer-4')402402 expect(framed.headers.get('X-SVG-Palette')).toBe('diagramzip-palette-2')403403 expect(framed.headers.get('ETag')).not.toBe(raw.headers.get('ETag'))404404 const framedSvg = await framed.text()@@ -1,6 +1,7 @@
11 import { env, SELF } from 'cloudflare:test'22 import { deflate } from 'pako'33 import { describe, expect, it } from 'vitest'4+import { extractEditableDocument } from '../../../shared/svg/index.js'45 56 function packedSvg(source: string): string {67 const bytes = deflate(new TextEncoder().encode(source), { level: 9 })@@ -39,8 +40,11 @@ describe('diagram.zip shell', () => {
3940 const source = `<svg xmlns="http://www.w3.org/2000/svg" data-dz-schema="1" data-dz-document="1"><metadata data-dz-kind="document" data-dz-schema="1">${document}</metadata><script>alert(1)</script><rect width="10" height="10" onload="alert(1)"/></svg>`4041 const response = await SELF.fetch(`https://diagram.zip/svg/${packedSvg(source)}`)4142 const body = await response.text()43+ const embedded = extractEditableDocument(body) as { diagram: { presentation: { appearance: string } } }4244 4345 expect(response.status).toBe(200)46+ expect(body).toContain('data-dz-appearance=\"raw\"')47+ expect(embedded.diagram.presentation.appearance).toBe('raw')4448 expect(response.headers.get('content-type')).toContain('image/svg+xml')4549 expect(response.headers.get('cache-control')).toBe('public, max-age=31536000, immutable')4650 expect(response.headers.get('x-diagram-document')).toBe('editable-svg-1')@@ -1,7 +1,7 @@
11 export const SVG_SCHEMA: '1'22 export const EDITABLE_SVG_SCHEMA: '1'33 export const NORMALIZER_BUILD: 'svg-normalizer-2'4-export const MATERIALIZER_BUILD: 'svg-materializer-3'4+export const MATERIALIZER_BUILD: 'svg-materializer-4'55 export const PALETTE_BUILD: 'diagramzip-palette-2'66 export const RAW_PROFILE: 'safe-raw-1'77 export const APPEARANCES: readonly [@@ -12,7 +12,7 @@ const SAFE_DATA_FONT = /^data:(?:font\/(?:woff2?|opentype|truetype)|application\
1212 export const SVG_SCHEMA = '1'1313 export const EDITABLE_SVG_SCHEMA = '1'1414 export const NORMALIZER_BUILD = 'svg-normalizer-2'15-export const MATERIALIZER_BUILD = 'svg-materializer-3'15+export const MATERIALIZER_BUILD = 'svg-materializer-4'1616 export const PALETTE_BUILD = 'diagramzip-palette-2'1717 export const RAW_PROFILE = 'safe-raw-1'1818 export const APPEARANCES = Object.freeze([@@ -910,6 +910,10 @@ export function attachEditableDocument(source, document) {
910910 if (root.attributes.get('data-dz-schema') !== SVG_SCHEMA) {911911 throw new SvgNormalizationError(422, 'not_canonical', 'SVG does not use the current Diagram.zip canonical schema.')912912 }913+ const appearance = document?.diagram?.presentation?.appearance914+ if (typeof appearance === 'string' && APPEARANCES.includes(appearance)) {915+ root.attributes.set('data-dz-appearance', appearance)916+ }913917 root.children = root.children.filter(child => child.type === 'text'914918 || !(localName(child) === 'metadata' && child.attributes.get('data-dz-kind') === 'document'))915919 root.attributes.set('data-dz-document', EDITABLE_SVG_SCHEMA)@@ -1011,17 +1015,18 @@ function branchPaletteVariables(palette) {
10111015 return `--dz-accent-1:${palette.accent1};--dz-accent-2:${palette.accent2};--dz-accent-3:${palette.accent3};--dz-accent-4:${palette.accent4};--dz-on-accent:${palette.onAccent};`10121016 }10131017 1014-function materializerCss(appearance) {1015- const scheme = appearance.startsWith('dark-') ? DARK_PALETTE : LIGHT_PALETTE1016- const automatic = appearance.startsWith('auto-')1017- const variables = automatic1018- ? `:root{${paletteVariables(LIGHT_PALETTE)}}@media(prefers-color-scheme:dark){:root{${paletteVariables(DARK_PALETTE)}}}`1019- : `:root{${paletteVariables(scheme)}}`1020- const trnSelector = ':root[data-dz-profile="trn-semantic-4"]'1021- const trnVariables = automatic1022- ? `${trnSelector}{${branchPaletteVariables(TRN_LIGHT_BRANCH_PALETTE)}}@media(prefers-color-scheme:dark){${trnSelector}{${branchPaletteVariables(DARK_PALETTE)}}}`1023- : appearance.startsWith('light-') ? `${trnSelector}{${branchPaletteVariables(TRN_LIGHT_BRANCH_PALETTE)}}` : ''1024- return `${variables}${trnVariables}[data-dz-role="canvas"]:not([data-dz-owned="materializer"]){display:none!important}[data-dz-fill="none"]{fill:none!important}[data-dz-fill="surface-1"]{fill:var(--dz-surface-1)!important}[data-dz-fill="surface-2"]{fill:var(--dz-surface-2)!important}[data-dz-fill="surface-3"]{fill:var(--dz-surface-3)!important}[data-dz-fill="ink"]{fill:var(--dz-ink)!important;color:var(--dz-ink)!important}[data-dz-fill="ink-muted"]{fill:var(--dz-ink-muted)!important;color:var(--dz-ink-muted)!important}[data-dz-fill="line"]{fill:var(--dz-line)!important}[data-dz-fill="accent-1"]{fill:var(--dz-accent-1)!important}[data-dz-fill="accent-2"]{fill:var(--dz-accent-2)!important}[data-dz-fill="accent-3"]{fill:var(--dz-accent-3)!important}[data-dz-fill="accent-4"]{fill:var(--dz-accent-4)!important}[data-dz-fill="on-accent"]{fill:var(--dz-on-accent)!important;color:var(--dz-on-accent)!important}[data-dz-stroke="line"]{stroke:var(--dz-line)!important}[data-dz-stroke="line-muted"]{stroke:var(--dz-line-muted)!important}[data-dz-stroke="accent-1"]{stroke:var(--dz-accent-1)!important}[data-dz-stroke="accent-2"]{stroke:var(--dz-accent-2)!important}[data-dz-stroke="accent-3"]{stroke:var(--dz-accent-3)!important}[data-dz-stroke="accent-4"]{stroke:var(--dz-accent-4)!important}`1018+function materializerCss() {1019+ const light = ':root[data-dz-appearance^="light-"]'1020+ const dark = ':root[data-dz-appearance^="dark-"]'1021+ const automatic = ':root[data-dz-appearance^="auto-"]'1022+ const themed = ':root:not([data-dz-appearance="raw"])'1023+ const trnLight = ':root[data-dz-profile="trn-semantic-4"][data-dz-appearance^="light-"]'1024+ const trnAutomatic = ':root[data-dz-profile="trn-semantic-4"][data-dz-appearance^="auto-"]'1025+ const variables = `${light}{${paletteVariables(LIGHT_PALETTE)}}${dark}{${paletteVariables(DARK_PALETTE)}}${automatic}{${paletteVariables(LIGHT_PALETTE)}}@media(prefers-color-scheme:dark){${automatic}{${paletteVariables(DARK_PALETTE)}}}`1026+ const trnVariables = `${trnLight}{${branchPaletteVariables(TRN_LIGHT_BRANCH_PALETTE)}}${trnAutomatic}{${branchPaletteVariables(TRN_LIGHT_BRANCH_PALETTE)}}@media(prefers-color-scheme:dark){${trnAutomatic}{${branchPaletteVariables(DARK_PALETTE)}}}`1027+ const materializerVisibility = `:root[data-dz-appearance="raw"] [data-dz-owned="materializer"],:root[data-dz-appearance$="-transparent"] [data-dz-owned="materializer"]{display:none!important}`1028+ const roles = `${themed} [data-dz-role="canvas"]:not([data-dz-owned="materializer"]){display:none!important}${themed} [data-dz-fill="none"]{fill:none!important}${themed} [data-dz-fill="surface-1"]{fill:var(--dz-surface-1)!important}${themed} [data-dz-fill="surface-2"]{fill:var(--dz-surface-2)!important}${themed} [data-dz-fill="surface-3"]{fill:var(--dz-surface-3)!important}${themed} [data-dz-fill="ink"]{fill:var(--dz-ink)!important;color:var(--dz-ink)!important}${themed} [data-dz-fill="ink-muted"]{fill:var(--dz-ink-muted)!important;color:var(--dz-ink-muted)!important}${themed} [data-dz-fill="line"]{fill:var(--dz-line)!important}${themed} [data-dz-fill="accent-1"]{fill:var(--dz-accent-1)!important}${themed} [data-dz-fill="accent-2"]{fill:var(--dz-accent-2)!important}${themed} [data-dz-fill="accent-3"]{fill:var(--dz-accent-3)!important}${themed} [data-dz-fill="accent-4"]{fill:var(--dz-accent-4)!important}${themed} [data-dz-fill="on-accent"]{fill:var(--dz-on-accent)!important;color:var(--dz-on-accent)!important}${themed} [data-dz-stroke="line"]{stroke:var(--dz-line)!important}${themed} [data-dz-stroke="line-muted"]{stroke:var(--dz-line-muted)!important}${themed} [data-dz-stroke="accent-1"]{stroke:var(--dz-accent-1)!important}${themed} [data-dz-stroke="accent-2"]{stroke:var(--dz-accent-2)!important}${themed} [data-dz-stroke="accent-3"]{stroke:var(--dz-accent-3)!important}${themed} [data-dz-stroke="accent-4"]{stroke:var(--dz-accent-4)!important}`1029+ return `${variables}${trnVariables}${materializerVisibility}${roles}`10251030 }10261031 10271032 function canonicalBounds(root) {@@ -1058,7 +1063,7 @@ export function materializeSvg(canonical, appearance) {
10581063 root.attributes.set('data-dz-appearance', appearance)10591064 root.attributes.set('data-dz-palette', PALETTE_BUILD)10601065 root.attributes.set('data-dz-materializer', MATERIALIZER_BUILD)1061- root.children.unshift(element('style', [['data-dz-owned', 'materializer']], [{ type: 'text', value: materializerCss(appearance) }]))1066+ root.children.unshift(element('style', [['data-dz-owned', 'materializer']], [{ type: 'text', value: materializerCss() }]))10621067 if (appearance.endsWith('-framed')) {10631068 const [x, y, width, height] = bounds10641069 const padding = 24@@ -48,9 +48,16 @@ the user wants server persistence or a share alias.
4848 4949 For an image link without persistence, **Copy SVG URL** or **Copy as Markdown**5050 packs the editable SVG into a self-contained URL. Published open aliases use51-their stable render URL instead. Packed links expose the embedded source and52-metadata to anyone who has the URL; use a file or alias when the packed URL is53-too large, and do not suggest public embeds for locked diagrams.51+their stable render URL instead. For an anonymous packed link, the editor52+prepares transparent or framed geometry and embeds palette CSS selected by the53+root `data-dz-appearance`. The `/svg/{packed}` route synchronizes that single54+value from the embedded `diagram.presentation.appearance`. Verify those values55+agree; report a mismatch as a contract violation, and do not simulate the56+palette or patch SVG paint client-side. CSS does not change the outer `viewBox`,57+so changing between transparent and framed geometry requires a fresh export.58+Packed links expose the embedded source and metadata to anyone who has the URL;59+use a file or alias when the packed URL is too large, and do not suggest public60+embeds for locked diagrams.5461 5562 ## Prior art and standards5663