Replace the 3D menu with a 2D | 3D switch; 3D controls in the dock; 0.6.1

The toolbar button next to Export is now a two-state switch: both
labels are visible and the filled half is the current view, so a
diagram that opens in 2D shows "2D" and one click flips it to 3D. The
dropdown is gone; nothing is greyed out and no menu stays open after a
choice.

The 3D-only controls moved to Archify's bottom-right dock beside PATH
and LENS: STACK (levels by kind, on or off), TOP (from above), and the
zoom stepper's middle button reads RESET while 3D is on, instead of a
2D zoom level that meant nothing there.
This commit is contained in:
2026-10-12 17:35:38 +03:00
parent fb7d7706aa
commit c71d2e40b9
7 changed files with 112 additions and 208 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog # Changelog
## 0.6.1 — 2026-10-12
- The 3D menu is replaced by a **2D | 3D** switch next to **Export**: the filled half is the view you are in, one click flips it.
- The 3D-only controls moved to the bottom-right dock next to **PATH** and **LENS**: **STACK** (levels by kind, on or off), **TOP** (from above), and the zoom stepper's middle button reads **RESET** in 3D instead of a 2D zoom level.
- Nothing is greyed out any more, and no menu stays open over the diagram after a choice.
## 0.6.0 — 2026-10-11 ## 0.6.0 — 2026-10-11
- **3D view:** a **3D ▾** menu in the diagram toolbar, styled like Archify's own menus, switches any diagram to 3D. Nodes become raised blocks topped with the node as Archify draws it, relationships become lit pipes along Archify's routes, and the canvas, frames, lanes and legend form the floor. Works for all five diagram kinds, in every theme and style. - **3D view:** a **3D ▾** menu in the diagram toolbar, styled like Archify's own menus, switches any diagram to 3D. Nodes become raised blocks topped with the node as Archify draws it, relationships become lit pipes along Archify's routes, and the canvas, frames, lanes and legend form the floor. Works for all five diagram kinds, in every theme and style.
+6 -6
View File
@@ -72,21 +72,21 @@ The Archify CLI and the AI agents that use it write diagrams as JSON, named `nam
## 3D view ## 3D view
Every diagram can also be seen in 3D. Open the **3D ▾** menu next to **Export** in the diagram's toolbar and choose **3D**; choose **2D** to go back. The button wears a cyan ring while the 3D view is on. Every diagram can also be seen in 3D. The **2D | 3D** switch next to **Export** in the diagram's toolbar shows which view you are in; click it to flip.
![The same diagram in the 3D view](docs/preview-3d.png) ![The same diagram in the 3D view](docs/preview-3d.png)
The 3D view is built from the diagram Archify draws, so it looks the same: each node becomes a raised block whose top is the node as Archify draws it, relationships become pipes along Archify's routes with their colours, dashes and arrowheads, and the canvas, frames, lanes and legend form the floor. It works for all five diagram kinds, in every theme and style (Classic, Signal Flow, Blueprint, Editorial). The 3D view is built from the diagram Archify draws, so it looks the same: each node becomes a raised block whose top is the node as Archify draws it, relationships become pipes along Archify's routes with their colours, dashes and arrowheads, and the canvas, frames, lanes and legend form the floor. It works for all five diagram kinds, in every theme and style (Classic, Signal Flow, Blueprint, Editorial).
The menu has three groups: While 3D is on, three controls join the bottom-right dock next to **PATH** and **LENS**:
- **View**: 2D or 3D. - **STACK** lifts nodes into levels by kind: clients and external systems on top, security below them, services in the middle, data on the floor. Click again for the flat layout.
- **Levels**: *Flat* keeps every node on the floor; *Stacked* lifts nodes into levels by kind: clients and external systems on top, security below them, services in the middle, data on the floor. - **TOP** looks straight down, like 2D.
- **Camera**: *Reset view* (or <kbd>0</kbd>) flies back to the whole diagram, wherever you have moved to; *From above* looks straight down, like 2D. - **RESET** (the middle of the zoom stepper, or <kbd>0</kbd>) flies back to the whole diagram, wherever you have moved to.
Moving around: Moving around:
- Drag to turn, right-drag to move. On a trackpad, scroll with two fingers to move in any direction and pinch to zoom; a mouse wheel zooms. The diagram's zoom buttons and the <kbd>+</kbd> and <kbd>-</kbd> keys work too, beyond the 2D limits. - Drag to turn, right-drag to move. On a trackpad, scroll with two fingers to move in any direction and pinch to zoom; a mouse wheel zooms. The dock's **−** and **+** and the <kbd>-</kbd> and <kbd>+</kbd> keys work too, beyond the 2D limits.
- Click a block to focus it, as in 2D: the Semantic Passport opens and unrelated parts fade. Hover a block to see its one-hop trace. Double-click a block to fly to it, or empty space to show everything. - Click a block to focus it, as in 2D: the Semantic Passport opens and unrelated parts fade. Hover a block to see its one-hop trace. Double-click a block to fly to it, or empty space to show everything.
Animation follows 2D exactly, because the 3D view reads Archify's own animations as they play: the trace (relationships drawing in, nodes pulsing) when the diagram opens or 3D is chosen, the hover trace, and the flows from the Semantic Passport, Path and Lens. *Live* off, or reduced motion, keeps it still. Animation follows 2D exactly, because the 3D view reads Archify's own animations as they play: the trace (relationships drawing in, nodes pulsing) when the diagram opens or 3D is chosen, the hover trace, and the flows from the Semantic Passport, Path and Lens. *Live* off, or reduced motion, keeps it still.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 228 KiB

+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "archify-vscode-ext", "name": "archify-vscode-ext",
"version": "0.6.0", "version": "0.6.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "archify-vscode-ext", "name": "archify-vscode-ext",
"version": "0.6.0", "version": "0.6.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"three": "^0.186.1", "three": "^0.186.1",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "archify-vscode-ext", "name": "archify-vscode-ext",
"displayName": "Archify Diagram Viewer", "displayName": "Archify Diagram Viewer",
"description": "Archify diagrams in VS Code. Write diagrams as .archify files in YAML: they open as the interactive diagram, with live updates, validation, templates and completion. Also previews Archify's own JSON files (*.architecture.json, …) and opens rendered Archify HTML.", "description": "Archify diagrams in VS Code. Write diagrams as .archify files in YAML: they open as the interactive diagram, with live updates, validation, templates and completion. Also previews Archify's own JSON files (*.architecture.json, …) and opens rendered Archify HTML.",
"version": "0.6.0", "version": "0.6.1",
"publisher": "root-at-skic", "publisher": "root-at-skic",
"license": "MIT", "license": "MIT",
"icon": "media/icon.png", "icon": "media/icon.png",
+69 -167
View File
@@ -4,9 +4,9 @@ import { DiagramModel, readModel } from './svgModel';
import { STYLE } from './style'; import { STYLE } from './style';
/** /**
* 3D mode for the Archify viewer. It adds a 3D menu to Archify's toolbar * 3D mode for the Archify viewer. It adds a 2D | 3D switch to Archify's
* and, when on, replaces the 2D canvas with a three.js stage built from the * toolbar and, when on, replaces the 2D canvas with a three.js stage built
* live SVG. Archify keeps owning the diagram: clicks in 3D go to the SVG * from the live SVG. Archify keeps owning the diagram: clicks in 3D go to the SVG
* nodes, so focus, Semantic Passport, reach and lens work as in 2D; the * nodes, so focus, Semantic Passport, reach and lens work as in 2D; the
* stage redraws whenever Archify changes the SVG, theme or preset, and * stage redraws whenever Archify changes the SVG, theme or preset, and
* follows Archify's animations as they play. * follows Archify's animations as they play.
@@ -36,41 +36,6 @@ const MARGIN = 80;
/** Texture pixels per SVG unit; more costs memory and upload time for little gain. */ /** Texture pixels per SVG unit; more costs memory and upload time for little gain. */
const TEXTURE_SCALE = 2; const TEXTURE_SCALE = 2;
type MenuValue = '2d' | '3d' | 'flat' | 'stacked' | 'reset' | 'top';
interface MenuEntry {
value: MenuValue;
title: string;
hint: string;
/** Radio group; actions have none. */
group?: 'view' | 'levels';
shortcut?: string;
}
const MENU: { heading: string; entries: MenuEntry[] }[] = [
{
heading: 'View',
entries: [
{ value: '2d', group: 'view', title: '2D', hint: 'The diagram as Archify draws it' },
{ value: '3d', group: 'view', title: '3D', hint: 'Blocks and pipes you can turn' },
],
},
{
heading: 'Levels',
entries: [
{ value: 'flat', group: 'levels', title: 'Flat', hint: 'Every node on the floor' },
{ value: 'stacked', group: 'levels', title: 'Stacked', hint: 'Clients on top, data at the bottom' },
],
},
{
heading: 'Camera',
entries: [
{ value: 'reset', title: 'Reset view', hint: 'Show the whole diagram', shortcut: '0' },
{ value: 'top', title: 'From above', hint: 'Look straight down, like 2D' },
],
},
];
class Viewer3D { class Viewer3D {
private stage: Stage | undefined; private stage: Stage | undefined;
@@ -83,8 +48,10 @@ class Viewer3D {
/** Keeps Archify's zoom buttons usable in 3D (see holdZoomButtons). */ /** Keeps Archify's zoom buttons usable in 3D (see holdZoomButtons). */
private readonly zoomObserver = new MutationObserver(() => this.enableZoomButtons()); private readonly zoomObserver = new MutationObserver(() => this.enableZoomButtons());
private zoomDisabled = new Map<HTMLButtonElement, boolean>(); private zoomDisabled = new Map<HTMLButtonElement, boolean>();
private readonly button: HTMLButtonElement; private readonly toggle: HTMLButtonElement;
private readonly menu: HTMLDivElement; private readonly stackButton: HTMLButtonElement;
private readonly topButton: HTMLButtonElement;
private readonly resetLabel: HTMLSpanElement;
private ambientReplayed = false; private ambientReplayed = false;
private exportItem: HTMLButtonElement | undefined; private exportItem: HTMLButtonElement | undefined;
private saved: SavedView; private saved: SavedView;
@@ -95,145 +62,79 @@ class Viewer3D {
private readonly svg: SVGSVGElement, private readonly svg: SVGSVGElement,
) { ) {
this.saved = (host.getState().view3d as SavedView | undefined) ?? {}; this.saved = (host.getState().view3d as SavedView | undefined) ?? {};
({ button: this.button, menu: this.menu } = this.buildMenu()); this.toggle = this.buildSwitch();
({ stack: this.stackButton, top: this.topButton, reset: this.resetLabel } = this.buildNavControls());
this.addExportItem(); this.addExportItem();
this.interceptViewerControls(); this.interceptViewerControls();
if (this.saved.active ?? host.defaultView === '3d') this.setActive(true); if (this.saved.active ?? host.defaultView === '3d') this.setActive(true);
else this.syncButtons();
} }
// -------------------------------------------------------------- chrome // -------------------------------------------------------------- chrome
/** /**
* The 3D menu: a toolbar button with a dropdown, built from Archify's own * The 2D | 3D switch: Export's twin in the toolbar. Both states are always
* style-menu classes so it looks and behaves like the menus beside it. * visible, the filled half is the current one, a click flips it.
*/ */
private buildMenu(): { button: HTMLButtonElement; menu: HTMLDivElement } { private buildSwitch(): HTMLButtonElement {
const wrap = document.createElement('div');
wrap.className = 'preset-wrap archify-3d-wrap';
const button = document.createElement('button'); const button = document.createElement('button');
button.id = 'btn-3d'; button.id = 'btn-3d';
button.type = 'button'; button.type = 'button';
button.title = '3D view: switch 2D / 3D, levels and camera'; button.setAttribute('role', 'switch');
button.setAttribute('aria-label', '3D view options'); button.setAttribute('aria-checked', 'false');
button.setAttribute('aria-haspopup', 'menu'); button.innerHTML =
button.setAttribute('aria-expanded', 'false'); '<span class="archify-3d-seg" data-seg="2d" aria-hidden="true">2D</span>' +
button.setAttribute('aria-controls', 'archify-3d-menu'); '<span class="archify-3d-seg" data-seg="3d" aria-hidden="true">3D</span>';
// Same make-up as Archify's Export button: a label and a chevron.
button.innerHTML = '3D <span class="toolbar-icon toolbar-chevron" aria-hidden="true"></span>';
const menu = document.createElement('div');
menu.id = 'archify-3d-menu';
menu.className = 'preset-menu archify-3d-menu';
menu.setAttribute('role', 'menu');
menu.setAttribute('aria-label', 'View options');
for (const section of MENU) {
const heading = document.createElement('div');
heading.className = 'preset-menu-heading';
heading.setAttribute('role', 'presentation');
heading.innerHTML = `<span>${section.heading}</span>`;
menu.appendChild(heading);
for (const entry of section.entries) {
const option = document.createElement('button');
option.type = 'button';
option.className = 'preset-option archify-3d-option';
option.tabIndex = -1;
option.dataset.archify3d = entry.value;
option.setAttribute('role', entry.group ? 'menuitemradio' : 'menuitem');
if (entry.group) option.setAttribute('aria-checked', 'false');
option.innerHTML =
`<span class="archify-3d-swatch" aria-hidden="true"><span class="archify-3d-swatch-icon archify-3d-i-${entry.value}"></span></span>` +
`<span class="preset-option-copy"><strong>${entry.title}</strong><small>${entry.hint}</small></span>` +
(entry.group
? '<span class="preset-option-check" aria-hidden="true"></span>'
: `<kbd class="archify-3d-kbd" aria-hidden="true">${entry.shortcut ?? ''}</kbd>`);
option.addEventListener('click', (event) => {
event.stopPropagation();
if (option.getAttribute('aria-disabled') === 'true') return;
this.choose(entry.value);
if (!entry.group) this.closeMenu();
else this.syncButtons();
});
menu.appendChild(option);
}
}
button.addEventListener('click', (event) => { button.addEventListener('click', (event) => {
event.stopPropagation(); event.stopPropagation();
if (menu.classList.contains('open')) this.closeMenu(); this.setActive(!this.active);
else this.openMenu();
}); });
menu.addEventListener('keydown', (event) => this.menuKey(event));
document.addEventListener('click', (event) => {
if (menu.classList.contains('open') && !wrap.contains(event.target as Node)) this.closeMenu();
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && menu.classList.contains('open')) {
this.closeMenu();
button.focus();
}
});
wrap.append(button, menu);
// Beside Export, outside the tools capsule, as its twin.
const exportWrap = document.querySelector('.toolbar .export-wrap'); const exportWrap = document.querySelector('.toolbar .export-wrap');
if (exportWrap?.parentElement) exportWrap.parentElement.insertBefore(wrap, exportWrap); if (exportWrap?.parentElement) exportWrap.parentElement.insertBefore(button, exportWrap);
else document.querySelector('.toolbar')?.appendChild(wrap); else document.querySelector('.toolbar')?.appendChild(button);
return { button, menu }; return button;
} }
private options(): HTMLButtonElement[] { /**
return [...this.menu.querySelectorAll<HTMLButtonElement>('.archify-3d-option')].filter((o) => o.getAttribute('aria-disabled') !== 'true'); * 3D-only controls in Archify's bottom-right dock, where its own view
} * controls live: Stacked (toggle) and From above, before the zoom stepper;
* the stepper's "100%" (a 2D zoom level) reads "Reset" while 3D is on.
private openMenu(): void { */
// Only one toolbar menu at a time: close Archify's own. private buildNavControls(): { stack: HTMLButtonElement; top: HTMLButtonElement; reset: HTMLSpanElement } {
document.querySelectorAll('.toolbar .preset-menu.open, .toolbar .export-menu.open').forEach((m) => { const nav = this.container.querySelector<HTMLElement>('.diagram-nav');
if (m !== this.menu) (m.previousElementSibling as HTMLElement | null)?.click(); const make = (id: string, label: string, title: string, pressed: boolean, action: () => void) => {
const button = document.createElement('button');
button.id = id;
button.type = 'button';
button.title = title;
button.setAttribute('aria-label', title);
if (pressed) button.setAttribute('aria-pressed', 'false');
button.textContent = label;
button.hidden = true;
button.addEventListener('click', (event) => {
event.stopPropagation();
action();
}); });
this.syncButtons(); return button;
this.menu.classList.add('open');
this.button.setAttribute('aria-expanded', 'true');
const checked = this.options().find((o) => o.getAttribute('aria-checked') === 'true');
(checked ?? this.options()[0])?.focus();
}
private closeMenu(): void {
this.menu.classList.remove('open');
this.button.setAttribute('aria-expanded', 'false');
}
private menuKey(event: KeyboardEvent): void {
const options = this.options();
const index = options.indexOf(document.activeElement as HTMLButtonElement);
const move = (to: number) => {
event.preventDefault();
options[(to + options.length) % options.length]?.focus();
}; };
if (event.key === 'ArrowDown') move(index + 1); const stack = make('btn-3d-stack', 'STACK', 'Stacked: clients on top, data at the bottom', true, () => {
else if (event.key === 'ArrowUp') move(index - 1); this.stage?.setLayered(!this.stage.layered);
else if (event.key === 'Home') move(0); this.syncButtons();
else if (event.key === 'End') move(options.length - 1);
else if (event.key === 'Tab') this.closeMenu();
}
private choose(value: MenuValue): void {
switch (value) {
case '2d':
case '3d':
if ((value === '3d') !== this.active) this.setActive(value === '3d');
return;
case 'flat':
case 'stacked':
this.stage?.setLayered(value === 'stacked');
this.save(); this.save();
return; });
case 'reset': const top = make('btn-3d-top', 'TOP', 'From above: look straight down, like 2D', false, () => this.stage?.top());
this.stage?.fit(); const reset = document.createElement('span');
return; reset.className = 'archify-3d-reset';
case 'top': reset.textContent = 'RESET';
this.stage?.top(); const zoomOut = nav?.querySelector('[data-view="out"]') ?? null;
return; nav?.insertBefore(stack, zoomOut);
nav?.insertBefore(top, zoomOut);
const resetButton = nav?.querySelector<HTMLButtonElement>('[data-view="reset"]');
if (resetButton) {
resetButton.appendChild(reset);
resetButton.dataset.archify3dTitle = resetButton.title;
} }
return { stack, top, reset };
} }
private addExportItem(): void { private addExportItem(): void {
@@ -295,16 +196,17 @@ class Viewer3D {
} }
private syncButtons(): void { private syncButtons(): void {
const layered = !!this.stage?.layered; const on = this.active;
this.button.toggleAttribute('data-active', this.active); this.toggle.setAttribute('aria-checked', String(on));
for (const option of this.menu.querySelectorAll<HTMLButtonElement>('.archify-3d-option')) { this.toggle.setAttribute('aria-label', on ? '3D view on; switch to 2D' : '2D view; switch to 3D');
const value = option.dataset.archify3d as MenuValue; this.toggle.title = on ? 'Switch to 2D' : 'Switch to 3D';
if (value === '2d' || value === '3d') option.setAttribute('aria-checked', String((value === '3d') === this.active)); for (const seg of this.toggle.querySelectorAll<HTMLElement>('.archify-3d-seg')) seg.classList.toggle('on', (seg.dataset.seg === '3d') === on);
if (value === 'flat' || value === 'stacked') option.setAttribute('aria-checked', String((value === 'stacked') === layered)); this.stackButton.hidden = !on;
// Levels and camera only apply to the 3D view. this.stackButton.setAttribute('aria-pressed', String(!!this.stage?.layered));
if (value !== '2d' && value !== '3d') option.setAttribute('aria-disabled', String(!this.active)); this.topButton.hidden = !on;
} const resetButton = this.resetLabel.parentElement as HTMLButtonElement | null;
if (this.exportItem) this.exportItem.hidden = !this.active; if (resetButton) resetButton.title = on ? 'Reset view: show the whole diagram (0)' : resetButton.dataset.archify3dTitle ?? resetButton.title;
if (this.exportItem) this.exportItem.hidden = !on;
} }
// ---------------------------------------------------------------- mode // ---------------------------------------------------------------- mode
+26 -30
View File
@@ -10,36 +10,32 @@ html.archify-3d-on .diagram-container { min-height: max(420px, 62vh); }
.archify-3d-stage canvas:active { cursor: grabbing; } .archify-3d-stage canvas:active { cursor: grabbing; }
html.archify-3d-on .diagram-container > :not(svg):not(.archify-3d-stage) { z-index: 2; } html.archify-3d-on .diagram-container > :not(svg):not(.archify-3d-stage) { z-index: 2; }
/* The 3D button is Export's twin: same pill, same type, same menu. */ /* 2D | 3D switch: Export's twin. Both states visible, the filled half is current. */
.toolbar #btn-3d { padding-right: 0.95rem; padding-left: 1rem; color: var(--bg); font-weight: 550; } .toolbar #btn-3d { padding: 0 0.25rem; gap: 0; color: var(--text-muted); }
.toolbar #btn-3d::before { inset: 0.25rem 0; border-radius: 0.6875rem; background: var(--text); .toolbar #btn-3d::before { inset: 0.25rem 0; border-radius: 0.6875rem; background: color-mix(in srgb, var(--panel) 78%, transparent);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12), 0 6px 16px -8px rgba(15, 23, 42, 0.4); } box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--panel-border) 85%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); backdrop-filter: blur(12px); }
.toolbar #btn-3d:hover, .toolbar #btn-3d:hover::before { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 22%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); }
.toolbar #btn-3d[aria-expanded="true"] { color: var(--bg); } .toolbar #btn-3d[aria-checked] { color: var(--text-muted); }
.toolbar #btn-3d:hover::before, .archify-3d-seg { display: inline-flex; align-items: center; height: 1.75rem; padding: 0 0.7rem; border-radius: 0.5rem; font-weight: 550; transition: background 0.15s, color 0.15s; }
.toolbar #btn-3d[aria-expanded="true"]::before { background: color-mix(in srgb, var(--text) 86%, var(--bg)); } .archify-3d-seg.on { color: var(--bg); background: var(--text); box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12), 0 6px 16px -8px rgba(15, 23, 42, 0.4); }
/* While the 3D view is on, the pill wears a cyan ring. */ .toolbar #btn-3d:hover .archify-3d-seg:not(.on) { color: var(--text); }
.toolbar #btn-3d[data-active]::before { box-shadow: 0 0 0 2px var(--frontend-stroke), 0 6px 16px -8px rgba(15, 23, 42, 0.4); }
.toolbar .archify-3d-wrap .archify-3d-menu { right: 0; } /* 3D-only controls in the bottom-right dock, in Archify's mode-button style. */
.diagram-nav #btn-3d-stack,
.diagram-nav #btn-3d-top { display: inline-flex; align-items: center; gap: 0.375rem; min-width: auto; padding-inline: 0.55rem 0.65rem; color: var(--text-muted); font-weight: 500; }
.diagram-nav #btn-3d-stack[hidden],
.diagram-nav #btn-3d-top[hidden] { display: none; }
.diagram-nav #btn-3d-stack { position: relative; margin-left: 0.375rem; }
.diagram-nav #btn-3d-stack::after { position: absolute; top: 0.55rem; bottom: 0.55rem; left: -0.19rem; width: 1px; background: color-mix(in srgb, var(--panel-border) 90%, transparent); content: ""; pointer-events: none; }
.diagram-nav #btn-3d-stack::before,
.diagram-nav #btn-3d-top::before { flex: none; width: 0.875rem; height: 0.875rem; background: currentColor; content: ""; opacity: 0.85; -webkit-mask: var(--nav-glyph) center / contain no-repeat; mask: var(--nav-glyph) center / contain no-repeat; }
.diagram-nav #btn-3d-stack { --nav-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linejoin='round'%3E%3Cpath d='M10 2.5 17.5 6.3 10 10 2.5 6.3z'/%3E%3Cpath d='m2.5 10 7.5 3.8 7.5-3.8M2.5 13.7 10 17.5l7.5-3.8'/%3E%3C/g%3E%3C/svg%3E"); }
.diagram-nav #btn-3d-top { --nav-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 2.5v9M6.5 8l3.5 3.5L13.5 8'/%3E%3Cpath d='M3 14.5h14M5 17.5h10'/%3E%3C/g%3E%3C/svg%3E"); }
.diagram-nav #btn-3d-stack[aria-pressed="true"] { color: var(--text); background: color-mix(in srgb, var(--frontend-stroke) 14%, transparent); }
/* The stepper's middle button shows the 2D zoom level; in 3D it is the reset. */
.archify-3d-reset { display: none; color: var(--text-muted); font-size: 0.75rem; font-weight: 500; }
html.archify-3d-on .diagram-nav [data-view="reset"] > :not(.archify-3d-reset) { display: none !important; }
html.archify-3d-on .archify-3d-reset { display: inline; }
.toolbar .export-menu button[data-archify3d-export]::before { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7z'/%3E%3Cpath d='M3 6.5l7 4 7-4M10 10.5v7'/%3E%3C/g%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7z'/%3E%3Cpath d='M3 6.5l7 4 7-4M10 10.5v7'/%3E%3C/g%3E%3C/svg%3E"); } .toolbar .export-menu button[data-archify3d-export]::before { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7z'/%3E%3Cpath d='M3 6.5l7 4 7-4M10 10.5v7'/%3E%3C/g%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7z'/%3E%3Cpath d='M3 6.5l7 4 7-4M10 10.5v7'/%3E%3C/g%3E%3C/svg%3E"); }
/* Menu rows: Archify's .preset-option layout, with an icon swatch. */
.toolbar .archify-3d-menu { width: 18.5rem; }
.toolbar .archify-3d-menu .preset-menu-heading { padding-top: 0.6rem; }
.toolbar .archify-3d-menu .preset-menu-heading:first-child { padding-top: 0.45rem; }
.archify-3d-swatch { display: grid; place-items: center; width: 2.375rem; height: 1.75rem; border-radius: 0.4375rem; color: var(--text-muted);
background: color-mix(in srgb, var(--text) 5%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 14%, transparent);
transition: box-shadow 0.14s ease, color 0.14s ease; }
.toolbar .archify-3d-option[aria-checked="true"] .archify-3d-swatch { color: var(--frontend-stroke); box-shadow: 0 0 0 2px var(--toolbar-menu-bg), 0 0 0 3.5px var(--frontend-stroke); }
.archify-3d-swatch-icon { width: 1.05rem; height: 1.05rem; background: currentColor; -webkit-mask: center / contain no-repeat; mask: center / contain no-repeat; }
.archify-3d-i-2d { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='14' height='12' rx='1.5'/%3E%3Cpath d='M6 8h3M6 11h5M12.5 8h1.5'/%3E%3C/g%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='14' height='12' rx='1.5'/%3E%3Cpath d='M6 8h3M6 11h5M12.5 8h1.5'/%3E%3C/g%3E%3C/svg%3E"); }
.archify-3d-i-3d { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7z'/%3E%3Cpath d='M3 6.5l7 4 7-4M10 10.5v7'/%3E%3C/g%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7z'/%3E%3Cpath d='M3 6.5l7 4 7-4M10 10.5v7'/%3E%3C/g%3E%3C/svg%3E"); }
.archify-3d-i-flat { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 6 17.5 9.8 10 13.6 2.5 9.8z'/%3E%3C/g%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 6 17.5 9.8 10 13.6 2.5 9.8z'/%3E%3C/g%3E%3C/svg%3E"); }
.archify-3d-i-stacked { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 2.5 17.5 6.3 10 10 2.5 6.3z'/%3E%3Cpath d='m2.5 10 7.5 3.8 7.5-3.8M2.5 13.7 10 17.5l7.5-3.8'/%3E%3C/g%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 2.5 17.5 6.3 10 10 2.5 6.3z'/%3E%3Cpath d='m2.5 10 7.5 3.8 7.5-3.8M2.5 13.7 10 17.5l7.5-3.8'/%3E%3C/g%3E%3C/svg%3E"); }
.archify-3d-i-reset { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 10a6.5 6.5 0 1 0 2-4.7'/%3E%3Cpath d='M3.5 3.5v3.5H7'/%3E%3C/g%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 10a6.5 6.5 0 1 0 2-4.7'/%3E%3Cpath d='M3.5 3.5v3.5H7'/%3E%3C/g%3E%3C/svg%3E"); }
.archify-3d-i-top { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 2.5v9M6.5 8l3.5 3.5L13.5 8'/%3E%3Cpath d='M3 14.5h14M5 17.5h10'/%3E%3C/g%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 2.5v9M6.5 8l3.5 3.5L13.5 8'/%3E%3Cpath d='M3 14.5h14M5 17.5h10'/%3E%3C/g%3E%3C/svg%3E"); }
.toolbar .archify-3d-option[aria-disabled="true"] { opacity: 0.42; cursor: default; }
.toolbar .archify-3d-option[aria-disabled="true"]:hover { background: transparent; }
.archify-3d-kbd { justify-self: end; min-width: 1rem; color: var(--text-faint); font: inherit; font-size: 0.6875rem; text-align: center; }
.archify-3d-kbd:empty { visibility: hidden; }
`; `;