@k2b/cloud 0.37.0 → 0.39.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 (149) hide show
  1. package/package.json +8 -3
  2. package/scripts/build-canvas-workers.ts +109 -29
  3. package/scripts/build.ts +3 -5
  4. package/src/_internal/app-path.ts +25 -0
  5. package/src/_internal/app-presentation.ts +4 -0
  6. package/src/_internal/canvas-worker.ts +5 -3
  7. package/src/_internal/define-app.ts +28 -2
  8. package/src/_internal/help-catalog.ts +123 -4
  9. package/src/_internal/help.ts +27 -6
  10. package/src/_internal/panel-cookie.ts +39 -0
  11. package/src/_internal/registry-validation.ts +21 -26
  12. package/src/_internal/runtime-context.ts +1 -0
  13. package/src/access/PermissionEditor.browser-harness.tsx +57 -38
  14. package/src/access/PermissionEditor.tsx +30 -5
  15. package/src/access/ResourceApiKeys.tsx +5 -1
  16. package/src/account/Avatar.tsx +20 -2
  17. package/src/account/ui.ts +1 -1
  18. package/src/ai/approval-routes.ts +5 -2
  19. package/src/ai/approvals.ts +139 -28
  20. package/src/ai/browser.ts +11 -0
  21. package/src/ai/chart-block.ts +44 -13
  22. package/src/ai/chat/action-sentences.ts +1 -44
  23. package/src/ai/chat/chart-block.tsx +2 -11
  24. package/src/ai/chat/messages.ts +10 -0
  25. package/src/ai/chat/turn-view.tsx +35 -3
  26. package/src/ai/chat/view-block.tsx +7 -2
  27. package/src/ai/chat/web-tools.tsx +23 -6
  28. package/src/ai/chat-task-contracts.ts +28 -3
  29. package/src/ai/chat-task-reasons-migrate.ts +68 -0
  30. package/src/ai/chat-task-reasons.ts +92 -0
  31. package/src/ai/chat-tasks.ts +460 -66
  32. package/src/ai/default-tool-contracts.ts +30 -13
  33. package/src/ai/fetch-file-tool.ts +37 -3
  34. package/src/ai/index.ts +12 -0
  35. package/src/ai/migrate.ts +46 -3
  36. package/src/ai/readable-html.ts +1057 -0
  37. package/src/ai/request-context.ts +7 -1
  38. package/src/ai/routes.ts +27 -1
  39. package/src/ai/runtime.ts +12 -27
  40. package/src/ai/store.ts +5 -3
  41. package/src/ai/timeline.ts +9 -1
  42. package/src/ai/tool-wording.ts +45 -0
  43. package/src/ai/turn-abort.ts +33 -0
  44. package/src/ai/types.ts +12 -1
  45. package/src/api/admin-core-settings.ts +190 -139
  46. package/src/api/admin-outgoing-mail.ts +4 -2
  47. package/src/api/app-approval.ts +9 -2
  48. package/src/api/approvals.ts +1 -1
  49. package/src/api/help.ts +23 -4
  50. package/src/api/index.ts +2 -1
  51. package/src/api/mcp.ts +8 -2
  52. package/src/browser/CloudResourceSearch.tsx +10 -6
  53. package/src/browser/FileChooser.tsx +11 -3
  54. package/src/browser/app-approval.ts +7 -2
  55. package/src/browser/choose-files.browser-harness.tsx +4 -1
  56. package/src/browser/composer-catalog.ts +103 -0
  57. package/src/browser/composer-insert-messages.ts +31 -0
  58. package/src/browser/composer-insert.ts +150 -0
  59. package/src/browser/file-chooser-messages.ts +4 -0
  60. package/src/browser/file-providers.ts +14 -1
  61. package/src/browser/panel-messages.ts +24 -0
  62. package/src/browser/panel-slot.ts +354 -0
  63. package/src/browser/panel.ts +28 -0
  64. package/src/browser/provider-folder.tsx +58 -5
  65. package/src/browser/resource-picker.tsx +17 -2
  66. package/src/cli/admin/workflows.ts +1 -0
  67. package/src/config/env.ts +14 -0
  68. package/src/contracts/app-approval.ts +49 -9
  69. package/src/contracts/app.ts +14 -0
  70. package/src/contracts/file-provider.ts +48 -1
  71. package/src/contracts/outgoing-mail.ts +8 -1
  72. package/src/contracts/registry.ts +3 -1
  73. package/src/contracts/shared.ts +2 -0
  74. package/src/server/help.ts +136 -10
  75. package/src/server/services/access.ts +5 -1
  76. package/src/server/services/geo.ts +21 -4
  77. package/src/services/accounts/posix.ts +2 -1
  78. package/src/services/allowed-network-fetch.ts +189 -0
  79. package/src/services/app-approval.ts +158 -38
  80. package/src/services/approvals/device.ts +2 -0
  81. package/src/services/help/index.ts +46 -5
  82. package/src/services/help/store.ts +26 -5
  83. package/src/services/help/types.ts +10 -2
  84. package/src/services/index.ts +22 -3
  85. package/src/services/notifications/browser.ts +16 -5
  86. package/src/services/outgoing-mail/dispatcher.ts +7 -2
  87. package/src/services/outgoing-mail/resolve-host.ts +39 -0
  88. package/src/services/outgoing-mail/test-send.ts +18 -3
  89. package/src/services/outgoing-mail/transport.ts +24 -13
  90. package/src/services/pdf/image-protocol.ts +135 -0
  91. package/src/services/pdf/image-variants.ts +124 -0
  92. package/src/services/pdf/image-worker.ts +31 -0
  93. package/src/services/pdf/image.ts +109 -0
  94. package/src/services/pdf/index.ts +9 -0
  95. package/src/services/settings/api.ts +11 -6
  96. package/src/services/settings/app.ts +5 -4
  97. package/src/services/settings/core-settings.ts +4 -3
  98. package/src/services/settings/index.ts +67 -43
  99. package/src/services/settings/namespace.ts +2 -1
  100. package/src/services/settings/store.ts +83 -11
  101. package/src/shared/ai-platform-prompt.ts +1 -1
  102. package/src/shared/app-presentation.ts +1 -0
  103. package/src/shared/help.ts +41 -0
  104. package/src/shared/index.ts +9 -1
  105. package/src/shared/markdown/extensions/guided-help.ts +208 -64
  106. package/src/shared/markdown/formula.ts +8 -8
  107. package/src/shared/markdown/index.ts +72 -11
  108. package/src/shared/redirect.ts +12 -0
  109. package/src/ssr/CloudShell.island.tsx +86 -9
  110. package/src/ssr/Layout.tsx +62 -11
  111. package/src/ssr/LayoutHeader.tsx +15 -9
  112. package/src/ssr/LayoutHelp.browser-harness.tsx +130 -27
  113. package/src/ssr/LayoutHelp.tsx +569 -410
  114. package/src/ssr/LayoutRail.tsx +2 -2
  115. package/src/ssr/LayoutTabBar.tsx +29 -0
  116. package/src/ssr/PanelLayout.tsx +27 -0
  117. package/src/ssr/PanelRuntime.island.tsx +87 -0
  118. package/src/ssr/PanelSlot.tsx +108 -0
  119. package/src/ssr/PanelTrigger.tsx +49 -0
  120. package/src/ssr/ShellRuntime.island.tsx +72 -25
  121. package/src/ssr/app-appearance.ts +3 -2
  122. package/src/ssr/app-color.ts +100 -0
  123. package/src/ssr/app-launchpad.tsx +18 -1
  124. package/src/ssr/app-navigation.ts +15 -0
  125. package/src/ssr/compact-shell.baseline.json +5270 -0
  126. package/src/ssr/compact-shell.browser-harness.ts +431 -0
  127. package/src/ssr/help-messages.ts +60 -38
  128. package/src/ssr/help.ts +3 -0
  129. package/src/ssr/index.ts +3 -0
  130. package/src/ssr/layout-help-article.ts +41 -0
  131. package/src/ssr/layout-help-navigation.ts +0 -12
  132. package/src/ssr/layout-help-registry.tsx +4 -11
  133. package/src/ssr/layout-help-search.ts +9 -12
  134. package/src/ssr/layout-help-start.ts +68 -0
  135. package/src/ssr/panel-slot.browser-harness.tsx +110 -0
  136. package/src/ssr/platform-messages.ts +34 -0
  137. package/src/ssr/rail-navigation.ts +44 -0
  138. package/src/ssr/shell-data.tsx +11 -0
  139. package/src/ssr/shell-shortcuts.ts +19 -0
  140. package/src/styles/effects.css +689 -185
  141. package/src/styles/global.css +269 -55
  142. package/src/styles/panel.css +373 -0
  143. package/src/styles/resource-search.css +25 -0
  144. package/src/styles/tokens.css +23 -6
  145. package/src/styles/utilities-navigation.css +5 -21
  146. package/src/workflows/store/events.ts +24 -0
  147. package/src/workflows/store/index.ts +8 -0
  148. package/src/workflows/store/observability.ts +6 -1
  149. package/src/workflows/store/retention.ts +157 -0
@@ -1,19 +1,7 @@
1
- import type { HelpDocumentManifest } from "@k2b/cloud/shared";
2
-
3
1
  type HelpArticleSession = { articleScrollTop: number };
4
2
  type HelpArticleScroller = { scrollTop: number };
5
3
  type HelpArticleHeading = { focus: (options?: FocusOptions) => void };
6
4
 
7
- export const adjacentHelpDocuments = (documents: readonly HelpDocumentManifest[], activeId: string | null) => {
8
- const index = documents.findIndex((document) => document.id === activeId);
9
- if (index < 0) return { previous: null, next: null };
10
-
11
- return {
12
- previous: documents[index - 1] ?? null,
13
- next: documents[index + 1] ?? null,
14
- };
15
- };
16
-
17
5
  export const resetHelpArticleScroll = (session: HelpArticleSession, scroller?: HelpArticleScroller) => {
18
6
  session.articleScrollTop = 0;
19
7
  if (scroller) scroller.scrollTop = 0;
@@ -1,20 +1,13 @@
1
1
  import type { HelpDocumentManifest } from "@k2b/cloud/shared";
2
- import { type JSX, onCleanup, onMount } from "solid-js";
3
-
4
- type HelpTopicBase = {
5
- id: string;
6
- title: string;
7
- icon?: string;
8
- description?: string;
9
- order?: number;
10
- };
2
+ import { onCleanup, onMount } from "solid-js";
11
3
 
12
4
  export type LayoutHelpDocumentsProps = {
13
5
  documents: readonly HelpDocumentManifest[];
14
6
  /** Canonical standalone Help route owned by the current app. */
15
7
  pageBase: string;
16
8
  };
17
- export type HelpTopic = (HelpTopicBase & { kind: "content"; children: JSX.Element }) | (HelpDocumentManifest & { kind: "document" });
9
+ /** One article of the current app's Help, as the page registered it. */
10
+ export type HelpTopic = HelpDocumentManifest;
18
11
 
19
12
  export const HELP_TOPICS_EVENT = "cloud:layout-help-topics";
20
13
 
@@ -50,7 +43,7 @@ export function LayoutHelpDocuments(props: LayoutHelpDocumentsProps) {
50
43
  // Cleanup runs while this unmounts, when a parent `<Show>` may already have dropped the props' source.
51
44
  const pageBase = props.pageBase;
52
45
  window.__cloudLayoutHelpPageBase = pageBase;
53
- const disposers = props.documents.map((document) => registerTopic({ ...document, kind: "document" }));
46
+ const disposers = props.documents.map((document) => registerTopic(document));
54
47
  onCleanup(() => {
55
48
  disposers.forEach((dispose) => dispose());
56
49
  if (window.__cloudLayoutHelpPageBase === pageBase) delete window.__cloudLayoutHelpPageBase;
@@ -1,15 +1,12 @@
1
- type SearchTopic = { id: string; title: string; description?: string; kind: "document" | "content" };
1
+ type SearchTopic = { id: string; title: string; description?: string };
2
2
 
3
- /** Use server ranking for documents; metadata remains usable during loading or outages. */
3
+ /** Use server ranking for articles; metadata matches remain usable during loading or outages. */
4
4
  export const searchHelpTopics = <T extends SearchTopic>(topics: readonly T[], query: string, remoteIds: readonly string[] | null): T[] => {
5
- const local = topics.filter((topic) => [topic.title, topic.description].some((part) => part?.toLocaleLowerCase().includes(query)));
6
- if (remoteIds === null) return local;
7
- const documents = new Map(topics.filter((topic) => topic.kind === "document").map((topic) => [topic.id, topic]));
8
- return [
9
- ...remoteIds.flatMap((id) => {
10
- const topic = documents.get(id);
11
- return topic ? [topic] : [];
12
- }),
13
- ...local.filter((topic) => topic.kind === "content"),
14
- ];
5
+ if (remoteIds === null)
6
+ return topics.filter((topic) => [topic.title, topic.description].some((part) => part?.toLocaleLowerCase().includes(query)));
7
+ const byId = new Map(topics.map((topic) => [topic.id, topic]));
8
+ return remoteIds.flatMap((id) => {
9
+ const topic = byId.get(id);
10
+ return topic ? [topic] : [];
11
+ });
15
12
  };
@@ -0,0 +1,68 @@
1
+ import type { HelpKind } from "../shared/help";
2
+ import { SHELL_SHORTCUT_ORDER } from "./shell-shortcuts";
3
+
4
+ /** The concept asks for at most eight tasks as tiles; more task articles follow as a list. */
5
+ export const HELP_START_TILE_LIMIT = 8;
6
+ /** "For this view" shows two or three articles the page names. */
7
+ export const HELP_VIEW_LIMIT = 3;
8
+ export const HELP_START_GROUP_KINDS = ["start", "reference", "troubleshoot"] as const;
9
+ export type HelpStartGroupKind = (typeof HELP_START_GROUP_KINDS)[number];
10
+ export type HelpStartTopic = { id: string; title: string; order?: number; kind?: HelpKind };
11
+ export type HelpStart<T> = {
12
+ /** Declared ids in declared order: only ids of known topics, without duplicates, at most HELP_VIEW_LIMIT. */
13
+ view: T[];
14
+ /** Articles of kind task (missing kind = task), by order (missing = 100) then title, at most HELP_START_TILE_LIMIT. */
15
+ tiles: T[];
16
+ /** The remaining task articles in the same order, so no task becomes unreachable. */
17
+ moreTasks: T[];
18
+ /** Fixed order start, reference, troubleshoot; a kind without articles is absent. Each list sorted like tiles. */
19
+ groups: { kind: HelpStartGroupKind; topics: T[] }[];
20
+ };
21
+
22
+ export const helpStart = <T extends HelpStartTopic>(topics: readonly T[], viewIds: readonly string[] = []): HelpStart<T> => {
23
+ const byId = new Map(topics.map((topic) => [topic.id, topic]));
24
+ const view = [...new Set(viewIds)]
25
+ .flatMap((id) => {
26
+ const topic = byId.get(id);
27
+ return topic ? [topic] : [];
28
+ })
29
+ .slice(0, HELP_VIEW_LIMIT);
30
+ const sorted = [...topics].sort((a, b) => (a.order ?? 100) - (b.order ?? 100) || a.title.localeCompare(b.title));
31
+ const tasks = sorted.filter((topic) => (topic.kind ?? "task") === "task");
32
+ return {
33
+ view,
34
+ tiles: tasks.slice(0, HELP_START_TILE_LIMIT),
35
+ moreTasks: tasks.slice(HELP_START_TILE_LIMIT),
36
+ groups: HELP_START_GROUP_KINDS.flatMap((kind) => {
37
+ const topics = sorted.filter((topic) => topic.kind === kind);
38
+ return topics.length ? [{ kind, topics }] : [];
39
+ }),
40
+ };
41
+ };
42
+
43
+ export type HelpShortcutScope = "everywhere" | "app" | "selection";
44
+ export type HelpShortcutCommand = { id: string; title: string; description?: string; shortcut?: string; scope?: "selection" | "page" };
45
+ /**
46
+ * Group commands with a shortcut: scope "selection" -> "selection"; id starting with "cloud." -> "everywhere"; the rest -> "app".
47
+ * Group order everywhere, app, selection; empty groups are absent. Inside "everywhere", the ids of SHELL_SHORTCUT_ORDER come first in that
48
+ * order (so rows rendered on the server keep their place when the browser adds more), then the rest in input order; other groups keep input order.
49
+ * Commands without a shortcut are dropped.
50
+ */
51
+ export const helpShortcutGroups = <T extends HelpShortcutCommand>(
52
+ commands: readonly T[],
53
+ ): { scope: HelpShortcutScope; shortcuts: (T & { shortcut: string })[] }[] => {
54
+ const groups: Record<HelpShortcutScope, (T & { shortcut: string })[]> = { everywhere: [], app: [], selection: [] };
55
+ for (const command of commands) {
56
+ if (!command.shortcut) continue;
57
+ const scope = command.scope === "selection" ? "selection" : command.id.startsWith("cloud.") ? "everywhere" : "app";
58
+ groups[scope].push({ ...command, shortcut: command.shortcut });
59
+ }
60
+ const rank = (id: string) => {
61
+ const index = SHELL_SHORTCUT_ORDER.findIndex((shellId) => shellId === id);
62
+ return index < 0 ? SHELL_SHORTCUT_ORDER.length : index;
63
+ };
64
+ groups.everywhere.sort((a, b) => rank(a.id) - rank(b.id));
65
+ return (["everywhere", "app", "selection"] as const).flatMap((scope) =>
66
+ groups[scope].length ? [{ scope, shortcuts: groups[scope] }] : [],
67
+ );
68
+ };
@@ -0,0 +1,110 @@
1
+ import { AppWorkspace } from "@k2b/ui";
2
+ import { Hono } from "hono";
3
+ import { defineApp } from "../_internal/define-app";
4
+ import { person } from "../_internal/define-app.test-fixture";
5
+ import type { RuntimeAppMeta } from "../contracts/app";
6
+ import type { User } from "../contracts/shared";
7
+ import Layout from "./Layout";
8
+ import PanelLayout from "./PanelLayout";
9
+ import type { RuntimeContext } from "./runtime";
10
+
11
+ const apps: RuntimeAppMeta[] = ["fixture", "second"].map((id) => ({
12
+ id,
13
+ name: id === "fixture" ? "Fixture" : "Second",
14
+ icon: "ti ti-box",
15
+ description: "Panel browser fixture",
16
+ routes: [`/app/${id}`],
17
+ nav: { href: `/app/${id}`, section: "primary" },
18
+ panel: { href: `/app/${id}/panel`, label: id === "fixture" ? "Fixture" : "Second", icon: "ti ti-box" },
19
+ }));
20
+ const probe = defineApp({
21
+ id: "fixture",
22
+ name: "Fixture",
23
+ icon: "ti ti-box",
24
+ description: "Panel browser fixture",
25
+ baseUrl: "http://fixture:3000",
26
+ routes: ["/app/fixture", "/app/second"],
27
+ });
28
+ type Environment = { Variables: { runtime: RuntimeContext; user: User } };
29
+ export const server = new Hono<Environment>()
30
+ .use(async (c, next) => {
31
+ c.set("runtime", { apps });
32
+ c.set("user", person);
33
+ await next();
34
+ })
35
+ .get(
36
+ "/app/:provider/panel",
37
+ ...probe.ssr<Environment>((c) => () => (
38
+ <PanelLayout c={c} title="Fixture panel">
39
+ <label>
40
+ Panel input
41
+ <input aria-label="Panel input" />
42
+ </label>
43
+ <button type="button" id="prevent-escape">
44
+ Keep Escape
45
+ </button>
46
+ <a href="/app/fixture/next" id="leave-panel">
47
+ Open in the app
48
+ </a>
49
+ </PanelLayout>
50
+ )),
51
+ )
52
+ .get(
53
+ "/app/fixture/long",
54
+ ...probe.ssr<Environment>((c) => () => (
55
+ <Layout c={c} title="Long fixture">
56
+ <button type="button" id="open-panel">
57
+ Open fixture
58
+ </button>
59
+ <button type="button" id="open-silent-panel">
60
+ Open silent fixture
61
+ </button>
62
+ <div style="height:150vh">Long content</div>
63
+ <p id="last-row">Last row</p>
64
+ </Layout>
65
+ )),
66
+ )
67
+ .get(
68
+ "/app/fixture/:page?",
69
+ ...probe.ssr<Environment>((c) => () => (
70
+ <Layout c={c} fullWidth fullPage title="Panel fixture">
71
+ <AppWorkspace mobileSurface="flush" class="min-h-0 flex-1">
72
+ <AppWorkspace.Sidebar id="panel-navigation" label="Navigation">
73
+ <AppWorkspace.SidebarDesktop>
74
+ <AppWorkspace.SidebarItem href="/app/fixture" active>
75
+ Workspace
76
+ </AppWorkspace.SidebarItem>
77
+ </AppWorkspace.SidebarDesktop>
78
+ </AppWorkspace.Sidebar>
79
+ <AppWorkspace.Content>
80
+ <AppWorkspace.Main>
81
+ <button type="button" id="open-panel">
82
+ Open fixture
83
+ </button>
84
+ <button type="button" id="open-silent-panel">
85
+ Open silent fixture
86
+ </button>
87
+ <a href="/app/fixture/next" id="next-page">
88
+ Next page
89
+ </a>
90
+ <textarea aria-label="App editor" />
91
+ </AppWorkspace.Main>
92
+ <AppWorkspace.Detail id="panel-fixture-detail" width="lg" open>
93
+ <h2>Selected item</h2>
94
+ <button type="button" data-workspace-detail-close>
95
+ Close detail
96
+ </button>
97
+ </AppWorkspace.Detail>
98
+ </AppWorkspace.Content>
99
+ </AppWorkspace>
100
+ </Layout>
101
+ )),
102
+ );
103
+
104
+ /** Runs inline during parsing and again immediately after mounting the real runtime. */
105
+ export function panelGeometry() {
106
+ return [".layout-shell-content", "#cloud-panel", ".k2b-app-workspace"].map((selector) => {
107
+ const rect = document.querySelector(selector)?.getBoundingClientRect();
108
+ return rect && { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
109
+ });
110
+ }
@@ -4,6 +4,23 @@ export const platformMessages = i18n.define({
4
4
  baseLocale: "en",
5
5
  messages: {
6
6
  en: {
7
+ panel: "Panel",
8
+ panels: "Panels",
9
+ openPanel: "Open panel",
10
+ closePanel: "Close panel",
11
+ togglePanel: "Open or close the panel",
12
+ togglePanelDescription: "Shows a panel beside the app.",
13
+ panelShortcut: ({ mod }: { mod: string }) => `Panel (${mod}J)`,
14
+ resizePanel: "Resize panel",
15
+ releaseClosesPanel: "Release to close the panel",
16
+ panelLoadFailed: "The panel did not load",
17
+ panelNotResponding: ({ label }: { label: string }) => `${label} is not responding.`,
18
+ retry: "Try again",
19
+ panelLoading: ({ label }: { label: string }) => `${label} is loading`,
20
+ enlargePanel: "Enlarge panel",
21
+ shrinkPanel: "Shrink panel",
22
+ printFallbackTitle: "This view does not print well.",
23
+ printFallbackHint: "Open the item you want to print, or use the app's export.",
7
24
  admin: "Admin",
8
25
  platformAdministration: "Platform administration.",
9
26
  start: "Start",
@@ -76,6 +93,23 @@ export const platformMessages = i18n.define({
76
93
  clearSearch: "Clear search",
77
94
  },
78
95
  de: {
96
+ panel: "Panel",
97
+ panels: "Panels",
98
+ openPanel: "Panel öffnen",
99
+ closePanel: "Panel schließen",
100
+ togglePanel: "Panel öffnen oder schließen",
101
+ togglePanelDescription: "Zeigt ein Panel neben der App.",
102
+ panelShortcut: ({ mod }: { mod: string }) => `Panel (${mod}J)`,
103
+ resizePanel: "Panelbreite ändern",
104
+ releaseClosesPanel: "Loslassen schließt das Panel",
105
+ panelLoadFailed: "Das Panel lädt nicht",
106
+ panelNotResponding: ({ label }: { label: string }) => `${label} antwortet nicht.`,
107
+ retry: "Erneut versuchen",
108
+ panelLoading: ({ label }: { label: string }) => `${label} lädt`,
109
+ enlargePanel: "Panel vergrößern",
110
+ shrinkPanel: "Panel verkleinern",
111
+ printFallbackTitle: "Diese Ansicht lässt sich nicht gut drucken.",
112
+ printFallbackHint: "Öffne den Eintrag, den du drucken möchtest, oder nutze den Export der App.",
79
113
  admin: "Administration",
80
114
  platformAdministration: "Plattform verwalten.",
81
115
  start: "Start",
@@ -1,3 +1,4 @@
1
+ import type { TabBarItem } from "@k2b/ui";
1
2
  import type { RailPreferences, RailSnapshot } from "../contracts/rail-preferences";
2
3
  import { helpPageAppId } from "./layout-help-url";
3
4
 
@@ -66,3 +67,46 @@ export const railLinkActive = (link: RailLink, currentPath: string): boolean =>
66
67
  (target.pathname !== "/" && current.pathname.startsWith(`${target.pathname.replace(/\/$/, "")}/`))
67
68
  );
68
69
  };
70
+
71
+ /**
72
+ * The phone bar's apps: the rail's links in the person's order, at most `limit`. The current page's link stays in the
73
+ * bar; when it comes later in the rail, it takes the last place, so the bar always marks where the person is.
74
+ */
75
+ export const phoneBarLinks = (navigation: { shortcuts: RailLink[]; apps: RailLink[] }, currentPath: string, limit: number): RailLink[] => {
76
+ const links = [...navigation.shortcuts, ...navigation.apps];
77
+ const current = links.findIndex((link) => railLinkActive(link, currentPath));
78
+ if (current < limit) return links.slice(0, limit);
79
+ return [...links.slice(0, limit - 1), links[current]!];
80
+ };
81
+
82
+ /** Start and four apps fill the phone bar's five places. */
83
+ const PHONE_BAR_APPS = 4;
84
+
85
+ const underPath = (pathname: string, path: string) => pathname === path || pathname.startsWith(`${path.replace(/\/$/, "")}/`);
86
+
87
+ /**
88
+ * The phone bar: Home, then the person's first apps in their rail order. Home leads to `/`, which opens the
89
+ * installation's start page (`home`), so it is the open item on `/` and on that start page, unless an app in the bar
90
+ * is open there.
91
+ */
92
+ export const phoneBarItems = (
93
+ navigation: { shortcuts: RailLink[]; apps: RailLink[] },
94
+ currentPath: string,
95
+ home: { label: string; path: string },
96
+ ): TabBarItem[] => {
97
+ const links = phoneBarLinks(navigation, currentPath, PHONE_BAR_APPS);
98
+ const items = links.map((link) => ({
99
+ id: link.id,
100
+ label: link.label,
101
+ icon: link.iconClass,
102
+ href: link.href,
103
+ current: railLinkActive(link, currentPath),
104
+ }));
105
+ const pathname = new URL(currentPath, "https://cloud.invalid").pathname;
106
+ const start = new URL(home.path, "https://cloud.invalid").pathname;
107
+ const atHome = pathname === "/" || (!items.some((item) => item.current) && underPath(pathname, start));
108
+ return [
109
+ { id: "home", label: home.label, icon: "ti ti-home", href: "/", current: atHome },
110
+ ...items.map((item) => ({ ...item, current: !atHome && item.current })),
111
+ ];
112
+ };
@@ -1,15 +1,20 @@
1
+ import type { KeyboardPlatform } from "@k2b/ui";
1
2
  import { createContext, type JSX, useContext } from "solid-js";
2
3
  import type { AppSearchLink } from "../contracts/app";
3
4
  import type { RailSnapshot } from "../contracts/rail-preferences";
4
5
  import type { CloudThemePreference } from "../shared/theme";
5
6
  import type { AppLaunchpadLegalLink } from "./app-launchpad";
7
+ import type { PanelProvider } from "./app-navigation";
6
8
  import type { RailApp } from "./rail-navigation";
7
9
 
8
10
  export type ShellData = {
11
+ panel?: { providers: PanelProvider[]; provider?: string };
9
12
  /** The authorized navigation catalog, sorted for the request locale: the rail, the app grid and the phone menu show it. */
10
13
  apps: RailApp[];
11
14
  /** Signed-in pages: the person's rail preferences. */
12
15
  rail?: RailSnapshot;
16
+ /** Signed-in pages: the start page that `/` opens, for the phone bar's Home. */
17
+ home?: string;
13
18
  legalLinks: AppLaunchpadLegalLink[];
14
19
  /** Signed-in pages: the person, for the profile menu and the phone menu's profile actions. */
15
20
  profile?: { name: string; avatarSrc?: string; appHref?: string };
@@ -20,6 +25,12 @@ export type ShellData = {
20
25
  searchLinks: Record<string, readonly AppSearchLink[]>;
21
26
  /** The current app, for links inside its Help. */
22
27
  appId?: string;
28
+ /** The reader's keyboard platform, resolved from the request User-Agent for server-rendered key caps. */
29
+ platform?: KeyboardPlatform;
30
+ /** "For this view" article ids, already filtered for the reader. */
31
+ help?: string[];
32
+ /** The current app's Help metadata could not be read for this request; Help offers a reload. */
33
+ helpFailed?: true;
23
34
  };
24
35
 
25
36
  const ShellDataContext = createContext<() => ShellData>();
@@ -0,0 +1,19 @@
1
+ import type { platformMessages } from "./platform-messages";
2
+
3
+ export const SHELL_SHORTCUT_ORDER = ["cloud.search", "cloud.help", "cloud.panel"] as const;
4
+ /** The shell's always-present shortcuts, without their actions; ShellRuntime adds the actions, Help renders them before the browser reports commands. */
5
+ export const shellShortcuts = (t: ReturnType<typeof platformMessages.resolve>["t"], options: { panel?: boolean } = {}) => [
6
+ { id: "cloud.search", title: t.openGlobalSearch, description: t.globalSearchDescription, icon: "ti ti-search", shortcut: "mod+k" },
7
+ { id: "cloud.help", title: t.openShortcutHelp, description: t.shortcutHelpDescription, icon: "ti ti-help-circle", shortcut: "shift+/" },
8
+ ...(options.panel
9
+ ? [
10
+ {
11
+ id: "cloud.panel",
12
+ title: t.togglePanel,
13
+ description: t.togglePanelDescription,
14
+ icon: "ti ti-layout-sidebar-right",
15
+ shortcut: "mod+j",
16
+ },
17
+ ]
18
+ : []),
19
+ ];