@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
@@ -0,0 +1,181 @@
1
+ import type { CloudTheme } from "../shared/theme";
2
+ import { MobileProfileActions } from "./MobileProfileActions";
3
+ import { Dynamic } from "solid-js/web";
4
+ import { platformMessages } from "./platform-messages";
5
+ import { IconButton, ScrollArea, TextInput, useLocale } from "@k2b/ui";
6
+ import { createSignal, For, Show, type JSX } from "solid-js";
7
+ import { documentNavigate } from "@k2b/ssr/nav";
8
+ import { openRailEditor } from "./RailEditor";
9
+ import { readRailContext } from "./rail-context";
10
+ import { railMessages } from "./rail-messages";
11
+ import { projectRailNavigation } from "./rail-navigation";
12
+
13
+ export type AppLaunchpadApp = {
14
+ id: string;
15
+ iconClass: string;
16
+ label: string;
17
+ href: string;
18
+ description?: string;
19
+ accent?: string;
20
+ };
21
+
22
+ export type AppLaunchpadLegalLink = {
23
+ label: string;
24
+ href: string;
25
+ icon?: string;
26
+ };
27
+
28
+ export type AppLaunchpadContext = {
29
+ profile?: { name: string; theme: CloudTheme };
30
+ apps: AppLaunchpadApp[];
31
+ legalLinks: AppLaunchpadLegalLink[];
32
+ };
33
+
34
+ type AppIconPaletteEntry = { from: string };
35
+
36
+ const appIconPalette: readonly [AppIconPaletteEntry, ...AppIconPaletteEntry[]] = [
37
+ { from: "#2563eb" },
38
+ { from: "#059669" },
39
+ { from: "#7c3aed" },
40
+ { from: "#d97706" },
41
+ { from: "#e11d48" },
42
+ { from: "#0891b2" },
43
+ { from: "#52525b" },
44
+ ];
45
+
46
+ const paletteForId = (id: string) => {
47
+ let hash = 0;
48
+ for (let i = 0; i < id.length; i++) hash = (hash + id.charCodeAt(i)) % appIconPalette.length;
49
+ return appIconPalette[hash] ?? appIconPalette[0];
50
+ };
51
+
52
+ const appIconStyle = (app: AppLaunchpadApp) => {
53
+ const tone = /^#[0-9a-f]{6}$/i.test(app.accent ?? "") ? app.accent! : paletteForId(app.id).from;
54
+ return `--app-icon-color:${tone}`;
55
+ };
56
+
57
+ export const AppLaunchpadPanel = (
58
+ props: AppLaunchpadContext & { close: () => void; beforeSelect?: () => Promise<boolean>; surface?: "panel" | "sheet" },
59
+ ) => {
60
+ const locale = useLocale();
61
+ const t = () => railMessages.resolve([locale()]).t;
62
+ const rail = readRailContext();
63
+ const [query, setQuery] = createSignal("");
64
+ const messages = () => platformMessages.resolve([locale()]).t;
65
+ const matches = (label: string, description = "") =>
66
+ `${label} ${description}`.toLocaleLowerCase(locale()).includes(query().trim().toLocaleLowerCase(locale()));
67
+ const apps = () => props.apps.filter((app) => matches(app.label, app.description));
68
+ const shortcuts = () =>
69
+ (rail ? projectRailNavigation(rail.apps, rail.settings, locale()).shortcuts : []).filter((shortcut) => matches(shortcut.label));
70
+ const follow: JSX.EventHandler<HTMLAnchorElement, MouseEvent> = (event) => {
71
+ if (!props.beforeSelect || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
72
+ event.preventDefault();
73
+ const href = event.currentTarget.href;
74
+ void props.beforeSelect().then((allowed) => {
75
+ if (allowed) documentNavigate(href);
76
+ });
77
+ };
78
+ return (
79
+ <Dynamic
80
+ component={props.surface === "sheet" ? "div" : ScrollArea}
81
+ class={
82
+ props.surface === "sheet"
83
+ ? "cloud-mobile-apps"
84
+ : "launchpad-panel mx-auto max-h-[min(86vh,var(--ui-dialog-available-height))] w-[var(--ui-dialog-available-width)] max-w-[var(--ui-dialog-available-width)] overscroll-contain p-4 text-primary sm:w-fit sm:p-6 md:p-7 dark:text-white"
85
+ }
86
+ >
87
+ <div class="launchpad-tools">
88
+ <Show when={props.surface === "sheet"}>
89
+ <TextInput
90
+ value={query}
91
+ onValueChange={setQuery}
92
+ icon="ti ti-search"
93
+ clearable
94
+ placeholder={messages().findApps}
95
+ aria-label={messages().findApps}
96
+ />
97
+ </Show>
98
+ <Show when={rail}>
99
+ <div class="launchpad-customize mb-2 flex justify-end">
100
+ <IconButton
101
+ variant="ghost"
102
+ size="sm"
103
+ label={t().customize}
104
+ tooltip={t().customize}
105
+ tooltipDelay={0}
106
+ onClick={async () => {
107
+ if (props.beforeSelect) {
108
+ if (!(await props.beforeSelect())) return;
109
+ } else props.close();
110
+ openRailEditor(locale());
111
+ }}
112
+ >
113
+ <i class="ti ti-adjustments-horizontal" aria-hidden="true" />
114
+ </IconButton>
115
+ </div>
116
+ </Show>
117
+ </div>
118
+ <Show when={shortcuts().length > 0}>
119
+ <section class="mb-5" aria-label={t().shortcuts}>
120
+ <div class="flex flex-wrap justify-center gap-3">
121
+ <For each={shortcuts()}>
122
+ {(shortcut) => (
123
+ <a
124
+ onClick={follow}
125
+ href={shortcut.href}
126
+ class="group flex w-16 min-w-0 flex-col items-center gap-1 rounded-lg p-1 text-center focus-ui"
127
+ >
128
+ <span class="grid h-9 w-9 place-items-center rounded-xl bg-[var(--ui-hover)] text-lg transition-colors group-hover:bg-[var(--ui-active)]">
129
+ <i class={shortcut.iconClass} aria-hidden="true" />
130
+ </span>
131
+ <span class="max-w-full truncate text-[11px] font-medium text-primary dark:text-white">{shortcut.label}</span>
132
+ </a>
133
+ )}
134
+ </For>
135
+ </div>
136
+ </section>
137
+ </Show>
138
+ <div class="launchpad-apps-grid flex flex-wrap justify-center gap-x-4 gap-y-4 sm:gap-x-7 sm:gap-y-6">
139
+ <For each={apps()}>
140
+ {(app) => (
141
+ <a
142
+ onClick={follow}
143
+ href={app.href}
144
+ class="launchpad-app group flex w-[4.75rem] min-w-0 flex-col items-center gap-1.5 rounded-2xl p-1 text-center focus-ui sm:w-[6.25rem] sm:gap-2 sm:p-2"
145
+ >
146
+ <span
147
+ class="app-icon grid h-12 w-12 place-items-center rounded-[0.95rem] text-[1.25rem] sm:h-16 sm:w-16 sm:rounded-[1.25rem] sm:text-[1.7rem]"
148
+ style={appIconStyle(app)}
149
+ >
150
+ <i class={app.iconClass} />
151
+ </span>
152
+ <span class="max-w-full truncate text-[11px] font-medium text-primary sm:text-xs dark:text-white">{app.label}</span>
153
+ </a>
154
+ )}
155
+ </For>
156
+ </div>
157
+ <Show when={!apps().length && !shortcuts().length}>
158
+ <p class="py-6 text-center text-dimmed">{messages().noAppsFound}</p>
159
+ </Show>
160
+ <Show when={props.surface === "sheet" && props.profile && props.beforeSelect}>
161
+ <MobileProfileActions profile={props.profile!} beforeSelect={props.beforeSelect!} />
162
+ </Show>
163
+ <Show when={props.legalLinks.length > 0}>
164
+ <div class="mt-7 flex flex-wrap justify-center text-[11px] text-dimmed dark:text-white/56">
165
+ <For each={props.legalLinks.filter((link) => !(props.surface === "sheet" && props.profile && link.href === "/me"))}>
166
+ {(link) => (
167
+ <a
168
+ onClick={follow}
169
+ href={link.href}
170
+ class="inline-flex items-center gap-1.5 rounded-md px-2 py-1 transition-colors hover:text-primary dark:hover:text-white"
171
+ >
172
+ <i class={link.icon ?? "ti ti-file-text"} />
173
+ {link.label}
174
+ </a>
175
+ )}
176
+ </For>
177
+ </div>
178
+ </Show>
179
+ </Dynamic>
180
+ );
181
+ };
@@ -0,0 +1,12 @@
1
+ import { onMount } from "solid-js";
2
+ import { browserNotificationClient } from "../browser/notifications";
3
+
4
+ export default function BrowserPushRegistration() {
5
+ onMount(() => {
6
+ void browserNotificationClient.refreshExisting().catch((error) => {
7
+ console.warn("[notifications] Failed to refresh the browser endpoint", error);
8
+ });
9
+ });
10
+
11
+ return null;
12
+ }
@@ -1,35 +1,182 @@
1
- import { dialogCore, useLocale } from "@k2b/ui";
1
+ import { type DialogRender, dialogCore, toast, useLocale } from "@k2b/ui";
2
+ import { query } from "@k2b/stdlib/solid";
3
+ import { resolveCommand } from "../capabilities/client";
4
+ import { COMMANDS_CHANGED, collectContextAwareCommands } from "../browser/command-bridge";
5
+ import { contextCommandsWithShortcuts } from "../browser/command-shortcuts";
6
+ import { openCommand, runContextAwareCommand } from "../browser/commands";
7
+ import { loadSearchCommands, type PaletteCommand } from "../browser/search-commands";
8
+ import { createEffect, createSignal, For, onCleanup, onMount } from "solid-js";
2
9
  import type { SearchItem } from "../api/search/schemas";
3
10
  import CloudResourceSearch from "../browser/CloudResourceSearch";
4
- import { type GlobalSearchHelpApp, openGlobalSearchHelpDialog } from "./GlobalSearchHelpDialog";
11
+ import type { NavigationSearchItem } from "../browser/navigation-search";
12
+ import { resourceSearchDialogOptions } from "../browser/resource-search-dialog";
13
+ import { resourceSearchMessages } from "../browser/resource-search-messages";
14
+ import { type GlobalSearchOptions, requestSearchNavigation } from "../browser/search-bridge";
15
+ import { attachSpotlightPosition } from "../browser/spotlight-position";
5
16
 
6
17
  type GlobalSearchDialogProps = {
18
+ request?: GlobalSearchOptions;
7
19
  close: () => void;
8
- helpApps: GlobalSearchHelpApp[];
20
+ context?: Parameters<DialogRender<void>>[1];
21
+ searchLinks?: NavigationSearchItem[];
22
+ searchResources?: boolean;
9
23
  };
10
24
 
11
25
  export default function GlobalSearchDialog(props: GlobalSearchDialogProps) {
12
26
  const locale = useLocale();
13
- const openHelp = () => {
27
+ const messages = () => resourceSearchMessages.resolve([locale()]).t;
28
+ const [contextCommands, setContextCommands] = createSignal<PaletteCommand[]>([]);
29
+ const updateContextCommands = () => setContextCommands(contextCommandsWithShortcuts().map((command) => ({ ...command, context: true })));
30
+ const commandsQuery = query.create({
31
+ source: locale,
32
+ enabled: () => props.searchResources !== false,
33
+ load: (locale, { abortSignal }) => loadSearchCommands(locale, abortSignal),
34
+ });
35
+ onMount(() => {
36
+ window.addEventListener(COMMANDS_CHANGED, updateContextCommands);
37
+ updateContextCommands();
38
+ onCleanup(() => window.removeEventListener(COMMANDS_CHANGED, updateContextCommands));
39
+ });
40
+ const runCommand = async (command: PaletteCommand, newTab: boolean) => {
41
+ if (navigating()) return;
42
+ // Check live ownership once more; a background panel may have closed since rendering.
43
+ if (
44
+ command.context &&
45
+ !collectContextAwareCommands().some((current) => current.id === command.id && current.action === command.action)
46
+ ) {
47
+ updateContextCommands();
48
+ toast.error(messages().commandUnavailable);
49
+ return;
50
+ }
51
+ const target = command.action;
52
+ if (typeof target !== "function" && "search" in target) {
53
+ if (!newTab) await runContextAwareCommand(command);
54
+ return;
55
+ }
56
+ setNavigating(true);
57
+ setNavigationError(false);
58
+ if (newTab && typeof target !== "function") {
59
+ // Reserve synchronously inside the user gesture; never lose it to popup blocking.
60
+ const tab = window.open("about:blank", "_blank");
61
+ if (tab) tab.opener = null;
62
+ try {
63
+ if (!tab) throw new Error("Popup blocked");
64
+ const result = await resolveCommand(target.command, target.input, target.options);
65
+ if (!result.ok) throw new Error(result.error.message);
66
+ tab.location.replace(result.data.href);
67
+ } catch {
68
+ tab?.close();
69
+ if (active) setNavigationError(true);
70
+ } finally {
71
+ if (active) setNavigating(false);
72
+ }
73
+ return;
74
+ }
14
75
  props.close();
15
- queueMicrotask(() => openGlobalSearchHelpDialog(props.helpApps, locale()));
76
+ try {
77
+ if (command.context) await runContextAwareCommand(command);
78
+ else if (typeof target === "function") await target();
79
+ else await openCommand(target.command, target.input, target.options);
80
+ } catch (error) {
81
+ toast.error(error instanceof Error && error.message ? error.message : messages().commandFailed);
82
+ }
16
83
  };
17
-
18
- const openItem = (item: SearchItem) => {
19
- props.close();
20
- window.location.href = item.href;
84
+ let host!: HTMLDivElement;
85
+ onMount(() => {
86
+ if (props.context) onCleanup(attachSpotlightPosition(host, props.context, messages));
87
+ });
88
+ const openInNewTab = (item: SearchItem) => {
89
+ window.open(item.href, "_blank", "noopener,noreferrer");
21
90
  };
22
-
23
- return <CloudResourceSearch helpApps={props.helpApps} onHelp={openHelp} onSelect={openItem} />;
91
+ const [navigating, setNavigating] = createSignal(false);
92
+ const [navigationError, setNavigationError] = createSignal(false);
93
+ createEffect(() => {
94
+ props.request;
95
+ setNavigationError(false);
96
+ });
97
+ let active = true;
98
+ onCleanup(() => {
99
+ active = false;
100
+ });
101
+ const openItem = async (item: SearchItem) => {
102
+ if (navigating()) return;
103
+ setNavigating(true);
104
+ setNavigationError(false);
105
+ try {
106
+ const handled = await requestSearchNavigation({
107
+ href: item.href,
108
+ ...(item.ref.type === "cloud.navigation" ? {} : { ref: item.ref }),
109
+ });
110
+ if (!active) return;
111
+ props.close();
112
+ if (!handled) window.location.assign(item.href);
113
+ } catch {
114
+ if (active) setNavigationError(true);
115
+ } finally {
116
+ if (active) setNavigating(false);
117
+ }
118
+ };
119
+ return (
120
+ <div ref={host} class="cloud-global-search">
121
+ <CloudResourceSearch
122
+ commands={[
123
+ ...contextCommands(),
124
+ ...(commandsQuery.data() ?? []).filter((command) => !contextCommands().some((context) => context.id === command.id)),
125
+ ]}
126
+ commandsLoading={commandsQuery.loading()}
127
+ commandsError={Boolean(commandsQuery.error())}
128
+ onCommand={props.searchResources === false ? undefined : (command, newTab) => void runCommand(command, newTab)}
129
+ request={props.request}
130
+ disabled={navigating()}
131
+ navigationError={navigationError() ? messages().navigationFailed : undefined}
132
+ navigationItems={props.searchLinks}
133
+ searchResources={props.searchResources}
134
+ onClose={props.close}
135
+ onSelect={(item) => void openItem(item)}
136
+ onOpenInNewTab={openInNewTab}
137
+ />
138
+ <For each={["top", "right", "bottom", "left"]}>{(edge) => <div aria-hidden="true" data-search-grip={edge} />}</For>
139
+ </div>
140
+ );
24
141
  }
25
142
 
26
- export const openGlobalSearchDialog = (helpApps: GlobalSearchHelpApp[] = []) => {
27
- if (dialogCore.isOpen()) return;
28
-
29
- void dialogCore.open<void>((close) => <GlobalSearchDialog close={close} helpApps={helpApps} />, {
30
- panelClassName:
31
- "fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 m-0 h-[var(--spotlight-dialog-height)] w-[min(var(--ui-dialog-available-width),72rem)] [--spotlight-dialog-height:min(50vh,var(--ui-dialog-available-height))] overflow-hidden overscroll-y-contain rounded-2xl border-0 bg-white/92 p-0 text-zinc-900 shadow-xl ring-1 ring-inset ring-zinc-300/60 backdrop:bg-black/35 backdrop:backdrop-blur-sm dark:bg-zinc-950/92 dark:text-zinc-100 dark:ring-zinc-700/60 [@media(min-height:1100px)]:[--spotlight-dialog-height:min(33vh,var(--ui-dialog-available-height))]",
32
- contentClassName: "h-full min-h-0",
33
- initialFocus: "none",
34
- });
143
+ /** Owned by the layout island; neither dialog state nor callbacks cross SSR props. */
144
+ export const createGlobalSearchHost = (searchLinks: () => NavigationSearchItem[], searchResources = true) => {
145
+ const [request, setRequest] = createSignal<GlobalSearchOptions>({});
146
+ let controller: AbortController | undefined;
147
+ return {
148
+ open: (options: GlobalSearchOptions) => {
149
+ setRequest({ ...options });
150
+ if (controller) {
151
+ document.querySelector<HTMLInputElement>(".cloud-global-search input[role=combobox]")?.focus();
152
+ return;
153
+ }
154
+ const current = new AbortController();
155
+ controller = current;
156
+ const locale = document.documentElement.lang || "en";
157
+ void dialogCore
158
+ .open<void>(
159
+ (close, context) => (
160
+ <GlobalSearchDialog
161
+ request={request()}
162
+ searchLinks={searchLinks()}
163
+ searchResources={searchResources}
164
+ close={close}
165
+ context={context}
166
+ />
167
+ ),
168
+ { ...resourceSearchDialogOptions(resourceSearchMessages.resolve([locale]).t.searchCloudResources), signal: current.signal },
169
+ )
170
+ .catch(() => {
171
+ toast.error(resourceSearchMessages.resolve([locale]).t.searchFailed);
172
+ })
173
+ .finally(() => {
174
+ if (controller === current) controller = undefined;
175
+ });
176
+ },
177
+ dispose: () => {
178
+ controller?.abort();
179
+ controller = undefined;
180
+ },
181
+ };
35
182
  };
@@ -1,38 +1,54 @@
1
- import { hotkeys } from "@k2b/stdlib/solid";
1
+ import { registerContextAwareCommand, runContextAwareCommand } from "../browser/commands";
2
+ import { attachCommandShortcuts } from "../browser/command-shortcuts";
2
3
  import { IconButton, Tooltip, useLocale } from "@k2b/ui";
3
- import { openGlobalSearchDialog } from "./GlobalSearchDialog";
4
- import type { GlobalSearchHelpApp } from "./GlobalSearchHelpDialog";
4
+ import { createEffect, onCleanup, onMount } from "solid-js";
5
+ import type { NavigationSearchItem } from "../browser/navigation-search";
6
+ import { openGlobalSearch } from "../browser/search";
7
+ import { registerGlobalSearchHost } from "../browser/search-bridge";
8
+ import { createGlobalSearchHost } from "./GlobalSearchDialog";
5
9
  import { platformMessages } from "./platform-messages";
6
10
 
7
11
  type GlobalSearchTriggerProps = {
8
- variant: "header" | "rail";
12
+ variant: "header" | "rail" | "host";
9
13
  class?: string;
10
- registerHotkey?: boolean;
11
- searchHelpApps?: GlobalSearchHelpApp[];
14
+ registerCommand?: boolean;
15
+ searchLinks?: NavigationSearchItem[];
16
+ searchResources?: boolean;
12
17
  };
13
18
 
14
19
  /** Opens the spotlight-style global search dialog from nav/header trigger points. */
15
20
  export default function GlobalSearchTrigger(props: GlobalSearchTriggerProps) {
16
21
  const locale = useLocale();
17
22
  const t = () => platformMessages.resolve([locale()]).t;
18
- const searchHelpApps = props.searchHelpApps ?? [];
19
23
 
20
- if (props.registerHotkey) {
21
- hotkeys.create(() => ({
22
- "mod+k": {
23
- label: t().openGlobalSearch,
24
- desc: t().globalSearchDescription,
25
- run: () => openGlobalSearchDialog(searchHelpApps),
26
- },
27
- }));
24
+ if (props.registerCommand) {
25
+ const host = createGlobalSearchHost(() => props.searchLinks ?? [], props.searchResources);
26
+ onMount(() => {
27
+ onCleanup(registerGlobalSearchHost(host.open, host.dispose));
28
+ });
29
+ onMount(() => onCleanup(attachCommandShortcuts((command) => void runContextAwareCommand(command))));
30
+ createEffect(() =>
31
+ onCleanup(
32
+ registerContextAwareCommand({
33
+ id: "cloud.search",
34
+ title: t().openGlobalSearch,
35
+ description: t().globalSearchDescription,
36
+ icon: "ti ti-search",
37
+ shortcut: "mod+k",
38
+ action: { search: {} },
39
+ }),
40
+ ),
41
+ );
28
42
  }
29
43
 
44
+ if (props.variant === "host") return null;
45
+
30
46
  if (props.variant === "rail")
31
47
  return (
32
48
  <Tooltip.Trigger
33
49
  type="button"
34
50
  class={`rail-item text-blue-500 hover:bg-blue-500/10 hover:text-blue-600 dark:text-blue-400 dark:hover:bg-blue-500/15 dark:hover:text-blue-300 ${props.class ?? ""}`}
35
- onClick={() => openGlobalSearchDialog(searchHelpApps)}
51
+ onClick={() => openGlobalSearch()}
36
52
  aria-label={t().openGlobalSearch}
37
53
  placement="right"
38
54
  delay={0}
@@ -43,12 +59,7 @@ export default function GlobalSearchTrigger(props: GlobalSearchTriggerProps) {
43
59
  );
44
60
 
45
61
  return (
46
- <IconButton
47
- class={props.class}
48
- onClick={() => openGlobalSearchDialog(searchHelpApps)}
49
- label={t().openGlobalSearch}
50
- title={t().searchShortcut}
51
- >
62
+ <IconButton class={props.class} onClick={() => openGlobalSearch()} label={t().openGlobalSearch} title={t().searchShortcut}>
52
63
  <i class="ti ti-search text-base" />
53
64
  </IconButton>
54
65
  );
@@ -1,3 +1,5 @@
1
+ import { getLayoutHelp } from "./help";
2
+ import { navigationSearchItems } from "../browser/navigation-search";
1
3
  import type { DateContext } from "@k2b/stdlib";
2
4
  import { AppWorkspace, appWorkspaceLayoutStyle, LocaleProvider, NoticeCard } from "@k2b/ui";
3
5
  import type { JSX } from "solid-js/jsx-runtime";
@@ -7,13 +9,13 @@ import { defaultRailPreferences } from "../contracts/rail-preferences";
7
9
  import { hasRole, type User } from "../contracts/shared";
8
10
  import { getLocale } from "../server/locale";
9
11
  import { getDateConfig } from "../server/time";
10
- import { dates, resolveHelpManifest } from "../shared";
12
+ import { dates } from "../shared";
11
13
  import { readThemeFromCookieHeader } from "../shared/theme";
12
14
  import type { AppLaunchpadApp } from "./AppLaunchpad.island";
13
15
  import AppWorkspaceController from "./AppWorkspaceController.island";
14
16
  import { appAppearanceStyle, resolveCurrentApp } from "./app-appearance";
15
17
  import { visibleNavigationApps } from "./app-navigation";
16
- import BrowserNotifications from "./BrowserNotifications.island";
18
+ import BrowserPushRegistration from "./BrowserPushRegistration.island";
17
19
  import GlobalAnnouncements from "./GlobalAnnouncements.island";
18
20
  import type { GlobalSearchHelpApp } from "./GlobalSearchHelpDialog";
19
21
  import LayoutFooter from "./LayoutFooter";
@@ -155,7 +157,7 @@ export default function Layout(props: LayoutProps) {
155
157
  const user = c.get("user");
156
158
  const pathname = new URL(c.req.raw.url).pathname;
157
159
  const currentApp = resolveCurrentApp(runtime.apps, pathname);
158
- const registeredHelp = currentApp?.help ? resolveHelpManifest(currentApp.help, lang) : undefined;
160
+ const registeredHelp = getLayoutHelp(c);
159
161
  const workspaceLayout = resolveAppWorkspaceLayoutForSidebar(
160
162
  readAppWorkspaceLayoutCookie(cookie, currentApp?.id),
161
163
  workspaceSidebarCollapsible,
@@ -170,6 +172,10 @@ export default function Layout(props: LayoutProps) {
170
172
  description: app.description,
171
173
  accent: app.accent,
172
174
  }));
175
+ const searchLinks = navigationSearchItems(
176
+ runtime.apps,
177
+ allApps.map((app) => app.id),
178
+ );
173
179
  const searchHelpApps: GlobalSearchHelpApp[] = runtime.apps
174
180
  .filter((app) => (app.searchTags?.length ?? 0) > 0)
175
181
  .map((app) => ({
@@ -203,7 +209,7 @@ export default function Layout(props: LayoutProps) {
203
209
  user?.avatarHash && user.id
204
210
  ? `/api/accounts/users/${encodeURIComponent(user.id)}/avatar?rev=${encodeURIComponent(user.avatarHash)}`
205
211
  : undefined;
206
- const mainLayoutClass = fullPage || fullWidth ? "flex flex-col" : "md:overflow-auto";
212
+ const mainLayoutClass = fullPage || fullWidth ? "flex flex-col" : "lg:overflow-auto";
207
213
  const canvasStyle =
208
214
  [appAppearanceStyle(currentApp?.appearance), appWorkspaceLayoutStyle(workspaceLayout), focusMode && flushCanvas ? "padding:0" : ""]
209
215
  .filter(Boolean)
@@ -215,6 +221,7 @@ export default function Layout(props: LayoutProps) {
215
221
  class="cloud-app-canvas flex h-dvh w-full overflow-hidden"
216
222
  style={canvasStyle}
217
223
  data-app-id={currentApp?.id}
224
+ data-layout-full-page="true"
218
225
  data-workspace-sidebar-collapsed={workspaceLayout?.sidebarCollapsed ? "true" : undefined}
219
226
  >
220
227
  <TimezoneCookie />
@@ -229,16 +236,17 @@ export default function Layout(props: LayoutProps) {
229
236
  return (
230
237
  <LocaleProvider locale={lang}>
231
238
  <div
232
- class={`cloud-app-canvas relative flex w-full ${fullPage ? "h-dvh overflow-hidden" : "min-h-screen md:h-screen md:overflow-hidden"}`}
239
+ class={`cloud-app-canvas relative flex w-full ${fullPage ? "h-dvh overflow-hidden" : "min-h-screen lg:h-screen lg:overflow-hidden"}`}
233
240
  style={canvasStyle}
234
241
  data-app-id={currentApp?.id}
242
+ data-layout-full-page={fullPage ? "true" : undefined}
235
243
  data-layout-authenticated={showRail ? "true" : undefined}
236
244
  data-workspace-sidebar-collapsed={workspaceLayout?.sidebarCollapsed ? "true" : undefined}
237
245
  >
238
246
  <TimezoneCookie />
239
247
  {registeredHelp && <RegisteredHelpDocuments documents={registeredHelp.documents} pageBase={registeredHelp.pageBase} />}
240
248
  <AppWorkspaceController appId={currentApp?.id} />
241
- {user && <BrowserNotifications userId={user.id} />}
249
+ {user && <BrowserPushRegistration />}
242
250
  {showRail && (
243
251
  <LayoutRail
244
252
  accent={currentApp?.appearance?.accent}
@@ -253,11 +261,13 @@ export default function Layout(props: LayoutProps) {
253
261
  profileAvatarSrc={profileAvatarSrc}
254
262
  profileName={profileName}
255
263
  searchHelpApps={searchHelpApps}
264
+ searchLinks={searchLinks}
256
265
  theme={theme}
257
266
  />
258
267
  )}
259
268
  <div class="layout-shell-content flex min-h-0 min-w-0 flex-1 flex-col">
260
269
  <LayoutHeader
270
+ appLabel={currentApp?.name ?? (pathname.startsWith("/admin") ? t.admin : undefined)}
261
271
  accent={currentApp?.appearance?.accent}
262
272
  authenticated={showRail}
263
273
  breadcrumbs={breadcrumbs}
@@ -268,6 +278,7 @@ export default function Layout(props: LayoutProps) {
268
278
  profileAvatarSrc={profileAvatarSrc}
269
279
  profileName={profileName}
270
280
  searchHelpApps={searchHelpApps}
281
+ searchLinks={searchLinks}
271
282
  signInLabel={t.signIn}
272
283
  theme={theme}
273
284
  />
@@ -3,12 +3,10 @@ import { LAYOUT_UPDATE_EVENT, type LayoutBreadcrumb, type LayoutUpdate } from ".
3
3
 
4
4
  type Props = {
5
5
  breadcrumbs: LayoutBreadcrumb[];
6
- mobile?: boolean;
7
6
  };
8
7
 
9
8
  export default function LayoutBreadcrumbs(props: Props) {
10
9
  const [breadcrumbs, setBreadcrumbs] = createSignal(props.breadcrumbs);
11
- const visibleBreadcrumbs = () => (props.mobile ? breadcrumbs().slice(-1) : breadcrumbs());
12
10
 
13
11
  onMount(() => {
14
12
  const onUpdate = (event: Event) => {
@@ -21,10 +19,10 @@ export default function LayoutBreadcrumbs(props: Props) {
21
19
  });
22
20
 
23
21
  return (
24
- <nav class="flex items-center gap-1 sm:gap-2 min-w-0 text-sm md:text-xs">
25
- <For each={visibleBreadcrumbs()}>
22
+ <nav class="flex items-center gap-1 sm:gap-2 min-w-0 text-xs">
23
+ <For each={breadcrumbs()}>
26
24
  {(crumb, i) => {
27
- const isLast = () => i() === visibleBreadcrumbs().length - 1;
25
+ const isLast = () => i() === breadcrumbs().length - 1;
28
26
  return (
29
27
  <>
30
28
  {i() > 0 && <span class="text-zinc-400 dark:text-zinc-600 text-xs">/</span>}
@@ -5,7 +5,7 @@ type LayoutFooterProps = {
5
5
 
6
6
  export default function LayoutFooter(props: LayoutFooterProps) {
7
7
  return (
8
- <footer class="hidden shrink-0 flex-wrap items-center justify-center gap-x-4 gap-y-1 px-3 py-2 text-xs text-dimmed md:flex">
8
+ <footer class="hidden shrink-0 flex-wrap items-center justify-center gap-x-4 gap-y-1 px-3 py-2 text-xs text-dimmed lg:flex">
9
9
  {props.legalLinks.map((link) => (
10
10
  <a href={link.href} class="flex items-center gap-1 transition-colors hover:text-primary">
11
11
  {link.icon && <i class={`${link.icon} text-xs`} />}