@k2b/cloud 0.30.0 → 0.32.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 (171) hide show
  1. package/package.json +7 -4
  2. package/src/_internal/app-presentation.ts +22 -1
  3. package/src/_internal/capabilities.ts +2 -2
  4. package/src/_internal/define-app.ts +17 -5
  5. package/src/_internal/registry.ts +50 -6
  6. package/src/_internal/widget-declarations.ts +53 -0
  7. package/src/ai/app-skills.ts +1 -7
  8. package/src/ai/approval-routes.ts +40 -4
  9. package/src/ai/approvals.ts +68 -10
  10. package/src/ai/audio-tool.ts +2 -1
  11. package/src/ai/browser.ts +109 -0
  12. package/src/ai/capabilities.ts +6 -2
  13. package/src/ai/chart-block.ts +431 -81
  14. package/src/ai/chat/action-sentences.ts +13 -7
  15. package/src/ai/chat/blocks.tsx +64 -39
  16. package/src/ai/chat/builtin-tools.tsx +9 -7
  17. package/src/ai/chat/chart-block.tsx +218 -44
  18. package/src/ai/chat/explorer-block.tsx +276 -0
  19. package/src/ai/chat/file-tools.tsx +40 -1
  20. package/src/ai/chat/live-turn.browser-harness.tsx +4 -1
  21. package/src/ai/chat/message-actions.tsx +9 -1
  22. package/src/ai/chat/message-utils.ts +7 -0
  23. package/src/ai/chat/messages.ts +34 -0
  24. package/src/ai/chat/presentation.tsx +13 -3
  25. package/src/ai/chat/reference-text.tsx +34 -0
  26. package/src/ai/chat/tool-groups.ts +67 -12
  27. package/src/ai/chat/tool-names.ts +18 -2
  28. package/src/ai/chat/turn-layout.ts +8 -0
  29. package/src/ai/chat/turn-view.tsx +111 -32
  30. package/src/ai/chat/view-block.browser-harness.tsx +35 -0
  31. package/src/ai/chat/view-block.tsx +417 -0
  32. package/src/ai/chat/visual-tools.tsx +32 -53
  33. package/src/ai/chat/web-tools.tsx +95 -1
  34. package/src/ai/client/controller.ts +6 -3
  35. package/src/ai/client-tool-results.ts +9 -0
  36. package/src/ai/code-runtime-tools.ts +50 -17
  37. package/src/ai/code-source-contracts.ts +25 -21
  38. package/src/ai/code-source-tools.ts +3 -1
  39. package/src/ai/default-tool-contracts.ts +334 -91
  40. package/src/ai/default-tools.ts +87 -11
  41. package/src/ai/executor.ts +343 -45
  42. package/src/ai/fetch-file-tool.ts +41 -4
  43. package/src/ai/file-error.ts +2 -0
  44. package/src/ai/file-tools.ts +27 -7
  45. package/src/ai/files-store.ts +60 -0
  46. package/src/ai/firecrawl-tools.ts +87 -4
  47. package/src/ai/html-pdf-tool.ts +3 -2
  48. package/src/ai/http.ts +37 -12
  49. package/src/ai/index.ts +98 -0
  50. package/src/ai/local-tool-contracts.ts +244 -0
  51. package/src/ai/malformed-tool-call-repair.ts +144 -0
  52. package/src/ai/markdown-pdf-tool.ts +2 -1
  53. package/src/ai/migrate.ts +120 -2
  54. package/src/ai/openai-api-messages.ts +53 -0
  55. package/src/ai/openai-api-slots.ts +40 -0
  56. package/src/ai/openai-api.ts +383 -0
  57. package/src/ai/protocol.ts +28 -3
  58. package/src/ai/provider.ts +16 -4
  59. package/src/ai/request-context.ts +33 -0
  60. package/src/ai/routes.ts +99 -49
  61. package/src/ai/runtime.ts +90 -15
  62. package/src/ai/settings.ts +5 -0
  63. package/src/ai/skill-catalog.ts +20 -5
  64. package/src/ai/skill-format.ts +17 -7
  65. package/src/ai/skill-tool.ts +15 -5
  66. package/src/ai/skills-routes.ts +20 -6
  67. package/src/ai/skills.ts +26 -7
  68. package/src/ai/store.ts +91 -1
  69. package/src/ai/system-prompt.ts +69 -10
  70. package/src/ai/timeline.ts +3 -1
  71. package/src/ai/tool-audit.ts +11 -1
  72. package/src/ai/tools.ts +94 -3
  73. package/src/ai/types.ts +48 -0
  74. package/src/ai/view-block.ts +941 -0
  75. package/src/ai/vision-tool.ts +2 -1
  76. package/src/ai/web-provenance.ts +103 -0
  77. package/src/api/admin-identity.ts +6 -1
  78. package/src/api/app-approval-actions.ts +36 -0
  79. package/src/api/app-approval.ts +10 -2
  80. package/src/api/approvals-loader.ts +15 -0
  81. package/src/api/approvals.ts +120 -0
  82. package/src/api/capabilities.ts +3 -2
  83. package/src/api/index.ts +3 -0
  84. package/src/api/me.ts +12 -3
  85. package/src/api/widgets.ts +36 -5
  86. package/src/approvals/Approvals.browser-harness.tsx +16 -0
  87. package/src/approvals/Approvals.tsx +621 -0
  88. package/src/approvals/ApprovalsNavStatus.tsx +20 -0
  89. package/src/approvals/messages.ts +95 -0
  90. package/src/approvals/ui.ts +8 -0
  91. package/src/browser/CloudResourceSearch.tsx +1 -1
  92. package/src/browser/app-approval.ts +32 -0
  93. package/src/browser/choose-files.tsx +26 -13
  94. package/src/browser/command-shortcuts.ts +21 -6
  95. package/src/browser/commands.ts +13 -4
  96. package/src/browser/file-provider-error.ts +11 -0
  97. package/src/browser/file-providers.ts +3 -12
  98. package/src/browser/files.tsx +1 -2
  99. package/src/browser/live-connection.ts +2 -3
  100. package/src/browser/provider-list.ts +2 -1
  101. package/src/browser/save-files.tsx +13 -2
  102. package/src/browser/spotlight-position.ts +4 -1
  103. package/src/browser/widgets.ts +7 -5
  104. package/src/capabilities/executions.ts +3 -0
  105. package/src/cli/account.ts +21 -2
  106. package/src/cli/commands.ts +24 -2
  107. package/src/cli/index.ts +16 -0
  108. package/src/cli/plugin.ts +30 -0
  109. package/src/contracts/app-approval.ts +120 -0
  110. package/src/contracts/app.ts +34 -2
  111. package/src/contracts/approval-paths.ts +4 -0
  112. package/src/contracts/approvals.ts +81 -0
  113. package/src/contracts/command-path.ts +17 -0
  114. package/src/contracts/commands.ts +5 -15
  115. package/src/contracts/registry.ts +9 -1
  116. package/src/contracts/shared.ts +5 -0
  117. package/src/contracts/widgets.ts +47 -2
  118. package/src/desktop/solid.tsx +6 -7
  119. package/src/events/live-protocol.ts +2 -1
  120. package/src/events/live-server-message.ts +40 -0
  121. package/src/events/testing.ts +88 -0
  122. package/src/server/index.ts +3 -0
  123. package/src/server/middleware/auth.ts +11 -1
  124. package/src/server/request-timeout.ts +22 -0
  125. package/src/server/services/access.ts +4 -3
  126. package/src/server/widget-request.ts +27 -0
  127. package/src/services/app-approval.ts +103 -15
  128. package/src/services/approvals/cursor.ts +15 -0
  129. package/src/services/approvals/device.ts +112 -0
  130. package/src/services/approvals/index.ts +241 -0
  131. package/src/services/approvals/list.ts +120 -0
  132. package/src/services/approvals/migrate.ts +41 -0
  133. package/src/services/identity/models-only-auth.ts +25 -0
  134. package/src/services/notifications/email-frame.ts +16 -3
  135. package/src/services/service-account-credentials.ts +22 -10
  136. package/src/services/settings/core-settings.ts +27 -0
  137. package/src/shared/ai-platform-prompt.ts +26 -1
  138. package/src/shared/app-presentation.ts +11 -0
  139. package/src/shared/email-html.ts +46 -7
  140. package/src/shared/index.ts +6 -1
  141. package/src/shared/markdown/extensions/code.ts +14 -0
  142. package/src/shared/markdown/index.ts +4 -4
  143. package/src/ssr/AdminLayout.tsx +4 -7
  144. package/src/ssr/AppBadge.tsx +4 -4
  145. package/src/ssr/AppLaunchpad.island.tsx +1 -1
  146. package/src/ssr/AppLaunchpadPanel.tsx +4 -4
  147. package/src/ssr/Layout.tsx +38 -14
  148. package/src/ssr/LayoutFooter.tsx +1 -1
  149. package/src/ssr/LayoutHeader.tsx +17 -20
  150. package/src/ssr/LayoutHelp.tsx +1 -1
  151. package/src/ssr/LayoutRail.tsx +1 -2
  152. package/src/ssr/PageTrail.tsx +23 -0
  153. package/src/ssr/PageTrailRow.island.tsx +20 -0
  154. package/src/ssr/PageTrailRuntime.island.tsx +26 -0
  155. package/src/ssr/ProfilePreferences.island.tsx +7 -3
  156. package/src/ssr/RailApps.island.tsx +2 -2
  157. package/src/ssr/app-badges.ts +50 -6
  158. package/src/ssr/islands/index.ts +1 -0
  159. package/src/ssr/layout-runtime.ts +60 -0
  160. package/src/ssr/mobile-menu-history.ts +1 -1
  161. package/src/ssr/page-trail-runtime.ts +313 -0
  162. package/src/ssr/page-trail.browser-harness.tsx +265 -0
  163. package/src/ssr/page-trail.ts +59 -0
  164. package/src/ssr/platform-messages.ts +4 -0
  165. package/src/ssr/rail-navigation.ts +5 -3
  166. package/src/ssr/runtime.ts +6 -1
  167. package/src/styles/approvals.css +390 -0
  168. package/src/styles/effects.css +21 -4
  169. package/src/styles/global.css +39 -3
  170. package/src/styles/utilities-layout.css +24 -1
  171. package/src/styles/utilities-navigation.css +6 -18
@@ -0,0 +1,265 @@
1
+ import { AppWorkspace } from "@k2b/ui";
2
+ import { Hono } from "hono";
3
+ import type { JSX } from "solid-js";
4
+ import { defineApp } from "../_internal/define-app";
5
+ import { person } from "../_internal/define-app.test-fixture";
6
+ import Layout from "./Layout";
7
+ import PageTrail from "./PageTrail";
8
+
9
+ // Pages for `page-trail.browser.test.ts`: Accounts-like list and detail pages
10
+ // in a workspace, a Files-like folder at any depth, a page without a
11
+ // workspace, and a third-party app that draws its own header. All names are
12
+ // invented.
13
+
14
+ const probe = defineApp({
15
+ id: "page-trail-probe",
16
+ name: "Page trail probe",
17
+ icon: "ti ti-route",
18
+ description: "Page trail probe",
19
+ baseUrl: "http://page-trail-probe:3000",
20
+ routes: ["/app/accounts"],
21
+ });
22
+ const apps = [
23
+ {
24
+ id: "accounts",
25
+ name: "Accounts",
26
+ icon: "ti ti-users",
27
+ description: "People",
28
+ routes: ["/app/accounts"],
29
+ nav: { href: "/app/accounts", section: "primary" },
30
+ },
31
+ {
32
+ id: "files",
33
+ name: "Files",
34
+ icon: "ti ti-files",
35
+ description: "Files",
36
+ routes: ["/app/files"],
37
+ nav: { href: "/app/files", section: "primary" },
38
+ },
39
+ // A third-party app: it draws its own header and places the trail there.
40
+ {
41
+ id: "timetrack",
42
+ name: "Time",
43
+ icon: "ti ti-clock",
44
+ description: "Time tracking",
45
+ routes: ["/app/timetrack"],
46
+ nav: { href: "/app/timetrack", section: "primary" },
47
+ },
48
+ { id: "core", name: "Account", icon: "ti ti-user", description: "Account", routes: ["/me"], nav: { href: "/me", section: "hidden" } },
49
+ ];
50
+ type LayoutContext = Parameters<typeof Layout>[0]["c"];
51
+ type Title = { title: string; href?: string }[];
52
+
53
+ const start = { title: "Start", href: "/" };
54
+ const accountsRoot = { title: "Accounts", href: "/app/accounts" };
55
+ export const users = Array.from({ length: 60 }, (_, index) => `user${String(index + 1).padStart(2, "0")}`);
56
+ export const folders = [
57
+ "Projects for the new community centre",
58
+ "2026 budget year with the second construction phase",
59
+ "Quotes from all invited suppliers",
60
+ "Supplier A, Hartmann and Sons, Bremen branch",
61
+ "Questions about the foundation",
62
+ ];
63
+
64
+ const Page = (props: { c: unknown; title: Title; children: JSX.Element }) => (
65
+ <Layout c={props.c as LayoutContext} fullWidth fullPage title={props.title}>
66
+ {props.children}
67
+ </Layout>
68
+ );
69
+ const AccountsWorkspace = (props: { children: JSX.Element; active?: boolean }) => (
70
+ <AppWorkspace mobileSurface="flush" class="min-h-0 flex-1">
71
+ <AppWorkspace.Sidebar id="accounts-navigation" label="Accounts">
72
+ <AppWorkspace.SidebarDesktop>
73
+ <AppWorkspace.SidebarItem href="/app/accounts/users" icon="ti ti-user" active={props.active ?? true}>
74
+ Users
75
+ </AppWorkspace.SidebarItem>
76
+ </AppWorkspace.SidebarDesktop>
77
+ </AppWorkspace.Sidebar>
78
+ <AppWorkspace.Content>
79
+ <AppWorkspace.Main scrollPreserveKey="accounts-users">{props.children}</AppWorkspace.Main>
80
+ </AppWorkspace.Content>
81
+ </AppWorkspace>
82
+ );
83
+ const folderHref = (depth: number) => `/app/files?path=${encodeURIComponent(folders.slice(0, depth).join("/"))}`;
84
+ const filesTitle = (depth: number): Title => [
85
+ { title: "Files", href: "/app/files" },
86
+ ...folders.slice(0, depth - 1).map((name, index) => ({ title: name, href: folderHref(index + 1) })),
87
+ { title: folders[depth - 1]! },
88
+ ];
89
+ /** A document in the editor, as Files names it: its way up is its folder with the document's row selected. */
90
+ const editorTitle = (depth: number, file: string): Title => [
91
+ ...filesTitle(depth).slice(0, -1),
92
+ { title: folders[depth - 1]!, href: `${folderHref(depth)}&file=${encodeURIComponent(file)}` },
93
+ { title: file.split("/").at(-1)! },
94
+ ];
95
+
96
+ export const server = new Hono()
97
+ .use(async (c, next) => {
98
+ c.set("runtime" as never, { apps } as never);
99
+ c.set("user" as never, person as never);
100
+ await next();
101
+ })
102
+ .get(
103
+ "/app/accounts/users",
104
+ ...probe.ssr((c) => {
105
+ const group = c.req.query("group");
106
+ const shown = group ? users.filter((_, index) => index % 2 === 0) : users;
107
+ return () => (
108
+ <Page c={c} title={[start, accountsRoot, { title: "Users" }]}>
109
+ <AccountsWorkspace>
110
+ <h1 class="p-3 font-semibold">{group ? `Users in ${group}` : "Users"}</h1>
111
+ {/* A region that scrolls sideways once its content arrives, as a wide table that renders late does. */}
112
+ <div data-scroll-preserve="accounts-wide" class="accounts-wide" style="width:300px;overflow:auto">
113
+ <div class="accounts-wide-content" style="width:300px;height:8px" />
114
+ </div>
115
+ <table class="w-full">
116
+ <tbody>
117
+ {shown.map((uid) => (
118
+ <tr style="height:48px">
119
+ <td>
120
+ <a href={`/app/accounts/users/${uid}${group ? `?group=${group}` : ""}`}>{uid}</a>
121
+ </td>
122
+ </tr>
123
+ ))}
124
+ </tbody>
125
+ </table>
126
+ </AccountsWorkspace>
127
+ </Page>
128
+ );
129
+ }),
130
+ )
131
+ .get(
132
+ "/app/accounts/users/:uid",
133
+ ...probe.ssr((c) => {
134
+ const uid = c.req.param("uid") ?? "";
135
+ return () => (
136
+ <Page c={c} title={[start, accountsRoot, { title: "Users", href: "/app/accounts/users" }, { title: uid }]}>
137
+ <AccountsWorkspace active={false}>
138
+ <h1 class="p-3">User Seven</h1>
139
+ <p class="p-3">Profile of {uid}</p>
140
+ <input aria-label="Note" class="m-3" />
141
+ </AccountsWorkspace>
142
+ </Page>
143
+ );
144
+ }),
145
+ )
146
+ .get(
147
+ "/app/accounts/teams",
148
+ ...probe.ssr((c) => () => (
149
+ <Page c={c} title={[start, accountsRoot, { title: "Directory", href: "/app/accounts/directory" }, { title: "Teams" }]}>
150
+ <AppWorkspace mobileSurface="flush" class="min-h-0 flex-1">
151
+ <AppWorkspace.Sidebar label="Directory">
152
+ <AppWorkspace.SidebarDesktop>
153
+ <AppWorkspace.SidebarItem href="/app/accounts/teams" active>
154
+ Teams
155
+ </AppWorkspace.SidebarItem>
156
+ </AppWorkspace.SidebarDesktop>
157
+ </AppWorkspace.Sidebar>
158
+ <AppWorkspace.Content>
159
+ <AppWorkspace.Main>
160
+ <input aria-label="Search teams" placeholder="Search Teams…" />
161
+ </AppWorkspace.Main>
162
+ </AppWorkspace.Content>
163
+ </AppWorkspace>
164
+ </Page>
165
+ )),
166
+ )
167
+ .get(
168
+ "/app/accounts/teams/engineering",
169
+ ...probe.ssr((c) => () => (
170
+ <Page c={c} title={[start, accountsRoot, { title: "Teams", href: "/app/accounts/teams" }, { title: "Engineering" }]}>
171
+ <AppWorkspace mobileSurface="flush" class="min-h-0 flex-1">
172
+ <AppWorkspace.Sidebar label="Directory">
173
+ <AppWorkspace.SidebarDesktop>
174
+ <AppWorkspace.SidebarItem href="/app/accounts/teams" active="ancestor">
175
+ Teams
176
+ </AppWorkspace.SidebarItem>
177
+ </AppWorkspace.SidebarDesktop>
178
+ </AppWorkspace.Sidebar>
179
+ <AppWorkspace.Content>
180
+ <AppWorkspace.Main>Team members</AppWorkspace.Main>
181
+ </AppWorkspace.Content>
182
+ </AppWorkspace>
183
+ </Page>
184
+ )),
185
+ )
186
+ .get(
187
+ "/app/accounts/directory",
188
+ ...probe.ssr((c) => () => (
189
+ <Page c={c} title={[{ title: "Directory" }]}>
190
+ <h1>Directory</h1>
191
+ </Page>
192
+ )),
193
+ )
194
+ .get(
195
+ "/app/files",
196
+ ...probe.ssr((c) => {
197
+ const depth = (c.req.query("path") ?? "").split("/").filter(Boolean).length || 1;
198
+ const file = c.req.query("view") === "edit" ? c.req.query("file") : undefined;
199
+ return () => (
200
+ <Page c={c} title={file ? editorTitle(depth, file) : filesTitle(depth)}>
201
+ <AppWorkspace mobileSurface="flush" class="min-h-0 flex-1">
202
+ <AppWorkspace.Sidebar id="files-navigation" label="Storage">
203
+ <AppWorkspace.SidebarDesktop>
204
+ <AppWorkspace.SidebarItem href="/app/files" icon="ti ti-folder">
205
+ My files
206
+ </AppWorkspace.SidebarItem>
207
+ </AppWorkspace.SidebarDesktop>
208
+ </AppWorkspace.Sidebar>
209
+ <AppWorkspace.Content>
210
+ <AppWorkspace.Main>Folder contents</AppWorkspace.Main>
211
+ <AppWorkspace.Detail id="file-detail" open={c.req.query("detail") === "1"} width="lg">
212
+ Details
213
+ </AppWorkspace.Detail>
214
+ </AppWorkspace.Content>
215
+ </AppWorkspace>
216
+ </Page>
217
+ );
218
+ }),
219
+ )
220
+ .get(
221
+ "/me/notifications/history",
222
+ ...probe.ssr((c) => () => (
223
+ <Page
224
+ c={c}
225
+ title={[
226
+ start,
227
+ { title: "Account", href: "/me" },
228
+ { title: "Notifications", href: "/me/notifications" },
229
+ { title: "Delivery history" },
230
+ ]}
231
+ >
232
+ <div class="p-3">
233
+ <h1 class="text-xl font-semibold">Delivery history</h1>
234
+ <p>Every message sent to you.</p>
235
+ </div>
236
+ </Page>
237
+ )),
238
+ )
239
+ .get(
240
+ "/app/timetrack/entries/:id",
241
+ ...probe.ssr((c) => () => (
242
+ <Page
243
+ c={c}
244
+ title={[
245
+ start,
246
+ { title: "Time", href: "/app/timetrack" },
247
+ { title: "Website relaunch", href: "/app/timetrack/projects/web" },
248
+ { title: "Homepage: agree on drafts" },
249
+ ]}
250
+ >
251
+ <div class="flex min-h-0 flex-1 flex-col">
252
+ <header class="timetrack-head flex min-h-14 items-center gap-2 px-3">
253
+ <PageTrail subtitle="2:15 h" />
254
+ <button type="button" class="ml-auto">
255
+ Start timer
256
+ </button>
257
+ </header>
258
+ <dl class="p-3">
259
+ <dt>Project</dt>
260
+ <dd>Website relaunch</dd>
261
+ </dl>
262
+ </div>
263
+ </Page>
264
+ )),
265
+ );
@@ -0,0 +1,59 @@
1
+ import type { PageTrailLink, PageTrailState } from "@k2b/ui";
2
+ import type { LayoutBreadcrumb } from "./layout-runtime";
3
+
4
+ /** `path` without trailing slashes, in one pass: a regular expression would backtrack on long runs of `/`. */
5
+ const withoutTrailingSlashes = (path: string): string => {
6
+ let end = path.length;
7
+ while (end > 0 && path[end - 1] === "/") end--;
8
+ return path.slice(0, end);
9
+ };
10
+
11
+ /** Path and query of a same-origin link, without a trailing slash; the hash names no other page. */
12
+ const pageOf = (href: string): string => {
13
+ const hash = href.indexOf("#");
14
+ const withoutHash = hash >= 0 ? href.slice(0, hash) : href;
15
+ const queryStart = withoutHash.indexOf("?");
16
+ const path = queryStart >= 0 ? withoutHash.slice(0, queryStart) : withoutHash;
17
+ const query = queryStart >= 0 ? withoutHash.slice(queryStart) : "";
18
+ return (path.length > 1 ? withoutTrailingSlashes(path) : path) + (query.length > 1 ? query : "");
19
+ };
20
+
21
+ /** The trail without levels that link to `page` itself, by path and query. */
22
+ export const dropSelfLinks = (trail: PageTrailState, page: string): PageTrailState => {
23
+ const self = pageOf(page);
24
+ const ancestors = trail.ancestors.filter((level) => pageOf(level.href) !== self);
25
+ return ancestors.length === trail.ancestors.length ? trail : { ...trail, ancestors };
26
+ };
27
+
28
+ /**
29
+ * The page trail from a page's title array, by one rule for every app.
30
+ *
31
+ * The last entry is the page itself. Above it, Start (`href: "/"`) and the
32
+ * app's own root (`href` equal to its base path) are dropped: the rail already
33
+ * leads there. So is an entry without an `href`, because nobody can go back to
34
+ * it, and one that links to the page itself (`currentPage`, path and query).
35
+ * What remains are the levels below the app root, nearest last; their number
36
+ * alone decides how the shell shows the way up, so an app never chooses a form
37
+ * and every app with the same depth looks the same.
38
+ */
39
+ export const derivePageTrail = (
40
+ title: string | readonly LayoutBreadcrumb[] | undefined,
41
+ appBasePath: string | undefined,
42
+ fallback: string,
43
+ currentPage?: string,
44
+ ): PageTrailState => {
45
+ if (typeof title === "string") return { title, ancestors: [] };
46
+ const current = title?.at(-1);
47
+ if (!title || !current) return { title: fallback, ancestors: [] };
48
+ const base = appBasePath ? pageOf(appBasePath) : undefined;
49
+ const ancestors = title
50
+ .slice(0, -1)
51
+ .filter((entry): entry is PageTrailLink => typeof entry.href === "string" && entry.href.length > 0)
52
+ .filter((entry) => {
53
+ const page = pageOf(entry.href);
54
+ return page !== "/" && page !== "" && page !== base;
55
+ })
56
+ .map((entry) => ({ title: entry.title, href: entry.href }));
57
+ const trail = { title: current.title, ancestors };
58
+ return currentPage ? dropSelfLinks(trail, currentPage) : trail;
59
+ };
@@ -65,6 +65,8 @@ export const platformMessages = i18n.define({
65
65
  searchShortcut: "Search (⌘/Ctrl+K)",
66
66
  openShortcutHelp: "Open shortcut help",
67
67
  shortcutHelpDescription: "Open help, guides, and keyboard shortcuts.",
68
+ pageUp: ({ page }: { page: string }) => `One level up: ${page}`,
69
+ pageUpDescription: "Go to the page above this one, as you left it in this tab.",
68
70
  openHelp: "Open help",
69
71
  helpShortcut: "Help (Shift+/)",
70
72
  signIn: "Sign in",
@@ -134,6 +136,8 @@ export const platformMessages = i18n.define({
134
136
  searchShortcut: "Suchen (⌘/Strg+K)",
135
137
  openShortcutHelp: "Tastaturhilfe öffnen",
136
138
  shortcutHelpDescription: "Hilfe, Anleitungen und Tastaturkürzel öffnen.",
139
+ pageUp: ({ page }: { page: string }) => `Eine Ebene höher: ${page}`,
140
+ pageUpDescription: "Zur Seite darüber, so wie du sie in diesem Tab verlassen hast.",
137
141
  openHelp: "Hilfe öffnen",
138
142
  helpShortcut: "Hilfe (Umschalt+/)",
139
143
  signIn: "Anmelden",
@@ -12,7 +12,7 @@ export type RailApp = {
12
12
  /** The app's `nav.badge` endpoint. */
13
13
  badge?: string;
14
14
  };
15
- export type RailLink = Omit<RailApp, "defaultVisible"> & { exact?: boolean };
15
+ export type RailLink = Omit<RailApp, "defaultVisible"> & { appId?: string; exact?: boolean };
16
16
 
17
17
  export const isRailAppVisible = (app: RailApp, settings: RailPreferences): boolean =>
18
18
  Object.hasOwn(settings.visibility, app.id) ? (settings.visibility[app.id] ?? app.defaultVisible) : app.defaultVisible;
@@ -32,7 +32,7 @@ export const projectRailNavigation = (apps: readonly RailApp[], settings: RailSn
32
32
  const app = catalog.get(shortcut.appId);
33
33
  if (!app || pinned.has(app.id)) continue;
34
34
  pinned.add(app.id);
35
- shortcuts.push({ ...app, id: shortcut.id });
35
+ shortcuts.push({ ...app, id: shortcut.id, appId: app.id });
36
36
  } else {
37
37
  shortcuts.push({
38
38
  id: shortcut.id,
@@ -46,7 +46,9 @@ export const projectRailNavigation = (apps: readonly RailApp[], settings: RailSn
46
46
  }
47
47
  return {
48
48
  shortcuts,
49
- apps: sortRailApps(apps, locale).filter((app) => !pinned.has(app.id) && isRailAppVisible(app, settings)),
49
+ apps: sortRailApps(apps, locale)
50
+ .filter((app) => !pinned.has(app.id) && isRailAppVisible(app, settings))
51
+ .map((app) => ({ ...app, appId: app.id })),
50
52
  };
51
53
  };
52
54
 
@@ -36,9 +36,14 @@ export const localizedAppName = (
36
36
  appId: string,
37
37
  fallback: string,
38
38
  ): string => {
39
+ return appNameIn(carrier, appId, getLocale(carrier as Parameters<typeof getLocale>[0]), fallback);
40
+ };
41
+
42
+ /** An app's registered name in the locale of a recorded request. */
43
+ export const appNameIn = (carrier: RuntimeCarrier, appId: string, locale: string, fallback: string): string => {
39
44
  const runtime = carrier.get("runtime");
40
45
  const apps =
41
46
  runtime && typeof runtime === "object" && Array.isArray((runtime as RuntimeContext).apps) ? (runtime as RuntimeContext).apps : [];
42
47
  const app = apps.find((entry) => entry.id === appId);
43
- return app ? resolveAppPresentation(app, getLocale(carrier as Parameters<typeof getLocale>[0])).name : fallback;
48
+ return app ? resolveAppPresentation(app, locale).name : fallback;
44
49
  };