@cosmoops/connect-sdk 0.9.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +497 -0
  2. package/README.md +111 -41
  3. package/dist/cjs/cli/doctor.js +3 -3
  4. package/dist/cjs/cli/init.js +3 -3
  5. package/dist/cjs/client/components/access-gate.js +20 -1
  6. package/dist/cjs/client/components/app-shell.js +75 -41
  7. package/dist/cjs/client/components/apps-grid.js +3 -2
  8. package/dist/cjs/client/components/ask-ai.js +20 -1
  9. package/dist/cjs/client/components/confirm-dialog.js +40 -0
  10. package/dist/cjs/client/components/cursor-pagination.js +22 -0
  11. package/dist/cjs/client/components/filter-tabs.js +24 -0
  12. package/dist/cjs/client/components/grievance-notice.js +30 -0
  13. package/dist/cjs/client/components/link-button.js +17 -10
  14. package/dist/cjs/client/components/logo.js +4 -3
  15. package/dist/cjs/client/components/nav.js +41 -0
  16. package/dist/cjs/client/components/plan-pricing.js +27 -0
  17. package/dist/cjs/client/components/plans.js +1 -1
  18. package/dist/cjs/client/components/platform-gate.js +48 -0
  19. package/dist/cjs/client/components/shell-context.js +149 -0
  20. package/dist/cjs/client/components/side-nav.js +49 -23
  21. package/dist/cjs/client/components/sidebar.js +127 -0
  22. package/dist/cjs/client/components/subscribe-dialog.js +2 -2
  23. package/dist/cjs/client/components/subscription.js +1 -1
  24. package/dist/cjs/client/components/user-menu.js +96 -37
  25. package/dist/cjs/client/components/workspace-switcher.js +100 -0
  26. package/dist/cjs/client/index.js +69 -2
  27. package/dist/cjs/client/provider.js +57 -2
  28. package/dist/cjs/client/use-checkout.js +9 -5
  29. package/dist/cjs/errors.js +1 -1
  30. package/dist/cjs/index.js +11 -1
  31. package/dist/cjs/next/index.js +2 -2
  32. package/dist/cjs/server/client.js +25 -2
  33. package/dist/cjs/server/handler.js +196 -18
  34. package/dist/cjs/server/http.js +6 -2
  35. package/dist/cjs/server/resources/acting-as.js +16 -0
  36. package/dist/cjs/server/resources/ai.js +15 -2
  37. package/dist/cjs/server/resources/data.js +374 -0
  38. package/dist/cjs/server/resources/identity.js +7 -3
  39. package/dist/cjs/server/resources/kv.js +84 -0
  40. package/dist/cjs/server/resources/platform.js +43 -0
  41. package/dist/cjs/server/resources/sessions.js +1 -1
  42. package/dist/cjs/server/resources/workspaces.js +74 -0
  43. package/dist/cjs/store.js +152 -0
  44. package/dist/cjs/ui/empty-state.js +28 -0
  45. package/dist/cjs/ui/index.js +44 -0
  46. package/dist/cjs/ui/page-header.js +27 -0
  47. package/dist/cjs/ui/page-shell.js +44 -0
  48. package/dist/cjs/ui/section.js +20 -0
  49. package/dist/cjs/ui/setting-row.js +14 -0
  50. package/dist/cjs/ui/status-chip.js +17 -0
  51. package/dist/esm/cli/doctor.js +3 -3
  52. package/dist/esm/cli/init.js +3 -3
  53. package/dist/esm/client/components/access-gate.js +20 -1
  54. package/dist/esm/client/components/app-shell.js +81 -47
  55. package/dist/esm/client/components/apps-grid.js +3 -2
  56. package/dist/esm/client/components/ask-ai.js +20 -1
  57. package/dist/esm/client/components/confirm-dialog.js +37 -0
  58. package/dist/esm/client/components/cursor-pagination.js +19 -0
  59. package/dist/esm/client/components/filter-tabs.js +21 -0
  60. package/dist/esm/client/components/grievance-notice.js +27 -0
  61. package/dist/esm/client/components/link-button.js +17 -10
  62. package/dist/esm/client/components/logo.js +4 -3
  63. package/dist/esm/client/components/nav.js +40 -0
  64. package/dist/esm/client/components/plan-pricing.js +25 -0
  65. package/dist/esm/client/components/plans.js +2 -2
  66. package/dist/esm/client/components/platform-gate.js +45 -0
  67. package/dist/esm/client/components/shell-context.js +141 -0
  68. package/dist/esm/client/components/side-nav.js +48 -21
  69. package/dist/esm/client/components/sidebar.js +120 -0
  70. package/dist/esm/client/components/subscribe-dialog.js +3 -3
  71. package/dist/esm/client/components/subscription.js +2 -2
  72. package/dist/esm/client/components/user-menu.js +95 -38
  73. package/dist/esm/client/components/workspace-switcher.js +97 -0
  74. package/dist/esm/client/index.js +46 -4
  75. package/dist/esm/client/provider.js +53 -2
  76. package/dist/esm/client/use-checkout.js +9 -5
  77. package/dist/esm/errors.js +1 -1
  78. package/dist/esm/index.js +1 -0
  79. package/dist/esm/next/index.js +2 -2
  80. package/dist/esm/server/client.js +25 -2
  81. package/dist/esm/server/handler.js +196 -18
  82. package/dist/esm/server/http.js +6 -2
  83. package/dist/esm/server/resources/acting-as.js +13 -0
  84. package/dist/esm/server/resources/ai.js +15 -2
  85. package/dist/esm/server/resources/data.js +369 -0
  86. package/dist/esm/server/resources/identity.js +7 -3
  87. package/dist/esm/server/resources/kv.js +80 -0
  88. package/dist/esm/server/resources/platform.js +39 -0
  89. package/dist/esm/server/resources/sessions.js +1 -1
  90. package/dist/esm/server/resources/workspaces.js +70 -0
  91. package/dist/esm/store.js +141 -0
  92. package/dist/esm/ui/empty-state.js +25 -0
  93. package/dist/esm/ui/index.js +34 -0
  94. package/dist/esm/ui/page-header.js +24 -0
  95. package/dist/esm/ui/page-shell.js +41 -0
  96. package/dist/esm/ui/section.js +17 -0
  97. package/dist/esm/ui/setting-row.js +11 -0
  98. package/dist/esm/ui/status-chip.js +14 -0
  99. package/dist/types/client/components/app-shell.d.ts +65 -27
  100. package/dist/types/client/components/apps-grid.d.ts +11 -2
  101. package/dist/types/client/components/confirm-dialog.d.ts +34 -0
  102. package/dist/types/client/components/cursor-pagination.d.ts +27 -0
  103. package/dist/types/client/components/filter-tabs.d.ts +26 -0
  104. package/dist/types/client/components/grievance-notice.d.ts +31 -0
  105. package/dist/types/client/components/link-button.d.ts +32 -9
  106. package/dist/types/client/components/logo.d.ts +18 -2
  107. package/dist/types/client/components/nav.d.ts +24 -0
  108. package/dist/types/client/components/plan-pricing.d.ts +10 -0
  109. package/dist/types/client/components/platform-gate.d.ts +53 -0
  110. package/dist/types/client/components/shell-context.d.ts +116 -0
  111. package/dist/types/client/components/side-nav.d.ts +27 -13
  112. package/dist/types/client/components/sidebar.d.ts +95 -0
  113. package/dist/types/client/components/user-menu.d.ts +68 -22
  114. package/dist/types/client/components/workspace-switcher.d.ts +49 -0
  115. package/dist/types/client/index.d.ts +27 -5
  116. package/dist/types/client/provider.d.ts +109 -6
  117. package/dist/types/client/use-checkout.d.ts +8 -4
  118. package/dist/types/errors.d.ts +1 -1
  119. package/dist/types/index.d.ts +2 -0
  120. package/dist/types/next/index.d.ts +1 -1
  121. package/dist/types/server/client.d.ts +25 -2
  122. package/dist/types/server/http.d.ts +8 -2
  123. package/dist/types/server/index.d.ts +4 -0
  124. package/dist/types/server/resources/acting-as.d.ts +11 -0
  125. package/dist/types/server/resources/ai.d.ts +9 -1
  126. package/dist/types/server/resources/data.d.ts +258 -0
  127. package/dist/types/server/resources/kv.d.ts +63 -0
  128. package/dist/types/server/resources/platform.d.ts +32 -0
  129. package/dist/types/server/resources/workspaces.d.ts +55 -0
  130. package/dist/types/store.d.ts +96 -0
  131. package/dist/types/types.d.ts +319 -15
  132. package/dist/types/ui/empty-state.d.ts +34 -0
  133. package/dist/types/ui/index.d.ts +40 -0
  134. package/dist/types/ui/page-header.d.ts +34 -0
  135. package/dist/types/ui/page-shell.d.ts +39 -0
  136. package/dist/types/ui/section.d.ts +26 -0
  137. package/dist/types/ui/setting-row.d.ts +18 -0
  138. package/dist/types/ui/status-chip.d.ts +36 -0
  139. package/package.json +6 -2
@@ -0,0 +1,40 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectConfirmDialog = ConnectConfirmDialog;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("react");
7
+ const react_2 = require("@heroui/react");
8
+ /**
9
+ * The confirmation in front of a destructive action.
10
+ *
11
+ * **It closes on confirm, not on response.** Holding the modal open until the
12
+ * server answers makes every delete feel like a form submission, and the error
13
+ * path already has somewhere to go - a toast, or the row reappearing. A dialog
14
+ * that stays open to report failure is right only where the failure is
15
+ * *expected* and correctable in place (a seat cap, a last-admin invariant),
16
+ * and that is a blocking form, not this.
17
+ *
18
+ * `onConfirm` may return a promise. It is not awaited before closing, but a
19
+ * rejection is caught here rather than escaping - an error thrown out of an
20
+ * async transition reaches the nearest error boundary and takes the page down.
21
+ *
22
+ * The anatomy is HeroUI's: `Modal.Backdrop` owns the open state,
23
+ * `Modal.Container` the sizing, `Modal.Dialog` the focus trap. The trigger
24
+ * goes inside `Modal.Trigger`, which *is* a React Aria button - so pass a
25
+ * plain element, never a `<Button>`, or the nested button swallows the press
26
+ * and the dialog silently never opens.
27
+ */
28
+ function ConnectConfirmDialog({ trigger, title, description, confirmLabel = "Confirm", cancelLabel = "Cancel", tone = "danger", onConfirm, }) {
29
+ const [isOpen, setIsOpen] = (0, react_1.useState)(false);
30
+ const confirm = () => {
31
+ setIsOpen(false);
32
+ void Promise.resolve()
33
+ .then(() => onConfirm())
34
+ .catch(() => {
35
+ // Swallowed on purpose - see the note above. Reporting belongs to the
36
+ // caller, which owns the toast and the rollback.
37
+ });
38
+ };
39
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_2.Modal.Trigger, { onClick: () => setIsOpen(true), children: trigger }), (0, jsx_runtime_1.jsx)(react_2.Modal.Backdrop, { isOpen: isOpen, onOpenChange: setIsOpen, children: (0, jsx_runtime_1.jsx)(react_2.Modal.Container, { size: "sm", children: (0, jsx_runtime_1.jsxs)(react_2.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_2.Modal.CloseTrigger, { onPress: () => setIsOpen(false) }), (0, jsx_runtime_1.jsx)(react_2.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Heading, { children: title }) }), description && ((0, jsx_runtime_1.jsx)(react_2.Modal.Body, { children: (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: description }) })), (0, jsx_runtime_1.jsxs)(react_2.Modal.Footer, { children: [(0, jsx_runtime_1.jsx)(react_2.Button, { variant: "secondary", onPress: () => setIsOpen(false), children: cancelLabel }), (0, jsx_runtime_1.jsx)(react_2.Button, { variant: tone === "danger" ? "danger" : "primary", onPress: confirm, children: confirmLabel })] })] }) }) })] }));
40
+ }
@@ -0,0 +1,22 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectCursorPagination = ConnectCursorPagination;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("@heroui/react");
7
+ /**
8
+ * Next / previous for a cursor-paginated list.
9
+ *
10
+ * Cursor rather than page numbers, and the component shape follows from that:
11
+ * there is no "page 7 of 42" because a keyset-paginated list does not know how
12
+ * many pages there are without a second count query over the same filter -
13
+ * which is a billed index scan for a number nobody acts on. What a user does
14
+ * act on is "more" and "back", and those are exact.
15
+ *
16
+ * The count is announced in a live region so a screen-reader user learns that
17
+ * filtering or paging changed the result set. Without it the table silently
18
+ * swaps its contents.
19
+ */
20
+ function ConnectCursorPagination({ shown, nextCursor, hasPrevious, onNext, onPrevious, isPending = false, noun = ["row", "rows"], }) {
21
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between gap-3 pt-3", children: [(0, jsx_runtime_1.jsxs)("p", { "aria-live": "polite", className: "text-xs text-muted", children: [shown, " ", shown === 1 ? noun[0] : noun[1]] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2", children: [(0, jsx_runtime_1.jsx)(react_1.Button, { size: "sm", variant: "secondary", isDisabled: !hasPrevious || isPending, onPress: onPrevious, children: "Previous" }), (0, jsx_runtime_1.jsx)(react_1.Button, { size: "sm", variant: "secondary", isDisabled: nextCursor === null || isPending, onPress: () => nextCursor && onNext(nextCursor), children: "Next" })] })] }));
22
+ }
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectFilterTabs = ConnectFilterTabs;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("@heroui/react");
7
+ /**
8
+ * A single-select filter strip - the "All / Open / Won / Lost" row above a
9
+ * list.
10
+ *
11
+ * Real radio semantics rather than a row of buttons: it is a single choice
12
+ * from a closed set, so arrow keys should move between options and the
13
+ * selected one should be announced as selected. A group of `<button>`s gets
14
+ * neither, and that is the usual hand-rolled shape.
15
+ *
16
+ * Scrolls horizontally rather than wrapping on a narrow viewport - a filter
17
+ * strip that wraps to three lines pushes the list itself off the screen.
18
+ */
19
+ function ConnectFilterTabs({ options, value, onChange, label, }) {
20
+ return ((0, jsx_runtime_1.jsx)("div", { role: "radiogroup", "aria-label": label, className: "-mx-1 flex gap-2 overflow-x-auto px-1 pb-1", children: options.map((option) => {
21
+ const isSelected = option.value === value;
22
+ return ((0, jsx_runtime_1.jsx)("button", { type: "button", role: "radio", "aria-checked": isSelected, onClick: () => onChange(option.value), className: "shrink-0 rounded-full focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent", children: (0, jsx_runtime_1.jsxs)(react_1.Chip, { size: "sm", variant: isSelected ? "primary" : "soft", color: isSelected ? "accent" : "default", children: [option.label, option.count !== undefined && (0, jsx_runtime_1.jsx)("span", { className: "ml-1 opacity-70", children: option.count })] }) }, option.value ?? "__all"));
23
+ }) }));
24
+ }
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectGrievanceNotice = ConnectGrievanceNotice;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const provider_js_1 = require("../provider.js");
7
+ /**
8
+ * The DPDP s.13 grievance channel, rendered.
9
+ *
10
+ * Every application on the platform is a Data Processor under one notice, so
11
+ * the wording lives here rather than being retyped per application - a user
12
+ * comparing two applications' privacy pages should not find two different
13
+ * accounts of who their Data Fiduciary is.
14
+ *
15
+ * Renders nothing at all while loading and nothing on failure, rather than a
16
+ * skeleton or an error. That is unusual here and deliberate: this component
17
+ * is almost always inside a larger privacy page that has its own content, and
18
+ * a red error box in the middle of a legal notice reads as the notice itself
19
+ * being broken. The address is also on the public web either way. Pass
20
+ * `useConnectCompanyProfile()` directly to your own layout if you want to
21
+ * handle those states yourself.
22
+ */
23
+ function ConnectGrievanceNotice({ title = "Grievance redressal", applicationName, className, }) {
24
+ const { data } = (0, provider_js_1.useConnectCompanyProfile)();
25
+ const profile = data?.companyProfile;
26
+ if (!profile)
27
+ return null;
28
+ const { grievance } = profile;
29
+ return ((0, jsx_runtime_1.jsxs)("section", { className: className ?? "space-y-3", children: [title !== null && (0, jsx_runtime_1.jsx)("h3", { className: "text-base font-semibold", children: title }), (0, jsx_runtime_1.jsxs)("p", { className: "text-sm text-muted", children: [applicationName ? `${applicationName} is operated on ` : "This service runs on ", "the CosmoOps Connect platform. ", (0, jsx_runtime_1.jsx)("strong", { className: "font-medium text-foreground", children: profile.legalName }), " ", "is the Data Fiduciary for personal data processed here, under the Digital Personal Data Protection Act, 2023."] }), (0, jsx_runtime_1.jsxs)("dl", { className: "grid grid-cols-1 gap-x-6 gap-y-3 text-sm sm:grid-cols-[auto_1fr]", children: [(0, jsx_runtime_1.jsx)("dt", { className: "text-muted", children: "Grievance Officer" }), (0, jsx_runtime_1.jsx)("dd", { className: "text-foreground", children: grievance.officerName }), (0, jsx_runtime_1.jsx)("dt", { className: "text-muted", children: "Email" }), (0, jsx_runtime_1.jsx)("dd", { children: (0, jsx_runtime_1.jsx)("a", { className: "text-accent underline underline-offset-2", href: `mailto:${grievance.officerEmail}`, children: grievance.officerEmail }) }), (0, jsx_runtime_1.jsx)("dt", { className: "text-muted", children: "Contactable" }), (0, jsx_runtime_1.jsx)("dd", { className: "text-foreground", children: grievance.hours }), (0, jsx_runtime_1.jsx)("dt", { className: "text-muted", children: "Registered address" }), (0, jsx_runtime_1.jsx)("dd", { className: "text-foreground", children: profile.registeredAddress })] }), (0, jsx_runtime_1.jsxs)("p", { className: "text-sm text-muted", children: ["Complaints are acknowledged and resolved within ", grievance.responseDays, " days of receipt."] })] }));
30
+ }
@@ -5,20 +5,27 @@ exports.ConnectLinkButton = ConnectLinkButton;
5
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const react_1 = require("@heroui/react");
7
7
  /**
8
- * A HeroUI `Button` that renders a real `<a>`.
8
+ * A HeroUI `Button` that renders a link.
9
9
  *
10
- * A plain anchor, deliberately not a router link: this package imports nothing
11
- * from `next` (see the `./next` entry's doc comment for why that is a hard
12
- * rule), so it cannot know which router - if any - the host app uses. An
13
- * `<a href>` navigates under all of them, and a host that wants client-side
14
- * routing can pass its own element via `render` on the underlying `Button`.
10
+ * HeroUI v3's `Button` has no `as` prop, so `<Button as={Link}>` does not type
11
+ * check and does not work - this is the supported route, via `render`.
15
12
  *
16
13
  * The cast is the same one HeroUI's own "Render Function" example needs:
17
14
  * `render` types its argument as the props of the element `Button` would have
18
- * emitted, which is a `<button>` - including a `ref` typed to
15
+ * emitted, which is a `<button>`, including a `ref` typed to
19
16
  * `HTMLButtonElement`. Spreading that onto an anchor is sound at runtime and
20
- * unrepresentable in the type, which is exactly what a cast is for.
17
+ * unrepresentable in the type, which is what the cast is for.
21
18
  */
22
- function ConnectLinkButton({ href, ...props }) {
23
- return ((0, jsx_runtime_1.jsx)(react_1.Button, { ...props, render: (renderProps) => (0, jsx_runtime_1.jsx)("a", { ...renderProps, href: href }) }));
19
+ function ConnectLinkButton({ href, renderLink, ...props }) {
20
+ return ((0, jsx_runtime_1.jsx)(react_1.Button, { ...props, render: (renderProps) => {
21
+ const anchorProps = renderProps;
22
+ if (renderLink) {
23
+ return renderLink({
24
+ href,
25
+ ...(anchorProps.className ? { className: anchorProps.className } : {}),
26
+ children: anchorProps.children,
27
+ });
28
+ }
29
+ return (0, jsx_runtime_1.jsx)("a", { ...anchorProps, href: href });
30
+ } }));
24
31
  }
@@ -18,10 +18,11 @@ const mark_js_1 = require("../mark.js");
18
18
  * as `text-sm`, with the badge, gap and tracking all in proportion. That is
19
19
  * why there is no `size` prop.
20
20
  */
21
- function ConnectLogo({ name, icon, collapsed = false, className }) {
21
+ function ConnectLogo({ name, icon, variant, collapsed = false, className }) {
22
+ const resolved = variant ?? (collapsed ? "mark" : "full");
22
23
  const badge = ((0, jsx_runtime_1.jsxs)("span", { className: "relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-[0.28em] bg-accent text-accent-foreground", style: { width: "2em", height: "2em" }, "aria-hidden": "true", children: [icon ? (0, jsx_runtime_1.jsx)("span", { className: "inline-flex items-center justify-center [&>*]:size-[1.25em]", children: icon }) : null, !icon && (0, jsx_runtime_1.jsx)(mark_js_1.ConnectMark, { className: "absolute inset-0 size-full" })] }));
23
- if (collapsed) {
24
+ if (resolved === "mark") {
24
25
  return ((0, jsx_runtime_1.jsx)("span", { className: `inline-flex text-sm leading-none ${className ?? ""}`, role: "img", "aria-label": name, children: badge }));
25
26
  }
26
- return ((0, jsx_runtime_1.jsxs)("span", { className: `inline-flex min-w-0 items-center gap-[0.6em] text-sm leading-none ${className ?? ""}`, role: "img", "aria-label": `cosmoops ${name}`, children: [badge, (0, jsx_runtime_1.jsxs)("span", { className: "flex min-w-0 flex-col leading-none", children: [(0, jsx_runtime_1.jsxs)("span", { className: "inline-flex items-center font-extrabold tracking-tight text-foreground", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("span", { children: "cosmo" }), (0, jsx_runtime_1.jsx)("span", { className: "mx-[0.12em] inline-block size-[0.19em] shrink-0 rounded-[0.04em] bg-accent" }), (0, jsx_runtime_1.jsx)("span", { children: "ops" })] }), (0, jsx_runtime_1.jsx)("span", { className: "mt-[0.42em] truncate text-[0.6em] font-semibold uppercase tracking-[0.26em] text-accent", "aria-hidden": "true", children: name })] })] }));
27
+ return ((0, jsx_runtime_1.jsxs)("span", { className: `inline-flex min-w-0 items-center gap-[0.6em] text-sm leading-none ${className ?? ""}`, role: "img", "aria-label": `cosmoops ${name}`, children: [resolved === "full" && badge, (0, jsx_runtime_1.jsxs)("span", { className: "flex min-w-0 flex-col leading-none", children: [(0, jsx_runtime_1.jsxs)("span", { className: "inline-flex items-center font-extrabold tracking-tight text-foreground", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("span", { children: "cosmo" }), (0, jsx_runtime_1.jsx)("span", { className: "mx-[0.12em] inline-block size-[0.19em] shrink-0 rounded-[0.04em] bg-accent" }), (0, jsx_runtime_1.jsx)("span", { children: "ops" })] }), (0, jsx_runtime_1.jsx)("span", { className: "mt-[0.42em] truncate text-[0.6em] font-semibold uppercase tracking-[0.26em] text-accent", "aria-hidden": "true", children: name })] })] }));
27
28
  }
@@ -5,6 +5,7 @@ exports.flattenNav = flattenNav;
5
5
  exports.normalizePath = normalizePath;
6
6
  exports.resolveActivePath = resolveActivePath;
7
7
  exports.isNavItemActive = isNavItemActive;
8
+ exports.filterNavForAudience = filterNavForAudience;
8
9
  exports.gateExemptPaths = gateExemptPaths;
9
10
  function isSection(entry) {
10
11
  return !("href" in entry) && Array.isArray(entry.items);
@@ -89,6 +90,46 @@ function isNavItemActive(item, activeHref) {
89
90
  return false;
90
91
  return normalizePath(item.href) === activeHref;
91
92
  }
93
+ /**
94
+ * `nav` with every item the current session should not see removed - see
95
+ * `ConnectNavItem.audience`.
96
+ *
97
+ * Applied to nested `items` too, and a parent whose children all disappear is
98
+ * dropped along with them: a section header pointing at nothing reads as a
99
+ * broken link rather than as a hidden one.
100
+ */
101
+ function filterNavForAudience(nav, isStaff) {
102
+ const keep = (item) => item.audience === undefined || (item.audience === "staff") === isStaff;
103
+ const filterItem = (item) => {
104
+ if (!keep(item))
105
+ return null;
106
+ if (!item.items)
107
+ return item;
108
+ const items = item.items.filter(keep);
109
+ // A parent that had children and now has none, but is a real destination
110
+ // in its own right, still renders - only one that exists purely to hold
111
+ // them is dropped.
112
+ if (items.length === 0 && item.items.length > 0 && !item.href)
113
+ return null;
114
+ return { ...item, items };
115
+ };
116
+ // A plain loop rather than `flatMap`: the array's element type is a union,
117
+ // so a callback returning either shape widens past what `flatMap` accepts.
118
+ const out = [];
119
+ for (const entry of nav) {
120
+ if ("items" in entry && !("href" in entry)) {
121
+ const section = entry;
122
+ const items = section.items.map(filterItem).filter((i) => i !== null);
123
+ if (items.length > 0)
124
+ out.push({ ...section, items });
125
+ continue;
126
+ }
127
+ const item = filterItem(entry);
128
+ if (item)
129
+ out.push(item);
130
+ }
131
+ return out;
132
+ }
92
133
  /** Hrefs declared `gateExempt` - the routes `AppShell` renders outside its `ConnectAccessGate`. */
93
134
  function gateExemptPaths(nav) {
94
135
  return flattenNav(nav)
@@ -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;