App review, and the work it proposed: v3.7.0 → v3.16.2 - #44
Merged
Conversation
Full scan at v3.7.0 covering the command-bar quick-type experience, the saved-entry cards, the design system, new feature directions, and a handful of correctness/trust issues (share links dropping pricing, frozen saved prices, unwired saved-entry APIs). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
Sprint 0 + Sprint A of docs/REVIEW_2026-08.md. The saved card was a list row with two numbers glued on, while ~10 already-implemented functions in useSaved (rename, notes, tags, duplicate, bulk delete, usage counts) had no UI at all. And its money was a snapshot: change the rate in Settings and every card kept showing the old total, silently, while the line it restored priced differently. - A shared SavedCard used by the desktop grid and the mobile library: the real cross-section drawing (ProfileDrawing gained a `thumb` variant rather than a second geometry source), spec-first title with the name as an optional override, the mode-leading metric dominant, kg/m and kg/piece, hover actions, and an expander with the full breakdown, notes and tags. - Saved entries re-price at the current rate every render, state the rate they used, and badge what they cost when saved if it moved. Opening one restores at today's rate (inputToQuery `omitPrice`) so the command line agrees with the card, and counts the use. - Search, sort (newest / most used / recently used / name), tag filter, pin, rename/notes/tags, duplicate, bulk select, card/compact density. - Save is a toggle driven by savedFingerprint (geometry, never price, so a re-priced entry can't duplicate itself); deletes are undoable and the save toast offers "name it". - Share links carry the sender's rate, currency, waste and VAT, and say so when they change local pricing. - One EmptyState atom across Saved, Projects, Compare and the sheets. Covered by e2e/saved.spec.ts, two new axe scans, and unit tests for the pure parts (share round-trip, filter/sort, fingerprint, omitPrice). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Finishes the last Sprint 0 item. The vitest suites covered the engine, parser and stores; the bar's actual behaviour — keyboard routing, chip editing, suggestion insertion, ghost accept, history recall — existed only behind e2e smoke tests, which is thin ground for the keyboard rewrite Sprint B does next. Mounts the real CommandShell in jsdom with real messages, stubbing only what jsdom lacks (service worker, locale routing, clipboard, matchMedia, randomUUID). Each render resets the module graph and the URL, since the shell's stores are caching singletons and jsdom keeps the mirrored ?q= between tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
…y layer
Sprint B of docs/REVIEW_2026-08.md.
The moment a query was complete, the suggestion row showed exactly one
chip ("Save calculation") — the highest-intent moment in the app spent
on a single button. And every keyboard affordance the bar had (Tab
completion, history arrows, chip navigation) was invisible, with Enter
meaning different things on different surfaces.
- A finished line now offers variations: twice the pieces, the other
stock lengths, the neighbouring catalog sizes, another grade — each
with the resulting total under it, from arithmetic on the parse we
already have, so the new chips cost no extra parses. Picking one swaps
that token in place (new `refine` kind + `replaceKind`).
- Key routing is one pure resolver (command-keys.ts) shared by both
surfaces instead of two drifting copies: Enter always advances, ⌥1–9
picks a chip, ⌘S saves, ⌘⏎ compares, ? opens a reference generated
from the alias table.
- A hint strip names the keys that work right now — "↵ insert" while a
suggestion is pending, "↵ log" once the line is complete.
- Suggestions carry their provenance (yours / presets / standard) and
the desktop row shows the headers.
- Returning users start on their last line, not the demo query.
- Phone: recents strip above the keypad, hold-backspace deletes a whole
token, haptics (with a Settings switch), chips wrap to a second row.
- Suggestions no longer re-parse the query the shell already parsed, and
per-metre weights for candidate sizes are cached.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
First half of Sprint C (docs/REVIEW_2026-08.md §4.1, §4.2, §4.4). Pricing was a single rate for every material. Stainless costs roughly four times mild steel per kg, so anyone working across materials was quoting one of them wrong unless they retyped `@` on every line. - Price book: a rate per material grade, synced like every other collection. Resolution order is inline `@rate/unit` > the grade's book rate > the single default, so an empty book behaves exactly as before. Editable from Settings on every viewport. - Margin: one setting adds a sell-price row to the breakdown wherever a breakdown is rendered, turning cost into an offer without touching the cost figures themselves. - Session → project: the tape already summed; now one button turns it into a project with all its lines. Two things surfaced on the way. `addCalculation` reports success through a flag its state updater sets, which only reads back correctly for the first call in a batch — a loop silently added nothing, so bulk work now goes through `addCalculations` (one updater, fingerprint-deduped). And the price book needed a module-level store rather than component state, since the calculator and the settings surface both read it and an edit in one has to show up in the other. Covered by e2e/quoting.spec.ts and parser tests for the resolution order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
Second half of Sprint C (docs/REVIEW_2026-08.md §4.3, §4.7). - Assemblies: a saved entry can hold several parts, which the storage model always supported and nothing surfaced. Add the line in the bar to any entry and its card becomes a bill of materials — parts listed, weight and price summed, each part re-priced at today's rate like the whole. Per-metre and per-piece figures drop out for an assembly, since they describe one profile and not a set. - Printable quote: any project prints (or saves as PDF) as a clean document — header, line items, lengths, weights, totals, margin, and a note that the weights are theoretical. It is a portalled node revealed by `@media print`, so there is no route, no dependency and no network; it works offline like the rest of the app. Isolation uses visibility rather than display so it survives the app's fixed-position shell, and the node stays mounted until `afterprint` because unmounting mid-print prints nothing. Covered by e2e/quoting.spec.ts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
…tion set Sprint D of docs/REVIEW_2026-08.md (§3.1, §3.2, §3.4, §3.5, part of §3.6). The 640–1023 range was a fixed 560px card floating on a background with no tape, no library and no breakdown — an iPad in portrait, or a half-width laptop window, getting the least capable surface in the app. Everything ≥640 now runs the workspace with a `compact` flag: one column, breakdown stacked under the tape, wordmark and ⌘K dropped from the header for room. Sheets become phone-only, which let ~200 lines of unreachable medium-viewport code leave the shell, and moved the e2e specs that exercise sheet semantics onto the phone viewport. - One numeric rule: tabular figures everywhere, so a value never shifts sideways as it counts up. Compare's headline weight joins the rest in mono. - Accent discipline: --accent was the primary action, the weight metric, the profile glyphs, the selected tab, the density control and the focus glow. It is now the first two. The raised surface and its shadow were already saying "selected" without help. - A shared motion vocabulary (fsPop / fsRise / fsFade / fsDrop) so movement means the same thing everywhere: something arrived, a view changed, a warning appeared. All of it off under reduced motion. - First slice of the inline-style migration: a DeskPanel atom replaces the border/surface/shadow block written out in every desktop view. - Breakdown rows carry a stable `data-row` hook, which also un-flakes the price-book e2e that was matching rows by DOM position. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
`hea120 6m =500kg` answers "how many of these make 500 kg"; with a quantity fixed instead (`hea120 x10 =500kg`) it solves the length. Money targets (`=250eur`, `=250€`) work the same way against the grand total. The solver probes the engine rather than restating the pricing rules, so waste, VAT, price basis and the price book are all counted in for free. Two probes on the length branch separate the per-metre slope from any fixed part, which is what keeps a per-piece rate — flat with respect to length — from being "solved" into a nonsense answer. Pieces round up, because you buy whole bars, so the parse result carries what the solution actually comes to and a purple badge on the equation line states the target and the overshoot. The token joins the grammar reference, travels in ?q= links, and appears in copied summaries. Also fixes a pre-existing type error in external-stores.test.ts, where a string literal narrowed the store's generic and rejected the later set(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
Usage stats ranked the suggestion bar from what you typed, but only on the device you typed it — so a phone and a laptop each started from zero. They now travel with the rest of the encrypted snapshot. The merge is a grow-only counter per device rather than one shared total. Each device pushes usage:<deviceId> holding only what it learned itself, skips its own record when it comes back on a pull, and the suggestion source sums every device's tally at read time: counts add, timestamps take the later of the two. A shared total would double-count on every round trip — five uses here plus the five just pulled back reading as ten — which is the whole reason for the split. With no peers the merge is skipped outright, so a single-device user's ordering is unchanged. The shell now subscribes to a usage version store instead of holding a counter of its own, so a pull that brings a peer's habits in refreshes the suggestions without a reload. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
`hea120 6m x2 + ipe200 4m x3` is now one line holding two calculations. The grammar itself doesn't change — each `+`-joined segment goes through the ordinary parser — so order tolerance, glue splitting, per-item grades, per-item `@` rates and per-item `=` targets all work per item for free, and a one-item line is exactly what it was. `+` is safe as the separator because nothing else in the grammar uses it: sizes join with `x`, rates with `@`, targets with `=`. The hero shows the line total and the equation line becomes one row per item, because a sum with no breakdown is a number you can't check. Chips group per item with the separator drawn between them, and every edit is scoped to its own item: removing a token from the first can't reflow the second, and pulling a token back for editing can't teleport it across a `+`. That index arithmetic lives in one tested module rather than being written out twice in the phone and desktop views. Saving a multi-item line makes one assembly with a part per item — the saved model already held parts, this is what fills them. Save stays a toggle for single lines only, since an assembly is a new object each time and matching it against one of its own parts would un-save that part. Logging drops one tape line per item, so the tape still adds up and still becomes a project. A share link carries the separator: `+` means a space in a query string, so a literal one is escaped on the way out and restored on the way in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
The item model, the ranking and the shared action builder, with the strings for both locales. Ranking is prefix > word start > substring so typing "sav" puts Save above Go to saved above a saved entry that merely contains the letters, and every word of the term has to match somewhere so a second word narrows instead of widening. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
wip: renders, navigates and runs; e2e and help entry still to come. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
Typing `>` turns the command line into an action list: navigate to saved, projects, compare or settings; save, compare, copy or share the current line; open the reference or flip the theme. It also searches the user's own saved entries and projects by name, so picking one loads it straight into the bar — one input for calculate, navigate and recall. Ranking is prefix > word start > substring, so `>sav` puts "Save this line" above "Go to saved" above a saved entry that merely contains the letters, and every word of the term has to match somewhere so a second word narrows instead of widening. A `>` line is a command, not a calculation: it isn't tokenized into chips and the parser's "didn't understand that" never fires on it — otherwise every palette keystroke would draw a warning underneath the list. Palette mode also takes the arrows and Enter away from the calculator's key map while it's open, since the list is the whole interface at that point. Actions that need a finished calculation are greyed rather than hidden, so the line explains itself instead of quietly having fewer options. Both surfaces build their list from one shared builder, differing only in what "navigate" means: workspace views on desktop, Library tabs on phone. The palette is keyboard-driven by nature and the phone keypad has no `>` key, so on phone it is reachable only via a shared link — the phone's own navigation already covers the same ground in one tap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
A cut list arrives as "the six-metre stock less the fifty-millimetre joint", not as round numbers, and doing that subtraction in your head before typing is the friction the bar exists to remove. Only + and -, evaluated left to right: this is shorthand for a measurement, not an expression language. Terms without a unit inherit the first term's rather than the global default, so "6m-50-50" is three metre terms and can't silently mix scales. The answer is reported in the first term's unit, because that is the one the user was thinking in. The + here collides with the item separator, and the collision is settled by shape: a + glued between a character and a digit is arithmetic, anything else separates items. A new item always opens with a profile alias, so "digit on the right" is enough to tell them apart. An expression that doesn't come to a usable amount (50mm-6m, x2-2) is reported rather than ignored — a silently dropped token would price the line as if the cut had never been typed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
A cut list lives in a spreadsheet or an email as one part per row, and that is one item per row here now that a line can hold several. Pasting one produces the line the user would otherwise have typed instead of a single unparseable blob. Tabs, semicolons and commas collapse to spaces because the grammar is order-tolerant and reads the row either way, and a paste far longer than anyone would work with is capped rather than building a query nobody can read or edit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
Letter-spacing was an inline number on a dozen elements across five desktop views, so "loosen the small caps a touch" was a find-and-replace rather than an edit. Three named steps in globals.css cover every repeated use, and the 10px tracked micro-label — written out by hand in four views — is SectionLabel everywhere now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
Rolled steel is sold by theoretical mass and delivered within a tolerance, so a buyer working to a budget — or to a crane's limit — wants the worst case as well as the nominal. A ±% in Settings produces the band in the breakdown on every viewport, and under the hero when weight is the headline metric. The percentage is the user's own figure and the app says so. The EN part of this idea is deliberately not implemented: mass tolerances differ per product standard, and some (EN 10029 plate) derive from a thickness class rather than being a single percentage, so a flat per-family table would be an oversimplification wearing a standard's name — the opposite of the credibility the feature was meant to buy. Sourced figures drop into one table when someone has them. Section properties from the same review section are not implemented either, and the review is updated to say why: the premise that "the dataset rows already carry geometry" is wrong — the standard profiles carry area and perimeter only, so Ix/Wx/iy would have to be generated rather than read, and inventing section properties for a structural tool is worse than the second browser tab it was meant to replace. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
Pasting a cut list replaced the query outright, so a line the user had already typed vanished with no undo. The rows are appended as further items now: "I meant to add these" is by far the likelier intent, and a destructive edit needs a better reason than convenience. The breakdown describes one calculation — kg/m and per-piece weight don't sum — but on a multi-item line it sat unlabelled under a hero holding the line's total. The mass tolerance band made the mismatch legible: ±4% of 268 kg in the breakdown, directly below ±4% of 507 kg under the hero, both reading as the same claim about the same thing. Both surfaces now title it "Breakdown · item 2 of 2" when the line holds more than one, so each number is correctly scoped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
Multi-item lines added a distinction the surrounding code predates: the item under the caret versus the whole line. Three places still answered for the item while presenting themselves as answering for the line. Copy summary wrote only the active item, so copying a two-item quote into an email sent one item's numbers. It now writes every item and the line total — and withholds that total while any item is incomplete, because a partial sum labelled "the total" is worse than none. The Save button's filled state came from matching the active item against the library, while pressing it creates a new assembly. A line whose last part was saved therefore looked saved and then saved again. State and behaviour now share the guard. Usage learning recorded the active item only, so a two-part line taught the suggestion ranking half of what was typed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
The mode switch moves into the hero's label row as two small KG/€ pills instead of taking a full-width row of its own, and the 109px two-stat card becomes a 34px strip — per piece, the other headline metric, and "Breakdown ›" as the way into the result sheet. A Save / Compare / Share row sits under it, which the phone previously only offered once you had opened the sheet. Net effect at 390×844: the hero block goes 270px → 221px and the flexible gap grows 73px → 136px, which is the room the session ribbon needs next. The screen still does not scroll. The label above the number now names the metric rather than the mode, since the highlighted pill already says which mode is on. PreviewCard, ChipBadge and formatCommandAliasName went with the old stat card — nothing else used them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
The fold puts a dashed ribbon under the hero — SESSION, the running total, "Open ›" and a + that logs the current line. The phone could add to the session before but never look at it, so the desktop rail's running total had no counterpart here and the tape was write-only. The library sheet gains SESSION as a fourth tab (the mockup shows three; per the brief PROJECTS stays too). It lists the tape newest first, each line loadable or removable, with the total and "Save as project" — the same two actions the desktop rail has had since 3.10.0. The recents strip it replaces was a partial stand-in for this: four queries, no totals, no way to remove one. Those lines are now in the session tab with everything else. At 390×844 the ribbon costs 54px and the flexible gap absorbs it — 70px still spare, no scrolling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
The fold draws a `>` key in the keypad's bottom row, which closes a gap I left open in 3.15.0: the palette shipped keyboard-driven and the phone keypad has no way to type `>`, so phones had no route to it at all. Tap it and the line becomes a command line. The `+` beside Save / Compare / Share is not in the mockup, but without it a phone can only view a multi-item line, never start one — and going through the palette is a long way round for something the desktop does in one press. It's disabled until the current line is a finished calculation, since there's nothing to add an item to before that. Three e2e now hold the fold's central property: at 390×844 the document never scrolls, and the hero, ribbon, palette key and keypad are on screen together. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
The two loose stat tiles become the fold's four-cell grid — kg/m, per piece, total weight, total cost. The values come from the same builder the breakdown panel uses rather than being recomputed, so the row above and the panel below cannot disagree about the same number. The action row gains "+ another item", which is what the desktop fold offers in place of hunting for the palette, and the top bar gains a `>` commands hint. That hint is the only place in the app that says how the palette opens — it shipped in 3.15.0 discoverable only by someone who already knew. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
Screenshotting the fold at 390×844 showed the bug: the strip wrapped to two rows, and the second row was clipped by the query line sitting under it. The layout has no vertical give to lend a strip whose height depends on how many chips the current stage happens to produce. The fold draws this as a single row that scrolls sideways, which is the fix as well as the design — the strip is now a fixed height whatever the stage offers. A test pins that, since the failure mode is silent: the chips are still in the DOM and still tappable, they just can't be seen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
Screenshotting a long line found what the tests hadn't: the chip box was flex-wrap with no ceiling, so a three-item query grew to four rows and pushed the keypad's bottom row — ↵, the unit key, the rate key — below the fold. The box now stops at two rows and scrolls to follow the caret, which sits at the end of the line; without that, typing walks off the visible area even with the box capped. The keypad also ended flush with the viewport edge, so on a device with a home indicator the bottom row sat underneath it. It pads by env(safe-area-inset-bottom) now, with a floor for browsers reporting none. And the hero had no flex-shrink-0, so on a short viewport it compressed and clipped itself rather than letting the flexible gap give — which is the only thing that should ever shrink. With a general tightening on top (keys 40→36, hero 64→56, less padding through the strip, actions and ribbon) the fold now fits a 667px screen with room, not just the 844 it was drawn for. Three e2e assert the ↵ key is fully inside the viewport at 844, 700 and 667. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
Logging a line moved the whole screen down. The ribbon carried weight and money side by side, and once both had digits there was no room left for "Open ›" on one line — it wrapped, the row went 48px to 72px, and everything below shifted with it. It now shows the total in whichever unit the hero is showing, then the line count, which is what the fold draws and is narrow enough that the row holds one height empty or full. My first attempt truncated the money figure instead, which was worse: a number cut mid-digit reads as wrong rather than as abbreviated, and the full split is one tap away in the session tab. The e2e measures the ribbon empty and populated and asserts both height and y-position, since the symptom was everything below it moving. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
Asked whether this was merge-ready, I screenshotted the surfaces I had changed but never looked at. Two were broken. The desktop glance row shared a flex row with the action cluster, which crushed each of the four cells to 21px against the 91–114px its value needed — every figure cut mid-number. The fold stacks these; they now sit on their own full-width row with the actions beneath. My e2e for it passed because getByText matches text that is in the DOM but invisible. The library's fourth tab ran off a 390px screen with PROJECTS cut in half. I first tuned the font down until English fitted, which was the wrong fix: Bosnian's POREĐENJE is longer than COMPARE, so it would have broken on the next locale. The pills are sized to their own labels now and the row scrolls. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
The rail's weight and money columns were a fixed 82px and 92px, so a value that outgrew them wrapped its unit onto a second line — KM 1,396.41 split into KM above 1,396.41, with empty space next to it. Reported on BAM, whose KM is two characters where € is one; that is why it never showed up in testing, and why the fix is verified by switching the app to BAM rather than by reasoning about it. The columns are a minimum width with nowrap now: still aligned, free to grow, and the unit cannot leave its number. The saved card had the same fixed 92px on both its figures and is fixed with it. The phone library tabs now label only the open one; the rest are their icon and count. Four labelled tabs never fitted 390px — my previous attempt let the row scroll, which just moved PROJECTS off screen. This keeps all four visible, and since only one label is ever rendered the row is the same width in every language rather than depending on how long the words happen to be. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvGsTcCt5QTiGFkJNYwPH1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Started as a review of the app at v3.7.0 and became the work that review
proposed. The branch now carries v3.7.0 → v3.16.2: nine feature
releases and two fix releases, plus
docs/REVIEW_2026-08.md, whichtracks what shipped and — for the one item that didn't — why not.
93 files changed. The review document and the changelogs account for a
large share of the additions;
packages/metal-coregained four newmodules and the command shell gained six.
What shipped
Saved entries became objects (3.8.0) — cross-section thumbnail,
spec-first title, live re-pricing against today's rate, search / sort /
tags / pin / duplicate. About ten functions in
useSaved.tswereimplemented and called by nothing; they have a UI now. Saved identity
moved to a geometry-only fingerprint so a re-priced entry can't
duplicate itself.
The bar got faster (3.9.0) — refine chips at the finished stage,
1–9to insert a chip, one Enter rule, a visible key map,?for thereference, last-query restore for returning users, memoized suggestions.
Quoting (3.10.0) — a rate per material grade (one global €/kg priced
stainless like mild steel), margin → sell price, session → project in
one press, assemblies, and a printable quote.
Design pass (3.11.0) — the 640–1023 range became the real workspace
instead of a 560px card on a background; one numeric type rule; accent
discipline; a shared motion vocabulary that honours reduced motion.
Target queries (3.12.0) —
hea120 6m =500kganswers how many ofthese make 500 kg; with a quantity fixed it solves the length. The
solver probes the engine rather than restating the pricing rules, so
waste, VAT and price basis count in for free, and a per-piece rate —
flat with respect to length — is left unsolved rather than answered
wrongly. Pieces round up and the badge states the overshoot.
Habits sync (3.13.0) — usage stats travel with the encrypted
snapshot as a grow-only counter per device. A shared total would
double-count on every round trip; each device pushes only what it
learned and the reader sums.
Multi-item lines (3.14.0) —
hea120 6m x2 + ipe200 4m x3. Thegrammar doesn't change, it repeats: each segment goes through the
ordinary parser, so order tolerance, per-item grades, per-item rates and
per-item targets work per item for free. Saving one makes an assembly;
logging drops one tape line per item.
The
>palette (3.15.0) — navigate, run an action, or find a savedentry or project by name. Ranking is prefix > word start > substring.
Arithmetic, cut-list paste, mass tolerance (3.16.0) —
6m-50mm,x2+3; paste a cut list and each row becomes an item; a ±% mass band onevery result.
Two fix releases (3.16.1, 3.16.2) from auditing the features against
each other rather than one at a time — see below.
Decisions worth a reviewer's attention
+does two jobs. It joins items and appears in arithmetic. Thecollision is settled by shape, not by guesswork: a
+glued between acharacter and a digit is arithmetic, anything else separates items,
because a new item always opens with a profile alias. A
+also means aspace in a query string, so share links escape it — there's a test
pinning that round trip specifically.
The EN part of the tolerance band is deliberately missing. The
mechanism ships; the standards table does not. EN mass tolerances differ
per product standard and some (EN 10029 plate) derive from a thickness
class rather than being a single percentage, so a flat per-family table
would be an oversimplification wearing a standard's name — the opposite
of the credibility the feature was for. The percentage is the user's own
figure and the UI says so. Sourced values drop into one table.
Section properties are not implemented at all. The review assumed
"the dataset rows already carry geometry"; they don't — the standard
profiles carry
areaMm2andperimeterMmonly. Ix/Wx/iy for ~150 sizeswould have to be generated rather than read, and invented section
properties in a structural tool are worse than the second browser tab
they were meant to replace.
docs/REVIEW_2026-08.md§4.8 records this.Five bugs came from features contradicting each other, not from any
feature being wrong on its own — multi-item lines introduced an
item-vs-line distinction the surrounding code predated. Paste replaced
the query outright and lost typed work; the breakdown showed one item's
mass band (±4% of 268 kg) directly under the hero's line band (±4% of
507 kg); copy summary sent one item of a two-item quote; the Save
button's state disagreed with what Save did; usage learning saw only the
last item. All fixed in 3.16.1–3.16.2, each with a regression test.
Checks
lint,test:all(442 unit tests across the engine, parser, stores andjsdom component tests),
i18n:check(en + bs parity),build, and 44Playwright e2e including axe scans. Every new pure module landed with
unit tests; new UI landed with e2e.
Not done
Section properties (§4.8), as above — a data-sourcing task, not a code
one. Everything else in the review is closed.