calendar-tsx

Writing an adapter

Bind the core to a framework or a renderer.

An adapter does three things: keeps one instance per component, syncs the options on every render, and re-renders when the state changes. The React and Solid bindings are built this way.

bind-calendar.ts
// the shape of a framework binding: create once, sync options on every render, mount with the component
export const bindCalendar = <
  TData,
  TResourceData,
  TViews extends Calendar_Views,
  TFeatures extends Calendar_ConfiguredFeatures,
>(
  // intersect the validation, as the bindings do, so missing features stay a type error for your users
  initial: Calendar_Options<TData, TResourceData, TViews, TFeatures> & Calendar_ValidateFeatures<TViews, TFeatures>,
  onChange: () => void,
) => {
  const calendar: Calendar_Instance<TData, TResourceData, TViews, TFeatures> = createCalendar(initial);

  return {
    calendar,
    /** On every render, with the current options; writes only when a tracked option changed. */
    update: (options: Calendar_Options<TData, TResourceData, TViews, TFeatures>) => calendar['~update'](options),
    /** When the component mounts: start the clock and listen to the store. Returns the cleanup. */
    mount: () => {
      const unmount = calendar['~mount']();
      const subscription = calendar.store.subscribe(onChange);

      return () => {
        subscription.unsubscribe();
        unmount();
      };
    },
  };
};

Lifecycle

MemberWhenWhat it does
createCalendarOnce, on creationDerives the first state, with the current time from initialNow or the clock.
'~update'(options)On every renderReplaces the options. Writes the store only when a tracked option changed, so it's cheap to call.
'~mount'()When the component mountsReads the clock and subscribes to its ticks. Returns the cleanup.
store.subscribeWhile mountedNotifies on every state change. TanStack Store's adapters (@tanstack/react-store, …) select from it.
  • Create the calendar once. Creating one per render loses feature state and subscriptions.
  • Don't call ~mount on the server. Without it the clock never starts, so server rendering leaves no timer behind.
  • Type your options with Calendar_ValidateFeatures, as the example does. It preserves the check that the calendar's registry satisfies the bound views.

Context

Your parts need the instance. The bindings put it in context with its types erased, because a context cannot be generic, and let users get the types back by annotating selectors with Calendar_StateOf.

The erased context accepts the core state fields and a layout with kind: string. It does not require merged feature fields. Typing it with the wide Calendar_Features would require every imported feature's state and make unrelated calendar instances incompatible.

Navigation triggers take the typed instance explicitly so their target keys remain checked across the context seam.

Parts

To reuse the themes, render the same attributes and custom properties as the bindings' parts: data-scope='calendar', a data-part name, boolean state attributes as data-x="", and the geometry as unitless --cal-* properties. Custom themes lists every part and the variables it carries.

Use native buttons for events, triggers and overflow, and the roles the bindings use, so keyboard and screen reader behavior matches.