@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.
- package/CHANGELOG.md +566 -0
- package/README.md +113 -42
- package/dist/cjs/cli/doctor.js +3 -3
- package/dist/cjs/cli/init.js +3 -3
- package/dist/cjs/client/components/access-gate.js +20 -1
- package/dist/cjs/client/components/ai-screens.js +73 -0
- package/dist/cjs/client/components/ai-widgets.js +37 -0
- package/dist/cjs/client/components/app-shell.js +75 -41
- package/dist/cjs/client/components/apps-grid.js +3 -2
- package/dist/cjs/client/components/ask-ai.js +50 -5
- package/dist/cjs/client/components/confirm-dialog.js +40 -0
- package/dist/cjs/client/components/cursor-pagination.js +22 -0
- package/dist/cjs/client/components/filter-tabs.js +24 -0
- package/dist/cjs/client/components/grievance-notice.js +30 -0
- package/dist/cjs/client/components/link-button.js +17 -10
- package/dist/cjs/client/components/logo.js +4 -3
- package/dist/cjs/client/components/nav.js +41 -0
- package/dist/cjs/client/components/plan-pricing.js +27 -0
- package/dist/cjs/client/components/plans.js +1 -1
- package/dist/cjs/client/components/platform-gate.js +48 -0
- package/dist/cjs/client/components/shell-context.js +149 -0
- package/dist/cjs/client/components/side-nav.js +49 -23
- package/dist/cjs/client/components/sidebar.js +127 -0
- package/dist/cjs/client/components/subscribe-dialog.js +2 -2
- package/dist/cjs/client/components/subscription.js +1 -1
- package/dist/cjs/client/components/user-menu.js +96 -37
- package/dist/cjs/client/components/workspace-switcher.js +100 -0
- package/dist/cjs/client/index.js +72 -2
- package/dist/cjs/client/provider.js +57 -2
- package/dist/cjs/client/use-checkout.js +9 -5
- package/dist/cjs/errors.js +1 -1
- package/dist/cjs/index.js +16 -1
- package/dist/cjs/mcp/index.js +90 -0
- package/dist/cjs/mcp/tools.js +369 -0
- package/dist/cjs/next/index.js +2 -2
- package/dist/cjs/server/client.js +25 -2
- package/dist/cjs/server/handler.js +203 -18
- package/dist/cjs/server/http.js +6 -2
- package/dist/cjs/server/resources/acting-as.js +16 -0
- package/dist/cjs/server/resources/ai.js +22 -2
- package/dist/cjs/server/resources/data.js +374 -0
- package/dist/cjs/server/resources/identity.js +7 -3
- package/dist/cjs/server/resources/kv.js +84 -0
- package/dist/cjs/server/resources/platform.js +43 -0
- package/dist/cjs/server/resources/sessions.js +1 -1
- package/dist/cjs/server/resources/workspaces.js +74 -0
- package/dist/cjs/store-schema.js +66 -0
- package/dist/cjs/store.js +152 -0
- package/dist/cjs/ui/empty-state.js +28 -0
- package/dist/cjs/ui/index.js +44 -0
- package/dist/cjs/ui/page-header.js +27 -0
- package/dist/cjs/ui/page-shell.js +44 -0
- package/dist/cjs/ui/section.js +20 -0
- package/dist/cjs/ui/setting-row.js +14 -0
- package/dist/cjs/ui/status-chip.js +17 -0
- package/dist/esm/cli/doctor.js +3 -3
- package/dist/esm/cli/init.js +3 -3
- package/dist/esm/client/components/access-gate.js +20 -1
- package/dist/esm/client/components/ai-screens.js +69 -0
- package/dist/esm/client/components/ai-widgets.js +37 -1
- package/dist/esm/client/components/app-shell.js +81 -47
- package/dist/esm/client/components/apps-grid.js +3 -2
- package/dist/esm/client/components/ask-ai.js +51 -6
- package/dist/esm/client/components/confirm-dialog.js +37 -0
- package/dist/esm/client/components/cursor-pagination.js +19 -0
- package/dist/esm/client/components/filter-tabs.js +21 -0
- package/dist/esm/client/components/grievance-notice.js +27 -0
- package/dist/esm/client/components/link-button.js +17 -10
- package/dist/esm/client/components/logo.js +4 -3
- package/dist/esm/client/components/nav.js +40 -0
- package/dist/esm/client/components/plan-pricing.js +25 -0
- package/dist/esm/client/components/plans.js +2 -2
- package/dist/esm/client/components/platform-gate.js +45 -0
- package/dist/esm/client/components/shell-context.js +141 -0
- package/dist/esm/client/components/side-nav.js +48 -21
- package/dist/esm/client/components/sidebar.js +120 -0
- package/dist/esm/client/components/subscribe-dialog.js +3 -3
- package/dist/esm/client/components/subscription.js +2 -2
- package/dist/esm/client/components/user-menu.js +95 -38
- package/dist/esm/client/components/workspace-switcher.js +97 -0
- package/dist/esm/client/index.js +47 -4
- package/dist/esm/client/provider.js +53 -2
- package/dist/esm/client/use-checkout.js +9 -5
- package/dist/esm/errors.js +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/mcp/index.js +85 -0
- package/dist/esm/mcp/tools.js +366 -0
- package/dist/esm/next/index.js +2 -2
- package/dist/esm/server/client.js +25 -2
- package/dist/esm/server/handler.js +203 -18
- package/dist/esm/server/http.js +6 -2
- package/dist/esm/server/resources/acting-as.js +13 -0
- package/dist/esm/server/resources/ai.js +22 -2
- package/dist/esm/server/resources/data.js +369 -0
- package/dist/esm/server/resources/identity.js +7 -3
- package/dist/esm/server/resources/kv.js +80 -0
- package/dist/esm/server/resources/platform.js +39 -0
- package/dist/esm/server/resources/sessions.js +1 -1
- package/dist/esm/server/resources/workspaces.js +70 -0
- package/dist/esm/store-schema.js +62 -0
- package/dist/esm/store.js +141 -0
- package/dist/esm/ui/empty-state.js +25 -0
- package/dist/esm/ui/index.js +34 -0
- package/dist/esm/ui/page-header.js +24 -0
- package/dist/esm/ui/page-shell.js +41 -0
- package/dist/esm/ui/section.js +17 -0
- package/dist/esm/ui/setting-row.js +11 -0
- package/dist/esm/ui/status-chip.js +14 -0
- package/dist/types/client/components/ai-screens.d.ts +25 -0
- package/dist/types/client/components/ai-widgets.d.ts +34 -1
- package/dist/types/client/components/app-shell.d.ts +65 -27
- package/dist/types/client/components/apps-grid.d.ts +11 -2
- package/dist/types/client/components/ask-ai.d.ts +47 -1
- package/dist/types/client/components/confirm-dialog.d.ts +34 -0
- package/dist/types/client/components/cursor-pagination.d.ts +27 -0
- package/dist/types/client/components/filter-tabs.d.ts +26 -0
- package/dist/types/client/components/grievance-notice.d.ts +31 -0
- package/dist/types/client/components/link-button.d.ts +32 -9
- package/dist/types/client/components/logo.d.ts +18 -2
- package/dist/types/client/components/nav.d.ts +24 -0
- package/dist/types/client/components/plan-pricing.d.ts +10 -0
- package/dist/types/client/components/platform-gate.d.ts +53 -0
- package/dist/types/client/components/shell-context.d.ts +116 -0
- package/dist/types/client/components/side-nav.d.ts +27 -13
- package/dist/types/client/components/sidebar.d.ts +95 -0
- package/dist/types/client/components/user-menu.d.ts +68 -22
- package/dist/types/client/components/workspace-switcher.d.ts +49 -0
- package/dist/types/client/index.d.ts +29 -5
- package/dist/types/client/provider.d.ts +109 -6
- package/dist/types/client/use-checkout.d.ts +8 -4
- package/dist/types/errors.d.ts +1 -1
- package/dist/types/index.d.ts +6 -0
- package/dist/types/mcp/index.d.ts +74 -0
- package/dist/types/mcp/tools.d.ts +107 -0
- package/dist/types/next/index.d.ts +1 -1
- package/dist/types/server/client.d.ts +25 -2
- package/dist/types/server/http.d.ts +8 -2
- package/dist/types/server/index.d.ts +4 -0
- package/dist/types/server/resources/acting-as.d.ts +11 -0
- package/dist/types/server/resources/ai.d.ts +20 -2
- package/dist/types/server/resources/data.d.ts +258 -0
- package/dist/types/server/resources/kv.d.ts +63 -0
- package/dist/types/server/resources/platform.d.ts +32 -0
- package/dist/types/server/resources/workspaces.d.ts +55 -0
- package/dist/types/store-schema.d.ts +52 -0
- package/dist/types/store.d.ts +96 -0
- package/dist/types/types.d.ts +369 -16
- package/dist/types/ui/empty-state.d.ts +34 -0
- package/dist/types/ui/index.d.ts +40 -0
- package/dist/types/ui/page-header.d.ts +34 -0
- package/dist/types/ui/page-shell.d.ts +39 -0
- package/dist/types/ui/section.d.ts +26 -0
- package/dist/types/ui/setting-row.d.ts +18 -0
- package/dist/types/ui/status-chip.d.ts +36 -0
- 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
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
77
|
-
* one product. Everything that should be identical across platforms
|
|
78
|
-
*
|
|
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: "
|
|
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
|
|
109
|
-
*
|
|
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 =
|
|
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
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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", {
|
|
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 ${
|
|
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
|
|
8
|
+
* A HeroUI `Button` that renders a link.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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
|
|
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) =>
|
|
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 (
|
|
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)
|