Currently press-enter-in-cell uses highest-row/highest-col to decide where to move, coupling navigation to data extent. Intended design:
highest-row/highest-col should include the cursor's current positionpress-enter-in-cell should only increment the positionAdd a dedicated event handler for Tab (native Tab is no longer sufficient)
[ ] 1.2 Add arrow-key navigation
Implement ArrowUp, ArrowDown, ArrowLeft, ArrowRight handlers that move the cursor between cells, firing from on-keyDown (not on-keyPress). Navigation logic should live alongside Enter/Tab handling (1.1).
maxrow = 20 and maxcol = "G" are hardcoded. Decide whether the grid grows as the cursor reaches the edge or stays fixed with a scroll container. A fixed viewport with overflow scroll is likely right for the quick-calculation use case.
When a range is selected, Enter/Tab/arrow keys should cycle within the selection rather than the full table. Depends on the selection model (see section 5).
parse-variables already uppercases symbol names, but confirm end-to-end that typing =a1 + b2 in a cell evaluates correctly.
gather-variables-and-evaluate-cell returns :error when forward references are dirty, but the intended semantics vs. :insufficient-data are not fully specified. Clarify and document the distinction.
evaluate-all handles cycles correctlyevaluate-all calls evaluate-from-cell which runs find-cycle, so cycles should be caught at initialization. Confirm this is sufficient and clean up the open question.
The model processes one cell change at a time. A batch-update path will be needed for paste operations or future server sync.
denotify-references + notify-references wipe and re-add all back-references on every edit. Diffing old vs. new :refs and updating only the delta may help at scale.
find-cycleEvaluate whether a dynamic memoization strategy with cache invalidation on data change is worth the complexity.
Implemented in frontend/src/test/microtables_frontend/evaluation_test.cljs using cljs.test + shadow-cljs :node-test target. Covers:
sum, average, etc.):calc-error, :cycle-error)Run with npm test in frontend/
[x] 3.2 Integration tests for state transitions
Implemented in frontend/src/test/microtables_frontend/events_test.cljs. Tests the pure function pipelines that event handlers compose (not through re-frame dispatch). Covers:
change-datum-value marks dirty without evaluatingDeeply nested dependency chain propagation
[ ] 3.3 End-to-end tests
Browser-level tests covering full user flows:
= is typedWhen a cell value begins with =, display a dropdown of available math.js functions. Filter the list as the user continues typing. Selecting an item inserts the function name with a placeholder for arguments.
As the user types a column letter that could start a cell reference (e.g. A), suggest cell addresses that contain data. For ranges, trigger suggestions when : follows a cell reference.
Inside a function call, show a tooltip indicating expected arguments and their types (e.g. sum(value1, value2, ...)).
The :selection key in :position is always nil. Implement {:start {:col "A" :row 1} :end {:col "C" :row 3}} as the selection shape. Set on shift+arrow or click-drag; clear on unmodified navigation.
highlight-cells in subs.cljs already sets :view :highlighted on cells within a selection range, but the ::table-data subscription never calls it when :selection is non-nil. Connect the two so selected cells receive the highlighted CSS class.
When a numeric range is selected, display the sum and average in the UI (e.g. a status bar or floating chip). Offer to insert the result as a formula into a user-chosen cell.
When a cell or range is selected, show a compact set of floating action buttons positioned around the selection. Buttons must be large enough to tap comfortably on a phone screen. Implement in this order:
Under consideration.
A pinned row below the table showing the sum (and possibly average) of each column, updating reactively. Show only for columns that contain numbers.
Equivalent pinned column to the right of the table for row aggregates.
A small modal (triggered from the control panel) listing and explaining:
How cell references and ranges work
[ ] 8.4 Replace the logo
The current logo.svg is a placeholder. Design and replace with a better mark.
The goal is intentional minimalism — not bare HTML. The current stylesheet has good structural foundations but is visibly unfinished: placeholder colours throughout, a content-template base, and unstyled interactive elements. Items are ordered so that each one builds on the previous.
Define all colours, spacing, and radii as CSS custom properties on :root before touching anything else. Suggested palette:
--color-bg: #f9fafb (page background)--color-surface: #ffffff (cell / panel background)--color-border: #e5e7eb (grid lines, panel edges)--color-header-bg: #f3f4f6 (row/column headers)--color-text: #111827 (primary text)--color-text-muted: #6b7280 (header labels, hints)--color-accent: #3b82f6 (focus ring, selection border)--color-accent-subtle: #eff6ff (selection fill, hover tint)
[ ] 13.2 Strip out content-template CSS
The stylesheet was adapted from a blog template. Remove or replace:
max-width: 600px on body — clips the table on most screens; a spreadsheet should use available widthh1, h2, h3 type scale — not used in the appline-height: 1.5em on body — intended for prose, not a grida tag styles (text-decoration, color: #09f) — not relevant to the app chrome
[ ] 13.3 Replace all placeholder colours
Several elements have placeholder colours that were clearly never updated:
#controls-left: background-color: pink → var(--color-surface)#controls-bottom: background-color: lightblue → var(--color-surface)#main-logo: background-color: lightblue → transparent (let the SVG stand alone).control-group: background-color: lightgreen → remove; use spacing to separate groups
[ ] 13.4 Change grid lines from black to the border token
Replace border: 1px solid black on td, th with border: 1px solid var(--color-border). This is the single highest-impact change to the table's appearance.
Replace the current 'Helvetica Neue', Verdana, Helvetica, Arial, sans-serif with system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif. This makes the app feel native on each platform rather than web-circa-2010.
Numbers in a proportional font look accidental; numbers in a monospace font look deliberate. Apply font-family: 'SF Mono', ui-monospace, 'Fira Code', monospace to td input and use a slightly smaller size (around 0.875rem) relative to the header labels.
td input currently has a fixed width: 80px, leaving dead space in the <td> that does not respond to clicks. Set width: 100% and ensure box-sizing: border-box so the input expands to fill the full cell width.
Current states (#ccc hover, #ccf focus background) are heavy and generic. Suggested approach:
var(--color-accent-subtle) — a very light tintFocus: remove the background change entirely; rely on the smartborder ring (13.9) as the focus indicator, which avoids the "form field in a table" feeling
[ ] 13.9 Style the smartborder selection ring
The smartborder concept (an absolutely-positioned overlay extending ~2px beyond the cell boundary) is the right approach — it mirrors how professional spreadsheets handle selection. Replace the current 3px solid blue with 2px solid var(--color-accent) and ensure pointer-events: none so it does not interfere with clicking adjacent cells.
smartborder action buttonsThe range-action buttons (extend, fill, move, empty, delete, copy) are currently unstyled blue circles. Redesign as small, clean pill buttons: white background, var(--color-border) border, var(--color-text) icon or label, with a shadow on hover. Size them to be comfortably tappable (minimum 32×32px touch target).
Apply background: var(--color-header-bg) and color: var(--color-text-muted) to th. Remove bold weight — a medium-weight muted label reads as a header without competing with cell content.
#left-controls-button and #bottom-controls-button currently render as xx-large bold unicode arrows with a 2px border radius — too raw. Replace with small, refined pill or tab buttons: consistent padding, var(--color-border) border, border-radius: 6px, normal font weight, subtle hover state.
Ensure cells are large enough to tap accurately on mobile. Tap to select, double-tap or long-press to enter edit mode. The current on-focus/on-blur model may need adjustment for touch events.
Implement tap-and-drag or tap-then-drag-handle selection. The floating control buttons (section 6) should be comfortably tappable.
Adapt the layout to small screens: control panel sidebars should collapse by default on mobile; consider a bottom sheet instead of a side panel.
On mobile, the software keyboard resizes the viewport and may conflict with cell navigation. Ensure formula entry and cell-to-cell movement work correctly with a software keyboard open.
Longer-term.
When a user selects a range and triggers fill (via the control button in 6.7), infer the pattern (arithmetic sequence, geometric, repeated value, date sequence, etc.) and propose a continuation into adjacent empty cells.
Show a preview of the proposed fill before committing, allowing the user to accept, adjust the inferred pattern, or cancel.
Long-term consideration — not yet committed.
Save the table to the browser's localStorage on change and restore on page load. Provides durable-but-local storage with no backend required.
Extend the Express server to store table data, with considerations for:
Conflict resolution for future collaboration
[ ] 11.3 Table sharing
Allow a user to share a table with others via a link. Requires server-side persistence (11.2). Considerations:
:inbound entriesnotify-references uses conj on a list with no deduplication. A formula like =A1 + A1 adds A1's back-reference twice. denotify-references removes only the first match via filter, leaving one stale copy. Use a set for :inbound, or deduplicate on insert.
:inbound with a concrete collectiondenotify-references stores the result of (partial filter ...) — a lazy sequence — into the app db. Lazy seqs in re-frame state can cause subtle equality and inspection issues. Replace with (into [] (filter ...)).
evaluate-allThe retry loop — (recur data (concat (rest queue) (list cur))) — re-queues cells with unsatisfied dependencies indefinitely. Cycle detection in evaluate-from-cell should prevent an infinite loop in practice, but there is no explicit guard. Add a visited set or iteration counter as a safety net.
highest-col and order-two-colsBoth functions call (apply max ...) on sequences of column-letter strings. ClojureScript's max is defined for numbers only — it does not perform lexicographic comparison on strings. This likely produces incorrect results for any column comparison and will break entirely once multi-letter columns (AA, AB, …) are introduced. Replace with an explicit comparator (e.g. (apply max-key #(vector (count %) %) ...) or a sort-based approach).
parse-range fail loudly on malformed inputparse-range runs four independent re-find calls on the same string. If any one fails to match, it silently passes nil into range->list, which cascades into bad data with no error surfaced to the user. Either validate the full string with a single comprehensive regex up front, or add an assertion/guard that all four captures succeeded.
on-keyPress with on-key-downThe cell's enter-key handler in views/sheet.cljs uses :on-keyPress, which is deprecated in both React and the underlying browser API. Move to :on-key-down.
reagent/render with reagent.dom/rendercore.cljs calls reagent/render, which was deprecated in Reagent 1.x in favour of reagent.dom/render. Verify the installed Reagent version and update accordingly.
evaluate-expression memoization cacheevaluate-expression is memoized on (expression, variables-map). Every unique combination ever computed is cached for the lifetime of the page. In a long editing session this becomes a gradual memory leak. Consider a bounded LRU cache or explicit cache invalidation.
println debug statementsEvery event handler and subscription (::table-data in particular) fires println on every call. This floods the console and makes debugging new code harder. Remove or gate behind a dev-mode flag.
maxrow = 20 and maxcol = "G" are hardcoded in views/sheet.cljs while min-max-row and min-max-col defined in db.cljs go unused. The view should read from the db values, leaving a single source of truth. (Related: 1.3.)
gather-variables-and-evaluate-cellAt ~35 lines with six boolean flags and a multi-branch cond, the function is near its readability limit. Adding another error type or evaluation mode will tip it over. Consider extracting the disqualification checks and the variable-collection step into named helpers.
create-all-references to use walk-modify-dataBoth functions perform a reduce-kv double-walk over the data map. Eliminate the duplication by having create-all-references call walk-modify-data.
main-panel to a form-2 Reagent componentmain-panel in views.cljs is a form-1 component that calls re-frame/subscribe at the top level. Re-frame's subscription cache makes this work in practice, but the canonical pattern for components holding subscriptions is form-2 — subscriptions are set up in the outer defn, and only the inner fn renders. Form-1 subscription calls can fail to clean up correctly across hot reloads.
dev-setup before mount-root in core.cljsinit currently calls mount-root before dev-setup. If dev-setup ever does real setup work (beyond a println), it will run after the first render. Swap the order so the dev environment is fully configured before any rendering occurs.