diff --git a/docs/contribution/diagrams.md b/docs/contribution/diagrams.md
index a9c6fd2..32af7fe 100644
--- a/docs/contribution/diagrams.md
+++ b/docs/contribution/diagrams.md
@@ -37,7 +37,7 @@ import ArchifyDiagram from '@site/src/components/ArchifyDiagram';
```
-Supply a text alternative below each diagram. The wrapper uses a local, base-path-aware iframe with a descriptive title, scripts and downloads enabled, and no same-origin or parent-navigation permissions. A full-viewer link provides a wider canvas in a new tab; that direct page is not iframe-sandboxed. Only publish reviewed, trusted diagrams, and do not describe iframe isolation as protection for the standalone route. Generated output is ignored by Git and rebuilt in CI.
+Supply a text alternative below each diagram. The wrapper uses a local, base-path-aware iframe with a descriptive title, scripts and downloads enabled, and no same-origin or parent-navigation permissions. The Open full viewer button expands the diagram in a full-screen modal on the current page, with the same iframe sandbox. Close it with the Close button or Escape; focus returns to the opener. It does not open a new tab. The separate standalone HTML asset remains directly addressable and is not itself iframe-sandboxed, so publish only reviewed, trusted diagrams. Generated output is ignored by Git and rebuilt in CI.
## Integration boundaries
diff --git a/scripts/portal.test.mjs b/scripts/portal.test.mjs
index cf4cbe7..91f922c 100644
--- a/scripts/portal.test.mjs
+++ b/scripts/portal.test.mjs
@@ -55,6 +55,9 @@ test('Archify is pinned, licensed and rebuilt, not a remote embed', () => {
assert(wrapper.includes('sandbox="allow-scripts allow-downloads"'));
assert(!wrapper.includes('allow-same-origin'));
assert(wrapper.includes('useBaseUrl'));
+ assert(wrapper.includes('showModal()') && wrapper.includes('onClose='));
+ assert(!wrapper.includes('target="_blank"'));
+ assert(wrapper.includes('Close full-screen diagram'));
});
test('example is explicitly illustrative and contains valid connected nodes', () => {
const spec = JSON.parse(read('diagrams/organization-structure.architecture.json'));
diff --git a/src/components/ArchifyDiagram/index.tsx b/src/components/ArchifyDiagram/index.tsx
index 5e4c81c..10a67c2 100644
--- a/src/components/ArchifyDiagram/index.tsx
+++ b/src/components/ArchifyDiagram/index.tsx
@@ -1,12 +1,28 @@
-import React from 'react';
+import React, {useEffect, useId, useRef, useState} from 'react';
import useBaseUrl from '@docusaurus/useBaseUrl';
export default function ArchifyDiagram({name, title}: {name: string; title: string}): React.ReactElement {
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) throw new Error('Invalid diagram name');
const source = useBaseUrl(`/diagrams/${name}/`);
const json = useBaseUrl(`/diagrams/${name}.json`);
+ const dialog = useRef(null);
+ const opener = useRef(null);
+ const headingId = useId();
+ const [expanded, setExpanded] = useState(false);
+ useEffect(() => {
+ if (!expanded) return;
+ dialog.current?.showModal();
+ const previousOverflow = document.body.style.overflow;
+ document.body.style.overflow = 'hidden';
+ return () => {document.body.style.overflow = previousOverflow;};
+ }, [expanded]);
+ const close = () => {dialog.current?.close();};
return
-