@k2b/cloud 0.42.0 → 0.44.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 (352) hide show
  1. package/package.json +13 -5
  2. package/scripts/app-favicon.ts +7 -6
  3. package/scripts/build.ts +3 -0
  4. package/scripts/installable-assets.ts +61 -0
  5. package/scripts/preload.ts +3 -0
  6. package/src/_internal/app-access-path.ts +12 -0
  7. package/src/_internal/app-access-request.ts +13 -0
  8. package/src/_internal/app-access-resources.ts +141 -0
  9. package/src/_internal/app-event-catalog.ts +40 -0
  10. package/src/_internal/app-event-schemas.ts +127 -0
  11. package/src/_internal/application-package.ts +40 -0
  12. package/src/_internal/built-in-app-ids.json +27 -0
  13. package/src/_internal/capabilities.ts +185 -12
  14. package/src/_internal/capability-origin.ts +4 -0
  15. package/src/_internal/capability-streams.ts +10 -5
  16. package/src/_internal/define-app.ts +159 -45
  17. package/src/_internal/installable-assets.ts +90 -0
  18. package/src/_internal/installable-head.ts +22 -0
  19. package/src/_internal/object-store-area.ts +9 -0
  20. package/src/_internal/object-store-config.ts +101 -0
  21. package/src/_internal/object-store-key.ts +14 -0
  22. package/src/_internal/page-responses.ts +34 -3
  23. package/src/_internal/process-identity.ts +7 -2
  24. package/src/_internal/registry-validation.ts +15 -1
  25. package/src/_internal/registry.ts +44 -1
  26. package/src/_internal/runtime-context.ts +9 -1
  27. package/src/_internal/runtime-watcher.ts +8 -0
  28. package/src/_internal/status-preserving-ssr.ts +12 -1
  29. package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
  30. package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
  31. package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
  32. package/src/access/AppAccessEditor.tsx +380 -0
  33. package/src/access/PermissionEditor.browser-harness.tsx +22 -2
  34. package/src/access/PermissionEditor.tsx +386 -77
  35. package/src/access/PrincipalAvatar.tsx +63 -0
  36. package/src/access/PrincipalPicker.tsx +29 -8
  37. package/src/access/ResourceApiKeys.tsx +3 -4
  38. package/src/access/app-access-adapter.ts +51 -0
  39. package/src/access/app-access-messages.ts +172 -0
  40. package/src/access/messages.ts +14 -0
  41. package/src/access/ui.ts +4 -1
  42. package/src/account/Avatar.tsx +21 -3
  43. package/src/account/ProfilePictureDialog.tsx +699 -0
  44. package/src/account/profile-picture-messages.ts +269 -0
  45. package/src/account/ui.ts +8 -1
  46. package/src/ai/admin.ts +3 -0
  47. package/src/ai/app-frame.ts +12 -0
  48. package/src/ai/application-tasks.ts +101 -0
  49. package/src/ai/assistant-models.ts +126 -64
  50. package/src/ai/capabilities.ts +19 -4
  51. package/src/ai/capability-execution.ts +5 -2
  52. package/src/ai/chart-block.ts +63 -30
  53. package/src/ai/chat/chart-block.tsx +12 -3
  54. package/src/ai/chat/composer-adapter.ts +8 -7
  55. package/src/ai/chat/message-actions.tsx +1 -1
  56. package/src/ai/chat/message-utils.ts +2 -2
  57. package/src/ai/chat/presentation.tsx +22 -1
  58. package/src/ai/chat-quotas.ts +5 -4
  59. package/src/ai/chat-tasks.ts +2 -2
  60. package/src/ai/client/controller.ts +11 -10
  61. package/src/ai/client/projection.ts +3 -3
  62. package/src/ai/code-capability-routes.ts +1 -0
  63. package/src/ai/compaction.ts +3 -3
  64. package/src/ai/credentials.ts +13 -114
  65. package/src/ai/default-tool-contracts.ts +4 -3
  66. package/src/ai/dictation-runtime.ts +4 -4
  67. package/src/ai/enrich.ts +2 -2
  68. package/src/ai/executor.ts +38 -27
  69. package/src/ai/http.ts +17 -29
  70. package/src/ai/index.ts +11 -5
  71. package/src/ai/inference-calls.ts +6 -4
  72. package/src/ai/maintenance.ts +1 -1
  73. package/src/ai/memory-learning-runs.ts +5 -5
  74. package/src/ai/memory-learning.ts +3 -3
  75. package/src/ai/message-queue.ts +1 -2
  76. package/src/ai/migrate.ts +75 -42
  77. package/src/ai/model-access-migrate.ts +3 -17
  78. package/src/ai/model-access.ts +44 -85
  79. package/src/ai/model-card.tsx +134 -0
  80. package/src/ai/model-catalog.ts +359 -0
  81. package/src/ai/model-chooser-messages.ts +88 -0
  82. package/src/ai/model-chooser.tsx +577 -0
  83. package/src/ai/model-delete.ts +234 -0
  84. package/src/ai/model-fixture.ts +39 -0
  85. package/src/ai/model-icon.tsx +36 -0
  86. package/src/ai/model-overview.ts +53 -0
  87. package/src/ai/model-usage.ts +45 -0
  88. package/src/ai/openai-api-messages.ts +0 -4
  89. package/src/ai/openai-api-slots.ts +10 -2
  90. package/src/ai/openai-api.ts +83 -16
  91. package/src/ai/personal-agent.ts +1 -1
  92. package/src/ai/prefs.ts +12 -10
  93. package/src/ai/projects-routes.ts +2 -1
  94. package/src/ai/projects.ts +13 -12
  95. package/src/ai/protocol.ts +2 -2
  96. package/src/ai/provider-connections.ts +161 -0
  97. package/src/ai/provider-errors.ts +138 -0
  98. package/src/ai/provider-fetch.ts +4 -0
  99. package/src/ai/provider-retry.ts +1 -1
  100. package/src/ai/provider.ts +2 -1
  101. package/src/ai/quota-limits.ts +36 -0
  102. package/src/ai/quota-provider.ts +11 -2
  103. package/src/ai/quota-report.ts +7 -4
  104. package/src/ai/quotas-migrate.ts +1 -0
  105. package/src/ai/quotas.ts +19 -8
  106. package/src/ai/recent-resources.ts +6 -0
  107. package/src/ai/request-headers.ts +14 -87
  108. package/src/ai/resource-refs.ts +6 -2
  109. package/src/ai/routes.ts +17 -18
  110. package/src/ai/runtime.ts +50 -22
  111. package/src/ai/settings.ts +93 -580
  112. package/src/ai/store.ts +45 -23
  113. package/src/ai/stream.ts +1 -1
  114. package/src/ai/structured-runs.ts +2 -2
  115. package/src/ai/structured.ts +29 -46
  116. package/src/ai/task-contracts.ts +25 -5
  117. package/src/ai/task-execution.ts +15 -6
  118. package/src/ai/task-model.ts +9 -0
  119. package/src/ai/transcription.ts +37 -21
  120. package/src/ai/turn-failure.ts +24 -2
  121. package/src/ai/types.ts +50 -62
  122. package/src/ai/ui.tsx +13 -0
  123. package/src/ai/usage.ts +8 -8
  124. package/src/ai/validate.ts +8 -7
  125. package/src/ai/view-block.ts +2 -2
  126. package/src/api/accounts-entities.ts +40 -16
  127. package/src/api/admin-ai-quotas.ts +34 -56
  128. package/src/api/admin-ai-usage.ts +1 -1
  129. package/src/api/admin-ai.ts +173 -0
  130. package/src/api/admin-core-settings.ts +68 -244
  131. package/src/api/admin-directory-shares.ts +55 -0
  132. package/src/api/admin-identity.ts +81 -2
  133. package/src/api/app-access.ts +85 -0
  134. package/src/api/app-approval.ts +3 -1
  135. package/src/api/app-event-delivery.ts +51 -0
  136. package/src/api/app-events.ts +123 -0
  137. package/src/api/app-resource-access.ts +48 -0
  138. package/src/api/auth/account-mismatch.ts +24 -0
  139. package/src/api/auth.ts +26 -3
  140. package/src/api/capabilities.ts +127 -15
  141. package/src/api/capability-streams.ts +2 -5
  142. package/src/api/identity-child-invocation.ts +145 -0
  143. package/src/api/index.ts +11 -2
  144. package/src/api/mcp.ts +180 -5
  145. package/src/api/me.ts +38 -17
  146. package/src/api/pwa-phone.ts +87 -48
  147. package/src/api/pwa.ts +34 -8
  148. package/src/api/resource-links.ts +83 -6
  149. package/src/api/resource-resolve.ts +50 -0
  150. package/src/api/search/schemas.ts +3 -1
  151. package/src/api/search.ts +9 -7
  152. package/src/api/widgets.ts +1 -0
  153. package/src/app-pwa/authority.ts +75 -0
  154. package/src/app-pwa/contracts.ts +177 -0
  155. package/src/app-pwa/core-query-error.ts +20 -0
  156. package/src/app-pwa/index.ts +23 -0
  157. package/src/app-pwa/protocol.ts +27 -0
  158. package/src/app-pwa/schema.ts +59 -0
  159. package/src/app-pwa/service.ts +861 -0
  160. package/src/app-pwa/transaction.ts +338 -0
  161. package/src/app-pwa/transport-input.ts +57 -0
  162. package/src/app-pwa/transport.ts +235 -0
  163. package/src/approvals/messages.ts +2 -2
  164. package/src/browser/app-access-recovery.ts +44 -0
  165. package/src/browser/app-session.ts +14 -3
  166. package/src/browser/cloud-id.ts +86 -0
  167. package/src/browser/live-websocket.ts +5 -0
  168. package/src/browser/pwa-drafts.ts +57 -0
  169. package/src/browser/resource-clipboard.ts +4 -0
  170. package/src/browser/resource-links.ts +52 -0
  171. package/src/browser/resource-picker.tsx +3 -2
  172. package/src/capabilities/calendar-sources.ts +302 -0
  173. package/src/capabilities/executions.ts +10 -3
  174. package/src/capabilities/migrate.ts +4 -0
  175. package/src/capabilities/nested.ts +43 -0
  176. package/src/capabilities/server-invoke.ts +202 -0
  177. package/src/capabilities/server.ts +26 -187
  178. package/src/capabilities/testing.ts +5 -2
  179. package/src/cli/access.ts +6 -5
  180. package/src/cli/account.ts +57 -0
  181. package/src/cli/admin/agents.ts +66 -0
  182. package/src/cli/admin/ai-models.ts +199 -0
  183. package/src/cli/admin/ai-quotas.ts +6 -102
  184. package/src/cli/admin/ai-usage.ts +4 -4
  185. package/src/cli/admin/app-credentials.ts +5 -3
  186. package/src/cli/admin/index.ts +7 -3
  187. package/src/cli/app-access.ts +60 -0
  188. package/src/cli/apps.ts +89 -26
  189. package/src/cli/avatar.ts +64 -0
  190. package/src/cli/capabilities.ts +22 -1
  191. package/src/cli/events.ts +320 -0
  192. package/src/cli/index.ts +1 -1
  193. package/src/config/env.ts +40 -0
  194. package/src/contracts/app-access.ts +52 -5
  195. package/src/contracts/app-approval.ts +8 -1
  196. package/src/contracts/app-details.ts +89 -0
  197. package/src/contracts/app-events.ts +51 -0
  198. package/src/contracts/app.ts +24 -5
  199. package/src/contracts/calendar-source.ts +274 -0
  200. package/src/contracts/calendar-view.ts +35 -0
  201. package/src/contracts/capabilities.ts +164 -3
  202. package/src/contracts/geo-admin.ts +2 -0
  203. package/src/contracts/geo-route.ts +0 -11
  204. package/src/contracts/geo.ts +14 -2
  205. package/src/contracts/index.ts +6 -0
  206. package/src/contracts/installable.ts +40 -0
  207. package/src/contracts/notification-types.ts +36 -9
  208. package/src/contracts/profile.ts +32 -0
  209. package/src/contracts/pwa-builds.ts +14 -0
  210. package/src/contracts/pwa-location.ts +38 -0
  211. package/src/contracts/pwa.ts +24 -3
  212. package/src/contracts/registry.ts +8 -0
  213. package/src/contracts/resource-clipboard.ts +35 -0
  214. package/src/contracts/resource-pages.ts +140 -0
  215. package/src/contracts/resource-resolution.ts +13 -0
  216. package/src/contracts/shared.ts +4 -0
  217. package/src/contracts/user-notifications.ts +2 -0
  218. package/src/events/app-event-delivery.ts +100 -0
  219. package/src/events/app-events.testing.ts +65 -0
  220. package/src/events/app-events.ts +134 -0
  221. package/src/events/core-user-created.ts +24 -0
  222. package/src/events/index.ts +9 -0
  223. package/src/events/testing.ts +2 -0
  224. package/src/index.ts +2 -1
  225. package/src/server/api/respond.ts +8 -3
  226. package/src/server/index.ts +1 -0
  227. package/src/server/middleware/app-access.ts +7 -6
  228. package/src/server/middleware/auth.ts +168 -66
  229. package/src/server/middleware/index.ts +1 -0
  230. package/src/server/middleware/invocation.ts +4 -2
  231. package/src/server/middleware/workload.ts +3 -3
  232. package/src/server/oauth-scope.ts +10 -0
  233. package/src/server/public-storage.ts +106 -0
  234. package/src/server/services/access.ts +8 -0
  235. package/src/services/accounts/app.ts +35 -3
  236. package/src/services/accounts/avatar.ts +342 -35
  237. package/src/services/accounts/base-group.ts +4 -5
  238. package/src/services/accounts/directory-shares.ts +157 -0
  239. package/src/services/accounts/entities.ts +50 -6
  240. package/src/services/accounts/groups.ts +3 -1
  241. package/src/services/accounts/identities.ts +64 -12
  242. package/src/services/accounts/local-groups.ts +1 -1
  243. package/src/services/accounts/users.ts +23 -15
  244. package/src/services/app-access-management.ts +519 -0
  245. package/src/services/app-access.ts +25 -7
  246. package/src/services/app-approval.ts +15 -4
  247. package/src/services/app-details.ts +224 -0
  248. package/src/services/app-discovery.ts +2 -1
  249. package/src/services/app-event-shapes.ts +139 -0
  250. package/src/services/app-events/accept.ts +116 -0
  251. package/src/services/app-events/admin.ts +250 -0
  252. package/src/services/app-events/declarations.ts +187 -0
  253. package/src/services/app-events/deliver.ts +218 -0
  254. package/src/services/app-events.ts +92 -0
  255. package/src/services/config-conflict.ts +11 -0
  256. package/src/services/core-user-events.ts +1 -0
  257. package/src/services/freeipa-config.ts +9 -0
  258. package/src/services/geo/admin.ts +4 -1
  259. package/src/services/geo/ledger.ts +47 -14
  260. package/src/services/geo/route-chain.ts +40 -18
  261. package/src/services/geo/route-estimate.ts +19 -0
  262. package/src/services/geo/route-plan.ts +0 -2
  263. package/src/services/geo/route-service.ts +57 -39
  264. package/src/services/geo/service.ts +51 -25
  265. package/src/services/identity/core-origin.ts +12 -0
  266. package/src/services/identity/index.ts +2 -1
  267. package/src/services/identity/invocation-actor.ts +50 -2
  268. package/src/services/identity/invocation-authority.ts +38 -1
  269. package/src/services/identity/invocation-token.ts +255 -50
  270. package/src/services/identity/issue-invocation.ts +9 -1
  271. package/src/services/identity/models-only-auth.ts +13 -4
  272. package/src/services/identity/nested-invocation.ts +128 -0
  273. package/src/services/image-metadata.ts +65 -1
  274. package/src/services/index.ts +24 -1
  275. package/src/services/ipa/sync.ts +6 -2
  276. package/src/services/ipa/users.ts +4 -1
  277. package/src/services/mandates/policy.ts +19 -12
  278. package/src/services/notifications/browser.ts +4 -0
  279. package/src/services/notifications/catalog.ts +5 -2
  280. package/src/services/notifications/platform.ts +7 -2
  281. package/src/services/notifications/user.ts +18 -7
  282. package/src/services/oauth-tokens.ts +28 -2
  283. package/src/services/object-store.ts +217 -0
  284. package/src/services/providers/local/users.ts +5 -0
  285. package/src/services/pwa-devices.ts +147 -37
  286. package/src/services/service-account-credentials.ts +5 -2
  287. package/src/services/session/index.ts +14 -0
  288. package/src/services/settings/core-settings.ts +91 -47
  289. package/src/services/settings/index.ts +8 -0
  290. package/src/services/settings/versions.ts +26 -0
  291. package/src/shared/ai-models.ts +274 -0
  292. package/src/shared/ai-quotas.ts +2 -2
  293. package/src/shared/ai-usage.ts +2 -2
  294. package/src/shared/index.ts +2 -0
  295. package/src/shared-text/documents.ts +25 -8
  296. package/src/shared-text/log.ts +1 -1
  297. package/src/shared-text/text-diff.ts +75 -0
  298. package/src/ssr/AdminArea.tsx +131 -0
  299. package/src/ssr/AdminLayout.tsx +11 -3
  300. package/src/ssr/AdminSidebar.tsx +7 -7
  301. package/src/ssr/AppAccessDenied.tsx +114 -0
  302. package/src/ssr/AppBadge.tsx +14 -0
  303. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  304. package/src/ssr/AppPwaRuntime.island.tsx +82 -0
  305. package/src/ssr/CloudShell.island.tsx +25 -4
  306. package/src/ssr/Layout.tsx +15 -7
  307. package/src/ssr/PwaLayout.tsx +73 -8
  308. package/src/ssr/PwaPartCount.island.tsx +23 -0
  309. package/src/ssr/PwaRuntime.island.tsx +85 -6
  310. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  311. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  312. package/src/ssr/ShellRuntime.island.tsx +5 -0
  313. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  314. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
  315. package/src/ssr/admin-active-link.ts +16 -0
  316. package/src/ssr/admin-navigation.ts +26 -20
  317. package/src/ssr/app-badges.ts +31 -7
  318. package/src/ssr/app-pwa-document.tsx +16 -0
  319. package/src/ssr/app-pwa-messages.ts +17 -0
  320. package/src/ssr/index.ts +8 -0
  321. package/src/ssr/layout-context.ts +1 -1
  322. package/src/ssr/page-trail-runtime.ts +3 -1
  323. package/src/ssr/platform-messages.ts +9 -1
  324. package/src/ssr/pwa-messages.ts +10 -2
  325. package/src/ssr/rail-navigation.ts +5 -1
  326. package/src/ssr/settings-draft.ts +153 -0
  327. package/src/ssr/shell-data.tsx +2 -0
  328. package/src/styles/global.css +1 -0
  329. package/src/styles/profile-picture.css +270 -0
  330. package/src/styles/utilities-navigation.css +6 -0
  331. package/src/workflows/ai/policy.ts +49 -0
  332. package/src/workflows/ai/runtime.ts +131 -21
  333. package/src/workflows/ai/store.ts +283 -35
  334. package/src/workflows/ai/types.ts +8 -2
  335. package/src/workflows/ai-actions.ts +148 -120
  336. package/src/workflows/authoring.ts +31 -0
  337. package/src/workflows/contracts.ts +1 -0
  338. package/src/workflows/definition.ts +2 -0
  339. package/src/workflows/editor.ts +15 -0
  340. package/src/workflows/index.ts +2 -1
  341. package/src/workflows/language/compiler.ts +34 -3
  342. package/src/workflows/runtime/executor.ts +132 -33
  343. package/src/workflows/runtime/index.ts +2 -0
  344. package/src/workflows/runtime/ports.ts +5 -1
  345. package/src/workflows/store/actions.ts +1 -1
  346. package/src/workflows/store/definitions.ts +5 -0
  347. package/src/workflows/store/runs.ts +5 -3
  348. package/src/workflows/store/worker.ts +7 -2
  349. package/src/ai/model-pricing.ts +0 -43
  350. package/src/ai/model-request-settings.ts +0 -98
  351. package/src/ai/settings-messages.ts +0 -129
  352. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -1,12 +1,13 @@
1
1
  import { mutation } from "@k2b/stdlib/solid";
2
- import { announce, IconButton, Placeholder, prompts, SelectChip, Tooltip, useLocale } from "@k2b/ui";
3
- import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js";
2
+ import { announce, Button, IconButton, Placeholder, prompts, Select, SelectChip, Tooltip, useLocale } from "@k2b/ui";
3
+ import { createEffect, createMemo, createSignal, For, on, onCleanup, Show } from "solid-js";
4
4
  import { createStore, reconcile } from "solid-js/store";
5
5
  import { CloudAvatar } from "../account/Avatar";
6
- import type { AccessEntry, PermissionLevel, Principal, ServiceAccountKind } from "../contracts/shared";
6
+ import type { AccessEntry, PermissionLevel, Principal, ServiceAccountKind, ServiceAccountStatus } from "../contracts/shared";
7
7
  import { createGroupCoverage, type GroupCoverage, GroupCoveragePanel, GroupCoverageToggle } from "./GroupCoverage";
8
8
  import { isManagerEntry } from "./managers";
9
9
  import { accessMessages } from "./messages";
10
+ import PrincipalAvatar from "./PrincipalAvatar";
10
11
  import PrincipalPicker, { principalKey } from "./PrincipalPicker";
11
12
  import { principalLabel } from "./principal-label";
12
13
  import { serviceAccountKindDisplay } from "./service-account-kind";
@@ -27,6 +28,13 @@ export type GrantableLevel = Exclude<PermissionLevel, "none">;
27
28
  export type AllowedLevel = GrantableLevel | { level: GrantableLevel; scope?: string; label?: string; icon?: string; description?: string };
28
29
 
29
30
  type PermissionEditorProps = {
31
+ /** Expanded form: choose a principal and level before adding; show durable inline feedback. */
32
+ expanded?: boolean;
33
+ /** Mark these entries as the viewer's own grants in the expanded form. */
34
+ ownEntryIds?: readonly string[];
35
+ /** Confirm a consequential change. When supplied, replaces the default removal confirmation. */
36
+ beforeChange?: (entry: AccessEntry, next: GrantableLevel | null) => Promise<boolean>;
37
+
30
38
  /** The editor mounts from this list and merges later lists by entry id once no change is saving.
31
39
  * Rows the caller changed since its last list win; unchanged rows keep the editor's confirmed result.
32
40
  * Keep the editor mounted (no keyed Show or unmount during refresh) and pass refetched data. */
@@ -34,7 +42,10 @@ type PermissionEditorProps = {
34
42
 
35
43
  /** Whether the current user can edit permissions. When `false`, the
36
44
  * editor renders the entries read-only — no row dropdowns, no
37
- * delete buttons, no add form. */
45
+ * delete buttons, no add form. When it becomes `true` again, failed
46
+ * changes and their retries are cleared, so re-enable editing only
47
+ * after the list passed in is canonical again. In expanded mode the caller
48
+ * explains why editing stopped; row errors leave with editing. */
38
49
  canEdit?: boolean;
39
50
 
40
51
  /** Grant access. The caller closes over the resource id. The optional
@@ -70,9 +81,22 @@ type PermissionEditorProps = {
70
81
  * true; resources with an explicit principal allowlist can disable it. */
71
82
  allowAuthenticated?: boolean;
72
83
 
73
- /** Allow granting service accounts. Off by default so ordinary
74
- * permission pickers stay user/group focused. */
84
+ /** Allow granting service accounts. Defaults to true, including agents.
85
+ * Disable only when the resource documents a user-backed identity requirement. */
75
86
  allowServiceAccounts?: boolean;
87
+ /** Optional directory filters for the resource's accepted credential kinds. */
88
+ serviceAccountKinds?: ServiceAccountKind[];
89
+ /** Grant selection defaults to active accounts; existing entries are unaffected. */
90
+ serviceAccountStatus?: ServiceAccountStatus;
91
+
92
+ /** The principal search's placeholder, in the consumer's terms. Defaults to the principal kinds it offers. */
93
+ placeholder?: string;
94
+
95
+ /** The consumer's name and description for every signed-in person, in rows and in the principal search. */
96
+ signedIn?: { label: string; description: string };
97
+
98
+ /** Expanded form: a row's subtitle instead of its principal kind, such as a group's member count. */
99
+ entryDetail?: (entry: AccessEntry) => string | undefined;
76
100
 
77
101
  /** Which levels the UI offers — and what they're called. Bare strings
78
102
  * use the default labels (View / Edit / Manage). Objects override
@@ -172,6 +196,21 @@ export default function PermissionEditor(props: PermissionEditorProps) {
172
196
  const initialEntries = props.initialEntries;
173
197
  let appliedCallerEntries = new Map(initialEntries.map((entry) => [entry.id, JSON.stringify(entry)]));
174
198
  const [entries, setEntries] = createStore<AccessEntry[]>(initialEntries.map((entry) => ({ ...entry })));
199
+ type Draft = {
200
+ principal: Principal;
201
+ display: { displayName: string; avatarHash?: string | null; serviceAccountKind?: ServiceAccountKind };
202
+ };
203
+ const [draft, setDraft] = createSignal<Draft>();
204
+ const [draftLevel, setDraftLevel] = createSignal("read");
205
+ const [feedback, setFeedback] = createStore<Record<string, { message: string; retry?: () => void } | undefined>>({});
206
+ const failure = (id: string, error: unknown, retry: () => void) => {
207
+ const message = error instanceof Error ? error.message : String(error);
208
+ if (props.expanded) setFeedback(id, { message, retry });
209
+ else prompts.error(message);
210
+ };
211
+ const saved = (id: string) => {
212
+ if (props.expanded) setFeedback(id, { message: t().saved });
213
+ };
175
214
  const [pending, setPending] = createSignal<string | null>(null);
176
215
  const grantPending = () => pending() !== null;
177
216
  let disposed = false;
@@ -183,6 +222,8 @@ export default function PermissionEditor(props: PermissionEditorProps) {
183
222
  const allowAuthenticated = () => props.allowAuthenticated !== false;
184
223
  const allowed = (principal: Principal) =>
185
224
  resolveAllowedLevels(typeof props.allowedLevels === "function" ? props.allowedLevels(principal) : props.allowedLevels, t());
225
+ const labelOf = (entry: AccessEntry) =>
226
+ entry.principal.type === "authenticated" && props.signedIn ? props.signedIn.label : principalLabel(entry, locale());
186
227
 
187
228
  // Defensive dev-warning: an empty allowedLevels array makes the editor
188
229
  // unable to grant anything.
@@ -197,14 +238,25 @@ export default function PermissionEditor(props: PermissionEditorProps) {
197
238
  const grant = async (
198
239
  principal: Principal,
199
240
  display: { displayName: string; avatarHash?: string | null; serviceAccountKind?: ServiceAccountKind },
241
+ choice?: ResolvedLevel,
200
242
  ) => {
201
- if (disposed || grantPending() || updateMut.loading() || revokeMut.loading()) return;
243
+ if (!canStart()) return;
202
244
  const key = principalKey(principal);
203
- if (entries.some((entry) => principalKey(entry.principal) === key)) return;
204
- const first = allowed(principal)[0];
245
+ if (entries.some((entry) => principalKey(entry.principal) === key)) {
246
+ // A retry or selection for a principal already in the list has nothing left to add.
247
+ setFeedback("add", undefined);
248
+ if (draft() && principalKey(draft()!.principal) === key) {
249
+ setDraft(undefined);
250
+ if (props.expanded) focusPrincipalPicker();
251
+ }
252
+ return;
253
+ }
254
+ const first = choice ?? allowed(principal)[0];
205
255
  if (!first) return;
206
256
  const id = `pending:${key}`;
257
+ let granted = false;
207
258
  setPending(id);
259
+ setFeedback("add", undefined);
208
260
  setEntries(entries.length, {
209
261
  id,
210
262
  principal,
@@ -224,15 +276,19 @@ export default function PermissionEditor(props: PermissionEditorProps) {
224
276
  serviceAccountKind: entry.serviceAccountKind ?? display.serviceAccountKind,
225
277
  };
226
278
  if (index !== -1) setEntries(index, reconcile(saved));
227
- announce(t().accessGranted({ name: principalLabel(saved, locale()) }));
279
+ announce(t().accessGranted({ name: labelOf(saved) }));
280
+ setDraft(undefined);
281
+ granted = true;
282
+ if (props.expanded) setFeedback(saved.id, { message: t().saved });
228
283
  } catch (err) {
229
284
  if (!disposed) setEntries((current) => current.filter((entry) => entry.id !== id));
230
285
  // Prompts are global, so a grant that fails after its dialog closed still says so.
231
286
  if (!(typeof err === "object" && err !== null && "name" in err && err.name === "AbortError")) {
232
- prompts.error(err instanceof Error ? err.message : String(err));
287
+ failure("add", err, () => void grant(principal, display, first));
233
288
  }
234
289
  } finally {
235
290
  if (!disposed) setPending(null);
291
+ if (props.expanded && granted) focusPrincipalPicker();
236
292
  }
237
293
  };
238
294
 
@@ -244,18 +300,28 @@ export default function PermissionEditor(props: PermissionEditorProps) {
244
300
  const change = updating();
245
301
  return change?.accessId === accessId ? change : undefined;
246
302
  };
247
- const updateMut = mutation.create<LevelChange, LevelChange>({
303
+ const updateMut = mutation.create<LevelChange | null, LevelChange>({
248
304
  mutation: async (data) => {
305
+ const entry = entries.find((entry) => entry.id === data.accessId);
306
+ if (!entry || disposed || !canEdit()) return null;
307
+ if (props.beforeChange && !(await props.beforeChange(entry, data.permission))) return null;
308
+ if (disposed || !canEdit()) return null;
249
309
  setUpdating(data);
310
+ setFeedback(data.accessId, undefined);
250
311
  await props.updateAccess(data.accessId, data.permission, data.scope);
251
312
  return data;
252
313
  },
253
314
  onSuccess: (result) => {
254
315
  if (result) {
255
316
  setEntries((entry) => entry.id === result.accessId, { permission: result.permission, scope: result.scope });
317
+ saved(result.accessId);
256
318
  }
257
319
  },
258
- onError: (err) => prompts.error(err.message),
320
+ onError: (err) => {
321
+ const change = updating();
322
+ if (change) failure(change.accessId, err, () => startUpdate(change));
323
+ else prompts.error(err.message);
324
+ },
259
325
  onFinally: () => {
260
326
  if (!disposed) setUpdating(null);
261
327
  },
@@ -263,25 +329,63 @@ export default function PermissionEditor(props: PermissionEditorProps) {
263
329
 
264
330
  const revokeMut = mutation.create<string | null, AccessEntry>({
265
331
  mutation: async (entry) => {
266
- const displayName = principalLabel(entry, locale());
267
- const confirmed = await prompts.confirm(t().removeAccessConfirm({ name: displayName }), {
268
- title: t().removeAccess,
269
- variant: "danger",
270
- });
271
- if (!confirmed) return null;
332
+ if (disposed || !canEdit()) return null;
333
+ const displayName = labelOf(entry);
334
+ const confirmed = props.beforeChange
335
+ ? await props.beforeChange(entry, null)
336
+ : await prompts.confirm(t().removeAccessConfirm({ name: displayName }), {
337
+ title: t().removeAccess,
338
+ variant: "danger",
339
+ });
340
+ if (!confirmed || disposed || !canEdit()) return null;
272
341
  setRevoking(entry.id);
342
+ setFeedback(entry.id, undefined);
273
343
  await props.revokeAccess(entry.id);
274
344
  return entry.id;
275
345
  },
276
346
  onSuccess: (accessId) => {
277
- if (accessId) setEntries((current) => current.filter((entry) => entry.id !== accessId));
347
+ if (!accessId) return;
348
+ const index = entries.findIndex((entry) => entry.id === accessId);
349
+ setEntries((current) => current.filter((entry) => entry.id !== accessId));
350
+ focusAfterRemoval(index);
351
+ },
352
+ onError: (err) => {
353
+ const entry = entries.find((entry) => entry.id === revoking());
354
+ if (entry) failure(entry.id, err, () => startRevoke(entry));
355
+ else prompts.error(err.message);
278
356
  },
279
- onError: (err) => prompts.error(err.message),
280
357
  onFinally: () => {
281
358
  if (!disposed) setRevoking(null);
282
359
  },
283
360
  });
284
- const busy = () => grantPending() || updateMut.loading() || revokeMut.loading();
361
+ // Confirmation holds the operation lock, but its opener must stay focusable until the dialog returns focus.
362
+ const saving = () => grantPending() || updating() !== null || revoking() !== null;
363
+ const busy = () => saving() || updateMut.loading() || revokeMut.loading();
364
+ const canStart = () => !disposed && canEdit() && !busy();
365
+ const startUpdate = (change: LevelChange): void => {
366
+ if (canStart()) void updateMut.mutate(change);
367
+ };
368
+ const startRevoke = (entry: AccessEntry): void => {
369
+ if (canStart()) void revokeMut.mutate(entry);
370
+ };
371
+ let editorRoot: HTMLDivElement | undefined;
372
+ let principalPickerHost: HTMLDivElement | undefined;
373
+ const focusPrincipalPicker = () => {
374
+ queueMicrotask(() => {
375
+ if (!disposed && canEdit() && !busy()) principalPickerHost?.querySelector<HTMLInputElement>('input[role="combobox"]')?.focus();
376
+ });
377
+ };
378
+ // A removed row takes its focused button along; continue at the neighbouring row, else the principal search.
379
+ const focusAfterRemoval = (index: number) => {
380
+ queueMicrotask(() => {
381
+ if (disposed || busy()) return;
382
+ const active = document.activeElement;
383
+ if (active && active !== document.body && active.isConnected) return;
384
+ const removes = Array.from(editorRoot?.querySelectorAll<HTMLButtonElement>("button[data-access-remove]") ?? []);
385
+ const next = [removes[index], removes[index - 1]].find((button) => button && !button.disabled);
386
+ (next ?? editorRoot?.querySelector<HTMLInputElement>('input[role="combobox"]:enabled'))?.focus();
387
+ });
388
+ };
285
389
  createEffect(() => {
286
390
  const next = props.initialEntries;
287
391
  const nextCallerEntries = new Map(next.map((entry) => [entry.id, JSON.stringify(entry)]));
@@ -305,6 +409,16 @@ export default function PermissionEditor(props: PermissionEditorProps) {
305
409
  setEntries(reconcile(merged, { key: "id" }));
306
410
  appliedCallerEntries = nextCallerEntries;
307
411
  });
412
+ // Editing returns once the caller has the canonical list again, even an unchanged one: earlier failures and their
413
+ // retries no longer apply. Rows, confirmations and an unrelated selection stay.
414
+ createEffect(
415
+ on(canEdit, (editable, wasEditable) => {
416
+ if (!editable || wasEditable !== false) return;
417
+ for (const [id, state] of Object.entries(feedback)) if (state?.retry) setFeedback(id, undefined);
418
+ const selection = draft();
419
+ if (selection && entries.some((entry) => principalKey(entry.principal) === principalKey(selection.principal))) setDraft(undefined);
420
+ }),
421
+ );
308
422
  // The service refuses to remove the last manager; the row says so up front.
309
423
  const lastManagerId = createMemo(() => {
310
424
  // A grant on part of the resource never manages it.
@@ -314,31 +428,127 @@ export default function PermissionEditor(props: PermissionEditorProps) {
314
428
  });
315
429
 
316
430
  return (
317
- <div class="flex flex-col gap-3">
431
+ <div
432
+ ref={editorRoot}
433
+ class={props.expanded ? "flex flex-col gap-4" : "flex flex-col gap-3"}
434
+ style={
435
+ props.expanded
436
+ ? { "--k2b-field-surface": "var(--k2b-surface)", "--k2b-field-border": "var(--k2b-border)", "--k2b-field-font-size": "15px" }
437
+ : undefined
438
+ }
439
+ >
440
+ <Show when={props.expanded && canEdit()}>
441
+ {/* Every control in the row stretches to the 36px field height, including the selection that replaces the search. */}
442
+ <div class="grid grid-cols-[minmax(0,1fr)_auto] gap-2 sm:grid-cols-[minmax(0,1fr)_176px_auto]">
443
+ <div ref={principalPickerHost} class="col-span-2 flex min-w-0 flex-col sm:col-span-1">
444
+ <Show
445
+ when={draft()}
446
+ fallback={
447
+ <PrincipalPicker
448
+ existing={entries.map((entry) => entry.principal)}
449
+ allowPublic={allowPublic()}
450
+ allowAuthenticated={allowAuthenticated()}
451
+ allowServiceAccounts={props.allowServiceAccounts}
452
+ placeholder={props.placeholder}
453
+ signedIn={props.signedIn}
454
+ disabled={saving()}
455
+ onSelect={(principal, display) => {
456
+ if (!canStart()) return;
457
+ setDraft({ principal, display });
458
+ setDraftLevel(allowed(principal)[0]?.key ?? "read");
459
+ }}
460
+ />
461
+ }
462
+ >
463
+ {(selection) => (
464
+ <Button
465
+ variant="input"
466
+ style={{ "font-size": "15px" }}
467
+ align="start"
468
+ disabled={saving()}
469
+ class="w-full font-normal"
470
+ aria-label={t().clearSelection({ name: selection().display.displayName })}
471
+ onClick={() => {
472
+ if (!canStart()) return;
473
+ setDraft(undefined);
474
+ focusPrincipalPicker();
475
+ }}
476
+ >
477
+ <span class="min-w-0 flex-1 truncate">{selection().display.displayName}</span>
478
+ <i class="ti ti-x" aria-hidden="true" />
479
+ </Button>
480
+ )}
481
+ </Show>
482
+ </div>
483
+ <Select
484
+ aria-label={t().newPermission}
485
+ value={draftLevel}
486
+ disabled={saving()}
487
+ options={allowed(draft()?.principal ?? { type: "user", userId: "" }).map((option) => ({
488
+ value: option.key,
489
+ label: option.label,
490
+ }))}
491
+ onValueChange={(value) => {
492
+ if (value && canStart()) setDraftLevel(value);
493
+ }}
494
+ />
495
+ <Button
496
+ style={{ "font-size": "15px" }}
497
+ disabled={!draft() || saving()}
498
+ loading={grantPending()}
499
+ onClick={() => {
500
+ const selection = draft();
501
+ if (selection)
502
+ void grant(
503
+ selection.principal,
504
+ selection.display,
505
+ allowed(selection.principal).find((option) => option.key === draftLevel()),
506
+ );
507
+ }}
508
+ >
509
+ {t().add}
510
+ </Button>
511
+ </div>
512
+ <Show when={feedback.add}>
513
+ {(state) => (
514
+ <div role="alert" class="text-sm text-danger">
515
+ {state().message}{" "}
516
+ <Button variant="text" disabled={saving()} onClick={() => state().retry?.()}>
517
+ {t().retry}
518
+ </Button>
519
+ </div>
520
+ )}
521
+ </Show>
522
+ </Show>
318
523
  {/* Existing entries */}
319
- <div class="flex flex-col gap-1">
524
+ <div class={props.expanded ? "flex flex-col" : "flex flex-col gap-1"}>
320
525
  <For each={entries}>
321
526
  {(entry) => {
322
- // A row keeps its principal, so its group coverage is created once per row.
323
- const coverage = entry.principal.type === "group" ? createGroupCoverage(entry.principal.groupId) : undefined;
527
+ // A row keeps its principal, so its group coverage is created once per row. Expanded rows name the
528
+ // group's size in their subtitle instead.
529
+ const coverage = entry.principal.type === "group" && !props.expanded ? createGroupCoverage(entry.principal.groupId) : undefined;
324
530
  return (
325
531
  <AccessEntryRow
326
532
  entry={entry}
533
+ label={labelOf(entry)}
534
+ detail={props.expanded ? props.entryDetail?.(entry) : undefined}
535
+ signedInDescription={props.signedIn?.description}
536
+ expanded={props.expanded}
537
+ ownEntry={props.ownEntryIds?.includes(entry.id)}
538
+ feedback={feedback[entry.id]}
327
539
  coverage={coverage}
328
540
  canEdit={canEdit()}
329
541
  pending={entry.id === pending()}
330
542
  updating={updatingRow(entry.id)}
331
543
  revoking={entry.id === revoking()}
332
- disabled={busy()}
544
+ disabled={saving()}
333
545
  lastManager={entry.id === lastManagerId()}
334
546
  allowed={allowed(entry.principal)}
335
547
  singlePicker={allowed(entry.principal).length === 1}
336
548
  onUpdatePermission={(option) => {
337
- if (!busy()) void updateMut.mutate({ accessId: entry.id, permission: option.level, scope: option.scope });
338
- }}
339
- onRevoke={() => {
340
- if (!busy()) void revokeMut.mutate(entry);
549
+ startUpdate({ accessId: entry.id, permission: option.level, scope: option.scope });
341
550
  }}
551
+ onRevoke={() => startRevoke(entry)}
342
552
  />
343
553
  );
344
554
  }}
@@ -351,14 +561,18 @@ export default function PermissionEditor(props: PermissionEditorProps) {
351
561
  {/* Add access — single Combobox, granted at the lowest allowed
352
562
  level on pick. The user upgrades via the row pill if they want
353
563
  a higher level. KISS: one decision per step. */}
354
- <Show when={canEdit()}>
564
+ <Show when={canEdit() && !props.expanded}>
355
565
  <PrincipalPicker
356
566
  existing={entries.map((e) => e.principal)}
357
567
  allowPublic={allowPublic()}
358
568
  allowAuthenticated={allowAuthenticated()}
359
569
  allowServiceAccounts={props.allowServiceAccounts}
570
+ serviceAccountKinds={props.serviceAccountKinds}
571
+ serviceAccountStatus={props.serviceAccountStatus ?? "active"}
572
+ placeholder={props.placeholder}
573
+ signedIn={props.signedIn}
360
574
  readOnly={grantPending()}
361
- disabled={updateMut.loading() || revokeMut.loading()}
575
+ disabled={updating() !== null || revoking() !== null}
362
576
  onSelect={(principal, display) => void grant(principal, display)}
363
577
  />
364
578
  </Show>
@@ -372,6 +586,13 @@ export default function PermissionEditor(props: PermissionEditorProps) {
372
586
 
373
587
  function AccessEntryRow(props: {
374
588
  entry: AccessEntry;
589
+ label: string;
590
+ /** Replaces the expanded subtitle's principal kind. */
591
+ detail?: string;
592
+ signedInDescription?: string;
593
+ expanded?: boolean;
594
+ ownEntry?: boolean;
595
+ feedback?: { message: string; retry?: () => void };
375
596
  canEdit: boolean;
376
597
  /** Another change is saving: the row keeps its controls, disabled. */
377
598
  disabled: boolean;
@@ -394,7 +615,7 @@ function AccessEntryRow(props: {
394
615
  }) {
395
616
  const locale = useLocale();
396
617
  const t = () => accessMessages.resolve([locale()]).t;
397
- const displayName = () => principalLabel(props.entry, locale());
618
+ const displayName = () => props.label;
398
619
  const entryKey = () => levelKey(props.entry.permission, props.entry.scope);
399
620
  const display = () => resolveEntryDisplay(props.entry.permission, props.entry.scope, props.allowed, t());
400
621
  const manageLabel = () => resolveEntryDisplay("admin", undefined, props.allowed, t()).label;
@@ -420,37 +641,65 @@ function AccessEntryRow(props: {
420
641
 
421
642
  return (
422
643
  <div>
423
- <div class="group/access-row flex items-center gap-2 py-1.5" aria-busy={saving() ? "true" : undefined}>
424
- <Show
425
- when={props.entry.principal.type === "user"}
426
- fallback={
427
- <div class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-secondary dark:bg-zinc-700">
428
- <i class={`ti ${getPrincipalIcon(props.entry, t())} text-sm`} />
429
- </div>
430
- }
431
- >
432
- <CloudAvatar
433
- username={displayName()}
434
- userId={props.entry.principal.type === "user" ? props.entry.principal.userId : undefined}
435
- avatarHash={props.entry.avatarHash}
436
- size="xs"
437
- class="h-7 w-7"
438
- />
644
+ <div
645
+ class={
646
+ props.expanded
647
+ ? "group/access-row grid min-h-[52px] grid-cols-[32px_minmax(0,1fr)_36px] items-center gap-x-3 gap-y-1.5 rounded-[var(--ui-radius-control)] py-2 sm:flex sm:px-2 sm:py-1.5 sm:hover:bg-[var(--ui-surface-subtle)]"
648
+ : "group/access-row flex items-center gap-2 py-1.5"
649
+ }
650
+ aria-busy={saving() ? "true" : undefined}
651
+ >
652
+ <Show when={!props.expanded} fallback={<PrincipalAvatar entry={props.entry} label={displayName()} />}>
653
+ <Show
654
+ when={props.entry.principal.type === "user"}
655
+ fallback={
656
+ <div class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-secondary dark:bg-zinc-700">
657
+ <i class={`ti ${getPrincipalIcon(props.entry, t())} text-sm`} />
658
+ </div>
659
+ }
660
+ >
661
+ <CloudAvatar
662
+ username={displayName()}
663
+ userId={props.entry.principal.type === "user" ? props.entry.principal.userId : undefined}
664
+ avatarHash={props.entry.avatarHash}
665
+ size="xs"
666
+ class="h-7 w-7"
667
+ />
668
+ </Show>
439
669
  </Show>
440
670
 
441
671
  {/* Display name. Name and label share one line so every row keeps its height on phones:
442
672
  when both do not fit, each gets an equal share and the shorter one stays whole. A group's
443
673
  members toggle always stays whole; the name takes the rest. */}
444
674
  <div
445
- class="grid min-w-0 flex-1 auto-cols-[minmax(0,max-content)] grid-flow-col items-baseline gap-1"
675
+ class={
676
+ props.expanded ? "min-w-0 flex-1" : "grid min-w-0 flex-1 auto-cols-[minmax(0,max-content)] grid-flow-col items-baseline gap-1"
677
+ }
446
678
  classList={{ "grid-cols-[minmax(0,max-content)_max-content]": props.coverage !== undefined }}
447
679
  >
448
- <span class="truncate text-sm">{displayName()}</span>
680
+ <span class={props.expanded ? "block truncate text-[15px] font-medium" : "truncate text-sm"}>{displayName()}</span>
681
+ <Show when={props.expanded}>
682
+ <span class="block text-[13px] text-dimmed">
683
+ {props.detail ??
684
+ (props.entry.principal.type === "user"
685
+ ? t().person
686
+ : props.entry.principal.type === "group"
687
+ ? t().group
688
+ : props.entry.principal.type === "authenticated"
689
+ ? (props.signedInDescription ?? t().signedInDescription)
690
+ : props.entry.principal.type === "service_account"
691
+ ? (props.entry.serviceAccountKind && serviceAccountKindDisplay(props.entry.serviceAccountKind, t()).label) ||
692
+ t().standaloneServiceAccount
693
+ : "")}
694
+ <Show when={props.ownEntry}> · {t().you}</Show>
695
+ </span>
696
+ </Show>
449
697
  <Show when={props.entry.principal.type === "public"}>
450
698
  <span class="truncate text-xs text-dimmed">({t().anyoneWithLink})</span>
451
699
  </Show>
452
700
  <Show
453
701
  when={
702
+ !props.expanded &&
454
703
  props.entry.principal.type === "service_account" &&
455
704
  props.entry.serviceAccountKind &&
456
705
  serviceAccountKindDisplay(props.entry.serviceAccountKind, t()).label
@@ -463,58 +712,118 @@ function AccessEntryRow(props: {
463
712
  </Show>
464
713
  </div>
465
714
 
715
+ <Show when={props.expanded}>
716
+ <span class="col-start-3 row-start-2 w-5 text-center text-sm" role="status">
717
+ <Show
718
+ when={saving()}
719
+ fallback={
720
+ <Show when={props.feedback && !props.feedback.retry}>
721
+ <>
722
+ <i class="ti ti-check text-success" aria-hidden="true" />
723
+ <span class="sr-only">{t().saved}</span>
724
+ </>
725
+ </Show>
726
+ }
727
+ >
728
+ <>
729
+ <i class="ti ti-loader-2 animate-spin" aria-hidden="true" />
730
+ <span class="sr-only">{t().saving}</span>
731
+ </>
732
+ </Show>
733
+ </span>
734
+ </Show>
735
+
466
736
  {/* Permission badge — interactive single-value picker when editable,
467
737
  plain span otherwise. */}
468
- <Show when={hasPicker()} fallback={<span class={`${badgeClass} cursor-default`}>{badgeContent}</span>}>
469
- <SelectChip
470
- aria-label={t().permissionFor({ name: displayName() })}
471
- value={shownKey}
472
- disabled={props.disabled}
473
- loading={props.pending || props.updating !== undefined}
474
- options={props.allowed.map((option) => {
475
- const locked = props.lastManager && (option.level !== "admin" || option.scope !== undefined);
476
- return {
738
+ <div class={props.expanded ? "col-start-2 row-start-2 min-w-0 sm:w-44 sm:flex-none" : "contents"}>
739
+ <Show
740
+ when={props.expanded}
741
+ fallback={
742
+ <Show when={hasPicker()} fallback={<span class={`${badgeClass} cursor-default`}>{badgeContent}</span>}>
743
+ <SelectChip
744
+ aria-label={t().permissionFor({ name: displayName() })}
745
+ value={shownKey}
746
+ disabled={props.disabled}
747
+ loading={props.pending || props.updating !== undefined}
748
+ options={props.allowed.map((option) => {
749
+ const locked = props.lastManager && (option.level !== "admin" || option.scope !== undefined);
750
+ return {
751
+ value: option.key,
752
+ label: option.label,
753
+ icon: `ti ${option.icon}`,
754
+ disabled: locked,
755
+ description: locked ? t().lastManagerOption({ level: manageLabel() }) : option.description,
756
+ };
757
+ })}
758
+ icon={`ti ${shownDisplay().icon}`}
759
+ position="bottom-left"
760
+ onValueChange={(key) => {
761
+ const option = props.allowed.find((candidate) => candidate.key === key);
762
+ if (option && key !== entryKey()) props.onUpdatePermission(option);
763
+ }}
764
+ />
765
+ </Show>
766
+ }
767
+ >
768
+ <Select
769
+ aria-label={t().permissionFor({ name: displayName() })}
770
+ value={shownKey}
771
+ disabled={props.disabled || !props.canEdit || props.singlePicker}
772
+ options={props.allowed.map((option) => ({
477
773
  value: option.key,
478
774
  label: option.label,
479
- icon: `ti ${option.icon}`,
480
- disabled: locked,
481
- description: locked ? t().lastManagerOption({ level: manageLabel() }) : option.description,
482
- };
483
- })}
484
- icon={`ti ${shownDisplay().icon}`}
485
- position="bottom-left"
486
- onValueChange={(key) => {
487
- const option = props.allowed.find((candidate) => candidate.key === key);
488
- if (option && key !== entryKey()) props.onUpdatePermission(option);
489
- }}
490
- />
491
- </Show>
775
+ disabled: props.lastManager && option.level !== "admin",
776
+ }))}
777
+ onValueChange={(key) => {
778
+ const option = props.allowed.find((candidate) => candidate.key === key);
779
+ if (option && key !== entryKey()) props.onUpdatePermission(option);
780
+ }}
781
+ />
782
+ </Show>
783
+ </div>
492
784
 
493
785
  {/* Destructive row actions stay quiet until the row is engaged. They
494
786
  remain keyboard reachable and stay visible on touch-sized layouts. */}
495
787
  <Show when={props.canEdit}>
496
788
  <Tooltip.Anchor
497
789
  content={removeLabel()}
498
- class="shrink-0 opacity-100 transition-opacity sm:opacity-0 sm:group-hover/access-row:opacity-100 sm:group-focus-within/access-row:opacity-100"
790
+ class={
791
+ props.expanded
792
+ ? "col-start-3 row-start-1 shrink-0"
793
+ : "shrink-0 opacity-100 transition-opacity sm:opacity-0 sm:group-hover/access-row:opacity-100 sm:group-focus-within/access-row:opacity-100"
794
+ }
499
795
  >
500
796
  <IconButton
501
797
  type="button"
502
798
  label={t().remove({ name: displayName() })}
503
799
  variant="ghost"
504
- size="xs"
800
+ size={props.expanded ? "md" : "xs"}
505
801
  onClick={props.onRevoke}
506
802
  disabled={props.disabled || props.pending || props.lastManager}
507
803
  loading={props.revoking}
508
804
  aria-label={t().remove({ name: displayName() })}
509
805
  // A disabled button takes no focus and touch has no hover, so the reason also reaches assistive technology.
510
806
  aria-description={props.lastManager ? removeLabel() : undefined}
807
+ data-access-remove=""
808
+ style={props.expanded ? { width: "2.25rem", height: "2.25rem", "min-height": "2.25rem" } : undefined}
511
809
  class="focus-ui flex h-7 w-7 items-center justify-center rounded text-dimmed transition-colors focus:opacity-100 enabled:hover:bg-red-500/[0.08] enabled:hover:text-red-600 dark:enabled:hover:text-red-400"
512
810
  >
513
- <i class="ti ti-x text-sm" />
811
+ <i class={props.expanded ? "ti ti-x text-lg" : "ti ti-x text-sm"} />
514
812
  </IconButton>
515
813
  </Tooltip.Anchor>
516
814
  </Show>
517
815
  </div>
816
+ <Show when={props.expanded && props.lastManager && props.canEdit}>
817
+ <p class="pb-2 pl-11 text-[13px] text-dimmed sm:pl-13">{t().lastManager({ level: manageLabel() })}</p>
818
+ </Show>
819
+ <Show when={props.expanded && props.canEdit && props.feedback?.retry}>
820
+ <div role="alert" class="pl-11 text-[13px] text-danger sm:pl-13">
821
+ {props.feedback?.message}{" "}
822
+ <Button variant="text" disabled={props.disabled} onClick={() => props.feedback?.retry?.()}>
823
+ {t().retry}
824
+ </Button>
825
+ </div>
826
+ </Show>
518
827
  {/* Expanding pushes the rows below down, only on the viewer's own request. */}
519
828
  <Show when={props.coverage}>{(coverage) => <GroupCoveragePanel coverage={coverage()} groupName={displayName()} />}</Show>
520
829
  </div>