Skip to content

fix(grid): canvas render (blank + tab-switch blackout), motion/spinners, ERD readability, silent reconnect + perf - #41

Merged
broisnees merged 8 commits into
masterfrom
worktree-fix-canvas-color-and-motion
Jul 11, 2026
Merged

broisnees merged 8 commits into
masterfrom
worktree-fix-canvas-color-and-motion

Conversation

@broisnees

@broisnees broisnees commented Jul 11, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes a set of issues introduced by the connection-redesign / grid-rewrite PR (#40), plus a memory/perf pass and silent auto-reconnect. Labeled release:minor; a changeset (.changeset/canvas-render-motion-reconnect.md) drives the release notes.

Bug fixes

  • Data grid rendered blank — macOS WebKit returned a stale getComputedStyle().color from the shared canvas color-probe, collapsing every theme token to one value (text + grid lines invisible; only rgba-literal badges showed). Each color is now resolved on its own node. Verified in a real WKWebView via pixel readback.
  • Grid blacked out on tab switch — the cached 2D context kept pointing at a <canvas> WebKit had recreated on the switch, so draws hit an off-screen element. _ctx now always tracks the live canvas and re-binds on element change. Confirmed on-device (draw ran with perfect state but ctx.canvas !== canvasEl).
  • Spinners/loaders ran ~8× too fast under macOS Reduce Motion — a global @media (prefers-reduced-motion) rule forced animation-duration: 0.12s !important on everything. Removed the global motion/easing overrides.
  • Schema Visualizer opened as an unreadable sliver — a deep LR graph was fit-to-viewport down to a thin band. Now lands at a legible zoom anchored top-left (pan to explore), with roomier/larger cards and balanced spacing.

New

  • Silent auto-reconnect — a dropped connection reconnects automatically on network return / window focus / table switch / refresh, with only a subtle status-bar indicator instead of a persistent "Connection lost" popup.

Performance / memory

  • Hoisted the per-frame draw context out of the row loop; split canvas resurface vs. repaint effects (selection/hover no longer resurface the backing store); O(1) row offsets (no per-page Float64Array on 1M-row tables); cached settings on the zoom hot path; LRU-capped the per-tab expand cache.

Verification

  • npm run build clean.
  • Color reader re-verified in WKWebView (correct distinct colors).
  • Tab-switch blackout instrumented on-device and confirmed fixed (no stale-context draws after the fix).

…errides

Canvas grid rendered blank (only rgba-literal badges/icons visible) on macOS
WKWebView: createColorReader reused one probe and reassigned style.color between
synchronous getComputedStyle reads, but WKWebView returns a STALE computed color
in that case — every token after the first froze at the first color's value, so
text and grid lines painted the same color as the panel and vanished. Resolve
each token on a throwaway node instead. Verified via pixel readback in a real
WKWebView: foreground/grid/muted now resolve to distinct correct colors.

Also remove the global motion additions from the connection-redesign PR:
- the @media (prefers-reduced-motion: reduce) block forced
  animation-duration/transition-duration: 0.12s !important on every element,
  so spinners/loaders ran ~8x fast (broken) when macOS Reduce Motion is on.
- the --ease-out/--ease-in-out/--ease-drawer token overrides silently repunched
  every ease-* utility app-wide (--ease-drawer was unused).
@changeset-bot

changeset-bot Bot commented Jul 11, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 17c1995

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

… layout

Data grid render/reactivity/memory:
- draw(): build the ~20-field per-row draw context ONCE per frame instead of a
  fresh literal per visible row (was thousands of short-lived objects/sec during
  scroll → GC jank); use geom.frozenWidth instead of re-summing pinned cols.
- computeRowTops returns null when no row is expanded (the common case) and
  rowIndexAtY/rowAtContentY fall back to O(1) idx*rowHeight math — no 8 MB
  Float64Array reallocated per infinite-scroll page on 1M-row tables. Verified
  the null path is numerically identical to the array path.
- Split the canvas effects: the resurface effect now tracks only true layout deps;
  selection/focus/edit/hover/staged-edits moved to the lightweight repaint effect,
  so arrow-key nav and drag-select no longer re-run resurface or touch the backing
  store. syncCanvasSurface() skips all DOM writes + setTransform when size/DPR are
  unchanged.
- settings: in-memory cache so the zoom/pinch hot path no longer does
  localStorage getItem + JSON.parse twice per step.
- LRU-cap _tabExpandCache (retained fetched FK sub-view rows) at 12 entries.

ERD schema visualizer:
- Much larger layout spacing (ranksep 220→340, nodesep 48→84, COL/ROW/RANK gaps
  96/52/260 → 150/84/400, roomier orphan grid) so cards don't crowd and edges have
  room to fan — fixes the cramped/tangled look.
- Bigger, more legible cards (NODE_W 230→268, ROW_H 22→28, HDR_H 34→42) with more
  padding, larger column/type fonts, and roomier PK/FK badges.
… layout

The LR dagre layout of a deep schema is inherently very wide, so fit-to-view
scaled the whole bounding box down to an unreadable horizontal sliver (all tables
in one thin band). The previous "bump to 0.55 centred" landed the user in the
middle of that band, still unreadable.

- ensureFit: if the whole graph can't fit at a legible size, land at a fixed
  READABLE_ZOOM (0.62) anchored at the graph's top-left and let the user pan,
  instead of fitting everything into a sliver. Small graphs still fit-and-centre.
- Moderate the layout gaps (ranksep 340→240, nodesep 84→56, COL/ROW/RANK
  110/72/200) so the graph isn't stretched into an even wider sliver, and raise
  TARGET_H to 2600 so each rank (esp. a hub) stacks into a squarer, narrower block
  rather than one long line — more vertical breathing room, less horizontal sprawl.
…opup

The 30s health-ping showed a persistent (duration: Infinity) "Connection lost /
Reconnect" toast that never auto-dismissed even after recovery, and nothing
reconnected on its own — the user had to click it.

Now the app heals itself, no popup:
- reconnectPool()/silentReconnect(): rebuild the pool for the CURRENT connection
  config in place — no UI teardown, no full-screen overlay, no toast. Rate-limited
  (3s) and re-entrancy-guarded so bursts of triggers / an unreachable host can't
  loop connect_*.
- Health monitor: on ping failure, flag connectionLost (subtle StatusBar red dot)
  and silently reconnect instead of toasting. Pings every 30s when healthy, every
  6s while lost, and clears the instant it recovers.
- Auto-reconnect triggers: `online` / window focus / visibilitychange (network is
  back / window refocused), table switch, schema change, manual refresh, and the
  failing query itself (network-error catch paths).
- Widen isNetworkError to also match pool-timeout / connection-closed / "cannot
  reach" / idle-termination so cloud-Postgres idle drops are treated as network
  blips (auto-reconnected) rather than surfaced as SQL errors.

The StatusBar keeps a subtle red indicator while reconnecting; no modal/toast.
…ader

createColorReader fell back to opaque rgb(0,0,0) if getComputedStyle ever
returned an empty colour, and that value fills the ENTIRE canvas (the --panel
background) — the worst-case failure for a full-canvas fill. Empty results are
only really possible when the throwaway probe node is measured against a
momentarily-detached subtree (e.g. mid tab remount).

- Append the throwaway node to a guaranteed-connected host (probe's parent only
  when the probe itself isConnected, else <body>); theme tokens live on <html> so
  <body> inherits them identically.
- On an empty computed colour, reuse the last successfully-resolved colour (or a
  neutral grey, never black) and do NOT cache it, so the next read resolves cleanly
  once the DOM settles.

Re-verified in a real WKWebView: all tokens still resolve to correct distinct
colours (panel [11,11,11], foreground [235,235,235], grid [39,39,39], …).
The earlier isNetworkError widening included bare 'timed out' and 'i/o error',
which also match a Postgres statement/lock timeout (a slow query, not a dropped
connection). That would flip connectionLost and make every following table tap
tear down + rebuild the pool. Drop those two over-broad patterns; keep the
connection-specific ones (pool timed out, connection/server closed, terminating
connection, cannot reach, os errors).
…b-switch blank)

Belt-and-suspenders for canvas painting across mounts / tab switches:
- syncCanvasSurface() no longer short-circuits on unchanged dimensions; it always
  re-applies setTransform. (After the earlier effect split this is only called
  from the low-frequency layout effect, not on scroll/selection, so the cost is
  negligible.) A short-circuit here risked leaving a fresh/remounted canvas
  untransformed.
- The layout effect now calls draw() synchronously whenever the surface is ok
  (not only on resize), so a fresh mount / tab switch can never composite an
  unpainted canvas (which would show the dark app background as a black grid).
  scheduleDraw() still coalesces concurrent updates.

(isNetworkError over-match — 'timed out' / 'i/o error' — was already tightened in
cccfbe4.)
…kout)

Confirmed via on-device WKWebView instrumentation: after opening/switching tabs
the grid went fully black while draw() ran with perfect state (correct size,
rows, columns, colours). The context (_ctx) was still bound to a canvas WebKit
had recreated on the tab switch (ctx.canvas detached, ctx.canvas !== canvasEl),
so every draw painted an off-screen element and the visible canvas stayed black.
Root cause was the c1114f3 perf change caching _ctx behind `if (!_ctx)`.

- syncCanvasSurface() always re-fetches _ctx for the current canvasEl (getContext
  is idempotent per element, so it's cheap) — _ctx can never go stale.
- New effect re-binds the context + re-applies size/transform + repaints whenever
  the <canvas> element identity changes, covering swaps not accompanied by a
  layout change.

Adds a changeset so the release notes cover this PR's fixes (blank grid, tab-switch
blackout, spinner/reduce-motion, ERD readability) + silent auto-reconnect.
@broisnees broisnees added the release:minor Bump minor version (0.x.0) label Jul 11, 2026
@broisnees broisnees changed the title fix(grid): blank data table on WKWebView + broken spinners from motion overrides fix(grid): canvas render (blank + tab-switch blackout), motion/spinners, ERD readability, silent reconnect + perf Jul 11, 2026
@broisnees broisnees self-assigned this Jul 11, 2026
@broisnees
broisnees marked this pull request as ready for review July 11, 2026 08:01
@broisnees

Copy link
Copy Markdown
Collaborator Author

lgtm.

@broisnees
broisnees merged commit e8b6d8a into master Jul 11, 2026
1 of 2 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release:minor Bump minor version (0.x.0)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant