@cosmoops/connect-sdk 0.9.0 → 0.16.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 (155) hide show
  1. package/CHANGELOG.md +566 -0
  2. package/README.md +113 -42
  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/ai-screens.js +73 -0
  7. package/dist/cjs/client/components/ai-widgets.js +37 -0
  8. package/dist/cjs/client/components/app-shell.js +75 -41
  9. package/dist/cjs/client/components/apps-grid.js +3 -2
  10. package/dist/cjs/client/components/ask-ai.js +50 -5
  11. package/dist/cjs/client/components/confirm-dialog.js +40 -0
  12. package/dist/cjs/client/components/cursor-pagination.js +22 -0
  13. package/dist/cjs/client/components/filter-tabs.js +24 -0
  14. package/dist/cjs/client/components/grievance-notice.js +30 -0
  15. package/dist/cjs/client/components/link-button.js +17 -10
  16. package/dist/cjs/client/components/logo.js +4 -3
  17. package/dist/cjs/client/components/nav.js +41 -0
  18. package/dist/cjs/client/components/plan-pricing.js +27 -0
  19. package/dist/cjs/client/components/plans.js +1 -1
  20. package/dist/cjs/client/components/platform-gate.js +48 -0
  21. package/dist/cjs/client/components/shell-context.js +149 -0
  22. package/dist/cjs/client/components/side-nav.js +49 -23
  23. package/dist/cjs/client/components/sidebar.js +127 -0
  24. package/dist/cjs/client/components/subscribe-dialog.js +2 -2
  25. package/dist/cjs/client/components/subscription.js +1 -1
  26. package/dist/cjs/client/components/user-menu.js +96 -37
  27. package/dist/cjs/client/components/workspace-switcher.js +100 -0
  28. package/dist/cjs/client/index.js +72 -2
  29. package/dist/cjs/client/provider.js +57 -2
  30. package/dist/cjs/client/use-checkout.js +9 -5
  31. package/dist/cjs/errors.js +1 -1
  32. package/dist/cjs/index.js +16 -1
  33. package/dist/cjs/mcp/index.js +90 -0
  34. package/dist/cjs/mcp/tools.js +369 -0
  35. package/dist/cjs/next/index.js +2 -2
  36. package/dist/cjs/server/client.js +25 -2
  37. package/dist/cjs/server/handler.js +203 -18
  38. package/dist/cjs/server/http.js +6 -2
  39. package/dist/cjs/server/resources/acting-as.js +16 -0
  40. package/dist/cjs/server/resources/ai.js +22 -2
  41. package/dist/cjs/server/resources/data.js +374 -0
  42. package/dist/cjs/server/resources/identity.js +7 -3
  43. package/dist/cjs/server/resources/kv.js +84 -0
  44. package/dist/cjs/server/resources/platform.js +43 -0
  45. package/dist/cjs/server/resources/sessions.js +1 -1
  46. package/dist/cjs/server/resources/workspaces.js +74 -0
  47. package/dist/cjs/store-schema.js +66 -0
  48. package/dist/cjs/store.js +152 -0
  49. package/dist/cjs/ui/empty-state.js +28 -0
  50. package/dist/cjs/ui/index.js +44 -0
  51. package/dist/cjs/ui/page-header.js +27 -0
  52. package/dist/cjs/ui/page-shell.js +44 -0
  53. package/dist/cjs/ui/section.js +20 -0
  54. package/dist/cjs/ui/setting-row.js +14 -0
  55. package/dist/cjs/ui/status-chip.js +17 -0
  56. package/dist/esm/cli/doctor.js +3 -3
  57. package/dist/esm/cli/init.js +3 -3
  58. package/dist/esm/client/components/access-gate.js +20 -1
  59. package/dist/esm/client/components/ai-screens.js +69 -0
  60. package/dist/esm/client/components/ai-widgets.js +37 -1
  61. package/dist/esm/client/components/app-shell.js +81 -47
  62. package/dist/esm/client/components/apps-grid.js +3 -2
  63. package/dist/esm/client/components/ask-ai.js +51 -6
  64. package/dist/esm/client/components/confirm-dialog.js +37 -0
  65. package/dist/esm/client/components/cursor-pagination.js +19 -0
  66. package/dist/esm/client/components/filter-tabs.js +21 -0
  67. package/dist/esm/client/components/grievance-notice.js +27 -0
  68. package/dist/esm/client/components/link-button.js +17 -10
  69. package/dist/esm/client/components/logo.js +4 -3
  70. package/dist/esm/client/components/nav.js +40 -0
  71. package/dist/esm/client/components/plan-pricing.js +25 -0
  72. package/dist/esm/client/components/plans.js +2 -2
  73. package/dist/esm/client/components/platform-gate.js +45 -0
  74. package/dist/esm/client/components/shell-context.js +141 -0
  75. package/dist/esm/client/components/side-nav.js +48 -21
  76. package/dist/esm/client/components/sidebar.js +120 -0
  77. package/dist/esm/client/components/subscribe-dialog.js +3 -3
  78. package/dist/esm/client/components/subscription.js +2 -2
  79. package/dist/esm/client/components/user-menu.js +95 -38
  80. package/dist/esm/client/components/workspace-switcher.js +97 -0
  81. package/dist/esm/client/index.js +47 -4
  82. package/dist/esm/client/provider.js +53 -2
  83. package/dist/esm/client/use-checkout.js +9 -5
  84. package/dist/esm/errors.js +1 -1
  85. package/dist/esm/index.js +4 -0
  86. package/dist/esm/mcp/index.js +85 -0
  87. package/dist/esm/mcp/tools.js +366 -0
  88. package/dist/esm/next/index.js +2 -2
  89. package/dist/esm/server/client.js +25 -2
  90. package/dist/esm/server/handler.js +203 -18
  91. package/dist/esm/server/http.js +6 -2
  92. package/dist/esm/server/resources/acting-as.js +13 -0
  93. package/dist/esm/server/resources/ai.js +22 -2
  94. package/dist/esm/server/resources/data.js +369 -0
  95. package/dist/esm/server/resources/identity.js +7 -3
  96. package/dist/esm/server/resources/kv.js +80 -0
  97. package/dist/esm/server/resources/platform.js +39 -0
  98. package/dist/esm/server/resources/sessions.js +1 -1
  99. package/dist/esm/server/resources/workspaces.js +70 -0
  100. package/dist/esm/store-schema.js +62 -0
  101. package/dist/esm/store.js +141 -0
  102. package/dist/esm/ui/empty-state.js +25 -0
  103. package/dist/esm/ui/index.js +34 -0
  104. package/dist/esm/ui/page-header.js +24 -0
  105. package/dist/esm/ui/page-shell.js +41 -0
  106. package/dist/esm/ui/section.js +17 -0
  107. package/dist/esm/ui/setting-row.js +11 -0
  108. package/dist/esm/ui/status-chip.js +14 -0
  109. package/dist/types/client/components/ai-screens.d.ts +25 -0
  110. package/dist/types/client/components/ai-widgets.d.ts +34 -1
  111. package/dist/types/client/components/app-shell.d.ts +65 -27
  112. package/dist/types/client/components/apps-grid.d.ts +11 -2
  113. package/dist/types/client/components/ask-ai.d.ts +47 -1
  114. package/dist/types/client/components/confirm-dialog.d.ts +34 -0
  115. package/dist/types/client/components/cursor-pagination.d.ts +27 -0
  116. package/dist/types/client/components/filter-tabs.d.ts +26 -0
  117. package/dist/types/client/components/grievance-notice.d.ts +31 -0
  118. package/dist/types/client/components/link-button.d.ts +32 -9
  119. package/dist/types/client/components/logo.d.ts +18 -2
  120. package/dist/types/client/components/nav.d.ts +24 -0
  121. package/dist/types/client/components/plan-pricing.d.ts +10 -0
  122. package/dist/types/client/components/platform-gate.d.ts +53 -0
  123. package/dist/types/client/components/shell-context.d.ts +116 -0
  124. package/dist/types/client/components/side-nav.d.ts +27 -13
  125. package/dist/types/client/components/sidebar.d.ts +95 -0
  126. package/dist/types/client/components/user-menu.d.ts +68 -22
  127. package/dist/types/client/components/workspace-switcher.d.ts +49 -0
  128. package/dist/types/client/index.d.ts +29 -5
  129. package/dist/types/client/provider.d.ts +109 -6
  130. package/dist/types/client/use-checkout.d.ts +8 -4
  131. package/dist/types/errors.d.ts +1 -1
  132. package/dist/types/index.d.ts +6 -0
  133. package/dist/types/mcp/index.d.ts +74 -0
  134. package/dist/types/mcp/tools.d.ts +107 -0
  135. package/dist/types/next/index.d.ts +1 -1
  136. package/dist/types/server/client.d.ts +25 -2
  137. package/dist/types/server/http.d.ts +8 -2
  138. package/dist/types/server/index.d.ts +4 -0
  139. package/dist/types/server/resources/acting-as.d.ts +11 -0
  140. package/dist/types/server/resources/ai.d.ts +20 -2
  141. package/dist/types/server/resources/data.d.ts +258 -0
  142. package/dist/types/server/resources/kv.d.ts +63 -0
  143. package/dist/types/server/resources/platform.d.ts +32 -0
  144. package/dist/types/server/resources/workspaces.d.ts +55 -0
  145. package/dist/types/store-schema.d.ts +52 -0
  146. package/dist/types/store.d.ts +96 -0
  147. package/dist/types/types.d.ts +369 -16
  148. package/dist/types/ui/empty-state.d.ts +34 -0
  149. package/dist/types/ui/index.d.ts +40 -0
  150. package/dist/types/ui/page-header.d.ts +34 -0
  151. package/dist/types/ui/page-shell.d.ts +39 -0
  152. package/dist/types/ui/section.d.ts +26 -0
  153. package/dist/types/ui/setting-row.d.ts +18 -0
  154. package/dist/types/ui/status-chip.d.ts +36 -0
  155. package/package.json +15 -2
@@ -20,6 +20,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.ACTION_LABEL = void 0;
21
21
  exports.formatPrice = formatPrice;
22
22
  exports.monthlyOf = monthlyOf;
23
+ exports.intervalLabel = intervalLabel;
24
+ exports.pricePerLabel = pricePerLabel;
23
25
  exports.discountLabel = discountLabel;
24
26
  exports.resolvePlanGroup = resolvePlanGroup;
25
27
  exports.groupPlansByTier = groupPlansByTier;
@@ -41,6 +43,31 @@ function formatPrice(paise) {
41
43
  function monthlyOf(price, billingInterval) {
42
44
  return billingInterval === "annual" ? price / 12 : price;
43
45
  }
46
+ /**
47
+ * How to name a plan's cadence, in the three forms the plan screens need.
48
+ *
49
+ * One table rather than a `=== "annual" ? "year" : "month"` at each call site.
50
+ * That ternary was correct while there were two intervals and quietly became
51
+ * wrong when `"one-time"` arrived: a plan paid for once was labelled
52
+ * "per month", which is not a rendering imperfection but a false price.
53
+ */
54
+ const INTERVAL_LABELS = {
55
+ monthly: { per: "month", short: "mo", adverb: "monthly" },
56
+ annual: { per: "year", short: "yr", adverb: "annually" },
57
+ // "once" reads correctly in all three positions a cadence appears in -
58
+ // "₹4,999 once", "Total once", "Only sold once".
59
+ "one-time": { per: "once", short: "once", adverb: "once" },
60
+ };
61
+ function intervalLabel(billingInterval) {
62
+ return INTERVAL_LABELS[billingInterval];
63
+ }
64
+ /**
65
+ * `/month`, `/year` - and nothing at all for a one-time plan, where a price
66
+ * with no slash is the honest rendering.
67
+ */
68
+ function pricePerLabel(billingInterval) {
69
+ return billingInterval === "one-time" ? "" : `/${INTERVAL_LABELS[billingInterval].per}`;
70
+ }
44
71
  function discountLabel(discount) {
45
72
  const amount = discount.discountType === "percentage" ? `${discount.value}% off` : `${formatPrice(discount.value)} off`;
46
73
  if (discount.duration === "forever")
@@ -126,7 +126,7 @@ function ConnectPlans({ planIds, hideCoupon = false, className }) {
126
126
  (tier.monthly?.id === subscription.plan.id || tier.annual?.id === subscription.plan.id);
127
127
  const isCurrent = action === "current";
128
128
  const cadenceUnavailable = showAnnual ? tier.annual === null : tier.monthly === null;
129
- 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.jsxs)(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, 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.jsxs)("span", { className: "text-sm text-muted", children: ["/", plan.billingInterval === "annual" ? "year" : "month"] })] }, plan.billingInterval) }), (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 ", plan.billingInterval === "annual" ? "annually" : "monthly", "."] }))] }), plan.trialDays ? ((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) => (
129
+ 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.jsxs)(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, 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, 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, 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) => (
130
130
  // `name`, not `key` - the key is an identifier
131
131
  // ("campaign-module"), and printing it here is what this
132
132
  // card used to do. The description rides along as a
@@ -0,0 +1,48 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectPlatformGate = ConnectPlatformGate;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const provider_js_1 = require("../provider.js");
7
+ /**
8
+ * Renders `children` only for a Connect platform staff session.
9
+ *
10
+ * ```tsx
11
+ * <ConnectPlatformGate>
12
+ * <LinkMasterConsole />
13
+ * </ConnectPlatformGate>
14
+ * ```
15
+ *
16
+ * Staff are Connect's own operators - `platform_admin`, `platform_manager`,
17
+ * `developer` - signing into *your* application. They belong to no workspace,
18
+ * so a staff session carries no `workspaceId`, no subscription and no member
19
+ * role; see `SessionAccess.staff`. Staff access is off by default and turned on
20
+ * per application by a Connect platform admin, so this gate renders nothing
21
+ * until that happens.
22
+ *
23
+ * **This is a UX gate, not a security boundary** - the same caveat every gate
24
+ * in this package carries, and it matters more here than anywhere else,
25
+ * because what is behind this one is usually an operator console. The browser
26
+ * can ignore this component entirely. Every route it reveals must re-check
27
+ * `staff` on your own server:
28
+ *
29
+ * ```ts
30
+ * const { staff, role } = await client.identity.getUser(sessionToken);
31
+ * if (!staff) return forbidden();
32
+ * ```
33
+ *
34
+ * Renders `loading` - not `fallback` - while the session is unknown, so an
35
+ * operator never flickers through the customer layout on the way in.
36
+ */
37
+ function ConnectPlatformGate({ roles, children, fallback = null, loading = null, }) {
38
+ const { isStaff, roles: held, isLoading } = (0, provider_js_1.useConnectPlatformAccess)();
39
+ if (isLoading)
40
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: loading });
41
+ if (!isStaff)
42
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: fallback });
43
+ // An empty `roles` array is treated as "no constraint", same as omitting it.
44
+ // The alternative - refusing everyone - would turn a `roles={[]}` computed
45
+ // from an empty config into a silently dead console.
46
+ const accepted = !roles || roles.length === 0 || roles.some((role) => held.includes(role));
47
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: accepted ? children : fallback });
48
+ }
@@ -0,0 +1,149 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.SIDEBAR_ROW_TILE = exports.SIDEBAR_ROW_ICON = exports.SIDEBAR_ROW = exports.RAIL_GUTTER = exports.RAIL_EXPANDED = exports.RAIL_COLLAPSED = exports.defaultRenderLink = void 0;
5
+ exports.sidebarRowClass = sidebarRowClass;
6
+ exports.ConnectShellProvider = ConnectShellProvider;
7
+ exports.useConnectShell = useConnectShell;
8
+ exports.SidebarLabel = SidebarLabel;
9
+ const jsx_runtime_1 = require("react/jsx-runtime");
10
+ const react_1 = require("react");
11
+ const motion_js_1 = require("./motion.js");
12
+ /** A plain anchor - correct anywhere, and a full page load. See `ConnectLinkRenderer`. */
13
+ const defaultRenderLink = ({ children, ...props }) => (0, jsx_runtime_1.jsx)("a", { ...props, children: children });
14
+ exports.defaultRenderLink = defaultRenderLink;
15
+ /**
16
+ * The rail's two widths, in pixels, and the one place they are written down.
17
+ *
18
+ * They are numbers rather than Tailwind classes because the rail animates
19
+ * between them - `motion` interpolates a number and cannot interpolate
20
+ * `w-16` into `w-64`.
21
+ *
22
+ * **The pairing is load-bearing, not arbitrary.** Every row in the sidebar is
23
+ * laid out so its glyph sits exactly `RAIL_GUTTER + ROW_GUTTER + 10px` from
24
+ * the rail's left edge - 32px, which is also `RAIL_COLLAPSED / 2`. So the
25
+ * collapsed rail centres every icon *without a single centring rule*, and
26
+ * nothing moves horizontally while the width animates. Change either width
27
+ * and that identity breaks: the icons drift sideways during the transition,
28
+ * which is the exact stutter this geometry exists to avoid.
29
+ */
30
+ exports.RAIL_COLLAPSED = 64;
31
+ exports.RAIL_EXPANDED = 264;
32
+ /** Horizontal padding inside the rail, on every region. Tailwind `px-3`. */
33
+ exports.RAIL_GUTTER = 12;
34
+ /**
35
+ * The row shape every sidebar control shares - nav items, the workspace tile,
36
+ * the launcher, the user button.
37
+ *
38
+ * `overflow-hidden` + `whitespace-nowrap` is what makes the collapse smooth:
39
+ * the label is *clipped* by the shrinking rail rather than re-wrapped or
40
+ * re-measured, so a width animation costs no reflow. Fading the label with
41
+ * `SidebarLabel` on top of that hides the clip itself.
42
+ */
43
+ exports.SIDEBAR_ROW = "group/row relative flex h-10 w-full shrink-0 items-center justify-start gap-3 overflow-hidden whitespace-nowrap rounded-lg pe-2 text-sm font-normal transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus";
44
+ /**
45
+ * Leading inset for a row whose glyph is 20px wide (a nav icon, the launcher).
46
+ * `10 + 10 = 20`, and `RAIL_GUTTER + 20 = 32`. See `RAIL_COLLAPSED`.
47
+ */
48
+ exports.SIDEBAR_ROW_ICON = "ps-[10px]";
49
+ /**
50
+ * Leading inset for a row whose glyph is 32px wide (the avatar, the workspace
51
+ * tile). `4 + 16 = 20`, landing on the same 32px centre as `SIDEBAR_ROW_ICON`.
52
+ */
53
+ exports.SIDEBAR_ROW_TILE = "ps-[4px]";
54
+ /**
55
+ * The class list for one sidebar row, in one place.
56
+ *
57
+ * Three different elements need it - a plain `<button>` (`ConnectSidebarRow`),
58
+ * a rendered link (`ConnectSidebarNav`), and a HeroUI `<Button>` acting as a
59
+ * `Modal`/`Dropdown` trigger (the account row, the launcher, the workspace
60
+ * tile). They cannot share a component, because a React Aria overlay has to
61
+ * own its own trigger element - so they share this instead. A second copy of
62
+ * the string is a row that drifts off the 32px centre and takes the collapse
63
+ * animation's smoothness with it.
64
+ *
65
+ * On the HeroUI trigger: these are all Tailwind *utilities*, and HeroUI's
66
+ * `.button` is a component class, so the utility layer wins - which is what
67
+ * lets a `w-fit`, centred, `px-`-padded button take this shape at all.
68
+ */
69
+ function sidebarRowClass(options) {
70
+ // The rail's two text tones, and both were wrong before.
71
+ //
72
+ // **Resting rows are `muted`, not `foreground`.** Against `--surface`,
73
+ // `--foreground` measures 17.6:1 in light and 15.8:1 in dark - body-copy
74
+ // weight, applied to eight items at once, in a column whose whole job is to
75
+ // sit still at the edge of the screen. The result reads as heavy and, worse,
76
+ // gives every inactive destination exactly the emphasis of the active one,
77
+ // so the selected row has to win on background alone. `--muted` is 4.8:1
78
+ // light and 6.2:1 dark - comfortably past AA for normal text, and quiet
79
+ // enough that "where am I" is answerable at a glance. Hover restores
80
+ // `foreground`, so pointing at a row is what brings it forward.
81
+ //
82
+ // **The active row uses `accent-soft-foreground`, never `accent`.**
83
+ // `text-accent` on `bg-accent-soft` is 2.8:1 in light and 4.0:1 in dark -
84
+ // both below AA, on the one row the user most needs to read. HeroUI derives
85
+ // `--accent-soft-foreground` for exactly this pairing (a color-mix of accent
86
+ // into foreground), which measures 4.8:1 and 5.6:1. It is derived rather
87
+ // than fixed, so a host that re-themes `--accent` keeps the contrast.
88
+ //
89
+ // **`bg-transparent` is not redundant.** Three of these rows are HeroUI
90
+ // `<Button variant="tertiary">` (the launcher, the account row, the
91
+ // workspace tile) because they trigger a `Modal` or a `Dropdown`, and
92
+ // `.button--tertiary` sets `--button-bg: var(--default)` - a real fill. Next
93
+ // to the plain anchors the nav renders, those three came out looking
94
+ // permanently hovered, as though three rows in the rail were selected at
95
+ // once. Only the resting tone clears it; the active tone paints its own.
96
+ const tone = options?.active
97
+ ? "bg-accent-soft font-medium text-accent-soft-foreground"
98
+ : "bg-transparent text-muted hover:bg-surface-secondary hover:text-foreground";
99
+ return [exports.SIDEBAR_ROW, options?.tile ? exports.SIDEBAR_ROW_TILE : exports.SIDEBAR_ROW_ICON, tone, options?.className]
100
+ .filter(Boolean)
101
+ .join(" ");
102
+ }
103
+ const FALLBACK = {
104
+ collapsed: false,
105
+ collapsible: false,
106
+ setCollapsed: () => { },
107
+ renderLink: exports.defaultRenderLink,
108
+ };
109
+ const ShellContext = (0, react_1.createContext)(null);
110
+ /**
111
+ * Shared state for every part of the shell, so `ConnectSidebarNav`,
112
+ * `ConnectWorkspaceSwitcher` and `ConnectUserButton` all know whether the rail
113
+ * is collapsed without the host threading a prop through three layers.
114
+ *
115
+ * This is the piece that makes the shell *composable* rather than only
116
+ * configurable: the parts are exported and can be rearranged, and they still
117
+ * agree about collapse, routing and the mobile drawer because they read one
118
+ * context instead of one another's props.
119
+ */
120
+ function ConnectShellProvider({ value, children, }) {
121
+ return (0, jsx_runtime_1.jsx)(ShellContext, { value: value, children: children });
122
+ }
123
+ /**
124
+ * The shell state a sidebar part is rendering inside.
125
+ *
126
+ * Falls back to "expanded, not collapsible, plain anchors" rather than
127
+ * throwing, because every part is also exported for use *outside* a shell -
128
+ * a host dropping `ConnectSidebarNav` into a layout it already owns should
129
+ * not have to mount a provider to get a nav column.
130
+ */
131
+ function useConnectShell() {
132
+ return (0, react_1.use)(ShellContext) ?? FALLBACK;
133
+ }
134
+ /**
135
+ * A sidebar row's text, faded out as the rail collapses.
136
+ *
137
+ * Opacity only - never width, height or margin. The rail's own width
138
+ * animation already moves the clip edge past this text; animating the text's
139
+ * box as well would make two things fight over the same pixels and is what
140
+ * makes a collapsing sidebar look like it stutters.
141
+ *
142
+ * Asymmetric timing on purpose. Collapsing, the label leaves *first* (no
143
+ * delay, quick) so the rail never appears to squeeze text; expanding, it
144
+ * arrives *after* the rail has opened enough to hold it.
145
+ */
146
+ function SidebarLabel({ children, className, }) {
147
+ const { collapsed } = useConnectShell();
148
+ return ((0, jsx_runtime_1.jsx)(motion_js_1.m.span, { className: `min-w-0 flex-1 truncate text-start ${className ?? ""}`, animate: { opacity: collapsed ? 0 : 1 }, transition: collapsed ? { duration: 0.1 } : { duration: 0.18, delay: 0.1 }, "aria-hidden": collapsed, children: children }));
149
+ }
@@ -1,58 +1,84 @@
1
1
  "use client";
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.defaultRenderLink = void 0;
5
- exports.ConnectSideNav = ConnectSideNav;
4
+ exports.ConnectSideNav = exports.defaultRenderLink = void 0;
5
+ exports.ConnectSidebarNav = ConnectSidebarNav;
6
6
  const jsx_runtime_1 = require("react/jsx-runtime");
7
7
  const react_1 = require("@heroui/react");
8
8
  const lucide_react_1 = require("lucide-react");
9
+ const motion_js_1 = require("./motion.js");
10
+ const shell_context_js_1 = require("./shell-context.js");
11
+ Object.defineProperty(exports, "defaultRenderLink", { enumerable: true, get: function () { return shell_context_js_1.defaultRenderLink; } });
9
12
  const use_active_path_js_1 = require("../use-active-path.js");
10
13
  const nav_js_1 = require("./nav.js");
11
- /** A plain anchor - correct anywhere, and a full page load. See `ConnectLinkRenderer`. */
12
- const defaultRenderLink = ({ children, ...props }) => (0, jsx_runtime_1.jsx)("a", { ...props, children: children });
13
- exports.defaultRenderLink = defaultRenderLink;
14
- const ROW = "group relative flex items-center rounded-lg text-sm transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus";
15
- function rowClass(isActive, collapsed, depth) {
16
- const shape = collapsed ? "size-10 justify-center" : `min-h-9 justify-start gap-3 px-3 py-2 ${depth > 0 ? "ml-4" : ""}`;
17
- const tone = isActive
18
- ? "bg-accent-soft font-medium text-accent"
19
- : "text-foreground hover:bg-surface-secondary";
20
- return `${ROW} ${shape} ${tone}`;
14
+ function rowClass(isActive, depth) {
15
+ // The indent is a *margin*, not padding, so a child row steps in while the
16
+ // glyph inside it keeps the shared 32px centre every other row uses.
17
+ return (0, shell_context_js_1.sidebarRowClass)({
18
+ active: isActive,
19
+ className: depth > 0 ? "ms-4 w-[calc(100%-1rem)]" : undefined,
20
+ });
21
21
  }
22
22
  function NavRow({ item, isActive, hasActiveChild, collapsed, depth, renderLink, onNavigate, }) {
23
23
  const linkProps = {
24
24
  href: item.href,
25
- className: rowClass(isActive, collapsed, depth),
25
+ className: rowClass(isActive, depth),
26
26
  "aria-label": item.label,
27
27
  "aria-current": isActive ? "page" : undefined,
28
28
  onClick: onNavigate,
29
29
  ...(item.external ? { target: "_blank", rel: "noopener noreferrer" } : {}),
30
- children: ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [item.icon && (0, jsx_runtime_1.jsx)("span", { className: "relative flex size-5 shrink-0 items-center justify-center", children: item.icon }), !collapsed && (0, jsx_runtime_1.jsx)("span", { className: "relative flex-1 truncate", children: item.label }), !collapsed && item.badge, !collapsed && item.external && ((0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpRight, { className: "size-3.5 shrink-0 text-muted", "aria-hidden": "true" })), collapsed && hasActiveChild && !isActive && ((0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", className: "absolute bottom-1 size-1 rounded-full bg-accent" }))] })),
30
+ children: ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "flex size-5 shrink-0 items-center justify-center", children: item.icon }), (0, jsx_runtime_1.jsx)(shell_context_js_1.SidebarLabel, { children: item.label }), (item.badge || item.external) && ((0, jsx_runtime_1.jsxs)(motion_js_1.m.span, { className: "flex shrink-0 items-center gap-1", animate: { opacity: collapsed ? 0 : 1 }, transition: collapsed ? { duration: 0.1 } : { duration: 0.18, delay: 0.1 }, "aria-hidden": collapsed, children: [item.badge, item.external && (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpRight, { className: "size-3.5 text-muted", "aria-hidden": "true" })] })), collapsed && hasActiveChild && !isActive && ((0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", className: "absolute bottom-1 start-[18px] size-1 rounded-full bg-accent" }))] })),
31
31
  };
32
32
  // The tooltip stays mounted across expand/collapse and only toggles
33
33
  // `isDisabled`. Branching into two trees instead re-creates every item's
34
34
  // overlay and focus machinery on each toggle, which is what makes the width
35
35
  // animation stutter.
36
- return ((0, jsx_runtime_1.jsxs)(react_1.Tooltip, { delay: 200, isDisabled: !collapsed, children: [(0, jsx_runtime_1.jsx)(react_1.Tooltip.Trigger, { "aria-label": item.label, children: renderLink(linkProps) }), (0, jsx_runtime_1.jsx)(react_1.Tooltip.Content, { placement: "right", children: item.label })] }));
36
+ return ((0, jsx_runtime_1.jsxs)(react_1.Tooltip, { delay: 400, isDisabled: !collapsed, children: [(0, jsx_runtime_1.jsx)(react_1.Tooltip.Trigger, { children: renderLink(linkProps) }), (0, jsx_runtime_1.jsx)(react_1.Tooltip.Content, { placement: "right", children: item.label })] }));
37
+ }
38
+ /**
39
+ * A section heading that keeps its box when the rail collapses.
40
+ *
41
+ * The old rail swapped the heading for a hairline, which changed the group's
42
+ * height and made every row below it jump at the moment the width was already
43
+ * moving. Here the box is a constant height in both states: the text fades out
44
+ * and a rule fades in, in the same place.
45
+ */
46
+ function SectionHeading({ title, collapsed }) {
47
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "relative flex h-7 shrink-0 items-center px-[10px]", children: [(0, jsx_runtime_1.jsx)(motion_js_1.m.h2, { className: "truncate text-xs font-semibold uppercase tracking-wider text-muted", animate: { opacity: collapsed ? 0 : 1 }, transition: collapsed ? { duration: 0.1 } : { duration: 0.18, delay: 0.1 }, "aria-hidden": collapsed, children: title }), (0, jsx_runtime_1.jsx)(motion_js_1.m.span, { "aria-hidden": "true", className: "absolute inset-x-[10px] top-1/2 h-px bg-separator", animate: { opacity: collapsed ? 1 : 0 }, transition: { duration: 0.15 } })] }));
37
48
  }
38
49
  /**
39
50
  * The sidebar's nav column on its own - sections, items, one level of
40
51
  * children, and the active highlight.
41
52
  *
42
53
  * `AppShell` renders this inside its rail and its mobile drawer, and it
43
- * is exported for a host that wants the shell's navigation without its
44
- * chrome: the same items, the same active resolution, dropped into a layout
45
- * you already have.
54
+ * is exported for a host that wants the shell's navigation without its chrome:
55
+ * the same items, the same active resolution, dropped into a layout you
56
+ * already have.
57
+ *
58
+ * Every prop is optional except `nav`, because inside a shell they are all
59
+ * inherited from it. Passing one wins over what the shell says - which is how
60
+ * a second nav column (see `secondaryNav`) can share the rail's collapse state
61
+ * while keeping its own items.
46
62
  */
47
- function ConnectSideNav({ nav, activePath, renderLink = exports.defaultRenderLink, collapsed = false, onNavigate, className, }) {
48
- const pathname = (0, use_active_path_js_1.useActivePath)(activePath);
63
+ function ConnectSidebarNav({ nav, activePath, renderLink, collapsed, onNavigate, className, }) {
64
+ const shell = (0, shell_context_js_1.useConnectShell)();
65
+ const isCollapsed = collapsed ?? shell.collapsed;
66
+ const link = renderLink ?? shell.renderLink;
67
+ const navigate = onNavigate ?? shell.onNavigate;
68
+ const pathname = (0, use_active_path_js_1.useActivePath)(activePath ?? shell.activePath);
49
69
  const active = (0, nav_js_1.resolveActivePath)(pathname, nav);
50
70
  const sections = (0, nav_js_1.normalizeNav)(nav);
51
- return ((0, jsx_runtime_1.jsx)("div", { className: `flex flex-col gap-4 ${className ?? ""}`, children: sections.map((section, index) => ((0, jsx_runtime_1.jsxs)("nav", { "aria-label": section.title ?? "Main", className: `flex flex-col gap-1 ${collapsed ? "items-center" : ""}`, children: [section.title && !collapsed && ((0, jsx_runtime_1.jsx)("h2", { className: "px-3 pb-1 text-xs font-semibold uppercase tracking-wider text-muted", children: section.title })), section.title && collapsed && index > 0 && (0, jsx_runtime_1.jsx)("span", { className: "my-1 h-px w-6 bg-separator", "aria-hidden": "true" }), section.items.map((item) => {
71
+ return ((0, jsx_runtime_1.jsx)("div", { className: `flex flex-col gap-2 ${className ?? ""}`, children: sections.map((section, index) => ((0, jsx_runtime_1.jsxs)("nav", { "aria-label": section.title ?? "Main", className: "flex flex-col gap-1", children: [section.title && (0, jsx_runtime_1.jsx)(SectionHeading, { title: section.title, collapsed: isCollapsed }), section.items.map((item) => {
52
72
  const isActive = (0, nav_js_1.isNavItemActive)(item, active);
53
73
  const activeChild = (item.items ?? []).some((child) => (0, nav_js_1.isNavItemActive)(child, active));
54
- return ((0, jsx_runtime_1.jsxs)("div", { className: `flex flex-col gap-1 ${collapsed ? "" : "w-full"}`, children: [(0, jsx_runtime_1.jsx)(NavRow, { item: item, isActive: isActive, hasActiveChild: activeChild, collapsed: collapsed, depth: 0, renderLink: renderLink, onNavigate: onNavigate }), !collapsed &&
74
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "flex w-full flex-col gap-1", children: [(0, jsx_runtime_1.jsx)(NavRow, { item: item, isActive: isActive, hasActiveChild: activeChild, collapsed: isCollapsed, depth: 0, renderLink: link, onNavigate: navigate }), !isCollapsed &&
55
75
  (isActive || activeChild) &&
56
- item.items?.map((child) => ((0, jsx_runtime_1.jsx)(NavRow, { item: child, isActive: (0, nav_js_1.isNavItemActive)(child, active), hasActiveChild: false, collapsed: false, depth: 1, renderLink: renderLink, onNavigate: onNavigate }, child.href)))] }, item.href));
76
+ item.items?.map((child) => ((0, jsx_runtime_1.jsx)(NavRow, { item: child, isActive: (0, nav_js_1.isNavItemActive)(child, active), hasActiveChild: false, collapsed: false, depth: 1, renderLink: link, onNavigate: navigate }, child.href)))] }, item.href));
57
77
  })] }, section.title ?? index))) }));
58
78
  }
79
+ /**
80
+ * @deprecated Renamed to `ConnectSidebarNav` in 0.12 - the rail is a
81
+ * `ConnectSidebar` now, and its pieces are named after it. Identical
82
+ * behaviour; this alias will be removed in a later minor.
83
+ */
84
+ exports.ConnectSideNav = ConnectSidebarNav;
@@ -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) {