@k2b/cloud 0.38.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/README.md +8 -0
  2. package/package.json +12 -5
  3. package/scripts/build-canvas-workers.ts +109 -29
  4. package/scripts/build.ts +3 -5
  5. package/src/_internal/app-favicon.ts +4 -2
  6. package/src/_internal/app-path.ts +25 -0
  7. package/src/_internal/app-presentation.ts +4 -0
  8. package/src/_internal/canvas-worker.ts +5 -3
  9. package/src/_internal/capabilities.ts +72 -48
  10. package/src/_internal/capability-reviews.ts +81 -0
  11. package/src/_internal/capability-schemas.ts +29 -0
  12. package/src/_internal/capability-sentences.ts +216 -1
  13. package/src/_internal/capability-streams.ts +7 -0
  14. package/src/_internal/define-app.ts +63 -16
  15. package/src/_internal/font-preloads.ts +2 -1
  16. package/src/_internal/panel-cookie.ts +39 -0
  17. package/src/_internal/registry-validation.ts +21 -26
  18. package/src/_internal/runtime-context.ts +1 -0
  19. package/src/_internal/runtime-watcher.ts +6 -0
  20. package/src/_internal/static-assets.ts +52 -3
  21. package/src/_internal/status-preserving-ssr.ts +2 -0
  22. package/src/access/PermissionEditor.browser-harness.tsx +57 -38
  23. package/src/access/PermissionEditor.tsx +32 -7
  24. package/src/access/ResourceApiKeys.tsx +5 -1
  25. package/src/account/EntitySearch.tsx +1 -1
  26. package/src/ai/action-review.ts +101 -0
  27. package/src/ai/approval-routes.ts +5 -2
  28. package/src/ai/approvals.ts +139 -28
  29. package/src/ai/background-review.ts +72 -0
  30. package/src/ai/browser.ts +10 -0
  31. package/src/ai/capabilities.ts +29 -11
  32. package/src/ai/capability-execution.ts +76 -43
  33. package/src/ai/capability-result.ts +95 -0
  34. package/src/ai/chat/action-card.ts +398 -0
  35. package/src/ai/chat/action-messages.ts +129 -0
  36. package/src/ai/chat/action-question.browser-harness.tsx +245 -0
  37. package/src/ai/chat/action-question.fixtures.ts +733 -0
  38. package/src/ai/chat/action-question.tsx +669 -0
  39. package/src/ai/chat/action-sentences.ts +1 -44
  40. package/src/ai/chat/blocks.tsx +19 -25
  41. package/src/ai/chat/message-actions.tsx +6 -0
  42. package/src/ai/chat/tool-action.tsx +481 -0
  43. package/src/ai/chat/turn-view.tsx +45 -163
  44. package/src/ai/chat/view-block.tsx +22 -7
  45. package/src/ai/chat-task-contracts.ts +31 -3
  46. package/src/ai/chat-task-reasons-migrate.ts +75 -0
  47. package/src/ai/chat-task-reasons.ts +92 -0
  48. package/src/ai/chat-tasks.ts +438 -78
  49. package/src/ai/client/controller.ts +59 -0
  50. package/src/ai/default-tools.ts +9 -1
  51. package/src/ai/executor.ts +190 -39
  52. package/src/ai/index.ts +14 -0
  53. package/src/ai/migrate.ts +39 -4
  54. package/src/ai/projects-routes.ts +3 -0
  55. package/src/ai/protocol.ts +14 -2
  56. package/src/ai/recent-resources.ts +240 -0
  57. package/src/ai/recent-sent-refs.ts +38 -0
  58. package/src/ai/request-context.ts +7 -1
  59. package/src/ai/routes.ts +172 -2
  60. package/src/ai/runtime.ts +13 -27
  61. package/src/ai/skills-routes.ts +3 -0
  62. package/src/ai/skills.ts +10 -1
  63. package/src/ai/store.ts +232 -27
  64. package/src/ai/timeline.ts +9 -1
  65. package/src/ai/tool-audit.ts +8 -0
  66. package/src/ai/tool-wording.ts +67 -0
  67. package/src/ai/turn-abort.ts +33 -0
  68. package/src/ai/types.ts +40 -4
  69. package/src/api/accounts-entities.ts +2 -1
  70. package/src/api/admin-ai-projects.ts +2 -0
  71. package/src/api/admin-ai-skills.ts +2 -0
  72. package/src/api/admin-ai-usage.ts +2 -0
  73. package/src/api/admin-core-settings.ts +190 -139
  74. package/src/api/admin-geoip.ts +80 -0
  75. package/src/api/admin-identity.ts +3 -0
  76. package/src/api/admin-lifecycle.ts +3 -0
  77. package/src/api/admin-outgoing-mail.ts +6 -2
  78. package/src/api/app-approval.ts +54 -6
  79. package/src/api/approvals-loader.ts +23 -1
  80. package/src/api/approvals.ts +3 -1
  81. package/src/api/auth.ts +25 -6
  82. package/src/api/capabilities.ts +44 -11
  83. package/src/api/capability-streams.ts +12 -0
  84. package/src/api/help.ts +4 -28
  85. package/src/api/index.ts +7 -1
  86. package/src/api/lasting-access-messages.ts +17 -0
  87. package/src/api/mcp.ts +3 -0
  88. package/src/api/me-panel-consent.ts +71 -0
  89. package/src/api/me.ts +80 -33
  90. package/src/api/search.ts +3 -1
  91. package/src/api/sync-ops.ts +2 -0
  92. package/src/api/widgets.ts +3 -0
  93. package/src/approvals/Approvals.tsx +180 -238
  94. package/src/approvals/messages.ts +12 -24
  95. package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
  96. package/src/browser/CloudResourceSearch.tsx +10 -6
  97. package/src/browser/app-approval.ts +60 -6
  98. package/src/browser/commands.ts +22 -6
  99. package/src/browser/composer-catalog.ts +145 -0
  100. package/src/browser/composer-insert-messages.ts +31 -0
  101. package/src/browser/composer-insert.ts +160 -0
  102. package/src/browser/composer-open-messages.ts +17 -0
  103. package/src/browser/composer-open.ts +110 -0
  104. package/src/browser/discoverable-command.ts +6 -0
  105. package/src/browser/hints.ts +6 -0
  106. package/src/browser/live-connection.ts +26 -2
  107. package/src/browser/navigation-apps.ts +7 -0
  108. package/src/browser/panel-context-bridge.ts +30 -0
  109. package/src/browser/panel-context-receiver.ts +53 -0
  110. package/src/browser/panel-menu.tsx +56 -0
  111. package/src/browser/panel-messages.ts +35 -0
  112. package/src/browser/panel-shortcuts.ts +31 -0
  113. package/src/browser/panel-slot.ts +756 -0
  114. package/src/browser/panel.ts +65 -0
  115. package/src/browser/resource-picker.tsx +12 -1
  116. package/src/browser/search-commands.ts +6 -3
  117. package/src/capabilities/client.ts +8 -2
  118. package/src/capabilities/reviews.ts +205 -0
  119. package/src/capabilities/server.ts +10 -3
  120. package/src/capabilities/testing.ts +47 -0
  121. package/src/capabilities/types.ts +2 -2
  122. package/src/cli/account.ts +13 -3
  123. package/src/cli/admin/geoip.ts +115 -0
  124. package/src/cli/admin/index.ts +3 -0
  125. package/src/contracts/app-approval.ts +159 -12
  126. package/src/contracts/app.ts +14 -0
  127. package/src/contracts/approvals.ts +2 -2
  128. package/src/contracts/capabilities.ts +410 -18
  129. package/src/contracts/geoip.ts +42 -0
  130. package/src/contracts/index.ts +3 -0
  131. package/src/contracts/panel-context-plain.ts +99 -0
  132. package/src/contracts/panel-context.ts +93 -0
  133. package/src/contracts/registry.ts +3 -1
  134. package/src/contracts/seen-hints.ts +35 -0
  135. package/src/contracts/shared.ts +2 -0
  136. package/src/server/client-ip.ts +5 -0
  137. package/src/server/help-payload.ts +38 -0
  138. package/src/server/middleware/auth.ts +11 -1
  139. package/src/server/middleware/middleware.ts +9 -0
  140. package/src/server/middleware/no-transform.ts +19 -0
  141. package/src/server/middleware/rate-limit.ts +4 -6
  142. package/src/server/request-context/address.ts +45 -0
  143. package/src/server/request-context/geoip.ts +380 -0
  144. package/src/server/request-context/index.ts +33 -0
  145. package/src/server/request-context/user-agent.ts +95 -0
  146. package/src/server/services/access.ts +5 -1
  147. package/src/services/accounts/posix.ts +2 -1
  148. package/src/services/app-approval.ts +533 -90
  149. package/src/services/approvals/device.ts +9 -5
  150. package/src/services/approvals/index.ts +6 -5
  151. package/src/services/approvals/list.ts +26 -5
  152. package/src/services/auth-flows/notification-sender.ts +3 -0
  153. package/src/services/geoip/index.ts +164 -0
  154. package/src/services/geoip/store.ts +30 -0
  155. package/src/services/geoip/upload.ts +150 -0
  156. package/src/services/identity/routes.ts +5 -1
  157. package/src/services/index.ts +32 -3
  158. package/src/services/known-browsers.ts +312 -0
  159. package/src/services/login-address-store.ts +51 -0
  160. package/src/services/login-context.ts +118 -0
  161. package/src/services/mandates/index.ts +3 -0
  162. package/src/services/mandates/policy.ts +15 -3
  163. package/src/services/notifications/browser.ts +16 -5
  164. package/src/services/notifications/email-frame.ts +1 -1
  165. package/src/services/oauth-tokens.ts +16 -2
  166. package/src/services/panel-context-consent.ts +58 -0
  167. package/src/services/pdf/image-protocol.ts +135 -0
  168. package/src/services/pdf/image-variants.ts +124 -0
  169. package/src/services/pdf/image-worker.ts +31 -0
  170. package/src/services/pdf/image.ts +109 -0
  171. package/src/services/pdf/index.ts +9 -0
  172. package/src/services/service-account-credentials.ts +21 -0
  173. package/src/services/session/index.ts +97 -16
  174. package/src/services/session/recent.ts +13 -0
  175. package/src/services/settings/api.ts +11 -6
  176. package/src/services/settings/app.ts +5 -4
  177. package/src/services/settings/core-settings.ts +4 -3
  178. package/src/services/settings/index.ts +19 -17
  179. package/src/services/settings/namespace.ts +2 -1
  180. package/src/services/settings/store.ts +83 -11
  181. package/src/services/webauthn.ts +12 -0
  182. package/src/shared/ai-platform-prompt.ts +1 -1
  183. package/src/shared/app-presentation.ts +1 -0
  184. package/src/shared/email-html.ts +2 -0
  185. package/src/shared/help.ts +3 -0
  186. package/src/shared/redirect.ts +12 -0
  187. package/src/shared-text/cursor.ts +22 -0
  188. package/src/shared-text/documents.ts +142 -0
  189. package/src/shared-text/events.ts +43 -0
  190. package/src/shared-text/index.ts +12 -0
  191. package/src/shared-text/log.ts +48 -0
  192. package/src/shared-text/text-diff.ts +23 -0
  193. package/src/shared-text/types.ts +41 -0
  194. package/src/ssr/AdminLayout.tsx +2 -0
  195. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  196. package/src/ssr/CloudShell.island.tsx +95 -10
  197. package/src/ssr/Layout.tsx +66 -10
  198. package/src/ssr/LayoutHeader.tsx +15 -9
  199. package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
  200. package/src/ssr/LayoutHelp.tsx +435 -274
  201. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
  202. package/src/ssr/LayoutHelpTrigger.tsx +27 -2
  203. package/src/ssr/LayoutRail.tsx +2 -2
  204. package/src/ssr/LayoutTabBar.tsx +29 -0
  205. package/src/ssr/MobileNavigation.tsx +2 -2
  206. package/src/ssr/PanelLayout.tsx +35 -0
  207. package/src/ssr/PanelRuntime.island.tsx +94 -0
  208. package/src/ssr/PanelSlot.tsx +158 -0
  209. package/src/ssr/PanelTrigger.tsx +49 -0
  210. package/src/ssr/ShellRuntime.island.tsx +69 -5
  211. package/src/ssr/app-appearance.ts +3 -2
  212. package/src/ssr/app-color.ts +105 -0
  213. package/src/ssr/app-launchpad.tsx +18 -1
  214. package/src/ssr/app-navigation.ts +28 -0
  215. package/src/ssr/compact-shell.baseline.json +5270 -0
  216. package/src/ssr/compact-shell.browser-harness.ts +439 -0
  217. package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
  218. package/src/ssr/help-messages.ts +4 -0
  219. package/src/ssr/help.ts +20 -0
  220. package/src/ssr/hints.ts +25 -0
  221. package/src/ssr/index.ts +5 -0
  222. package/src/ssr/islands/index.ts +4 -3
  223. package/src/ssr/layout-context.ts +2 -0
  224. package/src/ssr/layout-help-article.server.ts +50 -0
  225. package/src/ssr/layout-help-start.ts +33 -7
  226. package/src/ssr/panel-consent.ts +29 -0
  227. package/src/ssr/panel-context.ts +10 -0
  228. package/src/ssr/panel-slot.browser-harness.tsx +159 -0
  229. package/src/ssr/platform-messages.ts +48 -0
  230. package/src/ssr/rail-navigation.ts +44 -0
  231. package/src/ssr/shell-data.tsx +5 -0
  232. package/src/ssr/shell-shortcuts.ts +13 -2
  233. package/src/styles/effects.css +201 -11
  234. package/src/styles/file-chooser.css +2 -1
  235. package/src/styles/global.css +314 -63
  236. package/src/styles/panel.css +416 -0
  237. package/src/styles/resource-search.css +6 -2
  238. package/src/styles/tokens.css +36 -13
  239. package/src/styles/utilities-buttons.css +3 -2
  240. package/src/styles/utilities-data.css +2 -1
  241. package/src/styles/utilities-feedback.css +2 -2
  242. package/src/styles/utilities-layout.css +3 -13
  243. package/src/styles/utilities-markdown-editor.css +6 -6
  244. package/src/styles/utilities-navigation.css +22 -43
@@ -4,6 +4,7 @@ import {
4
4
  installAppWorkspaceController,
5
5
  installInputModeController,
6
6
  installPageTitleController,
7
+ keyboardPlatform,
7
8
  serializeAppWorkspaceLayoutState,
8
9
  useLocale,
9
10
  } from "@k2b/ui";
@@ -12,6 +13,7 @@ import { appWorkspaceCookieName, readAppWorkspaceLayoutCookie } from "../_intern
12
13
  import { registerContextAwareCommand } from "../browser/command-bridge";
13
14
  import { attachCommandShortcuts } from "../browser/command-shortcuts";
14
15
  import { navigationSearchItems } from "../browser/navigation-search";
16
+ import { openPanel, type PanelRequest } from "../browser/panel";
15
17
  import { importOnDemand } from "../browser/reload";
16
18
  import { registerGlobalSearchHost } from "../browser/search-bridge";
17
19
  import { attachShellFocusControls, readShellFocus, SHELL_FOCUS_CHANGE_EVENT, toggleShellFocus } from "../browser/shell-focus";
@@ -32,6 +34,8 @@ import { shellShortcuts } from "./shell-shortcuts";
32
34
  import { syncTimezoneCookie } from "./timezone-cookie";
33
35
 
34
36
  export type ShellRuntimeProps = {
37
+ /** This page has an authorized panel slot. */
38
+ panel?: boolean;
35
39
  /** Layout pages outside focus mode: keep the current app's workspace layout in its cookie. */
36
40
  workspace?: { appId?: string };
37
41
  /** Layout pages: page titles, the way up (Alt+Up) and its per-tab memory. */
@@ -70,6 +74,61 @@ export default function ShellRuntime(props: ShellRuntimeProps) {
70
74
  onMount(() => syncTimezoneCookie(true));
71
75
  onMount(() => onCleanup(installInputModeController()));
72
76
 
77
+ const togglePanel = () => {
78
+ window.dispatchEvent(new CustomEvent<PanelRequest>("cloud.panel.request", { detail: { action: "toggle" } }));
79
+ };
80
+ if (props.panel) {
81
+ onMount(() => {
82
+ let stopped = false;
83
+ let dispose: (() => void) | undefined;
84
+ const load = async (request?: PanelRequest) => {
85
+ const panel = await importOnDemand(() => import("../browser/panel-slot"));
86
+ if (!panel || stopped) return;
87
+ dispose ??= panel.installPanelSlot();
88
+ if (request) panel.handlePanelRequest(request);
89
+ };
90
+ const request = (event: CustomEvent<PanelRequest>) => {
91
+ const detail = event.detail;
92
+ if (detail.action === "open") {
93
+ if (detail.accepted || !readShellData()?.panel?.providers.some((provider) => provider.id === detail.provider)) return;
94
+ detail.accepted = true;
95
+ }
96
+ void load(event.detail);
97
+ };
98
+ const key = (event: KeyboardEvent) => {
99
+ const apple = keyboardPlatform() === "apple";
100
+ if (
101
+ event.defaultPrevented ||
102
+ event.isComposing ||
103
+ event.repeat ||
104
+ event.altKey ||
105
+ event.shiftKey ||
106
+ event.key.toLowerCase() !== "j" ||
107
+ event.metaKey !== apple ||
108
+ event.ctrlKey !== !apple
109
+ )
110
+ return;
111
+ event.preventDefault();
112
+ togglePanel();
113
+ };
114
+ // The panel controller loads lazily; it reads this mark to recheck consent after a restore it did not see.
115
+ const restored = (event: PageTransitionEvent) => {
116
+ if (event.persisted) document.documentElement.setAttribute("data-cloud-page-restored", "");
117
+ };
118
+ window.addEventListener("pageshow", restored);
119
+ window.addEventListener("cloud.panel.request", request);
120
+ if (!props.commands) window.addEventListener("keydown", key);
121
+ if (document.querySelector('.cloud-app-canvas[data-panel="open"]')) void load();
122
+ onCleanup(() => {
123
+ stopped = true;
124
+ dispose?.();
125
+ window.removeEventListener("pageshow", restored);
126
+ window.removeEventListener("cloud.panel.request", request);
127
+ window.removeEventListener("keydown", key);
128
+ });
129
+ });
130
+ }
131
+
73
132
  if (props.workspace) {
74
133
  onMount(() => {
75
134
  const appId = props.workspace?.appId;
@@ -146,15 +205,20 @@ export default function ShellRuntime(props: ShellRuntimeProps) {
146
205
  ),
147
206
  );
148
207
  createEffect(() => {
149
- for (const command of shellShortcuts(t())) {
208
+ for (const command of shellShortcuts(t(), { panel: props.panel })) {
150
209
  onCleanup(
151
210
  registerContextAwareCommand({
152
211
  ...command,
153
212
  action:
154
- command.id === "cloud.search"
155
- ? { search: {} }
156
- : // Returning the load keeps the Command busy until the dialog is open; a second press opens no second dialog.
157
- () => importOnDemand(() => import("./LayoutHelp")).then((help) => help?.openLayoutHelpDialog()),
213
+ command.id === "cloud.panel"
214
+ ? togglePanel
215
+ : command.id === "cloud.search"
216
+ ? { search: {} }
217
+ : // Returning the load keeps the Command busy until the dialog is open; a second press opens no second dialog.
218
+ () => {
219
+ if (openPanel("core")) return;
220
+ return importOnDemand(() => import("./LayoutHelp")).then((help) => help?.openLayoutHelpDialog());
221
+ },
158
222
  }),
159
223
  );
160
224
  }
@@ -1,4 +1,5 @@
1
1
  import type { AppAppearance } from "../contracts/app";
2
+ import { appColorStyle } from "./app-color";
2
3
  import { resolveRuntimeRoute } from "./app-navigation";
3
4
  import { helpPageAppId } from "./layout-help-url";
4
5
  import type { RuntimeContext } from "./runtime";
@@ -15,11 +16,11 @@ export const appAppearanceStyle = (appearance: AppAppearance | undefined): strin
15
16
  const angle = Number.isFinite(appearance.background?.angle) ? Math.max(0, Math.min(360, appearance.background?.angle ?? 135)) : 135;
16
17
  const strength = clampStrength(appearance.background?.strength, 20);
17
18
  const darkStrength = Math.round(strength / 2);
18
- return `--app-accent:${appearance.accent};--app-canvas-from:${from};--app-canvas-via:${via};--app-canvas-to:${to};--app-canvas-angle:${angle}deg;--app-canvas-strength:${strength}%;--app-canvas-dark-strength:${darkStrength}%`;
19
+ return `--app-accent:${appearance.accent};${appColorStyle(appearance.accent)};--app-canvas-from:${from};--app-canvas-via:${via};--app-canvas-to:${to};--app-canvas-angle:${angle}deg;--app-canvas-strength:${strength}%;--app-canvas-dark-strength:${darkStrength}%`;
19
20
  };
20
21
 
21
22
  export const appAccentStyle = (accent: string | undefined): string | undefined =>
22
- isHexColor(accent) ? `--app-accent:${accent}` : undefined;
23
+ isHexColor(accent) ? `--app-accent:${accent};${appColorStyle(accent)}` : undefined;
23
24
 
24
25
  /** Core's full-page Help of an app belongs to that app. */
25
26
  export const resolveCurrentApp = (apps: RuntimeContext["apps"], pathname: string) => {
@@ -0,0 +1,105 @@
1
+ type HexColor = `#${string}`;
2
+
3
+ const minimumContrast = 4.5;
4
+ const fillShares = {
5
+ light: { selected: 0.07, selectedNav: 0.1 },
6
+ dark: { selected: 0.14, selectedNav: 0.17 },
7
+ } as const;
8
+
9
+ /**
10
+ * Opaque theme backgrounds checked for readable app ink; a test pins them to
11
+ * tokens.css and @k2b/ui. `sidebar` is the workspace sidebar's own background
12
+ * (`--k2b-surface` since the one-surface workspace); a test reads that
13
+ * declaration from @k2b/ui. `sidebarHover` is its row hover over it; `subtle`,
14
+ * `muted`, `hover`, and `platformSelected` are the `--ui-surface-subtle`,
15
+ * `--ui-surface-muted`, `--ui-hover`, and blue `--ui-selected` fills app ink
16
+ * also sits on.
17
+ */
18
+ export const appColorSurfaces = {
19
+ light: {
20
+ canvas: "#fafafa",
21
+ surface: "#ffffff",
22
+ sidebar: "#ffffff",
23
+ sidebarHover: "#f3f3f3",
24
+ subtle: "#f8f8f8",
25
+ muted: "#f3f4f3",
26
+ raised: "#ffffff",
27
+ hover: "#f0f1f0",
28
+ platformSelected: "#f0f6ff",
29
+ },
30
+ dark: {
31
+ canvas: "#090d12",
32
+ surface: "#11151b",
33
+ sidebar: "#11151b",
34
+ sidebarHover: "#1d2026",
35
+ subtle: "#161a20",
36
+ muted: "#20242a",
37
+ raised: "#181d24",
38
+ hover: "#252c35",
39
+ platformSelected: "#192636",
40
+ },
41
+ } as const;
42
+
43
+ const channels = (hex: HexColor) =>
44
+ [Number.parseInt(hex.slice(1, 3), 16), Number.parseInt(hex.slice(3, 5), 16), Number.parseInt(hex.slice(5, 7), 16)] as const;
45
+
46
+ /** WCAG 2.x relative luminance from a six-digit sRGB hex color. */
47
+ export const relativeLuminance = (hex: HexColor): number => {
48
+ const linearize = (value: number) => {
49
+ const c = value / 255;
50
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
51
+ };
52
+ const [r, g, b] = channels(hex);
53
+ return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b);
54
+ };
55
+
56
+ /** WCAG 2.x contrast ratio between two opaque hex colors. */
57
+ export const contrastRatio = (a: HexColor, b: HexColor): number => {
58
+ const first = relativeLuminance(a);
59
+ const second = relativeLuminance(b);
60
+ return (Math.max(first, second) + 0.05) / (Math.min(first, second) + 0.05);
61
+ };
62
+
63
+ /** Rounded sRGB channel mix, with share indicating the fraction of a. */
64
+ export const mixHex = (a: HexColor, b: HexColor, share: number): HexColor => {
65
+ const second = channels(b);
66
+ const fraction = Math.max(0, Math.min(1, share));
67
+ return `#${channels(a)
68
+ .map((value, index) =>
69
+ Math.round(value * fraction + second[index]! * (1 - fraction))
70
+ .toString(16)
71
+ .padStart(2, "0"),
72
+ )
73
+ .join("")}`;
74
+ };
75
+
76
+ /** Derive selection fills, the shell's open-app fill, and the nearest AA-readable app ink in each theme. */
77
+ export const appColors = (accent: HexColor) => {
78
+ const forTheme = (theme: keyof typeof appColorSurfaces) => {
79
+ const surfaces = appColorSurfaces[theme];
80
+ const selected = mixHex(accent, surfaces.surface, fillShares[theme].selected);
81
+ const selectedNav = mixHex(accent, surfaces.sidebar, fillShares[theme].selectedNav);
82
+ // Cloud's shell marks the open app on its app-colored bar (global.css):
83
+ // with the surface in light, and in dark with the navigation share over the
84
+ // muted surface, which stays apart from the dark bar.
85
+ const shellSelected = theme === "light" ? surfaces.surface : mixHex(accent, surfaces.muted, fillShares.dark.selectedNav);
86
+ const backgrounds = [...Object.values(surfaces), selected, selectedNav, shellSelected];
87
+ const toward = theme === "light" ? "#000000" : "#ffffff";
88
+ let ink: HexColor = toward;
89
+ for (let i = 100; i >= 0; i--) {
90
+ const candidate = mixHex(accent, toward, i / 100);
91
+ if (backgrounds.every((background) => contrastRatio(candidate, background) >= minimumContrast)) {
92
+ ink = candidate;
93
+ break;
94
+ }
95
+ }
96
+ return { ink, selected, selectedNav, shellSelected };
97
+ };
98
+ return { light: forTheme("light"), dark: forTheme("dark") };
99
+ };
100
+
101
+ /** Plain hex declarations for server-rendered app appearance and accent scopes. */
102
+ export const appColorStyle = (accent: HexColor): string => {
103
+ const { light, dark } = appColors(accent);
104
+ return `--app-ink-light:${light.ink};--app-ink-dark:${dark.ink};--app-selected-light:${light.selected};--app-selected-dark:${dark.selected};--app-selected-nav-light:${light.selectedNav};--app-selected-nav-dark:${dark.selectedNav}`;
105
+ };
@@ -66,7 +66,7 @@ export function AppLaunchpadProvider(props: AppLaunchpadContext) {
66
66
  return <span class="hidden" data-cloud-app-launchpad-provider />;
67
67
  }
68
68
 
69
- export function AppLaunchpadButton(props: AppLaunchpadContext & { variant: "rail" | "header" | "menu"; label?: string }) {
69
+ export function AppLaunchpadButton(props: AppLaunchpadContext & { variant: "rail" | "header" | "title" | "menu"; label?: string }) {
70
70
  const locale = useLocale();
71
71
  const t = () => platformMessages.resolve([locale()]).t;
72
72
  const action = createBusyAction();
@@ -98,6 +98,23 @@ export function AppLaunchpadButton(props: AppLaunchpadContext & { variant: "rail
98
98
  );
99
99
  }
100
100
 
101
+ // The page title as the app menu's trigger on phones; global.css shows it only when the page has app navigation.
102
+ if (props.variant === "title") {
103
+ return (
104
+ <button
105
+ type="button"
106
+ class="layout-header-title-menu"
107
+ aria-haspopup="dialog"
108
+ onClick={open}
109
+ aria-busy={action.busy() ? "true" : undefined}
110
+ >
111
+ <span class="truncate font-semibold">{props.label}</span>
112
+ <span class="sr-only">{`, ${t().appMenu}`}</span>
113
+ <i class={action.busy() ? "ti ti-loader-2 animate-spin" : "ti ti-chevron-down"} aria-hidden="true" />
114
+ </button>
115
+ );
116
+ }
117
+
101
118
  if (props.variant === "header") {
102
119
  return (
103
120
  <IconButton label={props.label ?? t().openApps} onClick={open} aria-busy={action.busy() ? "true" : undefined}>
@@ -1,5 +1,7 @@
1
+ import { isOwnAppPath } from "../_internal/app-path";
1
2
  import { hasAnyAppRole } from "../_internal/app-roles";
2
3
  import type { RuntimeAppMeta } from "../contracts/app";
4
+ import { panelProviderNeedsConsent } from "../contracts/panel-context-plain";
3
5
  import { isPwaPartId } from "../contracts/pwa-paths";
4
6
  import type { User } from "../contracts/shared";
5
7
  import { resolveAppPresentation } from "../shared/app-presentation";
@@ -52,3 +54,29 @@ export const visiblePwaParts = (apps: readonly RuntimeAppMeta[], user: User | un
52
54
  .map((app) => resolveAppPresentation(app, locale))
53
55
  .sort((a, b) => a.name.localeCompare(b.name, locale) || a.id.localeCompare(b.id));
54
56
  };
57
+
58
+ export type PanelProvider = { id: string; label: string; icon: string; href: string; appName: string; consent: boolean };
59
+
60
+ /** Authorized presentation only: the panel route still authorizes every request. */
61
+ export const visiblePanelProviders = (apps: readonly RuntimeAppMeta[], user: User | undefined, locale: string): PanelProvider[] => {
62
+ if (!user) return [];
63
+ return apps
64
+ .flatMap((app) => {
65
+ if (!app.panel || !isOwnAppPath(app.panel.href, app.routes) || !hasAnyAppRole(user, app.panel.requiresRoles)) return [];
66
+ const presentation = resolveAppPresentation(app, locale);
67
+ const panel = presentation.panel;
68
+ return panel
69
+ ? [
70
+ {
71
+ id: app.id,
72
+ label: panel.label,
73
+ icon: panel.icon,
74
+ href: panel.href,
75
+ appName: presentation.name,
76
+ consent: panelProviderNeedsConsent(app.id),
77
+ },
78
+ ]
79
+ : [];
80
+ })
81
+ .sort((a, b) => a.label.localeCompare(b.label, locale) || a.id.localeCompare(b.id));
82
+ };