@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.
- package/package.json +13 -5
- package/scripts/app-favicon.ts +7 -6
- package/scripts/build.ts +3 -0
- package/scripts/installable-assets.ts +61 -0
- package/scripts/preload.ts +3 -0
- package/src/_internal/app-access-path.ts +12 -0
- package/src/_internal/app-access-request.ts +13 -0
- package/src/_internal/app-access-resources.ts +141 -0
- package/src/_internal/app-event-catalog.ts +40 -0
- package/src/_internal/app-event-schemas.ts +127 -0
- package/src/_internal/application-package.ts +40 -0
- package/src/_internal/built-in-app-ids.json +27 -0
- package/src/_internal/capabilities.ts +185 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +10 -5
- package/src/_internal/define-app.ts +159 -45
- package/src/_internal/installable-assets.ts +90 -0
- package/src/_internal/installable-head.ts +22 -0
- package/src/_internal/object-store-area.ts +9 -0
- package/src/_internal/object-store-config.ts +101 -0
- package/src/_internal/object-store-key.ts +14 -0
- package/src/_internal/page-responses.ts +34 -3
- package/src/_internal/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +15 -1
- package/src/_internal/registry.ts +44 -1
- package/src/_internal/runtime-context.ts +9 -1
- package/src/_internal/runtime-watcher.ts +8 -0
- package/src/_internal/status-preserving-ssr.ts +12 -1
- package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
- package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
- package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
- package/src/access/AppAccessEditor.tsx +380 -0
- package/src/access/PermissionEditor.browser-harness.tsx +22 -2
- package/src/access/PermissionEditor.tsx +386 -77
- package/src/access/PrincipalAvatar.tsx +63 -0
- package/src/access/PrincipalPicker.tsx +29 -8
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-adapter.ts +51 -0
- package/src/access/app-access-messages.ts +172 -0
- package/src/access/messages.ts +14 -0
- package/src/access/ui.ts +4 -1
- package/src/account/Avatar.tsx +21 -3
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +8 -1
- package/src/ai/admin.ts +3 -0
- package/src/ai/app-frame.ts +12 -0
- package/src/ai/application-tasks.ts +101 -0
- package/src/ai/assistant-models.ts +126 -64
- package/src/ai/capabilities.ts +19 -4
- package/src/ai/capability-execution.ts +5 -2
- package/src/ai/chart-block.ts +63 -30
- package/src/ai/chat/chart-block.tsx +12 -3
- package/src/ai/chat/composer-adapter.ts +8 -7
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +2 -2
- package/src/ai/chat/presentation.tsx +22 -1
- package/src/ai/chat-quotas.ts +5 -4
- package/src/ai/chat-tasks.ts +2 -2
- package/src/ai/client/controller.ts +11 -10
- package/src/ai/client/projection.ts +3 -3
- package/src/ai/code-capability-routes.ts +1 -0
- package/src/ai/compaction.ts +3 -3
- package/src/ai/credentials.ts +13 -114
- package/src/ai/default-tool-contracts.ts +4 -3
- package/src/ai/dictation-runtime.ts +4 -4
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +38 -27
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +11 -5
- package/src/ai/inference-calls.ts +6 -4
- package/src/ai/maintenance.ts +1 -1
- package/src/ai/memory-learning-runs.ts +5 -5
- package/src/ai/memory-learning.ts +3 -3
- package/src/ai/message-queue.ts +1 -2
- package/src/ai/migrate.ts +75 -42
- package/src/ai/model-access-migrate.ts +3 -17
- package/src/ai/model-access.ts +44 -85
- package/src/ai/model-card.tsx +134 -0
- package/src/ai/model-catalog.ts +359 -0
- package/src/ai/model-chooser-messages.ts +88 -0
- package/src/ai/model-chooser.tsx +577 -0
- package/src/ai/model-delete.ts +234 -0
- package/src/ai/model-fixture.ts +39 -0
- package/src/ai/model-icon.tsx +36 -0
- package/src/ai/model-overview.ts +53 -0
- package/src/ai/model-usage.ts +45 -0
- package/src/ai/openai-api-messages.ts +0 -4
- package/src/ai/openai-api-slots.ts +10 -2
- package/src/ai/openai-api.ts +83 -16
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/projects-routes.ts +2 -1
- package/src/ai/projects.ts +13 -12
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +138 -0
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider-retry.ts +1 -1
- package/src/ai/provider.ts +2 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +11 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas-migrate.ts +1 -0
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +6 -0
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +17 -18
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/store.ts +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +29 -46
- package/src/ai/task-contracts.ts +25 -5
- package/src/ai/task-execution.ts +15 -6
- package/src/ai/task-model.ts +9 -0
- package/src/ai/transcription.ts +37 -21
- package/src/ai/turn-failure.ts +24 -2
- package/src/ai/types.ts +50 -62
- package/src/ai/ui.tsx +13 -0
- package/src/ai/usage.ts +8 -8
- package/src/ai/validate.ts +8 -7
- package/src/ai/view-block.ts +2 -2
- package/src/api/accounts-entities.ts +40 -16
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-usage.ts +1 -1
- package/src/api/admin-ai.ts +173 -0
- package/src/api/admin-core-settings.ts +68 -244
- package/src/api/admin-directory-shares.ts +55 -0
- package/src/api/admin-identity.ts +81 -2
- package/src/api/app-access.ts +85 -0
- package/src/api/app-approval.ts +3 -1
- package/src/api/app-event-delivery.ts +51 -0
- package/src/api/app-events.ts +123 -0
- package/src/api/app-resource-access.ts +48 -0
- package/src/api/auth/account-mismatch.ts +24 -0
- package/src/api/auth.ts +26 -3
- package/src/api/capabilities.ts +127 -15
- package/src/api/capability-streams.ts +2 -5
- package/src/api/identity-child-invocation.ts +145 -0
- package/src/api/index.ts +11 -2
- package/src/api/mcp.ts +180 -5
- package/src/api/me.ts +38 -17
- package/src/api/pwa-phone.ts +87 -48
- package/src/api/pwa.ts +34 -8
- package/src/api/resource-links.ts +83 -6
- package/src/api/resource-resolve.ts +50 -0
- package/src/api/search/schemas.ts +3 -1
- package/src/api/search.ts +9 -7
- package/src/api/widgets.ts +1 -0
- package/src/app-pwa/authority.ts +75 -0
- package/src/app-pwa/contracts.ts +177 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +23 -0
- package/src/app-pwa/protocol.ts +27 -0
- package/src/app-pwa/schema.ts +59 -0
- package/src/app-pwa/service.ts +861 -0
- package/src/app-pwa/transaction.ts +338 -0
- package/src/app-pwa/transport-input.ts +57 -0
- package/src/app-pwa/transport.ts +235 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/app-access-recovery.ts +44 -0
- package/src/browser/app-session.ts +14 -3
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/live-websocket.ts +5 -0
- package/src/browser/pwa-drafts.ts +57 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/browser/resource-picker.tsx +3 -2
- package/src/capabilities/calendar-sources.ts +302 -0
- package/src/capabilities/executions.ts +10 -3
- package/src/capabilities/migrate.ts +4 -0
- package/src/capabilities/nested.ts +43 -0
- package/src/capabilities/server-invoke.ts +202 -0
- package/src/capabilities/server.ts +26 -187
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- package/src/cli/account.ts +57 -0
- package/src/cli/admin/agents.ts +66 -0
- package/src/cli/admin/ai-models.ts +199 -0
- package/src/cli/admin/ai-quotas.ts +6 -102
- package/src/cli/admin/ai-usage.ts +4 -4
- package/src/cli/admin/app-credentials.ts +5 -3
- package/src/cli/admin/index.ts +7 -3
- package/src/cli/app-access.ts +60 -0
- package/src/cli/apps.ts +89 -26
- package/src/cli/avatar.ts +64 -0
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/cli/index.ts +1 -1
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +52 -5
- package/src/contracts/app-approval.ts +8 -1
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +24 -5
- package/src/contracts/calendar-source.ts +274 -0
- package/src/contracts/calendar-view.ts +35 -0
- package/src/contracts/capabilities.ts +164 -3
- package/src/contracts/geo-admin.ts +2 -0
- package/src/contracts/geo-route.ts +0 -11
- package/src/contracts/geo.ts +14 -2
- package/src/contracts/index.ts +6 -0
- package/src/contracts/installable.ts +40 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/profile.ts +32 -0
- package/src/contracts/pwa-builds.ts +14 -0
- package/src/contracts/pwa-location.ts +38 -0
- package/src/contracts/pwa.ts +24 -3
- package/src/contracts/registry.ts +8 -0
- package/src/contracts/resource-clipboard.ts +35 -0
- package/src/contracts/resource-pages.ts +140 -0
- package/src/contracts/resource-resolution.ts +13 -0
- package/src/contracts/shared.ts +4 -0
- package/src/contracts/user-notifications.ts +2 -0
- package/src/events/app-event-delivery.ts +100 -0
- package/src/events/app-events.testing.ts +65 -0
- package/src/events/app-events.ts +134 -0
- package/src/events/core-user-created.ts +24 -0
- package/src/events/index.ts +9 -0
- package/src/events/testing.ts +2 -0
- package/src/index.ts +2 -1
- package/src/server/api/respond.ts +8 -3
- package/src/server/index.ts +1 -0
- package/src/server/middleware/app-access.ts +7 -6
- package/src/server/middleware/auth.ts +168 -66
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +4 -2
- package/src/server/middleware/workload.ts +3 -3
- package/src/server/oauth-scope.ts +10 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +8 -0
- package/src/services/accounts/app.ts +35 -3
- package/src/services/accounts/avatar.ts +342 -35
- package/src/services/accounts/base-group.ts +4 -5
- package/src/services/accounts/directory-shares.ts +157 -0
- package/src/services/accounts/entities.ts +50 -6
- package/src/services/accounts/groups.ts +3 -1
- package/src/services/accounts/identities.ts +64 -12
- package/src/services/accounts/local-groups.ts +1 -1
- package/src/services/accounts/users.ts +23 -15
- package/src/services/app-access-management.ts +519 -0
- package/src/services/app-access.ts +25 -7
- package/src/services/app-approval.ts +15 -4
- package/src/services/app-details.ts +224 -0
- package/src/services/app-discovery.ts +2 -1
- package/src/services/app-event-shapes.ts +139 -0
- package/src/services/app-events/accept.ts +116 -0
- package/src/services/app-events/admin.ts +250 -0
- package/src/services/app-events/declarations.ts +187 -0
- package/src/services/app-events/deliver.ts +218 -0
- package/src/services/app-events.ts +92 -0
- package/src/services/config-conflict.ts +11 -0
- package/src/services/core-user-events.ts +1 -0
- package/src/services/freeipa-config.ts +9 -0
- package/src/services/geo/admin.ts +4 -1
- package/src/services/geo/ledger.ts +47 -14
- package/src/services/geo/route-chain.ts +40 -18
- package/src/services/geo/route-estimate.ts +19 -0
- package/src/services/geo/route-plan.ts +0 -2
- package/src/services/geo/route-service.ts +57 -39
- package/src/services/geo/service.ts +51 -25
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/index.ts +2 -1
- package/src/services/identity/invocation-actor.ts +50 -2
- package/src/services/identity/invocation-authority.ts +38 -1
- package/src/services/identity/invocation-token.ts +255 -50
- package/src/services/identity/issue-invocation.ts +9 -1
- package/src/services/identity/models-only-auth.ts +13 -4
- package/src/services/identity/nested-invocation.ts +128 -0
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +24 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/policy.ts +19 -12
- package/src/services/notifications/browser.ts +4 -0
- package/src/services/notifications/catalog.ts +5 -2
- package/src/services/notifications/platform.ts +7 -2
- package/src/services/notifications/user.ts +18 -7
- package/src/services/oauth-tokens.ts +28 -2
- package/src/services/object-store.ts +217 -0
- package/src/services/providers/local/users.ts +5 -0
- package/src/services/pwa-devices.ts +147 -37
- package/src/services/service-account-credentials.ts +5 -2
- package/src/services/session/index.ts +14 -0
- package/src/services/settings/core-settings.ts +91 -47
- package/src/services/settings/index.ts +8 -0
- package/src/services/settings/versions.ts +26 -0
- package/src/shared/ai-models.ts +274 -0
- package/src/shared/ai-quotas.ts +2 -2
- package/src/shared/ai-usage.ts +2 -2
- package/src/shared/index.ts +2 -0
- package/src/shared-text/documents.ts +25 -8
- package/src/shared-text/log.ts +1 -1
- package/src/shared-text/text-diff.ts +75 -0
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +11 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +114 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/AppPwaRuntime.island.tsx +82 -0
- package/src/ssr/CloudShell.island.tsx +25 -4
- package/src/ssr/Layout.tsx +15 -7
- package/src/ssr/PwaLayout.tsx +73 -8
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/PwaRuntime.island.tsx +85 -6
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/ShellRuntime.island.tsx +5 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -20
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-pwa-document.tsx +16 -0
- package/src/ssr/app-pwa-messages.ts +17 -0
- package/src/ssr/index.ts +8 -0
- package/src/ssr/layout-context.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +9 -1
- package/src/ssr/pwa-messages.ts +10 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +2 -0
- package/src/styles/global.css +1 -0
- package/src/styles/profile-picture.css +270 -0
- package/src/styles/utilities-navigation.css +6 -0
- package/src/workflows/ai/policy.ts +49 -0
- package/src/workflows/ai/runtime.ts +131 -21
- package/src/workflows/ai/store.ts +283 -35
- package/src/workflows/ai/types.ts +8 -2
- package/src/workflows/ai-actions.ts +148 -120
- package/src/workflows/authoring.ts +31 -0
- package/src/workflows/contracts.ts +1 -0
- package/src/workflows/definition.ts +2 -0
- package/src/workflows/editor.ts +15 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/language/compiler.ts +34 -3
- package/src/workflows/runtime/executor.ts +132 -33
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- 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
|
+
);
|