@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.
- package/CHANGELOG.md +497 -0
- package/README.md +111 -41
- package/dist/cjs/cli/doctor.js +3 -3
- package/dist/cjs/cli/init.js +3 -3
- package/dist/cjs/client/components/access-gate.js +20 -1
- package/dist/cjs/client/components/app-shell.js +75 -41
- package/dist/cjs/client/components/apps-grid.js +3 -2
- package/dist/cjs/client/components/ask-ai.js +20 -1
- package/dist/cjs/client/components/confirm-dialog.js +40 -0
- package/dist/cjs/client/components/cursor-pagination.js +22 -0
- package/dist/cjs/client/components/filter-tabs.js +24 -0
- package/dist/cjs/client/components/grievance-notice.js +30 -0
- package/dist/cjs/client/components/link-button.js +17 -10
- package/dist/cjs/client/components/logo.js +4 -3
- package/dist/cjs/client/components/nav.js +41 -0
- package/dist/cjs/client/components/plan-pricing.js +27 -0
- package/dist/cjs/client/components/plans.js +1 -1
- package/dist/cjs/client/components/platform-gate.js +48 -0
- package/dist/cjs/client/components/shell-context.js +149 -0
- package/dist/cjs/client/components/side-nav.js +49 -23
- package/dist/cjs/client/components/sidebar.js +127 -0
- package/dist/cjs/client/components/subscribe-dialog.js +2 -2
- package/dist/cjs/client/components/subscription.js +1 -1
- package/dist/cjs/client/components/user-menu.js +96 -37
- package/dist/cjs/client/components/workspace-switcher.js +100 -0
- package/dist/cjs/client/index.js +69 -2
- package/dist/cjs/client/provider.js +57 -2
- package/dist/cjs/client/use-checkout.js +9 -5
- package/dist/cjs/errors.js +1 -1
- package/dist/cjs/index.js +11 -1
- package/dist/cjs/next/index.js +2 -2
- package/dist/cjs/server/client.js +25 -2
- package/dist/cjs/server/handler.js +196 -18
- package/dist/cjs/server/http.js +6 -2
- package/dist/cjs/server/resources/acting-as.js +16 -0
- package/dist/cjs/server/resources/ai.js +15 -2
- package/dist/cjs/server/resources/data.js +374 -0
- package/dist/cjs/server/resources/identity.js +7 -3
- package/dist/cjs/server/resources/kv.js +84 -0
- package/dist/cjs/server/resources/platform.js +43 -0
- package/dist/cjs/server/resources/sessions.js +1 -1
- package/dist/cjs/server/resources/workspaces.js +74 -0
- package/dist/cjs/store.js +152 -0
- package/dist/cjs/ui/empty-state.js +28 -0
- package/dist/cjs/ui/index.js +44 -0
- package/dist/cjs/ui/page-header.js +27 -0
- package/dist/cjs/ui/page-shell.js +44 -0
- package/dist/cjs/ui/section.js +20 -0
- package/dist/cjs/ui/setting-row.js +14 -0
- package/dist/cjs/ui/status-chip.js +17 -0
- package/dist/esm/cli/doctor.js +3 -3
- package/dist/esm/cli/init.js +3 -3
- package/dist/esm/client/components/access-gate.js +20 -1
- package/dist/esm/client/components/app-shell.js +81 -47
- package/dist/esm/client/components/apps-grid.js +3 -2
- package/dist/esm/client/components/ask-ai.js +20 -1
- package/dist/esm/client/components/confirm-dialog.js +37 -0
- package/dist/esm/client/components/cursor-pagination.js +19 -0
- package/dist/esm/client/components/filter-tabs.js +21 -0
- package/dist/esm/client/components/grievance-notice.js +27 -0
- package/dist/esm/client/components/link-button.js +17 -10
- package/dist/esm/client/components/logo.js +4 -3
- package/dist/esm/client/components/nav.js +40 -0
- package/dist/esm/client/components/plan-pricing.js +25 -0
- package/dist/esm/client/components/plans.js +2 -2
- package/dist/esm/client/components/platform-gate.js +45 -0
- package/dist/esm/client/components/shell-context.js +141 -0
- package/dist/esm/client/components/side-nav.js +48 -21
- package/dist/esm/client/components/sidebar.js +120 -0
- package/dist/esm/client/components/subscribe-dialog.js +3 -3
- package/dist/esm/client/components/subscription.js +2 -2
- package/dist/esm/client/components/user-menu.js +95 -38
- package/dist/esm/client/components/workspace-switcher.js +97 -0
- package/dist/esm/client/index.js +46 -4
- package/dist/esm/client/provider.js +53 -2
- package/dist/esm/client/use-checkout.js +9 -5
- package/dist/esm/errors.js +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/next/index.js +2 -2
- package/dist/esm/server/client.js +25 -2
- package/dist/esm/server/handler.js +196 -18
- package/dist/esm/server/http.js +6 -2
- package/dist/esm/server/resources/acting-as.js +13 -0
- package/dist/esm/server/resources/ai.js +15 -2
- package/dist/esm/server/resources/data.js +369 -0
- package/dist/esm/server/resources/identity.js +7 -3
- package/dist/esm/server/resources/kv.js +80 -0
- package/dist/esm/server/resources/platform.js +39 -0
- package/dist/esm/server/resources/sessions.js +1 -1
- package/dist/esm/server/resources/workspaces.js +70 -0
- package/dist/esm/store.js +141 -0
- package/dist/esm/ui/empty-state.js +25 -0
- package/dist/esm/ui/index.js +34 -0
- package/dist/esm/ui/page-header.js +24 -0
- package/dist/esm/ui/page-shell.js +41 -0
- package/dist/esm/ui/section.js +17 -0
- package/dist/esm/ui/setting-row.js +11 -0
- package/dist/esm/ui/status-chip.js +14 -0
- package/dist/types/client/components/app-shell.d.ts +65 -27
- package/dist/types/client/components/apps-grid.d.ts +11 -2
- package/dist/types/client/components/confirm-dialog.d.ts +34 -0
- package/dist/types/client/components/cursor-pagination.d.ts +27 -0
- package/dist/types/client/components/filter-tabs.d.ts +26 -0
- package/dist/types/client/components/grievance-notice.d.ts +31 -0
- package/dist/types/client/components/link-button.d.ts +32 -9
- package/dist/types/client/components/logo.d.ts +18 -2
- package/dist/types/client/components/nav.d.ts +24 -0
- package/dist/types/client/components/plan-pricing.d.ts +10 -0
- package/dist/types/client/components/platform-gate.d.ts +53 -0
- package/dist/types/client/components/shell-context.d.ts +116 -0
- package/dist/types/client/components/side-nav.d.ts +27 -13
- package/dist/types/client/components/sidebar.d.ts +95 -0
- package/dist/types/client/components/user-menu.d.ts +68 -22
- package/dist/types/client/components/workspace-switcher.d.ts +49 -0
- package/dist/types/client/index.d.ts +27 -5
- package/dist/types/client/provider.d.ts +109 -6
- package/dist/types/client/use-checkout.d.ts +8 -4
- package/dist/types/errors.d.ts +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/next/index.d.ts +1 -1
- package/dist/types/server/client.d.ts +25 -2
- package/dist/types/server/http.d.ts +8 -2
- package/dist/types/server/index.d.ts +4 -0
- package/dist/types/server/resources/acting-as.d.ts +11 -0
- package/dist/types/server/resources/ai.d.ts +9 -1
- package/dist/types/server/resources/data.d.ts +258 -0
- package/dist/types/server/resources/kv.d.ts +63 -0
- package/dist/types/server/resources/platform.d.ts +32 -0
- package/dist/types/server/resources/workspaces.d.ts +55 -0
- package/dist/types/store.d.ts +96 -0
- package/dist/types/types.d.ts +319 -15
- package/dist/types/ui/empty-state.d.ts +34 -0
- package/dist/types/ui/index.d.ts +40 -0
- package/dist/types/ui/page-header.d.ts +34 -0
- package/dist/types/ui/page-shell.d.ts +39 -0
- package/dist/types/ui/section.d.ts +26 -0
- package/dist/types/ui/setting-row.d.ts +18 -0
- package/dist/types/ui/status-chip.d.ts +36 -0
- 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
|
-
/**
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
54
|
+
/** The app launcher, in the sidebar footer. On by default. */
|
|
38
55
|
showAppsGrid?: boolean;
|
|
39
|
-
/** "Continue with Connect" / "Create account" in the
|
|
56
|
+
/** "Continue with Connect" / "Create account" in the sidebar footer when signed out. On by default. */
|
|
40
57
|
showAuthButtons?: boolean;
|
|
41
|
-
/** Passed to `
|
|
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
|
|
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:
|
|
87
|
-
*
|
|
88
|
-
*
|
|
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
|
|
92
|
-
* one product. Everything that should be identical across platforms
|
|
93
|
-
*
|
|
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: "
|
|
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
|
|
124
|
-
*
|
|
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,
|
|
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;
|