@doany-ai/calendar 0.1.0-alpha.1 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,19 +7,36 @@ import { Input } from "@doany-ai/app-core/ui/input";
7
7
  import { Label } from "@doany-ai/app-core/ui/label";
8
8
  import { Skeleton } from "@doany-ai/app-core/ui/skeleton";
9
9
  import { CalendarCheck, CheckCircle2, Clock, Globe, MapPin, Tag, Users } from "lucide-react";
10
+ import { useQueryClient } from "@tanstack/react-query";
10
11
  import { useState } from "react";
11
12
  import { browserTimezone, defaultVariant, formatDateTime, formatTimezone, variantPriceLabel } from "../format.js";
12
- import { useBook, useBookableService, useServiceDetails } from "../hooks.js";
13
+ import { calendarKeys, useBook, useBookableService, useServiceDetails } from "../hooks.js";
13
14
  import { calendarLabels } from "../labels.js";
14
15
  import { AttendeeForm } from "./attendee-form.js";
15
16
  import { DateTimePicker } from "./date-time-picker.js";
17
+ import { SessionList } from "./session-list.js";
16
18
  function messageOf(cause, labels, given) {
17
19
  const { code, message } = (cause ?? {});
18
20
  if (code === "SLOT_UNAVAILABLE")
19
- return { text: labels.slotTaken, slotTaken: true };
21
+ return { text: labels.slotTaken, slotTaken: true, stale: false };
22
+ if (code === "VERSION_CONFLICT") {
23
+ // The slot's quote lasts five minutes; after that, or once availability
24
+ // changed, the platform says so and the visitor picks a time again. The
25
+ // same code also answers a service or price that changed under the
26
+ // visitor ("Service details changed; reload before booking"): then the
27
+ // service is read again and the visitor hears the platform's words.
28
+ const quote = /slot|availability/i.test(message ?? "");
29
+ return { text: message || labels.slotTaken, slotTaken: quote, stale: !quote };
30
+ }
20
31
  if (code === "RATE_LIMITED")
21
- return { text: labels.rateLimited, slotTaken: false };
22
- return { text: given?.bookingFailed ?? (message || labels.bookingFailed), slotTaken: false };
32
+ return { text: labels.rateLimited, slotTaken: false, stale: false };
33
+ // A refusal the platform explains (it carries a code) is shown in its
34
+ // words: a site's generic line hid "Availability changed; choose a new
35
+ // slot" behind "Something went wrong" (2026-10-10). A failure with no code
36
+ // (the network, a timeout) keeps the site's line.
37
+ if (code && message)
38
+ return { text: message, slotTaken: false, stale: false };
39
+ return { text: given?.bookingFailed ?? labels.bookingFailed, slotTaken: false, stale: false };
23
40
  }
24
41
  function VariantChoice({ service, value, onChange, locale, labels }) {
25
42
  if (service.variants.length < 2)
@@ -66,6 +83,7 @@ export function BookingFlow({ serviceSettingId, catalogItemVariantId, timezone:
66
83
  const [step, setStep] = useState(order === "who-first" ? "who" : "when");
67
84
  const [result, setResult] = useState(null);
68
85
  const book = useBook();
86
+ const queryClient = useQueryClient();
69
87
  const stacked = layout === "stacked";
70
88
  if (isLoading) {
71
89
  return (_jsxs("div", { className: cn("space-y-4", className), "aria-busy": "true", children: [_jsx(Skeleton, { className: "h-6 w-1/3" }), _jsx(Skeleton, { className: "h-72 w-full rounded-2xl" })] }));
@@ -73,6 +91,15 @@ export function BookingFlow({ serviceSettingId, catalogItemVariantId, timezone:
73
91
  if (isError || !service) {
74
92
  return (_jsx("p", { role: "alert", className: cn("py-10 text-center text-sm text-destructive", className), children: labels.notAvailable }));
75
93
  }
94
+ if (service.booking_mode === "class") {
95
+ // A class has no free times to pick from: it has sessions, each with its
96
+ // seats. The flow is the class's upcoming sessions with Register, so a
97
+ // site's one booking page serves both modes without knowing which is which.
98
+ return (_jsxs("div", { className: cn("space-y-8", className), children: [_jsx("h2", { className: "font-heading text-2xl font-semibold tracking-tight", children: service.item_name }), _jsx(SessionList, { variant: "detailed", params: { service_setting_id: serviceSettingId }, timezone: given, catalogItemVariantId: catalogItemVariantId, funding: funding, renderPayment: renderPayment, onRegistered: (r) => {
99
+ setResult(r);
100
+ onBooked?.(r);
101
+ }, locale: locale, labels: givenLabels }), result ? children?.(result) : null] }));
102
+ }
76
103
  const variant = service.variants.find((v) => v.id === variantId) ?? defaultVariant(service);
77
104
  if (!variant) {
78
105
  return (_jsx("p", { role: "alert", className: cn("py-10 text-center text-sm text-destructive", className), children: labels.notAvailable }));
@@ -97,7 +124,16 @@ export function BookingFlow({ serviceSettingId, catalogItemVariantId, timezone:
97
124
  onBooked?.(booked);
98
125
  },
99
126
  onError: (cause) => {
100
- if (messageOf(cause, labels, givenLabels).slotTaken) {
127
+ const why = messageOf(cause, labels, givenLabels);
128
+ if (why.stale) {
129
+ // The service or its prices changed under the visitor: read it again.
130
+ void queryClient.invalidateQueries({ queryKey: calendarKeys.service(serviceSettingId) });
131
+ }
132
+ if (why.slotTaken) {
133
+ // The cached slots carry the quotes that just failed: fetch new ones.
134
+ void queryClient.invalidateQueries({
135
+ predicate: (query) => Array.isArray(query.queryKey) && query.queryKey.includes("slots") && query.queryKey.includes(serviceSettingId),
136
+ });
101
137
  setSlot(null);
102
138
  setStep("when");
103
139
  }
@@ -5,7 +5,7 @@ import { Calendar } from "@doany-ai/app-core/ui/calendar";
5
5
  import { Skeleton } from "@doany-ai/app-core/ui/skeleton";
6
6
  import { Clock, Globe } from "lucide-react";
7
7
  import { useMemo, useState } from "react";
8
- import { formatDay, formatTime, formatTimezone, localDate } from "../format.js";
8
+ import { calendarDate, formatCalendarDay, formatTime, formatTimezone } from "../format.js";
9
9
  import { useSlots } from "../hooks.js";
10
10
  import { calendarLabels } from "../labels.js";
11
11
  /** The timezones offered besides the visitor's own, when the runtime cannot list them all. */
@@ -64,7 +64,7 @@ export function DateTimePicker({ service, variant, timezone, onTimezoneChange, v
64
64
  const today = useMemo(() => new Date(), []);
65
65
  const [month, setMonth] = useState(() => new Date(today.getFullYear(), today.getMonth(), 1));
66
66
  const [day, setDay] = useState(undefined);
67
- const date = day ? localDate(day, timezone) : null;
67
+ const date = day ? calendarDate(day) : null;
68
68
  const { slots, isLoading, isError, refetch } = useSlots(service.id, { catalogItemVariantId: variant.id, date, timezone });
69
69
  const horizon = service.booking_horizon_days
70
70
  ? new Date(today.getFullYear(), today.getMonth(), today.getDate() + service.booking_horizon_days)
@@ -72,7 +72,7 @@ export function DateTimePicker({ service, variant, timezone, onTimezoneChange, v
72
72
  const zones = useMemo(() => timezoneChoices(timezone), [timezone]);
73
73
  const wide = layout === "wide";
74
74
  const times = cn("grid gap-2 grid-cols-[repeat(auto-fill,minmax(5.5rem,1fr))]", wide && "[@container(min-width:40rem)]:grid-cols-1");
75
- return (_jsx("div", { className: cn("[container-type:inline-size]", className), children: _jsxs("div", { className: cn("grid gap-6", wide && "[@container(min-width:40rem)]:grid-cols-[auto_minmax(14rem,1fr)]"), children: [_jsxs("div", { className: "space-y-4", children: [_jsx("h3", { className: "font-heading text-base font-semibold", children: labels.selectDateTime }), _jsx(Calendar, { mode: "single", month: month, onMonthChange: setMonth, selected: day, onSelect: (picked) => setDay(picked ?? undefined), fromDate: today, toDate: horizon, disabled: { before: today }, className: "w-fit p-0", classNames: calendarClassNames }), _jsxs("label", { className: "block space-y-1.5 text-sm", children: [_jsx("span", { className: "font-medium text-foreground", children: labels.timezone }), _jsxs("span", { className: "relative block", children: [_jsx(Globe, { className: "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground", "aria-hidden": "true" }), _jsx("select", { value: timezone, onChange: (e) => onTimezoneChange?.(e.target.value), className: "h-10 w-full rounded-lg border border-input bg-background pl-9 pr-3 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring", children: zones.map((zone) => (_jsx("option", { value: zone, children: formatTimezone(zone, locale) }, zone))) })] })] })] }), _jsxs("div", { className: cn("min-w-0 space-y-3", wide && "[@container(min-width:40rem)]:min-h-80"), children: [_jsxs("p", { className: "flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-muted-foreground", children: [_jsxs("span", { className: "inline-flex items-center gap-1.5 whitespace-nowrap", children: [_jsx(Clock, { className: "h-4 w-4", "aria-hidden": "true" }), variant.duration_minutes ? labels.duration(variant.duration_minutes) : variant.name] }), day ? _jsx("span", { className: "whitespace-nowrap font-medium text-foreground", children: formatDay(day.toISOString(), timezone, locale) }) : null] }), !day ? null : isLoading ? (_jsx("div", { className: times, "aria-busy": "true", children: [0, 1, 2, 3, 4, 5].map((i) => (_jsx(Skeleton, { className: "h-11 rounded-lg" }, i))) })) : isError ? (_jsxs("div", { role: "alert", className: "space-y-2 text-sm text-destructive", children: [_jsx("p", { children: given?.loadError ?? labels.loadError }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => refetch(), children: labels.tryAgain })] })) : !slots.length ? (_jsx("p", { className: "rounded-xl border border-dashed border-border py-10 text-center text-sm text-muted-foreground", children: labels.noSlots })) : (_jsx("div", { role: "listbox", "aria-label": labels.selectDateTime, className: cn(times, wide && "[@container(min-width:40rem)]:max-h-[26rem] [@container(min-width:40rem)]:overflow-y-auto [@container(min-width:40rem)]:pr-1"), children: slots.map((slot) => {
75
+ return (_jsx("div", { className: cn("[container-type:inline-size]", className), children: _jsxs("div", { className: cn("grid gap-6", wide && "[@container(min-width:40rem)]:grid-cols-[auto_minmax(14rem,1fr)]"), children: [_jsxs("div", { className: "space-y-4", children: [_jsx("h3", { className: "font-heading text-base font-semibold", children: labels.selectDateTime }), _jsx(Calendar, { mode: "single", month: month, onMonthChange: setMonth, selected: day, onSelect: (picked) => setDay(picked ?? undefined), fromDate: today, toDate: horizon, disabled: { before: today }, className: "w-fit p-0", classNames: calendarClassNames }), _jsxs("label", { className: "block space-y-1.5 text-sm", children: [_jsx("span", { className: "font-medium text-foreground", children: labels.timezone }), _jsxs("span", { className: "relative block", children: [_jsx(Globe, { className: "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground", "aria-hidden": "true" }), _jsx("select", { value: timezone, onChange: (e) => onTimezoneChange?.(e.target.value), className: "h-10 w-full rounded-lg border border-input bg-background pl-9 pr-3 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring", children: zones.map((zone) => (_jsx("option", { value: zone, children: formatTimezone(zone, locale) }, zone))) })] })] })] }), _jsxs("div", { className: cn("min-w-0 space-y-3", wide && "[@container(min-width:40rem)]:min-h-80"), children: [_jsxs("p", { className: "flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-muted-foreground", children: [_jsxs("span", { className: "inline-flex items-center gap-1.5 whitespace-nowrap", children: [_jsx(Clock, { className: "h-4 w-4", "aria-hidden": "true" }), variant.duration_minutes ? labels.duration(variant.duration_minutes) : variant.name] }), day ? _jsx("span", { className: "whitespace-nowrap font-medium text-foreground", children: formatCalendarDay(day, locale) }) : null] }), !day ? null : isLoading ? (_jsx("div", { className: times, "aria-busy": "true", children: [0, 1, 2, 3, 4, 5].map((i) => (_jsx(Skeleton, { className: "h-11 rounded-lg" }, i))) })) : isError ? (_jsxs("div", { role: "alert", className: "space-y-2 text-sm text-destructive", children: [_jsx("p", { children: given?.loadError ?? labels.loadError }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => refetch(), children: labels.tryAgain })] })) : !slots.length ? (_jsx("p", { className: "rounded-xl border border-dashed border-border py-10 text-center text-sm text-muted-foreground", children: labels.noSlots })) : (_jsx("div", { role: "listbox", "aria-label": labels.selectDateTime, className: cn(times, wide && "[@container(min-width:40rem)]:max-h-[26rem] [@container(min-width:40rem)]:overflow-y-auto [@container(min-width:40rem)]:pr-1"), children: slots.map((slot) => {
76
76
  const chosen = value?.slot_token === slot.slot_token;
77
77
  if (chosen && onConfirm) {
78
78
  return (_jsxs("div", { className: cn("col-span-2 grid grid-cols-2 gap-2", wide && "[@container(min-width:40rem)]:col-span-1"), children: [_jsx("button", { type: "button", role: "option", "aria-selected": true, className: "h-11 rounded-lg bg-muted text-sm font-medium text-muted-foreground", onClick: () => onChange(slot), children: formatTime(slot.start_at, timezone, locale) }), _jsx(Button, { type: "button", className: "h-11 rounded-lg font-semibold", onClick: () => onConfirm(slot), children: confirmLabel ?? labels.selectTime })] }, slot.slot_token));
@@ -24,6 +24,8 @@ export interface SessionListProps {
24
24
  renderPayment?: (result: BookingResult) => ReactNode;
25
25
  /** Pay with a package the customer holds (its entitlement) instead of the ticket's default. */
26
26
  funding?: BookingFunding;
27
+ /** Preselect this ticket in the register dialog; the session's own otherwise. */
28
+ catalogItemVariantId?: string;
27
29
  onRegistered?: (result: BookingResult) => void;
28
30
  /** Shown when there are no sessions. Default: the `noSessions` label. */
29
31
  empty?: ReactNode;
@@ -40,11 +42,13 @@ export interface RegisterDialogProps {
40
42
  onRegistered?: (result: BookingResult) => void;
41
43
  /** Pay with a package the customer holds (its entitlement) instead of the ticket's default. */
42
44
  funding?: BookingFunding;
45
+ /** Preselect this ticket (its variant id); the session's own otherwise. */
46
+ catalogItemVariantId?: string;
43
47
  locale?: string;
44
48
  labels?: Partial<CalendarLabels>;
45
49
  onClose: () => void;
46
50
  }
47
51
  /** Take a seat in one session: the ticket, who it is for, then the confirmation or the payment. */
48
- export declare function RegisterDialog({ session, timezone, renderPayment, onRegistered, funding, locale, labels: given, onClose }: RegisterDialogProps): import("react").JSX.Element;
52
+ export declare function RegisterDialog({ session, timezone, renderPayment, onRegistered, funding, catalogItemVariantId, locale, labels: given, onClose }: RegisterDialogProps): import("react").JSX.Element;
49
53
  /** Upcoming class sessions by day, each with its seats and a Register button. */
50
- export declare function SessionList({ params, timezone: given, variant, paging, maxWeeks, renderPayment, onRegistered, funding, empty, locale, labels: givenLabels, className, }: SessionListProps): import("react").JSX.Element;
54
+ export declare function SessionList({ params, timezone: given, variant, paging, maxWeeks, renderPayment, onRegistered, funding, catalogItemVariantId, empty, locale, labels: givenLabels, className, }: SessionListProps): import("react").JSX.Element;
@@ -16,7 +16,9 @@ function messageOf(cause, labels, given) {
16
16
  const { code, message } = (cause ?? {});
17
17
  if (code === "RATE_LIMITED")
18
18
  return labels.rateLimited;
19
- return given?.bookingFailed ?? (message || labels.bookingFailed);
19
+ if (code && message)
20
+ return message;
21
+ return given?.bookingFailed ?? labels.bookingFailed;
20
22
  }
21
23
  /** The tickets a visitor can take; with a package, the ones open to a package holder too. */
22
24
  function openVariants(session, funding) {
@@ -27,11 +29,14 @@ export function sessionIsFull(session, funding) {
27
29
  return !openVariants(session, funding).length || (session.remaining_seats ?? 0) <= 0 || !session.allowed_actions.includes("create_registration");
28
30
  }
29
31
  /** Take a seat in one session: the ticket, who it is for, then the confirmation or the payment. */
30
- export function RegisterDialog({ session, timezone, renderPayment, onRegistered, funding, locale, labels: given, onClose }) {
32
+ export function RegisterDialog({ session, timezone, renderPayment, onRegistered, funding, catalogItemVariantId, locale, labels: given, onClose }) {
31
33
  const labels = calendarLabels(given);
32
34
  const { user } = useAuth();
33
35
  const tickets = openVariants(session, funding);
34
- const [ticket, setTicket] = useState(tickets.find((t) => t.catalog_item_variant_id === session.catalog_item_variant_id) ?? tickets[0] ?? null);
36
+ const [ticket, setTicket] = useState(tickets.find((t) => t.catalog_item_variant_id === catalogItemVariantId) ??
37
+ tickets.find((t) => t.catalog_item_variant_id === session.catalog_item_variant_id) ??
38
+ tickets[0] ??
39
+ null);
35
40
  const [result, setResult] = useState(null);
36
41
  const register = useRegister();
37
42
  const submit = (attendee) => {
@@ -71,7 +76,7 @@ function Seats({ session, labels, funding }) {
71
76
  }
72
77
  const minutesBetween = (a, b) => Math.round((new Date(b).getTime() - new Date(a).getTime()) / 60000);
73
78
  /** Upcoming class sessions by day, each with its seats and a Register button. */
74
- export function SessionList({ params, timezone: given, variant = "compact", paging = "all", maxWeeks = 8, renderPayment, onRegistered, funding, empty, locale, labels: givenLabels, className, }) {
79
+ export function SessionList({ params, timezone: given, variant = "compact", paging = "all", maxWeeks = 8, renderPayment, onRegistered, funding, catalogItemVariantId, empty, locale, labels: givenLabels, className, }) {
75
80
  const labels = calendarLabels(givenLabels);
76
81
  const timezone = given ?? browserTimezone();
77
82
  const detailed = variant === "detailed";
@@ -135,5 +140,5 @@ export function SessionList({ params, timezone: given, variant = "compact", pagi
135
140
  return (_jsxs("li", { className: "flex flex-wrap items-center gap-x-4 gap-y-2 px-5 py-4", children: [_jsx("span", { className: "w-24 font-semibold tabular-nums", children: formatTime(session.start_at, timezone, locale) }), _jsxs("span", { className: "min-w-40 flex-1", children: [_jsx("span", { className: "block font-medium", children: session.item_name }), _jsx("span", { className: "block text-sm text-muted-foreground", children: session.variant_name })] }), _jsxs("span", { className: cn("inline-flex items-center gap-1.5 text-sm", full ? "text-muted-foreground" : "text-foreground"), children: [_jsx(Users, { className: "h-4 w-4", "aria-hidden": "true" }), full ? labels.full : labels.seatsLeft(seats)] }), _jsx(Button, { className: "rounded-lg", size: "sm", disabled: full, onClick: () => setOpen(session), children: labels.register })] }, session.id));
136
141
  }) })] }, day))) }));
137
142
  }
138
- return (_jsxs("div", { className: cn("space-y-6", className), children: [weekBar, body, hasNextPage && !isLoading && !isError && (_jsx("div", { className: "flex justify-center", children: _jsx(Button, { variant: "outline", className: "min-w-32 rounded-lg", onClick: () => fetchNextPage(), disabled: isFetchingNextPage, children: isFetchingNextPage ? labels.loadingMore : labels.loadMore }) })), open && _jsx(RegisterDialog, { session: open, timezone: timezone, renderPayment: renderPayment, onRegistered: onRegistered, funding: funding, locale: locale, labels: givenLabels, onClose: () => setOpen(null) })] }));
143
+ return (_jsxs("div", { className: cn("space-y-6", className), children: [weekBar, body, hasNextPage && !isLoading && !isError && (_jsx("div", { className: "flex justify-center", children: _jsx(Button, { variant: "outline", className: "min-w-32 rounded-lg", onClick: () => fetchNextPage(), disabled: isFetchingNextPage, children: isFetchingNextPage ? labels.loadingMore : labels.loadMore }) })), open && _jsx(RegisterDialog, { session: open, timezone: timezone, renderPayment: renderPayment, onRegistered: onRegistered, funding: funding, catalogItemVariantId: catalogItemVariantId, locale: locale, labels: givenLabels, onClose: () => setOpen(null) })] }));
139
144
  }
package/dist/format.d.ts CHANGED
@@ -3,6 +3,16 @@ import type { BookableService, BookableVariant } from "./types.js";
3
3
  export declare function formatDuration(minutes: number): string;
4
4
  /** The browser's IANA timezone, else UTC. */
5
5
  export declare function browserTimezone(): string;
6
+ /**
7
+ * `YYYY-MM-DD` of a calendar day the visitor picked (a `Date` at local
8
+ * midnight), read from its own fields. Not `localDate`: that converts an
9
+ * instant into a timezone, and a day picked at midnight in Shanghai is the
10
+ * evening before in Johannesburg, so the slots came out one day early for
11
+ * every visitor east of the business (measured 2026-10-10).
12
+ */
13
+ export declare function calendarDate(day: Date): string;
14
+ /** `Thu, Oct 15, 2026` of a picked calendar day, from its own fields. */
15
+ export declare function formatCalendarDay(day: Date, locale?: string): string;
6
16
  /** `YYYY-MM-DD` of an instant, in a timezone. */
7
17
  export declare function localDate(date: Date, timezone: string): string;
8
18
  /** `3:30 PM` (or the locale's form) of an instant, in a timezone. */
package/dist/format.js CHANGED
@@ -16,6 +16,21 @@ export function browserTimezone() {
16
16
  return "UTC";
17
17
  }
18
18
  }
19
+ /**
20
+ * `YYYY-MM-DD` of a calendar day the visitor picked (a `Date` at local
21
+ * midnight), read from its own fields. Not `localDate`: that converts an
22
+ * instant into a timezone, and a day picked at midnight in Shanghai is the
23
+ * evening before in Johannesburg, so the slots came out one day early for
24
+ * every visitor east of the business (measured 2026-10-10).
25
+ */
26
+ export function calendarDate(day) {
27
+ const pad = (n) => String(n).padStart(2, "0");
28
+ return `${day.getFullYear()}-${pad(day.getMonth() + 1)}-${pad(day.getDate())}`;
29
+ }
30
+ /** `Thu, Oct 15, 2026` of a picked calendar day, from its own fields. */
31
+ export function formatCalendarDay(day, locale) {
32
+ return new Intl.DateTimeFormat(locale, { weekday: "short", month: "short", day: "numeric", year: "numeric" }).format(day);
33
+ }
19
34
  /** `YYYY-MM-DD` of an instant, in a timezone. */
20
35
  export function localDate(date, timezone) {
21
36
  const parts = new Intl.DateTimeFormat("en-CA", { timeZone: timezone, year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(date);
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { calendarKeys, usePackages, useServiceDetails, useBook, useBookableService, useBookableServices, useCancelRegistration, useRegister, useRegistration, useCalendarOperation, operationSettled, useSession, useSessions, useSlots, type BookableServicesParams, type SessionsParams, type UseSlotsParams, } from "./hooks.js";
2
2
  export { readBookingLinkToken, useBookingAccessToken } from "./booking-link.js";
3
- export { browserTimezone, defaultVariant, formatDateTime, formatDay, formatDayParts, formatDuration, formatTime, formatTimezone, localDate, servicePriceLabel, variantPriceLabel, } from "./format.js";
3
+ export { browserTimezone, defaultVariant, formatDateTime, formatDay, formatDayParts, formatDuration, formatTime, formatTimezone, localDate, servicePriceLabel, variantPriceLabel, calendarDate, formatCalendarDay, } from "./format.js";
4
4
  export { calendarLabels, defaultCalendarLabels, type CalendarLabels } from "./labels.js";
5
5
  export { ServiceCard, ServiceList, type ServiceCardProps, type ServiceListProps } from "./components/service-list.js";
6
6
  export { DateTimePicker, calendarClassNames, type DateTimePickerProps } from "./components/date-time-picker.js";
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // Tailwind build scans it (see @doany-ai/app-core/tailwind-preset).
8
8
  export { calendarKeys, usePackages, useServiceDetails, useBook, useBookableService, useBookableServices, useCancelRegistration, useRegister, useRegistration, useCalendarOperation, operationSettled, useSession, useSessions, useSlots, } from "./hooks.js";
9
9
  export { readBookingLinkToken, useBookingAccessToken } from "./booking-link.js";
10
- export { browserTimezone, defaultVariant, formatDateTime, formatDay, formatDayParts, formatDuration, formatTime, formatTimezone, localDate, servicePriceLabel, variantPriceLabel, } from "./format.js";
10
+ export { browserTimezone, defaultVariant, formatDateTime, formatDay, formatDayParts, formatDuration, formatTime, formatTimezone, localDate, servicePriceLabel, variantPriceLabel, calendarDate, formatCalendarDay, } from "./format.js";
11
11
  export { calendarLabels, defaultCalendarLabels } from "./labels.js";
12
12
  export { ServiceCard, ServiceList } from "./components/service-list.js";
13
13
  export { DateTimePicker, calendarClassNames } from "./components/date-time-picker.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doany-ai/calendar",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.3",
4
4
  "description": "Booking the business's services, on a Doany site: calendar hooks and default components (service list, date and time picker, booking flow, class sessions, booking status)",
5
5
  "type": "module",
6
6
  "sideEffects": false,