Compare commits

...
Author SHA1 Message Date
jarvis-at-skic 9347de298f fix: restore inline diagram controls and synchronize portal theme
Build and publish Eldorado documentation / build (pull_request) Successful in 43s
2026-10-05 12:18:45 +00:00
jarvis-at-skic 62d2afc152 Merge pull request 'docs: redesign AI strategy as a diagram-first story' (#15) from docs/strategy-visual-story into main
Build and publish Eldorado documentation / build (push) Successful in 38s
2026-10-04 05:25:56 -07:00
9 changed files with 267 additions and 6 deletions

No files matched your search

+4
View File
@@ -31,6 +31,10 @@ Top-level sections are Organization, Strategy, and Contribution, each with an in
Archify 3.0.1 is vendored under `vendor/archify/` from commit `d5a1333d7447c866a765adac7d4d062f2f02e4d2`. MIT and third-party notices are preserved. `diagrams/` holds editable JSON; `pnpm diagrams:build` validates and generates local standalone viewers before every site build. The iframe is sandboxed without same-origin permission. See `docs/contribution/diagrams.md` for capability analysis, authoring, boundaries, and upgrade procedure. Archify 3.0.1 is vendored under `vendor/archify/` from commit `d5a1333d7447c866a765adac7d4d062f2f02e4d2`. MIT and third-party notices are preserved. `diagrams/` holds editable JSON; `pnpm diagrams:build` validates and generates local standalone viewers before every site build. The iframe is sandboxed without same-origin permission. See `docs/contribution/diagrams.md` for capability analysis, authoring, boundaries, and upgrade procedure.
The build checks upstream provenance before applying the portal integration: `scripts/inline-viewer.css` restores compact embed controls, two guarded preset restrictions are removed from generated viewers only, and `scripts/viewer-theme-bridge.js` synchronizes theme with the portal through source-validated messages. Vendor files remain unchanged. Delivery receipts describe the upstream artifact before this integration, not the final served bytes. Theme changes do not reload viewers or discard visual presets. PNG, JPEG, WebP and SVG downloads remain available; browser-dependent exports stay disabled when unsupported.
For interaction acceptance, serve the production build on port 4173 and run `pnpm test:browser` with an installed Playwright Chromium (or `PLAYWRIGHT_CHROMIUM_EXECUTABLE`). This checks story layouts, portal/diagram theme synchronization, all four presets, actual exported artifact decoding, route changes, and fullscreen click/Escape/focus behavior at desktop and mobile widths. Screenshots, downloads and results are saved under ignored `.cache/` directories.
## Design ## Design
Inspired by https://www.eldorado.gg/: Lato, near-black navigation, charcoal, gold `#ffc950`, light panels, bold headings. Original documentation artwork and square panels; no official marketplace assets or business claims. See `docs/core/reference.md`. Inspired by https://www.eldorado.gg/: Lato, near-black navigation, charcoal, gold `#ffc950`, light panels, bold headings. Original documentation artwork and square panels; no official marketplace assets or business claims. See `docs/core/reference.md`.
+2 -1
View File
@@ -15,12 +15,13 @@
"verify:build": "node scripts/verify.mjs --build", "verify:build": "node scripts/verify.mjs --build",
"diagrams:build": "node scripts/build-diagrams.mjs", "diagrams:build": "node scripts/build-diagrams.mjs",
"test": "node --test scripts/*.test.mjs", "test": "node --test scripts/*.test.mjs",
"test:browser": "node scripts/browser/strategy-story.cjs", "test:browser": "node scripts/browser/strategy-story.cjs && node scripts/browser/inline-controls.cjs",
"kpi:example": "node scripts/ai-kpi.mjs" "kpi:example": "node scripts/ai-kpi.mjs"
}, },
"dependencies": { "dependencies": {
"@docusaurus/core": "3.10.2", "@docusaurus/core": "3.10.2",
"@docusaurus/preset-classic": "3.10.2", "@docusaurus/preset-classic": "3.10.2",
"@docusaurus/theme-common": "3.10.2",
"@easyops-cn/docusaurus-search-local": "0.55.3", "@easyops-cn/docusaurus-search-local": "0.55.3",
"@fontsource/lato": "5.2.6", "@fontsource/lato": "5.2.6",
"@mdx-js/react": "3.1.1", "@mdx-js/react": "3.1.1",
+3
View File
@@ -17,6 +17,9 @@ importers:
'@docusaurus/preset-classic': '@docusaurus/preset-classic':
specifier: 3.10.2 specifier: 3.10.2
version: 3.10.2(@algolia/client-search@5.59.0)(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@19.1.1))(@types/react@19.1.13)(postcss@8.5.28)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(search-insights@2.17.3)(typescript@5.9.3) version: 3.10.2(@algolia/client-search@5.59.0)(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@19.1.1))(@types/react@19.1.13)(postcss@8.5.28)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(search-insights@2.17.3)(typescript@5.9.3)
'@docusaurus/theme-common':
specifier: 3.10.2
version: 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@19.1.1))(postcss@8.5.28)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.3))(postcss@8.5.28)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@easyops-cn/docusaurus-search-local': '@easyops-cn/docusaurus-search-local':
specifier: 0.55.3 specifier: 0.55.3
version: 0.55.3(@docusaurus/theme-common@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@19.1.1))(postcss@8.5.28)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.3))(postcss@8.5.28)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@19.1.1))(postcss@8.5.28)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.3) version: 0.55.3(@docusaurus/theme-common@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@19.1.1))(postcss@8.5.28)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.3))(postcss@8.5.28)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mdx-js/react@3.1.1(@types/react@19.1.13)(react@19.1.1))(postcss@8.5.28)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.3)
+117
View File
@@ -0,0 +1,117 @@
const {chromium} = require('playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
(async () => {
const base = process.argv[2] || 'http://127.0.0.1:4173';
const output = `.cache/inline-${process.argv[3] || 'local'}`;
fs.mkdirSync(output, {recursive: true});
const browser = await chromium.launch({headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE, args: ['--no-sandbox']});
const results = [];
try {
for (const width of [1440, 390, 320]) {
const context = await browser.newContext({viewport: {width, height: 1000}, colorScheme: 'light'});
const page = await context.newPage();
await page.goto(`${base}/eldorado-v1/documentation/strategy/`, {waitUntil: 'networkidle'});
const panel = page.locator('.archify-panel--story');
const iframe = panel.locator(width > 700 ? '.story-desktop' : '.story-mobile');
await iframe.scrollIntoViewIfNeeded();
const frame = await (await iframe.elementHandle()).contentFrame();
const theme = async value => {
await page.waitForFunction(t => document.documentElement.dataset.theme === t, value);
await frame.waitForFunction(t => document.documentElement.dataset.theme === t, value);
};
await theme('light');
for (const id of ['btn-theme', 'btn-preset', 'btn-export']) {
const box = await frame.locator(`#${id}`).boundingBox();
assert(box && box.width >= 40 && box.height >= 44, id);
}
const light = await frame.locator('body').evaluate(n => getComputedStyle(n).backgroundColor);
await frame.locator('#btn-theme').click(); await theme('dark');
assert.notEqual(await frame.locator('body').evaluate(n => getComputedStyle(n).backgroundColor), light);
await frame.locator('#btn-theme').click(); await theme('light');
// Mobile portal theme control lives in the navigation drawer.
const portalTheme = async target => {
if (width <= 700) await page.getByRole('button', {name: 'Toggle navigation bar'}).click();
for (let attempt = 0; attempt < 3; attempt++) {
await page.getByRole('button', {name: /Switch between dark and light mode/}).filter({visible: true}).click();
if (await page.locator('html').getAttribute('data-theme') === target) break;
}
if (width <= 700) await page.getByRole('button', {name: 'Close navigation bar'}).click();
};
await portalTheme('dark'); await theme('dark');
await portalTheme('light'); await theme('light');
for (const preset of ['signal-flow', 'blueprint', 'editorial', 'classic']) {
await frame.locator('#btn-preset').click();
await frame.locator(`[data-preset-value="${preset}"]`).click();
assert.equal(await frame.locator('html').getAttribute('data-preset'), preset);
}
// A theme change must not reload the iframe or lose the selected visual preset.
await frame.locator('#btn-preset').click();
await frame.locator('[data-preset-value="blueprint"]').click();
await portalTheme('dark'); await theme('dark');
assert.equal(await frame.locator('html').getAttribute('data-preset'), 'blueprint');
const exports = [];
for (const format of ['png', 'jpeg', 'webp', 'svg', 'svg-light', 'svg-dark', 'webm']) {
await frame.locator('#btn-export').click();
if (format === 'webm' && await frame.locator('[data-format="webm"]').isDisabled()) {
exports.push({format, unsupported: true});
await frame.locator('#btn-export').click();
continue;
}
console.log(`Export ${width} ${format}`);
const [download] = await Promise.all([
page.waitForEvent('download', {timeout: 60000}),
frame.locator(`[data-format="${format}"]`).click(),
]);
assert.equal(await download.failure(), null);
const path = `${output}/${width}-${format}-${download.suggestedFilename()}`;
await download.saveAs(path);
const bytes = fs.readFileSync(path);
assert(bytes.length > 1000);
if (format.startsWith('svg')) {
const valid = await page.evaluate(xml => {
const doc = new DOMParser().parseFromString(xml, 'image/svg+xml');
return !doc.querySelector('parsererror') && doc.documentElement.localName === 'svg' && doc.querySelectorAll('text').length > 0;
}, bytes.toString());
assert(valid, format);
} else {
const mime = {png:'image/png', jpeg:'image/jpeg', webp:'image/webp', webm:'video/webm'}[format];
const dimensions = await page.evaluate(async ({data, mime}) => {
const media = document.createElement(mime.startsWith('video') ? 'video' : 'img');
const loaded = new Promise((resolve, reject) => {media[mime.startsWith('video') ? 'onloadedmetadata' : 'onload'] = resolve; media.onerror = reject;});
media.src = `data:${mime};base64,${data}`; await loaded;
return [media.videoWidth || media.naturalWidth, media.videoHeight || media.naturalHeight];
}, {data: bytes.toString('base64'), mime});
assert(dimensions.every(n => n > 0), format);
}
exports.push({format, path, bytes:bytes.length});
}
const opener = panel.getByRole('button', {name:/^Expand /});
await opener.click();
let full = await (await panel.locator('dialog iframe').elementHandle()).contentFrame();
await full.waitForFunction(() => document.documentElement.dataset.theme === 'dark');
await full.locator('#btn-theme').click(); await theme('light');
const close = panel.getByRole('button', {name:'Close full-screen diagram'});
assert.equal(await close.innerText(), '×');
await close.click(); assert(await opener.evaluate(n => n === document.activeElement));
await opener.click();
full = await (await panel.locator('dialog iframe').elementHandle()).contentFrame();
await full.waitForFunction(() => document.documentElement.dataset.theme === 'light');
await close.focus(); await page.keyboard.press('Escape');
assert(await opener.evaluate(n => n === document.activeElement));
await iframe.scrollIntoViewIfNeeded();
await page.evaluate(() => scrollTo(0, 0));
await page.screenshot({path:`${output}/${width}.png`, fullPage:true});
await frame.locator('#btn-theme').click(); await theme('dark');
await page.locator('.strategy-content a[href$="/strategy/target-operating-model/"]').filter({visible:true}).click();
const next = page.locator(`.archify-panel--story[data-diagram="strategy-target"] > .story-${width > 700 ? 'desktop' : 'mobile'}`);
await next.scrollIntoViewIfNeeded();
await (await (await next.elementHandle()).contentFrame()).waitForFunction(() => document.documentElement.dataset.theme === 'dark');
assert(!await page.evaluate(() => document.documentElement.scrollWidth > innerWidth));
results.push({width, themeSync:true, presets:4, exports, fullscreen:true, routeTheme:true});
fs.writeFileSync(`${output}/results.json`, JSON.stringify(results,null,2));
await context.close();
}
console.log(`PASS ${results.length} viewports: bidirectional themes, 4 presets, all supported exports decoded, fullscreen and route persistence`);
} finally {await browser.close();}
})().catch(error => {console.error(error); process.exitCode = 1;});
+16 -1
View File
@@ -1,8 +1,10 @@
import {readdirSync, readFileSync, mkdirSync, copyFileSync, renameSync, rmSync} from 'node:fs'; import {readdirSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, renameSync, rmSync} from 'node:fs';
import {join} from 'node:path'; import {join} from 'node:path';
import {execFileSync} from 'node:child_process'; import {execFileSync} from 'node:child_process';
const cli = 'vendor/archify/bin/archify.mjs'; const cli = 'vendor/archify/bin/archify.mjs';
const types = new Set(['architecture','workflow','sequence','dataflow','lifecycle']); const types = new Set(['architecture','workflow','sequence','dataflow','lifecycle']);
const inlineStyles = readFileSync(new URL('./inline-viewer.css', import.meta.url), 'utf8');
const themeBridge = readFileSync(new URL('./viewer-theme-bridge.js', import.meta.url), 'utf8');
// This directory is generated-only and ignored by Git. Remove stale artifacts. // This directory is generated-only and ignored by Git. Remove stale artifacts.
rmSync('static/diagrams', {recursive: true, force: true}); rmSync('static/diagrams', {recursive: true, force: true});
mkdirSync('static/diagrams', {recursive: true}); mkdirSync('static/diagrams', {recursive: true});
@@ -23,6 +25,19 @@ for (const file of readdirSync('diagrams').sort()) {
run(['validate',type,input,'--quality','showcase','--json']); run(['validate',type,input,'--quality','showcase','--json']);
run(['deliver',type,input,output,'--quality','showcase','--json']); run(['deliver',type,input,output,'--quality','showcase','--json']);
run(['check',output,'--json','--require-provenance']); run(['check',output,'--json','--require-provenance']);
// Verify the upstream artifact first, then add the scoped portal integration.
let viewer = readFileSync(output, 'utf8');
// Upstream intentionally disables preset selection in gallery embeds. Enable
// only those two guards; fail closed if a future vendor update changes them.
const start = viewer.indexOf('Archify.preset = (function () {');
const end = viewer.indexOf('Archify.theme = (function () {', start);
const preset = viewer.slice(start, end);
const guard = "if (html.getAttribute('data-embed') === 'true') return false;";
if (start < 0 || end < start || preset.split(guard).length !== 3) throw new Error('Archify preset integration drift');
viewer = viewer.slice(0, start) + preset.replaceAll(guard, '') + viewer.slice(end);
if (!viewer.includes('</head>') || !viewer.includes('</body>')) throw new Error(`Invalid viewer: ${output}`);
writeFileSync(output, viewer.replace('</head>', `<style id="eldorado-inline-controls">${inlineStyles}</style></head>`)
.replace('</body>', `<script>${themeBridge}</script></body>`));
renameSync(`static/diagrams/${name}/index.delivery.json`, `.cache/archify/${name}.delivery.json`); renameSync(`static/diagrams/${name}/index.delivery.json`, `.cache/archify/${name}.delivery.json`);
} }
copyFileSync('vendor/archify/LICENSE','static/diagrams/ARCHIFY-LICENSE.txt'); copyFileSync('vendor/archify/LICENSE','static/diagrams/ARCHIFY-LICENSE.txt');
+32
View File
@@ -0,0 +1,32 @@
/* Portal integration only; keep the upstream renderer and export code pinned. */
html[data-embed="true"] .toolbar {
display: flex !important;
position: relative;
inset: auto;
justify-content: center;
height: 52px;
padding: 4px;
box-sizing: border-box;
background: var(--bg);
}
html[data-embed="true"] #btn-present,
html[data-embed="true"] #btn-motion { display: none !important; }
html[data-embed="true"] .diagram-container > svg {
max-height: calc(100vh - 68px);
}
html[data-embed="true"] .toolbar .preset-menu,
html[data-embed="true"] .toolbar .export-menu {
position: absolute;
top: 100%;
left: 0;
right: auto;
width: min(320px, 100vw);
max-width: 100vw;
max-height: calc(100vh - 52px);
overflow-y: auto;
box-sizing: border-box;
}
html[data-embed="true"] .toolbar .export-wrap { position: static; }
html[data-embed="true"] .toolbar .export-menu { left: auto; right: 0; }
html[data-embed="true"] .toolbar .preset-menu button,
html[data-embed="true"] .toolbar .export-menu button { min-height: 44px; }
+51
View File
@@ -0,0 +1,51 @@
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {runInNewContext} from 'node:vm';
const read = path => readFileSync(path, 'utf8');
test('both story layouts permit downloads without weakening origin isolation', () => {
const wrapper = read('src/components/ArchifyDiagram/index.tsx');
for (const layout of ['desktop', 'mobile']) {
const iframe = wrapper.split(`<iframe className="story-${layout}"`)[1].split(' />')[0];
assert(iframe.includes('sandbox="allow-scripts allow-downloads"'));
assert(iframe.includes('?embed=1&theme=${initialTheme.current}'));
}
assert(!wrapper.includes('allow-same-origin'));
});
test('build adds maintained embed-only controls without changing pinned vendor', () => {
const build = read('scripts/build-diagrams.mjs');
assert(build.includes("new URL('./inline-viewer.css', import.meta.url)"));
assert(build.includes('eldorado-inline-controls'));
const css = read('scripts/inline-viewer.css');
assert(css.includes('html[data-embed="true"] .toolbar'));
assert(css.includes('display: flex !important'));
assert(css.includes('position: relative'));
assert(css.includes('max-height: calc(100vh - 52px)'));
});
test('theme bridge rejects unrelated senders/values and never echoes parent updates', () => {
const root = {dataset:{theme:'light'}};
let receive, observerCallback, mutations = 0;
const messages = [];
const parent = {postMessage: message => messages.push(message)};
const window = {parent, addEventListener: (type, callback) => {receive = callback;}};
const button = {click: () => {root.dataset.theme = root.dataset.theme === 'light' ? 'dark' : 'light'; mutations++;}};
class MutationObserver {
constructor(callback) {observerCallback = callback;}
observe() {}
takeRecords() {mutations = 0;}
}
runInNewContext(read('scripts/viewer-theme-bridge.js'), {window, document:{documentElement:root, getElementById: () => button}, MutationObserver});
const send = (source, theme) => receive({source, data:{type:'eldorado:set-theme', theme}});
send({}, 'dark'); send(parent, 'invalid');
assert.equal(root.dataset.theme, 'light');
send(parent, 'dark');
assert.equal(root.dataset.theme, 'dark');
assert.equal(mutations, 0);
assert.equal(messages.length, 0);
button.click(); observerCallback();
assert.equal(messages.length, 1);
assert.equal(messages[0].theme, 'light');
});
+21
View File
@@ -0,0 +1,21 @@
// Sandboxed viewers have opaque origins: authenticate the sender by WindowProxy,
// not origin. Only exchange a validated theme, never HTML, URLs or commands.
(() => {
if (window.parent === window) return;
let connected = false;
window.addEventListener('message', event => {
if (event.source !== window.parent || event.data?.type !== 'eldorado:set-theme') return;
const theme = event.data.theme;
if (theme !== 'light' && theme !== 'dark') return;
connected = true;
if (document.documentElement.dataset.theme !== theme) {
document.getElementById('btn-theme').click();
// Parent updates are not new user choices; do not echo stale acknowledgements.
observer.takeRecords();
}
});
const observer = new MutationObserver(() => {
if (connected) window.parent.postMessage({type: 'eldorado:theme', theme: document.documentElement.dataset.theme}, '*');
});
observer.observe(document.documentElement, {attributes: true, attributeFilter: ['data-theme']});
})();
+21 -4
View File
@@ -1,6 +1,7 @@
import React, {useEffect, useId, useRef, useState} from 'react'; import React, {useEffect, useId, useRef, useState} from 'react';
import useBaseUrl from '@docusaurus/useBaseUrl'; import useBaseUrl from '@docusaurus/useBaseUrl';
import {useLocation} from '@docusaurus/router'; import {useLocation} from '@docusaurus/router';
import {useColorMode} from '@docusaurus/theme-common';
export default function ArchifyDiagram({name, title, compact = false, story = false}: {name: string; title: string; compact?: boolean; story?: boolean}): React.ReactElement { export default function ArchifyDiagram({name, title, compact = false, story = false}: {name: string; title: string; compact?: boolean; story?: boolean}): React.ReactElement {
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) throw new Error('Invalid diagram name'); if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) throw new Error('Invalid diagram name');
@@ -10,6 +11,22 @@ export default function ArchifyDiagram({name, title, compact = false, story = fa
const opener = useRef<HTMLButtonElement>(null); const opener = useRef<HTMLButtonElement>(null);
const headingId = useId(); const headingId = useId();
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const {colorMode, setColorMode} = useColorMode();
const initialTheme = useRef(colorMode);
const desktop = useRef<HTMLIFrameElement>(null);
const mobile = useRef<HTMLIFrameElement>(null);
const fullscreen = useRef<HTMLIFrameElement>(null);
const syncTheme = (frame: HTMLIFrameElement | null) => frame?.contentWindow?.postMessage({type: 'eldorado:set-theme', theme: colorMode}, '*');
useEffect(() => {
for (const frame of [desktop, mobile, fullscreen]) syncTheme(frame.current);
const receiveTheme = (event: MessageEvent) => {
if (!event.source || ![desktop, mobile, fullscreen].some(frame => frame.current?.contentWindow === event.source)) return;
if (event.data?.type !== 'eldorado:theme' || !['light', 'dark'].includes(event.data.theme)) return;
if (event.data.theme !== colorMode) setColorMode(event.data.theme);
};
window.addEventListener('message', receiveTheme);
return () => window.removeEventListener('message', receiveTheme);
}, [colorMode, setColorMode, expanded]);
const {pathname, hash} = useLocation(); const {pathname, hash} = useLocation();
useEffect(() => { useEffect(() => {
if (!story || !hash) return; if (!story || !hash) return;
@@ -43,12 +60,12 @@ export default function ArchifyDiagram({name, title, compact = false, story = fa
return <section className={`archify-panel${compact ? ' archify-panel--compact' : ''}${story ? ' archify-panel--story' : ''}`} data-diagram={name} aria-label={title}> return <section className={`archify-panel${compact ? ' archify-panel--compact' : ''}${story ? ' archify-panel--story' : ''}`} data-diagram={name} aria-label={title}>
<div className="archify-toolbar"><button ref={opener} type="button" className="archify-expand" aria-label={`Expand ${title}`} title="Expand diagram" onClick={() => setExpanded(true)}><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"><path d="M8 3H3v5M16 3h5v5M21 16v5h-5M8 21H3v-5" /></svg></button></div> <div className="archify-toolbar"><button ref={opener} type="button" className="archify-expand" aria-label={`Expand ${title}`} title="Expand diagram" onClick={() => setExpanded(true)}><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"><path d="M8 3H3v5M16 3h5v5M21 16v5h-5M8 21H3v-5" /></svg></button></div>
{story ? <> {story ? <>
<iframe className="story-desktop" src={`${source}?embed=1&theme=light`} title={title} sandbox="allow-scripts" loading="lazy" referrerPolicy="no-referrer" /> <iframe className="story-desktop" ref={desktop} onLoad={() => syncTheme(desktop.current)} src={`${source}?embed=1&theme=${initialTheme.current}`} title={title} sandbox="allow-scripts allow-downloads" loading="lazy" referrerPolicy="no-referrer" />
<iframe className="story-mobile" src={`${mobileSource}?embed=1&theme=light`} title={`${title} — narrow layout`} sandbox="allow-scripts" loading="lazy" referrerPolicy="no-referrer" /> <iframe className="story-mobile" ref={mobile} onLoad={() => syncTheme(mobile.current)} src={`${mobileSource}?embed=1&theme=${initialTheme.current}`} title={`${title} — narrow layout`} sandbox="allow-scripts allow-downloads" loading="lazy" referrerPolicy="no-referrer" />
</> : <iframe src={source} title={title} sandbox="allow-scripts allow-downloads" loading="lazy" referrerPolicy="no-referrer" />} </> : <iframe ref={desktop} onLoad={() => syncTheme(desktop.current)} src={`${source}?theme=${initialTheme.current}`} title={title} sandbox="allow-scripts allow-downloads" loading="lazy" referrerPolicy="no-referrer" />}
<dialog ref={dialog} className="archify-modal" aria-labelledby={headingId} onClose={() => {setExpanded(false); opener.current?.focus();}}> <dialog ref={dialog} className="archify-modal" aria-labelledby={headingId} onClose={() => {setExpanded(false); opener.current?.focus();}}>
<div className="archify-modal-toolbar"><strong id={headingId}>{title}</strong><button type="button" onClick={close} autoFocus aria-label="Close full-screen diagram"><span aria-hidden="true">×</span></button></div> <div className="archify-modal-toolbar"><strong id={headingId}>{title}</strong><button type="button" onClick={close} autoFocus aria-label="Close full-screen diagram"><span aria-hidden="true">×</span></button></div>
{expanded && <iframe src={source} title={`${title} — expanded`} sandbox="allow-scripts allow-downloads" referrerPolicy="no-referrer" />} {expanded && <iframe ref={fullscreen} onLoad={() => syncTheme(fullscreen.current)} src={`${source}?theme=${initialTheme.current}`} title={`${title} — expanded`} sandbox="allow-scripts allow-downloads" referrerPolicy="no-referrer" />}
</dialog> </dialog>
</section>; </section>;
} }