@lessly/sdk-app 61.2.0 → 62.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_types/gen/client.gen.d.ts +53 -2
- package/dist/_types/gen/manifest.gen.d.ts +1 -1
- package/dist/_types/gen/organization/index.d.ts +1 -1
- package/dist/_types/gen/organization/queryOptions.gen.d.ts +57 -5
- package/dist/_types/gen/playground/connect.gen.d.ts +3 -0
- package/dist/_types/gen/playground/index.d.ts +3 -0
- package/dist/_types/gen/playground/queryOptions.gen.d.ts +50 -0
- package/dist/_types/gen/types.gen.d.ts +374 -10
- package/dist/_types/index.d.ts +1 -0
- package/dist/_types/runtime/access-reason.d.ts +20 -0
- package/dist/chunk-2YAA3AF4.js +10437 -0
- package/dist/chunk-2YAA3AF4.js.map +1 -0
- package/dist/index.cjs +444 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +60 -10106
- package/dist/index.js.map +1 -1
- package/dist/organization/index.cjs +70 -5
- package/dist/organization/index.cjs.map +1 -1
- package/dist/organization/index.js +57 -5
- package/dist/organization/index.js.map +1 -1
- package/dist/playground/index.cjs +81 -0
- package/dist/playground/index.cjs.map +1 -0
- package/dist/playground/index.d.cts +1 -0
- package/dist/playground/index.d.ts +1 -0
- package/dist/playground/index.js +58 -0
- package/dist/playground/index.js.map +1 -0
- package/docs/recipes/access.md +456 -15
- package/docs/rules.md +92 -0
- package/package.json +12 -2
- package/src/gen/bindings.gen.ts +395 -40
- package/src/gen/client.gen.ts +104 -3
- package/src/gen/manifest.gen.ts +53 -28
- package/src/gen/organization/index.ts +1 -1
- package/src/gen/organization/queryOptions.gen.ts +87 -6
- package/src/gen/playground/connect.gen.ts +9 -0
- package/src/gen/playground/index.ts +4 -0
- package/src/gen/playground/queryOptions.gen.ts +86 -0
- package/src/gen/types.gen.ts +441 -12
package/docs/rules.md
CHANGED
|
@@ -338,6 +338,91 @@ An App SHOULD reflect the caller's access in its UI: **disable, with a reason**
|
|
|
338
338
|
(not hide) a primary action the caller cannot perform, using `useCan` from
|
|
339
339
|
`@lessly/sdk-app/react` or `app.access.can()` directly, and render a 403 that
|
|
340
340
|
`isAccessDenied()` recognises as a **neutral empty state** rather than an error.
|
|
341
|
+
That reason MUST live on a `Tooltip` wrapper around the disabled control, never
|
|
342
|
+
on a native `title=` of the control itself: the base `buttonVariants` carry
|
|
343
|
+
`disabled:pointer-events-none`, so a disabled control fires no hover for the
|
|
344
|
+
browser to render a `title` from, and a native tooltip is unreachable by
|
|
345
|
+
keyboard and by touch in any case. An App that draws such a tooltip MUST mount
|
|
346
|
+
its own `TooltipProvider` once at the root of its remote — `Tooltip.Root` throws
|
|
347
|
+
without one, and the standalone build, the dev entry and the tests all render
|
|
348
|
+
outside the shell.
|
|
349
|
+
|
|
350
|
+
That reason MUST be exactly one sentence, the same in every App:
|
|
351
|
+
|
|
352
|
+
> `Requires level:<op.level> (<op.operationKey>). Ask an admin of this product.`
|
|
353
|
+
|
|
354
|
+
— e.g. `Requires level:admin (mail_domain_create). Ask an admin of this product.`
|
|
355
|
+
Both values MUST be read from the generated method, never written as literals.
|
|
356
|
+
`@lessly/sdk-app` exports `accessReason(op)`, which is that sentence; an App
|
|
357
|
+
SHOULD use it rather than assembling the text itself.
|
|
358
|
+
|
|
359
|
+
Where a control has only an operation key and no generated method to point at,
|
|
360
|
+
the App MUST NOT guess the level or render `level:undefined`; it says instead:
|
|
361
|
+
|
|
362
|
+
> `Requires access to <operationKey>. Ask an admin of this product.`
|
|
363
|
+
|
|
364
|
+
That string form is a **degradation** for operations no generated method covers,
|
|
365
|
+
not licence to pass a key where a method exists — the accessor path is a lossy
|
|
366
|
+
view of the key. An App whose every gate holds an `Operation` never uses it, and
|
|
367
|
+
that is the norm rather than an omission. `accessReason` explains ONE operation;
|
|
368
|
+
choosing which one to explain among a button's several is the caller's, not the
|
|
369
|
+
helper's.
|
|
370
|
+
It names what an admin can grant, not the caller's role: a role name gives them
|
|
371
|
+
nothing to act on. It names the operation key as well as the level because a
|
|
372
|
+
level alone does not identify the grant — seventeen controls on one screen can
|
|
373
|
+
all require `level:write` — and an admin grants both by level and by exact key,
|
|
374
|
+
so the user has to be able to pass on both. Building it from the method also
|
|
375
|
+
keeps it from drifting when the catalog re-levels a tool. Whatever renders it MUST be verified reachable **from the keyboard** —
|
|
376
|
+
Tab to the wrapper, the tooltip opens (in tests: `userEvent.tab()` then
|
|
377
|
+
`findByRole('tooltip')`). Asserting only that a hint attribute is present does
|
|
378
|
+
not satisfy this: that assertion is green against a hint no user can reach.
|
|
379
|
+
|
|
380
|
+
The level MUST come from the generated method itself (`op.level`, on the
|
|
381
|
+
`Operation` the method satisfies) and from nowhere else: not a lookup table in
|
|
382
|
+
the App, not a literal, and never a rule of thumb about verbs — `analytics_*_delete`
|
|
383
|
+
is `write` while `mail_domain_create` is `admin`, and only the catalog knows.
|
|
384
|
+
|
|
385
|
+
The gate belongs to the OPERATION, not to a gesture: where a write is reachable
|
|
386
|
+
by several paths (drag and arrow keys, click and Enter, button and hotkey) all of
|
|
387
|
+
them MUST be gated together, or the hole left behind is one only keyboard users
|
|
388
|
+
find. Implicit form submission is such a path: a disabled submit button does not
|
|
389
|
+
reliably stop Enter inside a field, so the `onSubmit` handler MUST be gated (or
|
|
390
|
+
the fields made `readOnly`) rather than the button alone.
|
|
391
|
+
|
|
392
|
+
A control that performs SEVERAL operations per click is disabled if ANY of them
|
|
393
|
+
is denied. A DISPATCHER control — one operation per click, selected by a mode or
|
|
394
|
+
a switch — MUST instead be gated on the operation it is about to call, and name
|
|
395
|
+
that operation's level and key; gating it on the union greys out a button the caller can
|
|
396
|
+
in fact use.
|
|
397
|
+
|
|
398
|
+
An App SHOULD state the constraint once in a card footer (`CardNote`) rather than
|
|
399
|
+
on every control of a form whose save is deferred: the reader meets the sentence
|
|
400
|
+
before editing rather than after, and one line replaces fifteen identical
|
|
401
|
+
tooltips. The fields stay inactive and the save stays disabled either way — the
|
|
402
|
+
note replaces the per-control hints, not the gate. And the `Tooltip` trigger MUST sit outside every element carrying
|
|
403
|
+
`pointer-events-none` — the App's own lists, chips and overlays as much as the
|
|
404
|
+
kit's disabled buttons — since the hint is swallowed by the same mechanics either
|
|
405
|
+
way.
|
|
406
|
+
|
|
407
|
+
This applies to every gated write, not only the ones with a button. A drag
|
|
408
|
+
handle, an inline-editable cell and a field that autosaves on blur are gated the
|
|
409
|
+
same way: the native inactive state for that control (`draggable={false}`,
|
|
410
|
+
`readOnly`, `disabled`) plus the reason on a keyboard-reachable `Tooltip`
|
|
411
|
+
wrapper. Leaving a write ungated for the gateway to refuse is permitted ONLY
|
|
412
|
+
where the element has no wrapper that can be made focusable, and that exception
|
|
413
|
+
MUST be written into the App's own spec with its reason. A button that performs
|
|
414
|
+
several operations is disabled if ANY of them is denied, and its reason names
|
|
415
|
+
the level and key of the operation that blocked it.
|
|
416
|
+
|
|
417
|
+
The reason is claimed only when the App actually knows it. With
|
|
418
|
+
`access.state === 'error'` the `/me` call failed and **nothing** is known about
|
|
419
|
+
the caller's role: the App MUST make no prediction — leave the control enabled,
|
|
420
|
+
name no missing operation, and let the gateway answer, rendering any refusal through
|
|
421
|
+
`isAccessDenied`. Disabling on error makes the client the authority and tells
|
|
422
|
+
the user their role forbids something no one has established. (`can()` itself is
|
|
423
|
+
unaffected: it stays `false` unless the state is `ready`.) While the state is
|
|
424
|
+
`idle` or `loading` the control SHOULD start disabled and enable itself, but
|
|
425
|
+
SHOULD NOT name a missing level or operation it does not yet know.
|
|
341
426
|
|
|
342
427
|
An App MUST NOT treat that prediction as enforcement. `can()` answers from a
|
|
343
428
|
snapshot fetched once from the `organization_product_me` tool; the gateway is
|
|
@@ -363,4 +448,11 @@ Pass the generated method rather than a hand-written tool id
|
|
|
363
448
|
(`app.access.can(app.mail.domain.create)`): the accessor path is a lossy view
|
|
364
449
|
of the tool id, so a literal string can silently drift from the call it gates.
|
|
365
450
|
|
|
451
|
+
One gap is known and accepted: `@lessly/ui`'s `ConfirmDialog` takes an `onError`
|
|
452
|
+
that returns a string, so the kit picks the tone and a refusal raised inside the
|
|
453
|
+
dialog cannot be painted neutrally yet. An App SHOULD still route that error
|
|
454
|
+
through `isAccessDenied()` for the *words*, and MUST NOT reimplement the confirm
|
|
455
|
+
flow to win the colour — it would forfeit the focus trap, busy state, confirm
|
|
456
|
+
phrase and step-up. Tracked as platform #2915.
|
|
457
|
+
|
|
366
458
|
See `recipes/access.md` for the load-once pattern and the empty-state shape.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessly/sdk-app",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "62.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"engines": {
|
|
@@ -148,6 +148,16 @@
|
|
|
148
148
|
"default": "./dist/organization/index.cjs"
|
|
149
149
|
}
|
|
150
150
|
},
|
|
151
|
+
"./playground": {
|
|
152
|
+
"import": {
|
|
153
|
+
"types": "./dist/playground/index.d.ts",
|
|
154
|
+
"default": "./dist/playground/index.js"
|
|
155
|
+
},
|
|
156
|
+
"require": {
|
|
157
|
+
"types": "./dist/playground/index.d.cts",
|
|
158
|
+
"default": "./dist/playground/index.cjs"
|
|
159
|
+
}
|
|
160
|
+
},
|
|
151
161
|
"./realtime": {
|
|
152
162
|
"import": {
|
|
153
163
|
"types": "./dist/realtime/index.d.ts",
|
|
@@ -199,5 +209,5 @@
|
|
|
199
209
|
}
|
|
200
210
|
}
|
|
201
211
|
},
|
|
202
|
-
"sdkContentHash": "sha256:
|
|
212
|
+
"sdkContentHash": "sha256:965d6441024647f1461d4e50bedeba9d79dca805ea4668ca8c9ecedefc1c0d0a"
|
|
203
213
|
}
|