@cosmoops/connect-sdk 0.9.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +497 -0
  2. package/README.md +111 -41
  3. package/dist/cjs/cli/doctor.js +3 -3
  4. package/dist/cjs/cli/init.js +3 -3
  5. package/dist/cjs/client/components/access-gate.js +20 -1
  6. package/dist/cjs/client/components/app-shell.js +75 -41
  7. package/dist/cjs/client/components/apps-grid.js +3 -2
  8. package/dist/cjs/client/components/ask-ai.js +20 -1
  9. package/dist/cjs/client/components/confirm-dialog.js +40 -0
  10. package/dist/cjs/client/components/cursor-pagination.js +22 -0
  11. package/dist/cjs/client/components/filter-tabs.js +24 -0
  12. package/dist/cjs/client/components/grievance-notice.js +30 -0
  13. package/dist/cjs/client/components/link-button.js +17 -10
  14. package/dist/cjs/client/components/logo.js +4 -3
  15. package/dist/cjs/client/components/nav.js +41 -0
  16. package/dist/cjs/client/components/plan-pricing.js +27 -0
  17. package/dist/cjs/client/components/plans.js +1 -1
  18. package/dist/cjs/client/components/platform-gate.js +48 -0
  19. package/dist/cjs/client/components/shell-context.js +149 -0
  20. package/dist/cjs/client/components/side-nav.js +49 -23
  21. package/dist/cjs/client/components/sidebar.js +127 -0
  22. package/dist/cjs/client/components/subscribe-dialog.js +2 -2
  23. package/dist/cjs/client/components/subscription.js +1 -1
  24. package/dist/cjs/client/components/user-menu.js +96 -37
  25. package/dist/cjs/client/components/workspace-switcher.js +100 -0
  26. package/dist/cjs/client/index.js +69 -2
  27. package/dist/cjs/client/provider.js +57 -2
  28. package/dist/cjs/client/use-checkout.js +9 -5
  29. package/dist/cjs/errors.js +1 -1
  30. package/dist/cjs/index.js +11 -1
  31. package/dist/cjs/next/index.js +2 -2
  32. package/dist/cjs/server/client.js +25 -2
  33. package/dist/cjs/server/handler.js +196 -18
  34. package/dist/cjs/server/http.js +6 -2
  35. package/dist/cjs/server/resources/acting-as.js +16 -0
  36. package/dist/cjs/server/resources/ai.js +15 -2
  37. package/dist/cjs/server/resources/data.js +374 -0
  38. package/dist/cjs/server/resources/identity.js +7 -3
  39. package/dist/cjs/server/resources/kv.js +84 -0
  40. package/dist/cjs/server/resources/platform.js +43 -0
  41. package/dist/cjs/server/resources/sessions.js +1 -1
  42. package/dist/cjs/server/resources/workspaces.js +74 -0
  43. package/dist/cjs/store.js +152 -0
  44. package/dist/cjs/ui/empty-state.js +28 -0
  45. package/dist/cjs/ui/index.js +44 -0
  46. package/dist/cjs/ui/page-header.js +27 -0
  47. package/dist/cjs/ui/page-shell.js +44 -0
  48. package/dist/cjs/ui/section.js +20 -0
  49. package/dist/cjs/ui/setting-row.js +14 -0
  50. package/dist/cjs/ui/status-chip.js +17 -0
  51. package/dist/esm/cli/doctor.js +3 -3
  52. package/dist/esm/cli/init.js +3 -3
  53. package/dist/esm/client/components/access-gate.js +20 -1
  54. package/dist/esm/client/components/app-shell.js +81 -47
  55. package/dist/esm/client/components/apps-grid.js +3 -2
  56. package/dist/esm/client/components/ask-ai.js +20 -1
  57. package/dist/esm/client/components/confirm-dialog.js +37 -0
  58. package/dist/esm/client/components/cursor-pagination.js +19 -0
  59. package/dist/esm/client/components/filter-tabs.js +21 -0
  60. package/dist/esm/client/components/grievance-notice.js +27 -0
  61. package/dist/esm/client/components/link-button.js +17 -10
  62. package/dist/esm/client/components/logo.js +4 -3
  63. package/dist/esm/client/components/nav.js +40 -0
  64. package/dist/esm/client/components/plan-pricing.js +25 -0
  65. package/dist/esm/client/components/plans.js +2 -2
  66. package/dist/esm/client/components/platform-gate.js +45 -0
  67. package/dist/esm/client/components/shell-context.js +141 -0
  68. package/dist/esm/client/components/side-nav.js +48 -21
  69. package/dist/esm/client/components/sidebar.js +120 -0
  70. package/dist/esm/client/components/subscribe-dialog.js +3 -3
  71. package/dist/esm/client/components/subscription.js +2 -2
  72. package/dist/esm/client/components/user-menu.js +95 -38
  73. package/dist/esm/client/components/workspace-switcher.js +97 -0
  74. package/dist/esm/client/index.js +46 -4
  75. package/dist/esm/client/provider.js +53 -2
  76. package/dist/esm/client/use-checkout.js +9 -5
  77. package/dist/esm/errors.js +1 -1
  78. package/dist/esm/index.js +1 -0
  79. package/dist/esm/next/index.js +2 -2
  80. package/dist/esm/server/client.js +25 -2
  81. package/dist/esm/server/handler.js +196 -18
  82. package/dist/esm/server/http.js +6 -2
  83. package/dist/esm/server/resources/acting-as.js +13 -0
  84. package/dist/esm/server/resources/ai.js +15 -2
  85. package/dist/esm/server/resources/data.js +369 -0
  86. package/dist/esm/server/resources/identity.js +7 -3
  87. package/dist/esm/server/resources/kv.js +80 -0
  88. package/dist/esm/server/resources/platform.js +39 -0
  89. package/dist/esm/server/resources/sessions.js +1 -1
  90. package/dist/esm/server/resources/workspaces.js +70 -0
  91. package/dist/esm/store.js +141 -0
  92. package/dist/esm/ui/empty-state.js +25 -0
  93. package/dist/esm/ui/index.js +34 -0
  94. package/dist/esm/ui/page-header.js +24 -0
  95. package/dist/esm/ui/page-shell.js +41 -0
  96. package/dist/esm/ui/section.js +17 -0
  97. package/dist/esm/ui/setting-row.js +11 -0
  98. package/dist/esm/ui/status-chip.js +14 -0
  99. package/dist/types/client/components/app-shell.d.ts +65 -27
  100. package/dist/types/client/components/apps-grid.d.ts +11 -2
  101. package/dist/types/client/components/confirm-dialog.d.ts +34 -0
  102. package/dist/types/client/components/cursor-pagination.d.ts +27 -0
  103. package/dist/types/client/components/filter-tabs.d.ts +26 -0
  104. package/dist/types/client/components/grievance-notice.d.ts +31 -0
  105. package/dist/types/client/components/link-button.d.ts +32 -9
  106. package/dist/types/client/components/logo.d.ts +18 -2
  107. package/dist/types/client/components/nav.d.ts +24 -0
  108. package/dist/types/client/components/plan-pricing.d.ts +10 -0
  109. package/dist/types/client/components/platform-gate.d.ts +53 -0
  110. package/dist/types/client/components/shell-context.d.ts +116 -0
  111. package/dist/types/client/components/side-nav.d.ts +27 -13
  112. package/dist/types/client/components/sidebar.d.ts +95 -0
  113. package/dist/types/client/components/user-menu.d.ts +68 -22
  114. package/dist/types/client/components/workspace-switcher.d.ts +49 -0
  115. package/dist/types/client/index.d.ts +27 -5
  116. package/dist/types/client/provider.d.ts +109 -6
  117. package/dist/types/client/use-checkout.d.ts +8 -4
  118. package/dist/types/errors.d.ts +1 -1
  119. package/dist/types/index.d.ts +2 -0
  120. package/dist/types/next/index.d.ts +1 -1
  121. package/dist/types/server/client.d.ts +25 -2
  122. package/dist/types/server/http.d.ts +8 -2
  123. package/dist/types/server/index.d.ts +4 -0
  124. package/dist/types/server/resources/acting-as.d.ts +11 -0
  125. package/dist/types/server/resources/ai.d.ts +9 -1
  126. package/dist/types/server/resources/data.d.ts +258 -0
  127. package/dist/types/server/resources/kv.d.ts +63 -0
  128. package/dist/types/server/resources/platform.d.ts +32 -0
  129. package/dist/types/server/resources/workspaces.d.ts +55 -0
  130. package/dist/types/store.d.ts +96 -0
  131. package/dist/types/types.d.ts +319 -15
  132. package/dist/types/ui/empty-state.d.ts +34 -0
  133. package/dist/types/ui/index.d.ts +40 -0
  134. package/dist/types/ui/page-header.d.ts +34 -0
  135. package/dist/types/ui/page-shell.d.ts +39 -0
  136. package/dist/types/ui/section.d.ts +26 -0
  137. package/dist/types/ui/setting-row.d.ts +18 -0
  138. package/dist/types/ui/status-chip.d.ts +36 -0
  139. package/package.json +6 -2
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Building blocks for `data.commit` - the transform sentinels, and the two
3
+ * ways to say "only if nothing has moved".
4
+ *
5
+ * Isomorphic and dependency-free, so a Server Component and a client component
6
+ * building the same write use the same constructors. They are functions rather
7
+ * than object literals you write by hand because the sentinel shape is an
8
+ * encoding, not an API: `{ __transform: "increment", by: 1 }` is what goes over
9
+ * the wire, and nothing outside this file should have to know that.
10
+ */
11
+ /**
12
+ * Add to a number, atomically.
13
+ *
14
+ * **The one operation you genuinely cannot do yourself.** Increments are
15
+ * commutative, so two concurrent `increment(500)` calls both land - where
16
+ * read-add-write from your own process loses one of them however carefully it
17
+ * retries, because the read and the write are two round trips with a gap in
18
+ * between. Use this for balances, counters and tallies, and reach for a
19
+ * precondition only when the new value depends on the old one in a way
20
+ * addition cannot express.
21
+ *
22
+ * Negative to subtract; there is no separate `decrement`.
23
+ */
24
+ export function increment(by) {
25
+ return { __transform: "increment", by };
26
+ }
27
+ /** Connect's own write time, rather than a clock reading from your process. */
28
+ export function serverTimestamp() {
29
+ return { __transform: "serverTimestamp" };
30
+ }
31
+ /** Adds the values not already in the array. Order is not preserved. */
32
+ export function arrayUnion(...values) {
33
+ return { __transform: "arrayUnion", values };
34
+ }
35
+ /** Removes every occurrence of each value. */
36
+ export function arrayRemove(...values) {
37
+ return { __transform: "arrayRemove", values };
38
+ }
39
+ /**
40
+ * Removes the field entirely.
41
+ *
42
+ * `merge` only - a `replace` already drops every field it does not list, so
43
+ * asking it to delete one is a contradiction rather than a narrowing.
44
+ */
45
+ export function deleteField() {
46
+ return { __transform: "delete" };
47
+ }
48
+ /**
49
+ * "Only if this document has not changed since I read it."
50
+ *
51
+ * Pass the document a read returned, and the precondition is built from its
52
+ * `_updateTime`. Taking the document rather than the timestamp is deliberate:
53
+ * a caller who has to reach for `doc.data._updateTime` themselves is one typo
54
+ * away from a precondition that silently does nothing.
55
+ *
56
+ * Throws rather than degrading when the document carries no `_updateTime` -
57
+ * that only happens for a document you built yourself rather than read, and a
58
+ * precondition that quietly became "no precondition" is worse than a crash at
59
+ * the call site.
60
+ */
61
+ export function unchangedSince(document) {
62
+ const updateTime = document.data._updateTime;
63
+ if (typeof updateTime !== "string") {
64
+ throw new Error(`Document "${document.id}" has no _updateTime, so there is no version to hold it to. Preconditions are built from documents you read back, not from ones you constructed.`);
65
+ }
66
+ return { updateTime };
67
+ }
68
+ /** "Only if this document exists" / "only if it does not." */
69
+ export function documentExists(exists = true) {
70
+ return { exists };
71
+ }
72
+ /** Firestore treats these as a range scan, which constrains where they may sit. */
73
+ const RANGE_OPERATORS = new Set(["!=", "not-in", "<", "<=", ">", ">="]);
74
+ /** Served by an equality-shaped index entry - `in` is a disjunction of equalities. */
75
+ const EQUALITY_OPERATORS = new Set(["==", "in"]);
76
+ /**
77
+ * The composite index a query shape needs, or `null` when Firestore's
78
+ * automatic single-field indexes already serve it.
79
+ *
80
+ * Use it to generate your Store tab declarations from the queries your code
81
+ * actually makes, rather than transcribing them by hand and discovering the
82
+ * gap in production:
83
+ *
84
+ * ```ts
85
+ * const needed = MY_QUERY_SHAPES.map(storeIndexFor).filter(Boolean);
86
+ * ```
87
+ *
88
+ * **Field order is Firestore's, not yours.** Equality fields first (sorted, so
89
+ * the order you passed them in cannot produce two different declarations for
90
+ * one query), then the `array-contains` field, then the field the query orders
91
+ * by. Declaring the same shape twice in two orders builds two indexes, one of
92
+ * which is dead weight on every document write.
93
+ *
94
+ * Connect runs the same derivation server-side and is the authority: a query
95
+ * with no matching declared index is refused with the index it needs spelled
96
+ * out, so a disagreement here surfaces on the first call rather than silently.
97
+ *
98
+ * Throws for a query Firestore cannot serve at all - two `array-contains`
99
+ * filters, ranges over two fields, or a range ordered by a different field.
100
+ */
101
+ export function storeIndexFor(query) {
102
+ const filters = query.filters ?? [];
103
+ const orderBy = query.orderBy ?? null;
104
+ const equalities = filters.filter((filter) => EQUALITY_OPERATORS.has(filter.op));
105
+ const arrayContains = filters.filter((filter) => filter.op === "array-contains");
106
+ const rangeFields = [
107
+ ...new Set(filters.filter((filter) => RANGE_OPERATORS.has(filter.op)).map((f) => f.field)),
108
+ ];
109
+ if (arrayContains.length > 1) {
110
+ throw new Error(`A query may use "array-contains" once; this one uses it ${arrayContains.length} times.`);
111
+ }
112
+ if (rangeFields.length > 1) {
113
+ throw new Error(`A query may range over one field; this one ranges over ${rangeFields.join(", ")}.`);
114
+ }
115
+ const rangeField = rangeFields[0] ?? null;
116
+ if (rangeField && orderBy && orderBy.field !== rangeField) {
117
+ throw new Error(`A range filter on "${rangeField}" must be ordered by "${rangeField}" - Firestore cannot order a range scan by "${orderBy.field}".`);
118
+ }
119
+ const ordered = orderBy?.field ?? rangeField;
120
+ const distinct = new Set([...filters.map((filter) => filter.field), ...(ordered ? [ordered] : [])]);
121
+ if (filters.length <= 1 && distinct.size <= 1)
122
+ return null;
123
+ const fields = [
124
+ ...[...new Set(equalities.map((filter) => filter.field))]
125
+ .sort()
126
+ .map((field) => ({ field, direction: "asc" })),
127
+ ...arrayContains.map((filter) => ({ field: filter.field, direction: "array-contains" })),
128
+ ];
129
+ if (ordered)
130
+ fields.push({ field: ordered, direction: orderBy?.direction ?? "asc" });
131
+ return fields;
132
+ }
133
+ /**
134
+ * An index as one comparable string - `"status:asc,createdAt:desc"`.
135
+ *
136
+ * Deduplicate a generated list on this before declaring it: a dozen query
137
+ * shapes across a screen usually collapse to two or three distinct indexes.
138
+ */
139
+ export function storeIndexSignature(fields) {
140
+ return fields.map((entry) => `${entry.field}:${entry.direction}`).join(",");
141
+ }
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Card } from "@heroui/react";
3
+ /**
4
+ * What a list, grid or table shows when it has nothing to show.
5
+ *
6
+ * **The two empty states are different facts and the copy must say so.** "You
7
+ * have no contacts" and "no contacts match this filter" call for different
8
+ * words and a different action - create the thing, versus undo the filter
9
+ * hiding it. Both render through here; the caller decides which by what it
10
+ * passes as `action`. Getting this wrong costs a user real time: an
11
+ * unfiltered-looking empty state in front of a filtered list reads as data
12
+ * loss.
13
+ *
14
+ * `variant` chooses the container by what the state stands *in*: `card` (the
15
+ * default), `dashed` for a region that will be filled in place - a file grid,
16
+ * a board column - where the outline is what says "things appear here", and
17
+ * `plain` for a slot already inside a card that would otherwise nest two
18
+ * borders.
19
+ */
20
+ export function ConnectEmptyState({ icon, title, description, action, variant = "card", className, }) {
21
+ const body = (_jsxs("div", { className: "flex flex-col items-center text-center", children: [icon && (_jsx("span", { "aria-hidden": true, className: "mb-3 text-muted [&>svg]:size-8", children: icon })), _jsx("p", { className: "text-sm font-medium text-foreground", children: title }), description && _jsx("p", { className: "mt-1 max-w-md text-sm text-muted", children: description }), action && _jsx("div", { className: "mt-3", children: action })] }));
22
+ if (variant === "plain")
23
+ return _jsx("div", { className: className, children: body });
24
+ return (_jsx(Card, { className: `p-5 sm:p-8 ${variant === "dashed" ? "border-dashed" : ""} ${className ?? ""}`, children: body }));
25
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Page-composition primitives - the layer between HeroUI and your features.
3
+ *
4
+ * Deliberately **not** `"use client"`. Everything here is presentational, with
5
+ * no hooks and no state, so it renders in a Server Component and stays out of
6
+ * the browser bundle. That is the whole reason this is a separate entry point
7
+ * from `@cosmoops/connect-sdk/client`: that module's barrel carries a
8
+ * `"use client"` directive, so re-exporting a `<Card>` wrapper through it
9
+ * would turn every page shell in your app into a client boundary for no gain.
10
+ *
11
+ * The interactive counterparts - a confirm dialog, cursor pagination, filter
12
+ * tabs - hold state and live in `/client` alongside the rest of the browser
13
+ * half.
14
+ *
15
+ * ```tsx
16
+ * import { ConnectPageShell, ConnectSection } from "@cosmoops/connect-sdk/ui";
17
+ *
18
+ * export default function ContactsPage() {
19
+ * return (
20
+ * <ConnectPageShell title="Contacts" description="Everyone you do business with.">
21
+ * <ConnectPageShell.Async fallback={<TableSkeleton />}>
22
+ * <ContactsTable />
23
+ * </ConnectPageShell.Async>
24
+ * </ConnectPageShell>
25
+ * );
26
+ * }
27
+ * ```
28
+ */
29
+ export { ConnectPageShell } from "./page-shell.js";
30
+ export { ConnectPageHeader } from "./page-header.js";
31
+ export { ConnectSection } from "./section.js";
32
+ export { ConnectEmptyState } from "./empty-state.js";
33
+ export { ConnectSettingRow } from "./setting-row.js";
34
+ export { ConnectStatusChip } from "./status-chip.js";
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The title / description / action block a page opens with.
4
+ *
5
+ * The structure is the point, not the styling. The action is a peer of the
6
+ * **title**, not of the block, so nothing below the first line can move it -
7
+ * the common hand-rolled shape puts the action after the description, which
8
+ * makes the vertical position of a page's primary control a function of how
9
+ * long someone wrote the paragraph above it. On a phone that pushes it most of
10
+ * a thumb's reach down the screen.
11
+ *
12
+ * Mobile-first, and the two axes are separate decisions: the title row is a
13
+ * `justify-between` at every width (title left, action right, where a thumb
14
+ * expects it), while the description is clamped to two lines on a phone and
15
+ * released from `sm` up.
16
+ *
17
+ * An `h2`, because the document's `h1` is the application itself.
18
+ *
19
+ * No hooks and no state, so this renders from a Server Component - which is
20
+ * why it lives in `@cosmoops/connect-sdk/ui` rather than `/client`.
21
+ */
22
+ export function ConnectPageHeader({ title, description, action, clamp = true, className, }) {
23
+ return (_jsxs("header", { className: `flex items-start justify-between gap-3 ${className ?? ""}`, children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h2", { className: "flex items-center gap-2 text-lg font-semibold", children: title }), description && (_jsx("p", { className: `mt-2 text-sm text-muted ${clamp ? "line-clamp-2 sm:line-clamp-none" : ""}`, children: description }))] }), action && _jsx("div", { className: "shrink-0", children: action })] }));
24
+ }
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Suspense } from "react";
3
+ import { ConnectPageHeader } from "./page-header.js";
4
+ /**
5
+ * The frame a page opens with: its vertical rhythm, its header, and the
6
+ * suspended regions beneath.
7
+ *
8
+ * **The header is a prop, so it renders in the static shell** - above and
9
+ * outside every `<Suspense>` boundary. That is the one bug this component
10
+ * exists to make unrepresentable: rendering the header inside the async child
11
+ * makes a page's own title wait on a data read, and under Next's partial
12
+ * prefetching the route's shared App Shell loses it as well. There is no
13
+ * arrangement of `children` that reaches the header slot.
14
+ *
15
+ * A detail route is the honest exception, and it has its own door:
16
+ * `ConnectPageShell.Header` renders the identical header from inside an async
17
+ * child, for a page whose title is data it does not have yet. Pair it with a
18
+ * skeleton header in the fallback. The rule is that a *static* title must
19
+ * never be made to wait, not that every title can be static.
20
+ *
21
+ * `space-y-5` is the page gap, owned here so it has exactly one definition.
22
+ */
23
+ export function ConnectPageShell({ title, description, action, clamp, above, children, }) {
24
+ return (_jsxs("div", { className: "space-y-5", children: [above, title !== undefined && (_jsx(ConnectPageHeader, { title: title, description: description, action: action, clamp: clamp })), children] }));
25
+ }
26
+ /**
27
+ * The page header, for a detail route that can only name itself once its data
28
+ * has loaded. The same component the shell renders from its props, so the two
29
+ * cannot drift apart.
30
+ */
31
+ ConnectPageShell.Header = ConnectPageHeader;
32
+ /**
33
+ * A suspended region of the page.
34
+ *
35
+ * The donut: a static shell with async children behind their own boundaries,
36
+ * so a slow read costs its own section rather than the whole page. Give each
37
+ * independent region its own `Async` and they stream separately.
38
+ */
39
+ ConnectPageShell.Async = function ConnectPageShellAsync({ fallback = null, children, }) {
40
+ return _jsx(Suspense, { fallback: fallback, children: children });
41
+ };
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Card } from "@heroui/react";
3
+ /**
4
+ * One titled block inside a page: a card, its heading, what it is for, and the
5
+ * one control that acts on it.
6
+ *
7
+ * The heading is an `h3` - `ConnectPageHeader` renders the page title as an
8
+ * `h2`, so a block *inside* a page is a level below it. Hand-rolled sections
9
+ * reliably use `h2`, which makes every settings block a sibling of the page it
10
+ * sits in and flattens the outline a screen reader announces.
11
+ *
12
+ * As in the page header, the action is a peer of the title rather than of the
13
+ * block, so its position does not move with the length of the description.
14
+ */
15
+ export function ConnectSection({ title, description, action, titleId, className, bodyClassName, children, }) {
16
+ return (_jsxs(Card, { className: className ?? "p-4 sm:p-5", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h3", { id: titleId, className: "text-base font-semibold", children: title }), description && _jsx("p", { className: "mt-1 text-sm text-muted", children: description })] }), action && _jsx("div", { className: "shrink-0", children: action })] }), children && _jsx("div", { className: bodyClassName ?? "mt-4 space-y-4", children: children })] }));
17
+ }
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * One labelled setting inside a `ConnectSection`.
4
+ *
5
+ * Stacks on a phone and goes side-by-side from `sm` up, rather than shrinking
6
+ * a two-column row until neither column is readable. The control keeps its
7
+ * intrinsic width so truncation lands on the description.
8
+ */
9
+ export function ConnectSettingRow({ title, description, action, children, className, }) {
10
+ return (_jsxs("div", { className: `flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between ${className ?? ""}`, children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-sm font-medium text-foreground", children: title }), description && _jsx("p", { className: "mt-1 text-sm text-muted", children: description }), children && _jsx("div", { className: "mt-3", children: children })] }), action && _jsx("div", { className: "shrink-0 sm:pl-4", children: action })] }));
11
+ }
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Chip } from "@heroui/react";
3
+ /**
4
+ * One status, rendered the way every status should be.
5
+ *
6
+ * Domain knowledge stays in the domain: each caller exports its own
7
+ * `ConnectStatusPresentation` next to the union it describes, and this renders
8
+ * it. A status whose *rendering* carries real logic - a scheduled cancellation
9
+ * outranking a running trial, say - keeps its own component and calls this for
10
+ * the final chip. A map cannot express precedence.
11
+ */
12
+ export function ConnectStatusChip({ status, presentation, className, }) {
13
+ return (_jsx(Chip, { color: presentation.tone[status], size: "sm", variant: "soft", className: className, children: presentation.label[status] }));
14
+ }
@@ -11,34 +11,51 @@ export interface AppShellProps {
11
11
  homeHref?: string;
12
12
  /** The sidebar's destinations - items, sections, or a mix. See `ConnectNav`. */
13
13
  nav: ConnectNav;
14
- /** Current path. Pass `usePathname()` in Next; see `ConnectSideNavProps.activePath`. */
14
+ /**
15
+ * A second nav group, pinned to the foot of the rail above the account row.
16
+ *
17
+ * For the routes a product has but does not want competing with its own:
18
+ * settings, help, changelog, status. They are the same `ConnectNav` shape
19
+ * and get the same active resolution and audience filtering - the only
20
+ * difference is where they sit, which is the difference between "what this
21
+ * product is for" and "everything else".
22
+ *
23
+ * A route belongs up top if a user goes there to *do the job*, and down here
24
+ * if they go there to change how the job works. Putting Settings in the
25
+ * primary list is what makes a nine-item rail out of a five-item product.
26
+ */
27
+ secondaryNav?: ConnectNav;
28
+ /** Current path. Pass `usePathname()` in Next; see `ConnectSidebarNavProps.activePath`. */
15
29
  activePath?: string;
16
30
  /** See `ConnectLinkRenderer` - how this app renders a link. Defaults to a plain `<a>`. */
17
31
  renderLink?: ConnectLinkRenderer;
32
+ /** Extra rows pinned to the bottom of the sidebar, above the account row. See `ConnectSidebarRow`. */
33
+ sidebarFooter?: ReactNode;
18
34
  /**
19
- * Page heading in the top bar. A string renders as the page's `<h1>`, so
20
- * don't also put one at the top of the page; pass a node for breadcrumbs or
21
- * anything else.
35
+ * The workspace switcher under the brand. On by default, and it hides itself
36
+ * for an account with only one workspace - so the reason to pass `false` is
37
+ * not "most users have one" (already handled) but "this application renders
38
+ * its own".
22
39
  */
23
- title?: ReactNode;
24
- /** Your own controls in the top bar, left of the Connect ones. */
25
- headerActions?: ReactNode;
26
- /** Extra content pinned to the bottom of the sidebar, above the fold-away. */
27
- sidebarFooter?: ReactNode;
40
+ workspaceSwitcher?: false;
28
41
  /** Rendered below `children`, inside the scrolling content pane. */
29
42
  footer?: ReactNode;
30
- /** Start the rail collapsed. Ignored when `collapsed` is supplied. */
43
+ /**
44
+ * Start the rail collapsed. **Defaults to `true`** - see the component's
45
+ * own doc comment. Ignored when `collapsed` is supplied, and overridden by
46
+ * a stored preference from the user's last visit.
47
+ */
31
48
  defaultCollapsed?: boolean;
32
49
  /** Drive the rail yourself. Supply `onCollapsedChange` with it. */
33
50
  collapsed?: boolean;
34
51
  onCollapsedChange?: (collapsed: boolean) => void;
35
52
  /** Remove the collapse control and pin the rail expanded. */
36
53
  collapsible?: boolean;
37
- /** The app launcher in the top bar. On by default. */
54
+ /** The app launcher, in the sidebar footer. On by default. */
38
55
  showAppsGrid?: boolean;
39
- /** "Continue with Connect" / "Create account" in the top bar when signed out. On by default. */
56
+ /** "Continue with Connect" / "Create account" in the sidebar footer when signed out. On by default. */
40
57
  showAuthButtons?: boolean;
41
- /** Passed to `ConnectUserMenu` - the SDK has no theme provider, so this is host-controlled. */
58
+ /** Passed to `ConnectUserButton` - the SDK has no theme provider, so this is host-controlled. */
42
59
  theme?: ThemeMode;
43
60
  onThemeChange?: (theme: ThemeMode) => void;
44
61
  /**
@@ -48,7 +65,7 @@ export interface AppShellProps {
48
65
  * gate asks for, and an app that mounts a shell and forgets the gate is an
49
66
  * app that lets a suspended workspace keep working.
50
67
  *
51
- * The chrome is never gated: the sidebar, the lockup, the user menu and
68
+ * The chrome is never gated: the sidebar, the lockup, the account row and
52
69
  * sign-out stay reachable in every blocked state, so a blocked customer can
53
70
  * always get to the page that unblocks them.
54
71
  *
@@ -83,23 +100,21 @@ export interface AppShellProps {
83
100
  children: ReactNode;
84
101
  }
85
102
  /**
86
- * The application frame every CosmoOps application platform runs inside: a
87
- * collapsible sidebar, a top bar carrying the app launcher and the account
88
- * menu, and your pages in the middle.
103
+ * The application frame every CosmoOps application platform runs inside: one
104
+ * rail down the side holding the brand, the workspace, the navigation and the
105
+ * account, and your pages in the rest.
89
106
  *
90
107
  * This is the piece each partner was otherwise writing by hand, and writing
91
- * slightly differently - which is how a family of applications stops looking like
92
- * one product. Everything that should be identical across platforms (the
93
- * lockup, the rail geometry, the active-route treatment, the launcher, the
94
- * account menu, the access gate) lives here; everything that genuinely
95
- * differs is a prop:
108
+ * slightly differently - which is how a family of applications stops looking
109
+ * like one product. Everything that should be identical across platforms
110
+ * lives here; everything that genuinely differs is a prop:
96
111
  *
97
112
  * ```tsx
98
113
  * "use client";
99
114
  * import Link from "next/link";
100
115
  * import { usePathname } from "next/navigation";
101
116
  * import { AppShell } from "@cosmoops/connect-sdk/client";
102
- * import { LayoutDashboard, Truck } from "lucide-react";
117
+ * import { LayoutDashboard, Settings, Truck } from "lucide-react";
103
118
  *
104
119
  * export function Shell({ children }: { children: React.ReactNode }) {
105
120
  * return (
@@ -110,7 +125,10 @@ export interface AppShellProps {
110
125
  * renderLink={(props) => <Link {...props} />}
111
126
  * nav={[
112
127
  * { label: "Overview", href: "/", icon: <LayoutDashboard className="size-5" /> },
113
- * { label: "Plans", href: "/plans", icon: <Truck className="size-5" />, gateExempt: true },
128
+ * { label: "Shipments", href: "/shipments", icon: <Truck className="size-5" /> },
129
+ * ]}
130
+ * secondaryNav={[
131
+ * { label: "Settings", href: "/settings", icon: <Settings className="size-5" /> },
114
132
  * ]}
115
133
  * >
116
134
  * {children}
@@ -119,13 +137,33 @@ export interface AppShellProps {
119
137
  * }
120
138
  * ```
121
139
  *
140
+ * **There is no top bar.** There used to be one, carrying a page title, the
141
+ * launcher and the account menu - and it was a second, half-width strip of
142
+ * chrome saying things the rail was better placed to say. The launcher and
143
+ * the account row moved into the rail's footer, where they are one press from
144
+ * wherever the pointer already is; the page title moved into the page, where
145
+ * `ConnectPageShell` renders it next to that page's own actions and
146
+ * breadcrumbs. What is left is a single column of chrome and a full-height
147
+ * content pane, which is what every application this shell is modelled on
148
+ * settled at.
149
+ *
150
+ * The one exception is below `md`, where a slim bar carries the drawer
151
+ * trigger. A phone has no rail to put it in, and a navigation you cannot open
152
+ * is not a smaller navigation.
153
+ *
154
+ * **The rail starts collapsed.** `defaultCollapsed` is `true`: the icon rail
155
+ * is the resting state, and expanding is the deliberate act. That is the right
156
+ * default because the rail's job is to be *there*, not to be read - a person
157
+ * learns five glyphs in a day and then wants the 200px back for their data.
158
+ * A stored preference from the user's last visit always wins over it.
159
+ *
122
160
  * There is no `isSignedIn` branch anywhere in here, and there should be none
123
- * in yours: the auth buttons render nothing once a session exists, the user
124
- * menu renders nothing without one, and the launcher renders nothing when
161
+ * in yours: the auth buttons render nothing once a session exists, the account
162
+ * row renders nothing without one, and the launcher renders nothing when
125
163
  * signed out. The swap happens inside the components.
126
164
  *
127
165
  * It renders no provider of its own - mount `ConnectProvider` above it, once,
128
166
  * so a page with a shell, a plan table and a usage meter still makes one
129
167
  * session read.
130
168
  */
131
- export declare function AppShell({ name, icon, homeHref, nav, activePath, renderLink, title, headerActions, sidebarFooter, footer, defaultCollapsed, collapsed: controlledCollapsed, onCollapsedChange, collapsible, showAppsGrid, showAuthButtons, theme, onThemeChange, gate, gateExemptRoutes, askAi, className, contentClassName, children, }: AppShellProps): import("react").JSX.Element;
169
+ export declare function AppShell({ name, icon, homeHref, nav, secondaryNav, activePath, renderLink, sidebarFooter, workspaceSwitcher, footer, defaultCollapsed, collapsed: controlledCollapsed, onCollapsedChange, collapsible, showAppsGrid, showAuthButtons, theme, onThemeChange, gate, gateExemptRoutes, askAi, className, contentClassName, children, }: AppShellProps): import("react").JSX.Element;
@@ -1,8 +1,17 @@
1
1
  export interface ConnectAppsGridProps {
2
2
  /** Extra classes on the trigger button. */
3
3
  className?: string;
4
- /** Trigger label for screen readers. */
4
+ /** Trigger label for screen readers, and the collapsed rail's tooltip. */
5
5
  label?: string;
6
+ /**
7
+ * How the trigger is drawn.
8
+ *
9
+ * - `"row"` - a full-width sidebar row, glyph on the rail's shared 32px
10
+ * centre. The default, since the launcher lives in the sidebar footer now
11
+ * that there is no top bar to hold it.
12
+ * - `"icon"` - the square accent button, for a bar of your own.
13
+ */
14
+ variant?: "row" | "icon";
6
15
  }
7
16
  /**
8
17
  * The Google-style app launcher: a grid icon in your top bar that opens a
@@ -17,4 +26,4 @@ export interface ConnectAppsGridProps {
17
26
  * Renders nothing when signed out: a launcher with nothing to launch is just a
18
27
  * button that opens an empty box.
19
28
  */
20
- export declare function ConnectAppsGrid({ className, label }: ConnectAppsGridProps): import("react").JSX.Element | null;
29
+ export declare function ConnectAppsGrid({ className, label, variant }: ConnectAppsGridProps): import("react").JSX.Element | null;
@@ -0,0 +1,34 @@
1
+ import { type ReactNode } from "react";
2
+ export interface ConnectConfirmDialogProps {
3
+ /** The control that opens it. Rendered inside `Modal.Trigger`. */
4
+ trigger: ReactNode;
5
+ title: ReactNode;
6
+ /** What will happen, in the user's words. Name the thing being acted on. */
7
+ description?: ReactNode;
8
+ confirmLabel?: string;
9
+ cancelLabel?: string;
10
+ /** `danger` for anything that destroys data. */
11
+ tone?: "danger" | "primary";
12
+ onConfirm: () => void | Promise<void>;
13
+ }
14
+ /**
15
+ * The confirmation in front of a destructive action.
16
+ *
17
+ * **It closes on confirm, not on response.** Holding the modal open until the
18
+ * server answers makes every delete feel like a form submission, and the error
19
+ * path already has somewhere to go - a toast, or the row reappearing. A dialog
20
+ * that stays open to report failure is right only where the failure is
21
+ * *expected* and correctable in place (a seat cap, a last-admin invariant),
22
+ * and that is a blocking form, not this.
23
+ *
24
+ * `onConfirm` may return a promise. It is not awaited before closing, but a
25
+ * rejection is caught here rather than escaping - an error thrown out of an
26
+ * async transition reaches the nearest error boundary and takes the page down.
27
+ *
28
+ * The anatomy is HeroUI's: `Modal.Backdrop` owns the open state,
29
+ * `Modal.Container` the sizing, `Modal.Dialog` the focus trap. The trigger
30
+ * goes inside `Modal.Trigger`, which *is* a React Aria button - so pass a
31
+ * plain element, never a `<Button>`, or the nested button swallows the press
32
+ * and the dialog silently never opens.
33
+ */
34
+ export declare function ConnectConfirmDialog({ trigger, title, description, confirmLabel, cancelLabel, tone, onConfirm, }: Readonly<ConnectConfirmDialogProps>): import("react").JSX.Element;
@@ -0,0 +1,27 @@
1
+ export interface ConnectCursorPaginationProps {
2
+ /** How many rows the current page is showing. Used for the live-region count. */
3
+ shown: number;
4
+ /** The cursor for the next page, or `null` when this is the last one. */
5
+ nextCursor: string | null;
6
+ /** Whether a previous page exists - the caller owns the cursor history. */
7
+ hasPrevious: boolean;
8
+ onNext: (cursor: string) => void;
9
+ onPrevious: () => void;
10
+ isPending?: boolean;
11
+ /** Singular/plural noun for the count, e.g. `["contact", "contacts"]`. */
12
+ noun?: readonly [string, string];
13
+ }
14
+ /**
15
+ * Next / previous for a cursor-paginated list.
16
+ *
17
+ * Cursor rather than page numbers, and the component shape follows from that:
18
+ * there is no "page 7 of 42" because a keyset-paginated list does not know how
19
+ * many pages there are without a second count query over the same filter -
20
+ * which is a billed index scan for a number nobody acts on. What a user does
21
+ * act on is "more" and "back", and those are exact.
22
+ *
23
+ * The count is announced in a live region so a screen-reader user learns that
24
+ * filtering or paging changed the result set. Without it the table silently
25
+ * swaps its contents.
26
+ */
27
+ export declare function ConnectCursorPagination({ shown, nextCursor, hasPrevious, onNext, onPrevious, isPending, noun, }: Readonly<ConnectCursorPaginationProps>): import("react").JSX.Element;
@@ -0,0 +1,26 @@
1
+ export interface ConnectFilterTabsOption<TValue extends string> {
2
+ value: TValue | null;
3
+ label: string;
4
+ /** Shown as a count beside the label. Omit where a count would need its own query. */
5
+ count?: number;
6
+ }
7
+ export interface ConnectFilterTabsProps<TValue extends string> {
8
+ options: ReadonlyArray<ConnectFilterTabsOption<TValue>>;
9
+ value: TValue | null;
10
+ onChange: (value: TValue | null) => void;
11
+ /** Names the group for assistive technology - "Filter contacts by lifecycle". */
12
+ label: string;
13
+ }
14
+ /**
15
+ * A single-select filter strip - the "All / Open / Won / Lost" row above a
16
+ * list.
17
+ *
18
+ * Real radio semantics rather than a row of buttons: it is a single choice
19
+ * from a closed set, so arrow keys should move between options and the
20
+ * selected one should be announced as selected. A group of `<button>`s gets
21
+ * neither, and that is the usual hand-rolled shape.
22
+ *
23
+ * Scrolls horizontally rather than wrapping on a narrow viewport - a filter
24
+ * strip that wraps to three lines pushes the list itself off the screen.
25
+ */
26
+ export declare function ConnectFilterTabs<TValue extends string>({ options, value, onChange, label, }: Readonly<ConnectFilterTabsProps<TValue>>): import("react").JSX.Element;
@@ -0,0 +1,31 @@
1
+ import type { ReactNode } from "react";
2
+ export interface ConnectGrievanceNoticeProps {
3
+ /**
4
+ * Heading text. `null` renders none, for embedding inside a page that
5
+ * already has one.
6
+ */
7
+ title?: ReactNode;
8
+ /**
9
+ * What this application calls itself in the sentence naming the processor
10
+ * relationship. Omit to leave that sentence out entirely.
11
+ */
12
+ applicationName?: string;
13
+ className?: string;
14
+ }
15
+ /**
16
+ * The DPDP s.13 grievance channel, rendered.
17
+ *
18
+ * Every application on the platform is a Data Processor under one notice, so
19
+ * the wording lives here rather than being retyped per application - a user
20
+ * comparing two applications' privacy pages should not find two different
21
+ * accounts of who their Data Fiduciary is.
22
+ *
23
+ * Renders nothing at all while loading and nothing on failure, rather than a
24
+ * skeleton or an error. That is unusual here and deliberate: this component
25
+ * is almost always inside a larger privacy page that has its own content, and
26
+ * a red error box in the middle of a legal notice reads as the notice itself
27
+ * being broken. The address is also on the public web either way. Pass
28
+ * `useConnectCompanyProfile()` directly to your own layout if you want to
29
+ * handle those states yourself.
30
+ */
31
+ export declare function ConnectGrievanceNotice({ title, applicationName, className, }: Readonly<ConnectGrievanceNoticeProps>): import("react").JSX.Element | null;