Team Ai
Apppublic

build-small-hackathon/CodeFlow

sourceHugging Facemitupdated 4mo agoView on Hugging Face
10likes
frontend.html1484 linesDownload Raw Back to root
1<!DOCTYPE html>2<html lang="en">3<head>4    <meta charset="UTF-8">5    <meta name="viewport" content="width=device-width, initial-scale=1.0">6    <title>CodeFlow — Code to Flowchart</title>7    <!-- All assets are vendored locally (see static/) — no external CDN/API at runtime. -->8    <link rel="stylesheet" href="static/fonts.css">9    <style>10        /* ============================================================11           PINE & SAGE — the CodeFlow theme (green + light).12           Soft sage-paper base, deep pine-green accent (dark enough to clear13           WCAG AA on the light bg), muted moss branch lines. Light by default;14           toggle → forest "charcoal" dark. Colorblind-safe by design: meaning15           rides on text + node shape, never hue — green is purely decorative,16           and luminance contrast does the work.17           NOTE: --cyan / --violet are historical var names kept so every18           reference stays valid; here --cyan = the pine accent and19           --violet = moss green. (Safe future cleanup: rename to20           --accent / --accent-2.)21           ============================================================ */22        :root {23            --bg:        #edf0e6;   /* sage paper */24            --bg-glow:   #e8efdd;   /* faint header tint */25            --bg-panel:  #f9fbf4;   /* near-white card */26            --bg-inset:  #e2e7d6;   /* sage inset (input / canvas) */27            --border:    #d3dac3;   /* sage hairline */28            --cyan:      #2c6e49;   /* ACCENT — pine green (deep, AA-friendly) */29            --cyan-dim:  #245a3b;30            --on-accent: #fbfdf8;   /* label on the accent — near-white (dark accent → ~6:1 AA) */31            --violet:    #6b7a55;   /* SECONDARY — moss green (branch lines) */32            --text:      #1e251c;   /* deep green-black ink */33            --text-dim:  #5d6b54;34            --label:     #5a7048;   /* moss-green section labels */35            --scroll:    #c8d3b8;36            --scroll-hi: #b2c29c;37            --gutter:    #9aa888;38            /* Editor syntax tokens — green family (pine / gold / olive / moss) */39            --tok-keyword: #2c6e49;   /* pine */40            --tok-fn:      #876618;   /* dark gold */41            --tok-string:  #66722f;   /* olive */42            --tok-number:  #9e521f;   /* burnt orange */43            --tok-comment: #8c9a7e;   /* sage gray */44            --tok-prop:    #6f7d52;   /* moss */45            --tok-punct:   #5d6b54;46            --tok-var:     #1e251c;47            --tok-invalid: #b5362a;48            --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;49            --sans: 'Hanken Grotesk', system-ui, -apple-system, sans-serif;50            --display: 'Fraunces', Georgia, 'Times New Roman', serif;  /* serif for the wordmark */51        }52 53        /* ---- Forest charcoal (dark counterpart, via toggle): same green identity ---- */54        body.dark {55            --bg:        #121711;56            --bg-glow:   #1f271d;57            --bg-panel:  #1a211a;58            --bg-inset:  #0e130d;59            --border:    #2c372a;60            --cyan:      #3f9e63;   /* pine lifted for dark */61            --cyan-dim:  #348352;62            --on-accent: #122012;   /* label on the accent — dark ink (light accent → ~5:1 AA) */63            --violet:    #8fa274;   /* moss lifted */64            --text:      #e7eede;   /* green cream */65            --text-dim:  #9cac8e;66            --label:     #9fc98a;67            --scroll:    #34402f;68            --scroll-hi: #45543d;69            --gutter:    #59674a;70            --tok-keyword: #5fb37e;71            --tok-fn:      #d6a85e;72            --tok-string:  #b8c47a;73            --tok-number:  #e0a86a;74            --tok-comment: #7e8b70;75            --tok-prop:    #9fb381;76            --tok-punct:   #9cac8e;77            --tok-var:     #e7eede;78            --tok-invalid: #ff6b6b;79        }80 81        /* ============================================================82           RUST & SIENNA — fallback palette (the previous warm theme).83           Activated by adding `rust` to <body> (see the PALETTE flag in JS).84           Same var names, so everything that reads var(--cyan)/etc. — incl.85           the color-mix focus rings — follows automatically; the Mermaid86           diagram is handled by the matching MERMAID_THEMES.rust entry.87           ============================================================ */88        body.rust {89            --bg:        #f6ece0;90            --bg-glow:   #f0e3d2;91            --bg-panel:  #fdf6ec;92            --bg-inset:  #efe2d2;93            --border:    #e0cdb6;94            --cyan:      #b0532e;95            --cyan-dim:  #95421f;96            --on-accent: #fffaf0;   /* dark accent → near-white label (~4.8 AA) */97            --violet:    #9c6f4f;98            --text:      #2a201a;99            --text-dim:  #75614f;100            --label:     #9c5a33;101            --scroll:    #d6c4a8;102            --scroll-hi: #c2ab88;103            --gutter:    #ad9d7e;104            --tok-keyword: #a8472a;105            --tok-fn:      #876618;106            --tok-string:  #66722f;107            --tok-number:  #9e521f;108            --tok-comment: #a4937b;109            --tok-prop:    #92633a;110            --tok-punct:   #75614f;111            --tok-var:     #2a201a;112            --tok-invalid: #b5362a;113        }114        /* Rust dark — full var set so it wins over green's body.dark on toggle */115        body.rust.dark {116            --bg:        #1b1512;117            --bg-glow:   #29201a;118            --bg-panel:  #231b16;119            --bg-inset:  #16100c;120            --border:    #392d24;121            --cyan:      #d56a3c;122            --cyan-dim:  #b85730;123            --on-accent: #1e1408;   /* light accent → dark ink label (~4.8 AA) */124            --violet:    #b3855f;125            --text:      #f0e6d6;126            --text-dim:  #b3a489;127            --label:     #d98a5a;128            --scroll:    #443629;129            --scroll-hi: #594636;130            --gutter:    #6c5b47;131            --tok-keyword: #e07a52;132            --tok-fn:      #d6a85e;133            --tok-string:  #b8c47a;134            --tok-number:  #e0a86a;135            --tok-comment: #8f8169;136            --tok-prop:    #d39a6e;137            --tok-punct:   #b3a489;138            --tok-var:     #f0e6d6;139            --tok-invalid: #ff6b6b;140        }141 142        * { box-sizing: border-box; }143 144        body {145            font-family: var(--sans);146            background: radial-gradient(1200px 600px at 50% -10%, var(--bg-glow) 0%, var(--bg) 55%);147            color: var(--text);148            margin: 0;149            padding: 24px 28px 28px;150            min-height: 100vh;151            transition: background .25s, color .25s;152        }153 154        /* ---- Header ---- */155        header {156            display: flex;157            align-items: center;158            gap: 14px;159            margin-bottom: 22px;160        }161        /* Standalone "decision node" mark — no badge; inherits the accent (so it162           flips to amber in dark mode). The svg uses currentColor. */163        .logo-mark {164            display: grid; place-items: center;165            color: var(--cyan);166        }167        .logo-mark svg { width: 34px; height: 34px; }168        .wordmark { font-family: var(--display); font-size: 25px; font-weight: 600; letter-spacing: -0.2px; }169        .wordmark .flow { color: var(--cyan); }170        /* Boxed instruction callout — accent left-bar draws the eye to the "how to" */171        .tagline {172            color: var(--text);173            font-size: 12.5px;174            font-weight: 500;175            margin-left: 10px;176            padding: 8px 14px;177            background: var(--bg-panel);178            border: 1px solid var(--border);179            border-left: 3px solid var(--cyan);180            border-radius: 9px;181            box-shadow: 0 1px 2px rgba(30, 45, 22, .08);182        }183        .tagline b { color: var(--cyan); font-weight: 700; }184 185        /* ---- Theme toggle (far right) ---- */186        .theme-toggle {187            margin-left: auto;188            width: 38px; height: 38px;189            display: grid; place-items: center;190            color: var(--text-dim);191            background: var(--bg-panel);192            border: 1px solid var(--border);193            border-radius: 10px;194            cursor: pointer;195            transition: color .15s, border-color .15s;196        }197        .theme-toggle:hover { color: var(--cyan); border-color: var(--cyan); }198        .theme-toggle svg { width: 18px; height: 18px; }199        /* Show moon in dark mode, sun in light mode */200        .theme-toggle .icon-moon { display: none; }   /* default = light → show sun */201        body.dark .theme-toggle .icon-sun { display: none; }202        body.dark .theme-toggle .icon-moon { display: block; }203 204        /* ---- Flow cue between the panels ---- */205        .flow-arrow {206            flex: 0 0 auto;207            align-self: center;208            display: grid; place-items: center;209            color: var(--cyan);210            opacity: .7;211        }212        .flow-arrow svg { width: 26px; height: 26px; }213 214        /* ---- Layout ---- */215        .container {216            display: flex;217            gap: 22px;218            height: calc(100vh - 110px);219        }220        .panel {221            flex: 1;222            min-width: 0;223            display: flex;224            flex-direction: column;225            background: var(--bg-panel);226            border: 1px solid var(--border);227            border-radius: 14px;228            padding: 18px;229        }230        /* Give the diagram more room than the code input so charts read bigger */231        .panel-diagram { flex: 1.5; position: relative; }232        .panel-head {233            display: flex;234            align-items: center;235            justify-content: space-between;236            margin-bottom: 14px;237            gap: 12px;238        }239        .panel-title {240            font-size: 13px;241            font-weight: 600;242            text-transform: uppercase;243            letter-spacing: 1.2px;244            color: var(--label);245        }246 247        /* Subtle "scroll" hint — only shown when the chart overflows its box */248        .scroll-hint {249            position: absolute;250            bottom: 28px;251            left: 50%;252            transform: translateX(-50%);253            display: flex;254            align-items: center;255            gap: 6px;256            background: var(--bg-panel);257            backdrop-filter: blur(4px);258            border: 1px solid var(--border);259            color: var(--text-dim);260            font-size: 11.5px;261            font-weight: 500;262            padding: 5px 12px;263            border-radius: 999px;264            pointer-events: none;265            box-shadow: 0 4px 14px rgba(0, 0, 0, .3);266        }267        .scroll-hint[hidden] { display: none; }268        .scroll-hint svg { width: 13px; height: 13px; stroke: var(--cyan); }269 270        /* ---- Zoom controls (bottom-left of the canvas) ---- */271        .zoom-ctrl {272            position: absolute;273            bottom: 28px;274            left: 28px;275            display: flex;276            align-items: stretch;277            gap: 1px;278            background: var(--bg-panel);279            border: 1px solid var(--border);280            border-radius: 9px;281            overflow: hidden;282            box-shadow: 0 4px 14px rgba(0, 0, 0, .25);283        }284        .zoom-ctrl[hidden] { display: none; }285        .zoom-btn {286            font-family: var(--sans);287            font-size: 14px;288            font-weight: 600;289            color: var(--text-dim);290            background: transparent;291            border: none;292            min-width: 30px;293            padding: 5px 8px;294            cursor: pointer;295            transition: color .12s, background .12s;296        }297        .zoom-btn.zoom-fit { font-size: 11px; text-transform: uppercase; letter-spacing: .5px; border-left: 1px solid var(--border); border-right: 1px solid var(--border); }298        .zoom-btn:hover { color: var(--cyan); background: var(--bg-inset); }299 300        /* ---- Examples dropdown ---- */301        .examples {302            position: relative;303        }304        select {305            font-family: var(--sans);306            font-size: 13px;307            font-weight: 500;308            color: var(--text);309            background: var(--bg-inset);310            border: 1px solid var(--border);311            border-radius: 8px;312            padding: 8px 32px 8px 12px;313            cursor: pointer;314            appearance: none;315            -webkit-appearance: none;316            /* Chevron: warm neutral, theme-aware via the body.dark override below */317            background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%235d6b54' stroke-width='2'><path d='M2 4l4 4 4-4'/></svg>");318            background-repeat: no-repeat;319            background-position: right 12px center;320            transition: border-color .15s;321        }322        select:hover, select:focus { border-color: var(--cyan); outline: none; }323        /* Dark-mode chevron (lighter neutral on the charcoal field) */324        body.dark select {325            background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%239cac8e' stroke-width='2'><path d='M2 4l4 4 4-4'/></svg>");326        }327 328        /* ---- Code input ---- */329        textarea {330            flex: 1;331            background: var(--bg-inset);332            color: var(--cyan);333            border: 1px solid var(--border);334            border-radius: 10px;335            padding: 14px;336            font-family: var(--mono);337            font-size: 13.5px;338            line-height: 1.6;339            resize: none;340            tab-size: 4;341        }342        textarea:focus { outline: none; border-color: var(--cyan); box-shadow: 0 0 0 3px color-mix(in srgb, var(--cyan) 22%, transparent); }343        textarea::placeholder { color: #5a5a8a; }344 345        /* ---- Editor controls (language + examples) ---- */346        .editor-controls { display: flex; align-items: center; gap: 8px; }347        select.ctrl-select { padding: 7px 28px 7px 10px; font-size: 12.5px; }348 349        /* ---- Code editor (CodeMirror) wrapper ---- */350        .editor-wrap {351            flex: 1;352            min-height: 0;353            display: flex;354            flex-direction: column;355            background: var(--bg-inset);356            border: 1px solid var(--border);357            border-radius: 10px;358            overflow: hidden;359            transition: border-color .15s, box-shadow .15s;360        }361        .editor-wrap[hidden] { display: none; }   /* author display:flex would else beat the hidden attr */362        .editor-wrap.focused { border-color: var(--cyan); box-shadow: 0 0 0 3px color-mix(in srgb, var(--cyan) 22%, transparent); }363        #editor { flex: 1; min-height: 0; overflow: hidden; }364        .cm-editor { height: 100%; background: transparent; font-size: 13.5px; }365        .cm-editor.cm-focused { outline: none; }366        .cm-editor .cm-scroller {367            font-family: var(--mono);368            line-height: 1.6;369            overflow: auto;370            scrollbar-width: thin;371            scrollbar-color: var(--scroll) var(--bg-inset);372        }373        .cm-editor .cm-scroller::-webkit-scrollbar { width: 10px; height: 10px; }374        .cm-editor .cm-scroller::-webkit-scrollbar-track { background: var(--bg-inset); border-radius: 999px; }375        .cm-editor .cm-scroller::-webkit-scrollbar-thumb {376            background: var(--scroll); border-radius: 999px; border: 2px solid var(--bg-inset);377        }378        .cm-editor .cm-scroller::-webkit-scrollbar-thumb:hover { background: var(--scroll-hi); }379 380        /* Editor footer — VS Code-style status bar: position (left) + actions (right) */381        .editor-foot {382            display: flex;383            align-items: center;384            justify-content: space-between;385            padding: 6px 10px;386            border-top: 1px solid var(--border);387            background: var(--bg-panel);388        }389        .editor-status { font-family: var(--mono); font-size: 11.5px; color: var(--text-dim); }390        .editor-actions { display: flex; gap: 6px; }391        .icon-btn {392            font-family: var(--sans);393            font-size: 11.5px;394            font-weight: 600;395            color: var(--text-dim);396            background: var(--bg-inset);397            border: 1px solid var(--border);398            border-radius: 7px;399            padding: 5px 11px;400            cursor: pointer;401            transition: color .12s, border-color .12s;402        }403        .icon-btn:hover:not(:disabled) { color: var(--cyan); border-color: var(--cyan); }404        .icon-btn.ok { color: var(--cyan); border-color: var(--cyan); }405        .icon-btn:disabled { opacity: .4; cursor: not-allowed; }406 407        /* ---- Bottom action bar (source actions + Generate) ---- */408        .action-bar {409            display: flex;410            align-items: center;411            justify-content: space-between;412            gap: 10px;413            margin-top: 14px;414        }415 416        /* ---- Generate button (compact primary, aligned with the controls) ---- */417        .btn-primary {418            font-family: var(--sans);419            font-size: 13.5px;420            font-weight: 600;421            color: var(--on-accent);   /* label color picked by accent luminance (AA in every palette/theme) */422            background: var(--cyan);   /* flat accent — no gradient */423            border: none;424            padding: 9px 18px;425            cursor: pointer;426            border-radius: 9px;427            transition: filter .15s, transform .05s;428        }429        .btn-primary:hover { filter: brightness(1.08); }430        .btn-primary:active { transform: translateY(1px); }431        .btn-primary:disabled {432            background: var(--border);433            color: var(--text-dim);434            cursor: not-allowed;435            filter: none;436        }437 438        /* ---- Output actions (Copy Mermaid / SVG / PNG) in the diagram header ---- */439        .output-actions { display: flex; gap: 6px; }440 441        /* ---- Progress bar ---- */442        .progress {443            height: 5px;444            background: var(--bg-inset);445            border: 1px solid var(--border);446            border-radius: 999px;447            overflow: hidden;448            margin-bottom: 12px;449        }450        .progress[hidden] { display: none; }451        .progress-fill {452            height: 100%;453            width: 0%;454            background: var(--cyan);   /* flat terracotta — no gradient, no glow */455            border-radius: 999px;456        }457        /* Live path: duration unknown → honest indeterminate sweep */458        .progress-fill.indeterminate {459            width: 35%;460            animation: cf-sweep 1.15s ease-in-out infinite;461        }462        @keyframes cf-sweep {463            0%   { margin-left: -35%; }464            100% { margin-left: 100%; }465        }466 467        /* ---- Flowchart canvas (bigger) ---- */468        #flowchart-target {469            flex: 1;470            background: var(--bg-inset);   /* plain canvas — grid removed */471            border: 1px solid var(--border);472            border-radius: 10px;473            padding: 18px;474            overflow: auto;475            display: flex;476            justify-content: safe center;  /* 'safe' → no clipping when zoomed past the edge */477            align-items: flex-start;   /* tall charts scroll from the top */478            transition: background .25s ease, border-color .25s ease;   /* ease the canvas on theme toggle (#9) */479        }480        /* ---- Custom scrollbar (muted) — shared by the chart box + code input ---- */481        #flowchart-target, textarea {482            scrollbar-width: thin;                       /* Firefox */483            scrollbar-color: var(--scroll) var(--bg-inset);484        }485        #flowchart-target::-webkit-scrollbar,486        textarea::-webkit-scrollbar { width: 10px; height: 10px; }487        #flowchart-target::-webkit-scrollbar-track,488        textarea::-webkit-scrollbar-track {489            background: var(--bg-inset);490            border-radius: 999px;491        }492        #flowchart-target::-webkit-scrollbar-thumb,493        textarea::-webkit-scrollbar-thumb {494            background: var(--scroll);495            border-radius: 999px;496            border: 2px solid var(--bg-inset);497        }498        #flowchart-target::-webkit-scrollbar-thumb:hover,499        textarea::-webkit-scrollbar-thumb:hover { background: var(--scroll-hi); }500        #flowchart-target::-webkit-scrollbar-corner,501        textarea::-webkit-scrollbar-corner { background: var(--bg-inset); }502        /* Render the SVG a bit under full panel width (centred) so it reads at a503           comfortable size; long charts still grow + scroll instead of shrinking.504           --fit is the default "fit" scale; the zoom control multiplies it. */505        #flowchart-target svg {506            width: calc(72% * var(--zoom, 1)) !important;   /* fit-to-width, slightly inset; zoom scales it */507            max-width: none !important;   /* override Mermaid's inline max-width so zoom can grow it */508            height: auto !important;509            min-height: 320px;510            flex: 0 0 auto;511        }512        /* Empty state: the lone Start anchor renders small + centred — not stretched513           to fill the canvas. A DEFINITE height makes SVG sizing deterministic (with514           just width:auto an SVG falls back to the ~300px default and scales UP); the515           width then follows the viewBox aspect ratio. */516        #flowchart-target.anchor-only { align-items: center; }517        #flowchart-target.anchor-only svg {518            width: auto !important;519            height: 56px !important;520            min-height: 0 !important;521            max-width: none !important;522        }523        /* Bold + glow the node's cyan edge when hovered */524        #flowchart-target .node rect,525        #flowchart-target .node polygon,526        #flowchart-target .node circle,527        #flowchart-target .node path {528            transition: stroke .12s ease, stroke-width .12s ease, filter .12s ease;529        }530        #flowchart-target .node:hover rect,531        #flowchart-target .node:hover polygon,532        #flowchart-target .node:hover circle,533        #flowchart-target .node:hover path,534        /* Code→node link: emphasise nodes whose source range holds the cursor line. */535        #flowchart-target .node.cf-active rect,536        #flowchart-target .node.cf-active polygon,537        #flowchart-target .node.cf-active circle,538        #flowchart-target .node.cf-active path {539            stroke: var(--cyan) !important;540            stroke-width: 3px !important;541            filter: drop-shadow(0 0 5px color-mix(in srgb, var(--cyan) 40%, transparent));542        }543 544        /* ===== Node restyle — warm "paper card" flowchart aesthetic ===== */545        /* Soft lift on every node so they read as little cards on the canvas */546        #flowchart-target .node { filter: drop-shadow(0 2px 5px rgba(30, 45, 22, .12)); }547 548        /* ===== Diagram-reveal animation (#1) — "Trace the path" ===== */549        /* Scale each node from its own centre (SVG <g> needs an explicit box). */550        #flowchart-target .node { transform-box: fill-box; transform-origin: 50% 50%; }551        /* Anti-flash while a new chart mounts: hide everything WAAPI will animate in,552           except the Start anchor (.cf-start), which stays put as the chart grows out of it. */553        #flowchart-target.revealing .node:not(.cf-start),554        #flowchart-target.revealing g.edgePaths path,555        #flowchart-target.revealing .edgeLabel { opacity: 0; }556        /* Process nodes (rect): rounded warm-white cards, hairline border */557        #flowchart-target .node rect {558            rx: 9px; ry: 9px;559            fill: var(--bg-panel);560            stroke: var(--border);561            stroke-width: 1.5px;562        }563        /* Decision nodes (diamond/polygon): sand fill + accent edge so they pop */564        #flowchart-target .node polygon {565            fill: var(--bg-inset);566            stroke: var(--cyan);567            stroke-width: 1.75px;568        }569        /* Node text: UI font, inked, medium weight */570        #flowchart-target .nodeLabel,571        #flowchart-target .node .label,572        #flowchart-target .node foreignObject div {573            font-family: var(--sans) !important;574            font-weight: 500;575            color: var(--text) !important;576            fill: var(--text);577        }578        /* Edges: warm-brown, a touch thicker; refined arrowheads */579        #flowchart-target .edgePath path.path,580        #flowchart-target .flowchart-link {581            stroke: var(--violet);582            stroke-width: 1.8px;583        }584        #flowchart-target marker path,585        #flowchart-target .arrowMarkerPath,586        #flowchart-target .arrowheadPath { fill: var(--violet) !important; stroke: none; }587        /* Edge labels (Yes / No / True / False) → centered warm pill chips.588           Mermaid sizes the foreignObject to the BARE text, so we let it overflow589           and recenter the padded chip by half its added padding+border (-10,-3). */590        #flowchart-target .edgeLabel foreignObject { overflow: visible; }591        #flowchart-target .edgeLabel foreignObject > div { transform: translate(-10px, -3px); }592        #flowchart-target .edgeLabel span {593            display: inline-block;594            font-family: var(--sans) !important;595            font-size: 11px !important;596            font-weight: 600 !important;597            line-height: 1;598            color: var(--text-dim) !important;599            background: var(--bg-panel) !important;600            border: 1px solid var(--border);601            border-radius: 999px;602            padding: 2px 9px;603        }604 605        #flowchart-target .placeholder { color: var(--text-dim); font-size: 14px; }606        #flowchart-target .err { color: var(--tok-invalid); align-self: flex-start; }607        #flowchart-target .raw {608            color: var(--text-dim); font-family: var(--mono); font-size: 12px;609            white-space: pre-wrap; text-align: left; align-self: flex-start; width: 100%;610        }611    </style>612</head>613<body>614    <header>615        <div class="logo-mark">616            <!-- Decision node (C4): FILLED diamond "decision" forking to two HOLLOW outcome nodes -->617            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">618                <path d="M12 3 L15.4 6.5 L12 10 L8.6 6.5 Z" fill="currentColor"/>619                <path d="M10.3 8.4 L7 15.8"/>620                <path d="M13.7 8.4 L17 15.8"/>621                <circle cx="6" cy="18" r="2.7"/>622                <circle cx="18" cy="18" r="2.7"/>623            </svg>624        </div>625        <div>626            <div class="wordmark">Code<span class="flow">Flow</span></div>627        </div>628        <span class="tagline">Paste your code on the left, hit <b>Generate</b>, and read its logic as a flowchart →</span>629        <button id="theme-toggle" class="theme-toggle" type="button" title="Toggle light / dark" aria-label="Toggle light / dark">630            <svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">631                <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/>632            </svg>633            <svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">634                <circle cx="12" cy="12" r="4"/>635                <path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>636            </svg>637        </button>638    </header>639 640    <div class="container">641        <!-- LEFT: input -->642        <div class="panel">643            <div class="panel-head">644                <span class="panel-title">Source Code</span>645                <div class="editor-controls">646                    <select id="lang-select" class="ctrl-select" title="Editor language" aria-label="Editor language">647                        <option value="python" selected>Python</option>648                        <option value="javascript">JavaScript</option>649                        <option value="java">Java</option>650                        <option value="cpp">C / C++</option>651                    </select>652                    <select id="examples-select">653                        <option value="" disabled selected>Code examples…</option>654                        <option value="loop">Sum a list of numbers</option>655                        <option value="ifelse">Check status (if / else)</option>656                        <option value="func">Find the first even number</option>657                    </select>658                </div>659            </div>660            <!-- CodeMirror mounts into #editor and reveals .editor-wrap; the661                 textarea below is the always-present fallback if CM can't load. -->662            <div id="editor-wrap" class="editor-wrap" hidden>663                <div id="editor"></div>664                <div class="editor-foot">665                    <span id="editor-status" class="editor-status">Ln 1, Col 1 · 0 lines</span>666                </div>667            </div>668            <textarea id="code-fallback" placeholder="Paste your code here, or pick a code example…" spellcheck="false"></textarea>669            <!-- Always-visible (outside editor-wrap, so it survives the textarea fallback) -->670            <div class="action-bar">671                <div class="editor-actions">672                    <button id="copy-btn" class="icon-btn" type="button">Copy</button>673                    <button id="clear-btn" class="icon-btn" type="button">Clear</button>674                </div>675                <button id="submit-btn" class="btn-primary" type="button">Generate Flowchart</button>676            </div>677        </div>678 679        <!-- Visual cue: code flows left → right into the diagram -->680        <div class="flow-arrow" aria-hidden="true">681            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">682                <path d="M4 12h14M13 6l6 6-6 6"/>683            </svg>684        </div>685 686        <!-- RIGHT: diagram -->687        <div class="panel panel-diagram">688            <div class="panel-head">689                <span class="panel-title">Flowchart</span>690                <div class="output-actions">691                    <button id="copy-mmd-btn" class="icon-btn" type="button" disabled title="Copy the Mermaid source">Copy Mermaid</button>692                    <button id="svg-btn" class="icon-btn" type="button" disabled title="Download as SVG">SVG</button>693                    <button id="png-btn" class="icon-btn" type="button" disabled title="Download as PNG">PNG</button>694                </div>695            </div>696            <div id="progress" class="progress" hidden><div id="progress-fill" class="progress-fill"></div></div>697            <div id="flowchart-target">698                <span class="placeholder">Loading…</span>699            </div>700            <div id="scroll-hint" class="scroll-hint" hidden>701                <svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">702                    <path d="M8 7l4-4 4 4M8 17l4 4 4-4"/>703                </svg>704                Scroll to view full chart705            </div>706            <div id="zoom-ctrl" class="zoom-ctrl" hidden>707                <button id="zoom-out" class="zoom-btn" type="button" title="Zoom out" aria-label="Zoom out">−</button>708                <button id="zoom-fit" class="zoom-btn zoom-fit" type="button" title="Fit to width">fit</button>709                <button id="zoom-in" class="zoom-btn" type="button" title="Zoom in" aria-label="Zoom in">+</button>710            </div>711        </div>712    </div>713 714    <!-- Vendored libraries (classic scripts → globals): Mermaid (window.mermaid),715         CodeMirror bundle (window.CM6), Gradio client (window.GradioClient).716         Loaded before the module below so the module can use the globals. -->717    <script src="static/mermaid.min.js"></script>718    <script src="static/cm.bundle.js"></script>719    <script src="static/gradio-client.js"></script>720 721    <script type="module">722        // `mermaid` is the global from static/mermaid.min.js (vendored, no CDN).723        // CodeMirror is read from the window.CM6 global inside initEditor() — kept in724        // a try/catch so any editor failure is contained and falls back to a textarea.725        // take down the rest of the page (examples, Generate). See initEditor().726 727        // Per-palette, per-theme Mermaid palettes. Keyed [PALETTE][theme] so the728        // diagram tracks whichever palette is active (see the PALETTE flag below).729        const MERMAID_THEMES = {730            green: {731                light: { background: '#e2e7d6', primaryColor: '#f9fbf4', primaryTextColor: '#1e251c',732                         primaryBorderColor: '#2c6e49', lineColor: '#6b7a55', tertiaryColor: '#eef2e2',733                         edgeLabelBackground: '#edf0e6' },734                dark:  { background: '#0e130d', primaryColor: '#1a211a', primaryTextColor: '#e7eede',735                         primaryBorderColor: '#3f9e63', lineColor: '#8fa274', tertiaryColor: '#1f271d',736                         edgeLabelBackground: '#1a211a' },737            },738            rust: {739                light: { background: '#efe2d2', primaryColor: '#fdf6ec', primaryTextColor: '#2a201a',740                         primaryBorderColor: '#b0532e', lineColor: '#9c6f4f', tertiaryColor: '#f3e8d6',741                         edgeLabelBackground: '#f6ece0' },742                dark:  { background: '#16100c', primaryColor: '#231b16', primaryTextColor: '#f0e6d6',743                         primaryBorderColor: '#d56a3c', lineColor: '#b3855f', tertiaryColor: '#29201a',744                         edgeLabelBackground: '#231b16' },745            },746        };747        function initMermaid(themeName) {748            mermaid.initialize({749                startOnLoad: false,750                theme: 'base',751                // Tighter node padding + spacing so decision diamonds hug their text (#8).752                flowchart: { padding: 6, nodeSpacing: 45, rankSpacing: 45, useMaxWidth: true },753                themeVariables: { fontFamily: "'Hanken Grotesk', sans-serif", ...MERMAID_THEMES[PALETTE][themeName] },754            });755        }756 757        // ── Active palette (quick switch back to the old warm theme) ──────758        // Flip this ONE word to swap the whole UI: 'green' (Pine & Sage, the759        // current default) or 'rust' (Rust & Sienna, the previous warm theme).760        // It just toggles a `rust` body class — every var(--cyan)/etc., the761        // color-mix focus rings, and the matching Mermaid palette all follow.762        const PALETTE = 'green';763        document.body.classList.toggle('rust', PALETTE === 'rust');764 765        // Theme state — default = light; toggle → forest charcoal. Persisted.766        let currentTheme = 'light';767        try { if (localStorage.getItem('cf-theme-warm') === 'dark') currentTheme = 'dark'; } catch {}768        function applyTheme(name) {769            currentTheme = name;770            document.body.classList.toggle('dark', name === 'dark');771            initMermaid(name);772            try { localStorage.setItem('cf-theme-warm', name); } catch {}773        }774        applyTheme(currentTheme);   // sets body class + initializes Mermaid775 776        /* ----- Pre-rendered presets -----777           Each preset stores BOTH the Python source and its already-computed778           Mermaid, so selecting one paints the diagram instantly — no model779           call. This is the "perception of fast inference" demo path and is780           also what makes this page fully previewable offline. */781        const PRESETS = {782            loop: {783                code:784`total = 0785for n in numbers:786    total += n787print(total)`,788                mermaid:789`graph TD790    A["Start"] --> B["Initialise total to zero"]791    B --> C{"More numbers to process?"}792    C -- Yes --> D["Add current number to total"]793    D --> C794    C -- No --> E["Print total"]795    E --> F["End"]`,796                linemap: { B: [1, 1], C: [2, 2], D: [3, 3], E: [4, 4] }797            },798            ifelse: {799                code:800`def check_status(val):801    if val > 10:802        return "Active"803    else:804        return "Inactive"`,805                mermaid:806`graph TD807    A["Start: check_status"] --> B{"Value greater than ten?"}808    B -- True --> C["Return Active"]809    B -- False --> D["Return Inactive"]810    C --> E["End"]811    D --> E`,812                linemap: { A: [1, 1], B: [2, 2], C: [3, 3], D: [5, 5] }813            },814            func: {815                code:816`def find_first_even(nums):817    for i in range(len(nums)):818        if nums[i] % 2 == 0:819            return nums[i]820    return None`,821                mermaid:822`graph TD823    A["Start: find_first_even"] --> B{"More elements to check?"}824    B -- Yes --> C{"Element is even?"}825    C -- Yes --> D["Return that element"]826    C -- No --> B827    B -- No --> E["Return None"]828    D --> F["End"]829    E --> F`,830                linemap: { A: [1, 1], B: [2, 2], C: [3, 3], D: [4, 4], E: [5, 5] }831            }832        };833 834        const target        = document.getElementById('flowchart-target');835        const submitBtn      = document.getElementById('submit-btn');836        const select         = document.getElementById('examples-select');837        const progress       = document.getElementById('progress');838        const progressFill   = document.getElementById('progress-fill');839        const scrollHint     = document.getElementById('scroll-hint');840        const langSelect     = document.getElementById('lang-select');841        const copyBtn        = document.getElementById('copy-btn');842        const clearBtn       = document.getElementById('clear-btn');843        const fallback       = document.getElementById('code-fallback');844        const editorWrap     = document.getElementById('editor-wrap');845        const editorStatus   = document.getElementById('editor-status');846        const copyMmdBtn     = document.getElementById('copy-mmd-btn');847        const svgBtn         = document.getElementById('svg-btn');848        const pngBtn         = document.getElementById('png-btn');849        const themeToggle    = document.getElementById('theme-toggle');850        const zoomCtrl       = document.getElementById('zoom-ctrl');851        const zoomInBtn      = document.getElementById('zoom-in');852        const zoomOutBtn     = document.getElementById('zoom-out');853        const zoomFitBtn     = document.getElementById('zoom-fit');854 855        /* ===== Real code editor (CodeMirror 6) =====856           The plain <textarea> (#code-fallback) renders by default; CodeMirror857           hides it only once it mounts. So a blocked CDN / CM failure degrades to858           a working textarea instead of a broken page. getCode()/setCode() below859           hide which input is live, so the preset + submit logic never branches. */860 861        // CodeMirror is vendored as a single IIFE bundle (static/cm.bundle.js → the862        // CM6 global), so every package shares ONE @codemirror/state/view/@lezer863        // instance by construction — no ?deps= juggling, no CDN. Grammars come from864        // the same bundle (CM6.python / .javascript / .java / .cpp).865        const LANG_LOADERS = {866            python:     () => CM6.python(),867            javascript: () => CM6.javascript(),868            java:       () => CM6.java(),869            cpp:        () => CM6.cpp(),870        };871        async function makeLangExt(name) {872            try { return (LANG_LOADERS[name] || LANG_LOADERS.python)(); } catch { return []; }873        }874 875        let cmView = null;876        let lastLineMap = {};             // nodeId -> [startLine, endLine] for the current chart877        let cfHighlightLines = () => {};  // set once CM mounts: highlight editor lines (node→code hover)878        let cfSelectLines = () => {};     // set once CM mounts: select + focus a line range (node→code edit)879        let langCompartment = null;880 881        function getCode() {882            return cmView ? cmView.state.doc.toString() : fallback.value;883        }884        function setCode(text) {885            if (cmView) cmView.dispatch({ changes: { from: 0, to: cmView.state.doc.length, insert: text } });886            else fallback.value = text;887            updateStatus();888        }889        function focusEditor() { (cmView || fallback).focus(); }890 891        function updateStatus() {892            const doc = getCode();893            const lines = doc === '' ? 0 : doc.split('\n').length;894            let ln = 1, col = 1;895            if (cmView) {896                const pos = cmView.state.selection.main.head;897                const line = cmView.state.doc.lineAt(pos);898                ln = line.number;899                col = pos - line.from + 1;900            }901            editorStatus.textContent = `Ln ${ln}, Col ${col} · ${lines} line${lines === 1 ? '' : 's'}`;902        }903 904        async function setLanguage(name) {905            if (cmView && langCompartment) {906                cmView.dispatch({ effects: langCompartment.reconfigure(await makeLangExt(name)) });907            }908        }909 910        async function initEditor() {911            try {912                // Everything comes from the vendored CM6 global (static/cm.bundle.js).913                // Wrapped in try/catch so any editor failure is contained to the914                // editor and falls back to the plain textarea — the rest of the page915                // (mermaid, examples, Generate) keeps working.916                if (!window.CM6) throw new Error('CodeMirror bundle not loaded');917                const { EditorState, Compartment, StateField, StateEffect } = CM6;918                const { EditorView, keymap, placeholder, lineNumbers, highlightActiveLine,919                        highlightActiveLineGutter, drawSelection, dropCursor, Decoration } = CM6;920                const { history, defaultKeymap, historyKeymap, indentWithTab } = CM6;921                const { indentOnInput, bracketMatching, syntaxHighlighting, HighlightStyle } = CM6;922                const tg = CM6.tags;923 924                // Lean "basic setup" — line numbers, active line, history/undo,925                // indent-on-input, bracket matching. (No autocomplete/search/lint;926                // this is a code-input box, not a full IDE.)927                const setup = [928                    lineNumbers(), highlightActiveLineGutter(), highlightActiveLine(),929                    drawSelection(), dropCursor(), history(), indentOnInput(), bracketMatching(),930                    keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),931                ];932 933                // Indigo / cyan / violet token theme so highlighting matches the UI.934                // Token colors reference CSS vars (--tok-*) so they flip with the theme.935                const cfHighlight = HighlightStyle.define([936                    { tag: [tg.keyword, tg.controlKeyword, tg.operatorKeyword, tg.modifier,937                            tg.definitionKeyword, tg.moduleKeyword], color: 'var(--tok-keyword)' },938                    { tag: [tg.function(tg.variableName), tg.function(tg.propertyName)], color: 'var(--tok-fn)' },939                    { tag: [tg.className, tg.typeName], color: 'var(--tok-fn)' },940                    { tag: [tg.string, tg.special(tg.string)], color: 'var(--tok-string)' },941                    { tag: [tg.number, tg.bool, tg.atom], color: 'var(--tok-number)' },942                    { tag: [tg.comment, tg.lineComment, tg.blockComment], color: 'var(--tok-comment)', fontStyle: 'italic' },943                    { tag: [tg.propertyName], color: 'var(--tok-prop)' },944                    { tag: [tg.operator, tg.punctuation, tg.bracket], color: 'var(--tok-punct)' },945                    { tag: [tg.variableName], color: 'var(--tok-var)' },946                    { tag: [tg.invalid], color: 'var(--tok-invalid)' },947                ]);948                const cfTheme = EditorView.theme({949                    '&': { color: 'var(--text)', backgroundColor: 'transparent', height: '100%' },950                    '.cm-content': { caretColor: 'var(--cyan)', padding: '12px 0' },951                    '.cm-cursor, .cm-dropCursor': { borderLeftColor: 'var(--cyan)' },952                    '&.cm-focused .cm-selectionBackground, .cm-selectionBackground': { backgroundColor: 'color-mix(in srgb, var(--cyan) 16%, transparent)' },953                    '.cm-gutters': { backgroundColor: 'transparent', color: 'var(--gutter)', border: 'none' },954                    '.cm-lineNumbers .cm-gutterElement': { padding: '0 8px 0 14px' },955                    '.cm-activeLineGutter': { backgroundColor: 'rgba(150,110,60,.07)', color: 'var(--text-dim)' },956                    '.cm-activeLine': { backgroundColor: 'rgba(150,110,60,.05)' },957                    '.cm-matchingBracket': { backgroundColor: 'rgba(156,111,79,.28)', outline: '1px solid var(--violet)' },958                    '.cm-line.cf-linked': { backgroundColor: 'color-mix(in srgb, var(--cyan) 14%, transparent)' },959                });960 961                // Node→code highlight: a line-decoration field toggled by an effect.962                const setHL = StateEffect.define();963                const lineDeco = Decoration.line({ class: 'cf-linked' });964                const hlField = StateField.define({965                    create: () => Decoration.none,966                    update(deco, tr) {967                        deco = deco.map(tr.changes);968                        for (const e of tr.effects) if (e.is(setHL)) {969                            if (!e.value) { deco = Decoration.none; continue; }970                            const { a, b } = e.value, ranges = [];971                            for (let n = a; n <= b; n++)972                                if (n >= 1 && n <= tr.state.doc.lines)973                                    ranges.push(lineDeco.range(tr.state.doc.line(n).from));974                            deco = Decoration.set(ranges, true);975                        }976                        return deco;977                    },978                    provide: f => EditorView.decorations.from(f),979                });980 981                langCompartment = new Compartment();982                cmView = new EditorView({983                    parent: document.getElementById('editor'),984                    state: EditorState.create({985                        doc: fallback.value || '',986                        extensions: [987                            setup,988                            langCompartment.of(await makeLangExt(langSelect.value)),989                            keymap.of([indentWithTab]),990                            placeholder("Paste your code here, or pick a code example…"),991                            syntaxHighlighting(cfHighlight),992                            cfTheme,993                            hlField,994                            EditorView.updateListener.of(u => {995                                if (u.selectionSet || u.docChanged) updateStatus();996                                if (u.focusChanged) editorWrap.classList.toggle('focused', u.view.hasFocus);997                                if (u.selectionSet || u.docChanged) highlightNodesForCursor(u.view);998                            }),999                        ],1000                    }),1001                });1002                // Expose the highlight/select ops to the node→code handlers.1003                cfHighlightLines = (a, b) => {1004                    if (!a) { cmView.dispatch({ effects: setHL.of(null) }); return; }1005                    const n = Math.min(Math.max(1, a), cmView.state.doc.lines);1006                    cmView.dispatch({ effects: [setHL.of({ a, b }),1007                        EditorView.scrollIntoView(cmView.state.doc.line(n).from, { y: 'center' })] });1008                };1009                cfSelectLines = (a, b) => {1010                    const doc = cmView.state.doc;1011                    a = Math.min(Math.max(1, a), doc.lines); b = Math.min(Math.max(1, b), doc.lines);1012                    cmView.dispatch({ selection: { anchor: doc.line(a).from, head: doc.line(b).to }, scrollIntoView: true });1013                    cmView.focus();1014                };1015 1016                fallback.hidden = true;1017                editorWrap.hidden = false;1018                updateStatus();1019            } catch (e) {1020                // CM unavailable → leave the (already-visible) textarea as the editor.1021                cmView = null;1022                console.warn('CodeMirror unavailable, using textarea fallback:', e);1023            }1024        }1025 1026        // Brief "Copied"/"Saved" confirmation on a button, then restore its label.1027        function flashOk(btn, msg) {1028            const orig = btn.dataset.orig || (btn.dataset.orig = btn.textContent);1029            btn.textContent = msg;1030            btn.classList.add('ok');1031            setTimeout(() => { btn.textContent = orig; btn.classList.remove('ok'); }, 1200);1032        }1033 1034        langSelect.addEventListener('change', () => setLanguage(langSelect.value));1035        copyBtn.addEventListener('click', async () => {1036            const text = getCode();1037            if (!text) return;1038            try { await navigator.clipboard.writeText(text); flashOk(copyBtn, 'Copied'); } catch {}1039        });1040        clearBtn.addEventListener('click', () => { setCode(''); focusEditor(); });1041 1042        /* ===== Diagram export (Copy Mermaid / SVG / PNG) =====1043           These act on the most recent render. `lastMermaid` holds the source1044           even on a parse error (so it stays copyable); SVG/PNG need a real svg. */1045        let lastMermaid = '';1046 1047        function refreshExportButtons() {1048            // A real generated chart (lastMermaid set), not the empty-state Start anchor.1049            const hasSvg = !!lastMermaid && !!target.querySelector('svg');1050            copyMmdBtn.disabled = !lastMermaid;1051            svgBtn.disabled = !hasSvg;1052            pngBtn.disabled = !hasSvg;1053            zoomCtrl.hidden = !hasSvg;   // zoom controls only make sense with a chart1054        }1055 1056        /* ===== Zoom (#3) — scales the SVG width via the --zoom custom property;1057           overflow + 'safe center' let the user scroll/pan a zoomed-in chart. ===== */1058        let zoom = 1;1059        const ZOOM_MIN = 0.4, ZOOM_MAX = 3, ZOOM_STEP = 1.2;1060        function applyZoom() {1061            target.style.setProperty('--zoom', zoom.toFixed(3));1062            updateScrollHint();1063        }1064        function setZoom(z) { zoom = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, z)); applyZoom(); }1065        zoomInBtn.addEventListener('click', () => setZoom(zoom * ZOOM_STEP));1066        zoomOutBtn.addEventListener('click', () => setZoom(zoom / ZOOM_STEP));1067        zoomFitBtn.addEventListener('click', () => setZoom(1));1068 1069        /* ===== Light / dark toggle (#9) — with a diagram crossfade ===== */1070        themeToggle.addEventListener('click', async () => {1071            const next = currentTheme === 'light' ? 'dark' : 'light';1072            const svg = (lastMermaid && target.querySelector('svg')) ? target.querySelector('svg') : null;1073            if (svg && !prefersReduced) {1074                // Fade the current chart out FIRST so the palette swap + re-render1075                // (Mermaid bakes some colors in, so a re-render is required) happen1076                // unseen — then fade the re-themed chart back in. Fade-through, not a1077                // dual-SVG dissolve: our node colors are CSS-var-driven, so a cloned1078                // "old" SVG would instantly re-resolve to the new theme.1079                await svg.animate([{ opacity: 1 }, { opacity: 0 }],1080                                  { duration: 150, easing: 'ease', fill: 'forwards' }).finished;1081                applyTheme(next);                  // swap vars + re-init Mermaid while invisible1082                await renderMermaid(lastMermaid);  // re-themed chart, mounted at full opacity1083                const fresh = target.querySelector('svg');1084                if (fresh) fresh.animate([{ opacity: 0 }, { opacity: 1 }],1085                                         { duration: 220, easing: 'ease', fill: 'both' });1086            } else {1087                applyTheme(next);1088                if (lastMermaid && target.querySelector('svg')) await renderMermaid(lastMermaid);1089            }1090        });1091 1092        function download(filename, blob) {1093            const url = URL.createObjectURL(blob);1094            const a = document.createElement('a');1095            a.href = url;1096            a.download = filename;1097            a.click();1098            setTimeout(() => URL.revokeObjectURL(url), 1000);1099        }1100 1101        // Serialize the rendered SVG, namespaced so it opens standalone.1102        function serializedSvg() {1103            const svg = target.querySelector('svg');1104            if (!svg) return null;1105            const clone = svg.cloneNode(true);1106            clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');1107            return { svg, data: new XMLSerializer().serializeToString(clone) };1108        }1109 1110        copyMmdBtn.addEventListener('click', async () => {1111            if (!lastMermaid) return;1112            try { await navigator.clipboard.writeText(lastMermaid); flashOk(copyMmdBtn, 'Copied'); } catch {}1113        });1114 1115        svgBtn.addEventListener('click', () => {1116            const s = serializedSvg();1117            if (!s) return;1118            download('flowchart.svg', new Blob([s.data], { type: 'image/svg+xml;charset=utf-8' }));1119            flashOk(svgBtn, 'Saved');1120        });1121 1122        pngBtn.addEventListener('click', () => {1123            const s = serializedSvg();1124            if (!s) return;1125            // Natural pixel size from the viewBox (the on-screen svg is CSS-stretched1126            // to 100%); render at 2× for a crisp export on a solid canvas bg.1127            const vb = s.svg.viewBox && s.svg.viewBox.baseVal;1128            const w = (vb && vb.width)  || s.svg.clientWidth  || 800;1129            const h = (vb && vb.height) || s.svg.clientHeight || 600;1130            const scale = 2;1131            const img = new Image();1132            img.onload = () => {1133                const canvas = document.createElement('canvas');1134                canvas.width = Math.round(w * scale);1135                canvas.height = Math.round(h * scale);1136                const ctx = canvas.getContext('2d');1137                // Solid canvas bg = the current theme's --bg-inset, so labels stay readable.1138                ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--bg-inset').trim() || '#e2e7d6';1139                ctx.fillRect(0, 0, canvas.width, canvas.height);1140                ctx.drawImage(img, 0, 0, canvas.width, canvas.height);1141                canvas.toBlob(b => { if (b) { download('flowchart.png', b); flashOk(pngBtn, 'Saved'); } }, 'image/png');1142            };1143            img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(s.data);1144        });1145 1146        // Simulated inference time for the pre-rendered demo path. The bar1147        // tracks this exactly and the chart is revealed at 100% — tune freely.1148        const PRESET_DURATION_MS = 2000;1149 1150        let renderSeq = 0; // guards against out-of-order async renders1151 1152        // Show the "scroll" hint only when the chart overflows its box AND the1153        // user isn't already scrolled to the bottom. Hides at the bottom,1154        // reappears as soon as they scroll back up.1155        function updateScrollHint() {1156            const overflowing = target.scrollHeight > target.clientHeight + 4;1157            const atBottom =1158                target.scrollTop + target.clientHeight >= target.scrollHeight - 4;1159            scrollHint.hidden = !(overflowing && !atBottom);1160        }1161        window.addEventListener('resize', updateScrollHint);1162        target.addEventListener('scroll', updateScrollHint);1163 1164        /* ===== Diagram-reveal animation (#1) — "Trace the path" + "Calm" =====1165           A new chart draws itself in: the topmost node is the persistent anchor1166           (it stays put), and everything else flows OUT of it top-to-bottom — nodes1167           scale in, edges draw on (stroke dash), each arrowhead landing as its line1168           arrives. Runs ONLY on a fresh Generate (not zoom / theme / re-layout) and1169           is skipped under prefers-reduced-motion. */1170        const REVEAL_STEP = 95;   // ms between successive elements — "Calm"1171        const prefersReduced = matchMedia('(prefers-reduced-motion: reduce)').matches;1172        let anchorNode = null;    // topmost node — never animates1173        let edgeMarkers = new Map();1174        let revealToken = 0;      // bumped per reveal so a stale scroll-follow loop self-cancels1175        const yTop = el => el.getBoundingClientRect().top;1176 1177        function animNodeIn(n, delay) {1178            // Animate the INDIVIDUAL scale/translate props (not `transform`) so we1179            // compose with Mermaid's positioning transform="translate(x,y)" attribute1180            // instead of overriding it (which would pile every node in the corner).1181            n.animate(1182                [{ opacity: 0, scale: '.97', translate: '0 6px' }, { opacity: 1, scale: '1', translate: '0 0' }],1183                { duration: 280, delay, easing: 'cubic-bezier(.2,.7,.3,1)', fill: 'both' }1184            );1185        }1186        function animEdgeIn(e, delay) {1187            const len = e.getTotalLength();1188            e.style.strokeDasharray = len;1189            const a = e.animate(1190                [{ strokeDashoffset: len }, { strokeDashoffset: 0 }],1191                { duration: 440, delay, easing: 'ease-in-out', fill: 'both' }1192            );1193            a.onfinish = () => {                 // land the arrowhead when the line arrives1194                const m = edgeMarkers.get(e);1195                if (m?.end) e.setAttribute('marker-end', m.end);1196                if (m?.start) e.setAttribute('marker-start', m.start);1197                e.style.strokeDashoffset = 0;1198            };1199        }1200        function fadeIn(l, delay) {

Showing the first 1,200 of 1484 lines. Download the file for the rest.