4 Commits e51266610b ... 043570b1c8

Autor SHA1 Mensagem Data
  Brandon Wong 043570b1c8 Document agent-skill conventions: issue tracker and domain docs 1 mês atrás
  Brandon Wong 37d33fb730 Fix totals row sticky offset double-counting the control bar height 1 mês atrás
  Brandon Wong 560ee4b0c4 Add always-visible column totals row (todo 7.1) 1 mês atrás
  Brandon Wong 7a92dd1ed2 reduced the size of CLAUDE.md 1 mês atrás

+ 14 - 40
CLAUDE.md

@@ -1,21 +1,11 @@
 # Microtables - LLM Navigation Guide
 
 ## Project Overview
-A lightweight spreadsheet application designed for quick calculations — positioned between a calculator and a full office suite. The goal is fast loading with support for common spreadsheet operations.
+A lightweight spreadsheet application positioned between a calculator and a full office suite — fast loading, common spreadsheet operations.
 
 ## Architecture
-
-**Frontend** (`/frontend/`)
-- **Language**: ClojureScript (compiled via shadow-cljs)
-- **Framework**: Reagent + re-frame (React wrapper)
-- **Build**: shadow-cljs (via npm scripts)
-- **Dev server**: `npm run dev` → localhost:8280
-
-**Server** (`/server/`)
-- **Language**: JavaScript (ES6+ with Babel)
-- **Framework**: Express
-- **Status**: Minimal implementation - currently the frontend is fully functional standalone
-- **Future**: Will handle persistence and collaboration
+- `/frontend/` - ClojureScript (Reagent + re-frame, shadow-cljs). Fully functional standalone.
+- `/server/` - Express, minimal; will eventually handle persistence and collaboration.
 
 ## Key Concepts
 
@@ -40,37 +30,21 @@ Example:
   - `:dirty` - flag indicating re-evaluation needed
 
 ### Evaluation Flow
-1. When a cell changes, parse for references
-2. Update forward (`:refs`) and back (`:inbound`) reference trees
-3. Evaluate the cell
-4. Recursively evaluate all cells in `:inbound` queue
-5. Cycle detection prevents infinite loops
+A cell change re-parses references, updates `:refs`/`:inbound`, evaluates the cell, then recursively evaluates its `:inbound` queue (cycle detection prevents infinite loops).
 
-## Important Files
+## Development Notes
+- `todo.md` has a running list of desired changes
+- Data structures prioritize clarity over micro-optimization (small dataset assumption)
 
-**Frontend Core Logic**
-- `frontend/src/cljs/microtables_frontend/core.cljs` - App initialization
-- `frontend/src/cljs/microtables_frontend/db.cljs` - Data model definition
-- `frontend/src/cljs/microtables_frontend/events.cljs` - State mutations (re-frame)
-- `frontend/src/cljs/microtables_frontend/subs.cljs` - Data queries (re-frame)
-- `frontend/src/cljs/microtables_frontend/evaluation.cljs` - Formula evaluation engine
-- `frontend/src/cljs/microtables_frontend/views/sheet.cljs` - Grid rendering
+## Common Tasks
+- **Install an npm dependency**: add it to `shadow-cljs.edn` (not just `package.json`), then restart the dev server
 
-**Project Configuration**
-- `frontend/shadow-cljs.edn` - ClojureScript build config (source paths, deps, build targets)
-- `frontend/package.json` - npm scripts and JS dependencies
+## Agent skills
 
-## Development Notes
+### Issue tracker
 
-- there is a todo.md file which contains some desired changes
-- Almost everything is subject to change (architecture, structure, design)
-- The frontend currently operates completely standalone
-- Focus is on simplicity and performance for common use cases
-- Data structures prioritize clarity over micro-optimization (small dataset assumption)
+Issues live as local markdown files under `.scratch/<feature-slug>/` in this repo. See `docs/agents/issue-tracker.md`.
 
-## Common Tasks
+### Domain docs
 
-**Run frontend**: `cd frontend && npm run dev`
-**Install npm dependency**: Add to `shadow-cljs.edn`, then restart
-**Understand formula eval**: Start in `evaluation.cljs`, trace through `evaluate-from-cell`
-**Modify grid UI**: Look in `views/sheet.cljs`
+Single-context layout: root `CONTEXT.md` + `docs/adr/`. See `docs/agents/domain.md`.

+ 12 - 0
CONTEXT.md

@@ -0,0 +1,12 @@
+# Microtables Frontend
+
+The spreadsheet grid: cell data, formula evaluation, and keyboard/selection interaction for a lightweight spreadsheet app.
+
+## Language
+
+**Totals Row**:
+A display-only summary rendered below the grid, viewport-pinned (sticking just above the control bar) so it stays visible while scrolling. Shows one sum per column, computed from that column's numeric cells; non-numeric and blank cells are skipped. It is not stored in `:table-data` and has no row number — it cannot be selected, edited, or referenced by a formula. Styled distinctly from both the header row and data rows so it isn't mistaken for either.
+_Avoid_: treating it as "row 21" or as a stored/data row
+
+**Numeric Cell** (for Totals Row purposes):
+A cell whose display value is a number, whether typed directly or produced by a formula. If any cell in a column is in an error state, that column's total shows an error indicator instead of a sum — an errored cell is never silently skipped.

Diferenças do arquivo suprimidas por serem muito extensas
+ 5 - 0
docs/adr/0001-totals-row-view-only.md


+ 51 - 0
docs/agents/domain.md

@@ -0,0 +1,51 @@
+# Domain Docs
+
+How the engineering skills should consume this repo's domain documentation when exploring the codebase.
+
+## Before exploring, read these
+
+- **`CONTEXT.md`** at the repo root, or
+- **`CONTEXT-MAP.md`** at the repo root if it exists: it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
+- **`docs/adr/`**: read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
+
+If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
+
+## File structure
+
+Single-context repo (most repos):
+
+```
+/
+├── CONTEXT.md
+├── docs/adr/
+│   ├── 0001-event-sourced-orders.md
+│   └── 0002-postgres-for-write-model.md
+└── src/
+```
+
+Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
+
+```
+/
+├── CONTEXT-MAP.md
+├── docs/adr/                          ← system-wide decisions
+└── src/
+    ├── ordering/
+    │   ├── CONTEXT.md
+    │   └── docs/adr/                  ← context-specific decisions
+    └── billing/
+        ├── CONTEXT.md
+        └── docs/adr/
+```
+
+## Use the glossary's vocabulary
+
+When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
+
+If the concept you need isn't in the glossary yet, that's a signal: either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
+
+## Flag ADR conflicts
+
+If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
+
+> _Contradicts ADR-0007 (event-sourced orders), but worth reopening because…_

+ 30 - 0
docs/agents/issue-tracker.md

@@ -0,0 +1,30 @@
+# Issue tracker: Local Markdown
+
+Issues and specs for this repo live as markdown files in `.scratch/`.
+
+## Conventions
+
+- One feature per directory: `.scratch/<feature-slug>/`
+- The spec is `.scratch/<feature-slug>/spec.md`
+- Implementation issues are one file per ticket at `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01`, never a single combined tickets file
+- Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings)
+- Comments and conversation history append to the bottom of the file under a `## Comments` heading
+
+## When a skill says "publish to the issue tracker"
+
+Create a new file under `.scratch/<feature-slug>/` (creating the directory if needed).
+
+## When a skill says "fetch the relevant ticket"
+
+Read the file at the referenced path. The user will normally pass the path or the issue number directly.
+
+## Wayfinding operations
+
+Used by `/wayfinder`. The **map** is a file with one **child** file per ticket.
+
+- **Map**: `.scratch/<effort>/map.md` (the Notes / Decisions-so-far / Fog body).
+- **Child ticket**: `.scratch/<effort>/issues/NN-<slug>.md`, numbered from `01`, with the question in the body. A `Type:` line records the ticket type (`research`/`prototype`/`grilling`/`task`); a `Status:` line records `claimed`/`resolved`.
+- **Blocking**: a `Blocked by: NN, NN` line near the top. A ticket is unblocked when every file it lists is `resolved`.
+- **Frontier**: scan `.scratch/<effort>/issues/` for files that are open, unblocked, and unclaimed; first by number wins.
+- **Claim**: set `Status: claimed` and save before any work.
+- **Resolve**: append the answer under an `## Answer` heading, set `Status: resolved`, then append a context pointer (gist + link) to the map's Decisions-so-far in `map.md`.

+ 42 - 5
frontend/resources/public/site.css

@@ -1,8 +1,18 @@
+:root {
+  --control-bar-height: 44px;
+}
+
+/* html (not body) is the actual scrolling element on desktop, so the
+   reserved space for the fixed control bar belongs on it: that's the
+   ancestor position:sticky measures its offsets against. */
+html {
+  padding-bottom: var(--control-bar-height);
+}
+
 body {
   font-family: 'Helvetica Neue', Verdana, Helvetica, Arial, sans-serif;
   max-width: 600px;
   margin: 0;
-  padding-bottom: 44px;
   -webkit-font-smoothing: antialiased;
   font-size: 1.125em;
   color: #333;
@@ -15,7 +25,7 @@ body {
   html, body {
     height: 100%;
     overflow: hidden;
-    padding-bottom: 0;
+    padding-bottom: 0; /* .sheet-container becomes the scrolling ancestor instead; see below */
   }
   #app {
     height: 100%;
@@ -32,7 +42,7 @@ body {
     flex: 1;
     overflow: auto;
     min-height: 0;
-    padding-bottom: 44px;
+    padding-bottom: var(--control-bar-height);
   }
 }
 
@@ -98,6 +108,33 @@ td, th {
     left: 0;
     z-index: 2;
 }
+
+/* Totals row: pinned to the bottom of the viewport, just above the control bar.
+   bottom:0 rests it flush against the scrolling ancestor's own reserved
+   padding-bottom (html on desktop, .sheet-container on mobile) - deliberately
+   not an explicit --control-bar-height offset here, so the space is only
+   ever accounted for once. */
+#main-table .totals-row th,
+#main-table .totals-row td {
+    position: sticky;
+    bottom: 0;
+    z-index: 1;
+    background: #e8edf5;
+    border-top: 2px solid #333;
+}
+/* Bottom-left corner: sticky in both directions, like the top-left corner cell */
+#main-table .totals-row th:first-child {
+    z-index: 2;
+}
+.totals-row td {
+    padding: 5px;
+    text-align: right;
+    font-weight: 600;
+}
+.totals-row .totals-error {
+    color: #b00020;
+}
+
 th {
     text-align: center;
     min-width: 40px;
@@ -133,7 +170,7 @@ td input:not(:focus) {
     bottom: 0;
     left: 0;
     right: 0;
-    height: 44px;
+    height: var(--control-bar-height);
     display: flex;
     align-items: center;
     background: #f5f5f5;
@@ -142,7 +179,7 @@ td input:not(:focus) {
 }
 
 #bar-logo {
-    height: 44px;
+    height: var(--control-bar-height);
     width: 44px;
     flex-shrink: 0;
     display: flex;

+ 45 - 0
frontend/src/cljs/microtables_frontend/subs.cljs

@@ -1,6 +1,8 @@
 (ns microtables-frontend.subs
   (:require
+   [clojure.string :as string]
    [microtables-frontend.utils.coordinates :as coords]
+   [microtables-frontend.utils.data :as data-utils]
    [re-frame.core :as re-frame]))
 
 (re-frame/reg-sub
@@ -40,4 +42,47 @@
        (assoc-in highlighted [(:col cursor) (:row cursor) :view] :value)
        highlighted))))
 
+(defn- numeric-cell-value
+  "Returns the numeric value of a datum for totals purposes: a number, :error, or nil (not numeric)."
+  [datum]
+  (let [value (:value datum)
+        formula (data-utils/formula? value)
+        raw (if formula (:display datum) value)]
+    (cond
+      (keyword? raw) :error
+      (number? raw) raw
+      (and (string? raw) (not (string/blank? raw)))
+      (let [n (js/Number raw)]
+        (if (js/isNaN n) nil n))
+      :else nil)))
+
+(defn column-total
+  "Sums the numeric cells in a column (a {row-num datum} map).
+  Returns a number, :error (if any cell is in an error state), or nil (no numeric cells)."
+  [column]
+  (reduce (fn [acc [_ datum]]
+            (if (= acc :error)
+              acc
+              (let [v (numeric-cell-value datum)]
+                (cond
+                  (= v :error) :error
+                  (number? v) (+ (or acc 0) v)
+                  :else acc))))
+          nil
+          column))
+
+(defn- round-for-display
+  "Rounds a sum for display, clearing floating-point artifacts (e.g. 0.1 + 0.2) without truncating real precision."
+  [n]
+  (js/parseFloat (.toFixed n 10)))
+
+(re-frame/reg-sub
+ ::totals-row
+ (fn [db]
+   (reduce-kv (fn [acc col rows]
+                (let [total (column-total rows)]
+                  (assoc acc col (if (number? total) (round-for-display total) total))))
+              {}
+              (:table-data db))))
+
 

+ 2 - 1
frontend/src/cljs/microtables_frontend/views.cljs

@@ -7,9 +7,10 @@
 
 (defn main-panel []
   (let [data (re-frame/subscribe [::subs/table-data])
+        totals (re-frame/subscribe [::subs/totals-row])
         controls-state (re-frame/subscribe [::subs/controls-state])]
     [:div#main-layout
-     [:div.sheet-container [sheet @data]]
+     [:div.sheet-container [sheet @data @totals]]
      [control-panel @controls-state]]))
 
 

+ 20 - 4
frontend/src/cljs/microtables_frontend/views/sheet.cljs

@@ -48,7 +48,22 @@
                      ^{:key "corner"} [:th]
                      (map (fn [c] ^{:key (str "col-head-" c)} [:th c]) cols))])
 
-(defn sheet [data]
+(defn totals-cell [c totals]
+  (let [total (get totals c)]
+    ^{:key (str "totals-" c)}
+    [:td {:class (when (= total :error) "totals-error")}
+     (cond
+       (= total :error) "Error"
+       (number? total) (str total)
+       :else "")]))
+
+(defn totals-row [cols totals]
+  ^{:key "totals"} [:tr.totals-row
+                    (cons
+                     ^{:key "totals-corner"} [:th "Σ"]
+                     (map #(totals-cell % totals) cols))])
+
+(defn sheet [data totals]
   [:table
    {:id "main-table"}
    [:tbody
@@ -57,6 +72,7 @@
     (let [maxrow 20;(coords/highest-row data)
           maxcol "G";(coords/highest-col data)
           cols (take-while (partial not= (coords/next-letter maxcol)) coords/col-letters)]
-      (cons
-       (header-row cols)
-       (map #(row % cols maxrow data) (range 1 (inc maxrow)))))]])
+      (concat
+       [(header-row cols)]
+       (map #(row % cols maxrow data) (range 1 (inc maxrow)))
+       [(totals-row cols totals)]))]])

+ 3 - 5
todo.md

@@ -130,15 +130,13 @@ When a cell or range is selected, show a compact set of floating action buttons
 
 ## 7. Column & Row Aggregates
 
-*Under consideration.*
+- [x] **7.1 Always-visible column totals row**
 
-- [ ] **7.1 Always-visible column totals row**
-
-  A pinned row below the table showing the sum (and possibly average) of each column, updating reactively. Show only for columns that contain numbers.
+  A pinned row below the table showing the sum (and possibly average) of each column, updating reactively. Show only for columns that contain numbers. Make sure that it is not visually confused for another cell/row.
 
 - [ ] **7.2 Always-visible row totals column**
 
-  Equivalent pinned column to the right of the table for row aggregates.
+  Equivalent pinned column to the right of the table for row aggregates. Perhaps, due to concerns about mobile screen real estate, the row totals could be toggleable (not "always-visible" after all) - maybe behind the row header.
 
 ---