Skip to content

Overhaul UI to minimal real-time expression tree visualizer with dark mode and synchronous rendering#4

Merged
psaegert merged 3 commits into
masterfrom
copilot/major-ux-ui-overhaul
Apr 21, 2026
Merged

Overhaul UI to minimal real-time expression tree visualizer with dark mode and synchronous rendering#4
psaegert merged 3 commits into
masterfrom
copilot/major-ux-ui-overhaul

Conversation

Copilot AI commented Apr 21, 2026

Copy link
Copy Markdown

This PR replaces the button-driven, animation-heavy UI with a minimal real-time expression tree visualizer. Rendering now updates on debounced input, preserves the last valid tree on invalid expressions, and adds a persistent light/dark theme with theme-aware canvas drawing.

  • Real-time input-driven rendering

    • Removed Generate flow; added debounced (~150ms) render on #expression-input changes.
    • Invalid expressions no longer clear canvas; last valid tree remains visible and a ⚠️ indicator is shown in the input.
    • Kept initial random sample expression on load and rendered immediately (synchronous path).
  • Rendering pipeline simplification (no animation)

    • Removed all animation code (drawEdgeAnimated, RAF waypoint logic, animate plumbing).
    • drawTree is now synchronous; edges are drawn in a single stroke.
    • Node fill/stroke/text and edge colors are read from CSS variables at draw time for theme consistency.
  • Control/UI redesign

    • Removed: page title, animate toggle, background export selector, old clear/export buttons, SweetAlert usage, patterned backgrounds, top-right floating GitHub badge.
    • Added input-embedded trash icon (clear input + clear canvas).
    • Replaced export button with icon-only save/download control.
  • Export behavior

    • Export is now always transparent PNG (canvas.toDataURL('image/png')), no white-background mode.
  • Layout + theming

    • Rebuilt to minimal centered layout on plain background.
    • Added bottom-centered GitHub icon linking to https://github.com/psaegert/expression-tree-gen.
    • Added bottom dark-mode toggle (sun/moon), persisted in localStorage (etg-theme), with first-load fallback to prefers-color-scheme.
  • Security hardening

    • Added SRI + crossorigin="anonymous" to the MathJS CDN script tag.
const DEBOUNCE_MS = 150;
let debounceId = null;

input.addEventListener('input', () => {
  clearTimeout(debounceId);
  debounceId = setTimeout(renderExpression, DEBOUNCE_MS);
});

function renderExpression() {
  try {
    const postfix = infixToPostfix(input.value.replace(/\s+/g, ''));
    if (postfix === null) return setWarningVisible(true); // keep last valid tree
    currentRoot = constructTree(postfix);
    setWarningVisible(false);
    renderRoot(currentRoot);
  } catch {
    setWarningVisible(true); // keep last valid tree
  }
}

UI overhaul screenshot

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • cdnjs.cloudflare.com
    • Triggering command: /usr/bin/curl curl -Ls REDACTED (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

Goal

Major UX/UI overhaul. Strip the app down to a minimal, centered, real-time expression tree visualizer with a dark-mode toggle.

Scope summary

  1. Real-time rendering — tree updates on every input change (debounced). Invalid expressions leave the last valid tree on screen and show a ⚠️ warning indicator.
  2. Remove the Generate button, the Animate toggle, all animation code, the background-mode selector, and the page title.
  3. Replace the Clear button with a trash-can icon inside the input field (right side).
  4. Replace the Export button with a save/download icon.
  5. Always transparent PNG export. Nodes still filled white (or theme-appropriate — see below).
  6. Minimal centered UI on a plain white page (no background SVG patterns).
  7. GitHub icon at the bottom center linking to the repo.
  8. Dark mode toggle next to the GitHub icon (button or switch). Opt-in, default light.

Files involved

  • index.html — restructure markup
  • app.css — rewrite the layout, remove unused styles, add dark-mode variables
  • canvas.js — real-time input listener with debounce, remove generate/animate logic, wire icon buttons, handle dark mode
  • tree.jsremove all animation code (drawEdgeAnimated, the waypoint/requestAnimationFrame logic). Edges drawn synchronously. drawTree becomes synchronous. Node/edge colors become theme-aware.

Detailed requirements

1. Real-time rendering (canvas.js)

  • Attach an input event listener to #expression-input.
  • Debounce updates by ~150ms (simple setTimeout/clearTimeout pattern — no library).
  • On each debounced fire:
    1. Strip whitespace, call infixToPostfix.
    2. If valid: constructTreesetCoordinatesclearCanvasdrawTree. Hide the warning indicator.
    3. If invalid (null postfix or thrown error): do not clear the canvas. Leave the last valid tree on screen. Show the ⚠️ warning indicator.
  • Remove the existing "Generate" click-handler entirely. Remove the displayErrorMessage() SweetAlert popup and the SweetAlert CDN <script> tag from index.html.
  • Keep the initial random sample-expression behavior on page load, but render synchronously (no setTimeout dance).

Warning indicator: A small ⚠️ emoji (or icon) positioned, say, inside the input field on the right — to the left of the trash icon, only visible when the current input is invalid. A simple absolutely-positioned <span> overlaying the input, toggled via a CSS class like .invalid, works well.

2. Remove animation entirely (tree.js, canvas.js)

  • Delete drawEdgeAnimated and all waypoint/requestAnimationFrame code.
  • Node.drawEdge(context, childNode) becomes synchronous: compute the trimmed endpoints (the logic already computed — startX/Y, endX/Y with radius trimming) and do a single beginPath/moveTo/lineTo/stroke. No resolve callback, no promise.
  • drawTree(root, context) becomes a synchronous, non-async function. Iterate children and recurse. Draw edges before/after children as appropriate — the current order works.
  • Remove the animate parameter and the #animate-toggle element, its CSS, and all related code.

3. Input field with trash-can icon

  • Wrap the <input> in a container so icons can be absolutely positioned inside it.
  • Add a trash-can icon button on the right side inside the input. Use an inline SVG (Feather/Lucide-style, stroke-based) so it adapts to dark mode via currentColor. Reasonable SVG source: the Feather trash-2 icon.
  • Clicking the trash icon clears the input and clears the canvas (replace the old #clear-tree button behavior).
  • Give the input enough right-padding so typed text doesn't overlap the icon(s).

4. Save icon (export)

  • Replace the "Export PNG" button with a save/download icon button. Use Feather download or save inline SVG.
  • Position: below (or adjacent to) the input field, centered. Keep it subtle — icon-only, no filled button.
  • Clicking downloads the current canvas as a transparent PNG (use the existing transparent-mode logic; remove the white-background option and the <select> entirely).

5. Always transparent export

  • Delete the #export-bg-mode select and all branching on 'white' vs 'transparent'.
  • Export is always canvas.toDataURL('image/png') on the current canvas.
  • Trigger download via a temporary <a download> element.

6. Minimal centered layout (app.css, index.html)

  • Remove the .contact-section with the SVG gradient background.
  • Remove the #canvas-container dotted SVG pattern background.
  • Remove the <h1>Expression Tree Generator</h1> title.
  • Remove the old floating .github-link in the top-right corner.
  • Page body: plain white (or dark theme bg when toggled). No background images.
  • Layout:
    • Vertical flex column, centered horizontally on the page.
    • Top stack (centered): input field with trash icon inside, save icon below/next to it, warning indicator when invalid.
    • Ca...

This pull request was created from Copilot chat.

Copilot AI changed the title [WIP] Revamp app for real-time expression tree visualization Overhaul UI to minimal real-time expression tree visualizer with dark mode and synchronous rendering Apr 21, 2026
Copilot AI requested a review from psaegert April 21, 2026 10:09
@psaegert psaegert marked this pull request as ready for review April 21, 2026 10:09
Copilot AI review requested due to automatic review settings April 21, 2026 10:09
@psaegert psaegert merged commit 0a10b82 into master Apr 21, 2026
2 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the app into a minimal, real-time expression tree visualizer: input changes (debounced) trigger synchronous re-rendering, invalid expressions preserve the last valid tree while showing a warning indicator, and a persisted light/dark theme is introduced with theme-aware canvas drawing.

Changes:

  • Replaced the button/animation-driven flow with debounced, input-driven rendering and “keep last valid tree” behavior.
  • Removed animation/export background modes and simplified canvas rendering to synchronous drawing.
  • Rebuilt the UI layout and styling for a minimal centered design with dark-mode support and icon-based controls.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
index.html Restructures markup to minimal controls, icon buttons, footer GitHub link, theme toggle, and adds SRI for MathJS.
app.css Replaces legacy styles with a minimal layout + CSS-variable-based light/dark theme tokens.
canvas.js Implements debounced rendering, “last valid tree” behavior, export, resize handling, and persisted theme toggling.
tree.js Removes edge animation, makes rendering synchronous, and reads draw colors from CSS variables.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread canvas.js
Comment on lines +27 to +32
canvas.height = container.offsetHeight
canvas.width = container.offsetWidth
}

function clearCanvas() {
context.clearRect(0, 0, canvas.width, canvas.height)

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resizeCanvas() sizes the drawing buffer from container.offsetWidth/offsetHeight, but the canvas is styled to width/height: 100% inside a padded container (#canvas-container). This causes the canvas to be scaled by CSS (blurry) and can shift/clip the tree coordinates. Consider sizing from canvas.getBoundingClientRect() (and applying devicePixelRatio) so the backing store matches the rendered canvas size.

Suggested change
canvas.height = container.offsetHeight
canvas.width = container.offsetWidth
}
function clearCanvas() {
context.clearRect(0, 0, canvas.width, canvas.height)
var rect = canvas.getBoundingClientRect()
var dpr = window.devicePixelRatio || 1
var width = Math.round(rect.width * dpr)
var height = Math.round(rect.height * dpr)
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width
canvas.height = height
}
context.setTransform(1, 0, 0, 1, 0, 0)
context.scale(dpr, dpr)
}
function clearCanvas() {
var rect = canvas.getBoundingClientRect()
context.clearRect(0, 0, rect.width, rect.height)

Copilot uses AI. Check for mistakes.
Comment thread canvas.js
}

function setWarningVisible(isVisible) {
warning.hidden = !isVisible

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setWarningVisible() only toggles visibility of the warning icon. Since the warning <span> is aria-hidden and the input doesn’t get aria-invalid, screen readers won’t be informed when the expression becomes invalid. Consider toggling input.setAttribute('aria-invalid', ...) and optionally wiring aria-describedby to a non-hidden message region.

Suggested change
warning.hidden = !isVisible
warning.hidden = !isVisible
input.setAttribute('aria-invalid', isVisible ? 'true' : 'false')
warning.setAttribute('aria-hidden', isVisible ? 'false' : 'true')

Copilot uses AI. Check for mistakes.
Comment thread canvas.js
Comment on lines +90 to +95
function getPreferredTheme() {
var storedTheme = localStorage.getItem(THEME_STORAGE_KEY)
if (storedTheme === 'light' || storedTheme === 'dark') {
return storedTheme
}
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getPreferredTheme() reads from localStorage unguarded; in some environments (e.g., certain privacy modes / blocked storage) localStorage.getItem can throw and prevent the app from loading. Consider wrapping localStorage access in a try/catch and falling back to prefers-color-scheme when storage is unavailable.

Copilot uses AI. Check for mistakes.
Comment thread tree.js
Comment on lines +80 to +84
const styles = getComputedStyle(document.documentElement)
const radius = this.getRadius(context)
const childRadius = childNode.getRadius(context)
context.strokeStyle = 'gray';
const edgeColor = styles.getPropertyValue('--edge').trim() || '#9e9e9e'
context.strokeStyle = edgeColor

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getComputedStyle(document.documentElement) is executed for every edge draw. For larger trees this adds avoidable overhead. Consider computing the theme colors once per render (e.g., in drawTree or the caller) and passing the resolved edge color down or setting context.strokeStyle once before traversing.

Copilot uses AI. Check for mistakes.
Comment thread tree.js
Comment on lines +102 to +106
const styles = getComputedStyle(document.documentElement)
const radius = this.getRadius(context)
const nodeFill = styles.getPropertyValue('--node-fill').trim() || '#ffffff'
const nodeStroke = styles.getPropertyValue('--node-stroke').trim() || '#212121'
const textColor = styles.getPropertyValue('--fg').trim() || '#212121'

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getComputedStyle(document.documentElement) is executed for every node draw. For large trees this can become a bottleneck. Consider resolving --node-fill/--node-stroke/--fg once per render and passing them into Node.draw (or caching them on the context) instead of re-querying computed styles per node.

Copilot uses AI. Check for mistakes.
Comment thread index.html
Comment on lines +19 to +21
<button class="icon-btn input-icon" id="clear-tree" aria-label="Clear expression">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="3 6 5 6 21 6"></polyline>

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These icon buttons don’t specify type="button". It’s fine in the current markup (no <form>), but adding type="button" prevents accidental form submission if the controls are ever placed inside a form later.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants