@cosmoops/connect-sdk 0.36.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 (46) hide show
  1. package/CHANGELOG.md +35 -2
  2. package/dist/cjs/client/components/app-shell.js +1 -1
  3. package/dist/cjs/client/components/availability-form.js +75 -0
  4. package/dist/cjs/client/components/event-type-form.js +82 -0
  5. package/dist/cjs/client/components/plan-pricing.js +9 -4
  6. package/dist/cjs/client/components/plans.js +9 -1
  7. package/dist/cjs/client/components/user-menu.js +3 -24
  8. package/dist/cjs/client/components/workspace-switcher.js +23 -13
  9. package/dist/cjs/client/index.js +21 -3
  10. package/dist/cjs/client/portal-links.js +38 -0
  11. package/dist/cjs/client/use-scheduling.js +67 -8
  12. package/dist/cjs/server/client.js +1 -1
  13. package/dist/cjs/server/handler.js +78 -0
  14. package/dist/cjs/server/http.js +13 -1
  15. package/dist/cjs/server/resources/ai.js +6 -0
  16. package/dist/cjs/server/resources/scheduling.js +62 -0
  17. package/dist/cjs/server/resources/visitor.js +6 -0
  18. package/dist/esm/client/components/app-shell.js +1 -1
  19. package/dist/esm/client/components/availability-form.js +72 -0
  20. package/dist/esm/client/components/event-type-form.js +79 -0
  21. package/dist/esm/client/components/plan-pricing.js +9 -4
  22. package/dist/esm/client/components/plans.js +9 -1
  23. package/dist/esm/client/components/user-menu.js +3 -24
  24. package/dist/esm/client/components/workspace-switcher.js +26 -16
  25. package/dist/esm/client/index.js +12 -1
  26. package/dist/esm/client/portal-links.js +34 -0
  27. package/dist/esm/client/use-scheduling.js +64 -8
  28. package/dist/esm/server/client.js +1 -1
  29. package/dist/esm/server/handler.js +78 -0
  30. package/dist/esm/server/http.js +12 -1
  31. package/dist/esm/server/resources/ai.js +7 -1
  32. package/dist/esm/server/resources/scheduling.js +62 -0
  33. package/dist/esm/server/resources/visitor.js +7 -1
  34. package/dist/types/client/components/app-shell.d.ts +7 -1
  35. package/dist/types/client/components/availability-form.d.ts +22 -0
  36. package/dist/types/client/components/event-type-form.d.ts +22 -0
  37. package/dist/types/client/components/plan-pricing.d.ts +10 -0
  38. package/dist/types/client/components/workspace-switcher.d.ts +25 -6
  39. package/dist/types/client/index.d.ts +6 -1
  40. package/dist/types/client/portal-links.d.ts +22 -0
  41. package/dist/types/client/use-scheduling.d.ts +49 -1
  42. package/dist/types/server/client.d.ts +1 -1
  43. package/dist/types/server/http.d.ts +35 -0
  44. package/dist/types/server/resources/scheduling.d.ts +67 -1
  45. package/dist/types/types.d.ts +27 -0
  46. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,40 @@
3
3
  `@cosmoops/connect-sdk` follows semver, with the 0.x convention that **minor
4
4
  versions may carry breaking changes** and patch versions never do.
5
5
 
6
+ ## 0.37.0
7
+
8
+ ### The workspace switcher can stay visible at one workspace, and offer a second
9
+
10
+ `ConnectWorkspaceSwitcher` has always hidden itself below two workspaces -
11
+ right, since a switcher with one option teaches nothing. But a partner with no
12
+ way to *create* a workspace from its own chrome had no way to help somebody
13
+ discover that a second one is possible, either. `alwaysShow` is the opt-in:
14
+
15
+ ```tsx
16
+ <AppShell workspaceSwitcher={{ alwaysShow: true }} /* ... */ />
17
+ ```
18
+
19
+ Off by default, so every existing host keeps today's behaviour. On, the
20
+ dropdown stays mounted below two workspaces and gains a "Create workspace"
21
+ row that opens Connect - there is still no `workspaces.create()` on the SDK,
22
+ deliberately: creating one is tied to KYC/KYB and a billing profile, which is
23
+ Connect's job and not something an API key should be able to trigger.
24
+
25
+ ### A one-time plan no longer renews under the annual toggle
26
+
27
+ `groupPlansByTier` filed a `billingInterval: "one-time"` plan as that tier's
28
+ *annual* plan, because `PlanTier` only ever had two slots. For a tier that
29
+ also sold a recurring plan under the same `planGroup`, that meant `annualSaving`
30
+ compared a monthly charge against a one-time lump sum and produced a bogus
31
+ "Save X%" badge, and switching `ConnectPlans`'s toggle to annual showed the
32
+ one-time purchase as if it were this year's discounted rate on a plan that
33
+ renews.
34
+
35
+ `PlanTier.oneTime` is the fix - its own slot, which `annualSaving` and the
36
+ annual/monthly toggle never read from. A tier that is *only* a one-time plan
37
+ ignores the toggle entirely now, rather than happening to work by the other
38
+ slot being empty.
39
+
6
40
  ## 0.36.0
7
41
 
8
42
  ### A workspace's billing details can be read, so a tax id is typed once
@@ -71,12 +105,11 @@ release would have carried a `User-Agent` a version behind. `src/sdk.test.ts`
71
105
  is what caught it, and is the reason the constant is pinned to
72
106
  `package.json` rather than trusted.
73
107
 
108
+ ### A diary and a roster could not be joined
74
109
 
75
110
  One field, `ConnectWorkspaceMember.memberId`, without which no integration
76
111
  could say who an appointment belongs to.
77
112
 
78
- ### A diary and a roster could not be joined
79
-
80
113
  `Booking.hostMemberIds` holds `WorkspaceMember.id` values, and so does
81
114
  `listBookings`' `hostMemberId` filter. `workspaces.members()` returned
82
115
  `userId` and no member id at all - so the two sets of ids had nothing in
@@ -197,6 +197,6 @@ function AppShell({ name, icon, homeHref = "/", nav, secondaryNav, activePath, r
197
197
  onNavigate: closeMobile,
198
198
  }), [renderLink, activePath, closeMobile]);
199
199
  const accountRow = signedOut ? (showAuthButtons && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectLoginButton, { size: "sm", fullWidth: true }), (0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectRegisterButton, { size: "sm", fullWidth: true })] }))) : ((0, jsx_runtime_1.jsx)(user_menu_js_1.ConnectUserButton, { variant: "row", theme: theme, onThemeChange: onThemeChange }));
200
- const railContent = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarHeader, { name: name, icon: icon, homeHref: homeHref }), workspaceSwitcher !== false && ((0, jsx_runtime_1.jsx)("div", { className: "shrink-0 px-3 pb-1", children: (0, jsx_runtime_1.jsx)(workspace_switcher_js_1.ConnectWorkspaceSwitcher, {}) })), (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarBody, { children: (0, jsx_runtime_1.jsx)(side_nav_js_1.ConnectSidebarNav, { nav: visibleNav }) }), (visibleSecondaryNav || sidebarFooter || showAppsGrid) && ((0, jsx_runtime_1.jsxs)(sidebar_js_1.ConnectSidebarFooter, { separated: false, children: [visibleSecondaryNav && (0, jsx_runtime_1.jsx)(side_nav_js_1.ConnectSidebarNav, { nav: visibleSecondaryNav }), sidebarFooter, showAppsGrid && (0, jsx_runtime_1.jsx)(apps_grid_js_1.ConnectAppsGrid, { variant: "row" })] })), accountRow && (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarFooter, { children: accountRow })] }));
200
+ const railContent = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarHeader, { name: name, icon: icon, homeHref: homeHref }), workspaceSwitcher !== false && ((0, jsx_runtime_1.jsx)("div", { className: "shrink-0 px-3 pb-1", children: (0, jsx_runtime_1.jsx)(workspace_switcher_js_1.ConnectWorkspaceSwitcher, { alwaysShow: workspaceSwitcher?.alwaysShow ?? false }) })), (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarBody, { children: (0, jsx_runtime_1.jsx)(side_nav_js_1.ConnectSidebarNav, { nav: visibleNav }) }), (visibleSecondaryNav || sidebarFooter || showAppsGrid) && ((0, jsx_runtime_1.jsxs)(sidebar_js_1.ConnectSidebarFooter, { separated: false, children: [visibleSecondaryNav && (0, jsx_runtime_1.jsx)(side_nav_js_1.ConnectSidebarNav, { nav: visibleSecondaryNav }), sidebarFooter, showAppsGrid && (0, jsx_runtime_1.jsx)(apps_grid_js_1.ConnectAppsGrid, { variant: "row" })] })), accountRow && (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarFooter, { children: accountRow })] }));
201
201
  return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)("div", { className: (0, cn_js_1.cn)("flex h-dvh bg-background", className), children: [(0, jsx_runtime_1.jsx)("a", { href: `#${CONTENT_ID}`, className: "sr-only focus:not-sr-only focus:absolute focus:start-3 focus:top-3 focus:z-50 focus:rounded-lg focus:bg-surface focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:text-foreground focus:shadow-lg focus:outline-2 focus:outline-offset-2 focus:outline-focus", children: "Skip to content" }), (0, jsx_runtime_1.jsx)(shell_context_js_1.ConnectShellProvider, { value: railShell, children: (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebar, { children: railContent }) }), (0, jsx_runtime_1.jsx)(react_2.Drawer.Backdrop, { isOpen: mobileOpen, onOpenChange: setMobileOpen, children: (0, jsx_runtime_1.jsx)(react_2.Drawer.Content, { placement: "left", className: "w-[min(20rem,85vw)]", children: (0, jsx_runtime_1.jsxs)(react_2.Drawer.Dialog, { "aria-label": "Navigation menu", className: "p-0 pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]", children: [(0, jsx_runtime_1.jsx)(react_2.Drawer.CloseTrigger, { className: "end-3 top-[calc(--spacing(3)+env(safe-area-inset-top))]" }), (0, jsx_runtime_1.jsx)(shell_context_js_1.ConnectShellProvider, { value: drawerShell, children: (0, jsx_runtime_1.jsx)("div", { className: "flex h-full flex-col [&>:first-child]:pe-10", children: railContent }) })] }) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col", children: [(0, jsx_runtime_1.jsxs)("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border bg-surface px-3 md:hidden", children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "tertiary", size: "sm", "aria-label": "Open navigation menu", onPress: () => setMobileOpen(true), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Menu, { className: "size-5" }) }), (0, jsx_runtime_1.jsx)(logo_js_1.ConnectLogo, { name: name, icon: icon })] }), (0, jsx_runtime_1.jsx)("main", { id: CONTENT_ID, tabIndex: -1, className: "flex-1 overflow-y-auto outline-none", children: (0, jsx_runtime_1.jsxs)("div", { className: contentClassName ?? "w-full px-4 py-5 md:px-6 md:py-6", children: [gated ? (0, jsx_runtime_1.jsx)(access_gate_js_1.ConnectAccessGate, { children: children }) : children, footer] }) })] }), askAi && (0, jsx_runtime_1.jsx)(ask_ai_js_1.ConnectAskAi, { ...(askAi === true ? {} : askAi) })] }) }));
202
202
  }
@@ -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
+ }
@@ -140,20 +140,25 @@ function groupPlansByTier(plans) {
140
140
  description: plan.description,
141
141
  monthly: null,
142
142
  annual: null,
143
+ oneTime: null,
143
144
  annualSavingPercent: null,
144
145
  };
145
146
  if (plan.billingInterval === "monthly") {
146
147
  if (!tier.monthly || plan.price < tier.monthly.price)
147
148
  tier.monthly = plan;
148
149
  }
149
- else if (!tier.annual || plan.price < tier.annual.price) {
150
- tier.annual = plan;
150
+ else if (plan.billingInterval === "annual") {
151
+ if (!tier.annual || plan.price < tier.annual.price)
152
+ tier.annual = plan;
153
+ }
154
+ else if (!tier.oneTime || plan.price < tier.oneTime.price) {
155
+ tier.oneTime = plan;
151
156
  }
152
157
  tiers.set(planGroup, tier);
153
158
  }
154
159
  return [...tiers.values()]
155
160
  .map((tier) => {
156
- const identity = tier.monthly ?? tier.annual;
161
+ const identity = tier.monthly ?? tier.annual ?? tier.oneTime;
157
162
  return {
158
163
  ...tier,
159
164
  name: identity?.name ?? tier.name,
@@ -164,7 +169,7 @@ function groupPlansByTier(plans) {
164
169
  .sort((a, b) => tierFloorPrice(a) - tierFloorPrice(b));
165
170
  }
166
171
  function tierFloorPrice(tier) {
167
- const prices = [tier.monthly, tier.annual]
172
+ const prices = [tier.monthly, tier.annual, tier.oneTime]
168
173
  .filter((plan) => plan !== null)
169
174
  .map((plan) => monthlyOf(plan.price, plan.billingInterval));
170
175
  return prices.length > 0 ? Math.min(...prices) : Number.MAX_SAFE_INTEGER;
@@ -129,7 +129,9 @@ function ConnectPlans({ planIds, hideCoupon = false, className }) {
129
129
  // Growth tier, and marking only the row it literally subscribes to
130
130
  // would leave its own card unmarked half the time.
131
131
  const isCurrentTier = subscription?.plan != null &&
132
- (tier.monthly?.id === subscription.plan.id || tier.annual?.id === subscription.plan.id);
132
+ (tier.monthly?.id === subscription.plan.id ||
133
+ tier.annual?.id === subscription.plan.id ||
134
+ tier.oneTime?.id === subscription.plan.id);
133
135
  const isCurrent = action === "current";
134
136
  const cadenceUnavailable = showAnnual ? tier.annual === null : tier.monthly === null;
135
137
  return ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { variants: { hidden: { opacity: 0, y: 12 }, show: { opacity: 1, y: 0, transition: motion_js_1.ENTER } }, children: (0, jsx_runtime_1.jsxs)(react_1.Card, { className: isCurrentTier ? "h-full border-accent" : "h-full", children: [(0, jsx_runtime_1.jsxs)(react_1.Card.Header, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [(0, jsx_runtime_1.jsx)(react_1.Card.Title, { children: tier.name }), (0, jsx_runtime_1.jsxs)("div", { className: "flex shrink-0 items-center gap-1.5", children: [tier.annualSavingPercent !== null && showAnnual && ((0, jsx_runtime_1.jsxs)(react_1.Chip, { className: "bg-accent-soft text-accent", children: ["Save ", tier.annualSavingPercent, "%"] })), isCurrentTier && (0, jsx_runtime_1.jsx)(react_1.Chip, { className: "bg-accent-soft text-accent", children: "Current" })] })] }), tier.description && (0, jsx_runtime_1.jsx)(react_1.Card.Description, { children: tier.description })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Content, { className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(motion_js_1.AnimatePresence, { mode: "popLayout", initial: false, children: (0, jsx_runtime_1.jsx)(motion_js_1.m.div, { initial: { opacity: 0, y: 6 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -6 }, transition: { duration: 0.15 }, children: (0, plan_pricing_js_1.isFreePlan)(plan.price) ? ((0, jsx_runtime_1.jsx)("span", { className: "text-2xl font-semibold text-foreground", children: "Free" })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "text-2xl font-semibold text-foreground", children: (0, plan_pricing_js_1.formatPrice)(plan.price) }), (0, jsx_runtime_1.jsx)("span", { className: "text-sm text-muted", children: (0, plan_pricing_js_1.pricePerLabel)(plan.billingInterval) })] })) }, plan.billingInterval) }), !(0, plan_pricing_js_1.isFreePlan)(plan.price) && ((0, jsx_runtime_1.jsxs)("p", { className: "mt-1 text-xs text-muted", children: ["+", plan.gstRate, "% GST"] })), cadenceUnavailable && ((0, jsx_runtime_1.jsxs)("p", { className: "mt-1 text-xs text-muted", children: ["Only sold ", (0, plan_pricing_js_1.intervalLabel)(plan.billingInterval).adverb, "."] }))] }), plan.trialDays && !(0, plan_pricing_js_1.isFreePlan)(plan.price) ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs font-medium text-accent", children: [plan.trialDays, "-day free trial"] })) : null, plan.features.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { className: "flex flex-col gap-1.5", children: plan.features.map((feature) => (
@@ -214,7 +216,13 @@ function ConnectUnsubscribeRow({ subscription, appUrl, checkout, }) {
214
216
  * monthly-only is a real thing (a free or entry plan usually is), and hiding
215
217
  * it while a customer looks at annual pricing loses them an option that is
216
218
  * still available to buy.
219
+ *
220
+ * A tier that is *only* a one-time plan ignores the toggle entirely - there
221
+ * is no cadence to switch between, so the price it shows must not depend on
222
+ * `showAnnual`.
217
223
  */
218
224
  function pickTierPlan(tier, showAnnual) {
225
+ if (tier.oneTime && !tier.monthly && !tier.annual)
226
+ return tier.oneTime;
219
227
  return showAnnual ? (tier.annual ?? tier.monthly) : (tier.monthly ?? tier.annual);
220
228
  }
@@ -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 }) {
@@ -14,6 +14,9 @@ function workspaceInitials(name) {
14
14
  const parts = name.trim().split(/\s+/).filter(Boolean);
15
15
  return (parts.slice(0, 2).map((part) => part[0]).join("") || "?").toUpperCase();
16
16
  }
17
+ function openExternal(url) {
18
+ window.open(url, "_blank", "noopener,noreferrer");
19
+ }
17
20
  /**
18
21
  * Switches which workspace this application is looking at.
19
22
  *
@@ -22,11 +25,15 @@ function workspaceInitials(name) {
22
25
  * it reads as a heading for the nav rather than as one more destination.
23
26
  *
24
27
  * It decides for itself whether to appear: an account with one workspace is
25
- * shown nothing, because a switcher offering one option teaches the user
26
- * nothing and costs a row of chrome on every screen. That is the opposite of
27
- * Connect's own switcher, which always renders - Connect is where a workspace
28
- * is *created*, so hiding it at one workspace would guarantee nobody ever
29
- * reached two. A partner application creates nothing, so it has no such duty.
28
+ * shown nothing by default, because a switcher offering one option teaches
29
+ * the user nothing and costs a row of chrome on every screen. That is the
30
+ * opposite of Connect's own switcher, which always renders - Connect is where
31
+ * a workspace is *created*, so hiding it at one workspace would guarantee
32
+ * nobody ever reached two. `alwaysShow` is the escape hatch for a partner
33
+ * application that wants that same discoverability without becoming Connect:
34
+ * it keeps the dropdown open below two workspaces and adds a row that sends
35
+ * the member to Connect to actually create one, since there is nothing here
36
+ * that can.
30
37
  *
31
38
  * **Authorization is the data, not a prop.** The list comes from the caller's
32
39
  * own session (`GET /workspaces` takes no user id), so a workspace the user
@@ -45,19 +52,22 @@ function workspaceInitials(name) {
45
52
  * component - so the only thing to do afterwards is reload, which is what
46
53
  * `onSwitched` defaults to.
47
54
  */
48
- function ConnectWorkspaceSwitcher({ onSwitched, row = true, className, }) {
49
- const { mutateResource } = (0, provider_js_1.useConnect)();
55
+ function ConnectWorkspaceSwitcher({ onSwitched, row = true, className, alwaysShow = false, }) {
56
+ const { mutateResource, appUrl } = (0, provider_js_1.useConnect)();
50
57
  const { collapsed } = (0, shell_context_js_1.useConnectShell)();
51
58
  const user = (0, provider_js_1.useConnectUser)();
52
59
  const { data, isLoading } = (0, provider_js_1.useConnectWorkspaces)();
53
60
  const [switching, setSwitching] = (0, react_1.useState)(null);
54
61
  const [error, setError] = (0, react_1.useState)(null);
55
62
  const workspaces = data?.workspaces ?? [];
56
- // Nothing to switch between, or nothing loaded yet. Deliberately renders
57
- // `null` rather than a skeleton: this sits in persistent chrome, and a
58
- // placeholder that resolves to nothing for most accounts is worse than
59
- // appearing a moment late for the few who have two.
60
- if (isLoading || workspaces.length < 2)
63
+ // Nothing loaded yet. Deliberately renders `null` rather than a skeleton:
64
+ // this sits in persistent chrome, and a placeholder that resolves to
65
+ // nothing for most accounts is worse than appearing a moment late.
66
+ //
67
+ // Below one workspace, `alwaysShow` is the only thing that keeps this
68
+ // rendering - the default stays "nothing to switch between" for every
69
+ // host that hasn't opted in.
70
+ if (isLoading || (!alwaysShow && workspaces.length < 2))
61
71
  return null;
62
72
  const active = workspaces.find((workspace) => workspace.isActive);
63
73
  const label = active?.name ?? user?.workspaceName ?? "Workspace";
@@ -87,7 +97,7 @@ function ConnectWorkspaceSwitcher({ onSwitched, row = true, className, }) {
87
97
  // A 32px tile rather than a 20px glyph, so the initials land on the
88
98
  // same optical centre the avatar below them uses - see
89
99
  // `SIDEBAR_ROW_TILE`. Collapsed, this tile *is* the switcher.
90
- (0, jsx_runtime_1.jsxs)(react_2.Button, { variant: "tertiary", "aria-label": `Workspace: ${label}. Switch workspace`, className: (0, shell_context_js_1.sidebarRowClass)({ tile: true, className }), children: [(0, jsx_runtime_1.jsx)("span", { className: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-accent-soft text-xs font-semibold text-accent", children: workspaceInitials(label) }), (0, jsx_runtime_1.jsx)(shell_context_js_1.SidebarLabel, { className: "text-sm font-medium text-foreground", children: label }), (0, jsx_runtime_1.jsx)(motion_js_1.m.span, { className: "shrink-0 text-muted", animate: { opacity: collapsed ? 0 : 1 }, transition: collapsed ? { duration: 0.1 } : { duration: 0.18, delay: 0.1 }, "aria-hidden": true, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronsUpDown, { className: "size-4" }) })] })) : ((0, jsx_runtime_1.jsxs)(react_2.Button, { variant: "secondary", size: "sm", className: "w-full justify-between", children: [(0, jsx_runtime_1.jsx)("span", { className: "truncate", children: label }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronsUpDown, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" })] })), (0, jsx_runtime_1.jsx)(react_2.Dropdown.Popover, { children: (0, jsx_runtime_1.jsx)(react_2.ListBox, { children: workspaces.map((workspace) => ((0, jsx_runtime_1.jsxs)(react_2.ListBox.Item, { id: workspace.workspaceId, textValue: workspace.name, isDisabled: !selectable(workspace) || switching !== null, onAction: () => void handleSwitch(workspace), children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm", children: workspace.name }), selectable(workspace) ? null : ((0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: reasonUnavailable(workspace) }))] }), (0, jsx_runtime_1.jsx)(react_2.ListBox.ItemIndicator, {})] }, workspace.workspaceId))) }) })] }), error && !collapsed ? (0, jsx_runtime_1.jsx)("p", { className: "mt-1 px-2 text-xs text-danger", children: error }) : null] }));
100
+ (0, jsx_runtime_1.jsxs)(react_2.Button, { variant: "tertiary", "aria-label": `Workspace: ${label}. Switch workspace`, className: (0, shell_context_js_1.sidebarRowClass)({ tile: true, className }), children: [(0, jsx_runtime_1.jsx)("span", { className: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-accent-soft text-xs font-semibold text-accent", children: workspaceInitials(label) }), (0, jsx_runtime_1.jsx)(shell_context_js_1.SidebarLabel, { className: "text-sm font-medium text-foreground", children: label }), (0, jsx_runtime_1.jsx)(motion_js_1.m.span, { className: "shrink-0 text-muted", animate: { opacity: collapsed ? 0 : 1 }, transition: collapsed ? { duration: 0.1 } : { duration: 0.18, delay: 0.1 }, "aria-hidden": true, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronsUpDown, { className: "size-4" }) })] })) : ((0, jsx_runtime_1.jsxs)(react_2.Button, { variant: "secondary", size: "sm", className: "w-full justify-between", children: [(0, jsx_runtime_1.jsx)("span", { className: "truncate", children: label }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronsUpDown, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" })] })), (0, jsx_runtime_1.jsx)(react_2.Dropdown.Popover, { children: (0, jsx_runtime_1.jsxs)(react_2.ListBox, { children: [workspaces.map((workspace) => ((0, jsx_runtime_1.jsxs)(react_2.ListBox.Item, { id: workspace.workspaceId, textValue: workspace.name, isDisabled: !selectable(workspace) || switching !== null, onAction: () => void handleSwitch(workspace), children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm", children: workspace.name }), selectable(workspace) ? null : ((0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: reasonUnavailable(workspace) }))] }), (0, jsx_runtime_1.jsx)(react_2.ListBox.ItemIndicator, {})] }, workspace.workspaceId))), alwaysShow ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [workspaces.length > 0 && (0, jsx_runtime_1.jsx)(react_2.Separator, {}), (0, jsx_runtime_1.jsxs)(react_2.ListBox.Item, { id: "__create_workspace", textValue: "Create workspace", onAction: () => openExternal(appUrl), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { className: "size-4 shrink-0 text-muted", "aria-hidden": true }), (0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm", children: "Create workspace" })] })] })) : null] }) })] }), error && !collapsed ? (0, jsx_runtime_1.jsx)("p", { className: "mt-1 px-2 text-xs text-danger", children: error }) : null] }));
91
101
  }
92
102
  /** Belongs to the user, and usable here. The same two facts `switch` enforces. */
93
103
  function selectable(workspace) {
@@ -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
+ }