feat(scheduler): add scheduler state machine#3087
Merged
segunadebayo merged 55 commits intochakra-ui:v2from Apr 23, 2026
Merged
feat(scheduler): add scheduler state machine#3087segunadebayo merged 55 commits intochakra-ui:v2from
segunadebayo merged 55 commits intochakra-ui:v2from
Conversation
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The machine already supports `view: "agenda"` (30-day rolling window from the focused date) and connect exposes the "Agenda" label, but there was no example page and the shared control panel's view select didn't offer it. Add an agenda page that groups events by day and register it in the examples route. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fallback copy said "next 30 days" even after navigating backwards, which was wrong once the window no longer starts at today. Show the real visibleRange bounds instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neither was hooked up to real behavior: - Resource/resourceId/getResourceHeaderProps/resources prop threaded through every layer but no example used it and the drag flow never populated `newResourceId`. Removing the speculative API lets us re-introduce a proven shape once a real multi-resource use case (booking, Gantt) drives the requirements. - `SchedulerEvent.display: "default" | "background"` was declared but neither machine nor connect ever read it — consumers couldn't rely on anything. BREAKING: these types no longer exist — `Resource`, `ResourceHeaderProps`, `resources` prop/api, `resourceId` on events and details, `newResourceId` on drop details, and `display` on events. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull locale-aware data and RTL-unsafe position math out of every
example and into the api:
- api.today — locale/timezone-aware today date
- api.visibleDays — enumerated dates in the visible range
- api.weekDays — 7 localized day-of-week labels ordered by weekStartDay
- api.hourRange — { start, end, hours } for the day/week grid
- api.visibleRangeText — { start, end, formatted } header text
- api.getEventById — O(1) Map lookup (replaces .find scans)
- api.getEventStyle — ready-to-spread CSS with inset-inline logical
props; used by getEventProps internally
- api.getTimePercent — 0..1 fraction of the visible day range
- api.dir — writing direction, also emitted on root
Internal getEventProps now delegates to getEventStyle, and the
current-time indicator switched to inset-inline-start/end so RTL
locales render correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Year view was rebuilding the month grid, weekday initials and today/outside data attrs by hand. Pull all of it into the api: - api.getMonthGrid(date?) — weeks × days with inMonth/isToday/isWeekend - api.getMonthName / api.monthNames — locale-aware month labels - getDayCellProps now emits data-today, data-outside, data-weekend, data-selected, and aria-current="date" when applicable; accepts an optional referenceDate so mini-month cells know what "outside" means - Root exposes --scheduler-visible-days, --scheduler-day-count, and --scheduler-hour-count so grid-template-columns can live in CSS instead of being recomputed inline per example Also dedupe DAY_NAMES (token strings for @internationalized/date startOfWeek, not display labels) — they already existed in utils/visible-range.ts. Shared via a new toDayOfWeekToken export. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- EventDropDetails and EventResizeDetails now carry `index` + a
strongly-typed `apply(events)` so consumers can write
`setEvents(d.apply)` instead of mapping by hand. The generic is
constrained to `{ id; start; end }` — no `any` leaks.
- Grid height moves to CSS: --scheduler-hour-height (default 56px) and
--scheduler-grid-height (computed from hour-count × hour-height) are
applied on the time grid, time gutter, and day columns. basic.tsx no
longer passes `style={{ height: gridHeight }}` on every element.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consumers were computing hour positions by hand:
style={{ top: `${((h - start) / (end - start)) * 100}%` }}
and formatting labels with padStart. Push both into the api.
HourEntry is now { value, label, percent } where label is
locale-formatted via Intl.DateTimeFormat and percent is the pre-computed
0..1 grid position. Consumers just do `style={{ top: `${h.percent * 100}%` }}`
and render `{h.label}`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Export `scheduler.getToday(timeZone?)` so examples and apps stop
reaching into @internationalized/date to construct a starting date.
Returns a CalendarDateTime at midnight in the given/local timezone.
- basic.tsx drops `defaultDate` entirely — the machine already defaults
to `today(timeZone)` — and anchors demo events relative to
`scheduler.getToday()` so the initial view always has visible content
regardless of wall-clock date.
- HourEntry gains `style: { top }` so consumers stop doing
`style={{ top: \`${percent * 100}%\` }}` in each example. Kept
`percent` for custom layouts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a drag/resize/slot-select pushes the pointer within 50px of the scheduler's scroll container edge, auto-scroll the viewport at up to 12px/frame with proximity ramping. Mirrors the Mantine UX so gestures can reach events that are below the fold without letting go. The scrollable ancestor is discovered once per gesture by walking up from the grid element; if nothing scrolls, auto-scroll is a no-op. Scroll RAF is stopped on pointer-up, escape, and effect teardown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously a plain click on an empty slot fired onSlotSelect with start === end, forcing consumers to detect "was this a click?" themselves. Now the machine does the fork at invoke time: - onSlotClick(details) fires when the gesture never crossed a snap boundary — i.e. the user clicked without dragging. Details carry start plus a convenience `end` (start + slotInterval) so "create event at this time" flows can one-line it. - onSlotSelect(details) fires only when a real drag selection happened (start < end). No callback wiring changes for consumers using onSlotSelect alone — they'll just stop seeing zero-width selections. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- utils/layout.ts replaces per-event overlap resolution with a single sweep-line pass. O(N log N) for the full set instead of O(N cubed). - connect: computeEventLayout runs once; getEventPosition is a Map.get. getEventsForDay reads a pre-built per-day bucket. Slot lookups scan only that day's bucket. hasConflict / getEventState read a pre-built conflict Set. Multi-day events bucket into every day they touch. - stress.tsx generates 100 to 5000 events via a seeded PRNG so perf can be eyeballed and drag/resize exercised at scale. - Renamed recurrenceExpansionLimit to maxRecurrenceInstances. - api.dragOrigin exposes the original bounds of the active gesture target so consumers can paint a faint origin outline. - onSlotClick forked from onSlotSelect — clicks fire with a convenience end = start plus slotInterval. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure mechanical refactor — no feature changes. Every scheduler example except basic.tsx was still running the old hand-rolled patterns that basic.tsx dropped: - DAY_LABELS arrays → api.weekDays - enumerateDays util → api.visibleDays - HOURS Array.from → api.hourRange.hours (with .style + .label) - gridHeight + gridTemplateColumns inline → driven by CSS vars - per-event top/height/left/width math → api.getEventStyle - visibleRange.start.toString().slice(0,10) → api.visibleRangeText.formatted - hand-rolled month grid + data-today/data-outside → api.getMonthGrid + getDayCellProps - onEventDrop/Resize .map boilerplate → d.apply Files touched and line-count delta: all-day.tsx 195 → 151 (-44) controlled.tsx 179 → 145 (-34) mobile-month-view.tsx 175 → 156 (-19) recurring.tsx 170 → 134 (-36) views.tsx 316 → 238 (-78) with-date-picker.tsx 258 → 213 (-45) work-week.tsx 161 → 125 (-36) Total: 1454 → 1162 lines (~20% off). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…utline Drag previously felt like "event teleports on release". Now mirrors Mantine's feel: - Source event fades to 0.25 opacity in place while [data-dragging] — the "was here" cue without tearing the element out. - Root emits data-dragging / data-resizing / data-slot-selecting so other events stop intercepting pointer events during a gesture. - Day columns emit data-drop-target when the live drag resolves to them. CSS paints a soft background + inline-start accent rule so the user sees where the drop will land before releasing. - basic.tsx renders two overlays inside each day column: a filled .scheduler-drag-ghost that tracks the predicted drop position, and a dashed .scheduler-drag-origin outline at the gesture's starting bounds. Both read from api.dragPreview / api.dragOrigin — no state plumbing in user code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consumers were still rebuilding the ghost and origin-outline styles
by hand: top/height percentages, inset-inline 2px padding, and the
--event-color CSS var. Push all of that into the api:
- getDragGhost({ date }) → { style, event } | null
- getDragOrigin({ date }) → { style, event } | null
Both return null when nothing should render in the given column so
consumers just do `ghost && <div {...}>`. Style is an EventStyle with
logical props and --event-color baked in. getEventStyle also now
includes --event-color so callers don't spread it manually.
basic.tsx drops ~40 lines of positional math and extraneous imports.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 2px inline padding on .scheduler-drag-ghost and .scheduler-drag-origin was being emitted inline from the api on every pointer-move, which mixed presentation into the machine's responsibilities. It now lives in CSS via --scheduler-event-inset (default 2px), overridable from the consumer's stylesheet. getDragGhost / getDragOrigin return only positioning (top/height) and --event-color. EventStyle's insetInlineStart/End are now optional since ghost/origin omit them entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e icons
Every example now flips cleanly when dir="rtl" without a separate
rtl demo page — just toggle the control panel. Instead of a standalone
example we wire rtl into schedulerControls.dir.
- Shared CSS swept from physical to logical properties everywhere
basic.tsx touches: border-left/right → border-inline-start/end,
left/right on absolute children → inset-inline-start/end,
margin-left → margin-inline-start, padding-left/right → padding-inline,
border-radius corners on the resize handle → border-start/end-*.
- api exposes direction-aware prev/next glyphs:
api.prevTriggerIcon // "←" in LTR, "→" in RTL
api.nextTriggerIcon // "→" in LTR, "←" in RTL
basic.tsx consumes those; the hardcoded arrows are gone.
- Dropped the planned rtl.tsx example route — the control handles it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"7:00 AM" was rendering as "AM 7:00" when the grid is RTL because the unicode bidi algorithm reordered the two weakly-LTR runs. Add direction: ltr + unicode-bidi: isolate to .scheduler-hour-label so the formatter output renders as a single LTR atom regardless of the surrounding direction. Header title gets unicode-bidi: plaintext so it auto-picks direction from content — English formatter output stays LTR, Arabic locale output stays RTL, no container reordering. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eview" in rtl text-overflow clips from the inline-end side, which for LTR English content inside an RTL grid is the physical left — producing "...sign review" instead of "Design review..." Added unicode-bidi: plaintext to .scheduler-event-title so the Latin run picks its own direction and truncates from its own end. Same fix was already applied to .scheduler-hour-label and the header title; this rolls it out to event titles too. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o-create, type payload cleanly Comment cleanup across scheduler source — dropped running commentary, kept only the one-liner explaining the +1 day range-filter extension (the single non-obvious why). Types / machine / connect / layout / visible-range all slimmer. Over-emphatic selected state: shadow was 2px ring + drop shadow. Dropped to a 1px ring so clicking to select doesn't swamp the card. Click-to-create: basic.tsx now wires onSlotClick to prompt for a title and insert the event. Confirms the onSlotClick vs onSlotSelect fork fires correctly on a plain click. Payload example: rewritten to show title + compact meta line in the card, full payload in a side panel on click. Latin padStart time format replaced with Intl.DateTimeFormat. Typed payload without any/as-unknown: added SchedulerPayload type alias (= any) mirroring collection's CollectionItem pattern — now every generic (SchedulerEvent, SchedulerProps, SchedulerSchema, SchedulerApi, connect's E, layout utils) uses `T extends SchedulerPayload = SchedulerPayload`. Consumers specialize with a single cast at the useMachine call site: useMachine(scheduler.machine as scheduler.Machine<MyPayload>, props) The connect return is inferred from the service type — no second cast. Matches the select / combobox / listbox / gridlist / tree-view pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
basic.tsx wired onSlotClick but nothing fired because day columns had no pointerdown handler — only getTimeSlotProps (unused here) and getDayCellProps (month view) did. Added pointerdown to getDayColumnProps that derives start/end from the click Y position using the configured slotInterval, ignoring clicks that land on an existing event. Result: clicking on empty space inside a day column fires SLOT_POINTER_DOWN, which on release (no drag) fires onSlotClick with the snapped slot bounds — the promised click-to-create flow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mple
A plain slot click now lights up the clicked slot immediately, so
users get visual confirmation before any create flow opens:
- Machine sets context.selectedSlot on click / drag-select; clears on
escape or a fresh SLOT_POINTER_DOWN.
- api adds: selectedSlot (readonly), clearSelectedSlot(), and
getSelectedSlot({ date }) returning { style, start, end } or null —
same shape as getDragGhost / getDragOrigin.
- New .scheduler-slot-selected CSS (dashed outline + soft fill) reads
the shared --scheduler-event-inset var.
basic.tsx drops the blocking window.prompt and just renders the
highlight — that's enough to demonstrate the UX primitive.
New example click-to-create.tsx shows the proper create flow built
on @zag-js/dialog: on onSlotClick open a dialog prefilled with the
slot, Enter / Create inserts the event, Escape / Cancel clears the
highlight via api.clearSelectedSlot. Dialog styles added to shared
scheduler.css (backdrop, positioner, content, primary button).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same treatment as the .ts files — section headers and running commentary removed. Selectors are self-documenting.
Matches google/apple calendar UX: single click highlights the slot, double click opens the create flow. Previously the two were conflated on onSlotClick, so users could never just select a slot without triggering create. - New onSlotDoubleClick callback fires from a dblclick handler on getDayColumnProps (time-grid views) and getDayCellProps (month view). Slot math reuses the same helper as the pointerdown handler. - onSlotClick still fires for the highlight path — basic.tsx's slot highlight still shows on single click. - click-to-create.tsx now opens the dialog on onSlotDoubleClick. Route relabeled to "Double-click to Create (Dialog)". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… workWeekOnly
Two subagents in parallel landed most of this:
Machine additions:
- api.visibleEvents — events filtered to visibleRange (already
computed internally, now exposed).
- api.agendaGroups — [{ date, events }] grouped by day and sorted.
Kills hand-rolled groupByDay/bucketing in every agenda-style page.
- api.formatTime / formatTimeRange / formatLongDate — Intl-backed
helpers; replaces padStart + toLocaleDateString scattered in 5 files.
- api.todayTriggerLabel — pulled from translations; consumers stop
hardcoding "Today".
- workWeekOnly prop: when true + view="week", api.visibleDays filters
to workWeekDays. work-week.tsx can drop its manual .getDay() sieve.
Example sweep (13 pages touched):
- ←/→ → api.prevTriggerIcon / api.nextTriggerIcon (10 pages)
- events.find(e=>e.id===id) → api.getEventById (event-details, payload)
- views.tsx dropped its ~22-line drag-ghost reimplementation in favor
of api.getDragGhost.
- "Today" hardcode → {api.todayTriggerLabel} (13 pages).
- local formatTime / formatLongDate / padStart deleted from agenda,
event-details, mobile-month-view, payload. All use api.formatTime*
/ formatLongDate.
- agenda.tsx: groupByDay + the visible-range filter gone — uses
api.agendaGroups + api.visibleRangeText.formatted. Inline
marginBottom moved to a .scheduler-agenda-group class backed by
--scheduler-agenda-group-gap.
- agenda.tsx events anchored to scheduler.getToday() instead of
hardcoded 2026 CalendarDateTime constants.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
payload.tsx was ~30 lines of inline style objects on the event detail
aside and grid container. Moved everything to shared CSS classes
backed by CSS vars where sensible:
- .scheduler-with-aside (grid container) with --scheduler-aside-width
and --scheduler-aside-gap
- .scheduler-event-panel + sub-classes for title / time / sections /
row / link / empty
mobile-month-view's inline `new Date(...).toLocaleDateString({ month,
year })` swapped for `api.getMonthName(api.date) + api.date.year` —
uses the existing machine formatter instead of constructing a JS Date
just for localization.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…urationMinutes Two leftover spots the earlier audit flagged: 1. work-week.tsx was still hand-filtering api.visibleDays via `new Date(d.year, d.month-1, d.day).getDay()` — the exact pattern `workWeekOnly` was introduced to replace. Flip `workWeekOnly: true` and destructure `visibleDays` directly. Dropped the local WORK_WEEK const. 2. recurring.tsx's weeklyExpander computed event duration by round- tripping DateValue → string → JS Date → getTime() three times per instance — fragile (timezone-dependent via toString) and noisy. Exposed `scheduler.getDurationMinutes(start, end)` from utils and used it; the expander is now 6 lines instead of 20 and doesn't touch JS Date at all. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ommon rules
Structured recurrence the machine expands itself:
recurrence: { freq: "daily" | "weekly" | "monthly" | "yearly";
interval?: number; count?: number; until?: DateValue;
exdate?: DateValue[] }
The old { rrule: string } variant still works and still routes through
the user's expandRecurrence prop — opt-in for rrule-string users.
connect's expansion pipeline:
- rec has `freq` → expandNativeRecurrence (built-in sweep)
- rec has `rrule` → delegate to expandRecurrence
- no rec → pass through
recurring.tsx drops its 13-line weeklyExpander and its
`expandRecurrence` prop. Events just declare
`recurrence: { freq: "weekly" }` or `{ freq: "weekly", interval: 2,
count: 8 }` and the machine produces the instances, filtered to
visibleRange and capped by maxRecurrenceInstances.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final cleanup pass:
- api.formatDuration(start, end) returns "1h 30m" / "45m" etc. Uses
getDurationMinutes internally. event-details.tsx drops its 20-line
toMinutes / formatDuration helpers and uses the api method.
- event-details popover: 6 inline style objects moved to shared CSS —
.scheduler-event-popover + body/row/dot/title/time/duration/actions
classes. --event-color scoped via inline style on the body wrapper,
consumed by .scheduler-event-popover-dot via var().
- agenda.tsx drops its lone `style={{ marginTop: 12 }}` override
(the class already has margin-top: 16px).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Anatomy now covers the overlays / hour marks / agenda groups that
consumers render today:
dragGhost, dragOrigin, slotHighlight, hourLabel, hourLine,
agendaGroup, agendaGroupTitle
getDragGhost / getDragOrigin / getSelectedSlot now return
`{ props, event? }` where props spreads the anatomy data-attr +
positioning style. Consumers spread instead of wiring a className:
{ghost && <div {...ghost.props}>{ghost.event.title}</div>}
CSS selectors for the overlays switched from `.scheduler-drag-ghost`
class to `[data-scheduler-drag-ghost]` attribute so users styling
via their own classnames aren't blocked.
README rewritten against the current api surface — install, quick
start, event model, views, props table, callbacks table, full api
reference grouped by purpose, recurrence (native + rrule fallback),
styling via CSS vars, RTL, perf guarantees, controlled vs uncontrolled,
composition examples, anatomy parts list. No invented symbols — every
referenced name exists in scheduler.types.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #
📝 Description
Adds
@zag-js/scheduler— a framework-agnostic headless calendar machine with day / week / month / year / agenda views, drag-move / resize / click-to-create / double-click-to-create, native recurring events, conflict detection, RTL, and locale-aware formatting. 14 example pages under/scheduler/*.⛳️ Current behavior (updates)
No scheduler primitive exists in the monorepo. Consumers building calendar UIs reach for external libraries, each of which ships opinionated markup and styling.
🚀 New behavior
Machine
Single
createMachinewith four states:idle | slot-selecting | event-dragging | event-resizing. All date math uses@internationalized/date. Drag viatrackPointerMovefrom@zag-js/dom-query(no HTML5 DnD).recurrence: { freq, interval?, count?, until?, exdate? }expanded inside the machine;{ rrule: string }still routes to user-suppliedexpandRecurrencefor rrule-library users.SchedulerEvent<T>threads through every callback detail. Single cast atuseMachine(scheduler.machine as scheduler.Machine<MyPayload>, …)—connectinfersEfrom the service.api surface (consumer renders markup; machine supplies everything else)
today,visibleDays,visibleEvents,agendaGroups,visibleRange,visibleRangeText,weekDays,hourRange,events,dragPreview,dragOrigin,selectedSlot,monthNames,dir,prevTriggerIcon,nextTriggerIcon,todayTriggerLabelgetEventById,getEventsForDay,getEventsForSlot,hasConflict,getEventStategetEventPosition,getEventStyle,getTimePercent,getMonthGrid,getMonthNamegetDragGhost({ date }),getDragOrigin({ date }),getSelectedSlot({ date })formatTime,formatTimeRange,formatLongDate,formatDurationsetView,setDate,goToToday,goToNext,goToPrev,clearSelectedSlotscheduler.getToday(timeZone?),scheduler.getDurationMinutes(start, end)UX
onSlotClick(plain click → highlight),onSlotDoubleClick(create-event trigger),onSlotSelect(drag selection).@zag-js/dialog.@zag-js/popover(anchored viagetAnchorRect).RTL + i18n
inset-inline-start/end,border-inline-start,padding-inline, etc.).api.prevTriggerIcon/nextTriggerIcon.unicode-bidi: plaintexton headers / titles,direction: ltr+isolateon numeric hour labels so Latin times stay readable inside RTL grids.dir="rtl"on props — works across every example.CSS variables emitted on root
--scheduler-visible-days,--scheduler-day-count,--scheduler-hour-count. Consumer-overridable:--scheduler-time-gutter-width(60px),--scheduler-hour-height(56px),--scheduler-grid-height,--scheduler-event-inset(2px),--scheduler-agenda-group-gap,--scheduler-aside-width,--scheduler-aside-gap.Examples
basic,views,controlled,work-week,all-day,recurring,agenda,year-view,mobile-month-view,with-date-picker,payload,event-details,click-to-create,stress— 14 pages, ~2150 LOC total (basic.tsx is 122).💣 Is this a breaking change (Yes/No):
No — net-new package.
📝 Additional Information
Deferred to follow-ups:
ViewType— today it'sview="week"+workWeekOnly: true.Performance: the stress example generates 100–5000 events via a seeded PRNG and is drag-interactive without jank at 5000.
Composability: the
click-to-createandevent-detailsexamples validate the "compose scheduler with another zag machine" story —@zag-js/dialogand@zag-js/popoverrespectively.