Vibecode Gleek
track this build4 phases, 12 steps, beginner friendly0%Gleek's solo core is compact: build a browser-based keyboard diagramming editor with drag, resize, connect, keyboard shortcuts, autosave, and SVG or PNG export. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on interaction polish, collaboration.
You are building a lean indie version of Gleek. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Gleek · indie build A text-to-diagram editor whose hard problems are staying useful while the source is half-typed, keeping a hand-placed box where you put it after the text changes, and producing an export that still looks right in a program you did not write. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | React with TypeScript and Vite, everything in the browser and no server at all | a diagram tool that needs a backend is a diagram tool you cannot use on a train | | Source of truth | The diagram is the text; the canvas is a view of it | undo, autosave and export all become easy once there is only one thing to save | | Storage | IndexedDB holding a versioned document record, with autosave on a debounce | browser storage fails in more ways than people expect and the format needs a version from day one | | Layout | A deterministic layered layout recomputed only when the graph structure changes | a layout that reruns on every keystroke makes the diagram unreadable while you type | | Export | SVG with the font embedded, and PNG rendered from that same SVG at a chosen scale | an SVG rendered as an image cannot fetch anything, so the font has to be inside it | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds and serves the application. Nothing runs server-side once it is built. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **One typeface with a licence that permits embedding** · free - Why: The export embeds it in the file, which is a distribution of the font, and not every licence allows that. - Get it: Pick an open licensed family and read its terms before embedding. - Verify: The licence text permits embedding and a copy sits in the repository - [ ] **A second SVG viewer that is not your browser** · free - Why: Browsers are forgiving about fonts in ways other renderers are not, and the export bug in this category only shows up elsewhere. - Get it: A vector editor, a document previewer, anything that renders SVG independently. - Verify: An exported diagram opens in it with correct text - [ ] **The grammar, written down before the parser** · free - Why: Every later decision about layout, overrides and export follows from what the source text is allowed to say. - Get it: Write the statements you want to type, then design the grammar that accepts exactly those. - Verify: Every statement in the documentation parses and is covered by a test ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multiplayer cursors and comments. - High-fidelity import of other tools' formats. - A large template library. - Freeform shapes with the canvas as the source of truth. - multiplayer cursors and comments - large template libraries - high-fidelity import and export - presentation and enterprise controls If one of those is essential to you, that is the reason to keep paying for Gleek, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Gleek The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a keyboard-first diagram editor that turns a few lines of text into a picture you can export to replace Gleek. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: React with TypeScript and Vite, everything in the browser and no server at all · a diagram tool that needs a backend is a diagram tool you cannot use on a train - Source of truth: The diagram is the text; the canvas is a view of it · undo, autosave and export all become easy once there is only one thing to save - Storage: IndexedDB holding a versioned document record, with autosave on a debounce · browser storage fails in more ways than people expect and the format needs a version from day one - Layout: A deterministic layered layout recomputed only when the graph structure changes · a layout that reruns on every keystroke makes the diagram unreadable while you type - Export: SVG with the font embedded, and PNG rendered from that same SVG at a chosen scale · an SVG rendered as an image cannot fetch anything, so the font has to be inside it ### Data model (create this before Phase 1) - documents(id, title, source_text, format_version, created_at, updated_at, last_opened_at) · the source text is the document and everything else is a cache - revisions(document_id, ordinal, source_text, at) · a capped ring of previous sources, which is a cheaper undo than a command stack and survives a reload - nodes(doc_id, node_id, label, shape) · derived from the source by the parser, never edited directly - edges(doc_id, from_node, to_node, label, style) · likewise derived - overrides(doc_id, node_id, dx, dy, pinned) · manual nudges keyed on the node identifier, which is what lets them survive a re-parse - parse_errors(doc_id, line, column, message) · so a mistake points at a line rather than blanking the canvas - Nothing in this schema stores pixel coordinates as the truth. The layout computes them and the overrides adjust them, which means a diagram opened on a different screen is still the same diagram. ### Phase 1 · Text in, graph out, errors on a line Build: Define a compact grammar for nodes, edges, labels and a few shapes. Parse to a graph with stable node identifiers derived from the declared names. On a parse error, keep the last good graph on screen, mark the offending line and column in the editor gutter, and say what was expected. Trap: Throwing away the graph on the first parse error means the diagram vanishes the moment you start typing a new line, which is every few seconds, and the flicker makes the tool unusable for exactly the keyboard-driven workflow that is its reason to exist. Keep the last successful parse rendered, overlay the error on the line it came from, and only replace the graph when a parse succeeds. Done when: Typing an incomplete line leaves the previous diagram on screen with an error marker on that line, tested by typing a partial statement. A parse error reports a line and column and a description of what was expected, checked against three deliberately broken inputs. Node identifiers are derived from declared names and are stable across re-parses, asserted in the suite. An empty document renders an empty canvas with a hint rather than an error. Do not build yet: No layout, no dragging, no export and no persistence yet. ### Phase 2 · A layout that does not jump Build: Compute a layered layout from the graph, ordering nodes by their declaration order so the result is deterministic. Recompute only when the set of nodes or edges changes, not when a label changes. Animate position changes over a short interval so a genuine structural change is followable. Trap: Recomputing the layout on every keystroke means that adding a character to a label reshuffles the entire diagram, and since typing is continuous the picture is in permanent motion and you can never point at anything. Hash the structural part of the graph, recompute only when that hash changes, and treat a label edit as a repaint. Done when: Two runs over the same source produce byte-identical positions, tested by comparing serialised layouts. Editing a label repaints without recomputing the layout, verified by counting layout calls in a test. Adding a node animates the change rather than snapping, checked by eye and by asserting the transition exists. A graph with a cycle lays out without hanging, tested with a deliberate cycle. Do not build yet: No dragging, no export and no persistence yet. ### Phase 3 · Nudges that survive the next edit Build: Dragging a node stores an offset against its node identifier rather than an absolute position. The layout runs as normal and the offset is applied afterwards. A pinned node keeps its offset even when the layout moves everything around it, and a reset clears offsets per node or entirely. Trap: Storing a nudge against the node's position in an array means inserting a line anywhere above it silently transfers that nudge to a different box, and the user watches an unrelated shape drift across the canvas with no idea why. Key the override on the node identifier from the parser, and drop overrides for identifiers that no longer exist. Done when: Nudging a node and then adding an unrelated node above it in the source leaves the nudge on the original node, tested. Deleting a node removes its override rather than leaving an orphan row, tested. A per-node reset and a reset-all both work and are undoable. Keyboard arrows nudge the selected node by a fixed step and shift-arrow by a larger one, tested. Do not build yet: No export and no persistence yet. ### Phase 4 · An export that opens somewhere else Build: Export SVG with the typeface embedded as a data URI inside the file, and a fallback stack on every text element. Render PNG by drawing that same SVG into a canvas at a chosen scale. Offer the source text as an export too, since it is the smallest and most durable form of the diagram. Trap: An exported SVG that names a web font by family renders in whatever the viewer happens to have, because the SVG specification places a document referenced as an image into a secure processing mode where external references are not resolved at all, so the font never loads, the metrics change and every label overflows its box. Embed the font in the file as a data URI or convert text to paths, and check the export in a viewer that is not a browser tab you already had open. Done when: An exported SVG opened as an image in a separate program shows the same typeface and no overflowing labels, checked by hand in at least two viewers. The PNG export honours a scale factor and the output dimensions match the requested scale exactly, tested. Exporting the source text round-trips: importing it reproduces the diagram including overrides, tested. Export never contains a reference to a URL outside the file, asserted by scanning the output. Do not build yet: No persistence, no document list and no quota handling yet. ### Phase 5 · Work that cannot be lost Build: Autosave the source on a debounce into IndexedDB with a format version on every record. Keep a capped ring of previous sources so a reload still has undo. Detect storage being unavailable or full and fall back to keeping the document in memory with a visible warning and a download prompt. Import and export whole documents as files. Trap: Assuming browser storage is always there means that in a private window, or with site data blocked, or when the quota is full, the autosave silently fails and the user loses an afternoon on a refresh with no warning at any point. Wrap every storage call, surface a failure as a banner offering an immediate download, and never let a save error be swallowed. Done when: Reloading mid-edit restores the document and its undo history, tested. With storage blocked, the app runs, shows a persistent warning and offers a download, tested by denying access in the test environment. Every stored record carries a format version and a record written under an older version still opens, tested with a fixture. Documents export as files and import back with identical source and overrides. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer cursors and comments, which need a server, a presence protocol and conflict handling, and are the reason the paid tier exists. - Importing other tools' diagram formats at high fidelity, which is a long tail of undocumented quirks rather than a feature. - A large template library, since templates are content and this is a weekend of code, not a weekend of drawing. - Arbitrary freeform shapes and connectors, because the moment the canvas becomes the source of truth the text stops being the document and the whole design collapses. ### README must contain - That the source text is the document and everything else can be recomputed, which is why exports include it - The grammar in full, with one example of every statement the parser accepts - That exported SVGs embed the typeface because an SVG rendered as an image cannot resolve external references - That nudges are stored against node identifiers, so renaming a node loses its nudge and that is deliberate - Where documents are stored in the browser, what happens when that storage is unavailable, and how to get everything out ===== AGENTS.md ===== # Agent instructions · Gleek indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: React with TypeScript and Vite, everything in the browser and no server at all, The diagram is the text; the canvas is a view of it, IndexedDB holding a versioned document record, with autosave on a debounce, A deterministic layered layout recomputed only when the graph structure changes, SVG with the font embedded, and PNG rendered from that same SVG at a chosen scale. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Throwing away the graph on the first parse error means the diagram vanishes the moment you start typing a new line, which is every few seconds, and the flicker makes the tool unusable for exactly the keyboard-driven workflow that is its reason to exist. Keep the last successful parse rendered, overlay the error on the line it came from, and only replace the graph when a parse succeeds. - Recomputing the layout on every keystroke means that adding a character to a label reshuffles the entire diagram, and since typing is continuous the picture is in permanent motion and you can never point at anything. Hash the structural part of the graph, recompute only when that hash changes, and treat a label edit as a repaint. - Storing a nudge against the node's position in an array means inserting a line anywhere above it silently transfers that nudge to a different box, and the user watches an unrelated shape drift across the canvas with no idea why. Key the override on the node identifier from the parser, and drop overrides for identifiers that no longer exist. - An exported SVG that names a web font by family renders in whatever the viewer happens to have, because the SVG specification places a document referenced as an image into a secure processing mode where external references are not resolved at all, so the font never loads, the metrics change and every label overflows its box. Embed the font in the file as a data URI or convert text to paths, and check the export in a viewer that is not a browser tab you already had open. ===== BUILD_PLAN.md ===== # Build plan · Gleek A text-to-diagram editor whose hard problems are staying useful while the source is half-typed, keeping a hand-placed box where you put it after the text changes, and producing an export that still looks right in a program you did not write. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Text in, graph out, errors on a line A small grammar parses to nodes and edges, and a half-typed line never blanks the canvas. ### Steps 1. Write the grammar down before the parser A grammar you can read is a grammar you can extend on a Tuesday. Files: `docs/grammar.md`, `src/parse/grammar.ts` 2. Keep the last good graph when a parse fails The canvas flickering is what kills this kind of editor. Files: `src/state/document.ts`, `tests/parse-recovery.test.ts` 3. Report errors with a line, a column and an expectation Somewhere is not a useful place for a mistake to be. Files: `src/parse/errors.ts` ### Done when - [ ] A half-typed line keeps the previous diagram visible - [ ] Errors carry line, column and expectation - [ ] Node identifiers are stable across re-parses - [ ] An empty document is a hint, not an error ### Watch out - Throwing away the graph on the first parse error means the diagram vanishes the moment you start typing a new line, which is every few seconds, and the flicker makes the tool unusable for exactly the keyboard-driven workflow that is its reason to exist. Keep the last successful parse rendered, overlay the error on the line it came from, and only replace the graph when a parse succeeds. ## Phase 2 · A layout that does not jump The same graph always lays out the same way, and relabelling a box does not rearrange the diagram. ### Steps 1. Order by declaration so layout is deterministic A layout that differs between runs makes every diff meaningless. Files: `src/layout/layered.ts`, `tests/deterministic.test.ts` 2. Hash the structure and recompute only on change A label edit is a repaint, not a rearrangement. Files: `src/layout/cache.ts` 3. Animate real structural changes So you can see what moved rather than finding it again. Files: `src/canvas/transition.ts` ### Done when - [ ] The same source always lays out identically - [ ] Label edits do not trigger a relayout - [ ] Structural changes animate - [ ] A cyclic graph lays out without hanging ### Watch out - Recomputing the layout on every keystroke means that adding a character to a label reshuffles the entire diagram, and since typing is continuous the picture is in permanent motion and you can never point at anything. Hash the structural part of the graph, recompute only when that hash changes, and treat a label edit as a repaint. ## Phase 3 · Nudges that survive the next edit Dragging a box to where you want it keeps it there after the text changes. ### Steps 1. Key overrides on the node identifier Positional keys migrate to the wrong shape. Files: `src/state/overrides.ts`, `tests/overrides.test.ts` 2. Apply offsets after layout, never instead of it The layout still has to make sense for everything you did not touch. Files: `src/layout/apply.ts` 3. Give keyboard nudging the same powers as the mouse This is a keyboard tool and the mouse is the afterthought. Files: `src/canvas/keyboard.ts` ### Done when - [ ] A nudge stays with its node when the source changes - [ ] Deleting a node drops its override - [ ] Per-node and global resets both work - [ ] Arrow keys nudge the selection ### Watch out - Storing a nudge against the node's position in an array means inserting a line anywhere above it silently transfers that nudge to a different box, and the user watches an unrelated shape drift across the canvas with no idea why. Key the override on the node identifier from the parser, and drop overrides for identifiers that no longer exist. ## Phase 4 · An export that opens somewhere else The exported SVG and PNG look like what was on screen, in programs that are not this one. ### Steps 1. Embed the font in the SVG, or convert text to paths An exported SVG cannot go and fetch anything. Files: `src/export/svg.ts`, `tests/no-external-refs.test.ts` 2. Render PNG from the same SVG at a stated scale One rendering path means one set of bugs. Files: `src/export/png.ts` 3. Export the source text as a first-class format It is the version that will still open in ten years. Files: `src/export/source.ts` ### Done when - [ ] An exported SVG renders correctly outside the app - [ ] PNG dimensions match the requested scale - [ ] Source export and import round-trips including overrides - [ ] No export references anything outside itself ### Watch out - An exported SVG that names a web font by family renders in whatever the viewer happens to have, because the SVG specification places a document referenced as an image into a secure processing mode where external references are not resolved at all, so the font never loads, the metrics change and every label overflows its box. Embed the font in the file as a data URI or convert text to paths, and check the export in a viewer that is not a browser tab you already had open. ## Not in this build - Multiplayer cursors and comments. - High-fidelity import of other tools' formats. - A large template library. - Freeform shapes with the canvas as the source of truth. ## After v1, if you want it - A second layout direction, since left-to-right and top-to-bottom suit different diagrams and the graph does not change - A print stylesheet that puts one diagram per page at a sensible scale, which is surprisingly often what people wanted - A compact share format that encodes the source in a link, so a diagram travels without a file ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Optional. Shown in the tab. There are no secrets in this build because there is no server. VITE_APP_TITLE=Diagrams # Required. Debounce before a save. Lower costs writes, higher costs your afternoon. VITE_AUTOSAVE_MS=800 # Required. How many previous sources are kept for undo across reloads. VITE_REVISION_RING=50 # Required. Above this the layout and the canvas both stop being pleasant, so warn rather than freeze. VITE_MAX_NODES=300
You are building a lean indie version of Gleek. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Gleek · indie build A text-to-diagram editor whose hard problems are staying useful while the source is half-typed, keeping a hand-placed box where you put it after the text changes, and producing an export that still looks right in a program you did not write. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | React with TypeScript and Vite, everything in the browser and no server at all | a diagram tool that needs a backend is a diagram tool you cannot use on a train | | Source of truth | The diagram is the text; the canvas is a view of it | undo, autosave and export all become easy once there is only one thing to save | | Storage | IndexedDB holding a versioned document record, with autosave on a debounce | browser storage fails in more ways than people expect and the format needs a version from day one | | Layout | A deterministic layered layout recomputed only when the graph structure changes | a layout that reruns on every keystroke makes the diagram unreadable while you type | | Export | SVG with the font embedded, and PNG rendered from that same SVG at a chosen scale | an SVG rendered as an image cannot fetch anything, so the font has to be inside it | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds and serves the application. Nothing runs server-side once it is built. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **One typeface with a licence that permits embedding** · free - Why: The export embeds it in the file, which is a distribution of the font, and not every licence allows that. - Get it: Pick an open licensed family and read its terms before embedding. - Verify: The licence text permits embedding and a copy sits in the repository - [ ] **A second SVG viewer that is not your browser** · free - Why: Browsers are forgiving about fonts in ways other renderers are not, and the export bug in this category only shows up elsewhere. - Get it: A vector editor, a document previewer, anything that renders SVG independently. - Verify: An exported diagram opens in it with correct text - [ ] **The grammar, written down before the parser** · free - Why: Every later decision about layout, overrides and export follows from what the source text is allowed to say. - Get it: Write the statements you want to type, then design the grammar that accepts exactly those. - Verify: Every statement in the documentation parses and is covered by a test ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multiplayer cursors and comments. - High-fidelity import of other tools' formats. - A large template library. - Freeform shapes with the canvas as the source of truth. - multiplayer cursors and comments - large template libraries - high-fidelity import and export - presentation and enterprise controls If one of those is essential to you, that is the reason to keep paying for Gleek, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Gleek The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a keyboard-first diagram editor that turns a few lines of text into a picture you can export to replace Gleek. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: React with TypeScript and Vite, everything in the browser and no server at all · a diagram tool that needs a backend is a diagram tool you cannot use on a train - Source of truth: The diagram is the text; the canvas is a view of it · undo, autosave and export all become easy once there is only one thing to save - Storage: IndexedDB holding a versioned document record, with autosave on a debounce · browser storage fails in more ways than people expect and the format needs a version from day one - Layout: A deterministic layered layout recomputed only when the graph structure changes · a layout that reruns on every keystroke makes the diagram unreadable while you type - Export: SVG with the font embedded, and PNG rendered from that same SVG at a chosen scale · an SVG rendered as an image cannot fetch anything, so the font has to be inside it ### Data model (create this before Phase 1) - documents(id, title, source_text, format_version, created_at, updated_at, last_opened_at) · the source text is the document and everything else is a cache - revisions(document_id, ordinal, source_text, at) · a capped ring of previous sources, which is a cheaper undo than a command stack and survives a reload - nodes(doc_id, node_id, label, shape) · derived from the source by the parser, never edited directly - edges(doc_id, from_node, to_node, label, style) · likewise derived - overrides(doc_id, node_id, dx, dy, pinned) · manual nudges keyed on the node identifier, which is what lets them survive a re-parse - parse_errors(doc_id, line, column, message) · so a mistake points at a line rather than blanking the canvas - Nothing in this schema stores pixel coordinates as the truth. The layout computes them and the overrides adjust them, which means a diagram opened on a different screen is still the same diagram. ### Phase 1 · Text in, graph out, errors on a line Build: Define a compact grammar for nodes, edges, labels and a few shapes. Parse to a graph with stable node identifiers derived from the declared names. On a parse error, keep the last good graph on screen, mark the offending line and column in the editor gutter, and say what was expected. Trap: Throwing away the graph on the first parse error means the diagram vanishes the moment you start typing a new line, which is every few seconds, and the flicker makes the tool unusable for exactly the keyboard-driven workflow that is its reason to exist. Keep the last successful parse rendered, overlay the error on the line it came from, and only replace the graph when a parse succeeds. Done when: Typing an incomplete line leaves the previous diagram on screen with an error marker on that line, tested by typing a partial statement. A parse error reports a line and column and a description of what was expected, checked against three deliberately broken inputs. Node identifiers are derived from declared names and are stable across re-parses, asserted in the suite. An empty document renders an empty canvas with a hint rather than an error. Do not build yet: No layout, no dragging, no export and no persistence yet. ### Phase 2 · A layout that does not jump Build: Compute a layered layout from the graph, ordering nodes by their declaration order so the result is deterministic. Recompute only when the set of nodes or edges changes, not when a label changes. Animate position changes over a short interval so a genuine structural change is followable. Trap: Recomputing the layout on every keystroke means that adding a character to a label reshuffles the entire diagram, and since typing is continuous the picture is in permanent motion and you can never point at anything. Hash the structural part of the graph, recompute only when that hash changes, and treat a label edit as a repaint. Done when: Two runs over the same source produce byte-identical positions, tested by comparing serialised layouts. Editing a label repaints without recomputing the layout, verified by counting layout calls in a test. Adding a node animates the change rather than snapping, checked by eye and by asserting the transition exists. A graph with a cycle lays out without hanging, tested with a deliberate cycle. Do not build yet: No dragging, no export and no persistence yet. ### Phase 3 · Nudges that survive the next edit Build: Dragging a node stores an offset against its node identifier rather than an absolute position. The layout runs as normal and the offset is applied afterwards. A pinned node keeps its offset even when the layout moves everything around it, and a reset clears offsets per node or entirely. Trap: Storing a nudge against the node's position in an array means inserting a line anywhere above it silently transfers that nudge to a different box, and the user watches an unrelated shape drift across the canvas with no idea why. Key the override on the node identifier from the parser, and drop overrides for identifiers that no longer exist. Done when: Nudging a node and then adding an unrelated node above it in the source leaves the nudge on the original node, tested. Deleting a node removes its override rather than leaving an orphan row, tested. A per-node reset and a reset-all both work and are undoable. Keyboard arrows nudge the selected node by a fixed step and shift-arrow by a larger one, tested. Do not build yet: No export and no persistence yet. ### Phase 4 · An export that opens somewhere else Build: Export SVG with the typeface embedded as a data URI inside the file, and a fallback stack on every text element. Render PNG by drawing that same SVG into a canvas at a chosen scale. Offer the source text as an export too, since it is the smallest and most durable form of the diagram. Trap: An exported SVG that names a web font by family renders in whatever the viewer happens to have, because the SVG specification places a document referenced as an image into a secure processing mode where external references are not resolved at all, so the font never loads, the metrics change and every label overflows its box. Embed the font in the file as a data URI or convert text to paths, and check the export in a viewer that is not a browser tab you already had open. Done when: An exported SVG opened as an image in a separate program shows the same typeface and no overflowing labels, checked by hand in at least two viewers. The PNG export honours a scale factor and the output dimensions match the requested scale exactly, tested. Exporting the source text round-trips: importing it reproduces the diagram including overrides, tested. Export never contains a reference to a URL outside the file, asserted by scanning the output. Do not build yet: No persistence, no document list and no quota handling yet. ### Phase 5 · Work that cannot be lost Build: Autosave the source on a debounce into IndexedDB with a format version on every record. Keep a capped ring of previous sources so a reload still has undo. Detect storage being unavailable or full and fall back to keeping the document in memory with a visible warning and a download prompt. Import and export whole documents as files. Trap: Assuming browser storage is always there means that in a private window, or with site data blocked, or when the quota is full, the autosave silently fails and the user loses an afternoon on a refresh with no warning at any point. Wrap every storage call, surface a failure as a banner offering an immediate download, and never let a save error be swallowed. Done when: Reloading mid-edit restores the document and its undo history, tested. With storage blocked, the app runs, shows a persistent warning and offers a download, tested by denying access in the test environment. Every stored record carries a format version and a record written under an older version still opens, tested with a fixture. Documents export as files and import back with identical source and overrides. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer cursors and comments, which need a server, a presence protocol and conflict handling, and are the reason the paid tier exists. - Importing other tools' diagram formats at high fidelity, which is a long tail of undocumented quirks rather than a feature. - A large template library, since templates are content and this is a weekend of code, not a weekend of drawing. - Arbitrary freeform shapes and connectors, because the moment the canvas becomes the source of truth the text stops being the document and the whole design collapses. ### README must contain - That the source text is the document and everything else can be recomputed, which is why exports include it - The grammar in full, with one example of every statement the parser accepts - That exported SVGs embed the typeface because an SVG rendered as an image cannot resolve external references - That nudges are stored against node identifiers, so renaming a node loses its nudge and that is deliberate - Where documents are stored in the browser, what happens when that storage is unavailable, and how to get everything out ===== AGENTS.md ===== # Agent instructions · Gleek indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: React with TypeScript and Vite, everything in the browser and no server at all, The diagram is the text; the canvas is a view of it, IndexedDB holding a versioned document record, with autosave on a debounce, A deterministic layered layout recomputed only when the graph structure changes, SVG with the font embedded, and PNG rendered from that same SVG at a chosen scale. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Throwing away the graph on the first parse error means the diagram vanishes the moment you start typing a new line, which is every few seconds, and the flicker makes the tool unusable for exactly the keyboard-driven workflow that is its reason to exist. Keep the last successful parse rendered, overlay the error on the line it came from, and only replace the graph when a parse succeeds. - Recomputing the layout on every keystroke means that adding a character to a label reshuffles the entire diagram, and since typing is continuous the picture is in permanent motion and you can never point at anything. Hash the structural part of the graph, recompute only when that hash changes, and treat a label edit as a repaint. - Storing a nudge against the node's position in an array means inserting a line anywhere above it silently transfers that nudge to a different box, and the user watches an unrelated shape drift across the canvas with no idea why. Key the override on the node identifier from the parser, and drop overrides for identifiers that no longer exist. - An exported SVG that names a web font by family renders in whatever the viewer happens to have, because the SVG specification places a document referenced as an image into a secure processing mode where external references are not resolved at all, so the font never loads, the metrics change and every label overflows its box. Embed the font in the file as a data URI or convert text to paths, and check the export in a viewer that is not a browser tab you already had open. ===== BUILD_PLAN.md ===== # Build plan · Gleek A text-to-diagram editor whose hard problems are staying useful while the source is half-typed, keeping a hand-placed box where you put it after the text changes, and producing an export that still looks right in a program you did not write. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Text in, graph out, errors on a line A small grammar parses to nodes and edges, and a half-typed line never blanks the canvas. ### Steps 1. Write the grammar down before the parser A grammar you can read is a grammar you can extend on a Tuesday. Files: `docs/grammar.md`, `src/parse/grammar.ts` 2. Keep the last good graph when a parse fails The canvas flickering is what kills this kind of editor. Files: `src/state/document.ts`, `tests/parse-recovery.test.ts` 3. Report errors with a line, a column and an expectation Somewhere is not a useful place for a mistake to be. Files: `src/parse/errors.ts` ### Done when - [ ] A half-typed line keeps the previous diagram visible - [ ] Errors carry line, column and expectation - [ ] Node identifiers are stable across re-parses - [ ] An empty document is a hint, not an error ### Watch out - Throwing away the graph on the first parse error means the diagram vanishes the moment you start typing a new line, which is every few seconds, and the flicker makes the tool unusable for exactly the keyboard-driven workflow that is its reason to exist. Keep the last successful parse rendered, overlay the error on the line it came from, and only replace the graph when a parse succeeds. ## Phase 2 · A layout that does not jump The same graph always lays out the same way, and relabelling a box does not rearrange the diagram. ### Steps 1. Order by declaration so layout is deterministic A layout that differs between runs makes every diff meaningless. Files: `src/layout/layered.ts`, `tests/deterministic.test.ts` 2. Hash the structure and recompute only on change A label edit is a repaint, not a rearrangement. Files: `src/layout/cache.ts` 3. Animate real structural changes So you can see what moved rather than finding it again. Files: `src/canvas/transition.ts` ### Done when - [ ] The same source always lays out identically - [ ] Label edits do not trigger a relayout - [ ] Structural changes animate - [ ] A cyclic graph lays out without hanging ### Watch out - Recomputing the layout on every keystroke means that adding a character to a label reshuffles the entire diagram, and since typing is continuous the picture is in permanent motion and you can never point at anything. Hash the structural part of the graph, recompute only when that hash changes, and treat a label edit as a repaint. ## Phase 3 · Nudges that survive the next edit Dragging a box to where you want it keeps it there after the text changes. ### Steps 1. Key overrides on the node identifier Positional keys migrate to the wrong shape. Files: `src/state/overrides.ts`, `tests/overrides.test.ts` 2. Apply offsets after layout, never instead of it The layout still has to make sense for everything you did not touch. Files: `src/layout/apply.ts` 3. Give keyboard nudging the same powers as the mouse This is a keyboard tool and the mouse is the afterthought. Files: `src/canvas/keyboard.ts` ### Done when - [ ] A nudge stays with its node when the source changes - [ ] Deleting a node drops its override - [ ] Per-node and global resets both work - [ ] Arrow keys nudge the selection ### Watch out - Storing a nudge against the node's position in an array means inserting a line anywhere above it silently transfers that nudge to a different box, and the user watches an unrelated shape drift across the canvas with no idea why. Key the override on the node identifier from the parser, and drop overrides for identifiers that no longer exist. ## Phase 4 · An export that opens somewhere else The exported SVG and PNG look like what was on screen, in programs that are not this one. ### Steps 1. Embed the font in the SVG, or convert text to paths An exported SVG cannot go and fetch anything. Files: `src/export/svg.ts`, `tests/no-external-refs.test.ts` 2. Render PNG from the same SVG at a stated scale One rendering path means one set of bugs. Files: `src/export/png.ts` 3. Export the source text as a first-class format It is the version that will still open in ten years. Files: `src/export/source.ts` ### Done when - [ ] An exported SVG renders correctly outside the app - [ ] PNG dimensions match the requested scale - [ ] Source export and import round-trips including overrides - [ ] No export references anything outside itself ### Watch out - An exported SVG that names a web font by family renders in whatever the viewer happens to have, because the SVG specification places a document referenced as an image into a secure processing mode where external references are not resolved at all, so the font never loads, the metrics change and every label overflows its box. Embed the font in the file as a data URI or convert text to paths, and check the export in a viewer that is not a browser tab you already had open. ## Not in this build - Multiplayer cursors and comments. - High-fidelity import of other tools' formats. - A large template library. - Freeform shapes with the canvas as the source of truth. ## After v1, if you want it - A second layout direction, since left-to-right and top-to-bottom suit different diagrams and the graph does not change - A print stylesheet that puts one diagram per page at a sensible scale, which is surprisingly often what people wanted - A compact share format that encodes the source in a link, so a diagram travels without a file ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Optional. Shown in the tab. There are no secrets in this build because there is no server. VITE_APP_TITLE=Diagrams # Required. Debounce before a save. Lower costs writes, higher costs your afternoon. VITE_AUTOSAVE_MS=800 # Required. How many previous sources are kept for undo across reloads. VITE_REVISION_RING=50 # Required. Above this the layout and the canvas both stop being pleasant, so warn rather than freeze. VITE_MAX_NODES=300
You are building a production product version of Gleek. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Gleek · product brief ## Problem Gleek's solo core is compact: build a browser-based keyboard diagramming editor with drag, resize, connect, keyboard shortcuts, autosave, and SVG or PNG export. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on interaction polish, collaboration. ## Product outcome A diagram editor where the text is the document, the picture stops moving while you type, a box you dragged stays dragged, and the exported file looks the same in a program that is not a browser. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - modern browser - no account required ## Explicit non-goals for v1 - Multiplayer cursors and comments. - High-fidelity import of other tools' formats. - A large template library. - Freeform shapes with the canvas as the source of truth. - multiplayer cursors and comments - large template libraries - high-fidelity import and export - presentation and enterprise controls ## Success criteria - A parse error is confirmed to keep the previous diagram visible - The same source is confirmed to lay out identically twice - A nudge is confirmed to survive an unrelated source edit - An exported SVG is confirmed correct in a non-browser viewer - Blocked storage is confirmed to warn and offer a download ===== BRIEF.md ===== # Build brief · Gleek The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a keyboard-first diagram editor that turns a few lines of text into a picture you can export to replace Gleek. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: React with TypeScript and Vite, everything in the browser and no server at all · a diagram tool that needs a backend is a diagram tool you cannot use on a train - Source of truth: The diagram is the text; the canvas is a view of it · undo, autosave and export all become easy once there is only one thing to save - Storage: IndexedDB holding a versioned document record, with autosave on a debounce · browser storage fails in more ways than people expect and the format needs a version from day one - Layout: A deterministic layered layout recomputed only when the graph structure changes · a layout that reruns on every keystroke makes the diagram unreadable while you type - Export: SVG with the font embedded, and PNG rendered from that same SVG at a chosen scale · an SVG rendered as an image cannot fetch anything, so the font has to be inside it ### Data model (create this before Phase 1) - documents(id, title, source_text, format_version, created_at, updated_at, last_opened_at) · the source text is the document and everything else is a cache - revisions(document_id, ordinal, source_text, at) · a capped ring of previous sources, which is a cheaper undo than a command stack and survives a reload - nodes(doc_id, node_id, label, shape) · derived from the source by the parser, never edited directly - edges(doc_id, from_node, to_node, label, style) · likewise derived - overrides(doc_id, node_id, dx, dy, pinned) · manual nudges keyed on the node identifier, which is what lets them survive a re-parse - parse_errors(doc_id, line, column, message) · so a mistake points at a line rather than blanking the canvas - Nothing in this schema stores pixel coordinates as the truth. The layout computes them and the overrides adjust them, which means a diagram opened on a different screen is still the same diagram. ### Phase 1 · Text in, graph out, errors on a line Build: Define a compact grammar for nodes, edges, labels and a few shapes. Parse to a graph with stable node identifiers derived from the declared names. On a parse error, keep the last good graph on screen, mark the offending line and column in the editor gutter, and say what was expected. Trap: Throwing away the graph on the first parse error means the diagram vanishes the moment you start typing a new line, which is every few seconds, and the flicker makes the tool unusable for exactly the keyboard-driven workflow that is its reason to exist. Keep the last successful parse rendered, overlay the error on the line it came from, and only replace the graph when a parse succeeds. Done when: Typing an incomplete line leaves the previous diagram on screen with an error marker on that line, tested by typing a partial statement. A parse error reports a line and column and a description of what was expected, checked against three deliberately broken inputs. Node identifiers are derived from declared names and are stable across re-parses, asserted in the suite. An empty document renders an empty canvas with a hint rather than an error. Do not build yet: No layout, no dragging, no export and no persistence yet. ### Phase 2 · A layout that does not jump Build: Compute a layered layout from the graph, ordering nodes by their declaration order so the result is deterministic. Recompute only when the set of nodes or edges changes, not when a label changes. Animate position changes over a short interval so a genuine structural change is followable. Trap: Recomputing the layout on every keystroke means that adding a character to a label reshuffles the entire diagram, and since typing is continuous the picture is in permanent motion and you can never point at anything. Hash the structural part of the graph, recompute only when that hash changes, and treat a label edit as a repaint. Done when: Two runs over the same source produce byte-identical positions, tested by comparing serialised layouts. Editing a label repaints without recomputing the layout, verified by counting layout calls in a test. Adding a node animates the change rather than snapping, checked by eye and by asserting the transition exists. A graph with a cycle lays out without hanging, tested with a deliberate cycle. Do not build yet: No dragging, no export and no persistence yet. ### Phase 3 · Nudges that survive the next edit Build: Dragging a node stores an offset against its node identifier rather than an absolute position. The layout runs as normal and the offset is applied afterwards. A pinned node keeps its offset even when the layout moves everything around it, and a reset clears offsets per node or entirely. Trap: Storing a nudge against the node's position in an array means inserting a line anywhere above it silently transfers that nudge to a different box, and the user watches an unrelated shape drift across the canvas with no idea why. Key the override on the node identifier from the parser, and drop overrides for identifiers that no longer exist. Done when: Nudging a node and then adding an unrelated node above it in the source leaves the nudge on the original node, tested. Deleting a node removes its override rather than leaving an orphan row, tested. A per-node reset and a reset-all both work and are undoable. Keyboard arrows nudge the selected node by a fixed step and shift-arrow by a larger one, tested. Do not build yet: No export and no persistence yet. ### Phase 4 · An export that opens somewhere else Build: Export SVG with the typeface embedded as a data URI inside the file, and a fallback stack on every text element. Render PNG by drawing that same SVG into a canvas at a chosen scale. Offer the source text as an export too, since it is the smallest and most durable form of the diagram. Trap: An exported SVG that names a web font by family renders in whatever the viewer happens to have, because the SVG specification places a document referenced as an image into a secure processing mode where external references are not resolved at all, so the font never loads, the metrics change and every label overflows its box. Embed the font in the file as a data URI or convert text to paths, and check the export in a viewer that is not a browser tab you already had open. Done when: An exported SVG opened as an image in a separate program shows the same typeface and no overflowing labels, checked by hand in at least two viewers. The PNG export honours a scale factor and the output dimensions match the requested scale exactly, tested. Exporting the source text round-trips: importing it reproduces the diagram including overrides, tested. Export never contains a reference to a URL outside the file, asserted by scanning the output. Do not build yet: No persistence, no document list and no quota handling yet. ### Phase 5 · Work that cannot be lost Build: Autosave the source on a debounce into IndexedDB with a format version on every record. Keep a capped ring of previous sources so a reload still has undo. Detect storage being unavailable or full and fall back to keeping the document in memory with a visible warning and a download prompt. Import and export whole documents as files. Trap: Assuming browser storage is always there means that in a private window, or with site data blocked, or when the quota is full, the autosave silently fails and the user loses an afternoon on a refresh with no warning at any point. Wrap every storage call, surface a failure as a banner offering an immediate download, and never let a save error be swallowed. Done when: Reloading mid-edit restores the document and its undo history, tested. With storage blocked, the app runs, shows a persistent warning and offers a download, tested by denying access in the test environment. Every stored record carries a format version and a record written under an older version still opens, tested with a fixture. Documents export as files and import back with identical source and overrides. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer cursors and comments, which need a server, a presence protocol and conflict handling, and are the reason the paid tier exists. - Importing other tools' diagram formats at high fidelity, which is a long tail of undocumented quirks rather than a feature. - A large template library, since templates are content and this is a weekend of code, not a weekend of drawing. - Arbitrary freeform shapes and connectors, because the moment the canvas becomes the source of truth the text stops being the document and the whole design collapses. ### README must contain - That the source text is the document and everything else can be recomputed, which is why exports include it - The grammar in full, with one example of every statement the parser accepts - That exported SVGs embed the typeface because an SVG rendered as an image cannot resolve external references - That nudges are stored against node identifiers, so renaming a node loses its nudge and that is deliberate - Where documents are stored in the browser, what happens when that storage is unavailable, and how to get everything out ===== ARCHITECTURE.md ===== # Architecture · Gleek ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | React with TypeScript and Vite, everything in the browser and no server at all | a diagram tool that needs a backend is a diagram tool you cannot use on a train | | Source of truth | The diagram is the text; the canvas is a view of it | undo, autosave and export all become easy once there is only one thing to save | | Storage | IndexedDB holding a versioned document record, with autosave on a debounce | browser storage fails in more ways than people expect and the format needs a version from day one | | Layout | A deterministic layered layout recomputed only when the graph structure changes | a layout that reruns on every keystroke makes the diagram unreadable while you type | | Export | SVG with the font embedded, and PNG rendered from that same SVG at a chosen scale | an SVG rendered as an image cannot fetch anything, so the font has to be inside it | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Parser | Grammar, graph and located errors. | Any grammar, provided a failed parse keeps the last good graph. | | Layout | Deterministic positions and the structure hash. | Any algorithm, provided identical input gives identical output. | | Overrides | Per-node offsets keyed on identifiers. | Any store, provided the key is not positional. | | Export | Self-contained SVG, PNG and source. | Any renderer, provided no export references anything outside itself. | | Persistence | Versioned records, the undo ring and failure states. | Any storage, provided a failure is visible rather than silent. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `VITE_APP_TITLE` · optional · Shown in the tab. There are no secrets in this build because there is no server. - `VITE_AUTOSAVE_MS` · required · Debounce before a save. Lower costs writes, higher costs your afternoon. - `VITE_REVISION_RING` · required · How many previous sources are kept for undo across reloads. - `VITE_MAX_NODES` · required · Above this the layout and the canvas both stop being pleasant, so warn rather than freeze. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Gleek product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: React with TypeScript and Vite, everything in the browser and no server at all, The diagram is the text; the canvas is a view of it, IndexedDB holding a versioned document record, with autosave on a debounce, A deterministic layered layout recomputed only when the graph structure changes, SVG with the font embedded, and PNG rendered from that same SVG at a chosen scale. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Throwing away the graph on the first parse error means the diagram vanishes the moment you start typing a new line, which is every few seconds, and the flicker makes the tool unusable for exactly the keyboard-driven workflow that is its reason to exist. Keep the last successful parse rendered, overlay the error on the line it came from, and only replace the graph when a parse succeeds. - Recomputing the layout on every keystroke means that adding a character to a label reshuffles the entire diagram, and since typing is continuous the picture is in permanent motion and you can never point at anything. Hash the structural part of the graph, recompute only when that hash changes, and treat a label edit as a repaint. - Storing a nudge against the node's position in an array means inserting a line anywhere above it silently transfers that nudge to a different box, and the user watches an unrelated shape drift across the canvas with no idea why. Key the override on the node identifier from the parser, and drop overrides for identifiers that no longer exist. - An exported SVG that names a web font by family renders in whatever the viewer happens to have, because the SVG specification places a document referenced as an image into a secure processing mode where external references are not resolved at all, so the font never loads, the metrics change and every label overflows its box. Embed the font in the file as a data URI or convert text to paths, and check the export in a viewer that is not a browser tab you already had open. - Assuming browser storage is always there means that in a private window, or with site data blocked, or when the quota is full, the autosave silently fails and the user loses an afternoon on a refresh with no warning at any point. Wrap every storage call, surface a failure as a banner offering an immediate download, and never let a save error be swallowed. ===== MILESTONES.md ===== # Delivery milestones · Gleek Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Text in, graph out, errors on a line A small grammar parses to nodes and edges, and a half-typed line never blanks the canvas. ### Steps 1. Write the grammar down before the parser A grammar you can read is a grammar you can extend on a Tuesday. Files: `docs/grammar.md`, `src/parse/grammar.ts` 2. Keep the last good graph when a parse fails The canvas flickering is what kills this kind of editor. Files: `src/state/document.ts`, `tests/parse-recovery.test.ts` 3. Report errors with a line, a column and an expectation Somewhere is not a useful place for a mistake to be. Files: `src/parse/errors.ts` ### Done when - [ ] A half-typed line keeps the previous diagram visible - [ ] Errors carry line, column and expectation - [ ] Node identifiers are stable across re-parses - [ ] An empty document is a hint, not an error ### Watch out - Throwing away the graph on the first parse error means the diagram vanishes the moment you start typing a new line, which is every few seconds, and the flicker makes the tool unusable for exactly the keyboard-driven workflow that is its reason to exist. Keep the last successful parse rendered, overlay the error on the line it came from, and only replace the graph when a parse succeeds. ## M2 · A layout that does not jump The same graph always lays out the same way, and relabelling a box does not rearrange the diagram. ### Steps 1. Order by declaration so layout is deterministic A layout that differs between runs makes every diff meaningless. Files: `src/layout/layered.ts`, `tests/deterministic.test.ts` 2. Hash the structure and recompute only on change A label edit is a repaint, not a rearrangement. Files: `src/layout/cache.ts` 3. Animate real structural changes So you can see what moved rather than finding it again. Files: `src/canvas/transition.ts` ### Done when - [ ] The same source always lays out identically - [ ] Label edits do not trigger a relayout - [ ] Structural changes animate - [ ] A cyclic graph lays out without hanging ### Watch out - Recomputing the layout on every keystroke means that adding a character to a label reshuffles the entire diagram, and since typing is continuous the picture is in permanent motion and you can never point at anything. Hash the structural part of the graph, recompute only when that hash changes, and treat a label edit as a repaint. ## M3 · Nudges that survive the next edit Dragging a box to where you want it keeps it there after the text changes. ### Steps 1. Key overrides on the node identifier Positional keys migrate to the wrong shape. Files: `src/state/overrides.ts`, `tests/overrides.test.ts` 2. Apply offsets after layout, never instead of it The layout still has to make sense for everything you did not touch. Files: `src/layout/apply.ts` 3. Give keyboard nudging the same powers as the mouse This is a keyboard tool and the mouse is the afterthought. Files: `src/canvas/keyboard.ts` ### Done when - [ ] A nudge stays with its node when the source changes - [ ] Deleting a node drops its override - [ ] Per-node and global resets both work - [ ] Arrow keys nudge the selection ### Watch out - Storing a nudge against the node's position in an array means inserting a line anywhere above it silently transfers that nudge to a different box, and the user watches an unrelated shape drift across the canvas with no idea why. Key the override on the node identifier from the parser, and drop overrides for identifiers that no longer exist. ## M4 · An export that opens somewhere else The exported SVG and PNG look like what was on screen, in programs that are not this one. ### Steps 1. Embed the font in the SVG, or convert text to paths An exported SVG cannot go and fetch anything. Files: `src/export/svg.ts`, `tests/no-external-refs.test.ts` 2. Render PNG from the same SVG at a stated scale One rendering path means one set of bugs. Files: `src/export/png.ts` 3. Export the source text as a first-class format It is the version that will still open in ten years. Files: `src/export/source.ts` ### Done when - [ ] An exported SVG renders correctly outside the app - [ ] PNG dimensions match the requested scale - [ ] Source export and import round-trips including overrides - [ ] No export references anything outside itself ### Watch out - An exported SVG that names a web font by family renders in whatever the viewer happens to have, because the SVG specification places a document referenced as an image into a secure processing mode where external references are not resolved at all, so the font never loads, the metrics change and every label overflows its box. Embed the font in the file as a data URI or convert text to paths, and check the export in a viewer that is not a browser tab you already had open. ## M5 · Work that cannot be lost (production only) Autosave survives a reload, browser storage failing is handled, and nothing is trapped in this app. ### Steps 1. Version the record format from the first save The alternative is a migration you cannot write because you do not know what you wrote. Files: `src/store/schema.ts` 2. Treat storage failure as a first-class state Private windows and full quotas are normal, not exotic. Files: `src/store/persist.ts`, `tests/storage-failure.test.ts` 3. Make file import and export symmetrical The way out has to be as easy as the way in. Files: `src/store/files.ts` ### Done when - [ ] A reload restores the document and its undo ring - [ ] Blocked storage warns and offers a download - [ ] Older format versions still open - [ ] Document files round-trip exactly ### Watch out - Assuming browser storage is always there means that in a private window, or with site data blocked, or when the quota is full, the autosave silently fails and the user loses an afternoon on a refresh with no warning at any point. Wrap every storage call, surface a failure as a banner offering an immediate download, and never let a save error be swallowed. ===== OPERATIONS.md ===== # Operations · Gleek ## Backup This runs in a browser, so backup means export. Make the file export obvious and make the source text export the default, because it is small, readable and the only format that will certainly still open in a decade. ## Restore Import a document file and confirm the source, the overrides and the title all came back. If overrides did not survive, the export omitted them, which is the most likely thing to get wrong here because they feel like presentation rather than content. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring There is no server, so the equivalent is the storage failure banner and a node count warning. Both should be visible to the user rather than logged to a console nobody has open. ## Incident checklist The incident is lost work, and it almost always comes from storage failing quietly. That is why the failure state is a banner with a download button rather than a message in the console, and why the undo ring is stored rather than held in memory. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A parse error is confirmed to keep the previous diagram visible - [ ] The same source is confirmed to lay out identically twice - [ ] A nudge is confirmed to survive an unrelated source edit - [ ] An exported SVG is confirmed correct in a non-browser viewer - [ ] Blocked storage is confirmed to warn and offer a download ## Launch constraint Do not market omitted Gleek capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Optional. Shown in the tab. There are no secrets in this build because there is no server. VITE_APP_TITLE=Diagrams # Required. Debounce before a save. Lower costs writes, higher costs your afternoon. VITE_AUTOSAVE_MS=800 # Required. How many previous sources are kept for undo across reloads. VITE_REVISION_RING=50 # Required. Above this the layout and the canvas both stop being pleasant, so warn rather than freeze. VITE_MAX_NODES=300
# Gleek · indie build A text-to-diagram editor whose hard problems are staying useful while the source is half-typed, keeping a hand-placed box where you put it after the text changes, and producing an export that still looks right in a program you did not write. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | React with TypeScript and Vite, everything in the browser and no server at all | a diagram tool that needs a backend is a diagram tool you cannot use on a train | | Source of truth | The diagram is the text; the canvas is a view of it | undo, autosave and export all become easy once there is only one thing to save | | Storage | IndexedDB holding a versioned document record, with autosave on a debounce | browser storage fails in more ways than people expect and the format needs a version from day one | | Layout | A deterministic layered layout recomputed only when the graph structure changes | a layout that reruns on every keystroke makes the diagram unreadable while you type | | Export | SVG with the font embedded, and PNG rendered from that same SVG at a chosen scale | an SVG rendered as an image cannot fetch anything, so the font has to be inside it | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds and serves the application. Nothing runs server-side once it is built. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **One typeface with a licence that permits embedding** · free - Why: The export embeds it in the file, which is a distribution of the font, and not every licence allows that. - Get it: Pick an open licensed family and read its terms before embedding. - Verify: The licence text permits embedding and a copy sits in the repository - [ ] **A second SVG viewer that is not your browser** · free - Why: Browsers are forgiving about fonts in ways other renderers are not, and the export bug in this category only shows up elsewhere. - Get it: A vector editor, a document previewer, anything that renders SVG independently. - Verify: An exported diagram opens in it with correct text - [ ] **The grammar, written down before the parser** · free - Why: Every later decision about layout, overrides and export follows from what the source text is allowed to say. - Get it: Write the statements you want to type, then design the grammar that accepts exactly those. - Verify: Every statement in the documentation parses and is covered by a test ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multiplayer cursors and comments. - High-fidelity import of other tools' formats. - A large template library. - Freeform shapes with the canvas as the source of truth. - multiplayer cursors and comments - large template libraries - high-fidelity import and export - presentation and enterprise controls If one of those is essential to you, that is the reason to keep paying for Gleek, and the README should say so rather than pretend.
# Build brief · Gleek The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a keyboard-first diagram editor that turns a few lines of text into a picture you can export to replace Gleek. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: React with TypeScript and Vite, everything in the browser and no server at all · a diagram tool that needs a backend is a diagram tool you cannot use on a train - Source of truth: The diagram is the text; the canvas is a view of it · undo, autosave and export all become easy once there is only one thing to save - Storage: IndexedDB holding a versioned document record, with autosave on a debounce · browser storage fails in more ways than people expect and the format needs a version from day one - Layout: A deterministic layered layout recomputed only when the graph structure changes · a layout that reruns on every keystroke makes the diagram unreadable while you type - Export: SVG with the font embedded, and PNG rendered from that same SVG at a chosen scale · an SVG rendered as an image cannot fetch anything, so the font has to be inside it ### Data model (create this before Phase 1) - documents(id, title, source_text, format_version, created_at, updated_at, last_opened_at) · the source text is the document and everything else is a cache - revisions(document_id, ordinal, source_text, at) · a capped ring of previous sources, which is a cheaper undo than a command stack and survives a reload - nodes(doc_id, node_id, label, shape) · derived from the source by the parser, never edited directly - edges(doc_id, from_node, to_node, label, style) · likewise derived - overrides(doc_id, node_id, dx, dy, pinned) · manual nudges keyed on the node identifier, which is what lets them survive a re-parse - parse_errors(doc_id, line, column, message) · so a mistake points at a line rather than blanking the canvas - Nothing in this schema stores pixel coordinates as the truth. The layout computes them and the overrides adjust them, which means a diagram opened on a different screen is still the same diagram. ### Phase 1 · Text in, graph out, errors on a line Build: Define a compact grammar for nodes, edges, labels and a few shapes. Parse to a graph with stable node identifiers derived from the declared names. On a parse error, keep the last good graph on screen, mark the offending line and column in the editor gutter, and say what was expected. Trap: Throwing away the graph on the first parse error means the diagram vanishes the moment you start typing a new line, which is every few seconds, and the flicker makes the tool unusable for exactly the keyboard-driven workflow that is its reason to exist. Keep the last successful parse rendered, overlay the error on the line it came from, and only replace the graph when a parse succeeds. Done when: Typing an incomplete line leaves the previous diagram on screen with an error marker on that line, tested by typing a partial statement. A parse error reports a line and column and a description of what was expected, checked against three deliberately broken inputs. Node identifiers are derived from declared names and are stable across re-parses, asserted in the suite. An empty document renders an empty canvas with a hint rather than an error. Do not build yet: No layout, no dragging, no export and no persistence yet. ### Phase 2 · A layout that does not jump Build: Compute a layered layout from the graph, ordering nodes by their declaration order so the result is deterministic. Recompute only when the set of nodes or edges changes, not when a label changes. Animate position changes over a short interval so a genuine structural change is followable. Trap: Recomputing the layout on every keystroke means that adding a character to a label reshuffles the entire diagram, and since typing is continuous the picture is in permanent motion and you can never point at anything. Hash the structural part of the graph, recompute only when that hash changes, and treat a label edit as a repaint. Done when: Two runs over the same source produce byte-identical positions, tested by comparing serialised layouts. Editing a label repaints without recomputing the layout, verified by counting layout calls in a test. Adding a node animates the change rather than snapping, checked by eye and by asserting the transition exists. A graph with a cycle lays out without hanging, tested with a deliberate cycle. Do not build yet: No dragging, no export and no persistence yet. ### Phase 3 · Nudges that survive the next edit Build: Dragging a node stores an offset against its node identifier rather than an absolute position. The layout runs as normal and the offset is applied afterwards. A pinned node keeps its offset even when the layout moves everything around it, and a reset clears offsets per node or entirely. Trap: Storing a nudge against the node's position in an array means inserting a line anywhere above it silently transfers that nudge to a different box, and the user watches an unrelated shape drift across the canvas with no idea why. Key the override on the node identifier from the parser, and drop overrides for identifiers that no longer exist. Done when: Nudging a node and then adding an unrelated node above it in the source leaves the nudge on the original node, tested. Deleting a node removes its override rather than leaving an orphan row, tested. A per-node reset and a reset-all both work and are undoable. Keyboard arrows nudge the selected node by a fixed step and shift-arrow by a larger one, tested. Do not build yet: No export and no persistence yet. ### Phase 4 · An export that opens somewhere else Build: Export SVG with the typeface embedded as a data URI inside the file, and a fallback stack on every text element. Render PNG by drawing that same SVG into a canvas at a chosen scale. Offer the source text as an export too, since it is the smallest and most durable form of the diagram. Trap: An exported SVG that names a web font by family renders in whatever the viewer happens to have, because the SVG specification places a document referenced as an image into a secure processing mode where external references are not resolved at all, so the font never loads, the metrics change and every label overflows its box. Embed the font in the file as a data URI or convert text to paths, and check the export in a viewer that is not a browser tab you already had open. Done when: An exported SVG opened as an image in a separate program shows the same typeface and no overflowing labels, checked by hand in at least two viewers. The PNG export honours a scale factor and the output dimensions match the requested scale exactly, tested. Exporting the source text round-trips: importing it reproduces the diagram including overrides, tested. Export never contains a reference to a URL outside the file, asserted by scanning the output. Do not build yet: No persistence, no document list and no quota handling yet. ### Phase 5 · Work that cannot be lost Build: Autosave the source on a debounce into IndexedDB with a format version on every record. Keep a capped ring of previous sources so a reload still has undo. Detect storage being unavailable or full and fall back to keeping the document in memory with a visible warning and a download prompt. Import and export whole documents as files. Trap: Assuming browser storage is always there means that in a private window, or with site data blocked, or when the quota is full, the autosave silently fails and the user loses an afternoon on a refresh with no warning at any point. Wrap every storage call, surface a failure as a banner offering an immediate download, and never let a save error be swallowed. Done when: Reloading mid-edit restores the document and its undo history, tested. With storage blocked, the app runs, shows a persistent warning and offers a download, tested by denying access in the test environment. Every stored record carries a format version and a record written under an older version still opens, tested with a fixture. Documents export as files and import back with identical source and overrides. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer cursors and comments, which need a server, a presence protocol and conflict handling, and are the reason the paid tier exists. - Importing other tools' diagram formats at high fidelity, which is a long tail of undocumented quirks rather than a feature. - A large template library, since templates are content and this is a weekend of code, not a weekend of drawing. - Arbitrary freeform shapes and connectors, because the moment the canvas becomes the source of truth the text stops being the document and the whole design collapses. ### README must contain - That the source text is the document and everything else can be recomputed, which is why exports include it - The grammar in full, with one example of every statement the parser accepts - That exported SVGs embed the typeface because an SVG rendered as an image cannot resolve external references - That nudges are stored against node identifiers, so renaming a node loses its nudge and that is deliberate - Where documents are stored in the browser, what happens when that storage is unavailable, and how to get everything out
# Agent instructions · Gleek indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: React with TypeScript and Vite, everything in the browser and no server at all, The diagram is the text; the canvas is a view of it, IndexedDB holding a versioned document record, with autosave on a debounce, A deterministic layered layout recomputed only when the graph structure changes, SVG with the font embedded, and PNG rendered from that same SVG at a chosen scale. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Throwing away the graph on the first parse error means the diagram vanishes the moment you start typing a new line, which is every few seconds, and the flicker makes the tool unusable for exactly the keyboard-driven workflow that is its reason to exist. Keep the last successful parse rendered, overlay the error on the line it came from, and only replace the graph when a parse succeeds. - Recomputing the layout on every keystroke means that adding a character to a label reshuffles the entire diagram, and since typing is continuous the picture is in permanent motion and you can never point at anything. Hash the structural part of the graph, recompute only when that hash changes, and treat a label edit as a repaint. - Storing a nudge against the node's position in an array means inserting a line anywhere above it silently transfers that nudge to a different box, and the user watches an unrelated shape drift across the canvas with no idea why. Key the override on the node identifier from the parser, and drop overrides for identifiers that no longer exist. - An exported SVG that names a web font by family renders in whatever the viewer happens to have, because the SVG specification places a document referenced as an image into a secure processing mode where external references are not resolved at all, so the font never loads, the metrics change and every label overflows its box. Embed the font in the file as a data URI or convert text to paths, and check the export in a viewer that is not a browser tab you already had open.
# Build plan · Gleek A text-to-diagram editor whose hard problems are staying useful while the source is half-typed, keeping a hand-placed box where you put it after the text changes, and producing an export that still looks right in a program you did not write. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Text in, graph out, errors on a line A small grammar parses to nodes and edges, and a half-typed line never blanks the canvas. ### Steps 1. Write the grammar down before the parser A grammar you can read is a grammar you can extend on a Tuesday. Files: `docs/grammar.md`, `src/parse/grammar.ts` 2. Keep the last good graph when a parse fails The canvas flickering is what kills this kind of editor. Files: `src/state/document.ts`, `tests/parse-recovery.test.ts` 3. Report errors with a line, a column and an expectation Somewhere is not a useful place for a mistake to be. Files: `src/parse/errors.ts` ### Done when - [ ] A half-typed line keeps the previous diagram visible - [ ] Errors carry line, column and expectation - [ ] Node identifiers are stable across re-parses - [ ] An empty document is a hint, not an error ### Watch out - Throwing away the graph on the first parse error means the diagram vanishes the moment you start typing a new line, which is every few seconds, and the flicker makes the tool unusable for exactly the keyboard-driven workflow that is its reason to exist. Keep the last successful parse rendered, overlay the error on the line it came from, and only replace the graph when a parse succeeds. ## Phase 2 · A layout that does not jump The same graph always lays out the same way, and relabelling a box does not rearrange the diagram. ### Steps 1. Order by declaration so layout is deterministic A layout that differs between runs makes every diff meaningless. Files: `src/layout/layered.ts`, `tests/deterministic.test.ts` 2. Hash the structure and recompute only on change A label edit is a repaint, not a rearrangement. Files: `src/layout/cache.ts` 3. Animate real structural changes So you can see what moved rather than finding it again. Files: `src/canvas/transition.ts` ### Done when - [ ] The same source always lays out identically - [ ] Label edits do not trigger a relayout - [ ] Structural changes animate - [ ] A cyclic graph lays out without hanging ### Watch out - Recomputing the layout on every keystroke means that adding a character to a label reshuffles the entire diagram, and since typing is continuous the picture is in permanent motion and you can never point at anything. Hash the structural part of the graph, recompute only when that hash changes, and treat a label edit as a repaint. ## Phase 3 · Nudges that survive the next edit Dragging a box to where you want it keeps it there after the text changes. ### Steps 1. Key overrides on the node identifier Positional keys migrate to the wrong shape. Files: `src/state/overrides.ts`, `tests/overrides.test.ts` 2. Apply offsets after layout, never instead of it The layout still has to make sense for everything you did not touch. Files: `src/layout/apply.ts` 3. Give keyboard nudging the same powers as the mouse This is a keyboard tool and the mouse is the afterthought. Files: `src/canvas/keyboard.ts` ### Done when - [ ] A nudge stays with its node when the source changes - [ ] Deleting a node drops its override - [ ] Per-node and global resets both work - [ ] Arrow keys nudge the selection ### Watch out - Storing a nudge against the node's position in an array means inserting a line anywhere above it silently transfers that nudge to a different box, and the user watches an unrelated shape drift across the canvas with no idea why. Key the override on the node identifier from the parser, and drop overrides for identifiers that no longer exist. ## Phase 4 · An export that opens somewhere else The exported SVG and PNG look like what was on screen, in programs that are not this one. ### Steps 1. Embed the font in the SVG, or convert text to paths An exported SVG cannot go and fetch anything. Files: `src/export/svg.ts`, `tests/no-external-refs.test.ts` 2. Render PNG from the same SVG at a stated scale One rendering path means one set of bugs. Files: `src/export/png.ts` 3. Export the source text as a first-class format It is the version that will still open in ten years. Files: `src/export/source.ts` ### Done when - [ ] An exported SVG renders correctly outside the app - [ ] PNG dimensions match the requested scale - [ ] Source export and import round-trips including overrides - [ ] No export references anything outside itself ### Watch out - An exported SVG that names a web font by family renders in whatever the viewer happens to have, because the SVG specification places a document referenced as an image into a secure processing mode where external references are not resolved at all, so the font never loads, the metrics change and every label overflows its box. Embed the font in the file as a data URI or convert text to paths, and check the export in a viewer that is not a browser tab you already had open. ## Not in this build - Multiplayer cursors and comments. - High-fidelity import of other tools' formats. - A large template library. - Freeform shapes with the canvas as the source of truth. ## After v1, if you want it - A second layout direction, since left-to-right and top-to-bottom suit different diagrams and the graph does not change - A print stylesheet that puts one diagram per page at a sensible scale, which is surprisingly often what people wanted - A compact share format that encodes the source in a link, so a diagram travels without a file
# Copy to .env and fill in. Never commit .env; this file documents it. # Optional. Shown in the tab. There are no secrets in this build because there is no server. VITE_APP_TITLE=Diagrams # Required. Debounce before a save. Lower costs writes, higher costs your afternoon. VITE_AUTOSAVE_MS=800 # Required. How many previous sources are kept for undo across reloads. VITE_REVISION_RING=50 # Required. Above this the layout and the canvas both stop being pleasant, so warn rather than freeze. VITE_MAX_NODES=300
# Gleek · product brief ## Problem Gleek's solo core is compact: build a browser-based keyboard diagramming editor with drag, resize, connect, keyboard shortcuts, autosave, and SVG or PNG export. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on interaction polish, collaboration. ## Product outcome A diagram editor where the text is the document, the picture stops moving while you type, a box you dragged stays dragged, and the exported file looks the same in a program that is not a browser. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - modern browser - no account required ## Explicit non-goals for v1 - Multiplayer cursors and comments. - High-fidelity import of other tools' formats. - A large template library. - Freeform shapes with the canvas as the source of truth. - multiplayer cursors and comments - large template libraries - high-fidelity import and export - presentation and enterprise controls ## Success criteria - A parse error is confirmed to keep the previous diagram visible - The same source is confirmed to lay out identically twice - A nudge is confirmed to survive an unrelated source edit - An exported SVG is confirmed correct in a non-browser viewer - Blocked storage is confirmed to warn and offer a download
# Build brief · Gleek The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a keyboard-first diagram editor that turns a few lines of text into a picture you can export to replace Gleek. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: React with TypeScript and Vite, everything in the browser and no server at all · a diagram tool that needs a backend is a diagram tool you cannot use on a train - Source of truth: The diagram is the text; the canvas is a view of it · undo, autosave and export all become easy once there is only one thing to save - Storage: IndexedDB holding a versioned document record, with autosave on a debounce · browser storage fails in more ways than people expect and the format needs a version from day one - Layout: A deterministic layered layout recomputed only when the graph structure changes · a layout that reruns on every keystroke makes the diagram unreadable while you type - Export: SVG with the font embedded, and PNG rendered from that same SVG at a chosen scale · an SVG rendered as an image cannot fetch anything, so the font has to be inside it ### Data model (create this before Phase 1) - documents(id, title, source_text, format_version, created_at, updated_at, last_opened_at) · the source text is the document and everything else is a cache - revisions(document_id, ordinal, source_text, at) · a capped ring of previous sources, which is a cheaper undo than a command stack and survives a reload - nodes(doc_id, node_id, label, shape) · derived from the source by the parser, never edited directly - edges(doc_id, from_node, to_node, label, style) · likewise derived - overrides(doc_id, node_id, dx, dy, pinned) · manual nudges keyed on the node identifier, which is what lets them survive a re-parse - parse_errors(doc_id, line, column, message) · so a mistake points at a line rather than blanking the canvas - Nothing in this schema stores pixel coordinates as the truth. The layout computes them and the overrides adjust them, which means a diagram opened on a different screen is still the same diagram. ### Phase 1 · Text in, graph out, errors on a line Build: Define a compact grammar for nodes, edges, labels and a few shapes. Parse to a graph with stable node identifiers derived from the declared names. On a parse error, keep the last good graph on screen, mark the offending line and column in the editor gutter, and say what was expected. Trap: Throwing away the graph on the first parse error means the diagram vanishes the moment you start typing a new line, which is every few seconds, and the flicker makes the tool unusable for exactly the keyboard-driven workflow that is its reason to exist. Keep the last successful parse rendered, overlay the error on the line it came from, and only replace the graph when a parse succeeds. Done when: Typing an incomplete line leaves the previous diagram on screen with an error marker on that line, tested by typing a partial statement. A parse error reports a line and column and a description of what was expected, checked against three deliberately broken inputs. Node identifiers are derived from declared names and are stable across re-parses, asserted in the suite. An empty document renders an empty canvas with a hint rather than an error. Do not build yet: No layout, no dragging, no export and no persistence yet. ### Phase 2 · A layout that does not jump Build: Compute a layered layout from the graph, ordering nodes by their declaration order so the result is deterministic. Recompute only when the set of nodes or edges changes, not when a label changes. Animate position changes over a short interval so a genuine structural change is followable. Trap: Recomputing the layout on every keystroke means that adding a character to a label reshuffles the entire diagram, and since typing is continuous the picture is in permanent motion and you can never point at anything. Hash the structural part of the graph, recompute only when that hash changes, and treat a label edit as a repaint. Done when: Two runs over the same source produce byte-identical positions, tested by comparing serialised layouts. Editing a label repaints without recomputing the layout, verified by counting layout calls in a test. Adding a node animates the change rather than snapping, checked by eye and by asserting the transition exists. A graph with a cycle lays out without hanging, tested with a deliberate cycle. Do not build yet: No dragging, no export and no persistence yet. ### Phase 3 · Nudges that survive the next edit Build: Dragging a node stores an offset against its node identifier rather than an absolute position. The layout runs as normal and the offset is applied afterwards. A pinned node keeps its offset even when the layout moves everything around it, and a reset clears offsets per node or entirely. Trap: Storing a nudge against the node's position in an array means inserting a line anywhere above it silently transfers that nudge to a different box, and the user watches an unrelated shape drift across the canvas with no idea why. Key the override on the node identifier from the parser, and drop overrides for identifiers that no longer exist. Done when: Nudging a node and then adding an unrelated node above it in the source leaves the nudge on the original node, tested. Deleting a node removes its override rather than leaving an orphan row, tested. A per-node reset and a reset-all both work and are undoable. Keyboard arrows nudge the selected node by a fixed step and shift-arrow by a larger one, tested. Do not build yet: No export and no persistence yet. ### Phase 4 · An export that opens somewhere else Build: Export SVG with the typeface embedded as a data URI inside the file, and a fallback stack on every text element. Render PNG by drawing that same SVG into a canvas at a chosen scale. Offer the source text as an export too, since it is the smallest and most durable form of the diagram. Trap: An exported SVG that names a web font by family renders in whatever the viewer happens to have, because the SVG specification places a document referenced as an image into a secure processing mode where external references are not resolved at all, so the font never loads, the metrics change and every label overflows its box. Embed the font in the file as a data URI or convert text to paths, and check the export in a viewer that is not a browser tab you already had open. Done when: An exported SVG opened as an image in a separate program shows the same typeface and no overflowing labels, checked by hand in at least two viewers. The PNG export honours a scale factor and the output dimensions match the requested scale exactly, tested. Exporting the source text round-trips: importing it reproduces the diagram including overrides, tested. Export never contains a reference to a URL outside the file, asserted by scanning the output. Do not build yet: No persistence, no document list and no quota handling yet. ### Phase 5 · Work that cannot be lost Build: Autosave the source on a debounce into IndexedDB with a format version on every record. Keep a capped ring of previous sources so a reload still has undo. Detect storage being unavailable or full and fall back to keeping the document in memory with a visible warning and a download prompt. Import and export whole documents as files. Trap: Assuming browser storage is always there means that in a private window, or with site data blocked, or when the quota is full, the autosave silently fails and the user loses an afternoon on a refresh with no warning at any point. Wrap every storage call, surface a failure as a banner offering an immediate download, and never let a save error be swallowed. Done when: Reloading mid-edit restores the document and its undo history, tested. With storage blocked, the app runs, shows a persistent warning and offers a download, tested by denying access in the test environment. Every stored record carries a format version and a record written under an older version still opens, tested with a fixture. Documents export as files and import back with identical source and overrides. Do not build yet: Nothing after this. ### Out of scope (and why) - Multiplayer cursors and comments, which need a server, a presence protocol and conflict handling, and are the reason the paid tier exists. - Importing other tools' diagram formats at high fidelity, which is a long tail of undocumented quirks rather than a feature. - A large template library, since templates are content and this is a weekend of code, not a weekend of drawing. - Arbitrary freeform shapes and connectors, because the moment the canvas becomes the source of truth the text stops being the document and the whole design collapses. ### README must contain - That the source text is the document and everything else can be recomputed, which is why exports include it - The grammar in full, with one example of every statement the parser accepts - That exported SVGs embed the typeface because an SVG rendered as an image cannot resolve external references - That nudges are stored against node identifiers, so renaming a node loses its nudge and that is deliberate - Where documents are stored in the browser, what happens when that storage is unavailable, and how to get everything out
# Architecture · Gleek ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | React with TypeScript and Vite, everything in the browser and no server at all | a diagram tool that needs a backend is a diagram tool you cannot use on a train | | Source of truth | The diagram is the text; the canvas is a view of it | undo, autosave and export all become easy once there is only one thing to save | | Storage | IndexedDB holding a versioned document record, with autosave on a debounce | browser storage fails in more ways than people expect and the format needs a version from day one | | Layout | A deterministic layered layout recomputed only when the graph structure changes | a layout that reruns on every keystroke makes the diagram unreadable while you type | | Export | SVG with the font embedded, and PNG rendered from that same SVG at a chosen scale | an SVG rendered as an image cannot fetch anything, so the font has to be inside it | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Parser | Grammar, graph and located errors. | Any grammar, provided a failed parse keeps the last good graph. | | Layout | Deterministic positions and the structure hash. | Any algorithm, provided identical input gives identical output. | | Overrides | Per-node offsets keyed on identifiers. | Any store, provided the key is not positional. | | Export | Self-contained SVG, PNG and source. | Any renderer, provided no export references anything outside itself. | | Persistence | Versioned records, the undo ring and failure states. | Any storage, provided a failure is visible rather than silent. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `VITE_APP_TITLE` · optional · Shown in the tab. There are no secrets in this build because there is no server. - `VITE_AUTOSAVE_MS` · required · Debounce before a save. Lower costs writes, higher costs your afternoon. - `VITE_REVISION_RING` · required · How many previous sources are kept for undo across reloads. - `VITE_MAX_NODES` · required · Above this the layout and the canvas both stop being pleasant, so warn rather than freeze. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Gleek product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: React with TypeScript and Vite, everything in the browser and no server at all, The diagram is the text; the canvas is a view of it, IndexedDB holding a versioned document record, with autosave on a debounce, A deterministic layered layout recomputed only when the graph structure changes, SVG with the font embedded, and PNG rendered from that same SVG at a chosen scale. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Throwing away the graph on the first parse error means the diagram vanishes the moment you start typing a new line, which is every few seconds, and the flicker makes the tool unusable for exactly the keyboard-driven workflow that is its reason to exist. Keep the last successful parse rendered, overlay the error on the line it came from, and only replace the graph when a parse succeeds. - Recomputing the layout on every keystroke means that adding a character to a label reshuffles the entire diagram, and since typing is continuous the picture is in permanent motion and you can never point at anything. Hash the structural part of the graph, recompute only when that hash changes, and treat a label edit as a repaint. - Storing a nudge against the node's position in an array means inserting a line anywhere above it silently transfers that nudge to a different box, and the user watches an unrelated shape drift across the canvas with no idea why. Key the override on the node identifier from the parser, and drop overrides for identifiers that no longer exist. - An exported SVG that names a web font by family renders in whatever the viewer happens to have, because the SVG specification places a document referenced as an image into a secure processing mode where external references are not resolved at all, so the font never loads, the metrics change and every label overflows its box. Embed the font in the file as a data URI or convert text to paths, and check the export in a viewer that is not a browser tab you already had open. - Assuming browser storage is always there means that in a private window, or with site data blocked, or when the quota is full, the autosave silently fails and the user loses an afternoon on a refresh with no warning at any point. Wrap every storage call, surface a failure as a banner offering an immediate download, and never let a save error be swallowed.
# Delivery milestones · Gleek Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Text in, graph out, errors on a line A small grammar parses to nodes and edges, and a half-typed line never blanks the canvas. ### Steps 1. Write the grammar down before the parser A grammar you can read is a grammar you can extend on a Tuesday. Files: `docs/grammar.md`, `src/parse/grammar.ts` 2. Keep the last good graph when a parse fails The canvas flickering is what kills this kind of editor. Files: `src/state/document.ts`, `tests/parse-recovery.test.ts` 3. Report errors with a line, a column and an expectation Somewhere is not a useful place for a mistake to be. Files: `src/parse/errors.ts` ### Done when - [ ] A half-typed line keeps the previous diagram visible - [ ] Errors carry line, column and expectation - [ ] Node identifiers are stable across re-parses - [ ] An empty document is a hint, not an error ### Watch out - Throwing away the graph on the first parse error means the diagram vanishes the moment you start typing a new line, which is every few seconds, and the flicker makes the tool unusable for exactly the keyboard-driven workflow that is its reason to exist. Keep the last successful parse rendered, overlay the error on the line it came from, and only replace the graph when a parse succeeds. ## M2 · A layout that does not jump The same graph always lays out the same way, and relabelling a box does not rearrange the diagram. ### Steps 1. Order by declaration so layout is deterministic A layout that differs between runs makes every diff meaningless. Files: `src/layout/layered.ts`, `tests/deterministic.test.ts` 2. Hash the structure and recompute only on change A label edit is a repaint, not a rearrangement. Files: `src/layout/cache.ts` 3. Animate real structural changes So you can see what moved rather than finding it again. Files: `src/canvas/transition.ts` ### Done when - [ ] The same source always lays out identically - [ ] Label edits do not trigger a relayout - [ ] Structural changes animate - [ ] A cyclic graph lays out without hanging ### Watch out - Recomputing the layout on every keystroke means that adding a character to a label reshuffles the entire diagram, and since typing is continuous the picture is in permanent motion and you can never point at anything. Hash the structural part of the graph, recompute only when that hash changes, and treat a label edit as a repaint. ## M3 · Nudges that survive the next edit Dragging a box to where you want it keeps it there after the text changes. ### Steps 1. Key overrides on the node identifier Positional keys migrate to the wrong shape. Files: `src/state/overrides.ts`, `tests/overrides.test.ts` 2. Apply offsets after layout, never instead of it The layout still has to make sense for everything you did not touch. Files: `src/layout/apply.ts` 3. Give keyboard nudging the same powers as the mouse This is a keyboard tool and the mouse is the afterthought. Files: `src/canvas/keyboard.ts` ### Done when - [ ] A nudge stays with its node when the source changes - [ ] Deleting a node drops its override - [ ] Per-node and global resets both work - [ ] Arrow keys nudge the selection ### Watch out - Storing a nudge against the node's position in an array means inserting a line anywhere above it silently transfers that nudge to a different box, and the user watches an unrelated shape drift across the canvas with no idea why. Key the override on the node identifier from the parser, and drop overrides for identifiers that no longer exist. ## M4 · An export that opens somewhere else The exported SVG and PNG look like what was on screen, in programs that are not this one. ### Steps 1. Embed the font in the SVG, or convert text to paths An exported SVG cannot go and fetch anything. Files: `src/export/svg.ts`, `tests/no-external-refs.test.ts` 2. Render PNG from the same SVG at a stated scale One rendering path means one set of bugs. Files: `src/export/png.ts` 3. Export the source text as a first-class format It is the version that will still open in ten years. Files: `src/export/source.ts` ### Done when - [ ] An exported SVG renders correctly outside the app - [ ] PNG dimensions match the requested scale - [ ] Source export and import round-trips including overrides - [ ] No export references anything outside itself ### Watch out - An exported SVG that names a web font by family renders in whatever the viewer happens to have, because the SVG specification places a document referenced as an image into a secure processing mode where external references are not resolved at all, so the font never loads, the metrics change and every label overflows its box. Embed the font in the file as a data URI or convert text to paths, and check the export in a viewer that is not a browser tab you already had open. ## M5 · Work that cannot be lost (production only) Autosave survives a reload, browser storage failing is handled, and nothing is trapped in this app. ### Steps 1. Version the record format from the first save The alternative is a migration you cannot write because you do not know what you wrote. Files: `src/store/schema.ts` 2. Treat storage failure as a first-class state Private windows and full quotas are normal, not exotic. Files: `src/store/persist.ts`, `tests/storage-failure.test.ts` 3. Make file import and export symmetrical The way out has to be as easy as the way in. Files: `src/store/files.ts` ### Done when - [ ] A reload restores the document and its undo ring - [ ] Blocked storage warns and offers a download - [ ] Older format versions still open - [ ] Document files round-trip exactly ### Watch out - Assuming browser storage is always there means that in a private window, or with site data blocked, or when the quota is full, the autosave silently fails and the user loses an afternoon on a refresh with no warning at any point. Wrap every storage call, surface a failure as a banner offering an immediate download, and never let a save error be swallowed.
# Operations · Gleek ## Backup This runs in a browser, so backup means export. Make the file export obvious and make the source text export the default, because it is small, readable and the only format that will certainly still open in a decade. ## Restore Import a document file and confirm the source, the overrides and the title all came back. If overrides did not survive, the export omitted them, which is the most likely thing to get wrong here because they feel like presentation rather than content. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring There is no server, so the equivalent is the storage failure banner and a node count warning. Both should be visible to the user rather than logged to a console nobody has open. ## Incident checklist The incident is lost work, and it almost always comes from storage failing quietly. That is why the failure state is a banner with a download button rather than a message in the console, and why the undo ring is stored rather than held in memory. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A parse error is confirmed to keep the previous diagram visible - [ ] The same source is confirmed to lay out identically twice - [ ] A nudge is confirmed to survive an unrelated source edit - [ ] An exported SVG is confirmed correct in a non-browser viewer - [ ] Blocked storage is confirmed to warn and offer a download ## Launch constraint Do not market omitted Gleek capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Optional. Shown in the tab. There are no secrets in this build because there is no server. VITE_APP_TITLE=Diagrams # Required. Debounce before a save. Lower costs writes, higher costs your afternoon. VITE_AUTOSAVE_MS=800 # Required. How many previous sources are kept for undo across reloads. VITE_REVISION_RING=50 # Required. Above this the layout and the canvas both stop being pleasant, so warn rather than freeze. VITE_MAX_NODES=300
$ choose a build depth, inspect the files, then open the complete pack in your agent
Gleek: The shapes are easy; customers pay for low-friction editing, reliable exports, collaboration, and a canvas that never fights them.
xmultiplayer cursors and comments
xlarge template libraries
xhigh-fidelity import and export
xpresentation and enterprise controls
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Gleek pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0 | $0 | 5 files; 40 objects/file; PNG export only; local-browser storage; no AI |
| premium monthly | $8.02 | — | Unlimited diagrams and objects; premium export/collaboration features |
| premium per activity | $12.68 | — | Unlimited premium access only in months when the account is used |
| premium yearly | — | $5.62 | Unlimited diagrams and objects; premium export/collaboration features |
| teams | $8.02/user | — | Minimum 2 users; team collaboration and centralized billing |
free tier5 files; 40 objects/file; PNG only; local-browser storage; no AI
billingmonthly + per-active-month + annual; Teams monthly with 2-seat minimum
hidden costsThe per-activity plan charges €10.99 upfront for each month in which it is used, then freezes unused time. Teams has a 2-user minimum. Students and teachers can request a 50% discount.
verified 2026-08-12 · source ↗
Vibecode Gleek
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Gleek replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Gleek cost?
Gleek's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing Gleek?
Honestly: multiplayer cursors and comments; large template libraries; high-fidelity import and export; presentation and enterprise controls. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Gleek?
Yes: Mermaid Live Editor (Type Mermaid, see the diagram immediately, and export it; narrower than Gleek, but finished, free, and pleasantly uncomplicated.) PlantUML Server (A usable browser editor for architecture, sequence, process, and data-model diagrams written as text; the syntax is less forgiving than Gleek's.) The prompt is for when you want it exactly your way.