Building Blocks
When neither the generic nor the restaurant widget fits your use case, compose the flow yourself. Every step component is exported under @periscaleai/booking-widget/blocks and (for restaurants) @periscaleai/booking-widget/restaurant.
ts
import {
CalendarPicker,
TimeSlotGrid,
BookingDetailsForm,
BookingConfirmation,
MyBookings,
StepIndicator,
TabBar,
BackButton,
} from "@periscaleai/booking-widget/blocks";Step components
| Component | Inputs | Output |
|---|---|---|
CalendarPicker | selectedDate, maxAdvanceDays, onSelect(date) | Calls onSelect with "YYYY-MM-DD" when the user clicks a day. |
TimeSlotGrid | client, date, serviceId?, selectedSlot, onSelect(slot) | Loads slots from client.getSlots(date) and lets the user pick one. |
BookingDetailsForm | client, date, slot, service?, onConfirmed(appointment) | Renders a name/phone/email/notes form and calls client.createAppointment. |
BookingConfirmation | appointment, config, onViewMyBookings, onBookAnother | Success screen. |
MyBookings | client, config | Phone-lookup + cancel UI. Reads client.getMyAppointments(phone) and client.cancelAppointment(id, reason). |
Shell primitives
| Component | Purpose |
|---|---|
StepIndicator<K> | Generic step indicator. Pass steps: { key: K; label: string }[] and currentStep: K. |
TabBar<K> | Tab strip with icons. Pass tabs, active, onChange. |
BackButton | Back arrow + label. Pass onClick. |
Example: a salon flow with a stylist picker
tsx
import * as React from "react";
import { Calendar, ListChecks } from "lucide-react";
import {
CalendarPicker,
TimeSlotGrid,
BookingDetailsForm,
BookingConfirmation,
StepIndicator,
TabBar,
BackButton,
} from "@periscaleai/booking-widget/blocks";
import { createBookingClient } from "@periscaleai/booking-widget/client";
import type { Appointment, TimeSlot } from "@periscaleai/booking-widget";
import { StylistPicker } from "./StylistPicker"; // your custom step
type Step = "date" | "time" | "stylist" | "details" | "confirm";
const STEPS: { key: Step; label: string }[] = [
{ key: "date", label: "Date" },
{ key: "time", label: "Time" },
{ key: "stylist", label: "Stylist" },
{ key: "details", label: "Details" },
];
export function SalonBookingWidget({ apiBase, apiKey }: { apiBase: string; apiKey: string }) {
const client = React.useMemo(() => createBookingClient(apiBase, { apiKey }), [apiBase, apiKey]);
const [tab, setTab] = React.useState<"reserve" | "my-bookings">("reserve");
const [step, setStep] = React.useState<Step>("date");
const [date, setDate] = React.useState<string | null>(null);
const [slot, setSlot] = React.useState<TimeSlot | null>(null);
const [stylistId, setStylistId] = React.useState<string | null>(null);
const [confirmed, setConfirmed] = React.useState<Appointment | null>(null);
return (
<div className="rounded-2xl bg-white shadow-lg overflow-hidden">
<TabBar
tabs={[
{ key: "reserve", label: "Book", icon: <Calendar className="h-4 w-4" /> },
{ key: "my-bookings", label: "My visits", icon: <ListChecks className="h-4 w-4" /> },
]}
active={tab}
onChange={setTab}
/>
<div className="p-6">
{step !== "date" && step !== "confirm" && (
<BackButton onClick={() => /* …step-back logic… */ undefined} />
)}
{step !== "confirm" && <StepIndicator steps={STEPS} currentStep={step} />}
{step === "date" && (
<CalendarPicker
selectedDate={date}
maxAdvanceDays={30}
onSelect={(d) => { setDate(d); setStep("time"); }}
/>
)}
{step === "time" && date && (
<TimeSlotGrid
client={client}
date={date}
selectedSlot={slot}
onSelect={(s) => { setSlot(s); setStep("stylist"); }}
/>
)}
{step === "stylist" && date && slot && (
<StylistPicker
date={date}
slot={slot}
selected={stylistId}
onSelect={(id) => { setStylistId(id); setStep("details"); }}
/>
)}
{step === "details" && date && slot && stylistId && (
<BookingDetailsForm
client={client}
date={date}
slot={slot}
service={null}
onConfirmed={(apt) => { setConfirmed(apt); setStep("confirm"); }}
/>
)}
{/* …confirmation, my-bookings tab… */}
</div>
</div>
);
}Adding a new industry flavor to the package
If your industry pattern is reusable across customers, contribute it back as a new flavor:
- Create
src/<vertical>/types.ts— extendAppointmentwith vertical-specific fields. - Create
src/<vertical>/client.ts— extendBookingClientwith vertical endpoints. - Add components in
src/<vertical>/components/— compose withCalendarPicker,TimeSlotGrid,StepIndicator,TabBar,BackButton. - Add a top-level widget wiring it together.
- Re-export via
src/<vertical>/index.ts. - Add
./<vertical>topackage.jsonexportsandtsup.config.tsentries. - Update docs with a new page.
The restaurant flavor is the canonical reference — see src/restaurant/.