05customization
Settings dialog
The settings module — the optional preferences dialog, its KloqPreferences model, and the account helpers behind it.
@/components/kloq/settings is its own module because it is genuinely
optional: the calendar works with no settings surface at all, and a host with
its own preferences screen wants the KloqPreferences model without pulling
in a dialog it will never render.
The dialog owns no settings
SettingsDialog is a value / onChange pair over a plain KloqPreferences
object and nothing else — no context, no localStorage, no writes into
<Kloq>. That is forced by the architecture rather than chosen: "show
weekends" and "start week on" are props on the calendar, so the only honest
thing a settings surface can do is report intent upward and let the host pass
it back down. A dialog that secretly mutated the calendar would be a second
source of truth, and the two would drift the first time the host changed a
prop.
import { Kloq } from "@/components/kloq";
import {
SettingsDialog,
DEFAULT_PREFERENCES,
type KloqPreferences,
} from "@/components/kloq/settings";
const [prefs, setPrefs] = useState<KloqPreferences>(DEFAULT_PREFERENCES);
const [open, setOpen] = useState(false);
<>
<SettingsDialog open={open} onOpenChange={setOpen}
value={prefs} onChange={setPrefs} />
<Kloq
weekStartsOn={prefs.weekStartsOn}
showWeekNumbers={prefs.showWeekNumbers}
showDeclinedEvents={prefs.showDeclinedEvents}
navigationKeys={prefs.navigationKeys}
locale={prefs.language || undefined}
/>
</>The KloqPreferences model
Flat and serializable on purpose: persist the whole object under one key.
Every field maps to either a <Kloq> prop or a host behaviour, and nothing is
optional — a settings screen with tri-state values renders switches that are
neither on nor off. DEFAULT_PREFERENCES matches the calendar's own defaults.
showWeekends?booleanShow Saturday and Sunday columns.
showDeclinedEvents?booleanMaps to the showDeclinedEvents prop.
showWeekNumbers?booleanISO week numbers in the gutter.
weekStartsOn?WeekStart0 Sunday, 1 Monday, 6 Saturday.
navigationKeys?NavigationKeys"range" — ←/→ step the visible range; "day" — always one day.
language?stringBCP 47 tag, or "" for "follow the system". LANGUAGE_OPTIONS is the built-in list; pass your own.
desktopNotifications?booleanThe host may show desktop notifications at all.
eventReminders?booleanFire an alert before each event — delivery is the host's job.
defaultReminderMinutes?numberThe default reminder offset stamped on new events.
dailyAgenda?booleanA once-a-morning summary of the day.
SettingsDialogProps
Four panes — "general", "accounts", "conferencing", "notifications"
(SettingsSection; defaultSection picks which opens first). Beyond the
value / onChange pair, the props follow one honesty rule: a button with
nothing behind it isn't rendered. Omit onAddAccount and there is no "Add
calendar account" button — the package owns no OAuth flow, so a connect button
that couldn't connect would be a lie.
open / onOpenChange?boolean / (open) => voidStandard dialog control.
value / onChange?KloqPreferencesThe whole preferences object, fully controlled.
accounts / sources?CalendarAccount[] / CalendarSource[]Rows to render. Omit to read the calendar-source registry.
conferencing?readonly ConferencingProvider[]Options offered; default CONFERENCING_PROVIDERS.
languages?readonly {id; label}[]Default LANGUAGE_OPTIONS.
onAddAccount?() => voidOmit and the button isn't rendered.
onDisconnectAccount / onSetAccountConferencing?(accountId, …) => voidForwarded to the account rows. Omit → the registry handles it.
defaultSection?SettingsSectionWhich pane opens first; default "general".
Accounts as first-class
Accounts are the one exception to "the dialog owns nothing" — they already
have a registry (registerCalendarSources), and the account rows write
through it unless the host passes handlers: the same controlled/uncontrolled
split as everything else. The module re-exports the grouping's helpers so a
host's own settings screen can build the same rows: findAccount,
defaultAccount, setDefaultAccount, disconnectAccount, providerLabel,
conferencingLabel, accountDefaultCalendar, setAccountDefaultCalendar,
resolveDefaultCalendar, and setAccountConferencing.
SegmentedChoice
The package ships no Select and no RadioGroup, so every "pick one of a few"
control in the dialog is SegmentedChoice — a segmented control built from
buttons with a real role="radiogroup" / role="radio" pair, exported for
hosts building matching settings UI. For 2–5 options it is the better control
anyway: every choice is visible and it costs one click, not two.