@cosmoops/connect-sdk 0.36.0 → 0.37.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.
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
  }
@@ -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
  }
@@ -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) {
@@ -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.36.0";
182
+ exports.SDK_VERSION = "0.37.0";
@@ -194,6 +194,6 @@ export function AppShell({ name, icon, homeHref = "/", nav, secondaryNav, active
194
194
  onNavigate: closeMobile,
195
195
  }), [renderLink, activePath, closeMobile]);
196
196
  const accountRow = signedOut ? (showAuthButtons && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(ConnectLoginButton, { size: "sm", fullWidth: true }), _jsx(ConnectRegisterButton, { size: "sm", fullWidth: true })] }))) : (_jsx(ConnectUserButton, { variant: "row", theme: theme, onThemeChange: onThemeChange }));
197
- const railContent = (_jsxs(_Fragment, { children: [_jsx(ConnectSidebarHeader, { name: name, icon: icon, homeHref: homeHref }), workspaceSwitcher !== false && (_jsx("div", { className: "shrink-0 px-3 pb-1", children: _jsx(ConnectWorkspaceSwitcher, {}) })), _jsx(ConnectSidebarBody, { children: _jsx(ConnectSidebarNav, { nav: visibleNav }) }), (visibleSecondaryNav || sidebarFooter || showAppsGrid) && (_jsxs(ConnectSidebarFooter, { separated: false, children: [visibleSecondaryNav && _jsx(ConnectSidebarNav, { nav: visibleSecondaryNav }), sidebarFooter, showAppsGrid && _jsx(ConnectAppsGrid, { variant: "row" })] })), accountRow && _jsx(ConnectSidebarFooter, { children: accountRow })] }));
197
+ const railContent = (_jsxs(_Fragment, { children: [_jsx(ConnectSidebarHeader, { name: name, icon: icon, homeHref: homeHref }), workspaceSwitcher !== false && (_jsx("div", { className: "shrink-0 px-3 pb-1", children: _jsx(ConnectWorkspaceSwitcher, { alwaysShow: workspaceSwitcher?.alwaysShow ?? false }) })), _jsx(ConnectSidebarBody, { children: _jsx(ConnectSidebarNav, { nav: visibleNav }) }), (visibleSecondaryNav || sidebarFooter || showAppsGrid) && (_jsxs(ConnectSidebarFooter, { separated: false, children: [visibleSecondaryNav && _jsx(ConnectSidebarNav, { nav: visibleSecondaryNav }), sidebarFooter, showAppsGrid && _jsx(ConnectAppsGrid, { variant: "row" })] })), accountRow && _jsx(ConnectSidebarFooter, { children: accountRow })] }));
198
198
  return (_jsx(MotionProvider, { children: _jsxs("div", { className: cn("flex h-dvh bg-background", className), children: [_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" }), _jsx(ConnectShellProvider, { value: railShell, children: _jsx(ConnectSidebar, { children: railContent }) }), _jsx(Drawer.Backdrop, { isOpen: mobileOpen, onOpenChange: setMobileOpen, children: _jsx(Drawer.Content, { placement: "left", className: "w-[min(20rem,85vw)]", children: _jsxs(Drawer.Dialog, { "aria-label": "Navigation menu", className: "p-0 pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]", children: [_jsx(Drawer.CloseTrigger, { className: "end-3 top-[calc(--spacing(3)+env(safe-area-inset-top))]" }), _jsx(ConnectShellProvider, { value: drawerShell, children: _jsx("div", { className: "flex h-full flex-col [&>:first-child]:pe-10", children: railContent }) })] }) }) }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border bg-surface px-3 md:hidden", children: [_jsx(Button, { isIconOnly: true, variant: "tertiary", size: "sm", "aria-label": "Open navigation menu", onPress: () => setMobileOpen(true), children: _jsx(Menu, { className: "size-5" }) }), _jsx(ConnectLogo, { name: name, icon: icon })] }), _jsx("main", { id: CONTENT_ID, tabIndex: -1, className: "flex-1 overflow-y-auto outline-none", children: _jsxs("div", { className: contentClassName ?? "w-full px-4 py-5 md:px-6 md:py-6", children: [gated ? _jsx(ConnectAccessGate, { children: children }) : children, footer] }) })] }), askAi && _jsx(ConnectAskAi, { ...(askAi === true ? {} : askAi) })] }) }));
199
199
  }
@@ -126,20 +126,25 @@ export function groupPlansByTier(plans) {
126
126
  description: plan.description,
127
127
  monthly: null,
128
128
  annual: null,
129
+ oneTime: null,
129
130
  annualSavingPercent: null,
130
131
  };
131
132
  if (plan.billingInterval === "monthly") {
132
133
  if (!tier.monthly || plan.price < tier.monthly.price)
133
134
  tier.monthly = plan;
134
135
  }
135
- else if (!tier.annual || plan.price < tier.annual.price) {
136
- tier.annual = plan;
136
+ else if (plan.billingInterval === "annual") {
137
+ if (!tier.annual || plan.price < tier.annual.price)
138
+ tier.annual = plan;
139
+ }
140
+ else if (!tier.oneTime || plan.price < tier.oneTime.price) {
141
+ tier.oneTime = plan;
137
142
  }
138
143
  tiers.set(planGroup, tier);
139
144
  }
140
145
  return [...tiers.values()]
141
146
  .map((tier) => {
142
- const identity = tier.monthly ?? tier.annual;
147
+ const identity = tier.monthly ?? tier.annual ?? tier.oneTime;
143
148
  return {
144
149
  ...tier,
145
150
  name: identity?.name ?? tier.name,
@@ -150,7 +155,7 @@ export function groupPlansByTier(plans) {
150
155
  .sort((a, b) => tierFloorPrice(a) - tierFloorPrice(b));
151
156
  }
152
157
  function tierFloorPrice(tier) {
153
- const prices = [tier.monthly, tier.annual]
158
+ const prices = [tier.monthly, tier.annual, tier.oneTime]
154
159
  .filter((plan) => plan !== null)
155
160
  .map((plan) => monthlyOf(plan.price, plan.billingInterval));
156
161
  return prices.length > 0 ? Math.min(...prices) : Number.MAX_SAFE_INTEGER;
@@ -126,7 +126,9 @@ export function ConnectPlans({ planIds, hideCoupon = false, className }) {
126
126
  // Growth tier, and marking only the row it literally subscribes to
127
127
  // would leave its own card unmarked half the time.
128
128
  const isCurrentTier = subscription?.plan != null &&
129
- (tier.monthly?.id === subscription.plan.id || tier.annual?.id === subscription.plan.id);
129
+ (tier.monthly?.id === subscription.plan.id ||
130
+ tier.annual?.id === subscription.plan.id ||
131
+ tier.oneTime?.id === subscription.plan.id);
130
132
  const isCurrent = action === "current";
131
133
  const cadenceUnavailable = showAnnual ? tier.annual === null : tier.monthly === null;
132
134
  return (_jsx(m.div, { variants: { hidden: { opacity: 0, y: 12 }, show: { opacity: 1, y: 0, transition: ENTER } }, children: _jsxs(Card, { className: isCurrentTier ? "h-full border-accent" : "h-full", children: [_jsxs(Card.Header, { children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx(Card.Title, { children: tier.name }), _jsxs("div", { className: "flex shrink-0 items-center gap-1.5", children: [tier.annualSavingPercent !== null && showAnnual && (_jsxs(Chip, { className: "bg-accent-soft text-accent", children: ["Save ", tier.annualSavingPercent, "%"] })), isCurrentTier && _jsx(Chip, { className: "bg-accent-soft text-accent", children: "Current" })] })] }), tier.description && _jsx(Card.Description, { children: tier.description })] }), _jsxs(Card.Content, { className: "flex flex-col gap-4", children: [_jsxs("div", { children: [_jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(m.div, { initial: { opacity: 0, y: 6 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -6 }, transition: { duration: 0.15 }, children: isFreePlan(plan.price) ? (_jsx("span", { className: "text-2xl font-semibold text-foreground", children: "Free" })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-2xl font-semibold text-foreground", children: formatPrice(plan.price) }), _jsx("span", { className: "text-sm text-muted", children: pricePerLabel(plan.billingInterval) })] })) }, plan.billingInterval) }), !isFreePlan(plan.price) && (_jsxs("p", { className: "mt-1 text-xs text-muted", children: ["+", plan.gstRate, "% GST"] })), cadenceUnavailable && (_jsxs("p", { className: "mt-1 text-xs text-muted", children: ["Only sold ", intervalLabel(plan.billingInterval).adverb, "."] }))] }), plan.trialDays && !isFreePlan(plan.price) ? (_jsxs("p", { className: "text-xs font-medium text-accent", children: [plan.trialDays, "-day free trial"] })) : null, plan.features.length > 0 && (_jsx("ul", { className: "flex flex-col gap-1.5", children: plan.features.map((feature) => (
@@ -211,7 +213,13 @@ function ConnectUnsubscribeRow({ subscription, appUrl, checkout, }) {
211
213
  * monthly-only is a real thing (a free or entry plan usually is), and hiding
212
214
  * it while a customer looks at annual pricing loses them an option that is
213
215
  * still available to buy.
216
+ *
217
+ * A tier that is *only* a one-time plan ignores the toggle entirely - there
218
+ * is no cadence to switch between, so the price it shows must not depend on
219
+ * `showAnnual`.
214
220
  */
215
221
  function pickTierPlan(tier, showAnnual) {
222
+ if (tier.oneTime && !tier.monthly && !tier.annual)
223
+ return tier.oneTime;
216
224
  return showAnnual ? (tier.annual ?? tier.monthly) : (tier.monthly ?? tier.annual);
217
225
  }
@@ -1,8 +1,8 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
- import { Button, Dropdown, ListBox } from "@heroui/react";
5
- import { ChevronsUpDown } from "lucide-react";
4
+ import { Button, Dropdown, ListBox, Separator } from "@heroui/react";
5
+ import { ChevronsUpDown, Plus } from "lucide-react";
6
6
  import { useConnect, useConnectUser, useConnectWorkspaces } from "../provider.js";
7
7
  import { m } from "./motion.js";
8
8
  import { SidebarLabel, sidebarRowClass, useConnectShell } from "./shell-context.js";
@@ -11,6 +11,9 @@ function workspaceInitials(name) {
11
11
  const parts = name.trim().split(/\s+/).filter(Boolean);
12
12
  return (parts.slice(0, 2).map((part) => part[0]).join("") || "?").toUpperCase();
13
13
  }
14
+ function openExternal(url) {
15
+ window.open(url, "_blank", "noopener,noreferrer");
16
+ }
14
17
  /**
15
18
  * Switches which workspace this application is looking at.
16
19
  *
@@ -19,11 +22,15 @@ function workspaceInitials(name) {
19
22
  * it reads as a heading for the nav rather than as one more destination.
20
23
  *
21
24
  * It decides for itself whether to appear: an account with one workspace is
22
- * shown nothing, because a switcher offering one option teaches the user
23
- * nothing and costs a row of chrome on every screen. That is the opposite of
24
- * Connect's own switcher, which always renders - Connect is where a workspace
25
- * is *created*, so hiding it at one workspace would guarantee nobody ever
26
- * reached two. A partner application creates nothing, so it has no such duty.
25
+ * shown nothing by default, because a switcher offering one option teaches
26
+ * the user nothing and costs a row of chrome on every screen. That is the
27
+ * opposite of Connect's own switcher, which always renders - Connect is where
28
+ * a workspace is *created*, so hiding it at one workspace would guarantee
29
+ * nobody ever reached two. `alwaysShow` is the escape hatch for a partner
30
+ * application that wants that same discoverability without becoming Connect:
31
+ * it keeps the dropdown open below two workspaces and adds a row that sends
32
+ * the member to Connect to actually create one, since there is nothing here
33
+ * that can.
27
34
  *
28
35
  * **Authorization is the data, not a prop.** The list comes from the caller's
29
36
  * own session (`GET /workspaces` takes no user id), so a workspace the user
@@ -42,19 +49,22 @@ function workspaceInitials(name) {
42
49
  * component - so the only thing to do afterwards is reload, which is what
43
50
  * `onSwitched` defaults to.
44
51
  */
45
- export function ConnectWorkspaceSwitcher({ onSwitched, row = true, className, }) {
46
- const { mutateResource } = useConnect();
52
+ export function ConnectWorkspaceSwitcher({ onSwitched, row = true, className, alwaysShow = false, }) {
53
+ const { mutateResource, appUrl } = useConnect();
47
54
  const { collapsed } = useConnectShell();
48
55
  const user = useConnectUser();
49
56
  const { data, isLoading } = useConnectWorkspaces();
50
57
  const [switching, setSwitching] = useState(null);
51
58
  const [error, setError] = useState(null);
52
59
  const workspaces = data?.workspaces ?? [];
53
- // Nothing to switch between, or nothing loaded yet. Deliberately renders
54
- // `null` rather than a skeleton: this sits in persistent chrome, and a
55
- // placeholder that resolves to nothing for most accounts is worse than
56
- // appearing a moment late for the few who have two.
57
- if (isLoading || workspaces.length < 2)
60
+ // Nothing loaded yet. Deliberately renders `null` rather than a skeleton:
61
+ // this sits in persistent chrome, and a placeholder that resolves to
62
+ // nothing for most accounts is worse than appearing a moment late.
63
+ //
64
+ // Below one workspace, `alwaysShow` is the only thing that keeps this
65
+ // rendering - the default stays "nothing to switch between" for every
66
+ // host that hasn't opted in.
67
+ if (isLoading || (!alwaysShow && workspaces.length < 2))
58
68
  return null;
59
69
  const active = workspaces.find((workspace) => workspace.isActive);
60
70
  const label = active?.name ?? user?.workspaceName ?? "Workspace";
@@ -84,7 +94,7 @@ export function ConnectWorkspaceSwitcher({ onSwitched, row = true, className, })
84
94
  // A 32px tile rather than a 20px glyph, so the initials land on the
85
95
  // same optical centre the avatar below them uses - see
86
96
  // `SIDEBAR_ROW_TILE`. Collapsed, this tile *is* the switcher.
87
- _jsxs(Button, { variant: "tertiary", "aria-label": `Workspace: ${label}. Switch workspace`, className: sidebarRowClass({ tile: true, className }), children: [_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) }), _jsx(SidebarLabel, { className: "text-sm font-medium text-foreground", children: label }), _jsx(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: _jsx(ChevronsUpDown, { className: "size-4" }) })] })) : (_jsxs(Button, { variant: "secondary", size: "sm", className: "w-full justify-between", children: [_jsx("span", { className: "truncate", children: label }), _jsx(ChevronsUpDown, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" })] })), _jsx(Dropdown.Popover, { children: _jsx(ListBox, { children: workspaces.map((workspace) => (_jsxs(ListBox.Item, { id: workspace.workspaceId, textValue: workspace.name, isDisabled: !selectable(workspace) || switching !== null, onAction: () => void handleSwitch(workspace), children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-sm", children: workspace.name }), selectable(workspace) ? null : (_jsx("p", { className: "truncate text-xs text-muted", children: reasonUnavailable(workspace) }))] }), _jsx(ListBox.ItemIndicator, {})] }, workspace.workspaceId))) }) })] }), error && !collapsed ? _jsx("p", { className: "mt-1 px-2 text-xs text-danger", children: error }) : null] }));
97
+ _jsxs(Button, { variant: "tertiary", "aria-label": `Workspace: ${label}. Switch workspace`, className: sidebarRowClass({ tile: true, className }), children: [_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) }), _jsx(SidebarLabel, { className: "text-sm font-medium text-foreground", children: label }), _jsx(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: _jsx(ChevronsUpDown, { className: "size-4" }) })] })) : (_jsxs(Button, { variant: "secondary", size: "sm", className: "w-full justify-between", children: [_jsx("span", { className: "truncate", children: label }), _jsx(ChevronsUpDown, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" })] })), _jsx(Dropdown.Popover, { children: _jsxs(ListBox, { children: [workspaces.map((workspace) => (_jsxs(ListBox.Item, { id: workspace.workspaceId, textValue: workspace.name, isDisabled: !selectable(workspace) || switching !== null, onAction: () => void handleSwitch(workspace), children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-sm", children: workspace.name }), selectable(workspace) ? null : (_jsx("p", { className: "truncate text-xs text-muted", children: reasonUnavailable(workspace) }))] }), _jsx(ListBox.ItemIndicator, {})] }, workspace.workspaceId))), alwaysShow ? (_jsxs(_Fragment, { children: [workspaces.length > 0 && _jsx(Separator, {}), _jsxs(ListBox.Item, { id: "__create_workspace", textValue: "Create workspace", onAction: () => openExternal(appUrl), children: [_jsx(Plus, { className: "size-4 shrink-0 text-muted", "aria-hidden": true }), _jsx("p", { className: "truncate text-sm", children: "Create workspace" })] })] })) : null] }) })] }), error && !collapsed ? _jsx("p", { className: "mt-1 px-2 text-xs text-danger", children: error }) : null] }));
88
98
  }
89
99
  /** Belongs to the user, and usable here. The same two facts `switch` enforces. */
90
100
  function selectable(workspace) {
@@ -175,4 +175,4 @@ export class ConnectClient {
175
175
  }
176
176
  }
177
177
  /** Kept in step with `package.json` by `sdk.test.ts`. */
178
- export const SDK_VERSION = "0.36.0";
178
+ export const SDK_VERSION = "0.37.0";
@@ -36,8 +36,14 @@ export interface AppShellProps {
36
36
  * for an account with only one workspace - so the reason to pass `false` is
37
37
  * not "most users have one" (already handled) but "this application renders
38
38
  * its own".
39
+ *
40
+ * Pass `{ alwaysShow: true }` instead of `true` to keep it mounted below
41
+ * two workspaces too - see `ConnectWorkspaceSwitcherProps.alwaysShow` for
42
+ * what that changes about the dropdown.
39
43
  */
40
- workspaceSwitcher?: false;
44
+ workspaceSwitcher?: false | {
45
+ alwaysShow?: boolean;
46
+ };
41
47
  /** Rendered below `children`, inside the scrolling content pane. */
42
48
  footer?: ReactNode;
43
49
  /**
@@ -74,6 +74,16 @@ export interface PlanTier {
74
74
  description: string;
75
75
  monthly: Plan | null;
76
76
  annual: Plan | null;
77
+ /**
78
+ * A plan paid once, never on a cadence - its own slot rather than falling
79
+ * into `annual`. A one-time plan has no renewal to discount against, so
80
+ * filing it as "annual" used to let a tier mixing a recurring plan with a
81
+ * one-time one compute a `annualSavingPercent` between a monthly charge and
82
+ * a lump sum, and offered the one-time purchase under the annual toggle as
83
+ * though it renewed. `pickTierPlan` in `plans.tsx` is the only reader that
84
+ * needs to know this slot exists.
85
+ */
86
+ oneTime: Plan | null;
77
87
  /** Whole-percent saving from paying annually, or `null` when this tier has no pair to compare. */
78
88
  annualSavingPercent: number | null;
79
89
  }
@@ -14,6 +14,21 @@ export interface ConnectWorkspaceSwitcherProps {
14
14
  */
15
15
  row?: boolean;
16
16
  className?: string;
17
+ /**
18
+ * Render even when the signed-in account has fewer than two workspaces.
19
+ *
20
+ * Off by default, which keeps this component's documented posture for
21
+ * every other host: an account with one workspace sees nothing, because a
22
+ * switcher offering one option teaches it nothing. Pass `true` on an
23
+ * application that wants a single-workspace account to be able to
24
+ * discover - and start - creating a second one without leaving its own
25
+ * chrome. There is deliberately no `workspaces.create()` on the SDK:
26
+ * creating a workspace is Connect's job, tied to KYC/KYB and a billing
27
+ * profile, not something an API key should be able to trigger - so turning
28
+ * this on also adds a "Create workspace" row that opens Connect, rather
29
+ * than pretending the switcher can do it itself.
30
+ */
31
+ alwaysShow?: boolean;
17
32
  }
18
33
  /**
19
34
  * Switches which workspace this application is looking at.
@@ -23,11 +38,15 @@ export interface ConnectWorkspaceSwitcherProps {
23
38
  * it reads as a heading for the nav rather than as one more destination.
24
39
  *
25
40
  * It decides for itself whether to appear: an account with one workspace is
26
- * shown nothing, because a switcher offering one option teaches the user
27
- * nothing and costs a row of chrome on every screen. That is the opposite of
28
- * Connect's own switcher, which always renders - Connect is where a workspace
29
- * is *created*, so hiding it at one workspace would guarantee nobody ever
30
- * reached two. A partner application creates nothing, so it has no such duty.
41
+ * shown nothing by default, because a switcher offering one option teaches
42
+ * the user nothing and costs a row of chrome on every screen. That is the
43
+ * opposite of Connect's own switcher, which always renders - Connect is where
44
+ * a workspace is *created*, so hiding it at one workspace would guarantee
45
+ * nobody ever reached two. `alwaysShow` is the escape hatch for a partner
46
+ * application that wants that same discoverability without becoming Connect:
47
+ * it keeps the dropdown open below two workspaces and adds a row that sends
48
+ * the member to Connect to actually create one, since there is nothing here
49
+ * that can.
31
50
  *
32
51
  * **Authorization is the data, not a prop.** The list comes from the caller's
33
52
  * own session (`GET /workspaces` takes no user id), so a workspace the user
@@ -46,4 +65,4 @@ export interface ConnectWorkspaceSwitcherProps {
46
65
  * component - so the only thing to do afterwards is reload, which is what
47
66
  * `onSwitched` defaults to.
48
67
  */
49
- export declare function ConnectWorkspaceSwitcher({ onSwitched, row, className, }: Readonly<ConnectWorkspaceSwitcherProps>): import("react").JSX.Element | null;
68
+ export declare function ConnectWorkspaceSwitcher({ onSwitched, row, className, alwaysShow, }: Readonly<ConnectWorkspaceSwitcherProps>): import("react").JSX.Element | null;
@@ -138,4 +138,4 @@ export declare class ConnectClient {
138
138
  constructor(options: ConnectClientOptions);
139
139
  }
140
140
  /** Kept in step with `package.json` by `sdk.test.ts`. */
141
- export declare const SDK_VERSION = "0.36.0";
141
+ export declare const SDK_VERSION = "0.37.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmoops/connect-sdk",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "Official server-side SDK for the CosmoOps Connect platform API - Login with Connect, entitlements, billing and metering.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",