@cosmoops/connect-sdk 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/README.md +21 -1
  3. package/dist/cjs/client/components/access-gate.js +31 -5
  4. package/dist/cjs/client/components/account-sessions.js +4 -3
  5. package/dist/cjs/client/components/app-shell.js +11 -1
  6. package/dist/cjs/client/components/booking-calendar.js +2 -1
  7. package/dist/cjs/client/components/cursor-pagination.js +3 -2
  8. package/dist/cjs/client/components/filter-tabs.js +23 -5
  9. package/dist/cjs/client/components/grievance-notice.js +2 -1
  10. package/dist/cjs/client/components/logo.js +3 -2
  11. package/dist/cjs/client/components/one-tap.js +2 -1
  12. package/dist/cjs/client/components/plans.js +3 -2
  13. package/dist/cjs/client/components/readiness.js +4 -3
  14. package/dist/cjs/client/components/shell-context.js +2 -1
  15. package/dist/cjs/client/components/side-nav.js +2 -1
  16. package/dist/cjs/client/components/sidebar.js +19 -2
  17. package/dist/cjs/client/components/subscription.js +5 -4
  18. package/dist/cjs/client/components/usage-meter.js +2 -1
  19. package/dist/cjs/client/components/user-menu.js +3 -2
  20. package/dist/cjs/cn.js +53 -0
  21. package/dist/cjs/server/client.js +1 -1
  22. package/dist/cjs/server/handler.js +15 -0
  23. package/dist/cjs/server/resources/scheduling.js +23 -0
  24. package/dist/cjs/store.js +14 -1
  25. package/dist/cjs/ui/empty-state.js +2 -1
  26. package/dist/cjs/ui/index.js +8 -1
  27. package/dist/cjs/ui/page-header.js +2 -1
  28. package/dist/cjs/ui/page-shell.js +3 -2
  29. package/dist/cjs/ui/section.js +2 -1
  30. package/dist/cjs/ui/setting-row.js +2 -1
  31. package/dist/esm/client/components/access-gate.js +33 -7
  32. package/dist/esm/client/components/account-sessions.js +4 -3
  33. package/dist/esm/client/components/app-shell.js +11 -1
  34. package/dist/esm/client/components/booking-calendar.js +2 -1
  35. package/dist/esm/client/components/cursor-pagination.js +3 -2
  36. package/dist/esm/client/components/filter-tabs.js +23 -5
  37. package/dist/esm/client/components/grievance-notice.js +2 -1
  38. package/dist/esm/client/components/logo.js +3 -2
  39. package/dist/esm/client/components/one-tap.js +2 -1
  40. package/dist/esm/client/components/plans.js +3 -2
  41. package/dist/esm/client/components/readiness.js +4 -3
  42. package/dist/esm/client/components/shell-context.js +2 -1
  43. package/dist/esm/client/components/side-nav.js +2 -1
  44. package/dist/esm/client/components/sidebar.js +19 -2
  45. package/dist/esm/client/components/subscription.js +5 -4
  46. package/dist/esm/client/components/usage-meter.js +2 -1
  47. package/dist/esm/client/components/user-menu.js +3 -2
  48. package/dist/esm/cn.js +48 -0
  49. package/dist/esm/server/client.js +1 -1
  50. package/dist/esm/server/handler.js +15 -0
  51. package/dist/esm/server/resources/scheduling.js +23 -0
  52. package/dist/esm/store.js +14 -1
  53. package/dist/esm/ui/empty-state.js +2 -1
  54. package/dist/esm/ui/index.js +6 -0
  55. package/dist/esm/ui/page-header.js +2 -1
  56. package/dist/esm/ui/page-shell.js +3 -2
  57. package/dist/esm/ui/section.js +2 -1
  58. package/dist/esm/ui/setting-row.js +2 -1
  59. package/dist/theme.css +53 -0
  60. package/dist/types/client/components/access-gate.d.ts +35 -4
  61. package/dist/types/client/components/cursor-pagination.d.ts +6 -1
  62. package/dist/types/client/components/filter-tabs.d.ts +9 -3
  63. package/dist/types/cn.d.ts +48 -0
  64. package/dist/types/server/client.d.ts +1 -1
  65. package/dist/types/server/resources/scheduling.d.ts +19 -0
  66. package/dist/types/store.d.ts +6 -0
  67. package/dist/types/types.d.ts +12 -0
  68. package/dist/types/ui/index.d.ts +6 -0
  69. package/dist/types/ui/page-shell.d.ts +7 -1
  70. package/package.json +1 -1
@@ -12,6 +12,7 @@ const lucide_react_1 = require("lucide-react");
12
12
  const logo_js_1 = require("./logo.js");
13
13
  const motion_js_1 = require("./motion.js");
14
14
  const shell_context_js_1 = require("./shell-context.js");
15
+ const cn_js_1 = require("../../cn.js");
15
16
  /**
16
17
  * The rail's own transition.
17
18
  *
@@ -50,7 +51,7 @@ function ConnectSidebar({ children, label = "Sidebar", className }) {
50
51
  // from these parts by hand gets a sidebar that jumps between widths
51
52
  // instead of animating - a failure that looks like a styling bug and is
52
53
  // really a missing provider.
53
- (0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsx)(motion_js_1.m.aside, { "aria-label": label, className: `hidden shrink-0 flex-col overflow-hidden border-e border-border bg-surface md:flex ${className ?? ""}`,
54
+ (0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsx)(motion_js_1.m.aside, { "aria-label": label, className: (0, cn_js_1.cn)("hidden shrink-0 flex-col overflow-hidden border-e border-border bg-surface md:flex", className),
54
55
  // `initial={false}` is what stops the rail sweeping open on first paint
55
56
  // for a returning user whose stored preference is "collapsed".
56
57
  initial: false, animate: { width: collapsed ? shell_context_js_1.RAIL_COLLAPSED : shell_context_js_1.RAIL_EXPANDED }, transition: RAIL_TRANSITION, children: children }) }));
@@ -99,7 +100,23 @@ function ConnectSidebarHeader({ name, icon, homeHref = "/" }) {
99
100
  * the sidebar is `shrink-0`, so this is the only part that ever gives way.
100
101
  */
101
102
  function ConnectSidebarBody({ children }) {
102
- return ((0, jsx_runtime_1.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 }));
103
+ return (
104
+ // `touch-pan-y touch-pinch-zoom` is what lets this scroll under a finger
105
+ // inside the mobile drawer. `Drawer.Dialog` carries an inline
106
+ // `touch-action: none` whenever it is dismissable - HeroUI sets it so its
107
+ // own pointer-driven drag-to-dismiss is not fighting the browser's pan -
108
+ // and the declaration meant to hand the scroll back to the scrollable
109
+ // region is missing from the shipped stylesheet: `@heroui/styles`'
110
+ // `drawer.css` carries the comment ("touch-action: pan-y allows vertical
111
+ // scrolling while the dialog captures drag gestures") on a `.drawer__body`
112
+ // rule that never sets the property. A nav longer than the panel was
113
+ // therefore frozen on a phone while scrolling normally under a wheel.
114
+ //
115
+ // `pinch-zoom` alongside `pan-y` rather than `pan-y` on its own: taking
116
+ // zoom away over a region of text is a WCAG 1.4.4 failure, and nothing
117
+ // here needs it suppressed. On the desktop rail, where no ancestor
118
+ // restricts anything, the pair is equivalent to the default.
119
+ (0, jsx_runtime_1.jsx)("div", { className: "flex min-h-0 flex-1 touch-pan-y touch-pinch-zoom flex-col gap-1 overflow-y-auto overflow-x-hidden px-3 py-2", children: children }));
103
120
  }
104
121
  /**
105
122
  * A pinned region at the foot of the rail - the secondary nav, the launcher,
@@ -14,6 +14,7 @@ const provider_js_1 = require("../provider.js");
14
14
  const motion_js_1 = require("./motion.js");
15
15
  const plan_pricing_js_1 = require("./plan-pricing.js");
16
16
  const link_button_js_1 = require("./link-button.js");
17
+ const cn_js_1 = require("../../cn.js");
17
18
  /** Where the plan catalogue lives in the host app. Overridable, because not every app mounts it at `/plans`. */
18
19
  const DEFAULT_PLANS_HREF = "/plans";
19
20
  function formatDate(iso) {
@@ -82,7 +83,7 @@ function ConnectSubscriptionCard({ plansHref, className }) {
82
83
  const { appUrl } = (0, provider_js_1.useConnect)();
83
84
  const { isLoading, detail, plan, state } = useSubscriptionState();
84
85
  if (isLoading)
85
- return (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: `h-48 rounded-xl ${className ?? ""}` });
86
+ return (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: (0, cn_js_1.cn)("h-48 rounded-xl", className) });
86
87
  if (!detail || !plan)
87
88
  return (0, jsx_runtime_1.jsx)(ConnectNoSubscriptionCard, { plansHref: plansHref, className: className });
88
89
  return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsx)(motion_js_1.m.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.2 }, children: (0, jsx_runtime_1.jsxs)(react_1.Card, { className: className, 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: plan.name }), (0, jsx_runtime_1.jsx)(SubscriptionChip, { state: state })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Description, { children: [(0, plan_pricing_js_1.formatPrice)(plan.price), " + ", plan.gstRate, "% GST per", " ", (0, plan_pricing_js_1.intervalLabel)(plan.billingInterval).per] })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Content, { className: "flex flex-col gap-2 text-sm", children: [(0, jsx_runtime_1.jsx)(NextEvent, { detail: detail, state: state }), (0, jsx_runtime_1.jsx)(ConnectSeatUsage, { plan: plan, detail: detail })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Footer, { className: "flex flex-wrap gap-2", children: [(0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => window.open(`${appUrl}/billing/subscriptions/${detail.id}`, "_blank", "noopener,noreferrer"), children: ["Manage on Connect", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] }), (0, jsx_runtime_1.jsx)(link_button_js_1.ConnectLinkButton, { href: plansHref ?? DEFAULT_PLANS_HREF, variant: "tertiary", children: "Change plan" })] })] }) }) }));
@@ -132,7 +133,7 @@ function ConnectSeatUsage({ plan, detail, className, }) {
132
133
  return null;
133
134
  const included = plan.seats.included ?? 0;
134
135
  const over = detail.seatCount - included;
135
- return ((0, jsx_runtime_1.jsxs)("span", { className: `flex items-center gap-2 text-muted ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Users, { className: "size-4 shrink-0", "aria-hidden": "true" }), detail.seatCount, " of ", included, " seat", included === 1 ? "" : "s", " used", over > 0 &&
136
+ return ((0, jsx_runtime_1.jsxs)("span", { className: (0, cn_js_1.cn)("flex items-center gap-2 text-muted", className), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Users, { className: "size-4 shrink-0", "aria-hidden": "true" }), detail.seatCount, " of ", included, " seat", included === 1 ? "" : "s", " used", over > 0 &&
136
137
  (plan.seats.model === "per-seat"
137
138
  ? ` - ${over} extra at ${(0, plan_pricing_js_1.formatPrice)(plan.seats.pricePerExtraSeat ?? 0)} each`
138
139
  : ` - ${over} over the plan's limit`)] }));
@@ -154,7 +155,7 @@ function ConnectTrialBanner({ plansHref, className }) {
154
155
  const { detail, state } = useSubscriptionState();
155
156
  if (state !== "trialing" || !detail)
156
157
  return null;
157
- return ((0, jsx_runtime_1.jsxs)("div", { className: `flex flex-wrap items-center justify-between gap-3 rounded-lg bg-accent-soft px-3 py-2 text-sm text-accent ${className ?? ""}`, children: [(0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Clock, { className: "size-4 shrink-0", "aria-hidden": "true" }), "You're on a free trial", detail.nextBillingDate ? `. Your first charge is on ${formatDate(detail.nextBillingDate)}.` : "."] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(link_button_js_1.ConnectLinkButton, { href: plansHref ?? DEFAULT_PLANS_HREF, variant: "tertiary", children: "Compare plans" }), (0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => window.open(`${appUrl}/billing/subscriptions/${detail.id}`, "_blank", "noopener,noreferrer"), children: ["Manage", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] })] })] }));
158
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_js_1.cn)("flex flex-wrap items-center justify-between gap-3 rounded-lg bg-accent-soft px-3 py-2 text-sm text-accent", className), children: [(0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Clock, { className: "size-4 shrink-0", "aria-hidden": "true" }), "You're on a free trial", detail.nextBillingDate ? `. Your first charge is on ${formatDate(detail.nextBillingDate)}.` : "."] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(link_button_js_1.ConnectLinkButton, { href: plansHref ?? DEFAULT_PLANS_HREF, variant: "tertiary", children: "Compare plans" }), (0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => window.open(`${appUrl}/billing/subscriptions/${detail.id}`, "_blank", "noopener,noreferrer"), children: ["Manage", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] })] })] }));
158
159
  }
159
160
  /**
160
161
  * Renders `children` for a subscribed workspace, and the "pick a plan" card for
@@ -174,7 +175,7 @@ function ConnectSubscriptionGate({ children, plansHref, fallback, loading, allow
174
175
  const { session, isLoading: sessionLoading } = (0, provider_js_1.useConnect)();
175
176
  const { isLoading, state } = useSubscriptionState();
176
177
  if (sessionLoading || (session?.user && isLoading)) {
177
- return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: loading ?? (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: `h-48 rounded-xl ${className ?? ""}` }) });
178
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: loading ?? (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: (0, cn_js_1.cn)("h-48 rounded-xl", className) }) });
178
179
  }
179
180
  if (!session?.user)
180
181
  return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: fallback ?? null });
@@ -8,6 +8,7 @@ const lucide_react_1 = require("lucide-react");
8
8
  const provider_js_1 = require("../provider.js");
9
9
  const motion_js_1 = require("./motion.js");
10
10
  const plan_pricing_js_1 = require("./plan-pricing.js");
11
+ const cn_js_1 = require("../../cn.js");
11
12
  function formatUnits(value, unit) {
12
13
  return `${value.toLocaleString("en-IN")} ${unit}`;
13
14
  }
@@ -35,7 +36,7 @@ function ConnectUsageMeter({ addonIds, hideWhenEmpty = true, title = "Usage this
35
36
  if (!session?.user)
36
37
  return null;
37
38
  if (isLoading)
38
- return (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: `h-40 rounded-xl ${className ?? ""}` });
39
+ return (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: (0, cn_js_1.cn)("h-40 rounded-xl", className) });
39
40
  if (error || !data)
40
41
  return null;
41
42
  // Defended here as well as in `billing.getAddonUsage`, and deliberately so:
@@ -19,6 +19,7 @@ const account_sessions_js_1 = require("./account-sessions.js");
19
19
  const motion_js_1 = require("./motion.js");
20
20
  const shell_context_js_1 = require("./shell-context.js");
21
21
  const surfaces_js_1 = require("./surfaces.js");
22
+ const cn_js_1 = require("../../cn.js");
22
23
  const THEME_OPTIONS = [
23
24
  { value: "light", label: "Light", icon: lucide_react_1.Sun },
24
25
  { value: "dark", label: "Dark", icon: lucide_react_1.Moon },
@@ -199,7 +200,7 @@ function ConnectUserButton({ variant = "row", onSignOut, className, theme, onThe
199
200
  const { session, isLoading } = (0, provider_js_1.useConnect)();
200
201
  const { collapsed } = (0, shell_context_js_1.useConnectShell)();
201
202
  if (isLoading) {
202
- return ((0, jsx_runtime_1.jsxs)("div", { className: `flex h-10 items-center gap-3 ${variant === "row" ? "ps-[4px]" : ""} ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "size-8 shrink-0 rounded-full" }), variant !== "compact" && !collapsed && (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "h-4 w-24" })] }));
203
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_js_1.cn)("flex h-10 items-center gap-3", variant === "row" ? "ps-[4px]" : "", className), children: [(0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "size-8 shrink-0 rounded-full" }), variant !== "compact" && !collapsed && (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "h-4 w-24" })] }));
203
204
  }
204
205
  const user = session?.user;
205
206
  if (!user)
@@ -209,7 +210,7 @@ function ConnectUserButton({ variant = "row", onSignOut, className, theme, onThe
209
210
  // `ps-1` against the default trailing padding is optical, not
210
211
  // sloppy: a circular avatar needs less inset than a text label to
211
212
  // look evenly seated in the button.
212
- className: `gap-2 ps-1 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "sm" }), variant === "inline" && (0, jsx_runtime_1.jsx)("span", { className: "text-sm font-medium text-foreground", children: label })] })), (0, jsx_runtime_1.jsx)(ProfileSurface, { onSignOut: onSignOut, theme: theme, onThemeChange: onThemeChange })] }));
213
+ className: (0, cn_js_1.cn)("gap-2 ps-1", className), children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "sm" }), variant === "inline" && (0, jsx_runtime_1.jsx)("span", { className: "text-sm font-medium text-foreground", children: label })] })), (0, jsx_runtime_1.jsx)(ProfileSurface, { onSignOut: onSignOut, theme: theme, onThemeChange: onThemeChange })] }));
213
214
  }
214
215
  /**
215
216
  * @deprecated Renamed to `ConnectUserButton` in 0.12 - it is one button that
package/dist/cjs/cn.js ADDED
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.cn = void 0;
5
+ /**
6
+ * Merge class names so a caller's Tailwind utility actually wins.
7
+ *
8
+ * Every component in this package carries default utilities, and a partner
9
+ * styling one reaches for `className`. Concatenating the two does not do what
10
+ * it looks like it does:
11
+ *
12
+ * ```tsx
13
+ * // ConnectBookingCalendar with className="gap-8"
14
+ * <div className={`flex flex-col gap-4 ${className}`} />
15
+ * // -> class="flex flex-col gap-4 gap-8"
16
+ * ```
17
+ *
18
+ * Both rules are now in the stylesheet and both match, so the winner is
19
+ * decided by **source order in the generated CSS**, not by the order they
20
+ * appear in the attribute. Tailwind sorts its output by its own internal
21
+ * ordering, so whether `gap-8` beats `gap-4` depends on which utility Tailwind
22
+ * happens to emit later - `p-8` over `px-3 py-2` lands one way, `gap-8` over
23
+ * `gap-4` the other. An override that works on one prop and silently does
24
+ * nothing on the next is worse than one that never works, because nobody
25
+ * suspects the mechanism.
26
+ *
27
+ * `cn` resolves it by *removing* the conflicting default rather than adding to
28
+ * it - `cn("flex flex-col gap-4", "gap-8")` is `"flex flex-col gap-8"`, and
29
+ * `cn("px-3 py-2", "p-8")` is `"p-8"`. That is a real understanding of
30
+ * Tailwind's shorthand groups, which is why it is `tailwind-merge` and not a
31
+ * `join(" ")`.
32
+ *
33
+ * Re-exported rather than reimplemented. The implementation is
34
+ * `tailwind-variants`' `cn`, which `@heroui/react` re-exports and which every
35
+ * HeroUI component uses internally through `tv` - so a partner's override is
36
+ * resolved by exactly the same rules whether it lands on one of these
37
+ * components or on a HeroUI one. It is reached through `@heroui/react` rather
38
+ * than `tailwind-variants` directly because HeroUI is already a declared peer
39
+ * of both UI entry points, and importing a package this manifest does not name
40
+ * would be relying on someone else's transitive tree.
41
+ *
42
+ * Safe in the server-rendered `./ui` entry: the `@heroui/react` barrel carries
43
+ * no top-level `"use client"` (only its individual components do), so `cn`
44
+ * arrives as a real function rather than a client-reference proxy. That
45
+ * distinction matters for a *function* in a way it does not for a component -
46
+ * `ui/section.tsx` has always imported `Card` from the same barrel.
47
+ *
48
+ * Not exported from the root entry, which is isomorphic and React-free. It is
49
+ * public from `./ui`, because a partner composing on top of these primitives
50
+ * needs the same merge the primitives use.
51
+ */
52
+ var react_1 = require("@heroui/react");
53
+ Object.defineProperty(exports, "cn", { enumerable: true, get: function () { return react_1.cn; } });
@@ -161,4 +161,4 @@ class ConnectClient {
161
161
  }
162
162
  exports.ConnectClient = ConnectClient;
163
163
  /** Kept in step with `package.json` by `sdk.test.ts`. */
164
- exports.SDK_VERSION = "0.25.0";
164
+ exports.SDK_VERSION = "0.27.0";
@@ -511,6 +511,21 @@ function createConnectHandler(options) {
511
511
  asUser,
512
512
  }));
513
513
  },
514
+ /*
515
+ * Singular, and the id is a query param rather than a path segment - the
516
+ * table above is matched by exact string, which is the same reason
517
+ * reschedule and cancel take theirs in the body. Keying it as
518
+ * `/scheduling/bookings/{id}` would simply never match.
519
+ */
520
+ "GET /scheduling/booking": async (request) => {
521
+ const { workspaceId } = await requireWorkspace(request);
522
+ const asUser = await requireToken(request);
523
+ const bookingId = new URL(request.url).searchParams.get("bookingId");
524
+ if (!bookingId) {
525
+ return json({ error: { code: "validation_error", message: "bookingId is required" } }, 400);
526
+ }
527
+ return json(await client.scheduling.getBooking({ workspaceId, bookingId, asUser }));
528
+ },
514
529
  "GET /scheduling/bookings": async (request) => {
515
530
  const { workspaceId } = await requireWorkspace(request);
516
531
  const asUser = await requireToken(request);
@@ -61,6 +61,29 @@ class SchedulingResource {
61
61
  headers: (0, acting_as_js_1.actingAs)(input.asUser),
62
62
  });
63
63
  }
64
+ /**
65
+ * One appointment, by the id you stored.
66
+ *
67
+ * **This is the read to make when you kept a booking id on a record of your
68
+ * own** - a meeting on a deal, an appointment on a case. `listBookings`
69
+ * cannot serve it: that is a window over `startAt`, and the `startAt` you
70
+ * cached is exactly what is wrong once someone has moved the appointment on
71
+ * the workspace's own calendar, so the window you build from it comes back
72
+ * without the row.
73
+ *
74
+ * A retired booking still resolves. `status: "rescheduled"` answers with
75
+ * `rescheduledToId`, so an id stored months ago leads to the appointment
76
+ * that exists today; `cancelled` answers with its reason. `not_found` means
77
+ * this workspace never had that booking.
78
+ */
79
+ getBooking(input) {
80
+ return (0, http_js_1.request)(this.transport, {
81
+ method: "GET",
82
+ path: `/scheduling/bookings/${encodeURIComponent(input.bookingId)}`,
83
+ query: { workspaceId: input.workspaceId },
84
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
85
+ });
86
+ }
64
87
  /** One page of the workspace's diary. */
65
88
  listBookings(input) {
66
89
  return (0, http_js_1.request)(this.transport, {
package/dist/cjs/store.js CHANGED
@@ -108,6 +108,12 @@ const EQUALITY_OPERATORS = new Set(["==", "in"]);
108
108
  *
109
109
  * Throws for a query Firestore cannot serve at all - two `array-contains`
110
110
  * filters, ranges over two fields, or a range ordered by a different field.
111
+ *
112
+ * A **range bounded on both sides** over one field needs no declaration: two
113
+ * filters, one field, and the automatic single-field index serves it. Ask this
114
+ * function with the filters you actually send rather than with one of the two
115
+ * bounds - the answer is the same, and modelling the range as a single `>=`
116
+ * is how a schema comes to declare a shape its own runtime does not make.
111
117
  */
112
118
  function storeIndexFor(query) {
113
119
  const filters = query.filters ?? [];
@@ -129,7 +135,14 @@ function storeIndexFor(query) {
129
135
  }
130
136
  const ordered = orderBy?.field ?? rangeField;
131
137
  const distinct = new Set([...filters.map((filter) => filter.field), ...(ordered ? [ordered] : [])]);
132
- if (filters.length <= 1 && distinct.size <= 1)
138
+ // Distinct *fields*, not filter count. A two-sided range over one field -
139
+ // `day >= from AND day <= to ORDER BY day`, the shape of every date window -
140
+ // is exactly what an automatic single-field index is for. Counting filters
141
+ // instead answered `[day asc]`: an index Firestore will not build, since a
142
+ // composite needs two fields, and which is by definition the automatic one
143
+ // already serving the query. `array-contains` is the exception, because an
144
+ // array field's automatic index does not serve an inequality on that field.
145
+ if (distinct.size <= 1 && (arrayContains.length === 0 || filters.length <= 1))
133
146
  return null;
134
147
  const fields = [
135
148
  ...[...new Set(equalities.map((filter) => filter.field))]
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConnectEmptyState = ConnectEmptyState;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("@heroui/react");
6
+ const cn_js_1 = require("../cn.js");
6
7
  /**
7
8
  * What a list, grid or table shows when it has nothing to show.
8
9
  *
@@ -24,5 +25,5 @@ function ConnectEmptyState({ icon, title, description, action, variant = "card",
24
25
  const body = ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-center text-center", children: [icon && ((0, jsx_runtime_1.jsx)("span", { "aria-hidden": true, className: "mb-3 text-muted [&>svg]:size-8", children: icon })), (0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-foreground", children: title }), description && (0, jsx_runtime_1.jsx)("p", { className: "mt-1 max-w-md text-sm text-muted", children: description }), action && (0, jsx_runtime_1.jsx)("div", { className: "mt-3", children: action })] }));
25
26
  if (variant === "plain")
26
27
  return (0, jsx_runtime_1.jsx)("div", { className: className, children: body });
27
- return ((0, jsx_runtime_1.jsx)(react_1.Card, { className: `p-5 sm:p-8 ${variant === "dashed" ? "border-dashed" : ""} ${className ?? ""}`, children: body }));
28
+ return ((0, jsx_runtime_1.jsx)(react_1.Card, { className: (0, cn_js_1.cn)("p-5 sm:p-8", variant === "dashed" ? "border-dashed" : "", className), children: body }));
28
29
  }
@@ -29,7 +29,7 @@
29
29
  * ```
30
30
  */
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.ConnectStatusChip = exports.ConnectSettingRow = exports.ConnectEmptyState = exports.ConnectSection = exports.ConnectPageHeader = exports.ConnectPageShell = void 0;
32
+ exports.cn = exports.ConnectStatusChip = exports.ConnectSettingRow = exports.ConnectEmptyState = exports.ConnectSection = exports.ConnectPageHeader = exports.ConnectPageShell = void 0;
33
33
  var page_shell_js_1 = require("./page-shell.js");
34
34
  Object.defineProperty(exports, "ConnectPageShell", { enumerable: true, get: function () { return page_shell_js_1.ConnectPageShell; } });
35
35
  var page_header_js_1 = require("./page-header.js");
@@ -42,3 +42,10 @@ var setting_row_js_1 = require("./setting-row.js");
42
42
  Object.defineProperty(exports, "ConnectSettingRow", { enumerable: true, get: function () { return setting_row_js_1.ConnectSettingRow; } });
43
43
  var status_chip_js_1 = require("./status-chip.js");
44
44
  Object.defineProperty(exports, "ConnectStatusChip", { enumerable: true, get: function () { return status_chip_js_1.ConnectStatusChip; } });
45
+ /**
46
+ * Merge class names the way these components do internally, so a utility you
47
+ * pass wins over a conflicting default instead of racing it in the stylesheet.
48
+ * Use it when composing your own components on top of these.
49
+ */
50
+ var cn_js_1 = require("../cn.js");
51
+ Object.defineProperty(exports, "cn", { enumerable: true, get: function () { return cn_js_1.cn; } });
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConnectPageHeader = ConnectPageHeader;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const cn_js_1 = require("../cn.js");
5
6
  /**
6
7
  * The title / description / action block a page opens with.
7
8
  *
@@ -23,5 +24,5 @@ const jsx_runtime_1 = require("react/jsx-runtime");
23
24
  * why it lives in `@cosmoops/connect-sdk/ui` rather than `/client`.
24
25
  */
25
26
  function ConnectPageHeader({ title, description, action, clamp = true, className, }) {
26
- return ((0, jsx_runtime_1.jsxs)("header", { className: `flex items-start justify-between gap-3 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsx)("h2", { className: "flex items-center gap-2 text-lg font-semibold", children: title }), description && ((0, jsx_runtime_1.jsx)("p", { className: `mt-2 text-sm text-muted ${clamp ? "line-clamp-2 sm:line-clamp-none" : ""}`, children: description }))] }), action && (0, jsx_runtime_1.jsx)("div", { className: "shrink-0", children: action })] }));
27
+ return ((0, jsx_runtime_1.jsxs)("header", { className: (0, cn_js_1.cn)("flex items-start justify-between gap-3", className), children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsx)("h2", { className: "flex items-center gap-2 text-lg font-semibold", children: title }), description && ((0, jsx_runtime_1.jsx)("p", { className: `mt-2 text-sm text-muted ${clamp ? "line-clamp-2 sm:line-clamp-none" : ""}`, children: description }))] }), action && (0, jsx_runtime_1.jsx)("div", { className: "shrink-0", children: action })] }));
27
28
  }
@@ -4,6 +4,7 @@ exports.ConnectPageShell = ConnectPageShell;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
6
  const page_header_js_1 = require("./page-header.js");
7
+ const cn_js_1 = require("../cn.js");
7
8
  /**
8
9
  * The frame a page opens with: its vertical rhythm, its header, and the
9
10
  * suspended regions beneath.
@@ -23,8 +24,8 @@ const page_header_js_1 = require("./page-header.js");
23
24
  *
24
25
  * `space-y-5` is the page gap, owned here so it has exactly one definition.
25
26
  */
26
- function ConnectPageShell({ title, description, action, clamp, above, children, }) {
27
- return ((0, jsx_runtime_1.jsxs)("div", { className: "space-y-5", children: [above, title !== undefined && ((0, jsx_runtime_1.jsx)(page_header_js_1.ConnectPageHeader, { title: title, description: description, action: action, clamp: clamp })), children] }));
27
+ function ConnectPageShell({ title, description, action, clamp, above, children, className, }) {
28
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_js_1.cn)("space-y-5", className), children: [above, title !== undefined && ((0, jsx_runtime_1.jsx)(page_header_js_1.ConnectPageHeader, { title: title, description: description, action: action, clamp: clamp })), children] }));
28
29
  }
29
30
  /**
30
31
  * The page header, for a detail route that can only name itself once its data
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConnectSection = ConnectSection;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("@heroui/react");
6
+ const cn_js_1 = require("../cn.js");
6
7
  /**
7
8
  * One titled block inside a page: a card, its heading, what it is for, and the
8
9
  * one control that acts on it.
@@ -16,5 +17,5 @@ const react_1 = require("@heroui/react");
16
17
  * block, so its position does not move with the length of the description.
17
18
  */
18
19
  function ConnectSection({ title, description, action, titleId, className, bodyClassName, children, }) {
19
- return ((0, jsx_runtime_1.jsxs)(react_1.Card, { className: className ?? "p-4 sm:p-5", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsx)("h3", { id: titleId, className: "text-base font-semibold", children: title }), description && (0, jsx_runtime_1.jsx)("p", { className: "mt-1 text-sm text-muted", children: description })] }), action && (0, jsx_runtime_1.jsx)("div", { className: "shrink-0", children: action })] }), children && (0, jsx_runtime_1.jsx)("div", { className: bodyClassName ?? "mt-4 space-y-4", children: children })] }));
20
+ return ((0, jsx_runtime_1.jsxs)(react_1.Card, { className: (0, cn_js_1.cn)("p-4 sm:p-5", className), children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsx)("h3", { id: titleId, className: "text-base font-semibold", children: title }), description && (0, jsx_runtime_1.jsx)("p", { className: "mt-1 text-sm text-muted", children: description })] }), action && (0, jsx_runtime_1.jsx)("div", { className: "shrink-0", children: action })] }), children && (0, jsx_runtime_1.jsx)("div", { className: bodyClassName ?? "mt-4 space-y-4", children: children })] }));
20
21
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConnectSettingRow = ConnectSettingRow;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const cn_js_1 = require("../cn.js");
5
6
  /**
6
7
  * One labelled setting inside a `ConnectSection`.
7
8
  *
@@ -10,5 +11,5 @@ const jsx_runtime_1 = require("react/jsx-runtime");
10
11
  * intrinsic width so truncation lands on the description.
11
12
  */
12
13
  function ConnectSettingRow({ title, description, action, children, className, }) {
13
- return ((0, jsx_runtime_1.jsxs)("div", { className: `flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between ${className ?? ""}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-foreground", children: title }), description && (0, jsx_runtime_1.jsx)("p", { className: "mt-1 text-sm text-muted", children: description }), children && (0, jsx_runtime_1.jsx)("div", { className: "mt-3", children: children })] }), action && (0, jsx_runtime_1.jsx)("div", { className: "shrink-0 sm:pl-4", children: action })] }));
14
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_js_1.cn)("flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between", className), children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-foreground", children: title }), description && (0, jsx_runtime_1.jsx)("p", { className: "mt-1 text-sm text-muted", children: description }), children && (0, jsx_runtime_1.jsx)("div", { className: "mt-3", children: children })] }), action && (0, jsx_runtime_1.jsx)("div", { className: "shrink-0 sm:pl-4", children: action })] }));
14
15
  }
@@ -1,12 +1,13 @@
1
1
  "use client";
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
4
  import { Ban, AlertTriangle, Building2, Clock, CreditCard, ExternalLink, User, X } from "lucide-react";
5
- import { Button, Card, Skeleton } from "@heroui/react";
5
+ import { Button, Card } from "@heroui/react";
6
6
  import { blockingBlockers } from "../../helpers.js";
7
7
  import { useConnect, useConnectEntitlements } from "../provider.js";
8
8
  import { ENTER, m, MotionProvider } from "./motion.js";
9
9
  import { ConnectReadinessBanner } from "./readiness.js";
10
+ import { cn } from "../../cn.js";
10
11
  function formatDate(iso) {
11
12
  return new Date(iso).toLocaleDateString(undefined, { day: "numeric", month: "short", year: "numeric" });
12
13
  }
@@ -117,15 +118,40 @@ export function useConnectAccessGate() {
117
118
  * entire app must not hide a public landing page behind an empty gate. Your
118
119
  * own sign-in flow decides what a signed-out visitor sees.
119
120
  *
121
+ * **While the verdict is unknown, `children` render.** The two reads behind
122
+ * this gate are client-side round trips that happen on *every* page load, and
123
+ * this component is mounted around an entire application - so blocking on
124
+ * them replaced every partner's whole page with one placeholder until both
125
+ * came back. Anything the host had streamed underneath was discarded: per-
126
+ * section skeletons never painted, and a page built as a static shell with
127
+ * suspended regions arrived all at once anyway. That is the common path, paid
128
+ * by every workspace on every navigation.
129
+ *
130
+ * Rendering `children` instead makes the loading case agree with the two
131
+ * cases either side of it. A *failed* read already falls through to `clear`
132
+ * ("fail open rather than hard-block the whole app on a network hiccup", in
133
+ * `useConnectAccessGate`), and a signed-out visitor already renders
134
+ * `children` too - "we do not know yet" was the one unknown treated as a
135
+ * refusal.
136
+ *
137
+ * What it costs is honest and small: a genuinely blocked workspace sees its
138
+ * app for the length of one round trip before the block card replaces it.
139
+ * That is the rare path, it is self-correcting, and it is the same exposure
140
+ * the failed-read branch has always accepted. Pass `loading` to opt back into
141
+ * a placeholder for a gate around one small region.
142
+ *
120
143
  * **This is a UX gate, not a security boundary** - same caveat as every
121
- * other gate in this package. The authoritative check is
122
- * `client.entitlements.getReadiness` on your server, before you do the
123
- * thing.
144
+ * other gate in this package, and the reason the trade above is available at
145
+ * all. The authoritative check is `client.entitlements.getReadiness` on your
146
+ * server, before you do the thing.
124
147
  */
125
148
  export function ConnectAccessGate({ children, fallback, loading, className }) {
126
149
  const gate = useConnectAccessGate();
150
+ // `children`, unless the host explicitly asked for a placeholder. See the
151
+ // doc comment - this is the branch that decides whether a partner's page
152
+ // can stream.
127
153
  if (gate.state === "loading") {
128
- return _jsx(_Fragment, { children: loading ?? _jsx(Skeleton, { className: `h-32 rounded-xl ${className ?? ""}` }) });
154
+ return _jsx(_Fragment, { children: loading ?? children });
129
155
  }
130
156
  if (gate.state === "signed_out" || gate.state === "clear") {
131
157
  return _jsx(_Fragment, { children: children });
@@ -204,7 +230,7 @@ function PaymentRetryBanner({ blocker, className, }) {
204
230
  const [dismissed, setDismissed] = useState(() => dismissKey !== null && typeof window !== "undefined" && window.localStorage.getItem(dismissKey) === "1");
205
231
  if (dismissed)
206
232
  return null;
207
- return (_jsx(MotionProvider, { children: _jsxs(m.div, { className: `flex flex-wrap items-center justify-between gap-3 rounded-lg bg-accent-soft px-3 py-2 text-sm text-accent ${className ?? ""}`, role: "status", initial: { opacity: 0, y: -6 }, animate: { opacity: 1, y: 0 }, transition: ENTER, children: [_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(AlertTriangle, { className: "size-4 shrink-0", "aria-hidden": "true" }), blocker.message, blocker.graceEndsAt && ` Resolve by ${formatDate(blocker.graceEndsAt)} to avoid losing access.`] }), _jsxs("div", { className: "flex items-center gap-2", children: [blocker.resolveUrl && (_jsxs(Button, { variant: "secondary", onPress: () => window.open(blocker.resolveUrl, "_blank", "noopener,noreferrer"), children: ["Resolve payment", _jsx(ExternalLink, { className: "size-3.5" })] })), _jsx(Button, { variant: "tertiary", isIconOnly: true, "aria-label": "Dismiss", onPress: () => {
233
+ return (_jsx(MotionProvider, { children: _jsxs(m.div, { className: cn("flex flex-wrap items-center justify-between gap-3 rounded-lg bg-accent-soft px-3 py-2 text-sm text-accent", className), role: "status", initial: { opacity: 0, y: -6 }, animate: { opacity: 1, y: 0 }, transition: ENTER, children: [_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(AlertTriangle, { className: "size-4 shrink-0", "aria-hidden": "true" }), blocker.message, blocker.graceEndsAt && ` Resolve by ${formatDate(blocker.graceEndsAt)} to avoid losing access.`] }), _jsxs("div", { className: "flex items-center gap-2", children: [blocker.resolveUrl && (_jsxs(Button, { variant: "secondary", onPress: () => window.open(blocker.resolveUrl, "_blank", "noopener,noreferrer"), children: ["Resolve payment", _jsx(ExternalLink, { className: "size-3.5" })] })), _jsx(Button, { variant: "tertiary", isIconOnly: true, "aria-label": "Dismiss", onPress: () => {
208
234
  setDismissed(true);
209
235
  if (dismissKey && typeof window !== "undefined")
210
236
  window.localStorage.setItem(dismissKey, "1");
@@ -7,6 +7,7 @@ import { useConnect, useConnectSessions } from "../provider.js";
7
7
  import { AnimatePresence, EASE_OUT_QUART, m, MotionProvider } from "./motion.js";
8
8
  import { ICON, PANEL_FILL } from "./surfaces.js";
9
9
  import { formatRelativeTime } from "../../format-relative-time.js";
10
+ import { cn } from "../../cn.js";
10
11
  const DEVICE_ICONS = {
11
12
  desktop: Monitor,
12
13
  mobile: Smartphone,
@@ -77,13 +78,13 @@ export function ConnectAccountSessions({ className, maxVisible = 4 }) {
77
78
  const [isRevokingOthers, setIsRevokingOthers] = useState(false);
78
79
  const [showAll, setShowAll] = useState(false);
79
80
  if (isLoading) {
80
- return (_jsxs("section", { className: `flex flex-col gap-3 ${className ?? ""}`, children: [_jsx(Skeleton, { className: "h-4 w-28" }), _jsx("div", { className: `flex flex-col divide-y divide-border overflow-hidden rounded-xl border border-border ${PANEL_FILL}`, children: Array.from({ length: 2 }, (_, index) => (_jsxs("div", { className: "flex items-center gap-3 px-3 py-3", children: [_jsx(Skeleton, { className: "size-9 shrink-0 rounded-xl" }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [_jsx(Skeleton, { className: "h-3.5 w-32" }), _jsx(Skeleton, { className: "h-3 w-24" })] })] }, index))) })] }));
81
+ return (_jsxs("section", { className: cn("flex flex-col gap-3", className), children: [_jsx(Skeleton, { className: "h-4 w-28" }), _jsx("div", { className: `flex flex-col divide-y divide-border overflow-hidden rounded-xl border border-border ${PANEL_FILL}`, children: Array.from({ length: 2 }, (_, index) => (_jsxs("div", { className: "flex items-center gap-3 px-3 py-3", children: [_jsx(Skeleton, { className: "size-9 shrink-0 rounded-xl" }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [_jsx(Skeleton, { className: "h-3.5 w-32" }), _jsx(Skeleton, { className: "h-3 w-24" })] })] }, index))) })] }));
81
82
  }
82
83
  // A failed read used to render as *nothing*, which is indistinguishable
83
84
  // from "you have no other devices" - the one reading of this list that
84
85
  // should never be guessed at.
85
86
  if (error) {
86
- return (_jsxs("section", { className: `flex flex-col gap-2 ${className ?? ""}`, children: [_jsx("p", { className: "text-sm font-medium text-foreground", children: "Active sessions" }), _jsx("p", { className: `rounded-xl border border-border ${PANEL_FILL} px-3 py-4 text-xs text-muted`, children: "Could not load your sessions. Try again in a moment." })] }));
87
+ return (_jsxs("section", { className: cn("flex flex-col gap-2", className), children: [_jsx("p", { className: "text-sm font-medium text-foreground", children: "Active sessions" }), _jsx("p", { className: `rounded-xl border border-border ${PANEL_FILL} px-3 py-4 text-xs text-muted`, children: "Could not load your sessions. Try again in a moment." })] }));
87
88
  }
88
89
  const sessions = ordered(data?.items ?? []);
89
90
  if (sessions.length === 0)
@@ -91,7 +92,7 @@ export function ConnectAccountSessions({ className, maxVisible = 4 }) {
91
92
  const hasOtherSessions = sessions.some((session) => !session.isCurrent);
92
93
  const isCollapsible = maxVisible > 0 && sessions.length > maxVisible;
93
94
  const visible = isCollapsible && !showAll ? sessions.slice(0, maxVisible) : sessions;
94
- return (_jsx(MotionProvider, { children: _jsxs("section", { className: `flex flex-col gap-3 ${className ?? ""}`, children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("p", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: ["Active sessions", _jsx(Chip, { size: "sm", variant: "soft", children: sessions.length })] }), _jsx("p", { className: "text-xs text-muted", children: "Devices signed in to your Connect account." })] }), hasOtherSessions && (_jsx(Button, { size: "sm", variant: "tertiary", className: "shrink-0 text-xs text-danger", isPending: isRevokingOthers, isDisabled: isRevokingOthers, onPress: () => {
95
+ return (_jsx(MotionProvider, { children: _jsxs("section", { className: cn("flex flex-col gap-3", className), children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("p", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: ["Active sessions", _jsx(Chip, { size: "sm", variant: "soft", children: sessions.length })] }), _jsx("p", { className: "text-xs text-muted", children: "Devices signed in to your Connect account." })] }), hasOtherSessions && (_jsx(Button, { size: "sm", variant: "tertiary", className: "shrink-0 text-xs text-danger", isPending: isRevokingOthers, isDisabled: isRevokingOthers, onPress: () => {
95
96
  setIsRevokingOthers(true);
96
97
  void mutateResource("/sessions/revoke-others")
97
98
  .then(() => {
@@ -17,8 +17,18 @@ import { ConnectWorkspaceSwitcher } from "./workspace-switcher.js";
17
17
  import { ConnectUserButton } from "./user-menu.js";
18
18
  import { filterNavForAudience, gateExemptPaths, normalizePath, } from "./nav.js";
19
19
  import { useActivePath } from "../use-active-path.js";
20
+ import { cn } from "../../cn.js";
20
21
  const COLLAPSED_KEY = "connect_sidenav_collapsed";
21
22
  const COLLAPSED_EVENT = "connect:sidenav-collapsed";
23
+ /**
24
+ * The `<main>` element's id, and the skip link's target.
25
+ *
26
+ * Module-local rather than a prop: both ends of the pair live in this file, so
27
+ * there is nothing for a caller to keep in step - and a partner supplying
28
+ * their own id could only get it wrong. Namespaced because it lands in a
29
+ * document the partner also writes.
30
+ */
31
+ const CONTENT_ID = "connect-main-content";
22
32
  /**
23
33
  * The stored rail preference, or `null` when there isn't one (or when reading
24
34
  * it is not allowed).
@@ -185,5 +195,5 @@ export function AppShell({ name, icon, homeHref = "/", nav, secondaryNav, active
185
195
  }), [renderLink, activePath, closeMobile]);
186
196
  const accountRow = signedOut ? (showAuthButtons && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(ConnectLoginButton, { size: "sm", fullWidth: true }), _jsx(ConnectRegisterButton, { size: "sm", fullWidth: true })] }))) : (_jsx(ConnectUserButton, { variant: "row", theme: theme, onThemeChange: onThemeChange }));
187
197
  const railContent = (_jsxs(_Fragment, { children: [_jsx(ConnectSidebarHeader, { name: name, icon: icon, homeHref: homeHref }), workspaceSwitcher !== false && (_jsx("div", { className: "shrink-0 px-3 pb-1", children: _jsx(ConnectWorkspaceSwitcher, {}) })), _jsx(ConnectSidebarBody, { children: _jsx(ConnectSidebarNav, { nav: visibleNav }) }), (visibleSecondaryNav || sidebarFooter || showAppsGrid) && (_jsxs(ConnectSidebarFooter, { separated: false, children: [visibleSecondaryNav && _jsx(ConnectSidebarNav, { nav: visibleSecondaryNav }), sidebarFooter, showAppsGrid && _jsx(ConnectAppsGrid, { variant: "row" })] })), accountRow && _jsx(ConnectSidebarFooter, { children: accountRow })] }));
188
- return (_jsx(MotionProvider, { children: _jsxs("div", { className: `flex h-screen bg-background ${className ?? ""}`, children: [_jsx(ConnectShellProvider, { value: railShell, children: _jsx(ConnectSidebar, { children: railContent }) }), _jsx(Drawer.Backdrop, { isOpen: mobileOpen, onOpenChange: setMobileOpen, children: _jsx(Drawer.Content, { placement: "left", className: "w-[min(20rem,85vw)]", children: _jsx(Drawer.Dialog, { "aria-label": "Navigation menu", className: "p-0", children: _jsx(ConnectShellProvider, { value: drawerShell, children: _jsx("div", { className: "flex h-full flex-col", children: railContent }) }) }) }) }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border bg-surface px-3 md:hidden", children: [_jsx(Button, { isIconOnly: true, variant: "tertiary", size: "sm", "aria-label": "Open navigation menu", onPress: () => setMobileOpen(true), children: _jsx(Menu, { className: "size-5" }) }), _jsx(ConnectLogo, { name: name, icon: icon })] }), _jsx("main", { className: "flex-1 overflow-y-auto", children: _jsxs("div", { className: contentClassName ?? "mx-auto w-full max-w-6xl px-4 py-6 md:px-8 md:py-8", children: [gated ? _jsx(ConnectAccessGate, { children: children }) : children, footer] }) })] }), askAi && _jsx(ConnectAskAi, { ...(askAi === true ? {} : askAi) })] }) }));
198
+ return (_jsx(MotionProvider, { children: _jsxs("div", { className: cn("flex h-dvh bg-background", className), children: [_jsx("a", { href: `#${CONTENT_ID}`, className: "sr-only focus:not-sr-only focus:absolute focus:start-3 focus:top-3 focus:z-50 focus:rounded-lg focus:bg-surface focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:text-foreground focus:shadow-lg focus:outline-2 focus:outline-offset-2 focus:outline-focus", children: "Skip to content" }), _jsx(ConnectShellProvider, { value: railShell, children: _jsx(ConnectSidebar, { children: railContent }) }), _jsx(Drawer.Backdrop, { isOpen: mobileOpen, onOpenChange: setMobileOpen, children: _jsx(Drawer.Content, { placement: "left", className: "w-[min(20rem,85vw)]", children: _jsxs(Drawer.Dialog, { "aria-label": "Navigation menu", className: "p-0 pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]", children: [_jsx(Drawer.CloseTrigger, { className: "end-3 top-[calc(--spacing(3)+env(safe-area-inset-top))]" }), _jsx(ConnectShellProvider, { value: drawerShell, children: _jsx("div", { className: "flex h-full flex-col [&>:first-child]:pe-10", children: railContent }) })] }) }) }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border bg-surface px-3 md:hidden", children: [_jsx(Button, { isIconOnly: true, variant: "tertiary", size: "sm", "aria-label": "Open navigation menu", onPress: () => setMobileOpen(true), children: _jsx(Menu, { className: "size-5" }) }), _jsx(ConnectLogo, { name: name, icon: icon })] }), _jsx("main", { id: CONTENT_ID, tabIndex: -1, className: "flex-1 overflow-y-auto outline-none", children: _jsxs("div", { className: contentClassName ?? "w-full px-4 py-5 md:px-6 md:py-6", children: [gated ? _jsx(ConnectAccessGate, { children: children }) : children, footer] }) })] }), askAi && _jsx(ConnectAskAi, { ...(askAi === true ? {} : askAi) })] }) }));
189
199
  }
@@ -7,6 +7,7 @@ import { ConnectEmptyState } from "../../ui/empty-state.js";
7
7
  import { useBookableEventTypes, useBookings, } from "../use-scheduling.js";
8
8
  import { dayColumns, dayStartInstant, hourMarks, layoutBookings, zonedInstant, } from "./booking-grid.js";
9
9
  import { ConnectBookingDetail, ConnectBookingDialog } from "./booking-dialogs.js";
10
+ import { cn } from "../../cn.js";
10
11
  /**
11
12
  * The visible band of the day. Outside it a booking is drawn at the edge and
12
13
  * flagged rather than hidden - see `layoutBookings`.
@@ -64,7 +65,7 @@ export function ConnectBookingCalendar({ timeZone, hostMemberId, window: visible
64
65
  const marks = useMemo(() => hourMarks(visibleWindow), [visibleWindow]);
65
66
  const shiftWeek = (weeks) => setWeekAnchor((anchor) => new Date(anchor.getTime() + weeks * 7 * 24 * 60 * 60_000));
66
67
  const eventTypeById = useMemo(() => new Map(eventTypes.map((eventType) => [eventType.id, eventType])), [eventTypes]);
67
- return (_jsxs("div", { className: `flex flex-col gap-4 ${className ?? ""}`, children: [_jsxs("header", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { isIconOnly: true, size: "sm", variant: "tertiary", "aria-label": "Previous week", onPress: () => shiftWeek(-1), children: _jsx(ChevronLeft, { className: "size-4", "aria-hidden": true }) }), _jsx(Button, { size: "sm", variant: "secondary", onPress: () => setWeekAnchor(startOfWeek(new Date(), zone)), children: "Today" }), _jsx(Button, { isIconOnly: true, size: "sm", variant: "tertiary", "aria-label": "Next week", onPress: () => shiftWeek(1), children: _jsx(ChevronRight, { className: "size-4", "aria-hidden": true }) }), _jsx("p", { className: "ml-2 text-sm font-medium", children: weekLabel(columns, zone) })] }), canBook && eventTypes.length > 0 && (_jsx(Button, { size: "sm", variant: "primary", onPress: () => setBooking({ startAt: nextHour(now) }), children: "New appointment" }))] }), error && _jsx("p", { className: "text-sm text-danger", children: error }), isLoading ? (_jsx(Skeleton, { className: "h-96 w-full rounded-lg" })) : (_jsxs(_Fragment, { children: [_jsx(Card, { className: "hidden overflow-hidden sm:block", children: _jsx(WeekGrid, { columns: columns, marks: marks, window: visibleWindow, positioned: positioned, eventTypeById: eventTypeById, zone: zone, onSelect: setSelected, onPickEmpty: canBook ? (startAt) => setBooking({ startAt }) : undefined }) }), _jsx("div", { className: "sm:hidden", children: _jsx(AgendaView, { columns: columns, bookings: bookings, eventTypeById: eventTypeById, zone: zone, onSelect: setSelected }) })] })), booking && (_jsx(ConnectBookingDialog, { isOpen: true, onOpenChange: (open) => !open && setBooking(null), eventTypes: eventTypes, initialStartAt: booking.startAt, timeZone: zone, onBooked: () => {
68
+ return (_jsxs("div", { className: cn("flex flex-col gap-4", className), children: [_jsxs("header", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { isIconOnly: true, size: "sm", variant: "tertiary", "aria-label": "Previous week", onPress: () => shiftWeek(-1), children: _jsx(ChevronLeft, { className: "size-4", "aria-hidden": true }) }), _jsx(Button, { size: "sm", variant: "secondary", onPress: () => setWeekAnchor(startOfWeek(new Date(), zone)), children: "Today" }), _jsx(Button, { isIconOnly: true, size: "sm", variant: "tertiary", "aria-label": "Next week", onPress: () => shiftWeek(1), children: _jsx(ChevronRight, { className: "size-4", "aria-hidden": true }) }), _jsx("p", { className: "ml-2 text-sm font-medium", children: weekLabel(columns, zone) })] }), canBook && eventTypes.length > 0 && (_jsx(Button, { size: "sm", variant: "primary", onPress: () => setBooking({ startAt: nextHour(now) }), children: "New appointment" }))] }), error && _jsx("p", { className: "text-sm text-danger", children: error }), isLoading ? (_jsx(Skeleton, { className: "h-96 w-full rounded-lg" })) : (_jsxs(_Fragment, { children: [_jsx(Card, { className: "hidden overflow-hidden sm:block", children: _jsx(WeekGrid, { columns: columns, marks: marks, window: visibleWindow, positioned: positioned, eventTypeById: eventTypeById, zone: zone, onSelect: setSelected, onPickEmpty: canBook ? (startAt) => setBooking({ startAt }) : undefined }) }), _jsx("div", { className: "sm:hidden", children: _jsx(AgendaView, { columns: columns, bookings: bookings, eventTypeById: eventTypeById, zone: zone, onSelect: setSelected }) })] })), booking && (_jsx(ConnectBookingDialog, { isOpen: true, onOpenChange: (open) => !open && setBooking(null), eventTypes: eventTypes, initialStartAt: booking.startAt, timeZone: zone, onBooked: () => {
68
69
  setBooking(null);
69
70
  refetch();
70
71
  } })), selected && (_jsx(ConnectBookingDetail, { booking: selected, eventType: eventTypeById.get(selected.eventTypeId) ?? null, timeZone: zone, onOpenChange: (open) => !open && setSelected(null), onChanged: () => {
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
3
  import { Button } from "@heroui/react";
4
+ import { cn } from "../../cn.js";
4
5
  /**
5
6
  * Next / previous for a cursor-paginated list.
6
7
  *
@@ -14,6 +15,6 @@ import { Button } from "@heroui/react";
14
15
  * filtering or paging changed the result set. Without it the table silently
15
16
  * swaps its contents.
16
17
  */
17
- export function ConnectCursorPagination({ shown, nextCursor, hasPrevious, onNext, onPrevious, isPending = false, noun = ["row", "rows"], }) {
18
- return (_jsxs("div", { className: "flex items-center justify-between gap-3 pt-3", children: [_jsxs("p", { "aria-live": "polite", className: "text-xs text-muted", children: [shown, " ", shown === 1 ? noun[0] : noun[1]] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { size: "sm", variant: "secondary", isDisabled: !hasPrevious || isPending, onPress: onPrevious, children: "Previous" }), _jsx(Button, { size: "sm", variant: "secondary", isDisabled: nextCursor === null || isPending, onPress: () => nextCursor && onNext(nextCursor), children: "Next" })] })] }));
18
+ export function ConnectCursorPagination({ shown, nextCursor, hasPrevious, onNext, onPrevious, isPending = false, noun = ["row", "rows"], className, }) {
19
+ return (_jsxs("div", { className: cn("flex items-center justify-between gap-3 pt-3", className), children: [_jsxs("p", { "aria-live": "polite", className: "text-xs text-muted", children: [shown, " ", shown === 1 ? noun[0] : noun[1]] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { size: "sm", variant: "secondary", isDisabled: !hasPrevious || isPending, onPress: onPrevious, children: "Previous" }), _jsx(Button, { size: "sm", variant: "secondary", isDisabled: nextCursor === null || isPending, onPress: () => nextCursor && onNext(nextCursor), children: "Next" })] })] }));
19
20
  }