diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index b00cdc72..f61cd023 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -18,13 +18,13 @@ jobs: node-version: [20.x] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: - version: latest + version: 10 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index b3a5f656..1c6a784b 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -14,13 +14,13 @@ jobs: node-version: [20.x] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: - version: latest + version: 10 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' @@ -39,7 +39,7 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp-key.json pnpm deploy:staging - name: Upload artifacts # Find artifacts under actions/jobs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: chrome-extension path: extension diff --git a/.github/workflows/remove-old-artifacts.yml b/.github/workflows/remove-old-artifacts.yml index 1cfb1937..276ce970 100644 --- a/.github/workflows/remove-old-artifacts.yml +++ b/.github/workflows/remove-old-artifacts.yml @@ -16,9 +16,14 @@ jobs: steps: - name: Remove old artifacts - uses: c-hive/gha-remove-artifacts@v1 - with: - age: '1 month' # ' ', e.g. 5 days, 2 years, 90 seconds, parsed by Moment.js - # Optional inputs - # skip-tags: true - # skip-recent: 5 + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + run: | + cutoff="$(date -u -d '1 month ago' +%Y-%m-%dT%H:%M:%SZ)" + + gh api --paginate "repos/$REPO/actions/artifacts" \ + --jq ".artifacts[] | select(.created_at < \"$cutoff\") | .id" | + while read -r artifact_id; do + gh api --method DELETE "repos/$REPO/actions/artifacts/$artifact_id" + done diff --git a/dev-experience/agent-prompts.json b/dev-experience/agent-prompts.json new file mode 100644 index 00000000..f2fc3879 --- /dev/null +++ b/dev-experience/agent-prompts.json @@ -0,0 +1,1278 @@ +[ + { + "id": "GAP-01-002", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 1, + "caseTitle": "", + "title": "Sidebar icons have no tooltips or labels", + "desc": "The 6 sidebar icons (folder, code editor, keyboard, document, library, settings) have no hover tooltip and no visible label. Users must click each one to discover what it does.", + "evidence": "", + "fix": "Add tooltip on hover (100ms delay) and optionally a collapsible label rail. Modern tools (Figma, Linear, Notion) all label their nav icons." + }, + { + "id": "GAP-01-003", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 1, + "caseTitle": "", + "title": "Preview canvas wastes ~75% of available width", + "desc": "The diagram renders in a small floating frame (≈260px wide) pinned to the top-left of a large dark canvas. The remaining right-side space is completely unused.", + "evidence": "", + "fix": "Make the preview fill the available pane width and auto-fit the diagram to the viewport (like Mermaid Live Editor, draw.io). Add a \"fit to screen\" button." + }, + { + "id": "GAP-01-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 1, + "caseTitle": "", + "title": "Settings modal mislabeled as \"Editor\"", + "desc": "The gear icon in the sidebar opens a dialog titled \"Editor\", not \"Settings\". Users looking for app settings will be confused; \"Editor\" implies code editor preferences only.", + "evidence": "", + "fix": "Rename the modal to \"Settings\" or split into \"Editor Preferences\" and \"App Settings\". The icon should match the modal title." + }, + { + "id": "GAP-01-005", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 1, + "caseTitle": "", + "title": "Editor toolbar icons are unlabeled", + "desc": "8+ icon buttons appear above the code editor (participant, arrow types, loops, alt/else blocks) with no labels, no hover tooltips, and no visual grouping. The syntax they insert is non-obvious from the icon alone.", + "evidence": "", + "fix": "Add tooltips showing the inserted syntax snippet. Group related icons with a divider. Consider a label on hover like \"Insert if/else block (Alt)\"." + }, + { + "id": "GAP-01-006", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 1, + "caseTitle": "", + "title": "\"Preserve console logs\" exposed in user settings", + "desc": "A debug/developer option sits alongside user-facing preferences (Theme, Font, Line wrap). This is noise for the vast majority of users and reduces trust.", + "evidence": "", + "fix": "Move debug options behind an \"Advanced\" expandable section or remove from the UI entirely unless explicitly requested." + }, + { + "id": "GAP-01-007", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 1, + "caseTitle": "", + "title": "Title edit affordance is easy to miss", + "desc": "The diagram title \"Data Processing Flow\" has a small pencil icon to the right that only appears at a glance. There's no hover state or underline hint that the title is editable in-place.", + "evidence": "", + "fix": "Show a hover underline on the title text itself (not just the icon) so users know it's clickable. Industry standard: clicking the title text should open the inline editor." + }, + { + "id": "GAP-02-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 2, + "caseTitle": "", + "title": "Cmd+S has zero visual feedback", + "desc": "After pressing Cmd+S to save, the UI is pixel-identical before and after. No toast, no status indicator, no title asterisk, no brief flash — nothing confirms the save happened. Users must guess whether their work is safe.", + "evidence": "", + "fix": "Show a brief \"Saved\" toast (bottom-right, 2s auto-dismiss). Add an unsaved-changes indicator (e.g. \"●\" dot before the title or asterisk in browser tab title) to make the dirty/clean state explicit." + }, + { + "id": "GAP-02-002", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 2, + "caseTitle": "", + "title": "No unsaved-changes indicator", + "desc": "After typing new content but before saving, there is no visual signal that the document has unsaved changes. If the tab is closed or the page refreshes, users lose work with no warning.", + "evidence": "", + "fix": "Show a subtle \"unsaved\" dot/asterisk in the header title while there are uncommitted changes. Intercept the beforeunload event with a browser \"leave page?\" dialog when there are unsaved edits." + }, + { + "id": "GAP-02-003", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 2, + "caseTitle": "", + "title": "Export (PNG) surprises users with a login wall", + "desc": "Clicking \"PNG\" immediately triggers a \"Welcome to ZenUML.com\" login modal with no prior indication that export requires an account. The export button has no lock icon, tooltip, or disabled state to signal it's auth-gated.", + "evidence": "", + "fix": "Show a lock icon on the PNG button with a tooltip \"Sign in to export\". Alternatively, allow anonymous SVG/PNG export for basic diagrams and gate cloud sync/history behind auth. The surprise paywall is the worst UX pattern for conversion." + }, + { + "id": "GAP-02-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 2, + "caseTitle": "", + "title": "Login modal doesn't explain WHY auth is needed", + "desc": "The login modal says \"Welcome to ZenUML.com\" and offers GitHub / Google / Facebook sign-in, but gives no context for why it appeared. Users who clicked \"PNG\" don't know if they're signing in to export, to save to cloud, or for something else entirely.", + "evidence": "", + "fix": "Add a one-sentence context line above the login options: \"Sign in to download your diagram as PNG and access cloud save.\" This dramatically reduces confusion and improves conversion." + }, + { + "id": "GAP-02-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 2, + "caseTitle": "", + "title": "Invalid syntax renders silently as participant boxes", + "desc": "When the user types invalid syntax (e.g. B--> INVALID SYNTAX ???), the parser treats \"INVALID\", \"SYNTAX\", and \"???\" as participant names and renders a diagram with nonsensical boxes — with no error highlight, no inline warning, and no status indicator.", + "evidence": "", + "fix": "Highlight the erroneous line in the editor with a red underline (like VS Code). Show a small error badge in the preview frame (\"⚠ Syntax error on line 2\"). Keep the last valid diagram visible rather than rendering the broken one." + }, + { + "id": "GAP-03-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 3, + "caseTitle": "", + "title": "Delete icon permanently visible on tabs — triggers on misclick", + "desc": "The trash icon (🗑) is always displayed on every deletable page tab. Its position directly adjacent to the \"+ Add Page\" button caused an accidental delete confirmation to fire during normal use. A destructive action sits next to a creation action with no safe gap.", + "evidence": "", + "fix": "Show the delete icon only on tab hover (like browser tab × buttons or VS Code editor tabs). Add at least 24px separation between the last tab's delete zone and the \"+ Add Page\" button. Consider moving delete to a right-click context menu." + }, + { + "id": "GAP-03-002", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 3, + "caseTitle": "", + "title": "No page rename — tabs permanently named \"Page N\"", + "desc": "Pages can only be named \"Page 1\", \"Page 2\", etc. Double-clicking the tab does not trigger inline rename. There is no right-click context menu, no rename icon, and no other affordance. Users building multi-diagram projects have no way to label their pages meaningfully.", + "evidence": "", + "fix": "Support double-click-to-rename inline (standard tab paradigm in VS Code, Figma, Notion). Alternatively show a pencil icon on active tab hover. The rename input should auto-select all text and save on Enter/blur." + }, + { + "id": "GAP-03-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 3, + "caseTitle": "", + "title": "New page editor has no placeholder — only preview has empty state", + "desc": "Switching to a new empty page shows \"Click to add your first participant\" in the preview panel, but the code editor is a completely blank black area. The guidance appears in the wrong place — users need it in the editor where they'll type, not in the read-only preview.", + "evidence": "", + "fix": "Add a grey placeholder in the CodeMirror editor when empty: // Start typing — e.g.\\nA->B: hello(). The preview empty state can remain as a secondary cue, but the primary affordance must be in the editable area." + }, + { + "id": "GAP-03-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 3, + "caseTitle": "", + "title": "Delete confirmation uses \"lost forever\" language with no undo", + "desc": "The delete dialog says \"The data on this page will be lost forever.\" There is no undo option and no way to recover the deleted page. For users who accidentally click delete (which happens easily due to GAP-03-001), their work is permanently gone.", + "evidence": "", + "fix": "Implement a brief undo window (10s toast with \"Undo\" button, like Gmail's delete). Alternatively, move deleted pages to a recoverable trash. At minimum, the dialog should be dismissible with the Escape key." + }, + { + "id": "GAP-03-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 3, + "caseTitle": "", + "title": "No page reordering affordance", + "desc": "Page tabs cannot be reordered by drag-and-drop. Users who add pages in the wrong order must delete and recreate them. No drag handles are shown, and no visual cue suggests tabs are moveable.", + "evidence": "", + "fix": "Add drag-and-drop reordering to page tabs (HTML5 drag API or a library like dnd-kit). Show a drag-handle cursor on tab hover to signal the affordance." + }, + { + "id": "GAP-04-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 4, + "caseTitle": "", + "title": "Share Link — the #1 CTA — requires login with no anonymous fallback", + "desc": "\"Share Link\" is the most prominent button in the app (top-right, blue, always visible). Clicking it immediately shows a login wall. There is no URL-based sharing, no public link, no embed code, no \"share without account\" path — the core sharing value proposition is completely inaccessible to new users.", + "evidence": "", + "fix": "Generate an anonymous share URL immediately (encode diagram in URL hash, like Mermaid Live Editor or Carbon). Gate persistent cloud links behind auth, not the act of sharing itself. The share button should always work — login extends its capabilities." + }, + { + "id": "GAP-04-002", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 4, + "caseTitle": "", + "title": "Same generic login modal for 3+ different auth-gated actions", + "desc": "The identical \"Welcome to ZenUML.com\" modal fires for PNG export, Share Link, and likely other features. No context is given about what the user was trying to do or what they'll get by signing in. Users who clicked \"Share Link\" see no mention of sharing.", + "evidence": "", + "fix": "Each auth prompt must state the specific value: \"Sign in to generate a persistent share link for this diagram\". Show a preview of what they'll get post-login. One generic modal for all contexts is a conversion killer." + }, + { + "id": "GAP-04-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 4, + "caseTitle": "", + "title": "Auto-title \"Untitled 9-5-23:24\" is ambiguous and technical", + "desc": "New diagrams get an auto-generated title using a non-standard date-time format (M-D-HH:MM). It's unclear if \"9-5\" is May 9th or September 5th, what timezone \"23:24\" refers to, and why a timestamp is used instead of a sequential number. Compare: Google Docs uses \"Untitled document\", Figma uses \"Untitled\", VS Code uses \"Untitled-1\". \"Untitled 9-5-23:24\" \"Untitled diagram\" or \"My Diagram 1\"", + "evidence": "", + "fix": "Use a simple sequential name (\"Untitled diagram\", \"Untitled 2\") or prompt users to name the diagram on first save. If a timestamp must be included, use ISO format with timezone: \"2026-05-09 11:24 PM\"." + }, + { + "id": "GAP-04-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 4, + "caseTitle": "", + "title": "My Library shows \"Nothing saved yet\" with no path forward", + "desc": "The empty state for My Library says \"Nothing saved yet.\" but gives no instructions for how to save, no \"Sign in to access your saved diagrams\" CTA, and no explanation that cloud save requires authentication. Users are left in a dead end.", + "evidence": "", + "fix": "Replace the empty state with an actionable message: \"Press Cmd+S to save your diagram here\" (for logged-in users) or \"Sign in to save and organize your diagrams\" with a Sign In button (for guests). Good empty states are onboarding moments." + }, + { + "id": "GAP-04-005", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 4, + "caseTitle": "", + "title": "Opening My Library hides the code editor entirely", + "desc": "Clicking the folder icon replaces the left panel (editor) with the My Library panel. There is no way to see both the library and the editor simultaneously. Users who want to find a saved diagram and compare it to their current work must constantly toggle panels.", + "evidence": "", + "fix": "Implement the library as a slide-in overlay or a resizable third column — don't replace the editor. Alternatively, allow library to appear above the editor as a collapsible drawer. Notion, Linear, and Figma all allow navigation and content panels to coexist." + }, + { + "id": "GAP-04-006", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 4, + "caseTitle": "", + "title": "Library toolbar icons (refresh, upload, download) have no labels", + "desc": "Three small icon buttons appear next to \"New Folder\" in the My Library header. None have labels, visible tooltips, or accessible names. Their functions (refresh list, import, export) are non-obvious from the icons alone.", + "evidence": "", + "fix": "Add hover tooltips with descriptive text. Consider replacing the download/upload icons with text buttons (\"Import\", \"Export\") since the library panel has ample horizontal space." + }, + { + "id": "GAP-05-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 5, + "caseTitle": "", + "title": "Two separate help panels (Shortcuts + Cheat Sheet) split documentation across two unlabeled icons", + "desc": "The sidebar has two distinct help panels: \"Keyboard Shortcuts\" (keyboard icon) and \"Cheat sheet\" (document-info icon). Both are accessed via unlabeled icons with no hover tooltips. Users have no way to know which icon opens which panel without clicking both. Splitting help content across two unlabeled modals fragments the experience and increases cognitive load.", + "evidence": "", + "fix": "Merge both into a single \"Help\" panel with tabs (\"Syntax\" and \"Shortcuts\"), accessible from one labeled icon. Add hover tooltips to all sidebar icons. Figma's help panel, VS Code's keyboard shortcuts UI, and Linear's help center all follow this merged pattern." + }, + { + "id": "GAP-05-002", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 5, + "caseTitle": "", + "title": "Default diagram hints at \"Cheat sheet tab\" — no such tab exists", + "desc": "The pre-loaded example diagram includes the comment // Go to the \"Cheat sheet\" tab or https://docs.zenuml.com. There is no visible \"Cheat sheet tab\" in the interface. Users who follow this instruction will look for a tab in the editor header (ZenUML / CSS) and find nothing. The comment refers to the unlabeled sidebar icon, which appears nowhere near a \"tab\" in the conventional sense.", + "evidence": "", + "fix": "Update the default example comment to accurately describe the UI: // Click the book icon (left sidebar) or press Ctrl+Shift+? for syntax help. Alternatively, rename the sidebar icon's panel to match the comment." + }, + { + "id": "GAP-05-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 5, + "caseTitle": "", + "title": "Keyboard Shortcuts modal uses \"Emmet\" — jargon that means nothing in a diagram tool", + "desc": "The shortcuts modal lists Tab → Emmet code completion. Emmet is an HTML/CSS abbreviation expander with no recognized meaning in sequence diagram editing. ZenUML users (software architects, developers documenting APIs) won't know what \"Emmet completion\" means for participant/message syntax. The label misleads about what Tab actually does in this editor. Tab → Emmet code completion Tab → Auto-complete syntax", + "evidence": "", + "fix": "Replace \"Emmet code completion\" with \"Auto-complete (syntax snippets)\" or just \"Tab completion\". If Emmet is specifically used, add a brief parenthetical: \"Emmet (HTML abbreviation expansion — rarely used in ZenUML)\"." + }, + { + "id": "GAP-05-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 5, + "caseTitle": "", + "title": "Cheat sheet is not scrollable and appears to be cut off", + "desc": "The Cheat Sheet modal shows: Participant, Message, Async message, Nested message, Self-message, Alt, Loop — and the modal bottom is flush with the viewport edge. Scrolling inside the modal does not reveal additional content. ZenUML supports more constructs (title, note, divider, group, stereotype, opt, par, etc.) that are entirely absent. The cheat sheet is incomplete and gives users a false sense they've seen the full syntax.", + "evidence": "", + "fix": "Ensure the cheat sheet is scrollable and lists all supported constructs. Add a \"Full syntax reference →\" link at the bottom pointing to docs.zenuml.com. Show a scroll indicator (shadow or gradient) at the modal bottom to signal more content exists." + }, + { + "id": "GAP-05-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 5, + "caseTitle": "", + "title": "No keyboard shortcut for diagram-level actions (Add Page, Refresh Preview uses obscure Ctrl+Shift+5)", + "desc": "The Global shortcuts list contains Ctrl/⌘ + Shift + 5 for \"Refresh preview\" — a non-intuitive binding (5 has no mnemonic connection to refresh). There are no shortcuts for adding a page, switching pages, or toggling panels. Power users who want keyboard-only workflows are blocked from diagram-level navigation.", + "evidence": "", + "fix": "Replace Ctrl+Shift+5 with Ctrl+Shift+R (mnemonic: Refresh) or F5 (conventional refresh shortcut). Add shortcuts for Add Page (Ctrl+Shift+N), next/previous page (Ctrl+Tab / Ctrl+Shift+Tab), and toggle sidebar (Ctrl+B, like VS Code)." + }, + { + "id": "GAP-06-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 6, + "caseTitle": "", + "title": "Login modal does not trap keyboard focus — input leaks into underlying editor", + "desc": "When the auth modal appears (triggered by CSS tab click), keyboard focus is not constrained to the modal. Typing while the modal is visible sends keystrokes to the CodeMirror editor behind it, corrupting diagram content. Closing the modal then reveals broken ZenUML code — with no indication the editor was modified. This is an accessibility violation (WCAG 2.1 §2.1.2) and a data-integrity bug.", + "evidence": "", + "fix": "Implement proper focus trap in the modal: on open, move focus to the first interactive element; Tab/Shift+Tab must cycle within the modal; Escape closes and returns focus to the trigger element. Use a library like focus-trap or the native dialog element which traps focus by default." + }, + { + "id": "GAP-06-002", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 6, + "caseTitle": "", + "title": "CSS tab is auth-gated with no prior indication — 4th feature to use generic login modal", + "desc": "The CSS tab sits next to the ZenUML tab in the editor header — appearing as an equal peer, always accessible. Clicking it silently triggers the generic \"Welcome to ZenUML.com\" auth modal. No lock icon, no disabled state, no tooltip, no pricing tier badge signals that CSS customization requires an account. Users who've been working for hours on a diagram have no forewarning that switching tabs will interrupt them.", + "evidence": "", + "fix": "Show a lock icon or \"Pro\" badge on the CSS tab for non-authenticated users. On click, show a contextual modal: \"CSS customization is available to signed-in users. Sign in to style your diagrams with custom CSS.\" Alternatively, show a read-only CSS preview with example styles to demonstrate value bef" + }, + { + "id": "GAP-06-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 6, + "caseTitle": "", + "title": "Present button gives zero feedback — silently fails or opens invisible fullscreen", + "desc": "The \"Present\" button in the bottom bar has aria-label=\"Toggle Fullscreen\" and title=\"Toggle Fullscreen Presenting Mode\" — but the button label says only \"Present\". There is no visual feedback when clicked: no loading state, no fullscreen transition animation, no confirmation. In automation testing the button appeared completely non-functional. In real use, fullscreen mode may activate but without any transition or indicator that a mode change occurred.", + "evidence": "", + "fix": "Add a brief visual transition when entering fullscreen (e.g. the diagram panel expands with a smooth animation). Show a visible \"Exit Fullscreen\" (Esc) affordance once in fullscreen mode. The button title and label should match: either \"Present\" or \"Fullscreen\", not both." + }, + { + "id": "GAP-06-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 6, + "caseTitle": "", + "title": "Privacy badge (shield icon) is invisible unless users know to hover it", + "desc": "A shield-with-checkmark icon sits in the top-right corner of the diagram canvas. Hovering it reveals: \"We (the vendor) do not have access to your data. The diagram is generated in this browser.\" This is a genuine trust signal — but it is entirely invisible unless users discover the icon and hover it. New users will never see this message. The privacy guarantee is the strongest selling point for enterprise use, yet it's buried in a hover tooltip on an unlabeled icon.", + "evidence": "", + "fix": "Surface the privacy guarantee during onboarding (first load) as a brief callout or tooltip. Add a visible text label \"Private\" or \"Local-only\" next to the shield icon. Consider adding it to the footer or \"About\" section where users who care about data privacy actively look." + }, + { + "id": "GAP-06-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 6, + "caseTitle": "", + "title": "No way to restore the default example after deleting all editor content", + "desc": "If a user accidentally deletes all content (or over-applies Undo and clears the editor), there is no \"Reset to example\" or \"Insert starter template\" option. The editor becomes a blank black area. The toolbar insert buttons require existing content structure to work correctly. New users who erase the example while experimenting are stuck with an empty canvas and no guidance.", + "evidence": "", + "fix": "Add a \"Load example\" link that appears when the editor is empty (similar to VS Code's \"Open Folder\" empty state). Alternatively, show the starter example in a ghost/placeholder style when empty. A simple \"New from template\" option in the New button dropdown would also solve this." + }, + { + "id": "GAP-07-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 7, + "caseTitle": "", + "title": "All 8 toolbar buttons have zero accessible labels — hover shows nothing", + "desc": "The editor toolbar contains 8 insert buttons. JavaScript DOM inspection confirms every single one has title=\"\", no aria-label, and no visible text. Hovering shows no tooltip. Screen reader users get no announcement. Sighted users must guess icon meaning from small abstract SVGs, then click to discover what gets inserted — there is no \"safe\" way to learn what a button does before committing the action.", + "evidence": "", + "fix": "Add title and aria-label to every button. Show hover tooltips with: (1) the button name, (2) the syntax it inserts. Example: tooltip on the message button → \"Insert synchronous message — A.method()\". This is how VS Code, GitHub's markdown toolbar, and Mermaid Live Editor handle it." + }, + { + "id": "GAP-07-002", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 7, + "caseTitle": "", + "title": "Several toolbar buttons silently do nothing when editor focus is not set correctly", + "desc": "Clicking toolbar buttons transfers focus from the editor to the button itself, which can break the insert action — the button never receives the editor's cursor context. During testing, buttons 1, 2, 5, and 6 produced no visible change even when clicked repeatedly. There is no error message, no disabled state, no indication that a precondition was unmet. Users click, nothing happens, and they assume the button is broken.", + "evidence": "", + "fix": "Buttons should either: (a) always insert at end-of-file as a safe fallback regardless of cursor position, or (b) show a clear disabled/unavailable state with tooltip explaining the precondition. \"Click in the editor first, then use toolbar buttons\" is an invisible precondition users cannot be expect" + }, + { + "id": "GAP-07-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 7, + "caseTitle": "", + "title": "No visual grouping — 8 buttons in a single undifferentiated row", + "desc": "All 8 toolbar buttons appear as an unbroken row with equal spacing. There is no separator, no group label, no visual distinction between \"add participant\", \"message types\" (sync/async/self/return), and \"control structures\" (Alt/Loop). Users who know what an \"alt block\" is must scan all 8 icons to find it. Users who don't know the terminology have no group context to guide discovery.", + "evidence": "", + "fix": "Add a thin 1px separator between groups: [Participant] | [→ → ← ↩ ↔] | [Alt Loop]. Add optional group labels (\"Participants\", \"Messages\", \"Blocks\") above each section, collapsible on small screens. This is standard in markdown editors, code editors, and diagram tools like draw.io." + }, + { + "id": "GAP-07-004", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 7, + "caseTitle": "", + "title": "Toolbar inserts placeholder names (\"message\", \"selfMessage\") that users must manually replace", + "desc": "When toolbar buttons insert code snippets, they use generic placeholders: result = A.message { }, selfMessage(), //Note. These are valid ZenUML syntax but use non-descriptive generic names that break real diagrams unless edited. Better tools select the placeholder text after insertion, ready for the user to immediately type the real name — no double-click-to-select needed.", + "evidence": "", + "fix": "After inserting a snippet, auto-select the first meaningful placeholder token (e.g. \"message\" in A.message()) so the user can immediately type the real name. VS Code snippet tab-stops (${1:placeholder}) implement this pattern well — the cursor moves through editable fields with Tab." + }, + { + "id": "GAP-08-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 8, + "caseTitle": "", + "title": "49 themes with no preview — must close modal to see effect, modal covers editor during selection", + "desc": "The theme dropdown lists 49 CodeMirror themes by internal code name with no color swatches, no preview panel, and no live-preview while hovering. Selecting a theme auto-saves and applies it instantly — but the settings modal sits on top of the editor, blocking the view. Users must dismiss the modal to see whether the theme looks good, then re-open settings to change it again. With 49 options, iterating through themes takes significant effort.", + "evidence": "", + "fix": "Show a small color swatch next to each theme name (background + text + keyword colors). Or add a live mini-preview panel in the modal showing \"A→B: hello()\" in the selected theme. VS Code, JetBrains, and GitHub Codespaces all show real-time theme previews. The infrastructure already supports instant" + }, + { + "id": "GAP-08-002", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 8, + "caseTitle": "", + "title": "Settings modal titled \"Editor\" — gear icon conventionally means \"Settings\", not editor-specific config", + "desc": "The gear icon at the bottom of the sidebar universally signals \"Settings\" or \"Preferences\" in software UI (VS Code, Figma, Linear, Notion, GitHub). Opening it reveals a modal titled \"Editor\" — the scope is narrower than the affordance implies. Users who expect app-level settings (account, keyboard shortcuts, diagram defaults, data storage location) find only editor appearance options. There is no indication that more settings exist elsewhere. Modal title: \"Editor\" (3 appearance settings + 4 toggles) Modal title: \"Settings\" with \"Editor\" as a section header; room for future \"Diagram\", \"Account\"", + "evidence": "", + "fix": "Rename modal to \"Settings\" or \"Preferences\". Promote current options under an \"Editor Appearance\" section header. This leaves room to add \"Diagram defaults\" (background color, watermark, actor style) and \"Account\" sections as features grow — without needing a new mental model for users." + }, + { + "id": "GAP-08-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 8, + "caseTitle": "", + "title": "\"Preserve console logs\" debug toggle exposed to all users at the same level as user-facing settings", + "desc": "The \"Others\" section contains four toggles: Line wrap, Auto-preview, Preserve last written code, and Preserve console logs. The first three are clearly user-facing features. \"Preserve console logs\" is a developer debugging option — it retains browser console output across sessions, which has no meaning to the target audience of sequence diagram creators (architects, product managers, technical writers). It is displayed at identical prominence to Line wrap, with no explanation of what it does or who it's for.", + "evidence": "", + "fix": "Move \"Preserve console logs\" to a collapsible \"Advanced / Developer\" section, or remove it from the UI entirely if it was added only for debugging. If it must remain, add a tooltip: \"Keep browser console output across page reloads — for developers debugging ZenUML integration.\" Exposing internal deb" + }, + { + "id": "GAP-08-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 8, + "caseTitle": "", + "title": "No \"Reset to defaults\" or Cancel — settings are permanent with no undo path", + "desc": "Every setting change shows a \"Setting saved\" toast and immediately persists. The modal has only a close (×) button — no Cancel, no Reset, no Undo. If a user accidentally selects a hard-to-read theme (e.g. colorforth uses black background with dim colored text) and doesn't remember what the previous theme was called, they have no recovery path except scrolling through all 49 options to find something acceptable. The \"Setting saved\" toast actually makes this worse — it emphasizes irreversibility.", + "evidence": "", + "fix": "Add a \"Reset to defaults\" link at the bottom of the modal. Store the last-applied setting before a change (one level of undo). Alternatively, add a \"Cancel\" button that reverts to the state when the modal was opened. VS Code's settings undo (Ctrl+Z works in settings) and macOS System Settings' \"Reve" + }, + { + "id": "GAP-08-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 8, + "caseTitle": "", + "title": "Font size range capped at 12–18px — excludes accessibility users and large-display power users", + "desc": "The Font Size dropdown offers 7 fixed values: 12 px, 13 px, 14 px (default), 15 px, 16 px, 17 px, 18 px. The cap at 18px does not accommodate users with low vision who rely on larger text, nor power users on ultra-high-DPI displays who prefer 20–24px in code editors. Conversely, 12px is below the WCAG minimum of 18pt (24px) for normal text — it may fail accessibility requirements for users who expect accessible code editors.", + "evidence": "", + "fix": "Extend the range to at least 10–28px, or replace the fixed dropdown with a numeric input with +/- stepper (min: 10, max: 32). VS Code allows custom font size via settings JSON with no cap. A reasonable UI range of 10–28px with 1px steps covers virtually all real-world needs. Consider also inheriting" + }, + { + "id": "GAP-09-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 9, + "caseTitle": "", + "title": "Scroll on canvas scrolls the page — not zoom — violating the diagram tool genre convention", + "desc": "Scrolling the mouse wheel on the diagram canvas moves the page up/down. It does not zoom the diagram. This is the single most-violated interaction expectation in diagram tools: every major web-based diagram application (Figma, draw.io, Mermaid Live, Lucidchart, Excalidraw) uses scroll-to-zoom as the primary zoom gesture. Users who expect this behavior will scroll, see the page shift, and conclude either the diagram is broken or zooming is impossible — before ever discovering the tiny ⊕/⊖ icon buttons in the footer bar.", + "evidence": "", + "fix": "Intercept scroll events on the diagram canvas and use them to zoom the diagram (same as Figma, draw.io). Use event.preventDefault() on the iframe's wheel event, then apply the zoom delta. Add Ctrl+scroll as an explicit zoom modifier for users on systems where unmodified scroll is reserved for page n" + }, + { + "id": "GAP-09-002", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 9, + "caseTitle": "", + "title": "No drag-to-pan — diagram cannot be repositioned after zooming or on small viewports", + "desc": "Dragging the mouse on the diagram canvas has no effect — it does not pan the diagram. Once a user zooms in (via the footer ⊕ button), there is no way to scroll/pan to different parts of the diagram. The diagram is effectively locked in its rendered position. On displays smaller than ~900px height, the bottom of longer diagrams is clipped and inaccessible without scrolling the outer page (which moves away from the toolbar, not the diagram). Panning is the expected complement to zoom in every diagram and image tool.", + "evidence": "", + "fix": "Implement drag-to-pan: on mousedown in the canvas, switch cursor to grabbing, track mouse delta, and translate the diagram SVG. Release on mouseup. Space+drag (as in Figma) is an acceptable alternative if direct drag conflicts with future element selection. Add a \"Fit diagram\" button (the ⊡ icon use" + }, + { + "id": "GAP-09-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 9, + "caseTitle": "", + "title": "(i) info icon opens a full-screen syntax overlay — the 3rd separate help surface, covering the diagram entirely", + "desc": "The (i) icon in the canvas footer opens a \"ZenUML Tips\" panel that renders as a full-width, full-height overlay on top of the diagram canvas. This is the third distinct help surface in the app (alongside the keyboard-shortcut icon in the sidebar and the \"quick reference\" cheat-sheet icon). The Tips panel covers the entire diagram — users cannot read a syntax example and see its rendered result simultaneously. Additionally, the icon has no hover tooltip explaining what it opens, so users cannot know what action they are triggering before clicking.", + "evidence": "", + "fix": "Replace the full-screen overlay with a slide-in side panel or a resizable drawer that appears alongside (not over) the diagram. This allows users to read syntax examples while seeing their diagram update in real time — the key learning loop for new users. Consolidate all three help surfaces into one" + }, + { + "id": "GAP-09-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 9, + "caseTitle": "", + "title": "Canvas footer controls have zero tooltips or labels — \"1.2.3\" checkbox function is completely opaque", + "desc": "The canvas footer contains 6 controls: (i), checkbox labeled \"1.2.3\", ⊕, 100%, ⊖, and \"ZenUML.com\". None have hover tooltips, title attributes, or aria-labels. The most opaque is the \"1.2.3\" checkbox: its label literally shows the sequence number format — not what checking/unchecking does. Testing confirmed it toggles sequence number visibility on the diagram, but users must experiment to discover this. The \"ZenUML.com\" text in the footer is equally unclear — it appears to be a branding watermark but looks like a dead link.", + "evidence": "", + "fix": "Add title and aria-label to every footer control: (i) → \"Syntax tips\", 1.2.3 checkbox → \"Show sequence numbers\", ⊕ → \"Zoom in\", 100% → \"Reset zoom to 100%\", ⊖ → \"Zoom out\", ZenUML.com → \"Powered by ZenUML\" (or remove if it adds no value). Show these as hover tooltips. Rename the checkbox label from " + }, + { + "id": "GAP-09-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 9, + "caseTitle": "", + "title": "No \"Fit to screen\" button — no way to reset diagram position after zooming or viewport changes", + "desc": "After zooming in via the ⊕ button, there is no \"fit diagram\" or \"reset view\" shortcut to quickly return to a state where the full diagram is visible. The 100% label resets zoom to 100% but doesn't re-center the diagram. On smaller viewports, the diagram may be partially scrolled out of view and there is no keyboard shortcut or button to snap back to \"show full diagram\". Figma (Shift+1), draw.io (Ctrl+Shift+H), Mermaid Live, and Excalidraw all have a \"fit\" control.", + "evidence": "", + "fix": "Add a \"Fit diagram\" button (⊡ icon, standard in diagram tools) next to the zoom controls. Clicking it should set zoom to show the complete diagram with a small margin, and center it in the viewport. Also add a keyboard shortcut: Ctrl+Shift+F or F (when canvas is focused) as the \"fit\" binding, consis" + }, + { + "id": "GAP-10-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 10, + "caseTitle": "", + "title": "\"Start a blank creation\" loads a complex 18-line pre-filled example, not a blank editor", + "desc": "The most prominent call-to-action in the new-diagram modal is labeled \"Start a blank creation\" with an outlined button style that implies a primary action. Clicking it loads a pre-written example: BookLibService calling Session.findBooks() and BookRepository with try/catch/finally — 18 lines of code involving 4 participants. A new user who wants a blank canvas is immediately confronted with unfamiliar example code they must manually delete before starting their own work.", + "evidence": "", + "fix": "Either: (a) rename the button to \"Start from example\" to accurately describe what happens, or (b) actually deliver a blank editor with a single blank line when this button is clicked. If an example is pedagogically valuable, offer it as a named template (\"Library System Example\") alongside the truly" + }, + { + "id": "GAP-10-002", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 10, + "caseTitle": "", + "title": "Template thumbnails show style icons only — no content preview before committing", + "desc": "The 4 templates (Basic, Black & White, Blue, StarUML) show only a small icon representing the visual style (color scheme, line thickness). There is no way to preview the content of each template before clicking. Testing revealed that \"Blue\" is actually an internal template named \"Advanced\" — a multi-participant order management system with OrderService, PaymentService, InventoryService. Users who pick \"Blue\" because they want a blue color scheme get a complex business flow they didn't expect. Template selection is a blind commitment.", + "evidence": "", + "fix": "Show a hover preview panel (right side of modal) with the rendered diagram when hovering a template. Display: (1) the template's internal name (\"Advanced\"), (2) a small diagram thumbnail, (3) participant count and use-case description. Figma, Notion, and Miro all show content previews before templat" + }, + { + "id": "GAP-10-003", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 10, + "caseTitle": "", + "title": "No save warning before replacing current diagram — unsaved work is silently discarded", + "desc": "Selecting any template or \"Start a blank creation\" immediately replaces the current diagram without checking for unsaved changes. During testing, the active editor contained the \"(Forked) Advanced\" diagram — work that had just been created. Clicking \"Start a blank creation\" discarded it instantly, with no confirmation dialog, no \"Save changes?\" prompt, and no undo path back to the previous state. The only indication anything happened was a toast: \"New item created.\" Any user who clicked \"+ New\" by accident or curiosity loses their work permanently.", + "evidence": "", + "fix": "Before replacing the current diagram, check if there are unsaved changes. If yes, show a confirmation: \"You have unsaved changes in [tab name]. Discard and create new diagram?\" with Cancel and Discard buttons. This is standard in every code editor, document editor, and diagramming tool (Figma, Lucid" + }, + { + "id": "GAP-10-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 10, + "caseTitle": "", + "title": "Toast message \"was forked\" uses developer jargon opaque to end users", + "desc": "When a template is selected, the toast notification reads: \"Advanced was forked\". The term \"forked\" is borrowed from software version control (git fork) and means nothing to non-developer users. A marketing designer, business analyst, or student would not know what \"forked\" means in this context — does it mean copied, modified, saved, broken? Even technical users might expect \"forked\" to mean the template remains linked to an upstream source they can pull updates from (as in GitHub forks). The term is misleading and alienating to a broad user base.", + "evidence": "", + "fix": "Replace \"was forked\" with plain language: \"Advanced template opened as a new diagram\" or simply \"New diagram created from Advanced template.\" The word \"forked\" should never appear in user-facing UI — it belongs in developer console logs or commit messages, not product notifications." + }, + { + "id": "GAP-10-005", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 10, + "caseTitle": "", + "title": "Marketing tweet solicitation embedded inside the creation modal — wrong context", + "desc": "The new-diagram modal contains a social media call-to-action asking users to tweet about ZenUML. This appears directly alongside the template selection UI, competing for visual attention in a context where the user has a specific goal: create a new diagram. Embedding marketing asks inside task-completion flows violates the single-responsibility principle of UI screens and creates cognitive friction at the worst possible moment — when the user is about to start working.", + "evidence": "", + "fix": "Move social media asks to post-action moments (after export, after sharing), to onboarding completion, or to an \"About\" / settings page. Never interrupt a task-starting flow with marketing. If tweet solicitation is required, make it a dismissable banner elsewhere in the UI — not inside a creation di" + }, + { + "id": "GAP-10-006", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 10, + "caseTitle": "", + "title": "Only 4 style-variant templates — no use-case or domain-specific templates", + "desc": "The template library offers 4 options: Basic, Black & White, Blue, StarUML. All 4 appear to be style variants of the same or similar content — they differentiate by color scheme and visual style, not by use case or domain. There are no domain-specific starter templates for common sequence diagram scenarios: API authentication flow, microservices communication, e-commerce checkout, CI/CD pipeline, user login sequence, WebSocket handshake. These are the diagrams developers actually draw most often.", + "evidence": "", + "fix": "Add 6–10 use-case templates covering common developer scenarios: API auth (OAuth2/JWT), REST CRUD, WebSocket handshake, login flow, microservice call chain, database transaction. Label them by use case, not by visual style. Style selection can be a separate step or theme preference. Notion, Confluen" + }, + { + "id": "GAP-11-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 11, + "caseTitle": "", + "title": "Global undo stack crosses page boundaries — Cmd+Z on Page 1 undoes Page 2 edits", + "desc": "ZenUML uses a single CodeMirror editor instance shared across all pages. Switching pages replaces the editor content but does not reset or checkpoint the undo history. As a result, pressing Cmd+Z on Page 1 after having edited Page 2 undoes Page 2's changes while the UI shows Page 1 as selected. A second Cmd+Z empties the editor entirely. The diagram canvas continues rendering stale Page 2 content, creating a split-brain state where the editor and diagram are out of sync. This is a data corruption bug — a user can silently overwrite or erase another page's content simply by pressing Undo on the", + "evidence": "", + "fix": "Each page's CodeMirror instance must maintain an independent undo history. Either: (a) create separate CodeMirror instances per page (one per tab), or (b) save and restore the full history state when switching between pages. VS Code, Notion, and Obsidian all maintain per-document undo histories — sw" + }, + { + "id": "GAP-11-002", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 11, + "caseTitle": "", + "title": "No warning before Cmd+Z silently erases entire page content", + "desc": "When the undo history reaches the initial state (before any content was entered), a single Cmd+Z can clear the entire editor to an empty state. In testing: starting from \"A → B: hello\" (2 lines), one Cmd+Z jumped back to the 18-line example, and one more cleared everything. There is no safeguard, no \"Undo will erase all content on this page — continue?\" confirmation, no count of how many undos remain before the page goes blank. Silent erasure of all work with no recovery path is one of the most damaging editor UX failures.", + "evidence": "", + "fix": "Add a visual undo depth indicator (small counter near the editor, e.g. \"⌘Z · 3 steps\") so users know how close they are to the bottom of the stack. When the next Cmd+Z would clear all content, show a brief inline warning: \"Press again to clear all content (no more undo history).\" Sublime Text and VS" + }, + { + "id": "GAP-11-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 11, + "caseTitle": "", + "title": "Toolbar inserts are not atomic undo units — require character-by-character reversal", + "desc": "When the toolbar \"Participant\" button inserts NewParticipant (13 characters), pressing Cmd+Z once removes the entire token — that part works. However, the newline inserted by the toolbar to separate the token from surrounding content requires an additional Cmd+Z, making the complete reversal of one toolbar action require 2+ undo steps. More critically: clicking a toolbar button first steals focus from the editor, meaning the user must re-click the editor before pressing Cmd+Z — or the undo may fire on the wrong context. The disconnect between toolbar action and undo granularity violates user m", + "evidence": "", + "fix": "Wrap each toolbar button click into a single CodeMirror transaction (cm.operation()) that groups all text insertions and cursor movements into one atomic undo entry. Ensure focus returns to the editor before the transaction is committed so Cmd+Z immediately after toolbar use reverses exactly the ins" + }, + { + "id": "GAP-11-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 11, + "caseTitle": "", + "title": "~2 second diagram re-render delay creates a decoupled undo experience", + "desc": "After pressing Cmd+Z, the editor content updates immediately (correct behavior). However, the diagram canvas takes approximately 2 seconds to re-render the undone state. During this window, the editor shows the undone text but the diagram still shows the superseded content — creating a contradictory view where both panels are simultaneously \"right\" but at different points in time. Users who glance at the diagram after pressing Cmd+Z will see stale content and may press Cmd+Z again, overshooting their intended undo target.", + "evidence": "", + "fix": "Show an explicit loading/re-rendering indicator on the diagram canvas during the 2-second render delay — a subtle spinner, pulsing border, or \"Rendering…\" label. This tells users \"the diagram is catching up\" rather than leaving them in a false steady state. Consider debounce optimization: if the re-" + }, + { + "id": "GAP-11-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 11, + "caseTitle": "", + "title": "Redo shortcut (Cmd+Shift+Z) is undocumented — not listed in cheat sheet or any visible help", + "desc": "Redo works via Cmd+Shift+Z, which was confirmed in testing. However, this shortcut appears nowhere in the visible UI — not in the keyboard shortcuts panel, not in the cheat sheet, not in any tooltip. Users who know Cmd+Z for undo will typically try Cmd+Shift+Z (macOS standard) or Cmd+Y (Windows standard) for redo. The ZenUML cheat sheet panel lists multiple shortcuts but omits undo/redo entirely. A user who accidentally over-undoes cannot recover their work unless they already know the platform-specific redo shortcut from external knowledge.", + "evidence": "", + "fix": "Add Cmd+Z (undo) and Cmd+Shift+Z (redo) to the keyboard shortcuts cheat sheet panel. Add tooltips to any undo/redo buttons if they exist (currently none are visible in the toolbar). Even a one-line mention in the \"Help\" sidebar would dramatically reduce the chance of users losing work by not knowing" + }, + { + "id": "GAP-12-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 12, + "caseTitle": "", + "title": "No error message in the diagram panel — invalid syntax renders a silent partial or empty diagram", + "desc": "When the ZenUML parser encounters invalid syntax, the diagram canvas does not show an error state. For mid-document errors (e.g. @@@ bad token @@@ on line 2), the canvas renders only the valid content before the error — showing a partial diagram with no indication it is incomplete. For fully unparseable input (e.g. !!!BROKEN!!!SYNTAX!!!), the canvas renders a lone participant icon — which looks identical to an intentionally empty diagram. There is no \"Syntax error\" banner, no error count, no line reference, no red border or warning color on the canvas. Users believe their diagram is correct wh", + "evidence": "", + "fix": "When the parser produces an error, the diagram canvas should show a visible error state: a red/orange border or banner reading \"Syntax error on line N — diagram may be incomplete.\" Include the error line number and a brief description. Mermaid Live Editor, PlantUML server, and Kroki all show explici" + }, + { + "id": "GAP-12-002", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 12, + "caseTitle": "", + "title": "No editor gutter indicators — error lines have no squiggly underline, no line number icon, no hover tooltip", + "desc": "The CodeMirror editor has no error annotations for invalid ZenUML syntax. Lines with parse errors get a faint pink syntax-highlighting tint (inconsistently — some invalid inputs show no highlighting at all), but there is no red squiggly underline, no gutter icon (🔴 or ⚠), and no hover tooltip explaining what is wrong with the line. A user looking at @@@ bad token here @@@ on line 2 sees only a differently-colored line with no explanation. They cannot tell whether the color means \"this is a comment\", \"this is a string\", \"this is a keyword\", or \"this is an error.\"", + "evidence": "", + "fix": "Register CodeMirror linting annotations for the ANTLR parser's error output. On parse error, mark the offending token(s) with: (1) a red wavy underline on the token, (2) a red circle icon in the gutter at the error line number, (3) a hover tooltip with the parser error message. VS Code, Monaco Edito" + }, + { + "id": "GAP-12-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 12, + "caseTitle": "", + "title": "Parser silently truncates the diagram at the first error — valid content below the error is dropped without warning", + "desc": "When an error appears on line N of a multi-line diagram, the ZenUML parser halts and renders only lines 1 through N-1. Lines N+1 onward are silently discarded — they never appear in the diagram and produce no warning. In testing with a 3-line document (valid / invalid / valid), only line 1 appeared in the diagram. Line 3's C → D: world was completely dropped. A user who has a long diagram and accidentally introduces an error mid-document will see half their diagram disappear — with no indication of where the cutoff happened or how many lines were skipped.", + "evidence": "", + "fix": "Either: (a) implement error recovery in the parser so it skips the invalid line and continues rendering the rest of the document (best experience — user sees all valid parts), or (b) show a count of dropped lines at the bottom of the diagram: \"⚠ 2 lines skipped due to errors — see line 2.\" Option (a" + }, + { + "id": "GAP-12-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 12, + "caseTitle": "", + "title": "Syntax highlighting color alone signals errors — non-compliant with WCAG 1.4.1 (color not sole differentiator)", + "desc": "The only error signal in the editor is a faint pink/magenta tint on lines that contain invalid tokens. This coloring: (1) is inconsistent — completely garbage input (!!!BROKEN!!!) receives no tinting at all, (2) is indistinguishable from intentional syntax coloring for strings or keywords to a colorblind user, (3) carries no semantic label — hovering the colored line reveals nothing, and (4) violates WCAG 1.4.1, which requires that color not be the sole means of conveying information. A red-blind user sees the pink-tinted lines as identical to normal code lines.", + "evidence": "", + "fix": "Pair any color-based error indication with a non-color signal: a gutter icon, an underline style, a tooltip, or a text label. The WCAG test is simple: \"if I remove all color from this screenshot, can I still tell which lines have errors?\" Currently the answer is no. Adding a ⚠ glyph in the gutter sa" + }, + { + "id": "GAP-12-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 12, + "caseTitle": "", + "title": "Auto-bracket completion silently masks some syntax errors that the user intended to observe", + "desc": "The editor auto-closes { to {} and ( to () immediately on typing. While this is ergonomic for normal use, it means a user who types an intentionally partial token to test what the diagram does (e.g. typing A.method( then pausing to look at the diagram) sees an auto-corrected valid result rather than an error state. Developers debugging ZenUML syntax are robbed of the ability to observe incremental parse states. Additionally, auto-completion that fires immediately with no opt-out can be disruptive for users who intended to type a different character.", + "evidence": "", + "fix": "Add a settings option to disable auto-bracket completion (already logged as part of Case 08 settings gaps). More importantly, implement a brief delay before auto-completing (e.g. 300ms) so users can type the opening bracket, glance at the diagram, and continue — rather than having the completion fir" + }, + { + "id": "GAP-13-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 13, + "caseTitle": "", + "title": "Page tabs cannot be renamed — no UI affordance exists for page-level naming", + "desc": "Multi-page ZenUML diagrams permanently label their pages \"Page 1\", \"Page 2\", etc. with no way to rename them. Testing exhausted all standard interactions: double-click (nothing), right-click (browser toolbar, no app menu), DOM inspection (plain buttons, no contentEditable or event handlers). Users who use pages to separate diagram sections — e.g. \"Login Flow\", \"Checkout Flow\", \"Error Handling\" — must use workarounds: reading page content to identify them, or maintaining a mental map of which number corresponds to which concept. Figma, Notion, and every multi-page tool that exists supports page", + "evidence": "", + "fix": "Add page rename via double-click on the tab text (universal standard). Implement an inline text input that appears on double-click, pre-selected with the current name, confirmed by Enter and cancelled by Escape. Also add a right-click context menu with \"Rename\", \"Delete\", \"Duplicate\" options for dis" + }, + { + "id": "GAP-13-002", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 13, + "caseTitle": "", + "title": "Diagram title rename is hidden behind a hover-only pencil icon — clicking the title text does nothing", + "desc": "The diagram title rename workflow requires: (1) hovering over the title in the header to reveal the pencil icon, (2) clicking specifically on that ~16×16px icon — clicking the title text itself has no effect. During testing, clicking the title area without perfect icon targeting does nothing, providing no feedback that an editable field exists. Users who don't hover precisely enough never see the pencil. Users who click the title text expect it to become editable (standard in tools like Notion, Figma, Google Docs). The current UX forces pixel-precision discovery of a hidden affordance.", + "evidence": "", + "fix": "Make the entire title text area clickable to activate rename (not just the pencil icon). Show the pencil icon persistently (not only on hover) or replace it with a visible edit indicator. A subtitle label \"Click to rename\" or a subtle underline on the title signals editability without adding UI clut" + }, + { + "id": "GAP-13-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 13, + "caseTitle": "", + "title": "Long titles display only the tail — \"…overflow or truncation issues\" not \"A very long diagram…\"", + "desc": "When a diagram title exceeds the header width, the field scrolls to show the end of the string rather than the beginning. The result is a header displaying \"low or truncation issues\" — the final words of a 107-character title — with no indication that the title starts with different text, and no ellipsis or fade. Users see a fragment that looks like an incomplete or corrupted title. Industry standard for overflowing titles is to show the beginning with a trailing ellipsis (\"A very long diagram t…\") so the identifying start is always visible. Additionally, hovering the title shows no tooltip wi", + "evidence": "", + "fix": "When the title overflows in display mode, truncate at the end with an ellipsis: text-overflow: ellipsis; overflow: hidden; white-space: nowrap. Add title attribute with full text for hover tooltip. In edit mode, scrolling the input to the right is acceptable — but display mode must always show the s" + }, + { + "id": "GAP-13-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 13, + "caseTitle": "", + "title": "Empty title is accepted — clearing the name and pressing Enter produces a blank untitled diagram", + "desc": "Deleting all characters from the title field and pressing Enter commits the empty string as the diagram name. The header shows a bare empty input box — visually indistinguishable from the rename-mode state — with no placeholder, no fallback name, and no error. Pressing Escape correctly restores the previous title, but this recovery path requires the user to know to press Escape before clicking elsewhere (clicking elsewhere while empty does not restore). An empty diagram name breaks library display, share links, and any UI surface that references the diagram by name.", + "evidence": "", + "fix": "Validate the title field on submit: if the field is empty or whitespace-only, either (a) restore the previous title and show a brief inline message \"Diagram title cannot be empty\", or (b) replace with a default like \"Untitled Diagram\". Show a character counter and placeholder text (\"Enter diagram na" + }, + { + "id": "GAP-13-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 13, + "caseTitle": "", + "title": "Auto-generated names (\"Untitled 10-5-4:8\") are machine-readable timestamps, not human-readable defaults", + "desc": "New diagrams receive names like \"Untitled 10-5-4:8\" — which encodes a date/index in a format that is neither a natural language name nor a recognizable date format. Users who create multiple diagrams without renaming them see a list of cryptic timestamps in their Library. The pattern is not explained anywhere in the UI. \"Untitled\" is a clear and universal fallback; \"Untitled 10-5-4:8\" is confusing because the numbers suggest meaning but require decoding. Compare: GitHub creates repos named after your username; Notion creates \"Untitled\"; Figma creates \"Untitled\".", + "evidence": "", + "fix": "Default new diagram names to \"Untitled\" or \"New Diagram\" — simple, human-readable, internationally recognizable. If deduplication is needed, use \"Untitled (2)\", \"Untitled (3)\". Avoid exposing timestamps or internal counters in user-facing names. The rename prompt (or title click-to-edit) can appear " + }, + { + "id": "GAP-14-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 14, + "caseTitle": "", + "title": "All 6 sidebar icons missing aria-label — screen reader announces ligature text", + "desc": "Every sidebar button relies solely on the HTML title attribute for labeling. The button text content is the raw Material Icons ligature string (folder_open, code_blocks, quick_reference, etc.). A screen reader announces these strings verbatim, providing zero semantic value.", + "evidence": "
DOM evidence — all 6 icons", + "fix": "" + }, + { + "id": "GAP-14-002", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 14, + "caseTitle": "", + "title": "Library panel cannot be dismissed — Code Editor icon inert when Library is open", + "desc": "Once \"My Library\" is opened, the user has no working path back to the Code Editor:", + "evidence": "
Verified interactions — all fail to close Library", + "fix": "" + }, + { + "id": "GAP-14-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 14, + "caseTitle": "", + "title": "540px dead zone splits sidebar into two visually disconnected icon clusters", + "desc": "The 6 sidebar icons are distributed in two polarised groups with no visual grouping, separator, or label to explain the split:", + "evidence": "
y-positions from getBoundingClientRect()", + "fix": "" + }, + { + "id": "GAP-14-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 14, + "caseTitle": "", + "title": "Same icon strip, three different interaction patterns with no visual differentiation", + "desc": "The 6 icons look identical in style but trigger fundamentally different interaction types, with no affordance distinguishing them: Users cannot predict the outcome of clicking any icon. An external link inside an icon-only sidebar is a particularly dangerous pattern — the user loses context with no warning.", + "evidence": "", + "fix": "" + }, + { + "id": "GAP-14-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 14, + "caseTitle": "", + "title": "Typo in Cheatsheet: \"Asyc message\" and naming inconsistency \"Cheatsheet\" vs \"Cheat sheet\"", + "desc": "Minor copy quality issues in the help content:", + "evidence": "", + "fix": "" + }, + { + "id": "GAP-15-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 15, + "caseTitle": "", + "title": "Export buttons show generic auth modal with no context — user doesn't know why or what they'll get", + "desc": "Clicking \"Export as PNG\" or \"Copy PNG to Clipboard\" (both visible in the bottom bar) immediately shows the \"Welcome to ZenUML.com\" login modal. The modal provides zero context:", + "evidence": "
Auth modal observed after clicking either export button", + "fix": "" + }, + { + "id": "GAP-15-002", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 15, + "caseTitle": "", + "title": "Zero anonymous export path — user who built a diagram cannot get it out without creating an account", + "desc": "A new user can open the app, build a complete sequence diagram, and have no way to export or share it without creating an account. There is no free export tier whatsoever: This creates a high-friction drop-off point for the most natural first action after building a diagram. Industry standard is at least one free export format (Mermaid Live → SVG free, PlantUML → PNG free, Excalidraw → PNG/SVG free).", + "evidence": "
Competitor comparison — anonymous export availability", + "fix": "" + }, + { + "id": "GAP-15-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 15, + "caseTitle": "", + "title": "No SVG export option — only PNG, and PNG is locked", + "desc": "The app offers only two export actions (PNG download, Copy PNG), both locked. SVG export is completely absent:", + "evidence": "", + "fix": "" + }, + { + "id": "GAP-15-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 15, + "caseTitle": "", + "title": "Cmd+S is completely silent for anonymous users — no save, no feedback, no auth prompt", + "desc": "The keyboard shortcuts panel lists Ctrl/⌘ + S as \"Save current creations\". For an anonymous user, pressing Cmd+S with editor focused produces absolutely no response: The export buttons at least respond with a modal. Cmd+S is worse — it silently does nothing, leaving users to wonder if it worked, failed, or simply isn't implemented.", + "evidence": "", + "fix": "" + }, + { + "id": "GAP-15-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 15, + "caseTitle": "", + "title": "No right-click context menu on diagram canvas — missed export shortcut", + "desc": "Right-clicking the rendered diagram canvas shows only the browser's native context menu (with options like \"Inspect\", \"Save page as\", etc.). No app-level context menu appears:", + "evidence": "", + "fix": "" + }, + { + "id": "GAP-16-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 16, + "caseTitle": "", + "title": "No responsive layout — desktop split-pane renders at full width on 390px mobile", + "desc": "The entire application renders as a desktop layout with no breakpoint adaptations for mobile screen widths. The split-pane editor+canvas design assumes a wide viewport and fails catastrophically on narrow screens:", + "evidence": "", + "fix": "" + }, + { + "id": "GAP-16-002", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 16, + "caseTitle": "", + "title": "Diagram canvas reduced to 55px sliver — completely unusable on mobile", + "desc": "At 390px width, the layout allocates: ~50px sidebar + ~200px editor pane + ~55px canvas sliver + ~85px overflow. The canvas is visible as a 55px strip showing only the leftmost edge of the first participant box:", + "evidence": "
Space allocation at 390px", + "fix": "" + }, + { + "id": "GAP-16-003", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 16, + "caseTitle": "", + "title": "All export buttons (Present, PNG, Copy PNG) disappear completely on mobile", + "desc": "The bottom action bar contains \"Present | ↓ PNG | ⎘ Copy PNG\" buttons on the right side of the screen. At 390px, these buttons overflow beyond the viewport and are completely inaccessible:", + "evidence": "", + "fix": "" + }, + { + "id": "GAP-16-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 16, + "caseTitle": "", + "title": "Diagram title wraps to 3 lines in header — wastes vertical space and looks broken", + "desc": "The header title element contains \"My Auth Flow Diagram\" (from the previous session). At 390px, this wraps into three lines: \"My Auth / Flow / Diagram\". The header height inflates significantly:", + "evidence": "", + "fix": "" + }, + { + "id": "GAP-16-005", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 16, + "caseTitle": "", + "title": "Insert toolbar clips to 4 icons — 50%+ of editing shortcuts unreachable on mobile", + "desc": "The insert toolbar above the editor shows 8+ icon buttons (New participant, Async, Sync, Return value, Self message, New instance, Conditional, Loop, and more). At 390px, only the first 4 fit:", + "evidence": "", + "fix": "" + }, + { + "id": "GAP-16-006", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 16, + "caseTitle": "", + "title": "No touch-optimized interactions on diagram canvas — pinch-zoom and swipe-pan absent", + "desc": "Even at desktop sizes, the diagram canvas lacks scroll-to-zoom and drag-to-pan (documented in Case 09). On mobile these gaps are worse because touch users have no mouse wheel alternative:", + "evidence": "", + "fix": "" + }, + { + "id": "GAP-17-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 17, + "caseTitle": "Keyboard Navigation & Focus Management", + "title": "Focus rings fail WCAG 2.4.11 minimum area requirement", + "desc": "Every interactive element — sidebar icons, toolbar buttons, header buttons — uses the browser's 1px default focus outline in rgb(0, 95, 204). Against the dark #1e293b backgrounds this produces a contrast ratio well below the WCAG 2.4.11 (AA, 2024) minimum: a focus indicator must have at least a 3:1 contrast ratio against adjacent colors AND enclose an area ≥ the perimeter of a 2 CSS px border around the component. Impact: Keyboard-only users and low-vision users relying on focus rings cannot reliably track where keyboard focus is located. This is a WCAG 2.2 Level AA failure.", + "evidence": "// JS audit result for first 15 focusable elements:\n// All returned: outline = \"rgb(0, 95, 204) auto 1px\"\n// Share Link exception: \"rgb(245, 245, 245) auto 1.5px\"\n// Background of sidebar icon strip: #1e293b (#30353d effective)\n// Contrast of #005FCC on #30353d: ~3.4:1 border but at 1px width\n// the enclosed area is far below 2px border perimeter requirement", + "fix": "Replace the browser default with an explicit outline: 2px solid #60a5fa; outline-offset: 2px; on all focusable elements, or use the :focus-visible pseudo-class with a 2px+ ring that passes 3:1 against the adjacent background color. The Share Link button's 1.5px white ring is closer but still undersi" + }, + { + "id": "GAP-17-002", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 17, + "caseTitle": "Keyboard Navigation & Focus Management", + "title": "CodeMirror editor unreachable in first 20+ Tab stops", + "desc": "The CodeMirror editor is the primary work surface of the application, yet keyboard users must Tab through at least 20+ interactive elements — 3 header buttons, 6 sidebar icons, 8+ toolbar buttons — before the editor's textarea receives focus. The first Tab stop is the \"New\" button in the header, not the editor. Impact: Keyboard-only users opening the app to write ZenUML code face an excessive navigation burden before reaching the input surface. WCAG 2.1 Success Criterion 2.4.3 (Focus Order) requires a \"meaningful sequence\" — placing the primary editing canvas last in tab order violates this cr", + "evidence": "// Focus traversal order (programmatic JS audit, 15 elements):\n// 1: New 2: Share Link 3: Profile 4: My Library\n// 5: Code Editor 6: Keyboard Shortcuts 7: Cheatsheet\n// 8: Language Guide 9: Settings 10: New participant\n// 11: Async message 12: Sync message 13: Return value\n// 14: Self message 15: New instance ...\n// CodeMirror textarea: position >> 20", + "fix": "Add tabindex=\"1\" to the CodeMirror wrapper (or use CodeMirror's built-in tabIndex option) so the editor is the first Tab stop after the skip-nav link. Alternatively restructure DOM order: editor before sidebar/toolbar in source order, use CSS for visual layout." + }, + { + "id": "GAP-17-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 17, + "caseTitle": "Keyboard Navigation & Focus Management", + "title": "6 sidebar icons consume sequential Tab stops between header and toolbar", + "desc": "All 6 left-sidebar icon buttons (My Library, Code Editor, Keyboard Shortcuts, Cheatsheet, Language Guide, Settings) are individually focusable and appear consecutively in the tab order. For a tool-switching sidebar this is unnecessary: only the currently active panel icon needs to be reachable; the others could use a roving tabindex pattern (arrow keys to navigate icons, single Tab to leave the group). Impact: 6 extra Tab stops between the header and the toolbar adds navigation friction. Combined with GAP-17-002 this means keyboard users Tab 20+ times to reach the editor.", + "evidence": "", + "fix": "Implement the ARIA toolbar / roving tabindex pattern for the sidebar icon strip: give the strip role=\"toolbar\" or role=\"navigation\", make only the active icon tabindex=\"0\" and the rest tabindex=\"-1\", then use arrow key handlers to move between icons. This compresses 6 stops into 1." + }, + { + "id": "GAP-17-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 17, + "caseTitle": "Keyboard Navigation & Focus Management", + "title": "Toolbar insert buttons missing aria-label — announced by icon ligature text", + "desc": "All 8+ toolbar insert buttons (New participant, Async message, Sync message, etc.) have ariaLabel: null. The visible content is a Material Icons ligature string (\"person_add\", \"swap_horiz\", etc.) which is what screen readers announce verbatim. Users with assistive technology hear \"person add button\" rather than \"Insert new participant\". This was also partially documented in Case 14 for sidebar icons. The toolbar buttons are a separate, wider set of the same pattern.", + "evidence": "// JS audit:\nbuttons.map(b => ({label: b.ariaLabel, text: b.textContent.trim()}))\n// Results: [{label: null, text: \"person_add\"}, {label: null, text: \"swap_horiz\"}, ...]", + "fix": "Add descriptive aria-label to each toolbar button: aria-label=\"Insert new participant\", aria-label=\"Insert asynchronous message\", etc. Also add title attributes so sighted keyboard users see tooltips on focus (addresses Case 07 GAP-07-001 too)." + }, + { + "id": "GAP-17-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 17, + "caseTitle": "Keyboard Navigation & Focus Management", + "title": "No skip-to-content link for keyboard users", + "desc": "There is no \"Skip to editor\" (skip-nav) link at the top of the page. On every page load or tab switch, keyboard users must Tab through all header and sidebar controls to reach the editor. This is standard accessibility infrastructure expected by WCAG 2.4.1 (Bypass Blocks, Level A). Impact: Lower severity because GAP-17-002 (editor tabindex priority) would be the more impactful fix; a skip link is a complementary safeguard. However WCAG 2.4.1 is a Level A criterion, making this a baseline conformance failure.", + "evidence": "", + "fix": "Add a visually hidden but focusable Skip to editor as the first element in . Show it on focus with :focus { position: fixed; top: 0; left: 0; ... }. Assign id=\"editor\" to the CodeMirror container." + }, + { + "id": "GAP-18-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 18, + "caseTitle": "Color Contrast & Visual Accessibility", + "title": "Share Link CTA button fails WCAG AA — 3.04:1 (needs 4.5:1)", + "desc": "The primary conversion button \"Share Link\" renders near-white text rgb(245,245,245) on a medium-blue background rgb(103,134,247). At 14px font size this requires a contrast ratio of 4.5:1 — the measured ratio of 3.04:1 falls 33% short of WCAG AA compliance. Impact: This is the most prominent interactive element in the header, visible on every diagram. Users with low vision, in bright ambient light, or on low-gamut displays may struggle to read it. WCAG 1.4.3 (Contrast Minimum, Level AA) failure.", + "evidence": "// Measured via JavaScript WCAG luminance formula:\nfg: rgb(245, 245, 245) → L = 0.904\nbg: rgb(103, 134, 247) → L = 0.215\nratio = (0.904 + 0.05) / (0.215 + 0.05) = 3.04:1\nWCAG AA requires 4.5:1 for text ≤18px non-bold", + "fix": "Darken the button background to at least #4355CC (passes 4.74:1) or increase the text to 18px/bold (drops requirement to 3:1). Alternatively invert to a dark button with light text: background:#1e293b; color:#fff easily passes at 15:1." + }, + { + "id": "GAP-18-002", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 18, + "caseTitle": "Color Contrast & Visual Accessibility", + "title": "CodeMirror line numbers fail WCAG AA — 3.83:1 (needs 4.5:1)", + "desc": "Editor line numbers render as rgb(109,138,136) (a muted teal) on rgb(40,42,54) (the Dracula theme background). At 14px with normal weight, the measured 3.83:1 ratio is below the 4.5:1 WCAG AA threshold for normal text. While line numbers are often considered secondary UI, they are functional text that keyboard-navigating users, developers debugging syntax, and screen reader users rely on to locate specific lines. WCAG 1.4.3 applies to all meaningful text.", + "evidence": "// Line numbers (.CodeMirror-linenumber):\nfg: rgb(109, 138, 136) → L = 0.228\nbg: rgb(40, 42, 54) → L = 0.024\nratio = (0.228 + 0.05) / (0.024 + 0.05) = 3.83:1\nRequired: 4.5:1 (14px normal weight)", + "fix": "Brighten the line number color to approximately rgb(145,165,163) or higher. With Dracula theme, a value around #A8B8B7 achieves 5:1+ while remaining clearly secondary to code content. Alternatively, bump font size to 18px (drops threshold to 3:1)." + }, + { + "id": "GAP-18-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 18, + "caseTitle": "Color Contrast & Visual Accessibility", + "title": "Diagram SVG graphical elements use #A5A5A5 on white — 2.46:1 (needs 3:1)", + "desc": "Block-type icons (the <> diamond for \"Alt\" and the loop arrow for \"Loop\") are rendered as SVG text/paths in #A5A5A5 on the white diagram canvas. The measured contrast ratio is 2.46:1 — below the WCAG 1.4.11 (Non-text Contrast, Level AA) minimum of 3:1 required for graphical UI components that convey meaning. These icons are the only visual indicator of which block type (conditional vs. loop) is being represented. A user who cannot distinguish the icon must rely on the block label text instead.", + "evidence": "// SVG text elements with fill=\"#A5A5A5\":\n// [\"Alt\", \"Loop\"] — confirmed via document.querySelectorAll('svg text')\nfg: #A5A5A5 = rgb(165,165,165) → L = 0.376\nbg: white = rgb(255,255,255) → L = 1.000\nratio = (1.0 + 0.05) / (0.376 + 0.05) = 2.46:1\nWCAG 1.4.11 requires 3:1 for graphical objects", + "fix": "Change the SVG icon fill from #A5A5A5 to #767676 or darker. #767676 achieves exactly 4.54:1 on white — solidly passing AA. Or use a distinct shape/stroke instead of relying on the icon alone." + }, + { + "id": "GAP-18-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 18, + "caseTitle": "Color Contrast & Visual Accessibility", + "title": "Syntax highlighting uses color as the sole differentiator between token types", + "desc": "In the CodeMirror editor, different token types are distinguished exclusively by color: identifiers in green, message text in pink/magenta, arrows in green, keywords and punctuation in white. No secondary cues — bold weight, italics, underline — differentiate token types for users with color vision deficiency. An estimated 8% of males have some form of color vision deficiency. For a developer tool targeting engineers, this is a significant portion of the audience. WCAG 1.4.1 (Use of Color, Level A) prohibits using color as the only visual means of conveying information. Test: In a deuteranopia", + "evidence": "", + "fix": "Add a secondary visual signal to at least the most important distinctions: make keywords bold (font-weight: 700), make strings italic, or use the Dracula theme's built-in styles which provide some weight variation. CodeMirror supports per-token styles via the theme configuration." + }, + { + "id": "GAP-18-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 18, + "caseTitle": "Color Contrast & Visual Accessibility", + "title": "White diagram canvas creates extreme luminance split against dark editor — no dark-diagram option", + "desc": "The split-pane layout places a dark editor (#282a36, very dark) directly beside a bright white diagram canvas (#ffffff). The luminance ratio between the two panels is approximately 100:1. Users working extended sessions or with photosensitivity must continuously adapt their eyes between the two extreme luminance zones. Modern tools (Mermaid Live Editor, dbdiagram.io, Excalidraw) all offer dark diagram themes or allow diagram background color configuration. ZenUML's CSS tab could theoretically allow this, but the feature requires knowing the internal CSS class names — it is not discoverable.", + "evidence": "", + "fix": "Add a \"Diagram theme\" toggle in Settings (light/dark/auto). The dark diagram theme would simply change the diagram wrapper's background and SVG line/text colors. A one-line CSS variable change could power the entire switch: --diagram-bg: #1e1e2e; --diagram-ink: #cdd6f4;." + }, + { + "id": "GAP-19-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 19, + "caseTitle": "Tooltip & Contextual Help UX", + "title": "7 toolbar insert buttons: zero tooltip, no keyboard shortcut hint, no syntax preview", + "desc": "All 7 insert buttons in the editor toolbar (New participant, Async message, Sync message, Return value, Self message, New instance, ConditionalAlt) have title=null, aria-label=null, and no custom tooltip component. The visible text label alone leaves users without: 1. Keyboard shortcut — is there one? Can I trigger \"Async message\" from the keyboard? 2. Resulting syntax — hovering \"Async message\" should preview A ->> B: message 3. Insertion behavior — does it insert at cursor or append at end? Competing tools (Mermaid Live Editor, draw.io, PlantUML Editor) all show syntax snippets in toolbar to", + "evidence": "// JS audit result (all 7 toolbar buttons):\n{ text: \"New participant\", title: null, ariaLabel: null }\n{ text: \"Async message\", title: null, ariaLabel: null }\n{ text: \"Sync message\", title: null, ariaLabel: null }\n{ text: \"Return value\", title: null, ariaLabel: null }\n{ text: \"Self message\", title: null, ariaLabel: null }\n{ text: \"New instance\", title: null, ariaLabel: null }\n{ text:", + "fix": "Add a title attribute with the pattern \"[Label] — inserts: [syntax snippet]\". Example: title=\"Async message — inserts: A ->> B: message\". For a richer experience, replace with a Radix UI Tooltip showing the syntax with syntax highlighting and the keyboard shortcut if one exists." + }, + { + "id": "GAP-19-002", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 19, + "caseTitle": "Tooltip & Contextual Help UX", + "title": "Inconsistent tooltip mechanism across UI zones — 3 different patterns, one invisible", + "desc": "Three different tooltip strategies coexist with inconsistent behavior for sighted mouse users:", + "evidence": "", + "fix": "Standardize on one tooltip mechanism app-wide. Recommended: Radix UI Tooltip on every interactive element, with a consistent 300ms open delay and 100ms close delay. The existing Radix UI dependency is already in the project — use it." + }, + { + "id": "GAP-19-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 19, + "caseTitle": "Tooltip & Contextual Help UX", + "title": "Canvas footer controls (ⓘ, ☑ 1.2.3, zoom icons) have no tooltip — \"1.2.3\" is cryptic", + "desc": "The diagram canvas footer contains 5 interactive controls with zero tooltip: • ⓘ — what does this show? (appears to open a tips overlay) • ☑ 1.2.3 — completely cryptic; hovering reveals nothing. It toggles sequence step numbers, but this is undiscoverable without clicking. • ⊕ / ⊖ — zoom icons with no label or shortcut hint • 100% — clicking this resets zoom, but hover reveals no hint The \"1.2.3\" checkbox is the worst offender — even an experienced UML author might not know what this does on first sight. The label reads as a version number, not as \"show/hide sequence numbers.\"", + "evidence": "", + "fix": "Add title attributes: title=\"Show/hide step numbers\" on the 1.2.3 checkbox, title=\"Tips & shortcuts\" on ⓘ, title=\"Zoom in (Ctrl+Plus)\" on ⊕, title=\"Zoom out (Ctrl+Minus)\" on ⊖, title=\"Reset zoom to 100%\" on the percentage text. Replace the 1.2.3 label with \"1.2\" or better — a sequence-number icon — " + }, + { + "id": "GAP-19-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 19, + "caseTitle": "Tooltip & Contextual Help UX", + "title": "No keyboard shortcut hints in any tooltip across the entire app", + "desc": "Even the 9 elements that do have tooltips (sidebar icons, header buttons) include no keyboard shortcut information. Modern productivity tools universally surface shortcuts in tooltips — VS Code, Figma, Linear, and Notion all follow the pattern: \"Action Name (Shortcut)\". ZenUML has a dedicated Keyboard Shortcuts panel (accessible via the sidebar) but zero in-context shortcut hints. Users must remember to open that panel separately. For frequent actions like \"New diagram\" (Cmd+N if supported), the shortcut is entirely invisible at the point of action.", + "evidence": "// Example of what tooltips could say:\nsidebar \"My Library\": \"My Library\" → ❌ no shortcut\nsidebar \"Keyboard Shortcuts\": \"Keyboard Shortcuts\" → ❌ ironic: no shortcut hint\nheader \"New\": \"Start a new creation\" → ❌ should say \"(Cmd+N)\"", + "fix": "Append the keyboard shortcut (if one exists) to every tooltip: title=\"My Library (Ctrl+B)\". For the Keyboard Shortcuts icon specifically, the tooltip \"Keyboard Shortcuts (Ctrl+/)\" would be self-documenting. Use a consistent format: label first, shortcut in parentheses." + }, + { + "id": "GAP-19-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 19, + "caseTitle": "Tooltip & Contextual Help UX", + "title": "Bottom bar action buttons (Present, PNG, Copy PNG) lack descriptive tooltips", + "desc": "The three action buttons in the bottom-right corner of the app — \"Present\", \"PNG\", and \"Copy PNG\" — have no tooltip. \"Present\" is particularly ambiguous: does it open a link? Enter fullscreen? Launch a slideshow? A user who hasn't tried it has no pre-click information about the outcome. \"PNG\" is slightly better (the format name is self-explanatory to technical users), but new users may not know whether clicking triggers a download, opens a dialog, or copies to clipboard.", + "evidence": "// Confirmed zero title/ariaLabel on all three:\n{ text: \"Present\", title: null, ariaLabel: null }\n{ text: \"PNG\", title: null, ariaLabel: null }\n{ text: \"Copy PNG\", title: null, ariaLabel: null }", + "fix": "Add descriptive titles: title=\"Enter presentation mode (fullscreen)\", title=\"Download diagram as PNG image\", title=\"Copy diagram PNG to clipboard\". These would disambiguate before the user clicks — especially important since PNG and Copy PNG look visually similar." + }, + { + "id": "GAP-20-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 20, + "caseTitle": "CSS Tab & Custom Styling UX", + "title": "CSS tab has zero visual indicator it is auth-gated — no lock, no badge, looks identical to free ZenUML tab", + "desc": "Both the ZenUML tab and the CSS tab use identical visual styling: bg-black-800 font-semibold. The CSS tab carries no lock icon, no \"Pro\" or \"Sign in\" badge, no greyed-out state, no tooltip warning. A user has no pre-click information that the CSS tab requires authentication. This violates the principle of transparent feature gating — when a feature requires upgrade or authentication, that requirement should be visible at the entry point, not revealed only after the user clicks. Showing the gate only at click-time is a dark pattern known as \"bait and switch.\"", + "evidence": "", + "fix": "Add a lock icon (🔒) and a \"Pro\" or \"Sign in\" badge to the CSS tab. On hover, show a tooltip: \"CSS customization — sign in to unlock\". Alternatively, allow unauthenticated users to view a read-only CSS editor prefilled with a default template, with a banner prompting sign-in to save." + }, + { + "id": "GAP-20-002", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 20, + "caseTitle": "CSS Tab & Custom Styling UX", + "title": "5th generic \"Welcome to ZenUML.com\" auth modal — zero context about the CSS feature", + "desc": "Clicking the CSS tab shows the same generic modal as Share Link, Export PNG, Copy PNG, and Present mode — all 5 show identical text: \"Welcome to ZenUML.com / Login with Github / Login with Google / Login with Facebook / Join a community of 50,000+ Developers.\" The modal contains: • No mention of CSS customization — what does it unlock? • No preview or screenshot of what CSS styling looks like • No indication of tier — is it free with login, or paid? • No \"Learn more\" link for users who want to understand before committing The 5-feature pattern of identical auth gates means that by the 3rd or 4", + "evidence": "// All 5 features trigger the SAME modal:\n// 1. Share Link (Case 4)\n// 2. Present Mode (Case 6)\n// 3. Export PNG / Copy PNG (Case 15)\n// 4. CSS Tab (this case — #5)\n// Modal text: \"Welcome to ZenUML.com\" — no feature context", + "fix": "Replace the generic modal with a feature-specific upsell: show the CSS editor UI as a teaser (blurred or read-only), a 1-2 sentence description (\"Write custom CSS to brand your diagrams with company colors and fonts\"), and a clear CTA. Each feature's auth gate should contextualize WHY sign-in is nee" + }, + { + "id": "GAP-20-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 20, + "caseTitle": "CSS Tab & Custom Styling UX", + "title": "CSS editor is absent from DOM entirely — no preview, no placeholder, no teaser for unauthenticated users", + "desc": "When unauthenticated, the CSS tab's editor container is completely absent from the DOM — confirmed via document.querySelector('[class*=\"css-editor\"]') returning null and zero CSS-related divs rendered. There is no placeholder, empty state, or teaser of what the editor would contain. In contrast, tools like Mermaid Live Editor, draw.io, and CodePen show the full editor interface to all users but restrict saving/sharing behind auth. This \"try before you sign up\" approach drives significantly higher conversion because users can experience the value before being asked to commit.", + "evidence": "// JS audit:\ndocument.querySelector('[class*=\"css-editor\"]') // → null\ndocument.querySelectorAll('[class*=\"css\"]') // → 0 elements\n// The CSS editor panel is never rendered for anon users\n// No placeholder, no empty state, no sample CSS shown", + "fix": "Render the CSS editor for unauthenticated users with a sample CSS snippet (e.g., .participant { background: #e8f4fd; }). Allow free editing but block saving with an inline \"Sign in to save your changes\" banner. This lets users experience the feature's value before signing up." + }, + { + "id": "GAP-20-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 20, + "caseTitle": "CSS Tab & Custom Styling UX", + "title": "Active tab state relies on a single 2px blue underline — low visual salience, no background differentiation", + "desc": "The active tab (ZenUML) is differentiated from the inactive tab (CSS) by only: a border-b border-primary (2px bottom border in blue) and a minor background shift from bg-black-800 to bg-black-500. At normal viewing distance, the active state is not immediately obvious — the text color change to text-primary-400 (blue-ish) is subtle against the dark background. Modern tab implementations (Chrome DevTools, VS Code, Figma panels) use a clear background color contrast, not just a border underline, to communicate active state. The current implementation passes a quick glance test only because there", + "evidence": "// Active tab class audit:\n// ZenUML (active): \"... border-b border-primary bg-black-500 text-primary-400\"\n// CSS (inactive): \"... bg-black-800 font-semibold\"\n// Difference: border-b + 1 Tailwind shade lighter bg + blue text\n// bg-black-500 vs bg-black-800: very close luminance values in dark palette", + "fix": "Increase active tab visual contrast: use a distinctly lighter background (e.g., bg-zinc-700) with a rounded top-corners treatment, or switch to a pill/chip active-tab style with a full background fill. The border-b alone is the weakest form of tab indicator in dark UIs." + }, + { + "id": "GAP-20-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 20, + "caseTitle": "CSS Tab & Custom Styling UX", + "title": "No documentation or CSS selector reference anywhere in the app", + "desc": "Even if a user successfully authenticates to access the CSS editor, there is no in-app documentation about: • What CSS selectors target which diagram elements (e.g., .participant, .message-label) • Which CSS properties are supported (SVG CSS vs HTML CSS) • Example CSS snippets for common customizations (brand colors, font changes) • A link to external documentation or a \"CSS Reference\" panel The Language Guide sidebar panel exists for ZenUML syntax, but there is no equivalent guide for the CSS editor. A power user discovering CSS customization must reverse-engineer the SVG structure by hand.", + "evidence": "", + "fix": "Add a collapsible \"CSS Reference\" panel within the CSS editor (similar to the Language Guide for ZenUML). Include the top 10 most-used selectors with examples: .participant { }, .message { }, .divider { }, etc. A single-page CSS cookbook linked from the editor would dramatically improve discoverabil" + }, + { + "id": "GAP-21-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 21, + "caseTitle": "Split Pane Resize & Layout Control", + "title": "Split pane gutter is completely invisible — 6px, near-transparent background, no hover affordance", + "desc": "The divider between the editor and canvas panels is a 6px-wide .gutter.gutter-horizontal element with background: rgba(255,255,255,0.05) — effectively invisible against the dark UI. There is no CSS hover rule that highlights it, no grip dots, no color change. The cursor does switch to ew-resize when hovering the gutter, but discovering that 6px strip by accident is the only signal the feature exists. Impact: The vast majority of users will never discover they can resize the split. In 20 UX test cases, the gutter was never stumbled upon organically — it was only found through DOM inspection. Th", + "evidence": "// Gutter DOM audit:\nclass: \"gutter gutter-horizontal\"\nwidth: 6px\nbackground: rgba(255, 255, 255, 0.05) // 5% opacity white — invisible\ncursor: ew-resize // only signal, requires hitting the 6px strip\nNo CSS :hover rule defined for .gutter selector\nNo grip icon, no handle, no visual affordance", + "fix": "Add a visible grip affordance: center 3 horizontal dots or a subtle bar on the gutter. On hover, brighten background to rgba(255,255,255,0.15) and show the dots more clearly. A 2px visible stripe on the left edge of the canvas pane (like VS Code's activity bar separator) would be enough. Minimum: ad" + }, + { + "id": "GAP-21-002", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 21, + "caseTitle": "Split Pane Resize & Layout Control", + "title": "Neither pane can be collapsed — minSize:15% on both sides prevents focus mode", + "desc": "The paneforge configuration enforces minSize: 15 (15% of total width) on both the editor and canvas panes. This means: • Users cannot collapse the canvas to get a full-width editor for writing long ZenUML scripts • Users cannot collapse the editor to get a distraction-free, full-width diagram view • Maximum editor width is 85%, maximum canvas width is 85% Developer tools universally offer panel collapse: VS Code collapses any panel to 0, JetBrains has explicit collapse buttons, Mermaid Live Editor has a \"hide editor\" toggle. Power users regularly want to maximize one pane.", + "evidence": "// localStorage['paneforge:liveEditor']:\n// {\"defaultSize\":30,\"minSize\":15},{\"minSize\":15}\n// layout: [30, 70] (30% editor, 70% canvas default)\n// minSize: 15% on both = neither can collapse below 15%\n// At 1920px viewport: editor min = 288px, canvas min = 288px", + "fix": "Add explicit collapse/expand buttons to both panels (a chevron icon at the panel boundary), and set collapsible: true in paneforge config with collapsedSize: 0. Add keyboard shortcuts: Cmd+\\ to toggle the split, Cmd+B to collapse/expand the editor (following VS Code conventions)." + }, + { + "id": "GAP-21-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 21, + "caseTitle": "Split Pane Resize & Layout Control", + "title": "Default 30/70 split gives only 30% to the editor — primary work surface is undersized by default", + "desc": "The paneforge default configuration allocates defaultSize: 30 (30%) to the editor and 70% to the canvas. At a 1920px viewport, this gives the editor ~576px — barely enough for a 7-line ZenUML script. A typical sequence diagram with 8-10 participants and 15-20 messages will overflow the editor height, requiring scrolling while writing. The diagram canvas by contrast has 1344px of width, most of which is empty dark space. The actual rendered diagram sits centered in a fraction of that area. The default split optimizes for the output, not the input — backwards for an editor-first tool.", + "evidence": "// Default paneforge layout: [30, 70]\n// At 1920px viewport:\n// Editor: 30% = 576px (after sidebar)\n// Canvas: 70% = 1344px\n// Typical diagram canvas renders in ~400px SVG width\n// → 944px of canvas is empty dark space at default split", + "fix": "Change the default split to 40/60 or 50/50. Run an A/B test — for a code editor tool, giving the editor equal or majority space tends to improve session length and diagram complexity. The 30% default feels like a viewer app, not an editor app." + }, + { + "id": "GAP-21-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 21, + "caseTitle": "Split Pane Resize & Layout Control", + "title": "No double-click to reset split — persisted custom ratio has no escape hatch", + "desc": "The split ratio is persisted to localStorage['paneforge:liveEditor'] and survives page reloads. Once a user drags the gutter to an undesirable position (e.g., accidentally making the editor very narrow), there is no way to reset it except dragging back manually — with the invisible gutter. Double-clicking the gutter was tested and confirmed to have no reset behavior. VS Code, IntelliJ, and most modern split-pane implementations support double-click to reset to the default ratio. This is a discoverable escape hatch that doesn't require the user to find the Settings or clear localStorage.", + "evidence": "// Double-click test result:\n// Before: editorWidth = 795px (42.9%)\n// After double-click: editorWidth = 795px (unchanged)\n// paneforge does not implement double-click-to-reset\n// Only way to reset: drag back manually or clear localStorage", + "fix": "Add double-click handler on the gutter element that calls paneforge's reset() or sets the split back to [30, 70]. Also add a \"Reset layout\" option in the Settings modal for users who want to recover from a bad split position." + }, + { + "id": "GAP-21-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 21, + "caseTitle": "Split Pane Resize & Layout Control", + "title": "6px gutter is too narrow for reliable hit-testing, especially on high-DPI displays", + "desc": "The gutter's 6 CSS pixel width at 2× DPR renders as a 12 physical pixel strip. Fitts's Law quantifies this: the time to acquire a 6px target at 500px distance is approximately 3× longer than acquiring a 12px target at the same distance. This is compounded by the invisible background — users cannot see where the target is. VS Code uses a 5px gutter with an 8px transparent hover area on each side (effective target: 21px). IntelliJ uses a visible 6px border with highlight. Both are significantly more discoverable than ZenUML's invisible 6px strip.", + "evidence": "", + "fix": "Keep the visual strip at 4-6px but expand the interactive hit area to 20px via transparent padding or a wider wrapper element with pointer-events: all. The visual grip stays narrow; the click target becomes comfortable." + }, + { + "id": "GAP-22-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 22, + "caseTitle": "Error State & Syntax Validation UX", + "title": "Invalid syntax silently renders a blank canvas — no error message, no inline squiggle, no notification", + "desc": "When a user types completely invalid ZenUML (e.g., !!!INVALID@@@SYNTAX###), the canvas silently collapses to a near-empty diagram showing only an unlabeled internal _STARTER_ participant (a stickman box). There is: • No error banner above or below the diagram • No inline red squiggles in the editor (no CodeMirror lint markers; errorGutterChildCount: 0) • No toast or notification (toasts: 0) • No DOM error element visible (errorBanners: []) • The canvas iframe has errorElements: [] — nothing conveys the error state to the user Competing tools handle this dramatically better: Mermaid Live Editor", + "evidence": "// JS DOM audit with input: \"!!!INVALID@@@SYNTAX###\\nthis is not valid zenuml at all\\n{{{ broken\"\n// Main page:\nerrorBanners: [] // no visible error elements\neditorErrorMarks: 1 // only the empty error-gutter container div itself\ngutterMarkers: 0 // no error markers placed in gutter\ntoasts: 0 // no toast notifications\n// Canvas iframe:\nerrorElements: [] ", + "fix": "Show a parse error banner below the editor toolbar (or as a red stripe at the bottom of the canvas) with the ANTLR error message and the offending line number. Minimum viable: wrap the diagram renderer in an error boundary that catches parser exceptions and renders a styled
" + }, + { + "id": "GAP-22-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 22, + "caseTitle": "Error State & Syntax Validation UX", + "title": "CodeMirror error-gutter is configured but never populated — inline error markers are dead infrastructure", + "desc": "The CodeMirror instance has a custom gutter registered as \"error-gutter\" — confirmed by the presence of a
in the DOM. This gutter was clearly designed to show per-line error indicators (like a red ✕ or warning triangle next to the offending line number). However, gutterMarkers: 0 — no markers are ever placed in this gutter, even with 3 lines of completely invalid syntax. The gutter exists as a visible empty column next to the line numbers, occupying space without serving any purpose. The infrastructure for inline error indicators was planned but nev", + "evidence": "// DOM audit:\ndocument.querySelector('.error-gutter')\n// →
(empty)\n\n// Expected CodeMirror API to populate it:\ncm.setGutterMarker(lineNumber, \"error-gutter\", markerElement);\n// This call is never made anywhere in the codebase for parse errors.\n\n// Result: error-gutter exists (takes up space) but is permanently empty", + "fix": "Complete the error-gutter integration: when the ZenUML parser throws, extract the line number from the ANTLR error, create a styled marker element (e.g., a red circle with an ✕ icon), and call cm.setGutterMarker(errorLine - 1, \"error-gutter\", marker). Clear markers on valid parse. This provides exac" + }, + { + "id": "GAP-22-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 22, + "caseTitle": "Error State & Syntax Validation UX", + "title": "Editor and canvas desync after undo past initial load — canvas shows stale content while editor is empty", + "desc": "After typing invalid syntax and pressing Cmd+Z ten times, the editor empties completely (line 1 blank) while the canvas continues to show a fully rendered diagram (the previously saved \"Auth Flow\" diagram). The editor state and the rendered canvas state are out of sync. This represents a violation of the \"what you see is what you get\" contract: the canvas is showing a diagram that is no longer represented by any editor content. A user in this state would: • Think they still have a valid diagram when the editor shows nothing • Be unable to determine what code produced the current canvas • Be co", + "evidence": "// Observed state after 10× Cmd+Z:\neditor.getValue() → \"\" (empty)\ncanvas iframe shows: A→B: Login, B→C: Validate, 3. token, 4. Welcome!\n// The two panels disagree on the diagram content\n// Undo history exhausted past the initial load state\n// No visual indicator that editor/canvas are out of sync", + "fix": "When the editor is emptied (content length drops to 0), either: (a) render an empty-state placeholder in the canvas rather than the last diagram, or (b) set a minimum editor content of the default sample diagram (preventing undo past initial load). Track the \"floor\" undo state — when undo reaches th" + }, + { + "id": "GAP-22-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 22, + "caseTitle": "Error State & Syntax Validation UX", + "title": "Internal _STARTER_ participant name leaks into the visible canvas on parse failure", + "desc": "When ZenUML fails to parse the input, it renders a fallback participant named _STARTER_ — an internal implementation detail. This participant is visible in the canvas iframe as a stickman box with no label. While the participant label itself is hidden (the stickman icon is the only rendering), the text _STARTER_ is present in the DOM (confirmed via allVisibleText: [\"Click to add title\", \"_STARTER_\"]) and could be visible in exported PNGs or in certain zoom/render states. Users who export the diagram in an error state (not knowing the parse failed silently) may receive a PNG with a single unlab", + "evidence": "// Iframe DOM audit during invalid syntax state:\ndoc.querySelectorAll('[class*=\"participant\"]').length → 1\nallVisibleText: [\"Click to add title\", \"_STARTER_\"]\n// lifeline class: \"lifeline absolute flex flex-col h-full starter\"\n// The \"starter\" CSS class and \"_STARTER_\" text are internal implementation names\n// exposed to the DOM when parsing fails", + "fix": "Replace the _STARTER_ fallback rendering with an explicit empty state component that shows a message like \"Your diagram will appear here\" or \"Fix the syntax error to see the diagram.\" This separates the intentional empty state from the error state, and prevents internal implementation names from lea" + }, + { + "id": "GAP-23-001", + "severity": "high", + "sevColor": "#ef4444", + "caseNum": 23, + "caseTitle": "Responsive & Mobile Layout UX", + "title": "Split-pane layout never stacks vertically — both panels become unusably narrow on mobile", + "desc": "The .main-content-area is always display: flex; flex-direction: row with no responsive breakpoint that switches to flex-direction: column. At a 375px iPhone SE viewport, the default 30/70 split gives: • Editor: ~113px wide — a 113px code editor is entirely unusable. A typical variable name exceeds the visible width. • Canvas: ~263px wide — a 3-participant sequence diagram would be horizontally clipped at this width • No toggle to switch to single-pane view — unlike Mermaid Live Editor or StackBlitz which offer \"preview only\" mode on mobile The app has a viewport meta tag set to width=device-wi", + "evidence": "// CSS audit: .main-content-area has NO responsive flex-direction rules\n.main-content-area { display: flex; flex-direction: row; /* ← permanent, no breakpoint */ }\n\n// At 375px viewport (iPhone SE, scaling from 1920px base):\n// Scale factor: 375 / 1920 = 0.195\neditorWidth = (1920 × 0.30) × 0.195 = ~113px // unusable\ncanvasWidth = (1920 × 0.70) × 0.195 = ~263px // severely clipped\n\n// No respon", + "fix": "Add a responsive stacking breakpoint: at max-width: 768px, set .main-content-area { flex-direction: column; }. Stack the canvas above the editor (diagram-first, as users on mobile are more likely to view than edit). Add a tab-style toggle (\"Edit / Preview\") to switch between the two panes on mobile." + }, + { + "id": "GAP-23-003", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 23, + "caseTitle": "Responsive & Mobile Layout UX", + "title": "No mobile navigation pattern — the full icon sidebar and header persist at all viewport sizes", + "desc": "The left sidebar (5 icon buttons: My Diagrams, Editor, Keyboard, Language Guide, Settings) and the full header (New, diagram title, Share Link, avatar) remain unchanged at all viewport widths. There is no hamburger menu, no bottom tab bar, no collapsible sidebar — the mobile navigation is simply the desktop navigation crushed to unusability. Modern responsive apps follow one of: (a) hamburger menu that slides in a full sidebar, (b) bottom navigation bar for primary destinations, (c) progressive disclosure where secondary nav items hide behind a \"More\" button. ZenUML uses none of these — the si", + "evidence": "// Confirmed via DOM audit:\nhamburgerMenu: \"no\" // no hamburger/menu-toggle element\nsidebarToggle: \"none\" // no toggle button found\ntouchEventElements: 0 // no ontouchstart/ontouchmove handlers\npointerMediaQueries: [] // no @media (pointer: coarse) adaptations\n// Tailwind responsive class inventory (entire app):\nsm: classes: 0 // zero adaptations at 640px\nmd: cla", + "fix": "Add a mobile navigation pattern: at max-width: 768px, hide the vertical sidebar icon column and replace with a horizontal bottom bar showing the 3-4 most critical actions (My Diagrams, Language Guide, Settings, + a \"...\" overflow). Alternatively, convert the sidebar to a slide-in drawer triggered by" + }, + { + "id": "GAP-23-004", + "severity": "medium", + "sevColor": "#f59e0b", + "caseNum": 23, + "caseTitle": "Responsive & Mobile Layout UX", + "title": "Header becomes horizontally scrollable at 600px — diagram title and title bar clip silently", + "desc": "At max-width: 600px, the CSS rule .main-header { overflow-x: auto } kicks in, making the header horizontally scrollable. This means at narrow viewports, users must horizontally scroll to access the \"Share Link\" button, the diagram title, and the avatar — but there is no visual indicator (no scroll shadow, no chevron) that horizontal scrolling is available. The \"New\" button and the \"Share Link\" button are on opposite ends of the header. At 400px, a user may only see one of them without scrolling. The diagram title — centrally positioned — may be hidden off-screen. This violates the principle of", + "evidence": "// CSS at max-width: 600px:\n.main-header { overflow-x: auto; } // header becomes scrollable\n.main-header__btn-wrap { flex-shrink: 0; } // button group stays at full width\n\n// At 375px:\n// Header height: ~10px (53px × 0.195 scale)\n// Share Link button (rightmost): off-screen without scrolling\n// Diagram title (center): clipped\n// No scroll indicator shown (no -webkit-overflow-sc", + "fix": "Redesign the header for mobile: at max-width: 768px, remove the diagram title from the header center (it's secondary information on mobile), show only \"New\" on the left and a \"⋯\" overflow menu on the right containing Share Link and other actions. This keeps all primary actions reachable without hori" + }, + { + "id": "GAP-23-005", + "severity": "low", + "sevColor": "#60a5fa", + "caseNum": 23, + "caseTitle": "Responsive & Mobile Layout UX", + "title": "No touch/pointer event adaptations — swipe gestures and touch-specific diagram interactions absent", + "desc": "The app has zero ontouchstart, ontouchmove, or touch* event handlers anywhere in the DOM (confirmed: touchEventElements: 0). It also has no @media (pointer: coarse) CSS adaptations for touch-screen devices. This means: • No pinch-to-zoom on the canvas — users cannot use native touch gestures to zoom the diagram on a tablet or phone • No swipe-to-switch panels — on mobile, swiping left/right could naturally switch between editor and canvas views • No touch-friendly scrolling in the editor (CodeMirror's touch support is limited without explicit configuration) • Hover-dependent tooltips never tri", + "evidence": "// DOM audit:\ntouchEventElements: 0 // no touch event handlers anywhere\npointerMediaQueries: [] // no @media (pointer: coarse) rules\n// CodeMirror touch support requires:\n// cm.setOption(\"lineWrapping\", true) — not set by default\n// Hammer.js or similar for pinch/swipe on canvas — not installed\n// canvas iframe also has no touch event listeners", + "fix": "Add @media (pointer: coarse) CSS rules to increase spacing and target sizes for touch users (even before a full mobile layout redesign). For the canvas, add touch-action: none on the diagram iframe and implement pinch-zoom via the Pointer Events API. For the editor, enable CodeMirror's touch-friendl" + } +] \ No newline at end of file diff --git a/dev-experience/app.js b/dev-experience/app.js new file mode 100644 index 00000000..7d7b3d93 --- /dev/null +++ b/dev-experience/app.js @@ -0,0 +1,224 @@ +(function() { + 'use strict'; + + const state = { + data: null, + filteredGaps: [], + filters: { severity: 'all', case: 'all', principle: 'all', search: '' }, + }; + + const el = (id) => document.getElementById(id); + + fetch('gaps.json', { cache: 'no-store' }) + .then(r => r.json()) + .then(init) + .catch(err => { + el('gap-list').innerHTML = `

Failed to load gaps.json

${err.message}

`; + }); + + function init(data) { + state.data = data; + + // Flatten gaps with case context + const flatGaps = []; + for (const c of data.cases) { + for (const g of c.gaps) { + flatGaps.push({ + ...g, + caseNumber: c.number, + caseTitle: c.title, + caseSlug: c.slug, + caseGif: c.gif, + }); + } + } + state.allGaps = flatGaps; + + // KPIs + el('kpi-total').textContent = flatGaps.length; + el('kpi-high').textContent = data.totals.high; + el('kpi-medium').textContent = data.totals.medium; + el('kpi-low').textContent = data.totals.low; + el('kpi-cases').textContent = data.cases.length; + + const principleSet = new Set(); + for (const g of flatGaps) for (const p of g.principles) principleSet.add(p); + const principles = [...principleSet].sort(); + el('kpi-principles').textContent = principles.length; + + el('footer-cases').textContent = data.cases.length; + el('footer-gaps').textContent = flatGaps.length; + el('generated-date').textContent = data.generated.slice(0, 10); + + // Populate dropdowns + const caseSel = el('filter-case'); + caseSel.innerHTML = '' + + data.cases.map(c => ``).join(''); + + const principleSel = el('filter-principle'); + principleSel.innerHTML = '' + + principles.map(p => ``).join(''); + + // Wire up filters + document.querySelectorAll('#filter-severity .pill').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('#filter-severity .pill').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + state.filters.severity = btn.dataset.value; + syncSeverityKpis(); + render(); + }); + }); + + document.querySelectorAll('.kpi[data-sev]').forEach(kpi => { + kpi.addEventListener('click', () => { + const sev = kpi.dataset.sev; + const isActive = kpi.classList.contains('active-sev'); + const target = isActive ? 'all' : sev; + document.querySelectorAll('#filter-severity .pill').forEach(b => { + b.classList.toggle('active', b.dataset.value === target); + }); + state.filters.severity = target; + syncSeverityKpis(); + render(); + }); + }); + + caseSel.addEventListener('change', () => { + state.filters.case = caseSel.value; + render(); + }); + principleSel.addEventListener('change', () => { + state.filters.principle = principleSel.value; + render(); + }); + + el('search-input').addEventListener('input', debounce(e => { + state.filters.search = e.target.value.trim().toLowerCase(); + render(); + }, 120)); + + el('reset-filters').addEventListener('click', () => { + state.filters = { severity: 'all', case: 'all', principle: 'all', search: '' }; + el('search-input').value = ''; + caseSel.value = 'all'; + principleSel.value = 'all'; + document.querySelectorAll('#filter-severity .pill').forEach(b => { + b.classList.toggle('active', b.dataset.value === 'all'); + }); + syncSeverityKpis(); + render(); + }); + + // Modal close + const modal = el('gap-modal'); + modal.addEventListener('click', (e) => { + if (e.target === modal) modal.close(); + }); + + render(); + } + + function syncSeverityKpis() { + document.querySelectorAll('.kpi[data-sev]').forEach(kpi => { + kpi.classList.toggle('active-sev', kpi.dataset.sev === state.filters.severity); + }); + } + + function render() { + const f = state.filters; + const filtered = state.allGaps.filter(g => { + if (f.severity !== 'all' && g.severity !== f.severity) return false; + if (f.case !== 'all' && String(g.caseNumber) !== f.case) return false; + if (f.principle !== 'all' && !g.principles.includes(f.principle)) return false; + if (f.search) { + const hay = (g.title + ' ' + g.paragraphs.join(' ') + ' ' + (g.fix || '') + ' ' + (g.evidence || '') + ' ' + g.principles.join(' ')).toLowerCase(); + if (!hay.includes(f.search)) return false; + } + return true; + }); + + state.filteredGaps = filtered; + + // Sort: high first, then medium, low; within severity, by case number + const sevOrder = { high: 0, medium: 1, low: 2 }; + filtered.sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity] || a.caseNumber - b.caseNumber); + + el('result-count').textContent = `${filtered.length} of ${state.allGaps.length} gaps`; + + const list = el('gap-list'); + if (filtered.length === 0) { + list.innerHTML = `

No gaps match these filters

Try removing a filter or clearing the search.

`; + return; + } + + list.innerHTML = filtered.map((g, i) => renderCard(g, i)).join(''); + list.querySelectorAll('.gap').forEach(card => { + card.addEventListener('click', () => { + const idx = parseInt(card.dataset.idx, 10); + openModal(filtered[idx]); + }); + }); + } + + function renderCard(g, idx) { + const snippet = (g.paragraphs && g.paragraphs[0]) ? g.paragraphs[0] : ''; + const principles = (g.principles || []).slice(0, 3).map(p => `${escapeHtml(p)}`).join(''); + const thumb = g.id ? `` : ''; + return ` +
+ ${thumb} +
+ ${escapeHtml(g.id || '—')} + ${g.severity} + Case ${String(g.caseNumber).padStart(2,'0')} +
+

${escapeHtml(g.title || 'Untitled')}

+

${escapeHtml(snippet)}

+
${principles}
+
+ `; + } + + function openModal(g) { + const paragraphs = (g.paragraphs || []).map(p => `

${escapeHtml(p)}

`).join(''); + const evidence = g.evidence ? `
${escapeHtml(g.evidence)}
` : ''; + const fix = g.fix ? `
Suggested fix${escapeHtml(g.fix)}
` : ''; + const principles = (g.principles || []).map(p => `${escapeHtml(p)}`).join(''); + const sourceLink = `View Case ${String(g.caseNumber).padStart(2,'0')} (with screen recording) →`; + + const screenshot = g.id ? `Annotated screenshot for ${escapeAttr(g.id)}` : ''; + const html = ` + +

${escapeHtml(g.title || '')}

+
+ ${escapeHtml(g.id || '—')} + ${g.severity} + Case ${String(g.caseNumber).padStart(2,'0')} — ${escapeHtml(g.caseTitle || '')} +
+ ${screenshot} + ${paragraphs} + ${evidence} + ${fix} +
${principles}
+ ${sourceLink} + `; + + const modal = el('gap-modal'); + el('modal-content').innerHTML = html; + document.getElementById('modal-close-btn').addEventListener('click', () => modal.close()); + if (typeof modal.showModal === 'function') { + modal.showModal(); + } else { + modal.setAttribute('open', ''); + } + } + + function escapeHtml(s) { + return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); + } + function escapeAttr(s) { return escapeHtml(s); } + function debounce(fn, ms) { + let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; + } +})(); diff --git a/dev-experience/gaps-flat.json b/dev-experience/gaps-flat.json new file mode 100644 index 00000000..b77c3e00 --- /dev/null +++ b/dev-experience/gaps-flat.json @@ -0,0 +1,1779 @@ +[ + { + "id": "GAP-01-001", + "severity": "high", + "title": "No onboarding or empty-state guidance", + "paragraphs": [ + "New users land on a mostly-black screen with a single letter \"A\" in the editor and a tiny diagram. There is no welcome message, tutorial prompt, or \"get started\" example." + ], + "evidence": null, + "fix": "Show a rich default example (3-4 participants, arrows, return values) with an inline comment block explaining the syntax.", + "principles": [], + "caseNum": 1, + "caseTitle": null + }, + { + "id": "GAP-01-002", + "severity": "high", + "title": "Sidebar icons have no tooltips or labels", + "paragraphs": [ + "The 6 sidebar icons (folder, code editor, keyboard, document, library, settings) have no hover tooltip and no visible label. Users must click each one to discover what it does." + ], + "evidence": null, + "fix": "Add tooltip on hover (100ms delay) and optionally a collapsible label rail. Modern tools (Figma, Linear, Notion) all label their nav icons.", + "principles": [], + "caseNum": 1, + "caseTitle": null + }, + { + "id": "GAP-01-003", + "severity": "high", + "title": "Preview canvas wastes ~75% of available width", + "paragraphs": [ + "The diagram renders in a small floating frame (≈260px wide) pinned to the top-left of a large dark canvas. The remaining right-side space is completely unused." + ], + "evidence": null, + "fix": "Make the preview fill the available pane width and auto-fit the diagram to the viewport (like Mermaid Live Editor, draw.io). Add a \"fit to screen\" button.", + "principles": [], + "caseNum": 1, + "caseTitle": null + }, + { + "id": "GAP-01-004", + "severity": "medium", + "title": "Settings modal mislabeled as \"Editor\"", + "paragraphs": [ + "The gear icon in the sidebar opens a dialog titled \"Editor\", not \"Settings\". Users looking for app settings will be confused; \"Editor\" implies code editor preferences only." + ], + "evidence": null, + "fix": "Rename the modal to \"Settings\" or split into \"Editor Preferences\" and \"App Settings\". The icon should match the modal title.", + "principles": [], + "caseNum": 1, + "caseTitle": null + }, + { + "id": "GAP-01-005", + "severity": "medium", + "title": "Editor toolbar icons are unlabeled", + "paragraphs": [ + "8+ icon buttons appear above the code editor (participant, arrow types, loops, alt/else blocks) with no labels, no hover tooltips, and no visual grouping. The syntax they insert is non-obvious from the icon alone." + ], + "evidence": null, + "fix": "Add tooltips showing the inserted syntax snippet. Group related icons with a divider. Consider a label on hover like \"Insert if/else block (Alt)\".", + "principles": [], + "caseNum": 1, + "caseTitle": null + }, + { + "id": "GAP-01-006", + "severity": "medium", + "title": "\"Preserve console logs\" exposed in user settings", + "paragraphs": [ + "A debug/developer option sits alongside user-facing preferences (Theme, Font, Line wrap). This is noise for the vast majority of users and reduces trust." + ], + "evidence": null, + "fix": "Move debug options behind an \"Advanced\" expandable section or remove from the UI entirely unless explicitly requested.", + "principles": [], + "caseNum": 1, + "caseTitle": null + }, + { + "id": "GAP-01-007", + "severity": "low", + "title": "Title edit affordance is easy to miss", + "paragraphs": [ + "The diagram title \"Data Processing Flow\" has a small pencil icon to the right that only appears at a glance. There's no hover state or underline hint that the title is editable in-place." + ], + "evidence": null, + "fix": "Show a hover underline on the title text itself (not just the icon) so users know it's clickable. Industry standard: clicking the title text should open the inline editor.", + "principles": [], + "caseNum": 1, + "caseTitle": null + }, + { + "id": "GAP-02-001", + "severity": "high", + "title": "Cmd+S has zero visual feedback", + "paragraphs": [ + "After pressing Cmd+S to save, the UI is pixel-identical before and after. No toast, no status indicator, no title asterisk, no brief flash — nothing confirms the save happened. Users must guess whether their work is safe." + ], + "evidence": null, + "fix": "Show a brief \"Saved\" toast (bottom-right, 2s auto-dismiss). Add an unsaved-changes indicator (e.g. \"●\" dot before the title or asterisk in browser tab title) to make the dirty/clean state explicit.", + "principles": [], + "caseNum": 2, + "caseTitle": null + }, + { + "id": "GAP-02-002", + "severity": "high", + "title": "No unsaved-changes indicator", + "paragraphs": [ + "After typing new content but before saving, there is no visual signal that the document has unsaved changes. If the tab is closed or the page refreshes, users lose work with no warning." + ], + "evidence": null, + "fix": "Show a subtle \"unsaved\" dot/asterisk in the header title while there are uncommitted changes. Intercept the beforeunload event with a browser \"leave page?\" dialog when there are unsaved edits.", + "principles": [], + "caseNum": 2, + "caseTitle": null + }, + { + "id": "GAP-02-003", + "severity": "high", + "title": "Export (PNG) surprises users with a login wall", + "paragraphs": [ + "Clicking \"PNG\" immediately triggers a \"Welcome to ZenUML.com\" login modal with no prior indication that export requires an account. The export button has no lock icon, tooltip, or disabled state to signal it's auth-gated." + ], + "evidence": null, + "fix": "Show a lock icon on the PNG button with a tooltip \"Sign in to export\". Alternatively, allow anonymous SVG/PNG export for basic diagrams and gate cloud sync/history behind auth. The surprise paywall is the worst UX pattern for conversion.", + "principles": [], + "caseNum": 2, + "caseTitle": null + }, + { + "id": "GAP-02-004", + "severity": "medium", + "title": "Login modal doesn't explain WHY auth is needed", + "paragraphs": [ + "The login modal says \"Welcome to ZenUML.com\" and offers GitHub / Google / Facebook sign-in, but gives no context for why it appeared. Users who clicked \"PNG\" don't know if they're signing in to export, to save to cloud, or for something else entirely." + ], + "evidence": null, + "fix": "Add a one-sentence context line above the login options: \"Sign in to download your diagram as PNG and access cloud save.\" This dramatically reduces confusion and improves conversion.", + "principles": [], + "caseNum": 2, + "caseTitle": null + }, + { + "id": "GAP-02-005", + "severity": "low", + "title": "Invalid syntax renders silently as participant boxes", + "paragraphs": [ + "When the user types invalid syntax (e.g. B--> INVALID SYNTAX ???), the parser treats \"INVALID\", \"SYNTAX\", and \"???\" as participant names and renders a diagram with nonsensical boxes — with no error highlight, no inline warning, and no status indicator." + ], + "evidence": null, + "fix": "Highlight the erroneous line in the editor with a red underline (like VS Code). Show a small error badge in the preview frame (\"⚠ Syntax error on line 2\"). Keep the last valid diagram visible rather than rendering the broken one.", + "principles": [], + "caseNum": 2, + "caseTitle": null + }, + { + "id": "GAP-03-001", + "severity": "high", + "title": "Delete icon permanently visible on tabs — triggers on misclick", + "paragraphs": [ + "The trash icon (🗑) is always displayed on every deletable page tab. Its position directly adjacent to the \"+ Add Page\" button caused an accidental delete confirmation to fire during normal use. A destructive action sits next to a creation action with no safe gap." + ], + "evidence": null, + "fix": "Show the delete icon only on tab hover (like browser tab × buttons or VS Code editor tabs). Add at least 24px separation between the last tab's delete zone and the \"+ Add Page\" button. Consider moving delete to a right-click context menu.", + "principles": [], + "caseNum": 3, + "caseTitle": null + }, + { + "id": "GAP-03-002", + "severity": "high", + "title": "No page rename — tabs permanently named \"Page N\"", + "paragraphs": [ + "Pages can only be named \"Page 1\", \"Page 2\", etc. Double-clicking the tab does not trigger inline rename. There is no right-click context menu, no rename icon, and no other affordance. Users building multi-diagram projects have no way to label their pages meaningfully." + ], + "evidence": null, + "fix": "Support double-click-to-rename inline (standard tab paradigm in VS Code, Figma, Notion). Alternatively show a pencil icon on active tab hover. The rename input should auto-select all text and save on Enter/blur.", + "principles": [], + "caseNum": 3, + "caseTitle": null + }, + { + "id": "GAP-03-003", + "severity": "medium", + "title": "New page editor has no placeholder — only preview has empty state", + "paragraphs": [ + "Switching to a new empty page shows \"Click to add your first participant\" in the preview panel, but the code editor is a completely blank black area. The guidance appears in the wrong place — users need it in the editor where they'll type, not in the read-only preview." + ], + "evidence": null, + "fix": "Add a grey placeholder in the CodeMirror editor when empty: // Start typing — e.g.\\nA->B: hello(). The preview empty state can remain as a secondary cue, but the primary affordance must be in the editable area.", + "principles": [], + "caseNum": 3, + "caseTitle": null + }, + { + "id": "GAP-03-004", + "severity": "medium", + "title": "Delete confirmation uses \"lost forever\" language with no undo", + "paragraphs": [ + "The delete dialog says \"The data on this page will be lost forever.\" There is no undo option and no way to recover the deleted page. For users who accidentally click delete (which happens easily due to GAP-03-001), their work is permanently gone." + ], + "evidence": null, + "fix": "Implement a brief undo window (10s toast with \"Undo\" button, like Gmail's delete). Alternatively, move deleted pages to a recoverable trash. At minimum, the dialog should be dismissible with the Escape key.", + "principles": [], + "caseNum": 3, + "caseTitle": null + }, + { + "id": "GAP-03-005", + "severity": "low", + "title": "No page reordering affordance", + "paragraphs": [ + "Page tabs cannot be reordered by drag-and-drop. Users who add pages in the wrong order must delete and recreate them. No drag handles are shown, and no visual cue suggests tabs are moveable." + ], + "evidence": null, + "fix": "Add drag-and-drop reordering to page tabs (HTML5 drag API or a library like dnd-kit). Show a drag-handle cursor on tab hover to signal the affordance.", + "principles": [], + "caseNum": 3, + "caseTitle": null + }, + { + "id": "GAP-04-001", + "severity": "high", + "title": "Share Link — the #1 CTA — requires login with no anonymous fallback", + "paragraphs": [ + "\"Share Link\" is the most prominent button in the app (top-right, blue, always visible). Clicking it immediately shows a login wall. There is no URL-based sharing, no public link, no embed code, no \"share without account\" path — the core sharing value proposition is completely inaccessible to new users." + ], + "evidence": null, + "fix": "Generate an anonymous share URL immediately (encode diagram in URL hash, like Mermaid Live Editor or Carbon). Gate persistent cloud links behind auth, not the act of sharing itself. The share button should always work — login extends its capabilities.", + "principles": [], + "caseNum": 4, + "caseTitle": null + }, + { + "id": "GAP-04-002", + "severity": "high", + "title": "Same generic login modal for 3+ different auth-gated actions", + "paragraphs": [ + "The identical \"Welcome to ZenUML.com\" modal fires for PNG export, Share Link, and likely other features. No context is given about what the user was trying to do or what they'll get by signing in. Users who clicked \"Share Link\" see no mention of sharing." + ], + "evidence": null, + "fix": "Each auth prompt must state the specific value: \"Sign in to generate a persistent share link for this diagram\". Show a preview of what they'll get post-login. One generic modal for all contexts is a conversion killer.", + "principles": [], + "caseNum": 4, + "caseTitle": null + }, + { + "id": "GAP-04-003", + "severity": "medium", + "title": "Auto-title \"Untitled 9-5-23:24\" is ambiguous and technical", + "paragraphs": [ + "New diagrams get an auto-generated title using a non-standard date-time format (M-D-HH:MM). It's unclear if \"9-5\" is May 9th or September 5th, what timezone \"23:24\" refers to, and why a timestamp is used instead of a sequential number. Compare: Google Docs uses \"Untitled document\", Figma uses \"Untitled\", VS Code uses \"Untitled-1\".", + "\"Untitled 9-5-23:24\"", + "\"Untitled diagram\" or \"My Diagram 1\"" + ], + "evidence": null, + "fix": "Use a simple sequential name (\"Untitled diagram\", \"Untitled 2\") or prompt users to name the diagram on first save. If a timestamp must be included, use ISO format with timezone: \"2026-05-09 11:24 PM\".", + "principles": [], + "caseNum": 4, + "caseTitle": null + }, + { + "id": "GAP-04-004", + "severity": "medium", + "title": "My Library shows \"Nothing saved yet\" with no path forward", + "paragraphs": [ + "The empty state for My Library says \"Nothing saved yet.\" but gives no instructions for how to save, no \"Sign in to access your saved diagrams\" CTA, and no explanation that cloud save requires authentication. Users are left in a dead end." + ], + "evidence": null, + "fix": "Replace the empty state with an actionable message: \"Press Cmd+S to save your diagram here\" (for logged-in users) or \"Sign in to save and organize your diagrams\" with a Sign In button (for guests). Good empty states are onboarding moments.", + "principles": [], + "caseNum": 4, + "caseTitle": null + }, + { + "id": "GAP-04-005", + "severity": "medium", + "title": "Opening My Library hides the code editor entirely", + "paragraphs": [ + "Clicking the folder icon replaces the left panel (editor) with the My Library panel. There is no way to see both the library and the editor simultaneously. Users who want to find a saved diagram and compare it to their current work must constantly toggle panels." + ], + "evidence": null, + "fix": "Implement the library as a slide-in overlay or a resizable third column — don't replace the editor. Alternatively, allow library to appear above the editor as a collapsible drawer. Notion, Linear, and Figma all allow navigation and content panels to coexist.", + "principles": [], + "caseNum": 4, + "caseTitle": null + }, + { + "id": "GAP-04-006", + "severity": "low", + "title": "Library toolbar icons (refresh, upload, download) have no labels", + "paragraphs": [ + "Three small icon buttons appear next to \"New Folder\" in the My Library header. None have labels, visible tooltips, or accessible names. Their functions (refresh list, import, export) are non-obvious from the icons alone." + ], + "evidence": null, + "fix": "Add hover tooltips with descriptive text. Consider replacing the download/upload icons with text buttons (\"Import\", \"Export\") since the library panel has ample horizontal space.", + "principles": [], + "caseNum": 4, + "caseTitle": null + }, + { + "id": "GAP-05-001", + "severity": "high", + "title": "Two separate help panels (Shortcuts + Cheat Sheet) split documentation across two unlabeled icons", + "paragraphs": [ + "The sidebar has two distinct help panels: \"Keyboard Shortcuts\" (keyboard icon) and \"Cheat sheet\" (document-info icon). Both are accessed via unlabeled icons with no hover tooltips. Users have no way to know which icon opens which panel without clicking both. Splitting help content across two unlabeled modals fragments the experience and increases cognitive load." + ], + "evidence": null, + "fix": "Merge both into a single \"Help\" panel with tabs (\"Syntax\" and \"Shortcuts\"), accessible from one labeled icon. Add hover tooltips to all sidebar icons. Figma's help panel, VS Code's keyboard shortcuts UI, and Linear's help center all follow this merged pattern.", + "principles": [], + "caseNum": 5, + "caseTitle": null + }, + { + "id": "GAP-05-002", + "severity": "medium", + "title": "Default diagram hints at \"Cheat sheet tab\" — no such tab exists", + "paragraphs": [ + "The pre-loaded example diagram includes the comment // Go to the \"Cheat sheet\" tab or https://docs.zenuml.com. There is no visible \"Cheat sheet tab\" in the interface. Users who follow this instruction will look for a tab in the editor header (ZenUML / CSS) and find nothing. The comment refers to the unlabeled sidebar icon, which appears nowhere near a \"tab\" in the conventional sense." + ], + "evidence": null, + "fix": "Update the default example comment to accurately describe the UI: // Click the book icon (left sidebar) or press Ctrl+Shift+? for syntax help. Alternatively, rename the sidebar icon's panel to match the comment.", + "principles": [], + "caseNum": 5, + "caseTitle": null + }, + { + "id": "GAP-05-003", + "severity": "medium", + "title": "Keyboard Shortcuts modal uses \"Emmet\" — jargon that means nothing in a diagram tool", + "paragraphs": [ + "The shortcuts modal lists Tab → Emmet code completion. Emmet is an HTML/CSS abbreviation expander with no recognized meaning in sequence diagram editing. ZenUML users (software architects, developers documenting APIs) won't know what \"Emmet completion\" means for participant/message syntax. The label misleads about what Tab actually does in this editor.", + "Tab → Emmet code completion", + "Tab → Auto-complete syntax" + ], + "evidence": null, + "fix": "Replace \"Emmet code completion\" with \"Auto-complete (syntax snippets)\" or just \"Tab completion\". If Emmet is specifically used, add a brief parenthetical: \"Emmet (HTML abbreviation expansion — rarely used in ZenUML)\".", + "principles": [], + "caseNum": 5, + "caseTitle": null + }, + { + "id": "GAP-05-004", + "severity": "medium", + "title": "Cheat sheet is not scrollable and appears to be cut off", + "paragraphs": [ + "The Cheat Sheet modal shows: Participant, Message, Async message, Nested message, Self-message, Alt, Loop — and the modal bottom is flush with the viewport edge. Scrolling inside the modal does not reveal additional content. ZenUML supports more constructs (title, note, divider, group, stereotype, opt, par, etc.) that are entirely absent. The cheat sheet is incomplete and gives users a false sense they've seen the full syntax." + ], + "evidence": null, + "fix": "Ensure the cheat sheet is scrollable and lists all supported constructs. Add a \"Full syntax reference →\" link at the bottom pointing to docs.zenuml.com. Show a scroll indicator (shadow or gradient) at the modal bottom to signal more content exists.", + "principles": [], + "caseNum": 5, + "caseTitle": null + }, + { + "id": "GAP-05-005", + "severity": "low", + "title": "No keyboard shortcut for diagram-level actions (Add Page, Refresh Preview uses obscure Ctrl+Shift+5)", + "paragraphs": [ + "The Global shortcuts list contains Ctrl/⌘ + Shift + 5 for \"Refresh preview\" — a non-intuitive binding (5 has no mnemonic connection to refresh). There are no shortcuts for adding a page, switching pages, or toggling panels. Power users who want keyboard-only workflows are blocked from diagram-level navigation." + ], + "evidence": null, + "fix": "Replace Ctrl+Shift+5 with Ctrl+Shift+R (mnemonic: Refresh) or F5 (conventional refresh shortcut). Add shortcuts for Add Page (Ctrl+Shift+N), next/previous page (Ctrl+Tab / Ctrl+Shift+Tab), and toggle sidebar (Ctrl+B, like VS Code).", + "principles": [], + "caseNum": 5, + "caseTitle": null + }, + { + "id": "GAP-06-001", + "severity": "high", + "title": "Login modal does not trap keyboard focus — input leaks into underlying editor", + "paragraphs": [ + "When the auth modal appears (triggered by CSS tab click), keyboard focus is not constrained to the modal. Typing while the modal is visible sends keystrokes to the CodeMirror editor behind it, corrupting diagram content. Closing the modal then reveals broken ZenUML code — with no indication the editor was modified. This is an accessibility violation (WCAG 2.1 §2.1.2) and a data-integrity bug." + ], + "evidence": null, + "fix": "Implement proper focus trap in the modal: on open, move focus to the first interactive element; Tab/Shift+Tab must cycle within the modal; Escape closes and returns focus to the trigger element. Use a library like focus-trap or the native dialog element which traps focus by default.", + "principles": [], + "caseNum": 6, + "caseTitle": null + }, + { + "id": "GAP-06-002", + "severity": "high", + "title": "CSS tab is auth-gated with no prior indication — 4th feature to use generic login modal", + "paragraphs": [ + "The CSS tab sits next to the ZenUML tab in the editor header — appearing as an equal peer, always accessible. Clicking it silently triggers the generic \"Welcome to ZenUML.com\" auth modal. No lock icon, no disabled state, no tooltip, no pricing tier badge signals that CSS customization requires an account. Users who've been working for hours on a diagram have no forewarning that switching tabs will interrupt them." + ], + "evidence": null, + "fix": "Show a lock icon or \"Pro\" badge on the CSS tab for non-authenticated users. On click, show a contextual modal: \"CSS customization is available to signed-in users. Sign in to style your diagrams with custom CSS.\" Alternatively, show a read-only CSS preview with example styles to demonstrate value before gating.", + "principles": [], + "caseNum": 6, + "caseTitle": null + }, + { + "id": "GAP-06-003", + "severity": "medium", + "title": "Present button gives zero feedback — silently fails or opens invisible fullscreen", + "paragraphs": [ + "The \"Present\" button in the bottom bar has aria-label=\"Toggle Fullscreen\" and title=\"Toggle Fullscreen Presenting Mode\" — but the button label says only \"Present\". There is no visual feedback when clicked: no loading state, no fullscreen transition animation, no confirmation. In automation testing the button appeared completely non-functional. In real use, fullscreen mode may activate but without any transition or indicator that a mode change occurred." + ], + "evidence": null, + "fix": "Add a brief visual transition when entering fullscreen (e.g. the diagram panel expands with a smooth animation). Show a visible \"Exit Fullscreen\" (Esc) affordance once in fullscreen mode. The button title and label should match: either \"Present\" or \"Fullscreen\", not both.", + "principles": [], + "caseNum": 6, + "caseTitle": null + }, + { + "id": "GAP-06-004", + "severity": "medium", + "title": "Privacy badge (shield icon) is invisible unless users know to hover it", + "paragraphs": [ + "A shield-with-checkmark icon sits in the top-right corner of the diagram canvas. Hovering it reveals: \"We (the vendor) do not have access to your data. The diagram is generated in this browser.\" This is a genuine trust signal — but it is entirely invisible unless users discover the icon and hover it. New users will never see this message. The privacy guarantee is the strongest selling point for enterprise use, yet it's buried in a hover tooltip on an unlabeled icon." + ], + "evidence": null, + "fix": "Surface the privacy guarantee during onboarding (first load) as a brief callout or tooltip. Add a visible text label \"Private\" or \"Local-only\" next to the shield icon. Consider adding it to the footer or \"About\" section where users who care about data privacy actively look.", + "principles": [], + "caseNum": 6, + "caseTitle": null + }, + { + "id": "GAP-06-005", + "severity": "low", + "title": "No way to restore the default example after deleting all editor content", + "paragraphs": [ + "If a user accidentally deletes all content (or over-applies Undo and clears the editor), there is no \"Reset to example\" or \"Insert starter template\" option. The editor becomes a blank black area. The toolbar insert buttons require existing content structure to work correctly. New users who erase the example while experimenting are stuck with an empty canvas and no guidance." + ], + "evidence": null, + "fix": "Add a \"Load example\" link that appears when the editor is empty (similar to VS Code's \"Open Folder\" empty state). Alternatively, show the starter example in a ghost/placeholder style when empty. A simple \"New from template\" option in the New button dropdown would also solve this.", + "principles": [], + "caseNum": 6, + "caseTitle": null + }, + { + "id": "GAP-07-001", + "severity": "high", + "title": "All 8 toolbar buttons have zero accessible labels — hover shows nothing", + "paragraphs": [ + "The editor toolbar contains 8 insert buttons. JavaScript DOM inspection confirms every single one has title=\"\", no aria-label, and no visible text. Hovering shows no tooltip. Screen reader users get no announcement. Sighted users must guess icon meaning from small abstract SVGs, then click to discover what gets inserted — there is no \"safe\" way to learn what a button does before committing the action." + ], + "evidence": null, + "fix": "Add title and aria-label to every button. Show hover tooltips with: (1) the button name, (2) the syntax it inserts. Example: tooltip on the message button → \"Insert synchronous message — A.method()\". This is how VS Code, GitHub's markdown toolbar, and Mermaid Live Editor handle it.", + "principles": [], + "caseNum": 7, + "caseTitle": null + }, + { + "id": "GAP-07-002", + "severity": "medium", + "title": "Several toolbar buttons silently do nothing when editor focus is not set correctly", + "paragraphs": [ + "Clicking toolbar buttons transfers focus from the editor to the button itself, which can break the insert action — the button never receives the editor's cursor context. During testing, buttons 1, 2, 5, and 6 produced no visible change even when clicked repeatedly. There is no error message, no disabled state, no indication that a precondition was unmet. Users click, nothing happens, and they assume the button is broken." + ], + "evidence": null, + "fix": "Buttons should either: (a) always insert at end-of-file as a safe fallback regardless of cursor position, or (b) show a clear disabled/unavailable state with tooltip explaining the precondition. \"Click in the editor first, then use toolbar buttons\" is an invisible precondition users cannot be expected to know.", + "principles": [], + "caseNum": 7, + "caseTitle": null + }, + { + "id": "GAP-07-003", + "severity": "medium", + "title": "No visual grouping — 8 buttons in a single undifferentiated row", + "paragraphs": [ + "All 8 toolbar buttons appear as an unbroken row with equal spacing. There is no separator, no group label, no visual distinction between \"add participant\", \"message types\" (sync/async/self/return), and \"control structures\" (Alt/Loop). Users who know what an \"alt block\" is must scan all 8 icons to find it. Users who don't know the terminology have no group context to guide discovery." + ], + "evidence": null, + "fix": "Add a thin 1px separator between groups: [Participant] | [→ → ← ↩ ↔] | [Alt Loop]. Add optional group labels (\"Participants\", \"Messages\", \"Blocks\") above each section, collapsible on small screens. This is standard in markdown editors, code editors, and diagram tools like draw.io.", + "principles": [], + "caseNum": 7, + "caseTitle": null + }, + { + "id": "GAP-07-004", + "severity": "low", + "title": "Toolbar inserts placeholder names (\"message\", \"selfMessage\") that users must manually replace", + "paragraphs": [ + "When toolbar buttons insert code snippets, they use generic placeholders: result = A.message { }, selfMessage(), //Note. These are valid ZenUML syntax but use non-descriptive generic names that break real diagrams unless edited. Better tools select the placeholder text after insertion, ready for the user to immediately type the real name — no double-click-to-select needed." + ], + "evidence": null, + "fix": "After inserting a snippet, auto-select the first meaningful placeholder token (e.g. \"message\" in A.message()) so the user can immediately type the real name. VS Code snippet tab-stops (${1:placeholder}) implement this pattern well — the cursor moves through editable fields with Tab.", + "principles": [], + "caseNum": 7, + "caseTitle": null + }, + { + "id": "GAP-08-001", + "severity": "high", + "title": "49 themes with no preview — must close modal to see effect, modal covers editor during selection", + "paragraphs": [ + "The theme dropdown lists 49 CodeMirror themes by internal code name with no color swatches, no preview panel, and no live-preview while hovering. Selecting a theme auto-saves and applies it instantly — but the settings modal sits on top of the editor, blocking the view. Users must dismiss the modal to see whether the theme looks good, then re-open settings to change it again. With 49 options, iterating through themes takes significant effort." + ], + "evidence": null, + "fix": "Show a small color swatch next to each theme name (background + text + keyword colors). Or add a live mini-preview panel in the modal showing \"A→B: hello()\" in the selected theme. VS Code, JetBrains, and GitHub Codespaces all show real-time theme previews. The infrastructure already supports instant switching — the only missing piece is a visible preview.", + "principles": [], + "caseNum": 8, + "caseTitle": null + }, + { + "id": "GAP-08-002", + "severity": "medium", + "title": "Settings modal titled \"Editor\" — gear icon conventionally means \"Settings\", not editor-specific config", + "paragraphs": [ + "The gear icon at the bottom of the sidebar universally signals \"Settings\" or \"Preferences\" in software UI (VS Code, Figma, Linear, Notion, GitHub). Opening it reveals a modal titled \"Editor\" — the scope is narrower than the affordance implies. Users who expect app-level settings (account, keyboard shortcuts, diagram defaults, data storage location) find only editor appearance options. There is no indication that more settings exist elsewhere.", + "Modal title: \"Editor\" (3 appearance settings + 4 toggles)", + "Modal title: \"Settings\" with \"Editor\" as a section header; room for future \"Diagram\", \"Account\" sections" + ], + "evidence": null, + "fix": "Rename modal to \"Settings\" or \"Preferences\". Promote current options under an \"Editor Appearance\" section header. This leaves room to add \"Diagram defaults\" (background color, watermark, actor style) and \"Account\" sections as features grow — without needing a new mental model for users.", + "principles": [], + "caseNum": 8, + "caseTitle": null + }, + { + "id": "GAP-08-003", + "severity": "medium", + "title": "\"Preserve console logs\" debug toggle exposed to all users at the same level as user-facing settings", + "paragraphs": [ + "The \"Others\" section contains four toggles: Line wrap, Auto-preview, Preserve last written code, and Preserve console logs. The first three are clearly user-facing features. \"Preserve console logs\" is a developer debugging option — it retains browser console output across sessions, which has no meaning to the target audience of sequence diagram creators (architects, product managers, technical writers). It is displayed at identical prominence to Line wrap, with no explanation of what it does or who it's for." + ], + "evidence": null, + "fix": "Move \"Preserve console logs\" to a collapsible \"Advanced / Developer\" section, or remove it from the UI entirely if it was added only for debugging. If it must remain, add a tooltip: \"Keep browser console output across page reloads — for developers debugging ZenUML integration.\" Exposing internal debugging toggles to end users adds noise and erodes trust in the product's polish.", + "principles": [], + "caseNum": 8, + "caseTitle": null + }, + { + "id": "GAP-08-004", + "severity": "medium", + "title": "No \"Reset to defaults\" or Cancel — settings are permanent with no undo path", + "paragraphs": [ + "Every setting change shows a \"Setting saved\" toast and immediately persists. The modal has only a close (×) button — no Cancel, no Reset, no Undo. If a user accidentally selects a hard-to-read theme (e.g. colorforth uses black background with dim colored text) and doesn't remember what the previous theme was called, they have no recovery path except scrolling through all 49 options to find something acceptable. The \"Setting saved\" toast actually makes this worse — it emphasizes irreversibility." + ], + "evidence": null, + "fix": "Add a \"Reset to defaults\" link at the bottom of the modal. Store the last-applied setting before a change (one level of undo). Alternatively, add a \"Cancel\" button that reverts to the state when the modal was opened. VS Code's settings undo (Ctrl+Z works in settings) and macOS System Settings' \"Revert\" button are good models. A single \"Reset to defaults\" covers the most common recovery scenario.", + "principles": [], + "caseNum": 8, + "caseTitle": null + }, + { + "id": "GAP-08-005", + "severity": "low", + "title": "Font size range capped at 12–18px — excludes accessibility users and large-display power users", + "paragraphs": [ + "The Font Size dropdown offers 7 fixed values: 12 px, 13 px, 14 px (default), 15 px, 16 px, 17 px, 18 px. The cap at 18px does not accommodate users with low vision who rely on larger text, nor power users on ultra-high-DPI displays who prefer 20–24px in code editors. Conversely, 12px is below the WCAG minimum of 18pt (24px) for normal text — it may fail accessibility requirements for users who expect accessible code editors." + ], + "evidence": null, + "fix": "Extend the range to at least 10–28px, or replace the fixed dropdown with a numeric input with +/- stepper (min: 10, max: 32). VS Code allows custom font size via settings JSON with no cap. A reasonable UI range of 10–28px with 1px steps covers virtually all real-world needs. Consider also inheriting the browser's base font-size as the default rather than hardcoding 14px.", + "principles": [], + "caseNum": 8, + "caseTitle": null + }, + { + "id": "GAP-09-001", + "severity": "high", + "title": "Scroll on canvas scrolls the page — not zoom — violating the diagram tool genre convention", + "paragraphs": [ + "Scrolling the mouse wheel on the diagram canvas moves the page up/down. It does not zoom the diagram. This is the single most-violated interaction expectation in diagram tools: every major web-based diagram application (Figma, draw.io, Mermaid Live, Lucidchart, Excalidraw) uses scroll-to-zoom as the primary zoom gesture. Users who expect this behavior will scroll, see the page shift, and conclude either the diagram is broken or zooming is impossible — before ever discovering the tiny ⊕/⊖ icon buttons in the footer bar." + ], + "evidence": null, + "fix": "Intercept scroll events on the diagram canvas and use them to zoom the diagram (same as Figma, draw.io). Use event.preventDefault() on the iframe's wheel event, then apply the zoom delta. Add Ctrl+scroll as an explicit zoom modifier for users on systems where unmodified scroll is reserved for page navigation. Show a brief \"Scroll to zoom\" tooltip on first canvas hover to set expectations.", + "principles": [], + "caseNum": 9, + "caseTitle": null + }, + { + "id": "GAP-09-002", + "severity": "high", + "title": "No drag-to-pan — diagram cannot be repositioned after zooming or on small viewports", + "paragraphs": [ + "Dragging the mouse on the diagram canvas has no effect — it does not pan the diagram. Once a user zooms in (via the footer ⊕ button), there is no way to scroll/pan to different parts of the diagram. The diagram is effectively locked in its rendered position. On displays smaller than ~900px height, the bottom of longer diagrams is clipped and inaccessible without scrolling the outer page (which moves away from the toolbar, not the diagram). Panning is the expected complement to zoom in every diagram and image tool." + ], + "evidence": null, + "fix": "Implement drag-to-pan: on mousedown in the canvas, switch cursor to grabbing, track mouse delta, and translate the diagram SVG. Release on mouseup. Space+drag (as in Figma) is an acceptable alternative if direct drag conflicts with future element selection. Add a \"Fit diagram\" button (the ⊡ icon used by draw.io, Mermaid, Excalidraw) next to the zoom controls to instantly reset position and scale to show the full diagram.", + "principles": [], + "caseNum": 9, + "caseTitle": null + }, + { + "id": "GAP-09-003", + "severity": "medium", + "title": "(i) info icon opens a full-screen syntax overlay — the 3rd separate help surface, covering the diagram entirely", + "paragraphs": [ + "The (i) icon in the canvas footer opens a \"ZenUML Tips\" panel that renders as a full-width, full-height overlay on top of the diagram canvas. This is the third distinct help surface in the app (alongside the keyboard-shortcut icon in the sidebar and the \"quick reference\" cheat-sheet icon). The Tips panel covers the entire diagram — users cannot read a syntax example and see its rendered result simultaneously. Additionally, the icon has no hover tooltip explaining what it opens, so users cannot know what action they are triggering before clicking." + ], + "evidence": null, + "fix": "Replace the full-screen overlay with a slide-in side panel or a resizable drawer that appears alongside (not over) the diagram. This allows users to read syntax examples while seeing their diagram update in real time — the key learning loop for new users. Consolidate all three help surfaces into one unified \"Help\" panel (see GAP-05-001). Add a tooltip to the (i) icon: \"Syntax tips & reference\".", + "principles": [], + "caseNum": 9, + "caseTitle": null + }, + { + "id": "GAP-09-004", + "severity": "medium", + "title": "Canvas footer controls have zero tooltips or labels — \"1.2.3\" checkbox function is completely opaque", + "paragraphs": [ + "The canvas footer contains 6 controls: (i), checkbox labeled \"1.2.3\", ⊕, 100%, ⊖, and \"ZenUML.com\". None have hover tooltips, title attributes, or aria-labels. The most opaque is the \"1.2.3\" checkbox: its label literally shows the sequence number format — not what checking/unchecking does. Testing confirmed it toggles sequence number visibility on the diagram, but users must experiment to discover this. The \"ZenUML.com\" text in the footer is equally unclear — it appears to be a branding watermark but looks like a dead link." + ], + "evidence": null, + "fix": "Add title and aria-label to every footer control: (i) → \"Syntax tips\", 1.2.3 checkbox → \"Show sequence numbers\", ⊕ → \"Zoom in\", 100% → \"Reset zoom to 100%\", ⊖ → \"Zoom out\", ZenUML.com → \"Powered by ZenUML\" (or remove if it adds no value). Show these as hover tooltips. Rename the checkbox label from \"1.2.3\" to \"Numbers\" or add a \"Show numbers\" tooltip.", + "principles": [], + "caseNum": 9, + "caseTitle": null + }, + { + "id": "GAP-09-005", + "severity": "low", + "title": "No \"Fit to screen\" button — no way to reset diagram position after zooming or viewport changes", + "paragraphs": [ + "After zooming in via the ⊕ button, there is no \"fit diagram\" or \"reset view\" shortcut to quickly return to a state where the full diagram is visible. The 100% label resets zoom to 100% but doesn't re-center the diagram. On smaller viewports, the diagram may be partially scrolled out of view and there is no keyboard shortcut or button to snap back to \"show full diagram\". Figma (Shift+1), draw.io (Ctrl+Shift+H), Mermaid Live, and Excalidraw all have a \"fit\" control." + ], + "evidence": null, + "fix": "Add a \"Fit diagram\" button (⊡ icon, standard in diagram tools) next to the zoom controls. Clicking it should set zoom to show the complete diagram with a small margin, and center it in the viewport. Also add a keyboard shortcut: Ctrl+Shift+F or F (when canvas is focused) as the \"fit\" binding, consistent with Figma (Shift+1) and Excalidraw (Shift+1).", + "principles": [], + "caseNum": 9, + "caseTitle": null + }, + { + "id": "GAP-10-001", + "severity": "high", + "title": "\"Start a blank creation\" loads a complex 18-line pre-filled example, not a blank editor", + "paragraphs": [ + "The most prominent call-to-action in the new-diagram modal is labeled \"Start a blank creation\" with an outlined button style that implies a primary action. Clicking it loads a pre-written example: BookLibService calling Session.findBooks() and BookRepository with try/catch/finally — 18 lines of code involving 4 participants. A new user who wants a blank canvas is immediately confronted with unfamiliar example code they must manually delete before starting their own work." + ], + "evidence": null, + "fix": "Either: (a) rename the button to \"Start from example\" to accurately describe what happens, or (b) actually deliver a blank editor with a single blank line when this button is clicked. If an example is pedagogically valuable, offer it as a named template (\"Library System Example\") alongside the truly blank option. The distinction matters — a user starting their own diagram should not be burdened with deleting someone else's work.", + "principles": [], + "caseNum": 10, + "caseTitle": null + }, + { + "id": "GAP-10-002", + "severity": "high", + "title": "Template thumbnails show style icons only — no content preview before committing", + "paragraphs": [ + "The 4 templates (Basic, Black & White, Blue, StarUML) show only a small icon representing the visual style (color scheme, line thickness). There is no way to preview the content of each template before clicking. Testing revealed that \"Blue\" is actually an internal template named \"Advanced\" — a multi-participant order management system with OrderService, PaymentService, InventoryService. Users who pick \"Blue\" because they want a blue color scheme get a complex business flow they didn't expect. Template selection is a blind commitment." + ], + "evidence": null, + "fix": "Show a hover preview panel (right side of modal) with the rendered diagram when hovering a template. Display: (1) the template's internal name (\"Advanced\"), (2) a small diagram thumbnail, (3) participant count and use-case description. Figma, Notion, and Miro all show content previews before template commitment. At minimum, surface the internal name alongside the color name so users know what content they're getting.", + "principles": [], + "caseNum": 10, + "caseTitle": null + }, + { + "id": "GAP-10-003", + "severity": "high", + "title": "No save warning before replacing current diagram — unsaved work is silently discarded", + "paragraphs": [ + "Selecting any template or \"Start a blank creation\" immediately replaces the current diagram without checking for unsaved changes. During testing, the active editor contained the \"(Forked) Advanced\" diagram — work that had just been created. Clicking \"Start a blank creation\" discarded it instantly, with no confirmation dialog, no \"Save changes?\" prompt, and no undo path back to the previous state. The only indication anything happened was a toast: \"New item created.\" Any user who clicked \"+ New\" by accident or curiosity loses their work permanently." + ], + "evidence": null, + "fix": "Before replacing the current diagram, check if there are unsaved changes. If yes, show a confirmation: \"You have unsaved changes in [tab name]. Discard and create new diagram?\" with Cancel and Discard buttons. This is standard in every code editor, document editor, and diagramming tool (Figma, Lucidchart, draw.io). The behavior should also be reversible via Undo (Cmd+Z) even if no modal is shown.", + "principles": [], + "caseNum": 10, + "caseTitle": null + }, + { + "id": "GAP-10-004", + "severity": "medium", + "title": "Toast message \"was forked\" uses developer jargon opaque to end users", + "paragraphs": [ + "When a template is selected, the toast notification reads: \"Advanced was forked\". The term \"forked\" is borrowed from software version control (git fork) and means nothing to non-developer users. A marketing designer, business analyst, or student would not know what \"forked\" means in this context — does it mean copied, modified, saved, broken? Even technical users might expect \"forked\" to mean the template remains linked to an upstream source they can pull updates from (as in GitHub forks). The term is misleading and alienating to a broad user base." + ], + "evidence": null, + "fix": "Replace \"was forked\" with plain language: \"Advanced template opened as a new diagram\" or simply \"New diagram created from Advanced template.\" The word \"forked\" should never appear in user-facing UI — it belongs in developer console logs or commit messages, not product notifications.", + "principles": [], + "caseNum": 10, + "caseTitle": null + }, + { + "id": "GAP-10-005", + "severity": "medium", + "title": "Marketing tweet solicitation embedded inside the creation modal — wrong context", + "paragraphs": [ + "The new-diagram modal contains a social media call-to-action asking users to tweet about ZenUML. This appears directly alongside the template selection UI, competing for visual attention in a context where the user has a specific goal: create a new diagram. Embedding marketing asks inside task-completion flows violates the single-responsibility principle of UI screens and creates cognitive friction at the worst possible moment — when the user is about to start working." + ], + "evidence": null, + "fix": "Move social media asks to post-action moments (after export, after sharing), to onboarding completion, or to an \"About\" / settings page. Never interrupt a task-starting flow with marketing. If tweet solicitation is required, make it a dismissable banner elsewhere in the UI — not inside a creation dialog that should stay focused on one job: helping users start a new diagram quickly.", + "principles": [], + "caseNum": 10, + "caseTitle": null + }, + { + "id": "GAP-10-006", + "severity": "low", + "title": "Only 4 style-variant templates — no use-case or domain-specific templates", + "paragraphs": [ + "The template library offers 4 options: Basic, Black & White, Blue, StarUML. All 4 appear to be style variants of the same or similar content — they differentiate by color scheme and visual style, not by use case or domain. There are no domain-specific starter templates for common sequence diagram scenarios: API authentication flow, microservices communication, e-commerce checkout, CI/CD pipeline, user login sequence, WebSocket handshake. These are the diagrams developers actually draw most often." + ], + "evidence": null, + "fix": "Add 6–10 use-case templates covering common developer scenarios: API auth (OAuth2/JWT), REST CRUD, WebSocket handshake, login flow, microservice call chain, database transaction. Label them by use case, not by visual style. Style selection can be a separate step or theme preference. Notion, Confluence, and Miro all provide domain-specific templates as a primary discovery mechanism for new users.", + "principles": [], + "caseNum": 10, + "caseTitle": null + }, + { + "id": "GAP-11-001", + "severity": "high", + "title": "Global undo stack crosses page boundaries — Cmd+Z on Page 1 undoes Page 2 edits", + "paragraphs": [ + "ZenUML uses a single CodeMirror editor instance shared across all pages. Switching pages replaces the editor content but does not reset or checkpoint the undo history. As a result, pressing Cmd+Z on Page 1 after having edited Page 2 undoes Page 2's changes while the UI shows Page 1 as selected. A second Cmd+Z empties the editor entirely. The diagram canvas continues rendering stale Page 2 content, creating a split-brain state where the editor and diagram are out of sync. This is a data corruption bug — a user can silently overwrite or erase another page's content simply by pressing Undo on their current page." + ], + "evidence": null, + "fix": "Each page's CodeMirror instance must maintain an independent undo history. Either: (a) create separate CodeMirror instances per page (one per tab), or (b) save and restore the full history state when switching between pages. VS Code, Notion, and Obsidian all maintain per-document undo histories — switching tabs never cross-contaminates undo stacks. Fix requires storing the full cm.historySize() state per page on tab switch.", + "principles": [], + "caseNum": 11, + "caseTitle": null + }, + { + "id": "GAP-11-002", + "severity": "high", + "title": "No warning before Cmd+Z silently erases entire page content", + "paragraphs": [ + "When the undo history reaches the initial state (before any content was entered), a single Cmd+Z can clear the entire editor to an empty state. In testing: starting from \"A → B: hello\" (2 lines), one Cmd+Z jumped back to the 18-line example, and one more cleared everything. There is no safeguard, no \"Undo will erase all content on this page — continue?\" confirmation, no count of how many undos remain before the page goes blank. Silent erasure of all work with no recovery path is one of the most damaging editor UX failures." + ], + "evidence": null, + "fix": "Add a visual undo depth indicator (small counter near the editor, e.g. \"⌘Z · 3 steps\") so users know how close they are to the bottom of the stack. When the next Cmd+Z would clear all content, show a brief inline warning: \"Press again to clear all content (no more undo history).\" Sublime Text and VS Code both show \"Can't undo further\" as a status bar message when the stack is exhausted. Alternatively, never allow undo to go past the initial load state of a page.", + "principles": [], + "caseNum": 11, + "caseTitle": null + }, + { + "id": "GAP-11-003", + "severity": "medium", + "title": "Toolbar inserts are not atomic undo units — require character-by-character reversal", + "paragraphs": [ + "When the toolbar \"Participant\" button inserts NewParticipant (13 characters), pressing Cmd+Z once removes the entire token — that part works. However, the newline inserted by the toolbar to separate the token from surrounding content requires an additional Cmd+Z, making the complete reversal of one toolbar action require 2+ undo steps. More critically: clicking a toolbar button first steals focus from the editor, meaning the user must re-click the editor before pressing Cmd+Z — or the undo may fire on the wrong context. The disconnect between toolbar action and undo granularity violates user mental models where \"one action = one undo step.\"" + ], + "evidence": null, + "fix": "Wrap each toolbar button click into a single CodeMirror transaction (cm.operation()) that groups all text insertions and cursor movements into one atomic undo entry. Ensure focus returns to the editor before the transaction is committed so Cmd+Z immediately after toolbar use reverses exactly the insert. GitHub's markdown toolbar and VS Code's snippet insertions are always single-undo-step operations.", + "principles": [], + "caseNum": 11, + "caseTitle": null + }, + { + "id": "GAP-11-004", + "severity": "medium", + "title": "~2 second diagram re-render delay creates a decoupled undo experience", + "paragraphs": [ + "After pressing Cmd+Z, the editor content updates immediately (correct behavior). However, the diagram canvas takes approximately 2 seconds to re-render the undone state. During this window, the editor shows the undone text but the diagram still shows the superseded content — creating a contradictory view where both panels are simultaneously \"right\" but at different points in time. Users who glance at the diagram after pressing Cmd+Z will see stale content and may press Cmd+Z again, overshooting their intended undo target." + ], + "evidence": null, + "fix": "Show an explicit loading/re-rendering indicator on the diagram canvas during the 2-second render delay — a subtle spinner, pulsing border, or \"Rendering…\" label. This tells users \"the diagram is catching up\" rather than leaving them in a false steady state. Consider debounce optimization: if the re-render typically takes 500ms for small edits, the 2s delay for a 1-line undo suggests the full pipeline is being re-triggered rather than a diff-based update.", + "principles": [], + "caseNum": 11, + "caseTitle": null + }, + { + "id": "GAP-11-005", + "severity": "low", + "title": "Redo shortcut (Cmd+Shift+Z) is undocumented — not listed in cheat sheet or any visible help", + "paragraphs": [ + "Redo works via Cmd+Shift+Z, which was confirmed in testing. However, this shortcut appears nowhere in the visible UI — not in the keyboard shortcuts panel, not in the cheat sheet, not in any tooltip. Users who know Cmd+Z for undo will typically try Cmd+Shift+Z (macOS standard) or Cmd+Y (Windows standard) for redo. The ZenUML cheat sheet panel lists multiple shortcuts but omits undo/redo entirely. A user who accidentally over-undoes cannot recover their work unless they already know the platform-specific redo shortcut from external knowledge." + ], + "evidence": null, + "fix": "Add Cmd+Z (undo) and Cmd+Shift+Z (redo) to the keyboard shortcuts cheat sheet panel. Add tooltips to any undo/redo buttons if they exist (currently none are visible in the toolbar). Even a one-line mention in the \"Help\" sidebar would dramatically reduce the chance of users losing work by not knowing redo is available.", + "principles": [], + "caseNum": 11, + "caseTitle": null + }, + { + "id": "GAP-12-001", + "severity": "high", + "title": "No error message in the diagram panel — invalid syntax renders a silent partial or empty diagram", + "paragraphs": [ + "When the ZenUML parser encounters invalid syntax, the diagram canvas does not show an error state. For mid-document errors (e.g. @@@ bad token @@@ on line 2), the canvas renders only the valid content before the error — showing a partial diagram with no indication it is incomplete. For fully unparseable input (e.g. !!!BROKEN!!!SYNTAX!!!), the canvas renders a lone participant icon — which looks identical to an intentionally empty diagram. There is no \"Syntax error\" banner, no error count, no line reference, no red border or warning color on the canvas. Users believe their diagram is correct when it is not." + ], + "evidence": null, + "fix": "When the parser produces an error, the diagram canvas should show a visible error state: a red/orange border or banner reading \"Syntax error on line N — diagram may be incomplete.\" Include the error line number and a brief description. Mermaid Live Editor, PlantUML server, and Kroki all show explicit error panels. Even a simple \"⚠ Parse error\" badge on the canvas would be a major improvement over the current silent behavior.", + "principles": [], + "caseNum": 12, + "caseTitle": null + }, + { + "id": "GAP-12-002", + "severity": "high", + "title": "No editor gutter indicators — error lines have no squiggly underline, no line number icon, no hover tooltip", + "paragraphs": [ + "The CodeMirror editor has no error annotations for invalid ZenUML syntax. Lines with parse errors get a faint pink syntax-highlighting tint (inconsistently — some invalid inputs show no highlighting at all), but there is no red squiggly underline, no gutter icon (🔴 or ⚠), and no hover tooltip explaining what is wrong with the line. A user looking at @@@ bad token here @@@ on line 2 sees only a differently-colored line with no explanation. They cannot tell whether the color means \"this is a comment\", \"this is a string\", \"this is a keyword\", or \"this is an error.\"" + ], + "evidence": null, + "fix": "Register CodeMirror linting annotations for the ANTLR parser's error output. On parse error, mark the offending token(s) with: (1) a red wavy underline on the token, (2) a red circle icon in the gutter at the error line number, (3) a hover tooltip with the parser error message. VS Code, Monaco Editor, and CodeMirror 6 all provide linting APIs for exactly this. The fix is a CodeMirror linter plugin that feeds ANTLR error messages back into the editor.", + "principles": [], + "caseNum": 12, + "caseTitle": null + }, + { + "id": "GAP-12-003", + "severity": "medium", + "title": "Parser silently truncates the diagram at the first error — valid content below the error is dropped without warning", + "paragraphs": [ + "When an error appears on line N of a multi-line diagram, the ZenUML parser halts and renders only lines 1 through N-1. Lines N+1 onward are silently discarded — they never appear in the diagram and produce no warning. In testing with a 3-line document (valid / invalid / valid), only line 1 appeared in the diagram. Line 3's C → D: world was completely dropped. A user who has a long diagram and accidentally introduces an error mid-document will see half their diagram disappear — with no indication of where the cutoff happened or how many lines were skipped." + ], + "evidence": null, + "fix": "Either: (a) implement error recovery in the parser so it skips the invalid line and continues rendering the rest of the document (best experience — user sees all valid parts), or (b) show a count of dropped lines at the bottom of the diagram: \"⚠ 2 lines skipped due to errors — see line 2.\" Option (a) is used by Mermaid (renders what it can) and is strongly preferred over silent truncation. Option (b) at minimum prevents users from discovering missing content only after export.", + "principles": [], + "caseNum": 12, + "caseTitle": null + }, + { + "id": "GAP-12-004", + "severity": "medium", + "title": "Syntax highlighting color alone signals errors — non-compliant with WCAG 1.4.1 (color not sole differentiator)", + "paragraphs": [ + "The only error signal in the editor is a faint pink/magenta tint on lines that contain invalid tokens. This coloring: (1) is inconsistent — completely garbage input (!!!BROKEN!!!) receives no tinting at all, (2) is indistinguishable from intentional syntax coloring for strings or keywords to a colorblind user, (3) carries no semantic label — hovering the colored line reveals nothing, and (4) violates WCAG 1.4.1, which requires that color not be the sole means of conveying information. A red-blind user sees the pink-tinted lines as identical to normal code lines." + ], + "evidence": null, + "fix": "Pair any color-based error indication with a non-color signal: a gutter icon, an underline style, a tooltip, or a text label. The WCAG test is simple: \"if I remove all color from this screenshot, can I still tell which lines have errors?\" Currently the answer is no. Adding a ⚠ glyph in the gutter satisfies both the WCAG requirement and the UX requirement simultaneously.", + "principles": [], + "caseNum": 12, + "caseTitle": null + }, + { + "id": "GAP-12-005", + "severity": "low", + "title": "Auto-bracket completion silently masks some syntax errors that the user intended to observe", + "paragraphs": [ + "The editor auto-closes { to {} and ( to () immediately on typing. While this is ergonomic for normal use, it means a user who types an intentionally partial token to test what the diagram does (e.g. typing A.method( then pausing to look at the diagram) sees an auto-corrected valid result rather than an error state. Developers debugging ZenUML syntax are robbed of the ability to observe incremental parse states. Additionally, auto-completion that fires immediately with no opt-out can be disruptive for users who intended to type a different character." + ], + "evidence": null, + "fix": "Add a settings option to disable auto-bracket completion (already logged as part of Case 08 settings gaps). More importantly, implement a brief delay before auto-completing (e.g. 300ms) so users can type the opening bracket, glance at the diagram, and continue — rather than having the completion fire before they've observed the state. This is how VS Code, JetBrains IDEs, and CodeMirror 6 implement it.", + "principles": [], + "caseNum": 12, + "caseTitle": null + }, + { + "id": "GAP-13-001", + "severity": "high", + "title": "Page tabs cannot be renamed — no UI affordance exists for page-level naming", + "paragraphs": [ + "Multi-page ZenUML diagrams permanently label their pages \"Page 1\", \"Page 2\", etc. with no way to rename them. Testing exhausted all standard interactions: double-click (nothing), right-click (browser toolbar, no app menu), DOM inspection (plain buttons, no contentEditable or event handlers). Users who use pages to separate diagram sections — e.g. \"Login Flow\", \"Checkout Flow\", \"Error Handling\" — must use workarounds: reading page content to identify them, or maintaining a mental map of which number corresponds to which concept. Figma, Notion, and every multi-page tool that exists supports page renaming as a core feature." + ], + "evidence": null, + "fix": "Add page rename via double-click on the tab text (universal standard). Implement an inline text input that appears on double-click, pre-selected with the current name, confirmed by Enter and cancelled by Escape. Also add a right-click context menu with \"Rename\", \"Delete\", \"Duplicate\" options for discoverability. Tab names should persist to localStorage and cloud saves. The fix is a contentEditable span or input overlay on the button — a standard pattern used by Chrome, VS Code, Notion, and Figma.", + "principles": [], + "caseNum": 13, + "caseTitle": null + }, + { + "id": "GAP-13-002", + "severity": "high", + "title": "Diagram title rename is hidden behind a hover-only pencil icon — clicking the title text does nothing", + "paragraphs": [ + "The diagram title rename workflow requires: (1) hovering over the title in the header to reveal the pencil icon, (2) clicking specifically on that ~16×16px icon — clicking the title text itself has no effect. During testing, clicking the title area without perfect icon targeting does nothing, providing no feedback that an editable field exists. Users who don't hover precisely enough never see the pencil. Users who click the title text expect it to become editable (standard in tools like Notion, Figma, Google Docs). The current UX forces pixel-precision discovery of a hidden affordance." + ], + "evidence": null, + "fix": "Make the entire title text area clickable to activate rename (not just the pencil icon). Show the pencil icon persistently (not only on hover) or replace it with a visible edit indicator. A subtitle label \"Click to rename\" or a subtle underline on the title signals editability without adding UI clutter. This is how Google Docs (\"Click to add a title\"), Figma, Notion, and Obsidian handle document title editing.", + "principles": [], + "caseNum": 13, + "caseTitle": null + }, + { + "id": "GAP-13-003", + "severity": "medium", + "title": "Long titles display only the tail — \"…overflow or truncation issues\" not \"A very long diagram…\"", + "paragraphs": [ + "When a diagram title exceeds the header width, the field scrolls to show the end of the string rather than the beginning. The result is a header displaying \"low or truncation issues\" — the final words of a 107-character title — with no indication that the title starts with different text, and no ellipsis or fade. Users see a fragment that looks like an incomplete or corrupted title. Industry standard for overflowing titles is to show the beginning with a trailing ellipsis (\"A very long diagram t…\") so the identifying start is always visible. Additionally, hovering the title shows no tooltip with the full name." + ], + "evidence": null, + "fix": "When the title overflows in display mode, truncate at the end with an ellipsis: text-overflow: ellipsis; overflow: hidden; white-space: nowrap. Add title attribute with full text for hover tooltip. In edit mode, scrolling the input to the right is acceptable — but display mode must always show the start. Set a soft character limit (e.g. 60 chars) with a counter during editing, similar to GitHub repo descriptions.", + "principles": [], + "caseNum": 13, + "caseTitle": null + }, + { + "id": "GAP-13-004", + "severity": "medium", + "title": "Empty title is accepted — clearing the name and pressing Enter produces a blank untitled diagram", + "paragraphs": [ + "Deleting all characters from the title field and pressing Enter commits the empty string as the diagram name. The header shows a bare empty input box — visually indistinguishable from the rename-mode state — with no placeholder, no fallback name, and no error. Pressing Escape correctly restores the previous title, but this recovery path requires the user to know to press Escape before clicking elsewhere (clicking elsewhere while empty does not restore). An empty diagram name breaks library display, share links, and any UI surface that references the diagram by name." + ], + "evidence": null, + "fix": "Validate the title field on submit: if the field is empty or whitespace-only, either (a) restore the previous title and show a brief inline message \"Diagram title cannot be empty\", or (b) replace with a default like \"Untitled Diagram\". Show a character counter and placeholder text (\"Enter diagram name…\") inside the field during editing. This is standard in Google Docs (auto-restores \"Untitled document\"), Figma (reverts on blur if empty), and Notion.", + "principles": [], + "caseNum": 13, + "caseTitle": null + }, + { + "id": "GAP-13-005", + "severity": "low", + "title": "Auto-generated names (\"Untitled 10-5-4:8\") are machine-readable timestamps, not human-readable defaults", + "paragraphs": [ + "New diagrams receive names like \"Untitled 10-5-4:8\" — which encodes a date/index in a format that is neither a natural language name nor a recognizable date format. Users who create multiple diagrams without renaming them see a list of cryptic timestamps in their Library. The pattern is not explained anywhere in the UI. \"Untitled\" is a clear and universal fallback; \"Untitled 10-5-4:8\" is confusing because the numbers suggest meaning but require decoding. Compare: GitHub creates repos named after your username; Notion creates \"Untitled\"; Figma creates \"Untitled\"." + ], + "evidence": null, + "fix": "Default new diagram names to \"Untitled\" or \"New Diagram\" — simple, human-readable, internationally recognizable. If deduplication is needed, use \"Untitled (2)\", \"Untitled (3)\". Avoid exposing timestamps or internal counters in user-facing names. The rename prompt (or title click-to-edit) can appear automatically on first save to encourage meaningful names, similar to how Figma prompts \"Give your frame a name\" after creating from a template.", + "principles": [], + "caseNum": 13, + "caseTitle": null + }, + { + "id": "GAP-14-001", + "severity": "high", + "title": "All 6 sidebar icons missing aria-label — screen reader announces ligature text", + "paragraphs": [ + "Every sidebar button relies solely on the HTML title attribute for labeling. The button text content is the raw Material Icons ligature string (folder_open, code_blocks, quick_reference, etc.). A screen reader announces these strings verbatim, providing zero semantic value." + ], + "evidence": "
DOM evidence — all 6 icons", + "fix": null, + "principles": [ + "WCAG 4.1.2 Name, Role, Value (Level A)", + "Nielsen #4: Consistency & Standards", + "Touch accessibility" + ], + "caseNum": 14, + "caseTitle": null + }, + { + "id": "GAP-14-002", + "severity": "high", + "title": "Library panel cannot be dismissed — Code Editor icon inert when Library is open", + "paragraphs": [ + "Once \"My Library\" is opened, the user has no working path back to the Code Editor:" + ], + "evidence": "
Verified interactions — all fail to close Library", + "fix": null, + "principles": [ + "Nielsen #3: User Control & Freedom", + "Nielsen #1: Visibility of System Status", + "Interaction trap / dead end" + ], + "caseNum": 14, + "caseTitle": null + }, + { + "id": "GAP-14-003", + "severity": "medium", + "title": "540px dead zone splits sidebar into two visually disconnected icon clusters", + "paragraphs": [ + "The 6 sidebar icons are distributed in two polarised groups with no visual grouping, separator, or label to explain the split:" + ], + "evidence": "
y-positions from getBoundingClientRect()", + "fix": null, + "principles": [ + "Gestalt: Proximity", + "Nielsen #4: Consistency & Standards", + "Responsive layout" + ], + "caseNum": 14, + "caseTitle": null + }, + { + "id": "GAP-14-004", + "severity": "medium", + "title": "Same icon strip, three different interaction patterns with no visual differentiation", + "paragraphs": [ + "The 6 icons look identical in style but trigger fundamentally different interaction types, with no affordance distinguishing them:", + "Users cannot predict the outcome of clicking any icon. An external link inside an icon-only sidebar is a particularly dangerous pattern — the user loses context with no warning." + ], + "evidence": null, + "fix": null, + "principles": [ + "Nielsen #4: Consistency & Standards", + "Nielsen #1: Visibility of System Status", + "Principle of Least Surprise" + ], + "caseNum": 14, + "caseTitle": null + }, + { + "id": "GAP-14-005", + "severity": "low", + "title": "Typo in Cheatsheet: \"Asyc message\" and naming inconsistency \"Cheatsheet\" vs \"Cheat sheet\"", + "paragraphs": [ + "Minor copy quality issues in the help content:" + ], + "evidence": null, + "fix": null, + "principles": [ + "Nielsen #4: Consistency & Standards", + "Copy quality" + ], + "caseNum": 14, + "caseTitle": null + }, + { + "id": "GAP-15-001", + "severity": "high", + "title": "Export buttons show generic auth modal with no context — user doesn't know why or what they'll get", + "paragraphs": [ + "Clicking \"Export as PNG\" or \"Copy PNG to Clipboard\" (both visible in the bottom bar) immediately shows the \"Welcome to ZenUML.com\" login modal. The modal provides zero context:" + ], + "evidence": "
Auth modal observed after clicking either export button", + "fix": null, + "principles": [ + "Nielsen #1: Visibility of System Status", + "Nielsen #6: Recognition over Recall", + "Growth: Explain the value before the gate" + ], + "caseNum": 15, + "caseTitle": null + }, + { + "id": "GAP-15-002", + "severity": "high", + "title": "Zero anonymous export path — user who built a diagram cannot get it out without creating an account", + "paragraphs": [ + "A new user can open the app, build a complete sequence diagram, and have no way to export or share it without creating an account. There is no free export tier whatsoever:", + "This creates a high-friction drop-off point for the most natural first action after building a diagram. Industry standard is at least one free export format (Mermaid Live → SVG free, PlantUML → PNG free, Excalidraw → PNG/SVG free)." + ], + "evidence": "
Competitor comparison — anonymous export availability", + "fix": null, + "principles": [ + "Nielsen #3: User Control & Freedom", + "Freemium: Value before the gate", + "Conversion funnel: Export is the aha-moment" + ], + "caseNum": 15, + "caseTitle": null + }, + { + "id": "GAP-15-003", + "severity": "medium", + "title": "No SVG export option — only PNG, and PNG is locked", + "paragraphs": [ + "The app offers only two export actions (PNG download, Copy PNG), both locked. SVG export is completely absent:" + ], + "evidence": null, + "fix": null, + "principles": [ + "Feature completeness", + "Print/documentation use cases", + "Competitive parity" + ], + "caseNum": 15, + "caseTitle": null + }, + { + "id": "GAP-15-004", + "severity": "medium", + "title": "Cmd+S is completely silent for anonymous users — no save, no feedback, no auth prompt", + "paragraphs": [ + "The keyboard shortcuts panel lists Ctrl/⌘ + S as \"Save current creations\". For an anonymous user, pressing Cmd+S with editor focused produces absolutely no response:", + "The export buttons at least respond with a modal. Cmd+S is worse — it silently does nothing, leaving users to wonder if it worked, failed, or simply isn't implemented." + ], + "evidence": null, + "fix": null, + "principles": [ + "Nielsen #1: Visibility of System Status", + "Nielsen #5: Error Prevention", + "Keyboard shortcut contract" + ], + "caseNum": 15, + "caseTitle": null + }, + { + "id": "GAP-15-005", + "severity": "low", + "title": "No right-click context menu on diagram canvas — missed export shortcut", + "paragraphs": [ + "Right-clicking the rendered diagram canvas shows only the browser's native context menu (with options like \"Inspect\", \"Save page as\", etc.). No app-level context menu appears:" + ], + "evidence": null, + "fix": null, + "principles": [ + "Nielsen #6: Recognition over Recall", + "Discoverability", + "Power-user affordances" + ], + "caseNum": 15, + "caseTitle": null + }, + { + "id": "GAP-16-001", + "severity": "high", + "title": "No responsive layout — desktop split-pane renders at full width on 390px mobile", + "paragraphs": [ + "The entire application renders as a desktop layout with no breakpoint adaptations for mobile screen widths. The split-pane editor+canvas design assumes a wide viewport and fails catastrophically on narrow screens:" + ], + "evidence": null, + "fix": null, + "principles": [ + "Mobile-first design", + "Responsive design fundamentals", + "WCAG 1.4.10: Reflow (320px width requirement)" + ], + "caseNum": 16, + "caseTitle": null + }, + { + "id": "GAP-16-002", + "severity": "high", + "title": "Diagram canvas reduced to 55px sliver — completely unusable on mobile", + "paragraphs": [ + "At 390px width, the layout allocates: ~50px sidebar + ~200px editor pane + ~55px canvas sliver + ~85px overflow. The canvas is visible as a 55px strip showing only the leftmost edge of the first participant box:" + ], + "evidence": "
Space allocation at 390px", + "fix": null, + "principles": [ + "WCAG 1.4.10: Reflow", + "Core product functionality", + "Mobile touch interactions" + ], + "caseNum": 16, + "caseTitle": null + }, + { + "id": "GAP-16-003", + "severity": "high", + "title": "All export buttons (Present, PNG, Copy PNG) disappear completely on mobile", + "paragraphs": [ + "The bottom action bar contains \"Present | ↓ PNG | ⎘ Copy PNG\" buttons on the right side of the screen. At 390px, these buttons overflow beyond the viewport and are completely inaccessible:" + ], + "evidence": null, + "fix": null, + "principles": [ + "WCAG 1.4.10: Reflow", + "Nielsen #3: User Control & Freedom", + "Mobile layout overflow" + ], + "caseNum": 16, + "caseTitle": null + }, + { + "id": "GAP-16-004", + "severity": "medium", + "title": "Diagram title wraps to 3 lines in header — wastes vertical space and looks broken", + "paragraphs": [ + "The header title element contains \"My Auth Flow Diagram\" (from the previous session). At 390px, this wraps into three lines: \"My Auth / Flow / Diagram\". The header height inflates significantly:" + ], + "evidence": null, + "fix": null, + "principles": [ + "Nielsen #4: Consistency & Standards", + "Text overflow handling", + "Vertical space budget" + ], + "caseNum": 16, + "caseTitle": null + }, + { + "id": "GAP-16-005", + "severity": "medium", + "title": "Insert toolbar clips to 4 icons — 50%+ of editing shortcuts unreachable on mobile", + "paragraphs": [ + "The insert toolbar above the editor shows 8+ icon buttons (New participant, Async, Sync, Return value, Self message, New instance, Conditional, Loop, and more). At 390px, only the first 4 fit:" + ], + "evidence": null, + "fix": null, + "principles": [ + "Fitts's Law: reachability", + "Mobile touch target accessibility", + "Progressive disclosure" + ], + "caseNum": 16, + "caseTitle": null + }, + { + "id": "GAP-16-006", + "severity": "low", + "title": "No touch-optimized interactions on diagram canvas — pinch-zoom and swipe-pan absent", + "paragraphs": [ + "Even at desktop sizes, the diagram canvas lacks scroll-to-zoom and drag-to-pan (documented in Case 09). On mobile these gaps are worse because touch users have no mouse wheel alternative:" + ], + "evidence": null, + "fix": null, + "principles": [ + "Mobile touch patterns", + "Fitts's Law: touch target size (min 44×44px)", + "iOS/Android HIG touch conventions" + ], + "caseNum": 16, + "caseTitle": null + }, + { + "id": "GAP-17-001", + "severity": "high", + "title": "Focus rings fail WCAG 2.4.11 minimum area requirement", + "paragraphs": [ + "Every interactive element — sidebar icons, toolbar buttons, header buttons — uses the browser's 1px default focus outline in rgb(0, 95, 204). Against the dark #1e293b backgrounds this produces a contrast ratio well below the WCAG 2.4.11 (AA, 2024) minimum: a focus indicator must have at least a 3:1 contrast ratio against adjacent colors AND enclose an area ≥ the perimeter of a 2 CSS px border around the component.", + "Impact: Keyboard-only users and low-vision users relying on focus rings cannot reliably track where keyboard focus is located. This is a WCAG 2.2 Level AA failure." + ], + "evidence": "// JS audit result for first 15 focusable elements:\n// All returned: outline = \"rgb(0, 95, 204) auto 1px\"\n// Share Link exception: \"rgb(245, 245, 245) auto 1.5px\"\n// Background of sidebar icon strip: #1e293b (#30353d effective)\n// Contrast of #005FCC on #30353d: ~3.4:1 border but at 1px width\n// the enclosed area is far below 2px border perimeter requirement", + "fix": "Replace the browser default with an explicit outline: 2px solid #60a5fa; outline-offset: 2px; on all focusable elements, or use the :focus-visible pseudo-class with a 2px+ ring that passes 3:1 against the adjacent background color. The Share Link button's 1.5px white ring is closer but still undersized.", + "principles": [ + "WCAG 2.4.11", + "WCAG 2.4.7", + "Nielsen #1 Visibility" + ], + "caseNum": 17, + "caseTitle": "Keyboard Navigation & Focus Management" + }, + { + "id": "GAP-17-002", + "severity": "high", + "title": "CodeMirror editor unreachable in first 20+ Tab stops", + "paragraphs": [ + "The CodeMirror editor is the primary work surface of the application, yet keyboard users must Tab through at least 20+ interactive elements — 3 header buttons, 6 sidebar icons, 8+ toolbar buttons — before the editor's textarea receives focus. The first Tab stop is the \"New\" button in the header, not the editor.", + "Impact: Keyboard-only users opening the app to write ZenUML code face an excessive navigation burden before reaching the input surface. WCAG 2.1 Success Criterion 2.4.3 (Focus Order) requires a \"meaningful sequence\" — placing the primary editing canvas last in tab order violates this criterion." + ], + "evidence": "// Focus traversal order (programmatic JS audit, 15 elements):\n// 1: New 2: Share Link 3: Profile 4: My Library\n// 5: Code Editor 6: Keyboard Shortcuts 7: Cheatsheet\n// 8: Language Guide 9: Settings 10: New participant\n// 11: Async message 12: Sync message 13: Return value\n// 14: Self message 15: New instance ...\n// CodeMirror textarea: position >> 20", + "fix": "Add tabindex=\"1\" to the CodeMirror wrapper (or use CodeMirror's built-in tabIndex option) so the editor is the first Tab stop after the skip-nav link. Alternatively restructure DOM order: editor before sidebar/toolbar in source order, use CSS for visual layout.", + "principles": [ + "WCAG 2.4.3", + "Nielsen #6 Recognition over Recall" + ], + "caseNum": 17, + "caseTitle": "Keyboard Navigation & Focus Management" + }, + { + "id": "GAP-17-003", + "severity": "medium", + "title": "6 sidebar icons consume sequential Tab stops between header and toolbar", + "paragraphs": [ + "All 6 left-sidebar icon buttons (My Library, Code Editor, Keyboard Shortcuts, Cheatsheet, Language Guide, Settings) are individually focusable and appear consecutively in the tab order. For a tool-switching sidebar this is unnecessary: only the currently active panel icon needs to be reachable; the others could use a roving tabindex pattern (arrow keys to navigate icons, single Tab to leave the group).", + "Impact: 6 extra Tab stops between the header and the toolbar adds navigation friction. Combined with GAP-17-002 this means keyboard users Tab 20+ times to reach the editor." + ], + "evidence": null, + "fix": "Implement the ARIA toolbar / roving tabindex pattern for the sidebar icon strip: give the strip role=\"toolbar\" or role=\"navigation\", make only the active icon tabindex=\"0\" and the rest tabindex=\"-1\", then use arrow key handlers to move between icons. This compresses 6 stops into 1.", + "principles": [ + "WCAG 2.4.3", + "ARIA Authoring Practices" + ], + "caseNum": 17, + "caseTitle": "Keyboard Navigation & Focus Management" + }, + { + "id": "GAP-17-004", + "severity": "medium", + "title": "Toolbar insert buttons missing aria-label — announced by icon ligature text", + "paragraphs": [ + "All 8+ toolbar insert buttons (New participant, Async message, Sync message, etc.) have ariaLabel: null. The visible content is a Material Icons ligature string (\"person_add\", \"swap_horiz\", etc.) which is what screen readers announce verbatim. Users with assistive technology hear \"person add button\" rather than \"Insert new participant\".", + "This was also partially documented in Case 14 for sidebar icons. The toolbar buttons are a separate, wider set of the same pattern." + ], + "evidence": "// JS audit:\nbuttons.map(b => ({label: b.ariaLabel, text: b.textContent.trim()}))\n// Results: [{label: null, text: \"person_add\"}, {label: null, text: \"swap_horiz\"}, ...]", + "fix": "Add descriptive aria-label to each toolbar button: aria-label=\"Insert new participant\", aria-label=\"Insert asynchronous message\", etc. Also add title attributes so sighted keyboard users see tooltips on focus (addresses Case 07 GAP-07-001 too).", + "principles": [ + "WCAG 4.1.2", + "Nielsen #1 Visibility" + ], + "caseNum": 17, + "caseTitle": "Keyboard Navigation & Focus Management" + }, + { + "id": "GAP-17-005", + "severity": "low", + "title": "No skip-to-content link for keyboard users", + "paragraphs": [ + "There is no \"Skip to editor\" (skip-nav) link at the top of the page. On every page load or tab switch, keyboard users must Tab through all header and sidebar controls to reach the editor. This is standard accessibility infrastructure expected by WCAG 2.4.1 (Bypass Blocks, Level A).", + "Impact: Lower severity because GAP-17-002 (editor tabindex priority) would be the more impactful fix; a skip link is a complementary safeguard. However WCAG 2.4.1 is a Level A criterion, making this a baseline conformance failure." + ], + "evidence": null, + "fix": "Add a visually hidden but focusable Skip to editor as the first element in . Show it on focus with :focus { position: fixed; top: 0; left: 0; ... }. Assign id=\"editor\" to the CodeMirror container.", + "principles": [ + "WCAG 2.4.1", + "WCAG 2.4.3" + ], + "caseNum": 17, + "caseTitle": "Keyboard Navigation & Focus Management" + }, + { + "id": "GAP-18-001", + "severity": "high", + "title": "Share Link CTA button fails WCAG AA — 3.04:1 (needs 4.5:1)", + "paragraphs": [ + "The primary conversion button \"Share Link\" renders near-white text rgb(245,245,245) on a medium-blue background rgb(103,134,247). At 14px font size this requires a contrast ratio of 4.5:1 — the measured ratio of 3.04:1 falls 33% short of WCAG AA compliance.", + "Impact: This is the most prominent interactive element in the header, visible on every diagram. Users with low vision, in bright ambient light, or on low-gamut displays may struggle to read it. WCAG 1.4.3 (Contrast Minimum, Level AA) failure." + ], + "evidence": "// Measured via JavaScript WCAG luminance formula:\nfg: rgb(245, 245, 245) → L = 0.904\nbg: rgb(103, 134, 247) → L = 0.215\nratio = (0.904 + 0.05) / (0.215 + 0.05) = 3.04:1\nWCAG AA requires 4.5:1 for text ≤18px non-bold", + "fix": "Darken the button background to at least #4355CC (passes 4.74:1) or increase the text to 18px/bold (drops requirement to 3:1). Alternatively invert to a dark button with light text: background:#1e293b; color:#fff easily passes at 15:1.", + "principles": [ + "WCAG 1.4.3 AA", + "Nielsen #1 Visibility" + ], + "caseNum": 18, + "caseTitle": "Color Contrast & Visual Accessibility" + }, + { + "id": "GAP-18-002", + "severity": "medium", + "title": "CodeMirror line numbers fail WCAG AA — 3.83:1 (needs 4.5:1)", + "paragraphs": [ + "Editor line numbers render as rgb(109,138,136) (a muted teal) on rgb(40,42,54) (the Dracula theme background). At 14px with normal weight, the measured 3.83:1 ratio is below the 4.5:1 WCAG AA threshold for normal text.", + "While line numbers are often considered secondary UI, they are functional text that keyboard-navigating users, developers debugging syntax, and screen reader users rely on to locate specific lines. WCAG 1.4.3 applies to all meaningful text." + ], + "evidence": "// Line numbers (.CodeMirror-linenumber):\nfg: rgb(109, 138, 136) → L = 0.228\nbg: rgb(40, 42, 54) → L = 0.024\nratio = (0.228 + 0.05) / (0.024 + 0.05) = 3.83:1\nRequired: 4.5:1 (14px normal weight)", + "fix": "Brighten the line number color to approximately rgb(145,165,163) or higher. With Dracula theme, a value around #A8B8B7 achieves 5:1+ while remaining clearly secondary to code content. Alternatively, bump font size to 18px (drops threshold to 3:1).", + "principles": [ + "WCAG 1.4.3 AA" + ], + "caseNum": 18, + "caseTitle": "Color Contrast & Visual Accessibility" + }, + { + "id": "GAP-18-003", + "severity": "medium", + "title": "Diagram SVG graphical elements use #A5A5A5 on white — 2.46:1 (needs 3:1)", + "paragraphs": [ + "Block-type icons (the <> diamond for \"Alt\" and the loop arrow for \"Loop\") are rendered as SVG text/paths in #A5A5A5 on the white diagram canvas. The measured contrast ratio is 2.46:1 — below the WCAG 1.4.11 (Non-text Contrast, Level AA) minimum of 3:1 required for graphical UI components that convey meaning.", + "These icons are the only visual indicator of which block type (conditional vs. loop) is being represented. A user who cannot distinguish the icon must rely on the block label text instead." + ], + "evidence": "// SVG text elements with fill=\"#A5A5A5\":\n// [\"Alt\", \"Loop\"] — confirmed via document.querySelectorAll('svg text')\nfg: #A5A5A5 = rgb(165,165,165) → L = 0.376\nbg: white = rgb(255,255,255) → L = 1.000\nratio = (1.0 + 0.05) / (0.376 + 0.05) = 2.46:1\nWCAG 1.4.11 requires 3:1 for graphical objects", + "fix": "Change the SVG icon fill from #A5A5A5 to #767676 or darker. #767676 achieves exactly 4.54:1 on white — solidly passing AA. Or use a distinct shape/stroke instead of relying on the icon alone.", + "principles": [ + "WCAG 1.4.11 AA", + "WCAG 1.4.1" + ], + "caseNum": 18, + "caseTitle": "Color Contrast & Visual Accessibility" + }, + { + "id": "GAP-18-004", + "severity": "medium", + "title": "Syntax highlighting uses color as the sole differentiator between token types", + "paragraphs": [ + "In the CodeMirror editor, different token types are distinguished exclusively by color: identifiers in green, message text in pink/magenta, arrows in green, keywords and punctuation in white. No secondary cues — bold weight, italics, underline — differentiate token types for users with color vision deficiency.", + "An estimated 8% of males have some form of color vision deficiency. For a developer tool targeting engineers, this is a significant portion of the audience. WCAG 1.4.1 (Use of Color, Level A) prohibits using color as the only visual means of conveying information.", + "Test: In a deuteranopia simulation, green identifiers and green arrows blend to the same hue, making it visually impossible to distinguish participant names from message arrows without bold weight or other cues." + ], + "evidence": null, + "fix": "Add a secondary visual signal to at least the most important distinctions: make keywords bold (font-weight: 700), make strings italic, or use the Dracula theme's built-in styles which provide some weight variation. CodeMirror supports per-token styles via the theme configuration.", + "principles": [ + "WCAG 1.4.1 A", + "Nielsen #1 Visibility" + ], + "caseNum": 18, + "caseTitle": "Color Contrast & Visual Accessibility" + }, + { + "id": "GAP-18-005", + "severity": "low", + "title": "White diagram canvas creates extreme luminance split against dark editor — no dark-diagram option", + "paragraphs": [ + "The split-pane layout places a dark editor (#282a36, very dark) directly beside a bright white diagram canvas (#ffffff). The luminance ratio between the two panels is approximately 100:1. Users working extended sessions or with photosensitivity must continuously adapt their eyes between the two extreme luminance zones.", + "Modern tools (Mermaid Live Editor, dbdiagram.io, Excalidraw) all offer dark diagram themes or allow diagram background color configuration. ZenUML's CSS tab could theoretically allow this, but the feature requires knowing the internal CSS class names — it is not discoverable." + ], + "evidence": null, + "fix": "Add a \"Diagram theme\" toggle in Settings (light/dark/auto). The dark diagram theme would simply change the diagram wrapper's background and SVG line/text colors. A one-line CSS variable change could power the entire switch: --diagram-bg: #1e1e2e; --diagram-ink: #cdd6f4;.", + "principles": [ + "WCAG 1.4.3", + "Nielsen #4 Consistency" + ], + "caseNum": 18, + "caseTitle": "Color Contrast & Visual Accessibility" + }, + { + "id": "GAP-19-001", + "severity": "high", + "title": "7 toolbar insert buttons: zero tooltip, no keyboard shortcut hint, no syntax preview", + "paragraphs": [ + "All 7 insert buttons in the editor toolbar (New participant, Async message, Sync message, Return value, Self message, New instance, ConditionalAlt) have title=null, aria-label=null, and no custom tooltip component. The visible text label alone leaves users without:", + "1. Keyboard shortcut — is there one? Can I trigger \"Async message\" from the keyboard? 2. Resulting syntax — hovering \"Async message\" should preview A ->> B: message 3. Insertion behavior — does it insert at cursor or append at end?", + "Competing tools (Mermaid Live Editor, draw.io, PlantUML Editor) all show syntax snippets in toolbar tooltips. New ZenUML users must click and inspect the inserted code to reverse-engineer the syntax." + ], + "evidence": "// JS audit result (all 7 toolbar buttons):\n{ text: \"New participant\", title: null, ariaLabel: null }\n{ text: \"Async message\", title: null, ariaLabel: null }\n{ text: \"Sync message\", title: null, ariaLabel: null }\n{ text: \"Return value\", title: null, ariaLabel: null }\n{ text: \"Self message\", title: null, ariaLabel: null }\n{ text: \"New instance\", title: null, ariaLabel: null }\n{ text: \"ConditionalAlt\", title: null, ariaLabel: null }", + "fix": "Add a title attribute with the pattern \"[Label] — inserts: [syntax snippet]\". Example: title=\"Async message — inserts: A ->> B: message\". For a richer experience, replace with a Radix UI Tooltip showing the syntax with syntax highlighting and the keyboard shortcut if one exists.", + "principles": [ + "Nielsen #6 Recognition over Recall", + "Nielsen #4 Consistency", + "Nielsen #1 Visibility" + ], + "caseNum": 19, + "caseTitle": "Tooltip & Contextual Help UX" + }, + { + "id": "GAP-19-002", + "severity": "medium", + "title": "Inconsistent tooltip mechanism across UI zones — 3 different patterns, one invisible", + "paragraphs": [ + "Three different tooltip strategies coexist with inconsistent behavior for sighted mouse users:" + ], + "evidence": null, + "fix": "Standardize on one tooltip mechanism app-wide. Recommended: Radix UI Tooltip on every interactive element, with a consistent 300ms open delay and 100ms close delay. The existing Radix UI dependency is already in the project — use it.", + "principles": [ + "Nielsen #4 Consistency", + "WCAG 1.3.1" + ], + "caseNum": 19, + "caseTitle": "Tooltip & Contextual Help UX" + }, + { + "id": "GAP-19-003", + "severity": "medium", + "title": "Canvas footer controls (ⓘ, ☑ 1.2.3, zoom icons) have no tooltip — \"1.2.3\" is cryptic", + "paragraphs": [ + "The diagram canvas footer contains 5 interactive controls with zero tooltip:", + "• ⓘ — what does this show? (appears to open a tips overlay) • ☑ 1.2.3 — completely cryptic; hovering reveals nothing. It toggles sequence step numbers, but this is undiscoverable without clicking. • ⊕ / ⊖ — zoom icons with no label or shortcut hint • 100% — clicking this resets zoom, but hover reveals no hint", + "The \"1.2.3\" checkbox is the worst offender — even an experienced UML author might not know what this does on first sight. The label reads as a version number, not as \"show/hide sequence numbers.\"" + ], + "evidence": null, + "fix": "Add title attributes: title=\"Show/hide step numbers\" on the 1.2.3 checkbox, title=\"Tips & shortcuts\" on ⓘ, title=\"Zoom in (Ctrl+Plus)\" on ⊕, title=\"Zoom out (Ctrl+Minus)\" on ⊖, title=\"Reset zoom to 100%\" on the percentage text. Replace the 1.2.3 label with \"1.2\" or better — a sequence-number icon — and add a visible label \"Step numbers\" next to the checkbox.", + "principles": [ + "Nielsen #6 Recognition over Recall", + "Nielsen #1 Visibility" + ], + "caseNum": 19, + "caseTitle": "Tooltip & Contextual Help UX" + }, + { + "id": "GAP-19-004", + "severity": "medium", + "title": "No keyboard shortcut hints in any tooltip across the entire app", + "paragraphs": [ + "Even the 9 elements that do have tooltips (sidebar icons, header buttons) include no keyboard shortcut information. Modern productivity tools universally surface shortcuts in tooltips — VS Code, Figma, Linear, and Notion all follow the pattern: \"Action Name (Shortcut)\".", + "ZenUML has a dedicated Keyboard Shortcuts panel (accessible via the sidebar) but zero in-context shortcut hints. Users must remember to open that panel separately. For frequent actions like \"New diagram\" (Cmd+N if supported), the shortcut is entirely invisible at the point of action." + ], + "evidence": "// Example of what tooltips could say:\nsidebar \"My Library\": \"My Library\" → ❌ no shortcut\nsidebar \"Keyboard Shortcuts\": \"Keyboard Shortcuts\" → ❌ ironic: no shortcut hint\nheader \"New\": \"Start a new creation\" → ❌ should say \"(Cmd+N)\"", + "fix": "Append the keyboard shortcut (if one exists) to every tooltip: title=\"My Library (Ctrl+B)\". For the Keyboard Shortcuts icon specifically, the tooltip \"Keyboard Shortcuts (Ctrl+/)\" would be self-documenting. Use a consistent format: label first, shortcut in parentheses.", + "principles": [ + "Nielsen #6 Recognition over Recall", + "Nielsen #4 Consistency" + ], + "caseNum": 19, + "caseTitle": "Tooltip & Contextual Help UX" + }, + { + "id": "GAP-19-005", + "severity": "low", + "title": "Bottom bar action buttons (Present, PNG, Copy PNG) lack descriptive tooltips", + "paragraphs": [ + "The three action buttons in the bottom-right corner of the app — \"Present\", \"PNG\", and \"Copy PNG\" — have no tooltip. \"Present\" is particularly ambiguous: does it open a link? Enter fullscreen? Launch a slideshow? A user who hasn't tried it has no pre-click information about the outcome.", + "\"PNG\" is slightly better (the format name is self-explanatory to technical users), but new users may not know whether clicking triggers a download, opens a dialog, or copies to clipboard." + ], + "evidence": "// Confirmed zero title/ariaLabel on all three:\n{ text: \"Present\", title: null, ariaLabel: null }\n{ text: \"PNG\", title: null, ariaLabel: null }\n{ text: \"Copy PNG\", title: null, ariaLabel: null }", + "fix": "Add descriptive titles: title=\"Enter presentation mode (fullscreen)\", title=\"Download diagram as PNG image\", title=\"Copy diagram PNG to clipboard\". These would disambiguate before the user clicks — especially important since PNG and Copy PNG look visually similar.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #6 Recognition over Recall" + ], + "caseNum": 19, + "caseTitle": "Tooltip & Contextual Help UX" + }, + { + "id": "GAP-20-001", + "severity": "high", + "title": "CSS tab has zero visual indicator it is auth-gated — no lock, no badge, looks identical to free ZenUML tab", + "paragraphs": [ + "Both the ZenUML tab and the CSS tab use identical visual styling: bg-black-800 font-semibold. The CSS tab carries no lock icon, no \"Pro\" or \"Sign in\" badge, no greyed-out state, no tooltip warning. A user has no pre-click information that the CSS tab requires authentication.", + "This violates the principle of transparent feature gating — when a feature requires upgrade or authentication, that requirement should be visible at the entry point, not revealed only after the user clicks. Showing the gate only at click-time is a dark pattern known as \"bait and switch.\"" + ], + "evidence": null, + "fix": "Add a lock icon (🔒) and a \"Pro\" or \"Sign in\" badge to the CSS tab. On hover, show a tooltip: \"CSS customization — sign in to unlock\". Alternatively, allow unauthenticated users to view a read-only CSS editor prefilled with a default template, with a banner prompting sign-in to save.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #10 Help & Docs", + "Dark patterns: Bait & Switch" + ], + "caseNum": 20, + "caseTitle": "CSS Tab & Custom Styling UX" + }, + { + "id": "GAP-20-002", + "severity": "high", + "title": "5th generic \"Welcome to ZenUML.com\" auth modal — zero context about the CSS feature", + "paragraphs": [ + "Clicking the CSS tab shows the same generic modal as Share Link, Export PNG, Copy PNG, and Present mode — all 5 show identical text: \"Welcome to ZenUML.com / Login with Github / Login with Google / Login with Facebook / Join a community of 50,000+ Developers.\" The modal contains:", + "• No mention of CSS customization — what does it unlock? • No preview or screenshot of what CSS styling looks like • No indication of tier — is it free with login, or paid? • No \"Learn more\" link for users who want to understand before committing", + "The 5-feature pattern of identical auth gates means that by the 3rd or 4th encounter, users disengage completely — they no longer read the modal and simply dismiss it. The modal provides no conversion value for the CSS feature specifically." + ], + "evidence": "// All 5 features trigger the SAME modal:\n// 1. Share Link (Case 4)\n// 2. Present Mode (Case 6)\n// 3. Export PNG / Copy PNG (Case 15)\n// 4. CSS Tab (this case — #5)\n// Modal text: \"Welcome to ZenUML.com\" — no feature context", + "fix": "Replace the generic modal with a feature-specific upsell: show the CSS editor UI as a teaser (blurred or read-only), a 1-2 sentence description (\"Write custom CSS to brand your diagrams with company colors and fonts\"), and a clear CTA. Each feature's auth gate should contextualize WHY sign-in is needed.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #2 Match Real World", + "Conversion UX" + ], + "caseNum": 20, + "caseTitle": "CSS Tab & Custom Styling UX" + }, + { + "id": "GAP-20-003", + "severity": "medium", + "title": "CSS editor is absent from DOM entirely — no preview, no placeholder, no teaser for unauthenticated users", + "paragraphs": [ + "When unauthenticated, the CSS tab's editor container is completely absent from the DOM — confirmed via document.querySelector('[class*=\"css-editor\"]') returning null and zero CSS-related divs rendered. There is no placeholder, empty state, or teaser of what the editor would contain.", + "In contrast, tools like Mermaid Live Editor, draw.io, and CodePen show the full editor interface to all users but restrict saving/sharing behind auth. This \"try before you sign up\" approach drives significantly higher conversion because users can experience the value before being asked to commit." + ], + "evidence": "// JS audit:\ndocument.querySelector('[class*=\"css-editor\"]') // → null\ndocument.querySelectorAll('[class*=\"css\"]') // → 0 elements\n// The CSS editor panel is never rendered for anon users\n// No placeholder, no empty state, no sample CSS shown", + "fix": "Render the CSS editor for unauthenticated users with a sample CSS snippet (e.g., .participant { background: #e8f4fd; }). Allow free editing but block saving with an inline \"Sign in to save your changes\" banner. This lets users experience the feature's value before signing up.", + "principles": [ + "Nielsen #6 Recognition over Recall", + "Conversion UX" + ], + "caseNum": 20, + "caseTitle": "CSS Tab & Custom Styling UX" + }, + { + "id": "GAP-20-004", + "severity": "medium", + "title": "Active tab state relies on a single 2px blue underline — low visual salience, no background differentiation", + "paragraphs": [ + "The active tab (ZenUML) is differentiated from the inactive tab (CSS) by only: a border-b border-primary (2px bottom border in blue) and a minor background shift from bg-black-800 to bg-black-500. At normal viewing distance, the active state is not immediately obvious — the text color change to text-primary-400 (blue-ish) is subtle against the dark background.", + "Modern tab implementations (Chrome DevTools, VS Code, Figma panels) use a clear background color contrast, not just a border underline, to communicate active state. The current implementation passes a quick glance test only because there are two tabs — with 3+ tabs the active tab would be much harder to spot." + ], + "evidence": "// Active tab class audit:\n// ZenUML (active): \"... border-b border-primary bg-black-500 text-primary-400\"\n// CSS (inactive): \"... bg-black-800 font-semibold\"\n// Difference: border-b + 1 Tailwind shade lighter bg + blue text\n// bg-black-500 vs bg-black-800: very close luminance values in dark palette", + "fix": "Increase active tab visual contrast: use a distinctly lighter background (e.g., bg-zinc-700) with a rounded top-corners treatment, or switch to a pill/chip active-tab style with a full background fill. The border-b alone is the weakest form of tab indicator in dark UIs.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #4 Consistency" + ], + "caseNum": 20, + "caseTitle": "CSS Tab & Custom Styling UX" + }, + { + "id": "GAP-20-005", + "severity": "low", + "title": "No documentation or CSS selector reference anywhere in the app", + "paragraphs": [ + "Even if a user successfully authenticates to access the CSS editor, there is no in-app documentation about:", + "• What CSS selectors target which diagram elements (e.g., .participant, .message-label) • Which CSS properties are supported (SVG CSS vs HTML CSS) • Example CSS snippets for common customizations (brand colors, font changes) • A link to external documentation or a \"CSS Reference\" panel", + "The Language Guide sidebar panel exists for ZenUML syntax, but there is no equivalent guide for the CSS editor. A power user discovering CSS customization must reverse-engineer the SVG structure by hand." + ], + "evidence": null, + "fix": "Add a collapsible \"CSS Reference\" panel within the CSS editor (similar to the Language Guide for ZenUML). Include the top 10 most-used selectors with examples: .participant { }, .message { }, .divider { }, etc. A single-page CSS cookbook linked from the editor would dramatically improve discoverability.", + "principles": [ + "Nielsen #10 Help & Docs", + "Nielsen #6 Recognition over Recall" + ], + "caseNum": 20, + "caseTitle": "CSS Tab & Custom Styling UX" + }, + { + "id": "GAP-21-001", + "severity": "high", + "title": "Split pane gutter is completely invisible — 6px, near-transparent background, no hover affordance", + "paragraphs": [ + "The divider between the editor and canvas panels is a 6px-wide .gutter.gutter-horizontal element with background: rgba(255,255,255,0.05) — effectively invisible against the dark UI. There is no CSS hover rule that highlights it, no grip dots, no color change. The cursor does switch to ew-resize when hovering the gutter, but discovering that 6px strip by accident is the only signal the feature exists.", + "Impact: The vast majority of users will never discover they can resize the split. In 20 UX test cases, the gutter was never stumbled upon organically — it was only found through DOM inspection. This is a hidden power-user feature that could serve everyone." + ], + "evidence": "// Gutter DOM audit:\nclass: \"gutter gutter-horizontal\"\nwidth: 6px\nbackground: rgba(255, 255, 255, 0.05) // 5% opacity white — invisible\ncursor: ew-resize // only signal, requires hitting the 6px strip\nNo CSS :hover rule defined for .gutter selector\nNo grip icon, no handle, no visual affordance", + "fix": "Add a visible grip affordance: center 3 horizontal dots or a subtle bar on the gutter. On hover, brighten background to rgba(255,255,255,0.15) and show the dots more clearly. A 2px visible stripe on the left edge of the canvas pane (like VS Code's activity bar separator) would be enough. Minimum: add a CSS hover rule .gutter:hover { background: rgba(255,255,255,0.12); }.", + "principles": [ + "Nielsen #1 Visibility", + "Affordance (Gibson/Norman)" + ], + "caseNum": 21, + "caseTitle": "Split Pane Resize & Layout Control" + }, + { + "id": "GAP-21-002", + "severity": "high", + "title": "Neither pane can be collapsed — minSize:15% on both sides prevents focus mode", + "paragraphs": [ + "The paneforge configuration enforces minSize: 15 (15% of total width) on both the editor and canvas panes. This means:", + "• Users cannot collapse the canvas to get a full-width editor for writing long ZenUML scripts • Users cannot collapse the editor to get a distraction-free, full-width diagram view • Maximum editor width is 85%, maximum canvas width is 85%", + "Developer tools universally offer panel collapse: VS Code collapses any panel to 0, JetBrains has explicit collapse buttons, Mermaid Live Editor has a \"hide editor\" toggle. Power users regularly want to maximize one pane." + ], + "evidence": "// localStorage['paneforge:liveEditor']:\n// {\"defaultSize\":30,\"minSize\":15},{\"minSize\":15}\n// layout: [30, 70] (30% editor, 70% canvas default)\n// minSize: 15% on both = neither can collapse below 15%\n// At 1920px viewport: editor min = 288px, canvas min = 288px", + "fix": "Add explicit collapse/expand buttons to both panels (a chevron icon at the panel boundary), and set collapsible: true in paneforge config with collapsedSize: 0. Add keyboard shortcuts: Cmd+\\ to toggle the split, Cmd+B to collapse/expand the editor (following VS Code conventions).", + "principles": [ + "Nielsen #3 User Control", + "Nielsen #7 Flexibility" + ], + "caseNum": 21, + "caseTitle": "Split Pane Resize & Layout Control" + }, + { + "id": "GAP-21-003", + "severity": "medium", + "title": "Default 30/70 split gives only 30% to the editor — primary work surface is undersized by default", + "paragraphs": [ + "The paneforge default configuration allocates defaultSize: 30 (30%) to the editor and 70% to the canvas. At a 1920px viewport, this gives the editor ~576px — barely enough for a 7-line ZenUML script. A typical sequence diagram with 8-10 participants and 15-20 messages will overflow the editor height, requiring scrolling while writing.", + "The diagram canvas by contrast has 1344px of width, most of which is empty dark space. The actual rendered diagram sits centered in a fraction of that area. The default split optimizes for the output, not the input — backwards for an editor-first tool." + ], + "evidence": "// Default paneforge layout: [30, 70]\n// At 1920px viewport:\n// Editor: 30% = 576px (after sidebar)\n// Canvas: 70% = 1344px\n// Typical diagram canvas renders in ~400px SVG width\n// → 944px of canvas is empty dark space at default split", + "fix": "Change the default split to 40/60 or 50/50. Run an A/B test — for a code editor tool, giving the editor equal or majority space tends to improve session length and diagram complexity. The 30% default feels like a viewer app, not an editor app.", + "principles": [ + "Nielsen #4 Consistency", + "Nielsen #7 Flexibility" + ], + "caseNum": 21, + "caseTitle": "Split Pane Resize & Layout Control" + }, + { + "id": "GAP-21-004", + "severity": "medium", + "title": "No double-click to reset split — persisted custom ratio has no escape hatch", + "paragraphs": [ + "The split ratio is persisted to localStorage['paneforge:liveEditor'] and survives page reloads. Once a user drags the gutter to an undesirable position (e.g., accidentally making the editor very narrow), there is no way to reset it except dragging back manually — with the invisible gutter. Double-clicking the gutter was tested and confirmed to have no reset behavior.", + "VS Code, IntelliJ, and most modern split-pane implementations support double-click to reset to the default ratio. This is a discoverable escape hatch that doesn't require the user to find the Settings or clear localStorage." + ], + "evidence": "// Double-click test result:\n// Before: editorWidth = 795px (42.9%)\n// After double-click: editorWidth = 795px (unchanged)\n// paneforge does not implement double-click-to-reset\n// Only way to reset: drag back manually or clear localStorage", + "fix": "Add double-click handler on the gutter element that calls paneforge's reset() or sets the split back to [30, 70]. Also add a \"Reset layout\" option in the Settings modal for users who want to recover from a bad split position.", + "principles": [ + "Nielsen #3 User Control", + "Nielsen #5 Error Prevention" + ], + "caseNum": 21, + "caseTitle": "Split Pane Resize & Layout Control" + }, + { + "id": "GAP-21-005", + "severity": "low", + "title": "6px gutter is too narrow for reliable hit-testing, especially on high-DPI displays", + "paragraphs": [ + "The gutter's 6 CSS pixel width at 2× DPR renders as a 12 physical pixel strip. Fitts's Law quantifies this: the time to acquire a 6px target at 500px distance is approximately 3× longer than acquiring a 12px target at the same distance. This is compounded by the invisible background — users cannot see where the target is.", + "VS Code uses a 5px gutter with an 8px transparent hover area on each side (effective target: 21px). IntelliJ uses a visible 6px border with highlight. Both are significantly more discoverable than ZenUML's invisible 6px strip." + ], + "evidence": null, + "fix": "Keep the visual strip at 4-6px but expand the interactive hit area to 20px via transparent padding or a wider wrapper element with pointer-events: all. The visual grip stays narrow; the click target becomes comfortable.", + "principles": [ + "Fitts's Law", + "Nielsen #1 Visibility" + ], + "caseNum": 21, + "caseTitle": "Split Pane Resize & Layout Control" + }, + { + "id": "GAP-22-001", + "severity": "high", + "title": "Invalid syntax silently renders a blank canvas — no error message, no inline squiggle, no notification", + "paragraphs": [ + "When a user types completely invalid ZenUML (e.g., !!!INVALID@@@SYNTAX###), the canvas silently collapses to a near-empty diagram showing only an unlabeled internal _STARTER_ participant (a stickman box). There is:", + "• No error banner above or below the diagram • No inline red squiggles in the editor (no CodeMirror lint markers; errorGutterChildCount: 0) • No toast or notification (toasts: 0) • No DOM error element visible (errorBanners: []) • The canvas iframe has errorElements: [] — nothing conveys the error state to the user", + "Competing tools handle this dramatically better: Mermaid Live Editor shows a red \"Parse error\" banner with the specific line and token that failed. Kroki and PlantUML editors display the exception message inline. ZenUML's completely silent failure leaves users with no idea what went wrong — they must delete code manually to diagnose." + ], + "evidence": "// JS DOM audit with input: \"!!!INVALID@@@SYNTAX###\\nthis is not valid zenuml at all\\n{{{ broken\"\n// Main page:\nerrorBanners: [] // no visible error elements\neditorErrorMarks: 1 // only the empty error-gutter container div itself\ngutterMarkers: 0 // no error markers placed in gutter\ntoasts: 0 // no toast notifications\n// Canvas iframe:\nerrorElements: [] // zero error nodes in iframe\nerrorMsgCount: 0 // no parse-error class elements\nparticipantCount: 1 // only the internal _STARTER_ fallback\nallVisibleText: [\"Click to add title\", \"_STARTER_\"] // internal placeholder leaked", + "fix": "Show a parse error banner below the editor toolbar (or as a red stripe at the bottom of the canvas) with the ANTLR error message and the offending line number. Minimum viable: wrap the diagram renderer in an error boundary that catches parser exceptions and renders a styled
Syntax error on line N: [message]
above the canvas footer. For inline editor feedback, add CodeMirror lint integration via cm.setOption(\"lint\", true) with a custom linter that runs the ZenUML parser.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #9 Error Messages", + "Nielsen #6 Recognition over Recall" + ], + "caseNum": 22, + "caseTitle": "Error State & Syntax Validation UX" + }, + { + "id": "GAP-22-002", + "severity": "high", + "title": "Uncaught TypeError on every N-th keystroke — window.saveBtn is undefined, silently killing the unsaved-changes warning animation", + "paragraphs": [ + "Every UNSAVED_WARNING_COUNT keystrokes, onCodeChange in app.jsx:914 attempts to access window.saveBtn.classList.add('animated'). Since window.saveBtn is undefined in the current app build, this throws a TypeError: Cannot read properties of undefined (reading 'classList') — confirmed by 10+ identical console EXCEPTION entries during a single typing session.", + "The consequences are doubled:", + "• The save animation never fires — users are never visually warned they have N unsaved changes. The \"wobble\" animation on the save button (which is the intended UX) is completely dead code in production. • The exception is silently swallowed — the user sees nothing. There is no error boundary, no fallback, no degraded behavior — the app just quietly fails internally." + ], + "evidence": "// Console output during typing session (10+ occurrences at 08:28:54):\n[EXCEPTION] (app.jsx:729:21)\nTypeError: Cannot read properties of undefined (reading 'classList')\n at App.onCodeChange (app.jsx:730:22)\n\n// Source code at app.jsx:914:\nif (isUserChange && this.state.unsavedEditCount % UNSAVED_WARNING_COUNT === 0\n && this.state.unsavedEditCount >= UNSAVED_WARNING_COUNT) {\n window.saveBtn.classList.add('animated'); // ← TypeError: window.saveBtn is undefined\n window.saveBtn.classList.add('wobble');\n}", + "fix": "Guard with a null check: if (window.saveBtn) { window.saveBtn.classList.add('animated'); }. Better: replace the global window.saveBtn reference with a React ref (this.saveBtnRef = createRef() on the button element) — the global pattern is fragile and breaks if the button is conditionally rendered. This fix also restores the intended unsaved-changes animation.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #5 Error Prevention", + "Nielsen #9 Error Messages" + ], + "caseNum": 22, + "caseTitle": "Error State & Syntax Validation UX" + }, + { + "id": "GAP-22-003", + "severity": "medium", + "title": "CodeMirror error-gutter is configured but never populated — inline error markers are dead infrastructure", + "paragraphs": [ + "The CodeMirror instance has a custom gutter registered as \"error-gutter\" — confirmed by the presence of a
in the DOM. This gutter was clearly designed to show per-line error indicators (like a red ✕ or warning triangle next to the offending line number).", + "However, gutterMarkers: 0 — no markers are ever placed in this gutter, even with 3 lines of completely invalid syntax. The gutter exists as a visible empty column next to the line numbers, occupying space without serving any purpose. The infrastructure for inline error indicators was planned but never completed." + ], + "evidence": "// DOM audit:\ndocument.querySelector('.error-gutter')\n// →
(empty)\n\n// Expected CodeMirror API to populate it:\ncm.setGutterMarker(lineNumber, \"error-gutter\", markerElement);\n// This call is never made anywhere in the codebase for parse errors.\n\n// Result: error-gutter exists (takes up space) but is permanently empty", + "fix": "Complete the error-gutter integration: when the ZenUML parser throws, extract the line number from the ANTLR error, create a styled marker element (e.g., a red circle with an ✕ icon), and call cm.setGutterMarker(errorLine - 1, \"error-gutter\", marker). Clear markers on valid parse. This provides exactly the kind of inline feedback VS Code and other editors show.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #9 Error Messages" + ], + "caseNum": 22, + "caseTitle": "Error State & Syntax Validation UX" + }, + { + "id": "GAP-22-004", + "severity": "medium", + "title": "Editor and canvas desync after undo past initial load — canvas shows stale content while editor is empty", + "paragraphs": [ + "After typing invalid syntax and pressing Cmd+Z ten times, the editor empties completely (line 1 blank) while the canvas continues to show a fully rendered diagram (the previously saved \"Auth Flow\" diagram). The editor state and the rendered canvas state are out of sync.", + "This represents a violation of the \"what you see is what you get\" contract: the canvas is showing a diagram that is no longer represented by any editor content. A user in this state would:", + "• Think they still have a valid diagram when the editor shows nothing • Be unable to determine what code produced the current canvas • Be confused if they save — which version gets saved, the empty editor or the visible canvas?" + ], + "evidence": "// Observed state after 10× Cmd+Z:\neditor.getValue() → \"\" (empty)\ncanvas iframe shows: A→B: Login, B→C: Validate, 3. token, 4. Welcome!\n// The two panels disagree on the diagram content\n// Undo history exhausted past the initial load state\n// No visual indicator that editor/canvas are out of sync", + "fix": "When the editor is emptied (content length drops to 0), either: (a) render an empty-state placeholder in the canvas rather than the last diagram, or (b) set a minimum editor content of the default sample diagram (preventing undo past initial load). Track the \"floor\" undo state — when undo reaches the initial content, stop further undos. CodeMirror supports this via cm.clearHistory() at load time to set the baseline.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #3 User Control", + "Nielsen #4 Consistency" + ], + "caseNum": 22, + "caseTitle": "Error State & Syntax Validation UX" + }, + { + "id": "GAP-22-005", + "severity": "low", + "title": "Internal _STARTER_ participant name leaks into the visible canvas on parse failure", + "paragraphs": [ + "When ZenUML fails to parse the input, it renders a fallback participant named _STARTER_ — an internal implementation detail. This participant is visible in the canvas iframe as a stickman box with no label. While the participant label itself is hidden (the stickman icon is the only rendering), the text _STARTER_ is present in the DOM (confirmed via allVisibleText: [\"Click to add title\", \"_STARTER_\"]) and could be visible in exported PNGs or in certain zoom/render states.", + "Users who export the diagram in an error state (not knowing the parse failed silently) may receive a PNG with a single unlabeled stickman — and have no idea why their diagram is \"empty.\"" + ], + "evidence": "// Iframe DOM audit during invalid syntax state:\ndoc.querySelectorAll('[class*=\"participant\"]').length → 1\nallVisibleText: [\"Click to add title\", \"_STARTER_\"]\n// lifeline class: \"lifeline absolute flex flex-col h-full starter\"\n// The \"starter\" CSS class and \"_STARTER_\" text are internal implementation names\n// exposed to the DOM when parsing fails", + "fix": "Replace the _STARTER_ fallback rendering with an explicit empty state component that shows a message like \"Your diagram will appear here\" or \"Fix the syntax error to see the diagram.\" This separates the intentional empty state from the error state, and prevents internal implementation names from leaking into the visible DOM or exported artifacts.", + "principles": [ + "Nielsen #9 Error Messages", + "Nielsen #2 Match Real World" + ], + "caseNum": 22, + "caseTitle": "Error State & Syntax Validation UX" + }, + { + "id": "GAP-23-001", + "severity": "high", + "title": "Split-pane layout never stacks vertically — both panels become unusably narrow on mobile", + "paragraphs": [ + "The .main-content-area is always display: flex; flex-direction: row with no responsive breakpoint that switches to flex-direction: column. At a 375px iPhone SE viewport, the default 30/70 split gives:", + "• Editor: ~113px wide — a 113px code editor is entirely unusable. A typical variable name exceeds the visible width. • Canvas: ~263px wide — a 3-participant sequence diagram would be horizontally clipped at this width • No toggle to switch to single-pane view — unlike Mermaid Live Editor or StackBlitz which offer \"preview only\" mode on mobile", + "The app has a viewport meta tag set to width=device-width, initial-scale=1, meaning mobile browsers will render the layout at actual screen width rather than shrinking to a \"desktop\" view — making the broken layout fully visible to mobile users." + ], + "evidence": "// CSS audit: .main-content-area has NO responsive flex-direction rules\n.main-content-area { display: flex; flex-direction: row; /* ← permanent, no breakpoint */ }\n\n// At 375px viewport (iPhone SE, scaling from 1920px base):\n// Scale factor: 375 / 1920 = 0.195\neditorWidth = (1920 × 0.30) × 0.195 = ~113px // unusable\ncanvasWidth = (1920 × 0.70) × 0.195 = ~263px // severely clipped\n\n// No responsive stacking found in any stylesheet:\n// Tailwind sm: classes in entire app: 0\n// Tailwind md: classes in entire app: 1 (md:flex — on one element)\n// Tailwind lg: classes in entire app: 2 (lg:inline, lg:block)", + "fix": "Add a responsive stacking breakpoint: at max-width: 768px, set .main-content-area { flex-direction: column; }. Stack the canvas above the editor (diagram-first, as users on mobile are more likely to view than edit). Add a tab-style toggle (\"Edit / Preview\") to switch between the two panes on mobile. This follows the pattern used by CodePen, JSFiddle, and StackBlitz.", + "principles": [ + "Nielsen #4 Consistency", + "Mobile-first design", + "WCAG 1.3.4 Orientation" + ], + "caseNum": 23, + "caseTitle": "Responsive & Mobile Layout UX" + }, + { + "id": "GAP-23-002", + "severity": "high", + "title": "24 of 25 interactive elements fail WCAG 2.5.5 touch target minimum (44×44px) at mobile viewport", + "paragraphs": [ + "At a 375px viewport, all interactive elements scale proportionally (0.195×). The resulting touch targets are far below the WCAG 2.5.5 minimum of 44×44px and the WCAG 2.5.8 minimum of 24×24px:" + ], + "evidence": "// JS audit at 375px viewport (scale = 375/1920 = 0.195):\ntotalInteractive: 25\nfailingTouchTargets: 24 // 96% failure rate\npassingTouchTargets: 1 // only 1 element passes\n\n// Worst offenders:\nNew button: desktop 90×33px → mobile 18×6px (needs 44×44px)\nShare Link: desktop 102×40px → mobile 20×8px\nProfile button: desktop 40×40px → mobile 8×8px\nSidebar icons: desktop 44×50px → mobile 9×10px", + "fix": "For mobile-specific touch target sizing: add @media (max-width: 768px) { button, a { min-height: 44px; min-width: 44px; } } as a baseline. Better: redesign the mobile layout to use a bottom tab bar (replacing the sidebar icon column) with full-width touch-friendly items. The existing desktop design is appropriate for pointer devices — the issue is there is no separate mobile layout.", + "principles": [ + "WCAG 2.5.5 AA", + "WCAG 2.5.8 AA", + "Fitts's Law" + ], + "caseNum": 23, + "caseTitle": "Responsive & Mobile Layout UX" + }, + { + "id": "GAP-23-003", + "severity": "medium", + "title": "No mobile navigation pattern — the full icon sidebar and header persist at all viewport sizes", + "paragraphs": [ + "The left sidebar (5 icon buttons: My Diagrams, Editor, Keyboard, Language Guide, Settings) and the full header (New, diagram title, Share Link, avatar) remain unchanged at all viewport widths. There is no hamburger menu, no bottom tab bar, no collapsible sidebar — the mobile navigation is simply the desktop navigation crushed to unusability.", + "Modern responsive apps follow one of: (a) hamburger menu that slides in a full sidebar, (b) bottom navigation bar for primary destinations, (c) progressive disclosure where secondary nav items hide behind a \"More\" button. ZenUML uses none of these — the sidebar icon column at mobile becomes a 9px-wide strip occupying screen real estate while being untappable." + ], + "evidence": "// Confirmed via DOM audit:\nhamburgerMenu: \"no\" // no hamburger/menu-toggle element\nsidebarToggle: \"none\" // no toggle button found\ntouchEventElements: 0 // no ontouchstart/ontouchmove handlers\npointerMediaQueries: [] // no @media (pointer: coarse) adaptations\n// Tailwind responsive class inventory (entire app):\nsm: classes: 0 // zero adaptations at 640px\nmd: classes: 1 // one element uses md:flex\nlg: classes: 2 // two elements use lg:inline / lg:block\nxl: classes: 0 // zero adaptations above 1280px", + "fix": "Add a mobile navigation pattern: at max-width: 768px, hide the vertical sidebar icon column and replace with a horizontal bottom bar showing the 3-4 most critical actions (My Diagrams, Language Guide, Settings, + a \"...\" overflow). Alternatively, convert the sidebar to a slide-in drawer triggered by a hamburger button in the header. This is the standard pattern for responsive tool apps (Figma, Notion mobile, Linear mobile).", + "principles": [ + "Nielsen #4 Consistency", + "Nielsen #7 Flexibility", + "Mobile UX patterns" + ], + "caseNum": 23, + "caseTitle": "Responsive & Mobile Layout UX" + }, + { + "id": "GAP-23-004", + "severity": "medium", + "title": "Header becomes horizontally scrollable at 600px — diagram title and title bar clip silently", + "paragraphs": [ + "At max-width: 600px, the CSS rule .main-header { overflow-x: auto } kicks in, making the header horizontally scrollable. This means at narrow viewports, users must horizontally scroll to access the \"Share Link\" button, the diagram title, and the avatar — but there is no visual indicator (no scroll shadow, no chevron) that horizontal scrolling is available.", + "The \"New\" button and the \"Share Link\" button are on opposite ends of the header. At 400px, a user may only see one of them without scrolling. The diagram title — centrally positioned — may be hidden off-screen. This violates the principle of keeping primary actions within the visible viewport." + ], + "evidence": "// CSS at max-width: 600px:\n.main-header { overflow-x: auto; } // header becomes scrollable\n.main-header__btn-wrap { flex-shrink: 0; } // button group stays at full width\n\n// At 375px:\n// Header height: ~10px (53px × 0.195 scale)\n// Share Link button (rightmost): off-screen without scrolling\n// Diagram title (center): clipped\n// No scroll indicator shown (no -webkit-overflow-scrolling visual)", + "fix": "Redesign the header for mobile: at max-width: 768px, remove the diagram title from the header center (it's secondary information on mobile), show only \"New\" on the left and a \"⋯\" overflow menu on the right containing Share Link and other actions. This keeps all primary actions reachable without horizontal scrolling.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #3 User Control" + ], + "caseNum": 23, + "caseTitle": "Responsive & Mobile Layout UX" + }, + { + "id": "GAP-23-005", + "severity": "low", + "title": "No touch/pointer event adaptations — swipe gestures and touch-specific diagram interactions absent", + "paragraphs": [ + "The app has zero ontouchstart, ontouchmove, or touch* event handlers anywhere in the DOM (confirmed: touchEventElements: 0). It also has no @media (pointer: coarse) CSS adaptations for touch-screen devices. This means:", + "• No pinch-to-zoom on the canvas — users cannot use native touch gestures to zoom the diagram on a tablet or phone • No swipe-to-switch panels — on mobile, swiping left/right could naturally switch between editor and canvas views • No touch-friendly scrolling in the editor (CodeMirror's touch support is limited without explicit configuration) • Hover-dependent tooltips never trigger on touch devices — all the tooltip information (case 19) is inaccessible on mobile" + ], + "evidence": "// DOM audit:\ntouchEventElements: 0 // no touch event handlers anywhere\npointerMediaQueries: [] // no @media (pointer: coarse) rules\n// CodeMirror touch support requires:\n// cm.setOption(\"lineWrapping\", true) — not set by default\n// Hammer.js or similar for pinch/swipe on canvas — not installed\n// canvas iframe also has no touch event listeners", + "fix": "Add @media (pointer: coarse) CSS rules to increase spacing and target sizes for touch users (even before a full mobile layout redesign). For the canvas, add touch-action: none on the diagram iframe and implement pinch-zoom via the Pointer Events API. For the editor, enable CodeMirror's touch-friendly mode with lineWrapping: true.", + "principles": [ + "WCAG 2.5.5", + "Mobile UX patterns", + "Fitts's Law" + ], + "caseNum": 23, + "caseTitle": "Responsive & Mobile Layout UX" + } +] \ No newline at end of file diff --git a/dev-experience/gaps.json b/dev-experience/gaps.json new file mode 100644 index 00000000..051fea95 --- /dev/null +++ b/dev-experience/gaps.json @@ -0,0 +1,1779 @@ +{ + "generated": "2026-05-10T00:06:11.905Z", + "totals": { + "high": 45, + "medium": 51, + "low": 23 + }, + "cases": [ + { + "number": 1, + "slug": "case-01-app-load", + "title": null, + "subtitle": null, + "caption": null, + "gif": "ux-case-01-app-load.gif", + "gaps": [ + { + "id": "GAP-01-001", + "severity": "high", + "title": "No onboarding or empty-state guidance", + "paragraphs": [ + "New users land on a mostly-black screen with a single letter \"A\" in the editor and a tiny diagram. There is no welcome message, tutorial prompt, or \"get started\" example." + ], + "evidence": null, + "fix": "Show a rich default example (3-4 participants, arrows, return values) with an inline comment block explaining the syntax.", + "principles": [] + }, + { + "id": "GAP-01-002", + "severity": "high", + "title": "Sidebar icons have no tooltips or labels", + "paragraphs": [ + "The 6 sidebar icons (folder, code editor, keyboard, document, library, settings) have no hover tooltip and no visible label. Users must click each one to discover what it does." + ], + "evidence": null, + "fix": "Add tooltip on hover (100ms delay) and optionally a collapsible label rail. Modern tools (Figma, Linear, Notion) all label their nav icons.", + "principles": [] + }, + { + "id": "GAP-01-003", + "severity": "high", + "title": "Preview canvas wastes ~75% of available width", + "paragraphs": [ + "The diagram renders in a small floating frame (≈260px wide) pinned to the top-left of a large dark canvas. The remaining right-side space is completely unused." + ], + "evidence": null, + "fix": "Make the preview fill the available pane width and auto-fit the diagram to the viewport (like Mermaid Live Editor, draw.io). Add a \"fit to screen\" button.", + "principles": [] + }, + { + "id": "GAP-01-004", + "severity": "medium", + "title": "Settings modal mislabeled as \"Editor\"", + "paragraphs": [ + "The gear icon in the sidebar opens a dialog titled \"Editor\", not \"Settings\". Users looking for app settings will be confused; \"Editor\" implies code editor preferences only." + ], + "evidence": null, + "fix": "Rename the modal to \"Settings\" or split into \"Editor Preferences\" and \"App Settings\". The icon should match the modal title.", + "principles": [] + }, + { + "id": "GAP-01-005", + "severity": "medium", + "title": "Editor toolbar icons are unlabeled", + "paragraphs": [ + "8+ icon buttons appear above the code editor (participant, arrow types, loops, alt/else blocks) with no labels, no hover tooltips, and no visual grouping. The syntax they insert is non-obvious from the icon alone." + ], + "evidence": null, + "fix": "Add tooltips showing the inserted syntax snippet. Group related icons with a divider. Consider a label on hover like \"Insert if/else block (Alt)\".", + "principles": [] + }, + { + "id": "GAP-01-006", + "severity": "medium", + "title": "\"Preserve console logs\" exposed in user settings", + "paragraphs": [ + "A debug/developer option sits alongside user-facing preferences (Theme, Font, Line wrap). This is noise for the vast majority of users and reduces trust." + ], + "evidence": null, + "fix": "Move debug options behind an \"Advanced\" expandable section or remove from the UI entirely unless explicitly requested.", + "principles": [] + }, + { + "id": "GAP-01-007", + "severity": "low", + "title": "Title edit affordance is easy to miss", + "paragraphs": [ + "The diagram title \"Data Processing Flow\" has a small pencil icon to the right that only appears at a glance. There's no hover state or underline hint that the title is editable in-place." + ], + "evidence": null, + "fix": "Show a hover underline on the title text itself (not just the icon) so users know it's clickable. Industry standard: clicking the title text should open the inline editor.", + "principles": [] + } + ] + }, + { + "number": 2, + "slug": "case-02-save-export", + "title": null, + "subtitle": null, + "caption": null, + "gif": "ux-case-02-save-export.gif", + "gaps": [ + { + "id": "GAP-02-001", + "severity": "high", + "title": "Cmd+S has zero visual feedback", + "paragraphs": [ + "After pressing Cmd+S to save, the UI is pixel-identical before and after. No toast, no status indicator, no title asterisk, no brief flash — nothing confirms the save happened. Users must guess whether their work is safe." + ], + "evidence": null, + "fix": "Show a brief \"Saved\" toast (bottom-right, 2s auto-dismiss). Add an unsaved-changes indicator (e.g. \"●\" dot before the title or asterisk in browser tab title) to make the dirty/clean state explicit.", + "principles": [] + }, + { + "id": "GAP-02-002", + "severity": "high", + "title": "No unsaved-changes indicator", + "paragraphs": [ + "After typing new content but before saving, there is no visual signal that the document has unsaved changes. If the tab is closed or the page refreshes, users lose work with no warning." + ], + "evidence": null, + "fix": "Show a subtle \"unsaved\" dot/asterisk in the header title while there are uncommitted changes. Intercept the beforeunload event with a browser \"leave page?\" dialog when there are unsaved edits.", + "principles": [] + }, + { + "id": "GAP-02-003", + "severity": "high", + "title": "Export (PNG) surprises users with a login wall", + "paragraphs": [ + "Clicking \"PNG\" immediately triggers a \"Welcome to ZenUML.com\" login modal with no prior indication that export requires an account. The export button has no lock icon, tooltip, or disabled state to signal it's auth-gated." + ], + "evidence": null, + "fix": "Show a lock icon on the PNG button with a tooltip \"Sign in to export\". Alternatively, allow anonymous SVG/PNG export for basic diagrams and gate cloud sync/history behind auth. The surprise paywall is the worst UX pattern for conversion.", + "principles": [] + }, + { + "id": "GAP-02-004", + "severity": "medium", + "title": "Login modal doesn't explain WHY auth is needed", + "paragraphs": [ + "The login modal says \"Welcome to ZenUML.com\" and offers GitHub / Google / Facebook sign-in, but gives no context for why it appeared. Users who clicked \"PNG\" don't know if they're signing in to export, to save to cloud, or for something else entirely." + ], + "evidence": null, + "fix": "Add a one-sentence context line above the login options: \"Sign in to download your diagram as PNG and access cloud save.\" This dramatically reduces confusion and improves conversion.", + "principles": [] + }, + { + "id": "GAP-02-005", + "severity": "low", + "title": "Invalid syntax renders silently as participant boxes", + "paragraphs": [ + "When the user types invalid syntax (e.g. B--> INVALID SYNTAX ???), the parser treats \"INVALID\", \"SYNTAX\", and \"???\" as participant names and renders a diagram with nonsensical boxes — with no error highlight, no inline warning, and no status indicator." + ], + "evidence": null, + "fix": "Highlight the erroneous line in the editor with a red underline (like VS Code). Show a small error badge in the preview frame (\"⚠ Syntax error on line 2\"). Keep the last valid diagram visible rather than rendering the broken one.", + "principles": [] + } + ] + }, + { + "number": 3, + "slug": "case-03-multipage", + "title": null, + "subtitle": null, + "caption": null, + "gif": "ux-case-03-multipage.gif", + "gaps": [ + { + "id": "GAP-03-001", + "severity": "high", + "title": "Delete icon permanently visible on tabs — triggers on misclick", + "paragraphs": [ + "The trash icon (🗑) is always displayed on every deletable page tab. Its position directly adjacent to the \"+ Add Page\" button caused an accidental delete confirmation to fire during normal use. A destructive action sits next to a creation action with no safe gap." + ], + "evidence": null, + "fix": "Show the delete icon only on tab hover (like browser tab × buttons or VS Code editor tabs). Add at least 24px separation between the last tab's delete zone and the \"+ Add Page\" button. Consider moving delete to a right-click context menu.", + "principles": [] + }, + { + "id": "GAP-03-002", + "severity": "high", + "title": "No page rename — tabs permanently named \"Page N\"", + "paragraphs": [ + "Pages can only be named \"Page 1\", \"Page 2\", etc. Double-clicking the tab does not trigger inline rename. There is no right-click context menu, no rename icon, and no other affordance. Users building multi-diagram projects have no way to label their pages meaningfully." + ], + "evidence": null, + "fix": "Support double-click-to-rename inline (standard tab paradigm in VS Code, Figma, Notion). Alternatively show a pencil icon on active tab hover. The rename input should auto-select all text and save on Enter/blur.", + "principles": [] + }, + { + "id": "GAP-03-003", + "severity": "medium", + "title": "New page editor has no placeholder — only preview has empty state", + "paragraphs": [ + "Switching to a new empty page shows \"Click to add your first participant\" in the preview panel, but the code editor is a completely blank black area. The guidance appears in the wrong place — users need it in the editor where they'll type, not in the read-only preview." + ], + "evidence": null, + "fix": "Add a grey placeholder in the CodeMirror editor when empty: // Start typing — e.g.\\nA->B: hello(). The preview empty state can remain as a secondary cue, but the primary affordance must be in the editable area.", + "principles": [] + }, + { + "id": "GAP-03-004", + "severity": "medium", + "title": "Delete confirmation uses \"lost forever\" language with no undo", + "paragraphs": [ + "The delete dialog says \"The data on this page will be lost forever.\" There is no undo option and no way to recover the deleted page. For users who accidentally click delete (which happens easily due to GAP-03-001), their work is permanently gone." + ], + "evidence": null, + "fix": "Implement a brief undo window (10s toast with \"Undo\" button, like Gmail's delete). Alternatively, move deleted pages to a recoverable trash. At minimum, the dialog should be dismissible with the Escape key.", + "principles": [] + }, + { + "id": "GAP-03-005", + "severity": "low", + "title": "No page reordering affordance", + "paragraphs": [ + "Page tabs cannot be reordered by drag-and-drop. Users who add pages in the wrong order must delete and recreate them. No drag handles are shown, and no visual cue suggests tabs are moveable." + ], + "evidence": null, + "fix": "Add drag-and-drop reordering to page tabs (HTML5 drag API or a library like dnd-kit). Show a drag-handle cursor on tab hover to signal the affordance.", + "principles": [] + } + ] + }, + { + "number": 4, + "slug": "case-04-share-library", + "title": null, + "subtitle": null, + "caption": null, + "gif": "ux-case-04-share-library.gif", + "gaps": [ + { + "id": "GAP-04-001", + "severity": "high", + "title": "Share Link — the #1 CTA — requires login with no anonymous fallback", + "paragraphs": [ + "\"Share Link\" is the most prominent button in the app (top-right, blue, always visible). Clicking it immediately shows a login wall. There is no URL-based sharing, no public link, no embed code, no \"share without account\" path — the core sharing value proposition is completely inaccessible to new users." + ], + "evidence": null, + "fix": "Generate an anonymous share URL immediately (encode diagram in URL hash, like Mermaid Live Editor or Carbon). Gate persistent cloud links behind auth, not the act of sharing itself. The share button should always work — login extends its capabilities.", + "principles": [] + }, + { + "id": "GAP-04-002", + "severity": "high", + "title": "Same generic login modal for 3+ different auth-gated actions", + "paragraphs": [ + "The identical \"Welcome to ZenUML.com\" modal fires for PNG export, Share Link, and likely other features. No context is given about what the user was trying to do or what they'll get by signing in. Users who clicked \"Share Link\" see no mention of sharing." + ], + "evidence": null, + "fix": "Each auth prompt must state the specific value: \"Sign in to generate a persistent share link for this diagram\". Show a preview of what they'll get post-login. One generic modal for all contexts is a conversion killer.", + "principles": [] + }, + { + "id": "GAP-04-003", + "severity": "medium", + "title": "Auto-title \"Untitled 9-5-23:24\" is ambiguous and technical", + "paragraphs": [ + "New diagrams get an auto-generated title using a non-standard date-time format (M-D-HH:MM). It's unclear if \"9-5\" is May 9th or September 5th, what timezone \"23:24\" refers to, and why a timestamp is used instead of a sequential number. Compare: Google Docs uses \"Untitled document\", Figma uses \"Untitled\", VS Code uses \"Untitled-1\".", + "\"Untitled 9-5-23:24\"", + "\"Untitled diagram\" or \"My Diagram 1\"" + ], + "evidence": null, + "fix": "Use a simple sequential name (\"Untitled diagram\", \"Untitled 2\") or prompt users to name the diagram on first save. If a timestamp must be included, use ISO format with timezone: \"2026-05-09 11:24 PM\".", + "principles": [] + }, + { + "id": "GAP-04-004", + "severity": "medium", + "title": "My Library shows \"Nothing saved yet\" with no path forward", + "paragraphs": [ + "The empty state for My Library says \"Nothing saved yet.\" but gives no instructions for how to save, no \"Sign in to access your saved diagrams\" CTA, and no explanation that cloud save requires authentication. Users are left in a dead end." + ], + "evidence": null, + "fix": "Replace the empty state with an actionable message: \"Press Cmd+S to save your diagram here\" (for logged-in users) or \"Sign in to save and organize your diagrams\" with a Sign In button (for guests). Good empty states are onboarding moments.", + "principles": [] + }, + { + "id": "GAP-04-005", + "severity": "medium", + "title": "Opening My Library hides the code editor entirely", + "paragraphs": [ + "Clicking the folder icon replaces the left panel (editor) with the My Library panel. There is no way to see both the library and the editor simultaneously. Users who want to find a saved diagram and compare it to their current work must constantly toggle panels." + ], + "evidence": null, + "fix": "Implement the library as a slide-in overlay or a resizable third column — don't replace the editor. Alternatively, allow library to appear above the editor as a collapsible drawer. Notion, Linear, and Figma all allow navigation and content panels to coexist.", + "principles": [] + }, + { + "id": "GAP-04-006", + "severity": "low", + "title": "Library toolbar icons (refresh, upload, download) have no labels", + "paragraphs": [ + "Three small icon buttons appear next to \"New Folder\" in the My Library header. None have labels, visible tooltips, or accessible names. Their functions (refresh list, import, export) are non-obvious from the icons alone." + ], + "evidence": null, + "fix": "Add hover tooltips with descriptive text. Consider replacing the download/upload icons with text buttons (\"Import\", \"Export\") since the library panel has ample horizontal space.", + "principles": [] + } + ] + }, + { + "number": 5, + "slug": "case-05-shortcuts", + "title": null, + "subtitle": null, + "caption": null, + "gif": "ux-case-05-shortcuts.gif", + "gaps": [ + { + "id": "GAP-05-001", + "severity": "high", + "title": "Two separate help panels (Shortcuts + Cheat Sheet) split documentation across two unlabeled icons", + "paragraphs": [ + "The sidebar has two distinct help panels: \"Keyboard Shortcuts\" (keyboard icon) and \"Cheat sheet\" (document-info icon). Both are accessed via unlabeled icons with no hover tooltips. Users have no way to know which icon opens which panel without clicking both. Splitting help content across two unlabeled modals fragments the experience and increases cognitive load." + ], + "evidence": null, + "fix": "Merge both into a single \"Help\" panel with tabs (\"Syntax\" and \"Shortcuts\"), accessible from one labeled icon. Add hover tooltips to all sidebar icons. Figma's help panel, VS Code's keyboard shortcuts UI, and Linear's help center all follow this merged pattern.", + "principles": [] + }, + { + "id": "GAP-05-002", + "severity": "medium", + "title": "Default diagram hints at \"Cheat sheet tab\" — no such tab exists", + "paragraphs": [ + "The pre-loaded example diagram includes the comment // Go to the \"Cheat sheet\" tab or https://docs.zenuml.com. There is no visible \"Cheat sheet tab\" in the interface. Users who follow this instruction will look for a tab in the editor header (ZenUML / CSS) and find nothing. The comment refers to the unlabeled sidebar icon, which appears nowhere near a \"tab\" in the conventional sense." + ], + "evidence": null, + "fix": "Update the default example comment to accurately describe the UI: // Click the book icon (left sidebar) or press Ctrl+Shift+? for syntax help. Alternatively, rename the sidebar icon's panel to match the comment.", + "principles": [] + }, + { + "id": "GAP-05-003", + "severity": "medium", + "title": "Keyboard Shortcuts modal uses \"Emmet\" — jargon that means nothing in a diagram tool", + "paragraphs": [ + "The shortcuts modal lists Tab → Emmet code completion. Emmet is an HTML/CSS abbreviation expander with no recognized meaning in sequence diagram editing. ZenUML users (software architects, developers documenting APIs) won't know what \"Emmet completion\" means for participant/message syntax. The label misleads about what Tab actually does in this editor.", + "Tab → Emmet code completion", + "Tab → Auto-complete syntax" + ], + "evidence": null, + "fix": "Replace \"Emmet code completion\" with \"Auto-complete (syntax snippets)\" or just \"Tab completion\". If Emmet is specifically used, add a brief parenthetical: \"Emmet (HTML abbreviation expansion — rarely used in ZenUML)\".", + "principles": [] + }, + { + "id": "GAP-05-004", + "severity": "medium", + "title": "Cheat sheet is not scrollable and appears to be cut off", + "paragraphs": [ + "The Cheat Sheet modal shows: Participant, Message, Async message, Nested message, Self-message, Alt, Loop — and the modal bottom is flush with the viewport edge. Scrolling inside the modal does not reveal additional content. ZenUML supports more constructs (title, note, divider, group, stereotype, opt, par, etc.) that are entirely absent. The cheat sheet is incomplete and gives users a false sense they've seen the full syntax." + ], + "evidence": null, + "fix": "Ensure the cheat sheet is scrollable and lists all supported constructs. Add a \"Full syntax reference →\" link at the bottom pointing to docs.zenuml.com. Show a scroll indicator (shadow or gradient) at the modal bottom to signal more content exists.", + "principles": [] + }, + { + "id": "GAP-05-005", + "severity": "low", + "title": "No keyboard shortcut for diagram-level actions (Add Page, Refresh Preview uses obscure Ctrl+Shift+5)", + "paragraphs": [ + "The Global shortcuts list contains Ctrl/⌘ + Shift + 5 for \"Refresh preview\" — a non-intuitive binding (5 has no mnemonic connection to refresh). There are no shortcuts for adding a page, switching pages, or toggling panels. Power users who want keyboard-only workflows are blocked from diagram-level navigation." + ], + "evidence": null, + "fix": "Replace Ctrl+Shift+5 with Ctrl+Shift+R (mnemonic: Refresh) or F5 (conventional refresh shortcut). Add shortcuts for Add Page (Ctrl+Shift+N), next/previous page (Ctrl+Tab / Ctrl+Shift+Tab), and toggle sidebar (Ctrl+B, like VS Code).", + "principles": [] + } + ] + }, + { + "number": 6, + "slug": "case-06-present-css", + "title": null, + "subtitle": null, + "caption": null, + "gif": "ux-case-06-present-css.gif", + "gaps": [ + { + "id": "GAP-06-001", + "severity": "high", + "title": "Login modal does not trap keyboard focus — input leaks into underlying editor", + "paragraphs": [ + "When the auth modal appears (triggered by CSS tab click), keyboard focus is not constrained to the modal. Typing while the modal is visible sends keystrokes to the CodeMirror editor behind it, corrupting diagram content. Closing the modal then reveals broken ZenUML code — with no indication the editor was modified. This is an accessibility violation (WCAG 2.1 §2.1.2) and a data-integrity bug." + ], + "evidence": null, + "fix": "Implement proper focus trap in the modal: on open, move focus to the first interactive element; Tab/Shift+Tab must cycle within the modal; Escape closes and returns focus to the trigger element. Use a library like focus-trap or the native dialog element which traps focus by default.", + "principles": [] + }, + { + "id": "GAP-06-002", + "severity": "high", + "title": "CSS tab is auth-gated with no prior indication — 4th feature to use generic login modal", + "paragraphs": [ + "The CSS tab sits next to the ZenUML tab in the editor header — appearing as an equal peer, always accessible. Clicking it silently triggers the generic \"Welcome to ZenUML.com\" auth modal. No lock icon, no disabled state, no tooltip, no pricing tier badge signals that CSS customization requires an account. Users who've been working for hours on a diagram have no forewarning that switching tabs will interrupt them." + ], + "evidence": null, + "fix": "Show a lock icon or \"Pro\" badge on the CSS tab for non-authenticated users. On click, show a contextual modal: \"CSS customization is available to signed-in users. Sign in to style your diagrams with custom CSS.\" Alternatively, show a read-only CSS preview with example styles to demonstrate value before gating.", + "principles": [] + }, + { + "id": "GAP-06-003", + "severity": "medium", + "title": "Present button gives zero feedback — silently fails or opens invisible fullscreen", + "paragraphs": [ + "The \"Present\" button in the bottom bar has aria-label=\"Toggle Fullscreen\" and title=\"Toggle Fullscreen Presenting Mode\" — but the button label says only \"Present\". There is no visual feedback when clicked: no loading state, no fullscreen transition animation, no confirmation. In automation testing the button appeared completely non-functional. In real use, fullscreen mode may activate but without any transition or indicator that a mode change occurred." + ], + "evidence": null, + "fix": "Add a brief visual transition when entering fullscreen (e.g. the diagram panel expands with a smooth animation). Show a visible \"Exit Fullscreen\" (Esc) affordance once in fullscreen mode. The button title and label should match: either \"Present\" or \"Fullscreen\", not both.", + "principles": [] + }, + { + "id": "GAP-06-004", + "severity": "medium", + "title": "Privacy badge (shield icon) is invisible unless users know to hover it", + "paragraphs": [ + "A shield-with-checkmark icon sits in the top-right corner of the diagram canvas. Hovering it reveals: \"We (the vendor) do not have access to your data. The diagram is generated in this browser.\" This is a genuine trust signal — but it is entirely invisible unless users discover the icon and hover it. New users will never see this message. The privacy guarantee is the strongest selling point for enterprise use, yet it's buried in a hover tooltip on an unlabeled icon." + ], + "evidence": null, + "fix": "Surface the privacy guarantee during onboarding (first load) as a brief callout or tooltip. Add a visible text label \"Private\" or \"Local-only\" next to the shield icon. Consider adding it to the footer or \"About\" section where users who care about data privacy actively look.", + "principles": [] + }, + { + "id": "GAP-06-005", + "severity": "low", + "title": "No way to restore the default example after deleting all editor content", + "paragraphs": [ + "If a user accidentally deletes all content (or over-applies Undo and clears the editor), there is no \"Reset to example\" or \"Insert starter template\" option. The editor becomes a blank black area. The toolbar insert buttons require existing content structure to work correctly. New users who erase the example while experimenting are stuck with an empty canvas and no guidance." + ], + "evidence": null, + "fix": "Add a \"Load example\" link that appears when the editor is empty (similar to VS Code's \"Open Folder\" empty state). Alternatively, show the starter example in a ghost/placeholder style when empty. A simple \"New from template\" option in the New button dropdown would also solve this.", + "principles": [] + } + ] + }, + { + "number": 7, + "slug": "case-07-toolbar", + "title": null, + "subtitle": null, + "caption": null, + "gif": "ux-case-07-toolbar.gif", + "gaps": [ + { + "id": "GAP-07-001", + "severity": "high", + "title": "All 8 toolbar buttons have zero accessible labels — hover shows nothing", + "paragraphs": [ + "The editor toolbar contains 8 insert buttons. JavaScript DOM inspection confirms every single one has title=\"\", no aria-label, and no visible text. Hovering shows no tooltip. Screen reader users get no announcement. Sighted users must guess icon meaning from small abstract SVGs, then click to discover what gets inserted — there is no \"safe\" way to learn what a button does before committing the action." + ], + "evidence": null, + "fix": "Add title and aria-label to every button. Show hover tooltips with: (1) the button name, (2) the syntax it inserts. Example: tooltip on the message button → \"Insert synchronous message — A.method()\". This is how VS Code, GitHub's markdown toolbar, and Mermaid Live Editor handle it.", + "principles": [] + }, + { + "id": "GAP-07-002", + "severity": "medium", + "title": "Several toolbar buttons silently do nothing when editor focus is not set correctly", + "paragraphs": [ + "Clicking toolbar buttons transfers focus from the editor to the button itself, which can break the insert action — the button never receives the editor's cursor context. During testing, buttons 1, 2, 5, and 6 produced no visible change even when clicked repeatedly. There is no error message, no disabled state, no indication that a precondition was unmet. Users click, nothing happens, and they assume the button is broken." + ], + "evidence": null, + "fix": "Buttons should either: (a) always insert at end-of-file as a safe fallback regardless of cursor position, or (b) show a clear disabled/unavailable state with tooltip explaining the precondition. \"Click in the editor first, then use toolbar buttons\" is an invisible precondition users cannot be expected to know.", + "principles": [] + }, + { + "id": "GAP-07-003", + "severity": "medium", + "title": "No visual grouping — 8 buttons in a single undifferentiated row", + "paragraphs": [ + "All 8 toolbar buttons appear as an unbroken row with equal spacing. There is no separator, no group label, no visual distinction between \"add participant\", \"message types\" (sync/async/self/return), and \"control structures\" (Alt/Loop). Users who know what an \"alt block\" is must scan all 8 icons to find it. Users who don't know the terminology have no group context to guide discovery." + ], + "evidence": null, + "fix": "Add a thin 1px separator between groups: [Participant] | [→ → ← ↩ ↔] | [Alt Loop]. Add optional group labels (\"Participants\", \"Messages\", \"Blocks\") above each section, collapsible on small screens. This is standard in markdown editors, code editors, and diagram tools like draw.io.", + "principles": [] + }, + { + "id": "GAP-07-004", + "severity": "low", + "title": "Toolbar inserts placeholder names (\"message\", \"selfMessage\") that users must manually replace", + "paragraphs": [ + "When toolbar buttons insert code snippets, they use generic placeholders: result = A.message { }, selfMessage(), //Note. These are valid ZenUML syntax but use non-descriptive generic names that break real diagrams unless edited. Better tools select the placeholder text after insertion, ready for the user to immediately type the real name — no double-click-to-select needed." + ], + "evidence": null, + "fix": "After inserting a snippet, auto-select the first meaningful placeholder token (e.g. \"message\" in A.message()) so the user can immediately type the real name. VS Code snippet tab-stops (${1:placeholder}) implement this pattern well — the cursor moves through editable fields with Tab.", + "principles": [] + } + ] + }, + { + "number": 8, + "slug": "case-08-settings", + "title": null, + "subtitle": null, + "caption": null, + "gif": "ux-case-08-settings.gif", + "gaps": [ + { + "id": "GAP-08-001", + "severity": "high", + "title": "49 themes with no preview — must close modal to see effect, modal covers editor during selection", + "paragraphs": [ + "The theme dropdown lists 49 CodeMirror themes by internal code name with no color swatches, no preview panel, and no live-preview while hovering. Selecting a theme auto-saves and applies it instantly — but the settings modal sits on top of the editor, blocking the view. Users must dismiss the modal to see whether the theme looks good, then re-open settings to change it again. With 49 options, iterating through themes takes significant effort." + ], + "evidence": null, + "fix": "Show a small color swatch next to each theme name (background + text + keyword colors). Or add a live mini-preview panel in the modal showing \"A→B: hello()\" in the selected theme. VS Code, JetBrains, and GitHub Codespaces all show real-time theme previews. The infrastructure already supports instant switching — the only missing piece is a visible preview.", + "principles": [] + }, + { + "id": "GAP-08-002", + "severity": "medium", + "title": "Settings modal titled \"Editor\" — gear icon conventionally means \"Settings\", not editor-specific config", + "paragraphs": [ + "The gear icon at the bottom of the sidebar universally signals \"Settings\" or \"Preferences\" in software UI (VS Code, Figma, Linear, Notion, GitHub). Opening it reveals a modal titled \"Editor\" — the scope is narrower than the affordance implies. Users who expect app-level settings (account, keyboard shortcuts, diagram defaults, data storage location) find only editor appearance options. There is no indication that more settings exist elsewhere.", + "Modal title: \"Editor\" (3 appearance settings + 4 toggles)", + "Modal title: \"Settings\" with \"Editor\" as a section header; room for future \"Diagram\", \"Account\" sections" + ], + "evidence": null, + "fix": "Rename modal to \"Settings\" or \"Preferences\". Promote current options under an \"Editor Appearance\" section header. This leaves room to add \"Diagram defaults\" (background color, watermark, actor style) and \"Account\" sections as features grow — without needing a new mental model for users.", + "principles": [] + }, + { + "id": "GAP-08-003", + "severity": "medium", + "title": "\"Preserve console logs\" debug toggle exposed to all users at the same level as user-facing settings", + "paragraphs": [ + "The \"Others\" section contains four toggles: Line wrap, Auto-preview, Preserve last written code, and Preserve console logs. The first three are clearly user-facing features. \"Preserve console logs\" is a developer debugging option — it retains browser console output across sessions, which has no meaning to the target audience of sequence diagram creators (architects, product managers, technical writers). It is displayed at identical prominence to Line wrap, with no explanation of what it does or who it's for." + ], + "evidence": null, + "fix": "Move \"Preserve console logs\" to a collapsible \"Advanced / Developer\" section, or remove it from the UI entirely if it was added only for debugging. If it must remain, add a tooltip: \"Keep browser console output across page reloads — for developers debugging ZenUML integration.\" Exposing internal debugging toggles to end users adds noise and erodes trust in the product's polish.", + "principles": [] + }, + { + "id": "GAP-08-004", + "severity": "medium", + "title": "No \"Reset to defaults\" or Cancel — settings are permanent with no undo path", + "paragraphs": [ + "Every setting change shows a \"Setting saved\" toast and immediately persists. The modal has only a close (×) button — no Cancel, no Reset, no Undo. If a user accidentally selects a hard-to-read theme (e.g. colorforth uses black background with dim colored text) and doesn't remember what the previous theme was called, they have no recovery path except scrolling through all 49 options to find something acceptable. The \"Setting saved\" toast actually makes this worse — it emphasizes irreversibility." + ], + "evidence": null, + "fix": "Add a \"Reset to defaults\" link at the bottom of the modal. Store the last-applied setting before a change (one level of undo). Alternatively, add a \"Cancel\" button that reverts to the state when the modal was opened. VS Code's settings undo (Ctrl+Z works in settings) and macOS System Settings' \"Revert\" button are good models. A single \"Reset to defaults\" covers the most common recovery scenario.", + "principles": [] + }, + { + "id": "GAP-08-005", + "severity": "low", + "title": "Font size range capped at 12–18px — excludes accessibility users and large-display power users", + "paragraphs": [ + "The Font Size dropdown offers 7 fixed values: 12 px, 13 px, 14 px (default), 15 px, 16 px, 17 px, 18 px. The cap at 18px does not accommodate users with low vision who rely on larger text, nor power users on ultra-high-DPI displays who prefer 20–24px in code editors. Conversely, 12px is below the WCAG minimum of 18pt (24px) for normal text — it may fail accessibility requirements for users who expect accessible code editors." + ], + "evidence": null, + "fix": "Extend the range to at least 10–28px, or replace the fixed dropdown with a numeric input with +/- stepper (min: 10, max: 32). VS Code allows custom font size via settings JSON with no cap. A reasonable UI range of 10–28px with 1px steps covers virtually all real-world needs. Consider also inheriting the browser's base font-size as the default rather than hardcoding 14px.", + "principles": [] + } + ] + }, + { + "number": 9, + "slug": "case-09-canvas", + "title": null, + "subtitle": null, + "caption": null, + "gif": "ux-case-09-canvas.gif", + "gaps": [ + { + "id": "GAP-09-001", + "severity": "high", + "title": "Scroll on canvas scrolls the page — not zoom — violating the diagram tool genre convention", + "paragraphs": [ + "Scrolling the mouse wheel on the diagram canvas moves the page up/down. It does not zoom the diagram. This is the single most-violated interaction expectation in diagram tools: every major web-based diagram application (Figma, draw.io, Mermaid Live, Lucidchart, Excalidraw) uses scroll-to-zoom as the primary zoom gesture. Users who expect this behavior will scroll, see the page shift, and conclude either the diagram is broken or zooming is impossible — before ever discovering the tiny ⊕/⊖ icon buttons in the footer bar." + ], + "evidence": null, + "fix": "Intercept scroll events on the diagram canvas and use them to zoom the diagram (same as Figma, draw.io). Use event.preventDefault() on the iframe's wheel event, then apply the zoom delta. Add Ctrl+scroll as an explicit zoom modifier for users on systems where unmodified scroll is reserved for page navigation. Show a brief \"Scroll to zoom\" tooltip on first canvas hover to set expectations.", + "principles": [] + }, + { + "id": "GAP-09-002", + "severity": "high", + "title": "No drag-to-pan — diagram cannot be repositioned after zooming or on small viewports", + "paragraphs": [ + "Dragging the mouse on the diagram canvas has no effect — it does not pan the diagram. Once a user zooms in (via the footer ⊕ button), there is no way to scroll/pan to different parts of the diagram. The diagram is effectively locked in its rendered position. On displays smaller than ~900px height, the bottom of longer diagrams is clipped and inaccessible without scrolling the outer page (which moves away from the toolbar, not the diagram). Panning is the expected complement to zoom in every diagram and image tool." + ], + "evidence": null, + "fix": "Implement drag-to-pan: on mousedown in the canvas, switch cursor to grabbing, track mouse delta, and translate the diagram SVG. Release on mouseup. Space+drag (as in Figma) is an acceptable alternative if direct drag conflicts with future element selection. Add a \"Fit diagram\" button (the ⊡ icon used by draw.io, Mermaid, Excalidraw) next to the zoom controls to instantly reset position and scale to show the full diagram.", + "principles": [] + }, + { + "id": "GAP-09-003", + "severity": "medium", + "title": "(i) info icon opens a full-screen syntax overlay — the 3rd separate help surface, covering the diagram entirely", + "paragraphs": [ + "The (i) icon in the canvas footer opens a \"ZenUML Tips\" panel that renders as a full-width, full-height overlay on top of the diagram canvas. This is the third distinct help surface in the app (alongside the keyboard-shortcut icon in the sidebar and the \"quick reference\" cheat-sheet icon). The Tips panel covers the entire diagram — users cannot read a syntax example and see its rendered result simultaneously. Additionally, the icon has no hover tooltip explaining what it opens, so users cannot know what action they are triggering before clicking." + ], + "evidence": null, + "fix": "Replace the full-screen overlay with a slide-in side panel or a resizable drawer that appears alongside (not over) the diagram. This allows users to read syntax examples while seeing their diagram update in real time — the key learning loop for new users. Consolidate all three help surfaces into one unified \"Help\" panel (see GAP-05-001). Add a tooltip to the (i) icon: \"Syntax tips & reference\".", + "principles": [] + }, + { + "id": "GAP-09-004", + "severity": "medium", + "title": "Canvas footer controls have zero tooltips or labels — \"1.2.3\" checkbox function is completely opaque", + "paragraphs": [ + "The canvas footer contains 6 controls: (i), checkbox labeled \"1.2.3\", ⊕, 100%, ⊖, and \"ZenUML.com\". None have hover tooltips, title attributes, or aria-labels. The most opaque is the \"1.2.3\" checkbox: its label literally shows the sequence number format — not what checking/unchecking does. Testing confirmed it toggles sequence number visibility on the diagram, but users must experiment to discover this. The \"ZenUML.com\" text in the footer is equally unclear — it appears to be a branding watermark but looks like a dead link." + ], + "evidence": null, + "fix": "Add title and aria-label to every footer control: (i) → \"Syntax tips\", 1.2.3 checkbox → \"Show sequence numbers\", ⊕ → \"Zoom in\", 100% → \"Reset zoom to 100%\", ⊖ → \"Zoom out\", ZenUML.com → \"Powered by ZenUML\" (or remove if it adds no value). Show these as hover tooltips. Rename the checkbox label from \"1.2.3\" to \"Numbers\" or add a \"Show numbers\" tooltip.", + "principles": [] + }, + { + "id": "GAP-09-005", + "severity": "low", + "title": "No \"Fit to screen\" button — no way to reset diagram position after zooming or viewport changes", + "paragraphs": [ + "After zooming in via the ⊕ button, there is no \"fit diagram\" or \"reset view\" shortcut to quickly return to a state where the full diagram is visible. The 100% label resets zoom to 100% but doesn't re-center the diagram. On smaller viewports, the diagram may be partially scrolled out of view and there is no keyboard shortcut or button to snap back to \"show full diagram\". Figma (Shift+1), draw.io (Ctrl+Shift+H), Mermaid Live, and Excalidraw all have a \"fit\" control." + ], + "evidence": null, + "fix": "Add a \"Fit diagram\" button (⊡ icon, standard in diagram tools) next to the zoom controls. Clicking it should set zoom to show the complete diagram with a small margin, and center it in the viewport. Also add a keyboard shortcut: Ctrl+Shift+F or F (when canvas is focused) as the \"fit\" binding, consistent with Figma (Shift+1) and Excalidraw (Shift+1).", + "principles": [] + } + ] + }, + { + "number": 10, + "slug": "case-10-new-diagram", + "title": null, + "subtitle": null, + "caption": null, + "gif": "ux-case-10-new-diagram.gif", + "gaps": [ + { + "id": "GAP-10-001", + "severity": "high", + "title": "\"Start a blank creation\" loads a complex 18-line pre-filled example, not a blank editor", + "paragraphs": [ + "The most prominent call-to-action in the new-diagram modal is labeled \"Start a blank creation\" with an outlined button style that implies a primary action. Clicking it loads a pre-written example: BookLibService calling Session.findBooks() and BookRepository with try/catch/finally — 18 lines of code involving 4 participants. A new user who wants a blank canvas is immediately confronted with unfamiliar example code they must manually delete before starting their own work." + ], + "evidence": null, + "fix": "Either: (a) rename the button to \"Start from example\" to accurately describe what happens, or (b) actually deliver a blank editor with a single blank line when this button is clicked. If an example is pedagogically valuable, offer it as a named template (\"Library System Example\") alongside the truly blank option. The distinction matters — a user starting their own diagram should not be burdened with deleting someone else's work.", + "principles": [] + }, + { + "id": "GAP-10-002", + "severity": "high", + "title": "Template thumbnails show style icons only — no content preview before committing", + "paragraphs": [ + "The 4 templates (Basic, Black & White, Blue, StarUML) show only a small icon representing the visual style (color scheme, line thickness). There is no way to preview the content of each template before clicking. Testing revealed that \"Blue\" is actually an internal template named \"Advanced\" — a multi-participant order management system with OrderService, PaymentService, InventoryService. Users who pick \"Blue\" because they want a blue color scheme get a complex business flow they didn't expect. Template selection is a blind commitment." + ], + "evidence": null, + "fix": "Show a hover preview panel (right side of modal) with the rendered diagram when hovering a template. Display: (1) the template's internal name (\"Advanced\"), (2) a small diagram thumbnail, (3) participant count and use-case description. Figma, Notion, and Miro all show content previews before template commitment. At minimum, surface the internal name alongside the color name so users know what content they're getting.", + "principles": [] + }, + { + "id": "GAP-10-003", + "severity": "high", + "title": "No save warning before replacing current diagram — unsaved work is silently discarded", + "paragraphs": [ + "Selecting any template or \"Start a blank creation\" immediately replaces the current diagram without checking for unsaved changes. During testing, the active editor contained the \"(Forked) Advanced\" diagram — work that had just been created. Clicking \"Start a blank creation\" discarded it instantly, with no confirmation dialog, no \"Save changes?\" prompt, and no undo path back to the previous state. The only indication anything happened was a toast: \"New item created.\" Any user who clicked \"+ New\" by accident or curiosity loses their work permanently." + ], + "evidence": null, + "fix": "Before replacing the current diagram, check if there are unsaved changes. If yes, show a confirmation: \"You have unsaved changes in [tab name]. Discard and create new diagram?\" with Cancel and Discard buttons. This is standard in every code editor, document editor, and diagramming tool (Figma, Lucidchart, draw.io). The behavior should also be reversible via Undo (Cmd+Z) even if no modal is shown.", + "principles": [] + }, + { + "id": "GAP-10-004", + "severity": "medium", + "title": "Toast message \"was forked\" uses developer jargon opaque to end users", + "paragraphs": [ + "When a template is selected, the toast notification reads: \"Advanced was forked\". The term \"forked\" is borrowed from software version control (git fork) and means nothing to non-developer users. A marketing designer, business analyst, or student would not know what \"forked\" means in this context — does it mean copied, modified, saved, broken? Even technical users might expect \"forked\" to mean the template remains linked to an upstream source they can pull updates from (as in GitHub forks). The term is misleading and alienating to a broad user base." + ], + "evidence": null, + "fix": "Replace \"was forked\" with plain language: \"Advanced template opened as a new diagram\" or simply \"New diagram created from Advanced template.\" The word \"forked\" should never appear in user-facing UI — it belongs in developer console logs or commit messages, not product notifications.", + "principles": [] + }, + { + "id": "GAP-10-005", + "severity": "medium", + "title": "Marketing tweet solicitation embedded inside the creation modal — wrong context", + "paragraphs": [ + "The new-diagram modal contains a social media call-to-action asking users to tweet about ZenUML. This appears directly alongside the template selection UI, competing for visual attention in a context where the user has a specific goal: create a new diagram. Embedding marketing asks inside task-completion flows violates the single-responsibility principle of UI screens and creates cognitive friction at the worst possible moment — when the user is about to start working." + ], + "evidence": null, + "fix": "Move social media asks to post-action moments (after export, after sharing), to onboarding completion, or to an \"About\" / settings page. Never interrupt a task-starting flow with marketing. If tweet solicitation is required, make it a dismissable banner elsewhere in the UI — not inside a creation dialog that should stay focused on one job: helping users start a new diagram quickly.", + "principles": [] + }, + { + "id": "GAP-10-006", + "severity": "low", + "title": "Only 4 style-variant templates — no use-case or domain-specific templates", + "paragraphs": [ + "The template library offers 4 options: Basic, Black & White, Blue, StarUML. All 4 appear to be style variants of the same or similar content — they differentiate by color scheme and visual style, not by use case or domain. There are no domain-specific starter templates for common sequence diagram scenarios: API authentication flow, microservices communication, e-commerce checkout, CI/CD pipeline, user login sequence, WebSocket handshake. These are the diagrams developers actually draw most often." + ], + "evidence": null, + "fix": "Add 6–10 use-case templates covering common developer scenarios: API auth (OAuth2/JWT), REST CRUD, WebSocket handshake, login flow, microservice call chain, database transaction. Label them by use case, not by visual style. Style selection can be a separate step or theme preference. Notion, Confluence, and Miro all provide domain-specific templates as a primary discovery mechanism for new users.", + "principles": [] + } + ] + }, + { + "number": 11, + "slug": "case-11-undo-redo", + "title": null, + "subtitle": null, + "caption": null, + "gif": "ux-case-11-undo-redo.gif", + "gaps": [ + { + "id": "GAP-11-001", + "severity": "high", + "title": "Global undo stack crosses page boundaries — Cmd+Z on Page 1 undoes Page 2 edits", + "paragraphs": [ + "ZenUML uses a single CodeMirror editor instance shared across all pages. Switching pages replaces the editor content but does not reset or checkpoint the undo history. As a result, pressing Cmd+Z on Page 1 after having edited Page 2 undoes Page 2's changes while the UI shows Page 1 as selected. A second Cmd+Z empties the editor entirely. The diagram canvas continues rendering stale Page 2 content, creating a split-brain state where the editor and diagram are out of sync. This is a data corruption bug — a user can silently overwrite or erase another page's content simply by pressing Undo on their current page." + ], + "evidence": null, + "fix": "Each page's CodeMirror instance must maintain an independent undo history. Either: (a) create separate CodeMirror instances per page (one per tab), or (b) save and restore the full history state when switching between pages. VS Code, Notion, and Obsidian all maintain per-document undo histories — switching tabs never cross-contaminates undo stacks. Fix requires storing the full cm.historySize() state per page on tab switch.", + "principles": [] + }, + { + "id": "GAP-11-002", + "severity": "high", + "title": "No warning before Cmd+Z silently erases entire page content", + "paragraphs": [ + "When the undo history reaches the initial state (before any content was entered), a single Cmd+Z can clear the entire editor to an empty state. In testing: starting from \"A → B: hello\" (2 lines), one Cmd+Z jumped back to the 18-line example, and one more cleared everything. There is no safeguard, no \"Undo will erase all content on this page — continue?\" confirmation, no count of how many undos remain before the page goes blank. Silent erasure of all work with no recovery path is one of the most damaging editor UX failures." + ], + "evidence": null, + "fix": "Add a visual undo depth indicator (small counter near the editor, e.g. \"⌘Z · 3 steps\") so users know how close they are to the bottom of the stack. When the next Cmd+Z would clear all content, show a brief inline warning: \"Press again to clear all content (no more undo history).\" Sublime Text and VS Code both show \"Can't undo further\" as a status bar message when the stack is exhausted. Alternatively, never allow undo to go past the initial load state of a page.", + "principles": [] + }, + { + "id": "GAP-11-003", + "severity": "medium", + "title": "Toolbar inserts are not atomic undo units — require character-by-character reversal", + "paragraphs": [ + "When the toolbar \"Participant\" button inserts NewParticipant (13 characters), pressing Cmd+Z once removes the entire token — that part works. However, the newline inserted by the toolbar to separate the token from surrounding content requires an additional Cmd+Z, making the complete reversal of one toolbar action require 2+ undo steps. More critically: clicking a toolbar button first steals focus from the editor, meaning the user must re-click the editor before pressing Cmd+Z — or the undo may fire on the wrong context. The disconnect between toolbar action and undo granularity violates user mental models where \"one action = one undo step.\"" + ], + "evidence": null, + "fix": "Wrap each toolbar button click into a single CodeMirror transaction (cm.operation()) that groups all text insertions and cursor movements into one atomic undo entry. Ensure focus returns to the editor before the transaction is committed so Cmd+Z immediately after toolbar use reverses exactly the insert. GitHub's markdown toolbar and VS Code's snippet insertions are always single-undo-step operations.", + "principles": [] + }, + { + "id": "GAP-11-004", + "severity": "medium", + "title": "~2 second diagram re-render delay creates a decoupled undo experience", + "paragraphs": [ + "After pressing Cmd+Z, the editor content updates immediately (correct behavior). However, the diagram canvas takes approximately 2 seconds to re-render the undone state. During this window, the editor shows the undone text but the diagram still shows the superseded content — creating a contradictory view where both panels are simultaneously \"right\" but at different points in time. Users who glance at the diagram after pressing Cmd+Z will see stale content and may press Cmd+Z again, overshooting their intended undo target." + ], + "evidence": null, + "fix": "Show an explicit loading/re-rendering indicator on the diagram canvas during the 2-second render delay — a subtle spinner, pulsing border, or \"Rendering…\" label. This tells users \"the diagram is catching up\" rather than leaving them in a false steady state. Consider debounce optimization: if the re-render typically takes 500ms for small edits, the 2s delay for a 1-line undo suggests the full pipeline is being re-triggered rather than a diff-based update.", + "principles": [] + }, + { + "id": "GAP-11-005", + "severity": "low", + "title": "Redo shortcut (Cmd+Shift+Z) is undocumented — not listed in cheat sheet or any visible help", + "paragraphs": [ + "Redo works via Cmd+Shift+Z, which was confirmed in testing. However, this shortcut appears nowhere in the visible UI — not in the keyboard shortcuts panel, not in the cheat sheet, not in any tooltip. Users who know Cmd+Z for undo will typically try Cmd+Shift+Z (macOS standard) or Cmd+Y (Windows standard) for redo. The ZenUML cheat sheet panel lists multiple shortcuts but omits undo/redo entirely. A user who accidentally over-undoes cannot recover their work unless they already know the platform-specific redo shortcut from external knowledge." + ], + "evidence": null, + "fix": "Add Cmd+Z (undo) and Cmd+Shift+Z (redo) to the keyboard shortcuts cheat sheet panel. Add tooltips to any undo/redo buttons if they exist (currently none are visible in the toolbar). Even a one-line mention in the \"Help\" sidebar would dramatically reduce the chance of users losing work by not knowing redo is available.", + "principles": [] + } + ] + }, + { + "number": 12, + "slug": "case-12-syntax-errors", + "title": null, + "subtitle": null, + "caption": null, + "gif": "ux-case-12-syntax-errors.gif", + "gaps": [ + { + "id": "GAP-12-001", + "severity": "high", + "title": "No error message in the diagram panel — invalid syntax renders a silent partial or empty diagram", + "paragraphs": [ + "When the ZenUML parser encounters invalid syntax, the diagram canvas does not show an error state. For mid-document errors (e.g. @@@ bad token @@@ on line 2), the canvas renders only the valid content before the error — showing a partial diagram with no indication it is incomplete. For fully unparseable input (e.g. !!!BROKEN!!!SYNTAX!!!), the canvas renders a lone participant icon — which looks identical to an intentionally empty diagram. There is no \"Syntax error\" banner, no error count, no line reference, no red border or warning color on the canvas. Users believe their diagram is correct when it is not." + ], + "evidence": null, + "fix": "When the parser produces an error, the diagram canvas should show a visible error state: a red/orange border or banner reading \"Syntax error on line N — diagram may be incomplete.\" Include the error line number and a brief description. Mermaid Live Editor, PlantUML server, and Kroki all show explicit error panels. Even a simple \"⚠ Parse error\" badge on the canvas would be a major improvement over the current silent behavior.", + "principles": [] + }, + { + "id": "GAP-12-002", + "severity": "high", + "title": "No editor gutter indicators — error lines have no squiggly underline, no line number icon, no hover tooltip", + "paragraphs": [ + "The CodeMirror editor has no error annotations for invalid ZenUML syntax. Lines with parse errors get a faint pink syntax-highlighting tint (inconsistently — some invalid inputs show no highlighting at all), but there is no red squiggly underline, no gutter icon (🔴 or ⚠), and no hover tooltip explaining what is wrong with the line. A user looking at @@@ bad token here @@@ on line 2 sees only a differently-colored line with no explanation. They cannot tell whether the color means \"this is a comment\", \"this is a string\", \"this is a keyword\", or \"this is an error.\"" + ], + "evidence": null, + "fix": "Register CodeMirror linting annotations for the ANTLR parser's error output. On parse error, mark the offending token(s) with: (1) a red wavy underline on the token, (2) a red circle icon in the gutter at the error line number, (3) a hover tooltip with the parser error message. VS Code, Monaco Editor, and CodeMirror 6 all provide linting APIs for exactly this. The fix is a CodeMirror linter plugin that feeds ANTLR error messages back into the editor.", + "principles": [] + }, + { + "id": "GAP-12-003", + "severity": "medium", + "title": "Parser silently truncates the diagram at the first error — valid content below the error is dropped without warning", + "paragraphs": [ + "When an error appears on line N of a multi-line diagram, the ZenUML parser halts and renders only lines 1 through N-1. Lines N+1 onward are silently discarded — they never appear in the diagram and produce no warning. In testing with a 3-line document (valid / invalid / valid), only line 1 appeared in the diagram. Line 3's C → D: world was completely dropped. A user who has a long diagram and accidentally introduces an error mid-document will see half their diagram disappear — with no indication of where the cutoff happened or how many lines were skipped." + ], + "evidence": null, + "fix": "Either: (a) implement error recovery in the parser so it skips the invalid line and continues rendering the rest of the document (best experience — user sees all valid parts), or (b) show a count of dropped lines at the bottom of the diagram: \"⚠ 2 lines skipped due to errors — see line 2.\" Option (a) is used by Mermaid (renders what it can) and is strongly preferred over silent truncation. Option (b) at minimum prevents users from discovering missing content only after export.", + "principles": [] + }, + { + "id": "GAP-12-004", + "severity": "medium", + "title": "Syntax highlighting color alone signals errors — non-compliant with WCAG 1.4.1 (color not sole differentiator)", + "paragraphs": [ + "The only error signal in the editor is a faint pink/magenta tint on lines that contain invalid tokens. This coloring: (1) is inconsistent — completely garbage input (!!!BROKEN!!!) receives no tinting at all, (2) is indistinguishable from intentional syntax coloring for strings or keywords to a colorblind user, (3) carries no semantic label — hovering the colored line reveals nothing, and (4) violates WCAG 1.4.1, which requires that color not be the sole means of conveying information. A red-blind user sees the pink-tinted lines as identical to normal code lines." + ], + "evidence": null, + "fix": "Pair any color-based error indication with a non-color signal: a gutter icon, an underline style, a tooltip, or a text label. The WCAG test is simple: \"if I remove all color from this screenshot, can I still tell which lines have errors?\" Currently the answer is no. Adding a ⚠ glyph in the gutter satisfies both the WCAG requirement and the UX requirement simultaneously.", + "principles": [] + }, + { + "id": "GAP-12-005", + "severity": "low", + "title": "Auto-bracket completion silently masks some syntax errors that the user intended to observe", + "paragraphs": [ + "The editor auto-closes { to {} and ( to () immediately on typing. While this is ergonomic for normal use, it means a user who types an intentionally partial token to test what the diagram does (e.g. typing A.method( then pausing to look at the diagram) sees an auto-corrected valid result rather than an error state. Developers debugging ZenUML syntax are robbed of the ability to observe incremental parse states. Additionally, auto-completion that fires immediately with no opt-out can be disruptive for users who intended to type a different character." + ], + "evidence": null, + "fix": "Add a settings option to disable auto-bracket completion (already logged as part of Case 08 settings gaps). More importantly, implement a brief delay before auto-completing (e.g. 300ms) so users can type the opening bracket, glance at the diagram, and continue — rather than having the completion fire before they've observed the state. This is how VS Code, JetBrains IDEs, and CodeMirror 6 implement it.", + "principles": [] + } + ] + }, + { + "number": 13, + "slug": "case-13-rename", + "title": null, + "subtitle": null, + "caption": null, + "gif": "ux-case-13-rename.gif", + "gaps": [ + { + "id": "GAP-13-001", + "severity": "high", + "title": "Page tabs cannot be renamed — no UI affordance exists for page-level naming", + "paragraphs": [ + "Multi-page ZenUML diagrams permanently label their pages \"Page 1\", \"Page 2\", etc. with no way to rename them. Testing exhausted all standard interactions: double-click (nothing), right-click (browser toolbar, no app menu), DOM inspection (plain buttons, no contentEditable or event handlers). Users who use pages to separate diagram sections — e.g. \"Login Flow\", \"Checkout Flow\", \"Error Handling\" — must use workarounds: reading page content to identify them, or maintaining a mental map of which number corresponds to which concept. Figma, Notion, and every multi-page tool that exists supports page renaming as a core feature." + ], + "evidence": null, + "fix": "Add page rename via double-click on the tab text (universal standard). Implement an inline text input that appears on double-click, pre-selected with the current name, confirmed by Enter and cancelled by Escape. Also add a right-click context menu with \"Rename\", \"Delete\", \"Duplicate\" options for discoverability. Tab names should persist to localStorage and cloud saves. The fix is a contentEditable span or input overlay on the button — a standard pattern used by Chrome, VS Code, Notion, and Figma.", + "principles": [] + }, + { + "id": "GAP-13-002", + "severity": "high", + "title": "Diagram title rename is hidden behind a hover-only pencil icon — clicking the title text does nothing", + "paragraphs": [ + "The diagram title rename workflow requires: (1) hovering over the title in the header to reveal the pencil icon, (2) clicking specifically on that ~16×16px icon — clicking the title text itself has no effect. During testing, clicking the title area without perfect icon targeting does nothing, providing no feedback that an editable field exists. Users who don't hover precisely enough never see the pencil. Users who click the title text expect it to become editable (standard in tools like Notion, Figma, Google Docs). The current UX forces pixel-precision discovery of a hidden affordance." + ], + "evidence": null, + "fix": "Make the entire title text area clickable to activate rename (not just the pencil icon). Show the pencil icon persistently (not only on hover) or replace it with a visible edit indicator. A subtitle label \"Click to rename\" or a subtle underline on the title signals editability without adding UI clutter. This is how Google Docs (\"Click to add a title\"), Figma, Notion, and Obsidian handle document title editing.", + "principles": [] + }, + { + "id": "GAP-13-003", + "severity": "medium", + "title": "Long titles display only the tail — \"…overflow or truncation issues\" not \"A very long diagram…\"", + "paragraphs": [ + "When a diagram title exceeds the header width, the field scrolls to show the end of the string rather than the beginning. The result is a header displaying \"low or truncation issues\" — the final words of a 107-character title — with no indication that the title starts with different text, and no ellipsis or fade. Users see a fragment that looks like an incomplete or corrupted title. Industry standard for overflowing titles is to show the beginning with a trailing ellipsis (\"A very long diagram t…\") so the identifying start is always visible. Additionally, hovering the title shows no tooltip with the full name." + ], + "evidence": null, + "fix": "When the title overflows in display mode, truncate at the end with an ellipsis: text-overflow: ellipsis; overflow: hidden; white-space: nowrap. Add title attribute with full text for hover tooltip. In edit mode, scrolling the input to the right is acceptable — but display mode must always show the start. Set a soft character limit (e.g. 60 chars) with a counter during editing, similar to GitHub repo descriptions.", + "principles": [] + }, + { + "id": "GAP-13-004", + "severity": "medium", + "title": "Empty title is accepted — clearing the name and pressing Enter produces a blank untitled diagram", + "paragraphs": [ + "Deleting all characters from the title field and pressing Enter commits the empty string as the diagram name. The header shows a bare empty input box — visually indistinguishable from the rename-mode state — with no placeholder, no fallback name, and no error. Pressing Escape correctly restores the previous title, but this recovery path requires the user to know to press Escape before clicking elsewhere (clicking elsewhere while empty does not restore). An empty diagram name breaks library display, share links, and any UI surface that references the diagram by name." + ], + "evidence": null, + "fix": "Validate the title field on submit: if the field is empty or whitespace-only, either (a) restore the previous title and show a brief inline message \"Diagram title cannot be empty\", or (b) replace with a default like \"Untitled Diagram\". Show a character counter and placeholder text (\"Enter diagram name…\") inside the field during editing. This is standard in Google Docs (auto-restores \"Untitled document\"), Figma (reverts on blur if empty), and Notion.", + "principles": [] + }, + { + "id": "GAP-13-005", + "severity": "low", + "title": "Auto-generated names (\"Untitled 10-5-4:8\") are machine-readable timestamps, not human-readable defaults", + "paragraphs": [ + "New diagrams receive names like \"Untitled 10-5-4:8\" — which encodes a date/index in a format that is neither a natural language name nor a recognizable date format. Users who create multiple diagrams without renaming them see a list of cryptic timestamps in their Library. The pattern is not explained anywhere in the UI. \"Untitled\" is a clear and universal fallback; \"Untitled 10-5-4:8\" is confusing because the numbers suggest meaning but require decoding. Compare: GitHub creates repos named after your username; Notion creates \"Untitled\"; Figma creates \"Untitled\"." + ], + "evidence": null, + "fix": "Default new diagram names to \"Untitled\" or \"New Diagram\" — simple, human-readable, internationally recognizable. If deduplication is needed, use \"Untitled (2)\", \"Untitled (3)\". Avoid exposing timestamps or internal counters in user-facing names. The rename prompt (or title click-to-edit) can appear automatically on first save to encourage meaningful names, similar to how Figma prompts \"Give your frame a name\" after creating from a template.", + "principles": [] + } + ] + }, + { + "number": 14, + "slug": "case-14-sidebar-nav", + "title": null, + "subtitle": "6 icons, 3 interaction patterns, 0 aria-labels — and one panel that can't be closed", + "caption": null, + "gif": "ux-case-14-sidebar-nav.gif", + "gaps": [ + { + "id": "GAP-14-001", + "severity": "high", + "title": "All 6 sidebar icons missing aria-label — screen reader announces ligature text", + "paragraphs": [ + "Every sidebar button relies solely on the HTML title attribute for labeling. The button text content is the raw Material Icons ligature string (folder_open, code_blocks, quick_reference, etc.). A screen reader announces these strings verbatim, providing zero semantic value." + ], + "evidence": "
DOM evidence — all 6 icons", + "fix": null, + "principles": [ + "WCAG 4.1.2 Name, Role, Value (Level A)", + "Nielsen #4: Consistency & Standards", + "Touch accessibility" + ] + }, + { + "id": "GAP-14-002", + "severity": "high", + "title": "Library panel cannot be dismissed — Code Editor icon inert when Library is open", + "paragraphs": [ + "Once \"My Library\" is opened, the user has no working path back to the Code Editor:" + ], + "evidence": "
Verified interactions — all fail to close Library", + "fix": null, + "principles": [ + "Nielsen #3: User Control & Freedom", + "Nielsen #1: Visibility of System Status", + "Interaction trap / dead end" + ] + }, + { + "id": "GAP-14-003", + "severity": "medium", + "title": "540px dead zone splits sidebar into two visually disconnected icon clusters", + "paragraphs": [ + "The 6 sidebar icons are distributed in two polarised groups with no visual grouping, separator, or label to explain the split:" + ], + "evidence": "
y-positions from getBoundingClientRect()", + "fix": null, + "principles": [ + "Gestalt: Proximity", + "Nielsen #4: Consistency & Standards", + "Responsive layout" + ] + }, + { + "id": "GAP-14-004", + "severity": "medium", + "title": "Same icon strip, three different interaction patterns with no visual differentiation", + "paragraphs": [ + "The 6 icons look identical in style but trigger fundamentally different interaction types, with no affordance distinguishing them:", + "Users cannot predict the outcome of clicking any icon. An external link inside an icon-only sidebar is a particularly dangerous pattern — the user loses context with no warning." + ], + "evidence": null, + "fix": null, + "principles": [ + "Nielsen #4: Consistency & Standards", + "Nielsen #1: Visibility of System Status", + "Principle of Least Surprise" + ] + }, + { + "id": "GAP-14-005", + "severity": "low", + "title": "Typo in Cheatsheet: \"Asyc message\" and naming inconsistency \"Cheatsheet\" vs \"Cheat sheet\"", + "paragraphs": [ + "Minor copy quality issues in the help content:" + ], + "evidence": null, + "fix": null, + "principles": [ + "Nielsen #4: Consistency & Standards", + "Copy quality" + ] + } + ] + }, + { + "number": 15, + "slug": "case-15-export-clipboard", + "title": null, + "subtitle": "Both export buttons gated behind login with no explanation — and zero anonymous export path exists", + "caption": null, + "gif": "ux-case-15-export-clipboard.gif", + "gaps": [ + { + "id": "GAP-15-001", + "severity": "high", + "title": "Export buttons show generic auth modal with no context — user doesn't know why or what they'll get", + "paragraphs": [ + "Clicking \"Export as PNG\" or \"Copy PNG to Clipboard\" (both visible in the bottom bar) immediately shows the \"Welcome to ZenUML.com\" login modal. The modal provides zero context:" + ], + "evidence": "
Auth modal observed after clicking either export button", + "fix": null, + "principles": [ + "Nielsen #1: Visibility of System Status", + "Nielsen #6: Recognition over Recall", + "Growth: Explain the value before the gate" + ] + }, + { + "id": "GAP-15-002", + "severity": "high", + "title": "Zero anonymous export path — user who built a diagram cannot get it out without creating an account", + "paragraphs": [ + "A new user can open the app, build a complete sequence diagram, and have no way to export or share it without creating an account. There is no free export tier whatsoever:", + "This creates a high-friction drop-off point for the most natural first action after building a diagram. Industry standard is at least one free export format (Mermaid Live → SVG free, PlantUML → PNG free, Excalidraw → PNG/SVG free)." + ], + "evidence": "
Competitor comparison — anonymous export availability", + "fix": null, + "principles": [ + "Nielsen #3: User Control & Freedom", + "Freemium: Value before the gate", + "Conversion funnel: Export is the aha-moment" + ] + }, + { + "id": "GAP-15-003", + "severity": "medium", + "title": "No SVG export option — only PNG, and PNG is locked", + "paragraphs": [ + "The app offers only two export actions (PNG download, Copy PNG), both locked. SVG export is completely absent:" + ], + "evidence": null, + "fix": null, + "principles": [ + "Feature completeness", + "Print/documentation use cases", + "Competitive parity" + ] + }, + { + "id": "GAP-15-004", + "severity": "medium", + "title": "Cmd+S is completely silent for anonymous users — no save, no feedback, no auth prompt", + "paragraphs": [ + "The keyboard shortcuts panel lists Ctrl/⌘ + S as \"Save current creations\". For an anonymous user, pressing Cmd+S with editor focused produces absolutely no response:", + "The export buttons at least respond with a modal. Cmd+S is worse — it silently does nothing, leaving users to wonder if it worked, failed, or simply isn't implemented." + ], + "evidence": null, + "fix": null, + "principles": [ + "Nielsen #1: Visibility of System Status", + "Nielsen #5: Error Prevention", + "Keyboard shortcut contract" + ] + }, + { + "id": "GAP-15-005", + "severity": "low", + "title": "No right-click context menu on diagram canvas — missed export shortcut", + "paragraphs": [ + "Right-clicking the rendered diagram canvas shows only the browser's native context menu (with options like \"Inspect\", \"Save page as\", etc.). No app-level context menu appears:" + ], + "evidence": null, + "fix": null, + "principles": [ + "Nielsen #6: Recognition over Recall", + "Discoverability", + "Power-user affordances" + ] + } + ] + }, + { + "number": 16, + "slug": "case-16-mobile-responsive", + "title": null, + "subtitle": "At 390px (iPhone 14), the entire bottom toolbar disappears, the diagram shrinks to a 55px sliver, and the header title wraps to 3 lines", + "caption": null, + "gif": "ux-case-16-mobile-responsive.gif", + "gaps": [ + { + "id": "GAP-16-001", + "severity": "high", + "title": "No responsive layout — desktop split-pane renders at full width on 390px mobile", + "paragraphs": [ + "The entire application renders as a desktop layout with no breakpoint adaptations for mobile screen widths. The split-pane editor+canvas design assumes a wide viewport and fails catastrophically on narrow screens:" + ], + "evidence": null, + "fix": null, + "principles": [ + "Mobile-first design", + "Responsive design fundamentals", + "WCAG 1.4.10: Reflow (320px width requirement)" + ] + }, + { + "id": "GAP-16-002", + "severity": "high", + "title": "Diagram canvas reduced to 55px sliver — completely unusable on mobile", + "paragraphs": [ + "At 390px width, the layout allocates: ~50px sidebar + ~200px editor pane + ~55px canvas sliver + ~85px overflow. The canvas is visible as a 55px strip showing only the leftmost edge of the first participant box:" + ], + "evidence": "
Space allocation at 390px", + "fix": null, + "principles": [ + "WCAG 1.4.10: Reflow", + "Core product functionality", + "Mobile touch interactions" + ] + }, + { + "id": "GAP-16-003", + "severity": "high", + "title": "All export buttons (Present, PNG, Copy PNG) disappear completely on mobile", + "paragraphs": [ + "The bottom action bar contains \"Present | ↓ PNG | ⎘ Copy PNG\" buttons on the right side of the screen. At 390px, these buttons overflow beyond the viewport and are completely inaccessible:" + ], + "evidence": null, + "fix": null, + "principles": [ + "WCAG 1.4.10: Reflow", + "Nielsen #3: User Control & Freedom", + "Mobile layout overflow" + ] + }, + { + "id": "GAP-16-004", + "severity": "medium", + "title": "Diagram title wraps to 3 lines in header — wastes vertical space and looks broken", + "paragraphs": [ + "The header title element contains \"My Auth Flow Diagram\" (from the previous session). At 390px, this wraps into three lines: \"My Auth / Flow / Diagram\". The header height inflates significantly:" + ], + "evidence": null, + "fix": null, + "principles": [ + "Nielsen #4: Consistency & Standards", + "Text overflow handling", + "Vertical space budget" + ] + }, + { + "id": "GAP-16-005", + "severity": "medium", + "title": "Insert toolbar clips to 4 icons — 50%+ of editing shortcuts unreachable on mobile", + "paragraphs": [ + "The insert toolbar above the editor shows 8+ icon buttons (New participant, Async, Sync, Return value, Self message, New instance, Conditional, Loop, and more). At 390px, only the first 4 fit:" + ], + "evidence": null, + "fix": null, + "principles": [ + "Fitts's Law: reachability", + "Mobile touch target accessibility", + "Progressive disclosure" + ] + }, + { + "id": "GAP-16-006", + "severity": "low", + "title": "No touch-optimized interactions on diagram canvas — pinch-zoom and swipe-pan absent", + "paragraphs": [ + "Even at desktop sizes, the diagram canvas lacks scroll-to-zoom and drag-to-pan (documented in Case 09). On mobile these gaps are worse because touch users have no mouse wheel alternative:" + ], + "evidence": null, + "fix": null, + "principles": [ + "Mobile touch patterns", + "Fitts's Law: touch target size (min 44×44px)", + "iOS/Android HIG touch conventions" + ] + } + ] + }, + { + "number": 17, + "slug": "case-17-keyboard-nav", + "title": "Keyboard Navigation & Focus Management", + "subtitle": "Tab order traversal · Focus ring visibility · Screen-reader reachability · Skip links", + "caption": "Keyboard navigation walkthrough — Tab focus order, focus ring visibility, and CodeMirror reachability.", + "gif": "ux-case-17-keyboard-nav.gif", + "gaps": [ + { + "id": "GAP-17-001", + "severity": "high", + "title": "Focus rings fail WCAG 2.4.11 minimum area requirement", + "paragraphs": [ + "Every interactive element — sidebar icons, toolbar buttons, header buttons — uses the browser's 1px default focus outline in rgb(0, 95, 204). Against the dark #1e293b backgrounds this produces a contrast ratio well below the WCAG 2.4.11 (AA, 2024) minimum: a focus indicator must have at least a 3:1 contrast ratio against adjacent colors AND enclose an area ≥ the perimeter of a 2 CSS px border around the component.", + "Impact: Keyboard-only users and low-vision users relying on focus rings cannot reliably track where keyboard focus is located. This is a WCAG 2.2 Level AA failure." + ], + "evidence": "// JS audit result for first 15 focusable elements:\n// All returned: outline = \"rgb(0, 95, 204) auto 1px\"\n// Share Link exception: \"rgb(245, 245, 245) auto 1.5px\"\n// Background of sidebar icon strip: #1e293b (#30353d effective)\n// Contrast of #005FCC on #30353d: ~3.4:1 border but at 1px width\n// the enclosed area is far below 2px border perimeter requirement", + "fix": "Replace the browser default with an explicit outline: 2px solid #60a5fa; outline-offset: 2px; on all focusable elements, or use the :focus-visible pseudo-class with a 2px+ ring that passes 3:1 against the adjacent background color. The Share Link button's 1.5px white ring is closer but still undersized.", + "principles": [ + "WCAG 2.4.11", + "WCAG 2.4.7", + "Nielsen #1 Visibility" + ] + }, + { + "id": "GAP-17-002", + "severity": "high", + "title": "CodeMirror editor unreachable in first 20+ Tab stops", + "paragraphs": [ + "The CodeMirror editor is the primary work surface of the application, yet keyboard users must Tab through at least 20+ interactive elements — 3 header buttons, 6 sidebar icons, 8+ toolbar buttons — before the editor's textarea receives focus. The first Tab stop is the \"New\" button in the header, not the editor.", + "Impact: Keyboard-only users opening the app to write ZenUML code face an excessive navigation burden before reaching the input surface. WCAG 2.1 Success Criterion 2.4.3 (Focus Order) requires a \"meaningful sequence\" — placing the primary editing canvas last in tab order violates this criterion." + ], + "evidence": "// Focus traversal order (programmatic JS audit, 15 elements):\n// 1: New 2: Share Link 3: Profile 4: My Library\n// 5: Code Editor 6: Keyboard Shortcuts 7: Cheatsheet\n// 8: Language Guide 9: Settings 10: New participant\n// 11: Async message 12: Sync message 13: Return value\n// 14: Self message 15: New instance ...\n// CodeMirror textarea: position >> 20", + "fix": "Add tabindex=\"1\" to the CodeMirror wrapper (or use CodeMirror's built-in tabIndex option) so the editor is the first Tab stop after the skip-nav link. Alternatively restructure DOM order: editor before sidebar/toolbar in source order, use CSS for visual layout.", + "principles": [ + "WCAG 2.4.3", + "Nielsen #6 Recognition over Recall" + ] + }, + { + "id": "GAP-17-003", + "severity": "medium", + "title": "6 sidebar icons consume sequential Tab stops between header and toolbar", + "paragraphs": [ + "All 6 left-sidebar icon buttons (My Library, Code Editor, Keyboard Shortcuts, Cheatsheet, Language Guide, Settings) are individually focusable and appear consecutively in the tab order. For a tool-switching sidebar this is unnecessary: only the currently active panel icon needs to be reachable; the others could use a roving tabindex pattern (arrow keys to navigate icons, single Tab to leave the group).", + "Impact: 6 extra Tab stops between the header and the toolbar adds navigation friction. Combined with GAP-17-002 this means keyboard users Tab 20+ times to reach the editor." + ], + "evidence": null, + "fix": "Implement the ARIA toolbar / roving tabindex pattern for the sidebar icon strip: give the strip role=\"toolbar\" or role=\"navigation\", make only the active icon tabindex=\"0\" and the rest tabindex=\"-1\", then use arrow key handlers to move between icons. This compresses 6 stops into 1.", + "principles": [ + "WCAG 2.4.3", + "ARIA Authoring Practices" + ] + }, + { + "id": "GAP-17-004", + "severity": "medium", + "title": "Toolbar insert buttons missing aria-label — announced by icon ligature text", + "paragraphs": [ + "All 8+ toolbar insert buttons (New participant, Async message, Sync message, etc.) have ariaLabel: null. The visible content is a Material Icons ligature string (\"person_add\", \"swap_horiz\", etc.) which is what screen readers announce verbatim. Users with assistive technology hear \"person add button\" rather than \"Insert new participant\".", + "This was also partially documented in Case 14 for sidebar icons. The toolbar buttons are a separate, wider set of the same pattern." + ], + "evidence": "// JS audit:\nbuttons.map(b => ({label: b.ariaLabel, text: b.textContent.trim()}))\n// Results: [{label: null, text: \"person_add\"}, {label: null, text: \"swap_horiz\"}, ...]", + "fix": "Add descriptive aria-label to each toolbar button: aria-label=\"Insert new participant\", aria-label=\"Insert asynchronous message\", etc. Also add title attributes so sighted keyboard users see tooltips on focus (addresses Case 07 GAP-07-001 too).", + "principles": [ + "WCAG 4.1.2", + "Nielsen #1 Visibility" + ] + }, + { + "id": "GAP-17-005", + "severity": "low", + "title": "No skip-to-content link for keyboard users", + "paragraphs": [ + "There is no \"Skip to editor\" (skip-nav) link at the top of the page. On every page load or tab switch, keyboard users must Tab through all header and sidebar controls to reach the editor. This is standard accessibility infrastructure expected by WCAG 2.4.1 (Bypass Blocks, Level A).", + "Impact: Lower severity because GAP-17-002 (editor tabindex priority) would be the more impactful fix; a skip link is a complementary safeguard. However WCAG 2.4.1 is a Level A criterion, making this a baseline conformance failure." + ], + "evidence": null, + "fix": "Add a visually hidden but focusable Skip to editor as the first element in . Show it on focus with :focus { position: fixed; top: 0; left: 0; ... }. Assign id=\"editor\" to the CodeMirror container.", + "principles": [ + "WCAG 2.4.1", + "WCAG 2.4.3" + ] + } + ] + }, + { + "number": 18, + "slug": "case-18-color-contrast", + "title": "Color Contrast & Visual Accessibility", + "subtitle": "WCAG 2.1 contrast audit · Share Link CTA · Editor line numbers · Diagram SVG elements · Color-only encoding", + "caption": "Color contrast audit — Share Link button, editor line numbers, diagram SVG labels, and the dark/light split panel.", + "gif": "ux-case-18-color-contrast.gif", + "gaps": [ + { + "id": "GAP-18-001", + "severity": "high", + "title": "Share Link CTA button fails WCAG AA — 3.04:1 (needs 4.5:1)", + "paragraphs": [ + "The primary conversion button \"Share Link\" renders near-white text rgb(245,245,245) on a medium-blue background rgb(103,134,247). At 14px font size this requires a contrast ratio of 4.5:1 — the measured ratio of 3.04:1 falls 33% short of WCAG AA compliance.", + "Impact: This is the most prominent interactive element in the header, visible on every diagram. Users with low vision, in bright ambient light, or on low-gamut displays may struggle to read it. WCAG 1.4.3 (Contrast Minimum, Level AA) failure." + ], + "evidence": "// Measured via JavaScript WCAG luminance formula:\nfg: rgb(245, 245, 245) → L = 0.904\nbg: rgb(103, 134, 247) → L = 0.215\nratio = (0.904 + 0.05) / (0.215 + 0.05) = 3.04:1\nWCAG AA requires 4.5:1 for text ≤18px non-bold", + "fix": "Darken the button background to at least #4355CC (passes 4.74:1) or increase the text to 18px/bold (drops requirement to 3:1). Alternatively invert to a dark button with light text: background:#1e293b; color:#fff easily passes at 15:1.", + "principles": [ + "WCAG 1.4.3 AA", + "Nielsen #1 Visibility" + ] + }, + { + "id": "GAP-18-002", + "severity": "medium", + "title": "CodeMirror line numbers fail WCAG AA — 3.83:1 (needs 4.5:1)", + "paragraphs": [ + "Editor line numbers render as rgb(109,138,136) (a muted teal) on rgb(40,42,54) (the Dracula theme background). At 14px with normal weight, the measured 3.83:1 ratio is below the 4.5:1 WCAG AA threshold for normal text.", + "While line numbers are often considered secondary UI, they are functional text that keyboard-navigating users, developers debugging syntax, and screen reader users rely on to locate specific lines. WCAG 1.4.3 applies to all meaningful text." + ], + "evidence": "// Line numbers (.CodeMirror-linenumber):\nfg: rgb(109, 138, 136) → L = 0.228\nbg: rgb(40, 42, 54) → L = 0.024\nratio = (0.228 + 0.05) / (0.024 + 0.05) = 3.83:1\nRequired: 4.5:1 (14px normal weight)", + "fix": "Brighten the line number color to approximately rgb(145,165,163) or higher. With Dracula theme, a value around #A8B8B7 achieves 5:1+ while remaining clearly secondary to code content. Alternatively, bump font size to 18px (drops threshold to 3:1).", + "principles": [ + "WCAG 1.4.3 AA" + ] + }, + { + "id": "GAP-18-003", + "severity": "medium", + "title": "Diagram SVG graphical elements use #A5A5A5 on white — 2.46:1 (needs 3:1)", + "paragraphs": [ + "Block-type icons (the <> diamond for \"Alt\" and the loop arrow for \"Loop\") are rendered as SVG text/paths in #A5A5A5 on the white diagram canvas. The measured contrast ratio is 2.46:1 — below the WCAG 1.4.11 (Non-text Contrast, Level AA) minimum of 3:1 required for graphical UI components that convey meaning.", + "These icons are the only visual indicator of which block type (conditional vs. loop) is being represented. A user who cannot distinguish the icon must rely on the block label text instead." + ], + "evidence": "// SVG text elements with fill=\"#A5A5A5\":\n// [\"Alt\", \"Loop\"] — confirmed via document.querySelectorAll('svg text')\nfg: #A5A5A5 = rgb(165,165,165) → L = 0.376\nbg: white = rgb(255,255,255) → L = 1.000\nratio = (1.0 + 0.05) / (0.376 + 0.05) = 2.46:1\nWCAG 1.4.11 requires 3:1 for graphical objects", + "fix": "Change the SVG icon fill from #A5A5A5 to #767676 or darker. #767676 achieves exactly 4.54:1 on white — solidly passing AA. Or use a distinct shape/stroke instead of relying on the icon alone.", + "principles": [ + "WCAG 1.4.11 AA", + "WCAG 1.4.1" + ] + }, + { + "id": "GAP-18-004", + "severity": "medium", + "title": "Syntax highlighting uses color as the sole differentiator between token types", + "paragraphs": [ + "In the CodeMirror editor, different token types are distinguished exclusively by color: identifiers in green, message text in pink/magenta, arrows in green, keywords and punctuation in white. No secondary cues — bold weight, italics, underline — differentiate token types for users with color vision deficiency.", + "An estimated 8% of males have some form of color vision deficiency. For a developer tool targeting engineers, this is a significant portion of the audience. WCAG 1.4.1 (Use of Color, Level A) prohibits using color as the only visual means of conveying information.", + "Test: In a deuteranopia simulation, green identifiers and green arrows blend to the same hue, making it visually impossible to distinguish participant names from message arrows without bold weight or other cues." + ], + "evidence": null, + "fix": "Add a secondary visual signal to at least the most important distinctions: make keywords bold (font-weight: 700), make strings italic, or use the Dracula theme's built-in styles which provide some weight variation. CodeMirror supports per-token styles via the theme configuration.", + "principles": [ + "WCAG 1.4.1 A", + "Nielsen #1 Visibility" + ] + }, + { + "id": "GAP-18-005", + "severity": "low", + "title": "White diagram canvas creates extreme luminance split against dark editor — no dark-diagram option", + "paragraphs": [ + "The split-pane layout places a dark editor (#282a36, very dark) directly beside a bright white diagram canvas (#ffffff). The luminance ratio between the two panels is approximately 100:1. Users working extended sessions or with photosensitivity must continuously adapt their eyes between the two extreme luminance zones.", + "Modern tools (Mermaid Live Editor, dbdiagram.io, Excalidraw) all offer dark diagram themes or allow diagram background color configuration. ZenUML's CSS tab could theoretically allow this, but the feature requires knowing the internal CSS class names — it is not discoverable." + ], + "evidence": null, + "fix": "Add a \"Diagram theme\" toggle in Settings (light/dark/auto). The dark diagram theme would simply change the diagram wrapper's background and SVG line/text colors. A one-line CSS variable change could power the entire switch: --diagram-bg: #1e1e2e; --diagram-ink: #cdd6f4;.", + "principles": [ + "WCAG 1.4.3", + "Nielsen #4 Consistency" + ] + } + ] + }, + { + "number": 19, + "slug": "case-19-tooltip-help", + "title": "Tooltip & Contextual Help UX", + "subtitle": "Systematic hover audit · 39 interactive elements surveyed · tooltip presence, mechanism consistency, and content quality", + "caption": "Hover audit across all UI zones — toolbar buttons show no tooltip; canvas footer controls (ⓘ, ☑ 1.2.3, zoom icons) are equally silent.", + "gif": "ux-case-19-tooltip-help.gif", + "gaps": [ + { + "id": "GAP-19-001", + "severity": "high", + "title": "7 toolbar insert buttons: zero tooltip, no keyboard shortcut hint, no syntax preview", + "paragraphs": [ + "All 7 insert buttons in the editor toolbar (New participant, Async message, Sync message, Return value, Self message, New instance, ConditionalAlt) have title=null, aria-label=null, and no custom tooltip component. The visible text label alone leaves users without:", + "1. Keyboard shortcut — is there one? Can I trigger \"Async message\" from the keyboard? 2. Resulting syntax — hovering \"Async message\" should preview A ->> B: message 3. Insertion behavior — does it insert at cursor or append at end?", + "Competing tools (Mermaid Live Editor, draw.io, PlantUML Editor) all show syntax snippets in toolbar tooltips. New ZenUML users must click and inspect the inserted code to reverse-engineer the syntax." + ], + "evidence": "// JS audit result (all 7 toolbar buttons):\n{ text: \"New participant\", title: null, ariaLabel: null }\n{ text: \"Async message\", title: null, ariaLabel: null }\n{ text: \"Sync message\", title: null, ariaLabel: null }\n{ text: \"Return value\", title: null, ariaLabel: null }\n{ text: \"Self message\", title: null, ariaLabel: null }\n{ text: \"New instance\", title: null, ariaLabel: null }\n{ text: \"ConditionalAlt\", title: null, ariaLabel: null }", + "fix": "Add a title attribute with the pattern \"[Label] — inserts: [syntax snippet]\". Example: title=\"Async message — inserts: A ->> B: message\". For a richer experience, replace with a Radix UI Tooltip showing the syntax with syntax highlighting and the keyboard shortcut if one exists.", + "principles": [ + "Nielsen #6 Recognition over Recall", + "Nielsen #4 Consistency", + "Nielsen #1 Visibility" + ] + }, + { + "id": "GAP-19-002", + "severity": "medium", + "title": "Inconsistent tooltip mechanism across UI zones — 3 different patterns, one invisible", + "paragraphs": [ + "Three different tooltip strategies coexist with inconsistent behavior for sighted mouse users:" + ], + "evidence": null, + "fix": "Standardize on one tooltip mechanism app-wide. Recommended: Radix UI Tooltip on every interactive element, with a consistent 300ms open delay and 100ms close delay. The existing Radix UI dependency is already in the project — use it.", + "principles": [ + "Nielsen #4 Consistency", + "WCAG 1.3.1" + ] + }, + { + "id": "GAP-19-003", + "severity": "medium", + "title": "Canvas footer controls (ⓘ, ☑ 1.2.3, zoom icons) have no tooltip — \"1.2.3\" is cryptic", + "paragraphs": [ + "The diagram canvas footer contains 5 interactive controls with zero tooltip:", + "• ⓘ — what does this show? (appears to open a tips overlay) • ☑ 1.2.3 — completely cryptic; hovering reveals nothing. It toggles sequence step numbers, but this is undiscoverable without clicking. • ⊕ / ⊖ — zoom icons with no label or shortcut hint • 100% — clicking this resets zoom, but hover reveals no hint", + "The \"1.2.3\" checkbox is the worst offender — even an experienced UML author might not know what this does on first sight. The label reads as a version number, not as \"show/hide sequence numbers.\"" + ], + "evidence": null, + "fix": "Add title attributes: title=\"Show/hide step numbers\" on the 1.2.3 checkbox, title=\"Tips & shortcuts\" on ⓘ, title=\"Zoom in (Ctrl+Plus)\" on ⊕, title=\"Zoom out (Ctrl+Minus)\" on ⊖, title=\"Reset zoom to 100%\" on the percentage text. Replace the 1.2.3 label with \"1.2\" or better — a sequence-number icon — and add a visible label \"Step numbers\" next to the checkbox.", + "principles": [ + "Nielsen #6 Recognition over Recall", + "Nielsen #1 Visibility" + ] + }, + { + "id": "GAP-19-004", + "severity": "medium", + "title": "No keyboard shortcut hints in any tooltip across the entire app", + "paragraphs": [ + "Even the 9 elements that do have tooltips (sidebar icons, header buttons) include no keyboard shortcut information. Modern productivity tools universally surface shortcuts in tooltips — VS Code, Figma, Linear, and Notion all follow the pattern: \"Action Name (Shortcut)\".", + "ZenUML has a dedicated Keyboard Shortcuts panel (accessible via the sidebar) but zero in-context shortcut hints. Users must remember to open that panel separately. For frequent actions like \"New diagram\" (Cmd+N if supported), the shortcut is entirely invisible at the point of action." + ], + "evidence": "// Example of what tooltips could say:\nsidebar \"My Library\": \"My Library\" → ❌ no shortcut\nsidebar \"Keyboard Shortcuts\": \"Keyboard Shortcuts\" → ❌ ironic: no shortcut hint\nheader \"New\": \"Start a new creation\" → ❌ should say \"(Cmd+N)\"", + "fix": "Append the keyboard shortcut (if one exists) to every tooltip: title=\"My Library (Ctrl+B)\". For the Keyboard Shortcuts icon specifically, the tooltip \"Keyboard Shortcuts (Ctrl+/)\" would be self-documenting. Use a consistent format: label first, shortcut in parentheses.", + "principles": [ + "Nielsen #6 Recognition over Recall", + "Nielsen #4 Consistency" + ] + }, + { + "id": "GAP-19-005", + "severity": "low", + "title": "Bottom bar action buttons (Present, PNG, Copy PNG) lack descriptive tooltips", + "paragraphs": [ + "The three action buttons in the bottom-right corner of the app — \"Present\", \"PNG\", and \"Copy PNG\" — have no tooltip. \"Present\" is particularly ambiguous: does it open a link? Enter fullscreen? Launch a slideshow? A user who hasn't tried it has no pre-click information about the outcome.", + "\"PNG\" is slightly better (the format name is self-explanatory to technical users), but new users may not know whether clicking triggers a download, opens a dialog, or copies to clipboard." + ], + "evidence": "// Confirmed zero title/ariaLabel on all three:\n{ text: \"Present\", title: null, ariaLabel: null }\n{ text: \"PNG\", title: null, ariaLabel: null }\n{ text: \"Copy PNG\", title: null, ariaLabel: null }", + "fix": "Add descriptive titles: title=\"Enter presentation mode (fullscreen)\", title=\"Download diagram as PNG image\", title=\"Copy diagram PNG to clipboard\". These would disambiguate before the user clicks — especially important since PNG and Copy PNG look visually similar.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #6 Recognition over Recall" + ] + } + ] + }, + { + "number": 20, + "slug": "case-20-css-tab", + "title": "CSS Tab & Custom Styling UX", + "subtitle": "Auth gate discoverability · no preview · tab active state · feature lock transparency", + "caption": "CSS tab click → 5th instance of generic \"Welcome to ZenUML.com\" auth modal, zero context about the feature, CSS editor absent from DOM.", + "gif": "ux-case-20-css-tab.gif", + "gaps": [ + { + "id": "GAP-20-001", + "severity": "high", + "title": "CSS tab has zero visual indicator it is auth-gated — no lock, no badge, looks identical to free ZenUML tab", + "paragraphs": [ + "Both the ZenUML tab and the CSS tab use identical visual styling: bg-black-800 font-semibold. The CSS tab carries no lock icon, no \"Pro\" or \"Sign in\" badge, no greyed-out state, no tooltip warning. A user has no pre-click information that the CSS tab requires authentication.", + "This violates the principle of transparent feature gating — when a feature requires upgrade or authentication, that requirement should be visible at the entry point, not revealed only after the user clicks. Showing the gate only at click-time is a dark pattern known as \"bait and switch.\"" + ], + "evidence": null, + "fix": "Add a lock icon (🔒) and a \"Pro\" or \"Sign in\" badge to the CSS tab. On hover, show a tooltip: \"CSS customization — sign in to unlock\". Alternatively, allow unauthenticated users to view a read-only CSS editor prefilled with a default template, with a banner prompting sign-in to save.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #10 Help & Docs", + "Dark patterns: Bait & Switch" + ] + }, + { + "id": "GAP-20-002", + "severity": "high", + "title": "5th generic \"Welcome to ZenUML.com\" auth modal — zero context about the CSS feature", + "paragraphs": [ + "Clicking the CSS tab shows the same generic modal as Share Link, Export PNG, Copy PNG, and Present mode — all 5 show identical text: \"Welcome to ZenUML.com / Login with Github / Login with Google / Login with Facebook / Join a community of 50,000+ Developers.\" The modal contains:", + "• No mention of CSS customization — what does it unlock? • No preview or screenshot of what CSS styling looks like • No indication of tier — is it free with login, or paid? • No \"Learn more\" link for users who want to understand before committing", + "The 5-feature pattern of identical auth gates means that by the 3rd or 4th encounter, users disengage completely — they no longer read the modal and simply dismiss it. The modal provides no conversion value for the CSS feature specifically." + ], + "evidence": "// All 5 features trigger the SAME modal:\n// 1. Share Link (Case 4)\n// 2. Present Mode (Case 6)\n// 3. Export PNG / Copy PNG (Case 15)\n// 4. CSS Tab (this case — #5)\n// Modal text: \"Welcome to ZenUML.com\" — no feature context", + "fix": "Replace the generic modal with a feature-specific upsell: show the CSS editor UI as a teaser (blurred or read-only), a 1-2 sentence description (\"Write custom CSS to brand your diagrams with company colors and fonts\"), and a clear CTA. Each feature's auth gate should contextualize WHY sign-in is needed.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #2 Match Real World", + "Conversion UX" + ] + }, + { + "id": "GAP-20-003", + "severity": "medium", + "title": "CSS editor is absent from DOM entirely — no preview, no placeholder, no teaser for unauthenticated users", + "paragraphs": [ + "When unauthenticated, the CSS tab's editor container is completely absent from the DOM — confirmed via document.querySelector('[class*=\"css-editor\"]') returning null and zero CSS-related divs rendered. There is no placeholder, empty state, or teaser of what the editor would contain.", + "In contrast, tools like Mermaid Live Editor, draw.io, and CodePen show the full editor interface to all users but restrict saving/sharing behind auth. This \"try before you sign up\" approach drives significantly higher conversion because users can experience the value before being asked to commit." + ], + "evidence": "// JS audit:\ndocument.querySelector('[class*=\"css-editor\"]') // → null\ndocument.querySelectorAll('[class*=\"css\"]') // → 0 elements\n// The CSS editor panel is never rendered for anon users\n// No placeholder, no empty state, no sample CSS shown", + "fix": "Render the CSS editor for unauthenticated users with a sample CSS snippet (e.g., .participant { background: #e8f4fd; }). Allow free editing but block saving with an inline \"Sign in to save your changes\" banner. This lets users experience the feature's value before signing up.", + "principles": [ + "Nielsen #6 Recognition over Recall", + "Conversion UX" + ] + }, + { + "id": "GAP-20-004", + "severity": "medium", + "title": "Active tab state relies on a single 2px blue underline — low visual salience, no background differentiation", + "paragraphs": [ + "The active tab (ZenUML) is differentiated from the inactive tab (CSS) by only: a border-b border-primary (2px bottom border in blue) and a minor background shift from bg-black-800 to bg-black-500. At normal viewing distance, the active state is not immediately obvious — the text color change to text-primary-400 (blue-ish) is subtle against the dark background.", + "Modern tab implementations (Chrome DevTools, VS Code, Figma panels) use a clear background color contrast, not just a border underline, to communicate active state. The current implementation passes a quick glance test only because there are two tabs — with 3+ tabs the active tab would be much harder to spot." + ], + "evidence": "// Active tab class audit:\n// ZenUML (active): \"... border-b border-primary bg-black-500 text-primary-400\"\n// CSS (inactive): \"... bg-black-800 font-semibold\"\n// Difference: border-b + 1 Tailwind shade lighter bg + blue text\n// bg-black-500 vs bg-black-800: very close luminance values in dark palette", + "fix": "Increase active tab visual contrast: use a distinctly lighter background (e.g., bg-zinc-700) with a rounded top-corners treatment, or switch to a pill/chip active-tab style with a full background fill. The border-b alone is the weakest form of tab indicator in dark UIs.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #4 Consistency" + ] + }, + { + "id": "GAP-20-005", + "severity": "low", + "title": "No documentation or CSS selector reference anywhere in the app", + "paragraphs": [ + "Even if a user successfully authenticates to access the CSS editor, there is no in-app documentation about:", + "• What CSS selectors target which diagram elements (e.g., .participant, .message-label) • Which CSS properties are supported (SVG CSS vs HTML CSS) • Example CSS snippets for common customizations (brand colors, font changes) • A link to external documentation or a \"CSS Reference\" panel", + "The Language Guide sidebar panel exists for ZenUML syntax, but there is no equivalent guide for the CSS editor. A power user discovering CSS customization must reverse-engineer the SVG structure by hand." + ], + "evidence": null, + "fix": "Add a collapsible \"CSS Reference\" panel within the CSS editor (similar to the Language Guide for ZenUML). Include the top 10 most-used selectors with examples: .participant { }, .message { }, .divider { }, etc. A single-page CSS cookbook linked from the editor would dramatically improve discoverability.", + "principles": [ + "Nielsen #10 Help & Docs", + "Nielsen #6 Recognition over Recall" + ] + } + ] + }, + { + "number": 21, + "slug": "case-21-split-pane", + "title": "Split Pane Resize & Layout Control", + "subtitle": "Invisible 6px gutter · no collapse buttons · 30/70 default disadvantages editor · no reset gesture", + "caption": "The 6px gutter is functionally resizable (paneforge library, persisted via localStorage) but completely invisible — no hover highlight, no grip dots. Double-click does not reset to default.", + "gif": "ux-case-21-split-pane.gif", + "gaps": [ + { + "id": "GAP-21-001", + "severity": "high", + "title": "Split pane gutter is completely invisible — 6px, near-transparent background, no hover affordance", + "paragraphs": [ + "The divider between the editor and canvas panels is a 6px-wide .gutter.gutter-horizontal element with background: rgba(255,255,255,0.05) — effectively invisible against the dark UI. There is no CSS hover rule that highlights it, no grip dots, no color change. The cursor does switch to ew-resize when hovering the gutter, but discovering that 6px strip by accident is the only signal the feature exists.", + "Impact: The vast majority of users will never discover they can resize the split. In 20 UX test cases, the gutter was never stumbled upon organically — it was only found through DOM inspection. This is a hidden power-user feature that could serve everyone." + ], + "evidence": "// Gutter DOM audit:\nclass: \"gutter gutter-horizontal\"\nwidth: 6px\nbackground: rgba(255, 255, 255, 0.05) // 5% opacity white — invisible\ncursor: ew-resize // only signal, requires hitting the 6px strip\nNo CSS :hover rule defined for .gutter selector\nNo grip icon, no handle, no visual affordance", + "fix": "Add a visible grip affordance: center 3 horizontal dots or a subtle bar on the gutter. On hover, brighten background to rgba(255,255,255,0.15) and show the dots more clearly. A 2px visible stripe on the left edge of the canvas pane (like VS Code's activity bar separator) would be enough. Minimum: add a CSS hover rule .gutter:hover { background: rgba(255,255,255,0.12); }.", + "principles": [ + "Nielsen #1 Visibility", + "Affordance (Gibson/Norman)" + ] + }, + { + "id": "GAP-21-002", + "severity": "high", + "title": "Neither pane can be collapsed — minSize:15% on both sides prevents focus mode", + "paragraphs": [ + "The paneforge configuration enforces minSize: 15 (15% of total width) on both the editor and canvas panes. This means:", + "• Users cannot collapse the canvas to get a full-width editor for writing long ZenUML scripts • Users cannot collapse the editor to get a distraction-free, full-width diagram view • Maximum editor width is 85%, maximum canvas width is 85%", + "Developer tools universally offer panel collapse: VS Code collapses any panel to 0, JetBrains has explicit collapse buttons, Mermaid Live Editor has a \"hide editor\" toggle. Power users regularly want to maximize one pane." + ], + "evidence": "// localStorage['paneforge:liveEditor']:\n// {\"defaultSize\":30,\"minSize\":15},{\"minSize\":15}\n// layout: [30, 70] (30% editor, 70% canvas default)\n// minSize: 15% on both = neither can collapse below 15%\n// At 1920px viewport: editor min = 288px, canvas min = 288px", + "fix": "Add explicit collapse/expand buttons to both panels (a chevron icon at the panel boundary), and set collapsible: true in paneforge config with collapsedSize: 0. Add keyboard shortcuts: Cmd+\\ to toggle the split, Cmd+B to collapse/expand the editor (following VS Code conventions).", + "principles": [ + "Nielsen #3 User Control", + "Nielsen #7 Flexibility" + ] + }, + { + "id": "GAP-21-003", + "severity": "medium", + "title": "Default 30/70 split gives only 30% to the editor — primary work surface is undersized by default", + "paragraphs": [ + "The paneforge default configuration allocates defaultSize: 30 (30%) to the editor and 70% to the canvas. At a 1920px viewport, this gives the editor ~576px — barely enough for a 7-line ZenUML script. A typical sequence diagram with 8-10 participants and 15-20 messages will overflow the editor height, requiring scrolling while writing.", + "The diagram canvas by contrast has 1344px of width, most of which is empty dark space. The actual rendered diagram sits centered in a fraction of that area. The default split optimizes for the output, not the input — backwards for an editor-first tool." + ], + "evidence": "// Default paneforge layout: [30, 70]\n// At 1920px viewport:\n// Editor: 30% = 576px (after sidebar)\n// Canvas: 70% = 1344px\n// Typical diagram canvas renders in ~400px SVG width\n// → 944px of canvas is empty dark space at default split", + "fix": "Change the default split to 40/60 or 50/50. Run an A/B test — for a code editor tool, giving the editor equal or majority space tends to improve session length and diagram complexity. The 30% default feels like a viewer app, not an editor app.", + "principles": [ + "Nielsen #4 Consistency", + "Nielsen #7 Flexibility" + ] + }, + { + "id": "GAP-21-004", + "severity": "medium", + "title": "No double-click to reset split — persisted custom ratio has no escape hatch", + "paragraphs": [ + "The split ratio is persisted to localStorage['paneforge:liveEditor'] and survives page reloads. Once a user drags the gutter to an undesirable position (e.g., accidentally making the editor very narrow), there is no way to reset it except dragging back manually — with the invisible gutter. Double-clicking the gutter was tested and confirmed to have no reset behavior.", + "VS Code, IntelliJ, and most modern split-pane implementations support double-click to reset to the default ratio. This is a discoverable escape hatch that doesn't require the user to find the Settings or clear localStorage." + ], + "evidence": "// Double-click test result:\n// Before: editorWidth = 795px (42.9%)\n// After double-click: editorWidth = 795px (unchanged)\n// paneforge does not implement double-click-to-reset\n// Only way to reset: drag back manually or clear localStorage", + "fix": "Add double-click handler on the gutter element that calls paneforge's reset() or sets the split back to [30, 70]. Also add a \"Reset layout\" option in the Settings modal for users who want to recover from a bad split position.", + "principles": [ + "Nielsen #3 User Control", + "Nielsen #5 Error Prevention" + ] + }, + { + "id": "GAP-21-005", + "severity": "low", + "title": "6px gutter is too narrow for reliable hit-testing, especially on high-DPI displays", + "paragraphs": [ + "The gutter's 6 CSS pixel width at 2× DPR renders as a 12 physical pixel strip. Fitts's Law quantifies this: the time to acquire a 6px target at 500px distance is approximately 3× longer than acquiring a 12px target at the same distance. This is compounded by the invisible background — users cannot see where the target is.", + "VS Code uses a 5px gutter with an 8px transparent hover area on each side (effective target: 21px). IntelliJ uses a visible 6px border with highlight. Both are significantly more discoverable than ZenUML's invisible 6px strip." + ], + "evidence": null, + "fix": "Keep the visual strip at 4-6px but expand the interactive hit area to 20px via transparent padding or a wider wrapper element with pointer-events: all. The visual grip stays narrow; the click target becomes comfortable.", + "principles": [ + "Fitts's Law", + "Nielsen #1 Visibility" + ] + } + ] + }, + { + "number": 22, + "slug": "case-22-error-state", + "title": "Error State & Syntax Validation UX", + "subtitle": "Silent parse failure · uncaught TypeError on keystrokes · error-gutter never populated · editor/canvas desync on undo", + "caption": "Invalid syntax typed (line 1: !!!INVALID@@@SYNTAX###) — canvas silently replaces diagram with an empty _STARTER_ participant. No error message, no inline squiggle, no banner. Cmd+Z ×10 reveals editor/canvas desync: editor empties but canvas shows a different prior diagram.", + "gif": "ux-case-22-error-state.gif", + "gaps": [ + { + "id": "GAP-22-001", + "severity": "high", + "title": "Invalid syntax silently renders a blank canvas — no error message, no inline squiggle, no notification", + "paragraphs": [ + "When a user types completely invalid ZenUML (e.g., !!!INVALID@@@SYNTAX###), the canvas silently collapses to a near-empty diagram showing only an unlabeled internal _STARTER_ participant (a stickman box). There is:", + "• No error banner above or below the diagram • No inline red squiggles in the editor (no CodeMirror lint markers; errorGutterChildCount: 0) • No toast or notification (toasts: 0) • No DOM error element visible (errorBanners: []) • The canvas iframe has errorElements: [] — nothing conveys the error state to the user", + "Competing tools handle this dramatically better: Mermaid Live Editor shows a red \"Parse error\" banner with the specific line and token that failed. Kroki and PlantUML editors display the exception message inline. ZenUML's completely silent failure leaves users with no idea what went wrong — they must delete code manually to diagnose." + ], + "evidence": "// JS DOM audit with input: \"!!!INVALID@@@SYNTAX###\\nthis is not valid zenuml at all\\n{{{ broken\"\n// Main page:\nerrorBanners: [] // no visible error elements\neditorErrorMarks: 1 // only the empty error-gutter container div itself\ngutterMarkers: 0 // no error markers placed in gutter\ntoasts: 0 // no toast notifications\n// Canvas iframe:\nerrorElements: [] // zero error nodes in iframe\nerrorMsgCount: 0 // no parse-error class elements\nparticipantCount: 1 // only the internal _STARTER_ fallback\nallVisibleText: [\"Click to add title\", \"_STARTER_\"] // internal placeholder leaked", + "fix": "Show a parse error banner below the editor toolbar (or as a red stripe at the bottom of the canvas) with the ANTLR error message and the offending line number. Minimum viable: wrap the diagram renderer in an error boundary that catches parser exceptions and renders a styled
Syntax error on line N: [message]
above the canvas footer. For inline editor feedback, add CodeMirror lint integration via cm.setOption(\"lint\", true) with a custom linter that runs the ZenUML parser.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #9 Error Messages", + "Nielsen #6 Recognition over Recall" + ] + }, + { + "id": "GAP-22-002", + "severity": "high", + "title": "Uncaught TypeError on every N-th keystroke — window.saveBtn is undefined, silently killing the unsaved-changes warning animation", + "paragraphs": [ + "Every UNSAVED_WARNING_COUNT keystrokes, onCodeChange in app.jsx:914 attempts to access window.saveBtn.classList.add('animated'). Since window.saveBtn is undefined in the current app build, this throws a TypeError: Cannot read properties of undefined (reading 'classList') — confirmed by 10+ identical console EXCEPTION entries during a single typing session.", + "The consequences are doubled:", + "• The save animation never fires — users are never visually warned they have N unsaved changes. The \"wobble\" animation on the save button (which is the intended UX) is completely dead code in production. • The exception is silently swallowed — the user sees nothing. There is no error boundary, no fallback, no degraded behavior — the app just quietly fails internally." + ], + "evidence": "// Console output during typing session (10+ occurrences at 08:28:54):\n[EXCEPTION] (app.jsx:729:21)\nTypeError: Cannot read properties of undefined (reading 'classList')\n at App.onCodeChange (app.jsx:730:22)\n\n// Source code at app.jsx:914:\nif (isUserChange && this.state.unsavedEditCount % UNSAVED_WARNING_COUNT === 0\n && this.state.unsavedEditCount >= UNSAVED_WARNING_COUNT) {\n window.saveBtn.classList.add('animated'); // ← TypeError: window.saveBtn is undefined\n window.saveBtn.classList.add('wobble');\n}", + "fix": "Guard with a null check: if (window.saveBtn) { window.saveBtn.classList.add('animated'); }. Better: replace the global window.saveBtn reference with a React ref (this.saveBtnRef = createRef() on the button element) — the global pattern is fragile and breaks if the button is conditionally rendered. This fix also restores the intended unsaved-changes animation.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #5 Error Prevention", + "Nielsen #9 Error Messages" + ] + }, + { + "id": "GAP-22-003", + "severity": "medium", + "title": "CodeMirror error-gutter is configured but never populated — inline error markers are dead infrastructure", + "paragraphs": [ + "The CodeMirror instance has a custom gutter registered as \"error-gutter\" — confirmed by the presence of a
in the DOM. This gutter was clearly designed to show per-line error indicators (like a red ✕ or warning triangle next to the offending line number).", + "However, gutterMarkers: 0 — no markers are ever placed in this gutter, even with 3 lines of completely invalid syntax. The gutter exists as a visible empty column next to the line numbers, occupying space without serving any purpose. The infrastructure for inline error indicators was planned but never completed." + ], + "evidence": "// DOM audit:\ndocument.querySelector('.error-gutter')\n// →
(empty)\n\n// Expected CodeMirror API to populate it:\ncm.setGutterMarker(lineNumber, \"error-gutter\", markerElement);\n// This call is never made anywhere in the codebase for parse errors.\n\n// Result: error-gutter exists (takes up space) but is permanently empty", + "fix": "Complete the error-gutter integration: when the ZenUML parser throws, extract the line number from the ANTLR error, create a styled marker element (e.g., a red circle with an ✕ icon), and call cm.setGutterMarker(errorLine - 1, \"error-gutter\", marker). Clear markers on valid parse. This provides exactly the kind of inline feedback VS Code and other editors show.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #9 Error Messages" + ] + }, + { + "id": "GAP-22-004", + "severity": "medium", + "title": "Editor and canvas desync after undo past initial load — canvas shows stale content while editor is empty", + "paragraphs": [ + "After typing invalid syntax and pressing Cmd+Z ten times, the editor empties completely (line 1 blank) while the canvas continues to show a fully rendered diagram (the previously saved \"Auth Flow\" diagram). The editor state and the rendered canvas state are out of sync.", + "This represents a violation of the \"what you see is what you get\" contract: the canvas is showing a diagram that is no longer represented by any editor content. A user in this state would:", + "• Think they still have a valid diagram when the editor shows nothing • Be unable to determine what code produced the current canvas • Be confused if they save — which version gets saved, the empty editor or the visible canvas?" + ], + "evidence": "// Observed state after 10× Cmd+Z:\neditor.getValue() → \"\" (empty)\ncanvas iframe shows: A→B: Login, B→C: Validate, 3. token, 4. Welcome!\n// The two panels disagree on the diagram content\n// Undo history exhausted past the initial load state\n// No visual indicator that editor/canvas are out of sync", + "fix": "When the editor is emptied (content length drops to 0), either: (a) render an empty-state placeholder in the canvas rather than the last diagram, or (b) set a minimum editor content of the default sample diagram (preventing undo past initial load). Track the \"floor\" undo state — when undo reaches the initial content, stop further undos. CodeMirror supports this via cm.clearHistory() at load time to set the baseline.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #3 User Control", + "Nielsen #4 Consistency" + ] + }, + { + "id": "GAP-22-005", + "severity": "low", + "title": "Internal _STARTER_ participant name leaks into the visible canvas on parse failure", + "paragraphs": [ + "When ZenUML fails to parse the input, it renders a fallback participant named _STARTER_ — an internal implementation detail. This participant is visible in the canvas iframe as a stickman box with no label. While the participant label itself is hidden (the stickman icon is the only rendering), the text _STARTER_ is present in the DOM (confirmed via allVisibleText: [\"Click to add title\", \"_STARTER_\"]) and could be visible in exported PNGs or in certain zoom/render states.", + "Users who export the diagram in an error state (not knowing the parse failed silently) may receive a PNG with a single unlabeled stickman — and have no idea why their diagram is \"empty.\"" + ], + "evidence": "// Iframe DOM audit during invalid syntax state:\ndoc.querySelectorAll('[class*=\"participant\"]').length → 1\nallVisibleText: [\"Click to add title\", \"_STARTER_\"]\n// lifeline class: \"lifeline absolute flex flex-col h-full starter\"\n// The \"starter\" CSS class and \"_STARTER_\" text are internal implementation names\n// exposed to the DOM when parsing fails", + "fix": "Replace the _STARTER_ fallback rendering with an explicit empty state component that shows a message like \"Your diagram will appear here\" or \"Fix the syntax error to see the diagram.\" This separates the intentional empty state from the error state, and prevents internal implementation names from leaking into the visible DOM or exported artifacts.", + "principles": [ + "Nielsen #9 Error Messages", + "Nielsen #2 Match Real World" + ] + } + ] + }, + { + "number": 23, + "slug": "case-23-responsive-mobile", + "title": "Responsive & Mobile Layout UX", + "subtitle": "Desktop-only split pane · 24/25 touch targets fail WCAG 2.5.5 · 0 Tailwind sm: classes · no hamburger menu · no touch events", + "caption": "At simulated 375px viewport (iPhone SE), the entire app crushes into a ~10px-tall header strip with 9×10px touch targets. The split-pane layout never stacks vertically. Frame 2 shows scale simulation of how the app renders on mobile.", + "gif": "ux-case-23-responsive-mobile.gif", + "gaps": [ + { + "id": "GAP-23-001", + "severity": "high", + "title": "Split-pane layout never stacks vertically — both panels become unusably narrow on mobile", + "paragraphs": [ + "The .main-content-area is always display: flex; flex-direction: row with no responsive breakpoint that switches to flex-direction: column. At a 375px iPhone SE viewport, the default 30/70 split gives:", + "• Editor: ~113px wide — a 113px code editor is entirely unusable. A typical variable name exceeds the visible width. • Canvas: ~263px wide — a 3-participant sequence diagram would be horizontally clipped at this width • No toggle to switch to single-pane view — unlike Mermaid Live Editor or StackBlitz which offer \"preview only\" mode on mobile", + "The app has a viewport meta tag set to width=device-width, initial-scale=1, meaning mobile browsers will render the layout at actual screen width rather than shrinking to a \"desktop\" view — making the broken layout fully visible to mobile users." + ], + "evidence": "// CSS audit: .main-content-area has NO responsive flex-direction rules\n.main-content-area { display: flex; flex-direction: row; /* ← permanent, no breakpoint */ }\n\n// At 375px viewport (iPhone SE, scaling from 1920px base):\n// Scale factor: 375 / 1920 = 0.195\neditorWidth = (1920 × 0.30) × 0.195 = ~113px // unusable\ncanvasWidth = (1920 × 0.70) × 0.195 = ~263px // severely clipped\n\n// No responsive stacking found in any stylesheet:\n// Tailwind sm: classes in entire app: 0\n// Tailwind md: classes in entire app: 1 (md:flex — on one element)\n// Tailwind lg: classes in entire app: 2 (lg:inline, lg:block)", + "fix": "Add a responsive stacking breakpoint: at max-width: 768px, set .main-content-area { flex-direction: column; }. Stack the canvas above the editor (diagram-first, as users on mobile are more likely to view than edit). Add a tab-style toggle (\"Edit / Preview\") to switch between the two panes on mobile. This follows the pattern used by CodePen, JSFiddle, and StackBlitz.", + "principles": [ + "Nielsen #4 Consistency", + "Mobile-first design", + "WCAG 1.3.4 Orientation" + ] + }, + { + "id": "GAP-23-002", + "severity": "high", + "title": "24 of 25 interactive elements fail WCAG 2.5.5 touch target minimum (44×44px) at mobile viewport", + "paragraphs": [ + "At a 375px viewport, all interactive elements scale proportionally (0.195×). The resulting touch targets are far below the WCAG 2.5.5 minimum of 44×44px and the WCAG 2.5.8 minimum of 24×24px:" + ], + "evidence": "// JS audit at 375px viewport (scale = 375/1920 = 0.195):\ntotalInteractive: 25\nfailingTouchTargets: 24 // 96% failure rate\npassingTouchTargets: 1 // only 1 element passes\n\n// Worst offenders:\nNew button: desktop 90×33px → mobile 18×6px (needs 44×44px)\nShare Link: desktop 102×40px → mobile 20×8px\nProfile button: desktop 40×40px → mobile 8×8px\nSidebar icons: desktop 44×50px → mobile 9×10px", + "fix": "For mobile-specific touch target sizing: add @media (max-width: 768px) { button, a { min-height: 44px; min-width: 44px; } } as a baseline. Better: redesign the mobile layout to use a bottom tab bar (replacing the sidebar icon column) with full-width touch-friendly items. The existing desktop design is appropriate for pointer devices — the issue is there is no separate mobile layout.", + "principles": [ + "WCAG 2.5.5 AA", + "WCAG 2.5.8 AA", + "Fitts's Law" + ] + }, + { + "id": "GAP-23-003", + "severity": "medium", + "title": "No mobile navigation pattern — the full icon sidebar and header persist at all viewport sizes", + "paragraphs": [ + "The left sidebar (5 icon buttons: My Diagrams, Editor, Keyboard, Language Guide, Settings) and the full header (New, diagram title, Share Link, avatar) remain unchanged at all viewport widths. There is no hamburger menu, no bottom tab bar, no collapsible sidebar — the mobile navigation is simply the desktop navigation crushed to unusability.", + "Modern responsive apps follow one of: (a) hamburger menu that slides in a full sidebar, (b) bottom navigation bar for primary destinations, (c) progressive disclosure where secondary nav items hide behind a \"More\" button. ZenUML uses none of these — the sidebar icon column at mobile becomes a 9px-wide strip occupying screen real estate while being untappable." + ], + "evidence": "// Confirmed via DOM audit:\nhamburgerMenu: \"no\" // no hamburger/menu-toggle element\nsidebarToggle: \"none\" // no toggle button found\ntouchEventElements: 0 // no ontouchstart/ontouchmove handlers\npointerMediaQueries: [] // no @media (pointer: coarse) adaptations\n// Tailwind responsive class inventory (entire app):\nsm: classes: 0 // zero adaptations at 640px\nmd: classes: 1 // one element uses md:flex\nlg: classes: 2 // two elements use lg:inline / lg:block\nxl: classes: 0 // zero adaptations above 1280px", + "fix": "Add a mobile navigation pattern: at max-width: 768px, hide the vertical sidebar icon column and replace with a horizontal bottom bar showing the 3-4 most critical actions (My Diagrams, Language Guide, Settings, + a \"...\" overflow). Alternatively, convert the sidebar to a slide-in drawer triggered by a hamburger button in the header. This is the standard pattern for responsive tool apps (Figma, Notion mobile, Linear mobile).", + "principles": [ + "Nielsen #4 Consistency", + "Nielsen #7 Flexibility", + "Mobile UX patterns" + ] + }, + { + "id": "GAP-23-004", + "severity": "medium", + "title": "Header becomes horizontally scrollable at 600px — diagram title and title bar clip silently", + "paragraphs": [ + "At max-width: 600px, the CSS rule .main-header { overflow-x: auto } kicks in, making the header horizontally scrollable. This means at narrow viewports, users must horizontally scroll to access the \"Share Link\" button, the diagram title, and the avatar — but there is no visual indicator (no scroll shadow, no chevron) that horizontal scrolling is available.", + "The \"New\" button and the \"Share Link\" button are on opposite ends of the header. At 400px, a user may only see one of them without scrolling. The diagram title — centrally positioned — may be hidden off-screen. This violates the principle of keeping primary actions within the visible viewport." + ], + "evidence": "// CSS at max-width: 600px:\n.main-header { overflow-x: auto; } // header becomes scrollable\n.main-header__btn-wrap { flex-shrink: 0; } // button group stays at full width\n\n// At 375px:\n// Header height: ~10px (53px × 0.195 scale)\n// Share Link button (rightmost): off-screen without scrolling\n// Diagram title (center): clipped\n// No scroll indicator shown (no -webkit-overflow-scrolling visual)", + "fix": "Redesign the header for mobile: at max-width: 768px, remove the diagram title from the header center (it's secondary information on mobile), show only \"New\" on the left and a \"⋯\" overflow menu on the right containing Share Link and other actions. This keeps all primary actions reachable without horizontal scrolling.", + "principles": [ + "Nielsen #1 Visibility", + "Nielsen #3 User Control" + ] + }, + { + "id": "GAP-23-005", + "severity": "low", + "title": "No touch/pointer event adaptations — swipe gestures and touch-specific diagram interactions absent", + "paragraphs": [ + "The app has zero ontouchstart, ontouchmove, or touch* event handlers anywhere in the DOM (confirmed: touchEventElements: 0). It also has no @media (pointer: coarse) CSS adaptations for touch-screen devices. This means:", + "• No pinch-to-zoom on the canvas — users cannot use native touch gestures to zoom the diagram on a tablet or phone • No swipe-to-switch panels — on mobile, swiping left/right could naturally switch between editor and canvas views • No touch-friendly scrolling in the editor (CodeMirror's touch support is limited without explicit configuration) • Hover-dependent tooltips never trigger on touch devices — all the tooltip information (case 19) is inaccessible on mobile" + ], + "evidence": "// DOM audit:\ntouchEventElements: 0 // no touch event handlers anywhere\npointerMediaQueries: [] // no @media (pointer: coarse) rules\n// CodeMirror touch support requires:\n// cm.setOption(\"lineWrapping\", true) — not set by default\n// Hammer.js or similar for pinch/swipe on canvas — not installed\n// canvas iframe also has no touch event listeners", + "fix": "Add @media (pointer: coarse) CSS rules to increase spacing and target sizes for touch users (even before a full mobile layout redesign). For the canvas, add touch-action: none on the diagram iframe and implement pinch-zoom via the Pointer Events API. For the editor, enable CodeMirror's touch-friendly mode with lineWrapping: true.", + "principles": [ + "WCAG 2.5.5", + "Mobile UX patterns", + "Fitts's Law" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/dev-experience/index.html b/dev-experience/index.html new file mode 100644 index 00000000..5e4711fa --- /dev/null +++ b/dev-experience/index.html @@ -0,0 +1,93 @@ + + + + + + ZenUML DX — UX Gap Tracker + + + + + + +
+
+
+
Total gaps
+
+
+
+
High severity
+
+
+
+
Medium severity
+
+
+
+
Low severity
+
+
+
+
Cases audited
+
+
+
+
Principles cited
+
+
+ +
+
+ + +
+
+
+ Severity +
+ + + + +
+
+
+ Case + +
+
+ Principle + +
+ +
+
+ +
+ + + + + +
+ ZenUML DX · derived from UX cases · gaps tracked +
+ + + + diff --git a/dev-experience/screenshots/GAP-01-001.png b/dev-experience/screenshots/GAP-01-001.png new file mode 100644 index 00000000..4ad710d4 Binary files /dev/null and b/dev-experience/screenshots/GAP-01-001.png differ diff --git a/dev-experience/screenshots/GAP-01-002.png b/dev-experience/screenshots/GAP-01-002.png new file mode 100644 index 00000000..c8b8265b Binary files /dev/null and b/dev-experience/screenshots/GAP-01-002.png differ diff --git a/dev-experience/screenshots/GAP-01-003.png b/dev-experience/screenshots/GAP-01-003.png new file mode 100644 index 00000000..4c7ba0e3 Binary files /dev/null and b/dev-experience/screenshots/GAP-01-003.png differ diff --git a/dev-experience/screenshots/GAP-01-004.png b/dev-experience/screenshots/GAP-01-004.png new file mode 100644 index 00000000..be1b8287 Binary files /dev/null and b/dev-experience/screenshots/GAP-01-004.png differ diff --git a/dev-experience/screenshots/GAP-01-005.png b/dev-experience/screenshots/GAP-01-005.png new file mode 100644 index 00000000..87760768 Binary files /dev/null and b/dev-experience/screenshots/GAP-01-005.png differ diff --git a/dev-experience/screenshots/GAP-01-006.png b/dev-experience/screenshots/GAP-01-006.png new file mode 100644 index 00000000..d286caba Binary files /dev/null and b/dev-experience/screenshots/GAP-01-006.png differ diff --git a/dev-experience/screenshots/GAP-01-007.png b/dev-experience/screenshots/GAP-01-007.png new file mode 100644 index 00000000..278382de Binary files /dev/null and b/dev-experience/screenshots/GAP-01-007.png differ diff --git a/dev-experience/screenshots/GAP-02-001.png b/dev-experience/screenshots/GAP-02-001.png new file mode 100644 index 00000000..f259eea9 Binary files /dev/null and b/dev-experience/screenshots/GAP-02-001.png differ diff --git a/dev-experience/screenshots/GAP-02-002.png b/dev-experience/screenshots/GAP-02-002.png new file mode 100644 index 00000000..be8e63c1 Binary files /dev/null and b/dev-experience/screenshots/GAP-02-002.png differ diff --git a/dev-experience/screenshots/GAP-02-003.png b/dev-experience/screenshots/GAP-02-003.png new file mode 100644 index 00000000..142856c5 Binary files /dev/null and b/dev-experience/screenshots/GAP-02-003.png differ diff --git a/dev-experience/screenshots/GAP-02-004.png b/dev-experience/screenshots/GAP-02-004.png new file mode 100644 index 00000000..4f9d1612 Binary files /dev/null and b/dev-experience/screenshots/GAP-02-004.png differ diff --git a/dev-experience/screenshots/GAP-02-005.png b/dev-experience/screenshots/GAP-02-005.png new file mode 100644 index 00000000..97da6523 Binary files /dev/null and b/dev-experience/screenshots/GAP-02-005.png differ diff --git a/dev-experience/screenshots/GAP-03-001.png b/dev-experience/screenshots/GAP-03-001.png new file mode 100644 index 00000000..8b42596d Binary files /dev/null and b/dev-experience/screenshots/GAP-03-001.png differ diff --git a/dev-experience/screenshots/GAP-03-002.png b/dev-experience/screenshots/GAP-03-002.png new file mode 100644 index 00000000..2a173813 Binary files /dev/null and b/dev-experience/screenshots/GAP-03-002.png differ diff --git a/dev-experience/screenshots/GAP-03-003.png b/dev-experience/screenshots/GAP-03-003.png new file mode 100644 index 00000000..6e4a58f1 Binary files /dev/null and b/dev-experience/screenshots/GAP-03-003.png differ diff --git a/dev-experience/screenshots/GAP-03-004.png b/dev-experience/screenshots/GAP-03-004.png new file mode 100644 index 00000000..98ba2bbd Binary files /dev/null and b/dev-experience/screenshots/GAP-03-004.png differ diff --git a/dev-experience/screenshots/GAP-03-005.png b/dev-experience/screenshots/GAP-03-005.png new file mode 100644 index 00000000..e505e147 Binary files /dev/null and b/dev-experience/screenshots/GAP-03-005.png differ diff --git a/dev-experience/screenshots/GAP-04-001.png b/dev-experience/screenshots/GAP-04-001.png new file mode 100644 index 00000000..6bb97a14 Binary files /dev/null and b/dev-experience/screenshots/GAP-04-001.png differ diff --git a/dev-experience/screenshots/GAP-04-002.png b/dev-experience/screenshots/GAP-04-002.png new file mode 100644 index 00000000..ff6ed947 Binary files /dev/null and b/dev-experience/screenshots/GAP-04-002.png differ diff --git a/dev-experience/screenshots/GAP-04-003.png b/dev-experience/screenshots/GAP-04-003.png new file mode 100644 index 00000000..46f8bde9 Binary files /dev/null and b/dev-experience/screenshots/GAP-04-003.png differ diff --git a/dev-experience/screenshots/GAP-04-004.png b/dev-experience/screenshots/GAP-04-004.png new file mode 100644 index 00000000..d1546494 Binary files /dev/null and b/dev-experience/screenshots/GAP-04-004.png differ diff --git a/dev-experience/screenshots/GAP-04-005.png b/dev-experience/screenshots/GAP-04-005.png new file mode 100644 index 00000000..62338aaf Binary files /dev/null and b/dev-experience/screenshots/GAP-04-005.png differ diff --git a/dev-experience/screenshots/GAP-04-006.png b/dev-experience/screenshots/GAP-04-006.png new file mode 100644 index 00000000..ae047c3c Binary files /dev/null and b/dev-experience/screenshots/GAP-04-006.png differ diff --git a/dev-experience/screenshots/GAP-05-001.png b/dev-experience/screenshots/GAP-05-001.png new file mode 100644 index 00000000..7884dfb2 Binary files /dev/null and b/dev-experience/screenshots/GAP-05-001.png differ diff --git a/dev-experience/screenshots/GAP-05-002.png b/dev-experience/screenshots/GAP-05-002.png new file mode 100644 index 00000000..f86a7b4a Binary files /dev/null and b/dev-experience/screenshots/GAP-05-002.png differ diff --git a/dev-experience/screenshots/GAP-05-003.png b/dev-experience/screenshots/GAP-05-003.png new file mode 100644 index 00000000..b56a473b Binary files /dev/null and b/dev-experience/screenshots/GAP-05-003.png differ diff --git a/dev-experience/screenshots/GAP-05-004.png b/dev-experience/screenshots/GAP-05-004.png new file mode 100644 index 00000000..d42feffc Binary files /dev/null and b/dev-experience/screenshots/GAP-05-004.png differ diff --git a/dev-experience/screenshots/GAP-05-005.png b/dev-experience/screenshots/GAP-05-005.png new file mode 100644 index 00000000..a510644d Binary files /dev/null and b/dev-experience/screenshots/GAP-05-005.png differ diff --git a/dev-experience/screenshots/GAP-06-001.png b/dev-experience/screenshots/GAP-06-001.png new file mode 100644 index 00000000..16864938 Binary files /dev/null and b/dev-experience/screenshots/GAP-06-001.png differ diff --git a/dev-experience/screenshots/GAP-06-002.png b/dev-experience/screenshots/GAP-06-002.png new file mode 100644 index 00000000..b1acdb21 Binary files /dev/null and b/dev-experience/screenshots/GAP-06-002.png differ diff --git a/dev-experience/screenshots/GAP-06-003.png b/dev-experience/screenshots/GAP-06-003.png new file mode 100644 index 00000000..b1d475c6 Binary files /dev/null and b/dev-experience/screenshots/GAP-06-003.png differ diff --git a/dev-experience/screenshots/GAP-06-004.png b/dev-experience/screenshots/GAP-06-004.png new file mode 100644 index 00000000..a07140ef Binary files /dev/null and b/dev-experience/screenshots/GAP-06-004.png differ diff --git a/dev-experience/screenshots/GAP-06-005.png b/dev-experience/screenshots/GAP-06-005.png new file mode 100644 index 00000000..1f8b9bb0 Binary files /dev/null and b/dev-experience/screenshots/GAP-06-005.png differ diff --git a/dev-experience/screenshots/GAP-07-001.png b/dev-experience/screenshots/GAP-07-001.png new file mode 100644 index 00000000..c491186c Binary files /dev/null and b/dev-experience/screenshots/GAP-07-001.png differ diff --git a/dev-experience/screenshots/GAP-07-002.png b/dev-experience/screenshots/GAP-07-002.png new file mode 100644 index 00000000..ca532e1f Binary files /dev/null and b/dev-experience/screenshots/GAP-07-002.png differ diff --git a/dev-experience/screenshots/GAP-07-003.png b/dev-experience/screenshots/GAP-07-003.png new file mode 100644 index 00000000..4caa2370 Binary files /dev/null and b/dev-experience/screenshots/GAP-07-003.png differ diff --git a/dev-experience/screenshots/GAP-07-004.png b/dev-experience/screenshots/GAP-07-004.png new file mode 100644 index 00000000..051051bd Binary files /dev/null and b/dev-experience/screenshots/GAP-07-004.png differ diff --git a/dev-experience/screenshots/GAP-08-001.png b/dev-experience/screenshots/GAP-08-001.png new file mode 100644 index 00000000..4f412708 Binary files /dev/null and b/dev-experience/screenshots/GAP-08-001.png differ diff --git a/dev-experience/screenshots/GAP-08-002.png b/dev-experience/screenshots/GAP-08-002.png new file mode 100644 index 00000000..237adac4 Binary files /dev/null and b/dev-experience/screenshots/GAP-08-002.png differ diff --git a/dev-experience/screenshots/GAP-08-003.png b/dev-experience/screenshots/GAP-08-003.png new file mode 100644 index 00000000..5dc220ce Binary files /dev/null and b/dev-experience/screenshots/GAP-08-003.png differ diff --git a/dev-experience/screenshots/GAP-08-004.png b/dev-experience/screenshots/GAP-08-004.png new file mode 100644 index 00000000..0595fef7 Binary files /dev/null and b/dev-experience/screenshots/GAP-08-004.png differ diff --git a/dev-experience/screenshots/GAP-08-005.png b/dev-experience/screenshots/GAP-08-005.png new file mode 100644 index 00000000..ed96a8ae Binary files /dev/null and b/dev-experience/screenshots/GAP-08-005.png differ diff --git a/dev-experience/screenshots/GAP-09-001.png b/dev-experience/screenshots/GAP-09-001.png new file mode 100644 index 00000000..8cec1ded Binary files /dev/null and b/dev-experience/screenshots/GAP-09-001.png differ diff --git a/dev-experience/screenshots/GAP-09-002.png b/dev-experience/screenshots/GAP-09-002.png new file mode 100644 index 00000000..eb139e43 Binary files /dev/null and b/dev-experience/screenshots/GAP-09-002.png differ diff --git a/dev-experience/screenshots/GAP-09-003.png b/dev-experience/screenshots/GAP-09-003.png new file mode 100644 index 00000000..f892eaca Binary files /dev/null and b/dev-experience/screenshots/GAP-09-003.png differ diff --git a/dev-experience/screenshots/GAP-09-004.png b/dev-experience/screenshots/GAP-09-004.png new file mode 100644 index 00000000..e25dabc4 Binary files /dev/null and b/dev-experience/screenshots/GAP-09-004.png differ diff --git a/dev-experience/screenshots/GAP-09-005.png b/dev-experience/screenshots/GAP-09-005.png new file mode 100644 index 00000000..9df32383 Binary files /dev/null and b/dev-experience/screenshots/GAP-09-005.png differ diff --git a/dev-experience/screenshots/GAP-10-001.png b/dev-experience/screenshots/GAP-10-001.png new file mode 100644 index 00000000..ded19d4e Binary files /dev/null and b/dev-experience/screenshots/GAP-10-001.png differ diff --git a/dev-experience/screenshots/GAP-10-002.png b/dev-experience/screenshots/GAP-10-002.png new file mode 100644 index 00000000..ad710355 Binary files /dev/null and b/dev-experience/screenshots/GAP-10-002.png differ diff --git a/dev-experience/screenshots/GAP-10-003.png b/dev-experience/screenshots/GAP-10-003.png new file mode 100644 index 00000000..00b4e822 Binary files /dev/null and b/dev-experience/screenshots/GAP-10-003.png differ diff --git a/dev-experience/screenshots/GAP-10-004.png b/dev-experience/screenshots/GAP-10-004.png new file mode 100644 index 00000000..b48b0c2c Binary files /dev/null and b/dev-experience/screenshots/GAP-10-004.png differ diff --git a/dev-experience/screenshots/GAP-10-005.png b/dev-experience/screenshots/GAP-10-005.png new file mode 100644 index 00000000..0ed5dd7e Binary files /dev/null and b/dev-experience/screenshots/GAP-10-005.png differ diff --git a/dev-experience/screenshots/GAP-10-006.png b/dev-experience/screenshots/GAP-10-006.png new file mode 100644 index 00000000..9189dee8 Binary files /dev/null and b/dev-experience/screenshots/GAP-10-006.png differ diff --git a/dev-experience/screenshots/GAP-11-001.png b/dev-experience/screenshots/GAP-11-001.png new file mode 100644 index 00000000..6da0c75a Binary files /dev/null and b/dev-experience/screenshots/GAP-11-001.png differ diff --git a/dev-experience/screenshots/GAP-11-002.png b/dev-experience/screenshots/GAP-11-002.png new file mode 100644 index 00000000..a90fbc97 Binary files /dev/null and b/dev-experience/screenshots/GAP-11-002.png differ diff --git a/dev-experience/screenshots/GAP-11-003.png b/dev-experience/screenshots/GAP-11-003.png new file mode 100644 index 00000000..37fd3369 Binary files /dev/null and b/dev-experience/screenshots/GAP-11-003.png differ diff --git a/dev-experience/screenshots/GAP-11-004.png b/dev-experience/screenshots/GAP-11-004.png new file mode 100644 index 00000000..4eef47ac Binary files /dev/null and b/dev-experience/screenshots/GAP-11-004.png differ diff --git a/dev-experience/screenshots/GAP-11-005.png b/dev-experience/screenshots/GAP-11-005.png new file mode 100644 index 00000000..a443c014 Binary files /dev/null and b/dev-experience/screenshots/GAP-11-005.png differ diff --git a/dev-experience/screenshots/GAP-12-001.png b/dev-experience/screenshots/GAP-12-001.png new file mode 100644 index 00000000..8c4506a0 Binary files /dev/null and b/dev-experience/screenshots/GAP-12-001.png differ diff --git a/dev-experience/screenshots/GAP-12-002.png b/dev-experience/screenshots/GAP-12-002.png new file mode 100644 index 00000000..47ef3394 Binary files /dev/null and b/dev-experience/screenshots/GAP-12-002.png differ diff --git a/dev-experience/screenshots/GAP-12-003.png b/dev-experience/screenshots/GAP-12-003.png new file mode 100644 index 00000000..782f99f2 Binary files /dev/null and b/dev-experience/screenshots/GAP-12-003.png differ diff --git a/dev-experience/screenshots/GAP-12-004.png b/dev-experience/screenshots/GAP-12-004.png new file mode 100644 index 00000000..9dea0126 Binary files /dev/null and b/dev-experience/screenshots/GAP-12-004.png differ diff --git a/dev-experience/screenshots/GAP-12-005.png b/dev-experience/screenshots/GAP-12-005.png new file mode 100644 index 00000000..da3b02cd Binary files /dev/null and b/dev-experience/screenshots/GAP-12-005.png differ diff --git a/dev-experience/screenshots/GAP-13-001.png b/dev-experience/screenshots/GAP-13-001.png new file mode 100644 index 00000000..a4b83e97 Binary files /dev/null and b/dev-experience/screenshots/GAP-13-001.png differ diff --git a/dev-experience/screenshots/GAP-13-002.png b/dev-experience/screenshots/GAP-13-002.png new file mode 100644 index 00000000..059b53c4 Binary files /dev/null and b/dev-experience/screenshots/GAP-13-002.png differ diff --git a/dev-experience/screenshots/GAP-13-003.png b/dev-experience/screenshots/GAP-13-003.png new file mode 100644 index 00000000..ad212204 Binary files /dev/null and b/dev-experience/screenshots/GAP-13-003.png differ diff --git a/dev-experience/screenshots/GAP-13-004.png b/dev-experience/screenshots/GAP-13-004.png new file mode 100644 index 00000000..7d6e7aee Binary files /dev/null and b/dev-experience/screenshots/GAP-13-004.png differ diff --git a/dev-experience/screenshots/GAP-13-005.png b/dev-experience/screenshots/GAP-13-005.png new file mode 100644 index 00000000..c5d5fbac Binary files /dev/null and b/dev-experience/screenshots/GAP-13-005.png differ diff --git a/dev-experience/screenshots/GAP-14-001.png b/dev-experience/screenshots/GAP-14-001.png new file mode 100644 index 00000000..483cac79 Binary files /dev/null and b/dev-experience/screenshots/GAP-14-001.png differ diff --git a/dev-experience/screenshots/GAP-14-002.png b/dev-experience/screenshots/GAP-14-002.png new file mode 100644 index 00000000..aa992541 Binary files /dev/null and b/dev-experience/screenshots/GAP-14-002.png differ diff --git a/dev-experience/screenshots/GAP-14-003.png b/dev-experience/screenshots/GAP-14-003.png new file mode 100644 index 00000000..5b2f26e4 Binary files /dev/null and b/dev-experience/screenshots/GAP-14-003.png differ diff --git a/dev-experience/screenshots/GAP-14-004.png b/dev-experience/screenshots/GAP-14-004.png new file mode 100644 index 00000000..90a154e5 Binary files /dev/null and b/dev-experience/screenshots/GAP-14-004.png differ diff --git a/dev-experience/screenshots/GAP-14-005.png b/dev-experience/screenshots/GAP-14-005.png new file mode 100644 index 00000000..18fb0018 Binary files /dev/null and b/dev-experience/screenshots/GAP-14-005.png differ diff --git a/dev-experience/screenshots/GAP-15-001.png b/dev-experience/screenshots/GAP-15-001.png new file mode 100644 index 00000000..2a8b9925 Binary files /dev/null and b/dev-experience/screenshots/GAP-15-001.png differ diff --git a/dev-experience/screenshots/GAP-15-002.png b/dev-experience/screenshots/GAP-15-002.png new file mode 100644 index 00000000..2fc5090f Binary files /dev/null and b/dev-experience/screenshots/GAP-15-002.png differ diff --git a/dev-experience/screenshots/GAP-15-003.png b/dev-experience/screenshots/GAP-15-003.png new file mode 100644 index 00000000..d5b0b0ee Binary files /dev/null and b/dev-experience/screenshots/GAP-15-003.png differ diff --git a/dev-experience/screenshots/GAP-15-004.png b/dev-experience/screenshots/GAP-15-004.png new file mode 100644 index 00000000..eef6efc4 Binary files /dev/null and b/dev-experience/screenshots/GAP-15-004.png differ diff --git a/dev-experience/screenshots/GAP-15-005.png b/dev-experience/screenshots/GAP-15-005.png new file mode 100644 index 00000000..86a61d20 Binary files /dev/null and b/dev-experience/screenshots/GAP-15-005.png differ diff --git a/dev-experience/screenshots/GAP-16-001.png b/dev-experience/screenshots/GAP-16-001.png new file mode 100644 index 00000000..4e6e0431 Binary files /dev/null and b/dev-experience/screenshots/GAP-16-001.png differ diff --git a/dev-experience/screenshots/GAP-16-002.png b/dev-experience/screenshots/GAP-16-002.png new file mode 100644 index 00000000..54f840b4 Binary files /dev/null and b/dev-experience/screenshots/GAP-16-002.png differ diff --git a/dev-experience/screenshots/GAP-16-003.png b/dev-experience/screenshots/GAP-16-003.png new file mode 100644 index 00000000..357e1a2c Binary files /dev/null and b/dev-experience/screenshots/GAP-16-003.png differ diff --git a/dev-experience/screenshots/GAP-16-004.png b/dev-experience/screenshots/GAP-16-004.png new file mode 100644 index 00000000..b8716bfa Binary files /dev/null and b/dev-experience/screenshots/GAP-16-004.png differ diff --git a/dev-experience/screenshots/GAP-16-005.png b/dev-experience/screenshots/GAP-16-005.png new file mode 100644 index 00000000..4c5a0e92 Binary files /dev/null and b/dev-experience/screenshots/GAP-16-005.png differ diff --git a/dev-experience/screenshots/GAP-16-006.png b/dev-experience/screenshots/GAP-16-006.png new file mode 100644 index 00000000..3e957912 Binary files /dev/null and b/dev-experience/screenshots/GAP-16-006.png differ diff --git a/dev-experience/screenshots/GAP-17-001.png b/dev-experience/screenshots/GAP-17-001.png new file mode 100644 index 00000000..cb32f331 Binary files /dev/null and b/dev-experience/screenshots/GAP-17-001.png differ diff --git a/dev-experience/screenshots/GAP-17-002.png b/dev-experience/screenshots/GAP-17-002.png new file mode 100644 index 00000000..97359613 Binary files /dev/null and b/dev-experience/screenshots/GAP-17-002.png differ diff --git a/dev-experience/screenshots/GAP-17-003.png b/dev-experience/screenshots/GAP-17-003.png new file mode 100644 index 00000000..bfbff688 Binary files /dev/null and b/dev-experience/screenshots/GAP-17-003.png differ diff --git a/dev-experience/screenshots/GAP-17-004.png b/dev-experience/screenshots/GAP-17-004.png new file mode 100644 index 00000000..9d25a95b Binary files /dev/null and b/dev-experience/screenshots/GAP-17-004.png differ diff --git a/dev-experience/screenshots/GAP-17-005.png b/dev-experience/screenshots/GAP-17-005.png new file mode 100644 index 00000000..013442e3 Binary files /dev/null and b/dev-experience/screenshots/GAP-17-005.png differ diff --git a/dev-experience/screenshots/GAP-18-001.png b/dev-experience/screenshots/GAP-18-001.png new file mode 100644 index 00000000..c3e216b4 Binary files /dev/null and b/dev-experience/screenshots/GAP-18-001.png differ diff --git a/dev-experience/screenshots/GAP-18-002.png b/dev-experience/screenshots/GAP-18-002.png new file mode 100644 index 00000000..2b666982 Binary files /dev/null and b/dev-experience/screenshots/GAP-18-002.png differ diff --git a/dev-experience/screenshots/GAP-18-003.png b/dev-experience/screenshots/GAP-18-003.png new file mode 100644 index 00000000..a6ce50fe Binary files /dev/null and b/dev-experience/screenshots/GAP-18-003.png differ diff --git a/dev-experience/screenshots/GAP-18-004.png b/dev-experience/screenshots/GAP-18-004.png new file mode 100644 index 00000000..ed677e29 Binary files /dev/null and b/dev-experience/screenshots/GAP-18-004.png differ diff --git a/dev-experience/screenshots/GAP-18-005.png b/dev-experience/screenshots/GAP-18-005.png new file mode 100644 index 00000000..d76c913b Binary files /dev/null and b/dev-experience/screenshots/GAP-18-005.png differ diff --git a/dev-experience/screenshots/GAP-19-001.png b/dev-experience/screenshots/GAP-19-001.png new file mode 100644 index 00000000..ac68379c Binary files /dev/null and b/dev-experience/screenshots/GAP-19-001.png differ diff --git a/dev-experience/screenshots/GAP-19-002.png b/dev-experience/screenshots/GAP-19-002.png new file mode 100644 index 00000000..346f53f5 Binary files /dev/null and b/dev-experience/screenshots/GAP-19-002.png differ diff --git a/dev-experience/screenshots/GAP-19-003.png b/dev-experience/screenshots/GAP-19-003.png new file mode 100644 index 00000000..a1ed1c0d Binary files /dev/null and b/dev-experience/screenshots/GAP-19-003.png differ diff --git a/dev-experience/screenshots/GAP-19-004.png b/dev-experience/screenshots/GAP-19-004.png new file mode 100644 index 00000000..010aa24c Binary files /dev/null and b/dev-experience/screenshots/GAP-19-004.png differ diff --git a/dev-experience/screenshots/GAP-19-005.png b/dev-experience/screenshots/GAP-19-005.png new file mode 100644 index 00000000..b136e67d Binary files /dev/null and b/dev-experience/screenshots/GAP-19-005.png differ diff --git a/dev-experience/screenshots/GAP-20-001.png b/dev-experience/screenshots/GAP-20-001.png new file mode 100644 index 00000000..d04039ad Binary files /dev/null and b/dev-experience/screenshots/GAP-20-001.png differ diff --git a/dev-experience/screenshots/GAP-20-002.png b/dev-experience/screenshots/GAP-20-002.png new file mode 100644 index 00000000..feb9ede9 Binary files /dev/null and b/dev-experience/screenshots/GAP-20-002.png differ diff --git a/dev-experience/screenshots/GAP-20-003.png b/dev-experience/screenshots/GAP-20-003.png new file mode 100644 index 00000000..14364d6a Binary files /dev/null and b/dev-experience/screenshots/GAP-20-003.png differ diff --git a/dev-experience/screenshots/GAP-20-004.png b/dev-experience/screenshots/GAP-20-004.png new file mode 100644 index 00000000..1eb843b2 Binary files /dev/null and b/dev-experience/screenshots/GAP-20-004.png differ diff --git a/dev-experience/screenshots/GAP-20-005.png b/dev-experience/screenshots/GAP-20-005.png new file mode 100644 index 00000000..26e94910 Binary files /dev/null and b/dev-experience/screenshots/GAP-20-005.png differ diff --git a/dev-experience/screenshots/GAP-21-001.png b/dev-experience/screenshots/GAP-21-001.png new file mode 100644 index 00000000..4912bfea Binary files /dev/null and b/dev-experience/screenshots/GAP-21-001.png differ diff --git a/dev-experience/screenshots/GAP-21-002.png b/dev-experience/screenshots/GAP-21-002.png new file mode 100644 index 00000000..5a20ef42 Binary files /dev/null and b/dev-experience/screenshots/GAP-21-002.png differ diff --git a/dev-experience/screenshots/GAP-21-003.png b/dev-experience/screenshots/GAP-21-003.png new file mode 100644 index 00000000..42a31801 Binary files /dev/null and b/dev-experience/screenshots/GAP-21-003.png differ diff --git a/dev-experience/screenshots/GAP-21-004.png b/dev-experience/screenshots/GAP-21-004.png new file mode 100644 index 00000000..c1041a08 Binary files /dev/null and b/dev-experience/screenshots/GAP-21-004.png differ diff --git a/dev-experience/screenshots/GAP-21-005.png b/dev-experience/screenshots/GAP-21-005.png new file mode 100644 index 00000000..d757f2f8 Binary files /dev/null and b/dev-experience/screenshots/GAP-21-005.png differ diff --git a/dev-experience/screenshots/GAP-22-001.png b/dev-experience/screenshots/GAP-22-001.png new file mode 100644 index 00000000..ace92f39 Binary files /dev/null and b/dev-experience/screenshots/GAP-22-001.png differ diff --git a/dev-experience/screenshots/GAP-22-002.png b/dev-experience/screenshots/GAP-22-002.png new file mode 100644 index 00000000..b187a43d Binary files /dev/null and b/dev-experience/screenshots/GAP-22-002.png differ diff --git a/dev-experience/screenshots/GAP-22-003.png b/dev-experience/screenshots/GAP-22-003.png new file mode 100644 index 00000000..fd40108c Binary files /dev/null and b/dev-experience/screenshots/GAP-22-003.png differ diff --git a/dev-experience/screenshots/GAP-22-004.png b/dev-experience/screenshots/GAP-22-004.png new file mode 100644 index 00000000..48073015 Binary files /dev/null and b/dev-experience/screenshots/GAP-22-004.png differ diff --git a/dev-experience/screenshots/GAP-22-005.png b/dev-experience/screenshots/GAP-22-005.png new file mode 100644 index 00000000..f921e853 Binary files /dev/null and b/dev-experience/screenshots/GAP-22-005.png differ diff --git a/dev-experience/screenshots/GAP-23-001.png b/dev-experience/screenshots/GAP-23-001.png new file mode 100644 index 00000000..5df988e3 Binary files /dev/null and b/dev-experience/screenshots/GAP-23-001.png differ diff --git a/dev-experience/screenshots/GAP-23-002.png b/dev-experience/screenshots/GAP-23-002.png new file mode 100644 index 00000000..36206e15 Binary files /dev/null and b/dev-experience/screenshots/GAP-23-002.png differ diff --git a/dev-experience/screenshots/GAP-23-003.png b/dev-experience/screenshots/GAP-23-003.png new file mode 100644 index 00000000..c7b062e6 Binary files /dev/null and b/dev-experience/screenshots/GAP-23-003.png differ diff --git a/dev-experience/screenshots/GAP-23-004.png b/dev-experience/screenshots/GAP-23-004.png new file mode 100644 index 00000000..a5bd6ff5 Binary files /dev/null and b/dev-experience/screenshots/GAP-23-004.png differ diff --git a/dev-experience/screenshots/GAP-23-005.png b/dev-experience/screenshots/GAP-23-005.png new file mode 100644 index 00000000..b6e382e0 Binary files /dev/null and b/dev-experience/screenshots/GAP-23-005.png differ diff --git a/dev-experience/screenshots/case-01-base.png b/dev-experience/screenshots/case-01-base.png new file mode 100644 index 00000000..2b6dfee6 Binary files /dev/null and b/dev-experience/screenshots/case-01-base.png differ diff --git a/dev-experience/screenshots/case-02-base.png b/dev-experience/screenshots/case-02-base.png new file mode 100644 index 00000000..5e4623ce Binary files /dev/null and b/dev-experience/screenshots/case-02-base.png differ diff --git a/dev-experience/screenshots/case-03-base.png b/dev-experience/screenshots/case-03-base.png new file mode 100644 index 00000000..6271155c Binary files /dev/null and b/dev-experience/screenshots/case-03-base.png differ diff --git a/dev-experience/screenshots/case-04-base.png b/dev-experience/screenshots/case-04-base.png new file mode 100644 index 00000000..36ff0bc7 Binary files /dev/null and b/dev-experience/screenshots/case-04-base.png differ diff --git a/dev-experience/screenshots/case-05-base.png b/dev-experience/screenshots/case-05-base.png new file mode 100644 index 00000000..e09a695c Binary files /dev/null and b/dev-experience/screenshots/case-05-base.png differ diff --git a/dev-experience/screenshots/case-06-base.png b/dev-experience/screenshots/case-06-base.png new file mode 100644 index 00000000..ea216adb Binary files /dev/null and b/dev-experience/screenshots/case-06-base.png differ diff --git a/dev-experience/screenshots/case-07-base.png b/dev-experience/screenshots/case-07-base.png new file mode 100644 index 00000000..6da1d8c5 Binary files /dev/null and b/dev-experience/screenshots/case-07-base.png differ diff --git a/dev-experience/screenshots/case-08-base.png b/dev-experience/screenshots/case-08-base.png new file mode 100644 index 00000000..11f75c0d Binary files /dev/null and b/dev-experience/screenshots/case-08-base.png differ diff --git a/dev-experience/screenshots/case-09-base.png b/dev-experience/screenshots/case-09-base.png new file mode 100644 index 00000000..9d570028 Binary files /dev/null and b/dev-experience/screenshots/case-09-base.png differ diff --git a/dev-experience/screenshots/case-10-base.png b/dev-experience/screenshots/case-10-base.png new file mode 100644 index 00000000..09acf12b Binary files /dev/null and b/dev-experience/screenshots/case-10-base.png differ diff --git a/dev-experience/screenshots/case-11-base.png b/dev-experience/screenshots/case-11-base.png new file mode 100644 index 00000000..2f911ba3 Binary files /dev/null and b/dev-experience/screenshots/case-11-base.png differ diff --git a/dev-experience/screenshots/case-12-base.png b/dev-experience/screenshots/case-12-base.png new file mode 100644 index 00000000..c3b910d7 Binary files /dev/null and b/dev-experience/screenshots/case-12-base.png differ diff --git a/dev-experience/screenshots/case-13-base.png b/dev-experience/screenshots/case-13-base.png new file mode 100644 index 00000000..7b21b2ee Binary files /dev/null and b/dev-experience/screenshots/case-13-base.png differ diff --git a/dev-experience/screenshots/case-14-base.png b/dev-experience/screenshots/case-14-base.png new file mode 100644 index 00000000..6fb01f47 Binary files /dev/null and b/dev-experience/screenshots/case-14-base.png differ diff --git a/dev-experience/screenshots/case-15-base.png b/dev-experience/screenshots/case-15-base.png new file mode 100644 index 00000000..c3be7729 Binary files /dev/null and b/dev-experience/screenshots/case-15-base.png differ diff --git a/dev-experience/screenshots/case-16-base.png b/dev-experience/screenshots/case-16-base.png new file mode 100644 index 00000000..f0ae052e Binary files /dev/null and b/dev-experience/screenshots/case-16-base.png differ diff --git a/dev-experience/screenshots/case-17-base.png b/dev-experience/screenshots/case-17-base.png new file mode 100644 index 00000000..27d748d5 Binary files /dev/null and b/dev-experience/screenshots/case-17-base.png differ diff --git a/dev-experience/screenshots/case-18-base.png b/dev-experience/screenshots/case-18-base.png new file mode 100644 index 00000000..14d68ab8 Binary files /dev/null and b/dev-experience/screenshots/case-18-base.png differ diff --git a/dev-experience/screenshots/case-19-base.png b/dev-experience/screenshots/case-19-base.png new file mode 100644 index 00000000..00ece24f Binary files /dev/null and b/dev-experience/screenshots/case-19-base.png differ diff --git a/dev-experience/screenshots/case-20-base.png b/dev-experience/screenshots/case-20-base.png new file mode 100644 index 00000000..e31c4682 Binary files /dev/null and b/dev-experience/screenshots/case-20-base.png differ diff --git a/dev-experience/screenshots/case-21-base.png b/dev-experience/screenshots/case-21-base.png new file mode 100644 index 00000000..88e2f357 Binary files /dev/null and b/dev-experience/screenshots/case-21-base.png differ diff --git a/dev-experience/screenshots/case-22-base.png b/dev-experience/screenshots/case-22-base.png new file mode 100644 index 00000000..4d396fe8 Binary files /dev/null and b/dev-experience/screenshots/case-22-base.png differ diff --git a/dev-experience/screenshots/case-23-base.png b/dev-experience/screenshots/case-23-base.png new file mode 100644 index 00000000..7eff88c5 Binary files /dev/null and b/dev-experience/screenshots/case-23-base.png differ diff --git a/dev-experience/styles.css b/dev-experience/styles.css new file mode 100644 index 00000000..70d1a7b3 --- /dev/null +++ b/dev-experience/styles.css @@ -0,0 +1,373 @@ +:root { + --bg: #0a0e14; + --bg-1: #0f1620; + --bg-2: #1a2233; + --border: #2a3550; + --border-strong: #3b4a6a; + --text: #e2e8f0; + --text-dim: #94a3b8; + --text-faint: #64748b; + --blue: #60a5fa; + --blue-bg: #0f2042; + --red: #ef4444; + --red-bg: #3f1010; + --yellow: #f59e0b; + --yellow-bg: #3a2208; + --green: #4ade80; + --green-bg: #0a1f0a; + --shadow: 0 4px 14px rgba(0,0,0,0.4); + --mono: ui-monospace, SFMono-Regular, "SF Mono", Consolas, monospace; + --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +html, body { + background: var(--bg); + color: var(--text); + font-family: var(--sans); + line-height: 1.55; + font-size: 15px; + -webkit-font-smoothing: antialiased; +} + +.container { max-width: 1200px; margin: 0 auto; padding: 0 2rem; } + +/* Header */ +.site-header { + background: linear-gradient(180deg, #0f1620 0%, #0a0e14 100%); + border-bottom: 1px solid var(--border); + padding: 2.25rem 0 1.75rem; +} +.header-inner { + max-width: 1200px; + margin: 0 auto; + padding: 0 2rem; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 1rem; +} +.brand { display: flex; align-items: center; gap: 1rem; } +.brand-mark { + width: 52px; height: 52px; + background: linear-gradient(135deg, var(--blue) 0%, #3b82f6 100%); + border-radius: 12px; + display: flex; align-items: center; justify-content: center; + font-family: var(--mono); font-weight: 800; + font-size: 1.4rem; + color: #0a0e14; + box-shadow: 0 2px 8px rgba(96, 165, 250, 0.4); +} +.site-header h1 { font-size: 1.5rem; font-weight: 700; color: #f8fafc; letter-spacing: -0.01em; } +.tagline { color: var(--text-dim); font-size: 0.85rem; margin-top: 0.15rem; } +.header-meta { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; } +.meta-tag { + font-size: 0.75rem; font-family: var(--mono); + padding: 0.3rem 0.7rem; + background: var(--bg-2); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-dim); + text-decoration: none; +} +.meta-tag.link:hover { color: var(--blue); border-color: var(--blue); } + +/* KPI dashboard */ +.kpis { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 0.85rem; + padding-top: 2rem; + padding-bottom: 1.5rem; +} +.kpi { + background: var(--bg-1); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1rem 1.1rem; + cursor: default; + transition: border-color 0.15s, transform 0.15s; +} +.kpi[data-sev] { cursor: pointer; } +.kpi[data-sev]:hover { border-color: var(--border-strong); transform: translateY(-1px); } +.kpi[data-sev].active-sev { border-color: var(--blue); background: var(--blue-bg); } +.kpi-num { font-size: 2rem; font-weight: 800; color: #f8fafc; font-family: var(--mono); line-height: 1; letter-spacing: -0.03em; } +.kpi-num.kpi-high { color: var(--red); } +.kpi-num.kpi-medium { color: var(--yellow); } +.kpi-num.kpi-low { color: var(--blue); } +.kpi-label { font-size: 0.7rem; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.06em; margin-top: 0.5rem; font-weight: 600; } + +/* Filters */ +.filters { + position: sticky; + top: 0; + z-index: 20; + background: rgba(10, 14, 20, 0.92); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + padding-top: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border); + margin-bottom: 1.5rem; +} +.search-row { + display: flex; gap: 0.75rem; align-items: center; + margin-bottom: 0.85rem; +} +#search-input { + flex: 1; + background: var(--bg-1); + border: 1px solid var(--border); + border-radius: 8px; + padding: 0.7rem 1rem; + color: var(--text); + font-size: 0.95rem; + font-family: var(--sans); + outline: none; + transition: border-color 0.15s; +} +#search-input::placeholder { color: var(--text-faint); } +#search-input:focus { border-color: var(--blue); } +.result-count { + font-size: 0.8rem; + color: var(--text-dim); + font-family: var(--mono); + white-space: nowrap; + padding: 0.4rem 0.7rem; + background: var(--bg-1); + border-radius: 6px; +} + +.filter-row { + display: flex; + flex-wrap: wrap; + gap: 1.25rem; + align-items: center; +} +.filter-group { display: flex; gap: 0.6rem; align-items: center; } +.filter-label { font-size: 0.7rem; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; } +.pill-group { display: flex; gap: 0.3rem; } +.pill { + background: var(--bg-1); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.35rem 0.8rem; + color: var(--text-dim); + font-size: 0.78rem; + font-weight: 600; + cursor: pointer; + font-family: var(--sans); + transition: all 0.12s; +} +.pill:hover { border-color: var(--border-strong); color: var(--text); } +.pill.active { background: var(--blue-bg); border-color: var(--blue); color: var(--blue); } +.pill.pill-high.active { background: var(--red-bg); border-color: var(--red); color: var(--red); } +.pill.pill-medium.active { background: var(--yellow-bg); border-color: var(--yellow); color: var(--yellow); } +.pill.pill-low.active { background: var(--blue-bg); border-color: var(--blue); color: var(--blue); } + +select { + background: var(--bg-1); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.4rem 0.7rem; + color: var(--text); + font-size: 0.8rem; + cursor: pointer; + font-family: var(--sans); + outline: none; + min-width: 160px; +} +select:focus { border-color: var(--blue); } + +.reset-btn { + background: transparent; + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.4rem 0.85rem; + color: var(--text-dim); + font-size: 0.78rem; + cursor: pointer; + font-family: var(--sans); + margin-left: auto; + transition: all 0.12s; +} +.reset-btn:hover { border-color: var(--red); color: var(--red); } + +/* Gap list */ +#gap-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(440px, 1fr)); + gap: 1rem; + padding-bottom: 3rem; +} +.gap { + background: var(--bg-1); + border: 1px solid var(--border); + border-left: 3px solid var(--border); + border-radius: 10px; + padding: 1.15rem 1.25rem 1rem; + cursor: pointer; + transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s; + display: flex; + flex-direction: column; + gap: 0.65rem; +} +.gap:hover { transform: translateY(-2px); box-shadow: var(--shadow); border-color: var(--border-strong); } +.gap.gap-high { border-left-color: var(--red); } +.gap.gap-medium { border-left-color: var(--yellow); } +.gap.gap-low { border-left-color: var(--blue); } + +.gap-thumb { + width: calc(100% + 2.5rem); + margin: -1.15rem -1.25rem 0; + display: block; + aspect-ratio: 1568 / 728; + object-fit: cover; + object-position: top; + border-bottom: 1px solid var(--border); + border-top-left-radius: 8px; + border-top-right-radius: 8px; + background: #0a0e14; +} +.modal-screenshot { + width: 100%; + display: block; + border-radius: 8px; + border: 1px solid var(--border); + margin: 0 0 1.25rem; +} +.gap-top { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; } +.gap-id { + font-family: var(--mono); + font-size: 0.7rem; + font-weight: 700; + color: var(--text-faint); + background: var(--bg-2); + padding: 0.15rem 0.5rem; + border-radius: 4px; +} +.sev-badge { + font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; + padding: 0.18rem 0.55rem; border-radius: 4px; +} +.sev-badge.sev-high { background: var(--red-bg); color: var(--red); } +.sev-badge.sev-medium { background: var(--yellow-bg); color: var(--yellow); } +.sev-badge.sev-low { background: var(--blue-bg); color: var(--blue); } + +.case-tag { + font-size: 0.7rem; + color: var(--text-dim); + font-family: var(--mono); + margin-left: auto; +} + +.gap-title { font-size: 0.95rem; font-weight: 600; color: #f8fafc; line-height: 1.35; } +.gap-snippet { color: var(--text-dim); font-size: 0.85rem; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } +.gap-principles { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: auto; padding-top: 0.4rem; } +.principle-chip { + font-size: 0.66rem; + background: var(--bg-2); + border: 1px solid var(--border); + border-radius: 4px; + padding: 0.13rem 0.45rem; + color: var(--text-dim); + font-weight: 500; +} + +/* Empty state */ +.empty { + grid-column: 1 / -1; + text-align: center; + padding: 4rem 2rem; + color: var(--text-faint); +} +.empty h3 { color: var(--text-dim); font-weight: 600; margin-bottom: 0.5rem; } + +/* Modal */ +dialog.gap-modal { + background: var(--bg-1); + color: var(--text); + border: 1px solid var(--border); + border-radius: 14px; + max-width: 760px; + width: 92%; + padding: 0; + box-shadow: 0 20px 60px rgba(0,0,0,0.6); +} +dialog.gap-modal::backdrop { background: rgba(5, 8, 13, 0.75); backdrop-filter: blur(4px); } +.modal-content { padding: 1.75rem 2rem; max-height: 80vh; overflow-y: auto; } +.modal-content h2 { font-size: 1.15rem; font-weight: 700; margin-bottom: 0.75rem; line-height: 1.4; color: #f8fafc; } +.modal-content .meta-row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; margin-bottom: 1.25rem; } +.modal-content p { font-size: 0.92rem; color: var(--text-dim); margin-bottom: 0.85rem; line-height: 1.6; } +.modal-content .evidence { + background: #0a121c; + border: 1px solid #1e3a5f; + border-radius: 8px; + padding: 0.85rem 1.1rem; + margin: 1rem 0; + font-family: var(--mono); + font-size: 0.78rem; + color: #7dd3fc; + white-space: pre-wrap; + word-break: break-word; +} +.modal-content .fix-block { + background: var(--green-bg); + border: 1px solid #14532d; + border-radius: 8px; + padding: 0.85rem 1.1rem; + margin: 1rem 0 1.25rem; + font-size: 0.88rem; + color: #86efac; + line-height: 1.55; +} +.modal-content .fix-block strong { color: #4ade80; display: block; margin-bottom: 0.3rem; } +.modal-content .principles { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-bottom: 1rem; } +.modal-content .source-link { + display: inline-flex; align-items: center; gap: 0.3rem; + font-size: 0.85rem; + color: var(--blue); + text-decoration: none; + margin-top: 0.5rem; + padding: 0.5rem 0.9rem; + background: var(--blue-bg); + border: 1px solid var(--blue); + border-radius: 6px; +} +.modal-content .source-link:hover { background: var(--blue); color: #0a0e14; } + +.modal-close { + position: absolute; + top: 0.9rem; right: 1rem; + background: var(--bg-2); + border: 1px solid var(--border); + color: var(--text-dim); + width: 32px; height: 32px; + border-radius: 6px; + cursor: pointer; + font-size: 1rem; +} +.modal-close:hover { color: var(--text); border-color: var(--border-strong); } + +/* Footer */ +.site-footer { + text-align: center; + padding: 1.5rem 2rem; + color: var(--text-faint); + font-size: 0.78rem; + border-top: 1px solid var(--border); + margin-top: 2rem; + font-family: var(--mono); +} + +@media (max-width: 720px) { + .container { padding: 0 1.25rem; } + .header-inner { padding: 0 1.25rem; } + #gap-list { grid-template-columns: 1fr; } + .filter-row { gap: 0.6rem; } + select { min-width: 130px; flex: 1; } + .reset-btn { margin-left: 0; } + .kpi-num { font-size: 1.55rem; } +} diff --git a/docs/superpowers/specs/2026-05-07-playwright-smoke-tests-design.md b/docs/superpowers/specs/2026-05-07-playwright-smoke-tests-design.md index 28f5b6a2..f30befb3 100644 --- a/docs/superpowers/specs/2026-05-07-playwright-smoke-tests-design.md +++ b/docs/superpowers/specs/2026-05-07-playwright-smoke-tests-design.md @@ -19,7 +19,7 @@ What's missing is test files plus one config bug fix. ## Pre-existing config bug to fix -`playwright.config.js` has `baseURL: 'http://localhost:3001'` and `webServer.url: 'http://localhost:3001'`, but `vite.config.js` runs the dev server on port `3000`. As shipped, the suite cannot reach the app. Change both to `http://localhost:3000`. No other config changes. +`playwright.config.js` has `baseURL: 'http://localhost:23000'` and `webServer.url: 'http://localhost:23000'`, matching the Vite dev server on port `23000`. ## Test file diff --git a/e2e/README.md b/e2e/README.md index ae881501..d8075f9e 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -23,7 +23,7 @@ This directory contains end-to-end tests using Playwright. ## Configuration -- **Base URL**: `http://localhost:3001` (development server) +- **Base URL**: `http://localhost:23000` (development server) - **Browsers**: Chrome, Firefox, Safari - **Retries**: 2 in CI, 0 locally - **Screenshots**: Only on failure @@ -32,11 +32,11 @@ This directory contains end-to-end tests using Playwright. ## Development Server -Tests automatically start the development server (`pnpm dev`) if it's not already running. The server starts on port 3001 and tests wait for it to be ready. +Tests automatically start the development server (`pnpm dev`) if it's not already running. The server starts on port 23000 and tests wait for it to be ready. ## CI Integration Tests are configured for CI environments with: - Reduced parallelism (workers: 1) - Automatic retries (2x) -- Strict mode (forbidOnly: true) \ No newline at end of file +- Strict mode (forbidOnly: true) diff --git a/e2e/tests/smoke.spec.js b/e2e/tests/smoke.spec.js index 04047765..a1880679 100644 --- a/e2e/tests/smoke.spec.js +++ b/e2e/tests/smoke.spec.js @@ -479,6 +479,7 @@ test('default item title at startup is "Untitled"', async ({ page }) => { }); test('saved item survives a page reload with its content intact', async ({ page }) => { + test.setTimeout(60_000); // The biggest persistence guarantee: type → save → reload → content is back. // beforeEach's addInitScript fires on every navigation, so the // loginAndsaveMessageSeen flag is re-set for the post-reload load. diff --git a/functions/index.js b/functions/index.js index 407eef4f..a2be9dbb 100644 --- a/functions/index.js +++ b/functions/index.js @@ -171,7 +171,7 @@ exports.create_share = functions.https.onRequest(async (req, res) => { if (!baseUrl) { if (process.env.FUNCTIONS_EMULATOR === 'true') { // Local development fallback - baseUrl = 'http://localhost:3000'; + baseUrl = 'http://localhost:23000'; } else if (process.env.GCLOUD_PROJECT === 'staging-zenuml-27954') { // Staging environment fallback baseUrl = 'https://staging.zenuml.com'; diff --git a/package.json b/package.json index a76c120a..fee419ed 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-tooltip": "^1.0.7", - "@zenuml/core": "^3.43.2", + "@zenuml/core": "^3.47.8", "clsx": "^2.0.0", "code-blast-codemirror": "chinchang/code-blast-codemirror#web-maker", "codemirror": "^5.65.16", diff --git a/playwright.config.js b/playwright.config.js index e025de75..2f2c1a43 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -8,7 +8,7 @@ export default defineConfig({ workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { - baseURL: 'http://localhost:3000', + baseURL: 'http://localhost:23000', trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', @@ -31,8 +31,8 @@ export default defineConfig({ webServer: { command: 'pnpm dev', - url: 'http://localhost:3000', + url: 'http://localhost:23000', reuseExistingServer: !process.env.CI, timeout: 120 * 1000, }, -}); \ No newline at end of file +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c65489ac..92c47d64 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,25 +16,25 @@ importers: version: 1.2.4 '@headlessui/react': specifier: ^1.7.18 - version: 1.7.19(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.7.19(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@radix-ui/react-dialog': specifier: ^1.0.5 - version: 1.1.15(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.1.15(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@radix-ui/react-dropdown-menu': specifier: ^2.0.6 - version: 2.1.16(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 2.1.16(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@radix-ui/react-radio-group': specifier: ^1.1.3 - version: 1.3.8(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.3.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@radix-ui/react-select': specifier: ^2.0.0 - version: 2.2.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 2.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@radix-ui/react-tooltip': specifier: ^1.0.7 - version: 1.2.8(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.2.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@zenuml/core': - specifier: ^3.43.2 - version: 3.43.2(@babel/core@7.28.3)(@babel/template@7.27.2) + specifier: ^3.47.8 + version: 3.47.8(@babel/core@7.28.3)(@babel/template@7.27.2) clsx: specifier: ^2.0.0 version: 2.1.1 @@ -1168,8 +1168,8 @@ packages: react: ^16 || ^17 || ^18 react-dom: ^16 || ^17 || ^18 - '@headlessui/react@2.2.7': - resolution: {integrity: sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg==} + '@headlessui/react@2.2.10': + resolution: {integrity: sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA==} engines: {node: '>=10'} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc @@ -2053,8 +2053,8 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - '@zenuml/core@3.43.2': - resolution: {integrity: sha512-p08Wu7wlTb2sHNjE7NrUhlEA9c/TLhi9T13lysHhEwxa1VFsdkwJr5x4wK622VtH2Lq3t7TDNXELvcjWp2kp0Q==} + '@zenuml/core@3.47.8': + resolution: {integrity: sha512-5TAotLfdfBd6V6fLLp4pJW/PSeNi6tBUpxKnCB4Rpqp4vkG0a/pDehaMN1lMujRT89A3tPQayPXGgqLNWO3J1Q==} engines: {node: '>=20'} abab@2.0.6: @@ -2893,8 +2893,8 @@ packages: color-string@1.9.1: resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - color-string@2.0.1: - resolution: {integrity: sha512-5z9FbYTZPAo8iKsNEqRNv+OlpBbDcoE+SY9GjLfDUHEfcNNV7tS9eSAlFHEaub/r5tBL9LtskAeq1l9SaoZ5tQ==} + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} engines: {node: '>=18'} color-support@1.1.3: @@ -3254,8 +3254,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.2.6: - resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==} + dompurify@3.4.2: + resolution: {integrity: sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==} domutils@1.7.0: resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} @@ -4221,6 +4221,10 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + homedir-polyfill@1.0.3: resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} engines: {node: '>=0.10.0'} @@ -4310,8 +4314,8 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - immer@10.1.1: - resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} immutability-helper@2.9.1: resolution: {integrity: sha512-r/RmRG8xO06s/k+PIaif2r5rGc3j4Yhc01jSBfwPCXDLYZwp/yxralI37Df1mwmuzcCsen/E/ITKcTEvc1PQmQ==} @@ -4850,8 +4854,8 @@ packages: join-path@1.1.1: resolution: {integrity: sha512-jnt9OC34sLXMLJ6YfPQ2ZEKrR9mB5ZbSnQb4LPaOx1c5rTzxpR33L18jjp0r75mGGTJmsil3qwN1B5IBeTnSSA==} - jotai@2.13.1: - resolution: {integrity: sha512-cRsw6kFeGC9Z/D3egVKrTXRweycZ4z/k7i2MrfCzPYsL9SIWcPXTyqv258/+Ay8VUEcihNiE/coBLE6Kic6b8A==} + jotai@2.20.0: + resolution: {integrity: sha512-b5GAqgmXmXzB4WPaTH26ppk9Sl7AA9WSQX7yfdM+gJ1rFROiWcVbi97gFuN/yVCojOcbcvop2sfLL+fjxW0JVg==} engines: {node: '>=12.20.0'} peerDependencies: '@babel/core': '>=7.0.0' @@ -6259,19 +6263,12 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} - radash@12.1.1: - resolution: {integrity: sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA==} - engines: {node: '>=14.18.0'} - raf@3.4.1: resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} railroad-diagrams@1.0.0: resolution: {integrity: sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==} - ramda@0.28.0: - resolution: {integrity: sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==} - randexp@0.4.6: resolution: {integrity: sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==} engines: {node: '>=0.12'} @@ -6294,10 +6291,10 @@ packages: re2@1.22.1: resolution: {integrity: sha512-E4J0EtgyNLdIr0wTg0dQPefuiqNY29KaLacytiUAYYRzxCG+zOkWoUygt1rI+TA1LrhN49/njrfSO1DHtVC5Vw==} - react-dom@19.1.1: - resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} peerDependencies: - react: ^19.1.1 + react: ^19.2.6 react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -6332,8 +6329,8 @@ packages: '@types/react': optional: true - react@19.1.1: - resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -6628,8 +6625,8 @@ packages: resolution: {integrity: sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==} engines: {node: '>=8'} - scheduler@0.26.0: - resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} schema-utils@4.3.3: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} @@ -7085,14 +7082,19 @@ packages: tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} - tailwind-merge@3.3.1: - resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} tailwindcss@3.4.17: resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} engines: {node: '>=14.0.0'} hasBin: true + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} @@ -8929,26 +8931,26 @@ snapshots: '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@floating-ui/react-dom@2.1.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@floating-ui/dom': 1.7.4 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@floating-ui/react@0.26.28(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@floating-ui/react@0.26.28(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@floating-ui/react-dom': 2.1.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@floating-ui/utils': 0.2.10 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) tabbable: 6.2.0 - '@floating-ui/react@0.27.16(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@floating-ui/react@0.27.16(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@floating-ui/react-dom': 2.1.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@floating-ui/utils': 0.2.10 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) tabbable: 6.2.0 '@floating-ui/utils@0.2.10': {} @@ -9016,26 +9018,26 @@ snapshots: protobufjs: 7.5.4 yargs: 17.7.2 - '@headlessui/react@1.7.19(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@headlessui/react@1.7.19(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@tanstack/react-virtual': 3.13.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@tanstack/react-virtual': 3.13.12(react-dom@19.2.6(react@19.2.6))(react@19.2.6) client-only: 0.0.1 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@headlessui/react@2.2.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@headlessui/react@2.2.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@floating-ui/react': 0.26.28(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@react-aria/focus': 3.21.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@react-aria/interactions': 3.25.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@tanstack/react-virtual': 3.13.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - use-sync-external-store: 1.5.0(react@19.1.1) + '@floating-ui/react': 0.26.28(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/focus': 3.21.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/interactions': 3.25.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/react-virtual': 3.13.12(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + use-sync-external-store: 1.5.0(react@19.2.6) - '@headlessui/tailwindcss@0.2.2(tailwindcss@3.4.17)': + '@headlessui/tailwindcss@0.2.2(tailwindcss@3.4.19)': dependencies: - tailwindcss: 3.4.17 + tailwindcss: 3.4.19 '@humanwhocodes/config-array@0.13.0': dependencies: @@ -9356,320 +9358,320 @@ snapshots: '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-arrow@1.1.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-arrow@1.1.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@radix-ui/react-collection@1.1.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-collection@1.1.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1) - '@radix-ui/react-context': 1.1.2(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-slot': 1.2.3(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(react@19.2.6) + '@radix-ui/react-context': 1.1.2(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@radix-ui/react-compose-refs@1.1.2(react@19.1.1)': + '@radix-ui/react-compose-refs@1.1.2(react@19.2.6)': dependencies: - react: 19.1.1 + react: 19.2.6 - '@radix-ui/react-context@1.1.2(react@19.1.1)': + '@radix-ui/react-context@1.1.2(react@19.2.6)': dependencies: - react: 19.1.1 + react: 19.2.6 - '@radix-ui/react-dialog@1.1.15(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-dialog@1.1.15(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1) - '@radix-ui/react-context': 1.1.2(react@19.1.1) - '@radix-ui/react-dismissable-layer': 1.1.11(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-focus-guards': 1.1.3(react@19.1.1) - '@radix-ui/react-focus-scope': 1.1.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-id': 1.1.1(react@19.1.1) - '@radix-ui/react-portal': 1.1.9(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-presence': 1.1.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-slot': 1.2.3(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(react@19.2.6) + '@radix-ui/react-context': 1.1.2(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-focus-guards': 1.1.3(react@19.2.6) + '@radix-ui/react-focus-scope': 1.1.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(react@19.2.6) aria-hidden: 1.2.6 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-remove-scroll: 2.7.1(react@19.1.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-remove-scroll: 2.7.1(react@19.2.6) - '@radix-ui/react-direction@1.1.1(react@19.1.1)': + '@radix-ui/react-direction@1.1.1(react@19.2.6)': dependencies: - react: 19.1.1 + react: 19.2.6 - '@radix-ui/react-dismissable-layer@1.1.11(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-dismissable-layer@1.1.11(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-callback-ref': 1.1.1(react@19.1.1) - '@radix-ui/react-use-escape-keydown': 1.1.1(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(react@19.2.6) + '@radix-ui/react-use-escape-keydown': 1.1.1(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@radix-ui/react-dropdown-menu@2.1.16(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-dropdown-menu@2.1.16(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1) - '@radix-ui/react-context': 1.1.2(react@19.1.1) - '@radix-ui/react-id': 1.1.1(react@19.1.1) - '@radix-ui/react-menu': 2.1.16(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(react@19.2.6) + '@radix-ui/react-context': 1.1.2(react@19.2.6) + '@radix-ui/react-id': 1.1.1(react@19.2.6) + '@radix-ui/react-menu': 2.1.16(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@radix-ui/react-focus-guards@1.1.3(react@19.1.1)': + '@radix-ui/react-focus-guards@1.1.3(react@19.2.6)': dependencies: - react: 19.1.1 + react: 19.2.6 - '@radix-ui/react-focus-scope@1.1.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-focus-scope@1.1.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-callback-ref': 1.1.1(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@radix-ui/react-id@1.1.1(react@19.1.1)': + '@radix-ui/react-id@1.1.1(react@19.2.6)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(react@19.1.1) - react: 19.1.1 + '@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.6) + react: 19.2.6 - '@radix-ui/react-menu@2.1.16(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-menu@2.1.16(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1) - '@radix-ui/react-context': 1.1.2(react@19.1.1) - '@radix-ui/react-direction': 1.1.1(react@19.1.1) - '@radix-ui/react-dismissable-layer': 1.1.11(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-focus-guards': 1.1.3(react@19.1.1) - '@radix-ui/react-focus-scope': 1.1.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-id': 1.1.1(react@19.1.1) - '@radix-ui/react-popper': 1.2.8(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-portal': 1.1.9(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-presence': 1.1.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-roving-focus': 1.1.11(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-slot': 1.2.3(react@19.1.1) - '@radix-ui/react-use-callback-ref': 1.1.1(react@19.1.1) + '@radix-ui/react-collection': 1.1.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(react@19.2.6) + '@radix-ui/react-context': 1.1.2(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-focus-guards': 1.1.3(react@19.2.6) + '@radix-ui/react-focus-scope': 1.1.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(react@19.2.6) + '@radix-ui/react-popper': 1.2.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(react@19.2.6) aria-hidden: 1.2.6 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-remove-scroll: 2.7.1(react@19.1.1) - - '@radix-ui/react-popper@1.2.8(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': - dependencies: - '@floating-ui/react-dom': 2.1.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-arrow': 1.1.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1) - '@radix-ui/react-context': 1.1.2(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-callback-ref': 1.1.1(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(react@19.1.1) - '@radix-ui/react-use-rect': 1.1.1(react@19.1.1) - '@radix-ui/react-use-size': 1.1.1(react@19.1.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-remove-scroll: 2.7.1(react@19.2.6) + + '@radix-ui/react-popper@1.2.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-arrow': 1.1.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(react@19.2.6) + '@radix-ui/react-context': 1.1.2(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.6) + '@radix-ui/react-use-rect': 1.1.1(react@19.2.6) + '@radix-ui/react-use-size': 1.1.1(react@19.2.6) '@radix-ui/rect': 1.1.1 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@radix-ui/react-portal@1.1.9(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-portal@1.1.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@radix-ui/react-presence@1.1.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-presence@1.1.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@radix-ui/react-primitive@2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-primitive@2.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-slot': 1.2.3(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@radix-ui/react-radio-group@1.3.8(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-radio-group@1.3.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1) - '@radix-ui/react-context': 1.1.2(react@19.1.1) - '@radix-ui/react-direction': 1.1.1(react@19.1.1) - '@radix-ui/react-presence': 1.1.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-roving-focus': 1.1.11(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(react@19.1.1) - '@radix-ui/react-use-previous': 1.1.1(react@19.1.1) - '@radix-ui/react-use-size': 1.1.1(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - - '@radix-ui/react-roving-focus@1.1.11(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-compose-refs': 1.1.2(react@19.2.6) + '@radix-ui/react-context': 1.1.2(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(react@19.2.6) + '@radix-ui/react-use-previous': 1.1.1(react@19.2.6) + '@radix-ui/react-use-size': 1.1.1(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + '@radix-ui/react-roving-focus@1.1.11(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1) - '@radix-ui/react-context': 1.1.2(react@19.1.1) - '@radix-ui/react-direction': 1.1.1(react@19.1.1) - '@radix-ui/react-id': 1.1.1(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-callback-ref': 1.1.1(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - - '@radix-ui/react-select@2.2.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-collection': 1.1.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(react@19.2.6) + '@radix-ui/react-context': 1.1.2(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(react@19.2.6) + '@radix-ui/react-id': 1.1.1(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + '@radix-ui/react-select@2.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1) - '@radix-ui/react-context': 1.1.2(react@19.1.1) - '@radix-ui/react-direction': 1.1.1(react@19.1.1) - '@radix-ui/react-dismissable-layer': 1.1.11(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-focus-guards': 1.1.3(react@19.1.1) - '@radix-ui/react-focus-scope': 1.1.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-id': 1.1.1(react@19.1.1) - '@radix-ui/react-popper': 1.2.8(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-portal': 1.1.9(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-slot': 1.2.3(react@19.1.1) - '@radix-ui/react-use-callback-ref': 1.1.1(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(react@19.1.1) - '@radix-ui/react-use-previous': 1.1.1(react@19.1.1) - '@radix-ui/react-visually-hidden': 1.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-collection': 1.1.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(react@19.2.6) + '@radix-ui/react-context': 1.1.2(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-focus-guards': 1.1.3(react@19.2.6) + '@radix-ui/react-focus-scope': 1.1.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(react@19.2.6) + '@radix-ui/react-popper': 1.2.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.6) + '@radix-ui/react-use-previous': 1.1.1(react@19.2.6) + '@radix-ui/react-visually-hidden': 1.2.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) aria-hidden: 1.2.6 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-remove-scroll: 2.7.1(react@19.1.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-remove-scroll: 2.7.1(react@19.2.6) - '@radix-ui/react-slot@1.2.3(react@19.1.1)': + '@radix-ui/react-slot@1.2.3(react@19.2.6)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1) - react: 19.1.1 + '@radix-ui/react-compose-refs': 1.1.2(react@19.2.6) + react: 19.2.6 - '@radix-ui/react-tooltip@1.2.8(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-tooltip@1.2.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(react@19.1.1) - '@radix-ui/react-context': 1.1.2(react@19.1.1) - '@radix-ui/react-dismissable-layer': 1.1.11(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-id': 1.1.1(react@19.1.1) - '@radix-ui/react-popper': 1.2.8(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-portal': 1.1.9(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-presence': 1.1.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-slot': 1.2.3(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(react@19.1.1) - '@radix-ui/react-visually-hidden': 1.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(react@19.2.6) + '@radix-ui/react-context': 1.1.2(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(react@19.2.6) + '@radix-ui/react-popper': 1.2.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(react@19.2.6) + '@radix-ui/react-visually-hidden': 1.2.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@radix-ui/react-use-callback-ref@1.1.1(react@19.1.1)': + '@radix-ui/react-use-callback-ref@1.1.1(react@19.2.6)': dependencies: - react: 19.1.1 + react: 19.2.6 - '@radix-ui/react-use-controllable-state@1.2.2(react@19.1.1)': + '@radix-ui/react-use-controllable-state@1.2.2(react@19.2.6)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(react@19.1.1) - react: 19.1.1 + '@radix-ui/react-use-effect-event': 0.0.2(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.6) + react: 19.2.6 - '@radix-ui/react-use-effect-event@0.0.2(react@19.1.1)': + '@radix-ui/react-use-effect-event@0.0.2(react@19.2.6)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(react@19.1.1) - react: 19.1.1 + '@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.6) + react: 19.2.6 - '@radix-ui/react-use-escape-keydown@1.1.1(react@19.1.1)': + '@radix-ui/react-use-escape-keydown@1.1.1(react@19.2.6)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(react@19.1.1) - react: 19.1.1 + '@radix-ui/react-use-callback-ref': 1.1.1(react@19.2.6) + react: 19.2.6 - '@radix-ui/react-use-layout-effect@1.1.1(react@19.1.1)': + '@radix-ui/react-use-layout-effect@1.1.1(react@19.2.6)': dependencies: - react: 19.1.1 + react: 19.2.6 - '@radix-ui/react-use-previous@1.1.1(react@19.1.1)': + '@radix-ui/react-use-previous@1.1.1(react@19.2.6)': dependencies: - react: 19.1.1 + react: 19.2.6 - '@radix-ui/react-use-rect@1.1.1(react@19.1.1)': + '@radix-ui/react-use-rect@1.1.1(react@19.2.6)': dependencies: '@radix-ui/rect': 1.1.1 - react: 19.1.1 + react: 19.2.6 - '@radix-ui/react-use-size@1.1.1(react@19.1.1)': + '@radix-ui/react-use-size@1.1.1(react@19.2.6)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(react@19.1.1) - react: 19.1.1 + '@radix-ui/react-use-layout-effect': 1.1.1(react@19.2.6) + react: 19.2.6 - '@radix-ui/react-visually-hidden@1.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-visually-hidden@1.2.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-primitive': 2.1.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) '@radix-ui/rect@1.1.1': {} - '@react-aria/focus@3.21.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@react-aria/focus@3.21.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@react-aria/interactions': 3.25.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@react-aria/utils': 3.30.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@react-types/shared': 3.31.0(react@19.1.1) + '@react-aria/interactions': 3.25.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-aria/utils': 3.30.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-types/shared': 3.31.0(react@19.2.6) '@swc/helpers': 0.5.17 clsx: 2.1.1 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@react-aria/interactions@3.25.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@react-aria/interactions@3.25.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@react-aria/ssr': 3.9.10(react@19.1.1) - '@react-aria/utils': 3.30.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@react-aria/ssr': 3.9.10(react@19.2.6) + '@react-aria/utils': 3.30.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@react-stately/flags': 3.1.2 - '@react-types/shared': 3.31.0(react@19.1.1) + '@react-types/shared': 3.31.0(react@19.2.6) '@swc/helpers': 0.5.17 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - '@react-aria/ssr@3.9.10(react@19.1.1)': + '@react-aria/ssr@3.9.10(react@19.2.6)': dependencies: '@swc/helpers': 0.5.17 - react: 19.1.1 + react: 19.2.6 - '@react-aria/utils@3.30.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@react-aria/utils@3.30.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@react-aria/ssr': 3.9.10(react@19.1.1) + '@react-aria/ssr': 3.9.10(react@19.2.6) '@react-stately/flags': 3.1.2 - '@react-stately/utils': 3.10.8(react@19.1.1) - '@react-types/shared': 3.31.0(react@19.1.1) + '@react-stately/utils': 3.10.8(react@19.2.6) + '@react-types/shared': 3.31.0(react@19.2.6) '@swc/helpers': 0.5.17 clsx: 2.1.1 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) '@react-stately/flags@3.1.2': dependencies: '@swc/helpers': 0.5.17 - '@react-stately/utils@3.10.8(react@19.1.1)': + '@react-stately/utils@3.10.8(react@19.2.6)': dependencies: '@swc/helpers': 0.5.17 - react: 19.1.1 + react: 19.2.6 - '@react-types/shared@3.31.0(react@19.1.1)': + '@react-types/shared@3.31.0(react@19.2.6)': dependencies: - react: 19.1.1 + react: 19.2.6 '@rollup/pluginutils@4.2.1': dependencies: @@ -9742,11 +9744,11 @@ snapshots: dependencies: tslib: 2.8.1 - '@tanstack/react-virtual@3.13.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@tanstack/react-virtual@3.13.12(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@tanstack/virtual-core': 3.13.12 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) '@tanstack/virtual-core@3.13.12': {} @@ -9987,30 +9989,28 @@ snapshots: '@xtuc/long@4.2.2': {} - '@zenuml/core@3.43.2(@babel/core@7.28.3)(@babel/template@7.27.2)': + '@zenuml/core@3.47.8(@babel/core@7.28.3)(@babel/template@7.27.2)': dependencies: - '@floating-ui/react': 0.27.16(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@headlessui/react': 2.2.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@headlessui/tailwindcss': 0.2.2(tailwindcss@3.4.17) + '@floating-ui/react': 0.27.16(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@headlessui/react': 2.2.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@headlessui/tailwindcss': 0.2.2(tailwindcss@3.4.19) antlr4: 4.11.0 class-variance-authority: 0.7.1 clsx: 2.1.1 - color-string: 2.0.1 - dompurify: 3.2.6 - highlight.js: 10.7.3 + color-string: 2.1.4 + dompurify: 3.4.2 + highlight.js: 11.11.1 html-to-image: 1.11.13 - immer: 10.1.1 - jotai: 2.13.1(@babel/core@7.28.3)(@babel/template@7.27.2)(react@19.1.1) + immer: 10.2.0 + jotai: 2.20.0(@babel/core@7.28.3)(@babel/template@7.27.2)(react@19.2.6) lodash: 4.17.21 marked: 4.3.0 pako: 2.1.0 pino: 8.21.0 - radash: 12.1.1 - ramda: 0.28.0 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - tailwind-merge: 3.3.1 - tailwindcss: 3.4.17 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + tailwind-merge: 3.5.0 + tailwindcss: 3.4.19 transitivePeerDependencies: - '@babel/core' - '@babel/template' @@ -10944,7 +10944,7 @@ snapshots: color-name: 1.1.4 simple-swizzle: 0.2.2 - color-string@2.0.1: + color-string@2.1.4: dependencies: color-name: 2.0.0 @@ -11276,7 +11276,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.2.6: + dompurify@3.4.2: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -12724,6 +12724,8 @@ snapshots: highlight.js@10.7.3: {} + highlight.js@11.11.1: {} + homedir-polyfill@1.0.3: dependencies: parse-passwd: 1.0.0 @@ -12851,7 +12853,7 @@ snapshots: immediate@3.0.6: {} - immer@10.1.1: {} + immer@10.2.0: {} immutability-helper@2.9.1: dependencies: @@ -13603,11 +13605,11 @@ snapshots: url-join: 0.0.1 valid-url: 1.0.9 - jotai@2.13.1(@babel/core@7.28.3)(@babel/template@7.27.2)(react@19.1.1): + jotai@2.20.0(@babel/core@7.28.3)(@babel/template@7.27.2)(react@19.2.6): optionalDependencies: '@babel/core': 7.28.3 '@babel/template': 7.27.2 - react: 19.1.1 + react: 19.2.6 js-tokens@4.0.0: {} @@ -15126,16 +15128,12 @@ snapshots: quick-format-unescaped@4.0.4: {} - radash@12.1.1: {} - raf@3.4.1: dependencies: performance-now: 2.1.0 railroad-diagrams@1.0.0: {} - ramda@0.28.0: {} - randexp@0.4.6: dependencies: discontinuous-range: 1.0.0 @@ -15170,35 +15168,35 @@ snapshots: - supports-color optional: true - react-dom@19.1.1(react@19.1.1): + react-dom@19.2.6(react@19.2.6): dependencies: - react: 19.1.1 - scheduler: 0.26.0 + react: 19.2.6 + scheduler: 0.27.0 react-is@16.13.1: {} - react-remove-scroll-bar@2.3.8(react@19.1.1): + react-remove-scroll-bar@2.3.8(react@19.2.6): dependencies: - react: 19.1.1 - react-style-singleton: 2.2.3(react@19.1.1) + react: 19.2.6 + react-style-singleton: 2.2.3(react@19.2.6) tslib: 2.8.1 - react-remove-scroll@2.7.1(react@19.1.1): + react-remove-scroll@2.7.1(react@19.2.6): dependencies: - react: 19.1.1 - react-remove-scroll-bar: 2.3.8(react@19.1.1) - react-style-singleton: 2.2.3(react@19.1.1) + react: 19.2.6 + react-remove-scroll-bar: 2.3.8(react@19.2.6) + react-style-singleton: 2.2.3(react@19.2.6) tslib: 2.8.1 - use-callback-ref: 1.3.3(react@19.1.1) - use-sidecar: 1.1.3(react@19.1.1) + use-callback-ref: 1.3.3(react@19.2.6) + use-sidecar: 1.1.3(react@19.2.6) - react-style-singleton@2.2.3(react@19.1.1): + react-style-singleton@2.2.3(react@19.2.6): dependencies: get-nonce: 1.0.1 - react: 19.1.1 + react: 19.2.6 tslib: 2.8.1 - react@19.1.1: {} + react@19.2.6: {} read-cache@1.0.0: dependencies: @@ -15569,7 +15567,7 @@ snapshots: dependencies: xmlchars: 2.2.0 - scheduler@0.26.0: {} + scheduler@0.27.0: {} schema-utils@4.3.3: dependencies: @@ -16123,7 +16121,7 @@ snapshots: tabbable@6.2.0: {} - tailwind-merge@3.3.1: {} + tailwind-merge@3.5.0: {} tailwindcss@3.4.17: dependencies: @@ -16152,6 +16150,33 @@ snapshots: transitivePeerDependencies: - ts-node + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.0.1(postcss@8.5.6) + postcss-load-config: 4.0.2(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.10 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + tapable@2.3.0: {} tar-stream@3.1.7: @@ -16542,20 +16567,20 @@ snapshots: url-template@2.0.8: {} - use-callback-ref@1.3.3(react@19.1.1): + use-callback-ref@1.3.3(react@19.2.6): dependencies: - react: 19.1.1 + react: 19.2.6 tslib: 2.8.1 - use-sidecar@1.1.3(react@19.1.1): + use-sidecar@1.1.3(react@19.2.6): dependencies: detect-node-es: 1.1.0 - react: 19.1.1 + react: 19.2.6 tslib: 2.8.1 - use-sync-external-store@1.5.0(react@19.1.1): + use-sync-external-store@1.5.0(react@19.2.6): dependencies: - react: 19.1.1 + react: 19.2.6 use@3.1.1: {} diff --git a/src/CodeMirror.js b/src/CodeMirror.js index 63173c93..b7657012 100644 --- a/src/CodeMirror.js +++ b/src/CodeMirror.js @@ -1,6 +1,7 @@ // Most of the code from this file comes from: // https://github.com/codemirror/CodeMirror/blob/master/addon/mode/loadmode.js import CodeMirror from 'codemirror'; +import 'codemirror/addon/display/placeholder'; // Make CodeMirror available globally so the modes' can register themselves. window.CodeMirror = CodeMirror; diff --git a/src/assets/tailwind.css b/src/assets/tailwind.css index 8b151605..0c2b4130 100644 --- a/src/assets/tailwind.css +++ b/src/assets/tailwind.css @@ -2,6 +2,12 @@ @tailwind components; @tailwind utilities; +/* WCAG 2.4.11: Improved focus rings for keyboard navigation */ +:focus-visible { + outline: 2px solid #60a5fa; + outline-offset: 2px; +} + /*NOTE: try to fix tailwind backdrop-blur style after building*/ @layer base { *, diff --git a/src/components/CheatSheetModal.jsx b/src/components/CheatSheetModal.jsx index 272f12c9..40da2342 100644 --- a/src/components/CheatSheetModal.jsx +++ b/src/components/CheatSheetModal.jsx @@ -6,7 +6,7 @@ const CheatSheetModal = ({ open, onClose }) => { - + Cheat sheet @@ -44,7 +44,7 @@ const CheatSheetModal = ({ open, onClose }) => { - Asyc message + Async message
                     
@@ -105,6 +105,16 @@ const CheatSheetModal = ({ open, onClose }) => {
                 
               
             
+            
           
Or choose from a template:
@@ -34,10 +34,6 @@ export default function CreateNewModal({ /> ))}
-
- The development team needs your help. If you are actively using - ZenUML, please tweet about ZenUML at least once a month! -
diff --git a/src/components/DeletePageModal.jsx b/src/components/DeletePageModal.jsx index 3bd0befb..09ea4439 100644 --- a/src/components/DeletePageModal.jsx +++ b/src/components/DeletePageModal.jsx @@ -24,7 +24,7 @@ export default function DeletePageModal({ open, onClose, onConfirm }) { Confirm to Delete - Are you sure you want to delete this page? The data on this page will be lost forever. + This will remove the page and its diagram content. You can undo this with Cmd+Z right after deleting.
-
+ {/* Divider between panels and utility icons */} + diff --git a/src/components/LibraryPanel.jsx b/src/components/LibraryPanel.jsx index 7f0ae157..312e26ce 100644 --- a/src/components/LibraryPanel.jsx +++ b/src/components/LibraryPanel.jsx @@ -407,21 +407,24 @@ export default class LibraryPanel extends Component { @@ -453,9 +456,10 @@ export default class LibraryPanel extends Component { )} {!this.items.length ? ( -
+
folder_off -

Nothing saved yet.

+

Nothing saved yet.

+

Press ⌘S to save your current diagram here.

) : null}
diff --git a/src/components/LoginModal.jsx b/src/components/LoginModal.jsx index 0cc5ac73..b3eefb43 100644 --- a/src/components/LoginModal.jsx +++ b/src/components/LoginModal.jsx @@ -3,7 +3,7 @@ import * as Dialog from '@radix-ui/react-dialog'; import { useEffect } from 'preact/hooks'; import mixpanel from '../services/mixpanel'; -export default function LoginModal({ open, onClose }) { +export default function LoginModal({ open, onClose, reason }) { const login = (e) => { const provider = e.target.dataset.authProvider; mixpanel.track({ @@ -31,14 +31,28 @@ export default function LoginModal({ open, onClose }) { - - + { + // Explicitly focus first login button so keyboard focus stays inside modal + e.preventDefault(); + const firstBtn = e.currentTarget.querySelector('button[data-auth-provider]'); + if (firstBtn) firstBtn.focus(); + }} + className="text-white data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] overflow-hidden max-w-[466px] translate-x-[-50%] translate-y-[-50%] rounded-[6px] bg-black-400 p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none" + > +

Welcome to ZenUML.com

-
+ {reason && ( +

+ {reason} +

+ )} +
-
+
{isEditing ? ( ) : (
entryEditing()} + role="button" + tabIndex={0} + onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') entryEditing(); }} + aria-label={`Diagram title: ${props.title || 'Untitled'} — click to rename`} + title="Click to rename diagram" > - {props.title || 'Untitled'} - + {props.title || 'Untitled'} +
)} + {hasUnsavedChanges && ( + + )}
diff --git a/src/components/OnboardingModal.jsx b/src/components/OnboardingModal.jsx index 47c083b1..c916820f 100644 --- a/src/components/OnboardingModal.jsx +++ b/src/components/OnboardingModal.jsx @@ -1,14 +1,68 @@ -import Modal from './Modal.jsx'; +import * as Dialog from '@radix-ui/react-dialog'; + +export function OnboardingModal({ show, closeHandler }) { + if (!show) return null; -export function OnboardingModal(props) { return ( - -
-

Welcome to ZenUML Sequence

-
-
- ZenUML animation -
-
+ + + + + + Welcome to ZenUML + + + Turn text into sequence diagrams in seconds. + + +
+
+ edit +
+

Type in the editor on the left

+

Your diagram updates live as you type. Try: A->B: hello()

+
+
+
+ quick_reference +
+

Open the Syntax Cheat Sheet

+

Click the book icon in the left sidebar to see available commands.

+
+
+
+ save +
+

Save your work with Cmd+S

+

Diagrams are saved locally. Sign in to access them from any device.

+
+
+
+ download +
+

Export as PNG when ready

+

Use the PNG button in the tab bar. Sign in to unlock exports.

+
+
+
+ + + + + + +
+
+
); } diff --git a/src/components/PageTabs.jsx b/src/components/PageTabs.jsx index d8c66359..50859f5e 100644 --- a/src/components/PageTabs.jsx +++ b/src/components/PageTabs.jsx @@ -2,12 +2,15 @@ import { Component } from 'preact'; import DeletePageModal from './DeletePageModal'; /** - * PageTabs component displays tabs for each page and handles tab switching + * PageTabs component displays tabs for each page and handles tab switching. + * Double-click a tab title to rename the page inline. */ export class PageTabs extends Component { state = { isCloseModalOpen: false, pageToClose: null, + renamingPageId: null, + renameValue: '', }; /** @@ -41,9 +44,44 @@ export class PageTabs extends Component { this.setState({ isCloseModalOpen: false, pageToClose: null }); }; + handleDoubleClick = (e, page) => { + e.preventDefault(); + e.stopPropagation(); + this.setState({ renamingPageId: page.id, renameValue: page.title || '' }); + }; + + handleRenameChange = (e) => { + this.setState({ renameValue: e.target.value }); + }; + + handleRenameKeyDown = (e) => { + if (e.key === 'Enter') { + this.commitRename(); + } else if (e.key === 'Escape') { + this.cancelRename(); + } + }; + + handleRenameBlur = () => { + this.commitRename(); + }; + + commitRename = () => { + const { renamingPageId, renameValue } = this.state; + if (renamingPageId && typeof this.props.onRenamePage === 'function') { + const trimmed = renameValue.trim(); + this.props.onRenamePage(renamingPageId, trimmed || 'Untitled'); + } + this.setState({ renamingPageId: null, renameValue: '' }); + }; + + cancelRename = () => { + this.setState({ renamingPageId: null, renameValue: '' }); + }; + render() { const { pages, currentPageId, onTabClick, onToggleFullscreen, onExportPng, onCopyImage } = this.props; - const { isCloseModalOpen } = this.state; + const { isCloseModalOpen, renamingPageId, renameValue } = this.state; if (!pages || pages.length === 0) { return null; @@ -54,20 +92,36 @@ export class PageTabs extends Component {
{pages.map((page, index) => { + const isRenaming = renamingPageId === page.id; return (
- - {index !== 0 && + )} + {index !== 0 && !isRenaming &&
)} diff --git a/src/components/SettingsModal.jsx b/src/components/SettingsModal.jsx index c0a83d5e..63c8110b 100644 --- a/src/components/SettingsModal.jsx +++ b/src/components/SettingsModal.jsx @@ -2,7 +2,7 @@ import { editorThemes } from '../editorThemes'; import * as Dialog from '@radix-ui/react-dialog'; import { clsx } from 'clsx'; import * as Select from '@radix-ui/react-select'; -import * as RadioGroup from '@radix-ui/react-radio-group'; +import { useState } from 'preact/hooks'; function CheckboxSetting({ title, @@ -38,9 +38,7 @@ function CheckboxSetting({ } export default function SettingsModal(props) { - const updateSetting = (e) => { - console.log(e)(e); - }; + const [showAdvanced, setShowAdvanced] = useState(false); return ( @@ -48,7 +46,8 @@ export default function SettingsModal(props) {
-

Editor

+

Settings

+

Editor Appearance

Theme
@@ -202,6 +201,18 @@ export default function SettingsModal(props) { > + + 10 px + + + 11 px + 18 px + + 20 px + + + 22 px + + + 24 px + @@ -291,21 +320,46 @@ export default function SettingsModal(props) { }); }} /> - { - props.onChange({ - settingName: 'preserveConsoleLogs', - value: e.target.checked, - }); - }} - />
+
+
+ + {showAdvanced && ( +
+ { + props.onChange({ + settingName: 'preserveConsoleLogs', + value: e.target.checked, + }); + }} + /> +
+ )} +
+ {props.onResetDefaults && ( +
+ +
+ )} - - - - - - - -
@@ -1777,13 +1827,15 @@ BookLibService.Borrow(id) { open={this.state.isSettingsModalOpen} prefs={this.state.prefs} onChange={this.updateSetting.bind(this)} + onResetDefaults={this.resetSettingsToDefaults.bind(this)} onClose={async () => await this.setState({ isSettingsModalOpen: false }) } /> await this.setState({ isLoginModalOpen: false })} + reason={this.state.loginReason} + onClose={async () => await this.setState({ isLoginModalOpen: false, loginReason: null })} /> this.setState({ openCheatSheet: false })} /> + await this.setState({ isOnboardModalOpen: false })} + />