@cosmoops/connect-sdk 0.9.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +497 -0
- package/README.md +111 -41
- 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/app-shell.js +75 -41
- package/dist/cjs/client/components/apps-grid.js +3 -2
- package/dist/cjs/client/components/ask-ai.js +20 -1
- 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 +69 -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 +11 -1
- package/dist/cjs/next/index.js +2 -2
- package/dist/cjs/server/client.js +25 -2
- package/dist/cjs/server/handler.js +196 -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 +15 -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.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/app-shell.js +81 -47
- package/dist/esm/client/components/apps-grid.js +3 -2
- package/dist/esm/client/components/ask-ai.js +20 -1
- 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 +46 -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 +1 -0
- package/dist/esm/next/index.js +2 -2
- package/dist/esm/server/client.js +25 -2
- package/dist/esm/server/handler.js +196 -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 +15 -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.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/app-shell.d.ts +65 -27
- package/dist/types/client/components/apps-grid.d.ts +11 -2
- 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 +27 -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 +2 -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 +9 -1
- 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.d.ts +96 -0
- package/dist/types/types.d.ts +319 -15
- 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 +6 -2
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ConnectConfirmDialog = ConnectConfirmDialog;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const react_2 = require("@heroui/react");
|
|
8
|
+
/**
|
|
9
|
+
* The confirmation in front of a destructive action.
|
|
10
|
+
*
|
|
11
|
+
* **It closes on confirm, not on response.** Holding the modal open until the
|
|
12
|
+
* server answers makes every delete feel like a form submission, and the error
|
|
13
|
+
* path already has somewhere to go - a toast, or the row reappearing. A dialog
|
|
14
|
+
* that stays open to report failure is right only where the failure is
|
|
15
|
+
* *expected* and correctable in place (a seat cap, a last-admin invariant),
|
|
16
|
+
* and that is a blocking form, not this.
|
|
17
|
+
*
|
|
18
|
+
* `onConfirm` may return a promise. It is not awaited before closing, but a
|
|
19
|
+
* rejection is caught here rather than escaping - an error thrown out of an
|
|
20
|
+
* async transition reaches the nearest error boundary and takes the page down.
|
|
21
|
+
*
|
|
22
|
+
* The anatomy is HeroUI's: `Modal.Backdrop` owns the open state,
|
|
23
|
+
* `Modal.Container` the sizing, `Modal.Dialog` the focus trap. The trigger
|
|
24
|
+
* goes inside `Modal.Trigger`, which *is* a React Aria button - so pass a
|
|
25
|
+
* plain element, never a `<Button>`, or the nested button swallows the press
|
|
26
|
+
* and the dialog silently never opens.
|
|
27
|
+
*/
|
|
28
|
+
function ConnectConfirmDialog({ trigger, title, description, confirmLabel = "Confirm", cancelLabel = "Cancel", tone = "danger", onConfirm, }) {
|
|
29
|
+
const [isOpen, setIsOpen] = (0, react_1.useState)(false);
|
|
30
|
+
const confirm = () => {
|
|
31
|
+
setIsOpen(false);
|
|
32
|
+
void Promise.resolve()
|
|
33
|
+
.then(() => onConfirm())
|
|
34
|
+
.catch(() => {
|
|
35
|
+
// Swallowed on purpose - see the note above. Reporting belongs to the
|
|
36
|
+
// caller, which owns the toast and the rollback.
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_2.Modal.Trigger, { onClick: () => setIsOpen(true), children: trigger }), (0, jsx_runtime_1.jsx)(react_2.Modal.Backdrop, { isOpen: isOpen, onOpenChange: setIsOpen, children: (0, jsx_runtime_1.jsx)(react_2.Modal.Container, { size: "sm", children: (0, jsx_runtime_1.jsxs)(react_2.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_2.Modal.CloseTrigger, { onPress: () => setIsOpen(false) }), (0, jsx_runtime_1.jsx)(react_2.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Heading, { children: title }) }), description && ((0, jsx_runtime_1.jsx)(react_2.Modal.Body, { children: (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: description }) })), (0, jsx_runtime_1.jsxs)(react_2.Modal.Footer, { children: [(0, jsx_runtime_1.jsx)(react_2.Button, { variant: "secondary", onPress: () => setIsOpen(false), children: cancelLabel }), (0, jsx_runtime_1.jsx)(react_2.Button, { variant: tone === "danger" ? "danger" : "primary", onPress: confirm, children: confirmLabel })] })] }) }) })] }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ConnectCursorPagination = ConnectCursorPagination;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("@heroui/react");
|
|
7
|
+
/**
|
|
8
|
+
* Next / previous for a cursor-paginated list.
|
|
9
|
+
*
|
|
10
|
+
* Cursor rather than page numbers, and the component shape follows from that:
|
|
11
|
+
* there is no "page 7 of 42" because a keyset-paginated list does not know how
|
|
12
|
+
* many pages there are without a second count query over the same filter -
|
|
13
|
+
* which is a billed index scan for a number nobody acts on. What a user does
|
|
14
|
+
* act on is "more" and "back", and those are exact.
|
|
15
|
+
*
|
|
16
|
+
* The count is announced in a live region so a screen-reader user learns that
|
|
17
|
+
* filtering or paging changed the result set. Without it the table silently
|
|
18
|
+
* swaps its contents.
|
|
19
|
+
*/
|
|
20
|
+
function ConnectCursorPagination({ shown, nextCursor, hasPrevious, onNext, onPrevious, isPending = false, noun = ["row", "rows"], }) {
|
|
21
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between gap-3 pt-3", children: [(0, jsx_runtime_1.jsxs)("p", { "aria-live": "polite", className: "text-xs text-muted", children: [shown, " ", shown === 1 ? noun[0] : noun[1]] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2", children: [(0, jsx_runtime_1.jsx)(react_1.Button, { size: "sm", variant: "secondary", isDisabled: !hasPrevious || isPending, onPress: onPrevious, children: "Previous" }), (0, jsx_runtime_1.jsx)(react_1.Button, { size: "sm", variant: "secondary", isDisabled: nextCursor === null || isPending, onPress: () => nextCursor && onNext(nextCursor), children: "Next" })] })] }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ConnectFilterTabs = ConnectFilterTabs;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("@heroui/react");
|
|
7
|
+
/**
|
|
8
|
+
* A single-select filter strip - the "All / Open / Won / Lost" row above a
|
|
9
|
+
* list.
|
|
10
|
+
*
|
|
11
|
+
* Real radio semantics rather than a row of buttons: it is a single choice
|
|
12
|
+
* from a closed set, so arrow keys should move between options and the
|
|
13
|
+
* selected one should be announced as selected. A group of `<button>`s gets
|
|
14
|
+
* neither, and that is the usual hand-rolled shape.
|
|
15
|
+
*
|
|
16
|
+
* Scrolls horizontally rather than wrapping on a narrow viewport - a filter
|
|
17
|
+
* strip that wraps to three lines pushes the list itself off the screen.
|
|
18
|
+
*/
|
|
19
|
+
function ConnectFilterTabs({ options, value, onChange, label, }) {
|
|
20
|
+
return ((0, jsx_runtime_1.jsx)("div", { role: "radiogroup", "aria-label": label, className: "-mx-1 flex gap-2 overflow-x-auto px-1 pb-1", children: options.map((option) => {
|
|
21
|
+
const isSelected = option.value === value;
|
|
22
|
+
return ((0, jsx_runtime_1.jsx)("button", { type: "button", role: "radio", "aria-checked": isSelected, onClick: () => onChange(option.value), className: "shrink-0 rounded-full focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent", children: (0, jsx_runtime_1.jsxs)(react_1.Chip, { size: "sm", variant: isSelected ? "primary" : "soft", color: isSelected ? "accent" : "default", children: [option.label, option.count !== undefined && (0, jsx_runtime_1.jsx)("span", { className: "ml-1 opacity-70", children: option.count })] }) }, option.value ?? "__all"));
|
|
23
|
+
}) }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ConnectGrievanceNotice = ConnectGrievanceNotice;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const provider_js_1 = require("../provider.js");
|
|
7
|
+
/**
|
|
8
|
+
* The DPDP s.13 grievance channel, rendered.
|
|
9
|
+
*
|
|
10
|
+
* Every application on the platform is a Data Processor under one notice, so
|
|
11
|
+
* the wording lives here rather than being retyped per application - a user
|
|
12
|
+
* comparing two applications' privacy pages should not find two different
|
|
13
|
+
* accounts of who their Data Fiduciary is.
|
|
14
|
+
*
|
|
15
|
+
* Renders nothing at all while loading and nothing on failure, rather than a
|
|
16
|
+
* skeleton or an error. That is unusual here and deliberate: this component
|
|
17
|
+
* is almost always inside a larger privacy page that has its own content, and
|
|
18
|
+
* a red error box in the middle of a legal notice reads as the notice itself
|
|
19
|
+
* being broken. The address is also on the public web either way. Pass
|
|
20
|
+
* `useConnectCompanyProfile()` directly to your own layout if you want to
|
|
21
|
+
* handle those states yourself.
|
|
22
|
+
*/
|
|
23
|
+
function ConnectGrievanceNotice({ title = "Grievance redressal", applicationName, className, }) {
|
|
24
|
+
const { data } = (0, provider_js_1.useConnectCompanyProfile)();
|
|
25
|
+
const profile = data?.companyProfile;
|
|
26
|
+
if (!profile)
|
|
27
|
+
return null;
|
|
28
|
+
const { grievance } = profile;
|
|
29
|
+
return ((0, jsx_runtime_1.jsxs)("section", { className: className ?? "space-y-3", children: [title !== null && (0, jsx_runtime_1.jsx)("h3", { className: "text-base font-semibold", children: title }), (0, jsx_runtime_1.jsxs)("p", { className: "text-sm text-muted", children: [applicationName ? `${applicationName} is operated on ` : "This service runs on ", "the CosmoOps Connect platform. ", (0, jsx_runtime_1.jsx)("strong", { className: "font-medium text-foreground", children: profile.legalName }), " ", "is the Data Fiduciary for personal data processed here, under the Digital Personal Data Protection Act, 2023."] }), (0, jsx_runtime_1.jsxs)("dl", { className: "grid grid-cols-1 gap-x-6 gap-y-3 text-sm sm:grid-cols-[auto_1fr]", children: [(0, jsx_runtime_1.jsx)("dt", { className: "text-muted", children: "Grievance Officer" }), (0, jsx_runtime_1.jsx)("dd", { className: "text-foreground", children: grievance.officerName }), (0, jsx_runtime_1.jsx)("dt", { className: "text-muted", children: "Email" }), (0, jsx_runtime_1.jsx)("dd", { children: (0, jsx_runtime_1.jsx)("a", { className: "text-accent underline underline-offset-2", href: `mailto:${grievance.officerEmail}`, children: grievance.officerEmail }) }), (0, jsx_runtime_1.jsx)("dt", { className: "text-muted", children: "Contactable" }), (0, jsx_runtime_1.jsx)("dd", { className: "text-foreground", children: grievance.hours }), (0, jsx_runtime_1.jsx)("dt", { className: "text-muted", children: "Registered address" }), (0, jsx_runtime_1.jsx)("dd", { className: "text-foreground", children: profile.registeredAddress })] }), (0, jsx_runtime_1.jsxs)("p", { className: "text-sm text-muted", children: ["Complaints are acknowledged and resolved within ", grievance.responseDays, " days of receipt."] })] }));
|
|
30
|
+
}
|
|
@@ -5,20 +5,27 @@ exports.ConnectLinkButton = ConnectLinkButton;
|
|
|
5
5
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
6
|
const react_1 = require("@heroui/react");
|
|
7
7
|
/**
|
|
8
|
-
* A HeroUI `Button` that renders a
|
|
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)
|
|
@@ -20,6 +20,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
20
20
|
exports.ACTION_LABEL = void 0;
|
|
21
21
|
exports.formatPrice = formatPrice;
|
|
22
22
|
exports.monthlyOf = monthlyOf;
|
|
23
|
+
exports.intervalLabel = intervalLabel;
|
|
24
|
+
exports.pricePerLabel = pricePerLabel;
|
|
23
25
|
exports.discountLabel = discountLabel;
|
|
24
26
|
exports.resolvePlanGroup = resolvePlanGroup;
|
|
25
27
|
exports.groupPlansByTier = groupPlansByTier;
|
|
@@ -41,6 +43,31 @@ function formatPrice(paise) {
|
|
|
41
43
|
function monthlyOf(price, billingInterval) {
|
|
42
44
|
return billingInterval === "annual" ? price / 12 : price;
|
|
43
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* How to name a plan's cadence, in the three forms the plan screens need.
|
|
48
|
+
*
|
|
49
|
+
* One table rather than a `=== "annual" ? "year" : "month"` at each call site.
|
|
50
|
+
* That ternary was correct while there were two intervals and quietly became
|
|
51
|
+
* wrong when `"one-time"` arrived: a plan paid for once was labelled
|
|
52
|
+
* "per month", which is not a rendering imperfection but a false price.
|
|
53
|
+
*/
|
|
54
|
+
const INTERVAL_LABELS = {
|
|
55
|
+
monthly: { per: "month", short: "mo", adverb: "monthly" },
|
|
56
|
+
annual: { per: "year", short: "yr", adverb: "annually" },
|
|
57
|
+
// "once" reads correctly in all three positions a cadence appears in -
|
|
58
|
+
// "₹4,999 once", "Total once", "Only sold once".
|
|
59
|
+
"one-time": { per: "once", short: "once", adverb: "once" },
|
|
60
|
+
};
|
|
61
|
+
function intervalLabel(billingInterval) {
|
|
62
|
+
return INTERVAL_LABELS[billingInterval];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* `/month`, `/year` - and nothing at all for a one-time plan, where a price
|
|
66
|
+
* with no slash is the honest rendering.
|
|
67
|
+
*/
|
|
68
|
+
function pricePerLabel(billingInterval) {
|
|
69
|
+
return billingInterval === "one-time" ? "" : `/${INTERVAL_LABELS[billingInterval].per}`;
|
|
70
|
+
}
|
|
44
71
|
function discountLabel(discount) {
|
|
45
72
|
const amount = discount.discountType === "percentage" ? `${discount.value}% off` : `${formatPrice(discount.value)} off`;
|
|
46
73
|
if (discount.duration === "forever")
|
|
@@ -126,7 +126,7 @@ function ConnectPlans({ planIds, hideCoupon = false, className }) {
|
|
|
126
126
|
(tier.monthly?.id === subscription.plan.id || tier.annual?.id === subscription.plan.id);
|
|
127
127
|
const isCurrent = action === "current";
|
|
128
128
|
const cadenceUnavailable = showAnnual ? tier.annual === null : tier.monthly === null;
|
|
129
|
-
return ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { variants: { hidden: { opacity: 0, y: 12 }, show: { opacity: 1, y: 0, transition: motion_js_1.ENTER } }, children: (0, jsx_runtime_1.jsxs)(react_1.Card, { className: isCurrentTier ? "h-full border-accent" : "h-full", children: [(0, jsx_runtime_1.jsxs)(react_1.Card.Header, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [(0, jsx_runtime_1.jsx)(react_1.Card.Title, { children: tier.name }), (0, jsx_runtime_1.jsxs)("div", { className: "flex shrink-0 items-center gap-1.5", children: [tier.annualSavingPercent !== null && showAnnual && ((0, jsx_runtime_1.jsxs)(react_1.Chip, { className: "bg-accent-soft text-accent", children: ["Save ", tier.annualSavingPercent, "%"] })), isCurrentTier && (0, jsx_runtime_1.jsx)(react_1.Chip, { className: "bg-accent-soft text-accent", children: "Current" })] })] }), tier.description && (0, jsx_runtime_1.jsx)(react_1.Card.Description, { children: tier.description })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Content, { className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(motion_js_1.AnimatePresence, { mode: "popLayout", initial: false, children: (0, jsx_runtime_1.jsxs)(motion_js_1.m.div, { initial: { opacity: 0, y: 6 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -6 }, transition: { duration: 0.15 }, children: [(0, jsx_runtime_1.jsx)("span", { className: "text-2xl font-semibold text-foreground", children: (0, plan_pricing_js_1.formatPrice)(plan.price) }), (0, jsx_runtime_1.
|
|
129
|
+
return ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { variants: { hidden: { opacity: 0, y: 12 }, show: { opacity: 1, y: 0, transition: motion_js_1.ENTER } }, children: (0, jsx_runtime_1.jsxs)(react_1.Card, { className: isCurrentTier ? "h-full border-accent" : "h-full", children: [(0, jsx_runtime_1.jsxs)(react_1.Card.Header, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [(0, jsx_runtime_1.jsx)(react_1.Card.Title, { children: tier.name }), (0, jsx_runtime_1.jsxs)("div", { className: "flex shrink-0 items-center gap-1.5", children: [tier.annualSavingPercent !== null && showAnnual && ((0, jsx_runtime_1.jsxs)(react_1.Chip, { className: "bg-accent-soft text-accent", children: ["Save ", tier.annualSavingPercent, "%"] })), isCurrentTier && (0, jsx_runtime_1.jsx)(react_1.Chip, { className: "bg-accent-soft text-accent", children: "Current" })] })] }), tier.description && (0, jsx_runtime_1.jsx)(react_1.Card.Description, { children: tier.description })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Content, { className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(motion_js_1.AnimatePresence, { mode: "popLayout", initial: false, children: (0, jsx_runtime_1.jsxs)(motion_js_1.m.div, { initial: { opacity: 0, y: 6 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -6 }, transition: { duration: 0.15 }, children: [(0, jsx_runtime_1.jsx)("span", { className: "text-2xl font-semibold text-foreground", children: (0, plan_pricing_js_1.formatPrice)(plan.price) }), (0, jsx_runtime_1.jsx)("span", { className: "text-sm text-muted", children: (0, plan_pricing_js_1.pricePerLabel)(plan.billingInterval) })] }, plan.billingInterval) }), (0, jsx_runtime_1.jsxs)("p", { className: "mt-1 text-xs text-muted", children: ["+", plan.gstRate, "% GST"] }), cadenceUnavailable && ((0, jsx_runtime_1.jsxs)("p", { className: "mt-1 text-xs text-muted", children: ["Only sold ", (0, plan_pricing_js_1.intervalLabel)(plan.billingInterval).adverb, "."] }))] }), plan.trialDays ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs font-medium text-accent", children: [plan.trialDays, "-day free trial"] })) : null, plan.features.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { className: "flex flex-col gap-1.5", children: plan.features.map((feature) => (
|
|
130
130
|
// `name`, not `key` - the key is an identifier
|
|
131
131
|
// ("campaign-module"), and printing it here is what this
|
|
132
132
|
// card used to do. The description rides along as a
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ConnectPlatformGate = ConnectPlatformGate;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const provider_js_1 = require("../provider.js");
|
|
7
|
+
/**
|
|
8
|
+
* Renders `children` only for a Connect platform staff session.
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <ConnectPlatformGate>
|
|
12
|
+
* <LinkMasterConsole />
|
|
13
|
+
* </ConnectPlatformGate>
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Staff are Connect's own operators - `platform_admin`, `platform_manager`,
|
|
17
|
+
* `developer` - signing into *your* application. They belong to no workspace,
|
|
18
|
+
* so a staff session carries no `workspaceId`, no subscription and no member
|
|
19
|
+
* role; see `SessionAccess.staff`. Staff access is off by default and turned on
|
|
20
|
+
* per application by a Connect platform admin, so this gate renders nothing
|
|
21
|
+
* until that happens.
|
|
22
|
+
*
|
|
23
|
+
* **This is a UX gate, not a security boundary** - the same caveat every gate
|
|
24
|
+
* in this package carries, and it matters more here than anywhere else,
|
|
25
|
+
* because what is behind this one is usually an operator console. The browser
|
|
26
|
+
* can ignore this component entirely. Every route it reveals must re-check
|
|
27
|
+
* `staff` on your own server:
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* const { staff, role } = await client.identity.getUser(sessionToken);
|
|
31
|
+
* if (!staff) return forbidden();
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* Renders `loading` - not `fallback` - while the session is unknown, so an
|
|
35
|
+
* operator never flickers through the customer layout on the way in.
|
|
36
|
+
*/
|
|
37
|
+
function ConnectPlatformGate({ roles, children, fallback = null, loading = null, }) {
|
|
38
|
+
const { isStaff, roles: held, isLoading } = (0, provider_js_1.useConnectPlatformAccess)();
|
|
39
|
+
if (isLoading)
|
|
40
|
+
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: loading });
|
|
41
|
+
if (!isStaff)
|
|
42
|
+
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: fallback });
|
|
43
|
+
// An empty `roles` array is treated as "no constraint", same as omitting it.
|
|
44
|
+
// The alternative - refusing everyone - would turn a `roles={[]}` computed
|
|
45
|
+
// from an empty config into a silently dead console.
|
|
46
|
+
const accepted = !roles || roles.length === 0 || roles.some((role) => held.includes(role));
|
|
47
|
+
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: accepted ? children : fallback });
|
|
48
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.SIDEBAR_ROW_TILE = exports.SIDEBAR_ROW_ICON = exports.SIDEBAR_ROW = exports.RAIL_GUTTER = exports.RAIL_EXPANDED = exports.RAIL_COLLAPSED = exports.defaultRenderLink = void 0;
|
|
5
|
+
exports.sidebarRowClass = sidebarRowClass;
|
|
6
|
+
exports.ConnectShellProvider = ConnectShellProvider;
|
|
7
|
+
exports.useConnectShell = useConnectShell;
|
|
8
|
+
exports.SidebarLabel = SidebarLabel;
|
|
9
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
10
|
+
const react_1 = require("react");
|
|
11
|
+
const motion_js_1 = require("./motion.js");
|
|
12
|
+
/** A plain anchor - correct anywhere, and a full page load. See `ConnectLinkRenderer`. */
|
|
13
|
+
const defaultRenderLink = ({ children, ...props }) => (0, jsx_runtime_1.jsx)("a", { ...props, children: children });
|
|
14
|
+
exports.defaultRenderLink = defaultRenderLink;
|
|
15
|
+
/**
|
|
16
|
+
* The rail's two widths, in pixels, and the one place they are written down.
|
|
17
|
+
*
|
|
18
|
+
* They are numbers rather than Tailwind classes because the rail animates
|
|
19
|
+
* between them - `motion` interpolates a number and cannot interpolate
|
|
20
|
+
* `w-16` into `w-64`.
|
|
21
|
+
*
|
|
22
|
+
* **The pairing is load-bearing, not arbitrary.** Every row in the sidebar is
|
|
23
|
+
* laid out so its glyph sits exactly `RAIL_GUTTER + ROW_GUTTER + 10px` from
|
|
24
|
+
* the rail's left edge - 32px, which is also `RAIL_COLLAPSED / 2`. So the
|
|
25
|
+
* collapsed rail centres every icon *without a single centring rule*, and
|
|
26
|
+
* nothing moves horizontally while the width animates. Change either width
|
|
27
|
+
* and that identity breaks: the icons drift sideways during the transition,
|
|
28
|
+
* which is the exact stutter this geometry exists to avoid.
|
|
29
|
+
*/
|
|
30
|
+
exports.RAIL_COLLAPSED = 64;
|
|
31
|
+
exports.RAIL_EXPANDED = 264;
|
|
32
|
+
/** Horizontal padding inside the rail, on every region. Tailwind `px-3`. */
|
|
33
|
+
exports.RAIL_GUTTER = 12;
|
|
34
|
+
/**
|
|
35
|
+
* The row shape every sidebar control shares - nav items, the workspace tile,
|
|
36
|
+
* the launcher, the user button.
|
|
37
|
+
*
|
|
38
|
+
* `overflow-hidden` + `whitespace-nowrap` is what makes the collapse smooth:
|
|
39
|
+
* the label is *clipped* by the shrinking rail rather than re-wrapped or
|
|
40
|
+
* re-measured, so a width animation costs no reflow. Fading the label with
|
|
41
|
+
* `SidebarLabel` on top of that hides the clip itself.
|
|
42
|
+
*/
|
|
43
|
+
exports.SIDEBAR_ROW = "group/row relative flex h-10 w-full shrink-0 items-center justify-start gap-3 overflow-hidden whitespace-nowrap rounded-lg pe-2 text-sm font-normal transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus";
|
|
44
|
+
/**
|
|
45
|
+
* Leading inset for a row whose glyph is 20px wide (a nav icon, the launcher).
|
|
46
|
+
* `10 + 10 = 20`, and `RAIL_GUTTER + 20 = 32`. See `RAIL_COLLAPSED`.
|
|
47
|
+
*/
|
|
48
|
+
exports.SIDEBAR_ROW_ICON = "ps-[10px]";
|
|
49
|
+
/**
|
|
50
|
+
* Leading inset for a row whose glyph is 32px wide (the avatar, the workspace
|
|
51
|
+
* tile). `4 + 16 = 20`, landing on the same 32px centre as `SIDEBAR_ROW_ICON`.
|
|
52
|
+
*/
|
|
53
|
+
exports.SIDEBAR_ROW_TILE = "ps-[4px]";
|
|
54
|
+
/**
|
|
55
|
+
* The class list for one sidebar row, in one place.
|
|
56
|
+
*
|
|
57
|
+
* Three different elements need it - a plain `<button>` (`ConnectSidebarRow`),
|
|
58
|
+
* a rendered link (`ConnectSidebarNav`), and a HeroUI `<Button>` acting as a
|
|
59
|
+
* `Modal`/`Dropdown` trigger (the account row, the launcher, the workspace
|
|
60
|
+
* tile). They cannot share a component, because a React Aria overlay has to
|
|
61
|
+
* own its own trigger element - so they share this instead. A second copy of
|
|
62
|
+
* the string is a row that drifts off the 32px centre and takes the collapse
|
|
63
|
+
* animation's smoothness with it.
|
|
64
|
+
*
|
|
65
|
+
* On the HeroUI trigger: these are all Tailwind *utilities*, and HeroUI's
|
|
66
|
+
* `.button` is a component class, so the utility layer wins - which is what
|
|
67
|
+
* lets a `w-fit`, centred, `px-`-padded button take this shape at all.
|
|
68
|
+
*/
|
|
69
|
+
function sidebarRowClass(options) {
|
|
70
|
+
// The rail's two text tones, and both were wrong before.
|
|
71
|
+
//
|
|
72
|
+
// **Resting rows are `muted`, not `foreground`.** Against `--surface`,
|
|
73
|
+
// `--foreground` measures 17.6:1 in light and 15.8:1 in dark - body-copy
|
|
74
|
+
// weight, applied to eight items at once, in a column whose whole job is to
|
|
75
|
+
// sit still at the edge of the screen. The result reads as heavy and, worse,
|
|
76
|
+
// gives every inactive destination exactly the emphasis of the active one,
|
|
77
|
+
// so the selected row has to win on background alone. `--muted` is 4.8:1
|
|
78
|
+
// light and 6.2:1 dark - comfortably past AA for normal text, and quiet
|
|
79
|
+
// enough that "where am I" is answerable at a glance. Hover restores
|
|
80
|
+
// `foreground`, so pointing at a row is what brings it forward.
|
|
81
|
+
//
|
|
82
|
+
// **The active row uses `accent-soft-foreground`, never `accent`.**
|
|
83
|
+
// `text-accent` on `bg-accent-soft` is 2.8:1 in light and 4.0:1 in dark -
|
|
84
|
+
// both below AA, on the one row the user most needs to read. HeroUI derives
|
|
85
|
+
// `--accent-soft-foreground` for exactly this pairing (a color-mix of accent
|
|
86
|
+
// into foreground), which measures 4.8:1 and 5.6:1. It is derived rather
|
|
87
|
+
// than fixed, so a host that re-themes `--accent` keeps the contrast.
|
|
88
|
+
//
|
|
89
|
+
// **`bg-transparent` is not redundant.** Three of these rows are HeroUI
|
|
90
|
+
// `<Button variant="tertiary">` (the launcher, the account row, the
|
|
91
|
+
// workspace tile) because they trigger a `Modal` or a `Dropdown`, and
|
|
92
|
+
// `.button--tertiary` sets `--button-bg: var(--default)` - a real fill. Next
|
|
93
|
+
// to the plain anchors the nav renders, those three came out looking
|
|
94
|
+
// permanently hovered, as though three rows in the rail were selected at
|
|
95
|
+
// once. Only the resting tone clears it; the active tone paints its own.
|
|
96
|
+
const tone = options?.active
|
|
97
|
+
? "bg-accent-soft font-medium text-accent-soft-foreground"
|
|
98
|
+
: "bg-transparent text-muted hover:bg-surface-secondary hover:text-foreground";
|
|
99
|
+
return [exports.SIDEBAR_ROW, options?.tile ? exports.SIDEBAR_ROW_TILE : exports.SIDEBAR_ROW_ICON, tone, options?.className]
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.join(" ");
|
|
102
|
+
}
|
|
103
|
+
const FALLBACK = {
|
|
104
|
+
collapsed: false,
|
|
105
|
+
collapsible: false,
|
|
106
|
+
setCollapsed: () => { },
|
|
107
|
+
renderLink: exports.defaultRenderLink,
|
|
108
|
+
};
|
|
109
|
+
const ShellContext = (0, react_1.createContext)(null);
|
|
110
|
+
/**
|
|
111
|
+
* Shared state for every part of the shell, so `ConnectSidebarNav`,
|
|
112
|
+
* `ConnectWorkspaceSwitcher` and `ConnectUserButton` all know whether the rail
|
|
113
|
+
* is collapsed without the host threading a prop through three layers.
|
|
114
|
+
*
|
|
115
|
+
* This is the piece that makes the shell *composable* rather than only
|
|
116
|
+
* configurable: the parts are exported and can be rearranged, and they still
|
|
117
|
+
* agree about collapse, routing and the mobile drawer because they read one
|
|
118
|
+
* context instead of one another's props.
|
|
119
|
+
*/
|
|
120
|
+
function ConnectShellProvider({ value, children, }) {
|
|
121
|
+
return (0, jsx_runtime_1.jsx)(ShellContext, { value: value, children: children });
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The shell state a sidebar part is rendering inside.
|
|
125
|
+
*
|
|
126
|
+
* Falls back to "expanded, not collapsible, plain anchors" rather than
|
|
127
|
+
* throwing, because every part is also exported for use *outside* a shell -
|
|
128
|
+
* a host dropping `ConnectSidebarNav` into a layout it already owns should
|
|
129
|
+
* not have to mount a provider to get a nav column.
|
|
130
|
+
*/
|
|
131
|
+
function useConnectShell() {
|
|
132
|
+
return (0, react_1.use)(ShellContext) ?? FALLBACK;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A sidebar row's text, faded out as the rail collapses.
|
|
136
|
+
*
|
|
137
|
+
* Opacity only - never width, height or margin. The rail's own width
|
|
138
|
+
* animation already moves the clip edge past this text; animating the text's
|
|
139
|
+
* box as well would make two things fight over the same pixels and is what
|
|
140
|
+
* makes a collapsing sidebar look like it stutters.
|
|
141
|
+
*
|
|
142
|
+
* Asymmetric timing on purpose. Collapsing, the label leaves *first* (no
|
|
143
|
+
* delay, quick) so the rail never appears to squeeze text; expanding, it
|
|
144
|
+
* arrives *after* the rail has opened enough to hold it.
|
|
145
|
+
*/
|
|
146
|
+
function SidebarLabel({ children, className, }) {
|
|
147
|
+
const { collapsed } = useConnectShell();
|
|
148
|
+
return ((0, jsx_runtime_1.jsx)(motion_js_1.m.span, { className: `min-w-0 flex-1 truncate text-start ${className ?? ""}`, animate: { opacity: collapsed ? 0 : 1 }, transition: collapsed ? { duration: 0.1 } : { duration: 0.18, delay: 0.1 }, "aria-hidden": collapsed, children: children }));
|
|
149
|
+
}
|
|
@@ -1,58 +1,84 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.defaultRenderLink = void 0;
|
|
5
|
-
exports.
|
|
4
|
+
exports.ConnectSideNav = exports.defaultRenderLink = void 0;
|
|
5
|
+
exports.ConnectSidebarNav = ConnectSidebarNav;
|
|
6
6
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
7
7
|
const react_1 = require("@heroui/react");
|
|
8
8
|
const lucide_react_1 = require("lucide-react");
|
|
9
|
+
const motion_js_1 = require("./motion.js");
|
|
10
|
+
const shell_context_js_1 = require("./shell-context.js");
|
|
11
|
+
Object.defineProperty(exports, "defaultRenderLink", { enumerable: true, get: function () { return shell_context_js_1.defaultRenderLink; } });
|
|
9
12
|
const use_active_path_js_1 = require("../use-active-path.js");
|
|
10
13
|
const nav_js_1 = require("./nav.js");
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
? "bg-accent-soft font-medium text-accent"
|
|
19
|
-
: "text-foreground hover:bg-surface-secondary";
|
|
20
|
-
return `${ROW} ${shape} ${tone}`;
|
|
14
|
+
function rowClass(isActive, depth) {
|
|
15
|
+
// The indent is a *margin*, not padding, so a child row steps in while the
|
|
16
|
+
// glyph inside it keeps the shared 32px centre every other row uses.
|
|
17
|
+
return (0, shell_context_js_1.sidebarRowClass)({
|
|
18
|
+
active: isActive,
|
|
19
|
+
className: depth > 0 ? "ms-4 w-[calc(100%-1rem)]" : undefined,
|
|
20
|
+
});
|
|
21
21
|
}
|
|
22
22
|
function NavRow({ item, isActive, hasActiveChild, collapsed, depth, renderLink, onNavigate, }) {
|
|
23
23
|
const linkProps = {
|
|
24
24
|
href: item.href,
|
|
25
|
-
className: rowClass(isActive,
|
|
25
|
+
className: rowClass(isActive, depth),
|
|
26
26
|
"aria-label": item.label,
|
|
27
27
|
"aria-current": isActive ? "page" : undefined,
|
|
28
28
|
onClick: onNavigate,
|
|
29
29
|
...(item.external ? { target: "_blank", rel: "noopener noreferrer" } : {}),
|
|
30
|
-
children: ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [
|
|
30
|
+
children: ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "flex size-5 shrink-0 items-center justify-center", children: item.icon }), (0, jsx_runtime_1.jsx)(shell_context_js_1.SidebarLabel, { children: item.label }), (item.badge || item.external) && ((0, jsx_runtime_1.jsxs)(motion_js_1.m.span, { className: "flex shrink-0 items-center gap-1", animate: { opacity: collapsed ? 0 : 1 }, transition: collapsed ? { duration: 0.1 } : { duration: 0.18, delay: 0.1 }, "aria-hidden": collapsed, children: [item.badge, item.external && (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpRight, { className: "size-3.5 text-muted", "aria-hidden": "true" })] })), collapsed && hasActiveChild && !isActive && ((0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", className: "absolute bottom-1 start-[18px] size-1 rounded-full bg-accent" }))] })),
|
|
31
31
|
};
|
|
32
32
|
// The tooltip stays mounted across expand/collapse and only toggles
|
|
33
33
|
// `isDisabled`. Branching into two trees instead re-creates every item's
|
|
34
34
|
// overlay and focus machinery on each toggle, which is what makes the width
|
|
35
35
|
// animation stutter.
|
|
36
|
-
return ((0, jsx_runtime_1.jsxs)(react_1.Tooltip, { delay:
|
|
36
|
+
return ((0, jsx_runtime_1.jsxs)(react_1.Tooltip, { delay: 400, isDisabled: !collapsed, children: [(0, jsx_runtime_1.jsx)(react_1.Tooltip.Trigger, { children: renderLink(linkProps) }), (0, jsx_runtime_1.jsx)(react_1.Tooltip.Content, { placement: "right", children: item.label })] }));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A section heading that keeps its box when the rail collapses.
|
|
40
|
+
*
|
|
41
|
+
* The old rail swapped the heading for a hairline, which changed the group's
|
|
42
|
+
* height and made every row below it jump at the moment the width was already
|
|
43
|
+
* moving. Here the box is a constant height in both states: the text fades out
|
|
44
|
+
* and a rule fades in, in the same place.
|
|
45
|
+
*/
|
|
46
|
+
function SectionHeading({ title, collapsed }) {
|
|
47
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "relative flex h-7 shrink-0 items-center px-[10px]", children: [(0, jsx_runtime_1.jsx)(motion_js_1.m.h2, { className: "truncate text-xs font-semibold uppercase tracking-wider text-muted", animate: { opacity: collapsed ? 0 : 1 }, transition: collapsed ? { duration: 0.1 } : { duration: 0.18, delay: 0.1 }, "aria-hidden": collapsed, children: title }), (0, jsx_runtime_1.jsx)(motion_js_1.m.span, { "aria-hidden": "true", className: "absolute inset-x-[10px] top-1/2 h-px bg-separator", animate: { opacity: collapsed ? 1 : 0 }, transition: { duration: 0.15 } })] }));
|
|
37
48
|
}
|
|
38
49
|
/**
|
|
39
50
|
* The sidebar's nav column on its own - sections, items, one level of
|
|
40
51
|
* children, and the active highlight.
|
|
41
52
|
*
|
|
42
53
|
* `AppShell` renders this inside its rail and its mobile drawer, and it
|
|
43
|
-
* is exported for a host that wants the shell's navigation without its
|
|
44
|
-
*
|
|
45
|
-
*
|
|
54
|
+
* is exported for a host that wants the shell's navigation without its chrome:
|
|
55
|
+
* the same items, the same active resolution, dropped into a layout you
|
|
56
|
+
* already have.
|
|
57
|
+
*
|
|
58
|
+
* Every prop is optional except `nav`, because inside a shell they are all
|
|
59
|
+
* inherited from it. Passing one wins over what the shell says - which is how
|
|
60
|
+
* a second nav column (see `secondaryNav`) can share the rail's collapse state
|
|
61
|
+
* while keeping its own items.
|
|
46
62
|
*/
|
|
47
|
-
function
|
|
48
|
-
const
|
|
63
|
+
function ConnectSidebarNav({ nav, activePath, renderLink, collapsed, onNavigate, className, }) {
|
|
64
|
+
const shell = (0, shell_context_js_1.useConnectShell)();
|
|
65
|
+
const isCollapsed = collapsed ?? shell.collapsed;
|
|
66
|
+
const link = renderLink ?? shell.renderLink;
|
|
67
|
+
const navigate = onNavigate ?? shell.onNavigate;
|
|
68
|
+
const pathname = (0, use_active_path_js_1.useActivePath)(activePath ?? shell.activePath);
|
|
49
69
|
const active = (0, nav_js_1.resolveActivePath)(pathname, nav);
|
|
50
70
|
const sections = (0, nav_js_1.normalizeNav)(nav);
|
|
51
|
-
return ((0, jsx_runtime_1.jsx)("div", { className: `flex flex-col gap-
|
|
71
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: `flex flex-col gap-2 ${className ?? ""}`, children: sections.map((section, index) => ((0, jsx_runtime_1.jsxs)("nav", { "aria-label": section.title ?? "Main", className: "flex flex-col gap-1", children: [section.title && (0, jsx_runtime_1.jsx)(SectionHeading, { title: section.title, collapsed: isCollapsed }), section.items.map((item) => {
|
|
52
72
|
const isActive = (0, nav_js_1.isNavItemActive)(item, active);
|
|
53
73
|
const activeChild = (item.items ?? []).some((child) => (0, nav_js_1.isNavItemActive)(child, active));
|
|
54
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className:
|
|
74
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex w-full flex-col gap-1", children: [(0, jsx_runtime_1.jsx)(NavRow, { item: item, isActive: isActive, hasActiveChild: activeChild, collapsed: isCollapsed, depth: 0, renderLink: link, onNavigate: navigate }), !isCollapsed &&
|
|
55
75
|
(isActive || activeChild) &&
|
|
56
|
-
item.items?.map((child) => ((0, jsx_runtime_1.jsx)(NavRow, { item: child, isActive: (0, nav_js_1.isNavItemActive)(child, active), hasActiveChild: false, collapsed: false, depth: 1, renderLink:
|
|
76
|
+
item.items?.map((child) => ((0, jsx_runtime_1.jsx)(NavRow, { item: child, isActive: (0, nav_js_1.isNavItemActive)(child, active), hasActiveChild: false, collapsed: false, depth: 1, renderLink: link, onNavigate: navigate }, child.href)))] }, item.href));
|
|
57
77
|
})] }, section.title ?? index))) }));
|
|
58
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* @deprecated Renamed to `ConnectSidebarNav` in 0.12 - the rail is a
|
|
81
|
+
* `ConnectSidebar` now, and its pieces are named after it. Identical
|
|
82
|
+
* behaviour; this alias will be removed in a later minor.
|
|
83
|
+
*/
|
|
84
|
+
exports.ConnectSideNav = ConnectSidebarNav;
|