Calendar ranges, plus three demos: adaptive date field, LaTeX notation, normalizing grid - #5
Calendar ranges, plus three demos: adaptive date field, LaTeX notation, normalizing grid#5Aymericr wants to merge 7 commits into
Conversation
Empirical probing turned up two things the plan had to answer before any grammar work: `2+3 kg` is silently read as a range with full confidence (no issue, contradicting D4's honest-ambiguity rule), and additive compounds over affine units already delta-convert correctly but never say so. Both are recorded as fixes that stand on their own. The plan now takes the leading-`=` mode switch from spreadsheet formula entry, which makes expressions purely additive: bare input keeps every reading it has today, `=` opts into arithmetic. It also reframes the value around LLM tool calls — models set arithmetic up well and execute it badly, so having the model emit the expression and lingo evaluate it deterministically is the safety story, not a calculator feature. Co-authored-by: Cursor <cursoragent@cursor.com>
parseDateRange only understood clock slots, so "July 1 to July 5" and "next week" both came back UNSUPPORTED_DATE — which is most of what people type into a date field. A second pass now reuses the single-date parser on each side of the existing separators, and expands a coarse single date (week, month, quarter, year, weekend) into its real first and last day rather than a midnight instant. Two details the implementation forced. The bare-dash split has to stay off when the input contains an ISO date, or "2026-08-03" splits itself; that is now a flag on the separator table instead of a special case at the call site. And the result carries a `dated` flag, because humanizeDateRange otherwise renders a day span as "12:00 AM to 12:00 AM" and breaks the two-way guarantee. The date entry and the marginal ai/date budgets move up ~450 B; D71 records the measurement and the case for spending it. Co-authored-by: Cursor <cursoragent@cursor.com>
…emos Three demos for the shapes people actually build, each answering a question the prose alone leaves open. The date field shows that one input can serve a day picker, a two-month range picker, and a time slot, because the reading says which shape it found — no mode toggle for the person typing. It is the visible payoff of the calendar-range work in the previous commit. The notation demo makes the case that a canonical reading is structured, not just cleaned up: unit id and value are separate fields, so mapping a result to LaTeX is ~90 lines with no renderer in the library. KaTeX and its stylesheet load in their own chunk on mount. The grid puts a quantityField on a table column and lets every cell take whatever people paste, which is the same field that guards a tool call — "a column is a schema". It surfaces the honest-ambiguity story where it is easiest to feel: totals add up only because the column has one unit, assumptions are marked, and a GBP cell in a USD column refuses rather than inventing a rate. Shared useHydrated hook so both date-dependent demos hold a fixed reference time through hydration and then switch to the real clock. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
React Doctor found 6 new issues in 2 files · 6 warnings · score 77 / 100 (Needs work) · 0 fixed · vs 6 warnings
Reviewed by React Doctor for commit |
… the column code Browser-testing the three demos turned up three places where the surface undersold or obscured what is actually happening. The date field dropped the time. "tomorrow at 3pm" parses to an instant with grain=hour, but the summary rendered dateStyle:'full' and showed "Thursday, July 30, 2026" — the field looked less precise than the reading. It now prints the clock when one was pinned, and `tomorrow` sits beside `tomorrow at 3pm` in the examples so the difference is visible without typing. The grid's cells did not look editable: transparent inputs with no resting border read as static text, so the one thing to do in the demo was invisible. Vertical rules turn it into a spreadsheet, which is the metaphor that already means "type here" — cheaper than 30 boxes. Price also gains width, because RATE_REQUIRED was truncating to RATE_REQUIR…. The grid was the only demo with no source panel, and it is the one whose whole claim is the field declaration. Its columns.ts snippet now shows the four fields and the safeParse branch that separates a refusal from an assumption. Co-authored-by: Cursor <cursoragent@cursor.com>
… pairs swap Adversarial review of the calendar-range work found three wrong semantics, all shipped by the previous commit and all now covered by tests. "this weekend" rounded forward to the coming Saturday, so on a Sunday it meant next weekend and the weekend the reader was standing in could only be reached as "last weekend". Sunday now looks back a day. A coarse endpoint only widened when it stood alone, so "August" spanned the month but "July to August" ended on August 1. The widening is now shared by the standalone path, the closing side of a split range, and "until X" — while "from August" still opens on the 1st, which is the asymmetry we want. "2026-08-09 to 2026-08-03" came back descending with no issue, contradicting the claim that a pair never reads backwards. Dated pairs now swap and warn with the existing RANGE_REVERSED code. Overnight clock slots are untouched: 9pm to 5am is a real slot, not a typo. Tests go from 56 to 87: the weekend matrix runs all seven weekday references instead of one Friday, and the round-trip cases add Sunday, year-end, leap and common Februaries, and the reversed pair. Nine additive corpus rows, zero breaking. Budgets unchanged. Backlogged rather than fixed here: humanizeDateRange drops seconds (pre-existing, verified against the commit before this feature), elliptical right sides like "Aug 3-9", and quarters (needs a fiscalYearStart option to mean anything). Co-authored-by: Cursor <cursoragent@cursor.com>
…ut a mouse
The calendar demo classified anything undated as a time slot, so "3 days
starting monday" — one of its own example chips — showed a single-day time slot
instead of a three-day span. Day-grained endpoints now count as a span, and the
exclusive end of an anchored duration is normalized to the last day covered so
the highlight and the day count agree.
Copy the reviewers could disprove:
- "next quarter" was advertised; quarters parse as UNSUPPORTED_DATE.
- "lingo emits the source" sat under the LaTeX demo, while the same page says
lingo ships no renderer. The renderer is site code, and the caption now says
what actually makes it possible: value and unit id come back separately.
- "three widgets" promised a slot editor that does not exist — three readings.
- "accepts whatever people paste" is false for the GBP cell the demo
deliberately refuses two paragraphs later.
Accessibility, all of it reachable without a pointer:
- The calendar was 84 tab stops with no arrow keys. One roving tab stop now,
with arrows, Home/End, PageUp/PageDown, and paging when focus leaves the
visible months. In-range days report as pressed, endpoints say which end.
- KaTeX rendered html only, so a screen reader got loose glyphs. Paired with
MathML, visual layer hidden.
- Grid assumptions ("assuming short ton") lived in a title attribute; they are
in the described text now. Cell labels name the row item, not "row 1".
- Totals read "Accepted mass/price" while a cell is refused, since a sum that
silently drops a refusal is the exact failure the demo argues against.
Co-authored-by: Cursor <cursoragent@cursor.com>
The first question rendered as: How do I parse "5'11\"" into meters — a literal backslash on the page. Inside a template literal \\" emits a backslash and a quote, and the value needs no escaping at all; line 20 of this same file already writes 5'11" correctly. Dropped the wrapping quotes too, since nesting them around an inch mark is what invited the escape. The answer had the same escape buried in a code expression. Rewrote it as prose that names the option and the result without a nested string delimiter — the 1.8034 figure is verified against the parser. The rows hid the native disclosure marker and put nothing in its place, so six cards sat there with no sign they opened. Added a chevron that rotates on open, a hover tint, and the site's standard focus ring, all on the existing motion tokens. Co-authored-by: Cursor <cursoragent@cursor.com>
| } | ||
|
|
||
| function monthLabel(d: Date): string { | ||
| return d.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }) |
There was a problem hiding this comment.
React Doctor · react-doctor/no-locale-format-in-render (warning)
This can cause a hydration mismatch because toLocaleDateString() (reached from JSX through "monthLabel") formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.
Fix → Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.
| // would show the field as less precise than the reading actually is. | ||
| return isTimed(reading.result) | ||
| ? start.toLocaleString('en-US', { dateStyle: 'full', timeStyle: 'short' }) | ||
| : start.toLocaleDateString('en-US', { dateStyle: 'full' }) |
There was a problem hiding this comment.
React Doctor · react-doctor/no-locale-format-in-render (warning)
This can cause a hydration mismatch because toLocaleDateString() (reached from JSX through "summary") formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.
Fix → Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.
| start && end | ||
| ? Math.round((startOfDay(end).getTime() - startOfDay(start).getTime()) / 864e5) + 1 | ||
| : 0 | ||
| const left = start ? start.toLocaleDateString('en-US', { day: 'numeric', month: 'short' }) : '—' |
There was a problem hiding this comment.
React Doctor · react-doctor/no-locale-format-in-render (warning)
This can cause a hydration mismatch because toLocaleDateString() (reached from JSX through "summary") formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.
Fix → Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.
| : 0 | ||
| const left = start ? start.toLocaleDateString('en-US', { day: 'numeric', month: 'short' }) : '—' | ||
| const right = end | ||
| ? end.toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' }) |
There was a problem hiding this comment.
React Doctor · react-doctor/no-locale-format-in-render (warning)
This can cause a hydration mismatch because toLocaleDateString() (reached from JSX through "summary") formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.
Fix → Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.
| type="button" | ||
| > | ||
| <span className="sr-only"> | ||
| {day.toLocaleDateString('en-US', { dateStyle: 'full' })} |
There was a problem hiding this comment.
React Doctor · react-doctor/no-locale-format-in-render (warning)
This can cause a hydration mismatch because toLocaleDateString() formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.
Fix → Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.
| price: price.toFixed(2), | ||
| refused, | ||
| } | ||
| }, [rows]) |
There was a problem hiding this comment.
React Doctor · react-doctor/exhaustive-deps (warning)
useMemo can run with a stale fields & show your users old data.
Fix → Don't blindly add missing dependencies. Read the hook callback first.
Bad:
useEffect(() => {
setCount(count + 1);
}, [count]);
Better:
useEffect(() => {
setCount((currentCount) => currentCount + 1);
}, []);
If the missing value is recreated every render, move it inside the hook or stabilize it before adding it to deps.
Three demos for the shapes people actually build, plus the library work one of them needed.
Why
The docs prove that lingo parses strings. They don't prove what that buys you once it's wired into a real component. These three answer questions the prose leaves open — what a single input can replace, what "canonical" means structurally, and what happens when a column of free text has to add up.
What's in it
feat(date)— calendar ranges.parseDateRangeonly understood clock slots, so"July 1 to July 5"and"next week"both came backUNSUPPORTED_DATE. That's most of what people type into a date field. Now:July 1 to July 5,Aug 3 - Aug 9,2026-08-03 to 2026-08-09next week→ Mon–Sun,next month,August,2027. A coarse endpoint widens when it closes a span too, soJuly to Augustends August 31.this weekend,next weekend,last weekendTwo details worth flagging for review. The bare-dash separator has to stay off when the input contains an ISO date, or
2026-08-03splits itself — that's now a flag on the separator table rather than a special case at the call site. And the result carries adatedflag, becausehumanizeDateRangeotherwise renders a day span as"12:00 AM to 12:00 AM"and breaks the two-way guarantee.feat(site)— three demos.Adaptive date field. One input serves a day picker, a two-month range picker, and a time slot. No mode toggle, because the reading says which shape it found. This is the visible payoff of the date work above.
Notation. Renders results as LaTeX. The point isn't the math styling — it's that a canonical reading is structured, not just cleaned up. Unit id and value are separate fields, so mapping a result to LaTeX is ~90 lines of site code with no renderer in the library. KaTeX and its stylesheet load in their own chunk on mount.
Normalizing grid. A
quantityFieldper table column, each cell taking any notation that column can resolve. Same field that guards a tool call, so the pitch is "a column is a schema". It's also where honest ambiguity is easiest to feel: totals add up only because the column has one unit, assumptions are marked, and a GBP cell in a USD column refuses rather than inventing a rate.docs(plans)— plan 032 rewrite. Not shipped code. Empirical probing turned up two things the arithmetic plan had to answer first:2+3 kgis silently read as a range with full confidence (no issue, contradicting D4's honest-ambiguity rule), and additive compounds over affine units already delta-convert correctly but never say so. Both are recorded as fixes that stand on their own. The plan now takes the leading-=mode switch from spreadsheet formula entry so expressions are purely additive, and reframes the value around LLM tool calls — models set arithmetic up well and execute it badly.Review pass
Two adversarial reviews (GPT-5.6 on UX, Grok-4.5 on correctness) ran against the branch and converged on real defects. Everything below was found by them and is fixed in
a4dd686ande11782c.Library semantics, all three shipped wrong by the first commit:
this weekendwas wrong on a Sunday. It rounded forward to the coming Saturday, so the weekend the reader was standing in could only be reached aslast weekend. Sunday now looks back a day. The round-trip matrix runs all seven weekday references instead of one Friday.Augustspanned the month, butJuly to Augustended on August 1 anduntil Augustdid the same. The widening is shared now.from Auguststill opens on the 1st — that asymmetry is deliberate.2026-08-09 to 2026-08-03returnedstart > endwith no issue, contradicting this PR's own claim that a pair never reads backwards. Dated pairs swap and warn with the existingRANGE_REVERSEDcode. Overnight clock slots are untouched, since9pm to 5amis a real slot.Demo and copy, where the reviewers could disprove the page:
3 days starting monday— one of the calendar demo's own example chips — rendered as a single-day time slot. Day-grained endpoints now count as a span, and the exclusive end of an anchored duration is normalized so the highlight and the day count agree.next quarterwas advertised; quarters parse asUNSUPPORTED_DATE. Claim removed, real reason backlogged (fiscal-year offsets).Accessibility:
htmlonly, so a screen reader got loose glyphs. Now paired with MathML, visual layer hidden.titleattribute; they're in the described text now. Cell labels name the row item rather than "row 1".Deliberately not fixed here, logged in
plans/backlog.md:humanizeDateRangedrops seconds (pre-existing — verified against the commit before this feature, so it isn't a regression), elliptical right sides likeAug 3–9, and quarters.Budgets
The date entry and the marginal ai/date budgets move up ~450 B. D71 records the measurement and the case for spending it. The review fixes fit inside that headroom — no further recalibration. Everything else is unchanged and under budget.
Test plan
bun run check— typecheck, 1029 tests, build, size, corpus gate, zero-deps gatebun run typecheck,bun run lint, andbun run buildinapps/site(lint back to its pre-existing baseline: 3 errors, 3 warnings, none in touched files)/docs— resize to mobile, and try garbage input in eachMade with Cursor