diff --git a/.changeset/olive-donkeys-repeat.md b/.changeset/olive-donkeys-repeat.md new file mode 100644 index 0000000..426b69e --- /dev/null +++ b/.changeset/olive-donkeys-repeat.md @@ -0,0 +1,11 @@ +--- +'@visimer/dom': patch +--- + +Re-assert each canvas's mermaid config before it renders. + +`mermaid.initialize()` writes a module-global config, so on a page with more +than one canvas the last one to mount owned it: every other canvas rendered +with the wrong theme and layout options from its second render onwards. +`render()` now re-asserts its own config instead of trusting whatever was +initialized in the constructor. diff --git a/apps/site/index.html b/apps/site/index.html index 78ab270..1491e8e 100644 --- a/apps/site/index.html +++ b/apps/site/index.html @@ -74,7 +74,23 @@ -
+
+ +
+

WYSIWYG editor renders native mermaid

+

Click a node to edit it. Perfect for polishing AI-generated diagrams.

+
+
diff --git a/apps/site/src/App.tsx b/apps/site/src/App.tsx index e7593fc..aaed7d3 100644 --- a/apps/site/src/App.tsx +++ b/apps/site/src/App.tsx @@ -17,6 +17,60 @@ const REPO = 'inkeep/visimer' const REPO_URL = `https://github.com/${REPO}` const INSTALL_CMD = 'npm i @visimer/react' +/** + * The headline, as Mermaid. The verb rides the connector, which is where mermaid + * puts verbs, so the whole thing reads as one sentence. + * + * Direction is picked once at mount rather than on every resize: a phone fits the + * left-to-right layout by width, which shrinks the headline to caption size, but + * re-picking on resize would overwrite whatever the visitor had typed into it. + */ +const HERO_SOURCE_LR = `flowchart LR + A[WYSIWYG editor] -->|renders| B[native mermaid]` + +const HERO_SOURCE_TD = `flowchart TD + A[WYSIWYG editor] -->|renders| B[native mermaid]` + +/** + * Inclusive at 760 to match `@media (max-width: 760px)` in site.css, which sizes + * the band for this layout. A strict `<` disagrees with the media query at + * exactly 760px: the band goes tall for a stacked diagram while the source is + * still left-to-right. + */ +const HERO_STACK_MAX_WIDTH = 760 + +function initialHeroSource(): string { + if (typeof window === 'undefined') return HERO_SOURCE_LR + return window.innerWidth <= HERO_STACK_MAX_WIDTH ? HERO_SOURCE_TD : HERO_SOURCE_LR +} + +/** + * Mermaid lays the headline out at display size, so this config carries the type + * scale rather than CSS scaling a small render up. Node padding is deliberately + * tight: the masthead fits to width, so every unit of padding is paid for in + * headline size. + */ +const heroConfig = { + theme: 'base', + flowchart: { padding: 11, nodeSpacing: 44, rankSpacing: 44, useMaxWidth: false }, + themeVariables: { + fontFamily: "'Inter', sans-serif", + primaryColor: '#EAF3F0', + primaryBorderColor: '#0E7C6B', + primaryTextColor: '#1C1A17', + lineColor: '#0E7C6B', + secondaryColor: '#F5E9C9', + secondaryBorderColor: '#C9A227', + tertiaryColor: '#FBF9F4', + tertiaryBorderColor: '#E6E0D4', + mainBkg: '#EAF3F0', + nodeBorder: '#0E7C6B', + textColor: '#1C1A17', + // No edgeLabelBackground: mermaid ignores it for flowchart edge labels and + // backs them with the secondary tint instead, so site.css sets it. + }, +} + const FEATURES: Array<{ mark: string; title: string; body: string }> = [ { @@ -156,6 +210,14 @@ export default function App() { const [type, setType] = useState('flowchart') const [skin, setSkin] = useState<'light' | 'dark'>('light') const { editor } = useMermaidEditor(PRESETS.flowchart) + + // The headline runs on its own editor so editing it never disturbs the + // playground below, and vice versa. + const [heroInitial] = useState(initialHeroSource) + const { editor: heroEditor } = useMermaidEditor(heroInitial) + const [heroSource, setHeroSource] = useState(heroInitial) + useEffect(() => heroEditor.on('change', ({ code }) => setHeroSource(code.trim())), [heroEditor]) + useCanvasControlTracking() // Expanding hands off to the dedicated /playground page, carrying the @@ -409,8 +471,15 @@ export default function App() { -
-
+ {/* The hero canvas is 100vw, which counts the scrollbar on platforms that + reserve space for one, so it would sit a few pixels wider than the page + and add a horizontal scrollbar. `clip` trims that without creating a + scroll container, so the sticky header above is unaffected. */} +
+ {/* Wider than the old 1000px hero: the headline is now fit to the width of + this container, so a narrow one renders it well below the 76px the + text h1 used to carry. 1180 matches the demo section below it. */} +
Open source · {licenseLabel} · React & vanilla
-

WYSIWYG editor renders native mermaid

+
- Edit Mermaid diagrams visually. - + { + view.container.tabIndex = -1 + }} + /> +
+
+ That headline is a live Mermaid diagram. + Double-click a word to rewrite it. + {/* the statement line only: "flowchart LR" is noise in a one-line hint */} + + {heroSource.split('\n').slice(1).join(' ').replace(/\s+/g, ' ').trim()} + +

svg`, the diagram itself. A bare + `.mw-canvas svg` also matches the icons inside that popover, which would pin + each one to inset 0 and stack them into an unreadable pile. */ +/* The host has to be pinned to the card as well as the svg. Left to size itself + it takes the diagram's natural height, and since the svg is positioned against + the host rather than the card, the diagram then paints straight out of the + hero — most visibly on phones, where the stacked layout is tallest. */ +.hero-masthead .mw-svg-host { + position: absolute; + inset: 0; +} +.hero-masthead .mw-svg-host > svg { + position: absolute; + inset: 0; + width: 100% !important; + height: 100% !important; + max-width: none !important; + transform: none !important; +} +.hero-masthead .mw-svg-host > svg .nodeLabel, +.hero-masthead .mw-svg-host > svg .nodeLabel p { + font-family: 'Inter', sans-serif !important; + font-weight: 700 !important; + letter-spacing: -0.035em !important; + color: #1c1a17 !important; + fill: #1c1a17 !important; +} +.hero-masthead .mw-svg-host > svg foreignObject { + overflow: visible !important; +} +.hero-masthead .mw-svg-host > svg .nodeLabel, +.hero-masthead .mw-svg-host > svg .nodeLabel p, +.hero-masthead .mw-svg-host > svg .nodeLabel div { + overflow: visible !important; + white-space: nowrap !important; + max-width: none !important; +} +.hero-masthead .mw-svg-host > svg .edgePath path, +.hero-masthead .mw-svg-host > svg .flowchart-link { + stroke: #0e7c6b !important; + stroke-width: 2px !important; +} +.hero-masthead .mw-svg-host > svg marker path, +.hero-masthead .mw-svg-host > svg .marker { + fill: #0e7c6b !important; + stroke: #0e7c6b !important; +} +/* The edge label needs an opaque backing so the connector does not strike + through the word, but mermaid takes that colour from the theme's secondary + (the gold tint) and ignores edgeLabelBackground here, which reads as a + highlighter chip. It matches the page instead, since the hero canvas sits + directly on the site background with no card of its own. */ +.hero-masthead .mw-svg-host > svg .edgeLabel, +.hero-masthead .mw-svg-host > svg .edgeLabel span, +.hero-masthead .mw-svg-host > svg .edgeLabel p { + background: #f7f4ed !important; + background-color: #f7f4ed !important; + color: #544f47 !important; + fill: #544f47 !important; +} +.hero-masthead .mw-svg-host > svg .edgeLabel rect { + fill: #f7f4ed !important; +} +/* Zoom chrome in the corner of an h1 gives away that it is a widget, and a + headline should not pan. */ +.hero-masthead .mw-zoom-btn { + display: none !important; +} +.hero-masthead, +.hero-masthead .mw-canvas, +.hero-masthead .mw-svg-host { + overflow: visible !important; +} + +/* The canvas runs the full width of the window, so without an inset the diagram + would butt against both screen edges. This keeps a margin at every size while + leaving the canvas itself full-bleed. */ +.hero-masthead .mw-svg-host { + left: clamp(16px, 5vw, 90px); + right: clamp(16px, 5vw, 90px); +} + +/* Under 760px the headline switches to the stacked top-down layout, which is + nearly square rather than wide, so the open band it is centred into has to be + deep enough that it is fitted by width rather than squashed by height. */ +@media (max-width: 760px) { + .hero-masthead { + height: clamp(300px, 78vw, 360px) !important; + } + .hero-hint { + font-size: 12px !important; + } +} + +/* First-paint content, defined in index.html inside #root and replaced by the + app on mount. Vite emits the stylesheet as a real and the bundle as a + deferred module, so this paints styled before any JavaScript runs. Positioned + to sit roughly where the hero headline lands, so the swap is not a jump. */ +.boot { + max-width: 1180px; + margin: 0 auto; + padding: 150px 26px 30px; + text-align: center; +} +.boot-title { + font-family: 'Inter', sans-serif; + font-weight: 700; + font-size: clamp(34px, 5vw, 58px); + line-height: 1.06; + letter-spacing: -0.035em; + color: #1c1a17; + margin: 0; + text-wrap: balance; +} +.boot-sub { + max-width: 620px; + margin: 20px auto 0; + font-size: 18.5px; + line-height: 1.55; + color: #544f47; +} + +/* Keeps the headline text in the document for assistive tech and crawlers, + since the visible version is drawn as SVG by the diagram renderer. */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} + /* The live demo embeds the real editor; keep its canvas transparent so the dotted preview background shows through, and center the rendered SVG the way the design's preview pane does. */ diff --git a/packages/dom/src/view.ts b/packages/dom/src/view.ts index 2cbd885..cdd7294 100644 --- a/packages/dom/src/view.ts +++ b/packages/dom/src/view.ts @@ -749,6 +749,11 @@ export class MermaidCanvasView { const seq = ++this.renderSeq const id = `mw-render-${++renderIdCounter}` try { + // mermaid.initialize writes a module-global config, so with more than one + // canvas on a page the last one to mount owns it and every other canvas + // renders with the wrong theme and layout. Re-assert ours here rather than + // trusting whatever was initialized in the constructor. + this.mermaid.initialize(this.mermaidConfig) const { svg } = await this.mermaid.render(id, code) if (seq !== this.renderSeq) return // stale // an in-place edit may have kept typing since the live commit this render