06interaction
Quick add
Natural-language event entry: parseQuickAdd, the grammar it understands, and the offsets that power the live underline.
One line of typing becomes a draft event. parseQuickAdd("lunch with sam@acme.com tomorrow 1pm for 90 minutes", now) resolves the when, collects the attendees, and — the part that makes it feel magic rather than guessy — reports the exact source offsets of every phrase it consumed, so the input underlines what it understood as you type.
Using it
Double-click empty grid (or press C for a new event at the next hour) and type. The parser also powers the ⌘K palette's Quick add group. It's pure — no React, no DOM, not even an import — and exported for your own surfaces:
import { parseQuickAdd } from "@/components/kloq";
const r = parseQuickAdd("dinner @ Padella friday 7pm", new Date());
// r.title → "dinner"
// r.start/r.end → Friday 19:00–19:30 (local time)
// r.location → "Padella"
// r.confidence → "full"
// r.tokens → the claimed spans, sorted by source offsetnow is a parameter on purpose — nothing inside calls new Date(), so every relative phrase resolves against the caller's clock and the whole thing is deterministic and testable. All arithmetic is local-time through the Date constructor's overflow, which is what keeps it correct across DST in every zone.
What it understands
| Input | Type | Notes |
|---|---|---|
| standup tomorrow 2pm | date + time | Tomorrow 14:00–14:30 — a lone start defaults to 30 minutes. |
| review 1-2pm | range | 13:00–14:00 — the meridiem is borrowed leftwards across the dash. |
| call from 2 to 4 | range | 14:00–16:00 — bare hours read via the afternoon heuristic; from/to, until, till, dashes all work. |
| party tomorrow 9pm to midnight | cross-midnight | The end carries onto the next day rather than inverting. |
| deep work for 90 minutes at 2pm | duration | 14:00–15:30. for 30min, for 1h, for half an hour, fractional hours, and bare adjectival durations (45min gym) all parse. |
| gym tomorrow morning for 45 mins | part of day | morning / afternoon / evening / tonight supply the hour. |
| coffee next friday | weekday | A bare weekday resolves forwards (today counts); next friday is always exactly a week past the bare reading, so the two can never collide. |
| offsite all day on 12 Aug | all-day | Explicit all day, or any date with no time. Ordinals, month names, numeric dates (12/08, month-first by default) and years all parse. |
| dinner @ Padella | location | at / @ + a venue. at 3pm is never stolen — the time pass claims ranges first. |
| lunch with sam@acme.com | attendees | Bare emails anywhere, deduped case-insensitively; a leading with / w/ is swallowed. |
Passes run in one fixed left-to-right claim table, and each may only claim source ranges no earlier pass took — how at 3pm beats the location rule, and how an email that happens to contain 3pm is never read as a time. A time that already passed today rolls to tomorrow — unless a day was named, in which case you said what you meant. What isn't recognised stays in the title, casing and punctuation intact.
The result
title?stringThe input with every consumed phrase stripped and the seams tidied.
start / end?Date | nullnull only for blank input — otherwise there is always a usable draft.
allDay / durationMin?boolean / number | nulldurationMin is null for all-day.
attendees / location?string[] / string | nullBare emails in order; the first venue.
confidence?"none" | "partial" | "full"none: treat the whole string as a title. full: a day and a time (or an explicit all-day) both landed.
tokens?QuickAddToken[]Every claimed span — text, [start, end) offsets, kind: date | time | duration | location | attendee.
Options: dateOrder ("month-first" default; either way an unambiguous half wins) and defaultDurationMin (default 30, matching quick-create).
Building the preview UI
The view half is exported too. highlightSegments interleaves the input with its tokens — concatenating every segment's text reproduces the input exactly, the property the caret-aligned underline overlay depends on. quickAddPreview folds a result into one renderable line: the title (never blank — UNTITLED covers an all-metadata input), the resolved when, and chips for location and attendees.
And the finished field — underline, preview line, commit — ships assembled as
QuickAddInput, with a live demo on its own page.