when i import an event from ical, it should take me there
Status: brainstorm, not approved. Written 2026-08-23, after the cloud-sync audit.
Every idea here is filtered through the rules the repo already holds: chrome stays greyscale and colour belongs to events, the Worker keeps exactly one route, local-first with no readable server-side state, and everything findable by walking the days. Each entry notes what it would cost and what it must reuse. Two ideas at the end are recorded as refusals, so they are argued once instead of re-litigated.
-
Multi-day drag on the dial. — Shipped. Both halves: the arc drag's day-following already existed (
dayFollow); this added the draw's. The sweep is unbounded now, and the decision worth recording is when the face steps: not at the first midnight — the range draws whole on the anchor face up to a full revolution, wrapping the top (angleAtis linear,arcPathtakes raw degrees, so the past you drew stays visible) — but once no face can show it, onto the leading edge's day. The release commits against the grab day, Escape gives the stepped days back, and steps issue through a ref because two pointermoves can share one stale render closure and a counter in state would step twice. -
Travel time on events. — Shipped.
CalEvent.travel, minutes typed by hand, no geo API as sketched. Three decisions beyond the sketch. The point turned out to be arithmetic, not the pip: a commute is time spent, so it is booked, andbookedBlock— the run-up plus the event, or nothing — became the single statement of what "busy" means, read byfreeGaps,clashesWithand the face alike, so auto-schedule stops offering you your own commute. It is drawn as the dotted outline of the band it books, not as a pip and not as the filled dashed band that shipped first: at the arc's own weight a run-up reads as the meeting with a fault in it, where an outline says "this footprint, none of the event in it" and a broken line says "estimate". And a lead is measured back fromleaveByrather than from the start, so the notification, the headline row and the tab title all count down to the minute you have to go.X-APPLE-TRAVEL-DURATIONon the wire, Apple's own spelling: a private property would round-trip with nothing but circal, and Apple Calendar already reserves the same window and raises the same alert off it. -
Day-shape templates. "Tuesday is a deep-work day": a named waking window and default calendar per weekday. Cheap — it is settings — and it feeds
freeGaps, the palette's slot search, and TASKS.md's auto-schedule when that lands. One idea of the working day stays one idea; the template merely varies it by day. -
A
Joinverb on conferencing links. — Shipped (d764774).joinLinkinevents.ts(whole-hostname match: Meet/Zoom/Jitsi,urlthenlocation); the verb sits on the headline row inside the reminder lead and while running — you join the meetings you are late to most of all. -
Duplicate day. — Shipped (
7ea7107, plus4e1f822/c019934date grammar). The Jump row's ⌘↵:Copy N events here, counted offdayCopiesbefore Enter. One decision beyond the sketch: copies are one-offs — a day's shape once, never a second series — and only occurrences starting on the source day travel.last <weekday>and nearest-month-day parsing exist because the copy source is almost always in the past. -
Jog-wheel day scrubbing. Hold a modifier and drag on empty face = rotate the day like a jog wheel, one revolution = one day. Same family as the zoom/unroll transitions: moving between days is geometry, not a button.
-
Weekly totals band. — Shipped (
68f0b59). The strip's shared readout already counted events and booked hours; this added the missing half,dayLoad'sfree, in the centre panel's exact vocabulary —7h 10m booked · 93h 50m free— so "free" keeps one meaning across faces. Two decisions beyond the sketch: one line under the seven faces rather than seven captions (already the readout's own argument — the grid's point is the comparison, and seven sentences is the loudest thing that could sit under it), and the free clause yields belowsm, where the summed week's sentence no longer fits beside its head. Booked and free still measure two different windows and still do not sum — the strip inherited that whole sentence by calling the same function. -
Free-slot sharing. — Shipped (
d54b6dc). As sketched:freeGaps+ the share-token machinery, gaps never events, booking stays out. Decisions beyond the sketch: slots travel as absolute instants, the timed event's own rule — free time exists to be booked over, so it is a moment two people share, and wall minutes would offer hours the sender is asleep in; the window is seven days from now, not "this week" — Monday-to-Sunday minted on a Friday is four dead days; and the receiver lands on the week strip, the one face that reads the whole payload at a glance, with the gaps drawn hollow — the free event's own encoding, since a filled arc at any alpha read as the receiver's own events — and one quiet line naming whose the overlay is and how to take it down. Nothing is ever written: the fragment is consumed on arrival, a reload starts clean. "Booking stays out" was half right — the backend stays out: the completion (ROADMAP Tier 1) later gave the strip's slots a tap that opens a draft on the receiver's own calendar, and the save offers the reply — a#e=link back plus a one-event.ics— with the human as the channel. -
Natural-language edits in the palette. — Shipped. As sketched:
parseQuickEditgrows verb heads on the same right-to-left scanner, targets resolve throughfuzzyScoreover the corpus the palette already ranks, and the one/future/all question isScopeChoicesin the card a drag gets. Decisions beyond the sketch:pullispushwith the sign flipped, one grammar for both directions (delay/rescheduleare spellings, not branches);toandbybecome filler only on a verb line, since a head is what makes them structural where quick-add'stobelongs to titles ("go to gym"); a verb moves and never resizes unless the line names both ends (move sync to 13:00-14:00); all-day and subscription targets are no candidates rather than refusals — the ranking carries on, and the row naming what it resolved to is what keeps that readable; and the commit goes through the drag's own seam (seedOccurrence→adjustOccurrence), so scope is measured from the seed and the preview, the notice and "nothing written until answered" all come for free. A verb head stays a plain word when no event answers it:move sofa 3pmis still a create row. -
Day notes. One plain-text note per day key, stored in the blob, drawn as a dot on the mini-month and a paragraph in the interior at rest. Journaling without becoming a journal app: still keyed by day, still found by walking days. Must not grow markdown, titles, or a list of notes — one day, one note.
-
Ambient mode as a real clock face. — Already shipped before this list was written:
F/⇧F(fullscreen), auto-arrival afterambientAfter, wake lock, parked cursor, headline-only card, lightcycle dim. Nothing left to do. -
Daylight across the year, on the year ring. — Built and removed. It shipped as the rim's own background tint, then read on the face as a glow smear behind the ring rather than as data about the days it sat behind, so it came out again. The rim background was the last place left to try, the interior figure having been refused before it; the idea is done.
-
Rings closed. — Shipped (
e2cad30). One dash of light walked once round the track when the day's last open pip is ticked — the intro's own dash arithmetic, leaving from the current minute. Decisions beyond the sketch: what "the day's work is done" means is a predicate,dayClearedinevents.ts, not a check inlined in the shell — an empty checklist is not a finished one, and a subscription's task (which cannot be ticked here) keeps the day open; the once-a-day guard is a session ref, not storage — it exists against tick-untick-tick, not against tomorrow; and it never fires on an untick or underuseReducedMotion, where it would be the only thing left moving. -
Save this face. Print/export a day or year as SVG/PNG — the faces are resolution-independent drawings already. Screenshots of dials are how this app spreads; give them a verb. Shell save-panel bridge (
window.__circalSave) already answers the WKWebView half.
-
Theme presets. — Shipped (
5eab28b). Midnight, Ember, Paper — enumerated,sane()-pinned, never a theme editor, as sketched. Two corrections to the sketch's cost line. The tokens moving was the small half: the real work was every colour a stylesheet cannot reach — the elapsed ramp, the life grid's greys, the year ring's canvas — now fed bylib/theme.ts, whosewashis a lerp from the theme's own void; on a black void that is byte-identical to the multiply it replaced, so midnight repaints nothing (pinned intests/theme.test.ts). AndinkOnneeded no re-verification at all — an arc label sits inside its own pastel, not on the canvas; the contrast that did need computing was the accent as small text, which is why paper's is a darker aqua than the calendar default it echoes. -
Editable event palette.
ColorPickerhas an any-colour cell; let the tenPALETTEslots themselves be edited (settings, synced), withinkOnchecks so a user cannot pick a colour the arc label cannot sit on. -
Dial density presets. Numerals every hour vs. every three, tick weight —
DIALis already a table; expose two or three named presets ("minimal", "instrument", "dense"), never sliders. A slider is a thousand faces nobody tested; three presets are three faces somebody did. -
Per-calendar reminder lead. — Shipped (
d355909). The inherit chain gained its middle level — event → calendar → setting — exactlycolorOf's shape;leadOf(event, calendars, fallback)stays the sole reader of the precedence. Two decisions beyond the sketch: a calendar's silence quiets every inheriting event on it (the third state exists at this level too), and the event editor's inherit option now names the level that answers —Follow calendar · 1h beforewhen the calendar has an opinion.
- Google Calendar, both directions. The read half is already shipped by
construction: every Google calendar has a "secret address in iCal format",
and pasting it into subscriptions works today through the one route the
Worker has. The write half is not architecturally blocked, which is
worth recording because "we can't, no server" is the reflexive answer and
it is wrong: Google Identity Services' token flow hands the browser an
access token with no backend at all, and the Calendar API accepts requests
straight from the browser — so a
googleLink.tswould sit at exactlycloudLink.ts's tier and shape (opt-in, lazily imported, talks directly to Google, no new Worker route, no readable server-side state). The hard parts already exist in the repo:ics.tsspeaks full RRULE / EXDATE / RECURRENCE-ID, andmerge.ts/mutate.tsdefine what a conflict means. What it is actually blocked on: real engineering (a foreign data model, sync tokens, a mapping layer — months, not weeks), Google's OAuth verification for the calendar scope, and one honest line to draw out loud — events synced there sit plaintext in the user's own Google, which softens the sealed-box story. That is the user's choice about their own account, the travel prompt's shape; the standing refusal below is circal becoming a relay between users, which this is not. Decision deferred until demand shows up after launch, not refused.
-
Month grid view. The obvious ask and the wrong one: a grid is the thing every other calendar is. Circal's answer to "month" is the year ring plus the drawer's mini-month; a grid view would be a second product's home screen inside this one.
-
Accounts, collaboration, shared calendars. The invitation-reading section already draws this line: answering is a message, and circal has no channel to post one. Crossing it makes circal a sync product — the cloud copy stays a sealed box, never a relay.
#4 (Join verb), #7 (weekly totals band), #8 (free-slot sharing), #9 (palette verbs), #11 (ambient clock), #13 (rings closed) and #15 (theme presets) are done — see their entries. The shortlist is spent; what remains above is unranked.