@k2b/cloud 0.6.0 → 0.8.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 (349) hide show
  1. package/README.md +2 -2
  2. package/package.json +22 -11
  3. package/scripts/README.md +10 -0
  4. package/scripts/browser-performance.ts +14 -0
  5. package/scripts/build-pdf-renderer.ts +39 -0
  6. package/scripts/build.ts +20 -35
  7. package/scripts/preload.ts +16 -12
  8. package/scripts/sync-recovery-smoke.ts +22 -6
  9. package/src/_internal/app-presentation.ts +14 -2
  10. package/src/_internal/build-metadata.ts +8 -1
  11. package/src/_internal/capabilities.ts +131 -7
  12. package/src/_internal/capability-streams.ts +81 -0
  13. package/src/_internal/define-app.ts +80 -37
  14. package/src/_internal/heartbeat.ts +4 -0
  15. package/src/_internal/help-catalog.ts +27 -172
  16. package/src/_internal/help.ts +13 -76
  17. package/src/_internal/page-responses.ts +2 -0
  18. package/src/_internal/postgres-application-name.ts +30 -0
  19. package/src/_internal/process-identity.ts +15 -0
  20. package/src/_internal/process-sync.ts +1 -1
  21. package/src/_internal/readiness.ts +4 -1
  22. package/src/_internal/registry-validation.ts +29 -25
  23. package/src/_internal/registry.ts +2 -53
  24. package/src/_internal/runtime-context.ts +5 -16
  25. package/src/_internal/server-timing.ts +13 -0
  26. package/src/_internal/status-preserving-ssr.ts +18 -6
  27. package/src/_internal/web-vitals-asset.ts +10 -0
  28. package/src/access/PermissionEditor.tsx +18 -151
  29. package/src/access/PrincipalPicker.tsx +110 -0
  30. package/src/access/ResourceApiKeys.tsx +1 -1
  31. package/src/access/ui.ts +1 -0
  32. package/src/ai/admin.ts +14 -13
  33. package/src/ai/assistant-models.ts +12 -1
  34. package/src/ai/audio-format.ts +34 -0
  35. package/src/ai/audio-tool.ts +90 -0
  36. package/src/ai/browser-code-contracts.ts +201 -0
  37. package/src/ai/browser.ts +4 -0
  38. package/src/ai/capabilities.ts +55 -36
  39. package/src/ai/capability-execution.ts +47 -12
  40. package/src/ai/chat/blocks.tsx +229 -90
  41. package/src/ai/chat/builtin-tools.tsx +2 -4
  42. package/src/ai/chat/capability-result.ts +15 -0
  43. package/src/ai/chat/capability-table.tsx +2 -10
  44. package/src/ai/chat/composer-adapter.ts +83 -3
  45. package/src/ai/chat/message-actions.tsx +8 -7
  46. package/src/ai/chat/message-utils.ts +25 -1
  47. package/src/ai/chat/messages.ts +2 -0
  48. package/src/ai/chat/presentation.tsx +31 -47
  49. package/src/ai/chat/primitives.tsx +43 -1
  50. package/src/ai/chat/tool-disclosure.tsx +21 -2
  51. package/src/ai/chat/tool-groups.ts +58 -0
  52. package/src/ai/chat-quotas.ts +25 -0
  53. package/src/ai/chat-task-contracts.ts +14 -10
  54. package/src/ai/chat-tasks.ts +297 -43
  55. package/src/ai/client/controller.ts +165 -39
  56. package/src/ai/client/file-source.ts +12 -6
  57. package/src/ai/client/live-connection.ts +1 -0
  58. package/src/ai/client/projection.ts +29 -11
  59. package/src/ai/client/transport.ts +6 -0
  60. package/src/ai/code-capability-routes.ts +105 -0
  61. package/src/ai/code-capability-transport.ts +39 -0
  62. package/src/ai/code-execution.ts +53 -0
  63. package/src/ai/code-mode-skill.ts +118 -0
  64. package/src/ai/code-runtime-tools.ts +179 -0
  65. package/src/ai/code-source-contracts.ts +270 -0
  66. package/src/ai/code-source-tools.ts +188 -0
  67. package/src/ai/compaction.ts +27 -6
  68. package/src/ai/data-analysis-skill.ts +10 -0
  69. package/src/ai/default-tools.ts +99 -3
  70. package/src/ai/dictation-runtime.ts +171 -0
  71. package/src/ai/dictations.ts +188 -0
  72. package/src/ai/draft-content.ts +49 -0
  73. package/src/ai/enrich.ts +6 -0
  74. package/src/ai/executor.ts +189 -33
  75. package/src/ai/fetch-file-tool.ts +2 -2
  76. package/src/ai/file-content-version.ts +27 -0
  77. package/src/ai/file-context.ts +2 -1
  78. package/src/ai/file-media-type.ts +12 -0
  79. package/src/ai/file-reference-contracts.ts +15 -0
  80. package/src/ai/file-tools.ts +8 -1
  81. package/src/ai/files-store.ts +212 -71
  82. package/src/ai/fixtures/vision-encrypted.pdf +0 -0
  83. package/src/ai/grids-skill.ts +42 -10
  84. package/src/ai/http.ts +26 -8
  85. package/src/ai/index.ts +26 -3
  86. package/src/ai/inference-calls.ts +197 -0
  87. package/src/ai/live-events.ts +5 -1
  88. package/src/ai/live.ts +1 -1
  89. package/src/ai/markdown-pdf-tool.ts +2 -2
  90. package/src/ai/memories.ts +30 -27
  91. package/src/ai/memory-learning-runs.ts +1 -1
  92. package/src/ai/memory-learning.ts +64 -32
  93. package/src/ai/memory-tool.ts +1 -5
  94. package/src/ai/memory-workflow-evidence.ts +9 -4
  95. package/src/ai/message-queue.ts +178 -0
  96. package/src/ai/migrate.ts +189 -11
  97. package/src/ai/model-pricing.ts +43 -0
  98. package/src/ai/pdf-render-worker.ts +75 -0
  99. package/src/ai/pdf-render.fixture.ts +28 -0
  100. package/src/ai/pdf-render.ts +77 -0
  101. package/src/ai/projects-routes.ts +18 -43
  102. package/src/ai/projects.ts +33 -14
  103. package/src/ai/protocol.ts +3 -0
  104. package/src/ai/provider.ts +1 -8
  105. package/src/ai/quota-provider.ts +192 -0
  106. package/src/ai/quota-report.ts +152 -0
  107. package/src/ai/quotas-migrate.ts +45 -0
  108. package/src/ai/quotas.ts +168 -0
  109. package/src/ai/routes.ts +241 -11
  110. package/src/ai/run-timeout.ts +12 -0
  111. package/src/ai/runtime-tools.ts +7 -5
  112. package/src/ai/runtime.ts +74 -17
  113. package/src/ai/settings.ts +36 -8
  114. package/src/ai/short-id.ts +4 -5
  115. package/src/ai/skill-catalog.ts +0 -10
  116. package/src/ai/skill-search.ts +58 -0
  117. package/src/ai/skill-seeds.ts +114 -20
  118. package/src/ai/skill-tool.ts +21 -5
  119. package/src/ai/skills-routes.ts +49 -4
  120. package/src/ai/skills.ts +358 -163
  121. package/src/ai/store.ts +416 -201
  122. package/src/ai/stream.ts +5 -0
  123. package/src/ai/structured-runs.ts +4 -3
  124. package/src/ai/structured.ts +12 -1
  125. package/src/ai/system-prompt.ts +14 -2
  126. package/src/ai/task-contracts.ts +84 -0
  127. package/src/ai/task-execution.ts +92 -0
  128. package/src/ai/timeline.ts +2 -5
  129. package/src/ai/todo-contracts.ts +32 -0
  130. package/src/ai/todo-tool.ts +19 -0
  131. package/src/ai/tool-audit.ts +30 -40
  132. package/src/ai/tools.ts +6 -0
  133. package/src/ai/transcription.ts +202 -0
  134. package/src/ai/turn-timing.ts +114 -0
  135. package/src/ai/types.ts +58 -10
  136. package/src/ai/ui.tsx +3 -0
  137. package/src/ai/usage.ts +118 -118
  138. package/src/ai/vision-tool.ts +60 -10
  139. package/src/api/admin-ai-projects.ts +2 -2
  140. package/src/api/admin-ai-quotas.ts +80 -0
  141. package/src/api/admin-ai-skills.ts +48 -5
  142. package/src/api/admin-ai-usage.ts +2 -2
  143. package/src/api/admin-core-settings.ts +25 -0
  144. package/src/api/admin-identity.ts +91 -51
  145. package/src/api/admin-linux-identities.ts +10 -4
  146. package/src/api/admin-rail.ts +58 -0
  147. package/src/api/announcements.ts +14 -0
  148. package/src/api/app-approval.ts +2 -2
  149. package/src/api/auth.ts +1 -1
  150. package/src/api/capabilities.ts +324 -43
  151. package/src/api/capability-streams.ts +262 -0
  152. package/src/api/help.ts +8 -44
  153. package/src/api/index.ts +8 -2
  154. package/src/api/mcp.ts +45 -34
  155. package/src/api/me-web-vitals.ts +35 -0
  156. package/src/api/me.ts +2 -0
  157. package/src/api/search/schemas.ts +26 -15
  158. package/src/api/search.bench.ts +1 -1
  159. package/src/api/search.ts +54 -17
  160. package/src/browser/CloudResourceSearch.tsx +640 -702
  161. package/src/browser/app-approval-vault.ts +19 -7
  162. package/src/browser/command-bridge.ts +109 -0
  163. package/src/browser/command-shortcuts.ts +76 -0
  164. package/src/browser/commands.ts +67 -0
  165. package/src/browser/navigation-search.ts +48 -0
  166. package/src/browser/notifications.ts +1 -1
  167. package/src/browser/resource-picker.tsx +5 -18
  168. package/src/browser/resource-search-dialog.ts +9 -0
  169. package/src/browser/resource-search-input.ts +46 -0
  170. package/src/browser/resource-search-messages.ts +76 -40
  171. package/src/browser/resource-search.ts +7 -0
  172. package/src/browser/search-bridge.ts +102 -0
  173. package/src/browser/search-commands.ts +59 -0
  174. package/src/browser/search.ts +13 -0
  175. package/src/browser/spotlight-position.ts +208 -0
  176. package/src/browser/testing.ts +6 -0
  177. package/src/browser/web-vitals.ts +25 -0
  178. package/src/capabilities/claims.ts +179 -0
  179. package/src/capabilities/client.ts +34 -0
  180. package/src/capabilities/command-link.ts +16 -0
  181. package/src/capabilities/executions.ts +298 -0
  182. package/src/capabilities/migrate.ts +81 -0
  183. package/src/capabilities/server.ts +116 -35
  184. package/src/capabilities/stream-body.ts +16 -0
  185. package/src/capabilities/streams.ts +28 -0
  186. package/src/cli/admin/ai-quotas.ts +161 -0
  187. package/src/cli/admin/ai-skills.ts +64 -0
  188. package/src/cli/admin/ai-usage.ts +53 -30
  189. package/src/cli/admin/app-credentials.ts +77 -0
  190. package/src/cli/admin/data.ts +17 -0
  191. package/src/cli/admin/index.ts +10 -1
  192. package/src/cli/admin/instance.ts +2 -2
  193. package/src/cli/admin/linux.ts +4 -2
  194. package/src/cli/admin/sync.ts +1 -1
  195. package/src/cli/capabilities.ts +42 -0
  196. package/src/cli/capability-streams.ts +25 -0
  197. package/src/cli/index.ts +3 -2
  198. package/src/config/define-env.ts +97 -0
  199. package/src/config/env.ts +147 -51
  200. package/src/config/index.ts +3 -1
  201. package/src/contracts/app.ts +23 -3
  202. package/src/contracts/capabilities.ts +75 -5
  203. package/src/contracts/capability-streams.ts +46 -0
  204. package/src/contracts/commands.ts +49 -0
  205. package/src/contracts/index.ts +5 -2
  206. package/src/contracts/rail-admin.ts +38 -0
  207. package/src/contracts/rail-preferences.ts +16 -0
  208. package/src/contracts/registry.ts +5 -27
  209. package/src/contracts/settings-types.ts +1 -1
  210. package/src/contracts/shared.ts +18 -5
  211. package/src/contracts/web-vitals.ts +27 -0
  212. package/src/desktop/index.ts +1 -0
  213. package/src/desktop/solid.tsx +1 -1
  214. package/src/index.ts +8 -6
  215. package/src/server/index.ts +3 -0
  216. package/src/server/middleware/auth.ts +3 -2
  217. package/src/server/middleware/route-template.ts +9 -3
  218. package/src/server/middleware/runtime.ts +2 -1
  219. package/src/server/middleware/settings.ts +17 -11
  220. package/src/server/ratelimit.ts +1 -1
  221. package/src/server/services/access-revision.ts +10 -0
  222. package/src/server/services/access.ts +3 -1
  223. package/src/server/services/index.ts +1 -0
  224. package/src/services/account-category-policy.ts +9 -0
  225. package/src/services/account-lifecycle/audit.ts +1 -1
  226. package/src/services/accounts/app.ts +12 -2
  227. package/src/services/accounts/email-write.ts +23 -0
  228. package/src/services/accounts/groups.ts +15 -4
  229. package/src/services/accounts/identities.ts +249 -0
  230. package/src/services/accounts/identity-reconciliation.ts +87 -0
  231. package/src/services/accounts/ipa-data.ts +2 -2
  232. package/src/services/accounts/local-groups.ts +2 -2
  233. package/src/services/accounts/notification-sender.ts +12 -2
  234. package/src/services/accounts/posix.ts +45 -29
  235. package/src/services/accounts/users.ts +2 -2
  236. package/src/services/announcements/index.ts +69 -8
  237. package/src/services/app-approval.ts +11 -10
  238. package/src/services/audit/index.ts +1 -1
  239. package/src/services/cache-fill.ts +35 -0
  240. package/src/services/help/index.ts +109 -0
  241. package/src/services/help/maintenance.ts +25 -0
  242. package/src/services/help/store.ts +180 -0
  243. package/src/services/help/types.ts +46 -0
  244. package/src/services/identity/invocation-actor.ts +2 -2
  245. package/src/services/identity/key-config.ts +8 -5
  246. package/src/services/identity/key-ring.ts +1 -1
  247. package/src/services/identity/runtime-config.ts +6 -5
  248. package/src/services/index.ts +15 -0
  249. package/src/services/ipa/posix.ts +1 -1
  250. package/src/services/ipa/sync.ts +1 -1
  251. package/src/services/ipa/users.ts +268 -221
  252. package/src/services/legal-consent.ts +1 -1
  253. package/src/services/logging/index.ts +15 -13
  254. package/src/services/logging/trace.ts +2 -1
  255. package/src/services/logging/web-vitals.ts +81 -0
  256. package/src/services/mandates/index.ts +21 -4
  257. package/src/services/mandates/policy.ts +165 -2
  258. package/src/services/notifications/batches.ts +125 -57
  259. package/src/services/notifications/browser.ts +12 -0
  260. package/src/services/notifications/index.ts +0 -2
  261. package/src/services/notifications/platform.ts +0 -4
  262. package/src/services/oauth-tokens.ts +3 -3
  263. package/src/services/pdf/gotenberg.ts +131 -72
  264. package/src/services/pdf/index.ts +11 -8
  265. package/src/services/providers/local/users.ts +25 -16
  266. package/src/services/public-http.ts +101 -0
  267. package/src/services/rail-shortcuts.ts +95 -0
  268. package/src/services/rail-snapshot.ts +53 -0
  269. package/src/services/request-cache-redis.ts +29 -0
  270. package/src/services/session/index.ts +15 -2
  271. package/src/services/session/user.ts +62 -37
  272. package/src/services/settings/app.ts +5 -1
  273. package/src/services/settings/core-settings.ts +26 -35
  274. package/src/services/settings/defaults.ts +5 -0
  275. package/src/services/settings/index.ts +3 -3
  276. package/src/services/settings/store.ts +58 -84
  277. package/src/services/webauthn.ts +1 -1
  278. package/src/shared/ai-costs.ts +42 -0
  279. package/src/shared/ai-platform-prompt.ts +1 -1
  280. package/src/shared/ai-quotas.ts +136 -0
  281. package/src/shared/ai-usage.ts +6 -16
  282. package/src/shared/app-presentation.ts +21 -15
  283. package/src/shared/capability-messages.ts +14 -0
  284. package/src/shared/help.ts +6 -42
  285. package/src/shared/index.ts +3 -0
  286. package/src/shared/markdown/extensions/info-blocks.ts +1 -1
  287. package/src/ssr/AdminLayout.tsx +7 -5
  288. package/src/ssr/AdminSidebar.tsx +30 -16
  289. package/src/ssr/AppLaunchpad.island.tsx +33 -136
  290. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  291. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  292. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  293. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  294. package/src/ssr/Layout.tsx +17 -6
  295. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  296. package/src/ssr/LayoutFooter.tsx +1 -1
  297. package/src/ssr/LayoutHeader.tsx +46 -14
  298. package/src/ssr/LayoutHelp.tsx +22 -23
  299. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  300. package/src/ssr/LayoutRail.tsx +17 -9
  301. package/src/ssr/MinimalLayout.tsx +1 -1
  302. package/src/ssr/MobileNavigation.tsx +71 -0
  303. package/src/ssr/MobileProfileActions.tsx +49 -0
  304. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  305. package/src/ssr/RailApps.island.tsx +2 -2
  306. package/src/ssr/RailEditor.tsx +17 -2
  307. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  308. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  309. package/src/ssr/admin-navigation.ts +11 -1
  310. package/src/ssr/help.ts +29 -0
  311. package/src/ssr/islands/SearchBar.tsx +67 -0
  312. package/src/ssr/islands/index.ts +3 -1
  313. package/src/ssr/layout-context.ts +2 -2
  314. package/src/ssr/layout-help-search.ts +15 -0
  315. package/src/ssr/mobile-menu-history.ts +70 -0
  316. package/src/ssr/platform-messages.ts +16 -8
  317. package/src/ssr/profile-actions.ts +42 -0
  318. package/src/ssr/rail-context.ts +8 -4
  319. package/src/ssr/rail-messages.ts +2 -0
  320. package/src/ssr/rail-navigation.ts +3 -3
  321. package/src/ssr/workspace-navigation.ts +75 -0
  322. package/src/styles/effects.css +6 -6
  323. package/src/styles/global.css +54 -0
  324. package/src/styles/input.css +43 -41
  325. package/src/styles/resource-search.css +505 -0
  326. package/src/styles/utilities-feedback.css +13 -0
  327. package/src/styles/utilities-layout.css +1 -5
  328. package/src/styles/utilities-navigation.css +3 -39
  329. package/src/types/ambient.d.ts +5 -0
  330. package/src/workflows/ai/index.ts +2 -2
  331. package/src/workflows/ai/runtime.ts +19 -90
  332. package/src/workflows/ai/store.ts +2 -0
  333. package/src/workflows/ai/types.ts +6 -82
  334. package/src/workflows/ai-actions.ts +16 -13
  335. package/src/workflows/definition.ts +10 -5
  336. package/src/workflows/store/actions.ts +44 -20
  337. package/src/workflows/store/index.ts +4 -2
  338. package/src/workflows/store/runs.ts +36 -0
  339. package/src/workflows/store/worker-pool.ts +55 -0
  340. package/src/workflows/store/worker-runtime.ts +90 -0
  341. package/src/ai/kit-skill.ts +0 -22
  342. package/src/ai/usage-migrate.ts +0 -81
  343. package/src/browser/notification-target.ts +0 -27
  344. package/src/contracts/notification-live.ts +0 -66
  345. package/src/server/services/freeipa/test-certificates.ts +0 -44
  346. package/src/services/notifications/live.ts +0 -50
  347. package/src/services/session/test-fixture.ts +0 -18
  348. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  349. package/src/ssr/islands/SearchBar.island.tsx +0 -80
@@ -0,0 +1,29 @@
1
+ import type { Context } from "hono";
2
+ import { getLocale } from "../server/locale";
3
+ import { createHelpReader } from "../services/help";
4
+ import type { HelpManifestResult } from "../services/help/types";
5
+ import { logger } from "../services/logging";
6
+ import { resolveCurrentApp } from "./app-appearance";
7
+ import { getRuntimeContext } from "./runtime";
8
+
9
+ const manifests = new WeakMap<object, HelpManifestResult | null>();
10
+ const log = logger("help:ssr");
11
+ export const getLayoutHelp = (c: object): HelpManifestResult | null => manifests.get(c) ?? null;
12
+
13
+ /** Explicit Help pages also work anonymously; automatic layout loading requires a user. */
14
+ export const preloadLayoutHelp = async (c: Context, appId?: string): Promise<HelpManifestResult | null> => {
15
+ if (manifests.has(c)) return getLayoutHelp(c);
16
+ if (!c.get("runtime") || (!appId && !c.get("user"))) return null;
17
+ const id = appId ?? resolveCurrentApp(getRuntimeContext(c).apps, c.req.path)?.id;
18
+ if (!id) return null;
19
+ try {
20
+ const manifest = await createHelpReader(getLocale(c)).manifest(id);
21
+ manifests.set(c, manifest);
22
+ return manifest;
23
+ } catch (error) {
24
+ // An unavailable Help store must not prevent an unrelated application page from rendering.
25
+ log.warn("Help metadata unavailable", { appId: id, error: error instanceof Error ? error.message : String(error) });
26
+ manifests.set(c, null);
27
+ return null;
28
+ }
29
+ };
@@ -0,0 +1,67 @@
1
+ import { IconButtonLink, TextInput, useLocale } from "@k2b/ui";
2
+ import { createMemo, For, Show } from "solid-js";
3
+ import { platformMessages } from "../platform-messages";
4
+
5
+ type SearchBarProps = {
6
+ /** Destination URL including every filter to retain. */
7
+ action: string;
8
+ value?: string;
9
+ param?: string;
10
+ pageParam?: string;
11
+ placeholder?: string;
12
+ ariaLabel?: string;
13
+ disabled?: boolean;
14
+ };
15
+
16
+ /** Native GET search: works in server HTML and inside hydrated islands. */
17
+ export default function SearchBar(props: SearchBarProps) {
18
+ const locale = useLocale();
19
+ const t = () => platformMessages.resolve([locale()]).t;
20
+ const param = () => props.param ?? "search";
21
+ const target = createMemo(() => {
22
+ // Split without resolving against a browser-only origin; relative URLs stay relative.
23
+ const hashAt = props.action.indexOf("#");
24
+ const hash = hashAt < 0 ? "" : props.action.slice(hashAt);
25
+ const href = hashAt < 0 ? props.action : props.action.slice(0, hashAt);
26
+ const queryAt = href.indexOf("?");
27
+ const path = queryAt < 0 ? href : href.slice(0, queryAt);
28
+ const params = new URLSearchParams(queryAt < 0 ? "" : href.slice(queryAt + 1));
29
+ const value = props.value ?? params.get(param()) ?? "";
30
+ params.delete(param());
31
+ params.delete(props.pageParam ?? "page");
32
+ const query = params.toString();
33
+ return { action: path + hash, clear: path + (query || !path ? `?${query}` : "") + hash, fields: [...params], value };
34
+ });
35
+
36
+ return (
37
+ <form action={target().action} method="get" role="search" class="group/search w-full">
38
+ <For each={target().fields}>{([name, value]) => <input type="hidden" name={name} value={value} />}</For>
39
+ <TextInput
40
+ name={param()}
41
+ type="search"
42
+ placeholder={props.placeholder || `${t().search}…`}
43
+ aria-label={props.ariaLabel ?? t().search}
44
+ icon="ti ti-search"
45
+ activeIcon="ti ti-search"
46
+ value={() => target().value}
47
+ disabled={props.disabled}
48
+ suffix={
49
+ <Show when={!props.disabled}>
50
+ <IconButtonLink
51
+ href={target().clear}
52
+ label={t().clearSearch}
53
+ size="xs"
54
+ tooltip={false}
55
+ class="group-has-[input:placeholder-shown]/search:invisible"
56
+ >
57
+ <i class="ti ti-x" aria-hidden="true" />
58
+ </IconButtonLink>
59
+ </Show>
60
+ }
61
+ />
62
+ <button type="submit" class="hidden" disabled={props.disabled}>
63
+ {t().search}
64
+ </button>
65
+ </form>
66
+ );
67
+ }
@@ -1,4 +1,6 @@
1
- export { default as SearchBar } from "./SearchBar.island";
1
+ export { default as WorkspaceNavigationProvider } from "../WorkspaceNavigationProvider";
2
+ export { provideWorkspaceNavigation } from "../workspace-navigation";
3
+ export { default as SearchBar } from "./SearchBar";
2
4
 
3
5
  import { AppLaunchpadButton, AppLaunchpadProvider } from "../AppLaunchpad.island";
4
6
  import { LayoutHelpDocuments, LayoutHelpPage } from "../LayoutHelp";
@@ -1,11 +1,11 @@
1
- import type { RailPreferences } from "../contracts/rail-preferences";
1
+ import type { RailSnapshot } from "../contracts/rail-preferences";
2
2
  import type { User } from "../contracts/shared";
3
3
  import type { LayoutAnnouncementsState } from "../server/middleware/settings";
4
4
  import type { RuntimeContext } from "./runtime";
5
5
 
6
6
  export type LayoutContext = {
7
7
  get(key: "user"): User | undefined;
8
- get(key: "railPreferences"): RailPreferences | undefined;
8
+ get(key: "railPreferences"): RailSnapshot | undefined;
9
9
  get(key: "page"): { theme?: "light" | "dark" };
10
10
  get(key: "runtime"): RuntimeContext;
11
11
  get(key: "announcements"): LayoutAnnouncementsState | undefined;
@@ -0,0 +1,15 @@
1
+ type SearchTopic = { id: string; title: string; description?: string; kind: "document" | "content" };
2
+
3
+ /** Use server ranking for documents; metadata remains usable during loading or outages. */
4
+ export const searchHelpTopics = <T extends SearchTopic>(topics: readonly T[], query: string, remoteIds: readonly string[] | null): T[] => {
5
+ const local = topics.filter((topic) => [topic.title, topic.description].some((part) => part?.toLocaleLowerCase().includes(query)));
6
+ if (remoteIds === null) return local;
7
+ const documents = new Map(topics.filter((topic) => topic.kind === "document").map((topic) => [topic.id, topic]));
8
+ return [
9
+ ...remoteIds.flatMap((id) => {
10
+ const topic = documents.get(id);
11
+ return topic ? [topic] : [];
12
+ }),
13
+ ...local.filter((topic) => topic.kind === "content"),
14
+ ];
15
+ };
@@ -0,0 +1,70 @@
1
+ import { bottomSheetOptions, type DialogRender, dialogCore } from "@k2b/ui";
2
+
3
+ const marker = "cloudMobileMenu";
4
+ let running: Promise<void> | undefined;
5
+
6
+ /** One transient history entry; complete its Back before a selected operation runs. */
7
+ export function openMobileMenu(view: (beforeSelect: () => Promise<boolean>) => DialogRender<void>) {
8
+ if (running) return running;
9
+ let done: () => void = () => {};
10
+ const cleaned = new Promise<void>((resolve) => {
11
+ done = resolve;
12
+ });
13
+ running = (async () => {
14
+ const previous = { ...history.state };
15
+ delete previous[marker];
16
+ history.replaceState(previous, "");
17
+ const id = crypto.randomUUID();
18
+ history.pushState({ ...previous, [marker]: id }, "");
19
+ const abort = new AbortController();
20
+ let leavingPage = false;
21
+ const mobile = window.matchMedia("(max-width: 1023px)");
22
+ const pop = () => {
23
+ if (history.state?.[marker] !== id) abort.abort();
24
+ };
25
+ const resized = () => {
26
+ if (!mobile.matches) abort.abort();
27
+ };
28
+ const leaving = () => {
29
+ leavingPage = true;
30
+ abort.abort();
31
+ };
32
+ window.addEventListener("popstate", pop);
33
+ window.addEventListener("pagehide", leaving);
34
+ mobile.addEventListener("change", resized);
35
+ try {
36
+ await dialogCore.open<void>(
37
+ (close, context) =>
38
+ view(async () => {
39
+ close();
40
+ await cleaned;
41
+ return !abort.signal.aborted;
42
+ })(close, context),
43
+ { ...bottomSheetOptions, panelClassName: `${bottomSheetOptions.panelClassName} cloud-mobile-menu`, signal: abort.signal },
44
+ );
45
+ } finally {
46
+ window.removeEventListener("popstate", pop);
47
+ window.removeEventListener("pagehide", leaving);
48
+ mobile.removeEventListener("change", resized);
49
+ if (!leavingPage && history.state?.[marker] === id) {
50
+ await new Promise<void>((resolve) => {
51
+ const returned = () => {
52
+ window.removeEventListener("pagehide", returned);
53
+ window.removeEventListener("popstate", returned);
54
+ resolve();
55
+ };
56
+ window.addEventListener("popstate", returned, { once: true });
57
+ window.addEventListener("pagehide", returned, { once: true });
58
+ history.back();
59
+ });
60
+ }
61
+ if (!mobile.matches && !leavingPage) {
62
+ const launcher = document.querySelector<HTMLButtonElement>(".layout-rail [data-cloud-launchpad]");
63
+ launcher?.focus({ preventScroll: true });
64
+ }
65
+ done();
66
+ running = undefined;
67
+ }
68
+ })();
69
+ return running;
70
+ }
@@ -9,7 +9,9 @@ export const platformMessages = i18n.define({
9
9
  start: "Start",
10
10
  overview: "Overview",
11
11
  general: "General",
12
+ rail: "App bar",
12
13
  announcements: "Announcements",
14
+ appCredentials: "App credentials",
13
15
  ai: "AI",
14
16
  settings: "Settings",
15
17
  appAdmin: "App administration",
@@ -26,9 +28,15 @@ export const platformMessages = i18n.define({
26
28
  skills: "Skills",
27
29
  projects: "Projects",
28
30
  aiUsage: "Usage",
31
+ aiQuotas: "Assistant limits",
29
32
  backgroundJobs: "Background jobs",
30
33
  apps: "Apps",
31
34
  openApps: "Open apps",
35
+ appMenu: "Menu",
36
+ findApps: "Find an app…",
37
+ noAppsFound: "No matching apps.",
38
+ allApps: "All apps",
39
+ navigation: "Navigation",
32
40
  home: "Home",
33
41
  profile: "Profile",
34
42
  displayName: "display name",
@@ -40,10 +48,6 @@ export const platformMessages = i18n.define({
40
48
  guestAccountExpires: ({ date }: { date: string }) => `Your guest account expires on ${date}.`,
41
49
  passwordExpired: "Your password has expired. Sign out and in again to change it.",
42
50
  passwordExpires: ({ date }: { date: string }) => `Your password expires on ${date}.`,
43
- newNotification: "New notification",
44
- relatedItemReady: "A related item is ready.",
45
- open: "Open",
46
- dismissNotification: "Dismiss notification",
47
51
  light: "Light",
48
52
  dark: "Dark",
49
53
  account: "Account",
@@ -73,7 +77,9 @@ export const platformMessages = i18n.define({
73
77
  start: "Start",
74
78
  overview: "Übersicht",
75
79
  general: "Allgemein",
80
+ rail: "App-Leiste",
76
81
  announcements: "Ankündigungen",
82
+ appCredentials: "App-Zugänge",
77
83
  ai: "KI",
78
84
  settings: "Einstellungen",
79
85
  appAdmin: "App-Verwaltung",
@@ -90,8 +96,14 @@ export const platformMessages = i18n.define({
90
96
  skills: "Skills",
91
97
  projects: "Projekte",
92
98
  aiUsage: "Nutzung",
99
+ aiQuotas: "Assistant-Limits",
93
100
  backgroundJobs: "Hintergrundaufgaben",
94
101
  apps: "Apps",
102
+ appMenu: "Menü",
103
+ findApps: "App finden…",
104
+ noAppsFound: "Keine passenden Apps.",
105
+ allApps: "Alle Apps",
106
+ navigation: "Navigation",
95
107
  openApps: "Apps öffnen",
96
108
  home: "Start",
97
109
  profile: "Profil",
@@ -104,10 +116,6 @@ export const platformMessages = i18n.define({
104
116
  guestAccountExpires: ({ date }) => `Dein Gastkonto läuft am ${date} ab.`,
105
117
  passwordExpired: "Dein Passwort ist abgelaufen. Melde dich ab und erneut an, um es zu ändern.",
106
118
  passwordExpires: ({ date }) => `Dein Passwort läuft am ${date} ab.`,
107
- newNotification: "Neue Benachrichtigung",
108
- relatedItemReady: "Ein zugehöriger Inhalt ist verfügbar.",
109
- open: "Öffnen",
110
- dismissNotification: "Benachrichtigung ausblenden",
111
119
  light: "Hell",
112
120
  dark: "Dunkel",
113
121
  account: "Konto",
@@ -0,0 +1,42 @@
1
+ import { type DropdownItem, toast, useLocale } from "@k2b/ui";
2
+ import { createMemo, createSignal } from "solid-js";
3
+ import { browserNotificationClient } from "../browser/notifications";
4
+ import { apiClient } from "../clients/core";
5
+ import type { CloudTheme } from "../shared/theme";
6
+ import { createPreferenceController } from "./preference-controller";
7
+
8
+ export function createProfileActions(initialTheme: CloudTheme) {
9
+ const locale = useLocale();
10
+ const preferences = createPreferenceController(initialTheme, locale);
11
+ const [signingOut, setSigningOut] = createSignal(false);
12
+ const signOut = async () => {
13
+ if (signingOut()) return;
14
+ setSigningOut(true);
15
+ try {
16
+ await browserNotificationClient.disable().catch(() => undefined);
17
+ const response = await apiClient.auth.logout.$post();
18
+ if (!response.ok) throw new Error(preferences.messages().signOutFailed);
19
+ window.location.href = "/auth/login";
20
+ } catch {
21
+ toast.error(preferences.messages().signOutFailed);
22
+ setSigningOut(false);
23
+ }
24
+ };
25
+
26
+ const items = createMemo<DropdownItem[]>(() => [
27
+ ...preferences.items(),
28
+ {
29
+ href: "/me",
30
+ icon: "ti ti-user-circle",
31
+ label: preferences.messages().profileSettings,
32
+ },
33
+ {
34
+ action: signOut,
35
+ icon: "ti ti-logout",
36
+ label: preferences.messages().signOut,
37
+ disabled: signingOut(),
38
+ },
39
+ ]);
40
+
41
+ return { preferences, items, signOut, signingOut };
42
+ }
@@ -1,7 +1,7 @@
1
- import { type RailPreferences, RailPreferencesSchema } from "../contracts/rail-preferences";
1
+ import { type RailPreferences, type RailSnapshot, RailSnapshotSchema } from "../contracts/rail-preferences";
2
2
  import type { RailApp } from "./rail-navigation";
3
3
 
4
- export type RailContext = { apps: RailApp[]; settings: RailPreferences };
4
+ export type RailContext = { apps: RailApp[]; settings: RailSnapshot };
5
5
  export const RAIL_PREFERENCES_EVENT = "cloud:rail-preferences";
6
6
 
7
7
  export const readRailContext = (): RailContext | undefined => {
@@ -10,12 +10,16 @@ export const readRailContext = (): RailContext | undefined => {
10
10
  if (!text) return undefined;
11
11
  // Framework-generated, escaped SSR data; never loaded from localStorage.
12
12
  const value: RailContext = JSON.parse(text);
13
- return { apps: value.apps, settings: RailPreferencesSchema.parse(value.settings) };
13
+ return { apps: value.apps, settings: RailSnapshotSchema.parse(value.settings) };
14
14
  };
15
15
 
16
16
  export const publishRailPreferences = (settings: RailPreferences) => {
17
17
  const context = readRailContext();
18
18
  const element = document.getElementById("cloud-rail-data");
19
- if (context && element) element.textContent = JSON.stringify({ ...context, settings }).replace(/</g, "\\u003c");
19
+ if (context && element)
20
+ element.textContent = JSON.stringify({
21
+ ...context,
22
+ settings: { ...settings, managedShortcuts: context.settings.managedShortcuts },
23
+ }).replace(/</g, "\\u003c");
20
24
  window.dispatchEvent(new CustomEvent(RAIL_PREFERENCES_EVENT));
21
25
  };
@@ -4,6 +4,7 @@ export const railMessages = i18n.define({
4
4
  baseLocale: "en",
5
5
  messages: {
6
6
  en: {
7
+ managed: "Provided by the administration",
7
8
  customize: "Customize app bar",
8
9
  apps: "Visible apps",
9
10
  shortcuts: "App bar shortcuts",
@@ -33,6 +34,7 @@ export const railMessages = i18n.define({
33
34
  invalid: "Check shortcut titles, links and icons. Each app can be pinned once; settings must fit within 16 KiB.",
34
35
  },
35
36
  de: {
37
+ managed: "Von der Administration bereitgestellt",
36
38
  customize: "App-Leiste anpassen",
37
39
  apps: "Sichtbare Apps",
38
40
  shortcuts: "Shortcuts der App-Leiste",
@@ -1,4 +1,4 @@
1
- import type { RailPreferences } from "../contracts/rail-preferences";
1
+ import type { RailPreferences, RailSnapshot } from "../contracts/rail-preferences";
2
2
 
3
3
  export type RailApp = {
4
4
  id: string;
@@ -21,11 +21,11 @@ export const sortRailApps = (apps: readonly RailApp[], locale: string): RailApp[
21
21
  };
22
22
 
23
23
  /** Input is the authorized navigation catalog, never the unfiltered registry. */
24
- export const projectRailNavigation = (apps: readonly RailApp[], settings: RailPreferences, locale: string) => {
24
+ export const projectRailNavigation = (apps: readonly RailApp[], settings: RailSnapshot, locale: string) => {
25
25
  const catalog = new Map(apps.map((app) => [app.id, app]));
26
26
  const pinned = new Set<string>();
27
27
  const shortcuts: RailLink[] = [];
28
- for (const shortcut of settings.shortcuts) {
28
+ for (const shortcut of [...(settings.managedShortcuts ?? []), ...settings.shortcuts]) {
29
29
  if (shortcut.kind === "app") {
30
30
  const app = catalog.get(shortcut.appId);
31
31
  if (!app || pinned.has(app.id)) continue;
@@ -0,0 +1,75 @@
1
+ import { createNavigation, type NavigationController, type NavigationItem } from "@k2b/ui";
2
+ import { createEffect, onCleanup, onMount } from "solid-js";
3
+
4
+ export type WorkspaceNavigationState = { navigation: NavigationController; label: string; owner: Element };
5
+ const changedEvent = "cloud:workspace-navigation";
6
+
7
+ declare global {
8
+ interface Window {
9
+ __cloudWorkspaceNavigation?: WorkspaceNavigationState;
10
+ }
11
+ }
12
+
13
+ /** Bind only the outer application's navigation. Nested workspaces do not register. */
14
+ export function provideWorkspaceNavigation(navigation: NavigationController, options: { label: () => string; owner: () => Element }) {
15
+ onMount(() => {
16
+ const state = { navigation, label: options.label(), owner: options.owner() };
17
+ window.__cloudWorkspaceNavigation = state;
18
+ createEffect(() => {
19
+ navigation.items();
20
+ state.label = options.label();
21
+ window.dispatchEvent(new Event(changedEvent));
22
+ });
23
+ onCleanup(() => {
24
+ if (window.__cloudWorkspaceNavigation !== state) return;
25
+ delete window.__cloudWorkspaceNavigation;
26
+ window.dispatchEvent(new Event(changedEvent));
27
+ });
28
+ });
29
+ }
30
+
31
+ function validItems(value: unknown): value is NavigationItem[] {
32
+ return (
33
+ Array.isArray(value) &&
34
+ value.every((entry) => {
35
+ if (!entry || typeof entry !== "object" || typeof entry.id !== "string" || typeof entry.label !== "string") return false;
36
+ if (entry.href !== undefined && typeof entry.href !== "string") return false;
37
+ if (entry.action !== undefined && typeof entry.action !== "string") return false;
38
+ return (entry.children === undefined || validItems(entry.children)) && (entry.actions === undefined || validItems(entry.actions));
39
+ })
40
+ );
41
+ }
42
+
43
+ export function readWorkspaceNavigation(): WorkspaceNavigationState | undefined {
44
+ if (typeof window === "undefined") return;
45
+ const current = window.__cloudWorkspaceNavigation;
46
+ if (current?.owner.isConnected) return current;
47
+ const owner = document.querySelector<HTMLScriptElement>("script[data-cloud-workspace-navigation]");
48
+ if (!owner) return;
49
+ try {
50
+ const value: unknown = JSON.parse(owner.textContent ?? "");
51
+ if (!value || typeof value !== "object" || !("items" in value) || !validItems(value.items)) return;
52
+ // SSR links work before the app island loads; action handlers are not yet available.
53
+ const links = (items: readonly NavigationItem[]): NavigationItem[] =>
54
+ items.map((item) => ({
55
+ ...item,
56
+ disabled: item.disabled || Boolean(item.action && !item.href),
57
+ ...(item.href ? { action: undefined } : {}),
58
+ ...(item.children ? { children: links(item.children) } : {}),
59
+ ...(item.actions ? { actions: links(item.actions) } : {}),
60
+ }));
61
+ const items = links(value.items);
62
+ return {
63
+ owner,
64
+ label: "label" in value && typeof value.label === "string" ? value.label : "",
65
+ navigation: createNavigation({ items: () => items }),
66
+ };
67
+ } catch {
68
+ return;
69
+ }
70
+ }
71
+
72
+ export function observeWorkspaceNavigation(listener: () => void) {
73
+ window.addEventListener(changedEvent, listener);
74
+ return () => window.removeEventListener(changedEvent, listener);
75
+ }
@@ -794,7 +794,7 @@ dialog:has(.launchpad-panel)::backdrop {
794
794
 
795
795
  /* Dashboard launchpad app icon. One app colour drives the glyph and graded rim;
796
796
  the face stays clean so the tile feels crisp instead of plastic. */
797
- .launchpad-panel .app-icon {
797
+ :is(.launchpad-panel, .cloud-mobile-apps) .app-icon {
798
798
  --icon-face: #ffffff;
799
799
  --rim-top: color-mix(in oklab, var(--app-icon-color), white 34%);
800
800
  --rim-bottom: color-mix(in oklab, var(--app-icon-color), black 12%);
@@ -817,13 +817,13 @@ dialog:has(.launchpad-panel)::backdrop {
817
817
  }
818
818
 
819
819
  @supports (color: oklch(from red l c h)) {
820
- .launchpad-panel .app-icon {
820
+ :is(.launchpad-panel, .cloud-mobile-apps) .app-icon {
821
821
  --glyph: oklch(from var(--app-icon-color) min(l, 0.5) c h);
822
822
  --glyph-hover: oklch(from var(--app-icon-color) min(l, 0.4) c h);
823
823
  }
824
824
  }
825
825
 
826
- .dark .launchpad-panel .app-icon {
826
+ .dark :is(.launchpad-panel, .cloud-mobile-apps) .app-icon {
827
827
  --icon-face: #202026;
828
828
  --rim-top: color-mix(in oklab, var(--app-icon-color), white 30%);
829
829
  --rim-bottom: color-mix(in oklab, var(--app-icon-color), black 24%);
@@ -835,15 +835,15 @@ dialog:has(.launchpad-panel)::backdrop {
835
835
  }
836
836
 
837
837
  @supports (color: oklch(from red l c h)) {
838
- .dark .launchpad-panel .app-icon {
838
+ .dark :is(.launchpad-panel, .cloud-mobile-apps) .app-icon {
839
839
  --glyph: oklch(from var(--app-icon-color) max(l, 0.82) c h);
840
840
  --glyph-hover: oklch(from var(--app-icon-color) max(l, 0.96) c h);
841
841
  }
842
842
  }
843
843
 
844
844
  /* Hover changes only the icon tile, never the whole icon+label item. */
845
- .launchpad-panel .group:hover > .app-icon,
846
- .launchpad-panel .app-icon:hover {
845
+ :is(.launchpad-panel, .cloud-mobile-apps) .group:hover > .app-icon,
846
+ :is(.launchpad-panel, .cloud-mobile-apps) .app-icon:hover {
847
847
  background:
848
848
  linear-gradient(var(--icon-face), var(--icon-face)) padding-box,
849
849
  linear-gradient(180deg, var(--rim-top-hover), var(--rim-bottom-hover)) border-box;
@@ -29,6 +29,7 @@
29
29
  @import "./base-popover.css";
30
30
  @import "./effects.css";
31
31
  @import "./input.css";
32
+ @import "./resource-search.css";
32
33
 
33
34
  @theme {
34
35
  --color-dark: rgb(9 9 11);
@@ -63,6 +64,12 @@
63
64
  );
64
65
  }
65
66
 
67
+ /* Full-page workspaces own their inner scrolling. Stop iframe scroll gestures
68
+ from rubber-banding the outer viewport or triggering pull-to-refresh. */
69
+ html:has(.cloud-app-canvas[data-layout-full-page="true"]) {
70
+ overscroll-behavior: none;
71
+ }
72
+
66
73
  .cloud-app-canvas > :is(.layout-rail, .layout-shell-content) {
67
74
  gap: var(--ui-space-shell);
68
75
  }
@@ -266,3 +273,50 @@ solid-island {
266
273
  transition-duration: 0.01ms !important;
267
274
  }
268
275
  }
276
+
277
+ /* Cloud has exactly two shell modes: mobile below lg, desktop from lg. */
278
+ @media (max-width: 1023px) {
279
+ .cloud-app-canvas {
280
+ padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
281
+ background: var(--ui-surface);
282
+ }
283
+ .cloud-app-canvas > .layout-shell-content { gap: 0; }
284
+ .cloud-app-canvas .layout-header {
285
+ border-radius: 0; border: 0; border-bottom: 1px solid var(--ui-border);
286
+ box-shadow: none !important;
287
+ }
288
+
289
+ }
290
+
291
+ .cloud-mobile-menu .k2b-panel-dialog__header {
292
+ gap: .5rem;
293
+ padding-inline: 1rem;
294
+ background: var(--ui-surface);
295
+ }
296
+ .cloud-mobile-menu .k2b-bottom-sheet__handle { background: var(--ui-surface); }
297
+ /* The app name is the first segment; retain a heading for the dialog's name. */
298
+ .cloud-mobile-menu .k2b-panel-dialog__heading {
299
+ position: absolute; width: 1px; height: 1px; overflow: hidden;
300
+ clip-path: inset(50%); white-space: nowrap;
301
+ }
302
+ .cloud-mobile-menu .k2b-panel-dialog__actions { flex: 1; min-width: 0; }
303
+ .cloud-mobile-menu__switch { width: 100%; flex-wrap: nowrap; }
304
+ .cloud-mobile-menu__switch button { flex: 1; min-width: 0; min-height: 3rem; white-space: nowrap; }
305
+ .cloud-mobile-menu__switch button > span { overflow: hidden; text-overflow: ellipsis; }
306
+ .cloud-mobile-menu .k2b-dialog__close { width: 2.75rem; height: 2.75rem; flex-shrink: 0; }
307
+
308
+ .cloud-mobile-menu .k2b-panel-dialog__body { padding: .75rem 1rem 1rem; }
309
+ .cloud-mobile-apps { display: flex; flex-direction: column; gap: 1rem; }
310
+ .cloud-mobile-apps .launchpad-tools { display: flex; align-items: center; gap: .5rem; }
311
+ .cloud-mobile-apps .launchpad-tools > :first-child { flex: 1; min-width: 0; }
312
+ .cloud-mobile-apps .launchpad-customize { margin: 0; flex-shrink: 0; }
313
+ .cloud-mobile-apps .launchpad-apps-grid {
314
+ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem .5rem;
315
+ }
316
+ .cloud-mobile-apps .launchpad-app { width: auto; padding: .5rem .125rem; gap: .6rem; }
317
+ .cloud-mobile-apps .app-icon { width: 3.5rem; height: 3.5rem; border-radius: 1.1rem; font-size: 1.75rem; }
318
+ .cloud-mobile-apps .launchpad-app > span:last-child {
319
+ width: 100%; white-space: normal; overflow-wrap: anywhere; font-size: .8125rem; line-height: 1.3;
320
+ }
321
+ .cloud-mobile-profile { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .5rem; border-top: 1px solid var(--ui-border); padding-top: 1rem; }
322
+ .cloud-mobile-profile :is(a, button) { min-height: 2.75rem; white-space: normal; text-align: start; justify-content: flex-start; }