@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.
- package/CHANGELOG.md +218 -0
- package/README.md +39 -8
- package/dist/api/generated/schema.d.ts +187 -60
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/default/ActiveWorkspace.d.ts +43 -0
- package/dist/default/ActiveWorkspace.d.ts.map +1 -0
- package/dist/default/ActiveWorkspace.js +33 -0
- package/dist/default/ActiveWorkspace.js.map +1 -0
- package/dist/default/AuditTrailPane.d.ts +12 -0
- package/dist/default/AuditTrailPane.d.ts.map +1 -0
- package/dist/default/AuditTrailPane.js +154 -0
- package/dist/default/AuditTrailPane.js.map +1 -0
- package/dist/default/InvitationsPane.d.ts +19 -0
- package/dist/default/InvitationsPane.d.ts.map +1 -0
- package/dist/default/InvitationsPane.js +217 -0
- package/dist/default/InvitationsPane.js.map +1 -0
- package/dist/default/InviteAcceptPage.d.ts +7 -0
- package/dist/default/InviteAcceptPage.d.ts.map +1 -1
- package/dist/default/InviteAcceptPage.js +74 -38
- package/dist/default/InviteAcceptPage.js.map +1 -1
- package/dist/default/MembersManager.d.ts +12 -4
- package/dist/default/MembersManager.d.ts.map +1 -1
- package/dist/default/MembersManager.js +320 -84
- package/dist/default/MembersManager.js.map +1 -1
- package/dist/default/RoleSelectField.d.ts +64 -0
- package/dist/default/RoleSelectField.d.ts.map +1 -0
- package/dist/default/RoleSelectField.js +78 -0
- package/dist/default/RoleSelectField.js.map +1 -0
- package/dist/default/WorkspaceSettings.d.ts +7 -1
- package/dist/default/WorkspaceSettings.d.ts.map +1 -1
- package/dist/default/WorkspaceSettings.js +203 -45
- package/dist/default/WorkspaceSettings.js.map +1 -1
- package/dist/default/WorkspacesPage.d.ts +13 -0
- package/dist/default/WorkspacesPage.d.ts.map +1 -0
- package/dist/default/WorkspacesPage.js +109 -0
- package/dist/default/WorkspacesPage.js.map +1 -0
- package/dist/default/index.d.ts +28 -7
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +24 -7
- package/dist/default/index.js.map +1 -1
- package/dist/default/parts.d.ts +104 -0
- package/dist/default/parts.d.ts.map +1 -0
- package/dist/default/parts.js +143 -0
- package/dist/default/parts.js.map +1 -0
- package/dist/headless/Members.d.ts +59 -14
- package/dist/headless/Members.d.ts.map +1 -1
- package/dist/headless/Members.js +35 -10
- package/dist/headless/Members.js.map +1 -1
- package/dist/headless/RoleSelect.d.ts +9 -3
- package/dist/headless/RoleSelect.d.ts.map +1 -1
- package/dist/headless/RoleSelect.js +3 -2
- package/dist/headless/RoleSelect.js.map +1 -1
- package/dist/i18n/es.d.ts +11 -12
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +267 -14
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -1
- package/dist/i18n/generated/errors.es.gen.js +5 -0
- package/dist/i18n/generated/errors.es.gen.js.map +1 -1
- package/dist/i18n/generated/errors.gen.d.ts +30 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
- package/dist/i18n/generated/errors.gen.js +15 -0
- package/dist/i18n/generated/errors.gen.js.map +1 -1
- package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -1
- package/dist/i18n/generated/errors.ru.gen.js +5 -0
- package/dist/i18n/generated/errors.ru.gen.js.map +1 -1
- package/dist/i18n/keys.d.ts +209 -1
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +410 -4
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts +7 -2
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +209 -4
- package/dist/i18n/ru.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/model/format.d.ts +56 -0
- package/dist/model/format.d.ts.map +1 -0
- package/dist/model/format.js +125 -0
- package/dist/model/format.js.map +1 -0
- package/dist/model/selection.d.ts +15 -0
- package/dist/model/selection.d.ts.map +1 -1
- package/dist/model/selection.js +18 -1
- package/dist/model/selection.js.map +1 -1
- package/dist/nav/manifest.d.ts +45 -0
- package/dist/nav/manifest.d.ts.map +1 -0
- package/dist/nav/manifest.js +77 -0
- package/dist/nav/manifest.js.map +1 -0
- package/llms.txt +11 -10
- package/manifest.json +281 -69
- package/package.json +18 -12
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +187 -60
- package/src/default/ActiveWorkspace.tsx +97 -0
- package/src/default/AuditTrailPane.tsx +309 -0
- package/src/default/InvitationsPane.tsx +501 -0
- package/src/default/InviteAcceptPage.tsx +187 -90
- package/src/default/MembersManager.tsx +832 -222
- package/src/default/RoleSelectField.tsx +187 -0
- package/src/default/WorkspaceSettings.tsx +540 -125
- package/src/default/WorkspacesPage.tsx +394 -0
- package/src/default/index.ts +28 -7
- package/src/default/parts.tsx +315 -0
- package/src/headless/Members.tsx +84 -16
- package/src/headless/RoleSelect.tsx +12 -5
- package/src/i18n/es.ts +326 -14
- package/src/i18n/generated/errors.es.gen.ts +5 -0
- package/src/i18n/generated/errors.gen.ts +15 -0
- package/src/i18n/generated/errors.json +37 -0
- package/src/i18n/generated/errors.ru.gen.ts +5 -0
- package/src/i18n/keys.ts +462 -5
- package/src/i18n/ru.ts +259 -5
- package/src/index.ts +1 -0
- package/src/model/format.ts +158 -0
- package/src/model/selection.tsx +19 -1
- package/src/nav/manifest.ts +120 -0
- package/dist/default/ErrorAlert.d.ts +0 -8
- package/dist/default/ErrorAlert.d.ts.map +0 -1
- package/dist/default/ErrorAlert.js +0 -30
- package/dist/default/ErrorAlert.js.map +0 -1
- package/src/default/ErrorAlert.tsx +0 -47
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,223 @@
|
|
|
1
1
|
# @stapel/workspaces-react
|
|
2
2
|
|
|
3
|
+
## 0.19.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f79bdc3: tokens-antd: a gated control is semantically off and interactively ALIVE — it can be tapped, focused, and can say why it will not do the thing
|
|
8
|
+
|
|
9
|
+
`GatedControl` handed callers `bind.disabled` and its own JSDoc told them to spread it straight onto the control. That produced an html-`disabled` element, which fires no events in any browser: it cannot be clicked, cannot take focus, cannot be described to a screen reader that never reaches it, and cannot carry the one gesture that mattered — the tap that should open the sign-in door standing behind the gate. Every gated control across the ~20 pairs using it was inert, and the wrong instruction was half the defect: the docs taught the shape that broke it.
|
|
10
|
+
|
|
11
|
+
Measured on a live deployment: an anonymous visitor taps the favourite heart and nothing happens at all — no sentence, no tooltip, no door (walker defects D45/D72).
|
|
12
|
+
|
|
13
|
+
**The corrected contract.** While the gate is shut a control is now `aria-disabled="true"` and NOT html-disabled, so it stays focusable and keeps receiving events. The ACTION is suppressed by `GatedControl` itself, in a capture-phase wrapper (`display: contents`, so no pair's layout moves by a pixel): the caller's `onClick`, keyboard activation, typing, IME input, paste and drop are swallowed before the control sees them. Callers write their handlers exactly as if the gate did not exist. The activation comes back as the new `onBlockedActivate`, which is where a pair opens its door. The reason stays where it was — visible text wired by `aria-describedby` — and where a `PaneGate` pools it into one footnote, the gesture now brings a `role="status"` copy of the sentence back to the control it belongs to. A blocked `GatedButton` keeps antd's exact disabled paint (its own `-disabled` class, which sets no `pointer-events`), so nothing about any screen looks different.
|
|
14
|
+
|
|
15
|
+
`GatedControlProps.whenBlocked` holds the two deliberate opt-outs, neither of them the default:
|
|
16
|
+
|
|
17
|
+
- `"inert"` — html `disabled`, for the rare control that must be switched off at the browser level. `attributes-react`'s catalogue lock is the one place in the fleet that asks for it, and now says so.
|
|
18
|
+
- `"annotate"` — the control stays fully usable and only gains the sentence, for a gate that judges the VALUE rather than refusing the person: `calendar-react`'s slot-length field must stay editable, because editing it is how the reason goes away, and `search-react`'s sort must still pick the options that are not the blocked one.
|
|
19
|
+
|
|
20
|
+
`useBlockedButtonClassName()` is exported for render-prop call sites that paint their own button and want the same unavailable look rather than a second grey.
|
|
21
|
+
|
|
22
|
+
**⚠️ The readiness-signal hazard, and its cure.** `element.disabled` is now permanently `false` on every gated control in the fleet. Any test using it as a readiness signal — `await waitFor(() => expect(save.disabled).toBe(false))`, meaning "wait until this is allowed" — returns instantly and mis-times SILENTLY: every assertion after it reads an unseeded component, and the failure looks like broken product logic rather than a gate that had not opened. One pair's suite went green → 21 failures across unrelated files on exactly this. Wait on the stamp instead, which is what such a wait was always asking:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
await waitFor(() =>
|
|
26
|
+
expect(
|
|
27
|
+
screen.getByTestId("save-gate").getAttribute("data-stapel-gated")
|
|
28
|
+
).toBe("available")
|
|
29
|
+
);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`data-stapel-gated="available" | "blocked"` is on the wrapper of every gated control in all three modes (`GatedButton` names it `<testId>-gate`). For a point assertion on one element, read `aria-disabled`. Never `disabled`.
|
|
33
|
+
|
|
34
|
+
**ChoiceChips** carried the same defect on its own chips and is fixed the same way: a chip at the cap is `aria-disabled` and focusable, and the tap is refused in the handler, so the row's sentence reaches a keyboard.
|
|
35
|
+
|
|
36
|
+
**The consumers.** Every `GatedButton` call site (64 imports across 20 pairs) is fixed with no code change — the correction is in the substrate. The render-prop call sites that consumed the binding field-by-field now spread it whole: `billing-react`'s auto-recharge switch, `calendar-react`'s RSVP buttons, `moderation-react`'s sanction checkbox, `notifications-react`'s push switch, `attributes-react`'s at-max add button. `tasks-react`'s assignee picker is a host slot rendering its own control out of reach of the suppression, so it is handed a plain verdict on purpose. `workspaces-react` had two hand-rolled gates that never went through `GatedControl` at all — a row-action column and the create button on a failed roster read — and both now use the same anatomy.
|
|
37
|
+
|
|
38
|
+
## 0.19.0
|
|
39
|
+
|
|
40
|
+
### Minor Changes
|
|
41
|
+
|
|
42
|
+
- 57bd738: Visual pass 3: delete the legacy harness stories, and stop the screens saying
|
|
43
|
+
the same thing twice.
|
|
44
|
+
|
|
45
|
+
**The seven harness demos are gone** (`workspace-list`, `members--default`,
|
|
46
|
+
`accept-invitation`, `capability-gate`, `invite-accept-flow`,
|
|
47
|
+
`role-select--registry`, `workspaces-provider`). They shipped a debug card —
|
|
48
|
+
`state.step` chips, `1 workspace(s)`, "Grant exchanged", `members.invite
|
|
49
|
+
allowed` — beside the real skins that replaced them, so half of what this
|
|
50
|
+
package showed was the harness. The headless primitives they stood for are now
|
|
51
|
+
covered by the skin demo that renders each one (`covers:`), and `demo/_harness`
|
|
52
|
+
is the provider frame and nothing else. **Breaking for anyone who deep-links a
|
|
53
|
+
viewer story id.**
|
|
54
|
+
|
|
55
|
+
Screen fixes, each one a repetition or a contradiction:
|
|
56
|
+
|
|
57
|
+
- **Invitations.** A terminal row printed its refusal once per control — the
|
|
58
|
+
same sentence six times on one phone screen. `RowActions` states it once per
|
|
59
|
+
row, as a footnote spanning the row, with every switched-off control's
|
|
60
|
+
`aria-describedby` pointing at it.
|
|
61
|
+
- **Workspaces page.** A failed read said so twice, in two wordings, with two
|
|
62
|
+
recoveries (the create button's gate carried its own copy plus `HTTP 503`
|
|
63
|
+
above the alert that already had the retry). One alert now, the control
|
|
64
|
+
points at it, and the retry sits under the alert instead of squeezing its
|
|
65
|
+
text into ~110px at 390px. The empty state no longer repeats the restriction
|
|
66
|
+
the disabled control states.
|
|
67
|
+
- **Workspace settings.** "Require two-factor authentication" read ON while the
|
|
68
|
+
line under it said two-factor was not required here — the note derived from
|
|
69
|
+
the absence of an enforcement status instead of from the policy. Both now
|
|
70
|
+
read the same value, and "no check has run yet" is its own sentence.
|
|
71
|
+
- **Invitation page.** A dead expired link was drawn in antd's _info_ blue with
|
|
72
|
+
nothing to do next; it is the warning tone, names the next step, and takes an
|
|
73
|
+
optional `onExit` for the host's way out.
|
|
74
|
+
- **Members.** The refusals on the viewer's own row widened the action column
|
|
75
|
+
until that row wrapped to the phone layout in the middle of a desktop table.
|
|
76
|
+
One row geometry per breakpoint now. The empty roster has one "Invite", not
|
|
77
|
+
two, and no search field over nobody.
|
|
78
|
+
- **Roles.** "It is not a workspace without roles." was not English. An empty
|
|
79
|
+
registry no longer borrows the "we could not load" sentence.
|
|
80
|
+
- **Membership history.** `Role: admin` was the raw slug; it goes through the
|
|
81
|
+
same registry labels as the picker. Events are grouped under their day, so a
|
|
82
|
+
burst of three no longer prints one timestamp three times.
|
|
83
|
+
|
|
84
|
+
Additive API: `InviteAcceptPageProps.onExit`, `WorkspaceFormat.time()`,
|
|
85
|
+
`useRoleLabel()` (a role label with no `GET /roles` behind it — the public
|
|
86
|
+
invitation page cannot make that call).
|
|
87
|
+
|
|
88
|
+
## 0.18.0
|
|
89
|
+
|
|
90
|
+
### Minor Changes
|
|
91
|
+
|
|
92
|
+
- 80617e9: The pair becomes the product: the delete button stops lying, the MFA policy gets a screen, and the invitation console exists
|
|
93
|
+
|
|
94
|
+
**The refusal a screen used to promise.** `<WorkspaceSettings/>` drew "Delete
|
|
95
|
+
workspace" from `my_role === "owner"` — the exact derivation stapel-workspaces
|
|
96
|
+
0.26.0 added `WorkspaceResponse.can_delete` + `delete_blocked_reason` to
|
|
97
|
+
replace, because an owner of the instance's default workspace or of a personal
|
|
98
|
+
one gets a 409. The control now reads the server's verdict through
|
|
99
|
+
`GatedControl`, and prints the server's own refusal CODE as a sentence beside
|
|
100
|
+
it — translatable at last, because the generated error bundle was five codes
|
|
101
|
+
behind the backend (`error.409.workspace_is_instance_default`,
|
|
102
|
+
`workspace_is_personal`, `error.429.invitation_grant_pending`,
|
|
103
|
+
`error.503.billing_unavailable`, `error.503.mandate_unavailable`). The test
|
|
104
|
+
that cemented the old rule now asserts the new one. Name and security editing
|
|
105
|
+
are gated on the CAPABILITIES the server granted (`workspace.update`,
|
|
106
|
+
`workspace.security.manage`), falling back to the pre-0.6 owner rule only when
|
|
107
|
+
a backend sends no `my_capabilities` at all.
|
|
108
|
+
|
|
109
|
+
**Four screens that were hooks with no pixels.** `<WorkspacesPage/>` (the §54
|
|
110
|
+
default skin `WorkspaceList` never had: roster, create gated on
|
|
111
|
+
`can_create_workspace`, preferred-workspace choice, guest and closed-instance
|
|
112
|
+
copy), `<InvitationsPane/>` (list / revoke / resend / rename — four API methods
|
|
113
|
+
and zero pixels until now, each control gated on the invitation state the
|
|
114
|
+
endpoint would refuse), `<AuditTrailPane/>` (the membership history, action
|
|
115
|
+
vocabulary as sentences, timestamps as "3 days ago (23 Sept 2026, 09:00)"), and
|
|
116
|
+
`<RoleSelectField/>` (a real `<Select>` with an accessible name and the rank as
|
|
117
|
+
a caption, where the story used to show five bullet points).
|
|
118
|
+
|
|
119
|
+
**MFA enforcement, visible.** `mfa_enforcement` (state, coverage, unverified
|
|
120
|
+
members, last error) is drawn beside the `require_mfa` switch that
|
|
121
|
+
`useUpdateSecuritySettings` now actually has a consumer for, and
|
|
122
|
+
`MemberResponse.mfa_compliant` is a per-member tag with three states — true,
|
|
123
|
+
false, and "nobody has asked yet".
|
|
124
|
+
|
|
125
|
+
**Members reach page two.** `<Members>` takes `params` and exposes `page`
|
|
126
|
+
(anchor cursors + `has_next`/`has_prev`); the roster ships a pager and a
|
|
127
|
+
search box. `rosterComplete` now also requires `has_prev === false`, so the
|
|
128
|
+
last-owner claim is not made from the last page of a longer roster.
|
|
129
|
+
|
|
130
|
+
**Substrate migration.** Local `src/default/ErrorAlert.tsx` deleted; every
|
|
131
|
+
screen self-themes through `<SkinTheme>`, confirms are `SkinConfirm` (no
|
|
132
|
+
`Popconfirm`), load arms are `LoadList`/`LoadBoundary`, blocked controls are
|
|
133
|
+
`GatedButton`/`GatedControl`, and the unfilled `renderLoginPanel` slot renders
|
|
134
|
+
a `SlotPlaceholder` instead of a hole. Doctrine lint: 99 warnings → 0.
|
|
135
|
+
|
|
136
|
+
**i18n.** `es` went from 1 translated UI key to complete; `ru` stays complete
|
|
137
|
+
over a bundle that roughly tripled. Counts render through CLDR plurals
|
|
138
|
+
(`useTPlural`) instead of `1 workspace(s)`, and dates through a new
|
|
139
|
+
`useWorkspaceFormat()` in the model layer instead of raw ISO.
|
|
140
|
+
|
|
141
|
+
BREAKING (pre-1.0, hence minor): `MembersBag` gained `page`/`rename`/
|
|
142
|
+
`isRenaming`, `RoleSelect`'s `labelFor` now title-cases an unlabelled registry
|
|
143
|
+
role (`secretary` → `Secretary`) instead of returning the raw token, and
|
|
144
|
+
`src/default/ErrorAlert.tsx` is gone — import `ErrorAlert` from
|
|
145
|
+
`@stapel/tokens-antd/skin`.
|
|
146
|
+
|
|
147
|
+
- 308e3d6: `is_self` closes two lying controls, the four admin screens get doors, and every default-skin screen is finally photographed
|
|
148
|
+
|
|
149
|
+
**The row the roster could not identify.** stapel-workspaces 0.30.0 derives
|
|
150
|
+
`MemberResponse.is_self` server-side, and the pair now reads it off the
|
|
151
|
+
generated schema instead of a defensive cast. It gates TWO controls, not one.
|
|
152
|
+
"Remove" was already asking. "Reset password" was not — and it is the worse
|
|
153
|
+
case: `MemberPasswordResetView` refuses the caller's own row with the
|
|
154
|
+
byte-identical 404 it gives for a stranger ("Yourself is not in the set this
|
|
155
|
+
endpoint acts on"), so an ungated button reads the backend's correct refusal
|
|
156
|
+
as "this member has been removed". Both are switched off through `GatedButton`
|
|
157
|
+
with the reason beside them, in en/ru/es. A backend that sends no `is_self`
|
|
158
|
+
still claims nothing: the absence reads as "the server did not say".
|
|
159
|
+
|
|
160
|
+
**The administrative password reset now exists on the glass.**
|
|
161
|
+
`useResetMemberPassword` had no consumer at all — the endpoint (#110) was
|
|
162
|
+
unreachable from any shipped screen. `<MembersManager/>` grows the control and
|
|
163
|
+
the dialog around it, and the dialog states the three things this operation is:
|
|
164
|
+
the step-up is ANNOUNCED before the click (the capability is declared `high`,
|
|
165
|
+
so `requires_verification(scope="sensitive")` will demand one), the generated
|
|
166
|
+
password is labelled as the one-shot credential it is and leaves the screen
|
|
167
|
+
when the dialog closes (the mutation is reset), and `notified: false` is said
|
|
168
|
+
out loud — it means the account had no channel to be told on, which makes the
|
|
169
|
+
admin the only person who can tell them.
|
|
170
|
+
|
|
171
|
+
**Four finished screens stop being undeclared.** `<WorkspaceSettings/>`,
|
|
172
|
+
`<MembersManager/>`, `<InvitationsPane/>` and `<AuditTrailPane/>` are in the
|
|
173
|
+
nav manifest, on paths relative to the account section, and their `workspaceId`
|
|
174
|
+
prop became OPTIONAL. The architecture answer the pair was waiting for: the
|
|
175
|
+
active workspace is RUNTIME state — the same state the container writes when a
|
|
176
|
+
person switches — not a path param of a settings URL. So a nav-mounted screen
|
|
177
|
+
reads it from the selection seam (`useOptionalWorkspaceSelection`, new export)
|
|
178
|
+
and a screen with no active workspace renders a designed "choose a workspace"
|
|
179
|
+
(and "you are not in a workspace yet" for a person who belongs to none) rather
|
|
180
|
+
than a blank page or a throw from a provider a shell forgot to wire.
|
|
181
|
+
|
|
182
|
+
**The showcase stops showing the test bench.** Seven default-skin demos, one
|
|
183
|
+
per `/default` export, each with a phone variant and variants seeded at
|
|
184
|
+
distinct steps — the roster with the viewer's own row refused, the settings
|
|
185
|
+
screen an owner may not delete, the terminal invitations, the audit line
|
|
186
|
+
nobody performed, the role field with no registry to read, the invitation page
|
|
187
|
+
on the wrong account. `gen:demos` goes 0/7 → 7/7 skin covered. A render test
|
|
188
|
+
per new surface at 390 and 1024, in light and dark, plus the `is_self` and
|
|
189
|
+
chooser cases: 164 → 187 tests.
|
|
190
|
+
|
|
191
|
+
Peer floors raised to the substrate the pair actually imports:
|
|
192
|
+
`@stapel/core >=0.18.0 <1.0.0` (`useTPlural`, `STAPEL_UI_KEYS`) and
|
|
193
|
+
`@stapel/tokens-antd >=0.6.0` (the skin surface).
|
|
194
|
+
|
|
195
|
+
- 95e8eec: The invite dialog is a bottom sheet on a phone, and the members table stops
|
|
196
|
+
offering removals the backend will refuse.
|
|
197
|
+
|
|
198
|
+
`MembersManager`'s `Modal` renders through `@stapel/tokens-antd/skin`'s
|
|
199
|
+
`SkinDialog` — this package now declares `@stapel/tokens-antd` as an optional
|
|
200
|
+
peer, like every other antd-skinned pair. The table gained
|
|
201
|
+
`scroll={{ x: true }}`.
|
|
202
|
+
|
|
203
|
+
Remove was rendered identically on every row, including the last owner's, which
|
|
204
|
+
the backend enforces against. When the roster is COMPLETE (`has_next` is false
|
|
205
|
+
— now surfaced on the bag as `rosterComplete`) "exactly one row holds owner" is
|
|
206
|
+
a fact, so that row's Remove is disabled with the reason printed beside it. On a
|
|
207
|
+
paginated roster nothing is claimed: a count of a page is not a count of the
|
|
208
|
+
roster, and gating on it would refuse a removal the backend would allow.
|
|
209
|
+
|
|
210
|
+
The caller's OWN row is still not gated, and deliberately: `MemberResponse`
|
|
211
|
+
carries no `is_self`, and the pair has no caller identity to compare `user_id`
|
|
212
|
+
against (core's session exposes a status, the mandate axis a role — neither is
|
|
213
|
+
"who am I"). Guessing would grey out somebody else's row. The backend needs one
|
|
214
|
+
additive field: `is_self` on `MemberResponse`, or the caller's `user_id` on the
|
|
215
|
+
members page envelope.
|
|
216
|
+
|
|
217
|
+
When the role registry read fails, the per-row role `Select` no longer renders
|
|
218
|
+
enabled over an empty option list; the role still reads as text and the invite
|
|
219
|
+
dialog states the outage instead of offering a picker with nothing in it.
|
|
220
|
+
|
|
3
221
|
## 0.17.0
|
|
4
222
|
|
|
5
223
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -51,6 +51,38 @@ per-module override — `clients={{ workspaces: runtime.client }}` — and nest
|
|
|
51
51
|
providers (`StapelConfigProvider` + `QueryClientProvider` + `I18nProvider`)
|
|
52
52
|
remain exported for bespoke composition.
|
|
53
53
|
|
|
54
|
+
## The default skin (`/default`)
|
|
55
|
+
|
|
56
|
+
The pair ships the screens, not only the hooks (§83): seven antd surfaces
|
|
57
|
+
behind a separate entry point, so a host that brings its own visuals never
|
|
58
|
+
pulls `antd` into its bundle.
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import {
|
|
62
|
+
WorkspacesPage, WorkspaceSettings, MembersManager,
|
|
63
|
+
InvitationsPane, AuditTrailPane, RoleSelectField, InviteAcceptPage,
|
|
64
|
+
} from "@stapel/workspaces-react/default";
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Each one self-themes (light/dark from the live document), is a bottom sheet on
|
|
68
|
+
a phone and a modal above it, and states every refusal the backend would give
|
|
69
|
+
instead of offering a control that leads to one.
|
|
70
|
+
|
|
71
|
+
**Which workspace a screen is about.** `WorkspaceSettings`, `MembersManager`,
|
|
72
|
+
`InvitationsPane` and `AuditTrailPane` take an OPTIONAL `workspaceId`. Pass it
|
|
73
|
+
and they use it. Omit it — which is how the nav manifest mounts them, because
|
|
74
|
+
the nav contract routes a screen and does not hand it an ambient scope — and
|
|
75
|
+
they read the ACTIVE workspace from the runtime selection
|
|
76
|
+
(`<WorkspaceSelectionProvider>`, the same seam `switchTo` writes). With no
|
|
77
|
+
active workspace they render a designed "choose a workspace" state; they never
|
|
78
|
+
render blank and never throw at a host that has not wired the provider.
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
<WorkspaceSelectionProvider urlWorkspaceId={params.get("workspace")}>
|
|
82
|
+
<MembersManager /> {/* the workspace comes from the selection */}
|
|
83
|
+
</WorkspaceSelectionProvider>
|
|
84
|
+
```
|
|
85
|
+
|
|
54
86
|
## Layers
|
|
55
87
|
|
|
56
88
|
```
|
|
@@ -100,13 +132,13 @@ Backend error texts are generated from stapel-workspaces's
|
|
|
100
132
|
pair's UI keys carry hand-written ru copy. Register your own bundle AFTER the
|
|
101
133
|
pair's to override any key — registration order is override priority.
|
|
102
134
|
|
|
103
|
-
### Spanish locale (opt-in subpath
|
|
135
|
+
### Spanish locale (opt-in subpath)
|
|
104
136
|
|
|
105
|
-
The `es` bundle ships as its own subpath on the same terms as `ru`,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Spanish texts, so a
|
|
109
|
-
English
|
|
137
|
+
The `es` bundle ships as its own subpath on the same terms as `ru`, and it is
|
|
138
|
+
complete on both halves: the generated backend error texts AND hand-written
|
|
139
|
+
Spanish for every UI key the pair owns. `registerWorkspacesI18nEs` registers
|
|
140
|
+
the en floor UNDER the Spanish texts, so a key added tomorrow degrades to
|
|
141
|
+
English rather than to a raw key.
|
|
110
142
|
|
|
111
143
|
```tsx
|
|
112
144
|
import { registerWorkspacesI18nEs } from "@stapel/workspaces-react/i18n/es";
|
|
@@ -119,8 +151,7 @@ await i18n.setLocale("es"); // live switch; untranslated UI keys read English
|
|
|
119
151
|
Error texts are generated from stapel-workspaces's `translations/errors.es.json`
|
|
120
152
|
catalog (`pnpm gen:errors`, drift-gated) and are complete over the error
|
|
121
153
|
registry by construction. The coverage boundary is asserted in
|
|
122
|
-
`test/i18nEs.test.ts
|
|
123
|
-
additively — this subpath and `workspacesI18nBundleEs` keep their names.
|
|
154
|
+
`test/i18nEs.test.ts`, which asserts parity with the en bundle key for key.
|
|
124
155
|
|
|
125
156
|
## Guardrails
|
|
126
157
|
|