@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,114 @@
|
|
|
1
|
+
import { AppWorkspace, ButtonLink, useLocale } from "@k2b/ui";
|
|
2
|
+
import { For, type JSX, Show } from "solid-js";
|
|
3
|
+
import { appAccessMessages } from "../access/app-access-messages";
|
|
4
|
+
import PrincipalAvatar from "../access/PrincipalAvatar";
|
|
5
|
+
import { principalLabel } from "../access/principal-label";
|
|
6
|
+
import type { AppMeta } from "../contracts/app";
|
|
7
|
+
import type { AccessEntry } from "../contracts/shared";
|
|
8
|
+
import { appAccentStyle } from "./app-appearance";
|
|
9
|
+
import Layout from "./Layout";
|
|
10
|
+
import type { LayoutContext } from "./layout-context";
|
|
11
|
+
|
|
12
|
+
export function AppAccessDeniedContent(props: {
|
|
13
|
+
app: Pick<AppMeta, "id" | "name" | "icon" | "access" | "appearance">;
|
|
14
|
+
managers: AccessEntry[];
|
|
15
|
+
canRepair?: boolean;
|
|
16
|
+
revoked?: boolean;
|
|
17
|
+
}) {
|
|
18
|
+
const locale = useLocale(),
|
|
19
|
+
t = () => appAccessMessages.resolve([locale()]).t;
|
|
20
|
+
const grants = () => props.app.access?.kind === "grants";
|
|
21
|
+
// A fixed rule names itself before the contact, so the reader knows why and whom to ask.
|
|
22
|
+
const rule = () => {
|
|
23
|
+
const access = props.app.access;
|
|
24
|
+
if (access?.kind === "noGuests") return t().deniedNoGuests({ name: props.app.name });
|
|
25
|
+
if (access?.kind !== "roles") return undefined;
|
|
26
|
+
return access.roles.length === 1 && access.roles[0] === "admin"
|
|
27
|
+
? t().deniedAdmins({ name: props.app.name })
|
|
28
|
+
: t().deniedRoles({ name: props.app.name });
|
|
29
|
+
};
|
|
30
|
+
return (
|
|
31
|
+
<div class="mx-auto mt-8 w-full max-w-[460px] text-left sm:mt-[10vh]">
|
|
32
|
+
<span
|
|
33
|
+
class="app-accent-scope app-accent-text k2b-icon-tile mb-4 flex h-12 w-12 items-center justify-center rounded-xl text-[26px]"
|
|
34
|
+
style={appAccentStyle(props.app.appearance?.accent)}
|
|
35
|
+
aria-hidden="true"
|
|
36
|
+
>
|
|
37
|
+
<i class={props.app.icon} />
|
|
38
|
+
</span>
|
|
39
|
+
<h1 class="mb-2 text-[22px] font-semibold">{(props.revoked ? t().revokedTitle : t().deniedTitle)({ name: props.app.name })}</h1>
|
|
40
|
+
<p class="mb-4 text-[15px] text-secondary">
|
|
41
|
+
{props.revoked ? (
|
|
42
|
+
t().revokedDescription
|
|
43
|
+
) : props.app.access?.kind === "resources" ? (
|
|
44
|
+
t().deniedResources
|
|
45
|
+
) : grants() ? (
|
|
46
|
+
t().deniedGrants
|
|
47
|
+
) : (
|
|
48
|
+
<>
|
|
49
|
+
<Show when={rule()}>{(sentence) => <>{sentence()} </>}</Show>
|
|
50
|
+
{t().deniedFixed}
|
|
51
|
+
</>
|
|
52
|
+
)}
|
|
53
|
+
</p>
|
|
54
|
+
{/* A fixed rule's contact is the Cloud administration the sentence already names. */}
|
|
55
|
+
<Show when={props.revoked || grants()}>
|
|
56
|
+
<div class="mb-5 flex flex-col gap-2">
|
|
57
|
+
<For each={props.managers}>
|
|
58
|
+
{(entry) => (
|
|
59
|
+
<div class="flex items-center gap-2.5">
|
|
60
|
+
<PrincipalAvatar entry={entry} label={principalLabel(entry, locale())} />
|
|
61
|
+
<span>{principalLabel(entry, locale())}</span>
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
</For>
|
|
65
|
+
<Show when={!props.managers.length}>
|
|
66
|
+
<p class="text-sm">{t().askAdministration}</p>
|
|
67
|
+
</Show>
|
|
68
|
+
</div>
|
|
69
|
+
</Show>
|
|
70
|
+
<ButtonLink href="/" variant="ghost" style={{ "min-height": "36px", "font-size": "15px" }}>
|
|
71
|
+
{t().home}
|
|
72
|
+
</ButtonLink>
|
|
73
|
+
<Show when={props.canRepair && grants()}>
|
|
74
|
+
<p class="mt-5 text-[13px] text-dimmed">
|
|
75
|
+
{t().adminHintBefore}
|
|
76
|
+
{/* An inline link in the action colour and weight, as the approved denial page shows it. */}
|
|
77
|
+
<a
|
|
78
|
+
class="focus-ui whitespace-nowrap rounded-sm font-medium text-[var(--k2b-action)] hover:underline"
|
|
79
|
+
href={`/admin/apps?app=${encodeURIComponent(props.app.id)}`}
|
|
80
|
+
>
|
|
81
|
+
{t().adminHintLink}
|
|
82
|
+
</a>
|
|
83
|
+
{t().adminHintAfter}
|
|
84
|
+
</p>
|
|
85
|
+
</Show>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The white work surface every application page has; the denial page sits on it like the app it replaces. */
|
|
91
|
+
export function AppAccessDeniedFrame(props: { children: JSX.Element }) {
|
|
92
|
+
return (
|
|
93
|
+
<AppWorkspace mobileSurface="flush" class="min-h-0 flex-1" resizable={false}>
|
|
94
|
+
<AppWorkspace.Content>
|
|
95
|
+
<AppWorkspace.Main class="p-4 sm:p-6">{props.children}</AppWorkspace.Main>
|
|
96
|
+
</AppWorkspace.Content>
|
|
97
|
+
</AppWorkspace>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const renderAppAccessDenied =
|
|
102
|
+
(c: LayoutContext, app: Pick<AppMeta, "id" | "name" | "icon" | "access" | "appearance">, managers: AccessEntry[], canRepair: boolean) =>
|
|
103
|
+
() => (
|
|
104
|
+
<Layout c={c} title={[{ title: app.name }]} fullWidth fullPage>
|
|
105
|
+
<AppAccessDeniedFrame>
|
|
106
|
+
<AppAccessDeniedContent
|
|
107
|
+
app={app}
|
|
108
|
+
managers={managers}
|
|
109
|
+
canRepair={canRepair}
|
|
110
|
+
revoked={new URL(c.req.raw.url).searchParams.get("_cloud_access") === "revoked"}
|
|
111
|
+
/>
|
|
112
|
+
</AppAccessDeniedFrame>
|
|
113
|
+
</Layout>
|
|
114
|
+
);
|
package/src/ssr/AppBadge.tsx
CHANGED
|
@@ -18,6 +18,20 @@ export const useAppBadgeDescription = (input: () => { endpoint?: string; appId?:
|
|
|
18
18
|
* takes layout space, so appearing, growing to "99+", or clearing moves
|
|
19
19
|
* nothing. Decorative: the link carries the description.
|
|
20
20
|
*/
|
|
21
|
+
/**
|
|
22
|
+
* The count on a phone bar item: the rail's count, laid over the tab icon's corner, and its description for screen
|
|
23
|
+
* readers after the item's label.
|
|
24
|
+
*/
|
|
25
|
+
export function TabBarAppBadge(props: { endpoint?: string; appId?: string }) {
|
|
26
|
+
const description = useAppBadgeDescription(() => props);
|
|
27
|
+
return (
|
|
28
|
+
<Show when={description()}>
|
|
29
|
+
<AppBadge endpoint={props.endpoint} appId={props.appId} />
|
|
30
|
+
<span class="sr-only">, {description()}</span>
|
|
31
|
+
</Show>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
export function AppBadge(props: { endpoint?: string; appId?: string }) {
|
|
22
36
|
const count = () => appBadgeCount(props);
|
|
23
37
|
return (
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Button, createInstallPrompt, InstallGuide, type InstallPrompt, NoticeCard, useLocale } from "@k2b/ui";
|
|
2
|
+
import { createSignal, onCleanup, onMount, Show } from "solid-js";
|
|
3
|
+
import type { AppInstallable } from "../contracts/installable";
|
|
4
|
+
import { appPwaMessages } from "./app-pwa-messages";
|
|
5
|
+
|
|
6
|
+
/** Only the App-PWA browser lifecycle. No Cloud mobile-app session, identity, or persistent state. */
|
|
7
|
+
export default function AppPwaRuntime(props: { app: AppInstallable }) {
|
|
8
|
+
const locale = useLocale();
|
|
9
|
+
const t = () => appPwaMessages.resolve([locale()]).t;
|
|
10
|
+
const [installation, setInstallation] = createSignal<InstallPrompt>();
|
|
11
|
+
const [installUrl, setInstallUrl] = createSignal("");
|
|
12
|
+
const [workerFailed, setWorkerFailed] = createSignal(false);
|
|
13
|
+
const [registering, setRegistering] = createSignal(false);
|
|
14
|
+
let disposed = false;
|
|
15
|
+
|
|
16
|
+
const register = async () => {
|
|
17
|
+
if (registering()) return;
|
|
18
|
+
setRegistering(true);
|
|
19
|
+
try {
|
|
20
|
+
if (!navigator.serviceWorker) throw new Error("Service workers unavailable");
|
|
21
|
+
await navigator.serviceWorker.register(`${props.app.path}sw.js`, { scope: props.app.path, updateViaCache: "none" });
|
|
22
|
+
if (!disposed) setWorkerFailed(false);
|
|
23
|
+
} catch {
|
|
24
|
+
if (!disposed) setWorkerFailed(true);
|
|
25
|
+
} finally {
|
|
26
|
+
if (!disposed) setRegistering(false);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
onMount(() => {
|
|
31
|
+
setInstallation(createInstallPrompt());
|
|
32
|
+
setInstallUrl(new URL(props.app.path, location.origin).href);
|
|
33
|
+
const syncTheme = () => {
|
|
34
|
+
const theme = document.documentElement.classList.contains("dark") ? "dark" : "light";
|
|
35
|
+
document.querySelector<HTMLMetaElement>("meta[data-app-pwa-theme]")?.setAttribute("content", props.app.themeColor[theme]);
|
|
36
|
+
document
|
|
37
|
+
.querySelector<HTMLLinkElement>("link[data-app-pwa-manifest]")
|
|
38
|
+
?.setAttribute("href", `${props.app.path}manifest.webmanifest?theme=${theme}`);
|
|
39
|
+
document
|
|
40
|
+
.querySelector<HTMLLinkElement>("link[data-app-pwa-apple]")
|
|
41
|
+
?.setAttribute("href", `${props.app.path}apple-touch-icon-${theme}.png`);
|
|
42
|
+
};
|
|
43
|
+
syncTheme();
|
|
44
|
+
const observer = new MutationObserver(syncTheme);
|
|
45
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
|
|
46
|
+
void register();
|
|
47
|
+
onCleanup(() => {
|
|
48
|
+
disposed = true;
|
|
49
|
+
observer.disconnect();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div data-app-pwa-runtime aria-live="polite">
|
|
55
|
+
<Show when={installation()}>
|
|
56
|
+
{(install) => (
|
|
57
|
+
<Show
|
|
58
|
+
when={
|
|
59
|
+
!install().installed() &&
|
|
60
|
+
(install().platform.startsWith("apple-") ||
|
|
61
|
+
install().canPrompt() ||
|
|
62
|
+
install().busy() ||
|
|
63
|
+
install().requested() ||
|
|
64
|
+
install().failed())
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
<NoticeCard title={t().title({ name: props.app.name })}>
|
|
68
|
+
<InstallGuide appName={props.app.name} install={install()} url={installUrl()} />
|
|
69
|
+
</NoticeCard>
|
|
70
|
+
</Show>
|
|
71
|
+
)}
|
|
72
|
+
</Show>
|
|
73
|
+
<Show when={workerFailed()}>
|
|
74
|
+
<NoticeCard tone="warning" title={t().workerFailed}>
|
|
75
|
+
<Button variant="secondary" loading={registering()} onClick={() => void register()}>
|
|
76
|
+
{t().retry}
|
|
77
|
+
</Button>
|
|
78
|
+
</NoticeCard>
|
|
79
|
+
</Show>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { TabBar, useLocale } from "@k2b/ui";
|
|
2
2
|
import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from "solid-js";
|
|
3
3
|
import { isServer, Portal } from "solid-js/web";
|
|
4
|
+
import { TabBarAppBadge } from "./AppBadge";
|
|
5
|
+
import { appBadgeCount } from "./app-badges";
|
|
4
6
|
import { AppLaunchpadButton, setAppLaunchpadContext } from "./app-launchpad";
|
|
5
7
|
import GlobalSearchTrigger from "./GlobalSearchTrigger";
|
|
6
8
|
import LayoutBreadcrumbs from "./LayoutBreadcrumbs";
|
|
@@ -35,8 +37,9 @@ export default function CloudShell(props: CloudShellProps) {
|
|
|
35
37
|
/**
|
|
36
38
|
* The phone bar's live copy, mounted with the rail's island so the shell adds no island instance. It renders nothing
|
|
37
39
|
* while the server's bar is current, so mounting moves and refocuses nothing. When the saved rail preferences or the
|
|
38
|
-
* URL (a fragment, Back and Forward, an in-page navigation) change what the bar shows,
|
|
39
|
-
* bar's row, and CSS hides the server's.
|
|
40
|
+
* URL (a fragment, Back and Forward, an in-page navigation) change what the bar shows, or an app in it has a count,
|
|
41
|
+
* it renders the new bar into the bar's row, and CSS hides the server's. Counts, like the rail's, exist only in the
|
|
42
|
+
* browser.
|
|
40
43
|
*/
|
|
41
44
|
function BarNavigation(props: { currentPath: string }) {
|
|
42
45
|
if (isServer) return null;
|
|
@@ -51,6 +54,12 @@ function BarNavigation(props: { currentPath: string }) {
|
|
|
51
54
|
phoneBarItems(projectRailNavigation(data.apps, rail, locale()), path, { label: t().home, path: data.home ?? "/" });
|
|
52
55
|
const served = JSON.stringify(itemsFor(initial, props.currentPath));
|
|
53
56
|
const items = createMemo(() => itemsFor(settings(), currentPath()));
|
|
57
|
+
const counted = () => items().some((item) => item.counted && appBadgeCount(item.counted) > 0);
|
|
58
|
+
const live = createMemo(() =>
|
|
59
|
+
items().map((item) =>
|
|
60
|
+
item.counted ? { ...item, badge: <TabBarAppBadge endpoint={item.counted.endpoint} appId={item.counted.appId} /> } : item,
|
|
61
|
+
),
|
|
62
|
+
);
|
|
54
63
|
onMount(() => {
|
|
55
64
|
const sync = () => setSettings(readRailContext()?.settings ?? initial);
|
|
56
65
|
const syncLocation = () => setCurrentPath(`${window.location.pathname}${window.location.search}${window.location.hash}`);
|
|
@@ -76,10 +85,22 @@ function BarNavigation(props: { currentPath: string }) {
|
|
|
76
85
|
});
|
|
77
86
|
const row = document.querySelector<HTMLElement>(".cloud-tab-bar__row");
|
|
78
87
|
if (!row) return null;
|
|
88
|
+
// Swapping bars keeps a focused link: the bar that takes over focuses the same destination, once CSS shows it.
|
|
89
|
+
const [shown, setShown] = createSignal(false);
|
|
90
|
+
const focusIn = (selector: string, tab: string) =>
|
|
91
|
+
[...row.querySelectorAll<HTMLElement>(`${selector} a[data-tab]`)].find((link) => link.dataset.tab === tab)?.focus();
|
|
92
|
+
createEffect(() => {
|
|
93
|
+
const next = counted() || JSON.stringify(items()) !== served;
|
|
94
|
+
if (next === shown()) return;
|
|
95
|
+
const focused = document.activeElement;
|
|
96
|
+
const tab = focused instanceof HTMLElement && row.contains(focused) ? focused.dataset.tab : undefined;
|
|
97
|
+
setShown(next);
|
|
98
|
+
if (tab) queueMicrotask(() => focusIn(next ? ".cloud-tab-bar__live" : ".k2b-tab-bar:not(.cloud-tab-bar__live)", tab));
|
|
99
|
+
});
|
|
79
100
|
return (
|
|
80
|
-
<Show when={
|
|
101
|
+
<Show when={shown()}>
|
|
81
102
|
<Portal mount={row}>
|
|
82
|
-
<TabBar variant="floating" class="cloud-tab-bar__live" label={t().apps} items={
|
|
103
|
+
<TabBar variant="floating" class="cloud-tab-bar__live" label={t().apps} items={live()} />
|
|
83
104
|
</Portal>
|
|
84
105
|
</Show>
|
|
85
106
|
);
|
package/src/ssr/Layout.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import { isAppSessionActor } from "../server/actor";
|
|
|
14
14
|
import { getLocale } from "../server/locale";
|
|
15
15
|
import { getDateConfig } from "../server/time";
|
|
16
16
|
import { dates, resolveHomePath } from "../shared";
|
|
17
|
+
import { publicCloudOrigin } from "../shared/app-url";
|
|
17
18
|
import { readThemePreferenceFromCookieHeader } from "../shared/theme";
|
|
18
19
|
import AnnouncementBanners from "./AnnouncementBanners";
|
|
19
20
|
import { appAppearanceStyle, resolveCurrentApp } from "./app-appearance";
|
|
@@ -258,7 +259,9 @@ export default function Layout(props: LayoutProps) {
|
|
|
258
259
|
const panel: ShellData["panel"] = providers.length
|
|
259
260
|
? { providers, provider: panelState.provider ?? providers[0]?.id, context: pageContext, consent: c.get("panelConsent") }
|
|
260
261
|
: undefined;
|
|
262
|
+
const cloudOrigin = settings?.app?.url ? publicCloudOrigin(settings.app.url) : undefined;
|
|
261
263
|
const shellData: ShellData = {
|
|
264
|
+
...(cloudOrigin ? { cloudOrigin } : {}),
|
|
262
265
|
...(panel ? { panel } : {}),
|
|
263
266
|
platform: keyboardPlatform(c.req.raw.headers.get("User-Agent") ?? ""),
|
|
264
267
|
...(viewHelp.length ? { help: viewHelp } : {}),
|
|
@@ -294,6 +297,7 @@ export default function Layout(props: LayoutProps) {
|
|
|
294
297
|
apps: allApps,
|
|
295
298
|
legalLinks,
|
|
296
299
|
searchLinks,
|
|
300
|
+
cloudOrigin: shellData.cloudOrigin,
|
|
297
301
|
home: shellData.home,
|
|
298
302
|
profile: shellData.profile,
|
|
299
303
|
announcements: user ? (announcements?.latestAnnouncementVersion ?? null) : null,
|
|
@@ -314,18 +318,21 @@ export default function Layout(props: LayoutProps) {
|
|
|
314
318
|
data-layout-full-page="true"
|
|
315
319
|
data-workspace-sidebar-collapsed={workspaceLayout?.sidebarCollapsed ? "true" : undefined}
|
|
316
320
|
>
|
|
317
|
-
<ShellRuntime
|
|
321
|
+
<ShellRuntime
|
|
322
|
+
{...(currentApp?.access ? { appAccess: { id: currentApp.id, routes: currentApp.routes } } : {})}
|
|
323
|
+
pageTrail
|
|
324
|
+
help={runtimeHelp}
|
|
325
|
+
panel={panel ? true : undefined}
|
|
326
|
+
/>
|
|
318
327
|
<PageTrailProvider trail={pageTrail}>
|
|
319
328
|
<main class="min-h-0 min-w-0 flex-1" data-cloud-app-area="">
|
|
320
329
|
<AppWorkspace.LayoutStateProvider state={workspaceLayout}>{props.children}</AppWorkspace.LayoutStateProvider>
|
|
321
330
|
</main>
|
|
322
331
|
</PageTrailProvider>
|
|
323
|
-
{/* Focus pages
|
|
324
|
-
{panel
|
|
325
|
-
<
|
|
326
|
-
|
|
327
|
-
</ShellDataProvider>
|
|
328
|
-
)}
|
|
332
|
+
{/* Focus pages also expose the canonical origin to their islands. */}
|
|
333
|
+
<ShellDataProvider data={{ cloudOrigin, panel, platform: shellData.platform, theme, apps: [], legalLinks: [], searchLinks: {} }}>
|
|
334
|
+
{panel && <PanelSlot panel={panel} state={{ ...panelState, open: false }} platform={shellData.platform} />}
|
|
335
|
+
</ShellDataProvider>
|
|
329
336
|
<div data-print-fallback="">
|
|
330
337
|
<p>{t.printFallbackTitle}</p>
|
|
331
338
|
<p>{props.printHint ?? t.printFallbackHint}</p>
|
|
@@ -355,6 +362,7 @@ export default function Layout(props: LayoutProps) {
|
|
|
355
362
|
data-workspace-sidebar-collapsed={workspaceLayout?.sidebarCollapsed ? "true" : undefined}
|
|
356
363
|
>
|
|
357
364
|
<ShellRuntime
|
|
365
|
+
{...(currentApp?.access ? { appAccess: { id: currentApp.id, routes: currentApp.routes } } : {})}
|
|
358
366
|
panel={panel ? true : undefined}
|
|
359
367
|
workspace={{ appId: currentApp?.id }}
|
|
360
368
|
pageTrail
|
package/src/ssr/PwaLayout.tsx
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { LocaleProvider, MobileShell, TabBar, type TabBarItem } from "@k2b/ui";
|
|
2
2
|
import type { JSX } from "solid-js";
|
|
3
|
-
import {
|
|
3
|
+
import { buildMetadata } from "../_internal/build-metadata";
|
|
4
|
+
import { registeredPwaBuilds } from "../_internal/runtime-watcher";
|
|
5
|
+
import { PWA_COOKIES, PWA_LIMITS, PWA_SCOPE } from "../contracts/pwa";
|
|
6
|
+
import { pwaBuild } from "../contracts/pwa-builds";
|
|
4
7
|
import { getLocale } from "../server/locale";
|
|
5
8
|
import { visiblePwaParts } from "./app-navigation";
|
|
6
9
|
import type { LayoutContext } from "./layout-context";
|
|
10
|
+
import PwaPartCount from "./PwaPartCount.island";
|
|
7
11
|
import PwaRuntime from "./PwaRuntime.island";
|
|
8
12
|
import { pwaMessages } from "./pwa-messages";
|
|
9
13
|
import { getDisplayRuntimeContext } from "./runtime";
|
|
@@ -17,17 +21,28 @@ export type PwaLayoutProps = {
|
|
|
17
21
|
c: LayoutContext & { header(name: string, value: string): void };
|
|
18
22
|
/** Header heading and document title. */
|
|
19
23
|
title: string;
|
|
20
|
-
/**
|
|
21
|
-
|
|
24
|
+
/** The heading beside the sidebar from 48rem, which already names the installation; `title` by default. */
|
|
25
|
+
sidebarTitle?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Up-navigation inside the part, for example from a detail back to its list. `withSidebar: false` hides it beside
|
|
28
|
+
* the sidebar, which already leads there.
|
|
29
|
+
*/
|
|
30
|
+
back?: { href: string; label: string; withSidebar?: boolean };
|
|
22
31
|
/** At most two icon buttons at the header end. */
|
|
23
32
|
actions?: JSX.Element;
|
|
33
|
+
/**
|
|
34
|
+
* The page brings its own heads and scroll areas, such as `AppWorkspace.Main` levels; the frame keeps only its
|
|
35
|
+
* navigation. `title` still names the document.
|
|
36
|
+
*/
|
|
37
|
+
bare?: boolean;
|
|
24
38
|
children: JSX.Element;
|
|
25
39
|
};
|
|
26
40
|
|
|
27
41
|
/**
|
|
28
42
|
* The frame of every page in the installable mobile app (preview): the app's document head, a phone shell with a
|
|
29
|
-
* tab bar of Start and the first parts,
|
|
30
|
-
* Pages render it below `/pwa/` with
|
|
43
|
+
* tab bar of Start and the first parts, from 48rem a sidebar with Start, every part and Settings instead, and the
|
|
44
|
+
* browser runtime (service worker, offline notice, session keepalive). Pages render it below `/pwa/` with
|
|
45
|
+
* `ssr.pwaAccess`.
|
|
31
46
|
*/
|
|
32
47
|
export default function PwaLayout(props: PwaLayoutProps) {
|
|
33
48
|
const { c } = props;
|
|
@@ -44,25 +59,75 @@ export default function PwaLayout(props: PwaLayoutProps) {
|
|
|
44
59
|
|
|
45
60
|
const parts = visiblePwaParts(getDisplayRuntimeContext(c).apps, user, locale);
|
|
46
61
|
const path = new URL(c.req.raw.url).pathname;
|
|
62
|
+
const owner = page.pwaOwner;
|
|
63
|
+
const ownBuild = pwaBuild(buildMetadata);
|
|
64
|
+
const coreBuild = registeredPwaBuilds().core;
|
|
65
|
+
const builds = { ...(owner && ownBuild ? { [owner]: ownBuild } : {}), ...(coreBuild ? { core: coreBuild } : {}) };
|
|
66
|
+
// A part's badge calls for attention in the sidebar and on its tab; without one, its summary's count stays quiet in
|
|
67
|
+
// the sidebar. One watcher reads every route of the page.
|
|
68
|
+
const countRoutes = [...new Set(parts.flatMap((part) => [part.pwa.badge, part.pwa.summary].filter((path) => path !== undefined)))];
|
|
69
|
+
const badge = (part: (typeof parts)[number]) =>
|
|
70
|
+
part.pwa.badge ? <PwaPartCount endpoint={part.pwa.badge} endpoints={countRoutes} attention /> : undefined;
|
|
47
71
|
const tabs = parts.slice(0, TAB_PARTS);
|
|
48
72
|
const currentTab = tabs.find((part) => path === part.pwa.href || path.startsWith(`${part.pwa.href}/`));
|
|
49
73
|
// Start is titled with the installation's name; a part's first page carries the part's name.
|
|
50
74
|
const items: TabBarItem[] = [
|
|
51
75
|
{ id: "start", label: t.start, title: cloud, icon: "ti ti-home", href: PWA_SCOPE, current: !currentTab },
|
|
52
|
-
...tabs.map((part) => ({
|
|
76
|
+
...tabs.map((part) => ({
|
|
77
|
+
id: part.id,
|
|
78
|
+
label: part.name,
|
|
79
|
+
icon: part.icon,
|
|
80
|
+
href: part.pwa.href,
|
|
81
|
+
current: part === currentTab,
|
|
82
|
+
badge: badge(part),
|
|
83
|
+
})),
|
|
53
84
|
];
|
|
85
|
+
// The sidebar names the installation itself, so Start is titled Start there; Settings is a destination of its own.
|
|
86
|
+
const settingsHref = `${PWA_SCOPE}settings`;
|
|
87
|
+
const currentPart = parts.find((part) => path === part.pwa.href || path.startsWith(`${part.pwa.href}/`));
|
|
88
|
+
const sidebar = (
|
|
89
|
+
<MobileShell.Sidebar
|
|
90
|
+
label={t.tabs}
|
|
91
|
+
title={cloud}
|
|
92
|
+
logo="/branding/logo"
|
|
93
|
+
items={[{ id: "start", label: t.start, icon: "ti ti-home", href: PWA_SCOPE, current: path === PWA_SCOPE }]}
|
|
94
|
+
sections={[
|
|
95
|
+
{
|
|
96
|
+
title: t.apps,
|
|
97
|
+
items: parts.map((part) => ({
|
|
98
|
+
id: part.id,
|
|
99
|
+
label: part.name,
|
|
100
|
+
icon: part.icon,
|
|
101
|
+
href: part.pwa.href,
|
|
102
|
+
current: part === currentPart,
|
|
103
|
+
badge: badge(part),
|
|
104
|
+
count: part.pwa.summary ? <PwaPartCount endpoint={part.pwa.summary} endpoints={countRoutes} /> : undefined,
|
|
105
|
+
})),
|
|
106
|
+
},
|
|
107
|
+
]}
|
|
108
|
+
footer={[{ id: "settings", label: t.settings, icon: "ti ti-settings", href: settingsHref, current: path === settingsHref }]}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
54
111
|
|
|
55
112
|
return (
|
|
56
113
|
<LocaleProvider locale={locale}>
|
|
57
114
|
{/* Pages without a person show no times. Their pairing link lives only in memory, and a reload would lose it. */}
|
|
58
115
|
<TimezoneCookie reload={!!user} />
|
|
59
116
|
<MobileShell
|
|
60
|
-
header={<MobileShell.Header title={props.title} back={props.back} actions={props.actions} />}
|
|
117
|
+
header={<MobileShell.Header title={props.title} sidebarTitle={props.sidebarTitle} back={props.back} actions={props.actions} />}
|
|
118
|
+
scroll={!props.bare}
|
|
61
119
|
footer={user && parts.length > 0 ? <TabBar label={t.tabs} items={items} /> : undefined}
|
|
120
|
+
sidebar={user ? sidebar : undefined}
|
|
62
121
|
>
|
|
63
122
|
{props.children}
|
|
64
123
|
</MobileShell>
|
|
65
|
-
<PwaRuntime
|
|
124
|
+
<PwaRuntime
|
|
125
|
+
keepalive={!!user}
|
|
126
|
+
accountId={user?.id}
|
|
127
|
+
builds={builds}
|
|
128
|
+
cloud={cloud}
|
|
129
|
+
rememberAddress={page.pwaAddress ? { cookie: PWA_COOKIES.lastAddress, maxAge: PWA_LIMITS.idleDays * 86_400 } : undefined}
|
|
130
|
+
/>
|
|
66
131
|
</LocaleProvider>
|
|
67
132
|
);
|
|
68
133
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AppWorkspace } from "@k2b/ui";
|
|
2
|
+
import { onCleanup, onMount, Show } from "solid-js";
|
|
3
|
+
import { appBadgeCount, watchAppBadges } from "./app-badges";
|
|
4
|
+
|
|
5
|
+
export type PwaPartCountProps = {
|
|
6
|
+
/** This part's `pwa.badge` or, without one, its `pwa.summary` route. */
|
|
7
|
+
endpoint: string;
|
|
8
|
+
/** Every count route of the page; the first count on a page reads them all, once a minute like the web's. */
|
|
9
|
+
endpoints: string[];
|
|
10
|
+
/** A badge calls for attention, such as unread messages; a summary's count stays quiet beside the label. */
|
|
11
|
+
attention?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** The count of a part in the Cloud app's sidebar and tab bar; nothing at zero. */
|
|
15
|
+
export default function PwaPartCount(props: PwaPartCountProps) {
|
|
16
|
+
onMount(() => onCleanup(watchAppBadges(props.endpoints)));
|
|
17
|
+
const count = () => appBadgeCount({ endpoint: props.endpoint });
|
|
18
|
+
return (
|
|
19
|
+
<Show when={props.attention} fallback={<Show when={count() > 0}>{count() > 99 ? "99+" : count()}</Show>}>
|
|
20
|
+
<AppWorkspace.SidebarItemStatus unread={count()} />
|
|
21
|
+
</Show>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { observeMobileShell, syncThemeColor, type ToastHandle, toast, useLocale } from "@k2b/ui";
|
|
2
2
|
import { onCleanup, onMount } from "solid-js";
|
|
3
3
|
import { renewAppSession } from "../browser/app-session";
|
|
4
|
+
import { pwaDrafts } from "../browser/pwa-drafts";
|
|
5
|
+
import { reloadOnce } from "../browser/reload";
|
|
6
|
+
import { rememberedPwaAddress } from "../contracts/pwa-location";
|
|
4
7
|
import { PWA_SCOPE, PWA_SERVICE_WORKER_PATH } from "../contracts/pwa-paths";
|
|
5
8
|
import { pwaMessages } from "./pwa-messages";
|
|
6
9
|
|
|
@@ -15,6 +18,11 @@ export type PwaRuntimeProps = {
|
|
|
15
18
|
keepalive: boolean;
|
|
16
19
|
/** The installation name, for the account notice. */
|
|
17
20
|
cloud: string;
|
|
21
|
+
accountId?: string;
|
|
22
|
+
/** Builds used by this document: its owner and Core shared assets. */
|
|
23
|
+
builds?: Record<string, string>;
|
|
24
|
+
/** Only successful authenticated documents remember browser-history navigation. */
|
|
25
|
+
rememberAddress?: { cookie: string; maxAge: number };
|
|
18
26
|
};
|
|
19
27
|
|
|
20
28
|
const firstPageOfSession = (): boolean => {
|
|
@@ -75,21 +83,92 @@ export default function PwaRuntime(props: PwaRuntimeProps) {
|
|
|
75
83
|
|
|
76
84
|
if (!props.keepalive) return;
|
|
77
85
|
|
|
86
|
+
if (props.rememberAddress) {
|
|
87
|
+
const { cookie, maxAge } = props.rememberAddress;
|
|
88
|
+
const remember = () => {
|
|
89
|
+
const address = rememberedPwaAddress(location.pathname + location.search);
|
|
90
|
+
if (address)
|
|
91
|
+
document.cookie = `${cookie}=${encodeURIComponent(address)}; Path=${PWA_SCOPE}; Secure; SameSite=Lax; Max-Age=${maxAge}`;
|
|
92
|
+
};
|
|
93
|
+
// The place counts when a page arrives and when the app leaves the foreground, the moment the system may end
|
|
94
|
+
// it. Selections that parts keep in the address without a page load, such as a Spaces task, are covered this
|
|
95
|
+
// way too. A document that is being left writes nothing: the next page, or a sign-out that deleted the
|
|
96
|
+
// cookie, has already answered, and the outgoing page fires "pagehide" before it turns hidden.
|
|
97
|
+
let leaving = false;
|
|
98
|
+
const hidden = () => {
|
|
99
|
+
if (document.visibilityState === "hidden" && !leaving) remember();
|
|
100
|
+
};
|
|
101
|
+
const leave = () => {
|
|
102
|
+
leaving = true;
|
|
103
|
+
};
|
|
104
|
+
const back = (event: PageTransitionEvent) => {
|
|
105
|
+
if (!event.persisted) return;
|
|
106
|
+
leaving = false;
|
|
107
|
+
remember();
|
|
108
|
+
};
|
|
109
|
+
remember();
|
|
110
|
+
document.addEventListener("visibilitychange", hidden);
|
|
111
|
+
window.addEventListener("pagehide", leave);
|
|
112
|
+
window.addEventListener("pageshow", back);
|
|
113
|
+
cleanups.push(() => {
|
|
114
|
+
document.removeEventListener("visibilitychange", hidden);
|
|
115
|
+
window.removeEventListener("pagehide", leave);
|
|
116
|
+
window.removeEventListener("pageshow", back);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
78
120
|
let otherAccountShown = false;
|
|
79
121
|
let disposed = false;
|
|
80
122
|
cleanups.push(() => {
|
|
81
123
|
disposed = true;
|
|
82
124
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
125
|
+
let updateToast: ToastHandle | undefined;
|
|
126
|
+
let reloading = false;
|
|
127
|
+
cleanups.push(() => updateToast?.dismiss());
|
|
128
|
+
// Every control a person types into, numbers, dates and times included.
|
|
129
|
+
const textField =
|
|
130
|
+
'textarea, input:not([type="checkbox"], [type="radio"], [type="button"], [type="submit"], [type="reset"], [type="hidden"], [type="file"], [type="range"], [type="color"], [type="image"]), [contenteditable]:not([contenteditable="false"])';
|
|
131
|
+
const edited = new WeakSet<Element>();
|
|
132
|
+
const input = (event: Event) => {
|
|
133
|
+
const field = event.target instanceof Element ? event.target.closest(textField) : null;
|
|
134
|
+
if (field) edited.add(field);
|
|
135
|
+
};
|
|
136
|
+
document.addEventListener("input", input, true);
|
|
137
|
+
cleanups.push(() => document.removeEventListener("input", input, true));
|
|
138
|
+
const unsafe = () =>
|
|
139
|
+
document.activeElement?.matches(textField) ||
|
|
140
|
+
!!(props.accountId && pwaDrafts.has(props.accountId)) ||
|
|
141
|
+
!!document.querySelector('dialog[open]:not([aria-modal="false"])') ||
|
|
142
|
+
[...document.querySelectorAll(textField)].some(
|
|
143
|
+
(field) =>
|
|
144
|
+
edited.has(field) &&
|
|
145
|
+
(!(field instanceof window.HTMLInputElement || field instanceof window.HTMLTextAreaElement) ||
|
|
146
|
+
field.value !== field.defaultValue),
|
|
147
|
+
);
|
|
148
|
+
const renew = async (foreground = false) => {
|
|
149
|
+
const { ok, otherAccount, builds } = await renewAppSession();
|
|
150
|
+
if (disposed) return;
|
|
151
|
+
if (otherAccount && !otherAccountShown) {
|
|
152
|
+
otherAccountShown = true;
|
|
153
|
+
toast(t().otherAccount({ cloud: props.cloud, name: otherAccount.name }), { duration: 0, iconClass: "ti ti-alert-triangle" });
|
|
154
|
+
}
|
|
155
|
+
const changed = ok && Object.entries(props.builds ?? {}).some(([id, build]) => builds?.[id] !== undefined && builds[id] !== build);
|
|
156
|
+
if (!changed || reloading || document.visibilityState !== "visible") return;
|
|
157
|
+
// Only the foreground transition is an automatic reload opportunity. Re-check input after the request.
|
|
158
|
+
if (foreground && !unsafe() && reloadOnce("pwa-update")) {
|
|
159
|
+
reloading = true;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
updateToast ??= toast(t().newVersion({ cloud: props.cloud }), {
|
|
163
|
+
duration: 0,
|
|
164
|
+
iconClass: "ti ti-refresh",
|
|
165
|
+
action: { label: t().reload, onClick: () => location.reload() },
|
|
166
|
+
});
|
|
88
167
|
};
|
|
89
168
|
|
|
90
169
|
if (firstPageOfSession()) void renew();
|
|
91
170
|
const visible = () => {
|
|
92
|
-
if (document.visibilityState === "visible") void renew();
|
|
171
|
+
if (document.visibilityState === "visible") void renew(true);
|
|
93
172
|
};
|
|
94
173
|
document.addEventListener("visibilitychange", visible);
|
|
95
174
|
const timer = setInterval(() => {
|