@cosmoops/connect-sdk 0.37.0 → 0.38.0

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.
Files changed (34) hide show
  1. package/dist/cjs/client/components/availability-form.js +75 -0
  2. package/dist/cjs/client/components/event-type-form.js +82 -0
  3. package/dist/cjs/client/components/user-menu.js +3 -24
  4. package/dist/cjs/client/index.js +21 -3
  5. package/dist/cjs/client/portal-links.js +38 -0
  6. package/dist/cjs/client/use-scheduling.js +67 -8
  7. package/dist/cjs/server/client.js +1 -1
  8. package/dist/cjs/server/handler.js +78 -0
  9. package/dist/cjs/server/http.js +13 -1
  10. package/dist/cjs/server/resources/ai.js +6 -0
  11. package/dist/cjs/server/resources/scheduling.js +62 -0
  12. package/dist/cjs/server/resources/visitor.js +6 -0
  13. package/dist/esm/client/components/availability-form.js +72 -0
  14. package/dist/esm/client/components/event-type-form.js +79 -0
  15. package/dist/esm/client/components/user-menu.js +3 -24
  16. package/dist/esm/client/index.js +12 -1
  17. package/dist/esm/client/portal-links.js +34 -0
  18. package/dist/esm/client/use-scheduling.js +64 -8
  19. package/dist/esm/server/client.js +1 -1
  20. package/dist/esm/server/handler.js +78 -0
  21. package/dist/esm/server/http.js +12 -1
  22. package/dist/esm/server/resources/ai.js +7 -1
  23. package/dist/esm/server/resources/scheduling.js +62 -0
  24. package/dist/esm/server/resources/visitor.js +7 -1
  25. package/dist/types/client/components/availability-form.d.ts +22 -0
  26. package/dist/types/client/components/event-type-form.d.ts +22 -0
  27. package/dist/types/client/index.d.ts +6 -1
  28. package/dist/types/client/portal-links.d.ts +22 -0
  29. package/dist/types/client/use-scheduling.d.ts +49 -1
  30. package/dist/types/server/client.d.ts +1 -1
  31. package/dist/types/server/http.d.ts +35 -0
  32. package/dist/types/server/resources/scheduling.d.ts +67 -1
  33. package/dist/types/types.d.ts +27 -0
  34. package/package.json +1 -1
@@ -0,0 +1,75 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectAvailabilityFormModal = ConnectAvailabilityFormModal;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("react");
7
+ const react_2 = require("@heroui/react");
8
+ const use_scheduling_js_1 = require("../use-scheduling.js");
9
+ const WEEKDAY_LABELS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
10
+ const WEEKDAY_KEYS = [0, 1, 2, 3, 4, 5, 6];
11
+ /** Monday-Friday, 9am-5pm - the same starting point Connect's own Console form offers. */
12
+ function defaultWeeklyHours() {
13
+ const open = [{ startMinute: 9 * 60, endMinute: 17 * 60 }];
14
+ return { 0: [], 1: open, 2: open, 3: open, 4: open, 5: open, 6: [] };
15
+ }
16
+ function minutesToTimeInput(minutes) {
17
+ const hours = Math.floor(minutes / 60);
18
+ const mins = minutes % 60;
19
+ return `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
20
+ }
21
+ function timeInputToMinutes(value) {
22
+ const [hours, minutes] = value.split(":").map(Number);
23
+ return (hours ?? 0) * 60 + (minutes ?? 0);
24
+ }
25
+ /**
26
+ * Gives a workspace its first working hours, from inside a host application
27
+ * rather than a link-out to Connect.
28
+ *
29
+ * Deliberately narrower than Connect's own Calendar settings form: one
30
+ * interval per day (no split shifts), no date overrides, and the timezone
31
+ * defaults to the browser's own rather than asking. This is a first-run
32
+ * setup step, not a second copy of the Console editor - "Manage in Connect"
33
+ * is still where the rest of what a schedule can do lives.
34
+ *
35
+ * **Not optimistic.** `useCreateAvailabilitySchedule` settles the write
36
+ * against the server before this closes, the same posture every write in
37
+ * this package takes - see `ConnectBookingDialog`.
38
+ */
39
+ function ConnectAvailabilityFormModal({ isOpen, onOpenChange, onCreated, }) {
40
+ const [name, setName] = (0, react_1.useState)("Working hours");
41
+ const [timezone] = (0, react_1.useState)(() => Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC");
42
+ const [weeklyHours, setWeeklyHours] = (0, react_1.useState)(defaultWeeklyHours);
43
+ const { createSchedule, isPending, error, clearError } = (0, use_scheduling_js_1.useCreateAvailabilitySchedule)();
44
+ const toggleDay = (day, open) => {
45
+ setWeeklyHours((prev) => ({
46
+ ...prev,
47
+ [day]: open ? [{ startMinute: 9 * 60, endMinute: 17 * 60 }] : [],
48
+ }));
49
+ };
50
+ const setDayTime = (day, edge, value) => {
51
+ setWeeklyHours((prev) => {
52
+ const interval = prev[day][0];
53
+ if (!interval)
54
+ return prev;
55
+ return { ...prev, [day]: [{ ...interval, [edge]: timeInputToMinutes(value) }] };
56
+ });
57
+ };
58
+ const isFormValid = name.trim().length > 0 && !isPending;
59
+ const handleSubmit = async (e) => {
60
+ e.preventDefault();
61
+ if (!isFormValid)
62
+ return;
63
+ clearError();
64
+ const schedule = await createSchedule({ name: name.trim(), timezone, weeklyHours });
65
+ if (schedule) {
66
+ onCreated?.(schedule);
67
+ onOpenChange(false);
68
+ }
69
+ };
70
+ return ((0, jsx_runtime_1.jsx)(react_2.Modal, { isOpen: isOpen, onOpenChange: onOpenChange, children: (0, jsx_runtime_1.jsx)(react_2.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Container, { children: (0, jsx_runtime_1.jsxs)(react_2.Modal.Dialog, { className: "sm:max-w-120", children: [(0, jsx_runtime_1.jsx)(react_2.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_2.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Heading, { children: "Set your availability" }) }), (0, jsx_runtime_1.jsx)(react_2.Modal.Body, { children: (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { isRequired: true, variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Name" }), (0, jsx_runtime_1.jsx)(react_2.Input, { value: name, onChange: (e) => setName(e.currentTarget.value), disabled: isPending, autoFocus: true })] }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: ["Timezone: ", timezone] }), (0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-2", children: WEEKDAY_KEYS.map((day) => {
71
+ const interval = weeklyHours[day][0];
72
+ const isOpenDay = interval !== undefined;
73
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)(react_2.Switch, { isSelected: isOpenDay, onChange: (open) => toggleDay(day, open), isDisabled: isPending, children: (0, jsx_runtime_1.jsxs)(react_2.Switch.Content, { children: [(0, jsx_runtime_1.jsx)(react_2.Switch.Control, { children: (0, jsx_runtime_1.jsx)(react_2.Switch.Thumb, {}) }), (0, jsx_runtime_1.jsx)("span", { className: "w-24 shrink-0 text-sm", children: WEEKDAY_LABELS[day] })] }) }), isOpenDay && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(react_2.Input, { type: "time", value: minutesToTimeInput(interval.startMinute), onChange: (e) => setDayTime(day, "startMinute", e.currentTarget.value), disabled: isPending }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs text-muted", children: "to" }), (0, jsx_runtime_1.jsx)(react_2.Input, { type: "time", value: minutesToTimeInput(interval.endMinute), onChange: (e) => setDayTime(day, "endMinute", e.currentTarget.value), disabled: isPending })] }))] }, day));
74
+ }) }), error && (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-danger", children: error }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-end gap-2", children: [(0, jsx_runtime_1.jsx)(react_2.Button, { type: "button", variant: "secondary", onPress: () => onOpenChange(false), isDisabled: isPending, children: "Cancel" }), (0, jsx_runtime_1.jsx)(react_2.Button, { type: "submit", variant: "primary", isDisabled: !isFormValid, children: isPending ? "Saving..." : "Save" })] })] }) })] }) }) }) }));
75
+ }
@@ -0,0 +1,82 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectEventTypeFormModal = ConnectEventTypeFormModal;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("react");
7
+ const react_2 = require("@heroui/react");
8
+ const provider_js_1 = require("../provider.js");
9
+ const use_scheduling_js_1 = require("../use-scheduling.js");
10
+ const LOCATION_LABELS = {
11
+ phone: "Phone call",
12
+ address: "In person",
13
+ link: "Video link",
14
+ };
15
+ const LOCATION_KINDS = ["phone", "address", "link"];
16
+ /**
17
+ * Creates the first thing a workspace can be booked for, from inside a host
18
+ * application rather than a link-out to Connect.
19
+ *
20
+ * **Needs a schedule to exist first** - the picker reads `useAvailabilitySchedules`
21
+ * and renders "Set up availability first" with nothing to submit if none
22
+ * exists yet. Pair this with `ConnectAvailabilityFormModal` in a setup flow
23
+ * rather than assuming one.
24
+ *
25
+ * **Host is a specific member, never a team.** There is no groups read in
26
+ * this bridge, so a round-robin team host stays a Console-only capability -
27
+ * defaults to whoever is signed in, with an optional picker from the
28
+ * workspace's own roster.
29
+ */
30
+ function ConnectEventTypeFormModal({ isOpen, onOpenChange, onCreated, }) {
31
+ const { session } = (0, provider_js_1.useConnect)();
32
+ const { data: membersData } = (0, provider_js_1.useConnectWorkspaceMembers)();
33
+ const { schedules } = (0, use_scheduling_js_1.useAvailabilitySchedules)();
34
+ const members = (0, react_1.useMemo)(() => membersData?.members ?? [], [membersData]);
35
+ const defaultHostId = (0, react_1.useMemo)(() => {
36
+ const self = members.find((member) => member.userId === session?.user?.userId);
37
+ return self?.memberId ?? members[0]?.memberId ?? "";
38
+ }, [members, session?.user?.userId]);
39
+ const [name, setName] = (0, react_1.useState)("");
40
+ const [description, setDescription] = (0, react_1.useState)("");
41
+ const [durationMinutes, setDurationMinutes] = (0, react_1.useState)(30);
42
+ const [bufferBefore, setBufferBefore] = (0, react_1.useState)(5);
43
+ const [bufferAfter, setBufferAfter] = (0, react_1.useState)(5);
44
+ const [scheduleId, setScheduleId] = (0, react_1.useState)("");
45
+ const [hostMemberId, setHostMemberId] = (0, react_1.useState)("");
46
+ const [locationKind, setLocationKind] = (0, react_1.useState)("phone");
47
+ const [locationValue, setLocationValue] = (0, react_1.useState)("");
48
+ const effectiveScheduleId = scheduleId || schedules[0]?.id || "";
49
+ const effectiveHostMemberId = hostMemberId || defaultHostId;
50
+ const { createEventType, isPending, error, clearError } = (0, use_scheduling_js_1.useCreateEventType)();
51
+ const isFormValid = name.trim().length > 0 &&
52
+ durationMinutes >= 5 &&
53
+ durationMinutes <= 480 &&
54
+ effectiveScheduleId.length > 0 &&
55
+ effectiveHostMemberId.length > 0 &&
56
+ !isPending;
57
+ const handleSubmit = async (e) => {
58
+ e.preventDefault();
59
+ if (!isFormValid)
60
+ return;
61
+ clearError();
62
+ const trimmedDescription = description.trim();
63
+ const created = await createEventType({
64
+ name: name.trim(),
65
+ description: trimmedDescription.length > 0 ? trimmedDescription : null,
66
+ durationMinutes,
67
+ bufferBeforeMinutes: bufferBefore,
68
+ bufferAfterMinutes: bufferAfter,
69
+ hostMemberId: effectiveHostMemberId,
70
+ scheduleId: effectiveScheduleId,
71
+ location: {
72
+ kind: locationKind,
73
+ value: locationValue.trim().length > 0 ? locationValue.trim() : null,
74
+ },
75
+ });
76
+ if (created) {
77
+ onCreated?.(created);
78
+ onOpenChange(false);
79
+ }
80
+ };
81
+ return ((0, jsx_runtime_1.jsx)(react_2.Modal, { isOpen: isOpen, onOpenChange: onOpenChange, children: (0, jsx_runtime_1.jsx)(react_2.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Container, { children: (0, jsx_runtime_1.jsxs)(react_2.Modal.Dialog, { className: "sm:max-w-140", children: [(0, jsx_runtime_1.jsx)(react_2.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_2.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Heading, { children: "New appointment type" }) }), (0, jsx_runtime_1.jsx)(react_2.Modal.Body, { children: schedules.length === 0 ? ((0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: "Set up availability first - an appointment type needs working hours to book against." })) : ((0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { isRequired: true, variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Name" }), (0, jsx_runtime_1.jsx)(react_2.Input, { placeholder: "Consultation", value: name, onChange: (e) => setName(e.currentTarget.value), disabled: isPending, autoFocus: true })] }), (0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Description" }), (0, jsx_runtime_1.jsx)(react_2.TextArea, { placeholder: "What this appointment is for", value: description, onChange: (e) => setDescription(e.currentTarget.value), disabled: isPending, rows: 2 })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid gap-4 sm:grid-cols-3", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { isRequired: true, variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Length (min)" }), (0, jsx_runtime_1.jsx)(react_2.Input, { type: "number", value: String(durationMinutes), onChange: (e) => setDurationMinutes(Number(e.currentTarget.value)), disabled: isPending })] }), (0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Gap before" }), (0, jsx_runtime_1.jsx)(react_2.Input, { type: "number", value: String(bufferBefore), onChange: (e) => setBufferBefore(Number(e.currentTarget.value)), disabled: isPending })] }), (0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Gap after" }), (0, jsx_runtime_1.jsx)(react_2.Input, { type: "number", value: String(bufferAfter), onChange: (e) => setBufferAfter(Number(e.currentTarget.value)), disabled: isPending })] })] }), schedules.length > 1 && ((0, jsx_runtime_1.jsxs)(react_2.Select, { variant: "secondary", value: effectiveScheduleId, onChange: (value) => setScheduleId(String(value)), isDisabled: isPending, children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Availability" }), (0, jsx_runtime_1.jsxs)(react_2.Select.Trigger, { children: [(0, jsx_runtime_1.jsx)(react_2.Select.Value, {}), (0, jsx_runtime_1.jsx)(react_2.Select.Indicator, {})] }), (0, jsx_runtime_1.jsx)(react_2.Select.Popover, { children: (0, jsx_runtime_1.jsx)(react_2.ListBox, { children: schedules.map((schedule) => ((0, jsx_runtime_1.jsxs)(react_2.ListBox.Item, { id: schedule.id, textValue: schedule.name, children: [schedule.name, (0, jsx_runtime_1.jsx)(react_2.ListBox.ItemIndicator, {})] }, schedule.id))) }) })] })), members.length > 1 && ((0, jsx_runtime_1.jsxs)(react_2.Select, { variant: "secondary", value: effectiveHostMemberId, onChange: (value) => setHostMemberId(String(value)), isDisabled: isPending, children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Who hosts it" }), (0, jsx_runtime_1.jsxs)(react_2.Select.Trigger, { children: [(0, jsx_runtime_1.jsx)(react_2.Select.Value, {}), (0, jsx_runtime_1.jsx)(react_2.Select.Indicator, {})] }), (0, jsx_runtime_1.jsx)(react_2.Select.Popover, { children: (0, jsx_runtime_1.jsx)(react_2.ListBox, { children: members.map((member) => ((0, jsx_runtime_1.jsxs)(react_2.ListBox.Item, { id: member.memberId, textValue: member.displayName ?? member.email, children: [member.displayName ?? member.email, (0, jsx_runtime_1.jsx)(react_2.ListBox.ItemIndicator, {})] }, member.memberId))) }) })] })), (0, jsx_runtime_1.jsxs)("div", { className: "grid gap-4 sm:grid-cols-2", children: [(0, jsx_runtime_1.jsxs)(react_2.Select, { variant: "secondary", value: locationKind, onChange: (value) => setLocationKind(value), isDisabled: isPending, children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Where" }), (0, jsx_runtime_1.jsxs)(react_2.Select.Trigger, { children: [(0, jsx_runtime_1.jsx)(react_2.Select.Value, {}), (0, jsx_runtime_1.jsx)(react_2.Select.Indicator, {})] }), (0, jsx_runtime_1.jsx)(react_2.Select.Popover, { children: (0, jsx_runtime_1.jsx)(react_2.ListBox, { children: LOCATION_KINDS.map((kind) => ((0, jsx_runtime_1.jsxs)(react_2.ListBox.Item, { id: kind, textValue: LOCATION_LABELS[kind], children: [LOCATION_LABELS[kind], (0, jsx_runtime_1.jsx)(react_2.ListBox.ItemIndicator, {})] }, kind))) }) })] }), (0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Details" }), (0, jsx_runtime_1.jsx)(react_2.TextArea, { placeholder: locationKind === "address" ? "Where to come" : "Left blank is fine", value: locationValue, onChange: (e) => setLocationValue(e.currentTarget.value), disabled: isPending, rows: 2 })] })] }), error && (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-danger", children: error }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-end gap-2", children: [(0, jsx_runtime_1.jsx)(react_2.Button, { type: "button", variant: "secondary", onPress: () => onOpenChange(false), isDisabled: isPending, children: "Cancel" }), (0, jsx_runtime_1.jsx)(react_2.Button, { type: "submit", variant: "primary", isDisabled: !isFormValid, children: isPending ? "Saving..." : "Create" })] })] })) })] }) }) }) }));
82
+ }
@@ -13,6 +13,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
13
13
  const react_1 = require("@heroui/react");
14
14
  const lucide_react_1 = require("lucide-react");
15
15
  const provider_js_1 = require("../provider.js");
16
+ const portal_links_js_1 = require("../portal-links.js");
16
17
  const mark_js_1 = require("../mark.js");
17
18
  const identity_js_1 = require("./identity.js");
18
19
  const account_sessions_js_1 = require("./account-sessions.js");
@@ -28,28 +29,6 @@ const THEME_OPTIONS = [
28
29
  function openExternal(url) {
29
30
  window.open(url, "_blank", "noopener,noreferrer");
30
31
  }
31
- /**
32
- * Connect's profile page, with the context it needs to offer a way back here.
33
- *
34
- * Connect answers `/profile` for any signed-in user, so the bare URL works on
35
- * its own - these two params only add the "Back to <your app>" banner. It
36
- * validates `return_to` against this `client_id`'s registered redirect
37
- * origins and silently drops the banner if they don't match, so a wrong or
38
- * stale `appUrl`/`clientId` pairing degrades to what it did before rather
39
- * than erroring.
40
- *
41
- * `window.location.href` rather than a configured home URL: the point is to
42
- * come back to the page they left, and only the browser knows which one that
43
- * is.
44
- */
45
- function profileUrl(appUrl, clientId) {
46
- const url = new URL("/profile", appUrl);
47
- url.searchParams.set("client_id", clientId);
48
- if (typeof window !== "undefined") {
49
- url.searchParams.set("return_to", window.location.href);
50
- }
51
- return url.toString();
52
- }
53
32
  /**
54
33
  * The member's photo, with their initials underneath it.
55
34
  *
@@ -150,7 +129,7 @@ function ProfileSurface({ onSignOut, theme, onThemeChange, }) {
150
129
  const label = user.displayName ?? user.email ?? "Account";
151
130
  const role = session?.role ?? null;
152
131
  const planChip = subscriptionChip(session?.subscription ?? null);
153
- return ((0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { size: "md", scroll: "inside", children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: "Account" }) }), (0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { className: "flex flex-col gap-5", children: [(0, jsx_runtime_1.jsxs)("div", { className: `flex items-start gap-3 rounded-xl border border-border ${surfaces_js_1.PANEL_FILL} p-4`, children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm font-semibold text-foreground", children: label }), user.email && (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.email }), (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.workspaceName }), (role || planChip || !user.emailVerified) && ((0, jsx_runtime_1.jsxs)("div", { className: "mt-1.5 flex flex-wrap items-center gap-1.5", children: [role && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "accent", children: humanizeRole(role) })), planChip, !user.emailVerified && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "warning", children: "Email unverified" }))] }))] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-3 gap-2", children: [(0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: surfaces_js_1.ICON, "aria-hidden": "true" }), label: "Profile", url: profileUrl(appUrl, clientId) }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.CreditCard, { className: surfaces_js_1.ICON, "aria-hidden": "true" }), label: "Billing", url: `${appUrl}/billing` }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(mark_js_1.ConnectMark, { className: surfaces_js_1.ICON }), label: "Connect", url: appUrl })] }), theme && onThemeChange && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-foreground", children: "Appearance" }), (0, jsx_runtime_1.jsx)(react_1.ToggleButtonGroup, { fullWidth: true, "aria-label": "Theme", selectionMode: "single", disallowEmptySelection: true, selectedKeys: [theme], onSelectionChange: (keys) => {
132
+ return ((0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { size: "md", scroll: "inside", children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: "Account" }) }), (0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { className: "flex flex-col gap-5", children: [(0, jsx_runtime_1.jsxs)("div", { className: `flex items-start gap-3 rounded-xl border border-border ${surfaces_js_1.PANEL_FILL} p-4`, children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm font-semibold text-foreground", children: label }), user.email && (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.email }), (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.workspaceName }), (role || planChip || !user.emailVerified) && ((0, jsx_runtime_1.jsxs)("div", { className: "mt-1.5 flex flex-wrap items-center gap-1.5", children: [role && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "accent", children: humanizeRole(role) })), planChip, !user.emailVerified && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "warning", children: "Email unverified" }))] }))] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-3 gap-2", children: [(0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: surfaces_js_1.ICON, "aria-hidden": "true" }), label: "Profile", url: (0, portal_links_js_1.connectPortalUrl)(appUrl, clientId, "/profile") }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.CreditCard, { className: surfaces_js_1.ICON, "aria-hidden": "true" }), label: "Billing", url: `${appUrl}/billing` }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(mark_js_1.ConnectMark, { className: surfaces_js_1.ICON }), label: "Connect", url: appUrl })] }), theme && onThemeChange && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-foreground", children: "Appearance" }), (0, jsx_runtime_1.jsx)(react_1.ToggleButtonGroup, { fullWidth: true, "aria-label": "Theme", selectionMode: "single", disallowEmptySelection: true, selectedKeys: [theme], onSelectionChange: (keys) => {
154
133
  const [next] = [...keys];
155
134
  if (typeof next === "string")
156
135
  onThemeChange(next);
@@ -260,7 +239,7 @@ function ConnectProfileButton({ className }) {
260
239
  const { session, appUrl, clientId } = (0, provider_js_1.useConnect)();
261
240
  if (!session?.user)
262
241
  return null;
263
- return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () => openExternal(profileUrl(appUrl, clientId)), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: "size-4 shrink-0" }), "Profile", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
242
+ return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () => openExternal((0, portal_links_js_1.connectPortalUrl)(appUrl, clientId, "/profile")), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: "size-4 shrink-0" }), "Profile", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
264
243
  }
265
244
  /** A plain "Open Connect" button, for a footer or a help menu. */
266
245
  function ConnectHomeButton({ className }) {
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.useConnectPush = exports.useActivePath = exports.ConnectSideNav = exports.ConnectSidebarNav = exports.RAIL_EXPANDED = exports.RAIL_COLLAPSED = exports.sidebarRowClass = exports.useConnectShell = exports.ConnectShellProvider = exports.ConnectSidebarRow = exports.ConnectSidebarFooter = exports.ConnectSidebarBody = exports.ConnectSidebarHeader = exports.ConnectSidebar = exports.AppShell = exports.ConnectAccountSessions = exports.ConnectSignOutButton = exports.ConnectHomeButton = exports.ConnectProfileButton = exports.ConnectInvoicesButton = exports.ConnectManageSubscriptionButton = exports.ConnectUserMenu = exports.ConnectProfileModal = exports.ConnectUserButton = exports.ConnectOneTap = exports.ConnectRegisterButton = exports.ConnectLoginButton = exports.ConnectLogo = exports.ConnectMark = exports.ConnectGrievanceNotice = exports.isConnectError = exports.ConnectError = exports.normalizePlanFeatures = exports.blockingBlockers = exports.CONNECT_APP_URL = exports.useConnectResource = exports.useConnectIntegrations = exports.useConnectSessions = exports.useConnectAddonUsage = exports.useConnectCompanyProfile = exports.useConnectApps = exports.useConnectPlans = exports.useConnectEntitlements = exports.useConnectPlatformAccess = exports.useConnectAudience = exports.useConnectWorkspaceType = exports.useConnectUser = exports.useConnectOptional = exports.useConnect = exports.ConnectProvider = void 0;
5
- exports.ConnectFilterTabs = exports.ConnectCursorPagination = exports.ConnectConfirmDialog = exports.ConnectLinkButton = exports.ConnectPlansPage = exports.ConnectUsageMeter = exports.ConnectPlatformGate = exports.ConnectWorkspaceTypeGate = exports.useConnectFeature = exports.ConnectFeatureGate = exports.useConnectAccessGate = exports.ConnectAccessGate = exports.ConnectReadinessGate = exports.ConnectReadinessBanner = exports.resolveSubscriptionState = exports.ConnectSeatUsage = exports.ConnectTrialBanner = exports.ConnectSubscriptionCard = exports.ConnectNoSubscriptionCard = exports.ConnectSubscriptionGate = exports.ConnectSubscribeDialog = exports.ConnectPlans = exports.ConnectIntegrationsModal = exports.AI_TONE_DOT = exports.AI_TONE_CHIP = exports.AI_EMPTY_VALUE = exports.aiStatusTone = exports.isNumericAiFormat = exports.formatAiValue = exports.AiSources = exports.AiSteps = exports.AiDetails = exports.AiTimeline = exports.AiMetrics = exports.AiChart = exports.AiTable = exports.ConnectAskAi = exports.ConnectAppsGrid = exports.visitorSlotOfferFrom = exports.readVisitorStream = exports.ConnectVisitorChat = exports.isLinkableConnectScreen = exports.matchConnectScreen = exports.filterNavForAudience = exports.gateExemptPaths = exports.isNavItemActive = exports.resolveActivePath = exports.normalizePath = exports.flattenNav = exports.normalizeNav = void 0;
6
- exports.zonedInstant = exports.dayColumns = exports.layoutBookings = exports.useBookingActions = exports.useBookings = exports.useAvailableSlots = exports.useBookableEventTypes = exports.ConnectBookingDetail = exports.ConnectBookingDialog = exports.ConnectBookingCalendar = exports.useConnectWorkspaceMembers = exports.useConnectWorkspaces = exports.ConnectWorkspaceSwitcher = exports.resolvePlanGroup = exports.groupPlansByTier = exports.isFreePlan = exports.ACTION_LABEL = exports.discountLabel = exports.annualSaving = exports.checkoutSubscriptionId = exports.resolveAction = exports.monthlyOf = exports.formatPrice = exports.buildCheckoutUrl = exports.useConnectCheckout = void 0;
4
+ exports.ConnectSideNav = exports.ConnectSidebarNav = exports.RAIL_EXPANDED = exports.RAIL_COLLAPSED = exports.sidebarRowClass = exports.useConnectShell = exports.ConnectShellProvider = exports.ConnectSidebarRow = exports.ConnectSidebarFooter = exports.ConnectSidebarBody = exports.ConnectSidebarHeader = exports.ConnectSidebar = exports.AppShell = exports.useConnectPortalUrl = exports.connectPortalUrl = exports.ConnectAccountSessions = exports.ConnectSignOutButton = exports.ConnectHomeButton = exports.ConnectProfileButton = exports.ConnectInvoicesButton = exports.ConnectManageSubscriptionButton = exports.ConnectUserMenu = exports.ConnectProfileModal = exports.ConnectUserButton = exports.ConnectOneTap = exports.ConnectRegisterButton = exports.ConnectLoginButton = exports.ConnectLogo = exports.ConnectMark = exports.ConnectGrievanceNotice = exports.isConnectError = exports.ConnectError = exports.normalizePlanFeatures = exports.blockingBlockers = exports.CONNECT_APP_URL = exports.useConnectResource = exports.useConnectIntegrations = exports.useConnectSessions = exports.useConnectAddonUsage = exports.useConnectCompanyProfile = exports.useConnectApps = exports.useConnectPlans = exports.useConnectEntitlements = exports.useConnectPlatformAccess = exports.useConnectAudience = exports.useConnectWorkspaceType = exports.useConnectUser = exports.useConnectOptional = exports.useConnect = exports.ConnectProvider = void 0;
5
+ exports.ConnectConfirmDialog = exports.ConnectLinkButton = exports.ConnectPlansPage = exports.ConnectUsageMeter = exports.ConnectPlatformGate = exports.ConnectWorkspaceTypeGate = exports.useConnectFeature = exports.ConnectFeatureGate = exports.useConnectAccessGate = exports.ConnectAccessGate = exports.ConnectReadinessGate = exports.ConnectReadinessBanner = exports.resolveSubscriptionState = exports.ConnectSeatUsage = exports.ConnectTrialBanner = exports.ConnectSubscriptionCard = exports.ConnectNoSubscriptionCard = exports.ConnectSubscriptionGate = exports.ConnectSubscribeDialog = exports.ConnectPlans = exports.ConnectIntegrationsModal = exports.AI_TONE_DOT = exports.AI_TONE_CHIP = exports.AI_EMPTY_VALUE = exports.aiStatusTone = exports.isNumericAiFormat = exports.formatAiValue = exports.AiSources = exports.AiSteps = exports.AiDetails = exports.AiTimeline = exports.AiMetrics = exports.AiChart = exports.AiTable = exports.ConnectAskAi = exports.ConnectAppsGrid = exports.visitorSlotOfferFrom = exports.readVisitorStream = exports.ConnectVisitorChat = exports.isLinkableConnectScreen = exports.matchConnectScreen = exports.filterNavForAudience = exports.gateExemptPaths = exports.isNavItemActive = exports.resolveActivePath = exports.normalizePath = exports.flattenNav = exports.normalizeNav = exports.useConnectPush = exports.useActivePath = void 0;
6
+ exports.zonedInstant = exports.dayColumns = exports.layoutBookings = exports.useBookingActions = exports.useBookings = exports.useAvailableSlots = exports.useCreateEventType = exports.useCreateAvailabilitySchedule = exports.useAvailabilitySchedules = exports.useBookableEventTypes = exports.ConnectEventTypeFormModal = exports.ConnectAvailabilityFormModal = exports.ConnectBookingDetail = exports.ConnectBookingDialog = exports.ConnectBookingCalendar = exports.useConnectWorkspaceMembers = exports.useConnectWorkspaces = exports.ConnectWorkspaceSwitcher = exports.resolvePlanGroup = exports.groupPlansByTier = exports.isFreePlan = exports.ACTION_LABEL = exports.discountLabel = exports.annualSaving = exports.checkoutSubscriptionId = exports.resolveAction = exports.monthlyOf = exports.formatPrice = exports.buildCheckoutUrl = exports.useConnectCheckout = exports.ConnectFilterTabs = exports.ConnectCursorPagination = void 0;
7
7
  /**
8
8
  * Browser half of `@cosmoops/connect-sdk` - React components and hooks.
9
9
  *
@@ -69,6 +69,13 @@ Object.defineProperty(exports, "ConnectSignOutButton", { enumerable: true, get:
69
69
  // `ConnectUserMenu` was the only thing that could render it.
70
70
  var account_sessions_js_1 = require("./components/account-sessions.js");
71
71
  Object.defineProperty(exports, "ConnectAccountSessions", { enumerable: true, get: function () { return account_sessions_js_1.ConnectAccountSessions; } });
72
+ // A link to *any* Connect portal page that offers a "Back to <your app>" way
73
+ // home - what `ConnectProfileButton` builds for `/profile` internally, made
74
+ // reusable for a host linking out to a different Connect page (e.g.
75
+ // `/calendar/settings`) on its own.
76
+ var portal_links_js_1 = require("./portal-links.js");
77
+ Object.defineProperty(exports, "connectPortalUrl", { enumerable: true, get: function () { return portal_links_js_1.connectPortalUrl; } });
78
+ Object.defineProperty(exports, "useConnectPortalUrl", { enumerable: true, get: function () { return portal_links_js_1.useConnectPortalUrl; } });
72
79
  // The frame the rest of these components live inside. Exported next to the
73
80
  // pieces it composes rather than in a section of its own - an integration
74
81
  // reaching for a sidebar wants to find the launcher and the account menu in
@@ -262,8 +269,19 @@ Object.defineProperty(exports, "ConnectBookingCalendar", { enumerable: true, get
262
269
  var booking_dialogs_js_1 = require("./components/booking-dialogs.js");
263
270
  Object.defineProperty(exports, "ConnectBookingDialog", { enumerable: true, get: function () { return booking_dialogs_js_1.ConnectBookingDialog; } });
264
271
  Object.defineProperty(exports, "ConnectBookingDetail", { enumerable: true, get: function () { return booking_dialogs_js_1.ConnectBookingDetail; } });
272
+ // The write half of scheduling - a workspace's own first-time setup, not a
273
+ // partner integration's booking flow. `ConnectAvailabilityFormModal` and
274
+ // `ConnectEventTypeFormModal` give a host application a real inline setup
275
+ // step instead of a link-out to Connect's own Calendar settings.
276
+ var availability_form_js_1 = require("./components/availability-form.js");
277
+ Object.defineProperty(exports, "ConnectAvailabilityFormModal", { enumerable: true, get: function () { return availability_form_js_1.ConnectAvailabilityFormModal; } });
278
+ var event_type_form_js_1 = require("./components/event-type-form.js");
279
+ Object.defineProperty(exports, "ConnectEventTypeFormModal", { enumerable: true, get: function () { return event_type_form_js_1.ConnectEventTypeFormModal; } });
265
280
  var use_scheduling_js_1 = require("./use-scheduling.js");
266
281
  Object.defineProperty(exports, "useBookableEventTypes", { enumerable: true, get: function () { return use_scheduling_js_1.useBookableEventTypes; } });
282
+ Object.defineProperty(exports, "useAvailabilitySchedules", { enumerable: true, get: function () { return use_scheduling_js_1.useAvailabilitySchedules; } });
283
+ Object.defineProperty(exports, "useCreateAvailabilitySchedule", { enumerable: true, get: function () { return use_scheduling_js_1.useCreateAvailabilitySchedule; } });
284
+ Object.defineProperty(exports, "useCreateEventType", { enumerable: true, get: function () { return use_scheduling_js_1.useCreateEventType; } });
267
285
  Object.defineProperty(exports, "useAvailableSlots", { enumerable: true, get: function () { return use_scheduling_js_1.useAvailableSlots; } });
268
286
  Object.defineProperty(exports, "useBookings", { enumerable: true, get: function () { return use_scheduling_js_1.useBookings; } });
269
287
  Object.defineProperty(exports, "useBookingActions", { enumerable: true, get: function () { return use_scheduling_js_1.useBookingActions; } });
@@ -0,0 +1,38 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.connectPortalUrl = connectPortalUrl;
5
+ exports.useConnectPortalUrl = useConnectPortalUrl;
6
+ const provider_js_1 = require("./provider.js");
7
+ /**
8
+ * Any Connect portal page, with the context it needs to offer a way back
9
+ * here - `connectPortalUrl(appUrl, clientId, "/profile")`,
10
+ * `connectPortalUrl(appUrl, clientId, "/calendar/settings")`, and so on.
11
+ *
12
+ * Connect answers any of these paths for a signed-in user on the bare URL -
13
+ * these two params only add the "Back to <your app>" banner
14
+ * (`ApplicationReturnBar` on Connect's side). It validates `return_to`
15
+ * against this `client_id`'s registered redirect origins and silently drops
16
+ * the banner if they don't match, so a wrong or stale `appUrl`/`clientId`
17
+ * pairing degrades to what linking there did before rather than erroring.
18
+ *
19
+ * `window.location.href` rather than a configured home URL: the point is to
20
+ * come back to the page they left, and only the browser knows which one that
21
+ * is.
22
+ */
23
+ function connectPortalUrl(appUrl, clientId, path) {
24
+ const url = new URL(path, appUrl);
25
+ url.searchParams.set("client_id", clientId);
26
+ if (typeof window !== "undefined") {
27
+ url.searchParams.set("return_to", window.location.href);
28
+ }
29
+ return url.toString();
30
+ }
31
+ /**
32
+ * `connectPortalUrl`, with `appUrl`/`clientId` read from the provider instead
33
+ * of threaded through by every caller.
34
+ */
35
+ function useConnectPortalUrl(path) {
36
+ const { appUrl, clientId } = (0, provider_js_1.useConnect)();
37
+ return connectPortalUrl(appUrl, clientId, path);
38
+ }
@@ -2,6 +2,9 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.useBookableEventTypes = useBookableEventTypes;
5
+ exports.useAvailabilitySchedules = useAvailabilitySchedules;
6
+ exports.useCreateAvailabilitySchedule = useCreateAvailabilitySchedule;
7
+ exports.useCreateEventType = useCreateEventType;
5
8
  exports.useAvailableSlots = useAvailableSlots;
6
9
  exports.useBookings = useBookings;
7
10
  exports.useBookingActions = useBookingActions;
@@ -21,6 +24,48 @@ function useBookableEventTypes() {
21
24
  const { data, isLoading, error } = (0, provider_js_1.useConnectResource)("/scheduling/event-types");
22
25
  return { eventTypes: data?.eventTypes ?? [], isLoading, error };
23
26
  }
27
+ /**
28
+ * The working-hours templates an appointment type can point at.
29
+ *
30
+ * Read this before offering a "set up availability" step - a workspace that
31
+ * already has one does not need a second "Working hours" from your UI.
32
+ */
33
+ function useAvailabilitySchedules() {
34
+ const { data, isLoading, error } = (0, provider_js_1.useConnectResource)("/scheduling/schedules");
35
+ return { schedules: data?.schedules ?? [], isLoading, error };
36
+ }
37
+ /**
38
+ * Gives a workspace its first working hours.
39
+ *
40
+ * **Not optimistic** - same posture as `useBookingActions`: this is one of
41
+ * the two writes a host's first-run setup makes, and a modal that closes on
42
+ * a failed create is worse than one that stays open with the reason shown.
43
+ */
44
+ function useCreateAvailabilitySchedule() {
45
+ const { run, isPending, error, clearError } = useResourceAction();
46
+ return {
47
+ isPending,
48
+ error,
49
+ clearError,
50
+ createSchedule: (input) => run("/scheduling/schedules", input),
51
+ };
52
+ }
53
+ /**
54
+ * Creates the first thing a workspace can be booked for.
55
+ *
56
+ * Host is a specific member, never a team - there is no groups read in this
57
+ * bridge, so a round-robin team host stays a Console-only capability
58
+ * reachable by following "Manage in Connect".
59
+ */
60
+ function useCreateEventType() {
61
+ const { run, isPending, error, clearError } = useResourceAction();
62
+ return {
63
+ isPending,
64
+ error,
65
+ clearError,
66
+ createEventType: (input) => run("/scheduling/event-types", input),
67
+ };
68
+ }
24
69
  /**
25
70
  * Free times for one appointment type.
26
71
  *
@@ -74,15 +119,16 @@ function useBookings(query) {
74
119
  };
75
120
  }
76
121
  /**
77
- * Book, move and cancel.
122
+ * The isPending/error bookkeeping every write in this file shares, factored
123
+ * out once `useCreateAvailabilitySchedule` and `useCreateEventType` needed
124
+ * the identical shape `useBookingActions` already had.
78
125
  *
79
- * **None of these is optimistic**, and that is not an oversight. Whether a
80
- * slot is still free is a question only the server can answer, inside the
81
- * transaction that decides it - two people can be holding the same minute
82
- * right now. Painting a booking as made and withdrawing it is exactly the lie
83
- * the two-phase hold exists to prevent.
126
+ * **Never optimistic**, and that is not an oversight for any caller of this:
127
+ * whether a slot is free, or a schedule name collides, is a question only the
128
+ * server can answer. Painting the result before it comes back is exactly the
129
+ * lie a two-phase hold (or a modal that closes early) would tell.
84
130
  */
85
- function useBookingActions() {
131
+ function useResourceAction() {
86
132
  const { mutateResource } = (0, provider_js_1.useConnect)();
87
133
  const [isPending, setPending] = (0, react_1.useState)(false);
88
134
  const [error, setError] = (0, react_1.useState)(null);
@@ -102,10 +148,23 @@ function useBookingActions() {
102
148
  setPending(false);
103
149
  }
104
150
  }, [mutateResource]);
151
+ return { isPending, error, clearError: () => setError(null), run };
152
+ }
153
+ /**
154
+ * Book, move and cancel.
155
+ *
156
+ * **None of these is optimistic**, and that is not an oversight. Whether a
157
+ * slot is still free is a question only the server can answer, inside the
158
+ * transaction that decides it - two people can be holding the same minute
159
+ * right now. Painting a booking as made and withdrawing it is exactly the lie
160
+ * the two-phase hold exists to prevent.
161
+ */
162
+ function useBookingActions() {
163
+ const { run, isPending, error, clearError } = useResourceAction();
105
164
  return {
106
165
  isPending,
107
166
  error,
108
- clearError: () => setError(null),
167
+ clearError,
109
168
  createBooking: (input) => run("/scheduling/bookings", input),
110
169
  rescheduleBooking: (input) => run("/scheduling/bookings/reschedule", input).then((result) => result?.booking ?? null),
111
170
  cancelBooking: (input) => run("/scheduling/bookings/cancel", input),
@@ -179,4 +179,4 @@ class ConnectClient {
179
179
  }
180
180
  exports.ConnectClient = ConnectClient;
181
181
  /** Kept in step with `package.json` by `sdk.test.ts`. */
182
- exports.SDK_VERSION = "0.37.0";
182
+ exports.SDK_VERSION = "0.38.0";
@@ -853,6 +853,84 @@ function createConnectHandler(options) {
853
853
  const asUser = await requireToken(request);
854
854
  return json({ eventTypes: await client.scheduling.listEventTypes(workspaceId, { asUser }) });
855
855
  },
856
+ /*
857
+ * Creates the first thing a workspace can be booked for - a browser-side
858
+ * counterpart to `POST /scheduling/event-types` above, for a host's own
859
+ * inline setup UI (`ConnectEventTypeFormModal`). Host is a specific
860
+ * member, never a team - there is no groups read in this bridge, so a
861
+ * round-robin team host stays a Console-only capability.
862
+ */
863
+ "POST /scheduling/event-types": async (request) => {
864
+ const { workspaceId } = await requireWorkspace(request);
865
+ const asUser = await requireToken(request);
866
+ const body = (await request.json().catch(() => null));
867
+ if (typeof body?.name !== "string" ||
868
+ typeof body.durationMinutes !== "number" ||
869
+ typeof body.hostMemberId !== "string" ||
870
+ typeof body.scheduleId !== "string" ||
871
+ typeof body.location?.kind !== "string") {
872
+ throw new errors_js_1.ConnectError({
873
+ status: 400,
874
+ code: "validation_error",
875
+ message: "name, durationMinutes, hostMemberId, scheduleId and location are required",
876
+ endpoint: "/scheduling/event-types",
877
+ });
878
+ }
879
+ return json(await client.scheduling.createEventType({
880
+ workspaceId,
881
+ name: body.name,
882
+ description: typeof body.description === "string" ? body.description : null,
883
+ durationMinutes: body.durationMinutes,
884
+ bufferBeforeMinutes: typeof body.bufferBeforeMinutes === "number" ? body.bufferBeforeMinutes : undefined,
885
+ bufferAfterMinutes: typeof body.bufferAfterMinutes === "number" ? body.bufferAfterMinutes : undefined,
886
+ hostMemberId: body.hostMemberId,
887
+ scheduleId: body.scheduleId,
888
+ location: {
889
+ kind: body.location.kind,
890
+ value: typeof body.location.value === "string" ? body.location.value : null,
891
+ },
892
+ publicBookingEnabled: typeof body.publicBookingEnabled === "boolean" ? body.publicBookingEnabled : undefined,
893
+ asUser,
894
+ }), 201);
895
+ },
896
+ /*
897
+ * The working-hours templates an appointment type can point at - read
898
+ * before a setup UI offers to create one, so it can tell "none yet" from
899
+ * "already set up".
900
+ */
901
+ "GET /scheduling/schedules": async (request) => {
902
+ const { workspaceId } = await requireWorkspace(request);
903
+ const asUser = await requireToken(request);
904
+ return json({ schedules: await client.scheduling.listSchedules(workspaceId, { asUser }) });
905
+ },
906
+ /**
907
+ * Gives a workspace its first working hours - the browser-side
908
+ * counterpart to `POST /scheduling/schedules` above, for
909
+ * `ConnectAvailabilityFormModal`.
910
+ */
911
+ "POST /scheduling/schedules": async (request) => {
912
+ const { workspaceId } = await requireWorkspace(request);
913
+ const asUser = await requireToken(request);
914
+ const body = (await request.json().catch(() => null));
915
+ if (typeof body?.name !== "string" ||
916
+ typeof body.timezone !== "string" ||
917
+ typeof body.weeklyHours !== "object" ||
918
+ body.weeklyHours === null) {
919
+ throw new errors_js_1.ConnectError({
920
+ status: 400,
921
+ code: "validation_error",
922
+ message: "name, timezone and weeklyHours are required",
923
+ endpoint: "/scheduling/schedules",
924
+ });
925
+ }
926
+ return json(await client.scheduling.createSchedule({
927
+ workspaceId,
928
+ name: body.name,
929
+ timezone: body.timezone,
930
+ weeklyHours: body.weeklyHours,
931
+ asUser,
932
+ }), 201);
933
+ },
856
934
  "GET /scheduling/slots": async (request) => {
857
935
  const { workspaceId } = await requireWorkspace(request);
858
936
  const asUser = await requireToken(request);
@@ -1,8 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GENERATION_TIMEOUT_MS = void 0;
3
4
  exports.pathSegment = pathSegment;
4
5
  exports.request = request;
5
6
  const errors_js_1 = require("../errors.js");
7
+ /**
8
+ * How long a *generating* endpoint may take to send its headers.
9
+ *
10
+ * One number, named once, and it is not a guess: it is what
11
+ * `POST /visitor/chat` and `POST /ai/chat` declare as their `maxDuration` on
12
+ * the far side. A client bound shorter than the route's own ceiling cannot be
13
+ * right - it can only abort work Connect is still allowed to be doing - and
14
+ * the two numbers drifting apart is exactly how a 20s client came to be
15
+ * fronting a 60s route.
16
+ */
17
+ exports.GENERATION_TIMEOUT_MS = 60_000;
6
18
  const RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
7
19
  /**
8
20
  * One id, safe to interpolate into a request path.
@@ -149,7 +161,7 @@ async function request(options, spec) {
149
161
  // that has actually started to run as long as it needs. A caller that
150
162
  // wants to bound the stream itself passes `spec.signal`.
151
163
  const timeoutController = new AbortController();
152
- const timer = setTimeout(() => timeoutController.abort(), options.timeoutMs);
164
+ const timer = setTimeout(() => timeoutController.abort(), spec.timeoutMs ?? options.timeoutMs);
153
165
  const signal = spec.signal
154
166
  ? AbortSignal.any([spec.signal, timeoutController.signal])
155
167
  : timeoutController.signal;
@@ -43,6 +43,12 @@ class AiResource {
43
43
  ...(input.screenContext ? { screenContext: input.screenContext } : {}),
44
44
  },
45
45
  headers: (0, acting_as_js_1.actingAs)(input.asUser),
46
+ /*
47
+ * Matches the route's own `maxDuration`. This endpoint does not send
48
+ * headers until the model's first token, so the default 20s bound was
49
+ * aborting any turn that opened with a tool call - see `RequestSpec.timeoutMs`.
50
+ */
51
+ timeoutMs: http_js_1.GENERATION_TIMEOUT_MS,
46
52
  raw: true,
47
53
  signal: options.signal,
48
54
  });