@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
@@ -0,0 +1,699 @@
1
+ import {
2
+ Avatar,
3
+ announce,
4
+ Button,
5
+ ChoiceTiles,
6
+ ColorChoices,
7
+ dialogCore,
8
+ FileDropzone,
9
+ ImageCropper,
10
+ type ImageCropState,
11
+ PanelDialog,
12
+ panelDialogWorkspaceOptions,
13
+ panelDialogWorkspacePanelClass,
14
+ prompts,
15
+ Tabs,
16
+ useLocale,
17
+ } from "@k2b/ui";
18
+ import { createEffect, createMemo, createResource, createSignal, For, type JSX, Match, on, Show, Switch } from "solid-js";
19
+ import { chooseFiles } from "../browser/files";
20
+ import {
21
+ AVATAR_BACKGROUNDS,
22
+ type AvatarConfig,
23
+ AvatarConfigSchema,
24
+ type AvatarKind,
25
+ PERSON_BEARDS,
26
+ PERSON_BROWS,
27
+ PERSON_EXTRAS,
28
+ PERSON_EYES,
29
+ PERSON_FACES,
30
+ PERSON_GLASSES,
31
+ PERSON_HAIR_COLORS,
32
+ PERSON_HAIRSTYLES,
33
+ PERSON_HEADWEAR,
34
+ PERSON_MOUTHS,
35
+ PERSON_SKIN_TONES,
36
+ type PersonAvatar,
37
+ ROBOT_ANTENNAS,
38
+ ROBOT_BODIES,
39
+ ROBOT_COLORS,
40
+ ROBOT_EYES,
41
+ ROBOT_HEADS,
42
+ ROBOT_MOUTHS,
43
+ type RobotAvatar,
44
+ randomizePersonAvatar,
45
+ randomRobotAvatar,
46
+ startPersonAvatar,
47
+ } from "../contracts/avatar";
48
+ import { avatarSwatches, renderAvatarSvg } from "../shared/avatar-art";
49
+ import { AvatarUploadError, createAvatarDataUrlFromFile, validateAvatarSourceFile } from "./avatar-upload";
50
+ import { profilePictureMessages } from "./profile-picture-messages";
51
+
52
+ /** What the dialog reads and writes: the shape of `GET/PUT /api/me/avatar` and the service-account routes. */
53
+ export type ProfilePictureState = {
54
+ kind: AvatarKind;
55
+ built: AvatarConfig | null;
56
+ hasPhoto: boolean;
57
+ photo: string | null;
58
+ avatarHash: string | null;
59
+ };
60
+
61
+ export type ProfilePictureChange = { dataUrl: string } | { built: AvatarConfig } | { use: AvatarKind };
62
+
63
+ export type ProfilePictureDialogOptions = {
64
+ /** A person picks photo, avatar, or initials; an agent picks photo or robot. */
65
+ subject: "person" | "agent";
66
+ /** The account's name; initials come from it. */
67
+ name: string;
68
+ load: () => Promise<ProfilePictureState>;
69
+ save: (change: ProfilePictureChange) => Promise<ProfilePictureState>;
70
+ deletePhoto: () => Promise<ProfilePictureState>;
71
+ };
72
+
73
+ type Way = "photo" | "built" | "initials";
74
+ type Messages = ReturnType<typeof profilePictureMessages.resolve>["t"];
75
+ type Part = { key: string; label: string; options: readonly string[]; labels: string; color?: Record<string, string> };
76
+ type Category = { id: string; label: string; parts: Part[] };
77
+
78
+ const SIZES = [64, 40, 32, 24, 20] as const;
79
+
80
+ const partValue = (config: AvatarConfig, key: string): string => {
81
+ const record: Record<string, unknown> = config;
82
+ return String(record[key] ?? "");
83
+ };
84
+ /** A config with one part changed, validated against its shape set. */
85
+ const withPart = (config: AvatarConfig, key: string, value: string): AvatarConfig => AvatarConfigSchema.parse({ ...config, [key]: value });
86
+ const PHOTO_TYPES = "image/png,image/jpeg,image/webp";
87
+
88
+ /** A built avatar drawn inline from its config; the markup holds only shapes from fixed ids. */
89
+ function Drawing(props: { config: AvatarConfig; size: number | "fill"; crop?: "full" | "thumbnail"; label?: string }): JSX.Element {
90
+ const sizeStyle = () =>
91
+ props.size === "fill" ? { width: "100%", height: "100%" } : { width: `${props.size}px`, height: `${props.size}px` };
92
+ const markup = () => renderAvatarSvg(props.config, props.crop ?? "full");
93
+ const shape = () => (props.config.kind === "robot" ? "square" : "circle");
94
+ return (
95
+ <Show
96
+ when={props.label}
97
+ fallback={
98
+ <span class="cloud-profile-picture__drawing" data-shape={shape()} style={sizeStyle()} aria-hidden="true" innerHTML={markup()} />
99
+ }
100
+ >
101
+ {(label) => (
102
+ <span
103
+ class="cloud-profile-picture__drawing"
104
+ data-shape={shape()}
105
+ style={sizeStyle()}
106
+ role="img"
107
+ aria-label={label()}
108
+ innerHTML={markup()}
109
+ />
110
+ )}
111
+ </Show>
112
+ );
113
+ }
114
+
115
+ function Initials(props: { name: string; size: number }): JSX.Element {
116
+ return (
117
+ <Avatar
118
+ name={props.name}
119
+ style={{ width: `${props.size}px`, height: `${props.size}px`, "font-size": `${Math.round(props.size * 0.36)}px` }}
120
+ />
121
+ );
122
+ }
123
+
124
+ function Photo(props: { src: string; size: number; square: boolean }): JSX.Element {
125
+ return (
126
+ <img
127
+ class="cloud-profile-picture__photo"
128
+ data-shape={props.square ? "square" : "circle"}
129
+ src={props.src}
130
+ alt=""
131
+ style={{ width: `${props.size}px`, height: `${props.size}px` }}
132
+ />
133
+ );
134
+ }
135
+
136
+ const personCategories = (t: Messages): Category[] => {
137
+ const part = (key: keyof PersonAvatar, label: string, options: readonly string[], color?: Record<string, string>): Part => ({
138
+ key,
139
+ label,
140
+ options,
141
+ labels: key,
142
+ color,
143
+ });
144
+ const colorMap = (list: { id: string; color: string }[]) => Object.fromEntries(list.map((swatch) => [swatch.id, swatch.color]));
145
+ return [
146
+ {
147
+ id: "face",
148
+ label: t.face,
149
+ parts: [part("face", t.faceShape, PERSON_FACES), part("skin", t.skin, PERSON_SKIN_TONES, colorMap(avatarSwatches.skin))],
150
+ },
151
+ {
152
+ id: "hair",
153
+ label: t.hair,
154
+ parts: [
155
+ part("hair", t.hairstyle, PERSON_HAIRSTYLES),
156
+ part("hairColor", t.hairColor, PERSON_HAIR_COLORS, colorMap(avatarSwatches.hairColor)),
157
+ ],
158
+ },
159
+ { id: "eyes", label: t.eyes, parts: [part("eyes", t.eyes, PERSON_EYES)] },
160
+ { id: "brows", label: t.brows, parts: [part("brows", t.eyebrows, PERSON_BROWS)] },
161
+ { id: "mouth", label: t.mouth, parts: [part("mouth", t.mouth, PERSON_MOUTHS)] },
162
+ { id: "beard", label: t.beard, parts: [part("beard", t.beard, PERSON_BEARDS)] },
163
+ { id: "glasses", label: t.glasses, parts: [part("glasses", t.glasses, PERSON_GLASSES)] },
164
+ { id: "head", label: t.head, parts: [part("head", t.headwear, PERSON_HEADWEAR)] },
165
+ { id: "extra", label: t.extra, parts: [part("extra", t.extra, PERSON_EXTRAS)] },
166
+ { id: "bg", label: t.colour, parts: [part("bg", t.backgroundAndClothes, AVATAR_BACKGROUNDS, colorMap(avatarSwatches.bg))] },
167
+ ];
168
+ };
169
+
170
+ const robotCategories = (t: Messages): Category[] => {
171
+ const colorMap = (list: { id: string; color: string }[]) => Object.fromEntries(list.map((swatch) => [swatch.id, swatch.color]));
172
+ const part = (
173
+ key: keyof RobotAvatar,
174
+ label: string,
175
+ options: readonly string[],
176
+ labels: string,
177
+ color?: Record<string, string>,
178
+ ): Part => ({
179
+ key,
180
+ label,
181
+ options,
182
+ labels,
183
+ color,
184
+ });
185
+ return [
186
+ { id: "head", label: t.head, parts: [part("head", t.head, ROBOT_HEADS, "robotHead")] },
187
+ { id: "antenna", label: t.antenna, parts: [part("antenna", t.antenna, ROBOT_ANTENNAS, "antenna")] },
188
+ { id: "eyes", label: t.eyes, parts: [part("eyes", t.eyes, ROBOT_EYES, "robotEyes")] },
189
+ { id: "mouth", label: t.mouth, parts: [part("mouth", t.mouth, ROBOT_MOUTHS, "robotMouth")] },
190
+ { id: "body", label: t.body, parts: [part("body", t.body, ROBOT_BODIES, "body")] },
191
+ {
192
+ id: "colors",
193
+ label: t.colours,
194
+ parts: [
195
+ part("color", t.casing, ROBOT_COLORS, "color", colorMap(avatarSwatches.robotColor)),
196
+ part("bg", t.background, AVATAR_BACKGROUNDS, "bg", colorMap(avatarSwatches.bg)),
197
+ ],
198
+ },
199
+ ];
200
+ };
201
+
202
+ function Builder(props: {
203
+ subject: "person" | "agent";
204
+ draft: AvatarConfig;
205
+ base: AvatarConfig;
206
+ canUndo: boolean;
207
+ category: string;
208
+ onCategory: (id: string) => void;
209
+ onChange: (key: string, value: string, label: string) => void;
210
+ onRandom: () => void;
211
+ onUndo: () => void;
212
+ onReset: () => void;
213
+ t: Messages;
214
+ }): JSX.Element {
215
+ const categories = createMemo(() => (props.subject === "person" ? personCategories(props.t) : robotCategories(props.t)));
216
+ const changed = () => JSON.stringify(props.draft) !== JSON.stringify(props.base);
217
+ const hairHidden = () => props.draft.kind === "person" && (props.draft.head === "hijab" || props.draft.head === "turban");
218
+ const value = (key: string) => partValue(props.draft, key);
219
+ const label = (part: Part, id: string) => props.t.option({ part: part.labels, id });
220
+
221
+ const partControl = (part: Part) =>
222
+ part.color ? (
223
+ <ColorChoices
224
+ label={part.label}
225
+ value={value(part.key)}
226
+ options={part.options.map((id) => ({ value: id, label: label(part, id), color: part.color?.[id] ?? "transparent" }))}
227
+ onValueChange={(next) => props.onChange(part.key, next, label(part, next))}
228
+ />
229
+ ) : (
230
+ <ChoiceTiles
231
+ label={part.label}
232
+ description={
233
+ part.key === "hair" && hairHidden()
234
+ ? props.t.hairHidden({
235
+ cover: props.draft.kind === "person" && props.draft.head === "hijab" ? props.t.hijabCover : props.t.turbanCover,
236
+ })
237
+ : part.key === "beard"
238
+ ? props.t.beardColour
239
+ : undefined
240
+ }
241
+ dimmed={part.key === "hair" && hairHidden()}
242
+ value={value(part.key)}
243
+ options={part.options.map((id) => ({
244
+ value: id,
245
+ label: label(part, id),
246
+ visual: <Drawing config={withPart(props.draft, part.key, id)} size={60} crop="thumbnail" />,
247
+ }))}
248
+ onValueChange={(next) => props.onChange(part.key, next, label(part, next))}
249
+ />
250
+ );
251
+
252
+ return (
253
+ <div class="cloud-profile-picture__builder">
254
+ <div class="cloud-profile-picture__preview">
255
+ <span class="cloud-profile-picture__big">
256
+ <Drawing config={props.draft} size="fill" label={props.t.preview} />
257
+ </span>
258
+ <div class="cloud-profile-picture__stack">
259
+ <div class="cloud-profile-picture__sizes" role="group" aria-label={props.t.previewSizes}>
260
+ <For each={SIZES}>
261
+ {(size) => (
262
+ <figure>
263
+ <Drawing config={props.draft} size={size} />
264
+ <figcaption>{size}</figcaption>
265
+ </figure>
266
+ )}
267
+ </For>
268
+ </div>
269
+ <div
270
+ class="k2b-ui cloud-profile-picture__sizes cloud-profile-picture__sizes--dark"
271
+ data-theme="dark"
272
+ role="group"
273
+ aria-label={props.t.previewDark}
274
+ >
275
+ <For each={SIZES}>
276
+ {(size) => (
277
+ <figure>
278
+ <Drawing config={props.draft} size={size} />
279
+ <figcaption>{size}</figcaption>
280
+ </figure>
281
+ )}
282
+ </For>
283
+ </div>
284
+ <div class="cloud-profile-picture__tools">
285
+ <Button type="button" variant="ghost" size="sm" onClick={props.onRandom}>
286
+ <i class="ti ti-dice-5" aria-hidden="true" />
287
+ {props.t.random}
288
+ </Button>
289
+ <Button
290
+ type="button"
291
+ variant="ghost"
292
+ size="sm"
293
+ onClick={props.onUndo}
294
+ disabled={!props.canUndo}
295
+ aria-label={props.t.undo}
296
+ title={props.t.undo}
297
+ >
298
+ <i class="ti ti-arrow-back-up" aria-hidden="true" />
299
+ <span class="cloud-profile-picture__tool-label">{props.t.undo}</span>
300
+ </Button>
301
+ <Button
302
+ type="button"
303
+ variant="ghost"
304
+ size="sm"
305
+ onClick={props.onReset}
306
+ disabled={!changed()}
307
+ aria-label={props.t.reset}
308
+ title={props.t.reset}
309
+ >
310
+ <i class="ti ti-restore" aria-hidden="true" />
311
+ <span class="cloud-profile-picture__tool-label">{props.t.reset}</span>
312
+ </Button>
313
+ </div>
314
+ </div>
315
+ </div>
316
+ <div class="cloud-profile-picture__parts">
317
+ <Tabs
318
+ ariaLabel={props.t.parts}
319
+ variant="pill"
320
+ value={props.category}
321
+ onValueChange={props.onCategory}
322
+ options={categories().map((category) => ({
323
+ value: category.id,
324
+ label: category.label,
325
+ panel: <div class="cloud-profile-picture__panel">{category.parts.map(partControl)}</div>,
326
+ }))}
327
+ />
328
+ </div>
329
+ </div>
330
+ );
331
+ }
332
+
333
+ function ProfilePictureDialog(
334
+ props: ProfilePictureDialogOptions & {
335
+ close: (state?: ProfilePictureState) => void;
336
+ setDismissHandler: (handler: () => void | Promise<void>) => void;
337
+ },
338
+ ): JSX.Element {
339
+ const locale = useLocale();
340
+ const t = () => profilePictureMessages.resolve([locale()]).t;
341
+ const [state, { refetch, mutate }] = createResource(() => props.load());
342
+ // Reading a failed resource throws; every reader goes through this guard and the error branch shows the retry.
343
+ const loaded = () => (state.state === "ready" || state.state === "refreshing" ? state() : undefined);
344
+ const [way, setWay] = createSignal<Way | null>(null);
345
+ const [openWay, setOpenWay] = createSignal<Way>("built");
346
+ const [draft, setDraft] = createSignal<AvatarConfig | null>(null);
347
+ const [base, setBase] = createSignal<AvatarConfig | null>(null);
348
+ const [history, setHistory] = createSignal<AvatarConfig[]>([]);
349
+ const [category, setCategory] = createSignal(props.subject === "person" ? "face" : "head");
350
+ const [photoSource, setPhotoSource] = createSignal<File | null>(null);
351
+ const [crop, setCrop] = createSignal<ImageCropState | null>(null);
352
+ const [photoError, setPhotoError] = createSignal<string | null>(null);
353
+ const [saving, setSaving] = createSignal(false);
354
+ const [deleting, setDeleting] = createSignal(false);
355
+ const [saveError, setSaveError] = createSignal(false);
356
+ // A deleted photo changed the server state even when the person then closes without saving.
357
+ let changedOnServer: ProfilePictureState | undefined;
358
+ const square = props.subject === "agent";
359
+
360
+ // The first load decides where the dialog opens: a person without a built avatar starts on a random first draft.
361
+ createEffect(
362
+ on(loaded, (current) => {
363
+ if (!current || draft()) return;
364
+ const first = current.built ?? (props.subject === "person" ? startPersonAvatar() : randomRobotAvatar());
365
+ setDraft(first);
366
+ setBase(first);
367
+ const opening: Way = current.kind === "initials" && !current.built ? "built" : current.kind;
368
+ setOpenWay(opening);
369
+ setWay(opening);
370
+ }),
371
+ );
372
+ const ready = () => Boolean(loaded()) && draft() !== null;
373
+
374
+ const current = () => way() ?? openWay();
375
+ const photoReachable = () => current() !== "photo" || Boolean(loaded()?.hasPhoto) || photoSource() !== null;
376
+ const same = (a: AvatarConfig | null | undefined, b: AvatarConfig | null | undefined) => JSON.stringify(a) === JSON.stringify(b);
377
+ // Changed by the person in this dialog: closing asks first, and the footer says it is not saved yet.
378
+ const touched = () => photoSource() !== null || (current() !== openWay() && photoReachable()) || !same(draft(), base());
379
+ // Different from what is stored: only then does Save send anything. A first draft of a new avatar counts.
380
+ const differsFromStored = () => {
381
+ const stored = loaded();
382
+ if (!stored) return false;
383
+ if (photoSource()) return true;
384
+ if (current() === "built") return stored.kind !== "built" || !same(draft(), stored.built);
385
+ return current() !== stored.kind && photoReachable();
386
+ };
387
+ const busy = () => saving() || deleting();
388
+ const canSave = () =>
389
+ ready() &&
390
+ !busy() &&
391
+ // A new photo needs its crop; without a stored photo there is nothing else to show.
392
+ (photoSource() ? crop() !== null : !(current() === "photo" && !loaded()?.hasPhoto));
393
+
394
+ const requestClose = async () => {
395
+ if (busy()) return;
396
+ if (touched()) {
397
+ const discard = await prompts.confirm(t().discardBody, {
398
+ title: t().discardTitle,
399
+ variant: "danger",
400
+ confirmText: t().discardConfirm,
401
+ cancelText: t().discardCancel,
402
+ });
403
+ if (!discard || busy()) return;
404
+ }
405
+ props.close(changedOnServer);
406
+ };
407
+ props.setDismissHandler(requestClose);
408
+
409
+ const change = (key: string, value: string, label: string) => {
410
+ const before = draft();
411
+ if (!before || partValue(before, key) === value) return;
412
+ setHistory((list) => [...list, before]);
413
+ setDraft(withPart(before, key, value));
414
+ setSaveError(false);
415
+ announce(t().chosen({ name: label }));
416
+ };
417
+ const randomize = () => {
418
+ const before = draft();
419
+ if (!before) return;
420
+ setHistory((list) => [...list, before]);
421
+ setDraft(before.kind === "person" ? randomizePersonAvatar(before) : randomRobotAvatar());
422
+ setSaveError(false);
423
+ announce(t().randomized);
424
+ };
425
+ const undo = () => {
426
+ const list = history();
427
+ const previous = list.at(-1);
428
+ if (!previous) return;
429
+ setHistory(list.slice(0, -1));
430
+ setDraft(previous);
431
+ announce(t().undone);
432
+ };
433
+ const reset = () => {
434
+ const before = draft();
435
+ const original = base();
436
+ if (!before || !original) return;
437
+ setHistory((list) => [...list, before]);
438
+ setDraft(original);
439
+ announce(t().resetDone);
440
+ };
441
+
442
+ const choosePhoto = async (files: File[]) => {
443
+ const file = files[0];
444
+ if (!file) return;
445
+ setPhotoError(null);
446
+ try {
447
+ validateAvatarSourceFile(file);
448
+ setPhotoSource(file);
449
+ setCrop(null);
450
+ } catch (error) {
451
+ setPhotoError(uploadErrorMessage(error));
452
+ }
453
+ };
454
+
455
+ const uploadErrorMessage = (error: unknown) => {
456
+ if (!(error instanceof AvatarUploadError)) return t().photoProcessFailed;
457
+ if (error.code === "type_invalid") return t().photoTypeInvalid;
458
+ if (error.code === "too_large") return t().photoTooLarge;
459
+ if (error.code === "empty") return t().photoEmpty;
460
+ if (error.code === "unsupported") return t().photoUnsupported;
461
+ return t().photoProcessFailed;
462
+ };
463
+
464
+ const save = async () => {
465
+ if (!loaded() || !canSave()) return;
466
+ if (!differsFromStored()) {
467
+ props.close(changedOnServer);
468
+ return;
469
+ }
470
+ setSaving(true);
471
+ setSaveError(false);
472
+ try {
473
+ const target = current();
474
+ const file = photoSource();
475
+ const area = crop();
476
+ const built = draft();
477
+ // A new photo is saved only with its crop; it never falls back to showing the stored one.
478
+ if (target === "photo" && file && !area) return;
479
+ const next =
480
+ target === "photo" && file && area
481
+ ? await props.save({ dataUrl: await createAvatarDataUrlFromFile(file, area) })
482
+ : target === "built" && built
483
+ ? await props.save({ built })
484
+ : await props.save({ use: target });
485
+ props.close(next);
486
+ } catch (error) {
487
+ if (error instanceof AvatarUploadError) setPhotoError(uploadErrorMessage(error));
488
+ setSaveError(true);
489
+ } finally {
490
+ setSaving(false);
491
+ }
492
+ };
493
+
494
+ const deletePhoto = async () => {
495
+ if (busy()) return;
496
+ setDeleting(true);
497
+ setPhotoError(null);
498
+ try {
499
+ const next = await props.deletePhoto();
500
+ changedOnServer = next;
501
+ mutate(next);
502
+ setPhotoSource(null);
503
+ if (openWay() === "photo") setOpenWay(next.kind);
504
+ announce(t().photoDeleted);
505
+ } catch {
506
+ setPhotoError(t().photoDeleteFailed);
507
+ } finally {
508
+ setDeleting(false);
509
+ }
510
+ };
511
+
512
+ const wayOptions = () => {
513
+ const current = loaded();
514
+ const visual = (kind: Way) => {
515
+ if (kind === "photo") {
516
+ return current?.photo ? (
517
+ <Photo src={current.photo} size={36} square={square} />
518
+ ) : (
519
+ <span class="cloud-profile-picture__empty" data-shape={square ? "square" : "circle"}>
520
+ <i class="ti ti-photo" />
521
+ </span>
522
+ );
523
+ }
524
+ if (kind === "built") return <Show when={draft()}>{(config) => <Drawing config={config()} size={36} />}</Show>;
525
+ return <Initials name={props.name} size={36} />;
526
+ };
527
+ const ways: Way[] = props.subject === "person" ? ["photo", "built", "initials"] : ["photo", "built"];
528
+ return ways.map((kind) => ({
529
+ value: kind,
530
+ label: kind === "photo" ? t().photo : kind === "built" ? (props.subject === "person" ? t().avatar : t().robot) : t().initials,
531
+ status: current?.kind === kind ? t().active : "",
532
+ visual: visual(kind),
533
+ }));
534
+ };
535
+
536
+ const photoPane = () => {
537
+ const current = loaded();
538
+ const file = photoSource();
539
+ return (
540
+ <div class="cloud-profile-picture__pane">
541
+ <Show
542
+ when={file || current?.photo}
543
+ fallback={
544
+ <FileDropzone
545
+ accept={PHOTO_TYPES}
546
+ multiple={false}
547
+ choose={() => chooseFiles({ accept: PHOTO_TYPES })}
548
+ error={photoError}
549
+ icon="ti ti-photo-up"
550
+ title={t().photoDrop}
551
+ subtitle={t().photoFormats}
552
+ hint={t().photoChoose}
553
+ onDrop={choosePhoto}
554
+ />
555
+ }
556
+ >
557
+ <Show when={file} fallback={<Photo src={current?.photo ?? ""} size={160} square={square} />}>
558
+ <div class="cloud-profile-picture__cropper">
559
+ <ImageCropper
560
+ source={file as File}
561
+ aspect={{ width: 1, height: 1 }}
562
+ previewShape={square ? "rect" : "circle"}
563
+ onValueChange={setCrop}
564
+ />
565
+ </div>
566
+ </Show>
567
+ <Show when={photoError()}>
568
+ <p role="alert">{photoError()}</p>
569
+ </Show>
570
+ <p>{file ? t().photoNew : current?.kind === "photo" ? t().photoActive : t().photoHidden}</p>
571
+ <div class="cloud-profile-picture__row">
572
+ <Button
573
+ type="button"
574
+ variant="secondary"
575
+ size="sm"
576
+ onClick={() => void chooseFiles({ accept: PHOTO_TYPES }).then(choosePhoto)}
577
+ disabled={deleting()}
578
+ >
579
+ <i class="ti ti-replace" aria-hidden="true" />
580
+ {t().photoOther}
581
+ </Button>
582
+ <Show when={current?.hasPhoto}>
583
+ <Button type="button" variant="danger" size="sm" onClick={deletePhoto} disabled={deleting()}>
584
+ <i class="ti ti-trash" aria-hidden="true" />
585
+ {t().photoDelete}
586
+ </Button>
587
+ </Show>
588
+ </div>
589
+ </Show>
590
+ </div>
591
+ );
592
+ };
593
+
594
+ return (
595
+ <PanelDialog>
596
+ <PanelDialog.Header title={t().title} close={requestClose} />
597
+ <PanelDialog.Body>
598
+ {/* While a save or a deletion runs, the choices hold still, so the saved picture is the one on screen. */}
599
+ <div class="cloud-profile-picture" inert={busy() ? true : undefined} aria-busy={busy() ? "true" : undefined}>
600
+ <Switch>
601
+ <Match when={state.error}>
602
+ <div class="cloud-profile-picture__pane" role="alert">
603
+ <p>{t().loadFailed}</p>
604
+ <Button type="button" variant="secondary" size="sm" onClick={() => void refetch()}>
605
+ {t().retry}
606
+ </Button>
607
+ </div>
608
+ </Match>
609
+ <Match when={!ready()}>
610
+ <div class="cloud-profile-picture__pane" role="status" aria-label={t().loading}>
611
+ <span class="cloud-profile-picture__placeholder" />
612
+ </div>
613
+ </Match>
614
+ <Match when={ready()}>
615
+ <ChoiceTiles
616
+ aria-label={t().ways}
617
+ layout="row"
618
+ value={current()}
619
+ options={wayOptions()}
620
+ onValueChange={(next) => {
621
+ setWay(next);
622
+ setSaveError(false);
623
+ }}
624
+ />
625
+ {/* Every kind stays mounted, so a crop or a draft survives a look at another kind. */}
626
+ <div class="cloud-profile-picture__body">
627
+ <div hidden={current() !== "photo"}>{photoPane()}</div>
628
+ <div hidden={current() !== "built"}>
629
+ <Show when={draft()}>
630
+ {(config) => (
631
+ <Builder
632
+ subject={props.subject}
633
+ draft={config()}
634
+ base={base() ?? config()}
635
+ canUndo={history().length > 0}
636
+ category={category()}
637
+ onCategory={setCategory}
638
+ onChange={change}
639
+ onRandom={randomize}
640
+ onUndo={undo}
641
+ onReset={reset}
642
+ t={t()}
643
+ />
644
+ )}
645
+ </Show>
646
+ </div>
647
+ <div hidden={current() !== "initials"}>
648
+ <div class="cloud-profile-picture__pane">
649
+ <Initials name={props.name} size={160} />
650
+ <p>{t().initialsInfo}</p>
651
+ </div>
652
+ </div>
653
+ </div>
654
+ </Match>
655
+ </Switch>
656
+ </div>
657
+ </PanelDialog.Body>
658
+ <PanelDialog.Footer>
659
+ <div class="cloud-profile-picture__status" role={saveError() ? "alert" : "status"} data-error={saveError() ? "true" : undefined}>
660
+ <Show when={saveError()} fallback={touched() ? t().unsaved : props.subject === "person" ? t().visiblePerson : t().visibleAgent}>
661
+ <i class="ti ti-alert-circle" aria-hidden="true" />
662
+ <span>{t().saveFailed}</span>
663
+ <button type="button" class="cloud-profile-picture__retry" onClick={() => void save()}>
664
+ {t().retry}
665
+ </button>
666
+ </Show>
667
+ </div>
668
+ <div class="cloud-profile-picture__actions">
669
+ <Button type="button" variant="secondary" size="sm" onClick={requestClose} disabled={busy()}>
670
+ {t().cancel}
671
+ </Button>
672
+ <Button
673
+ type="button"
674
+ variant="primary"
675
+ size="sm"
676
+ class="cloud-profile-picture__save"
677
+ onClick={() => void save()}
678
+ disabled={!canSave()}
679
+ >
680
+ <Show when={saving()} fallback={t().save}>
681
+ <i class="ti ti-loader-2" aria-hidden="true" />
682
+ {t().saving}
683
+ </Show>
684
+ </Button>
685
+ </div>
686
+ </PanelDialog.Footer>
687
+ </PanelDialog>
688
+ );
689
+ }
690
+
691
+ /**
692
+ * Opens "Change profile picture": a person picks a photo, a built avatar, or initials; an agent picks a photo or its
693
+ * robot. Resolves with the new state when something changed on the server, or `undefined` when nothing did.
694
+ */
695
+ export const openProfilePictureDialog = (options: ProfilePictureDialogOptions): Promise<ProfilePictureState | undefined> =>
696
+ dialogCore.open<ProfilePictureState | undefined>(
697
+ (close, context) => <ProfilePictureDialog {...options} close={close} setDismissHandler={context.setDismissHandler} />,
698
+ { ...panelDialogWorkspaceOptions, panelClassName: `${panelDialogWorkspacePanelClass} cloud-profile-picture-dialog`, history: true },
699
+ );