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