@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
@@ -1,16 +1,18 @@
1
1
  import { ButtonLink } from "@k2b/ui";
2
+ import type { NavigationSearchItem } from "../browser/navigation-search";
2
3
  import type { CloudTheme } from "../shared/theme";
3
4
  import AppLaunchpad, { type AppLaunchpadApp } from "./AppLaunchpad.island";
4
5
  import type { GlobalSearchHelpApp } from "./GlobalSearchHelpDialog";
5
6
  import GlobalSearchTrigger from "./GlobalSearchTrigger.island";
6
- import HotkeysHelpRail from "./HotkeysHelpRail.island";
7
7
  import LayoutBreadcrumbs from "./LayoutBreadcrumbs.island";
8
- import type { LayoutBreadcrumb } from "./layout-runtime";
8
+ import LayoutHelpTrigger from "./LayoutHelpTrigger.island";
9
9
  import LayoutPreferences from "./LayoutPreferences.island";
10
+ import type { LayoutBreadcrumb } from "./layout-runtime";
10
11
  import ProfilePreferences from "./ProfilePreferences.island";
11
12
 
12
13
  type LayoutHeaderProps = {
13
14
  accent?: string;
15
+ appLabel?: string;
14
16
  authenticated: boolean;
15
17
  breadcrumbs: LayoutBreadcrumb[];
16
18
  homeLabel: string;
@@ -20,6 +22,7 @@ type LayoutHeaderProps = {
20
22
  profileAvatarSrc?: string;
21
23
  profileName: string;
22
24
  searchHelpApps: GlobalSearchHelpApp[];
25
+ searchLinks?: NavigationSearchItem[];
23
26
  signInLabel: string;
24
27
  theme: CloudTheme;
25
28
  };
@@ -27,7 +30,7 @@ type LayoutHeaderProps = {
27
30
  export default function LayoutHeader(props: LayoutHeaderProps) {
28
31
  return (
29
32
  <header
30
- class="layout-header paper flex min-h-[2.875rem] shrink-0 items-center justify-between px-2 py-1.5 md:px-3 md:py-2"
33
+ class="layout-header paper flex min-h-[2.875rem] shrink-0 items-center justify-between px-2 py-1.5 lg:px-3 lg:py-2"
31
34
  style="box-shadow: var(--ui-shadow-surface)"
32
35
  >
33
36
  <div class="flex min-w-0 items-center gap-2">
@@ -35,39 +38,68 @@ export default function LayoutHeader(props: LayoutHeaderProps) {
35
38
  href="/"
36
39
  class={
37
40
  props.authenticated
38
- ? "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-dimmed transition-colors hover:bg-zinc-100 hover:text-secondary md:hidden dark:hover:bg-zinc-800"
41
+ ? "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-dimmed transition-colors hover:bg-zinc-100 hover:text-secondary lg:hidden dark:hover:bg-zinc-800"
39
42
  : "flex shrink-0 items-center"
40
43
  }
41
44
  aria-label={props.homeLabel}
42
45
  >
43
46
  <img src="/branding/logo" alt="" class={props.authenticated ? "h-4 w-4" : "h-6 w-6"} />
44
47
  </a>
45
- <div class="hidden min-w-0 items-center md:flex">
48
+ <div class="hidden min-w-0 items-center lg:flex">
46
49
  <LayoutBreadcrumbs breadcrumbs={props.breadcrumbs} />
47
50
  </div>
48
- <div class="flex min-w-0 items-center md:hidden">
49
- <LayoutBreadcrumbs breadcrumbs={props.breadcrumbs} mobile />
51
+ <div class="flex min-w-0 items-center lg:hidden">
52
+ <span class="truncate font-semibold">{props.appLabel ?? props.breadcrumbs.at(-1)?.title}</span>
50
53
  </div>
51
54
  </div>
52
55
  <div class="flex shrink-0 items-center gap-1">
53
- <div class="flex items-center gap-1 md:hidden">
54
- <HotkeysHelpRail
56
+ <div class="flex items-center gap-1 lg:hidden">
57
+ <LayoutHelpTrigger
55
58
  variant="header"
56
- registerHotkey={!props.authenticated}
59
+ registerCommand={!props.authenticated}
57
60
  searchHelpApps={props.searchHelpApps}
58
61
  accent={props.accent}
59
62
  />
60
- {props.authenticated && <GlobalSearchTrigger variant="header" registerHotkey searchHelpApps={props.searchHelpApps} />}
63
+ <GlobalSearchTrigger
64
+ variant={props.authenticated ? "header" : "host"}
65
+ registerCommand
66
+ searchLinks={props.searchLinks}
67
+ searchResources={props.authenticated}
68
+ />
61
69
  </div>
62
70
  {props.authenticated ? (
63
71
  <>
64
- <div class="md:hidden">
65
- <AppLaunchpad apps={props.launchpadApps} legalLinks={props.legalLinks} variant="header" label={props.openAppsLabel} />
72
+ <div class="lg:hidden">
73
+ <AppLaunchpad
74
+ profile={props.authenticated ? { name: props.profileName, theme: props.theme } : undefined}
75
+ apps={props.launchpadApps}
76
+ legalLinks={props.legalLinks}
77
+ variant="header"
78
+ label={props.openAppsLabel}
79
+ />
80
+ </div>
81
+ <div class="hidden lg:block">
82
+ <ProfilePreferences
83
+ avatarSrc={props.profileAvatarSrc}
84
+ initialTheme={props.theme}
85
+ name={props.profileName}
86
+ placement="header"
87
+ />
66
88
  </div>
67
- <ProfilePreferences avatarSrc={props.profileAvatarSrc} initialTheme={props.theme} name={props.profileName} placement="header" />
68
89
  </>
69
90
  ) : (
70
91
  <>
92
+ {props.appLabel && (
93
+ <div class="lg:hidden">
94
+ <AppLaunchpad
95
+ profile={props.authenticated ? { name: props.profileName, theme: props.theme } : undefined}
96
+ apps={props.launchpadApps}
97
+ legalLinks={props.legalLinks}
98
+ variant="header"
99
+ label={props.openAppsLabel}
100
+ />
101
+ </div>
102
+ )}
71
103
  <LayoutPreferences initialTheme={props.theme} position="bottom-left" />
72
104
  <ButtonLink href="/auth/login" size="sm" variant="primary">
73
105
  <i class="ti ti-login" aria-hidden="true" />
@@ -1,12 +1,14 @@
1
- import { clipboard, hotkeys } from "@k2b/stdlib/solid";
2
- import { Button, IconButton, IconButtonLink, MarkdownView, NoticeCard, Placeholder, prompts, ScrollArea, useLocale } from "@k2b/ui";
3
1
  import type { HelpDocumentManifest, HelpDocumentPayload, HelpSearchPayload } from "@k2b/cloud/shared";
2
+ import { clipboard } from "@k2b/stdlib/solid";
3
+ import { Button, IconButton, IconButtonLink, MarkdownView, NoticeCard, Placeholder, prompts, ScrollArea, useLocale } from "@k2b/ui";
4
4
  import { createEffect, createMemo, createSignal, For, type JSX, onCleanup, onMount, Show } from "solid-js";
5
+ import { COMMANDS_CHANGED, contextCommandsWithShortcuts, shortcutLabel } from "../browser/command-shortcuts";
5
6
  import { appAccentStyle } from "./app-appearance";
6
7
  import { type GlobalSearchHelpApp, openGlobalSearchHelpDialog } from "./GlobalSearchHelpDialog";
7
8
  import { helpMessages } from "./help-messages";
8
9
  import { formatHelpBundleMarkdown, formatHelpDocumentMarkdown } from "./layout-help-markdown";
9
10
  import { adjacentHelpDocuments, focusHelpArticleHeading, resetHelpArticleScroll } from "./layout-help-navigation";
11
+ import { searchHelpTopics } from "./layout-help-search";
10
12
  import { layoutHelpTopicHref } from "./layout-help-url";
11
13
 
12
14
  type HelpTopicBase = {
@@ -106,7 +108,13 @@ export function LayoutHelpDocuments(props: LayoutHelpDocumentsProps) {
106
108
  const Shortcuts = (props: { openSearchHelp: () => void }) => {
107
109
  const locale = useLocale();
108
110
  const t = () => helpMessages.resolve([locale()]).t;
109
- const entries = createMemo(() => [...hotkeys.entries()].sort((a, b) => a.label.localeCompare(b.label) || a.keys.localeCompare(b.keys)));
111
+ const [commands, setCommands] = createSignal(contextCommandsWithShortcuts());
112
+ onMount(() => {
113
+ const update = () => setCommands(contextCommandsWithShortcuts());
114
+ window.addEventListener(COMMANDS_CHANGED, update);
115
+ onCleanup(() => window.removeEventListener(COMMANDS_CHANGED, update));
116
+ });
117
+ const entries = createMemo(() => commands().filter((command) => command.shortcut));
110
118
  return (
111
119
  <div class="flex flex-col gap-3">
112
120
  <p class="text-sm leading-relaxed text-dimmed">{t().shortcutsIntro}</p>
@@ -118,20 +126,14 @@ const Shortcuts = (props: { openSearchHelp: () => void }) => {
118
126
  {(entry) => (
119
127
  <div class="flex items-start justify-between gap-4 rounded-[var(--ui-radius-surface)] bg-[var(--ui-surface-subtle)] px-3 py-2.5">
120
128
  <div class="min-w-0">
121
- <p class="text-sm font-medium text-primary">{entry.label}</p>
122
- <Show when={entry.desc}>
123
- <p class="mt-0.5 text-xs text-dimmed">{entry.desc}</p>
129
+ <p class="text-sm font-medium text-primary">{entry.title}</p>
130
+ <Show when={entry.description}>
131
+ <p class="mt-0.5 text-xs text-dimmed">{entry.description}</p>
124
132
  </Show>
125
133
  </div>
126
- <div class="flex shrink-0 gap-1" role="group" aria-label={entry.keysPretty.map((part) => part.ariaLabel).join(" + ")}>
127
- <For each={entry.keysPretty}>
128
- {(part) => (
129
- <kbd class="rounded bg-[var(--ui-surface-raised)] px-1.5 py-1 text-[11px] ring-1 ring-black/10 dark:ring-white/10">
130
- {part.key}
131
- </kbd>
132
- )}
133
- </For>
134
- </div>
134
+ <kbd class="shrink-0 rounded bg-[var(--ui-surface-raised)] px-1.5 py-1 text-[11px] ring-1 ring-black/10 dark:ring-white/10">
135
+ {shortcutLabel(entry.shortcut!)}
136
+ </kbd>
135
137
  </div>
136
138
  )}
137
139
  </For>
@@ -161,7 +163,7 @@ const HelpShell = (props: {
161
163
  const [loading, setLoading] = createSignal(props.session.view === "article");
162
164
  const [loadError, setLoadError] = createSignal<string | null>(null);
163
165
  const [loadAttempt, setLoadAttempt] = createSignal(0);
164
- const [remoteMatches, setRemoteMatches] = createSignal<ReadonlySet<string>>(new Set());
166
+ const [remoteIds, setRemoteIds] = createSignal<readonly string[] | null>(null);
165
167
  const [searching, setSearching] = createSignal(false);
166
168
  const [copyingAll, setCopyingAll] = createSignal(false);
167
169
  const [copyAllError, setCopyAllError] = createSignal<string | null>(null);
@@ -234,10 +236,7 @@ const HelpShell = (props: {
234
236
  const results = createMemo(() => {
235
237
  const value = normalizedQuery();
236
238
  if (!value) return topics();
237
- const matches = remoteMatches();
238
- return topics().filter(
239
- (topic) => [topic.title, topic.description].some((part) => part?.toLocaleLowerCase().includes(value)) || matches.has(topic.id),
240
- );
239
+ return searchHelpTopics(topics(), value, remoteIds());
241
240
  });
242
241
 
243
242
  createEffect(() => {
@@ -250,7 +249,7 @@ const HelpShell = (props: {
250
249
  ),
251
250
  ];
252
251
  const version = ++searchVersion;
253
- setRemoteMatches(new Set<string>());
252
+ setRemoteIds(null);
254
253
  setSearching(false);
255
254
  if (!value || urls.length === 0) return;
256
255
 
@@ -269,7 +268,7 @@ const HelpShell = (props: {
269
268
  }),
270
269
  )
271
270
  .then((groups) => {
272
- if (version === searchVersion) setRemoteMatches(new Set<string>(groups.flat()));
271
+ if (version === searchVersion) setRemoteIds([...new Set(groups.flat())]);
273
272
  })
274
273
  .catch((error) => {
275
274
  if (error instanceof DOMException && error.name === "AbortError") return;
@@ -445,7 +444,7 @@ const HelpShell = (props: {
445
444
  };
446
445
  const showHub = () => {
447
446
  setQuery("");
448
- setRemoteMatches(new Set<string>());
447
+ setRemoteIds(null);
449
448
  setView("hub");
450
449
  };
451
450
  const modalTitle = createMemo(() => {
@@ -1,19 +1,20 @@
1
- import { hotkeys } from "@k2b/stdlib/solid";
2
1
  import { IconButton, Tooltip, useLocale } from "@k2b/ui";
2
+ import { createEffect, onCleanup } from "solid-js";
3
+ import { registerContextAwareCommand } from "../browser/commands";
3
4
  import type { GlobalSearchHelpApp } from "./GlobalSearchHelpDialog";
4
5
  import { openLayoutHelpDialog } from "./LayoutHelp";
5
6
  import { platformMessages } from "./platform-messages";
6
7
 
7
- type HotkeysHelpTriggerProps = {
8
+ type LayoutHelpTriggerProps = {
8
9
  variant: "header" | "rail";
9
10
  class?: string;
10
- registerHotkey?: boolean;
11
+ registerCommand?: boolean;
11
12
  searchHelpApps?: GlobalSearchHelpApp[];
12
13
  accent?: string;
13
14
  };
14
15
 
15
16
  /** Opens end-user help from either the desktop rail or the compact header. */
16
- export default function HotkeysHelpRail(props: HotkeysHelpTriggerProps) {
17
+ export default function LayoutHelpTrigger(props: LayoutHelpTriggerProps) {
17
18
  const locale = useLocale();
18
19
  const t = () => platformMessages.resolve([locale()]).t;
19
20
  const searchHelpApps = props.searchHelpApps ?? [];
@@ -22,14 +23,19 @@ export default function HotkeysHelpRail(props: HotkeysHelpTriggerProps) {
22
23
  openLayoutHelpDialog(searchHelpApps, props.accent);
23
24
  };
24
25
 
25
- if (props.registerHotkey) {
26
- hotkeys.create(() => ({
27
- "shift+/": {
28
- label: t().openShortcutHelp,
29
- desc: t().shortcutHelpDescription,
30
- run: openHelp,
31
- },
32
- }));
26
+ if (props.registerCommand) {
27
+ createEffect(() =>
28
+ onCleanup(
29
+ registerContextAwareCommand({
30
+ id: "cloud.help",
31
+ title: t().openShortcutHelp,
32
+ description: t().shortcutHelpDescription,
33
+ icon: "ti ti-help-circle",
34
+ shortcut: "shift+/",
35
+ action: openHelp,
36
+ }),
37
+ ),
38
+ );
33
39
  }
34
40
 
35
41
  if (props.variant === "rail")
@@ -1,9 +1,10 @@
1
- import type { RailPreferences } from "../contracts/rail-preferences";
1
+ import type { NavigationSearchItem } from "../browser/navigation-search";
2
+ import type { RailSnapshot } from "../contracts/rail-preferences";
2
3
  import type { CloudTheme } from "../shared/theme";
3
4
  import AppLaunchpad, { type AppLaunchpadApp } from "./AppLaunchpad.island";
4
5
  import type { GlobalSearchHelpApp } from "./GlobalSearchHelpDialog";
5
6
  import GlobalSearchTrigger from "./GlobalSearchTrigger.island";
6
- import HotkeysHelpRail from "./HotkeysHelpRail.island";
7
+ import LayoutHelpTrigger from "./LayoutHelpTrigger.island";
7
8
  import ProfilePreferences from "./ProfilePreferences.island";
8
9
  import RailApps from "./RailApps.island";
9
10
  import type { RailApp } from "./rail-navigation";
@@ -17,11 +18,12 @@ type LayoutRailProps = {
17
18
  legalLinks: Array<{ label: string; href: string; icon?: string }>;
18
19
  openAppsLabel: string;
19
20
  apps: RailApp[];
20
- railSettings: RailPreferences;
21
+ railSettings: RailSnapshot;
21
22
  currentUrl: string;
22
23
  profileAvatarSrc?: string;
23
24
  profileName: string;
24
25
  searchHelpApps: GlobalSearchHelpApp[];
26
+ searchLinks?: NavigationSearchItem[];
25
27
  theme: CloudTheme;
26
28
  };
27
29
 
@@ -30,14 +32,14 @@ const jsonScript = (value: unknown): string => JSON.stringify(value).replace(/</
30
32
  export default function LayoutRail(props: LayoutRailProps) {
31
33
  return (
32
34
  <>
33
- <AppLaunchpad apps={props.launchpadApps} legalLinks={props.legalLinks} />
35
+ <AppLaunchpad profile={{ name: props.profileName, theme: props.theme }} apps={props.launchpadApps} legalLinks={props.legalLinks} />
34
36
  <script id="cloud-app-launchpad-data" type="application/json">
35
- {jsonScript({ apps: props.launchpadApps, legalLinks: props.legalLinks })}
37
+ {jsonScript({ apps: props.launchpadApps, legalLinks: props.legalLinks, profile: { name: props.profileName, theme: props.theme } })}
36
38
  </script>
37
39
  <script id="cloud-rail-data" type="application/json">
38
40
  {jsonScript({ apps: props.apps, settings: props.railSettings })}
39
41
  </script>
40
- <aside class="layout-rail hidden w-10 shrink-0 flex-col md:flex">
42
+ <aside class="layout-rail hidden w-10 shrink-0 flex-col lg:flex">
41
43
  <div class="layout-rail-logo flex h-[2.875rem] shrink-0 items-center justify-center">
42
44
  <a href="/" aria-label={props.homeLabel}>
43
45
  <img src="/branding/logo" alt="Logo" class="h-5 w-5" />
@@ -45,10 +47,16 @@ export default function LayoutRail(props: LayoutRailProps) {
45
47
  </div>
46
48
  <nav class="layout-rail-navigation flex min-h-0 flex-1 flex-col items-center gap-1" aria-label={props.appsLabel}>
47
49
  <RailApps apps={props.apps} settings={props.railSettings} currentUrl={props.currentUrl} />
48
- <AppLaunchpad apps={props.launchpadApps} legalLinks={props.legalLinks} variant="rail" label={props.openAppsLabel} />
50
+ <AppLaunchpad
51
+ profile={{ name: props.profileName, theme: props.theme }}
52
+ apps={props.launchpadApps}
53
+ legalLinks={props.legalLinks}
54
+ variant="rail"
55
+ label={props.openAppsLabel}
56
+ />
49
57
  <div class="mt-auto shrink-0 flex flex-col items-center gap-1">
50
- <GlobalSearchTrigger variant="rail" searchHelpApps={props.searchHelpApps} />
51
- <HotkeysHelpRail variant="rail" registerHotkey searchHelpApps={props.searchHelpApps} accent={props.accent} />
58
+ <GlobalSearchTrigger variant="rail" searchLinks={props.searchLinks} />
59
+ <LayoutHelpTrigger variant="rail" registerCommand searchHelpApps={props.searchHelpApps} accent={props.accent} />
52
60
  <ProfilePreferences avatarSrc={props.profileAvatarSrc} initialTheme={props.theme} name={props.profileName} placement="rail" />
53
61
  </div>
54
62
  </nav>
@@ -2,8 +2,8 @@ import { LocaleProvider } from "@k2b/ui";
2
2
  import type { JSX } from "solid-js/jsx-runtime";
3
3
  import { getLocale } from "../server/locale";
4
4
  import { readThemeFromCookieHeader } from "../shared/theme";
5
- import type { MinimalLayoutContext } from "./layout-context";
6
5
  import LayoutPreferences from "./LayoutPreferences.island";
6
+ import type { MinimalLayoutContext } from "./layout-context";
7
7
  import TimezoneCookie from "./TimezoneCookie.island";
8
8
 
9
9
  export type MinimalLayoutPreferencePosition = "top-left" | "top-right" | "bottom-left" | "bottom-right";
@@ -0,0 +1,71 @@
1
+ import { BottomSheet, Navigation, SegmentedControl, useLocale } from "@k2b/ui";
2
+ import { createSignal, onCleanup, Show } from "solid-js";
3
+ import { type AppLaunchpadContext, AppLaunchpadPanel } from "./AppLaunchpadPanel";
4
+ import { openMobileMenu } from "./mobile-menu-history";
5
+ import { platformMessages } from "./platform-messages";
6
+ import { observeWorkspaceNavigation, readWorkspaceNavigation } from "./workspace-navigation";
7
+
8
+ export function openCloudMobileMenu(apps: AppLaunchpadContext) {
9
+ return openMobileMenu((beforeSelect) => (close, context) => (
10
+ <MobileNavigation apps={apps} beforeSelect={beforeSelect} close={close} dismiss={context.requestDismiss} />
11
+ ));
12
+ }
13
+
14
+ function MobileNavigation(props: {
15
+ apps: AppLaunchpadContext;
16
+ beforeSelect: () => Promise<boolean>;
17
+ close: () => void;
18
+ dismiss: () => Promise<void>;
19
+ }) {
20
+ const locale = useLocale();
21
+ const t = () => platformMessages.resolve([locale()]).t;
22
+ const [workspace, setWorkspace] = createSignal(readWorkspaceNavigation());
23
+ const [tab, setTab] = createSignal<"app" | "apps">("app");
24
+ // A new wrapper signal version propagates in-place updates of the owner snapshot.
25
+ onCleanup(
26
+ observeWorkspaceNavigation(() =>
27
+ setWorkspace(() => {
28
+ const next = readWorkspaceNavigation();
29
+ return next ? { ...next } : undefined;
30
+ }),
31
+ ),
32
+ );
33
+ return (
34
+ <BottomSheet onDismiss={props.dismiss}>
35
+ <BottomSheet.Header
36
+ title={workspace()?.label || t().apps}
37
+ close={() => void props.dismiss()}
38
+ actions={
39
+ <Show when={workspace()} fallback={<span class="font-semibold">{t().allApps}</span>}>
40
+ <SegmentedControl
41
+ class="cloud-mobile-menu__switch"
42
+ size="md"
43
+ ariaLabel={t().navigation}
44
+ value={tab}
45
+ onValueChange={setTab}
46
+ options={[
47
+ { value: "app", label: workspace()?.label || t().appMenu },
48
+ { value: "apps", label: t().allApps },
49
+ ]}
50
+ />
51
+ </Show>
52
+ }
53
+ />
54
+ <BottomSheet.Body>
55
+ <Show
56
+ when={workspace() && tab() === "app"}
57
+ fallback={<AppLaunchpadPanel surface="sheet" {...props.apps} close={props.close} beforeSelect={props.beforeSelect} />}
58
+ >
59
+ <Navigation
60
+ navigation={workspace()!.navigation}
61
+ label={workspace()!.label || t().appMenu}
62
+ beforeSelect={async () => {
63
+ const owner = workspace()?.owner;
64
+ return (await props.beforeSelect()) && Boolean(owner?.isConnected && readWorkspaceNavigation()?.owner === owner);
65
+ }}
66
+ />
67
+ </Show>
68
+ </BottomSheet.Body>
69
+ </BottomSheet>
70
+ );
71
+ }
@@ -0,0 +1,49 @@
1
+ import { documentNavigate } from "@k2b/ssr/nav";
2
+ import { Button, ButtonLink } from "@k2b/ui";
3
+ import type { CloudTheme } from "../shared/theme";
4
+ import { createProfileActions } from "./profile-actions";
5
+
6
+ export function MobileProfileActions(props: { profile: { name: string; theme: CloudTheme }; beforeSelect: () => Promise<boolean> }) {
7
+ const { preferences, signingOut, signOut } = createProfileActions(props.profile.theme);
8
+ return (
9
+ <section class="cloud-mobile-profile" aria-label={preferences.messages().menuLabel}>
10
+ <ButtonLink
11
+ href="/me"
12
+ variant="ghost"
13
+ onClick={(event) => {
14
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
15
+ event.preventDefault();
16
+ void props.beforeSelect().then((allowed) => {
17
+ if (allowed) documentNavigate("/me");
18
+ });
19
+ }}
20
+ >
21
+ <i class="ti ti-user-circle" aria-hidden="true" />
22
+ {preferences.messages().profileSettings}
23
+ </ButtonLink>
24
+ <Button variant="ghost" onClick={preferences.toggleTheme}>
25
+ <i class={`ti ${preferences.theme() === "light" ? "ti-moon" : "ti-sun-high"}`} aria-hidden="true" />
26
+ {preferences.themeLabel()}
27
+ </Button>
28
+ <Button
29
+ variant="ghost"
30
+ onClick={async () => {
31
+ if (await props.beforeSelect()) preferences.toggleLanguage();
32
+ }}
33
+ >
34
+ <i class="ti ti-language" aria-hidden="true" />
35
+ {preferences.languageLabel()}
36
+ </Button>
37
+ <Button
38
+ variant="ghost"
39
+ disabled={signingOut()}
40
+ onClick={async () => {
41
+ if (await props.beforeSelect()) await signOut();
42
+ }}
43
+ >
44
+ <i class="ti ti-logout" aria-hidden="true" />
45
+ {preferences.messages().signOut}
46
+ </Button>
47
+ </section>
48
+ );
49
+ }
@@ -1,9 +1,6 @@
1
- import { Avatar, Dropdown, 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";
1
+ import { Avatar, Dropdown } from "@k2b/ui";
5
2
  import type { CloudTheme } from "../shared/theme";
6
- import { createPreferenceController } from "./preference-controller";
3
+ import { createProfileActions } from "./profile-actions";
7
4
 
8
5
  type ProfilePreferencesProps = {
9
6
  avatarSrc?: string;
@@ -13,37 +10,7 @@ type ProfilePreferencesProps = {
13
10
  };
14
11
 
15
12
  export default function ProfilePreferences(props: ProfilePreferencesProps) {
16
- const locale = useLocale();
17
- const preferences = createPreferenceController(props.initialTheme, locale);
18
- const [signingOut, setSigningOut] = createSignal(false);
19
- const signOut = async () => {
20
- if (signingOut()) return;
21
- setSigningOut(true);
22
- try {
23
- await browserNotificationClient.disable().catch(() => undefined);
24
- const response = await apiClient.auth.logout.$post();
25
- if (!response.ok) throw new Error(preferences.messages().signOutFailed);
26
- window.location.href = "/auth/login";
27
- } catch {
28
- toast.error(preferences.messages().signOutFailed);
29
- setSigningOut(false);
30
- }
31
- };
32
-
33
- const items = createMemo<DropdownItem[]>(() => [
34
- ...preferences.items(),
35
- {
36
- href: "/me",
37
- icon: "ti ti-user-circle",
38
- label: preferences.messages().profileSettings,
39
- },
40
- {
41
- action: signOut,
42
- icon: "ti ti-logout",
43
- label: preferences.messages().signOut,
44
- disabled: signingOut(),
45
- },
46
- ]);
13
+ const { preferences, items, signOut, signingOut } = createProfileActions(props.initialTheme);
47
14
 
48
15
  const avatar = () => <Avatar name={props.name} src={props.avatarSrc} size="xs" />;
49
16
 
@@ -1,6 +1,6 @@
1
1
  import { ScrollArea, Tooltip, useLocale } from "@k2b/ui";
2
2
  import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js";
3
- import type { RailPreferences } from "../contracts/rail-preferences";
3
+ import type { RailSnapshot } from "../contracts/rail-preferences";
4
4
  import { appAccentStyle } from "./app-appearance";
5
5
  import { RAIL_PREFERENCES_EVENT, readRailContext } from "./rail-context";
6
6
  import { projectRailNavigation, type RailApp, type RailLink, railLinkActive } from "./rail-navigation";
@@ -26,7 +26,7 @@ function RailIcon(props: { link: RailLink; currentUrl: string }) {
26
26
  );
27
27
  }
28
28
 
29
- export default function RailApps(props: { apps: RailApp[]; settings: RailPreferences; currentUrl: string }) {
29
+ export default function RailApps(props: { apps: RailApp[]; settings: RailSnapshot; currentUrl: string }) {
30
30
  const locale = useLocale();
31
31
  const [settings, setSettings] = createSignal(props.settings);
32
32
  const [currentUrl, setCurrentUrl] = createSignal(props.currentUrl);
@@ -19,7 +19,7 @@ import { coreClient } from "../clients/core";
19
19
  import { type RailPreferences, RailPreferencesSchema, type RailShortcut } from "../contracts/rail-preferences";
20
20
  import { publishRailPreferences, readRailContext } from "./rail-context";
21
21
  import { railMessages } from "./rail-messages";
22
- import { isRailAppVisible, type RailApp, sortRailApps } from "./rail-navigation";
22
+ import { isRailAppVisible, projectRailNavigation, type RailApp, sortRailApps } from "./rail-navigation";
23
23
 
24
24
  export const readRailPreferences = async (signal: AbortSignal, message: string) => {
25
25
  const response = await coreClient.me.rail.$get({}, { init: { signal } });
@@ -35,7 +35,10 @@ function RailEditorForm(props: RailDialogProps & { initial: RailPreferences }) {
35
35
  const [settings, setSettings] = createSignal<RailPreferences>(structuredClone(props.initial));
36
36
  const valid = createMemo(() => RailPreferencesSchema.safeParse(settings()).success);
37
37
  const dirty = () => JSON.stringify(settings()) !== JSON.stringify(props.initial);
38
- const pinned = () => new Set(settings().shortcuts.flatMap((entry) => (entry.kind === "app" ? [entry.appId] : [])));
38
+ const managedShortcuts = readRailContext()?.settings.managedShortcuts ?? [];
39
+ const managedLinks = () => projectRailNavigation(props.apps, { ...settings(), shortcuts: [], managedShortcuts }, locale()).shortcuts;
40
+ const pinned = () =>
41
+ new Set([...managedShortcuts, ...settings().shortcuts].flatMap((entry) => (entry.kind === "app" ? [entry.appId] : [])));
39
42
  const appOptions = () => props.apps.map((app) => ({ id: app.id, label: app.label, icon: app.iconClass }));
40
43
  const nextApp = () => props.apps.find((app) => !pinned().has(app.id));
41
44
  const updateShortcut = (index: number, next: RailShortcut) =>
@@ -77,6 +80,18 @@ function RailEditorForm(props: RailDialogProps & { initial: RailPreferences }) {
77
80
  />
78
81
  <PanelDialog.Body>
79
82
  <fieldset disabled={save.loading()} class="flex min-h-0 flex-col gap-4">
83
+ <Show when={managedLinks().length > 0}>
84
+ <PanelDialog.Section title={t().managed} icon="ti ti-building">
85
+ <For each={managedLinks()}>
86
+ {(link) => (
87
+ <div class="flex items-center gap-2 text-sm">
88
+ <i class={link.iconClass} aria-hidden="true" />
89
+ <span>{link.label}</span>
90
+ </div>
91
+ )}
92
+ </For>
93
+ </PanelDialog.Section>
94
+ </Show>
80
95
  <PanelDialog.Section title={t().apps} icon="ti ti-apps">
81
96
  <div
82
97
  style={{ display: "grid", "grid-template-columns": "repeat(auto-fit, minmax(min(100%, 13rem), 1fr))", gap: "0.75rem 1.5rem" }}
@@ -0,0 +1,7 @@
1
+ import { createNavigation, type NavigationItem } from "@k2b/ui";
2
+ import WorkspaceNavigationProvider from "./WorkspaceNavigationProvider";
3
+
4
+ /** SSR-first link navigation. Use provideWorkspaceNavigation for island-owned actions. */
5
+ export default function WorkspaceNavigation(props: { items: readonly NavigationItem[]; label: string }) {
6
+ return <WorkspaceNavigationProvider navigation={createNavigation({ items: () => props.items })} label={props.label} />;
7
+ }
@@ -0,0 +1,16 @@
1
+ import type { NavigationController } from "@k2b/ui";
2
+ import { provideWorkspaceNavigation } from "./workspace-navigation";
3
+
4
+ /** Use within an existing app island when navigation includes local actions. */
5
+ export default function WorkspaceNavigationProvider(props: { navigation: NavigationController; label: string }) {
6
+ let owner!: HTMLScriptElement;
7
+ provideWorkspaceNavigation(props.navigation, { label: () => props.label, owner: () => owner });
8
+ return (
9
+ <script
10
+ ref={owner}
11
+ type="application/json"
12
+ data-cloud-workspace-navigation
13
+ innerHTML={JSON.stringify({ label: props.label, items: props.navigation.items() }).replace(/</g, "\\u003c")}
14
+ />
15
+ );
16
+ }
@@ -23,6 +23,7 @@ export const buildAdminGroups = (apps: readonly RuntimeContext["apps"][number][]
23
23
  const aiLinks: AdminLink[] = [
24
24
  { href: "/admin/settings?tab=ai-general", icon: "ti-adjustments", label: t.general },
25
25
  { href: "/admin/settings?tab=ai-providers", icon: "ti-sparkles", label: t.providers },
26
+ { href: "/admin/settings?tab=ai-quotas", icon: "ti-gauge", label: t.aiQuotas },
26
27
  { href: "/admin/settings?tab=ai-usage", icon: "ti-chart-histogram", label: t.aiUsage },
27
28
  { href: "/admin/settings?tab=ai-skills", icon: "ti-wand", label: t.skills },
28
29
  { href: "/admin/settings?tab=ai-projects", icon: "ti-folders", label: t.projects },
@@ -33,6 +34,7 @@ export const buildAdminGroups = (apps: readonly RuntimeContext["apps"][number][]
33
34
  app,
34
35
  groups: (app.adminNav ?? [])
35
36
  .map((group) => ({
37
+ section: group.section,
36
38
  label: group.label,
37
39
  links: group.links.flatMap((link) => {
38
40
  const href = normalizeAdminHref(link.href);
@@ -43,6 +45,10 @@ export const buildAdminGroups = (apps: readonly RuntimeContext["apps"][number][]
43
45
  }))
44
46
  .sort((a, b) => a.app.name.localeCompare(b.app.name));
45
47
 
48
+ aiLinks.push(
49
+ ...contributedApps.flatMap(({ groups }) => groups.filter((group) => group.section === "ai").flatMap((group) => group.links)),
50
+ );
51
+
46
52
  const appsWithGroups = new Set(contributedApps.filter(({ groups }) => groups.length > 0).map(({ app }) => app.id));
47
53
  const appLinks = apps
48
54
  .filter((app) => !appsWithGroups.has(app.id))
@@ -57,10 +63,14 @@ export const buildAdminGroups = (apps: readonly RuntimeContext["apps"][number][]
57
63
  label: t.general,
58
64
  links: [
59
65
  { href: "/admin", icon: "ti-dashboard", label: t.overview },
66
+ { href: "/admin/app-credentials", icon: "ti-key", label: t.appCredentials },
67
+ { href: "/admin/rail", icon: "ti-layout-sidebar", label: t.rail },
60
68
  { href: "/admin/announcements", icon: "ti-speakerphone", label: t.announcements },
61
69
  ],
62
70
  },
63
- ...contributedApps.flatMap(({ groups }) => groups),
71
+ ...contributedApps.flatMap(({ groups }) =>
72
+ groups.filter((group) => group.section !== "ai").map(({ label, links }) => ({ label, links })),
73
+ ),
64
74
  {
65
75
  label: t.userManagement,
66
76
  links: [