@cosmoops/connect-sdk 0.26.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,78 @@
3
3
  `@cosmoops/connect-sdk` follows semver, with the 0.x convention that **minor
4
4
  versions may carry breaking changes** and patch versions never do.
5
5
 
6
+ ## 0.27.0
7
+
8
+ A minor rather than a patch because `Booking` gains a required field, so a
9
+ hand-written object satisfying the old interface no longer type-checks. Nothing
10
+ you *read* from the API breaks.
11
+
12
+ ### Added
13
+
14
+ - **`connect.scheduling.getBooking()`** - one appointment, by the id you
15
+ stored.
16
+
17
+ ```ts
18
+ const booking = await connect.scheduling.getBooking({
19
+ workspaceId,
20
+ bookingId, // the id you kept on your own record
21
+ });
22
+ ```
23
+
24
+ This is the read to make when a record of yours - a meeting on a deal, an
25
+ appointment on a case - holds a booking id. **`listBookings` cannot serve
26
+ it.** That is a window over `startAt`, and the `startAt` you cached is
27
+ exactly what is wrong once somebody has moved the appointment on the
28
+ workspace's own calendar, so the window you build from it comes back without
29
+ the row - in the one case worth re-reading for.
30
+
31
+ A retired booking still resolves, deliberately. `status: "rescheduled"`
32
+ answers with `rescheduledToId`, so an id stored months ago leads to the
33
+ appointment that exists today; follow the pointer. `cancelled` answers with
34
+ its reason. `not_found` means this workspace never had that booking, which is
35
+ a different fact from "it moved".
36
+
37
+ `GET /scheduling/booking?bookingId=` is the matching browser bridge route -
38
+ singular, and the id in the query string, because the bridge's route table is
39
+ matched by exact string. Same reason `reschedule` and `cancel` take theirs in
40
+ the body.
41
+
42
+ - **`Booking.hostMemberIds`** - the workspace members committed to an
43
+ appointment, so an integration can render who it belongs to.
44
+
45
+ These are the `WorkspaceMember.id` values `workspaces.listMembers` returns,
46
+ so an owner column is a lookup against a roster you already hold rather than
47
+ another round trip. They were withheld as "Connect internals", which
48
+ `listBookings` had already contradicted by *filtering* on `hostMemberId` - a
49
+ partner could narrow a diary by a value it was never allowed to read back.
50
+
51
+ Resolved when the slot was held and frozen there: a round-robin event type
52
+ picks one host, a collective one commits the whole group, and re-pointing the
53
+ event type afterwards does not move an appointment already made.
54
+
55
+ **This is the breaking half.** The field is required, so code constructing a
56
+ `Booking` literal - a test fixture, a mock, a storybook arg - needs
57
+ `hostMemberIds: []` added. Code that only reads bookings from the API is
58
+ unaffected.
59
+
60
+ ## 0.26.1
61
+
62
+ ### Added
63
+
64
+ - **`<ConnectAppShell>` renders a skip link.** The rail is the shell's first
65
+ content, so a keyboard or switch user tabbed every navigation row, the app
66
+ launcher and the account menu again on **every** route change before reaching
67
+ the page they came for - WCAG 2.4.1, and the one bypass a single-rail layout
68
+ cannot do without, because there is no second landmark to jump between. The
69
+ link is the first focusable element in the document, visually hidden until it
70
+ takes focus, and `<main>` now carries `id="connect-main-content"` and
71
+ `tabIndex={-1}` so focus genuinely lands there. Without that `tabIndex` a
72
+ browser scrolls to the anchor and leaves focus where it was, so the next Tab
73
+ returns to the top of the rail and the link appears to do nothing.
74
+
75
+ Nothing to adopt: it is on for every platform. The only visible change is a
76
+ "Skip to content" button that appears at the top-left on the first Tab.
77
+
6
78
  ## 0.26.0
7
79
 
8
80
  A minor rather than a patch because two of these change how existing screens
package/README.md CHANGED
@@ -345,7 +345,7 @@ try {
345
345
  | `contacts` | `lookupByPhone` · `get` |
346
346
  | `channels` | `list` · `get` · `claim` · `setRouting` · `clearRouting` |
347
347
  | `calls` | `list` · `get` |
348
- | `scheduling` | `listEventTypes` · `findSlots` · `listBookings` · `createBooking` · `rescheduleBooking` · `cancelBooking` |
348
+ | `scheduling` | `listEventTypes` · `findSlots` · `listBookings` · `getBooking` · `createBooking` · `rescheduleBooking` · `cancelBooking` |
349
349
  | `ai` | `chat` · `extract` |
350
350
  | `otp` | `send` · `verify` |
351
351
  | `logs` | `write` |
@@ -404,6 +404,26 @@ try {
404
404
  `timezone` is the **business's** zone, not your user's - render the wall time in
405
405
  it, or you will offer an hour they are not open.
406
406
 
407
+ Keeping the booking id on a record of your own - a meeting on a deal, an
408
+ appointment on a case - is how you get back to it later:
409
+
410
+ ```ts
411
+ const booking = await client.scheduling.getBooking({ workspaceId, bookingId });
412
+
413
+ // Someone moved it on the workspace's own calendar. Follow the pointer.
414
+ if (booking.status === "rescheduled") {
415
+ const current = await client.scheduling.getBooking({
416
+ workspaceId,
417
+ bookingId: booking.rescheduledToId!,
418
+ });
419
+ }
420
+ ```
421
+
422
+ Use `getBooking`, not `listBookings`, for that. The list is a window over
423
+ `startAt`, and the `startAt` you cached is exactly what is wrong once the
424
+ appointment has moved - so the window you build from it comes back without the
425
+ row, in the one case worth re-reading for.
426
+
407
427
  ### Attributing a log to a user
408
428
 
409
429
  ```ts
@@ -23,6 +23,15 @@ const use_active_path_js_1 = require("../use-active-path.js");
23
23
  const cn_js_1 = require("../../cn.js");
24
24
  const COLLAPSED_KEY = "connect_sidenav_collapsed";
25
25
  const COLLAPSED_EVENT = "connect:sidenav-collapsed";
26
+ /**
27
+ * The `<main>` element's id, and the skip link's target.
28
+ *
29
+ * Module-local rather than a prop: both ends of the pair live in this file, so
30
+ * there is nothing for a caller to keep in step - and a partner supplying
31
+ * their own id could only get it wrong. Namespaced because it lands in a
32
+ * document the partner also writes.
33
+ */
34
+ const CONTENT_ID = "connect-main-content";
26
35
  /**
27
36
  * The stored rail preference, or `null` when there isn't one (or when reading
28
37
  * it is not allowed).
@@ -189,5 +198,5 @@ function AppShell({ name, icon, homeHref = "/", nav, secondaryNav, activePath, r
189
198
  }), [renderLink, activePath, closeMobile]);
190
199
  const accountRow = signedOut ? (showAuthButtons && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectLoginButton, { size: "sm", fullWidth: true }), (0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectRegisterButton, { size: "sm", fullWidth: true })] }))) : ((0, jsx_runtime_1.jsx)(user_menu_js_1.ConnectUserButton, { variant: "row", theme: theme, onThemeChange: onThemeChange }));
191
200
  const railContent = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarHeader, { name: name, icon: icon, homeHref: homeHref }), workspaceSwitcher !== false && ((0, jsx_runtime_1.jsx)("div", { className: "shrink-0 px-3 pb-1", children: (0, jsx_runtime_1.jsx)(workspace_switcher_js_1.ConnectWorkspaceSwitcher, {}) })), (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarBody, { children: (0, jsx_runtime_1.jsx)(side_nav_js_1.ConnectSidebarNav, { nav: visibleNav }) }), (visibleSecondaryNav || sidebarFooter || showAppsGrid) && ((0, jsx_runtime_1.jsxs)(sidebar_js_1.ConnectSidebarFooter, { separated: false, children: [visibleSecondaryNav && (0, jsx_runtime_1.jsx)(side_nav_js_1.ConnectSidebarNav, { nav: visibleSecondaryNav }), sidebarFooter, showAppsGrid && (0, jsx_runtime_1.jsx)(apps_grid_js_1.ConnectAppsGrid, { variant: "row" })] })), accountRow && (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarFooter, { children: accountRow })] }));
192
- return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)("div", { className: (0, cn_js_1.cn)("flex h-screen bg-background", className), children: [(0, jsx_runtime_1.jsx)(shell_context_js_1.ConnectShellProvider, { value: railShell, children: (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebar, { children: railContent }) }), (0, jsx_runtime_1.jsx)(react_2.Drawer.Backdrop, { isOpen: mobileOpen, onOpenChange: setMobileOpen, children: (0, jsx_runtime_1.jsx)(react_2.Drawer.Content, { placement: "left", className: "w-[min(20rem,85vw)]", children: (0, jsx_runtime_1.jsx)(react_2.Drawer.Dialog, { "aria-label": "Navigation menu", className: "p-0", children: (0, jsx_runtime_1.jsx)(shell_context_js_1.ConnectShellProvider, { value: drawerShell, children: (0, jsx_runtime_1.jsx)("div", { className: "flex h-full flex-col", children: railContent }) }) }) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col", children: [(0, jsx_runtime_1.jsxs)("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border bg-surface px-3 md:hidden", children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "tertiary", size: "sm", "aria-label": "Open navigation menu", onPress: () => setMobileOpen(true), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Menu, { className: "size-5" }) }), (0, jsx_runtime_1.jsx)(logo_js_1.ConnectLogo, { name: name, icon: icon })] }), (0, jsx_runtime_1.jsx)("main", { className: "flex-1 overflow-y-auto", children: (0, jsx_runtime_1.jsxs)("div", { className: contentClassName ?? "w-full px-4 py-5 md:px-6 md:py-6", children: [gated ? (0, jsx_runtime_1.jsx)(access_gate_js_1.ConnectAccessGate, { children: children }) : children, footer] }) })] }), askAi && (0, jsx_runtime_1.jsx)(ask_ai_js_1.ConnectAskAi, { ...(askAi === true ? {} : askAi) })] }) }));
201
+ return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)("div", { className: (0, cn_js_1.cn)("flex h-dvh bg-background", className), children: [(0, jsx_runtime_1.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" }), (0, jsx_runtime_1.jsx)(shell_context_js_1.ConnectShellProvider, { value: railShell, children: (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebar, { children: railContent }) }), (0, jsx_runtime_1.jsx)(react_2.Drawer.Backdrop, { isOpen: mobileOpen, onOpenChange: setMobileOpen, children: (0, jsx_runtime_1.jsx)(react_2.Drawer.Content, { placement: "left", className: "w-[min(20rem,85vw)]", children: (0, jsx_runtime_1.jsxs)(react_2.Drawer.Dialog, { "aria-label": "Navigation menu", className: "p-0 pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]", children: [(0, jsx_runtime_1.jsx)(react_2.Drawer.CloseTrigger, { className: "end-3 top-[calc(--spacing(3)+env(safe-area-inset-top))]" }), (0, jsx_runtime_1.jsx)(shell_context_js_1.ConnectShellProvider, { value: drawerShell, children: (0, jsx_runtime_1.jsx)("div", { className: "flex h-full flex-col [&>:first-child]:pe-10", children: railContent }) })] }) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col", children: [(0, jsx_runtime_1.jsxs)("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border bg-surface px-3 md:hidden", children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "tertiary", size: "sm", "aria-label": "Open navigation menu", onPress: () => setMobileOpen(true), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Menu, { className: "size-5" }) }), (0, jsx_runtime_1.jsx)(logo_js_1.ConnectLogo, { name: name, icon: icon })] }), (0, jsx_runtime_1.jsx)("main", { id: CONTENT_ID, tabIndex: -1, className: "flex-1 overflow-y-auto outline-none", children: (0, jsx_runtime_1.jsxs)("div", { className: contentClassName ?? "w-full px-4 py-5 md:px-6 md:py-6", children: [gated ? (0, jsx_runtime_1.jsx)(access_gate_js_1.ConnectAccessGate, { children: children }) : children, footer] }) })] }), askAi && (0, jsx_runtime_1.jsx)(ask_ai_js_1.ConnectAskAi, { ...(askAi === true ? {} : askAi) })] }) }));
193
202
  }
@@ -100,7 +100,23 @@ function ConnectSidebarHeader({ name, icon, homeHref = "/" }) {
100
100
  * the sidebar is `shrink-0`, so this is the only part that ever gives way.
101
101
  */
102
102
  function ConnectSidebarBody({ children }) {
103
- 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 }));
104
120
  }
105
121
  /**
106
122
  * A pinned region at the foot of the rail - the secondary nav, the launcher,
@@ -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.26.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, {
@@ -20,6 +20,15 @@ import { useActivePath } from "../use-active-path.js";
20
20
  import { cn } from "../../cn.js";
21
21
  const COLLAPSED_KEY = "connect_sidenav_collapsed";
22
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";
23
32
  /**
24
33
  * The stored rail preference, or `null` when there isn't one (or when reading
25
34
  * it is not allowed).
@@ -186,5 +195,5 @@ export function AppShell({ name, icon, homeHref = "/", nav, secondaryNav, active
186
195
  }), [renderLink, activePath, closeMobile]);
187
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 }));
188
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 })] }));
189
- return (_jsx(MotionProvider, { children: _jsxs("div", { className: cn("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 ?? "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) })] }) }));
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) })] }) }));
190
199
  }
@@ -93,7 +93,23 @@ export function ConnectSidebarHeader({ name, icon, homeHref = "/" }) {
93
93
  * the sidebar is `shrink-0`, so this is the only part that ever gives way.
94
94
  */
95
95
  export function ConnectSidebarBody({ children }) {
96
- 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
+ return (
97
+ // `touch-pan-y touch-pinch-zoom` is what lets this scroll under a finger
98
+ // inside the mobile drawer. `Drawer.Dialog` carries an inline
99
+ // `touch-action: none` whenever it is dismissable - HeroUI sets it so its
100
+ // own pointer-driven drag-to-dismiss is not fighting the browser's pan -
101
+ // and the declaration meant to hand the scroll back to the scrollable
102
+ // region is missing from the shipped stylesheet: `@heroui/styles`'
103
+ // `drawer.css` carries the comment ("touch-action: pan-y allows vertical
104
+ // scrolling while the dialog captures drag gestures") on a `.drawer__body`
105
+ // rule that never sets the property. A nav longer than the panel was
106
+ // therefore frozen on a phone while scrolling normally under a wheel.
107
+ //
108
+ // `pinch-zoom` alongside `pan-y` rather than `pan-y` on its own: taking
109
+ // zoom away over a region of text is a WCAG 1.4.4 failure, and nothing
110
+ // here needs it suppressed. On the desktop rail, where no ancestor
111
+ // restricts anything, the pair is equivalent to the default.
112
+ _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 }));
97
113
  }
98
114
  /**
99
115
  * A pinned region at the foot of the rail - the secondary nav, the launcher,
@@ -157,4 +157,4 @@ export class ConnectClient {
157
157
  }
158
158
  }
159
159
  /** Kept in step with `package.json` by `sdk.test.ts`. */
160
- export const SDK_VERSION = "0.26.0";
160
+ export const SDK_VERSION = "0.27.0";
@@ -508,6 +508,21 @@ export function createConnectHandler(options) {
508
508
  asUser,
509
509
  }));
510
510
  },
511
+ /*
512
+ * Singular, and the id is a query param rather than a path segment - the
513
+ * table above is matched by exact string, which is the same reason
514
+ * reschedule and cancel take theirs in the body. Keying it as
515
+ * `/scheduling/bookings/{id}` would simply never match.
516
+ */
517
+ "GET /scheduling/booking": async (request) => {
518
+ const { workspaceId } = await requireWorkspace(request);
519
+ const asUser = await requireToken(request);
520
+ const bookingId = new URL(request.url).searchParams.get("bookingId");
521
+ if (!bookingId) {
522
+ return json({ error: { code: "validation_error", message: "bookingId is required" } }, 400);
523
+ }
524
+ return json(await client.scheduling.getBooking({ workspaceId, bookingId, asUser }));
525
+ },
511
526
  "GET /scheduling/bookings": async (request) => {
512
527
  const { workspaceId } = await requireWorkspace(request);
513
528
  const asUser = await requireToken(request);
@@ -58,6 +58,29 @@ export class SchedulingResource {
58
58
  headers: actingAs(input.asUser),
59
59
  });
60
60
  }
61
+ /**
62
+ * One appointment, by the id you stored.
63
+ *
64
+ * **This is the read to make when you kept a booking id on a record of your
65
+ * own** - a meeting on a deal, an appointment on a case. `listBookings`
66
+ * cannot serve it: that is a window over `startAt`, and the `startAt` you
67
+ * cached is exactly what is wrong once someone has moved the appointment on
68
+ * the workspace's own calendar, so the window you build from it comes back
69
+ * without the row.
70
+ *
71
+ * A retired booking still resolves. `status: "rescheduled"` answers with
72
+ * `rescheduledToId`, so an id stored months ago leads to the appointment
73
+ * that exists today; `cancelled` answers with its reason. `not_found` means
74
+ * this workspace never had that booking.
75
+ */
76
+ getBooking(input) {
77
+ return request(this.transport, {
78
+ method: "GET",
79
+ path: `/scheduling/bookings/${encodeURIComponent(input.bookingId)}`,
80
+ query: { workspaceId: input.workspaceId },
81
+ headers: actingAs(input.asUser),
82
+ });
83
+ }
61
84
  /** One page of the workspace's diary. */
62
85
  listBookings(input) {
63
86
  return request(this.transport, {
@@ -123,4 +123,4 @@ export declare class ConnectClient {
123
123
  constructor(options: ConnectClientOptions);
124
124
  }
125
125
  /** Kept in step with `package.json` by `sdk.test.ts`. */
126
- export declare const SDK_VERSION = "0.26.0";
126
+ export declare const SDK_VERSION = "0.27.0";
@@ -97,6 +97,25 @@ export declare class SchedulingResource {
97
97
  * days rather than an error.
98
98
  */
99
99
  findSlots(input: FindSlotsInput): Promise<BookingSlots>;
100
+ /**
101
+ * One appointment, by the id you stored.
102
+ *
103
+ * **This is the read to make when you kept a booking id on a record of your
104
+ * own** - a meeting on a deal, an appointment on a case. `listBookings`
105
+ * cannot serve it: that is a window over `startAt`, and the `startAt` you
106
+ * cached is exactly what is wrong once someone has moved the appointment on
107
+ * the workspace's own calendar, so the window you build from it comes back
108
+ * without the row.
109
+ *
110
+ * A retired booking still resolves. `status: "rescheduled"` answers with
111
+ * `rescheduledToId`, so an id stored months ago leads to the appointment
112
+ * that exists today; `cancelled` answers with its reason. `not_found` means
113
+ * this workspace never had that booking.
114
+ */
115
+ getBooking(input: ActingAsOption & {
116
+ workspaceId: string;
117
+ bookingId: string;
118
+ }): Promise<Booking>;
100
119
  /** One page of the workspace's diary. */
101
120
  listBookings(input: ListBookingsInput): Promise<ListBookingsPage>;
102
121
  /**
@@ -1283,6 +1283,18 @@ export type BookingSource = "agent-call" | "portal" | "api";
1283
1283
  export interface Booking {
1284
1284
  id: string;
1285
1285
  eventTypeId: string;
1286
+ /**
1287
+ * The workspace members committed to this appointment.
1288
+ *
1289
+ * These are `WorkspaceMember.id` values - the same ids `workspaces.listMembers`
1290
+ * returns and `listBookings`' `hostMemberId` filter takes, so an owner column
1291
+ * is a lookup against a roster you already have rather than another round trip.
1292
+ *
1293
+ * Resolved when the slot was held and frozen there: a round-robin event type
1294
+ * picks one host, a collective one commits the whole group, and re-pointing
1295
+ * the event type afterwards does not move an appointment already made.
1296
+ */
1297
+ hostMemberIds: string[];
1286
1298
  status: BookingStatus;
1287
1299
  /** ISO-8601 instants. */
1288
1300
  startAt: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmoops/connect-sdk",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Official server-side SDK for the CosmoOps Connect platform API - Login with Connect, entitlements, billing and metering.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",