@cosmoops/connect-sdk 0.9.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/CHANGELOG.md +566 -0
  2. package/README.md +113 -42
  3. package/dist/cjs/cli/doctor.js +3 -3
  4. package/dist/cjs/cli/init.js +3 -3
  5. package/dist/cjs/client/components/access-gate.js +20 -1
  6. package/dist/cjs/client/components/ai-screens.js +73 -0
  7. package/dist/cjs/client/components/ai-widgets.js +37 -0
  8. package/dist/cjs/client/components/app-shell.js +75 -41
  9. package/dist/cjs/client/components/apps-grid.js +3 -2
  10. package/dist/cjs/client/components/ask-ai.js +50 -5
  11. package/dist/cjs/client/components/confirm-dialog.js +40 -0
  12. package/dist/cjs/client/components/cursor-pagination.js +22 -0
  13. package/dist/cjs/client/components/filter-tabs.js +24 -0
  14. package/dist/cjs/client/components/grievance-notice.js +30 -0
  15. package/dist/cjs/client/components/link-button.js +17 -10
  16. package/dist/cjs/client/components/logo.js +4 -3
  17. package/dist/cjs/client/components/nav.js +41 -0
  18. package/dist/cjs/client/components/plan-pricing.js +27 -0
  19. package/dist/cjs/client/components/plans.js +1 -1
  20. package/dist/cjs/client/components/platform-gate.js +48 -0
  21. package/dist/cjs/client/components/shell-context.js +149 -0
  22. package/dist/cjs/client/components/side-nav.js +49 -23
  23. package/dist/cjs/client/components/sidebar.js +127 -0
  24. package/dist/cjs/client/components/subscribe-dialog.js +2 -2
  25. package/dist/cjs/client/components/subscription.js +1 -1
  26. package/dist/cjs/client/components/user-menu.js +96 -37
  27. package/dist/cjs/client/components/workspace-switcher.js +100 -0
  28. package/dist/cjs/client/index.js +72 -2
  29. package/dist/cjs/client/provider.js +57 -2
  30. package/dist/cjs/client/use-checkout.js +9 -5
  31. package/dist/cjs/errors.js +1 -1
  32. package/dist/cjs/index.js +16 -1
  33. package/dist/cjs/mcp/index.js +90 -0
  34. package/dist/cjs/mcp/tools.js +369 -0
  35. package/dist/cjs/next/index.js +2 -2
  36. package/dist/cjs/server/client.js +25 -2
  37. package/dist/cjs/server/handler.js +203 -18
  38. package/dist/cjs/server/http.js +6 -2
  39. package/dist/cjs/server/resources/acting-as.js +16 -0
  40. package/dist/cjs/server/resources/ai.js +22 -2
  41. package/dist/cjs/server/resources/data.js +374 -0
  42. package/dist/cjs/server/resources/identity.js +7 -3
  43. package/dist/cjs/server/resources/kv.js +84 -0
  44. package/dist/cjs/server/resources/platform.js +43 -0
  45. package/dist/cjs/server/resources/sessions.js +1 -1
  46. package/dist/cjs/server/resources/workspaces.js +74 -0
  47. package/dist/cjs/store-schema.js +66 -0
  48. package/dist/cjs/store.js +152 -0
  49. package/dist/cjs/ui/empty-state.js +28 -0
  50. package/dist/cjs/ui/index.js +44 -0
  51. package/dist/cjs/ui/page-header.js +27 -0
  52. package/dist/cjs/ui/page-shell.js +44 -0
  53. package/dist/cjs/ui/section.js +20 -0
  54. package/dist/cjs/ui/setting-row.js +14 -0
  55. package/dist/cjs/ui/status-chip.js +17 -0
  56. package/dist/esm/cli/doctor.js +3 -3
  57. package/dist/esm/cli/init.js +3 -3
  58. package/dist/esm/client/components/access-gate.js +20 -1
  59. package/dist/esm/client/components/ai-screens.js +69 -0
  60. package/dist/esm/client/components/ai-widgets.js +37 -1
  61. package/dist/esm/client/components/app-shell.js +81 -47
  62. package/dist/esm/client/components/apps-grid.js +3 -2
  63. package/dist/esm/client/components/ask-ai.js +51 -6
  64. package/dist/esm/client/components/confirm-dialog.js +37 -0
  65. package/dist/esm/client/components/cursor-pagination.js +19 -0
  66. package/dist/esm/client/components/filter-tabs.js +21 -0
  67. package/dist/esm/client/components/grievance-notice.js +27 -0
  68. package/dist/esm/client/components/link-button.js +17 -10
  69. package/dist/esm/client/components/logo.js +4 -3
  70. package/dist/esm/client/components/nav.js +40 -0
  71. package/dist/esm/client/components/plan-pricing.js +25 -0
  72. package/dist/esm/client/components/plans.js +2 -2
  73. package/dist/esm/client/components/platform-gate.js +45 -0
  74. package/dist/esm/client/components/shell-context.js +141 -0
  75. package/dist/esm/client/components/side-nav.js +48 -21
  76. package/dist/esm/client/components/sidebar.js +120 -0
  77. package/dist/esm/client/components/subscribe-dialog.js +3 -3
  78. package/dist/esm/client/components/subscription.js +2 -2
  79. package/dist/esm/client/components/user-menu.js +95 -38
  80. package/dist/esm/client/components/workspace-switcher.js +97 -0
  81. package/dist/esm/client/index.js +47 -4
  82. package/dist/esm/client/provider.js +53 -2
  83. package/dist/esm/client/use-checkout.js +9 -5
  84. package/dist/esm/errors.js +1 -1
  85. package/dist/esm/index.js +4 -0
  86. package/dist/esm/mcp/index.js +85 -0
  87. package/dist/esm/mcp/tools.js +366 -0
  88. package/dist/esm/next/index.js +2 -2
  89. package/dist/esm/server/client.js +25 -2
  90. package/dist/esm/server/handler.js +203 -18
  91. package/dist/esm/server/http.js +6 -2
  92. package/dist/esm/server/resources/acting-as.js +13 -0
  93. package/dist/esm/server/resources/ai.js +22 -2
  94. package/dist/esm/server/resources/data.js +369 -0
  95. package/dist/esm/server/resources/identity.js +7 -3
  96. package/dist/esm/server/resources/kv.js +80 -0
  97. package/dist/esm/server/resources/platform.js +39 -0
  98. package/dist/esm/server/resources/sessions.js +1 -1
  99. package/dist/esm/server/resources/workspaces.js +70 -0
  100. package/dist/esm/store-schema.js +62 -0
  101. package/dist/esm/store.js +141 -0
  102. package/dist/esm/ui/empty-state.js +25 -0
  103. package/dist/esm/ui/index.js +34 -0
  104. package/dist/esm/ui/page-header.js +24 -0
  105. package/dist/esm/ui/page-shell.js +41 -0
  106. package/dist/esm/ui/section.js +17 -0
  107. package/dist/esm/ui/setting-row.js +11 -0
  108. package/dist/esm/ui/status-chip.js +14 -0
  109. package/dist/types/client/components/ai-screens.d.ts +25 -0
  110. package/dist/types/client/components/ai-widgets.d.ts +34 -1
  111. package/dist/types/client/components/app-shell.d.ts +65 -27
  112. package/dist/types/client/components/apps-grid.d.ts +11 -2
  113. package/dist/types/client/components/ask-ai.d.ts +47 -1
  114. package/dist/types/client/components/confirm-dialog.d.ts +34 -0
  115. package/dist/types/client/components/cursor-pagination.d.ts +27 -0
  116. package/dist/types/client/components/filter-tabs.d.ts +26 -0
  117. package/dist/types/client/components/grievance-notice.d.ts +31 -0
  118. package/dist/types/client/components/link-button.d.ts +32 -9
  119. package/dist/types/client/components/logo.d.ts +18 -2
  120. package/dist/types/client/components/nav.d.ts +24 -0
  121. package/dist/types/client/components/plan-pricing.d.ts +10 -0
  122. package/dist/types/client/components/platform-gate.d.ts +53 -0
  123. package/dist/types/client/components/shell-context.d.ts +116 -0
  124. package/dist/types/client/components/side-nav.d.ts +27 -13
  125. package/dist/types/client/components/sidebar.d.ts +95 -0
  126. package/dist/types/client/components/user-menu.d.ts +68 -22
  127. package/dist/types/client/components/workspace-switcher.d.ts +49 -0
  128. package/dist/types/client/index.d.ts +29 -5
  129. package/dist/types/client/provider.d.ts +109 -6
  130. package/dist/types/client/use-checkout.d.ts +8 -4
  131. package/dist/types/errors.d.ts +1 -1
  132. package/dist/types/index.d.ts +6 -0
  133. package/dist/types/mcp/index.d.ts +74 -0
  134. package/dist/types/mcp/tools.d.ts +107 -0
  135. package/dist/types/next/index.d.ts +1 -1
  136. package/dist/types/server/client.d.ts +25 -2
  137. package/dist/types/server/http.d.ts +8 -2
  138. package/dist/types/server/index.d.ts +4 -0
  139. package/dist/types/server/resources/acting-as.d.ts +11 -0
  140. package/dist/types/server/resources/ai.d.ts +20 -2
  141. package/dist/types/server/resources/data.d.ts +258 -0
  142. package/dist/types/server/resources/kv.d.ts +63 -0
  143. package/dist/types/server/resources/platform.d.ts +32 -0
  144. package/dist/types/server/resources/workspaces.d.ts +55 -0
  145. package/dist/types/store-schema.d.ts +52 -0
  146. package/dist/types/store.d.ts +96 -0
  147. package/dist/types/types.d.ts +369 -16
  148. package/dist/types/ui/empty-state.d.ts +34 -0
  149. package/dist/types/ui/index.d.ts +40 -0
  150. package/dist/types/ui/page-header.d.ts +34 -0
  151. package/dist/types/ui/page-shell.d.ts +39 -0
  152. package/dist/types/ui/section.d.ts +26 -0
  153. package/dist/types/ui/setting-row.d.ts +18 -0
  154. package/dist/types/ui/status-chip.d.ts +36 -0
  155. package/package.json +15 -2
@@ -12,7 +12,11 @@ const apps_grid_js_1 = require("./apps-grid.js");
12
12
  const ask_ai_js_1 = require("./ask-ai.js");
13
13
  const auth_buttons_js_1 = require("./auth-buttons.js");
14
14
  const logo_js_1 = require("./logo.js");
15
+ const motion_js_1 = require("./motion.js");
16
+ const sidebar_js_1 = require("./sidebar.js");
15
17
  const side_nav_js_1 = require("./side-nav.js");
18
+ const shell_context_js_1 = require("./shell-context.js");
19
+ const workspace_switcher_js_1 = require("./workspace-switcher.js");
16
20
  const user_menu_js_1 = require("./user-menu.js");
17
21
  const nav_js_1 = require("./nav.js");
18
22
  const use_active_path_js_1 = require("../use-active-path.js");
@@ -37,7 +41,7 @@ function collapsedSnapshot() {
37
41
  /**
38
42
  * No preference on the server, which is the point: `useSyncExternalStore` uses
39
43
  * this for the hydrating render and only then swaps in the real reading, so a
40
- * returning user's collapsed rail can never make the server's HTML and the
44
+ * returning user's rail state can never make the server's HTML and the
41
45
  * client's first pass disagree. Reading storage during render instead is a
42
46
  * hydration mismatch waiting for its first user.
43
47
  */
@@ -55,36 +59,21 @@ function subscribeCollapsed(onStoreChange) {
55
59
  };
56
60
  }
57
61
  /**
58
- * The sidebar's brand row: the lockup when expanded, the badge alone when not.
59
- */
60
- function BrandLink({ name, icon, homeHref, collapsed, renderLink, }) {
61
- return renderLink({
62
- href: homeHref,
63
- className: `flex items-center rounded-lg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus ${collapsed ? "justify-center" : ""}`,
64
- // `ConnectLogo` names itself, but it does it with `role="img"` on a
65
- // `<span>`, which the `<a>` wrapping it does not inherit as its own name.
66
- "aria-label": `${name} home`,
67
- children: (0, jsx_runtime_1.jsx)(logo_js_1.ConnectLogo, { name: name, icon: icon, collapsed: collapsed }),
68
- });
69
- }
70
- /**
71
- * The application frame every CosmoOps application platform runs inside: a
72
- * collapsible sidebar, a top bar carrying the app launcher and the account
73
- * menu, and your pages in the middle.
62
+ * The application frame every CosmoOps application platform runs inside: one
63
+ * rail down the side holding the brand, the workspace, the navigation and the
64
+ * account, and your pages in the rest.
74
65
  *
75
66
  * This is the piece each partner was otherwise writing by hand, and writing
76
- * slightly differently - which is how a family of applications stops looking like
77
- * one product. Everything that should be identical across platforms (the
78
- * lockup, the rail geometry, the active-route treatment, the launcher, the
79
- * account menu, the access gate) lives here; everything that genuinely
80
- * differs is a prop:
67
+ * slightly differently - which is how a family of applications stops looking
68
+ * like one product. Everything that should be identical across platforms
69
+ * lives here; everything that genuinely differs is a prop:
81
70
  *
82
71
  * ```tsx
83
72
  * "use client";
84
73
  * import Link from "next/link";
85
74
  * import { usePathname } from "next/navigation";
86
75
  * import { AppShell } from "@cosmoops/connect-sdk/client";
87
- * import { LayoutDashboard, Truck } from "lucide-react";
76
+ * import { LayoutDashboard, Settings, Truck } from "lucide-react";
88
77
  *
89
78
  * export function Shell({ children }: { children: React.ReactNode }) {
90
79
  * return (
@@ -95,7 +84,10 @@ function BrandLink({ name, icon, homeHref, collapsed, renderLink, }) {
95
84
  * renderLink={(props) => <Link {...props} />}
96
85
  * nav={[
97
86
  * { label: "Overview", href: "/", icon: <LayoutDashboard className="size-5" /> },
98
- * { label: "Plans", href: "/plans", icon: <Truck className="size-5" />, gateExempt: true },
87
+ * { label: "Shipments", href: "/shipments", icon: <Truck className="size-5" /> },
88
+ * ]}
89
+ * secondaryNav={[
90
+ * { label: "Settings", href: "/settings", icon: <Settings className="size-5" /> },
99
91
  * ]}
100
92
  * >
101
93
  * {children}
@@ -104,16 +96,36 @@ function BrandLink({ name, icon, homeHref, collapsed, renderLink, }) {
104
96
  * }
105
97
  * ```
106
98
  *
99
+ * **There is no top bar.** There used to be one, carrying a page title, the
100
+ * launcher and the account menu - and it was a second, half-width strip of
101
+ * chrome saying things the rail was better placed to say. The launcher and
102
+ * the account row moved into the rail's footer, where they are one press from
103
+ * wherever the pointer already is; the page title moved into the page, where
104
+ * `ConnectPageShell` renders it next to that page's own actions and
105
+ * breadcrumbs. What is left is a single column of chrome and a full-height
106
+ * content pane, which is what every application this shell is modelled on
107
+ * settled at.
108
+ *
109
+ * The one exception is below `md`, where a slim bar carries the drawer
110
+ * trigger. A phone has no rail to put it in, and a navigation you cannot open
111
+ * is not a smaller navigation.
112
+ *
113
+ * **The rail starts collapsed.** `defaultCollapsed` is `true`: the icon rail
114
+ * is the resting state, and expanding is the deliberate act. That is the right
115
+ * default because the rail's job is to be *there*, not to be read - a person
116
+ * learns five glyphs in a day and then wants the 200px back for their data.
117
+ * A stored preference from the user's last visit always wins over it.
118
+ *
107
119
  * There is no `isSignedIn` branch anywhere in here, and there should be none
108
- * in yours: the auth buttons render nothing once a session exists, the user
109
- * menu renders nothing without one, and the launcher renders nothing when
120
+ * in yours: the auth buttons render nothing once a session exists, the account
121
+ * row renders nothing without one, and the launcher renders nothing when
110
122
  * signed out. The swap happens inside the components.
111
123
  *
112
124
  * It renders no provider of its own - mount `ConnectProvider` above it, once,
113
125
  * so a page with a shell, a plan table and a usage meter still makes one
114
126
  * session read.
115
127
  */
116
- function AppShell({ name, icon, homeHref = "/", nav, activePath, renderLink = side_nav_js_1.defaultRenderLink, title, headerActions, sidebarFooter, footer, defaultCollapsed = false, collapsed: controlledCollapsed, onCollapsedChange, collapsible = true, showAppsGrid = true, showAuthButtons = true, theme, onThemeChange, gate = true, gateExemptRoutes, askAi = true, className, contentClassName, children, }) {
128
+ function AppShell({ name, icon, homeHref = "/", nav, secondaryNav, activePath, renderLink = shell_context_js_1.defaultRenderLink, sidebarFooter, workspaceSwitcher, footer, defaultCollapsed = true, collapsed: controlledCollapsed, onCollapsedChange, collapsible = true, showAppsGrid = true, showAuthButtons = true, theme, onThemeChange, gate = true, gateExemptRoutes, askAi = true, className, contentClassName, children, }) {
117
129
  const { session, isLoading } = (0, provider_js_1.useConnect)();
118
130
  const pathname = (0, use_active_path_js_1.useActivePath)(activePath);
119
131
  const [mobileOpen, setMobileOpen] = (0, react_1.useState)(false);
@@ -123,8 +135,7 @@ function AppShell({ name, icon, homeHref = "/", nav, activePath, renderLink = si
123
135
  const stored = (0, react_1.useSyncExternalStore)(subscribeCollapsed, collapsedSnapshot, serverCollapsedSnapshot);
124
136
  const isControlled = controlledCollapsed !== undefined;
125
137
  const collapsed = collapsible ? (isControlled ? controlledCollapsed : (stored ?? defaultCollapsed)) : false;
126
- const toggleCollapsed = (0, react_1.useCallback)(() => {
127
- const next = !collapsed;
138
+ const setCollapsed = (0, react_1.useCallback)((next) => {
128
139
  onCollapsedChange?.(next);
129
140
  if (isControlled)
130
141
  return;
@@ -135,24 +146,47 @@ function AppShell({ name, icon, homeHref = "/", nav, activePath, renderLink = si
135
146
  // Not persisted - but the event below still moves the rail for this session.
136
147
  }
137
148
  window.dispatchEvent(new Event(COLLAPSED_EVENT));
138
- }, [collapsed, isControlled, onCollapsedChange]);
149
+ }, [isControlled, onCollapsedChange]);
139
150
  const closeMobile = (0, react_1.useCallback)(() => setMobileOpen(false), []);
151
+ // Connect platform staff see a different set of routes - see
152
+ // `ConnectNavItem.audience`. Derived from the session rather than passed in,
153
+ // so a host app never has to thread "is this an operator" through its own
154
+ // layout to get the right rail.
155
+ const isStaff = session?.staff === true;
156
+ const visibleNav = (0, react_1.useMemo)(() => (0, nav_js_1.filterNavForAudience)(nav, isStaff), [nav, isStaff]);
157
+ const visibleSecondaryNav = (0, react_1.useMemo)(() => (secondaryNav ? (0, nav_js_1.filterNavForAudience)(secondaryNav, isStaff) : undefined), [secondaryNav, isStaff]);
140
158
  const currentPath = (0, nav_js_1.normalizePath)(pathname);
141
- const exempt = [...(0, nav_js_1.gateExemptPaths)(nav), ...(gateExemptRoutes ?? []).map(nav_js_1.normalizePath)];
159
+ // Computed from the *unfiltered* nav on purpose: `gateExempt` is a fact
160
+ // about a route, not about who can see it, and reading it from the filtered
161
+ // list would quietly re-gate a route the moment it became staff-only. Both
162
+ // navs contribute - a `gateExempt` "Plans" route is just as often in the
163
+ // secondary group as the primary one.
164
+ const exempt = [
165
+ ...(0, nav_js_1.gateExemptPaths)(nav),
166
+ ...(secondaryNav ? (0, nav_js_1.gateExemptPaths)(secondaryNav) : []),
167
+ ...(gateExemptRoutes ?? []).map(nav_js_1.normalizePath),
168
+ ];
142
169
  const isExempt = exempt.some((path) => currentPath === path || currentPath.startsWith(`${path}/`));
143
170
  // A signed-out visitor has no readiness to check and nothing to be blocked
144
171
  // by; `ConnectAccessGate` passes them straight through, but mounting it at
145
172
  // all costs an entitlements read that cannot succeed.
146
173
  const signedOut = !isLoading && !session?.user;
147
174
  const gated = gate && !isExempt && !signedOut;
148
- const sideNav = (mobile) => ((0, jsx_runtime_1.jsx)(side_nav_js_1.ConnectSideNav, { nav: nav, activePath: pathname, renderLink: renderLink, collapsed: mobile ? false : collapsed, onNavigate: mobile ? closeMobile : undefined }));
149
- return ((0, jsx_runtime_1.jsxs)("div", { className: `flex h-screen bg-background ${className ?? ""}`, children: [(0, jsx_runtime_1.jsxs)("aside", { "aria-label": "Sidebar", className: `hidden shrink-0 flex-col border-r border-border bg-surface transition-[width] duration-200 ease-in-out will-change-[width] md:flex ${collapsed ? "w-16" : "w-64"}`, children: [(0, jsx_runtime_1.jsx)("div", { className: `flex h-16 shrink-0 items-center border-b border-border ${collapsed ? "justify-center px-2" : "px-4"}`, children: (0, jsx_runtime_1.jsx)(BrandLink, { name: name, icon: icon, homeHref: homeHref, collapsed: collapsed, renderLink: renderLink }) }), (0, jsx_runtime_1.jsx)("div", { className: `flex-1 overflow-y-auto py-4 ${collapsed ? "px-2" : "px-3"}`, children: sideNav(false) }), (0, jsx_runtime_1.jsxs)("div", { className: `shrink-0 border-t border-border ${collapsed ? "flex flex-col items-center gap-2 p-2" : "flex flex-col gap-2 p-3"}`, children: [!collapsed && sidebarFooter, collapsible && (
150
- // The button goes in as a *direct* child, the way `Modal` and
151
- // `Dropdown` take theirs. `Tooltip.Trigger` renders a wrapper
152
- // `<div>` and puts the props it is given on that div - so an
153
- // `aria-label` passed there names a div nobody can reach and
154
- // leaves this icon-only button with no accessible name at all.
155
- // The wrapper is for trigger elements React Aria cannot drive
156
- // directly; a HeroUI `Button` is not one.
157
- (0, jsx_runtime_1.jsxs)(react_2.Tooltip, { delay: 200, children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "tertiary", size: "sm", "aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar", className: collapsed ? "" : "self-start", onPress: toggleCollapsed, children: collapsed ? (0, jsx_runtime_1.jsx)(lucide_react_1.PanelLeftOpen, { className: "size-4" }) : (0, jsx_runtime_1.jsx)(lucide_react_1.PanelLeftClose, { className: "size-4" }) }), (0, jsx_runtime_1.jsx)(react_2.Tooltip.Content, { placement: "right", children: collapsed ? "Expand sidebar" : "Collapse sidebar" })] }))] })] }), (0, jsx_runtime_1.jsx)(react_2.Drawer.Backdrop, { isOpen: mobileOpen, onOpenChange: setMobileOpen, children: (0, jsx_runtime_1.jsx)(react_2.Drawer.Content, { placement: "left", className: "w-72 max-w-[80vw]", children: (0, jsx_runtime_1.jsxs)(react_2.Drawer.Dialog, { "aria-label": "Navigation menu", children: [(0, jsx_runtime_1.jsx)(react_2.Drawer.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_2.Drawer.Header, { children: (0, jsx_runtime_1.jsx)(react_2.Drawer.Heading, { "aria-label": name, children: (0, jsx_runtime_1.jsx)(logo_js_1.ConnectLogo, { name: name, icon: icon }) }) }), (0, jsx_runtime_1.jsx)(react_2.Drawer.Body, { children: sideNav(true) }), sidebarFooter && (0, jsx_runtime_1.jsx)(react_2.Drawer.Footer, { className: "flex flex-col items-stretch gap-3", children: sidebarFooter })] }) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col", children: [(0, jsx_runtime_1.jsxs)("header", { className: "flex h-16 shrink-0 items-center gap-3 border-b border-border bg-surface px-4 md:px-6", children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "tertiary", size: "sm", className: "md:hidden", "aria-label": "Open navigation menu", onPress: () => setMobileOpen(true), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Menu, { className: "size-5" }) }), typeof title === "string" ? ((0, jsx_runtime_1.jsx)("h1", { className: "truncate text-lg font-semibold text-foreground", children: title })) : (title), (0, jsx_runtime_1.jsxs)("div", { className: "ml-auto flex items-center gap-1", children: [headerActions, showAppsGrid && (0, jsx_runtime_1.jsx)(apps_grid_js_1.ConnectAppsGrid, {}), (0, jsx_runtime_1.jsx)(user_menu_js_1.ConnectUserMenu, { compact: true, theme: theme, onThemeChange: onThemeChange }), showAuthButtons && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectLoginButton, { size: "sm" }), (0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectRegisterButton, { size: "sm" })] }))] })] }), (0, jsx_runtime_1.jsx)("main", { className: "flex-1 overflow-y-auto", children: (0, jsx_runtime_1.jsxs)("div", { className: contentClassName ?? "mx-auto w-full max-w-6xl px-4 py-6 md:px-6 md:py-8", children: [gated ? (0, jsx_runtime_1.jsx)(access_gate_js_1.ConnectAccessGate, { children: children }) : children, footer] }) })] }), askAi && (0, jsx_runtime_1.jsx)(ask_ai_js_1.ConnectAskAi, { ...(askAi === true ? {} : askAi) })] }));
175
+ // Two shell contexts, one set of children. The drawer's is always expanded
176
+ // and closes itself on navigation; the rail's carries the real collapse
177
+ // state. Building them here rather than inside the sidebar is what lets the
178
+ // same `railContent` render in both places with no `mobile` prop threaded
179
+ // through every part.
180
+ const railShell = (0, react_1.useMemo)(() => ({ collapsed, collapsible, setCollapsed, renderLink, activePath }), [collapsed, collapsible, setCollapsed, renderLink, activePath]);
181
+ const drawerShell = (0, react_1.useMemo)(() => ({
182
+ collapsed: false,
183
+ collapsible: false,
184
+ setCollapsed: () => { },
185
+ renderLink,
186
+ activePath,
187
+ onNavigate: closeMobile,
188
+ }), [renderLink, activePath, closeMobile]);
189
+ const accountRow = signedOut ? (showAuthButtons && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectLoginButton, { size: "sm", fullWidth: true }), (0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectRegisterButton, { size: "sm", fullWidth: true })] }))) : ((0, jsx_runtime_1.jsx)(user_menu_js_1.ConnectUserButton, { variant: "row", theme: theme, onThemeChange: onThemeChange }));
190
+ const railContent = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarHeader, { name: name, icon: icon, homeHref: homeHref }), workspaceSwitcher !== false && ((0, jsx_runtime_1.jsx)("div", { className: "shrink-0 px-3 pb-1", children: (0, jsx_runtime_1.jsx)(workspace_switcher_js_1.ConnectWorkspaceSwitcher, {}) })), (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarBody, { children: (0, jsx_runtime_1.jsx)(side_nav_js_1.ConnectSidebarNav, { nav: visibleNav }) }), (visibleSecondaryNav || sidebarFooter || showAppsGrid) && ((0, jsx_runtime_1.jsxs)(sidebar_js_1.ConnectSidebarFooter, { separated: false, children: [visibleSecondaryNav && (0, jsx_runtime_1.jsx)(side_nav_js_1.ConnectSidebarNav, { nav: visibleSecondaryNav }), sidebarFooter, showAppsGrid && (0, jsx_runtime_1.jsx)(apps_grid_js_1.ConnectAppsGrid, { variant: "row" })] })), accountRow && (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarFooter, { children: accountRow })] }));
191
+ return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)("div", { className: `flex h-screen bg-background ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(shell_context_js_1.ConnectShellProvider, { value: railShell, children: (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebar, { children: railContent }) }), (0, jsx_runtime_1.jsx)(react_2.Drawer.Backdrop, { isOpen: mobileOpen, onOpenChange: setMobileOpen, children: (0, jsx_runtime_1.jsx)(react_2.Drawer.Content, { placement: "left", className: "w-[min(20rem,85vw)]", children: (0, jsx_runtime_1.jsx)(react_2.Drawer.Dialog, { "aria-label": "Navigation menu", className: "p-0", children: (0, jsx_runtime_1.jsx)(shell_context_js_1.ConnectShellProvider, { value: drawerShell, children: (0, jsx_runtime_1.jsx)("div", { className: "flex h-full flex-col", children: railContent }) }) }) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col", children: [(0, jsx_runtime_1.jsxs)("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border bg-surface px-3 md:hidden", children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "tertiary", size: "sm", "aria-label": "Open navigation menu", onPress: () => setMobileOpen(true), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Menu, { className: "size-5" }) }), (0, jsx_runtime_1.jsx)(logo_js_1.ConnectLogo, { name: name, icon: icon })] }), (0, jsx_runtime_1.jsx)("main", { className: "flex-1 overflow-y-auto", children: (0, jsx_runtime_1.jsxs)("div", { className: contentClassName ?? "mx-auto w-full max-w-6xl px-4 py-6 md:px-8 md:py-8", children: [gated ? (0, jsx_runtime_1.jsx)(access_gate_js_1.ConnectAccessGate, { children: children }) : children, footer] }) })] }), askAi && (0, jsx_runtime_1.jsx)(ask_ai_js_1.ConnectAskAi, { ...(askAi === true ? {} : askAi) })] }) }));
158
192
  }
@@ -7,6 +7,7 @@ const react_1 = require("@heroui/react");
7
7
  const lucide_react_1 = require("lucide-react");
8
8
  const provider_js_1 = require("../provider.js");
9
9
  const motion_js_1 = require("./motion.js");
10
+ const shell_context_js_1 = require("./shell-context.js");
10
11
  function AppTile({ app }) {
11
12
  const disabled = app.launchUrl === null;
12
13
  const body = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("span", { className: "relative flex size-12 items-center justify-center overflow-hidden rounded-xl bg-surface-secondary", children: [app.iconUrl ? (
@@ -33,13 +34,13 @@ function AppTile({ app }) {
33
34
  * Renders nothing when signed out: a launcher with nothing to launch is just a
34
35
  * button that opens an empty box.
35
36
  */
36
- function ConnectAppsGrid({ className, label = "Switch app" }) {
37
+ function ConnectAppsGrid({ className, label = "Switch app", variant = "row" }) {
37
38
  const { session, isLoading: sessionLoading } = (0, provider_js_1.useConnect)();
38
39
  const { data, isLoading, error } = (0, provider_js_1.useConnectApps)();
39
40
  if (sessionLoading || !session?.user)
40
41
  return null;
41
42
  const apps = data?.apps ?? [];
42
- return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)(react_1.Modal, { children: [(0, jsx_runtime_1.jsx)(react_1.Button, { isIconOnly: true, variant: "primary", "aria-label": label, className: className, children: (0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { className: "size-5" }) }), (0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { className: "sm:max-w-md", children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: "Your apps" }) }), (0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { children: [isLoading && ((0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-3 gap-2", children: Array.from({ length: 6 }, (_, index) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-center gap-2 p-3", children: [(0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "size-12 rounded-xl" }), (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "h-3 w-14" })] }, index))) })), !isLoading && error && ((0, jsx_runtime_1.jsx)("p", { className: "py-6 text-center text-sm text-danger", children: "Could not load your apps." })), !isLoading && !error && apps.length === 0 && ((0, jsx_runtime_1.jsx)("p", { className: "py-6 text-center text-sm text-muted", children: "No other apps yet. Anything this workspace subscribes to shows up here." })), !isLoading && !error && apps.length > 0 && ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { className: "grid grid-cols-3 gap-2", variants: { show: { transition: { staggerChildren: 0.04 } } }, initial: "hidden", animate: "show", children: apps.map((app) => ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { variants: { hidden: { opacity: 0, y: 8 }, show: { opacity: 1, y: 0, transition: motion_js_1.ENTER } }, children: (0, jsx_runtime_1.jsx)(AppTile, { app: app }) }, app.applicationId))) }))] }), (0, jsx_runtime_1.jsx)(react_1.Modal.Footer, { children: (0, jsx_runtime_1.jsx)(ConnectBrowseAllLink, {}) })] }) }) })] }) }));
43
+ return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)(react_1.Modal, { children: [variant === "row" ? ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", "aria-label": label, className: (0, shell_context_js_1.sidebarRowClass)({ className }), children: [(0, jsx_runtime_1.jsx)("span", { className: "flex size-5 shrink-0 items-center justify-center", children: (0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { className: "size-5" }) }), (0, jsx_runtime_1.jsx)(shell_context_js_1.SidebarLabel, { children: label })] })) : ((0, jsx_runtime_1.jsx)(react_1.Button, { isIconOnly: true, variant: "primary", "aria-label": label, className: className, children: (0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { className: "size-5" }) })), (0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { className: "sm:max-w-md", children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: "Your apps" }) }), (0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { children: [isLoading && ((0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-3 gap-2", children: Array.from({ length: 6 }, (_, index) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-center gap-2 p-3", children: [(0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "size-12 rounded-xl" }), (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "h-3 w-14" })] }, index))) })), !isLoading && error && ((0, jsx_runtime_1.jsx)("p", { className: "py-6 text-center text-sm text-danger", children: "Could not load your apps." })), !isLoading && !error && apps.length === 0 && ((0, jsx_runtime_1.jsx)("p", { className: "py-6 text-center text-sm text-muted", children: "No other apps yet. Anything this workspace subscribes to shows up here." })), !isLoading && !error && apps.length > 0 && ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { className: "grid grid-cols-3 gap-2", variants: { show: { transition: { staggerChildren: 0.04 } } }, initial: "hidden", animate: "show", children: apps.map((app) => ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { variants: { hidden: { opacity: 0, y: 8 }, show: { opacity: 1, y: 0, transition: motion_js_1.ENTER } }, children: (0, jsx_runtime_1.jsx)(AppTile, { app: app }) }, app.applicationId))) }))] }), (0, jsx_runtime_1.jsx)(react_1.Modal.Footer, { children: (0, jsx_runtime_1.jsx)(ConnectBrowseAllLink, {}) })] }) }) })] }) }));
43
44
  }
44
45
  function ConnectBrowseAllLink() {
45
46
  const { appUrl } = (0, provider_js_1.useConnect)();
@@ -8,8 +8,29 @@ const react_2 = require("@heroui/react");
8
8
  const lucide_react_1 = require("lucide-react");
9
9
  const streamdown_1 = require("streamdown");
10
10
  const provider_js_1 = require("../provider.js");
11
+ const use_active_path_js_1 = require("../use-active-path.js");
12
+ const ai_screens_js_1 = require("./ai-screens.js");
11
13
  const ai_widgets_js_1 = require("./ai-widgets.js");
14
+ /**
15
+ * The panel's open width. The *inner* pane always carries it, so the content
16
+ * keeps its layout while the outer shell's width animates to zero around it.
17
+ */
12
18
  const PANEL_WIDTH = "w-full lg:w-[400px]";
19
+ /**
20
+ * The outer shell's width, which is the thing that animates.
21
+ *
22
+ * Two mutually exclusive branches rather than `PANEL_WIDTH` plus an
23
+ * `lg:w-0` override, because that override never applied: `lg:w-[400px]` and
24
+ * `lg:w-0` are the same utility at the same breakpoint, so the winner is
25
+ * whichever Tailwind emits later - and it emits arbitrary values after scale
26
+ * values. The closed panel therefore measured a full 400px at `lg` and up and
27
+ * sat there permanently, eating that much of the content pane on every page
28
+ * with `askAi` on. Only the `translate-x` hid it below `lg`, which is why this
29
+ * looked correct on a phone and wrong on a laptop.
30
+ */
31
+ function panelShellWidth(isOpen) {
32
+ return isOpen ? PANEL_WIDTH : "w-full lg:w-0 lg:overflow-hidden lg:border-s-0";
33
+ }
13
34
  /**
14
35
  * `blurIn` over the default `fadeIn`: Gemini arrives in bursts rather than an
15
36
  * even token drip, and a blur masks a batch landing at once where a plain
@@ -47,7 +68,7 @@ function ThinkingDisclosure({ text, isStreaming }) {
47
68
  */
48
69
  const isArray = (value) => Array.isArray(value) && value.length > 0;
49
70
  const field = (input, key) => typeof input === "object" && input !== null ? input[key] : undefined;
50
- function renderTool(name, input, renderMarkdown) {
71
+ function renderTool(name, input, renderMarkdown, screens, renderLink) {
51
72
  switch (name) {
52
73
  case "showTable":
53
74
  return isArray(field(input, "columns")) && Array.isArray(field(input, "rows")) ? ((0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiTable, { ...input })) : null;
@@ -63,6 +84,12 @@ function renderTool(name, input, renderMarkdown) {
63
84
  return isArray(field(input, "steps")) ? ((0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiSteps, { ...input, renderDetail: renderMarkdown })) : null;
64
85
  case "showSources":
65
86
  return isArray(field(input, "sources")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiSources, { ...input }) : null;
87
+ case "showActions":
88
+ // Every href is checked against the declared screens before anything is
89
+ // drawn - see `AiActions`. With none declared, nothing resolves and the
90
+ // tool renders nothing, which is the correct outcome rather than a
91
+ // collection of dead links.
92
+ return isArray(field(input, "actions")) ? ((0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiActions, { ...input, screens: screens, renderLink: renderLink })) : null;
66
93
  default:
67
94
  return null;
68
95
  }
@@ -204,7 +231,7 @@ function appendText(blocks, text) {
204
231
  * Neither is something the browser can discover, so both surface as a message
205
232
  * in the panel rather than as a hidden button.
206
233
  */
207
- function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placeholder = "Ask a question...", hint = "Ask a question about your workspace - answers are grounded in what your team has indexed.", defaultOpen = false, markdownPlugins, }) {
234
+ function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placeholder = "Ask a question...", hint = "Ask a question about your workspace - answers are grounded in what your team has indexed.", defaultOpen = false, markdownPlugins, screens = [], activePath, renderLink, }) {
208
235
  const { session, isLoading, streamResource } = (0, provider_js_1.useConnect)();
209
236
  const [isOpen, setIsOpen] = (0, react_1.useState)(defaultOpen);
210
237
  const [turns, setTurns] = (0, react_1.useState)([]);
@@ -223,6 +250,16 @@ function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placehold
223
250
  // A conversation left mid-stream should stop costing tokens the moment the
224
251
  // panel goes away.
225
252
  (0, react_1.useEffect)(() => () => abortRef.current?.abort(), []);
253
+ /**
254
+ * The declared screen the user is on, if any.
255
+ *
256
+ * Drives the opening chips here and travels with each message so the answer
257
+ * is written from where they are standing. `null` whenever nothing matches -
258
+ * an application need only describe the screens it wants the assistant to
259
+ * know about.
260
+ */
261
+ const resolvedPath = (0, use_active_path_js_1.useActivePath)(activePath);
262
+ const currentScreen = (0, react_1.useMemo)(() => (resolvedPath ? (0, ai_screens_js_1.matchConnectScreen)(screens, resolvedPath) : null), [screens, resolvedPath]);
226
263
  const renderMarkdown = (0, react_1.useCallback)((text, isStreaming = false) => ((0, jsx_runtime_1.jsx)(streamdown_1.Streamdown, { className: "connect-ai-body", mode: isStreaming ? "streaming" : "static", isAnimating: isStreaming, animated: ANIMATED, caret: isStreaming ? "block" : undefined, plugins: markdownPlugins,
227
264
  // Retrieved documents cite real URLs; an interstitial on every one of
228
265
  // them turns a citation into a chore. The renderer still sanitises the
@@ -267,7 +304,15 @@ function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placehold
267
304
  try {
268
305
  // Relative to the provider's own `basePath` - `streamResource` prefixes
269
306
  // it, the same way every other bridge call is addressed.
270
- const response = await streamResource("/ai/chat", { messages: history }, { signal: controller.signal });
307
+ const response = await streamResource("/ai/chat", {
308
+ messages: history,
309
+ // What the assistant is told about where the user is, and what your
310
+ // application's screens are. Both are prose: Connect uses them to
311
+ // write a better answer and to validate action links, never to
312
+ // decide what anyone may do. See `ConnectScreen`.
313
+ ...(resolvedPath ? { screenContext: { pathname: resolvedPath } } : {}),
314
+ ...(screens.length > 0 ? { screens } : {}),
315
+ }, { signal: controller.signal });
271
316
  // The assistant's turn is appended only once the stream is live, so a
272
317
  // failed request leaves an error under the question rather than an
273
318
  // empty bubble that never fills.
@@ -314,7 +359,7 @@ function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placehold
314
359
  // The user's own turn is shown as they typed it - running a
315
360
  // question that happens to start with `#` through the markdown
316
361
  // renderer would turn it into a heading.
317
- (0, jsx_runtime_1.jsx)("div", { children: turn.role === "user" ? ((0, jsx_runtime_1.jsx)("p", { className: "whitespace-pre-wrap", children: block.text })) : (renderMarkdown(block.text, isLive && index === lastTextIndex)) }, index)) : block.input === null ? ((0, jsx_runtime_1.jsx)(ToolSkeleton, {}, block.id)) : ((0, jsx_runtime_1.jsx)("div", { children: renderTool(block.name, block.input, renderMarkdown) }, block.id))), isLive && lastTextIndex === -1 && turn.blocks.length === 0 && (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] }, turn.id));
362
+ (0, jsx_runtime_1.jsx)("div", { children: turn.role === "user" ? ((0, jsx_runtime_1.jsx)("p", { className: "whitespace-pre-wrap", children: block.text })) : (renderMarkdown(block.text, isLive && index === lastTextIndex)) }, index)) : block.input === null ? ((0, jsx_runtime_1.jsx)(ToolSkeleton, {}, block.id)) : ((0, jsx_runtime_1.jsx)("div", { children: renderTool(block.name, block.input, renderMarkdown, screens, renderLink) }, block.id))), isLive && lastTextIndex === -1 && turn.blocks.length === 0 && (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] }, turn.id));
318
363
  }), [turns, isAnswering, lastTurn?.id, assistantName, renderMarkdown]);
319
364
  // Signed out there is no workspace to ground an answer in, and the bridge
320
365
  // would answer 401 - so there is nothing to offer, not even a button.
@@ -328,7 +373,7 @@ function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placehold
328
373
  setInput("");
329
374
  void send(text);
330
375
  };
331
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!isOpen && ((0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "primary", "aria-label": title, onPress: () => setIsOpen(true), className: "fixed bottom-4 end-4 z-30 size-12 rounded-full shadow-lg sm:bottom-6 sm:end-6", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-5" }) })), (0, jsx_runtime_1.jsx)("div", { className: `fixed inset-0 z-40 bg-backdrop transition-opacity lg:hidden ${isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"}`, onClick: () => setIsOpen(false), "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("aside", { "aria-hidden": !isOpen, className: `fixed inset-y-0 end-0 z-50 flex flex-col border-s border-border bg-surface shadow-xl transition-transform duration-300 ease-in-out ${PANEL_WIDTH} ${isOpen ? "translate-x-0" : "translate-x-full rtl:-translate-x-full"} lg:static lg:z-auto lg:translate-x-0 lg:shadow-none lg:transition-[width] ${isOpen ? "" : "lg:w-0 lg:overflow-hidden lg:border-s-0"}`, children: (0, jsx_runtime_1.jsxs)("div", { className: `flex h-full shrink-0 flex-col ${PANEL_WIDTH}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2 border-b border-border px-4 py-3", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-4 shrink-0 text-accent" }), (0, jsx_runtime_1.jsx)("span", { className: "flex-1 text-sm font-semibold", children: title }), (0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "tertiary", "aria-label": "Close", onPress: () => setIsOpen(false), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: "size-4" }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [turns.length === 0 && (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: hint }), rendered, isBusy && lastTurn?.role === "user" && ((0, jsx_runtime_1.jsxs)("div", { className: "rounded-md border border-border bg-accent/5 p-2.5 text-sm", children: [(0, jsx_runtime_1.jsx)("p", { className: "mb-1 text-[11px] font-semibold tracking-wide text-muted uppercase", children: assistantName }), (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] })), error && (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-danger", children: error }), (0, jsx_runtime_1.jsx)("div", { ref: bottomRef })] }), (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex gap-2 border-t border-border p-3", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", className: "flex-1", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { className: "sr-only", children: placeholder }), (0, jsx_runtime_1.jsx)(react_2.Input, { placeholder: placeholder, value: input, onChange: (event) => setInput(event.currentTarget.value), disabled: isBusy })] }), isBusy ? ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "button", isIconOnly: true, variant: "secondary", "aria-label": "Stop generating", onPress: stop, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Square, { className: "size-3.5" }) })) : ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "submit", isIconOnly: true, variant: "primary", "aria-label": "Send", isDisabled: !input.trim(), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Send, { className: "size-3.5" }) }))] })] }) })] }));
376
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!isOpen && ((0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "primary", "aria-label": title, onPress: () => setIsOpen(true), className: "fixed bottom-4 end-4 z-30 size-12 rounded-full shadow-lg sm:bottom-6 sm:end-6", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-5" }) })), (0, jsx_runtime_1.jsx)("div", { className: `fixed inset-0 z-40 bg-backdrop transition-opacity lg:hidden ${isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"}`, onClick: () => setIsOpen(false), "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("aside", { "aria-hidden": !isOpen, className: `fixed inset-y-0 end-0 z-50 flex flex-col border-s border-border bg-surface shadow-xl transition-transform duration-300 ease-in-out ${panelShellWidth(isOpen)} ${isOpen ? "translate-x-0" : "translate-x-full rtl:-translate-x-full"} lg:static lg:z-auto lg:translate-x-0 lg:shadow-none lg:transition-[width]`, children: (0, jsx_runtime_1.jsxs)("div", { className: `flex h-full shrink-0 flex-col ${PANEL_WIDTH}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2 border-b border-border px-4 py-3", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-4 shrink-0 text-accent" }), (0, jsx_runtime_1.jsx)("span", { className: "flex-1 text-sm font-semibold", children: title }), (0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "tertiary", "aria-label": "Close", onPress: () => setIsOpen(false), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: "size-4" }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [turns.length === 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "space-y-3", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: hint }), currentScreen?.suggestions?.length ? ((0, jsx_runtime_1.jsx)("div", { className: "flex flex-wrap gap-1.5", children: currentScreen.suggestions.map((suggestion) => ((0, jsx_runtime_1.jsx)(react_2.Button, { size: "sm", variant: "secondary", className: "h-auto rounded-full px-2.5 py-1 text-start text-xs whitespace-normal", isDisabled: isBusy, onPress: () => void send(suggestion), children: suggestion }, suggestion))) })) : null] })), rendered, isBusy && lastTurn?.role === "user" && ((0, jsx_runtime_1.jsxs)("div", { className: "rounded-md border border-border bg-accent/5 p-2.5 text-sm", children: [(0, jsx_runtime_1.jsx)("p", { className: "mb-1 text-[11px] font-semibold tracking-wide text-muted uppercase", children: assistantName }), (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] })), error && (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-danger", children: error }), (0, jsx_runtime_1.jsx)("div", { ref: bottomRef })] }), (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex gap-2 border-t border-border p-3", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", className: "flex-1", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { className: "sr-only", children: placeholder }), (0, jsx_runtime_1.jsx)(react_2.Input, { placeholder: placeholder, value: input, onChange: (event) => setInput(event.currentTarget.value), disabled: isBusy })] }), isBusy ? ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "button", isIconOnly: true, variant: "secondary", "aria-label": "Stop generating", onPress: stop, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Square, { className: "size-3.5" }) })) : ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "submit", isIconOnly: true, variant: "primary", "aria-label": "Send", isDisabled: !input.trim(), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Send, { className: "size-3.5" }) }))] })] }) })] }));
332
377
  }
333
378
  /** Hoisted for the same reference-equality reason as `ANIMATED` - Streamdown compares it by identity. */
334
379
  const LINK_SAFETY = { enabled: false };
@@ -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)