@cosmoops/connect-sdk 0.7.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 (154) hide show
  1. package/CHANGELOG.md +662 -0
  2. package/README.md +376 -124
  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 +59 -5
  6. package/dist/cjs/client/components/ai-format.js +135 -0
  7. package/dist/cjs/client/components/ai-widgets.js +232 -0
  8. package/dist/cjs/client/components/app-shell.js +80 -45
  9. package/dist/cjs/client/components/apps-grid.js +3 -2
  10. package/dist/cjs/client/components/ask-ai.js +353 -0
  11. package/dist/cjs/client/components/confirm-dialog.js +40 -0
  12. package/dist/cjs/client/components/cursor-pagination.js +22 -0
  13. package/dist/cjs/client/components/filter-tabs.js +24 -0
  14. package/dist/cjs/client/components/grievance-notice.js +30 -0
  15. package/dist/cjs/client/components/link-button.js +17 -10
  16. package/dist/cjs/client/components/logo.js +4 -3
  17. package/dist/cjs/client/components/nav.js +42 -1
  18. package/dist/cjs/client/components/plan-pricing.js +27 -0
  19. package/dist/cjs/client/components/plans.js +1 -1
  20. package/dist/cjs/client/components/platform-gate.js +48 -0
  21. package/dist/cjs/client/components/shell-context.js +149 -0
  22. package/dist/cjs/client/components/side-nav.js +50 -24
  23. package/dist/cjs/client/components/sidebar.js +127 -0
  24. package/dist/cjs/client/components/subscribe-dialog.js +2 -2
  25. package/dist/cjs/client/components/subscription.js +1 -1
  26. package/dist/cjs/client/components/user-menu.js +122 -40
  27. package/dist/cjs/client/components/workspace-switcher.js +100 -0
  28. package/dist/cjs/client/components/workspace-type-gate.js +48 -0
  29. package/dist/cjs/client/index.js +98 -3
  30. package/dist/cjs/client/provider.js +100 -2
  31. package/dist/cjs/client/use-checkout.js +9 -5
  32. package/dist/cjs/errors.js +1 -1
  33. package/dist/cjs/index.js +11 -1
  34. package/dist/cjs/next/index.js +2 -2
  35. package/dist/cjs/server/client.js +25 -2
  36. package/dist/cjs/server/handler.js +237 -16
  37. package/dist/cjs/server/http.js +52 -26
  38. package/dist/cjs/server/resources/acting-as.js +16 -0
  39. package/dist/cjs/server/resources/ai.js +15 -2
  40. package/dist/cjs/server/resources/data.js +374 -0
  41. package/dist/cjs/server/resources/entitlements.js +41 -0
  42. package/dist/cjs/server/resources/identity.js +9 -3
  43. package/dist/cjs/server/resources/kv.js +84 -0
  44. package/dist/cjs/server/resources/platform.js +43 -0
  45. package/dist/cjs/server/resources/sessions.js +1 -1
  46. package/dist/cjs/server/resources/workspaces.js +74 -0
  47. package/dist/cjs/store.js +152 -0
  48. package/dist/cjs/ui/empty-state.js +28 -0
  49. package/dist/cjs/ui/index.js +44 -0
  50. package/dist/cjs/ui/page-header.js +27 -0
  51. package/dist/cjs/ui/page-shell.js +44 -0
  52. package/dist/cjs/ui/section.js +20 -0
  53. package/dist/cjs/ui/setting-row.js +14 -0
  54. package/dist/cjs/ui/status-chip.js +17 -0
  55. package/dist/esm/cli/doctor.js +3 -3
  56. package/dist/esm/cli/init.js +3 -3
  57. package/dist/esm/client/components/access-gate.js +60 -6
  58. package/dist/esm/client/components/ai-format.js +129 -0
  59. package/dist/esm/client/components/ai-widgets.js +223 -0
  60. package/dist/esm/client/components/app-shell.js +85 -50
  61. package/dist/esm/client/components/apps-grid.js +3 -2
  62. package/dist/esm/client/components/ask-ai.js +350 -0
  63. package/dist/esm/client/components/confirm-dialog.js +37 -0
  64. package/dist/esm/client/components/cursor-pagination.js +19 -0
  65. package/dist/esm/client/components/filter-tabs.js +21 -0
  66. package/dist/esm/client/components/grievance-notice.js +27 -0
  67. package/dist/esm/client/components/link-button.js +17 -10
  68. package/dist/esm/client/components/logo.js +4 -3
  69. package/dist/esm/client/components/nav.js +41 -1
  70. package/dist/esm/client/components/plan-pricing.js +25 -0
  71. package/dist/esm/client/components/plans.js +2 -2
  72. package/dist/esm/client/components/platform-gate.js +45 -0
  73. package/dist/esm/client/components/shell-context.js +141 -0
  74. package/dist/esm/client/components/side-nav.js +49 -22
  75. package/dist/esm/client/components/sidebar.js +120 -0
  76. package/dist/esm/client/components/subscribe-dialog.js +3 -3
  77. package/dist/esm/client/components/subscription.js +2 -2
  78. package/dist/esm/client/components/user-menu.js +121 -41
  79. package/dist/esm/client/components/workspace-switcher.js +97 -0
  80. package/dist/esm/client/components/workspace-type-gate.js +45 -0
  81. package/dist/esm/client/index.js +58 -5
  82. package/dist/esm/client/provider.js +94 -2
  83. package/dist/esm/client/use-checkout.js +9 -5
  84. package/dist/esm/errors.js +1 -1
  85. package/dist/esm/index.js +1 -0
  86. package/dist/esm/next/index.js +2 -2
  87. package/dist/esm/server/client.js +25 -2
  88. package/dist/esm/server/handler.js +237 -16
  89. package/dist/esm/server/http.js +52 -26
  90. package/dist/esm/server/resources/acting-as.js +13 -0
  91. package/dist/esm/server/resources/ai.js +15 -2
  92. package/dist/esm/server/resources/data.js +369 -0
  93. package/dist/esm/server/resources/entitlements.js +41 -0
  94. package/dist/esm/server/resources/identity.js +9 -3
  95. package/dist/esm/server/resources/kv.js +80 -0
  96. package/dist/esm/server/resources/platform.js +39 -0
  97. package/dist/esm/server/resources/sessions.js +1 -1
  98. package/dist/esm/server/resources/workspaces.js +70 -0
  99. package/dist/esm/store.js +141 -0
  100. package/dist/esm/ui/empty-state.js +25 -0
  101. package/dist/esm/ui/index.js +34 -0
  102. package/dist/esm/ui/page-header.js +24 -0
  103. package/dist/esm/ui/page-shell.js +41 -0
  104. package/dist/esm/ui/section.js +17 -0
  105. package/dist/esm/ui/setting-row.js +11 -0
  106. package/dist/esm/ui/status-chip.js +14 -0
  107. package/dist/theme.css +87 -0
  108. package/dist/types/client/components/access-gate.d.ts +17 -3
  109. package/dist/types/client/components/ai-format.d.ts +24 -0
  110. package/dist/types/client/components/ai-widgets.d.ts +42 -0
  111. package/dist/types/client/components/app-shell.d.ts +85 -31
  112. package/dist/types/client/components/apps-grid.d.ts +11 -2
  113. package/dist/types/client/components/ask-ai.d.ts +73 -0
  114. package/dist/types/client/components/confirm-dialog.d.ts +34 -0
  115. package/dist/types/client/components/cursor-pagination.d.ts +27 -0
  116. package/dist/types/client/components/filter-tabs.d.ts +26 -0
  117. package/dist/types/client/components/grievance-notice.d.ts +31 -0
  118. package/dist/types/client/components/link-button.d.ts +32 -9
  119. package/dist/types/client/components/logo.d.ts +18 -2
  120. package/dist/types/client/components/nav.d.ts +27 -3
  121. package/dist/types/client/components/plan-pricing.d.ts +10 -0
  122. package/dist/types/client/components/platform-gate.d.ts +53 -0
  123. package/dist/types/client/components/shell-context.d.ts +116 -0
  124. package/dist/types/client/components/side-nav.d.ts +28 -14
  125. package/dist/types/client/components/sidebar.d.ts +95 -0
  126. package/dist/types/client/components/user-menu.d.ts +70 -23
  127. package/dist/types/client/components/workspace-switcher.d.ts +49 -0
  128. package/dist/types/client/components/workspace-type-gate.d.ts +48 -0
  129. package/dist/types/client/index.d.ts +36 -7
  130. package/dist/types/client/provider.d.ts +149 -3
  131. package/dist/types/client/use-checkout.d.ts +8 -4
  132. package/dist/types/errors.d.ts +1 -1
  133. package/dist/types/index.d.ts +2 -0
  134. package/dist/types/next/index.d.ts +1 -1
  135. package/dist/types/server/client.d.ts +25 -2
  136. package/dist/types/server/http.d.ts +14 -3
  137. package/dist/types/server/index.d.ts +4 -0
  138. package/dist/types/server/resources/acting-as.d.ts +11 -0
  139. package/dist/types/server/resources/ai.d.ts +9 -1
  140. package/dist/types/server/resources/data.d.ts +258 -0
  141. package/dist/types/server/resources/entitlements.d.ts +33 -1
  142. package/dist/types/server/resources/kv.d.ts +63 -0
  143. package/dist/types/server/resources/platform.d.ts +32 -0
  144. package/dist/types/server/resources/workspaces.d.ts +55 -0
  145. package/dist/types/store.d.ts +96 -0
  146. package/dist/types/types.d.ts +491 -13
  147. package/dist/types/ui/empty-state.d.ts +34 -0
  148. package/dist/types/ui/index.d.ts +40 -0
  149. package/dist/types/ui/page-header.d.ts +34 -0
  150. package/dist/types/ui/page-shell.d.ts +39 -0
  151. package/dist/types/ui/section.d.ts +26 -0
  152. package/dist/types/ui/setting-row.d.ts +18 -0
  153. package/dist/types/ui/status-chip.d.ts +36 -0
  154. package/package.json +13 -3
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.increment = increment;
4
+ exports.serverTimestamp = serverTimestamp;
5
+ exports.arrayUnion = arrayUnion;
6
+ exports.arrayRemove = arrayRemove;
7
+ exports.deleteField = deleteField;
8
+ exports.unchangedSince = unchangedSince;
9
+ exports.documentExists = documentExists;
10
+ exports.storeIndexFor = storeIndexFor;
11
+ exports.storeIndexSignature = storeIndexSignature;
12
+ /**
13
+ * Building blocks for `data.commit` - the transform sentinels, and the two
14
+ * ways to say "only if nothing has moved".
15
+ *
16
+ * Isomorphic and dependency-free, so a Server Component and a client component
17
+ * building the same write use the same constructors. They are functions rather
18
+ * than object literals you write by hand because the sentinel shape is an
19
+ * encoding, not an API: `{ __transform: "increment", by: 1 }` is what goes over
20
+ * the wire, and nothing outside this file should have to know that.
21
+ */
22
+ /**
23
+ * Add to a number, atomically.
24
+ *
25
+ * **The one operation you genuinely cannot do yourself.** Increments are
26
+ * commutative, so two concurrent `increment(500)` calls both land - where
27
+ * read-add-write from your own process loses one of them however carefully it
28
+ * retries, because the read and the write are two round trips with a gap in
29
+ * between. Use this for balances, counters and tallies, and reach for a
30
+ * precondition only when the new value depends on the old one in a way
31
+ * addition cannot express.
32
+ *
33
+ * Negative to subtract; there is no separate `decrement`.
34
+ */
35
+ function increment(by) {
36
+ return { __transform: "increment", by };
37
+ }
38
+ /** Connect's own write time, rather than a clock reading from your process. */
39
+ function serverTimestamp() {
40
+ return { __transform: "serverTimestamp" };
41
+ }
42
+ /** Adds the values not already in the array. Order is not preserved. */
43
+ function arrayUnion(...values) {
44
+ return { __transform: "arrayUnion", values };
45
+ }
46
+ /** Removes every occurrence of each value. */
47
+ function arrayRemove(...values) {
48
+ return { __transform: "arrayRemove", values };
49
+ }
50
+ /**
51
+ * Removes the field entirely.
52
+ *
53
+ * `merge` only - a `replace` already drops every field it does not list, so
54
+ * asking it to delete one is a contradiction rather than a narrowing.
55
+ */
56
+ function deleteField() {
57
+ return { __transform: "delete" };
58
+ }
59
+ /**
60
+ * "Only if this document has not changed since I read it."
61
+ *
62
+ * Pass the document a read returned, and the precondition is built from its
63
+ * `_updateTime`. Taking the document rather than the timestamp is deliberate:
64
+ * a caller who has to reach for `doc.data._updateTime` themselves is one typo
65
+ * away from a precondition that silently does nothing.
66
+ *
67
+ * Throws rather than degrading when the document carries no `_updateTime` -
68
+ * that only happens for a document you built yourself rather than read, and a
69
+ * precondition that quietly became "no precondition" is worse than a crash at
70
+ * the call site.
71
+ */
72
+ function unchangedSince(document) {
73
+ const updateTime = document.data._updateTime;
74
+ if (typeof updateTime !== "string") {
75
+ 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.`);
76
+ }
77
+ return { updateTime };
78
+ }
79
+ /** "Only if this document exists" / "only if it does not." */
80
+ function documentExists(exists = true) {
81
+ return { exists };
82
+ }
83
+ /** Firestore treats these as a range scan, which constrains where they may sit. */
84
+ const RANGE_OPERATORS = new Set(["!=", "not-in", "<", "<=", ">", ">="]);
85
+ /** Served by an equality-shaped index entry - `in` is a disjunction of equalities. */
86
+ const EQUALITY_OPERATORS = new Set(["==", "in"]);
87
+ /**
88
+ * The composite index a query shape needs, or `null` when Firestore's
89
+ * automatic single-field indexes already serve it.
90
+ *
91
+ * Use it to generate your Store tab declarations from the queries your code
92
+ * actually makes, rather than transcribing them by hand and discovering the
93
+ * gap in production:
94
+ *
95
+ * ```ts
96
+ * const needed = MY_QUERY_SHAPES.map(storeIndexFor).filter(Boolean);
97
+ * ```
98
+ *
99
+ * **Field order is Firestore's, not yours.** Equality fields first (sorted, so
100
+ * the order you passed them in cannot produce two different declarations for
101
+ * one query), then the `array-contains` field, then the field the query orders
102
+ * by. Declaring the same shape twice in two orders builds two indexes, one of
103
+ * which is dead weight on every document write.
104
+ *
105
+ * Connect runs the same derivation server-side and is the authority: a query
106
+ * with no matching declared index is refused with the index it needs spelled
107
+ * out, so a disagreement here surfaces on the first call rather than silently.
108
+ *
109
+ * Throws for a query Firestore cannot serve at all - two `array-contains`
110
+ * filters, ranges over two fields, or a range ordered by a different field.
111
+ */
112
+ function storeIndexFor(query) {
113
+ const filters = query.filters ?? [];
114
+ const orderBy = query.orderBy ?? null;
115
+ const equalities = filters.filter((filter) => EQUALITY_OPERATORS.has(filter.op));
116
+ const arrayContains = filters.filter((filter) => filter.op === "array-contains");
117
+ const rangeFields = [
118
+ ...new Set(filters.filter((filter) => RANGE_OPERATORS.has(filter.op)).map((f) => f.field)),
119
+ ];
120
+ if (arrayContains.length > 1) {
121
+ throw new Error(`A query may use "array-contains" once; this one uses it ${arrayContains.length} times.`);
122
+ }
123
+ if (rangeFields.length > 1) {
124
+ throw new Error(`A query may range over one field; this one ranges over ${rangeFields.join(", ")}.`);
125
+ }
126
+ const rangeField = rangeFields[0] ?? null;
127
+ if (rangeField && orderBy && orderBy.field !== rangeField) {
128
+ throw new Error(`A range filter on "${rangeField}" must be ordered by "${rangeField}" - Firestore cannot order a range scan by "${orderBy.field}".`);
129
+ }
130
+ const ordered = orderBy?.field ?? rangeField;
131
+ const distinct = new Set([...filters.map((filter) => filter.field), ...(ordered ? [ordered] : [])]);
132
+ if (filters.length <= 1 && distinct.size <= 1)
133
+ return null;
134
+ const fields = [
135
+ ...[...new Set(equalities.map((filter) => filter.field))]
136
+ .sort()
137
+ .map((field) => ({ field, direction: "asc" })),
138
+ ...arrayContains.map((filter) => ({ field: filter.field, direction: "array-contains" })),
139
+ ];
140
+ if (ordered)
141
+ fields.push({ field: ordered, direction: orderBy?.direction ?? "asc" });
142
+ return fields;
143
+ }
144
+ /**
145
+ * An index as one comparable string - `"status:asc,createdAt:desc"`.
146
+ *
147
+ * Deduplicate a generated list on this before declaring it: a dozen query
148
+ * shapes across a screen usually collapse to two or three distinct indexes.
149
+ */
150
+ function storeIndexSignature(fields) {
151
+ return fields.map((entry) => `${entry.field}:${entry.direction}`).join(",");
152
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectEmptyState = ConnectEmptyState;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("@heroui/react");
6
+ /**
7
+ * What a list, grid or table shows when it has nothing to show.
8
+ *
9
+ * **The two empty states are different facts and the copy must say so.** "You
10
+ * have no contacts" and "no contacts match this filter" call for different
11
+ * words and a different action - create the thing, versus undo the filter
12
+ * hiding it. Both render through here; the caller decides which by what it
13
+ * passes as `action`. Getting this wrong costs a user real time: an
14
+ * unfiltered-looking empty state in front of a filtered list reads as data
15
+ * loss.
16
+ *
17
+ * `variant` chooses the container by what the state stands *in*: `card` (the
18
+ * default), `dashed` for a region that will be filled in place - a file grid,
19
+ * a board column - where the outline is what says "things appear here", and
20
+ * `plain` for a slot already inside a card that would otherwise nest two
21
+ * borders.
22
+ */
23
+ function ConnectEmptyState({ icon, title, description, action, variant = "card", className, }) {
24
+ 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
+ if (variant === "plain")
26
+ 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
+ }
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ "use strict";
3
+ /**
4
+ * Page-composition primitives - the layer between HeroUI and your features.
5
+ *
6
+ * Deliberately **not** `"use client"`. Everything here is presentational, with
7
+ * no hooks and no state, so it renders in a Server Component and stays out of
8
+ * the browser bundle. That is the whole reason this is a separate entry point
9
+ * from `@cosmoops/connect-sdk/client`: that module's barrel carries a
10
+ * `"use client"` directive, so re-exporting a `<Card>` wrapper through it
11
+ * would turn every page shell in your app into a client boundary for no gain.
12
+ *
13
+ * The interactive counterparts - a confirm dialog, cursor pagination, filter
14
+ * tabs - hold state and live in `/client` alongside the rest of the browser
15
+ * half.
16
+ *
17
+ * ```tsx
18
+ * import { ConnectPageShell, ConnectSection } from "@cosmoops/connect-sdk/ui";
19
+ *
20
+ * export default function ContactsPage() {
21
+ * return (
22
+ * <ConnectPageShell title="Contacts" description="Everyone you do business with.">
23
+ * <ConnectPageShell.Async fallback={<TableSkeleton />}>
24
+ * <ContactsTable />
25
+ * </ConnectPageShell.Async>
26
+ * </ConnectPageShell>
27
+ * );
28
+ * }
29
+ * ```
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.ConnectStatusChip = exports.ConnectSettingRow = exports.ConnectEmptyState = exports.ConnectSection = exports.ConnectPageHeader = exports.ConnectPageShell = void 0;
33
+ var page_shell_js_1 = require("./page-shell.js");
34
+ Object.defineProperty(exports, "ConnectPageShell", { enumerable: true, get: function () { return page_shell_js_1.ConnectPageShell; } });
35
+ var page_header_js_1 = require("./page-header.js");
36
+ Object.defineProperty(exports, "ConnectPageHeader", { enumerable: true, get: function () { return page_header_js_1.ConnectPageHeader; } });
37
+ var section_js_1 = require("./section.js");
38
+ Object.defineProperty(exports, "ConnectSection", { enumerable: true, get: function () { return section_js_1.ConnectSection; } });
39
+ var empty_state_js_1 = require("./empty-state.js");
40
+ Object.defineProperty(exports, "ConnectEmptyState", { enumerable: true, get: function () { return empty_state_js_1.ConnectEmptyState; } });
41
+ var setting_row_js_1 = require("./setting-row.js");
42
+ Object.defineProperty(exports, "ConnectSettingRow", { enumerable: true, get: function () { return setting_row_js_1.ConnectSettingRow; } });
43
+ var status_chip_js_1 = require("./status-chip.js");
44
+ Object.defineProperty(exports, "ConnectStatusChip", { enumerable: true, get: function () { return status_chip_js_1.ConnectStatusChip; } });
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectPageHeader = ConnectPageHeader;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ /**
6
+ * The title / description / action block a page opens with.
7
+ *
8
+ * The structure is the point, not the styling. The action is a peer of the
9
+ * **title**, not of the block, so nothing below the first line can move it -
10
+ * the common hand-rolled shape puts the action after the description, which
11
+ * makes the vertical position of a page's primary control a function of how
12
+ * long someone wrote the paragraph above it. On a phone that pushes it most of
13
+ * a thumb's reach down the screen.
14
+ *
15
+ * Mobile-first, and the two axes are separate decisions: the title row is a
16
+ * `justify-between` at every width (title left, action right, where a thumb
17
+ * expects it), while the description is clamped to two lines on a phone and
18
+ * released from `sm` up.
19
+ *
20
+ * An `h2`, because the document's `h1` is the application itself.
21
+ *
22
+ * No hooks and no state, so this renders from a Server Component - which is
23
+ * why it lives in `@cosmoops/connect-sdk/ui` rather than `/client`.
24
+ */
25
+ 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
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectPageShell = ConnectPageShell;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const page_header_js_1 = require("./page-header.js");
7
+ /**
8
+ * The frame a page opens with: its vertical rhythm, its header, and the
9
+ * suspended regions beneath.
10
+ *
11
+ * **The header is a prop, so it renders in the static shell** - above and
12
+ * outside every `<Suspense>` boundary. That is the one bug this component
13
+ * exists to make unrepresentable: rendering the header inside the async child
14
+ * makes a page's own title wait on a data read, and under Next's partial
15
+ * prefetching the route's shared App Shell loses it as well. There is no
16
+ * arrangement of `children` that reaches the header slot.
17
+ *
18
+ * A detail route is the honest exception, and it has its own door:
19
+ * `ConnectPageShell.Header` renders the identical header from inside an async
20
+ * child, for a page whose title is data it does not have yet. Pair it with a
21
+ * skeleton header in the fallback. The rule is that a *static* title must
22
+ * never be made to wait, not that every title can be static.
23
+ *
24
+ * `space-y-5` is the page gap, owned here so it has exactly one definition.
25
+ */
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] }));
28
+ }
29
+ /**
30
+ * The page header, for a detail route that can only name itself once its data
31
+ * has loaded. The same component the shell renders from its props, so the two
32
+ * cannot drift apart.
33
+ */
34
+ ConnectPageShell.Header = page_header_js_1.ConnectPageHeader;
35
+ /**
36
+ * A suspended region of the page.
37
+ *
38
+ * The donut: a static shell with async children behind their own boundaries,
39
+ * so a slow read costs its own section rather than the whole page. Give each
40
+ * independent region its own `Async` and they stream separately.
41
+ */
42
+ ConnectPageShell.Async = function ConnectPageShellAsync({ fallback = null, children, }) {
43
+ return (0, jsx_runtime_1.jsx)(react_1.Suspense, { fallback: fallback, children: children });
44
+ };
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectSection = ConnectSection;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("@heroui/react");
6
+ /**
7
+ * One titled block inside a page: a card, its heading, what it is for, and the
8
+ * one control that acts on it.
9
+ *
10
+ * The heading is an `h3` - `ConnectPageHeader` renders the page title as an
11
+ * `h2`, so a block *inside* a page is a level below it. Hand-rolled sections
12
+ * reliably use `h2`, which makes every settings block a sibling of the page it
13
+ * sits in and flattens the outline a screen reader announces.
14
+ *
15
+ * As in the page header, the action is a peer of the title rather than of the
16
+ * block, so its position does not move with the length of the description.
17
+ */
18
+ 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
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectSettingRow = ConnectSettingRow;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ /**
6
+ * One labelled setting inside a `ConnectSection`.
7
+ *
8
+ * Stacks on a phone and goes side-by-side from `sm` up, rather than shrinking
9
+ * a two-column row until neither column is readable. The control keeps its
10
+ * intrinsic width so truncation lands on the description.
11
+ */
12
+ 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
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectStatusChip = ConnectStatusChip;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("@heroui/react");
6
+ /**
7
+ * One status, rendered the way every status should be.
8
+ *
9
+ * Domain knowledge stays in the domain: each caller exports its own
10
+ * `ConnectStatusPresentation` next to the union it describes, and this renders
11
+ * it. A status whose *rendering* carries real logic - a scheduled cancellation
12
+ * outranking a running trial, say - keeps its own component and calls this for
13
+ * the final chip. A map cannot express precedence.
14
+ */
15
+ function ConnectStatusChip({ status, presentation, className, }) {
16
+ return ((0, jsx_runtime_1.jsx)(react_1.Chip, { color: presentation.tone[status], size: "sm", variant: "soft", className: className, children: presentation.label[status] }));
17
+ }
@@ -159,8 +159,8 @@ function checkEnv(root) {
159
159
  status: "fail",
160
160
  title: `${name} set`,
161
161
  detail: declaredEmpty
162
- ? `Declared but empty in .env.local. The value is in your application's Integration tab in Connect.`
163
- : `Not found in .env.local or the environment. Both values are in your application's Integration tab in Connect.`,
162
+ ? `Declared but empty in .env.local. The value is in your application's Connect client section in Connect.`
163
+ : `Not found in .env.local or the environment. Both values are in your application's Connect client section in Connect.`,
164
164
  };
165
165
  });
166
166
  }
@@ -173,7 +173,7 @@ function checkRedirectUri() {
173
173
  return {
174
174
  status: "warn",
175
175
  title: "Redirect URI registered in Connect",
176
- detail: "Not checkable from here. Confirm {your origin}/api/connect/callback is listed as a redirect URI in your application's Integration tab - including the deployed origin, not just http://localhost:3000.",
176
+ detail: "Not checkable from here. Confirm {your origin}/api/connect/callback is listed as a redirect URI in your application's Connect client section - including the deployed origin, not just http://localhost:3000.",
177
177
  };
178
178
  }
179
179
  const MARKER = { pass: " ok ", warn: " note ", fail: " FAIL " };
@@ -39,7 +39,7 @@ const PLANS_PAGE = `"use client";
39
39
  // with the other exports) whenever you want your own copy or layout.
40
40
  export { ConnectPlansPage as default } from "@cosmoops/connect-sdk/client";
41
41
  `;
42
- const ENV_TEMPLATE = `# CosmoOps Connect - values come from your application's Integration tab.
42
+ const ENV_TEMPLATE = `# CosmoOps Connect - values come from your application's Connect client section.
43
43
  CONNECT_API_KEY=
44
44
  CONNECT_CLIENT_ID=
45
45
  `;
@@ -106,7 +106,7 @@ export function runInit(argv = []) {
106
106
  console.log(` ${marker} ${relative(root, target)} - ${scaffold.description}`);
107
107
  }
108
108
  const env = ensureEnv(root);
109
- console.log(` ${env === "skipped" ? "exists " : env === "appended" ? "updated" : "created"} .env.local - fill in the two values from your application's Integration tab`);
109
+ console.log(` ${env === "skipped" ? "exists " : env === "appended" ? "updated" : "created"} .env.local - fill in the two values from your application's Connect client section`);
110
110
  // The two steps a scaffolder must not do for you: editing an existing layout
111
111
  // means parsing and rewriting JSX someone else wrote, and the redirect URI
112
112
  // lives in Connect, not in this repo.
@@ -132,7 +132,7 @@ Three things left, all one-time:
132
132
  Tailwind roots its content scan inside node_modules and skips your app.
133
133
 
134
134
  3. Register {your origin}/api/connect/callback as a redirect URI in your
135
- application's Integration tab in Connect.
135
+ application's Connect client section in Connect.
136
136
 
137
137
  No next.config.ts changes needed - not even transpilePackages: dist/ is
138
138
  already ES2022 with "use client" hoisted, so Turbopack and webpack consume it
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
- import { Ban, AlertTriangle, Clock, CreditCard, ExternalLink, X } from "lucide-react";
4
+ import { Ban, AlertTriangle, Building2, Clock, CreditCard, ExternalLink, User, X } from "lucide-react";
5
5
  import { Button, Card, Skeleton } from "@heroui/react";
6
6
  import { blockingBlockers } from "../../helpers.js";
7
7
  import { useConnect, useConnectEntitlements } from "../provider.js";
@@ -21,19 +21,46 @@ function formatDate(iso) {
21
21
  * `ConnectReadinessGate`/`ConnectReadinessBanner` already use. `ConnectAccessGate`
22
22
  * is a priority ordering over the same data, not a second opinion about it.
23
23
  *
24
- * Priority, first match wins: `suspended` > `no_subscription` >
25
- * `onboarding_pending` > any other blocking code (`blocked`) >
26
- * `payment_retry` (advisory - `children` still render) > `clear`.
24
+ * Priority, first match wins: `suspended` > `audience_mismatch` >
25
+ * `no_subscription` > `onboarding_pending` > any other blocking code
26
+ * (`blocked`) > `payment_retry` (advisory - `children` still render) >
27
+ * `clear`.
28
+ *
29
+ * `audience_mismatch` outranks `no_subscription` deliberately, and the
30
+ * ordering is the whole point of it: a workspace of the wrong account type
31
+ * carries BOTH blockers at once, and showing the subscription one first sent
32
+ * the customer to a plan picker whose subscribe call
33
+ * (`ActivateSubscriptionUseCase`) rejects them for the audience anyway. The
34
+ * account type is the reason; the missing subscription is a consequence.
27
35
  */
28
36
  export function useConnectAccessGate() {
29
37
  const { session, isLoading: sessionLoading } = useConnect();
30
38
  const { data, isLoading: entitlementsLoading } = useConnectEntitlements();
31
- if (sessionLoading || (session?.user && entitlementsLoading)) {
39
+ if (sessionLoading) {
32
40
  return { state: "loading", blocker: null, paymentRetry: null };
33
41
  }
34
42
  if (!session?.user) {
35
43
  return { state: "signed_out", blocker: null, paymentRetry: null };
36
44
  }
45
+ // Connect platform staff pass straight through, and this check has to come
46
+ // *before* the entitlements read below or the gate would hard-block them.
47
+ //
48
+ // Every state this gate can report is a fact about a workspace - suspended,
49
+ // no plan, wrong audience, awaiting approval, payment due. A staff session
50
+ // has no workspace at all (see `SessionAccess.staff`), so it has no subscription
51
+ // either, and `subscription_missing` would send an operator to your plan
52
+ // picker to buy a plan for a workspace that does not exist. "Nothing is
53
+ // blocking this session" is the truthful answer, not a lenient one.
54
+ //
55
+ // The entitlements request is still issued - hooks cannot be called
56
+ // conditionally - and the bridge answers it `403 no_workspace`. That costs
57
+ // one failed request per staff page load, and its result is ignored here.
58
+ if (session.staff) {
59
+ return { state: "clear", blocker: null, paymentRetry: null };
60
+ }
61
+ if (entitlementsLoading) {
62
+ return { state: "loading", blocker: null, paymentRetry: null };
63
+ }
37
64
  if (!data) {
38
65
  // Signed in, not loading, yet nothing came back - a failed read (see
39
66
  // `error` on `useConnectEntitlements`). Fail open rather than hard-block
@@ -46,6 +73,10 @@ export function useConnectAccessGate() {
46
73
  const suspended = blocking.find((b) => b.code === "workspace_suspended");
47
74
  if (suspended)
48
75
  return { state: "suspended", blocker: suspended, paymentRetry: null };
76
+ const audienceMismatch = blocking.find((b) => b.code === "audience_mismatch");
77
+ if (audienceMismatch) {
78
+ return { state: "audience_mismatch", blocker: audienceMismatch, paymentRetry: null };
79
+ }
49
80
  const noSubscription = blocking.find((b) => b.code === "subscription_missing");
50
81
  if (noSubscription)
51
82
  return { state: "no_subscription", blocker: noSubscription, paymentRetry: null };
@@ -105,6 +136,29 @@ export function ConnectAccessGate({ children, fallback, loading, className }) {
105
136
  return (_jsx(_Fragment, { children: fallback ?? _jsx(AccessGateFallback, { state: gate.state, blocker: gate.blocker, className: className }) }));
106
137
  }
107
138
  function AccessGateFallback({ state, blocker, className, }) {
139
+ if (state === "audience_mismatch") {
140
+ // `requiredWorkspaceType` rather than the message, which is documented as
141
+ // human-facing and subject to rewording - the title has to name the right
142
+ // account type even after someone rephrases the sentence below it.
143
+ const required = blocker?.requiredWorkspaceType ?? null;
144
+ return (_jsx(GateCard, { icon: required === "individual" ? User : Building2, iconClassName: "text-muted", title: required === "business"
145
+ ? "Business account required"
146
+ : required === "individual"
147
+ ? "Individual account required"
148
+ : "Not available for this account type",
149
+ // The server names the application in its own message, which reads
150
+ // better than anything composable here; the fallback covers a blocker
151
+ // that arrived without one.
152
+ message: blocker?.message ??
153
+ (required
154
+ ? `This application is only available to ${required} accounts.`
155
+ : "This application isn't available to this account's type."),
156
+ // Deliberately no action, even if one were sent: `Workspace.type` is
157
+ // set at sign-up and no screen changes it, so any button here would
158
+ // only move the dead end somewhere else. Switching means a different
159
+ // account, which is a conversation with support, not a link.
160
+ resolveUrl: null, className: className }));
161
+ }
108
162
  if (state === "no_subscription") {
109
163
  return (_jsx(GateCard, { icon: CreditCard, iconClassName: "text-accent", title: "Choose a plan to continue", message: blocker?.message ?? "This workspace isn't subscribed to this application yet.", actionLabel: "See plans", resolveUrl: blocker?.resolveUrl ?? null, className: className }));
110
164
  }
@@ -114,7 +168,7 @@ function AccessGateFallback({ state, blocker, className, }) {
114
168
  if (state === "onboarding_pending") {
115
169
  return (_jsx(GateCard, { icon: Clock, iconClassName: "text-muted", title: "Waiting on approval", message: blocker?.message ?? "This workspace is waiting on approval before it can use this application.", resolveUrl: null, className: className }));
116
170
  }
117
- // "blocked" - any other blocking code (verification, audience, seats, ...).
171
+ // "blocked" - any other blocking code (verification, seats, ...).
118
172
  // Full fidelity for a code this component doesn't special-case: the same
119
173
  // banner-in-a-card `ConnectReadinessGate` falls back to, rather than
120
174
  // `GateCard`'s single-blocker shape - there can be more than one of these
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The one place a model-supplied value becomes a string.
3
+ *
4
+ * Ask AI's presentation tools send raw values with a `format` tag beside them
5
+ * rather than pre-formatted strings (see `AiValueFormat`), and this is the half
6
+ * that makes that worth doing: an amount renders in the viewer's own locale, a
7
+ * table sorts numerically, and the same figure reads identically on a chart
8
+ * axis, in a tooltip and in a metric tile.
9
+ *
10
+ * Every branch is total. A value that cannot be formatted the way it was
11
+ * tagged - a `date` that is not a date, a `currency` that is not a number -
12
+ * falls back to its plain string rather than throwing inside a render.
13
+ */
14
+ /** Renders in place of a missing value - an em dash, not the word "null". */
15
+ export const AI_EMPTY_VALUE = "—";
16
+ const NUMBER = new Intl.NumberFormat("en-IN");
17
+ const PERCENT = new Intl.NumberFormat("en-IN", { style: "percent", maximumFractionDigits: 1 });
18
+ /**
19
+ * Rupees, not paise - the tool contract says so, because the model is emitting
20
+ * a figure a person would say out loud. `formatPrice` in `plan-pricing.ts` is
21
+ * the paise-denominated twin, for amounts that came out of Connect's billing.
22
+ */
23
+ const CURRENCY = new Intl.NumberFormat("en-IN", {
24
+ style: "currency",
25
+ currency: "INR",
26
+ maximumFractionDigits: 2,
27
+ });
28
+ const DATE = new Intl.DateTimeFormat("en-IN", { day: "numeric", month: "short", year: "numeric" });
29
+ const DATE_TIME = new Intl.DateTimeFormat("en-IN", {
30
+ day: "numeric",
31
+ month: "short",
32
+ year: "numeric",
33
+ hour: "numeric",
34
+ minute: "2-digit",
35
+ });
36
+ function formatBytes(bytes) {
37
+ const units = ["B", "KB", "MB", "GB", "TB"];
38
+ let value = bytes;
39
+ let unit = 0;
40
+ while (value >= 1024 && unit < units.length - 1) {
41
+ value /= 1024;
42
+ unit++;
43
+ }
44
+ return `${value >= 10 || unit === 0 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
45
+ }
46
+ function formatDuration(seconds) {
47
+ if (seconds < 60)
48
+ return `${Math.round(seconds)}s`;
49
+ const minutes = Math.floor(seconds / 60);
50
+ if (minutes < 60)
51
+ return `${minutes}m`;
52
+ const hours = Math.floor(minutes / 60);
53
+ const remainder = minutes % 60;
54
+ if (hours < 24)
55
+ return remainder ? `${hours}h ${remainder}m` : `${hours}h`;
56
+ return `${Math.floor(hours / 24)}d ${hours % 24}h`;
57
+ }
58
+ function asDate(value) {
59
+ if (typeof value !== "string" && typeof value !== "number")
60
+ return null;
61
+ const date = new Date(value);
62
+ return Number.isNaN(date.getTime()) ? null : date;
63
+ }
64
+ export function formatAiValue(value, format = "text") {
65
+ if (value === null || value === undefined || value === "")
66
+ return AI_EMPTY_VALUE;
67
+ switch (format) {
68
+ case "number":
69
+ return typeof value === "number" ? NUMBER.format(value) : String(value);
70
+ case "currency":
71
+ return typeof value === "number" ? CURRENCY.format(value) : String(value);
72
+ case "percent":
73
+ return typeof value === "number" ? PERCENT.format(value) : String(value);
74
+ case "bytes":
75
+ return typeof value === "number" ? formatBytes(value) : String(value);
76
+ case "duration":
77
+ return typeof value === "number" ? formatDuration(value) : String(value);
78
+ case "date": {
79
+ const date = asDate(value);
80
+ return date ? DATE.format(date) : String(value);
81
+ }
82
+ case "datetime": {
83
+ const date = asDate(value);
84
+ return date ? DATE_TIME.format(date) : String(value);
85
+ }
86
+ default:
87
+ return String(value);
88
+ }
89
+ }
90
+ /** Numeric columns read far better right-aligned; everything else stays with the text. */
91
+ export function isNumericAiFormat(format = "text") {
92
+ return (format === "number" ||
93
+ format === "currency" ||
94
+ format === "percent" ||
95
+ format === "bytes" ||
96
+ format === "duration");
97
+ }
98
+ /**
99
+ * A status word to a tone. A small keyword list rather than an enum on the
100
+ * wire, because the model names states from the partner's own domain
101
+ * ("provisioning", "in transit") - and anything unrecognised is neutral, which
102
+ * is the right answer for a word nobody can interpret.
103
+ */
104
+ const POSITIVE = new Set(["active", "paid", "succeeded", "success", "complete", "completed", "approved", "healthy", "live", "enabled", "delivered", "on", "yes"]);
105
+ const WARNING = new Set(["pending", "processing", "trialing", "trial", "grace", "review", "queued", "partial", "draft", "in_transit"]);
106
+ const DANGER = new Set(["failed", "error", "overdue", "suspended", "cancelled", "canceled", "rejected", "expired", "past_due", "blocked", "disabled", "off"]);
107
+ export function aiStatusTone(value) {
108
+ const word = String(value ?? "").trim().toLowerCase().replace(/\s+/g, "_");
109
+ if (POSITIVE.has(word))
110
+ return "positive";
111
+ if (WARNING.has(word))
112
+ return "warning";
113
+ if (DANGER.has(word))
114
+ return "danger";
115
+ return "neutral";
116
+ }
117
+ /** HeroUI semantic tokens only, so these inherit whatever theme the host defined. */
118
+ export const AI_TONE_CHIP = {
119
+ neutral: "bg-surface-tertiary text-foreground",
120
+ positive: "bg-success/15 text-success",
121
+ warning: "bg-warning/20 text-warning",
122
+ danger: "bg-danger/15 text-danger",
123
+ };
124
+ export const AI_TONE_DOT = {
125
+ neutral: "bg-muted",
126
+ positive: "bg-success",
127
+ warning: "bg-warning",
128
+ danger: "bg-danger",
129
+ };