@k2b/cloud 0.6.0-rc.0 → 0.7.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 (315) hide show
  1. package/README.md +2 -2
  2. package/package.json +30 -17
  3. package/scripts/browser-performance.ts +14 -0
  4. package/scripts/build-pdf-renderer.ts +39 -0
  5. package/scripts/build.ts +15 -35
  6. package/scripts/preload.ts +9 -12
  7. package/scripts/sync-recovery-smoke.ts +2 -1
  8. package/src/_internal/app-presentation.ts +14 -2
  9. package/src/_internal/capabilities.ts +108 -7
  10. package/src/_internal/capability-streams.ts +81 -0
  11. package/src/_internal/define-app.ts +62 -36
  12. package/src/_internal/heartbeat.ts +4 -0
  13. package/src/_internal/help-catalog.ts +27 -172
  14. package/src/_internal/help.ts +13 -76
  15. package/src/_internal/page-responses.ts +2 -0
  16. package/src/_internal/postgres-application-name.ts +30 -0
  17. package/src/_internal/process-identity.ts +15 -0
  18. package/src/_internal/process-sync.ts +1 -1
  19. package/src/_internal/registry-validation.ts +29 -25
  20. package/src/_internal/registry.ts +2 -53
  21. package/src/_internal/runtime-context.ts +5 -16
  22. package/src/_internal/server-timing.ts +13 -0
  23. package/src/_internal/status-preserving-ssr.ts +18 -6
  24. package/src/_internal/web-vitals-asset.ts +10 -0
  25. package/src/access/PermissionEditor.tsx +14 -153
  26. package/src/access/PrincipalPicker.tsx +104 -0
  27. package/src/access/ui.ts +2 -0
  28. package/src/ai/admin.ts +14 -13
  29. package/src/ai/assistant-models.ts +12 -1
  30. package/src/ai/audio-format.ts +34 -0
  31. package/src/ai/audio-tool.ts +90 -0
  32. package/src/ai/browser-code-contracts.ts +79 -0
  33. package/src/ai/browser.ts +2 -0
  34. package/src/ai/capabilities.ts +55 -36
  35. package/src/ai/capability-execution.ts +47 -12
  36. package/src/ai/chat/blocks.tsx +157 -95
  37. package/src/ai/chat/capability-result.ts +15 -0
  38. package/src/ai/chat/capability-table.tsx +2 -10
  39. package/src/ai/chat/composer-adapter.ts +48 -3
  40. package/src/ai/chat/message-actions.tsx +8 -7
  41. package/src/ai/chat/message-utils.ts +12 -1
  42. package/src/ai/chat/messages.ts +2 -0
  43. package/src/ai/chat/presentation.tsx +19 -48
  44. package/src/ai/chat/primitives.tsx +43 -1
  45. package/src/ai/chat/tool-disclosure.tsx +17 -2
  46. package/src/ai/chat/tool-groups.ts +52 -0
  47. package/src/ai/chat-quotas.ts +25 -0
  48. package/src/ai/chat-task-contracts.ts +9 -1
  49. package/src/ai/chat-tasks.ts +276 -43
  50. package/src/ai/client/controller.ts +110 -33
  51. package/src/ai/client/file-source.ts +12 -6
  52. package/src/ai/client/live-connection.ts +1 -0
  53. package/src/ai/client/projection.ts +22 -11
  54. package/src/ai/client/transport.ts +4 -0
  55. package/src/ai/code-capability-routes.ts +110 -0
  56. package/src/ai/code-capability-transport.ts +38 -0
  57. package/src/ai/code-mode-skill.ts +114 -0
  58. package/src/ai/code-runtime-tools.ts +166 -0
  59. package/src/ai/code-source-contracts.ts +202 -0
  60. package/src/ai/code-source-tools.ts +145 -0
  61. package/src/ai/compaction.ts +27 -6
  62. package/src/ai/data-analysis-skill.ts +10 -0
  63. package/src/ai/default-tools.ts +25 -2
  64. package/src/ai/dictation-runtime.ts +171 -0
  65. package/src/ai/dictations.ts +188 -0
  66. package/src/ai/draft-content.ts +34 -0
  67. package/src/ai/enrich.ts +6 -0
  68. package/src/ai/executor.ts +191 -31
  69. package/src/ai/file-content-version.ts +22 -0
  70. package/src/ai/file-context.ts +2 -1
  71. package/src/ai/file-media-type.ts +12 -0
  72. package/src/ai/file-reference-contracts.ts +15 -0
  73. package/src/ai/file-tools.ts +2 -1
  74. package/src/ai/files-store.ts +174 -71
  75. package/src/ai/fixtures/vision-encrypted.pdf +0 -0
  76. package/src/ai/grids-skill.ts +40 -10
  77. package/src/ai/http.ts +15 -8
  78. package/src/ai/index.ts +26 -2
  79. package/src/ai/inference-calls.ts +198 -0
  80. package/src/ai/live-events.ts +5 -1
  81. package/src/ai/memory-learning.ts +60 -28
  82. package/src/ai/message-queue.ts +167 -0
  83. package/src/ai/migrate.ts +187 -11
  84. package/src/ai/model-pricing.ts +43 -0
  85. package/src/ai/pdf-render-worker.ts +74 -0
  86. package/src/ai/pdf-render.fixture.ts +28 -0
  87. package/src/ai/pdf-render.ts +77 -0
  88. package/src/ai/projects-routes.ts +3 -3
  89. package/src/ai/projects.ts +20 -6
  90. package/src/ai/protocol.ts +3 -0
  91. package/src/ai/provider.ts +1 -8
  92. package/src/ai/quota-provider.ts +191 -0
  93. package/src/ai/quota-report.ts +152 -0
  94. package/src/ai/quotas-migrate.ts +45 -0
  95. package/src/ai/quotas.ts +168 -0
  96. package/src/ai/routes.ts +215 -11
  97. package/src/ai/run-timeout.ts +10 -0
  98. package/src/ai/runtime-tools.ts +3 -0
  99. package/src/ai/runtime.ts +55 -16
  100. package/src/ai/settings.ts +36 -8
  101. package/src/ai/short-id.ts +2 -2
  102. package/src/ai/skill-catalog.ts +0 -10
  103. package/src/ai/skill-search.ts +47 -0
  104. package/src/ai/skill-seeds.ts +105 -20
  105. package/src/ai/skill-tool.ts +16 -5
  106. package/src/ai/skills-routes.ts +32 -4
  107. package/src/ai/skills.ts +324 -155
  108. package/src/ai/store.ts +373 -191
  109. package/src/ai/stream.ts +5 -0
  110. package/src/ai/structured-runs.ts +4 -3
  111. package/src/ai/structured.ts +8 -1
  112. package/src/ai/system-prompt.ts +12 -2
  113. package/src/ai/timeline.ts +2 -5
  114. package/src/ai/todo-contracts.ts +20 -0
  115. package/src/ai/todo-tool.ts +16 -0
  116. package/src/ai/tool-audit.ts +30 -40
  117. package/src/ai/tools.ts +6 -0
  118. package/src/ai/transcription.ts +202 -0
  119. package/src/ai/turn-timing.ts +114 -0
  120. package/src/ai/types.ts +58 -10
  121. package/src/ai/ui.tsx +2 -0
  122. package/src/ai/usage.ts +118 -118
  123. package/src/ai/vision-tool.ts +39 -10
  124. package/src/api/admin-ai-projects.ts +2 -2
  125. package/src/api/admin-ai-quotas.ts +74 -0
  126. package/src/api/admin-ai-skills.ts +48 -5
  127. package/src/api/admin-ai-usage.ts +2 -2
  128. package/src/api/admin-core-settings.ts +25 -0
  129. package/src/api/admin-identity.ts +91 -51
  130. package/src/api/admin-rail.ts +58 -0
  131. package/src/api/announcements.ts +14 -0
  132. package/src/api/capabilities.ts +292 -43
  133. package/src/api/capability-streams.ts +262 -0
  134. package/src/api/help.ts +8 -44
  135. package/src/api/index.ts +6 -0
  136. package/src/api/mcp.ts +44 -26
  137. package/src/api/me-web-vitals.ts +35 -0
  138. package/src/api/me.ts +2 -0
  139. package/src/api/search/schemas.ts +26 -15
  140. package/src/api/search.bench.ts +1 -1
  141. package/src/api/search.ts +54 -17
  142. package/src/browser/CloudResourceSearch.tsx +640 -702
  143. package/src/browser/app-approval-vault.ts +19 -7
  144. package/src/browser/command-bridge.ts +109 -0
  145. package/src/browser/command-shortcuts.ts +76 -0
  146. package/src/browser/commands.ts +66 -0
  147. package/src/browser/navigation-search.ts +48 -0
  148. package/src/browser/notifications.ts +1 -1
  149. package/src/browser/resource-picker.tsx +5 -18
  150. package/src/browser/resource-search-dialog.ts +9 -0
  151. package/src/browser/resource-search-input.ts +46 -0
  152. package/src/browser/resource-search-messages.ts +76 -40
  153. package/src/browser/resource-search.ts +7 -0
  154. package/src/browser/search-bridge.ts +102 -0
  155. package/src/browser/search-commands.ts +59 -0
  156. package/src/browser/search.ts +13 -0
  157. package/src/browser/spotlight-position.ts +208 -0
  158. package/src/browser/testing.ts +5 -0
  159. package/src/browser/web-vitals.ts +25 -0
  160. package/src/capabilities/claims.ts +179 -0
  161. package/src/capabilities/client.ts +34 -0
  162. package/src/capabilities/command-link.ts +16 -0
  163. package/src/capabilities/executions.ts +298 -0
  164. package/src/capabilities/migrate.ts +81 -0
  165. package/src/capabilities/server.ts +114 -34
  166. package/src/capabilities/stream-body.ts +16 -0
  167. package/src/capabilities/streams.ts +28 -0
  168. package/src/cli/admin/ai-quotas.ts +161 -0
  169. package/src/cli/admin/ai-skills.ts +64 -0
  170. package/src/cli/admin/ai-usage.ts +53 -30
  171. package/src/cli/admin/app-credentials.ts +79 -0
  172. package/src/cli/admin/data.ts +17 -0
  173. package/src/cli/admin/index.ts +9 -0
  174. package/src/cli/capabilities.ts +29 -0
  175. package/src/cli/capability-streams.ts +25 -0
  176. package/src/cli/index.ts +3 -2
  177. package/src/config/env.ts +11 -8
  178. package/src/contracts/app.ts +23 -3
  179. package/src/contracts/capabilities.ts +68 -5
  180. package/src/contracts/capability-streams.ts +46 -0
  181. package/src/contracts/commands.ts +49 -0
  182. package/src/contracts/index.ts +6 -1
  183. package/src/contracts/rail-admin.ts +38 -0
  184. package/src/contracts/rail-preferences.ts +16 -0
  185. package/src/contracts/registry.ts +3 -27
  186. package/src/contracts/settings-types.ts +1 -1
  187. package/src/contracts/shared.ts +9 -5
  188. package/src/contracts/web-vitals.ts +27 -0
  189. package/src/desktop/solid.tsx +3 -3
  190. package/src/index.ts +0 -4
  191. package/src/server/index.ts +3 -0
  192. package/src/server/middleware/auth.ts +3 -2
  193. package/src/server/middleware/route-template.ts +9 -3
  194. package/src/server/middleware/runtime.ts +2 -1
  195. package/src/server/middleware/settings.ts +17 -11
  196. package/src/server/services/access-revision.ts +10 -0
  197. package/src/server/services/access.ts +1 -1
  198. package/src/server/services/index.ts +1 -0
  199. package/src/services/account-category-policy.ts +9 -0
  200. package/src/services/account-lifecycle/audit.ts +1 -1
  201. package/src/services/accounts/app.ts +12 -2
  202. package/src/services/accounts/email-write.ts +23 -0
  203. package/src/services/accounts/groups.ts +15 -4
  204. package/src/services/accounts/identities.ts +249 -0
  205. package/src/services/accounts/identity-reconciliation.ts +87 -0
  206. package/src/services/accounts/ipa-data.ts +2 -2
  207. package/src/services/accounts/local-groups.ts +2 -2
  208. package/src/services/accounts/posix.ts +45 -29
  209. package/src/services/accounts/users.ts +2 -2
  210. package/src/services/announcements/index.ts +69 -8
  211. package/src/services/audit/index.ts +1 -1
  212. package/src/services/cache-fill.ts +35 -0
  213. package/src/services/help/index.ts +109 -0
  214. package/src/services/help/maintenance.ts +25 -0
  215. package/src/services/help/store.ts +180 -0
  216. package/src/services/help/types.ts +46 -0
  217. package/src/services/identity/invocation-actor.ts +2 -2
  218. package/src/services/identity/key-config.ts +2 -1
  219. package/src/services/identity/key-ring.ts +1 -1
  220. package/src/services/index.ts +15 -0
  221. package/src/services/ipa/users.ts +268 -221
  222. package/src/services/legal-consent.ts +1 -1
  223. package/src/services/logging/index.ts +15 -13
  224. package/src/services/logging/trace.ts +2 -1
  225. package/src/services/logging/web-vitals.ts +81 -0
  226. package/src/services/mandates/index.ts +18 -4
  227. package/src/services/mandates/policy.ts +65 -2
  228. package/src/services/notifications/batches.ts +124 -57
  229. package/src/services/notifications/browser.ts +12 -0
  230. package/src/services/notifications/index.ts +0 -2
  231. package/src/services/notifications/platform.ts +0 -4
  232. package/src/services/oauth-tokens.ts +3 -3
  233. package/src/services/pdf/gotenberg.ts +107 -84
  234. package/src/services/pdf/index.ts +3 -0
  235. package/src/services/providers/local/users.ts +25 -16
  236. package/src/services/public-http.ts +101 -0
  237. package/src/services/rail-shortcuts.ts +95 -0
  238. package/src/services/rail-snapshot.ts +53 -0
  239. package/src/services/request-cache-redis.ts +28 -0
  240. package/src/services/session/index.ts +15 -2
  241. package/src/services/session/user.ts +62 -37
  242. package/src/services/settings/core-settings.ts +21 -28
  243. package/src/services/settings/defaults.ts +5 -0
  244. package/src/services/settings/store.ts +58 -84
  245. package/src/shared/ai-costs.ts +42 -0
  246. package/src/shared/ai-platform-prompt.ts +1 -1
  247. package/src/shared/ai-quotas.ts +136 -0
  248. package/src/shared/ai-usage.ts +6 -16
  249. package/src/shared/app-presentation.ts +21 -15
  250. package/src/shared/capability-messages.ts +14 -0
  251. package/src/shared/help.ts +6 -42
  252. package/src/shared/index.ts +6 -0
  253. package/src/ssr/AdminLayout.tsx +7 -5
  254. package/src/ssr/AdminSidebar.tsx +29 -15
  255. package/src/ssr/AppLaunchpad.island.tsx +24 -137
  256. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  257. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  258. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  259. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  260. package/src/ssr/Layout.tsx +17 -6
  261. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  262. package/src/ssr/LayoutFooter.tsx +1 -1
  263. package/src/ssr/LayoutHeader.tsx +46 -14
  264. package/src/ssr/LayoutHelp.tsx +23 -22
  265. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  266. package/src/ssr/LayoutRail.tsx +17 -9
  267. package/src/ssr/MobileNavigation.tsx +71 -0
  268. package/src/ssr/MobileProfileActions.tsx +49 -0
  269. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  270. package/src/ssr/RailApps.island.tsx +2 -2
  271. package/src/ssr/RailEditor.tsx +17 -2
  272. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  273. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  274. package/src/ssr/admin-navigation.ts +7 -1
  275. package/src/ssr/help.ts +29 -0
  276. package/src/ssr/islands/SearchBar.tsx +67 -0
  277. package/src/ssr/islands/index.ts +3 -1
  278. package/src/ssr/layout-context.ts +2 -2
  279. package/src/ssr/layout-help-search.ts +15 -0
  280. package/src/ssr/mobile-menu-history.ts +70 -0
  281. package/src/ssr/platform-messages.ts +16 -8
  282. package/src/ssr/profile-actions.ts +42 -0
  283. package/src/ssr/rail-context.ts +8 -4
  284. package/src/ssr/rail-messages.ts +2 -0
  285. package/src/ssr/rail-navigation.ts +3 -3
  286. package/src/ssr/workspace-navigation.ts +75 -0
  287. package/src/styles/effects.css +6 -6
  288. package/src/styles/global.css +54 -0
  289. package/src/styles/input.css +43 -41
  290. package/src/styles/resource-search.css +505 -0
  291. package/src/styles/utilities-feedback.css +13 -0
  292. package/src/styles/utilities-layout.css +1 -5
  293. package/src/styles/utilities-navigation.css +3 -39
  294. package/src/types/ambient.d.ts +5 -0
  295. package/src/workflows/ai/runtime.ts +5 -1
  296. package/src/workflows/ai/store.ts +2 -0
  297. package/src/workflows/ai-actions.ts +16 -13
  298. package/src/workflows/definition.ts +10 -5
  299. package/src/workflows/store/actions.ts +44 -20
  300. package/src/workflows/store/index.ts +2 -0
  301. package/src/workflows/store/runs.ts +36 -0
  302. package/src/workflows/store/worker-pool.ts +55 -0
  303. package/src/workflows/store/worker-runtime.ts +90 -0
  304. package/scripts/app-favicon.test.ts +0 -49
  305. package/src/ai/chat/blocks.render.test.tsx +0 -1108
  306. package/src/ai/kit-skill.ts +0 -22
  307. package/src/ai/usage-migrate.ts +0 -81
  308. package/src/browser/CloudResourceSearch.behavior.test.tsx +0 -74
  309. package/src/browser/notification-target.ts +0 -27
  310. package/src/contracts/notification-live.ts +0 -66
  311. package/src/services/notifications/live.ts +0 -50
  312. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  313. package/src/ssr/GlobalSearchDialog.behavior.test.tsx +0 -96
  314. package/src/ssr/Layout.render.test.tsx +0 -148
  315. 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
+ import LayoutHelpTrigger from "./LayoutHelpTrigger.island";
7
8
  import LayoutBreadcrumbs from "./LayoutBreadcrumbs.island";
8
- import type { LayoutBreadcrumb } from "./layout-runtime";
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,4 +1,6 @@
1
- import { clipboard, hotkeys } from "@k2b/stdlib/solid";
1
+ import { COMMANDS_CHANGED, contextCommandsWithShortcuts, shortcutLabel } from "../browser/command-shortcuts";
2
+ import { searchHelpTopics } from "./layout-help-search";
3
+ import { clipboard } from "@k2b/stdlib/solid";
2
4
  import { Button, IconButton, IconButtonLink, MarkdownView, NoticeCard, Placeholder, prompts, ScrollArea, useLocale } from "@k2b/ui";
3
5
  import type { HelpDocumentManifest, HelpDocumentPayload, HelpSearchPayload } from "@k2b/cloud/shared";
4
6
  import { createEffect, createMemo, createSignal, For, type JSX, onCleanup, onMount, Show } from "solid-js";
@@ -106,7 +108,15 @@ 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(() =>
118
+ commands().filter((command) => command.shortcut),
119
+ );
110
120
  return (
111
121
  <div class="flex flex-col gap-3">
112
122
  <p class="text-sm leading-relaxed text-dimmed">{t().shortcutsIntro}</p>
@@ -118,20 +128,14 @@ const Shortcuts = (props: { openSearchHelp: () => void }) => {
118
128
  {(entry) => (
119
129
  <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
130
  <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>
131
+ <p class="text-sm font-medium text-primary">{entry.title}</p>
132
+ <Show when={entry.description}>
133
+ <p class="mt-0.5 text-xs text-dimmed">{entry.description}</p>
124
134
  </Show>
125
135
  </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>
136
+ <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">
137
+ {shortcutLabel(entry.shortcut!)}
138
+ </kbd>
135
139
  </div>
136
140
  )}
137
141
  </For>
@@ -161,7 +165,7 @@ const HelpShell = (props: {
161
165
  const [loading, setLoading] = createSignal(props.session.view === "article");
162
166
  const [loadError, setLoadError] = createSignal<string | null>(null);
163
167
  const [loadAttempt, setLoadAttempt] = createSignal(0);
164
- const [remoteMatches, setRemoteMatches] = createSignal<ReadonlySet<string>>(new Set());
168
+ const [remoteIds, setRemoteIds] = createSignal<readonly string[] | null>(null);
165
169
  const [searching, setSearching] = createSignal(false);
166
170
  const [copyingAll, setCopyingAll] = createSignal(false);
167
171
  const [copyAllError, setCopyAllError] = createSignal<string | null>(null);
@@ -234,10 +238,7 @@ const HelpShell = (props: {
234
238
  const results = createMemo(() => {
235
239
  const value = normalizedQuery();
236
240
  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
- );
241
+ return searchHelpTopics(topics(), value, remoteIds());
241
242
  });
242
243
 
243
244
  createEffect(() => {
@@ -250,7 +251,7 @@ const HelpShell = (props: {
250
251
  ),
251
252
  ];
252
253
  const version = ++searchVersion;
253
- setRemoteMatches(new Set<string>());
254
+ setRemoteIds(null);
254
255
  setSearching(false);
255
256
  if (!value || urls.length === 0) return;
256
257
 
@@ -269,7 +270,7 @@ const HelpShell = (props: {
269
270
  }),
270
271
  )
271
272
  .then((groups) => {
272
- if (version === searchVersion) setRemoteMatches(new Set<string>(groups.flat()));
273
+ if (version === searchVersion) setRemoteIds([...new Set(groups.flat())]);
273
274
  })
274
275
  .catch((error) => {
275
276
  if (error instanceof DOMException && error.name === "AbortError") return;
@@ -445,7 +446,7 @@ const HelpShell = (props: {
445
446
  };
446
447
  const showHub = () => {
447
448
  setQuery("");
448
- setRemoteMatches(new Set<string>());
449
+ setRemoteIds(null);
449
450
  setView("hub");
450
451
  };
451
452
  const modalTitle = createMemo(() => {
@@ -1,19 +1,20 @@
1
- import { hotkeys } from "@k2b/stdlib/solid";
1
+ import { registerContextAwareCommand } from "../browser/commands";
2
+ import { createEffect, onCleanup } from "solid-js";
2
3
  import { IconButton, Tooltip, useLocale } from "@k2b/ui";
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>
@@ -0,0 +1,71 @@
1
+ import { BottomSheet, Navigation, SegmentedControl, useLocale } from "@k2b/ui";
2
+ import { createSignal, onCleanup, Show } from "solid-js";
3
+ import { AppLaunchpadPanel, type AppLaunchpadContext } from "./AppLaunchpadPanel";
4
+ import { openMobileMenu } from "./mobile-menu-history";
5
+ import { observeWorkspaceNavigation, readWorkspaceNavigation } from "./workspace-navigation";
6
+ import { platformMessages } from "./platform-messages";
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 { createProfileActions } from "./profile-actions";
2
+ import { Avatar, Dropdown } from "@k2b/ui";
5
3
  import type { CloudTheme } from "../shared/theme";
6
- import { createPreferenceController } from "./preference-controller";
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,8 @@ 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(...contributedApps.flatMap(({groups}) => groups.filter(group => group.section === "ai").flatMap(group => group.links)));
49
+
46
50
  const appsWithGroups = new Set(contributedApps.filter(({ groups }) => groups.length > 0).map(({ app }) => app.id));
47
51
  const appLinks = apps
48
52
  .filter((app) => !appsWithGroups.has(app.id))
@@ -57,10 +61,12 @@ export const buildAdminGroups = (apps: readonly RuntimeContext["apps"][number][]
57
61
  label: t.general,
58
62
  links: [
59
63
  { href: "/admin", icon: "ti-dashboard", label: t.overview },
64
+ { href: "/admin/app-credentials", icon: "ti-key", label: t.appCredentials },
65
+ { href: "/admin/rail", icon: "ti-layout-sidebar", label: t.rail },
60
66
  { href: "/admin/announcements", icon: "ti-speakerphone", label: t.announcements },
61
67
  ],
62
68
  },
63
- ...contributedApps.flatMap(({ groups }) => groups),
69
+ ...contributedApps.flatMap(({ groups }) => groups.filter(group => group.section !== "ai").map(({label,links}) => ({label,links}))),
64
70
  {
65
71
  label: t.userManagement,
66
72
  links: [