@stapel/workspaces-react 0.17.0 → 0.19.1

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 (123) hide show
  1. package/CHANGELOG.md +218 -0
  2. package/README.md +39 -8
  3. package/dist/api/generated/schema.d.ts +187 -60
  4. package/dist/api/generated/schema.d.ts.map +1 -1
  5. package/dist/default/ActiveWorkspace.d.ts +43 -0
  6. package/dist/default/ActiveWorkspace.d.ts.map +1 -0
  7. package/dist/default/ActiveWorkspace.js +33 -0
  8. package/dist/default/ActiveWorkspace.js.map +1 -0
  9. package/dist/default/AuditTrailPane.d.ts +12 -0
  10. package/dist/default/AuditTrailPane.d.ts.map +1 -0
  11. package/dist/default/AuditTrailPane.js +154 -0
  12. package/dist/default/AuditTrailPane.js.map +1 -0
  13. package/dist/default/InvitationsPane.d.ts +19 -0
  14. package/dist/default/InvitationsPane.d.ts.map +1 -0
  15. package/dist/default/InvitationsPane.js +217 -0
  16. package/dist/default/InvitationsPane.js.map +1 -0
  17. package/dist/default/InviteAcceptPage.d.ts +7 -0
  18. package/dist/default/InviteAcceptPage.d.ts.map +1 -1
  19. package/dist/default/InviteAcceptPage.js +74 -38
  20. package/dist/default/InviteAcceptPage.js.map +1 -1
  21. package/dist/default/MembersManager.d.ts +12 -4
  22. package/dist/default/MembersManager.d.ts.map +1 -1
  23. package/dist/default/MembersManager.js +320 -84
  24. package/dist/default/MembersManager.js.map +1 -1
  25. package/dist/default/RoleSelectField.d.ts +64 -0
  26. package/dist/default/RoleSelectField.d.ts.map +1 -0
  27. package/dist/default/RoleSelectField.js +78 -0
  28. package/dist/default/RoleSelectField.js.map +1 -0
  29. package/dist/default/WorkspaceSettings.d.ts +7 -1
  30. package/dist/default/WorkspaceSettings.d.ts.map +1 -1
  31. package/dist/default/WorkspaceSettings.js +203 -45
  32. package/dist/default/WorkspaceSettings.js.map +1 -1
  33. package/dist/default/WorkspacesPage.d.ts +13 -0
  34. package/dist/default/WorkspacesPage.d.ts.map +1 -0
  35. package/dist/default/WorkspacesPage.js +109 -0
  36. package/dist/default/WorkspacesPage.js.map +1 -0
  37. package/dist/default/index.d.ts +28 -7
  38. package/dist/default/index.d.ts.map +1 -1
  39. package/dist/default/index.js +24 -7
  40. package/dist/default/index.js.map +1 -1
  41. package/dist/default/parts.d.ts +104 -0
  42. package/dist/default/parts.d.ts.map +1 -0
  43. package/dist/default/parts.js +143 -0
  44. package/dist/default/parts.js.map +1 -0
  45. package/dist/headless/Members.d.ts +59 -14
  46. package/dist/headless/Members.d.ts.map +1 -1
  47. package/dist/headless/Members.js +35 -10
  48. package/dist/headless/Members.js.map +1 -1
  49. package/dist/headless/RoleSelect.d.ts +9 -3
  50. package/dist/headless/RoleSelect.d.ts.map +1 -1
  51. package/dist/headless/RoleSelect.js +3 -2
  52. package/dist/headless/RoleSelect.js.map +1 -1
  53. package/dist/i18n/es.d.ts +11 -12
  54. package/dist/i18n/es.d.ts.map +1 -1
  55. package/dist/i18n/es.js +267 -14
  56. package/dist/i18n/es.js.map +1 -1
  57. package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -1
  58. package/dist/i18n/generated/errors.es.gen.js +5 -0
  59. package/dist/i18n/generated/errors.es.gen.js.map +1 -1
  60. package/dist/i18n/generated/errors.gen.d.ts +30 -0
  61. package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
  62. package/dist/i18n/generated/errors.gen.js +15 -0
  63. package/dist/i18n/generated/errors.gen.js.map +1 -1
  64. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -1
  65. package/dist/i18n/generated/errors.ru.gen.js +5 -0
  66. package/dist/i18n/generated/errors.ru.gen.js.map +1 -1
  67. package/dist/i18n/keys.d.ts +209 -1
  68. package/dist/i18n/keys.d.ts.map +1 -1
  69. package/dist/i18n/keys.js +410 -4
  70. package/dist/i18n/keys.js.map +1 -1
  71. package/dist/i18n/ru.d.ts +7 -2
  72. package/dist/i18n/ru.d.ts.map +1 -1
  73. package/dist/i18n/ru.js +209 -4
  74. package/dist/i18n/ru.js.map +1 -1
  75. package/dist/index.d.ts +1 -1
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +1 -1
  78. package/dist/index.js.map +1 -1
  79. package/dist/model/format.d.ts +56 -0
  80. package/dist/model/format.d.ts.map +1 -0
  81. package/dist/model/format.js +125 -0
  82. package/dist/model/format.js.map +1 -0
  83. package/dist/model/selection.d.ts +15 -0
  84. package/dist/model/selection.d.ts.map +1 -1
  85. package/dist/model/selection.js +18 -1
  86. package/dist/model/selection.js.map +1 -1
  87. package/dist/nav/manifest.d.ts +45 -0
  88. package/dist/nav/manifest.d.ts.map +1 -0
  89. package/dist/nav/manifest.js +77 -0
  90. package/dist/nav/manifest.js.map +1 -0
  91. package/llms.txt +11 -10
  92. package/manifest.json +281 -69
  93. package/package.json +18 -12
  94. package/src/analytics/generated/events.json +1 -1
  95. package/src/api/generated/schema.ts +187 -60
  96. package/src/default/ActiveWorkspace.tsx +97 -0
  97. package/src/default/AuditTrailPane.tsx +309 -0
  98. package/src/default/InvitationsPane.tsx +501 -0
  99. package/src/default/InviteAcceptPage.tsx +187 -90
  100. package/src/default/MembersManager.tsx +832 -222
  101. package/src/default/RoleSelectField.tsx +187 -0
  102. package/src/default/WorkspaceSettings.tsx +540 -125
  103. package/src/default/WorkspacesPage.tsx +394 -0
  104. package/src/default/index.ts +28 -7
  105. package/src/default/parts.tsx +315 -0
  106. package/src/headless/Members.tsx +84 -16
  107. package/src/headless/RoleSelect.tsx +12 -5
  108. package/src/i18n/es.ts +326 -14
  109. package/src/i18n/generated/errors.es.gen.ts +5 -0
  110. package/src/i18n/generated/errors.gen.ts +15 -0
  111. package/src/i18n/generated/errors.json +37 -0
  112. package/src/i18n/generated/errors.ru.gen.ts +5 -0
  113. package/src/i18n/keys.ts +462 -5
  114. package/src/i18n/ru.ts +259 -5
  115. package/src/index.ts +1 -0
  116. package/src/model/format.ts +158 -0
  117. package/src/model/selection.tsx +19 -1
  118. package/src/nav/manifest.ts +120 -0
  119. package/dist/default/ErrorAlert.d.ts +0 -8
  120. package/dist/default/ErrorAlert.d.ts.map +0 -1
  121. package/dist/default/ErrorAlert.js +0 -30
  122. package/dist/default/ErrorAlert.js.map +0 -1
  123. package/src/default/ErrorAlert.tsx +0 -47
@@ -0,0 +1,394 @@
1
+ /**
2
+ * `<WorkspacesPage/>` — the default skin for {@link WorkspaceList}: the
3
+ * roster of workspaces a person belongs to, the door into each one, the
4
+ * "open this one by default" choice, and the create control.
5
+ *
6
+ * §54 said every headless primitive gets an AntD default; this was the one
7
+ * missing, and it is the one every app needs on day one — the audit found
8
+ * `useCanCreateWorkspace` existing only to feed a create button nobody had
9
+ * drawn.
10
+ *
11
+ * Three answers come from the SERVER and are never re-derived here:
12
+ *
13
+ * - `can_create_workspace` — the instance's `WORKSPACE_CREATE_POLICY`, over
14
+ * the same helper the POST gate uses, so a drawn button always opens.
15
+ * - `preferred_workspace_id` — the person's explicit home, which the
16
+ * instance default documents itself as yielding to. Never `workspaces[0]`
17
+ * (that guess was tracker #239) and never `last_accessed_at`, which is
18
+ * telemetry.
19
+ * - `is_guest` — someone here on a link rather than a membership. An empty
20
+ * list means something different to them, and the screen says which.
21
+ */
22
+ import { useId, useState } from "react";
23
+ import type { ReactElement } from "react";
24
+ import { Button, Card, Flex, Input, Typography, theme as antdTheme } from "antd";
25
+ import {
26
+ actionAvailable,
27
+ actionBlocked,
28
+ loadStateFromQuery,
29
+ requireLoaded,
30
+ useT,
31
+ useTPlural,
32
+ } from "@stapel/core";
33
+ import {
34
+ EmptyState,
35
+ ErrorAlert,
36
+ GatedButton,
37
+ LoadList,
38
+ SkinDialog,
39
+ SkinTheme,
40
+ useBlockedButtonClassName,
41
+ } from "@stapel/tokens-antd/skin";
42
+ import { spacing } from "@stapel/tokens";
43
+ import { WorkspaceList } from "../headless/WorkspaceList.js";
44
+ import type { WorkspaceListBag } from "../headless/WorkspaceList.js";
45
+ import { useWorkspaces } from "../model/queries.js";
46
+ import {
47
+ useClearPreferredWorkspace,
48
+ useSetPreferredWorkspace,
49
+ } from "../model/mutations.js";
50
+ import type { Workspace } from "../api/types.js";
51
+ import { WORKSPACES_I18N_KEYS } from "../i18n/keys.js";
52
+ import { LoadFailure, Muted, StatusTag, SCREEN_STACK } from "./parts.js";
53
+ import { RoleLabel } from "./RoleSelectField.js";
54
+
55
+ export interface WorkspacesPageProps {
56
+ /**
57
+ * Open a workspace. The pair owns no router (there are several, and a
58
+ * library that picks one picks it for every host), so the door is the
59
+ * host's: omit it and no "Open" control is drawn, because a button that
60
+ * goes nowhere is worse than no button.
61
+ */
62
+ onOpen?(workspace: Workspace): void;
63
+ }
64
+
65
+ export function WorkspacesPage(props: WorkspacesPageProps): ReactElement {
66
+ return (
67
+ <SkinTheme surface="base" data-testid="workspaces-page">
68
+ <WorkspaceList>
69
+ {(bag) => (
70
+ <PageBody
71
+ bag={bag}
72
+ {...(props.onOpen !== undefined ? { onOpen: props.onOpen } : {})}
73
+ />
74
+ )}
75
+ </WorkspaceList>
76
+ </SkinTheme>
77
+ );
78
+ }
79
+
80
+ function PageBody(props: {
81
+ readonly bag: WorkspaceListBag;
82
+ readonly onOpen?: (workspace: Workspace) => void;
83
+ }): ReactElement {
84
+ const t = useT();
85
+ const tPlural = useTPlural();
86
+ const { bag } = props;
87
+ const listQuery = useWorkspaces();
88
+ const [creating, setCreating] = useState(false);
89
+ const list = listQuery.data ?? null;
90
+ const failureId = useId();
91
+
92
+ // The create verdict is a property of the LIST response, so it is blocked
93
+ // while the list is loading — with core's own sentence — and by the policy
94
+ // when the list arrived and says no.
95
+ //
96
+ // A FAILED read is deliberately not routed through the gate. `requireLoaded`
97
+ // would hand the button its own copy of the outage ("We could not load what
98
+ // this needs…" plus `HTTP 503`) directly above the alert that states the
99
+ // same outage with the retry — the same bad news twice, in two wordings,
100
+ // with two different recoveries. The read has exactly one failure surface on
101
+ // this screen, and the button points at it.
102
+ const listState = loadStateFromQuery(listQuery);
103
+ const listFailed = listState.status === "failed";
104
+ const blockedLook = useBlockedButtonClassName();
105
+ const createGate = requireLoaded(listState, (loaded) =>
106
+ loaded.can_create_workspace === true
107
+ ? actionAvailable()
108
+ : actionBlocked(WORKSPACES_I18N_KEYS.listCreateBlockedPolicy)
109
+ );
110
+
111
+ const rows = bag.state.status === "ready" ? bag.state.data : [];
112
+
113
+ return (
114
+ <div style={SCREEN_STACK}>
115
+ <Card>
116
+ <Flex justify="space-between" align="flex-start" gap={spacing["4"]} wrap>
117
+ <Flex vertical gap={spacing["1"]}>
118
+ <Typography.Title level={4} style={{ marginTop: 0, marginBottom: 0 }}>
119
+ {t(WORKSPACES_I18N_KEYS.pageTitle)}
120
+ </Typography.Title>
121
+ <Typography.Text type="secondary">
122
+ {t(WORKSPACES_I18N_KEYS.pageSubtitle)}
123
+ </Typography.Text>
124
+ {bag.state.status === "ready" && (
125
+ <Muted testId="workspaces-count">
126
+ {tPlural(WORKSPACES_I18N_KEYS.listCount, { count: rows.length })}
127
+ </Muted>
128
+ )}
129
+ </Flex>
130
+ {/* The one primary on the screen. */}
131
+ {listFailed ? (
132
+ <Button
133
+ type="primary"
134
+ // `aria-disabled` + antd's own unavailable paint, never html
135
+ // `disabled`: the button has to stay focusable for the
136
+ // `aria-describedby` below to be announced at all, and it
137
+ // carries no `onClick`, so there is nothing to suppress.
138
+ aria-disabled
139
+ className={blockedLook}
140
+ aria-describedby={failureId}
141
+ data-disabled-reason="the roster read failed; the alert below says so once and carries the retry"
142
+ data-analytics="none"
143
+ data-analytics-reason="local-ui-open-create-dialog"
144
+ data-testid="workspaces-create-open"
145
+ >
146
+ {t(WORKSPACES_I18N_KEYS.listCreate)}
147
+ </Button>
148
+ ) : (
149
+ <GatedButton
150
+ gate={createGate}
151
+ type="primary"
152
+ onClick={() => setCreating(true)}
153
+ testId="workspaces-create-open"
154
+ data-analytics="none"
155
+ data-analytics-reason="local-ui-open-create-dialog"
156
+ >
157
+ {t(WORKSPACES_I18N_KEYS.listCreate)}
158
+ </GatedButton>
159
+ )}
160
+ </Flex>
161
+
162
+ {list?.is_guest === true && (
163
+ <div style={{ marginTop: spacing["3"] }}>
164
+ <Muted testId="workspaces-guest">
165
+ {t(WORKSPACES_I18N_KEYS.listGuestNotice)}
166
+ </Muted>
167
+ </div>
168
+ )}
169
+
170
+ <ErrorAlert
171
+ thrown={bag.createError}
172
+ style={{ marginTop: spacing["3"] }}
173
+ testId="workspaces-create-error"
174
+ />
175
+
176
+ <div style={{ marginTop: spacing["4"] }}>
177
+ <LoadList
178
+ state={bag.state}
179
+ testId="workspaces-list"
180
+ onRetry={bag.refetch}
181
+ failed={(error) => (
182
+ <div id={failureId}>
183
+ <LoadFailure
184
+ error={error}
185
+ onRetry={bag.refetch}
186
+ testId="workspaces-list-failed"
187
+ />
188
+ </div>
189
+ )}
190
+ empty={
191
+ <EmptyState
192
+ title={t(WORKSPACES_I18N_KEYS.listEmpty)}
193
+ testId="workspaces-list-empty"
194
+ // The RESTRICTION is stated once, beside the switched-off
195
+ // control above; this line is the other half — what the person
196
+ // can actually do about it. Saying "this installation does not
197
+ // hand out workspaces" here as well was the same fact twice in
198
+ // two wordings on one 390px screen.
199
+ hint={
200
+ createGate.available === true
201
+ ? t(WORKSPACES_I18N_KEYS.listEmptyHint)
202
+ : t(WORKSPACES_I18N_KEYS.listInstanceClosed)
203
+ }
204
+ {...(createGate.available === true
205
+ ? {
206
+ action: (
207
+ <Button
208
+ type="primary"
209
+ onClick={() => setCreating(true)}
210
+ data-analytics="none"
211
+ data-analytics-reason="local-ui-open-create-dialog"
212
+ >
213
+ {t(WORKSPACES_I18N_KEYS.listCreate)}
214
+ </Button>
215
+ ),
216
+ }
217
+ : {})}
218
+ />
219
+ }
220
+ >
221
+ {(workspaces) => (
222
+ <div role="list" data-testid="workspaces-rows">
223
+ {workspaces.map((workspace) => (
224
+ <WorkspaceRow
225
+ key={workspace.id}
226
+ workspace={workspace}
227
+ isPreferred={list?.preferred_workspace_id === workspace.id}
228
+ {...(props.onOpen !== undefined ? { onOpen: props.onOpen } : {})}
229
+ />
230
+ ))}
231
+ </div>
232
+ )}
233
+ </LoadList>
234
+ </div>
235
+ </Card>
236
+
237
+ <CreateDialog
238
+ open={creating}
239
+ onClose={() => setCreating(false)}
240
+ isCreating={bag.isCreating}
241
+ onCreate={(name) => {
242
+ bag.create({ name });
243
+ setCreating(false);
244
+ }}
245
+ />
246
+ </div>
247
+ );
248
+ }
249
+
250
+ function WorkspaceRow(props: {
251
+ readonly workspace: Workspace;
252
+ readonly isPreferred: boolean;
253
+ readonly onOpen?: (workspace: Workspace) => void;
254
+ }): ReactElement {
255
+ const t = useT();
256
+ const tPlural = useTPlural();
257
+ const { token } = antdTheme.useToken();
258
+ const setPreferred = useSetPreferredWorkspace();
259
+ const clearPreferred = useClearPreferredWorkspace();
260
+ const { workspace } = props;
261
+ const owner = workspace.owner_display_name?.trim();
262
+
263
+ return (
264
+ <div
265
+ role="listitem"
266
+ data-testid={`workspace-row-${workspace.id}`}
267
+ style={{
268
+ display: "flex",
269
+ flexWrap: "wrap",
270
+ alignItems: "center",
271
+ justifyContent: "space-between",
272
+ gap: spacing["3"],
273
+ paddingTop: spacing["3"],
274
+ paddingBottom: spacing["3"],
275
+ borderBottom: `1px solid ${token.colorSplit}`,
276
+ }}
277
+ >
278
+ <Flex vertical gap={spacing["1"]} style={{ minWidth: 0 }}>
279
+ <Flex gap={spacing["2"]} align="center" wrap>
280
+ <Typography.Text strong>{workspace.name}</Typography.Text>
281
+ {props.isPreferred && (
282
+ <StatusTag tone="info" testId={`workspace-preferred-${workspace.id}`}>
283
+ {t(WORKSPACES_I18N_KEYS.listPreferredTag)}
284
+ </StatusTag>
285
+ )}
286
+ </Flex>
287
+ {/* A workspace NAME stopped identifying one — a person can belong to
288
+ several spaces all called "Personal", one per company. The owner is
289
+ the second line for exactly that reason. */}
290
+ {owner !== undefined && owner !== "" && (
291
+ <Muted>{t(WORKSPACES_I18N_KEYS.listOwnerLine, { owner })}</Muted>
292
+ )}
293
+ <Muted>
294
+ {tPlural(WORKSPACES_I18N_KEYS.listMemberCount, { count: workspace.member_count })}
295
+ {workspace.my_role !== null && (
296
+ <>
297
+ {" · "}
298
+ {t(WORKSPACES_I18N_KEYS.listYourRole)}:{" "}
299
+ <RoleLabel role={workspace.my_role} />
300
+ </>
301
+ )}
302
+ </Muted>
303
+ </Flex>
304
+ <Flex gap={spacing["2"]} align="center" wrap>
305
+ {props.isPreferred ? (
306
+ <Button
307
+ type="link"
308
+ size="small"
309
+ loading={clearPreferred.isPending}
310
+ onClick={() => clearPreferred.mutate()}
311
+ data-analytics="none"
312
+ data-analytics-reason="business action — host app wraps with its own tracked()"
313
+ data-testid={`workspace-clear-home-${workspace.id}`}
314
+ >
315
+ {t(WORKSPACES_I18N_KEYS.listClearPreferred)}
316
+ </Button>
317
+ ) : (
318
+ <Button
319
+ type="link"
320
+ size="small"
321
+ loading={setPreferred.isPending}
322
+ onClick={() => setPreferred.mutate({ workspace_id: workspace.id })}
323
+ data-analytics="none"
324
+ data-analytics-reason="business action — host app wraps with its own tracked()"
325
+ data-testid={`workspace-set-home-${workspace.id}`}
326
+ >
327
+ {t(WORKSPACES_I18N_KEYS.listSetPreferred)}
328
+ </Button>
329
+ )}
330
+ {props.onOpen !== undefined && (
331
+ <Button
332
+ onClick={() => props.onOpen?.(workspace)}
333
+ data-analytics="none"
334
+ data-analytics-reason="navigation — the host owns the router"
335
+ data-testid={`workspace-open-${workspace.id}`}
336
+ >
337
+ {t(WORKSPACES_I18N_KEYS.listOpen)}
338
+ </Button>
339
+ )}
340
+ </Flex>
341
+ </div>
342
+ );
343
+ }
344
+
345
+ function CreateDialog(props: {
346
+ readonly open: boolean;
347
+ readonly onClose: () => void;
348
+ readonly isCreating: boolean;
349
+ readonly onCreate: (name: string) => void;
350
+ }): ReactElement {
351
+ const t = useT();
352
+ const [name, setName] = useState("");
353
+ const gate =
354
+ name.trim() === ""
355
+ ? actionBlocked(WORKSPACES_I18N_KEYS.listCreateBlockedNoName)
356
+ : actionAvailable();
357
+
358
+ return (
359
+ <SkinDialog
360
+ open={props.open}
361
+ onClose={props.onClose}
362
+ title={t(WORKSPACES_I18N_KEYS.listCreateDialogTitle)}
363
+ dismissLabel={t(WORKSPACES_I18N_KEYS.dialogClose)}
364
+ data-testid="workspaces-create-dialog"
365
+ footer={
366
+ <GatedButton
367
+ gate={gate}
368
+ type="primary"
369
+ loading={props.isCreating}
370
+ onClick={() => {
371
+ props.onCreate(name.trim());
372
+ setName("");
373
+ }}
374
+ testId="workspaces-create-submit"
375
+ data-analytics="none"
376
+ data-analytics-reason="business action — host app wraps with its own tracked()"
377
+ >
378
+ {t(WORKSPACES_I18N_KEYS.listCreateSubmit)}
379
+ </GatedButton>
380
+ }
381
+ >
382
+ <Flex vertical gap={spacing["2"]}>
383
+ <Typography.Text>{t(WORKSPACES_I18N_KEYS.listCreateNameLabel)}</Typography.Text>
384
+ <Input
385
+ value={name}
386
+ onChange={(event) => setName(event.target.value)}
387
+ placeholder={t(WORKSPACES_I18N_KEYS.listCreateNamePlaceholder)}
388
+ aria-label={t(WORKSPACES_I18N_KEYS.listCreateNameLabel)}
389
+ data-testid="workspaces-create-name"
390
+ />
391
+ </Flex>
392
+ </SkinDialog>
393
+ );
394
+ }
@@ -1,19 +1,40 @@
1
1
  /**
2
- * `@stapel/workspaces-react/default` — the pilot default skin for this
3
- * pair's settings surfaces (mirrors auth-react's `/default` split, §54): a
4
- * separate entry point so consumers who bring their own visuals never pull
5
- * `antd` into their bundle; importing this subpath is the opt-in.
2
+ * `@stapel/workspaces-react/default` — this pair's DEFAULT SKIN: the antd
3
+ * screens a host mounts to have a working workspace product, not a set of
4
+ * examples (§83, §54). A separate entry point so consumers who bring their
5
+ * own visuals never pull `antd` into their bundle; importing this subpath is
6
+ * the opt-in.
7
+ *
8
+ * Every screen self-themes through `<SkinTheme>` from
9
+ * `@stapel/tokens-antd/skin`, so it is correct inside a dark document with no
10
+ * host wiring, and every one of them needs this pair's `<WorkspacesProvider>`
11
+ * plus core's `<I18nProvider>` above it.
6
12
  *
7
13
  * ```tsx
8
- * import { WorkspaceSettings, MembersManager } from "@stapel/workspaces-react/default";
9
- * // under this pair's <WorkspacesProvider> + core <I18nProvider>:
10
- * <WorkspaceSettings workspaceId={id} />
14
+ * import {
15
+ * WorkspacesPage, WorkspaceSettings, MembersManager,
16
+ * InvitationsPane, AuditTrailPane, InviteAcceptPage,
17
+ * } from "@stapel/workspaces-react/default";
18
+ *
19
+ * <WorkspacesPage onOpen={(ws) => navigate(`/w/${ws.slug}`)} />
20
+ * <WorkspaceSettings workspaceId={id} onDeleted={() => navigate("/")} />
11
21
  * <MembersManager workspaceId={id} />
22
+ * <InvitationsPane workspaceId={id} />
23
+ * <AuditTrailPane workspaceId={id} />
24
+ * <InviteAcceptPage token={token} sessionEmail={email} />
12
25
  * ```
13
26
  */
27
+ export { WorkspacesPage } from "./WorkspacesPage.js";
28
+ export type { WorkspacesPageProps } from "./WorkspacesPage.js";
14
29
  export { WorkspaceSettings } from "./WorkspaceSettings.js";
15
30
  export type { WorkspaceSettingsProps } from "./WorkspaceSettings.js";
16
31
  export { MembersManager } from "./MembersManager.js";
17
32
  export type { MembersManagerProps } from "./MembersManager.js";
33
+ export { InvitationsPane } from "./InvitationsPane.js";
34
+ export type { InvitationsPaneProps } from "./InvitationsPane.js";
35
+ export { AuditTrailPane } from "./AuditTrailPane.js";
36
+ export type { AuditTrailPaneProps } from "./AuditTrailPane.js";
37
+ export { RoleSelectField } from "./RoleSelectField.js";
38
+ export type { RoleSelectFieldProps } from "./RoleSelectField.js";
18
39
  export { InviteAcceptPage } from "./InviteAcceptPage.js";
19
40
  export type { InviteAcceptPageProps } from "./InviteAcceptPage.js";