@cosmoops/connect-sdk 0.3.0 → 0.6.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 +125 -0
- package/README.md +215 -6
- package/dist/cjs/cli/doctor.js +3 -3
- package/dist/cjs/cli/init.js +3 -3
- package/dist/cjs/client/components/access-gate.js +162 -0
- package/dist/cjs/client/components/account-sessions.js +21 -19
- package/dist/cjs/client/components/app-shell.js +157 -0
- package/dist/cjs/client/components/apps-grid.js +2 -1
- package/dist/cjs/client/components/integrations-modal.js +95 -0
- package/dist/cjs/client/components/motion.js +27 -0
- package/dist/cjs/client/components/nav.js +97 -0
- package/dist/cjs/client/components/plans.js +39 -37
- package/dist/cjs/client/components/readiness.js +3 -1
- package/dist/cjs/client/components/side-nav.js +58 -0
- package/dist/cjs/client/components/subscription.js +2 -1
- package/dist/cjs/client/components/surfaces.js +54 -0
- package/dist/cjs/client/components/usage-meter.js +3 -2
- package/dist/cjs/client/components/user-menu.js +30 -3
- package/dist/cjs/client/index.js +29 -1
- package/dist/cjs/client/provider.js +54 -2
- package/dist/cjs/client/use-active-path.js +77 -0
- package/dist/cjs/errors.js +1 -1
- package/dist/cjs/next/index.js +96 -15
- package/dist/cjs/server/client.js +10 -2
- package/dist/cjs/server/handler.js +89 -6
- package/dist/cjs/server/index.js +8 -1
- package/dist/cjs/server/resources/analytics.js +66 -0
- package/dist/cjs/server/resources/identity.js +1 -1
- package/dist/cjs/server/resources/integrations.js +66 -0
- package/dist/cjs/server/resources/sessions.js +1 -1
- package/dist/cjs/server/webhooks.js +127 -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 +158 -0
- package/dist/esm/client/components/account-sessions.js +21 -19
- package/dist/esm/client/components/app-shell.js +154 -0
- package/dist/esm/client/components/apps-grid.js +2 -1
- package/dist/esm/client/components/integrations-modal.js +92 -0
- package/dist/esm/client/components/motion.js +21 -0
- package/dist/esm/client/components/nav.js +89 -0
- package/dist/esm/client/components/plans.js +39 -37
- package/dist/esm/client/components/readiness.js +3 -1
- package/dist/esm/client/components/side-nav.js +53 -0
- package/dist/esm/client/components/subscription.js +2 -1
- package/dist/esm/client/components/surfaces.js +51 -0
- package/dist/esm/client/components/usage-meter.js +3 -2
- package/dist/esm/client/components/user-menu.js +31 -4
- package/dist/esm/client/index.js +15 -1
- package/dist/esm/client/provider.js +53 -2
- package/dist/esm/client/use-active-path.js +74 -0
- package/dist/esm/errors.js +1 -1
- package/dist/esm/next/index.js +95 -15
- package/dist/esm/server/client.js +10 -2
- package/dist/esm/server/handler.js +89 -6
- package/dist/esm/server/index.js +5 -0
- package/dist/esm/server/resources/analytics.js +62 -0
- package/dist/esm/server/resources/identity.js +1 -1
- package/dist/esm/server/resources/integrations.js +62 -0
- package/dist/esm/server/resources/sessions.js +1 -1
- package/dist/esm/server/webhooks.js +123 -0
- package/dist/theme.css +28 -0
- package/dist/types/client/components/access-gate.d.ts +79 -0
- package/dist/types/client/components/app-shell.d.ts +115 -0
- package/dist/types/client/components/integrations-modal.d.ts +21 -0
- package/dist/types/client/components/motion.d.ts +22 -0
- package/dist/types/client/components/nav.d.ts +110 -0
- package/dist/types/client/components/side-nav.d.ts +31 -0
- package/dist/types/client/components/surfaces.d.ts +51 -0
- package/dist/types/client/index.d.ts +12 -1
- package/dist/types/client/provider.d.ts +38 -3
- package/dist/types/client/use-active-path.d.ts +8 -0
- package/dist/types/errors.d.ts +1 -1
- package/dist/types/next/index.d.ts +40 -0
- package/dist/types/server/client.d.ts +8 -2
- package/dist/types/server/index.d.ts +7 -0
- package/dist/types/server/resources/analytics.d.ts +31 -0
- package/dist/types/server/resources/identity.d.ts +12 -2
- package/dist/types/server/resources/integrations.d.ts +40 -0
- package/dist/types/server/webhooks.d.ts +67 -0
- package/dist/types/types.d.ts +182 -1
- package/package.json +10 -4
|
@@ -7,6 +7,8 @@ const react_1 = require("react");
|
|
|
7
7
|
const react_2 = require("@heroui/react");
|
|
8
8
|
const lucide_react_1 = require("lucide-react");
|
|
9
9
|
const provider_js_1 = require("../provider.js");
|
|
10
|
+
const motion_js_1 = require("./motion.js");
|
|
11
|
+
const surfaces_js_1 = require("./surfaces.js");
|
|
10
12
|
const format_relative_time_js_1 = require("../../format-relative-time.js");
|
|
11
13
|
const DEVICE_ICONS = {
|
|
12
14
|
desktop: lucide_react_1.Monitor,
|
|
@@ -78,13 +80,13 @@ function ConnectAccountSessions({ className, maxVisible = 4 }) {
|
|
|
78
80
|
const [isRevokingOthers, setIsRevokingOthers] = (0, react_1.useState)(false);
|
|
79
81
|
const [showAll, setShowAll] = (0, react_1.useState)(false);
|
|
80
82
|
if (isLoading) {
|
|
81
|
-
return ((0, jsx_runtime_1.jsxs)("section", { className: `flex flex-col gap-3 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(react_2.Skeleton, { className: "h-4 w-28" }), (0, jsx_runtime_1.jsx)("div", { className:
|
|
83
|
+
return ((0, jsx_runtime_1.jsxs)("section", { className: `flex flex-col gap-3 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(react_2.Skeleton, { className: "h-4 w-28" }), (0, jsx_runtime_1.jsx)("div", { className: `flex flex-col divide-y divide-border overflow-hidden rounded-xl border border-border ${surfaces_js_1.PANEL_FILL}`, children: Array.from({ length: 2 }, (_, index) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3 px-3 py-3", children: [(0, jsx_runtime_1.jsx)(react_2.Skeleton, { className: "size-9 shrink-0 rounded-xl" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [(0, jsx_runtime_1.jsx)(react_2.Skeleton, { className: "h-3.5 w-32" }), (0, jsx_runtime_1.jsx)(react_2.Skeleton, { className: "h-3 w-24" })] })] }, index))) })] }));
|
|
82
84
|
}
|
|
83
85
|
// A failed read used to render as *nothing*, which is indistinguishable
|
|
84
86
|
// from "you have no other devices" - the one reading of this list that
|
|
85
87
|
// should never be guessed at.
|
|
86
88
|
if (error) {
|
|
87
|
-
return ((0, jsx_runtime_1.jsxs)("section", { className: `flex flex-col gap-2 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-foreground", children: "Active sessions" }), (0, jsx_runtime_1.jsx)("p", { className:
|
|
89
|
+
return ((0, jsx_runtime_1.jsxs)("section", { className: `flex flex-col gap-2 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-foreground", children: "Active sessions" }), (0, jsx_runtime_1.jsx)("p", { className: `rounded-xl border border-border ${surfaces_js_1.PANEL_FILL} px-3 py-4 text-xs text-muted`, children: "Could not load your sessions. Try again in a moment." })] }));
|
|
88
90
|
}
|
|
89
91
|
const sessions = ordered(data?.items ?? []);
|
|
90
92
|
if (sessions.length === 0)
|
|
@@ -92,21 +94,21 @@ function ConnectAccountSessions({ className, maxVisible = 4 }) {
|
|
|
92
94
|
const hasOtherSessions = sessions.some((session) => !session.isCurrent);
|
|
93
95
|
const isCollapsible = maxVisible > 0 && sessions.length > maxVisible;
|
|
94
96
|
const visible = isCollapsible && !showAll ? sessions.slice(0, maxVisible) : sessions;
|
|
95
|
-
return ((0, jsx_runtime_1.jsxs)("section", { className: `flex flex-col gap-3 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-start justify-between gap-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsxs)("p", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: ["Active sessions", (0, jsx_runtime_1.jsx)(react_2.Chip, { size: "sm", variant: "soft", children: sessions.length })] }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-muted", children: "Devices signed in to your Connect account." })] }), hasOtherSessions && ((0, jsx_runtime_1.jsx)(react_2.Button, { size: "sm", variant: "tertiary", className: "shrink-0 text-xs text-danger", isPending: isRevokingOthers, isDisabled: isRevokingOthers, onPress: () => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
97
|
+
return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)("section", { className: `flex flex-col gap-3 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-start justify-between gap-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsxs)("p", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: ["Active sessions", (0, jsx_runtime_1.jsx)(react_2.Chip, { size: "sm", variant: "soft", children: sessions.length })] }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-muted", children: "Devices signed in to your Connect account." })] }), hasOtherSessions && ((0, jsx_runtime_1.jsx)(react_2.Button, { size: "sm", variant: "tertiary", className: "shrink-0 text-xs text-danger", isPending: isRevokingOthers, isDisabled: isRevokingOthers, onPress: () => {
|
|
98
|
+
setIsRevokingOthers(true);
|
|
99
|
+
void mutateResource("/sessions/revoke-others")
|
|
100
|
+
.then(() => {
|
|
101
|
+
invalidateResource("/sessions");
|
|
102
|
+
refetch();
|
|
103
|
+
})
|
|
104
|
+
.finally(() => setIsRevokingOthers(false));
|
|
105
|
+
}, children: "Sign out others" }))] }), (0, jsx_runtime_1.jsx)("div", { className: `flex flex-col divide-y divide-border overflow-hidden rounded-xl border border-border ${surfaces_js_1.PANEL_FILL}`, children: (0, jsx_runtime_1.jsx)(motion_js_1.AnimatePresence, { initial: false, children: visible.map((session) => {
|
|
106
|
+
const Icon = DEVICE_ICONS[session.device.deviceType];
|
|
107
|
+
return ((0, jsx_runtime_1.jsxs)(motion_js_1.m.div, { layout: true, exit: { opacity: 0, height: 0, overflow: "hidden" }, transition: { duration: 0.25, ease: motion_js_1.EASE_OUT_QUART }, className: "flex items-center gap-3 px-3 py-2.5", children: [(0, jsx_runtime_1.jsx)("span", { className: `flex size-9 shrink-0 items-center justify-center rounded-xl ${session.isCurrent ? "bg-accent-soft text-accent-soft-foreground" : "bg-surface text-muted"}`, children: (0, jsx_runtime_1.jsx)(Icon, { className: surfaces_js_1.ICON, "aria-hidden": "true" }) }), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)("span", { className: "truncate text-sm font-medium text-foreground", children: deviceLabel(session) }), session.isCurrent && ((0, jsx_runtime_1.jsx)(react_2.Chip, { size: "sm", variant: "soft", color: "accent", className: "shrink-0", children: "This device" }))] }), (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: metaLine(session) })] }), !session.isCurrent && ((0, jsx_runtime_1.jsx)(RevokeRow, { session: session, onRevoke: () => {
|
|
108
|
+
void mutateResource("/sessions/revoke", { sessionId: session.id }).then(() => {
|
|
109
|
+
invalidateResource("/sessions");
|
|
110
|
+
refetch();
|
|
111
|
+
});
|
|
112
|
+
} }))] }, session.id));
|
|
113
|
+
}) }) }), isCollapsible && ((0, jsx_runtime_1.jsx)(react_2.Button, { size: "sm", variant: "tertiary", className: "self-start text-xs", onPress: () => setShowAll((open) => !open), children: showAll ? "Show fewer" : `Show all ${sessions.length} devices` }))] }) }));
|
|
112
114
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ConnectAppShell = ConnectAppShell;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const react_2 = require("@heroui/react");
|
|
8
|
+
const lucide_react_1 = require("lucide-react");
|
|
9
|
+
const provider_js_1 = require("../provider.js");
|
|
10
|
+
const access_gate_js_1 = require("./access-gate.js");
|
|
11
|
+
const apps_grid_js_1 = require("./apps-grid.js");
|
|
12
|
+
const auth_buttons_js_1 = require("./auth-buttons.js");
|
|
13
|
+
const logo_js_1 = require("./logo.js");
|
|
14
|
+
const side_nav_js_1 = require("./side-nav.js");
|
|
15
|
+
const user_menu_js_1 = require("./user-menu.js");
|
|
16
|
+
const nav_js_1 = require("./nav.js");
|
|
17
|
+
const use_active_path_js_1 = require("../use-active-path.js");
|
|
18
|
+
const COLLAPSED_KEY = "connect_sidenav_collapsed";
|
|
19
|
+
const COLLAPSED_EVENT = "connect:sidenav-collapsed";
|
|
20
|
+
/**
|
|
21
|
+
* The stored rail preference, or `null` when there isn't one (or when reading
|
|
22
|
+
* it is not allowed).
|
|
23
|
+
*
|
|
24
|
+
* `localStorage` throws outright in a partitioned iframe or with site data
|
|
25
|
+
* blocked, and an application frame is not worth losing over a preference.
|
|
26
|
+
*/
|
|
27
|
+
function collapsedSnapshot() {
|
|
28
|
+
try {
|
|
29
|
+
const stored = window.localStorage.getItem(COLLAPSED_KEY);
|
|
30
|
+
return stored === null ? null : stored === "1";
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* No preference on the server, which is the point: `useSyncExternalStore` uses
|
|
38
|
+
* this for the hydrating render and only then swaps in the real reading, so a
|
|
39
|
+
* returning user's collapsed rail can never make the server's HTML and the
|
|
40
|
+
* client's first pass disagree. Reading storage during render instead is a
|
|
41
|
+
* hydration mismatch waiting for its first user.
|
|
42
|
+
*/
|
|
43
|
+
function serverCollapsedSnapshot() {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function subscribeCollapsed(onStoreChange) {
|
|
47
|
+
// `storage` covers the app's other tabs; the custom event covers this one,
|
|
48
|
+
// which `storage` deliberately never fires in.
|
|
49
|
+
window.addEventListener("storage", onStoreChange);
|
|
50
|
+
window.addEventListener(COLLAPSED_EVENT, onStoreChange);
|
|
51
|
+
return () => {
|
|
52
|
+
window.removeEventListener("storage", onStoreChange);
|
|
53
|
+
window.removeEventListener(COLLAPSED_EVENT, onStoreChange);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The sidebar's brand row: the lockup when expanded, the badge alone when not.
|
|
58
|
+
*/
|
|
59
|
+
function BrandLink({ name, icon, homeHref, collapsed, renderLink, }) {
|
|
60
|
+
return renderLink({
|
|
61
|
+
href: homeHref,
|
|
62
|
+
className: `flex items-center rounded-lg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus ${collapsed ? "justify-center" : ""}`,
|
|
63
|
+
// `ConnectLogo` names itself, but it does it with `role="img"` on a
|
|
64
|
+
// `<span>`, which the `<a>` wrapping it does not inherit as its own name.
|
|
65
|
+
"aria-label": `${name} home`,
|
|
66
|
+
children: (0, jsx_runtime_1.jsx)(logo_js_1.ConnectLogo, { name: name, icon: icon, collapsed: collapsed }),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The application frame every CosmoOps service platform runs inside: a
|
|
71
|
+
* collapsible sidebar, a top bar carrying the app launcher and the account
|
|
72
|
+
* menu, and your pages in the middle.
|
|
73
|
+
*
|
|
74
|
+
* This is the piece each partner was otherwise writing by hand, and writing
|
|
75
|
+
* slightly differently - which is how a family of services stops looking like
|
|
76
|
+
* one product. Everything that should be identical across platforms (the
|
|
77
|
+
* lockup, the rail geometry, the active-route treatment, the launcher, the
|
|
78
|
+
* account menu, the access gate) lives here; everything that genuinely
|
|
79
|
+
* differs is a prop:
|
|
80
|
+
*
|
|
81
|
+
* ```tsx
|
|
82
|
+
* "use client";
|
|
83
|
+
* import Link from "next/link";
|
|
84
|
+
* import { usePathname } from "next/navigation";
|
|
85
|
+
* import { ConnectAppShell } from "@cosmoops/connect-sdk/client";
|
|
86
|
+
* import { LayoutDashboard, Truck } from "lucide-react";
|
|
87
|
+
*
|
|
88
|
+
* export function Shell({ children }: { children: React.ReactNode }) {
|
|
89
|
+
* return (
|
|
90
|
+
* <ConnectAppShell
|
|
91
|
+
* name="Dispatch"
|
|
92
|
+
* icon={<Truck />}
|
|
93
|
+
* activePath={usePathname()}
|
|
94
|
+
* renderLink={(props) => <Link {...props} />}
|
|
95
|
+
* nav={[
|
|
96
|
+
* { label: "Overview", href: "/", icon: <LayoutDashboard className="size-5" /> },
|
|
97
|
+
* { label: "Plans", href: "/plans", icon: <Truck className="size-5" />, gateExempt: true },
|
|
98
|
+
* ]}
|
|
99
|
+
* >
|
|
100
|
+
* {children}
|
|
101
|
+
* </ConnectAppShell>
|
|
102
|
+
* );
|
|
103
|
+
* }
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* There is no `isSignedIn` branch anywhere in here, and there should be none
|
|
107
|
+
* in yours: the auth buttons render nothing once a session exists, the user
|
|
108
|
+
* menu renders nothing without one, and the launcher renders nothing when
|
|
109
|
+
* signed out. The swap happens inside the components.
|
|
110
|
+
*
|
|
111
|
+
* It renders no provider of its own - mount `ConnectProvider` above it, once,
|
|
112
|
+
* so a page with a shell, a plan table and a usage meter still makes one
|
|
113
|
+
* session read.
|
|
114
|
+
*/
|
|
115
|
+
function ConnectAppShell({ 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, className, contentClassName, children, }) {
|
|
116
|
+
const { session, isLoading } = (0, provider_js_1.useConnect)();
|
|
117
|
+
const pathname = (0, use_active_path_js_1.useActivePath)(activePath);
|
|
118
|
+
const [mobileOpen, setMobileOpen] = (0, react_1.useState)(false);
|
|
119
|
+
// The rail's own state lives in `localStorage`, read as an external store
|
|
120
|
+
// rather than copied into React state: it is shared with this app's other
|
|
121
|
+
// tabs, and the reading is not available during the render that hydrates.
|
|
122
|
+
const stored = (0, react_1.useSyncExternalStore)(subscribeCollapsed, collapsedSnapshot, serverCollapsedSnapshot);
|
|
123
|
+
const isControlled = controlledCollapsed !== undefined;
|
|
124
|
+
const collapsed = collapsible ? (isControlled ? controlledCollapsed : (stored ?? defaultCollapsed)) : false;
|
|
125
|
+
const toggleCollapsed = (0, react_1.useCallback)(() => {
|
|
126
|
+
const next = !collapsed;
|
|
127
|
+
onCollapsedChange?.(next);
|
|
128
|
+
if (isControlled)
|
|
129
|
+
return;
|
|
130
|
+
try {
|
|
131
|
+
window.localStorage.setItem(COLLAPSED_KEY, next ? "1" : "0");
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Not persisted - but the event below still moves the rail for this session.
|
|
135
|
+
}
|
|
136
|
+
window.dispatchEvent(new Event(COLLAPSED_EVENT));
|
|
137
|
+
}, [collapsed, isControlled, onCollapsedChange]);
|
|
138
|
+
const closeMobile = (0, react_1.useCallback)(() => setMobileOpen(false), []);
|
|
139
|
+
const currentPath = (0, nav_js_1.normalizePath)(pathname);
|
|
140
|
+
const exempt = [...(0, nav_js_1.gateExemptPaths)(nav), ...(gateExemptRoutes ?? []).map(nav_js_1.normalizePath)];
|
|
141
|
+
const isExempt = exempt.some((path) => currentPath === path || currentPath.startsWith(`${path}/`));
|
|
142
|
+
// A signed-out visitor has no readiness to check and nothing to be blocked
|
|
143
|
+
// by; `ConnectAccessGate` passes them straight through, but mounting it at
|
|
144
|
+
// all costs an entitlements read that cannot succeed.
|
|
145
|
+
const signedOut = !isLoading && !session?.user;
|
|
146
|
+
const gated = gate && !isExempt && !signedOut;
|
|
147
|
+
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 }));
|
|
148
|
+
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 && (
|
|
149
|
+
// The button goes in as a *direct* child, the way `Modal` and
|
|
150
|
+
// `Dropdown` take theirs. `Tooltip.Trigger` renders a wrapper
|
|
151
|
+
// `<div>` and puts the props it is given on that div - so an
|
|
152
|
+
// `aria-label` passed there names a div nobody can reach and
|
|
153
|
+
// leaves this icon-only button with no accessible name at all.
|
|
154
|
+
// The wrapper is for trigger elements React Aria cannot drive
|
|
155
|
+
// directly; a HeroUI `Button` is not one.
|
|
156
|
+
(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] }) })] })] }));
|
|
157
|
+
}
|
|
@@ -6,6 +6,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
6
6
|
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
|
+
const motion_js_1 = require("./motion.js");
|
|
9
10
|
function AppTile({ app }) {
|
|
10
11
|
const disabled = app.launchUrl === null;
|
|
11
12
|
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 ? (
|
|
@@ -38,7 +39,7 @@ function ConnectAppsGrid({ className, label = "Switch app" }) {
|
|
|
38
39
|
if (sessionLoading || !session?.user)
|
|
39
40
|
return null;
|
|
40
41
|
const apps = data?.apps ?? [];
|
|
41
|
-
return ((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)(
|
|
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.serviceId))) }))] }), (0, jsx_runtime_1.jsx)(react_1.Modal.Footer, { children: (0, jsx_runtime_1.jsx)(ConnectBrowseAllLink, {}) })] }) }) })] }) }));
|
|
42
43
|
}
|
|
43
44
|
function ConnectBrowseAllLink() {
|
|
44
45
|
const { appUrl } = (0, provider_js_1.useConnect)();
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ConnectIntegrationsModal = ConnectIntegrationsModal;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const react_2 = require("@heroui/react");
|
|
8
|
+
const lucide_react_1 = require("lucide-react");
|
|
9
|
+
const provider_js_1 = require("../provider.js");
|
|
10
|
+
const motion_js_1 = require("./motion.js");
|
|
11
|
+
const STATUS_LABEL = {
|
|
12
|
+
not_connected: "Not connected",
|
|
13
|
+
pending: "Connecting…",
|
|
14
|
+
active: "Connected",
|
|
15
|
+
failed: "Failed",
|
|
16
|
+
disconnected: "Disconnected",
|
|
17
|
+
expired: "Expired",
|
|
18
|
+
};
|
|
19
|
+
const STATUS_COLOR = {
|
|
20
|
+
not_connected: "default",
|
|
21
|
+
pending: "warning",
|
|
22
|
+
active: "success",
|
|
23
|
+
failed: "danger",
|
|
24
|
+
disconnected: "default",
|
|
25
|
+
expired: "danger",
|
|
26
|
+
};
|
|
27
|
+
const CAN_CONNECT = new Set([
|
|
28
|
+
"not_connected",
|
|
29
|
+
"failed",
|
|
30
|
+
"disconnected",
|
|
31
|
+
"expired",
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* One toolkit's icon, status, and whatever action its current status allows.
|
|
35
|
+
* Never a dead end the way `AppTile` avoids one for a missing `launchUrl`:
|
|
36
|
+
* every status renders *some* affordance - connect, reconnect, disconnect,
|
|
37
|
+
* or a cancel for a stuck `pending` attempt.
|
|
38
|
+
*/
|
|
39
|
+
function ToolkitTile({ toolkit, isPending, onConnect, onDisconnect }) {
|
|
40
|
+
const [confirmingDisconnect, setConfirmingDisconnect] = (0, react_1.useState)(false);
|
|
41
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-center gap-2 rounded-xl border border-separator p-3", children: [(0, jsx_runtime_1.jsxs)("span", { className: "relative flex size-12 items-center justify-center overflow-hidden rounded-xl bg-surface-secondary", children: [toolkit.logoUrl ? (
|
|
42
|
+
// Deliberately a plain `<img>`: this package ships into any React app
|
|
43
|
+
// and cannot assume `next/image` or any other framework loader.
|
|
44
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
45
|
+
(0, jsx_runtime_1.jsx)("img", { src: toolkit.logoUrl, alt: "", className: "size-full object-cover" })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Puzzle, { className: "size-6 text-muted", "aria-hidden": "true" })), toolkit.status === "failed" && ((0, jsx_runtime_1.jsx)("span", { className: "absolute -right-0.5 -top-0.5 flex size-4 items-center justify-center rounded-full bg-danger text-danger-foreground", children: (0, jsx_runtime_1.jsx)(lucide_react_1.AlertCircle, { className: "size-3", "aria-hidden": "true" }) }))] }), (0, jsx_runtime_1.jsx)("span", { className: "line-clamp-2 text-center text-xs font-medium text-foreground", children: toolkit.name }), (0, jsx_runtime_1.jsx)(react_2.Chip, { size: "sm", variant: "soft", color: STATUS_COLOR[toolkit.status], children: STATUS_LABEL[toolkit.status] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex w-full flex-col gap-1.5 pt-1", children: [CAN_CONNECT.has(toolkit.status) && ((0, jsx_runtime_1.jsx)(react_2.Button, { size: "sm", variant: "secondary", className: "w-full text-xs", isPending: isPending, isDisabled: isPending, onPress: onConnect, children: "Connect" })), toolkit.status === "active" && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_2.Button, { size: "sm", variant: "secondary", className: "w-full text-xs", isPending: isPending, isDisabled: isPending, onPress: onConnect, children: "Reconnect" }), (0, jsx_runtime_1.jsxs)(react_2.AlertDialog, { isOpen: confirmingDisconnect, onOpenChange: setConfirmingDisconnect, children: [(0, jsx_runtime_1.jsx)(react_2.Button, { size: "sm", variant: "tertiary", className: "w-full text-xs text-danger", isDisabled: isPending, children: "Disconnect" }), (0, jsx_runtime_1.jsx)(react_2.AlertDialog.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_2.AlertDialog.Container, { children: (0, jsx_runtime_1.jsxs)(react_2.AlertDialog.Dialog, { className: "sm:max-w-100", children: [(0, jsx_runtime_1.jsx)(react_2.AlertDialog.CloseTrigger, {}), (0, jsx_runtime_1.jsxs)(react_2.AlertDialog.Header, { children: [(0, jsx_runtime_1.jsx)(react_2.AlertDialog.Icon, { status: "danger" }), (0, jsx_runtime_1.jsxs)(react_2.AlertDialog.Heading, { children: ["Disconnect ", toolkit.name, "?"] })] }), (0, jsx_runtime_1.jsx)(react_2.AlertDialog.Body, { children: (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: "Every member loses this connection immediately - it can be reconnected later." }) }), (0, jsx_runtime_1.jsxs)(react_2.AlertDialog.Footer, { children: [(0, jsx_runtime_1.jsx)(react_2.Button, { slot: "close", variant: "secondary", className: "text-xs", children: "Cancel" }), (0, jsx_runtime_1.jsx)(react_2.Button, { slot: "close", variant: "danger", className: "text-xs", onPress: onDisconnect, children: "Disconnect" })] })] }) }) })] })] })), toolkit.status === "pending" && ((0, jsx_runtime_1.jsx)(react_2.Button, { size: "sm", variant: "tertiary", className: "w-full text-xs text-danger", isDisabled: isPending, onPress: onDisconnect, children: "Cancel" }))] })] }));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A grid of every Composio-hosted toolkit this organization can connect -
|
|
49
|
+
* Google Calendar, Slack, Twitter/X, whatever a platform admin enabled in
|
|
50
|
+
* Connect's own Console. Each tile carries its own connect/reconnect/
|
|
51
|
+
* disconnect action and its own pending state, independent of every other
|
|
52
|
+
* tile - see the doc comment on `pendingSlug` below for why that matters.
|
|
53
|
+
*
|
|
54
|
+
* Not optimistic: a tile stays whatever it already was after "Connect" is
|
|
55
|
+
* pressed, because the connection isn't settled until Composio's webhook
|
|
56
|
+
* confirms it - `mutateResource` here only ever gets as far as `pending`
|
|
57
|
+
* before the browser navigates away to complete the provider's OAuth screen.
|
|
58
|
+
*
|
|
59
|
+
* Renders nothing when signed out, same as `ConnectAppsGrid`.
|
|
60
|
+
*/
|
|
61
|
+
function ConnectIntegrationsModal({ className, label = "Integrations" }) {
|
|
62
|
+
const { session, isLoading: sessionLoading, mutateResource, invalidateResource } = (0, provider_js_1.useConnect)();
|
|
63
|
+
const { data, isLoading, error, refetch } = (0, provider_js_1.useConnectIntegrations)();
|
|
64
|
+
// Per-toolkit, not one shared flag - a grid of independent connections is
|
|
65
|
+
// exactly the "list of rows" case a single `isPending` disables wholesale
|
|
66
|
+
// (see CLAUDE.md's mutation-conventions note on this).
|
|
67
|
+
const [pendingSlug, setPendingSlug] = (0, react_1.useState)(null);
|
|
68
|
+
if (sessionLoading || !session?.user)
|
|
69
|
+
return null;
|
|
70
|
+
const toolkits = data?.toolkits ?? [];
|
|
71
|
+
const connect = (slug) => {
|
|
72
|
+
setPendingSlug(slug);
|
|
73
|
+
void mutateResource("/integrations/connect", {
|
|
74
|
+
toolkitSlug: slug,
|
|
75
|
+
returnUrl: window.location.href,
|
|
76
|
+
})
|
|
77
|
+
.then(({ redirectUrl }) => {
|
|
78
|
+
window.location.href = redirectUrl;
|
|
79
|
+
})
|
|
80
|
+
.catch(() => setPendingSlug(null));
|
|
81
|
+
// No `finally(() => setPendingSlug(null))` on the success path: the
|
|
82
|
+
// browser is about to navigate away, and clearing the flag first would
|
|
83
|
+
// let a slow navigation show the button as re-enabled for a beat.
|
|
84
|
+
};
|
|
85
|
+
const disconnect = (slug) => {
|
|
86
|
+
setPendingSlug(slug);
|
|
87
|
+
void mutateResource("/integrations/disconnect", { toolkitSlug: slug })
|
|
88
|
+
.then(() => {
|
|
89
|
+
invalidateResource("/integrations");
|
|
90
|
+
refetch();
|
|
91
|
+
})
|
|
92
|
+
.finally(() => setPendingSlug(null));
|
|
93
|
+
};
|
|
94
|
+
return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)(react_2.Modal, { children: [(0, jsx_runtime_1.jsxs)(react_2.Button, { variant: "secondary", className: className, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Puzzle, { className: "size-4" }), label] }), (0, jsx_runtime_1.jsx)(react_2.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Container, { children: (0, jsx_runtime_1.jsxs)(react_2.Modal.Dialog, { className: "sm:max-w-lg", children: [(0, jsx_runtime_1.jsx)(react_2.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_2.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Heading, { children: "Integrations" }) }), (0, jsx_runtime_1.jsxs)(react_2.Modal.Body, { children: [isLoading && ((0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3", children: Array.from({ length: 6 }, (_, index) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-center gap-2 rounded-xl border border-separator p-3", children: [(0, jsx_runtime_1.jsx)(react_2.Skeleton, { className: "size-12 rounded-xl" }), (0, jsx_runtime_1.jsx)(react_2.Skeleton, { className: "h-3 w-16" }), (0, jsx_runtime_1.jsx)(react_2.Skeleton, { className: "h-7 w-full rounded-lg" })] }, index))) })), !isLoading && error && ((0, jsx_runtime_1.jsx)("p", { className: "py-6 text-center text-sm text-danger", children: "Could not load integrations." })), !isLoading && !error && toolkits.length === 0 && ((0, jsx_runtime_1.jsx)("p", { className: "py-6 text-center text-sm text-muted", children: "No integrations are enabled on this platform yet." })), !isLoading && !error && toolkits.length > 0 && ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { className: "grid grid-cols-2 gap-3 sm:grid-cols-3", variants: { show: { transition: { staggerChildren: 0.04 } } }, initial: "hidden", animate: "show", children: toolkits.map((toolkit) => ((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)(ToolkitTile, { toolkit: toolkit, isPending: pendingSlug === toolkit.slug, onConnect: () => connect(toolkit.slug), onDisconnect: () => disconnect(toolkit.slug) }) }, toolkit.slug))) }))] })] }) }) })] }) }));
|
|
95
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.METER_SPRING = exports.ENTER = exports.EASE_OUT_QUART = exports.AnimatePresence = exports.m = void 0;
|
|
5
|
+
exports.MotionProvider = MotionProvider;
|
|
6
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
7
|
+
const react_1 = require("motion/react");
|
|
8
|
+
var react_2 = require("motion/react");
|
|
9
|
+
Object.defineProperty(exports, "m", { enumerable: true, get: function () { return react_2.m; } });
|
|
10
|
+
Object.defineProperty(exports, "AnimatePresence", { enumerable: true, get: function () { return react_2.AnimatePresence; } });
|
|
11
|
+
/** HeroUI's --ease-out-quart, transcribed for motion transitions. */
|
|
12
|
+
exports.EASE_OUT_QUART = [0.165, 0.84, 0.44, 1];
|
|
13
|
+
exports.ENTER = { duration: 0.3, ease: exports.EASE_OUT_QUART };
|
|
14
|
+
exports.METER_SPRING = {
|
|
15
|
+
type: "spring",
|
|
16
|
+
stiffness: 170,
|
|
17
|
+
damping: 26,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Per-component motion root. domAnimation keeps the feature bundle small,
|
|
21
|
+
* `strict` restricts rendering to `m.*` components, and reducedMotion="user"
|
|
22
|
+
* drops transform animation (keeping opacity) under prefers-reduced-motion -
|
|
23
|
+
* the same contract HeroUI's own `motion-reduce` styles follow.
|
|
24
|
+
*/
|
|
25
|
+
function MotionProvider({ children }) {
|
|
26
|
+
return ((0, jsx_runtime_1.jsx)(react_1.MotionConfig, { reducedMotion: "user", children: (0, jsx_runtime_1.jsx)(react_1.LazyMotion, { features: react_1.domAnimation, strict: true, children: children }) }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeNav = normalizeNav;
|
|
4
|
+
exports.flattenNav = flattenNav;
|
|
5
|
+
exports.normalizePath = normalizePath;
|
|
6
|
+
exports.resolveActivePath = resolveActivePath;
|
|
7
|
+
exports.isNavItemActive = isNavItemActive;
|
|
8
|
+
exports.gateExemptPaths = gateExemptPaths;
|
|
9
|
+
function isSection(entry) {
|
|
10
|
+
return !("href" in entry) && Array.isArray(entry.items);
|
|
11
|
+
}
|
|
12
|
+
/** Folds a mixed `ConnectNav` into sections, gathering consecutive bare items into one untitled group. */
|
|
13
|
+
function normalizeNav(nav) {
|
|
14
|
+
const sections = [];
|
|
15
|
+
let loose = null;
|
|
16
|
+
for (const entry of nav) {
|
|
17
|
+
if (isSection(entry)) {
|
|
18
|
+
loose = null;
|
|
19
|
+
sections.push(entry);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (!loose) {
|
|
23
|
+
loose = [];
|
|
24
|
+
sections.push({ items: loose });
|
|
25
|
+
}
|
|
26
|
+
loose.push(entry);
|
|
27
|
+
}
|
|
28
|
+
return sections;
|
|
29
|
+
}
|
|
30
|
+
/** Every item in the tree, parents and children alike, in render order. */
|
|
31
|
+
function flattenNav(nav) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const section of normalizeNav(nav)) {
|
|
34
|
+
for (const item of section.items) {
|
|
35
|
+
out.push(item);
|
|
36
|
+
if (item.items)
|
|
37
|
+
out.push(...item.items);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `https://x.test/billing/?tab=1#top` -> `/billing`.
|
|
44
|
+
*
|
|
45
|
+
* Trailing slashes go too, so `/billing/` and `/billing` are the same route -
|
|
46
|
+
* they are, to every router, and treating them as different is how a nav item
|
|
47
|
+
* ends up unhighlighted on exactly one of the two URLs a user can arrive at.
|
|
48
|
+
*/
|
|
49
|
+
function normalizePath(path) {
|
|
50
|
+
const withoutOrigin = path.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]*/i, "");
|
|
51
|
+
const withoutQuery = withoutOrigin.split(/[?#]/)[0] ?? "";
|
|
52
|
+
const trimmed = withoutQuery.replace(/\/+$/, "");
|
|
53
|
+
return trimmed === "" ? "/" : trimmed;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The href of the nav item the current path belongs to, or `null`.
|
|
57
|
+
*
|
|
58
|
+
* Longest prefix wins, so `/settings/billing` lights up Billing and not also
|
|
59
|
+
* its parent Settings - a plain `startsWith` over an unordered list lights up
|
|
60
|
+
* both, and the sidebar grows two highlights.
|
|
61
|
+
*
|
|
62
|
+
* External items never match: they leave the app, so no path inside it is
|
|
63
|
+
* ever "on" them.
|
|
64
|
+
*/
|
|
65
|
+
function resolveActivePath(pathname, nav) {
|
|
66
|
+
const current = normalizePath(pathname);
|
|
67
|
+
let best = null;
|
|
68
|
+
for (const item of flattenNav(nav)) {
|
|
69
|
+
if (item.external)
|
|
70
|
+
continue;
|
|
71
|
+
const href = normalizePath(item.href);
|
|
72
|
+
const exact = item.exact || href === "/";
|
|
73
|
+
const matches = current === href || (!exact && current.startsWith(`${href}/`));
|
|
74
|
+
if (matches && (best === null || href.length > best.length))
|
|
75
|
+
best = href;
|
|
76
|
+
}
|
|
77
|
+
return best;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Whether `item` is the destination `resolveActivePath` picked.
|
|
81
|
+
*
|
|
82
|
+
* Not just `normalizePath(item.href) === activeHref`, which is the trap this
|
|
83
|
+
* exists to close: `normalizePath` strips the origin off an absolute URL, so
|
|
84
|
+
* an external item pointed at `https://docs.example.com` normalizes to `/` and
|
|
85
|
+
* lights up on every app's home route. External items are never active.
|
|
86
|
+
*/
|
|
87
|
+
function isNavItemActive(item, activeHref) {
|
|
88
|
+
if (activeHref === null || item.external)
|
|
89
|
+
return false;
|
|
90
|
+
return normalizePath(item.href) === activeHref;
|
|
91
|
+
}
|
|
92
|
+
/** Hrefs declared `gateExempt` - the routes `ConnectAppShell` renders outside its `ConnectAccessGate`. */
|
|
93
|
+
function gateExemptPaths(nav) {
|
|
94
|
+
return flattenNav(nav)
|
|
95
|
+
.filter((item) => item.gateExempt)
|
|
96
|
+
.map((item) => normalizePath(item.href));
|
|
97
|
+
}
|