@cosmoops/connect-sdk 0.7.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 (154) hide show
  1. package/CHANGELOG.md +662 -0
  2. package/README.md +376 -124
  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 +59 -5
  6. package/dist/cjs/client/components/ai-format.js +135 -0
  7. package/dist/cjs/client/components/ai-widgets.js +232 -0
  8. package/dist/cjs/client/components/app-shell.js +80 -45
  9. package/dist/cjs/client/components/apps-grid.js +3 -2
  10. package/dist/cjs/client/components/ask-ai.js +353 -0
  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 +42 -1
  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 +50 -24
  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 +122 -40
  27. package/dist/cjs/client/components/workspace-switcher.js +100 -0
  28. package/dist/cjs/client/components/workspace-type-gate.js +48 -0
  29. package/dist/cjs/client/index.js +98 -3
  30. package/dist/cjs/client/provider.js +100 -2
  31. package/dist/cjs/client/use-checkout.js +9 -5
  32. package/dist/cjs/errors.js +1 -1
  33. package/dist/cjs/index.js +11 -1
  34. package/dist/cjs/next/index.js +2 -2
  35. package/dist/cjs/server/client.js +25 -2
  36. package/dist/cjs/server/handler.js +237 -16
  37. package/dist/cjs/server/http.js +52 -26
  38. package/dist/cjs/server/resources/acting-as.js +16 -0
  39. package/dist/cjs/server/resources/ai.js +15 -2
  40. package/dist/cjs/server/resources/data.js +374 -0
  41. package/dist/cjs/server/resources/entitlements.js +41 -0
  42. package/dist/cjs/server/resources/identity.js +9 -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.js +152 -0
  48. package/dist/cjs/ui/empty-state.js +28 -0
  49. package/dist/cjs/ui/index.js +44 -0
  50. package/dist/cjs/ui/page-header.js +27 -0
  51. package/dist/cjs/ui/page-shell.js +44 -0
  52. package/dist/cjs/ui/section.js +20 -0
  53. package/dist/cjs/ui/setting-row.js +14 -0
  54. package/dist/cjs/ui/status-chip.js +17 -0
  55. package/dist/esm/cli/doctor.js +3 -3
  56. package/dist/esm/cli/init.js +3 -3
  57. package/dist/esm/client/components/access-gate.js +60 -6
  58. package/dist/esm/client/components/ai-format.js +129 -0
  59. package/dist/esm/client/components/ai-widgets.js +223 -0
  60. package/dist/esm/client/components/app-shell.js +85 -50
  61. package/dist/esm/client/components/apps-grid.js +3 -2
  62. package/dist/esm/client/components/ask-ai.js +350 -0
  63. package/dist/esm/client/components/confirm-dialog.js +37 -0
  64. package/dist/esm/client/components/cursor-pagination.js +19 -0
  65. package/dist/esm/client/components/filter-tabs.js +21 -0
  66. package/dist/esm/client/components/grievance-notice.js +27 -0
  67. package/dist/esm/client/components/link-button.js +17 -10
  68. package/dist/esm/client/components/logo.js +4 -3
  69. package/dist/esm/client/components/nav.js +41 -1
  70. package/dist/esm/client/components/plan-pricing.js +25 -0
  71. package/dist/esm/client/components/plans.js +2 -2
  72. package/dist/esm/client/components/platform-gate.js +45 -0
  73. package/dist/esm/client/components/shell-context.js +141 -0
  74. package/dist/esm/client/components/side-nav.js +49 -22
  75. package/dist/esm/client/components/sidebar.js +120 -0
  76. package/dist/esm/client/components/subscribe-dialog.js +3 -3
  77. package/dist/esm/client/components/subscription.js +2 -2
  78. package/dist/esm/client/components/user-menu.js +121 -41
  79. package/dist/esm/client/components/workspace-switcher.js +97 -0
  80. package/dist/esm/client/components/workspace-type-gate.js +45 -0
  81. package/dist/esm/client/index.js +58 -5
  82. package/dist/esm/client/provider.js +94 -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 +1 -0
  86. package/dist/esm/next/index.js +2 -2
  87. package/dist/esm/server/client.js +25 -2
  88. package/dist/esm/server/handler.js +237 -16
  89. package/dist/esm/server/http.js +52 -26
  90. package/dist/esm/server/resources/acting-as.js +13 -0
  91. package/dist/esm/server/resources/ai.js +15 -2
  92. package/dist/esm/server/resources/data.js +369 -0
  93. package/dist/esm/server/resources/entitlements.js +41 -0
  94. package/dist/esm/server/resources/identity.js +9 -3
  95. package/dist/esm/server/resources/kv.js +80 -0
  96. package/dist/esm/server/resources/platform.js +39 -0
  97. package/dist/esm/server/resources/sessions.js +1 -1
  98. package/dist/esm/server/resources/workspaces.js +70 -0
  99. package/dist/esm/store.js +141 -0
  100. package/dist/esm/ui/empty-state.js +25 -0
  101. package/dist/esm/ui/index.js +34 -0
  102. package/dist/esm/ui/page-header.js +24 -0
  103. package/dist/esm/ui/page-shell.js +41 -0
  104. package/dist/esm/ui/section.js +17 -0
  105. package/dist/esm/ui/setting-row.js +11 -0
  106. package/dist/esm/ui/status-chip.js +14 -0
  107. package/dist/theme.css +87 -0
  108. package/dist/types/client/components/access-gate.d.ts +17 -3
  109. package/dist/types/client/components/ai-format.d.ts +24 -0
  110. package/dist/types/client/components/ai-widgets.d.ts +42 -0
  111. package/dist/types/client/components/app-shell.d.ts +85 -31
  112. package/dist/types/client/components/apps-grid.d.ts +11 -2
  113. package/dist/types/client/components/ask-ai.d.ts +73 -0
  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 +27 -3
  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 +28 -14
  125. package/dist/types/client/components/sidebar.d.ts +95 -0
  126. package/dist/types/client/components/user-menu.d.ts +70 -23
  127. package/dist/types/client/components/workspace-switcher.d.ts +49 -0
  128. package/dist/types/client/components/workspace-type-gate.d.ts +48 -0
  129. package/dist/types/client/index.d.ts +36 -7
  130. package/dist/types/client/provider.d.ts +149 -3
  131. package/dist/types/client/use-checkout.d.ts +8 -4
  132. package/dist/types/errors.d.ts +1 -1
  133. package/dist/types/index.d.ts +2 -0
  134. package/dist/types/next/index.d.ts +1 -1
  135. package/dist/types/server/client.d.ts +25 -2
  136. package/dist/types/server/http.d.ts +14 -3
  137. package/dist/types/server/index.d.ts +4 -0
  138. package/dist/types/server/resources/acting-as.d.ts +11 -0
  139. package/dist/types/server/resources/ai.d.ts +9 -1
  140. package/dist/types/server/resources/data.d.ts +258 -0
  141. package/dist/types/server/resources/entitlements.d.ts +33 -1
  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.d.ts +96 -0
  146. package/dist/types/types.d.ts +491 -13
  147. package/dist/types/ui/empty-state.d.ts +34 -0
  148. package/dist/types/ui/index.d.ts +40 -0
  149. package/dist/types/ui/page-header.d.ts +34 -0
  150. package/dist/types/ui/page-shell.d.ts +39 -0
  151. package/dist/types/ui/section.d.ts +26 -0
  152. package/dist/types/ui/setting-row.d.ts +18 -0
  153. package/dist/types/ui/status-chip.d.ts +36 -0
  154. package/package.json +13 -3
@@ -0,0 +1,45 @@
1
+ "use client";
2
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
3
+ import { useConnectPlatformAccess } from "../provider.js";
4
+ /**
5
+ * Renders `children` only for a Connect platform staff session.
6
+ *
7
+ * ```tsx
8
+ * <ConnectPlatformGate>
9
+ * <LinkMasterConsole />
10
+ * </ConnectPlatformGate>
11
+ * ```
12
+ *
13
+ * Staff are Connect's own operators - `platform_admin`, `platform_manager`,
14
+ * `developer` - signing into *your* application. They belong to no workspace,
15
+ * so a staff session carries no `workspaceId`, no subscription and no member
16
+ * role; see `SessionAccess.staff`. Staff access is off by default and turned on
17
+ * per application by a Connect platform admin, so this gate renders nothing
18
+ * until that happens.
19
+ *
20
+ * **This is a UX gate, not a security boundary** - the same caveat every gate
21
+ * in this package carries, and it matters more here than anywhere else,
22
+ * because what is behind this one is usually an operator console. The browser
23
+ * can ignore this component entirely. Every route it reveals must re-check
24
+ * `staff` on your own server:
25
+ *
26
+ * ```ts
27
+ * const { staff, role } = await client.identity.getUser(sessionToken);
28
+ * if (!staff) return forbidden();
29
+ * ```
30
+ *
31
+ * Renders `loading` - not `fallback` - while the session is unknown, so an
32
+ * operator never flickers through the customer layout on the way in.
33
+ */
34
+ export function ConnectPlatformGate({ roles, children, fallback = null, loading = null, }) {
35
+ const { isStaff, roles: held, isLoading } = useConnectPlatformAccess();
36
+ if (isLoading)
37
+ return _jsx(_Fragment, { children: loading });
38
+ if (!isStaff)
39
+ return _jsx(_Fragment, { children: fallback });
40
+ // An empty `roles` array is treated as "no constraint", same as omitting it.
41
+ // The alternative - refusing everyone - would turn a `roles={[]}` computed
42
+ // from an empty config into a silently dead console.
43
+ const accepted = !roles || roles.length === 0 || roles.some((role) => held.includes(role));
44
+ return _jsx(_Fragment, { children: accepted ? children : fallback });
45
+ }
@@ -0,0 +1,141 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, use } from "react";
4
+ import { m } from "./motion.js";
5
+ /** A plain anchor - correct anywhere, and a full page load. See `ConnectLinkRenderer`. */
6
+ export const defaultRenderLink = ({ children, ...props }) => _jsx("a", { ...props, children: children });
7
+ /**
8
+ * The rail's two widths, in pixels, and the one place they are written down.
9
+ *
10
+ * They are numbers rather than Tailwind classes because the rail animates
11
+ * between them - `motion` interpolates a number and cannot interpolate
12
+ * `w-16` into `w-64`.
13
+ *
14
+ * **The pairing is load-bearing, not arbitrary.** Every row in the sidebar is
15
+ * laid out so its glyph sits exactly `RAIL_GUTTER + ROW_GUTTER + 10px` from
16
+ * the rail's left edge - 32px, which is also `RAIL_COLLAPSED / 2`. So the
17
+ * collapsed rail centres every icon *without a single centring rule*, and
18
+ * nothing moves horizontally while the width animates. Change either width
19
+ * and that identity breaks: the icons drift sideways during the transition,
20
+ * which is the exact stutter this geometry exists to avoid.
21
+ */
22
+ export const RAIL_COLLAPSED = 64;
23
+ export const RAIL_EXPANDED = 264;
24
+ /** Horizontal padding inside the rail, on every region. Tailwind `px-3`. */
25
+ export const RAIL_GUTTER = 12;
26
+ /**
27
+ * The row shape every sidebar control shares - nav items, the workspace tile,
28
+ * the launcher, the user button.
29
+ *
30
+ * `overflow-hidden` + `whitespace-nowrap` is what makes the collapse smooth:
31
+ * the label is *clipped* by the shrinking rail rather than re-wrapped or
32
+ * re-measured, so a width animation costs no reflow. Fading the label with
33
+ * `SidebarLabel` on top of that hides the clip itself.
34
+ */
35
+ export const 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";
36
+ /**
37
+ * Leading inset for a row whose glyph is 20px wide (a nav icon, the launcher).
38
+ * `10 + 10 = 20`, and `RAIL_GUTTER + 20 = 32`. See `RAIL_COLLAPSED`.
39
+ */
40
+ export const SIDEBAR_ROW_ICON = "ps-[10px]";
41
+ /**
42
+ * Leading inset for a row whose glyph is 32px wide (the avatar, the workspace
43
+ * tile). `4 + 16 = 20`, landing on the same 32px centre as `SIDEBAR_ROW_ICON`.
44
+ */
45
+ export const SIDEBAR_ROW_TILE = "ps-[4px]";
46
+ /**
47
+ * The class list for one sidebar row, in one place.
48
+ *
49
+ * Three different elements need it - a plain `<button>` (`ConnectSidebarRow`),
50
+ * a rendered link (`ConnectSidebarNav`), and a HeroUI `<Button>` acting as a
51
+ * `Modal`/`Dropdown` trigger (the account row, the launcher, the workspace
52
+ * tile). They cannot share a component, because a React Aria overlay has to
53
+ * own its own trigger element - so they share this instead. A second copy of
54
+ * the string is a row that drifts off the 32px centre and takes the collapse
55
+ * animation's smoothness with it.
56
+ *
57
+ * On the HeroUI trigger: these are all Tailwind *utilities*, and HeroUI's
58
+ * `.button` is a component class, so the utility layer wins - which is what
59
+ * lets a `w-fit`, centred, `px-`-padded button take this shape at all.
60
+ */
61
+ export function sidebarRowClass(options) {
62
+ // The rail's two text tones, and both were wrong before.
63
+ //
64
+ // **Resting rows are `muted`, not `foreground`.** Against `--surface`,
65
+ // `--foreground` measures 17.6:1 in light and 15.8:1 in dark - body-copy
66
+ // weight, applied to eight items at once, in a column whose whole job is to
67
+ // sit still at the edge of the screen. The result reads as heavy and, worse,
68
+ // gives every inactive destination exactly the emphasis of the active one,
69
+ // so the selected row has to win on background alone. `--muted` is 4.8:1
70
+ // light and 6.2:1 dark - comfortably past AA for normal text, and quiet
71
+ // enough that "where am I" is answerable at a glance. Hover restores
72
+ // `foreground`, so pointing at a row is what brings it forward.
73
+ //
74
+ // **The active row uses `accent-soft-foreground`, never `accent`.**
75
+ // `text-accent` on `bg-accent-soft` is 2.8:1 in light and 4.0:1 in dark -
76
+ // both below AA, on the one row the user most needs to read. HeroUI derives
77
+ // `--accent-soft-foreground` for exactly this pairing (a color-mix of accent
78
+ // into foreground), which measures 4.8:1 and 5.6:1. It is derived rather
79
+ // than fixed, so a host that re-themes `--accent` keeps the contrast.
80
+ //
81
+ // **`bg-transparent` is not redundant.** Three of these rows are HeroUI
82
+ // `<Button variant="tertiary">` (the launcher, the account row, the
83
+ // workspace tile) because they trigger a `Modal` or a `Dropdown`, and
84
+ // `.button--tertiary` sets `--button-bg: var(--default)` - a real fill. Next
85
+ // to the plain anchors the nav renders, those three came out looking
86
+ // permanently hovered, as though three rows in the rail were selected at
87
+ // once. Only the resting tone clears it; the active tone paints its own.
88
+ const tone = options?.active
89
+ ? "bg-accent-soft font-medium text-accent-soft-foreground"
90
+ : "bg-transparent text-muted hover:bg-surface-secondary hover:text-foreground";
91
+ return [SIDEBAR_ROW, options?.tile ? SIDEBAR_ROW_TILE : SIDEBAR_ROW_ICON, tone, options?.className]
92
+ .filter(Boolean)
93
+ .join(" ");
94
+ }
95
+ const FALLBACK = {
96
+ collapsed: false,
97
+ collapsible: false,
98
+ setCollapsed: () => { },
99
+ renderLink: defaultRenderLink,
100
+ };
101
+ const ShellContext = createContext(null);
102
+ /**
103
+ * Shared state for every part of the shell, so `ConnectSidebarNav`,
104
+ * `ConnectWorkspaceSwitcher` and `ConnectUserButton` all know whether the rail
105
+ * is collapsed without the host threading a prop through three layers.
106
+ *
107
+ * This is the piece that makes the shell *composable* rather than only
108
+ * configurable: the parts are exported and can be rearranged, and they still
109
+ * agree about collapse, routing and the mobile drawer because they read one
110
+ * context instead of one another's props.
111
+ */
112
+ export function ConnectShellProvider({ value, children, }) {
113
+ return _jsx(ShellContext, { value: value, children: children });
114
+ }
115
+ /**
116
+ * The shell state a sidebar part is rendering inside.
117
+ *
118
+ * Falls back to "expanded, not collapsible, plain anchors" rather than
119
+ * throwing, because every part is also exported for use *outside* a shell -
120
+ * a host dropping `ConnectSidebarNav` into a layout it already owns should
121
+ * not have to mount a provider to get a nav column.
122
+ */
123
+ export function useConnectShell() {
124
+ return use(ShellContext) ?? FALLBACK;
125
+ }
126
+ /**
127
+ * A sidebar row's text, faded out as the rail collapses.
128
+ *
129
+ * Opacity only - never width, height or margin. The rail's own width
130
+ * animation already moves the clip edge past this text; animating the text's
131
+ * box as well would make two things fight over the same pixels and is what
132
+ * makes a collapsing sidebar look like it stutters.
133
+ *
134
+ * Asymmetric timing on purpose. Collapsing, the label leaves *first* (no
135
+ * delay, quick) so the rail never appears to squeeze text; expanding, it
136
+ * arrives *after* the rail has opened enough to hold it.
137
+ */
138
+ export function SidebarLabel({ children, className, }) {
139
+ const { collapsed } = useConnectShell();
140
+ return (_jsx(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 }));
141
+ }
@@ -1,53 +1,80 @@
1
1
  "use client";
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { Tooltip } from "@heroui/react";
4
4
  import { ArrowUpRight } from "lucide-react";
5
+ import { m } from "./motion.js";
6
+ import { SidebarLabel, defaultRenderLink, sidebarRowClass, useConnectShell } from "./shell-context.js";
5
7
  import { useActivePath } from "../use-active-path.js";
6
8
  import { isNavItemActive, normalizeNav, resolveActivePath, } from "./nav.js";
7
- /** A plain anchor - correct anywhere, and a full page load. See `ConnectLinkRenderer`. */
8
- export const defaultRenderLink = ({ children, ...props }) => _jsx("a", { ...props, children: children });
9
- 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";
10
- function rowClass(isActive, collapsed, depth) {
11
- const shape = collapsed ? "size-10 justify-center" : `min-h-9 justify-start gap-3 px-3 py-2 ${depth > 0 ? "ml-4" : ""}`;
12
- const tone = isActive
13
- ? "bg-accent-soft font-medium text-accent"
14
- : "text-foreground hover:bg-surface-secondary";
15
- return `${ROW} ${shape} ${tone}`;
9
+ export { defaultRenderLink };
10
+ function rowClass(isActive, depth) {
11
+ // The indent is a *margin*, not padding, so a child row steps in while the
12
+ // glyph inside it keeps the shared 32px centre every other row uses.
13
+ return sidebarRowClass({
14
+ active: isActive,
15
+ className: depth > 0 ? "ms-4 w-[calc(100%-1rem)]" : undefined,
16
+ });
16
17
  }
17
18
  function NavRow({ item, isActive, hasActiveChild, collapsed, depth, renderLink, onNavigate, }) {
18
19
  const linkProps = {
19
20
  href: item.href,
20
- className: rowClass(isActive, collapsed, depth),
21
+ className: rowClass(isActive, depth),
21
22
  "aria-label": item.label,
22
23
  "aria-current": isActive ? "page" : undefined,
23
24
  onClick: onNavigate,
24
25
  ...(item.external ? { target: "_blank", rel: "noopener noreferrer" } : {}),
25
- children: (_jsxs(_Fragment, { children: [item.icon && _jsx("span", { className: "relative flex size-5 shrink-0 items-center justify-center", children: item.icon }), !collapsed && _jsx("span", { className: "relative flex-1 truncate", children: item.label }), !collapsed && item.badge, !collapsed && item.external && (_jsx(ArrowUpRight, { className: "size-3.5 shrink-0 text-muted", "aria-hidden": "true" })), collapsed && hasActiveChild && !isActive && (_jsx("span", { "aria-hidden": "true", className: "absolute bottom-1 size-1 rounded-full bg-accent" }))] })),
26
+ children: (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex size-5 shrink-0 items-center justify-center", children: item.icon }), _jsx(SidebarLabel, { children: item.label }), (item.badge || item.external) && (_jsxs(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 && _jsx(ArrowUpRight, { className: "size-3.5 text-muted", "aria-hidden": "true" })] })), collapsed && hasActiveChild && !isActive && (_jsx("span", { "aria-hidden": "true", className: "absolute bottom-1 start-[18px] size-1 rounded-full bg-accent" }))] })),
26
27
  };
27
28
  // The tooltip stays mounted across expand/collapse and only toggles
28
29
  // `isDisabled`. Branching into two trees instead re-creates every item's
29
30
  // overlay and focus machinery on each toggle, which is what makes the width
30
31
  // animation stutter.
31
- return (_jsxs(Tooltip, { delay: 200, isDisabled: !collapsed, children: [_jsx(Tooltip.Trigger, { "aria-label": item.label, children: renderLink(linkProps) }), _jsx(Tooltip.Content, { placement: "right", children: item.label })] }));
32
+ return (_jsxs(Tooltip, { delay: 400, isDisabled: !collapsed, children: [_jsx(Tooltip.Trigger, { children: renderLink(linkProps) }), _jsx(Tooltip.Content, { placement: "right", children: item.label })] }));
33
+ }
34
+ /**
35
+ * A section heading that keeps its box when the rail collapses.
36
+ *
37
+ * The old rail swapped the heading for a hairline, which changed the group's
38
+ * height and made every row below it jump at the moment the width was already
39
+ * moving. Here the box is a constant height in both states: the text fades out
40
+ * and a rule fades in, in the same place.
41
+ */
42
+ function SectionHeading({ title, collapsed }) {
43
+ return (_jsxs("div", { className: "relative flex h-7 shrink-0 items-center px-[10px]", children: [_jsx(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 }), _jsx(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 } })] }));
32
44
  }
33
45
  /**
34
46
  * The sidebar's nav column on its own - sections, items, one level of
35
47
  * children, and the active highlight.
36
48
  *
37
- * `ConnectAppShell` renders this inside its rail and its mobile drawer, and it
38
- * is exported for a host that wants the shell's navigation without its
39
- * chrome: the same items, the same active resolution, dropped into a layout
40
- * you already have.
49
+ * `AppShell` renders this inside its rail and its mobile drawer, and it
50
+ * is exported for a host that wants the shell's navigation without its chrome:
51
+ * the same items, the same active resolution, dropped into a layout you
52
+ * already have.
53
+ *
54
+ * Every prop is optional except `nav`, because inside a shell they are all
55
+ * inherited from it. Passing one wins over what the shell says - which is how
56
+ * a second nav column (see `secondaryNav`) can share the rail's collapse state
57
+ * while keeping its own items.
41
58
  */
42
- export function ConnectSideNav({ nav, activePath, renderLink = defaultRenderLink, collapsed = false, onNavigate, className, }) {
43
- const pathname = useActivePath(activePath);
59
+ export function ConnectSidebarNav({ nav, activePath, renderLink, collapsed, onNavigate, className, }) {
60
+ const shell = useConnectShell();
61
+ const isCollapsed = collapsed ?? shell.collapsed;
62
+ const link = renderLink ?? shell.renderLink;
63
+ const navigate = onNavigate ?? shell.onNavigate;
64
+ const pathname = useActivePath(activePath ?? shell.activePath);
44
65
  const active = resolveActivePath(pathname, nav);
45
66
  const sections = normalizeNav(nav);
46
- return (_jsx("div", { className: `flex flex-col gap-4 ${className ?? ""}`, children: sections.map((section, index) => (_jsxs("nav", { "aria-label": section.title ?? "Main", className: `flex flex-col gap-1 ${collapsed ? "items-center" : ""}`, children: [section.title && !collapsed && (_jsx("h2", { className: "px-3 pb-1 text-xs font-semibold uppercase tracking-wider text-muted", children: section.title })), section.title && collapsed && index > 0 && _jsx("span", { className: "my-1 h-px w-6 bg-separator", "aria-hidden": "true" }), section.items.map((item) => {
67
+ return (_jsx("div", { className: `flex flex-col gap-2 ${className ?? ""}`, children: sections.map((section, index) => (_jsxs("nav", { "aria-label": section.title ?? "Main", className: "flex flex-col gap-1", children: [section.title && _jsx(SectionHeading, { title: section.title, collapsed: isCollapsed }), section.items.map((item) => {
47
68
  const isActive = isNavItemActive(item, active);
48
69
  const activeChild = (item.items ?? []).some((child) => isNavItemActive(child, active));
49
- return (_jsxs("div", { className: `flex flex-col gap-1 ${collapsed ? "" : "w-full"}`, children: [_jsx(NavRow, { item: item, isActive: isActive, hasActiveChild: activeChild, collapsed: collapsed, depth: 0, renderLink: renderLink, onNavigate: onNavigate }), !collapsed &&
70
+ return (_jsxs("div", { className: "flex w-full flex-col gap-1", children: [_jsx(NavRow, { item: item, isActive: isActive, hasActiveChild: activeChild, collapsed: isCollapsed, depth: 0, renderLink: link, onNavigate: navigate }), !isCollapsed &&
50
71
  (isActive || activeChild) &&
51
- item.items?.map((child) => (_jsx(NavRow, { item: child, isActive: isNavItemActive(child, active), hasActiveChild: false, collapsed: false, depth: 1, renderLink: renderLink, onNavigate: onNavigate }, child.href)))] }, item.href));
72
+ item.items?.map((child) => (_jsx(NavRow, { item: child, isActive: isNavItemActive(child, active), hasActiveChild: false, collapsed: false, depth: 1, renderLink: link, onNavigate: navigate }, child.href)))] }, item.href));
52
73
  })] }, section.title ?? index))) }));
53
74
  }
75
+ /**
76
+ * @deprecated Renamed to `ConnectSidebarNav` in 0.12 - the rail is a
77
+ * `ConnectSidebar` now, and its pieces are named after it. Identical
78
+ * behaviour; this alias will be removed in a later minor.
79
+ */
80
+ export const ConnectSideNav = ConnectSidebarNav;
@@ -0,0 +1,120 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Tooltip } from "@heroui/react";
4
+ import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
5
+ import { ConnectLogo } from "./logo.js";
6
+ import { EASE_OUT_QUART, MotionProvider, m } from "./motion.js";
7
+ import { RAIL_COLLAPSED, RAIL_EXPANDED, SidebarLabel, sidebarRowClass, useConnectShell } from "./shell-context.js";
8
+ /**
9
+ * The rail's own transition.
10
+ *
11
+ * A tween, not a spring. A spring overshoots, and an overshooting *width*
12
+ * drags every row's clip edge past its resting place and back - which reads as
13
+ * the labels wobbling. 240ms on `--ease-out-quart` is long enough to follow
14
+ * and short enough that hammering the toggle never queues a visible backlog.
15
+ */
16
+ const RAIL_TRANSITION = { duration: 0.24, ease: EASE_OUT_QUART };
17
+ /** The chrome fades a touch faster than the rail moves, so nothing is caught mid-clip. */
18
+ const CHROME_TRANSITION = { duration: 0.15, ease: EASE_OUT_QUART };
19
+ /** Label fade timing, shared by the header wordmark and `SidebarLabel`. */
20
+ function labelTransition(collapsed) {
21
+ return collapsed ? { duration: 0.1 } : { duration: 0.18, delay: 0.1 };
22
+ }
23
+ /**
24
+ * The application rail: a fixed-width column that animates between an
25
+ * icon-only 64px and a labelled 264px.
26
+ *
27
+ * **Why width, and nothing else, animates.** Every row inside puts its glyph
28
+ * on the same 32px centre in both states (see `RAIL_COLLAPSED`), and every
29
+ * label is clipped rather than re-measured (see `SIDEBAR_ROW`). So the only
30
+ * property in flight is this element's width: no child moves, no text
31
+ * re-wraps, and there is no per-frame layout pass on the rows. That is why the
32
+ * collapse looks smooth rather than merely fast.
33
+ *
34
+ * Hidden below `md`. The mobile drawer renders the same children with
35
+ * `collapsed: false`, because a 64px icon rail on a phone is a worse drawer,
36
+ * not a smaller one.
37
+ */
38
+ export function ConnectSidebar({ children, label = "Sidebar", className }) {
39
+ const { collapsed } = useConnectShell();
40
+ return (
41
+ // Its own motion root, even though `AppShell` already mounts one.
42
+ // `LazyMotion` nests harmlessly, and without it a host composing the rail
43
+ // from these parts by hand gets a sidebar that jumps between widths
44
+ // instead of animating - a failure that looks like a styling bug and is
45
+ // really a missing provider.
46
+ _jsx(MotionProvider, { children: _jsx(m.aside, { "aria-label": label, className: `hidden shrink-0 flex-col overflow-hidden border-e border-border bg-surface md:flex ${className ?? ""}`,
47
+ // `initial={false}` is what stops the rail sweeping open on first paint
48
+ // for a returning user whose stored preference is "collapsed".
49
+ initial: false, animate: { width: collapsed ? RAIL_COLLAPSED : RAIL_EXPANDED }, transition: RAIL_TRANSITION, children: children }) }));
50
+ }
51
+ /**
52
+ * The brand row, and the rail's own expand/collapse control.
53
+ *
54
+ * **Two states, one DOM.** The badge is mounted once and never re-created; the
55
+ * wordmark fades beside it and the collapse button fades at the trailing edge.
56
+ * Swapping which *element* renders the brand would remount it halfway through
57
+ * the width animation and flicker.
58
+ *
59
+ * Collapsed, hovering or focusing the badge swaps it for the expand glyph, so
60
+ * the rail needs no permanent control eating a row. Expanded, the control sits
61
+ * at the header's trailing edge, where it can be aimed at without covering the
62
+ * brand. The swap is pure CSS on a `group` - hovering re-renders nothing,
63
+ * which is what keeps it instant.
64
+ *
65
+ * The trade this makes, deliberately: **collapsed, the lockup is no longer a
66
+ * link home.** It has become the expand control, and one target that navigates
67
+ * on click but expands on click-after-hover would be worse than either. Put
68
+ * home in the nav; every application already does.
69
+ */
70
+ export function ConnectSidebarHeader({ name, icon, homeHref = "/" }) {
71
+ const { collapsed, collapsible, setCollapsed, renderLink } = useConnectShell();
72
+ const logoExpands = collapsible && collapsed;
73
+ return (_jsxs("div", { className: "relative flex h-14 shrink-0 items-center overflow-hidden px-3", children: [_jsxs("div", { className: "group/brand relative flex size-10 shrink-0 items-center justify-center", children: [renderLink({
74
+ href: homeHref,
75
+ 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" : ""}`,
76
+ // `ConnectLogo` names itself with `role="img"`, which the `<a>` around
77
+ // it does not inherit as its own accessible name.
78
+ "aria-label": `${name} home`,
79
+ children: _jsx(ConnectLogo, { name: name, icon: icon, variant: "mark" }),
80
+ }), logoExpands && (_jsxs(Tooltip, { delay: 400, children: [_jsx(Tooltip.Trigger, { children: _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: _jsx(PanelLeftOpen, { className: "size-5" }) }) }), _jsx(Tooltip.Content, { placement: "right", children: "Expand sidebar" })] }))] }), _jsx(m.span, { className: "ms-2 min-w-0 flex-1 truncate", animate: { opacity: collapsed ? 0 : 1 }, transition: labelTransition(collapsed), "aria-hidden": true, children: _jsx(ConnectLogo, { name: name, icon: icon, variant: "wordmark" }) }), collapsible && (
81
+ // Absolutely positioned so it never takes part in the row's layout: in
82
+ // flow, `ms-auto` inside a 40px-wide collapsed row would shove the
83
+ // badge sideways for the length of the animation.
84
+ _jsx(m.div, { className: "absolute end-3 top-1/2 -translate-y-1/2", animate: { opacity: collapsed ? 0 : 1 }, transition: CHROME_TRANSITION,
85
+ // A faded-out control must not be reachable by keyboard or pointer.
86
+ // `inert` says that in one attribute; `hidden` would remove it from
87
+ // flow and make the header re-layout on every toggle.
88
+ inert: collapsed, children: _jsxs(Tooltip, { delay: 400, children: [_jsx(Tooltip.Trigger, { children: _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: _jsx(PanelLeftClose, { className: "size-4" }) }) }), _jsx(Tooltip.Content, { placement: "right", children: "Collapse sidebar" })] }) }))] }));
89
+ }
90
+ /**
91
+ * The rail's scrolling region, for the primary navigation. Everything else in
92
+ * the sidebar is `shrink-0`, so this is the only part that ever gives way.
93
+ */
94
+ export function ConnectSidebarBody({ children }) {
95
+ return (_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 }));
96
+ }
97
+ /**
98
+ * A pinned region at the foot of the rail - the secondary nav, the launcher,
99
+ * the account row.
100
+ *
101
+ * `separated` draws the hairline above it. The account row wants one; a
102
+ * secondary nav sitting directly on top of the account row does not, or the
103
+ * rail ends in two rules a few pixels apart.
104
+ */
105
+ export function ConnectSidebarFooter({ children, separated = true, }) {
106
+ return (_jsx("div", { className: `flex shrink-0 flex-col gap-1 px-3 py-2 ${separated ? "border-t border-border" : ""}`, children: children }));
107
+ }
108
+ /**
109
+ * A pressable row in the rail that is not a nav item - the launcher, the
110
+ * account button, anything a host adds to the footer.
111
+ *
112
+ * Exported so a host's own row lines up with the SDK's. It applies the same
113
+ * 32px glyph centre and the same label fade, which is what stops a partner's
114
+ * "Help" row drifting sideways while every row above it holds still.
115
+ */
116
+ export function ConnectSidebarRow({ icon, label, tile = false, trailing, className, "aria-label": ariaLabel, ...props }) {
117
+ const { collapsed } = useConnectShell();
118
+ const name = ariaLabel ?? (typeof label === "string" ? label : undefined);
119
+ return (_jsxs(Tooltip, { delay: 400, isDisabled: !collapsed, children: [_jsx(Tooltip.Trigger, { children: _jsxs("button", { type: "button", "aria-label": name, className: sidebarRowClass({ tile, className }), ...props, children: [_jsx("span", { className: `flex ${tile ? "size-8" : "size-5"} shrink-0 items-center justify-center`, children: icon }), _jsx(SidebarLabel, { children: label }), trailing] }) }), _jsx(Tooltip.Content, { placement: "right", children: name ?? label })] }));
120
+ }
@@ -2,13 +2,13 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { Button, Modal } from "@heroui/react";
4
4
  import { ArrowRight, ExternalLink, Info } from "lucide-react";
5
- import { formatPrice, monthlyOf } from "./plan-pricing.js";
5
+ import { formatPrice, intervalLabel, monthlyOf } from "./plan-pricing.js";
6
6
  /** GST is quoted exclusive everywhere in Connect; this is the same arithmetic `calculateGST` does server-side. */
7
7
  function withGst(price, gstRate) {
8
8
  return price + Math.round(price * (gstRate / 100));
9
9
  }
10
10
  function cadenceOf(plan) {
11
- return plan.billingInterval === "annual" ? "year" : "month";
11
+ return intervalLabel(plan.billingInterval).per;
12
12
  }
13
13
  const HEADING = {
14
14
  subscribe: "Confirm subscription",
@@ -47,7 +47,7 @@ export function ConnectSubscribeDialog({ plan, action, current, coupon, onClose,
47
47
  // member and the one most in need of it: the two prices differ by an order
48
48
  // of magnitude, and showing only the new one reads as a price rise.
49
49
  const isChange = action === "upgrade" || action === "downgrade" || isIntervalSwitch;
50
- return (_jsx(Modal.Backdrop, { isOpen: plan !== null, onOpenChange: (open) => !open && onClose(), children: _jsx(Modal.Container, { size: "sm", children: _jsxs(Modal.Dialog, { children: [_jsx(Modal.CloseTrigger, { onPress: onClose }), _jsx(Modal.Header, { children: _jsx(Modal.Heading, { children: HEADING[action] }) }), plan && (_jsxs(_Fragment, { children: [_jsxs(Modal.Body, { className: "flex flex-col gap-4", children: [isChange && currentPlan ? (_jsxs("div", { className: "flex items-center gap-3 text-sm", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-muted", children: currentPlan.name }), _jsxs("p", { className: "text-xs text-muted", children: [formatPrice(currentPlan.price), "/", currentPlan.billingInterval === "annual" ? "yr" : "mo"] })] }), _jsx(ArrowRight, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate font-medium text-foreground", children: plan.name }), _jsxs("p", { className: "text-xs text-muted", children: [formatPrice(plan.price), "/", plan.billingInterval === "annual" ? "yr" : "mo"] })] })] })) : (_jsxs("div", { children: [_jsx("p", { className: "font-medium text-foreground", children: plan.name }), plan.description && _jsx("p", { className: "text-xs text-muted", children: plan.description })] })), _jsxs("div", { className: "flex flex-col gap-1 border-t border-separator pt-3 text-sm", children: [_jsxs("div", { className: "flex justify-between text-muted", children: [_jsx("span", { children: plan.name }), _jsx("span", { children: formatPrice(plan.price) })] }), _jsxs("div", { className: "flex justify-between text-muted", children: [_jsxs("span", { children: ["GST (", plan.gstRate, "%)"] }), _jsx("span", { children: formatPrice(withGst(plan.price, plan.gstRate) - plan.price) })] }), _jsxs("div", { className: "flex justify-between border-t border-separator pt-1 font-semibold text-foreground", children: [_jsxs("span", { children: ["Per ", cadenceOf(plan)] }), _jsx("span", { children: formatPrice(withGst(plan.price, plan.gstRate)) })] }), plan.billingInterval === "annual" && (_jsxs("p", { className: "text-xs text-muted", children: ["Works out to ", formatPrice(monthlyOf(withGst(plan.price, plan.gstRate), "annual")), " a month, billed yearly."] }))] }), plan.trialDays ? (_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() ? (_jsxs("p", { className: "text-xs text-muted", children: ["Coupon ", _jsx("span", { className: "font-mono text-foreground", children: coupon.trim() }), " is carried over and validated on Connect."] })) : null, _jsxs("div", { className: "flex gap-2 rounded-lg bg-surface p-3 text-xs text-muted", children: [_jsx(Info, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": "true" }), _jsxs("div", { className: "flex flex-col gap-1", children: [action === "subscribe" && (_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" && (_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" && (_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 && (
50
+ return (_jsx(Modal.Backdrop, { isOpen: plan !== null, onOpenChange: (open) => !open && onClose(), children: _jsx(Modal.Container, { size: "sm", children: _jsxs(Modal.Dialog, { children: [_jsx(Modal.CloseTrigger, { onPress: onClose }), _jsx(Modal.Header, { children: _jsx(Modal.Heading, { children: HEADING[action] }) }), plan && (_jsxs(_Fragment, { children: [_jsxs(Modal.Body, { className: "flex flex-col gap-4", children: [isChange && currentPlan ? (_jsxs("div", { className: "flex items-center gap-3 text-sm", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-muted", children: currentPlan.name }), _jsxs("p", { className: "text-xs text-muted", children: [formatPrice(currentPlan.price), "/", intervalLabel(currentPlan.billingInterval).short] })] }), _jsx(ArrowRight, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate font-medium text-foreground", children: plan.name }), _jsxs("p", { className: "text-xs text-muted", children: [formatPrice(plan.price), "/", intervalLabel(plan.billingInterval).short] })] })] })) : (_jsxs("div", { children: [_jsx("p", { className: "font-medium text-foreground", children: plan.name }), plan.description && _jsx("p", { className: "text-xs text-muted", children: plan.description })] })), _jsxs("div", { className: "flex flex-col gap-1 border-t border-separator pt-3 text-sm", children: [_jsxs("div", { className: "flex justify-between text-muted", children: [_jsx("span", { children: plan.name }), _jsx("span", { children: formatPrice(plan.price) })] }), _jsxs("div", { className: "flex justify-between text-muted", children: [_jsxs("span", { children: ["GST (", plan.gstRate, "%)"] }), _jsx("span", { children: formatPrice(withGst(plan.price, plan.gstRate) - plan.price) })] }), _jsxs("div", { className: "flex justify-between border-t border-separator pt-1 font-semibold text-foreground", children: [_jsxs("span", { children: ["Per ", cadenceOf(plan)] }), _jsx("span", { children: formatPrice(withGst(plan.price, plan.gstRate)) })] }), plan.billingInterval === "annual" && (_jsxs("p", { className: "text-xs text-muted", children: ["Works out to ", formatPrice(monthlyOf(withGst(plan.price, plan.gstRate), "annual")), " a month, billed yearly."] }))] }), plan.trialDays ? (_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() ? (_jsxs("p", { className: "text-xs text-muted", children: ["Coupon ", _jsx("span", { className: "font-mono text-foreground", children: coupon.trim() }), " is carried over and validated on Connect."] })) : null, _jsxs("div", { className: "flex gap-2 rounded-lg bg-surface p-3 text-xs text-muted", children: [_jsx(Info, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": "true" }), _jsxs("div", { className: "flex flex-col gap-1", children: [action === "subscribe" && (_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" && (_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" && (_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 && (
51
51
  // Not a proration. The cycle you are on ends, so this is
52
52
  // the one action where the amount collected can exceed a
53
53
  // single cycle's list price - or be nothing at all, with
@@ -4,7 +4,7 @@ import { Button, Card, Chip, Skeleton } from "@heroui/react";
4
4
  import { AlertTriangle, ArrowRight, Clock, CreditCard, ExternalLink, Users } from "lucide-react";
5
5
  import { useConnect, useConnectEntitlements } from "../provider.js";
6
6
  import { m, MotionProvider } from "./motion.js";
7
- import { formatPrice } from "./plan-pricing.js";
7
+ import { formatPrice, intervalLabel } from "./plan-pricing.js";
8
8
  import { ConnectLinkButton } from "./link-button.js";
9
9
  /** Where the plan catalogue lives in the host app. Overridable, because not every app mounts it at `/plans`. */
10
10
  const DEFAULT_PLANS_HREF = "/plans";
@@ -77,7 +77,7 @@ export function ConnectSubscriptionCard({ plansHref, className }) {
77
77
  return _jsx(Skeleton, { className: `h-48 rounded-xl ${className ?? ""}` });
78
78
  if (!detail || !plan)
79
79
  return _jsx(ConnectNoSubscriptionCard, { plansHref: plansHref, className: className });
80
- return (_jsx(MotionProvider, { children: _jsx(m.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.2 }, children: _jsxs(Card, { className: className, children: [_jsxs(Card.Header, { children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx(Card.Title, { children: plan.name }), _jsx(SubscriptionChip, { state: state })] }), _jsxs(Card.Description, { children: [formatPrice(plan.price), " + ", plan.gstRate, "% GST per", " ", plan.billingInterval === "annual" ? "year" : "month"] })] }), _jsxs(Card.Content, { className: "flex flex-col gap-2 text-sm", children: [_jsx(NextEvent, { detail: detail, state: state }), _jsx(ConnectSeatUsage, { plan: plan, detail: detail })] }), _jsxs(Card.Footer, { className: "flex flex-wrap gap-2", children: [_jsxs(Button, { variant: "secondary", onPress: () => window.open(`${appUrl}/billing/subscriptions/${detail.id}`, "_blank", "noopener,noreferrer"), children: ["Manage on Connect", _jsx(ExternalLink, { className: "size-3.5" })] }), _jsx(ConnectLinkButton, { href: plansHref ?? DEFAULT_PLANS_HREF, variant: "tertiary", children: "Change plan" })] })] }) }) }));
80
+ return (_jsx(MotionProvider, { children: _jsx(m.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.2 }, children: _jsxs(Card, { className: className, children: [_jsxs(Card.Header, { children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx(Card.Title, { children: plan.name }), _jsx(SubscriptionChip, { state: state })] }), _jsxs(Card.Description, { children: [formatPrice(plan.price), " + ", plan.gstRate, "% GST per", " ", intervalLabel(plan.billingInterval).per] })] }), _jsxs(Card.Content, { className: "flex flex-col gap-2 text-sm", children: [_jsx(NextEvent, { detail: detail, state: state }), _jsx(ConnectSeatUsage, { plan: plan, detail: detail })] }), _jsxs(Card.Footer, { className: "flex flex-wrap gap-2", children: [_jsxs(Button, { variant: "secondary", onPress: () => window.open(`${appUrl}/billing/subscriptions/${detail.id}`, "_blank", "noopener,noreferrer"), children: ["Manage on Connect", _jsx(ExternalLink, { className: "size-3.5" })] }), _jsx(ConnectLinkButton, { href: plansHref ?? DEFAULT_PLANS_HREF, variant: "tertiary", children: "Change plan" })] })] }) }) }));
81
81
  }
82
82
  function SubscriptionChip({ state }) {
83
83
  switch (state) {