portal-mojo 0.2.8 → 0.2.10
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/README.md +59 -11
- package/docs/DetailView.md +3 -2
- package/docs/README.md +6 -2
- package/docs/account.md +4 -3
- package/docs/admin-bouncer.md +3 -3
- package/docs/admin-credentials.md +4 -3
- package/docs/admin-devices-geoip.md +1 -1
- package/docs/admin-dns-certificates.md +2 -2
- package/docs/admin-dns-records.md +1 -1
- package/docs/admin-edge.md +210 -0
- package/docs/admin-identity-directories-sources.json +2 -2
- package/docs/admin-identity-directories.md +9 -0
- package/docs/admin-identity-users.md +2 -2
- package/docs/admin-jobs.md +4 -4
- package/docs/admin-media-rendering.md +142 -0
- package/docs/admin-modals.md +7 -0
- package/docs/admin-monitoring.md +4 -4
- package/docs/admin-network-security.md +2 -2
- package/docs/admin-phonehub.md +4 -1
- package/docs/admin-storage.md +5 -2
- package/docs/auth.md +6 -0
- package/docs/calendar.md +1 -1
- package/docs/client.md +159 -26
- package/docs/collection-multiselect.md +2 -2
- package/docs/combobox.md +2 -2
- package/docs/dataview.md +1 -1
- package/docs/datepicker.md +3 -3
- package/docs/daterange.md +1 -1
- package/docs/datetimepicker.md +1 -1
- package/docs/defineModel.md +40 -7
- package/docs/detail-primitives.md +1 -1
- package/docs/feedback.md +9 -8
- package/docs/file-upload-ux.md +2 -2
- package/docs/idioms.md +1 -1
- package/docs/loading.md +2 -2
- package/docs/location-address.md +2 -1
- package/docs/markdown.md +3 -3
- package/docs/menus-and-access.md +21 -1
- package/docs/multiselect-dropdown.md +1 -1
- package/docs/popover.md +1 -1
- package/docs/record-feed.md +2 -2
- package/docs/taginput.md +2 -1
- package/docs/timepicker.md +1 -1
- package/docs/timezone-select.md +4 -3
- package/docs/user-menu.md +4 -3
- package/docs/worldmap.md +2 -2
- package/package.json +3 -2
- package/src/account/AccountModal.tsx +3 -2
- package/src/account/index.ts +2 -2
- package/src/admin/core/index.ts +1 -1
- package/src/admin/dns/models.ts +1 -1
- package/src/admin/domains/infrastructure.ts +12 -2
- package/src/admin/domains/security.ts +3 -1
- package/src/admin/edge/BlocklistPage.tsx +110 -0
- package/src/admin/edge/UpstreamsPage.tsx +113 -0
- package/src/admin/edge/VhostDetail.tsx +166 -0
- package/src/admin/edge/VhostWizard.tsx +273 -0
- package/src/admin/edge/VhostsPage.tsx +47 -0
- package/src/admin/edge/api.ts +56 -0
- package/src/admin/edge/index.ts +15 -0
- package/src/admin/edge/models.ts +395 -0
- package/src/admin/edge/permissions.ts +13 -0
- package/src/admin/identity/users/PersonalApiKeysPage.tsx +10 -0
- package/src/admin/identity/users/sections/actions.tsx +3 -3
- package/src/admin/index.ts +1 -0
- package/src/admin/phonehub/api.ts +5 -3
- package/src/admin/public/infrastructure.ts +1 -0
- package/src/admin/registry.ts +2 -1
- package/src/admin/storage/MediaRenderingPage.tsx +258 -0
- package/src/admin/storage/file-renditions.ts +12 -0
- package/src/admin/storage/index.ts +2 -0
- package/src/admin/storage/rendition-settings.ts +264 -0
- package/src/charts/exportChart.ts +1 -1
- package/src/client/action-result.ts +23 -61
- package/src/client/active-group.ts +9 -3
- package/src/client/auth.ts +38 -15
- package/src/client/client.ts +49 -10
- package/src/client/errors.ts +57 -2
- package/src/client/group-context.ts +2 -0
- package/src/client/group.tsx +112 -20
- package/src/client/hooks.ts +7 -3
- package/src/client/mock.ts +958 -14
- package/src/client/model.ts +15 -3
- package/src/client/renditions-mock.ts +212 -0
- package/src/client/scoped.ts +19 -5
- package/src/styles/components/account.css +304 -0
- package/src/styles/components/admin-assistant.css +28 -0
- package/src/styles/components/admin-bouncer.css +53 -0
- package/src/styles/components/admin-cloudwatch.css +23 -0
- package/src/styles/components/admin-dashboard.css +16 -0
- package/src/styles/components/admin-devices.css +260 -0
- package/src/styles/components/admin-dns-certificates.css +28 -0
- package/src/styles/components/admin-dns-registrar.css +1 -0
- package/src/styles/components/admin-dns.css +43 -0
- package/src/styles/components/admin-edge.css +1 -0
- package/src/styles/components/admin-incidents.css +14 -0
- package/src/styles/components/admin-jobs.css +237 -0
- package/src/styles/components/admin-media-rendering.css +38 -0
- package/src/styles/components/admin-messaging.css +3 -0
- package/src/styles/components/admin-monitoring.css +152 -0
- package/src/styles/components/admin-network.css +406 -0
- package/src/styles/components/admin-phonehub.css +61 -0
- package/src/styles/components/admin-push.css +30 -0
- package/src/styles/components/admin-rules.css +23 -0
- package/src/styles/components/admin-security.css +37 -0
- package/src/styles/components/admin-shortlinks.css +8 -0
- package/src/styles/components/admin-signin.css +64 -0
- package/src/styles/components/admin-storage.css +4 -0
- package/src/styles/components/auth.css +162 -0
- package/src/styles/components/calendar.css +228 -0
- package/src/styles/components/charts-c2.css +249 -0
- package/src/styles/components/collection-multiselect.css +114 -0
- package/src/styles/components/collection-select.css +63 -0
- package/src/styles/components/combobox.css +42 -0
- package/src/styles/components/dataview.css +139 -0
- package/src/styles/components/datepicker.css +106 -0
- package/src/styles/components/daterange.css +119 -0
- package/src/styles/components/datetimepicker.css +230 -0
- package/src/styles/components/detail-primitives.css +246 -0
- package/src/styles/components/detailview.css +69 -0
- package/src/styles/components/drawer.css +111 -0
- package/src/styles/components/file-upload.css +112 -0
- package/src/styles/components/form-wizard.css +11 -0
- package/src/styles/components/formview.css +74 -0
- package/src/styles/components/group-admin.css +302 -0
- package/src/styles/components/guardrail.css +29 -0
- package/src/styles/components/idioms.css +84 -0
- package/src/styles/components/image-editor.css +80 -0
- package/src/styles/components/loading.css +200 -0
- package/src/styles/components/location-address.css +32 -0
- package/src/styles/components/markdown.css +192 -0
- package/src/styles/components/multiselect-dropdown.css +92 -0
- package/src/styles/components/popover.css +35 -0
- package/src/styles/components/record-feed.css +161 -0
- package/src/styles/components/right-panel.css +37 -0
- package/src/styles/components/tabs.css +26 -0
- package/src/styles/components/taginput.css +132 -0
- package/src/styles/components/timepicker.css +282 -0
- package/src/styles/components/user-admin.css +189 -0
- package/src/styles/components/user-menu.css +98 -0
- package/src/styles/components/worldmap.css +97 -0
- package/src/styles/core.css +807 -0
- package/src/styles/index.css +69 -0
- package/src/styles/required-tokens.json +32 -0
- package/src/ui/FormWizard.tsx +9 -3
- package/src/ui/ThemeProvider.tsx +1 -1
- package/src/ui/detail/index.ts +2 -2
- package/src/ui/index.ts +3 -2
- package/src/ui/loading.tsx +1 -1
- package/src/ui/toast.tsx +1 -1
package/docs/admin-modals.md
CHANGED
|
@@ -62,6 +62,7 @@ the inventory using `node scripts/verify-admin-modals.mjs --write-inventory`.
|
|
|
62
62
|
| Storage/shortlinks | Bucket, FileManager, File, shortlink; owner/backend/test/CORS/upload/rendition/share | FileManager/shortlink/share active state; no File disable promise; bucket empty only |
|
|
63
63
|
| Email/support | Domain, mailbox, sent message, template, contact; credentials/onboarding/audit/reconcile/test/template/status | Preserve evidence; separate mailbox directions and support status |
|
|
64
64
|
| Phone/Push | Number, SMS, config, device, delivery, template; lookup/test/config editors | Config/template lifecycle; number/SMS/device/delivery evidence retained |
|
|
65
|
+
| Edge | Vhost with nested routes; shape-first wizard, route editor, upstream declare/retire, blocklist entry editor | Vhost and upstream enabled state; upstreams retire and are never deleted; vhost, route and blocklist deletion are retained operations |
|
|
65
66
|
| Assistant/settings | Conversation, Skill, Setting; feed continuation, scoped memory, typed settings editor | Owner-only continuation; Skill active selection; memory is scoped editing |
|
|
66
67
|
|
|
67
68
|
## Resource lifecycle matrix
|
|
@@ -84,6 +85,9 @@ the inventory using `node scripts/verify-admin-modals.mjs --write-inventory`.
|
|
|
84
85
|
| Push config / template | Existing active selection with separate manage/test grants |
|
|
85
86
|
| Assistant conversation | Retain history; no arbitrary REST save or invented archive |
|
|
86
87
|
| Assistant Skill | Existing `is_active` save requires `sys.view_admin`; authoritative reload after mutation |
|
|
88
|
+
| Edge vhost | Enable/disable from the header; Edit and Delete in the header menu. Disabling keeps the row and is how a replacement is staged |
|
|
89
|
+
| Edge upstream | `is_enabled` only. Retire (platform administrators) disables and keeps the row; server `CAN_DELETE=False` |
|
|
90
|
+
| Edge blocklist entry | Mode is the lifecycle: `off` parks an entry without losing it. Deletion is a retained operation |
|
|
87
91
|
| Other logs/devices/history | Read-only by default, even where low-level REST supports DELETE |
|
|
88
92
|
|
|
89
93
|
The public low-level exported delete APIs remain compatible. Built-in controls
|
|
@@ -101,6 +105,9 @@ conversation archival require backend work before new lifecycle UI is added.
|
|
|
101
105
|
| DNS record-set removal | DNS manage gate, provider capability and record coordinator. Preview all values, apex/service impact and exact name/type; stale-state preflight and reconcile failures; no domain deletion |
|
|
102
106
|
| Empty bucket | `sys.manage_aws OR sys.files`, then fresh auth; header to focused armed confirmation and exact case-sensitive bucket name. Removes objects, versions, markers and multipart uploads; retains bucket. Display partial/unknown outcomes and always refresh |
|
|
103
107
|
| Jobs retention purge | `sys.manage_jobs OR sys.jobs`; dry-run estimate bound to normalized days/status plus generation. Editing/replacing preview resets arming. Ignore stale/unmounted responses; lock inputs/dismissal during execution. Report actual cutoff and cascade-inclusive counts; rejection requires another preview |
|
|
108
|
+
| Edge vhost deletion | `sys.manage_dns OR sys.security`; house vhosts are platform administrators only. Header menu to a focused confirmation naming the address. Stops serving the name and deletes its routes with it; the confirmation points at Disable as the reversible choice |
|
|
109
|
+
| Edge route removal | Same grant, inside the vhost's Routes content with an armed control naming the prefix. Requests under the prefix stop being proxied. A quiet path left without a covering route makes the vhost's next save fail, and the server's message is shown |
|
|
110
|
+
| Edge blocklist entry removal | `sys.manage_security OR sys.security`; armed control in the entry's editor. The entry leaves every node within about ten minutes. `off` is the reversible choice |
|
|
104
111
|
| Queue/consumer cleanup | Jobs manage grant, exact channel/scope and armed confirmation; queue clear sends `confirm:yes` only after consent and may cancel pending database jobs. Report partial outcomes |
|
|
105
112
|
|
|
106
113
|
Other non-row removals retain their scoped editor semantics: rate-limit override
|
package/docs/admin-monitoring.md
CHANGED
|
@@ -93,7 +93,7 @@ current contract.
|
|
|
93
93
|
## Styling and showcase
|
|
94
94
|
|
|
95
95
|
Monitoring components use semantic `monitoring-*` classes and application
|
|
96
|
-
tokens only.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
tokens only. `admin-monitoring.css` ships in `portal-mojo/styles.css`. The
|
|
97
|
+
showcase's Admin Monitoring demo contains static request, response, JSON, and
|
|
98
|
+
HTML-looking plain-text cases for both-theme verification without mutating a
|
|
99
|
+
backend.
|
|
@@ -33,8 +33,8 @@ Perimeter control: block an abusive IP right now, whitelist a customer's NAT,
|
|
|
33
33
|
maintain country and datacenter CIDR sets synced to the firewall fleet, set
|
|
34
34
|
geographic policy — and prove the policy does what you think with a built-in
|
|
35
35
|
simulator before it bites real traffic. Demo: showcase → Admin → **Network
|
|
36
|
-
security**. Styles:
|
|
37
|
-
|
|
36
|
+
security**. Styles:
|
|
37
|
+
`packages/portal-mojo/src/styles/components/admin-network.css`.
|
|
38
38
|
|
|
39
39
|
## Surfaces
|
|
40
40
|
|
package/docs/admin-phonehub.md
CHANGED
|
@@ -13,7 +13,10 @@ Import `PhoneHubPage`, the three individual pages, models, permission clauses, a
|
|
|
13
13
|
- `PhoneConfigModel` reads sanitized scalar/default-graph rows. Saves and `test_connection` use imperative, cache-free calls. The editor follows web-mojo's provider-conditional layout: selecting Twilio, AWS SNS, or Mojo Remote shows and submits only that provider's connection fields. Mojo Remote uses `https://api.mojoverify.com` when the saved URL is unset or the editor is switched to Mojo; an explicitly configured custom URL is preserved. Secret inputs are DOM refs, not React or Query state: blank means untouched and is omitted; replacement must be non-empty; clear sends `null` only after a separate confirmation. Switching providers resets pending clears, and hidden provider credentials remain untouched. Responses are allowlisted before cache reconciliation. This surface edits existing provider credentials; it does not provision Django user/group API keys.
|
|
14
14
|
|
|
15
15
|
Connection tests read the provider's `success` verdict, not the outer REST
|
|
16
|
-
`status`.
|
|
16
|
+
`status`. The real wire answers the test flat (the action dict verbatim, as
|
|
17
|
+
the mock now does too), so the call passes `refusal: 'return'`: a failed
|
|
18
|
+
verdict is an answer for inline display, not a rejected request. A failed or
|
|
19
|
+
missing verdict is an error; `test_mode` is a warning
|
|
17
20
|
that the provider was not contacted. Results persist inside the detail modal
|
|
18
21
|
on both sections, alongside a toast. Scope is labeled in the header and
|
|
19
22
|
Connection section. Editing is disabled while a connection test is pending, so an old response
|
package/docs/admin-storage.md
CHANGED
|
@@ -8,8 +8,11 @@ import {
|
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
`STORAGE_ADMIN_SECTION` contributes global/no-group Infrastructure routes at
|
|
11
|
-
`storage/buckets`, `storage/backends`,
|
|
12
|
-
under an embedded `/system` mount).
|
|
11
|
+
`storage/buckets`, `storage/backends`, `storage/files` and `storage/rendering`
|
|
12
|
+
(or the same paths under an embedded `/system` mount). `storage/rendering` is
|
|
13
|
+
the Media rendering editor — see [admin-media-rendering.md](admin-media-rendering.md);
|
|
14
|
+
it carries the Settings gate (`sys.manage_settings | sys.groups`), which the
|
|
15
|
+
section audience therefore includes. There are no record routes: buckets,
|
|
13
16
|
backends, nested files, and files use `modal.detail`.
|
|
14
17
|
|
|
15
18
|
## Permissions
|
package/docs/auth.md
CHANGED
|
@@ -28,6 +28,12 @@ onAuth('rotated', () => queryClient.invalidateQueries()); // same user, re-issu
|
|
|
28
28
|
- JWT pair (`access_token`/`refresh_token`); single-flight refresh (3
|
|
29
29
|
concurrent 401s → ONE refresh POST); pre-request gate throws a synthetic
|
|
30
30
|
`AuthRequiredError` instead of firing doomed requests.
|
|
31
|
+
- A temporary refresh failure (network, timeout or server unavailable) blocks
|
|
32
|
+
the protected request and rejects with the original transport error. It
|
|
33
|
+
keeps the session and emits `refresh-failed`, so cached workspaces can stay
|
|
34
|
+
open. Only expired/unusable credentials or confirmed refresh 401/403 emit
|
|
35
|
+
`unauthorized` and become `AuthRequiredError`. `refreshTokens()` still
|
|
36
|
+
resolves a boolean, and concurrent gated calls share one refresh POST.
|
|
31
37
|
- `useAuthSnapshot()` → `{authenticated, uid, email}` — live, cross-tab.
|
|
32
38
|
- `logout()` is client-side (django-mojo has no logout endpoint) and emits
|
|
33
39
|
`'logout'`.
|
package/docs/calendar.md
CHANGED
|
@@ -101,7 +101,7 @@ Parse fns return `null` on invalid input, never throw; format fns return
|
|
|
101
101
|
|
|
102
102
|
## Styling
|
|
103
103
|
|
|
104
|
-
`
|
|
104
|
+
`packages/portal-mojo/src/styles/components/calendar.css` — engine anatomy only
|
|
105
105
|
(`mojo-calendar-*`: pane/head/nav/grid/weekday/cell + state classes), all
|
|
106
106
|
colors via theme tokens; component tokens `--mojo-cal-anchor-fill`,
|
|
107
107
|
`--mojo-cal-range-fill`, `--mojo-cal-range-fill-hover` (accent tints:
|
package/docs/client.md
CHANGED
|
@@ -34,6 +34,19 @@ status, semantic `error_code`, and structured safe failure evidence. Nothing out
|
|
|
34
34
|
parses envelopes; `mojoCall(path, {method, params, body})` is the typed
|
|
35
35
|
escape hatch for protocol modules and returns the unwrapped `Envelope`.
|
|
36
36
|
|
|
37
|
+
**Replacement tokens.** An envelope may carry `tokens: {access_token,
|
|
38
|
+
refresh_token}` beside `data`. django-mojo sends it when the request ended every
|
|
39
|
+
other session of the caller's own account — today a save that changed the
|
|
40
|
+
caller's own password (for example an admin using "Set password" on their own
|
|
41
|
+
row). The tokens the request was sent with are already dead, so the boundary
|
|
42
|
+
stores the new pair through the auth hook `tokensReplaced`, in the storage the
|
|
43
|
+
session already lives in. Callers do nothing; the realtime socket reconnects
|
|
44
|
+
with the new token. No `tokens` means no change. A token refresh that was in
|
|
45
|
+
flight when the session was replaced is discarded when it answers, success or
|
|
46
|
+
401: it was for the old session, and it neither overwrites the new pair nor
|
|
47
|
+
reports it unauthorized. The same guard keeps a late refresh from signing a
|
|
48
|
+
user back in after a sign-out.
|
|
49
|
+
|
|
37
50
|
`FetchOpts.beforeSend` is an optional synchronous final validity check, invoked
|
|
38
51
|
after the auth gate refreshes credentials and before headers/network activity.
|
|
39
52
|
The packaged app uses it to recheck its source-generation binding at the cookie
|
|
@@ -45,6 +58,19 @@ shape. Modern envelopes use numeric top-level `code` plus a string semantic
|
|
|
45
58
|
envelopes always reject. Structured `data` can contain deliberately safe
|
|
46
59
|
recovery evidence, but callers must still avoid logging it indiscriminately.
|
|
47
60
|
|
|
61
|
+
A handler's **flat refusal inside an HTTP 200** — a top-level
|
|
62
|
+
`success:false`, as a model action answers — rejects too, on every call
|
|
63
|
+
through the boundary (`mojoCall`, `mojoList`, `mojoGet`, `mojoSave`,
|
|
64
|
+
`mojoDelete`, `mojoMetrics`, `mojoScopedCall`). It throws
|
|
65
|
+
**`ActionRefusedError`**, a `MojoError` with `status: 200`, `message` = the
|
|
66
|
+
server's `error` (or `message`) text, `errorCode` = the string refusal
|
|
67
|
+
`code`, and `data`/`result.payload` = the full reply; a raw call's label is
|
|
68
|
+
its request path. A wrapped `{status:true, data:{success:false}}` is **data**
|
|
69
|
+
here — a successful POST can legitimately carry it. A call whose flat
|
|
70
|
+
`success:false` is a real answer (a connection test) passes
|
|
71
|
+
**`refusal: 'return'`** and reads the reply itself with `readActionResult`.
|
|
72
|
+
See [Upgrading to 0.3](#upgrading-to-03).
|
|
73
|
+
|
|
48
74
|
## Functions
|
|
49
75
|
|
|
50
76
|
| Fn | Wire | Returns |
|
|
@@ -62,6 +88,11 @@ desc; filters are Django lookups (`field`, `field__in=a,b`,
|
|
|
62
88
|
triple is `dr_field/dr_start/dr_end`; datetimes serialize as epoch seconds;
|
|
63
89
|
unknown params are silently ignored by the server.
|
|
64
90
|
|
|
91
|
+
`mojoGet`, `mojoSave` and `mojoDelete` take an optional last argument,
|
|
92
|
+
`{ unscoped?: boolean }` — the record-route opt-out from a registered
|
|
93
|
+
endpoint scope (see "Record routes vs custom views" below). `defineModel`
|
|
94
|
+
passes it for you.
|
|
95
|
+
|
|
65
96
|
The executable mock coerces comparisons narrowly: both operands must be
|
|
66
97
|
fully numeric for numeric comparison; temporal comparison only recognizes
|
|
67
98
|
canonical `YYYY[-MM[-DD]]` or full ISO datetime shapes; everything else is
|
|
@@ -77,15 +108,21 @@ answer 440. Ordinary requests and real transports are unaffected.
|
|
|
77
108
|
|
|
78
109
|
django-mojo POST_SAVE_ACTION handlers can refuse **inside an HTTP 200**, in
|
|
79
110
|
two wire shapes — flat `{success:false, code, error}` or wrapped
|
|
80
|
-
`{status:true, data:{success:false, …}}
|
|
81
|
-
|
|
82
|
-
|
|
111
|
+
`{status:true, data:{success:false, …}}`. The unwrap boundary rejects the
|
|
112
|
+
flat shape for every call (see the envelope section above); the wrapped shape
|
|
113
|
+
can be ordinary data on a successful POST, so only the action layer, which
|
|
114
|
+
knows it posted an action, reads it as a refusal. `mojoAction` and
|
|
115
|
+
`useAction` pass `refusal: 'return'` to unwrap and read the reply
|
|
116
|
+
themselves, so their error names the action rather than the path (envelope
|
|
117
|
+
`status:false` is a different failure and already rejects). Never
|
|
118
|
+
hand-sniff these shapes; there is one reader:
|
|
83
119
|
|
|
84
120
|
- `readActionResult(body)` → `{ok, code, error, payload}`. `payload` merges
|
|
85
121
|
the envelope with the action dict (action fields win), so one-shot
|
|
86
122
|
secrets/counters read from one place whichever direction they rode.
|
|
87
123
|
Payload `status:false` (the `revoke_sessions` spelling) also refuses;
|
|
88
|
-
bodies with no flag resolve `ok: true`.
|
|
124
|
+
bodies with no flag resolve `ok: true`. A refusal with no string `error`
|
|
125
|
+
takes its text from a string `message`; numeric codes are ignored.
|
|
89
126
|
- `mojoAction(endpoint, id, action, payload?)` — the raw-path primitive:
|
|
90
127
|
POSTs `{[action]: payload ?? true}` to `endpoint/<id>` under
|
|
91
128
|
`withFreshAuth`, normalizes the reply, and **REJECTS with
|
|
@@ -98,15 +135,18 @@ normalizer + cache maintenance; see defineModel.md "Action refusals" for
|
|
|
98
135
|
`refusal: 'reject' | 'return'`); **raw one-off action POST →
|
|
99
136
|
`mojoAction`**; **never** parse `success`/`status` off an action body at a
|
|
100
137
|
call site. Diagnostics whose flag is a result datum (connection testers)
|
|
101
|
-
either declare `refusal:'return'` on the model or
|
|
102
|
-
|
|
138
|
+
either declare `refusal:'return'` on the model or, posted raw, pass
|
|
139
|
+
`refusal: 'return'` to `mojoCall` and read the verdict with a comment.
|
|
103
140
|
|
|
104
141
|
## Endpoint scoping — the middle tier
|
|
105
142
|
|
|
106
|
-
The model layer
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
`
|
|
143
|
+
The model layer needs no scope on record routes — `<endpoint>/<id>` gets,
|
|
144
|
+
saves, deletes and actions send none, because the server binds the row's own
|
|
145
|
+
group (see "Record routes vs custom views" below). Everything hand-rolled
|
|
146
|
+
with `useQuery` + `mojoCall` historically forgot the scope param — the largest
|
|
147
|
+
bug class in the first consumer audit (eight files, every one a raw read or
|
|
148
|
+
verb missing its `group`). The middle tier makes the raw path as safe as the
|
|
149
|
+
blessed one:
|
|
110
150
|
apps *declare* their scoped endpoint families once, and every helper —
|
|
111
151
|
plus a dev tripwire in the transport itself — enforces the declaration.
|
|
112
152
|
|
|
@@ -130,13 +170,62 @@ query, below); `required: false` families only inject when a group is active.
|
|
|
130
170
|
|
|
131
171
|
### Where the group id comes from
|
|
132
172
|
|
|
133
|
-
`GroupProvider` **owns** the active group
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
read
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
173
|
+
`GroupProvider` **owns** the active group. Three rules decide which group a
|
|
174
|
+
request carries:
|
|
175
|
+
|
|
176
|
+
1. **Hooks read the group context.** `useScopedQuery`, `useCan` and
|
|
177
|
+
`CollectionSelect` take the group from `GroupContext` at render time.
|
|
178
|
+
Under a mounted `GroupProvider`, `useScopedQuery` uses the context and
|
|
179
|
+
nothing else: a null context group means *no group*, never "whatever the
|
|
180
|
+
signal still holds". Only with no provider mounted at all does it fall
|
|
181
|
+
back to the signal.
|
|
182
|
+
2. **Plain functions read the signal.** `mojoScopedCall` and `mojoRpc` have
|
|
183
|
+
no React context, so the provider mirrors its *resolved* group into a
|
|
184
|
+
module-level signal (`setActiveGroupSignal` / `getActiveGroupId`). It
|
|
185
|
+
writes the signal in the **layout phase** of the commit, before any child
|
|
186
|
+
query starts its fetch, so the first request after a group switch already
|
|
187
|
+
carries the new group.
|
|
188
|
+
3. **An explicit `group` id wins everywhere**, on hooks and plain functions
|
|
189
|
+
alike. `group: null` differs: on `useScopedQuery` it means "unscoped by
|
|
190
|
+
choice"; on `mojoScopedCall` and `mojoRpc` a null `group` is treated as
|
|
191
|
+
not passed and the signal is used.
|
|
192
|
+
|
|
193
|
+
Do not read `localStorage.active_group_id` yourself: the URL `?group=` param
|
|
194
|
+
BEATS the stored id (group.tsx resolution order), and only the provider
|
|
195
|
+
computes that resolution.
|
|
196
|
+
|
|
197
|
+
**Recommended default**
|
|
198
|
+
|
|
199
|
+
- Reads: `useScopedQuery`.
|
|
200
|
+
- Writes and other calls from event handlers: `mojoRpc` / `mojoScopedCall`.
|
|
201
|
+
- A query you write by hand with `useQuery`: put the group id in its key
|
|
202
|
+
(`['things', group?.id]`). The package cannot key a hand-rolled query for
|
|
203
|
+
you, and a key without the group serves one group's rows to the next.
|
|
204
|
+
|
|
205
|
+
**While the group is resolving.** At boot, or on a switch the cache was not
|
|
206
|
+
seeded for (`?group=`, or the fallback after a failed load), the provider
|
|
207
|
+
reports `loading` with a null group. `useScopedQuery` holds a registered
|
|
208
|
+
query, required or optional, **disabled** for that window, so nothing goes
|
|
209
|
+
out under the previous group or unscoped. A query given an explicit `group`
|
|
210
|
+
option is not held: it has nothing to wait for. A plain `mojoScopedCall` in the
|
|
211
|
+
same window sees an empty signal: a required family throws in dev and an
|
|
212
|
+
optional one goes out unscoped, exactly as at boot. Gate on `loading` or
|
|
213
|
+
render under `RequiresGroup`.
|
|
214
|
+
|
|
215
|
+
**Upgrading.** A blanket "invalidate everything when the group changes" is
|
|
216
|
+
no longer needed for `useScopedQuery`, nor for hand-rolled queries whose key
|
|
217
|
+
carries the group id.
|
|
218
|
+
|
|
219
|
+
**Caveats**
|
|
220
|
+
|
|
221
|
+
- A child's own `useLayoutEffect` runs before the provider's and still sees
|
|
222
|
+
the previous group in the signal. Use a hook, or pass `group` explicitly.
|
|
223
|
+
- A fetch started during render (`useSuspenseQuery`, `fetchOptimistic`) runs
|
|
224
|
+
before the commit, so the signal is not yet current. Pass `group`
|
|
225
|
+
explicitly.
|
|
226
|
+
- Signed out with a stored group, the provider is not `loading`, so an
|
|
227
|
+
optional family can fire once unscoped and again after sign-in. Scoped
|
|
228
|
+
product routes sit behind `RequireAuth`; nothing is held for this case.
|
|
140
229
|
|
|
141
230
|
### `useScopedQuery<T>(path, params?, {read?, group?, ...queryOpts})`
|
|
142
231
|
|
|
@@ -146,7 +235,8 @@ request params, and the group id rides the query key —
|
|
|
146
235
|
cached rows, while invalidation by `[path]` prefix keeps working. Keep that
|
|
147
236
|
key discipline if you write a raw scoped query by hand.
|
|
148
237
|
|
|
149
|
-
|
|
238
|
+
While the provider is still resolving its group, a registered query with no
|
|
239
|
+
explicit `group` is held disabled (see above). When a REQUIRED scope has no active group, the query renders **disabled** —
|
|
150
240
|
and a disabled query has *neither data nor error*. Do not draw an empty box
|
|
151
241
|
or a spinner that never resolves: state the wait honestly at the call site
|
|
152
242
|
("select a brand to load …").
|
|
@@ -161,14 +251,21 @@ or a spinner that never resolves: state the wait honestly at the call site
|
|
|
161
251
|
`withFreshAuth` with the scope injected; resolves `env.data as T`.
|
|
162
252
|
Action-shaped `{key: payload}` saves belong to `mojoAction`, not here.
|
|
163
253
|
|
|
164
|
-
### `mojoAction`
|
|
254
|
+
### `mojoAction` sends no scope
|
|
255
|
+
|
|
256
|
+
`mojoAction(endpoint, id, action, payload?)` posts exactly
|
|
257
|
+
`{[action]: payload}` to `<endpoint>/<id>` and never adds a scope key, on any
|
|
258
|
+
family, with or without an active group. Two reasons:
|
|
259
|
+
|
|
260
|
+
- A REST record route needs no group. django-mojo authorizes the request
|
|
261
|
+
against the **row's own** group, whatever the caller sends.
|
|
262
|
+
- A `group` in the body is not only a selector there: the save handler
|
|
263
|
+
writes it as the row's `group` field, and its presence turns the action
|
|
264
|
+
into a full field save. An action on a record read outside the active
|
|
265
|
+
group could move that record to the active group.
|
|
165
266
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
injected into the action POST body alongside `{[action]: payload}` — the
|
|
169
|
-
wallet-verbs class: actions the backend refuses for brand-scoped operators
|
|
170
|
-
without `group`. With nothing registered (the base admin) the injection is
|
|
171
|
-
inert.
|
|
267
|
+
The call is declared `unscoped: true`, so it passes the dev tripwire on a
|
|
268
|
+
registered-REQUIRED family. Earlier versions injected the scope here.
|
|
172
269
|
|
|
173
270
|
### The dev tripwire — and `unscoped: true`
|
|
174
271
|
|
|
@@ -182,6 +279,25 @@ for the rare genuinely-global call to a scoped family, mark it
|
|
|
182
279
|
skip the check (`import.meta.env.DEV` only); it is a development tripwire,
|
|
183
280
|
not a runtime gate.
|
|
184
281
|
|
|
282
|
+
### Record routes vs custom views
|
|
283
|
+
|
|
284
|
+
Two kinds of URL end in an id, and only the caller knows which it has:
|
|
285
|
+
|
|
286
|
+
- **REST record routes** — the model's own `<endpoint>/<id>` (get, save,
|
|
287
|
+
delete, POST_SAVE_ACTIONS). The server binds the row's group; send no
|
|
288
|
+
scope. `defineModel`'s record hooks, the generic `useModel` and
|
|
289
|
+
`useSaveModel` (with an id) and `mojoAction` already declare these
|
|
290
|
+
`unscoped: true`. A raw call to one does the same:
|
|
291
|
+
`mojoGet(endpoint, id, { unscoped: true })`.
|
|
292
|
+
- **Custom views on a record path** — `requests/<id>/resend`, a download by
|
|
293
|
+
id, any view the backend gates with a group permission decorator. These
|
|
294
|
+
read the group from the request and **do** need it: keep using `mojoRpc` /
|
|
295
|
+
`mojoScopedCall` / `useScopedQuery`, which inject it.
|
|
296
|
+
|
|
297
|
+
No path rule can tell the two apart, so the tripwire does not try. A
|
|
298
|
+
collection create (`POST <endpoint>`) is not a record route: the group on
|
|
299
|
+
the request decides where the new row lands, so pass it in the body.
|
|
300
|
+
|
|
185
301
|
## Mock admin contracts
|
|
186
302
|
|
|
187
303
|
The mock carries measured GroupView/admin transports, not UI-shaped
|
|
@@ -313,7 +429,7 @@ confirm `completed` before the task returns success. Completed scalar
|
|
|
313
429
|
## `mojoQueryDefaults()`
|
|
314
430
|
|
|
315
431
|
Spread into the app's `QueryClient` defaults. Provides: no retry on 4xx
|
|
316
|
-
`MojoError`s (deterministic failures), `networkMode: 'always'` under the
|
|
432
|
+
`MojoError`s or `ActionRefusedError` (deterministic failures), `networkMode: 'always'` under the
|
|
317
433
|
mock ('online' live), and `refetchOnWindowFocus: false` (freshness comes
|
|
318
434
|
from explicit refresh + opt-in autoRefresh — focus storms read as phantom
|
|
319
435
|
fetches).
|
|
@@ -325,3 +441,20 @@ fetches).
|
|
|
325
441
|
the semantic wire code, and `data` preserves safe structured evidence.
|
|
326
442
|
- `AuthRequiredError` — thrown by the pre-request gate when a request needs
|
|
327
443
|
a session that doesn't exist (synthetic 401; no network was touched).
|
|
444
|
+
|
|
445
|
+
## Upgrading to 0.3
|
|
446
|
+
|
|
447
|
+
0.3.0 is a breaking minor release: a raw call through the unwrap boundary
|
|
448
|
+
now **rejects** a flat HTTP-200 refusal (top-level `success:false`) with
|
|
449
|
+
`ActionRefusedError` instead of resolving it.
|
|
450
|
+
|
|
451
|
+
- Find raw `mojoCall` / `mojoScopedCall` sites that read a top-level
|
|
452
|
+
`.success`. Those refusal checks are now dead code: delete them. Their
|
|
453
|
+
`catch` receives `ActionRefusedError`, which is a `MojoError`, so existing
|
|
454
|
+
`instanceof MojoError` handling keeps working.
|
|
455
|
+
- A call whose flat `success:false` is a real answer, such as a connection
|
|
456
|
+
test, adds `refusal: 'return'` and keeps reading the reply.
|
|
457
|
+
- Reads of a wrapped `data.success` are unaffected.
|
|
458
|
+
- `err.errorCode` carries the refusal code and `err.data` the full reply, so
|
|
459
|
+
the server's reason is no longer lost.
|
|
460
|
+
- `defineModel` actions and `mojoAction` behave as before.
|
|
@@ -113,5 +113,5 @@ Sorting is the server's default unless `defaultParams` carries `sort`.
|
|
|
113
113
|
a permanently empty picker in global context.
|
|
114
114
|
- The dropdown's trigger/footer classes (`.multiselect-trigger`,
|
|
115
115
|
`.multiselect-footer`, `.multiselect-done`) live in
|
|
116
|
-
`multiselect-dropdown.css` —
|
|
117
|
-
`collection-multiselect.css
|
|
116
|
+
`multiselect-dropdown.css` — `portal-mojo/styles.css` imports it alongside
|
|
117
|
+
`collection-multiselect.css`.
|
package/docs/combobox.md
CHANGED
|
@@ -101,5 +101,5 @@ strings.
|
|
|
101
101
|
to the label, duplicating every row — not carried).
|
|
102
102
|
- The dropdown is mounted in the shared top-layer Popover, so overflow
|
|
103
103
|
containers and native dialogs do not clip it.
|
|
104
|
-
- Styles live in `
|
|
105
|
-
themes)
|
|
104
|
+
- Styles live in `packages/portal-mojo/src/styles/components/combobox.css` (tokens only, both
|
|
105
|
+
themes) and ship in `portal-mojo/styles.css`.
|
package/docs/dataview.md
CHANGED
|
@@ -11,7 +11,7 @@ Point it at any record and get a sensible detail grid. Field **names** and
|
|
|
11
11
|
**values** together pick the renderer, nested objects become nested grids,
|
|
12
12
|
and anything that isn't a scalar lands in a JSON block with copy and
|
|
13
13
|
collapse. Port of web-mojo `src/core/views/data/DataView.js` (1,153 lines).
|
|
14
|
-
Styles: `
|
|
14
|
+
Styles: `packages/portal-mojo/src/styles/components/dataview.css`. Demo: **Develop → Components →
|
|
15
15
|
DataView**.
|
|
16
16
|
|
|
17
17
|
## DataView vs KnownFieldsCard
|
package/docs/datepicker.md
CHANGED
|
@@ -144,8 +144,8 @@ lean on the per-precision default.
|
|
|
144
144
|
|
|
145
145
|
## Styling
|
|
146
146
|
|
|
147
|
-
`
|
|
148
|
-
box, inline host. The grid itself is `
|
|
149
|
-
surface is `
|
|
147
|
+
`packages/portal-mojo/src/styles/components/datepicker.css` — trigger, clear ✕, popover content
|
|
148
|
+
box, inline host. The grid itself is `components/calendar.css` and the floating
|
|
149
|
+
surface is `components/popover.css`; this file adds nothing they already cover.
|
|
150
150
|
Trigger metrics follow the house control (`.input`): `--surface2` fill,
|
|
151
151
|
`--line` border, 8px radius, accent focus ring. Tokens only, both themes.
|
package/docs/daterange.md
CHANGED
|
@@ -164,7 +164,7 @@ carry them when the registry lands:
|
|
|
164
164
|
`inline` is fluid to its host. Two panes + rail ≈ 850px — don't mount
|
|
165
165
|
two-pane popovers inside narrow scroll containers, and keep dialogs on
|
|
166
166
|
`months={1}`.
|
|
167
|
-
- Styling lives in `
|
|
167
|
+
- Styling lives in `packages/portal-mojo/src/styles/components/daterange.css` (trigger
|
|
168
168
|
`.mojo-daterange-*`, rail `.mojo-calendar-preset*`), tokens only, both
|
|
169
169
|
themes; the calendar grid ships in `calendar.css`, the popover shell in
|
|
170
170
|
`popover.css`.
|
package/docs/datetimepicker.md
CHANGED
|
@@ -222,7 +222,7 @@ not `new Date(epochSeconds)` without `× 1000`.
|
|
|
222
222
|
|
|
223
223
|
## Styling
|
|
224
224
|
|
|
225
|
-
`
|
|
225
|
+
`packages/portal-mojo/src/styles/components/datetimepicker.css` — layout glue only
|
|
226
226
|
(`mojo-datetime-*`: `trigger` / `trigger-text` / `trigger-clear` /
|
|
227
227
|
`popover-inner` / `row` / `cal-col` / `time-col` / `time-head` / `tz-row` /
|
|
228
228
|
`tz-label` / `tz-host` / `foot`), tokens only, both themes. The grid, the
|
package/docs/defineModel.md
CHANGED
|
@@ -28,11 +28,11 @@ export const UserModel = defineModel<User>({
|
|
|
28
28
|
| Member | Behavior |
|
|
29
29
|
|---|---|
|
|
30
30
|
| `useList(params?, {enabled?})` | `useQuery` list under key `[endpoint, params]` with `keepPreviousData`; `enabled:false` suppresses permission-hidden background reads |
|
|
31
|
-
| `useOne(id \| null)` | one record, key `[endpoint, 'one', id]`; disabled while `id` is null |
|
|
32
|
-
| `useSave()` | mutation `{id, changes}`; `id: null` creates. Handles one fresh-auth 440/retry, then writes the returned row into the one-record cache and invalidates `[endpoint]`. REJECTS on failure |
|
|
33
|
-
| `useDelete()` | mutation `{id}`; removes the one-record cache entry + invalidates |
|
|
34
|
-
| `useAction(name)` | mutation for ONE declared POST_SAVE_ACTION (below), with one fresh-auth 440/retry. THROWS at render time for an undeclared name |
|
|
35
|
-
| `fetchOne(queryClient, id)` | imperative fetch through the same cache key — dedupes with mounted `useOne`s (prefetch pattern) |
|
|
31
|
+
| `useOne(id \| null)` | one record, key `[endpoint, 'one', id]`; disabled while `id` is null. Sends no group scope |
|
|
32
|
+
| `useSave()` | mutation `{id, changes}`; `id: null` creates. Handles one fresh-auth 440/retry, then writes the returned row into the one-record cache and invalidates `[endpoint]`. REJECTS on failure. With an id it sends no group scope; a create sends only your `changes` |
|
|
33
|
+
| `useDelete()` | mutation `{id}`; removes the one-record cache entry + invalidates. Sends no group scope |
|
|
34
|
+
| `useAction(name)` | mutation for ONE declared POST_SAVE_ACTION (below), with one fresh-auth 440/retry. THROWS at render time for an undeclared name. The body is exactly `{[key]: payload}`; sends no group scope |
|
|
35
|
+
| `fetchOne(queryClient, id)` | imperative fetch through the same cache key — dedupes with mounted `useOne`s (prefetch pattern). Sends no group scope |
|
|
36
36
|
| `invalidate(queryClient)` | invalidate everything under `[endpoint]` |
|
|
37
37
|
| `keys.root / keys.list(params) / keys.one(id)` | key builders for targeted cache surgery |
|
|
38
38
|
|
|
@@ -45,6 +45,35 @@ only on final success. Delete remains unwrapped, as do direct `mojoCall`
|
|
|
45
45
|
mutations; callers opt those in only when the endpoint is both sensitive and
|
|
46
46
|
safe to retry once.
|
|
47
47
|
|
|
48
|
+
## Group scope
|
|
49
|
+
|
|
50
|
+
In a group portal an app registers its scoped endpoint families
|
|
51
|
+
(`registerEndpointScope`, [client.md](client.md)). The model layer treats
|
|
52
|
+
its two kinds of call differently:
|
|
53
|
+
|
|
54
|
+
- **Record calls send no group.** `useOne`, `fetchOne`, `useSave` with an
|
|
55
|
+
id, `useDelete` and `useAction` all hit `<endpoint>/<id>`. django-mojo
|
|
56
|
+
authorizes those against the row's own group, so a client group adds
|
|
57
|
+
nothing — and a `group` in a save or action body is written to the row as
|
|
58
|
+
its `group` field. The hooks declare these calls `unscoped: true`, so a
|
|
59
|
+
registered-REQUIRED family never blocks them in development.
|
|
60
|
+
- **A create sends only your `changes`.** `useSave` with `id: null` posts to
|
|
61
|
+
the collection, where the request's group decides where the row lands.
|
|
62
|
+
Put the group in `changes` yourself; on a REQUIRED family the dev tripwire
|
|
63
|
+
stops a create that carries none.
|
|
64
|
+
|
|
65
|
+
List reads (`useList`) are scoped only by the params you pass.
|
|
66
|
+
|
|
67
|
+
**Upgrading.** Record calls used to be stopped by the dev tripwire on a
|
|
68
|
+
REQUIRED family, and `mojoAction` used to inject the group. If your portal
|
|
69
|
+
grew a local wrapper to get past that — one that adds the group to an
|
|
70
|
+
action or save on a record, or refuses to send when the active group
|
|
71
|
+
changed since the record was read — delete it and call `M.useAction(verb)`
|
|
72
|
+
(or `mojoAction`) directly. Remove any hand-added `group` from record-path
|
|
73
|
+
bodies and params: it is at best ignored and at worst rewrites the row's
|
|
74
|
+
group. Custom views on a record path (`<id>/resend`) are not model calls and
|
|
75
|
+
keep their scope through `mojoRpc`.
|
|
76
|
+
|
|
48
77
|
## POST_SAVE_ACTIONS
|
|
49
78
|
|
|
50
79
|
Backend contract (`mojo/models/rest.py on_rest_save`): an action is a key
|
|
@@ -75,8 +104,12 @@ reaches the client in one of two wire shapes:
|
|
|
75
104
|
```
|
|
76
105
|
|
|
77
106
|
This is NOT the envelope failure: envelope-level `status:false` already
|
|
78
|
-
rejects at the unwrap boundary (`MojoError`).
|
|
79
|
-
|
|
107
|
+
rejects at the unwrap boundary (`MojoError`). Since 0.3 unwrap also rejects
|
|
108
|
+
the flat shape for raw calls; `useAction` opts out of that check
|
|
109
|
+
(`refusal: 'return'` on its own request) so the error names the action and
|
|
110
|
+
honours the model's declared mode. No behaviour change for models. The
|
|
111
|
+
action-refusal layer sits above the boundary and is handled by `useAction`
|
|
112
|
+
automatically:
|
|
80
113
|
|
|
81
114
|
- **`refusal: 'reject'` (default)** — a `success:false` (or payload
|
|
82
115
|
`status:false`) reply REJECTS with **`ActionRefusedError`**, so a refusal
|
|
@@ -10,7 +10,7 @@ Six small pieces admin record pages are composed from, ported from
|
|
|
10
10
|
web-mojo's `src/core/views/data/*` primitives plus its admin metadata
|
|
11
11
|
section. They are presentational and stateless (except `MetadataSection`,
|
|
12
12
|
which writes) — drop them inside a `DetailView` section, a `panel`, or a
|
|
13
|
-
`modal.detail`. Styles: `
|
|
13
|
+
`modal.detail`. Styles: `packages/portal-mojo/src/styles/components/detail-primitives.css`.
|
|
14
14
|
|
|
15
15
|
**All three trusted-HTML slots are gone.** web-mojo rendered
|
|
16
16
|
`StatusPanel.meta`, `FlowStrip.value/hint` and `Timeline.detail` as
|
package/docs/feedback.md
CHANGED
|
@@ -73,10 +73,10 @@ disable, member `manage_group` / `manage_members` grants (through
|
|
|
73
73
|
which of the watched keys changed (null/'' normalised), so a save confirms
|
|
74
74
|
ONLY the fields that break things and passes silently otherwise.
|
|
75
75
|
|
|
76
|
-
Styles: `
|
|
76
|
+
Styles: `packages/portal-mojo/src/styles/components/guardrail.css` (`.guardrail`,
|
|
77
77
|
`.guardrail-effect/-why/-why-head/-undo/-type`, `.guardrail-icon`) over
|
|
78
|
-
`--bad/--bad-soft` (danger) and `--warn/--warn-soft` (`danger: false`).
|
|
79
|
-
|
|
78
|
+
`--bad/--bad-soft` (danger) and `--warn/--warn-soft` (`danger: false`). It
|
|
79
|
+
ships in `portal-mojo/styles.css`; the app declares the tokens (README).
|
|
80
80
|
|
|
81
81
|
## modal.drawer
|
|
82
82
|
|
|
@@ -112,7 +112,7 @@ title · meta) is web-mojo's `Modal.drawer()` carried over.
|
|
|
112
112
|
otherwise there is no exit at all, so it falls back to dismissable with a
|
|
113
113
|
`console.warn` (house rule: degenerate input → default + warn).
|
|
114
114
|
- An unknown `width` warns and falls back to 480px.
|
|
115
|
-
- Styles: `
|
|
115
|
+
- Styles: `packages/portal-mojo/src/styles/components/drawer.css` (`.mojo-drawer`, `.drawer-panel`,
|
|
116
116
|
`.drawer-head/-eyebrow/-title/-meta`, `.drawer-body`) — tokens only, both
|
|
117
117
|
themes. `DRAWER_EXIT_MS` in `modal.tsx` must match the closing keyframes.
|
|
118
118
|
|
|
@@ -139,10 +139,11 @@ and drawers), then returns to the page when the last modal closes. Modal-local
|
|
|
139
139
|
ancestry keeps Undo and progress Cancel usable despite native dialog inertness.
|
|
140
140
|
Moving the host preserves card state and timers and does not focus the toast.
|
|
141
141
|
Mount it once; do not add per-dialog toast hosts or solve this with `z-index`.
|
|
142
|
-
|
|
143
|
-
`
|
|
144
|
-
and background,
|
|
145
|
-
|
|
142
|
+
The toast CSS ships in `portal-mojo/styles.css`
|
|
143
|
+
(`packages/portal-mojo/src/styles/core.css`): it resets the popover's UA
|
|
144
|
+
inset/margin/border/padding and background, hides closed popovers, and keeps
|
|
145
|
+
only the cards pointer-active. An app that still carries a copied theme keeps
|
|
146
|
+
its old toast rules until it deletes the copy.
|
|
146
147
|
The showcase's **Toasts inside modal** demo covers errors, Undo, progress,
|
|
147
148
|
and nested opening/closing. `node scripts/verify-toast-modal.mjs` checks the
|
|
148
149
|
mounted lifecycle; native visibility and interaction require browser checks.
|
package/docs/file-upload-ux.md
CHANGED
|
@@ -140,7 +140,7 @@ npm run verify:record-attachments
|
|
|
140
140
|
|
|
141
141
|
It covers pure validation, lazy concurrency, capacity, stable/safe snapshots,
|
|
142
142
|
consumer/destination dedupe, real progress updates, callback-only retry,
|
|
143
|
-
truthful cancellation, known-id recovery, and
|
|
144
|
-
|
|
143
|
+
truthful cancellation, known-id recovery, and the package stylesheet importing
|
|
144
|
+
the upload CSS. The form contract adds strict relation normalization, explicit
|
|
145
145
|
attach states, owner-response reconciliation, orphan retention, safe previews,
|
|
146
146
|
avatar #1488 semantics, and cache-boundary sanitization.
|
package/docs/idioms.md
CHANGED
|
@@ -47,7 +47,7 @@ Behavior contract (dom.js `armedButton` parity):
|
|
|
47
47
|
live trigger). Fail-safe cost is one extra click, never an unintended fire.
|
|
48
48
|
- Clicks `stopPropagation()` — safe inside clickable rows / expand rows.
|
|
49
49
|
|
|
50
|
-
Styling: `
|
|
50
|
+
Styling: `packages/portal-mojo/src/styles/components/idioms.css` — `.armed-btn` (rest) /
|
|
51
51
|
`.armed-btn.is-armed` (armed, pulsing; static under
|
|
52
52
|
`prefers-reduced-motion`). Tokens only, both themes; the dark theme swaps
|
|
53
53
|
the armed text color for contrast against the light --bad.
|
package/docs/loading.md
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
The "we're fetching / working" family. It **complements the skeleton
|
|
13
13
|
silhouette**, which stays the default for anything whose shape is already
|
|
14
14
|
known (see the decision table below). Live demo: portal → Develop →
|
|
15
|
-
Components → **Loaders**. Styling: `
|
|
15
|
+
Components → **Loaders**. Styling: `packages/portal-mojo/src/styles/components/loading.css` —
|
|
16
16
|
tokens only, both themes.
|
|
17
17
|
|
|
18
18
|
Ported from web-mojo `src/core/views/feedback/BusyIndicator.js` (its only
|
|
@@ -210,7 +210,7 @@ clicked away mid-save, they keep where they went.
|
|
|
210
210
|
restores focus to the element the inert switch blurred (see above). Its
|
|
211
211
|
veil is `pointer-events: none`: `inert` already stopped the clicks, and a
|
|
212
212
|
veil that swallowed them would break text selection after release.
|
|
213
|
-
- **Reduced motion:** the spinner does not spin. `
|
|
213
|
+
- **Reduced motion:** the spinner does not spin. `core.css` kills every
|
|
214
214
|
animation with a universal `!important` rule — right for a skeleton
|
|
215
215
|
(shimmer stops), wrong for a spinner (the ring would freeze mid-rotation
|
|
216
216
|
with one colored quarter and read as a stuck control). `loading.css`
|
package/docs/location-address.md
CHANGED
|
@@ -73,4 +73,5 @@ field in that batch back to its shared server snapshot.
|
|
|
73
73
|
|
|
74
74
|
The suggestion list uses the shared `Popover`, which is the sole owner of
|
|
75
75
|
outside-mousedown and Escape dismissal. Styles live in
|
|
76
|
-
`
|
|
76
|
+
`src/styles/components/location-address.css` (shipped in
|
|
77
|
+
`portal-mojo/styles.css`) and use design tokens in both themes.
|