Skip to content

Calendar ranges, plus three demos: adaptive date field, LaTeX notation, normalizing grid - #5

Open
Aymericr wants to merge 7 commits into
mainfrom
feat/calendar-notation-grid-demos
Open

Calendar ranges, plus three demos: adaptive date field, LaTeX notation, normalizing grid#5
Aymericr wants to merge 7 commits into
mainfrom
feat/calendar-notation-grid-demos

Conversation

@Aymericr

@Aymericr Aymericr commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

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. parseDateRange only understood clock slots, so "July 1 to July 5" and "next week" both came back UNSUPPORTED_DATE. That's most of what people type into a date field. Now:

  • date-to-date spans: July 1 to July 5, Aug 3 - Aug 9, 2026-08-03 to 2026-08-09
  • calendar periods: a coarse single date expands to its real first and last day — next week → Mon–Sun, next month, August, 2027. A coarse endpoint widens when it closes a span too, so July to August ends August 31.
  • weekends: this weekend, next weekend, last weekend

Two details worth flagging for review. The bare-dash separator has to stay off when the input contains an ISO date, or 2026-08-03 splits itself — that's now a flag on the separator table rather than 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.

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 quantityField per 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 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 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 a4dd686 and e11782c.

Library semantics, all three shipped wrong by the first commit:

  • this weekend was wrong on a Sunday. It rounded forward to the coming Saturday, so the weekend the reader was standing in could only be reached as last weekend. Sunday now looks back a day. The round-trip matrix runs all seven weekday references instead of one Friday.
  • A coarse endpoint only widened when it stood alone. August spanned the month, but July to August ended on August 1 and until August did the same. The widening is shared now. from August still opens on the 1st — that asymmetry is deliberate.
  • Descending pairs came back descending. 2026-08-09 to 2026-08-03 returned start > end with no issue, contradicting this PR's own claim that a pair never reads backwards. Dated pairs swap and warn with the existing RANGE_REVERSED code. Overnight clock slots are untouched, since 9pm to 5am is 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 quarter was advertised; quarters parse as UNSUPPORTED_DATE. Claim removed, real reason backlogged (fiscal-year offsets).
  • "lingo emits the source" sat under the LaTeX demo while the same page says lingo ships no renderer. The caption now says what actually makes it possible.
  • "three widgets" promised a slot editor that doesn't exist; "accepts whatever people paste" is false for the GBP cell the demo deliberately refuses two paragraphs later.

Accessibility:

  • The calendar was 84 tab stops with no arrow keys. One roving tab stop now, with arrows, Home/End, PageUp/PageDown, and month paging when focus leaves the visible range.
  • KaTeX rendered html only, so a screen reader got loose glyphs. Now paired with MathML, visual layer hidden.
  • Grid assumptions lived in a title attribute; they're in the described text now. Cell labels name the row item rather than "row 1".
  • Totals read "Accepted mass/price" while a cell is refused — a sum that silently drops a refusal is the exact failure this demo argues against.

Deliberately not fixed here, logged in plans/backlog.md: humanizeDateRange drops seconds (pre-existing — verified against the commit before this feature, so it isn't a regression), elliptical right sides like Aug 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 gate
  • bun run typecheck, bun run lint, and bun run build in apps/site (lint back to its pre-existing baseline: 3 errors, 3 warnings, none in touched files)
  • Round-trip tests for every new range shape, now including Sunday, year-end, leap and common Februaries, and the reversed pair
  • Date range tests 56 → 87; 9 additive corpus rows, 0 breaking
  • Browser pass: keyboard navigation across both months, one tab stop of 84, MathML present, warnings in described text
  • Reviewer: try the three demos on /docs — resize to mobile, and try garbage input in each

Made with Cursor

Aymericr and others added 3 commits July 29, 2026 14:28
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>
@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lingo Ready Ready Preview, Comment Jul 29, 2026 4:16pm

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

React Doctor found 6 new issues in 2 files · 6 warnings · score 77 / 100 (Needs work) · 0 fixed · vs main

6 warnings

src/components/site/calendar-field-demo.tsx

  • ⚠️ L120 Locale/timezone formatting during render no-locale-format-in-render
  • ⚠️ L141 Locale/timezone formatting during render no-locale-format-in-render
  • ⚠️ L150 Locale/timezone formatting during render no-locale-format-in-render
  • ⚠️ L152 Locale/timezone formatting during render no-locale-format-in-render
  • ⚠️ L428 Locale/timezone formatting during render no-locale-format-in-render

src/components/site/data-grid-demo.tsx

  • ⚠️ L338 Missing effect dependencies exhaustive-deps

Reviewed by React Doctor for commit bc42660. See inline comments for fixes.

… 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>
Aymericr and others added 2 commits July 29, 2026 15:11
… 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' })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Docs

// 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' })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Docs

start && end
? Math.round((startOfDay(end).getTime() - startOfDay(start).getTime()) / 864e5) + 1
: 0
const left = start ? start.toLocaleDateString('en-US', { day: 'numeric', month: 'short' }) : '—'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Docs

: 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' })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Docs

type="button"
>
<span className="sr-only">
{day.toLocaleDateString('en-US', { dateStyle: 'full' })}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Docs

price: price.toFixed(2),
refused,
}
}, [rows])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Docs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant