@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.
Files changed (38) hide show
  1. package/dist/_types/gen/client.gen.d.ts +53 -2
  2. package/dist/_types/gen/manifest.gen.d.ts +1 -1
  3. package/dist/_types/gen/organization/index.d.ts +1 -1
  4. package/dist/_types/gen/organization/queryOptions.gen.d.ts +57 -5
  5. package/dist/_types/gen/playground/connect.gen.d.ts +3 -0
  6. package/dist/_types/gen/playground/index.d.ts +3 -0
  7. package/dist/_types/gen/playground/queryOptions.gen.d.ts +50 -0
  8. package/dist/_types/gen/types.gen.d.ts +374 -10
  9. package/dist/_types/index.d.ts +1 -0
  10. package/dist/_types/runtime/access-reason.d.ts +20 -0
  11. package/dist/chunk-2YAA3AF4.js +10437 -0
  12. package/dist/chunk-2YAA3AF4.js.map +1 -0
  13. package/dist/index.cjs +444 -66
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.js +60 -10106
  16. package/dist/index.js.map +1 -1
  17. package/dist/organization/index.cjs +70 -5
  18. package/dist/organization/index.cjs.map +1 -1
  19. package/dist/organization/index.js +57 -5
  20. package/dist/organization/index.js.map +1 -1
  21. package/dist/playground/index.cjs +81 -0
  22. package/dist/playground/index.cjs.map +1 -0
  23. package/dist/playground/index.d.cts +1 -0
  24. package/dist/playground/index.d.ts +1 -0
  25. package/dist/playground/index.js +58 -0
  26. package/dist/playground/index.js.map +1 -0
  27. package/docs/recipes/access.md +456 -15
  28. package/docs/rules.md +92 -0
  29. package/package.json +12 -2
  30. package/src/gen/bindings.gen.ts +395 -40
  31. package/src/gen/client.gen.ts +104 -3
  32. package/src/gen/manifest.gen.ts +53 -28
  33. package/src/gen/organization/index.ts +1 -1
  34. package/src/gen/organization/queryOptions.gen.ts +87 -6
  35. package/src/gen/playground/connect.gen.ts +9 -0
  36. package/src/gen/playground/index.ts +4 -0
  37. package/src/gen/playground/queryOptions.gen.ts +86 -0
  38. 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": "61.2.0",
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:e136531e93f19a2cd0e3d8b282f4cb0037bfac84b281cea72e6bcb372d34925b"
212
+ "sdkContentHash": "sha256:965d6441024647f1461d4e50bedeba9d79dca805ea4668ca8c9ecedefc1c0d0a"
203
213
  }