@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,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
+ }
package/dist/theme.css CHANGED
@@ -110,3 +110,90 @@ body {
110
110
  background: var(--background);
111
111
  color: var(--foreground);
112
112
  }
113
+
114
+ /* ---------------------------------------------------------------------------
115
+ * Ask AI: Streamdown token compatibility.
116
+ *
117
+ * `ConnectAskAi` renders markdown with Streamdown, which paints using
118
+ * shadcn/ui's token names. HeroUI defines a different set - four happen to line
119
+ * up already (`--background`, `--foreground`, `--border`, `--radius`), and the
120
+ * rest are aliased here so an answer inherits the platform's own theme instead
121
+ * of rendering with missing backgrounds and borders.
122
+ *
123
+ * This lives in the shipped theme rather than in each partner's CSS because it
124
+ * is a fact about how these two libraries name colours, not a design choice
125
+ * anyone should have to rediscover.
126
+ *
127
+ * Additive - HeroUI uses none of these names - with exactly one real collision,
128
+ * handled below.
129
+ * ------------------------------------------------------------------------ */
130
+ :root,
131
+ .light,
132
+ .default,
133
+ [data-theme="light"],
134
+ [data-theme="default"],
135
+ .dark,
136
+ [data-theme="dark"] {
137
+ --card: var(--surface);
138
+ --card-foreground: var(--surface-foreground);
139
+ --input: var(--border);
140
+ --primary: var(--accent);
141
+ --primary-foreground: var(--accent-foreground);
142
+ }
143
+
144
+ /* ---------------------------------------------------------------------------
145
+ * The collision: `--muted` is a *surface* in shadcn (code-block fills, table
146
+ * header rows) and a *text colour* in HeroUI. Reading it as HeroUI's would
147
+ * paint every code block mid-grey and leave the text on it unreadable.
148
+ *
149
+ * So it is shadowed - but only inside the answer body, and only after the host
150
+ * element has captured HeroUI's value under shadcn's name for it. Splitting it
151
+ * across two elements is what makes this safe: `--muted-foreground` is computed
152
+ * on `.connect-ai-host`, where `--muted` still means HeroUI's text grey, and
153
+ * inherits that resolved colour downward. Both declarations on one element
154
+ * would resolve `var(--muted)` against the new value and lose the original.
155
+ * ------------------------------------------------------------------------ */
156
+ .connect-ai-host {
157
+ --muted-foreground: var(--muted);
158
+ }
159
+
160
+ .connect-ai-host .connect-ai-body {
161
+ --muted: var(--surface-secondary);
162
+ }
163
+
164
+ /* ---------------------------------------------------------------------------
165
+ * The Ask AI "thinking" shimmer - a highlight sweeping through the label while
166
+ * the model works, rather than a spinner.
167
+ *
168
+ * A gradient clipped to the glyphs, so the motion is *in* the word instead of
169
+ * beside it. This is the one state where the panel has nothing at all to show:
170
+ * retrieval and the model's first thought take a second or two, and a static
171
+ * "Thinking" reads as frozen.
172
+ * ------------------------------------------------------------------------ */
173
+ .connect-ai-shimmer {
174
+ background: linear-gradient(100deg, var(--muted) 32%, var(--accent) 48%, var(--muted) 64%);
175
+ background-size: 220% 100%;
176
+ -webkit-background-clip: text;
177
+ background-clip: text;
178
+ color: transparent;
179
+ animation: connect-ai-shimmer 1.7s linear infinite;
180
+ }
181
+
182
+ @keyframes connect-ai-shimmer {
183
+ from {
184
+ background-position: 180% 0;
185
+ }
186
+ to {
187
+ background-position: -40% 0;
188
+ }
189
+ }
190
+
191
+ /* Motion is the whole affordance here, so with it suppressed the label has to
192
+ go back to being a legible static word rather than a transparent one. */
193
+ @media (prefers-reduced-motion: reduce) {
194
+ .connect-ai-shimmer {
195
+ background: none;
196
+ color: var(--muted);
197
+ animation: none;
198
+ }
199
+ }
@@ -5,6 +5,12 @@ export type ConnectAccessGateState = "loading"
5
5
  | "signed_out"
6
6
  /** `workspace_suspended` - a platform admin took this workspace offline. Hard block, no dismiss. */
7
7
  | "suspended"
8
+ /**
9
+ * `audience_mismatch` - this application serves individual accounts and the
10
+ * workspace is a business, or the reverse. Hard block with nothing to
11
+ * offer: `Workspace.type` is fixed at sign-up.
12
+ */
13
+ | "audience_mismatch"
8
14
  /** `subscription_missing` - nothing to subscribe to yet. Hard block. */
9
15
  | "no_subscription"
10
16
  /** `onboarding_approval_pending` - waiting on a reviewer. Hard block, no action to offer. */
@@ -33,9 +39,17 @@ export interface ConnectAccessGateResult {
33
39
  * `ConnectReadinessGate`/`ConnectReadinessBanner` already use. `ConnectAccessGate`
34
40
  * is a priority ordering over the same data, not a second opinion about it.
35
41
  *
36
- * Priority, first match wins: `suspended` > `no_subscription` >
37
- * `onboarding_pending` > any other blocking code (`blocked`) >
38
- * `payment_retry` (advisory - `children` still render) > `clear`.
42
+ * Priority, first match wins: `suspended` > `audience_mismatch` >
43
+ * `no_subscription` > `onboarding_pending` > any other blocking code
44
+ * (`blocked`) > `payment_retry` (advisory - `children` still render) >
45
+ * `clear`.
46
+ *
47
+ * `audience_mismatch` outranks `no_subscription` deliberately, and the
48
+ * ordering is the whole point of it: a workspace of the wrong account type
49
+ * carries BOTH blockers at once, and showing the subscription one first sent
50
+ * the customer to a plan picker whose subscribe call
51
+ * (`ActivateSubscriptionUseCase`) rejects them for the audience anyway. The
52
+ * account type is the reason; the missing subscription is a consequence.
39
53
  */
40
54
  export declare function useConnectAccessGate(): ConnectAccessGateResult;
41
55
  export interface ConnectAccessGateProps {
@@ -0,0 +1,24 @@
1
+ import type { AiValue, AiValueFormat } from "../../types.js";
2
+ /**
3
+ * The one place a model-supplied value becomes a string.
4
+ *
5
+ * Ask AI's presentation tools send raw values with a `format` tag beside them
6
+ * rather than pre-formatted strings (see `AiValueFormat`), and this is the half
7
+ * that makes that worth doing: an amount renders in the viewer's own locale, a
8
+ * table sorts numerically, and the same figure reads identically on a chart
9
+ * axis, in a tooltip and in a metric tile.
10
+ *
11
+ * Every branch is total. A value that cannot be formatted the way it was
12
+ * tagged - a `date` that is not a date, a `currency` that is not a number -
13
+ * falls back to its plain string rather than throwing inside a render.
14
+ */
15
+ /** Renders in place of a missing value - an em dash, not the word "null". */
16
+ export declare const AI_EMPTY_VALUE = "\u2014";
17
+ export declare function formatAiValue(value: AiValue, format?: AiValueFormat): string;
18
+ /** Numeric columns read far better right-aligned; everything else stays with the text. */
19
+ export declare function isNumericAiFormat(format?: AiValueFormat): boolean;
20
+ export type AiStatusTone = "neutral" | "positive" | "warning" | "danger";
21
+ export declare function aiStatusTone(value: AiValue): AiStatusTone;
22
+ /** HeroUI semantic tokens only, so these inherit whatever theme the host defined. */
23
+ export declare const AI_TONE_CHIP: Record<AiStatusTone, string>;
24
+ export declare const AI_TONE_DOT: Record<AiStatusTone, string>;
@@ -0,0 +1,42 @@
1
+ import { type ReactNode } from "react";
2
+ import type { AiChartInput, AiDetailsInput, AiMetricsInput, AiSourcesInput, AiStepsInput, AiTableInput, AiTimelineInput } from "../../types.js";
3
+ /**
4
+ * `showTable`. Sorting and CSV are what a table is *for* - once an answer is
5
+ * tabular the next thing anyone wants is to reorder it or take it elsewhere,
6
+ * and a markdown table can do neither. Both work off the raw values, so
7
+ * "₹12,500" sorts as 12500 and the export carries numbers a spreadsheet can add.
8
+ */
9
+ export declare function AiTable({ title, columns, rows, caption }: Readonly<AiTableInput>): import("react").JSX.Element;
10
+ /**
11
+ * `showChart`. Cartesian kinds are drawn as SVG against a fixed viewBox and
12
+ * scaled by the browser, which is why there is no resize observer here: the
13
+ * chart is resolution-independent, so "responsive" costs nothing at runtime.
14
+ *
15
+ * `pie` is deliberately *not* a pie. In a 400px column a pie plus its legend
16
+ * is smaller than the labels it needs, and reading angles is worse than
17
+ * reading lengths at any size - so proportions render as a labelled bar list,
18
+ * which answers the same question and stays legible.
19
+ */
20
+ export declare function AiChart({ title, kind, xLabel, yLabel, valueFormat, series, caption }: Readonly<AiChartInput>): import("react").JSX.Element;
21
+ /**
22
+ * `showMetrics`. A delta is coloured by `deltaMeaning`, not by its sign -
23
+ * churn rising is bad and revenue rising is good, and painting both green
24
+ * because the arrow points up is exactly the confidently-wrong signal a
25
+ * dashboard must never send. The arrow still shows direction, so meaning and
26
+ * direction stay two separate channels.
27
+ */
28
+ export declare function AiMetrics({ title, metrics }: Readonly<AiMetricsInput>): import("react").JSX.Element;
29
+ /** `showTimeline`: dated events, oldest first, on a single rail. */
30
+ export declare function AiTimeline({ title, events }: Readonly<AiTimelineInput>): import("react").JSX.Element;
31
+ /** `showDetails`: one entity's fields, as a definition list rather than a one-row table. */
32
+ export declare function AiDetails({ title, fields }: Readonly<AiDetailsInput>): import("react").JSX.Element;
33
+ /** `showSteps`: an ordered procedure. `renderDetail` is how the panel feeds each step back through its markdown renderer. */
34
+ export declare function AiSteps({ title, steps, renderDetail, }: Readonly<AiStepsInput & {
35
+ renderDetail?: (markdown: string) => ReactNode;
36
+ }>): import("react").JSX.Element;
37
+ /**
38
+ * `showSources`: what a grounded answer actually rested on. The point of a
39
+ * citation is that it can be opened, so a source with a `uri` is a link and one
40
+ * without is still a named row - never a marker with nothing behind it.
41
+ */
42
+ export declare function AiSources({ sources }: Readonly<AiSourcesInput>): import("react").JSX.Element;