@cosmoops/connect-sdk 0.9.0 → 0.15.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 (139) hide show
  1. package/CHANGELOG.md +497 -0
  2. package/README.md +111 -41
  3. package/dist/cjs/cli/doctor.js +3 -3
  4. package/dist/cjs/cli/init.js +3 -3
  5. package/dist/cjs/client/components/access-gate.js +20 -1
  6. package/dist/cjs/client/components/app-shell.js +75 -41
  7. package/dist/cjs/client/components/apps-grid.js +3 -2
  8. package/dist/cjs/client/components/ask-ai.js +20 -1
  9. package/dist/cjs/client/components/confirm-dialog.js +40 -0
  10. package/dist/cjs/client/components/cursor-pagination.js +22 -0
  11. package/dist/cjs/client/components/filter-tabs.js +24 -0
  12. package/dist/cjs/client/components/grievance-notice.js +30 -0
  13. package/dist/cjs/client/components/link-button.js +17 -10
  14. package/dist/cjs/client/components/logo.js +4 -3
  15. package/dist/cjs/client/components/nav.js +41 -0
  16. package/dist/cjs/client/components/plan-pricing.js +27 -0
  17. package/dist/cjs/client/components/plans.js +1 -1
  18. package/dist/cjs/client/components/platform-gate.js +48 -0
  19. package/dist/cjs/client/components/shell-context.js +149 -0
  20. package/dist/cjs/client/components/side-nav.js +49 -23
  21. package/dist/cjs/client/components/sidebar.js +127 -0
  22. package/dist/cjs/client/components/subscribe-dialog.js +2 -2
  23. package/dist/cjs/client/components/subscription.js +1 -1
  24. package/dist/cjs/client/components/user-menu.js +96 -37
  25. package/dist/cjs/client/components/workspace-switcher.js +100 -0
  26. package/dist/cjs/client/index.js +69 -2
  27. package/dist/cjs/client/provider.js +57 -2
  28. package/dist/cjs/client/use-checkout.js +9 -5
  29. package/dist/cjs/errors.js +1 -1
  30. package/dist/cjs/index.js +11 -1
  31. package/dist/cjs/next/index.js +2 -2
  32. package/dist/cjs/server/client.js +25 -2
  33. package/dist/cjs/server/handler.js +196 -18
  34. package/dist/cjs/server/http.js +6 -2
  35. package/dist/cjs/server/resources/acting-as.js +16 -0
  36. package/dist/cjs/server/resources/ai.js +15 -2
  37. package/dist/cjs/server/resources/data.js +374 -0
  38. package/dist/cjs/server/resources/identity.js +7 -3
  39. package/dist/cjs/server/resources/kv.js +84 -0
  40. package/dist/cjs/server/resources/platform.js +43 -0
  41. package/dist/cjs/server/resources/sessions.js +1 -1
  42. package/dist/cjs/server/resources/workspaces.js +74 -0
  43. package/dist/cjs/store.js +152 -0
  44. package/dist/cjs/ui/empty-state.js +28 -0
  45. package/dist/cjs/ui/index.js +44 -0
  46. package/dist/cjs/ui/page-header.js +27 -0
  47. package/dist/cjs/ui/page-shell.js +44 -0
  48. package/dist/cjs/ui/section.js +20 -0
  49. package/dist/cjs/ui/setting-row.js +14 -0
  50. package/dist/cjs/ui/status-chip.js +17 -0
  51. package/dist/esm/cli/doctor.js +3 -3
  52. package/dist/esm/cli/init.js +3 -3
  53. package/dist/esm/client/components/access-gate.js +20 -1
  54. package/dist/esm/client/components/app-shell.js +81 -47
  55. package/dist/esm/client/components/apps-grid.js +3 -2
  56. package/dist/esm/client/components/ask-ai.js +20 -1
  57. package/dist/esm/client/components/confirm-dialog.js +37 -0
  58. package/dist/esm/client/components/cursor-pagination.js +19 -0
  59. package/dist/esm/client/components/filter-tabs.js +21 -0
  60. package/dist/esm/client/components/grievance-notice.js +27 -0
  61. package/dist/esm/client/components/link-button.js +17 -10
  62. package/dist/esm/client/components/logo.js +4 -3
  63. package/dist/esm/client/components/nav.js +40 -0
  64. package/dist/esm/client/components/plan-pricing.js +25 -0
  65. package/dist/esm/client/components/plans.js +2 -2
  66. package/dist/esm/client/components/platform-gate.js +45 -0
  67. package/dist/esm/client/components/shell-context.js +141 -0
  68. package/dist/esm/client/components/side-nav.js +48 -21
  69. package/dist/esm/client/components/sidebar.js +120 -0
  70. package/dist/esm/client/components/subscribe-dialog.js +3 -3
  71. package/dist/esm/client/components/subscription.js +2 -2
  72. package/dist/esm/client/components/user-menu.js +95 -38
  73. package/dist/esm/client/components/workspace-switcher.js +97 -0
  74. package/dist/esm/client/index.js +46 -4
  75. package/dist/esm/client/provider.js +53 -2
  76. package/dist/esm/client/use-checkout.js +9 -5
  77. package/dist/esm/errors.js +1 -1
  78. package/dist/esm/index.js +1 -0
  79. package/dist/esm/next/index.js +2 -2
  80. package/dist/esm/server/client.js +25 -2
  81. package/dist/esm/server/handler.js +196 -18
  82. package/dist/esm/server/http.js +6 -2
  83. package/dist/esm/server/resources/acting-as.js +13 -0
  84. package/dist/esm/server/resources/ai.js +15 -2
  85. package/dist/esm/server/resources/data.js +369 -0
  86. package/dist/esm/server/resources/identity.js +7 -3
  87. package/dist/esm/server/resources/kv.js +80 -0
  88. package/dist/esm/server/resources/platform.js +39 -0
  89. package/dist/esm/server/resources/sessions.js +1 -1
  90. package/dist/esm/server/resources/workspaces.js +70 -0
  91. package/dist/esm/store.js +141 -0
  92. package/dist/esm/ui/empty-state.js +25 -0
  93. package/dist/esm/ui/index.js +34 -0
  94. package/dist/esm/ui/page-header.js +24 -0
  95. package/dist/esm/ui/page-shell.js +41 -0
  96. package/dist/esm/ui/section.js +17 -0
  97. package/dist/esm/ui/setting-row.js +11 -0
  98. package/dist/esm/ui/status-chip.js +14 -0
  99. package/dist/types/client/components/app-shell.d.ts +65 -27
  100. package/dist/types/client/components/apps-grid.d.ts +11 -2
  101. package/dist/types/client/components/confirm-dialog.d.ts +34 -0
  102. package/dist/types/client/components/cursor-pagination.d.ts +27 -0
  103. package/dist/types/client/components/filter-tabs.d.ts +26 -0
  104. package/dist/types/client/components/grievance-notice.d.ts +31 -0
  105. package/dist/types/client/components/link-button.d.ts +32 -9
  106. package/dist/types/client/components/logo.d.ts +18 -2
  107. package/dist/types/client/components/nav.d.ts +24 -0
  108. package/dist/types/client/components/plan-pricing.d.ts +10 -0
  109. package/dist/types/client/components/platform-gate.d.ts +53 -0
  110. package/dist/types/client/components/shell-context.d.ts +116 -0
  111. package/dist/types/client/components/side-nav.d.ts +27 -13
  112. package/dist/types/client/components/sidebar.d.ts +95 -0
  113. package/dist/types/client/components/user-menu.d.ts +68 -22
  114. package/dist/types/client/components/workspace-switcher.d.ts +49 -0
  115. package/dist/types/client/index.d.ts +27 -5
  116. package/dist/types/client/provider.d.ts +109 -6
  117. package/dist/types/client/use-checkout.d.ts +8 -4
  118. package/dist/types/errors.d.ts +1 -1
  119. package/dist/types/index.d.ts +2 -0
  120. package/dist/types/next/index.d.ts +1 -1
  121. package/dist/types/server/client.d.ts +25 -2
  122. package/dist/types/server/http.d.ts +8 -2
  123. package/dist/types/server/index.d.ts +4 -0
  124. package/dist/types/server/resources/acting-as.d.ts +11 -0
  125. package/dist/types/server/resources/ai.d.ts +9 -1
  126. package/dist/types/server/resources/data.d.ts +258 -0
  127. package/dist/types/server/resources/kv.d.ts +63 -0
  128. package/dist/types/server/resources/platform.d.ts +32 -0
  129. package/dist/types/server/resources/workspaces.d.ts +55 -0
  130. package/dist/types/store.d.ts +96 -0
  131. package/dist/types/types.d.ts +319 -15
  132. package/dist/types/ui/empty-state.d.ts +34 -0
  133. package/dist/types/ui/index.d.ts +40 -0
  134. package/dist/types/ui/page-header.d.ts +34 -0
  135. package/dist/types/ui/page-shell.d.ts +39 -0
  136. package/dist/types/ui/section.d.ts +26 -0
  137. package/dist/types/ui/setting-row.d.ts +18 -0
  138. package/dist/types/ui/status-chip.d.ts +36 -0
  139. package/package.json +6 -2
@@ -0,0 +1,127 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectSidebar = ConnectSidebar;
5
+ exports.ConnectSidebarHeader = ConnectSidebarHeader;
6
+ exports.ConnectSidebarBody = ConnectSidebarBody;
7
+ exports.ConnectSidebarFooter = ConnectSidebarFooter;
8
+ exports.ConnectSidebarRow = ConnectSidebarRow;
9
+ const jsx_runtime_1 = require("react/jsx-runtime");
10
+ const react_1 = require("@heroui/react");
11
+ const lucide_react_1 = require("lucide-react");
12
+ const logo_js_1 = require("./logo.js");
13
+ const motion_js_1 = require("./motion.js");
14
+ const shell_context_js_1 = require("./shell-context.js");
15
+ /**
16
+ * The rail's own transition.
17
+ *
18
+ * A tween, not a spring. A spring overshoots, and an overshooting *width*
19
+ * drags every row's clip edge past its resting place and back - which reads as
20
+ * the labels wobbling. 240ms on `--ease-out-quart` is long enough to follow
21
+ * and short enough that hammering the toggle never queues a visible backlog.
22
+ */
23
+ const RAIL_TRANSITION = { duration: 0.24, ease: motion_js_1.EASE_OUT_QUART };
24
+ /** The chrome fades a touch faster than the rail moves, so nothing is caught mid-clip. */
25
+ const CHROME_TRANSITION = { duration: 0.15, ease: motion_js_1.EASE_OUT_QUART };
26
+ /** Label fade timing, shared by the header wordmark and `SidebarLabel`. */
27
+ function labelTransition(collapsed) {
28
+ return collapsed ? { duration: 0.1 } : { duration: 0.18, delay: 0.1 };
29
+ }
30
+ /**
31
+ * The application rail: a fixed-width column that animates between an
32
+ * icon-only 64px and a labelled 264px.
33
+ *
34
+ * **Why width, and nothing else, animates.** Every row inside puts its glyph
35
+ * on the same 32px centre in both states (see `RAIL_COLLAPSED`), and every
36
+ * label is clipped rather than re-measured (see `SIDEBAR_ROW`). So the only
37
+ * property in flight is this element's width: no child moves, no text
38
+ * re-wraps, and there is no per-frame layout pass on the rows. That is why the
39
+ * collapse looks smooth rather than merely fast.
40
+ *
41
+ * Hidden below `md`. The mobile drawer renders the same children with
42
+ * `collapsed: false`, because a 64px icon rail on a phone is a worse drawer,
43
+ * not a smaller one.
44
+ */
45
+ function ConnectSidebar({ children, label = "Sidebar", className }) {
46
+ const { collapsed } = (0, shell_context_js_1.useConnectShell)();
47
+ return (
48
+ // Its own motion root, even though `AppShell` already mounts one.
49
+ // `LazyMotion` nests harmlessly, and without it a host composing the rail
50
+ // from these parts by hand gets a sidebar that jumps between widths
51
+ // instead of animating - a failure that looks like a styling bug and is
52
+ // really a missing provider.
53
+ (0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsx)(motion_js_1.m.aside, { "aria-label": label, className: `hidden shrink-0 flex-col overflow-hidden border-e border-border bg-surface md:flex ${className ?? ""}`,
54
+ // `initial={false}` is what stops the rail sweeping open on first paint
55
+ // for a returning user whose stored preference is "collapsed".
56
+ initial: false, animate: { width: collapsed ? shell_context_js_1.RAIL_COLLAPSED : shell_context_js_1.RAIL_EXPANDED }, transition: RAIL_TRANSITION, children: children }) }));
57
+ }
58
+ /**
59
+ * The brand row, and the rail's own expand/collapse control.
60
+ *
61
+ * **Two states, one DOM.** The badge is mounted once and never re-created; the
62
+ * wordmark fades beside it and the collapse button fades at the trailing edge.
63
+ * Swapping which *element* renders the brand would remount it halfway through
64
+ * the width animation and flicker.
65
+ *
66
+ * Collapsed, hovering or focusing the badge swaps it for the expand glyph, so
67
+ * the rail needs no permanent control eating a row. Expanded, the control sits
68
+ * at the header's trailing edge, where it can be aimed at without covering the
69
+ * brand. The swap is pure CSS on a `group` - hovering re-renders nothing,
70
+ * which is what keeps it instant.
71
+ *
72
+ * The trade this makes, deliberately: **collapsed, the lockup is no longer a
73
+ * link home.** It has become the expand control, and one target that navigates
74
+ * on click but expands on click-after-hover would be worse than either. Put
75
+ * home in the nav; every application already does.
76
+ */
77
+ function ConnectSidebarHeader({ name, icon, homeHref = "/" }) {
78
+ const { collapsed, collapsible, setCollapsed, renderLink } = (0, shell_context_js_1.useConnectShell)();
79
+ const logoExpands = collapsible && collapsed;
80
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "relative flex h-14 shrink-0 items-center overflow-hidden px-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "group/brand relative flex size-10 shrink-0 items-center justify-center", children: [renderLink({
81
+ href: homeHref,
82
+ className: `flex size-10 items-center justify-center rounded-lg transition-opacity focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus ${logoExpands ? "group-hover/brand:opacity-0 group-focus-within/brand:opacity-0" : ""}`,
83
+ // `ConnectLogo` names itself with `role="img"`, which the `<a>` around
84
+ // it does not inherit as its own accessible name.
85
+ "aria-label": `${name} home`,
86
+ children: (0, jsx_runtime_1.jsx)(logo_js_1.ConnectLogo, { name: name, icon: icon, variant: "mark" }),
87
+ }), logoExpands && ((0, jsx_runtime_1.jsxs)(react_1.Tooltip, { delay: 400, children: [(0, jsx_runtime_1.jsx)(react_1.Tooltip.Trigger, { children: (0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": "Expand sidebar", onClick: () => setCollapsed(false), className: "absolute inset-0 flex items-center justify-center rounded-lg text-muted opacity-0 transition-opacity hover:bg-surface-secondary hover:text-foreground focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus group-hover/brand:opacity-100 group-focus-within/brand:opacity-100", children: (0, jsx_runtime_1.jsx)(lucide_react_1.PanelLeftOpen, { className: "size-5" }) }) }), (0, jsx_runtime_1.jsx)(react_1.Tooltip.Content, { placement: "right", children: "Expand sidebar" })] }))] }), (0, jsx_runtime_1.jsx)(motion_js_1.m.span, { className: "ms-2 min-w-0 flex-1 truncate", animate: { opacity: collapsed ? 0 : 1 }, transition: labelTransition(collapsed), "aria-hidden": true, children: (0, jsx_runtime_1.jsx)(logo_js_1.ConnectLogo, { name: name, icon: icon, variant: "wordmark" }) }), collapsible && (
88
+ // Absolutely positioned so it never takes part in the row's layout: in
89
+ // flow, `ms-auto` inside a 40px-wide collapsed row would shove the
90
+ // badge sideways for the length of the animation.
91
+ (0, jsx_runtime_1.jsx)(motion_js_1.m.div, { className: "absolute end-3 top-1/2 -translate-y-1/2", animate: { opacity: collapsed ? 0 : 1 }, transition: CHROME_TRANSITION,
92
+ // A faded-out control must not be reachable by keyboard or pointer.
93
+ // `inert` says that in one attribute; `hidden` would remove it from
94
+ // flow and make the header re-layout on every toggle.
95
+ inert: collapsed, children: (0, jsx_runtime_1.jsxs)(react_1.Tooltip, { delay: 400, children: [(0, jsx_runtime_1.jsx)(react_1.Tooltip.Trigger, { children: (0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": "Collapse sidebar", onClick: () => setCollapsed(true), className: "flex size-8 items-center justify-center rounded-lg text-muted transition-colors hover:bg-surface-secondary hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus", children: (0, jsx_runtime_1.jsx)(lucide_react_1.PanelLeftClose, { className: "size-4" }) }) }), (0, jsx_runtime_1.jsx)(react_1.Tooltip.Content, { placement: "right", children: "Collapse sidebar" })] }) }))] }));
96
+ }
97
+ /**
98
+ * The rail's scrolling region, for the primary navigation. Everything else in
99
+ * the sidebar is `shrink-0`, so this is the only part that ever gives way.
100
+ */
101
+ function ConnectSidebarBody({ children }) {
102
+ return ((0, jsx_runtime_1.jsx)("div", { className: "flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto overflow-x-hidden px-3 py-2", children: children }));
103
+ }
104
+ /**
105
+ * A pinned region at the foot of the rail - the secondary nav, the launcher,
106
+ * the account row.
107
+ *
108
+ * `separated` draws the hairline above it. The account row wants one; a
109
+ * secondary nav sitting directly on top of the account row does not, or the
110
+ * rail ends in two rules a few pixels apart.
111
+ */
112
+ function ConnectSidebarFooter({ children, separated = true, }) {
113
+ return ((0, jsx_runtime_1.jsx)("div", { className: `flex shrink-0 flex-col gap-1 px-3 py-2 ${separated ? "border-t border-border" : ""}`, children: children }));
114
+ }
115
+ /**
116
+ * A pressable row in the rail that is not a nav item - the launcher, the
117
+ * account button, anything a host adds to the footer.
118
+ *
119
+ * Exported so a host's own row lines up with the SDK's. It applies the same
120
+ * 32px glyph centre and the same label fade, which is what stops a partner's
121
+ * "Help" row drifting sideways while every row above it holds still.
122
+ */
123
+ function ConnectSidebarRow({ icon, label, tile = false, trailing, className, "aria-label": ariaLabel, ...props }) {
124
+ const { collapsed } = (0, shell_context_js_1.useConnectShell)();
125
+ const name = ariaLabel ?? (typeof label === "string" ? label : undefined);
126
+ return ((0, jsx_runtime_1.jsxs)(react_1.Tooltip, { delay: 400, isDisabled: !collapsed, children: [(0, jsx_runtime_1.jsx)(react_1.Tooltip.Trigger, { children: (0, jsx_runtime_1.jsxs)("button", { type: "button", "aria-label": name, className: (0, shell_context_js_1.sidebarRowClass)({ tile, className }), ...props, children: [(0, jsx_runtime_1.jsx)("span", { className: `flex ${tile ? "size-8" : "size-5"} shrink-0 items-center justify-center`, children: icon }), (0, jsx_runtime_1.jsx)(shell_context_js_1.SidebarLabel, { children: label }), trailing] }) }), (0, jsx_runtime_1.jsx)(react_1.Tooltip.Content, { placement: "right", children: name ?? label })] }));
127
+ }
@@ -11,7 +11,7 @@ function withGst(price, gstRate) {
11
11
  return price + Math.round(price * (gstRate / 100));
12
12
  }
13
13
  function cadenceOf(plan) {
14
- return plan.billingInterval === "annual" ? "year" : "month";
14
+ return (0, plan_pricing_js_1.intervalLabel)(plan.billingInterval).per;
15
15
  }
16
16
  const HEADING = {
17
17
  subscribe: "Confirm subscription",
@@ -50,7 +50,7 @@ function ConnectSubscribeDialog({ plan, action, current, coupon, onClose, onConf
50
50
  // member and the one most in need of it: the two prices differ by an order
51
51
  // of magnitude, and showing only the new one reads as a price rise.
52
52
  const isChange = action === "upgrade" || action === "downgrade" || isIntervalSwitch;
53
- return ((0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { isOpen: plan !== null, onOpenChange: (open) => !open && onClose(), children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { size: "sm", children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, { onPress: onClose }), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: HEADING[action] }) }), plan && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { className: "flex flex-col gap-4", children: [isChange && currentPlan ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-muted", children: currentPlan.name }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: [(0, plan_pricing_js_1.formatPrice)(currentPlan.price), "/", currentPlan.billingInterval === "annual" ? "yr" : "mo"] })] }), (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowRight, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate font-medium text-foreground", children: plan.name }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: [(0, plan_pricing_js_1.formatPrice)(plan.price), "/", plan.billingInterval === "annual" ? "yr" : "mo"] })] })] })) : ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "font-medium text-foreground", children: plan.name }), plan.description && (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-muted", children: plan.description })] })), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1 border-t border-separator pt-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between text-muted", children: [(0, jsx_runtime_1.jsx)("span", { children: plan.name }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(plan.price) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between text-muted", children: [(0, jsx_runtime_1.jsxs)("span", { children: ["GST (", plan.gstRate, "%)"] }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(withGst(plan.price, plan.gstRate) - plan.price) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between border-t border-separator pt-1 font-semibold text-foreground", children: [(0, jsx_runtime_1.jsxs)("span", { children: ["Per ", cadenceOf(plan)] }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(withGst(plan.price, plan.gstRate)) })] }), plan.billingInterval === "annual" && ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: ["Works out to ", (0, plan_pricing_js_1.formatPrice)((0, plan_pricing_js_1.monthlyOf)(withGst(plan.price, plan.gstRate), "annual")), " a month, billed yearly."] }))] }), plan.trialDays ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-accent", children: [plan.trialDays, "-day free trial, if this workspace has not subscribed to this application before."] })) : null, coupon?.trim() ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: ["Coupon ", (0, jsx_runtime_1.jsx)("span", { className: "font-mono text-foreground", children: coupon.trim() }), " is carried over and validated on Connect."] })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2 rounded-lg bg-surface p-3 text-xs text-muted", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Info, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1", children: [action === "subscribe" && ((0, jsx_runtime_1.jsx)("p", { children: "Payment is completed on Connect, where your card or UPI mandate is set up and the tax invoice is issued. This page updates by itself once it is done." })), action === "upgrade" && ((0, jsx_runtime_1.jsxs)("p", { children: ["Connect prices the difference for the days left in your current cycle and collects just that amount now. From the next cycle you are billed the ", plan.name, " rate."] })), action === "downgrade" && ((0, jsx_runtime_1.jsx)("p", { children: "You keep what you have paid for. Connect works out the unused part of this cycle and applies it as credit or a refund, depending on how this application is configured." })), isIntervalSwitch && (
53
+ return ((0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { isOpen: plan !== null, onOpenChange: (open) => !open && onClose(), children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { size: "sm", children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, { onPress: onClose }), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: HEADING[action] }) }), plan && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { className: "flex flex-col gap-4", children: [isChange && currentPlan ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-muted", children: currentPlan.name }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: [(0, plan_pricing_js_1.formatPrice)(currentPlan.price), "/", (0, plan_pricing_js_1.intervalLabel)(currentPlan.billingInterval).short] })] }), (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowRight, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate font-medium text-foreground", children: plan.name }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: [(0, plan_pricing_js_1.formatPrice)(plan.price), "/", (0, plan_pricing_js_1.intervalLabel)(plan.billingInterval).short] })] })] })) : ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "font-medium text-foreground", children: plan.name }), plan.description && (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-muted", children: plan.description })] })), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1 border-t border-separator pt-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between text-muted", children: [(0, jsx_runtime_1.jsx)("span", { children: plan.name }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(plan.price) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between text-muted", children: [(0, jsx_runtime_1.jsxs)("span", { children: ["GST (", plan.gstRate, "%)"] }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(withGst(plan.price, plan.gstRate) - plan.price) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between border-t border-separator pt-1 font-semibold text-foreground", children: [(0, jsx_runtime_1.jsxs)("span", { children: ["Per ", cadenceOf(plan)] }), (0, jsx_runtime_1.jsx)("span", { children: (0, plan_pricing_js_1.formatPrice)(withGst(plan.price, plan.gstRate)) })] }), plan.billingInterval === "annual" && ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: ["Works out to ", (0, plan_pricing_js_1.formatPrice)((0, plan_pricing_js_1.monthlyOf)(withGst(plan.price, plan.gstRate), "annual")), " a month, billed yearly."] }))] }), plan.trialDays ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-accent", children: [plan.trialDays, "-day free trial, if this workspace has not subscribed to this application before."] })) : null, coupon?.trim() ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: ["Coupon ", (0, jsx_runtime_1.jsx)("span", { className: "font-mono text-foreground", children: coupon.trim() }), " is carried over and validated on Connect."] })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2 rounded-lg bg-surface p-3 text-xs text-muted", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Info, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1", children: [action === "subscribe" && ((0, jsx_runtime_1.jsx)("p", { children: "Payment is completed on Connect, where your card or UPI mandate is set up and the tax invoice is issued. This page updates by itself once it is done." })), action === "upgrade" && ((0, jsx_runtime_1.jsxs)("p", { children: ["Connect prices the difference for the days left in your current cycle and collects just that amount now. From the next cycle you are billed the ", plan.name, " rate."] })), action === "downgrade" && ((0, jsx_runtime_1.jsx)("p", { children: "You keep what you have paid for. Connect works out the unused part of this cycle and applies it as credit or a refund, depending on how this application is configured." })), isIntervalSwitch && (
54
54
  // Not a proration. The cycle you are on ends, so this is
55
55
  // the one action where the amount collected can exceed a
56
56
  // single cycle's list price - or be nothing at all, with
@@ -85,7 +85,7 @@ function ConnectSubscriptionCard({ plansHref, className }) {
85
85
  return (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: `h-48 rounded-xl ${className ?? ""}` });
86
86
  if (!detail || !plan)
87
87
  return (0, jsx_runtime_1.jsx)(ConnectNoSubscriptionCard, { plansHref: plansHref, className: className });
88
- return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsx)(motion_js_1.m.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.2 }, children: (0, jsx_runtime_1.jsxs)(react_1.Card, { className: className, 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: plan.name }), (0, jsx_runtime_1.jsx)(SubscriptionChip, { state: state })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Description, { children: [(0, plan_pricing_js_1.formatPrice)(plan.price), " + ", plan.gstRate, "% GST per", " ", plan.billingInterval === "annual" ? "year" : "month"] })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Content, { className: "flex flex-col gap-2 text-sm", children: [(0, jsx_runtime_1.jsx)(NextEvent, { detail: detail, state: state }), (0, jsx_runtime_1.jsx)(ConnectSeatUsage, { plan: plan, detail: detail })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Footer, { className: "flex flex-wrap gap-2", children: [(0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => window.open(`${appUrl}/billing/subscriptions/${detail.id}`, "_blank", "noopener,noreferrer"), children: ["Manage on Connect", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] }), (0, jsx_runtime_1.jsx)(link_button_js_1.ConnectLinkButton, { href: plansHref ?? DEFAULT_PLANS_HREF, variant: "tertiary", children: "Change plan" })] })] }) }) }));
88
+ return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsx)(motion_js_1.m.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.2 }, children: (0, jsx_runtime_1.jsxs)(react_1.Card, { className: className, 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: plan.name }), (0, jsx_runtime_1.jsx)(SubscriptionChip, { state: state })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Description, { children: [(0, plan_pricing_js_1.formatPrice)(plan.price), " + ", plan.gstRate, "% GST per", " ", (0, plan_pricing_js_1.intervalLabel)(plan.billingInterval).per] })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Content, { className: "flex flex-col gap-2 text-sm", children: [(0, jsx_runtime_1.jsx)(NextEvent, { detail: detail, state: state }), (0, jsx_runtime_1.jsx)(ConnectSeatUsage, { plan: plan, detail: detail })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Footer, { className: "flex flex-wrap gap-2", children: [(0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => window.open(`${appUrl}/billing/subscriptions/${detail.id}`, "_blank", "noopener,noreferrer"), children: ["Manage on Connect", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] }), (0, jsx_runtime_1.jsx)(link_button_js_1.ConnectLinkButton, { href: plansHref ?? DEFAULT_PLANS_HREF, variant: "tertiary", children: "Change plan" })] })] }) }) }));
89
89
  }
90
90
  function SubscriptionChip({ state }) {
91
91
  switch (state) {
@@ -1,6 +1,8 @@
1
1
  "use client";
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectProfileModal = ConnectProfileModal;
5
+ exports.ConnectUserButton = ConnectUserButton;
4
6
  exports.ConnectUserMenu = ConnectUserMenu;
5
7
  exports.ConnectManageSubscriptionButton = ConnectManageSubscriptionButton;
6
8
  exports.ConnectInvoicesButton = ConnectInvoicesButton;
@@ -13,6 +15,8 @@ const lucide_react_1 = require("lucide-react");
13
15
  const provider_js_1 = require("../provider.js");
14
16
  const mark_js_1 = require("../mark.js");
15
17
  const account_sessions_js_1 = require("./account-sessions.js");
18
+ const motion_js_1 = require("./motion.js");
19
+ const shell_context_js_1 = require("./shell-context.js");
16
20
  const surfaces_js_1 = require("./surfaces.js");
17
21
  const THEME_OPTIONS = [
18
22
  { value: "light", label: "Light", icon: lucide_react_1.Sun },
@@ -61,7 +65,15 @@ function profileUrl(appUrl, clientId) {
61
65
  * had set on their profile.
62
66
  */
63
67
  function UserAvatar({ user, size, }) {
64
- return ((0, jsx_runtime_1.jsxs)(react_1.Avatar, { size: size, color: "accent", variant: "soft", children: [user.photoUrl && (0, jsx_runtime_1.jsx)(react_1.Avatar.Image, { alt: "", src: user.photoUrl }), (0, jsx_runtime_1.jsx)(react_1.Avatar.Fallback, { children: initials(user.displayName, user.email) })] }));
68
+ return (
69
+ // `rounded-full` is an override, not decoration. HeroUI's `.avatar` is a
70
+ // squircle - `rounded-2xl` at `sm`, `rounded-3xl` at `lg` - which is right
71
+ // for the media component in general and wrong for a *person*: every other
72
+ // product a customer uses draws an account avatar as a circle, and a
73
+ // squircle sitting beside circular status dots reads as a rendering fault
74
+ // rather than a choice. The root is `overflow-hidden`, so rounding it here
75
+ // rounds the image and the fallback with it.
76
+ (0, jsx_runtime_1.jsxs)(react_1.Avatar, { size: size, color: "accent", variant: "soft", className: "rounded-full", children: [user.photoUrl && (0, jsx_runtime_1.jsx)(react_1.Avatar.Image, { alt: "", src: user.photoUrl }), (0, jsx_runtime_1.jsx)(react_1.Avatar.Fallback, { children: initials(user.displayName, user.email) })] }));
65
77
  }
66
78
  /** `dispatch_admin` / `billing-viewer` -> `Dispatch admin`. */
67
79
  function humanizeRole(role) {
@@ -116,55 +128,102 @@ function DestinationTile({ icon, label, url, }) {
116
128
  return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", "aria-label": `${label} (opens in a new tab)`, className: `relative h-auto w-full flex-col items-center justify-center gap-2 rounded-xl border border-border ${surfaces_js_1.PANEL_FILL} p-3 hover:border-accent hover:bg-surface-secondary`, onPress: () => openExternal(url), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "absolute end-2 top-2 size-3.5 text-muted", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { className: `flex ${surfaces_js_1.MEDALLION} items-center justify-center rounded-xl bg-surface text-muted`, children: icon }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs font-medium text-foreground", children: label })] }));
117
129
  }
118
130
  /**
119
- * Avatar and name in the trigger; an identity card, the three Connect
120
- * destinations (Profile, Billing, Connect), an optional appearance control,
121
- * this account's active sessions, and sign-out in the modal it opens.
131
+ * The account modal's contents - identity card, the three Connect
132
+ * destinations, an optional appearance control, this account's sessions, and
133
+ * sign-out.
134
+ *
135
+ * Extracted from the trigger it used to be welded to, so the same surface can
136
+ * be opened from the sidebar's account row (`ConnectUserButton`) *or* from a
137
+ * host's own control (`ConnectProfileModal`) without a second copy of it. It
138
+ * renders the `Modal.Backdrop` downwards only; whoever renders it supplies the
139
+ * `<Modal>` and decides how it opens.
122
140
  *
123
141
  * Profile/Billing/Connect open in a **new tab**, each marked with an
124
- * external-link icon. They are a different application at a different
125
- * origin, and navigating the host app away to reach them would lose
126
- * whatever the user was doing here. Sign out is the only action that acts
127
- * in place, which is why it sits alone in the footer rather than as a fourth
128
- * item in the same list as three navigations.
142
+ * external-link icon. They are a different application at a different origin,
143
+ * and navigating the host app away to reach them would lose whatever the user
144
+ * was doing here. Sign out is the only action that acts in place, which is why
145
+ * it sits alone in the footer rather than as a fourth item in the same list as
146
+ * three navigations.
147
+ */
148
+ function ProfileSurface({ onSignOut, theme, onThemeChange, }) {
149
+ const { session, signOut, appUrl, clientId } = (0, provider_js_1.useConnect)();
150
+ const user = session?.user;
151
+ if (!user)
152
+ return null;
153
+ const label = user.displayName ?? user.email ?? "Account";
154
+ const role = session?.role ?? null;
155
+ const planChip = subscriptionChip(session?.subscription ?? null);
156
+ 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) => {
157
+ const [next] = [...keys];
158
+ if (typeof next === "string")
159
+ onThemeChange(next);
160
+ }, children: THEME_OPTIONS.map(({ value, label: optionLabel, icon: Icon }, index) => ((0, jsx_runtime_1.jsxs)(react_1.ToggleButton, { id: value, children: [index > 0 && (0, jsx_runtime_1.jsx)(react_1.ToggleButtonGroup.Separator, {}), (0, jsx_runtime_1.jsx)(Icon, { className: "size-4", "aria-hidden": "true" }), optionLabel] }, value))) })] })), (0, jsx_runtime_1.jsx)(account_sessions_js_1.ConnectAccountSessions, {})] }), (0, jsx_runtime_1.jsx)(react_1.Modal.Footer, { children: (0, jsx_runtime_1.jsxs)(react_1.Button, { slot: "close", fullWidth: true, variant: "tertiary", className: "text-danger", onPress: () => {
161
+ void signOut().then(onSignOut);
162
+ }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.LogOut, { className: "size-4 shrink-0" }), "Sign out"] }) })] }) }) }));
163
+ }
164
+ /**
165
+ * The account modal with **no trigger of its own**, driven by the host.
166
+ *
167
+ * For an application that already has an account control it wants to keep -
168
+ * an avatar in its own chrome, a "Manage account" row in its own settings
169
+ * page - and wants Connect's modal behind it rather than rebuilding identity,
170
+ * sessions and sign-out. `ConnectUserButton` is the same modal with the
171
+ * trigger supplied; reach for this one only when that trigger is in the way.
129
172
  *
130
- * The session list is account-wide, not scoped to this application - see
131
- * `ConnectUserSession`'s doc comment in `types.ts`. Appearance only renders
132
- * when both `theme` and `onThemeChange` are supplied.
173
+ * Renders nothing when signed out, open or not - there is no account to show.
174
+ */
175
+ function ConnectProfileModal({ isOpen, onOpenChange, onSignOut, theme, onThemeChange, }) {
176
+ const { session } = (0, provider_js_1.useConnect)();
177
+ if (!session?.user)
178
+ return null;
179
+ return ((0, jsx_runtime_1.jsx)(react_1.Modal, { isOpen: isOpen, onOpenChange: onOpenChange, children: (0, jsx_runtime_1.jsx)(ProfileSurface, { onSignOut: onSignOut, theme: theme, onThemeChange: onThemeChange }) }));
180
+ }
181
+ /**
182
+ * The signed-in account, as a control that opens the account modal.
133
183
  *
134
- * Renders nothing when signed out - pair it with `<ConnectLoginButton>`, which
135
- * renders nothing when signed *in*, and the two swap cleanly with no branch in
136
- * your own layout.
184
+ * In `"row"` form this is the last thing in the sidebar, which is where an
185
+ * account control belongs: it is the one item in the rail that is about *you*
186
+ * rather than about the product, and the foot of a column is where every
187
+ * application a customer already uses puts that. It collapses with the rail -
188
+ * the avatar holds its position and the name fades - because the avatar alone
189
+ * is still a complete account control, which is exactly what an icon rail
190
+ * needs.
191
+ *
192
+ * Renders nothing when signed out. Pair it with `<ConnectLoginButton>`, which
193
+ * renders nothing when signed *in*, and the two swap with no branch in your
194
+ * own layout.
137
195
  *
138
196
  * On the HeroUI shape: the trigger is a plain `<Button>` as a **direct child**
139
- * of `<Modal>`, not wrapped in `<Modal.Trigger>`. `Dropdown.Trigger` (and by
140
- * the same construction, `Modal.Trigger`) is itself a React Aria `Button`, so
141
- * wrapping one produces a button inside a button - invalid markup, and press
142
- * handling stops working. Kept uncontrolled (no `isOpen`/`onOpenChange`),
143
- * matching `<ConnectAppsGrid>`'s own `<Modal>` - nothing here needs to close
144
- * the modal programmatically; sign-out uses `slot="close"` instead.
197
+ * of `<Modal>`, not wrapped in `<Modal.Trigger>`. `Dropdown.Trigger` - and by
198
+ * the same construction `Modal.Trigger` - is itself a React Aria `Button`, so
199
+ * wrapping one produces a button inside a button: invalid markup, and press
200
+ * handling stops working.
145
201
  */
146
- function ConnectUserMenu({ compact = false, onSignOut, className, theme, onThemeChange, }) {
147
- const { session, isLoading, signOut, appUrl, clientId } = (0, provider_js_1.useConnect)();
202
+ function ConnectUserButton({ variant = "row", onSignOut, className, theme, onThemeChange, }) {
203
+ const { session, isLoading } = (0, provider_js_1.useConnect)();
204
+ const { collapsed } = (0, shell_context_js_1.useConnectShell)();
148
205
  if (isLoading) {
149
- return ((0, jsx_runtime_1.jsxs)("div", { className: `flex items-center gap-2 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "size-8 shrink-0 rounded-full" }), !compact && (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "h-4 w-24" })] }));
206
+ return ((0, jsx_runtime_1.jsxs)("div", { className: `flex h-10 items-center gap-3 ${variant === "row" ? "ps-[4px]" : ""} ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "size-8 shrink-0 rounded-full" }), variant !== "compact" && !collapsed && (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "h-4 w-24" })] }));
150
207
  }
151
208
  const user = session?.user;
152
209
  if (!user)
153
210
  return null;
154
211
  const label = user.displayName ?? user.email ?? "Account";
155
- const role = session?.applicationRole?.role;
156
- const planChip = subscriptionChip(session?.subscription ?? null);
157
- return ((0, jsx_runtime_1.jsxs)(react_1.Modal, { children: [(0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", "aria-label": `Account: ${label}`,
158
- // `pl-1` against the default right padding is optical, not sloppy: a
159
- // circular avatar needs less inset than a text label to look evenly
160
- // seated in the button.
161
- className: `gap-2 pl-1 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "sm" }), !compact && (0, jsx_runtime_1.jsx)("span", { className: "text-sm font-medium text-foreground", children: label })] }), (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) => {
162
- const [next] = [...keys];
163
- if (typeof next === "string")
164
- onThemeChange(next);
165
- }, children: THEME_OPTIONS.map(({ value, label: optionLabel, icon: Icon }, index) => ((0, jsx_runtime_1.jsxs)(react_1.ToggleButton, { id: value, children: [index > 0 && (0, jsx_runtime_1.jsx)(react_1.ToggleButtonGroup.Separator, {}), (0, jsx_runtime_1.jsx)(Icon, { className: "size-4", "aria-hidden": "true" }), optionLabel] }, value))) })] })), (0, jsx_runtime_1.jsx)(account_sessions_js_1.ConnectAccountSessions, {})] }), (0, jsx_runtime_1.jsx)(react_1.Modal.Footer, { children: (0, jsx_runtime_1.jsxs)(react_1.Button, { slot: "close", fullWidth: true, variant: "tertiary", className: "text-danger", onPress: () => {
166
- void signOut().then(onSignOut);
167
- }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.LogOut, { className: "size-4 shrink-0" }), "Sign out"] }) })] }) }) })] }));
212
+ return ((0, jsx_runtime_1.jsxs)(react_1.Modal, { children: [variant === "row" ? ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", "aria-label": `Account: ${label}`, className: (0, shell_context_js_1.sidebarRowClass)({ tile: true, className }), children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "sm" }), (0, jsx_runtime_1.jsx)(shell_context_js_1.SidebarLabel, { children: (0, jsx_runtime_1.jsxs)("span", { className: "flex min-w-0 flex-col text-start leading-tight", children: [(0, jsx_runtime_1.jsx)("span", { className: "truncate text-sm font-medium text-foreground", children: label }), user.email && user.email !== label && ((0, jsx_runtime_1.jsx)("span", { className: "truncate text-xs font-normal text-muted", children: user.email }))] }) }), (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_1.Button, { variant: "tertiary", "aria-label": `Account: ${label}`,
213
+ // `ps-1` against the default trailing padding is optical, not
214
+ // sloppy: a circular avatar needs less inset than a text label to
215
+ // look evenly seated in the button.
216
+ className: `gap-2 ps-1 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "sm" }), variant === "inline" && (0, jsx_runtime_1.jsx)("span", { className: "text-sm font-medium text-foreground", children: label })] })), (0, jsx_runtime_1.jsx)(ProfileSurface, { onSignOut: onSignOut, theme: theme, onThemeChange: onThemeChange })] }));
217
+ }
218
+ /**
219
+ * @deprecated Renamed to `ConnectUserButton` in 0.12 - it is one button that
220
+ * opens a modal, and calling it a "menu" had people looking for a dropdown
221
+ * that never existed. `compact` maps to `variant="compact"`, and the old
222
+ * default (avatar plus name) to `variant="inline"`. This alias will be removed
223
+ * in a later minor.
224
+ */
225
+ function ConnectUserMenu({ compact = false, ...rest }) {
226
+ return (0, jsx_runtime_1.jsx)(ConnectUserButton, { variant: compact ? "compact" : "inline", ...rest });
168
227
  }
169
228
  /**
170
229
  * The "Manage subscription" action on its own, for layouts that want it
@@ -0,0 +1,100 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectWorkspaceSwitcher = ConnectWorkspaceSwitcher;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("react");
7
+ const react_2 = require("@heroui/react");
8
+ const lucide_react_1 = require("lucide-react");
9
+ const provider_js_1 = require("../provider.js");
10
+ const motion_js_1 = require("./motion.js");
11
+ const shell_context_js_1 = require("./shell-context.js");
12
+ /** `Acme Logistics` -> `AL`. The collapsed rail's whole label for a workspace. */
13
+ function workspaceInitials(name) {
14
+ const parts = name.trim().split(/\s+/).filter(Boolean);
15
+ return (parts.slice(0, 2).map((part) => part[0]).join("") || "?").toUpperCase();
16
+ }
17
+ /**
18
+ * Switches which workspace this application is looking at.
19
+ *
20
+ * **Rendered in the `AppShell` sidebar header by default**, directly
21
+ * under the brand - which workspace you are in scopes everything below it, so
22
+ * it reads as a heading for the nav rather than as one more destination.
23
+ *
24
+ * 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.
30
+ *
31
+ * **Authorization is the data, not a prop.** The list comes from the caller's
32
+ * own session (`GET /workspaces` takes no user id), so a workspace the user
33
+ * may not reach is simply not in it. There is deliberately no `roles` prop to
34
+ * pass or forget - the only way to render a row is for Connect to have
35
+ * returned it.
36
+ *
37
+ * Rows the user *belongs to* but cannot use here - a workspace outside the
38
+ * application's audience, or one with no subscription - are shown **disabled
39
+ * with the reason** rather than hidden. Hiding them reads as data loss to the
40
+ * person who owns the workspace; `switch` refuses the same two cases, so the
41
+ * label and the outcome always agree.
42
+ *
43
+ * A switch rotates the session token. The bridge persists the new one in the
44
+ * host application's own session store and the token never reaches this
45
+ * component - so the only thing to do afterwards is reload, which is what
46
+ * `onSwitched` defaults to.
47
+ */
48
+ function ConnectWorkspaceSwitcher({ onSwitched, row = true, className, }) {
49
+ const { mutateResource } = (0, provider_js_1.useConnect)();
50
+ const { collapsed } = (0, shell_context_js_1.useConnectShell)();
51
+ const user = (0, provider_js_1.useConnectUser)();
52
+ const { data, isLoading } = (0, provider_js_1.useConnectWorkspaces)();
53
+ const [switching, setSwitching] = (0, react_1.useState)(null);
54
+ const [error, setError] = (0, react_1.useState)(null);
55
+ 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)
61
+ return null;
62
+ const active = workspaces.find((workspace) => workspace.isActive);
63
+ const label = active?.name ?? user?.workspaceName ?? "Workspace";
64
+ const handleSwitch = async (workspace) => {
65
+ if (workspace.isActive || !selectable(workspace))
66
+ return;
67
+ setSwitching(workspace.workspaceId);
68
+ setError(null);
69
+ try {
70
+ // Through the provider's own bridge caller rather than a bare `fetch`,
71
+ // so this inherits the configured `basePath`, the credentials mode and
72
+ // the error shape every other call in the SDK uses.
73
+ await mutateResource("/workspaces/switch", { workspaceId: workspace.workspaceId });
74
+ if (onSwitched) {
75
+ onSwitched(workspace);
76
+ }
77
+ else {
78
+ window.location.reload();
79
+ }
80
+ }
81
+ catch (cause) {
82
+ setError(cause instanceof Error ? cause.message : "Could not switch workspace");
83
+ setSwitching(null);
84
+ }
85
+ };
86
+ return ((0, jsx_runtime_1.jsxs)("div", { className: row ? "" : className, children: [(0, jsx_runtime_1.jsxs)(react_2.Dropdown, { children: [row ? (
87
+ // A 32px tile rather than a 20px glyph, so the initials land on the
88
+ // same optical centre the avatar below them uses - see
89
+ // `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] }));
91
+ }
92
+ /** Belongs to the user, and usable here. The same two facts `switch` enforces. */
93
+ function selectable(workspace) {
94
+ return workspace.inAudience && workspace.subscribed;
95
+ }
96
+ function reasonUnavailable(workspace) {
97
+ if (!workspace.inAudience)
98
+ return "Not available for this kind of workspace";
99
+ return "No active subscription";
100
+ }