02examples
Custom ⌘K commands
Domain commands in the palette, a remapped key, and a button that opens the shortcut sheet.
Four commands that belong to this calendar and nowhere else — book a focus block, jump to the next standup, toggle the working-hours shading, copy the week as ICS — registered into the same ⌘K the built-ins live in. The keymap is Vim-flavoured: G for today, H and L for the previous and next range.
Click inside the calendar, press ⌘K and type "focus". Press ? for the shortcut sheet; its "Customize shortcuts…" button opens the bindings row above the grid.
"use client";
import { useCallback, useEffect, useState } from "react";
import {
Kloq,
KEY_ACTIONS,
displayKey,
expandRecurrence,
formatDayLabel,
formatEventWhen,
formatInstant,
newEventId,
nextOccurrence,
parseInstant,
rangeOfView,
registerCommands,
resolveKeymap,
useKloq,
useKloqStore,
writeEventsToClipboard,
type CalEvent,
} from "kloq";
import { Kbd } from "@/components/ui/kbd";
import { cn } from "@/lib/utils";
import { EVENTS, KEYMAP } from "./data";
const FOCUS_MIN = 90;
/** The first 90-minute gap today, from the next half hour, that no busy event touches. */
function nextFreeSlot(events: CalEvent[], now: Date) {
const cursor = new Date(now);
cursor.setMinutes(now.getMinutes() < 30 ? 30 : 60, 0, 0);
for (let i = 0; i < 30; i++) {
const start = new Date(cursor.getTime() + i * 30 * 60_000);
const end = new Date(start.getTime() + FOCUS_MIN * 60_000);
if (end.getDate() !== now.getDate()) return null;
const busy = events.some(
(e) => !e.allDay && e.busy !== false && expandRecurrence(e, { start, end }).occurrences.length > 0,
);
if (!busy) return { start: formatInstant(start), end: formatInstant(end) };
}
return null;
}
function DomainCommands({
shade,
onToggleShade,
onStatus,
}: {
shade: boolean;
onToggleShade: () => void;
onStatus: (status: string) => void;
}) {
const store = useKloqStore();
const k = useKloq();
useEffect(
() =>
registerCommands(
{
id: "docs:focus-block",
label: "Book a focus block",
keywords: "deep work 90 minutes free slot",
group: "Focus",
run: () => {
const now = new Date();
const slot = nextFreeSlot(store.getSnapshot(), now);
if (!slot) return onStatus("no free 90 minutes left today");
const event: CalEvent = { id: newEventId(), title: "Focus block", color: "emerald", ...slot };
store.add(event);
if (!k.atToday) k.goToday();
onStatus(`booked · ${formatEventWhen(event, now)}`);
},
},
{
id: "docs:next-standup",
label: "Jump to the next standup",
keywords: "series occurrence go to",
group: "Focus",
run: () => {
const now = new Date();
const standup = store.get("standup");
const next = standup ? nextOccurrence(standup, formatInstant(now)) : null;
if (!next) return onStatus("no standup ahead");
const day = new Date(parseInstant(next.start));
k.goToDate(day);
onStatus(`next standup · ${formatDayLabel(day, now)}`);
},
},
{
id: "docs:shade",
label: "Shade hours outside 9–5",
keywords: "working hours tint",
group: "Focus",
checked: shade,
run: onToggleShade,
},
{
id: "docs:copy-week",
label: "Copy this week as ICS",
keywords: "export calendar file clipboard",
group: "Focus",
run: () => {
const range = rangeOfView(k.view);
const visible = store
.getSnapshot()
.filter((e) => expandRecurrence(e, range).occurrences.length > 0);
void writeEventsToClipboard(visible).then(() =>
onStatus(`copied ${visible.length} events — paste into any calendar app`),
);
},
},
),
[store, k, shade, onToggleShade, onStatus],
);
return null;
}
function Bindings({ onClose }: { onClose: () => void }) {
const { byAction } = resolveKeymap(KEYMAP);
return (
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5 border-b bg-muted/30 px-4 py-2 font-mono text-xs text-muted-foreground">
{KEY_ACTIONS.map((a) => {
const key = byAction[a.id];
return (
<span key={a.id} className="inline-flex items-center gap-1.5">
{a.label}
{key ? (
<Kbd className={cn(a.id in KEYMAP && "text-red-500")}>{displayKey(key)}</Kbd>
) : (
<span className="opacity-60">unbound</span>
)}
</span>
);
})}
<button type="button" onClick={onClose} className="ml-auto transition-colors hover:text-foreground">
close
</button>
</div>
);
}
export default function CommandsAndKeysExample() {
const [shade, setShade] = useState(true);
const [global, setGlobal] = useState(false);
const [bindings, setBindings] = useState(false);
const [status, setStatus] = useState<string | null>(null);
const toggleShade = useCallback(() => setShade((s) => !s), []);
return (
<div className="h-[34rem]">
<Kloq
defaultEvents={EVENTS}
keymap={KEYMAP}
onCustomizeKeybinds={() => setBindings(true)}
globalShortcuts={global}
workingHours={shade ? { start: 9, end: 17 } : false}
persistence={false}
designMode={false}
storage={false}
>
<Kloq.Toolbar />
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 border-b px-4 py-2 font-mono text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
press <Kbd>⌘K</Kbd> · try <span className="text-foreground">“focus”</span>
</span>
<span className="hidden items-center gap-1 sm:inline-flex">
<Kbd>G</Kbd> today <Kbd>H</Kbd> <Kbd>L</Kbd> range <Kbd>?</Kbd> sheet
</span>
<span className="truncate" aria-live="polite">
{status ? (
<><span className="text-red-500">●</span> {status}</>
) : (
"click inside the calendar first — keys are scoped to it"
)}
</span>
<label className="ml-auto inline-flex cursor-pointer items-center gap-1.5">
<input type="checkbox" checked={global} onChange={(e) => setGlobal(e.target.checked)} className="accent-red-500" />
⌘K from anywhere
</label>
<button type="button" onClick={() => setBindings((b) => !b)} className="transition-colors hover:text-foreground">
bindings
</button>
</div>
{bindings ? <Bindings onClose={() => setBindings(false)} /> : null}
<DomainCommands shade={shade} onToggleShade={toggleShade} onStatus={setStatus} />
</Kloq>
</div>
);
}
import { instantAt, resolveView, type CalEvent, type Keymap } from "kloq";
/** A light week: a standup series to jump between and a few meetings to book around. */
export function buildWeek(now: Date = new Date()): CalEvent[] {
const week = resolveView("week", 1, now);
const at = (day: number, hour: number, minute = 0) => instantAt(week, day, hour * 60 + minute);
return [
{
id: "standup",
title: "Standup",
color: "blue",
rrule: "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR",
start: at(0, 9, 30),
end: at(0, 9, 45),
},
{ id: "design", title: "Design review", color: "violet", start: at(1, 13), end: at(1, 14) },
{ id: "interview", title: "Interview", color: "rose", start: at(2, 15), end: at(2, 15, 45) },
{ id: "lunch", title: "Lunch with Sam", color: "amber", busy: false, start: at(4, 12), end: at(4, 13) },
];
}
export const EVENTS = buildWeek();
/** Vim-style navigation: `t` becomes `g`, and the shipped-unbound range pair gets `h` / `l`. */
export const KEYMAP: Keymap = { today: "g", "prev-period": "h", "next-period": "l" };
How it works
registerCommands(...commands)is a module-level registry: no provider, no prop threading. It returns an unregister function covering exactly what the call added, souseEffect(() => registerCommands(...), deps)registers on mount and cleans up on unmount. Re-registering anidreplaces the command in place, which is why the "Shade hours" toggle keeps its slot whilecheckedflips. Shape and grouping are on Command palette.- The commands run inside
<Kloq>—DomainCommandsis a child that renders nothing — soruncan reach the store throughuseKloqStore()and navigation throughuseKloq():store.add,store.get("standup"),k.goToDate,k.viewfor the visible range. - "Book a focus block" checks each candidate slot with
expandRecurrence(event, { start, end }), which returns the single occurrence for a non-recurring event and the expanded ones for a series, so one call handles both. keymaptakes overrides exactly as a settings screen would store them:{ today: "g", "prev-period": "h", "next-period": "l" }. The bindings row reads the effective table back throughresolveKeymap(KEYMAP).byActionand renders each key withdisplayKey; the remapped ones are in red. See Keyboard for what is fixed and why.onCustomizeKeybindsputs a "Customize shortcuts…" button in the ? sheet. kloq ships no editor of its own — the callback opens whatever you build.- Keys are scoped: the calendar claims them only when the last pointer press
landed inside it.
globalShortcutswidens ⌘K, ⌘Z and ⌥D to the whole page, which is right for an app whose screen is the calendar. It is off here by default because this page already owns ⌘K for search; the checkbox lets you feel the difference.
Take it further
Every command is just data with a run, so the same list can drive a menu or
a toolbar. getCommands / subscribeCommands and the pure groupCommands /
matchesQuery are exported for hosts building their own palette surface.