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 offset

now 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

InputTypeNotes
standup tomorrow 2pmdate + timeTomorrow 14:00–14:30 — a lone start defaults to 30 minutes.
review 1-2pmrange13:00–14:00 — the meridiem is borrowed leftwards across the dash.
call from 2 to 4range14:00–16:00 — bare hours read via the afternoon heuristic; from/to, until, till, dashes all work.
party tomorrow 9pm to midnightcross-midnightThe end carries onto the next day rather than inverting.
deep work for 90 minutes at 2pmduration14: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 minspart of daymorning / afternoon / evening / tonight supply the hour.
coffee next fridayweekdayA 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 Augall-dayExplicit all day, or any date with no time. Ordinals, month names, numeric dates (12/08, month-first by default) and years all parse.
dinner @ Padellalocationat / @ + a venue. at 3pm is never stolen — the time pass claims ranges first.
lunch with sam@acme.comattendeesBare 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?
string

The input with every consumed phrase stripped and the seams tidied.

start / end?
Date | null

null only for blank input — otherwise there is always a usable draft.

allDay / durationMin?
boolean / number | null

durationMin is null for all-day.

attendees / location?
string[] / string | null

Bare 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.

on this page