- Track window focus via performance.now() instead of suppressing all backdrop
clicks for 300ms after the modal appears. This fixes over-suppression (Ctrl+W
close-sheet now dismisses on immediate click) while keeping protection for
genuine stray refocus-clicks (native dialog closing, tray window refocus).
- Serialize concurrent askModal calls via a promise chain so a second call
queues instead of clobbering the first's handlers (was: hang on concurrent
modal show).
- Use monotonic performance.now() throughout to avoid clock-jump edge cases
with Date.now().
a click that focuses the window lands on the modal backdrop too, so
the update-available prompt after an unattended auto-check could get
dismissed by the very click that brought the window to front, before
it was ever seen
Both Tauri commands took a raw path string straight from the frontend
and wrote to it with no validation — any code able to call invoke()
(a compromised or malicious extension script, which run with full page
privileges) could write to any path the OS user has access to, not just
one chosen through the save dialog. Move the save-file dialog into the
Rust command itself so the only path ever used is one the native dialog
returned; the frontend now only sends the content to save.
lexLine had grown into one long function trying dates, hex/bin/oct,
grouped numbers, decimals and xrefs inline. Extracted each into a
tryLex* helper with the same fallback order, so the driving loop
reads as a priority list instead of nested ifs.
fetch_market_data hit Yahoo's v8 chart endpoint once per symbol in a
sequential loop — with 9 symbols that's 9 round trips end to end.
join_all fires them together.
Boot had grown into one long function covering settings load, engine
and workspace setup, and all the UI wiring. Split along those natural
phase boundaries; boot() itself is now just the three awaits in order.
The settings panel grew into tabs (General/Format/Data/Updates) but
the binding code was still one 170-line function. Split it to match:
bindFormattingSettings, bindHotkeySettings, bindBehaviorSettings,
bindDataDirSettings, bindSettingsNavigation.
Covers isTauri, and loadSettings/saveSettings across both the Tauri
invoke path and the localStorage fallback, including corrupt-data
and thrown-error recovery.
.jules/ and test_perf.ts kept showing up in Jules/Bolt-generated PRs
(agent notes and a throwaway benchmark script) — ignore them so they
can't land on main by accident. Also picked up the pending
src-tauri/.cargo/ ignore that was sitting uncommitted.
highlightField and varHighlightField rebuilt from scratch on every
CodeMirror transaction, including ones that touch neither the doc nor
the selection (e.g. the setResults effect firing after evaluation).
varHighlightField now only rebuilds when the doc or selection actually
changed; highlightField remaps existing decorations through doc edits
and only does a full rebuild when new results arrive.
resolving was an array checked with .includes() on every xref lookup
during resolution — an O(n) membership test where only membership
ever mattered, not order. Set makes it O(1).
invalidate() re-scans every non-dirty sheet's text with a regex on
every pass of its while loop, even though the text hasn't changed
between passes — cache the parsed refs per sheet id, keyed by text
identity, so repeated passes reuse the same result.
fetchNeededHistoricalRates awaited each date's fetch sequentially even
though they're independent requests — a sheet referencing several
historical dates paid for that latency serially instead of in parallel.
Toolbar buttons (☰ 🔍 ⚙ ✕ ─ ⤓ +) and the untitled doc-title/search
inputs had no accessible name beyond a title tooltip, which screen
readers don't reliably announce.
Covers detectDateOrder, resolveZone, addToDate and isCalendarUnit —
none of it had direct unit coverage before, only indirectly through
the engine's date-arithmetic tests.
releaseDraft: true (needed so build assets land on the same release
object across the two matrix jobs) was never followed by a step that
un-drafted it — the notes job set the changelog but left the release
sitting invisible on GitHub, which also means the auto-updater's
"latest release" lookup never found it (GitHub excludes drafts).
Confirmed against the v1.2.0 run: build + notes both succeeded, but
the release stayed in draft with no publish date. Publish it in the
same step that writes the notes.
The manual rebind flow in Settings already toasts hotkeyFailed when a
combo is taken by another app, but the initial registerHotkey() call
at boot discarded its success/failure result — if the saved hotkey
couldn't be registered (already bound elsewhere, etc.), there was no
error and no indication the shortcut just wasn't active.
appearance:none on the <input> alone didn't strip the native chrome
in every Chromium build — some keep ::-webkit-slider-runnable-track
styled natively unless that pseudo-element gets its own
appearance:none. Made the track's styling explicit there instead of
relying on it inheriting from the input.
Tauri's window-level file-drop hook (needed for dragging a .numi file
in from Explorer) intercepts WebView2's own drag-and-drop session, so
native HTML5 draggable="true" elements never get a valid drop target
inside the webview — the OS always shows "not allowed", confirmed
with an actual mouse-drag simulation before this fix.
Replaced with plain pointer tracking (mousedown/mousemove/mouseup),
the same pattern already used for the results-column divider — full
control over the cursor and the drag-over indicator, no dependency on
the webview's native DnD stack, and it doesn't touch the unrelated
OS file-drop feature at all.
Move-up/move-down felt disconnected from the result — clicking a
button and having two rows silently swap position doesn't read as
motion. Dropped both buttons for native HTML5 drag-and-drop: drag a
sheet, an accent line shows where it'll land, drop to reorder. Still
respects the pinned/unpinned grouping — dragging across that boundary
is a no-op, same as the old up/down buttons.
#market-info had no CSS at all — different font size and color from
#rates-info right next to it, and its own hardcoded English "Market"
text ignored the language setting. Merged its styling into the same
rule as #rates-info and gave it the same .spin pulse while a fetch
is in flight, so a market refresh is as visible as a rates refresh.
It was still the bare OS-native slider — flat gray track everywhere
else in Settings is themed. Now a thin themed track with an accent
thumb that grows slightly while dragging.
Settings, export menu, modal, and search all drove their entrance
with a @keyframes animation gated on :not(.hidden), triggered by a
display:none toggle — animations don't run when a property changes
away from display:none, only when the element (re)enters the render
tree. Closing just flipped straight to display:none with nothing to
watch. Replaced with opacity/transform transitions plus a delayed
visibility flip (visibility switches instantly on open, only after
the fade completes on close) — same visual effect, correct both ways.
The status bar just dimmed the "Rates" text to 0.5 opacity and left
it static for the whole fetch — easy to miss as feedback that
anything is happening. A slow pulse reads as "working" instead of
just "disabled".
Doc title, search input, and settings inputs/selects either zeroed
out the default outline or never set one — Tab-only navigation had
no visible indicator of what's focused. :focus-visible keeps mouse
clicks silent and only rings for keyboard focus.
Default Windows scrollbars were chunky and out of place next to the
minimal chrome elsewhere. WebView2 is Chromium under the hood, so a
plain ::-webkit-scrollbar override works everywhere without JS.