@cosmoops/connect-sdk 0.9.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +566 -0
- package/README.md +113 -42
- package/dist/cjs/cli/doctor.js +3 -3
- package/dist/cjs/cli/init.js +3 -3
- package/dist/cjs/client/components/access-gate.js +20 -1
- package/dist/cjs/client/components/ai-screens.js +73 -0
- package/dist/cjs/client/components/ai-widgets.js +37 -0
- package/dist/cjs/client/components/app-shell.js +75 -41
- package/dist/cjs/client/components/apps-grid.js +3 -2
- package/dist/cjs/client/components/ask-ai.js +50 -5
- package/dist/cjs/client/components/confirm-dialog.js +40 -0
- package/dist/cjs/client/components/cursor-pagination.js +22 -0
- package/dist/cjs/client/components/filter-tabs.js +24 -0
- package/dist/cjs/client/components/grievance-notice.js +30 -0
- package/dist/cjs/client/components/link-button.js +17 -10
- package/dist/cjs/client/components/logo.js +4 -3
- package/dist/cjs/client/components/nav.js +41 -0
- package/dist/cjs/client/components/plan-pricing.js +27 -0
- package/dist/cjs/client/components/plans.js +1 -1
- package/dist/cjs/client/components/platform-gate.js +48 -0
- package/dist/cjs/client/components/shell-context.js +149 -0
- package/dist/cjs/client/components/side-nav.js +49 -23
- package/dist/cjs/client/components/sidebar.js +127 -0
- package/dist/cjs/client/components/subscribe-dialog.js +2 -2
- package/dist/cjs/client/components/subscription.js +1 -1
- package/dist/cjs/client/components/user-menu.js +96 -37
- package/dist/cjs/client/components/workspace-switcher.js +100 -0
- package/dist/cjs/client/index.js +72 -2
- package/dist/cjs/client/provider.js +57 -2
- package/dist/cjs/client/use-checkout.js +9 -5
- package/dist/cjs/errors.js +1 -1
- package/dist/cjs/index.js +16 -1
- package/dist/cjs/mcp/index.js +90 -0
- package/dist/cjs/mcp/tools.js +369 -0
- package/dist/cjs/next/index.js +2 -2
- package/dist/cjs/server/client.js +25 -2
- package/dist/cjs/server/handler.js +203 -18
- package/dist/cjs/server/http.js +6 -2
- package/dist/cjs/server/resources/acting-as.js +16 -0
- package/dist/cjs/server/resources/ai.js +22 -2
- package/dist/cjs/server/resources/data.js +374 -0
- package/dist/cjs/server/resources/identity.js +7 -3
- package/dist/cjs/server/resources/kv.js +84 -0
- package/dist/cjs/server/resources/platform.js +43 -0
- package/dist/cjs/server/resources/sessions.js +1 -1
- package/dist/cjs/server/resources/workspaces.js +74 -0
- package/dist/cjs/store-schema.js +66 -0
- package/dist/cjs/store.js +152 -0
- package/dist/cjs/ui/empty-state.js +28 -0
- package/dist/cjs/ui/index.js +44 -0
- package/dist/cjs/ui/page-header.js +27 -0
- package/dist/cjs/ui/page-shell.js +44 -0
- package/dist/cjs/ui/section.js +20 -0
- package/dist/cjs/ui/setting-row.js +14 -0
- package/dist/cjs/ui/status-chip.js +17 -0
- package/dist/esm/cli/doctor.js +3 -3
- package/dist/esm/cli/init.js +3 -3
- package/dist/esm/client/components/access-gate.js +20 -1
- package/dist/esm/client/components/ai-screens.js +69 -0
- package/dist/esm/client/components/ai-widgets.js +37 -1
- package/dist/esm/client/components/app-shell.js +81 -47
- package/dist/esm/client/components/apps-grid.js +3 -2
- package/dist/esm/client/components/ask-ai.js +51 -6
- package/dist/esm/client/components/confirm-dialog.js +37 -0
- package/dist/esm/client/components/cursor-pagination.js +19 -0
- package/dist/esm/client/components/filter-tabs.js +21 -0
- package/dist/esm/client/components/grievance-notice.js +27 -0
- package/dist/esm/client/components/link-button.js +17 -10
- package/dist/esm/client/components/logo.js +4 -3
- package/dist/esm/client/components/nav.js +40 -0
- package/dist/esm/client/components/plan-pricing.js +25 -0
- package/dist/esm/client/components/plans.js +2 -2
- package/dist/esm/client/components/platform-gate.js +45 -0
- package/dist/esm/client/components/shell-context.js +141 -0
- package/dist/esm/client/components/side-nav.js +48 -21
- package/dist/esm/client/components/sidebar.js +120 -0
- package/dist/esm/client/components/subscribe-dialog.js +3 -3
- package/dist/esm/client/components/subscription.js +2 -2
- package/dist/esm/client/components/user-menu.js +95 -38
- package/dist/esm/client/components/workspace-switcher.js +97 -0
- package/dist/esm/client/index.js +47 -4
- package/dist/esm/client/provider.js +53 -2
- package/dist/esm/client/use-checkout.js +9 -5
- package/dist/esm/errors.js +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/mcp/index.js +85 -0
- package/dist/esm/mcp/tools.js +366 -0
- package/dist/esm/next/index.js +2 -2
- package/dist/esm/server/client.js +25 -2
- package/dist/esm/server/handler.js +203 -18
- package/dist/esm/server/http.js +6 -2
- package/dist/esm/server/resources/acting-as.js +13 -0
- package/dist/esm/server/resources/ai.js +22 -2
- package/dist/esm/server/resources/data.js +369 -0
- package/dist/esm/server/resources/identity.js +7 -3
- package/dist/esm/server/resources/kv.js +80 -0
- package/dist/esm/server/resources/platform.js +39 -0
- package/dist/esm/server/resources/sessions.js +1 -1
- package/dist/esm/server/resources/workspaces.js +70 -0
- package/dist/esm/store-schema.js +62 -0
- package/dist/esm/store.js +141 -0
- package/dist/esm/ui/empty-state.js +25 -0
- package/dist/esm/ui/index.js +34 -0
- package/dist/esm/ui/page-header.js +24 -0
- package/dist/esm/ui/page-shell.js +41 -0
- package/dist/esm/ui/section.js +17 -0
- package/dist/esm/ui/setting-row.js +11 -0
- package/dist/esm/ui/status-chip.js +14 -0
- package/dist/types/client/components/ai-screens.d.ts +25 -0
- package/dist/types/client/components/ai-widgets.d.ts +34 -1
- package/dist/types/client/components/app-shell.d.ts +65 -27
- package/dist/types/client/components/apps-grid.d.ts +11 -2
- package/dist/types/client/components/ask-ai.d.ts +47 -1
- package/dist/types/client/components/confirm-dialog.d.ts +34 -0
- package/dist/types/client/components/cursor-pagination.d.ts +27 -0
- package/dist/types/client/components/filter-tabs.d.ts +26 -0
- package/dist/types/client/components/grievance-notice.d.ts +31 -0
- package/dist/types/client/components/link-button.d.ts +32 -9
- package/dist/types/client/components/logo.d.ts +18 -2
- package/dist/types/client/components/nav.d.ts +24 -0
- package/dist/types/client/components/plan-pricing.d.ts +10 -0
- package/dist/types/client/components/platform-gate.d.ts +53 -0
- package/dist/types/client/components/shell-context.d.ts +116 -0
- package/dist/types/client/components/side-nav.d.ts +27 -13
- package/dist/types/client/components/sidebar.d.ts +95 -0
- package/dist/types/client/components/user-menu.d.ts +68 -22
- package/dist/types/client/components/workspace-switcher.d.ts +49 -0
- package/dist/types/client/index.d.ts +29 -5
- package/dist/types/client/provider.d.ts +109 -6
- package/dist/types/client/use-checkout.d.ts +8 -4
- package/dist/types/errors.d.ts +1 -1
- package/dist/types/index.d.ts +6 -0
- package/dist/types/mcp/index.d.ts +74 -0
- package/dist/types/mcp/tools.d.ts +107 -0
- package/dist/types/next/index.d.ts +1 -1
- package/dist/types/server/client.d.ts +25 -2
- package/dist/types/server/http.d.ts +8 -2
- package/dist/types/server/index.d.ts +4 -0
- package/dist/types/server/resources/acting-as.d.ts +11 -0
- package/dist/types/server/resources/ai.d.ts +20 -2
- package/dist/types/server/resources/data.d.ts +258 -0
- package/dist/types/server/resources/kv.d.ts +63 -0
- package/dist/types/server/resources/platform.d.ts +32 -0
- package/dist/types/server/resources/workspaces.d.ts +55 -0
- package/dist/types/store-schema.d.ts +52 -0
- package/dist/types/store.d.ts +96 -0
- package/dist/types/types.d.ts +369 -16
- package/dist/types/ui/empty-state.d.ts +34 -0
- package/dist/types/ui/index.d.ts +40 -0
- package/dist/types/ui/page-header.d.ts +34 -0
- package/dist/types/ui/page-shell.d.ts +39 -0
- package/dist/types/ui/section.d.ts +26 -0
- package/dist/types/ui/setting-row.d.ts +18 -0
- package/dist/types/ui/status-chip.d.ts +36 -0
- package/package.json +15 -2
|
@@ -31,6 +31,31 @@ export function formatPrice(paise) {
|
|
|
31
31
|
export function monthlyOf(price, billingInterval) {
|
|
32
32
|
return billingInterval === "annual" ? price / 12 : price;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* How to name a plan's cadence, in the three forms the plan screens need.
|
|
36
|
+
*
|
|
37
|
+
* One table rather than a `=== "annual" ? "year" : "month"` at each call site.
|
|
38
|
+
* That ternary was correct while there were two intervals and quietly became
|
|
39
|
+
* wrong when `"one-time"` arrived: a plan paid for once was labelled
|
|
40
|
+
* "per month", which is not a rendering imperfection but a false price.
|
|
41
|
+
*/
|
|
42
|
+
const INTERVAL_LABELS = {
|
|
43
|
+
monthly: { per: "month", short: "mo", adverb: "monthly" },
|
|
44
|
+
annual: { per: "year", short: "yr", adverb: "annually" },
|
|
45
|
+
// "once" reads correctly in all three positions a cadence appears in -
|
|
46
|
+
// "₹4,999 once", "Total once", "Only sold once".
|
|
47
|
+
"one-time": { per: "once", short: "once", adverb: "once" },
|
|
48
|
+
};
|
|
49
|
+
export function intervalLabel(billingInterval) {
|
|
50
|
+
return INTERVAL_LABELS[billingInterval];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `/month`, `/year` - and nothing at all for a one-time plan, where a price
|
|
54
|
+
* with no slash is the honest rendering.
|
|
55
|
+
*/
|
|
56
|
+
export function pricePerLabel(billingInterval) {
|
|
57
|
+
return billingInterval === "one-time" ? "" : `/${INTERVAL_LABELS[billingInterval].per}`;
|
|
58
|
+
}
|
|
34
59
|
export function discountLabel(discount) {
|
|
35
60
|
const amount = discount.discountType === "percentage" ? `${discount.value}% off` : `${formatPrice(discount.value)} off`;
|
|
36
61
|
if (discount.duration === "forever")
|
|
@@ -7,7 +7,7 @@ import { useConnect, useConnectEntitlements, useConnectPlans } from "../provider
|
|
|
7
7
|
import { buildCancelUrl, buildCheckoutUrl, useConnectCheckout, } from "../use-checkout.js";
|
|
8
8
|
import { AnimatePresence, ENTER, m, MotionProvider } from "./motion.js";
|
|
9
9
|
import { ConnectSubscribeDialog } from "./subscribe-dialog.js";
|
|
10
|
-
import { ACTION_LABEL, discountLabel, formatPrice, groupPlansByTier, resolveAction, } from "./plan-pricing.js";
|
|
10
|
+
import { ACTION_LABEL, discountLabel, formatPrice, groupPlansByTier, intervalLabel, pricePerLabel, resolveAction, } from "./plan-pricing.js";
|
|
11
11
|
/**
|
|
12
12
|
* The plan table: monthly/annual toggle, features, add-ons, the discount
|
|
13
13
|
* actually in effect, and the subscribe / upgrade / downgrade flows.
|
|
@@ -123,7 +123,7 @@ export function ConnectPlans({ planIds, hideCoupon = false, className }) {
|
|
|
123
123
|
(tier.monthly?.id === subscription.plan.id || tier.annual?.id === subscription.plan.id);
|
|
124
124
|
const isCurrent = action === "current";
|
|
125
125
|
const cadenceUnavailable = showAnnual ? tier.annual === null : tier.monthly === null;
|
|
126
|
-
return (_jsx(m.div, { variants: { hidden: { opacity: 0, y: 12 }, show: { opacity: 1, y: 0, transition: ENTER } }, children: _jsxs(Card, { className: isCurrentTier ? "h-full border-accent" : "h-full", children: [_jsxs(Card.Header, { children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx(Card.Title, { children: tier.name }), _jsxs("div", { className: "flex shrink-0 items-center gap-1.5", children: [tier.annualSavingPercent !== null && showAnnual && (_jsxs(Chip, { className: "bg-accent-soft text-accent", children: ["Save ", tier.annualSavingPercent, "%"] })), isCurrentTier && _jsx(Chip, { className: "bg-accent-soft text-accent", children: "Current" })] })] }), tier.description && _jsx(Card.Description, { children: tier.description })] }), _jsxs(Card.Content, { className: "flex flex-col gap-4", children: [_jsxs("div", { children: [_jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsxs(m.div, { initial: { opacity: 0, y: 6 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -6 }, transition: { duration: 0.15 }, children: [_jsx("span", { className: "text-2xl font-semibold text-foreground", children: formatPrice(plan.price) }),
|
|
126
|
+
return (_jsx(m.div, { variants: { hidden: { opacity: 0, y: 12 }, show: { opacity: 1, y: 0, transition: ENTER } }, children: _jsxs(Card, { className: isCurrentTier ? "h-full border-accent" : "h-full", children: [_jsxs(Card.Header, { children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx(Card.Title, { children: tier.name }), _jsxs("div", { className: "flex shrink-0 items-center gap-1.5", children: [tier.annualSavingPercent !== null && showAnnual && (_jsxs(Chip, { className: "bg-accent-soft text-accent", children: ["Save ", tier.annualSavingPercent, "%"] })), isCurrentTier && _jsx(Chip, { className: "bg-accent-soft text-accent", children: "Current" })] })] }), tier.description && _jsx(Card.Description, { children: tier.description })] }), _jsxs(Card.Content, { className: "flex flex-col gap-4", children: [_jsxs("div", { children: [_jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsxs(m.div, { initial: { opacity: 0, y: 6 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -6 }, transition: { duration: 0.15 }, children: [_jsx("span", { className: "text-2xl font-semibold text-foreground", children: formatPrice(plan.price) }), _jsx("span", { className: "text-sm text-muted", children: pricePerLabel(plan.billingInterval) })] }, plan.billingInterval) }), _jsxs("p", { className: "mt-1 text-xs text-muted", children: ["+", plan.gstRate, "% GST"] }), cadenceUnavailable && (_jsxs("p", { className: "mt-1 text-xs text-muted", children: ["Only sold ", intervalLabel(plan.billingInterval).adverb, "."] }))] }), plan.trialDays ? (_jsxs("p", { className: "text-xs font-medium text-accent", children: [plan.trialDays, "-day free trial"] })) : null, plan.features.length > 0 && (_jsx("ul", { className: "flex flex-col gap-1.5", children: plan.features.map((feature) => (
|
|
127
127
|
// `name`, not `key` - the key is an identifier
|
|
128
128
|
// ("campaign-module"), and printing it here is what this
|
|
129
129
|
// card used to do. The description rides along as a
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useConnectPlatformAccess } from "../provider.js";
|
|
4
|
+
/**
|
|
5
|
+
* Renders `children` only for a Connect platform staff session.
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <ConnectPlatformGate>
|
|
9
|
+
* <LinkMasterConsole />
|
|
10
|
+
* </ConnectPlatformGate>
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Staff are Connect's own operators - `platform_admin`, `platform_manager`,
|
|
14
|
+
* `developer` - signing into *your* application. They belong to no workspace,
|
|
15
|
+
* so a staff session carries no `workspaceId`, no subscription and no member
|
|
16
|
+
* role; see `SessionAccess.staff`. Staff access is off by default and turned on
|
|
17
|
+
* per application by a Connect platform admin, so this gate renders nothing
|
|
18
|
+
* until that happens.
|
|
19
|
+
*
|
|
20
|
+
* **This is a UX gate, not a security boundary** - the same caveat every gate
|
|
21
|
+
* in this package carries, and it matters more here than anywhere else,
|
|
22
|
+
* because what is behind this one is usually an operator console. The browser
|
|
23
|
+
* can ignore this component entirely. Every route it reveals must re-check
|
|
24
|
+
* `staff` on your own server:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* const { staff, role } = await client.identity.getUser(sessionToken);
|
|
28
|
+
* if (!staff) return forbidden();
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Renders `loading` - not `fallback` - while the session is unknown, so an
|
|
32
|
+
* operator never flickers through the customer layout on the way in.
|
|
33
|
+
*/
|
|
34
|
+
export function ConnectPlatformGate({ roles, children, fallback = null, loading = null, }) {
|
|
35
|
+
const { isStaff, roles: held, isLoading } = useConnectPlatformAccess();
|
|
36
|
+
if (isLoading)
|
|
37
|
+
return _jsx(_Fragment, { children: loading });
|
|
38
|
+
if (!isStaff)
|
|
39
|
+
return _jsx(_Fragment, { children: fallback });
|
|
40
|
+
// An empty `roles` array is treated as "no constraint", same as omitting it.
|
|
41
|
+
// The alternative - refusing everyone - would turn a `roles={[]}` computed
|
|
42
|
+
// from an empty config into a silently dead console.
|
|
43
|
+
const accepted = !roles || roles.length === 0 || roles.some((role) => held.includes(role));
|
|
44
|
+
return _jsx(_Fragment, { children: accepted ? children : fallback });
|
|
45
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, use } from "react";
|
|
4
|
+
import { m } from "./motion.js";
|
|
5
|
+
/** A plain anchor - correct anywhere, and a full page load. See `ConnectLinkRenderer`. */
|
|
6
|
+
export const defaultRenderLink = ({ children, ...props }) => _jsx("a", { ...props, children: children });
|
|
7
|
+
/**
|
|
8
|
+
* The rail's two widths, in pixels, and the one place they are written down.
|
|
9
|
+
*
|
|
10
|
+
* They are numbers rather than Tailwind classes because the rail animates
|
|
11
|
+
* between them - `motion` interpolates a number and cannot interpolate
|
|
12
|
+
* `w-16` into `w-64`.
|
|
13
|
+
*
|
|
14
|
+
* **The pairing is load-bearing, not arbitrary.** Every row in the sidebar is
|
|
15
|
+
* laid out so its glyph sits exactly `RAIL_GUTTER + ROW_GUTTER + 10px` from
|
|
16
|
+
* the rail's left edge - 32px, which is also `RAIL_COLLAPSED / 2`. So the
|
|
17
|
+
* collapsed rail centres every icon *without a single centring rule*, and
|
|
18
|
+
* nothing moves horizontally while the width animates. Change either width
|
|
19
|
+
* and that identity breaks: the icons drift sideways during the transition,
|
|
20
|
+
* which is the exact stutter this geometry exists to avoid.
|
|
21
|
+
*/
|
|
22
|
+
export const RAIL_COLLAPSED = 64;
|
|
23
|
+
export const RAIL_EXPANDED = 264;
|
|
24
|
+
/** Horizontal padding inside the rail, on every region. Tailwind `px-3`. */
|
|
25
|
+
export const RAIL_GUTTER = 12;
|
|
26
|
+
/**
|
|
27
|
+
* The row shape every sidebar control shares - nav items, the workspace tile,
|
|
28
|
+
* the launcher, the user button.
|
|
29
|
+
*
|
|
30
|
+
* `overflow-hidden` + `whitespace-nowrap` is what makes the collapse smooth:
|
|
31
|
+
* the label is *clipped* by the shrinking rail rather than re-wrapped or
|
|
32
|
+
* re-measured, so a width animation costs no reflow. Fading the label with
|
|
33
|
+
* `SidebarLabel` on top of that hides the clip itself.
|
|
34
|
+
*/
|
|
35
|
+
export const 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";
|
|
36
|
+
/**
|
|
37
|
+
* Leading inset for a row whose glyph is 20px wide (a nav icon, the launcher).
|
|
38
|
+
* `10 + 10 = 20`, and `RAIL_GUTTER + 20 = 32`. See `RAIL_COLLAPSED`.
|
|
39
|
+
*/
|
|
40
|
+
export const SIDEBAR_ROW_ICON = "ps-[10px]";
|
|
41
|
+
/**
|
|
42
|
+
* Leading inset for a row whose glyph is 32px wide (the avatar, the workspace
|
|
43
|
+
* tile). `4 + 16 = 20`, landing on the same 32px centre as `SIDEBAR_ROW_ICON`.
|
|
44
|
+
*/
|
|
45
|
+
export const SIDEBAR_ROW_TILE = "ps-[4px]";
|
|
46
|
+
/**
|
|
47
|
+
* The class list for one sidebar row, in one place.
|
|
48
|
+
*
|
|
49
|
+
* Three different elements need it - a plain `<button>` (`ConnectSidebarRow`),
|
|
50
|
+
* a rendered link (`ConnectSidebarNav`), and a HeroUI `<Button>` acting as a
|
|
51
|
+
* `Modal`/`Dropdown` trigger (the account row, the launcher, the workspace
|
|
52
|
+
* tile). They cannot share a component, because a React Aria overlay has to
|
|
53
|
+
* own its own trigger element - so they share this instead. A second copy of
|
|
54
|
+
* the string is a row that drifts off the 32px centre and takes the collapse
|
|
55
|
+
* animation's smoothness with it.
|
|
56
|
+
*
|
|
57
|
+
* On the HeroUI trigger: these are all Tailwind *utilities*, and HeroUI's
|
|
58
|
+
* `.button` is a component class, so the utility layer wins - which is what
|
|
59
|
+
* lets a `w-fit`, centred, `px-`-padded button take this shape at all.
|
|
60
|
+
*/
|
|
61
|
+
export function sidebarRowClass(options) {
|
|
62
|
+
// The rail's two text tones, and both were wrong before.
|
|
63
|
+
//
|
|
64
|
+
// **Resting rows are `muted`, not `foreground`.** Against `--surface`,
|
|
65
|
+
// `--foreground` measures 17.6:1 in light and 15.8:1 in dark - body-copy
|
|
66
|
+
// weight, applied to eight items at once, in a column whose whole job is to
|
|
67
|
+
// sit still at the edge of the screen. The result reads as heavy and, worse,
|
|
68
|
+
// gives every inactive destination exactly the emphasis of the active one,
|
|
69
|
+
// so the selected row has to win on background alone. `--muted` is 4.8:1
|
|
70
|
+
// light and 6.2:1 dark - comfortably past AA for normal text, and quiet
|
|
71
|
+
// enough that "where am I" is answerable at a glance. Hover restores
|
|
72
|
+
// `foreground`, so pointing at a row is what brings it forward.
|
|
73
|
+
//
|
|
74
|
+
// **The active row uses `accent-soft-foreground`, never `accent`.**
|
|
75
|
+
// `text-accent` on `bg-accent-soft` is 2.8:1 in light and 4.0:1 in dark -
|
|
76
|
+
// both below AA, on the one row the user most needs to read. HeroUI derives
|
|
77
|
+
// `--accent-soft-foreground` for exactly this pairing (a color-mix of accent
|
|
78
|
+
// into foreground), which measures 4.8:1 and 5.6:1. It is derived rather
|
|
79
|
+
// than fixed, so a host that re-themes `--accent` keeps the contrast.
|
|
80
|
+
//
|
|
81
|
+
// **`bg-transparent` is not redundant.** Three of these rows are HeroUI
|
|
82
|
+
// `<Button variant="tertiary">` (the launcher, the account row, the
|
|
83
|
+
// workspace tile) because they trigger a `Modal` or a `Dropdown`, and
|
|
84
|
+
// `.button--tertiary` sets `--button-bg: var(--default)` - a real fill. Next
|
|
85
|
+
// to the plain anchors the nav renders, those three came out looking
|
|
86
|
+
// permanently hovered, as though three rows in the rail were selected at
|
|
87
|
+
// once. Only the resting tone clears it; the active tone paints its own.
|
|
88
|
+
const tone = options?.active
|
|
89
|
+
? "bg-accent-soft font-medium text-accent-soft-foreground"
|
|
90
|
+
: "bg-transparent text-muted hover:bg-surface-secondary hover:text-foreground";
|
|
91
|
+
return [SIDEBAR_ROW, options?.tile ? SIDEBAR_ROW_TILE : SIDEBAR_ROW_ICON, tone, options?.className]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join(" ");
|
|
94
|
+
}
|
|
95
|
+
const FALLBACK = {
|
|
96
|
+
collapsed: false,
|
|
97
|
+
collapsible: false,
|
|
98
|
+
setCollapsed: () => { },
|
|
99
|
+
renderLink: defaultRenderLink,
|
|
100
|
+
};
|
|
101
|
+
const ShellContext = createContext(null);
|
|
102
|
+
/**
|
|
103
|
+
* Shared state for every part of the shell, so `ConnectSidebarNav`,
|
|
104
|
+
* `ConnectWorkspaceSwitcher` and `ConnectUserButton` all know whether the rail
|
|
105
|
+
* is collapsed without the host threading a prop through three layers.
|
|
106
|
+
*
|
|
107
|
+
* This is the piece that makes the shell *composable* rather than only
|
|
108
|
+
* configurable: the parts are exported and can be rearranged, and they still
|
|
109
|
+
* agree about collapse, routing and the mobile drawer because they read one
|
|
110
|
+
* context instead of one another's props.
|
|
111
|
+
*/
|
|
112
|
+
export function ConnectShellProvider({ value, children, }) {
|
|
113
|
+
return _jsx(ShellContext, { value: value, children: children });
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The shell state a sidebar part is rendering inside.
|
|
117
|
+
*
|
|
118
|
+
* Falls back to "expanded, not collapsible, plain anchors" rather than
|
|
119
|
+
* throwing, because every part is also exported for use *outside* a shell -
|
|
120
|
+
* a host dropping `ConnectSidebarNav` into a layout it already owns should
|
|
121
|
+
* not have to mount a provider to get a nav column.
|
|
122
|
+
*/
|
|
123
|
+
export function useConnectShell() {
|
|
124
|
+
return use(ShellContext) ?? FALLBACK;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A sidebar row's text, faded out as the rail collapses.
|
|
128
|
+
*
|
|
129
|
+
* Opacity only - never width, height or margin. The rail's own width
|
|
130
|
+
* animation already moves the clip edge past this text; animating the text's
|
|
131
|
+
* box as well would make two things fight over the same pixels and is what
|
|
132
|
+
* makes a collapsing sidebar look like it stutters.
|
|
133
|
+
*
|
|
134
|
+
* Asymmetric timing on purpose. Collapsing, the label leaves *first* (no
|
|
135
|
+
* delay, quick) so the rail never appears to squeeze text; expanding, it
|
|
136
|
+
* arrives *after* the rail has opened enough to hold it.
|
|
137
|
+
*/
|
|
138
|
+
export function SidebarLabel({ children, className, }) {
|
|
139
|
+
const { collapsed } = useConnectShell();
|
|
140
|
+
return (_jsx(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 }));
|
|
141
|
+
}
|
|
@@ -1,53 +1,80 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { Tooltip } from "@heroui/react";
|
|
4
4
|
import { ArrowUpRight } from "lucide-react";
|
|
5
|
+
import { m } from "./motion.js";
|
|
6
|
+
import { SidebarLabel, defaultRenderLink, sidebarRowClass, useConnectShell } from "./shell-context.js";
|
|
5
7
|
import { useActivePath } from "../use-active-path.js";
|
|
6
8
|
import { isNavItemActive, normalizeNav, resolveActivePath, } from "./nav.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
? "
|
|
14
|
-
|
|
15
|
-
return `${ROW} ${shape} ${tone}`;
|
|
9
|
+
export { defaultRenderLink };
|
|
10
|
+
function rowClass(isActive, depth) {
|
|
11
|
+
// The indent is a *margin*, not padding, so a child row steps in while the
|
|
12
|
+
// glyph inside it keeps the shared 32px centre every other row uses.
|
|
13
|
+
return sidebarRowClass({
|
|
14
|
+
active: isActive,
|
|
15
|
+
className: depth > 0 ? "ms-4 w-[calc(100%-1rem)]" : undefined,
|
|
16
|
+
});
|
|
16
17
|
}
|
|
17
18
|
function NavRow({ item, isActive, hasActiveChild, collapsed, depth, renderLink, onNavigate, }) {
|
|
18
19
|
const linkProps = {
|
|
19
20
|
href: item.href,
|
|
20
|
-
className: rowClass(isActive,
|
|
21
|
+
className: rowClass(isActive, depth),
|
|
21
22
|
"aria-label": item.label,
|
|
22
23
|
"aria-current": isActive ? "page" : undefined,
|
|
23
24
|
onClick: onNavigate,
|
|
24
25
|
...(item.external ? { target: "_blank", rel: "noopener noreferrer" } : {}),
|
|
25
|
-
children: (_jsxs(_Fragment, { children: [
|
|
26
|
+
children: (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex size-5 shrink-0 items-center justify-center", children: item.icon }), _jsx(SidebarLabel, { children: item.label }), (item.badge || item.external) && (_jsxs(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 && _jsx(ArrowUpRight, { className: "size-3.5 text-muted", "aria-hidden": "true" })] })), collapsed && hasActiveChild && !isActive && (_jsx("span", { "aria-hidden": "true", className: "absolute bottom-1 start-[18px] size-1 rounded-full bg-accent" }))] })),
|
|
26
27
|
};
|
|
27
28
|
// The tooltip stays mounted across expand/collapse and only toggles
|
|
28
29
|
// `isDisabled`. Branching into two trees instead re-creates every item's
|
|
29
30
|
// overlay and focus machinery on each toggle, which is what makes the width
|
|
30
31
|
// animation stutter.
|
|
31
|
-
return (_jsxs(Tooltip, { delay:
|
|
32
|
+
return (_jsxs(Tooltip, { delay: 400, isDisabled: !collapsed, children: [_jsx(Tooltip.Trigger, { children: renderLink(linkProps) }), _jsx(Tooltip.Content, { placement: "right", children: item.label })] }));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A section heading that keeps its box when the rail collapses.
|
|
36
|
+
*
|
|
37
|
+
* The old rail swapped the heading for a hairline, which changed the group's
|
|
38
|
+
* height and made every row below it jump at the moment the width was already
|
|
39
|
+
* moving. Here the box is a constant height in both states: the text fades out
|
|
40
|
+
* and a rule fades in, in the same place.
|
|
41
|
+
*/
|
|
42
|
+
function SectionHeading({ title, collapsed }) {
|
|
43
|
+
return (_jsxs("div", { className: "relative flex h-7 shrink-0 items-center px-[10px]", children: [_jsx(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 }), _jsx(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 } })] }));
|
|
32
44
|
}
|
|
33
45
|
/**
|
|
34
46
|
* The sidebar's nav column on its own - sections, items, one level of
|
|
35
47
|
* children, and the active highlight.
|
|
36
48
|
*
|
|
37
49
|
* `AppShell` renders this inside its rail and its mobile drawer, and it
|
|
38
|
-
* is exported for a host that wants the shell's navigation without its
|
|
39
|
-
*
|
|
40
|
-
*
|
|
50
|
+
* is exported for a host that wants the shell's navigation without its chrome:
|
|
51
|
+
* the same items, the same active resolution, dropped into a layout you
|
|
52
|
+
* already have.
|
|
53
|
+
*
|
|
54
|
+
* Every prop is optional except `nav`, because inside a shell they are all
|
|
55
|
+
* inherited from it. Passing one wins over what the shell says - which is how
|
|
56
|
+
* a second nav column (see `secondaryNav`) can share the rail's collapse state
|
|
57
|
+
* while keeping its own items.
|
|
41
58
|
*/
|
|
42
|
-
export function
|
|
43
|
-
const
|
|
59
|
+
export function ConnectSidebarNav({ nav, activePath, renderLink, collapsed, onNavigate, className, }) {
|
|
60
|
+
const shell = useConnectShell();
|
|
61
|
+
const isCollapsed = collapsed ?? shell.collapsed;
|
|
62
|
+
const link = renderLink ?? shell.renderLink;
|
|
63
|
+
const navigate = onNavigate ?? shell.onNavigate;
|
|
64
|
+
const pathname = useActivePath(activePath ?? shell.activePath);
|
|
44
65
|
const active = resolveActivePath(pathname, nav);
|
|
45
66
|
const sections = normalizeNav(nav);
|
|
46
|
-
return (_jsx("div", { className: `flex flex-col gap-
|
|
67
|
+
return (_jsx("div", { className: `flex flex-col gap-2 ${className ?? ""}`, children: sections.map((section, index) => (_jsxs("nav", { "aria-label": section.title ?? "Main", className: "flex flex-col gap-1", children: [section.title && _jsx(SectionHeading, { title: section.title, collapsed: isCollapsed }), section.items.map((item) => {
|
|
47
68
|
const isActive = isNavItemActive(item, active);
|
|
48
69
|
const activeChild = (item.items ?? []).some((child) => isNavItemActive(child, active));
|
|
49
|
-
return (_jsxs("div", { className:
|
|
70
|
+
return (_jsxs("div", { className: "flex w-full flex-col gap-1", children: [_jsx(NavRow, { item: item, isActive: isActive, hasActiveChild: activeChild, collapsed: isCollapsed, depth: 0, renderLink: link, onNavigate: navigate }), !isCollapsed &&
|
|
50
71
|
(isActive || activeChild) &&
|
|
51
|
-
item.items?.map((child) => (_jsx(NavRow, { item: child, isActive: isNavItemActive(child, active), hasActiveChild: false, collapsed: false, depth: 1, renderLink:
|
|
72
|
+
item.items?.map((child) => (_jsx(NavRow, { item: child, isActive: isNavItemActive(child, active), hasActiveChild: false, collapsed: false, depth: 1, renderLink: link, onNavigate: navigate }, child.href)))] }, item.href));
|
|
52
73
|
})] }, section.title ?? index))) }));
|
|
53
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* @deprecated Renamed to `ConnectSidebarNav` in 0.12 - the rail is a
|
|
77
|
+
* `ConnectSidebar` now, and its pieces are named after it. Identical
|
|
78
|
+
* behaviour; this alias will be removed in a later minor.
|
|
79
|
+
*/
|
|
80
|
+
export const ConnectSideNav = ConnectSidebarNav;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Tooltip } from "@heroui/react";
|
|
4
|
+
import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
|
5
|
+
import { ConnectLogo } from "./logo.js";
|
|
6
|
+
import { EASE_OUT_QUART, MotionProvider, m } from "./motion.js";
|
|
7
|
+
import { RAIL_COLLAPSED, RAIL_EXPANDED, SidebarLabel, sidebarRowClass, useConnectShell } from "./shell-context.js";
|
|
8
|
+
/**
|
|
9
|
+
* The rail's own transition.
|
|
10
|
+
*
|
|
11
|
+
* A tween, not a spring. A spring overshoots, and an overshooting *width*
|
|
12
|
+
* drags every row's clip edge past its resting place and back - which reads as
|
|
13
|
+
* the labels wobbling. 240ms on `--ease-out-quart` is long enough to follow
|
|
14
|
+
* and short enough that hammering the toggle never queues a visible backlog.
|
|
15
|
+
*/
|
|
16
|
+
const RAIL_TRANSITION = { duration: 0.24, ease: EASE_OUT_QUART };
|
|
17
|
+
/** The chrome fades a touch faster than the rail moves, so nothing is caught mid-clip. */
|
|
18
|
+
const CHROME_TRANSITION = { duration: 0.15, ease: EASE_OUT_QUART };
|
|
19
|
+
/** Label fade timing, shared by the header wordmark and `SidebarLabel`. */
|
|
20
|
+
function labelTransition(collapsed) {
|
|
21
|
+
return collapsed ? { duration: 0.1 } : { duration: 0.18, delay: 0.1 };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The application rail: a fixed-width column that animates between an
|
|
25
|
+
* icon-only 64px and a labelled 264px.
|
|
26
|
+
*
|
|
27
|
+
* **Why width, and nothing else, animates.** Every row inside puts its glyph
|
|
28
|
+
* on the same 32px centre in both states (see `RAIL_COLLAPSED`), and every
|
|
29
|
+
* label is clipped rather than re-measured (see `SIDEBAR_ROW`). So the only
|
|
30
|
+
* property in flight is this element's width: no child moves, no text
|
|
31
|
+
* re-wraps, and there is no per-frame layout pass on the rows. That is why the
|
|
32
|
+
* collapse looks smooth rather than merely fast.
|
|
33
|
+
*
|
|
34
|
+
* Hidden below `md`. The mobile drawer renders the same children with
|
|
35
|
+
* `collapsed: false`, because a 64px icon rail on a phone is a worse drawer,
|
|
36
|
+
* not a smaller one.
|
|
37
|
+
*/
|
|
38
|
+
export function ConnectSidebar({ children, label = "Sidebar", className }) {
|
|
39
|
+
const { collapsed } = useConnectShell();
|
|
40
|
+
return (
|
|
41
|
+
// Its own motion root, even though `AppShell` already mounts one.
|
|
42
|
+
// `LazyMotion` nests harmlessly, and without it a host composing the rail
|
|
43
|
+
// from these parts by hand gets a sidebar that jumps between widths
|
|
44
|
+
// instead of animating - a failure that looks like a styling bug and is
|
|
45
|
+
// really a missing provider.
|
|
46
|
+
_jsx(MotionProvider, { children: _jsx(m.aside, { "aria-label": label, className: `hidden shrink-0 flex-col overflow-hidden border-e border-border bg-surface md:flex ${className ?? ""}`,
|
|
47
|
+
// `initial={false}` is what stops the rail sweeping open on first paint
|
|
48
|
+
// for a returning user whose stored preference is "collapsed".
|
|
49
|
+
initial: false, animate: { width: collapsed ? RAIL_COLLAPSED : RAIL_EXPANDED }, transition: RAIL_TRANSITION, children: children }) }));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The brand row, and the rail's own expand/collapse control.
|
|
53
|
+
*
|
|
54
|
+
* **Two states, one DOM.** The badge is mounted once and never re-created; the
|
|
55
|
+
* wordmark fades beside it and the collapse button fades at the trailing edge.
|
|
56
|
+
* Swapping which *element* renders the brand would remount it halfway through
|
|
57
|
+
* the width animation and flicker.
|
|
58
|
+
*
|
|
59
|
+
* Collapsed, hovering or focusing the badge swaps it for the expand glyph, so
|
|
60
|
+
* the rail needs no permanent control eating a row. Expanded, the control sits
|
|
61
|
+
* at the header's trailing edge, where it can be aimed at without covering the
|
|
62
|
+
* brand. The swap is pure CSS on a `group` - hovering re-renders nothing,
|
|
63
|
+
* which is what keeps it instant.
|
|
64
|
+
*
|
|
65
|
+
* The trade this makes, deliberately: **collapsed, the lockup is no longer a
|
|
66
|
+
* link home.** It has become the expand control, and one target that navigates
|
|
67
|
+
* on click but expands on click-after-hover would be worse than either. Put
|
|
68
|
+
* home in the nav; every application already does.
|
|
69
|
+
*/
|
|
70
|
+
export function ConnectSidebarHeader({ name, icon, homeHref = "/" }) {
|
|
71
|
+
const { collapsed, collapsible, setCollapsed, renderLink } = useConnectShell();
|
|
72
|
+
const logoExpands = collapsible && collapsed;
|
|
73
|
+
return (_jsxs("div", { className: "relative flex h-14 shrink-0 items-center overflow-hidden px-3", children: [_jsxs("div", { className: "group/brand relative flex size-10 shrink-0 items-center justify-center", children: [renderLink({
|
|
74
|
+
href: homeHref,
|
|
75
|
+
className: `flex size-10 items-center justify-center rounded-lg transition-opacity focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus ${logoExpands ? "group-hover/brand:opacity-0 group-focus-within/brand:opacity-0" : ""}`,
|
|
76
|
+
// `ConnectLogo` names itself with `role="img"`, which the `<a>` around
|
|
77
|
+
// it does not inherit as its own accessible name.
|
|
78
|
+
"aria-label": `${name} home`,
|
|
79
|
+
children: _jsx(ConnectLogo, { name: name, icon: icon, variant: "mark" }),
|
|
80
|
+
}), logoExpands && (_jsxs(Tooltip, { delay: 400, children: [_jsx(Tooltip.Trigger, { children: _jsx("button", { type: "button", "aria-label": "Expand sidebar", onClick: () => setCollapsed(false), className: "absolute inset-0 flex items-center justify-center rounded-lg text-muted opacity-0 transition-opacity hover:bg-surface-secondary hover:text-foreground focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus group-hover/brand:opacity-100 group-focus-within/brand:opacity-100", children: _jsx(PanelLeftOpen, { className: "size-5" }) }) }), _jsx(Tooltip.Content, { placement: "right", children: "Expand sidebar" })] }))] }), _jsx(m.span, { className: "ms-2 min-w-0 flex-1 truncate", animate: { opacity: collapsed ? 0 : 1 }, transition: labelTransition(collapsed), "aria-hidden": true, children: _jsx(ConnectLogo, { name: name, icon: icon, variant: "wordmark" }) }), collapsible && (
|
|
81
|
+
// Absolutely positioned so it never takes part in the row's layout: in
|
|
82
|
+
// flow, `ms-auto` inside a 40px-wide collapsed row would shove the
|
|
83
|
+
// badge sideways for the length of the animation.
|
|
84
|
+
_jsx(m.div, { className: "absolute end-3 top-1/2 -translate-y-1/2", animate: { opacity: collapsed ? 0 : 1 }, transition: CHROME_TRANSITION,
|
|
85
|
+
// A faded-out control must not be reachable by keyboard or pointer.
|
|
86
|
+
// `inert` says that in one attribute; `hidden` would remove it from
|
|
87
|
+
// flow and make the header re-layout on every toggle.
|
|
88
|
+
inert: collapsed, children: _jsxs(Tooltip, { delay: 400, children: [_jsx(Tooltip.Trigger, { children: _jsx("button", { type: "button", "aria-label": "Collapse sidebar", onClick: () => setCollapsed(true), className: "flex size-8 items-center justify-center rounded-lg text-muted transition-colors hover:bg-surface-secondary hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus", children: _jsx(PanelLeftClose, { className: "size-4" }) }) }), _jsx(Tooltip.Content, { placement: "right", children: "Collapse sidebar" })] }) }))] }));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The rail's scrolling region, for the primary navigation. Everything else in
|
|
92
|
+
* the sidebar is `shrink-0`, so this is the only part that ever gives way.
|
|
93
|
+
*/
|
|
94
|
+
export function ConnectSidebarBody({ children }) {
|
|
95
|
+
return (_jsx("div", { className: "flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto overflow-x-hidden px-3 py-2", children: children }));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A pinned region at the foot of the rail - the secondary nav, the launcher,
|
|
99
|
+
* the account row.
|
|
100
|
+
*
|
|
101
|
+
* `separated` draws the hairline above it. The account row wants one; a
|
|
102
|
+
* secondary nav sitting directly on top of the account row does not, or the
|
|
103
|
+
* rail ends in two rules a few pixels apart.
|
|
104
|
+
*/
|
|
105
|
+
export function ConnectSidebarFooter({ children, separated = true, }) {
|
|
106
|
+
return (_jsx("div", { className: `flex shrink-0 flex-col gap-1 px-3 py-2 ${separated ? "border-t border-border" : ""}`, children: children }));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* A pressable row in the rail that is not a nav item - the launcher, the
|
|
110
|
+
* account button, anything a host adds to the footer.
|
|
111
|
+
*
|
|
112
|
+
* Exported so a host's own row lines up with the SDK's. It applies the same
|
|
113
|
+
* 32px glyph centre and the same label fade, which is what stops a partner's
|
|
114
|
+
* "Help" row drifting sideways while every row above it holds still.
|
|
115
|
+
*/
|
|
116
|
+
export function ConnectSidebarRow({ icon, label, tile = false, trailing, className, "aria-label": ariaLabel, ...props }) {
|
|
117
|
+
const { collapsed } = useConnectShell();
|
|
118
|
+
const name = ariaLabel ?? (typeof label === "string" ? label : undefined);
|
|
119
|
+
return (_jsxs(Tooltip, { delay: 400, isDisabled: !collapsed, children: [_jsx(Tooltip.Trigger, { children: _jsxs("button", { type: "button", "aria-label": name, className: sidebarRowClass({ tile, className }), ...props, children: [_jsx("span", { className: `flex ${tile ? "size-8" : "size-5"} shrink-0 items-center justify-center`, children: icon }), _jsx(SidebarLabel, { children: label }), trailing] }) }), _jsx(Tooltip.Content, { placement: "right", children: name ?? label })] }));
|
|
120
|
+
}
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { Button, Modal } from "@heroui/react";
|
|
4
4
|
import { ArrowRight, ExternalLink, Info } from "lucide-react";
|
|
5
|
-
import { formatPrice, monthlyOf } from "./plan-pricing.js";
|
|
5
|
+
import { formatPrice, intervalLabel, monthlyOf } from "./plan-pricing.js";
|
|
6
6
|
/** GST is quoted exclusive everywhere in Connect; this is the same arithmetic `calculateGST` does server-side. */
|
|
7
7
|
function withGst(price, gstRate) {
|
|
8
8
|
return price + Math.round(price * (gstRate / 100));
|
|
9
9
|
}
|
|
10
10
|
function cadenceOf(plan) {
|
|
11
|
-
return plan.billingInterval
|
|
11
|
+
return intervalLabel(plan.billingInterval).per;
|
|
12
12
|
}
|
|
13
13
|
const HEADING = {
|
|
14
14
|
subscribe: "Confirm subscription",
|
|
@@ -47,7 +47,7 @@ export function ConnectSubscribeDialog({ plan, action, current, coupon, onClose,
|
|
|
47
47
|
// member and the one most in need of it: the two prices differ by an order
|
|
48
48
|
// of magnitude, and showing only the new one reads as a price rise.
|
|
49
49
|
const isChange = action === "upgrade" || action === "downgrade" || isIntervalSwitch;
|
|
50
|
-
return (_jsx(Modal.Backdrop, { isOpen: plan !== null, onOpenChange: (open) => !open && onClose(), children: _jsx(Modal.Container, { size: "sm", children: _jsxs(Modal.Dialog, { children: [_jsx(Modal.CloseTrigger, { onPress: onClose }), _jsx(Modal.Header, { children: _jsx(Modal.Heading, { children: HEADING[action] }) }), plan && (_jsxs(_Fragment, { children: [_jsxs(Modal.Body, { className: "flex flex-col gap-4", children: [isChange && currentPlan ? (_jsxs("div", { className: "flex items-center gap-3 text-sm", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-muted", children: currentPlan.name }), _jsxs("p", { className: "text-xs text-muted", children: [formatPrice(currentPlan.price), "/", currentPlan.billingInterval
|
|
50
|
+
return (_jsx(Modal.Backdrop, { isOpen: plan !== null, onOpenChange: (open) => !open && onClose(), children: _jsx(Modal.Container, { size: "sm", children: _jsxs(Modal.Dialog, { children: [_jsx(Modal.CloseTrigger, { onPress: onClose }), _jsx(Modal.Header, { children: _jsx(Modal.Heading, { children: HEADING[action] }) }), plan && (_jsxs(_Fragment, { children: [_jsxs(Modal.Body, { className: "flex flex-col gap-4", children: [isChange && currentPlan ? (_jsxs("div", { className: "flex items-center gap-3 text-sm", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-muted", children: currentPlan.name }), _jsxs("p", { className: "text-xs text-muted", children: [formatPrice(currentPlan.price), "/", intervalLabel(currentPlan.billingInterval).short] })] }), _jsx(ArrowRight, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate font-medium text-foreground", children: plan.name }), _jsxs("p", { className: "text-xs text-muted", children: [formatPrice(plan.price), "/", intervalLabel(plan.billingInterval).short] })] })] })) : (_jsxs("div", { children: [_jsx("p", { className: "font-medium text-foreground", children: plan.name }), plan.description && _jsx("p", { className: "text-xs text-muted", children: plan.description })] })), _jsxs("div", { className: "flex flex-col gap-1 border-t border-separator pt-3 text-sm", children: [_jsxs("div", { className: "flex justify-between text-muted", children: [_jsx("span", { children: plan.name }), _jsx("span", { children: formatPrice(plan.price) })] }), _jsxs("div", { className: "flex justify-between text-muted", children: [_jsxs("span", { children: ["GST (", plan.gstRate, "%)"] }), _jsx("span", { children: formatPrice(withGst(plan.price, plan.gstRate) - plan.price) })] }), _jsxs("div", { className: "flex justify-between border-t border-separator pt-1 font-semibold text-foreground", children: [_jsxs("span", { children: ["Per ", cadenceOf(plan)] }), _jsx("span", { children: formatPrice(withGst(plan.price, plan.gstRate)) })] }), plan.billingInterval === "annual" && (_jsxs("p", { className: "text-xs text-muted", children: ["Works out to ", formatPrice(monthlyOf(withGst(plan.price, plan.gstRate), "annual")), " a month, billed yearly."] }))] }), plan.trialDays ? (_jsxs("p", { className: "text-xs text-accent", children: [plan.trialDays, "-day free trial, if this workspace has not subscribed to this application before."] })) : null, coupon?.trim() ? (_jsxs("p", { className: "text-xs text-muted", children: ["Coupon ", _jsx("span", { className: "font-mono text-foreground", children: coupon.trim() }), " is carried over and validated on Connect."] })) : null, _jsxs("div", { className: "flex gap-2 rounded-lg bg-surface p-3 text-xs text-muted", children: [_jsx(Info, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": "true" }), _jsxs("div", { className: "flex flex-col gap-1", children: [action === "subscribe" && (_jsx("p", { children: "Payment is completed on Connect, where your card or UPI mandate is set up and the tax invoice is issued. This page updates by itself once it is done." })), action === "upgrade" && (_jsxs("p", { children: ["Connect prices the difference for the days left in your current cycle and collects just that amount now. From the next cycle you are billed the ", plan.name, " rate."] })), action === "downgrade" && (_jsx("p", { children: "You keep what you have paid for. Connect works out the unused part of this cycle and applies it as credit or a refund, depending on how this application is configured." })), isIntervalSwitch && (
|
|
51
51
|
// Not a proration. The cycle you are on ends, so this is
|
|
52
52
|
// the one action where the amount collected can exceed a
|
|
53
53
|
// single cycle's list price - or be nothing at all, with
|
|
@@ -4,7 +4,7 @@ import { Button, Card, Chip, Skeleton } from "@heroui/react";
|
|
|
4
4
|
import { AlertTriangle, ArrowRight, Clock, CreditCard, ExternalLink, Users } from "lucide-react";
|
|
5
5
|
import { useConnect, useConnectEntitlements } from "../provider.js";
|
|
6
6
|
import { m, MotionProvider } from "./motion.js";
|
|
7
|
-
import { formatPrice } from "./plan-pricing.js";
|
|
7
|
+
import { formatPrice, intervalLabel } from "./plan-pricing.js";
|
|
8
8
|
import { ConnectLinkButton } from "./link-button.js";
|
|
9
9
|
/** Where the plan catalogue lives in the host app. Overridable, because not every app mounts it at `/plans`. */
|
|
10
10
|
const DEFAULT_PLANS_HREF = "/plans";
|
|
@@ -77,7 +77,7 @@ export function ConnectSubscriptionCard({ plansHref, className }) {
|
|
|
77
77
|
return _jsx(Skeleton, { className: `h-48 rounded-xl ${className ?? ""}` });
|
|
78
78
|
if (!detail || !plan)
|
|
79
79
|
return _jsx(ConnectNoSubscriptionCard, { plansHref: plansHref, className: className });
|
|
80
|
-
return (_jsx(MotionProvider, { children: _jsx(m.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.2 }, children: _jsxs(Card, { className: className, children: [_jsxs(Card.Header, { children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx(Card.Title, { children: plan.name }), _jsx(SubscriptionChip, { state: state })] }), _jsxs(Card.Description, { children: [formatPrice(plan.price), " + ", plan.gstRate, "% GST per", " ", plan.billingInterval
|
|
80
|
+
return (_jsx(MotionProvider, { children: _jsx(m.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.2 }, children: _jsxs(Card, { className: className, children: [_jsxs(Card.Header, { children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx(Card.Title, { children: plan.name }), _jsx(SubscriptionChip, { state: state })] }), _jsxs(Card.Description, { children: [formatPrice(plan.price), " + ", plan.gstRate, "% GST per", " ", intervalLabel(plan.billingInterval).per] })] }), _jsxs(Card.Content, { className: "flex flex-col gap-2 text-sm", children: [_jsx(NextEvent, { detail: detail, state: state }), _jsx(ConnectSeatUsage, { plan: plan, detail: detail })] }), _jsxs(Card.Footer, { className: "flex flex-wrap gap-2", children: [_jsxs(Button, { variant: "secondary", onPress: () => window.open(`${appUrl}/billing/subscriptions/${detail.id}`, "_blank", "noopener,noreferrer"), children: ["Manage on Connect", _jsx(ExternalLink, { className: "size-3.5" })] }), _jsx(ConnectLinkButton, { href: plansHref ?? DEFAULT_PLANS_HREF, variant: "tertiary", children: "Change plan" })] })] }) }) }));
|
|
81
81
|
}
|
|
82
82
|
function SubscriptionChip({ state }) {
|
|
83
83
|
switch (state) {
|