@k2b/cloud 0.40.0 → 0.42.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 (279) hide show
  1. package/package.json +9 -5
  2. package/src/_internal/bounded-json.ts +14 -2
  3. package/src/_internal/capabilities.ts +87 -8
  4. package/src/_internal/capability-reviews.ts +6 -0
  5. package/src/_internal/capability-sentences.ts +2 -1
  6. package/src/_internal/capability-timeout.ts +71 -0
  7. package/src/_internal/define-app.ts +158 -92
  8. package/src/_internal/heartbeat.ts +203 -14
  9. package/src/_internal/http-drain.ts +56 -0
  10. package/src/_internal/registry-validation.ts +3 -0
  11. package/src/_internal/registry.ts +28 -1
  12. package/src/_internal/runtime-context.ts +1 -0
  13. package/src/_internal/setup-lock.ts +103 -0
  14. package/src/_internal/status-preserving-ssr.ts +2 -0
  15. package/src/access/PermissionEditor.tsx +16 -17
  16. package/src/access/PrincipalPicker.tsx +45 -10
  17. package/src/access/messages.ts +8 -0
  18. package/src/access/principal-label.ts +18 -0
  19. package/src/access/ui.ts +1 -0
  20. package/src/account/EntitySearch.tsx +5 -4
  21. package/src/ai/action-review.ts +2 -1
  22. package/src/ai/app-skill-store.ts +1 -1
  23. package/src/ai/capabilities.ts +21 -3
  24. package/src/ai/capability-execution.ts +14 -12
  25. package/src/ai/chat/action-card.ts +76 -4
  26. package/src/ai/chat/action-messages.ts +14 -0
  27. package/src/ai/chat/action-question.browser-harness.tsx +12 -0
  28. package/src/ai/chat/action-question.tsx +214 -23
  29. package/src/ai/chat/action-text.ts +288 -0
  30. package/src/ai/chat/tool-action-words.ts +31 -0
  31. package/src/ai/chat/tool-action.tsx +1 -26
  32. package/src/ai/chat-tasks.ts +2 -1
  33. package/src/ai/client/controller.ts +150 -32
  34. package/src/ai/client/transport.ts +14 -2
  35. package/src/ai/code-capability-routes.ts +27 -23
  36. package/src/ai/code-runtime-tools.ts +14 -16
  37. package/src/ai/code-source-tools.ts +16 -18
  38. package/src/ai/executor.ts +64 -21
  39. package/src/ai/fetch-file-tool.ts +67 -76
  40. package/src/ai/firecrawl-tools.ts +15 -2
  41. package/src/ai/http.ts +1 -0
  42. package/src/ai/index.ts +1 -0
  43. package/src/ai/migrate.ts +28 -0
  44. package/src/ai/open-questions.ts +168 -0
  45. package/src/ai/projects.ts +1 -2
  46. package/src/ai/protocol.ts +2 -2
  47. package/src/ai/question-text.ts +8 -0
  48. package/src/ai/recent-resources.ts +12 -5
  49. package/src/ai/review-content.ts +77 -0
  50. package/src/ai/routes.ts +119 -35
  51. package/src/ai/runtime.ts +1 -0
  52. package/src/ai/skill-tool.ts +17 -6
  53. package/src/ai/skills-routes.ts +72 -27
  54. package/src/ai/skills.ts +139 -60
  55. package/src/ai/store.ts +184 -31
  56. package/src/ai/testing.ts +36 -0
  57. package/src/ai/tool-sentences.ts +152 -34
  58. package/src/ai/tool-wording.ts +12 -6
  59. package/src/ai/tools.ts +8 -3
  60. package/src/ai/types.ts +25 -4
  61. package/src/ai/usage.ts +2 -2
  62. package/src/api/admin-core-settings.ts +15 -2
  63. package/src/api/admin-geo.ts +54 -0
  64. package/src/api/admin-linux-identities.ts +6 -3
  65. package/src/api/admin-outgoing-mail.ts +29 -15
  66. package/src/api/app-approval-actions.ts +31 -7
  67. package/src/api/approvals-loader.ts +3 -3
  68. package/src/api/approvals.ts +51 -0
  69. package/src/api/apps.ts +4 -2
  70. package/src/api/auth/schemas.ts +3 -0
  71. package/src/api/auth.ts +54 -18
  72. package/src/api/capabilities.ts +258 -181
  73. package/src/api/capability-streams.ts +21 -13
  74. package/src/api/cli-plugins.ts +4 -1
  75. package/src/api/geo-glyphs.ts +11 -0
  76. package/src/api/geo.ts +128 -0
  77. package/src/api/help.ts +12 -4
  78. package/src/api/index.ts +5 -0
  79. package/src/api/mcp.ts +73 -41
  80. package/src/api/me-panel-consent.ts +8 -5
  81. package/src/api/me.ts +79 -3
  82. package/src/api/search.ts +21 -14
  83. package/src/api/sync-ops.ts +19 -13
  84. package/src/api/widgets.ts +42 -18
  85. package/src/approvals/Approvals.tsx +30 -66
  86. package/src/approvals/approval-card.ts +68 -0
  87. package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
  88. package/src/browser/CloudResourceSearch.tsx +3 -13
  89. package/src/browser/PanelLeaveNotice.tsx +18 -0
  90. package/src/browser/navigation-search.ts +78 -25
  91. package/src/browser/panel-leave.ts +69 -0
  92. package/src/browser/panel-messages.ts +12 -0
  93. package/src/browser/panel-slot.ts +173 -24
  94. package/src/browser/panel.ts +3 -0
  95. package/src/browser/resource-search-messages.ts +2 -0
  96. package/src/capabilities/client.ts +28 -1
  97. package/src/capabilities/executions.ts +22 -3
  98. package/src/capabilities/geo-client.ts +57 -0
  99. package/src/capabilities/response.ts +2 -1
  100. package/src/capabilities/review-storage.ts +24 -0
  101. package/src/capabilities/reviews.ts +17 -3
  102. package/src/capabilities/server.ts +36 -16
  103. package/src/cli/access.ts +15 -23
  104. package/src/cli/admin/linux.ts +13 -2
  105. package/src/cli/admin/outgoing-mail.ts +26 -4
  106. package/src/cli/capabilities.ts +19 -0
  107. package/src/config/env.ts +0 -6
  108. package/src/contracts/app-access.ts +48 -0
  109. package/src/contracts/app-approval.ts +3 -3
  110. package/src/contracts/app.ts +4 -1
  111. package/src/contracts/approvals.ts +2 -2
  112. package/src/contracts/avatar.ts +186 -0
  113. package/src/contracts/capabilities.ts +149 -55
  114. package/src/contracts/geo-admin.ts +127 -0
  115. package/src/contracts/geo-map.ts +19 -0
  116. package/src/contracts/geo-route.ts +168 -0
  117. package/src/contracts/geo.ts +248 -0
  118. package/src/contracts/index.ts +4 -0
  119. package/src/contracts/outgoing-mail.ts +17 -4
  120. package/src/contracts/registry.ts +2 -0
  121. package/src/contracts/shared.ts +3 -0
  122. package/src/events/app-access.ts +68 -0
  123. package/src/events/live-delivery.ts +128 -0
  124. package/src/events/live.ts +14 -130
  125. package/src/geo/CloudMap.tsx +95 -0
  126. package/src/geo/map-messages.ts +17 -0
  127. package/src/geo/ui.ts +1 -0
  128. package/src/index.ts +2 -0
  129. package/src/server/client-ip.ts +36 -0
  130. package/src/server/index.ts +3 -4
  131. package/src/server/middleware/app-access.ts +312 -0
  132. package/src/server/middleware/auth.ts +5 -3
  133. package/src/server/middleware/invocation.ts +11 -0
  134. package/src/server/middleware/rate-limit.ts +4 -4
  135. package/src/server/services/access.ts +13 -9
  136. package/src/server/services/index.ts +0 -2
  137. package/src/server/services/services.ts +0 -2
  138. package/src/services/accounts/app.ts +23 -3
  139. package/src/services/accounts/posix.ts +21 -7
  140. package/src/services/accounts/users.ts +12 -1
  141. package/src/services/allowed-network-fetch.ts +16 -10
  142. package/src/services/app-access-metrics.ts +24 -0
  143. package/src/services/app-access.ts +123 -0
  144. package/src/services/app-discovery.ts +27 -0
  145. package/src/services/approvals/content.ts +40 -0
  146. package/src/services/approvals/device.ts +49 -33
  147. package/src/services/approvals/index.ts +15 -6
  148. package/src/services/approvals/list.ts +6 -11
  149. package/src/services/approvals/question.ts +72 -0
  150. package/src/services/audit/index.ts +1 -0
  151. package/src/services/auth-flows/ipa.ts +39 -2
  152. package/src/services/auth-flows/password-reset.ts +32 -6
  153. package/src/services/downstream-budget.ts +87 -0
  154. package/src/services/geo/admin.ts +262 -0
  155. package/src/services/geo/bucket.ts +67 -0
  156. package/src/services/geo/cache.ts +57 -0
  157. package/src/services/geo/costs.ts +5 -0
  158. package/src/services/geo/errors.ts +41 -0
  159. package/src/services/geo/fixtures/README.md +17 -0
  160. package/src/services/geo/fixtures/along.json +355 -0
  161. package/src/services/geo/fixtures/autocomplete.json +218 -0
  162. package/src/services/geo/fixtures/geocode.json +129 -0
  163. package/src/services/geo/fixtures/places.json +355 -0
  164. package/src/services/geo/fixtures/reverse.json +61 -0
  165. package/src/services/geo/fixtures/routing.json +57 -0
  166. package/src/services/geo/geoapify-route.ts +93 -0
  167. package/src/services/geo/geoapify.ts +281 -0
  168. package/src/services/geo/ledger.ts +122 -0
  169. package/src/services/geo/map.ts +160 -0
  170. package/src/services/geo/migrate.ts +25 -0
  171. package/src/services/geo/polyline.ts +184 -0
  172. package/src/services/geo/route-chain.ts +145 -0
  173. package/src/services/geo/route-costs.ts +9 -0
  174. package/src/services/geo/route-geometry.ts +39 -0
  175. package/src/services/geo/route-plan.ts +172 -0
  176. package/src/services/geo/route-service.ts +433 -0
  177. package/src/services/geo/service.ts +300 -0
  178. package/src/services/geo/settings.ts +49 -0
  179. package/src/services/help/index.ts +16 -7
  180. package/src/services/help/types.ts +6 -1
  181. package/src/services/identity/invocation-actor.ts +20 -2
  182. package/src/services/identity/issue-invocation.ts +67 -0
  183. package/src/services/identity/key-ring.ts +5 -3
  184. package/src/services/identity/runtime-config.ts +23 -0
  185. package/src/services/index.ts +11 -0
  186. package/src/services/mandates/index.ts +40 -3
  187. package/src/services/network-security.ts +6 -3
  188. package/src/services/network-transport.ts +99 -0
  189. package/src/services/notifications/catalog.ts +3 -2
  190. package/src/services/notifications/runtime.ts +1 -0
  191. package/src/services/notifications/web-push-transport.ts +45 -38
  192. package/src/services/outgoing-mail/dispatcher.ts +6 -1
  193. package/src/services/outgoing-mail/index.ts +55 -1
  194. package/src/services/outgoing-mail/messages.ts +82 -3
  195. package/src/services/outgoing-mail/resolve-host.ts +2 -2
  196. package/src/services/outgoing-mail/retention.ts +13 -1
  197. package/src/services/outgoing-mail/store.ts +36 -6
  198. package/src/services/outgoing-mail/test-send.ts +34 -4
  199. package/src/services/outgoing-mail/transport.ts +1 -1
  200. package/src/services/pdf/gotenberg.ts +11 -38
  201. package/src/services/pdf/markdown.ts +160 -2
  202. package/src/services/pdf/template-preview.ts +1 -1
  203. package/src/services/public-http.ts +28 -40
  204. package/src/services/session/index.ts +203 -29
  205. package/src/services/settings/app.ts +4 -1
  206. package/src/services/settings/core-settings.ts +88 -0
  207. package/src/services/settings/defaults.ts +8 -2
  208. package/src/services/settings/errors.ts +11 -0
  209. package/src/services/settings/geo-budget.ts +40 -0
  210. package/src/services/settings/index.ts +4 -3
  211. package/src/services/settings/store.ts +25 -17
  212. package/src/services/weather/geo.ts +29 -111
  213. package/src/services/weather/index.ts +4 -3
  214. package/src/services/weather/locations.ts +8 -5
  215. package/src/services/weather/migrate.ts +1 -0
  216. package/src/services/weather/settings.ts +0 -16
  217. package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
  218. package/src/shared/avatar-art.ts +400 -0
  219. package/src/shared/index.ts +2 -0
  220. package/src/shared/network-address.ts +22 -3
  221. package/src/shared/template-rendering.ts +36 -4
  222. package/src/shared-text/documents.ts +1 -1
  223. package/src/shared-text/index.ts +2 -0
  224. package/src/shared-text/log.ts +51 -3
  225. package/src/shared-text/snapshots.ts +340 -0
  226. package/src/ssr/AdminLayout.tsx +4 -2
  227. package/src/ssr/CloudShell.island.tsx +6 -0
  228. package/src/ssr/HelpPanelArticle.island.tsx +308 -0
  229. package/src/ssr/HelpPanelBack.island.tsx +32 -0
  230. package/src/ssr/HelpPanelMenu.island.tsx +133 -0
  231. package/src/ssr/HelpPanelSearch.island.tsx +123 -0
  232. package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
  233. package/src/ssr/HelpPanelView.island.tsx +27 -0
  234. package/src/ssr/Layout.tsx +65 -15
  235. package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
  236. package/src/ssr/LayoutHelp.tsx +185 -542
  237. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
  238. package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
  239. package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
  240. package/src/ssr/PanelRuntime.island.tsx +11 -7
  241. package/src/ssr/PwaLayout.tsx +2 -2
  242. package/src/ssr/RailApps.tsx +6 -0
  243. package/src/ssr/ShellRuntime.island.tsx +23 -7
  244. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
  245. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
  246. package/src/ssr/admin-navigation.ts +1 -0
  247. package/src/ssr/app-badges.ts +6 -1
  248. package/src/ssr/compact-shell.browser-harness.ts +44 -5
  249. package/src/ssr/help.ts +7 -5
  250. package/src/ssr/index.ts +2 -1
  251. package/src/ssr/islands/index.ts +3 -4
  252. package/src/ssr/layout-context.ts +2 -0
  253. package/src/ssr/layout-help-panel-controls.tsx +43 -0
  254. package/src/ssr/layout-help-panel-document.ts +67 -0
  255. package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
  256. package/src/ssr/layout-help-panel-menu.tsx +113 -0
  257. package/src/ssr/layout-help-panel-modules.ts +7 -0
  258. package/src/ssr/layout-help-panel-reader.tsx +159 -0
  259. package/src/ssr/layout-help-panel-search.tsx +69 -0
  260. package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
  261. package/src/ssr/layout-help-panel-state.ts +141 -0
  262. package/src/ssr/layout-help-panel-types.ts +19 -0
  263. package/src/ssr/layout-help-views.tsx +484 -0
  264. package/src/ssr/mobile-menu-history.ts +4 -0
  265. package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
  266. package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
  267. package/src/ssr/page-swap.browser-harness.tsx +177 -0
  268. package/src/ssr/page-swap.ts +28 -0
  269. package/src/ssr/page-trail-runtime.ts +32 -13
  270. package/src/ssr/panel-consent.ts +2 -2
  271. package/src/ssr/panel-idle.ts +9 -0
  272. package/src/ssr/panel-slot.browser-harness.tsx +3 -0
  273. package/src/ssr/platform-messages.ts +8 -0
  274. package/src/ssr/runtime.ts +17 -0
  275. package/src/ssr/shell-data.tsx +10 -2
  276. package/src/styles/approvals.css +6 -0
  277. package/src/styles/effects.css +15 -0
  278. package/src/styles/global.css +70 -6
  279. package/src/server/services/geo.ts +0 -215
@@ -0,0 +1,141 @@
1
+ import { batch, createSignal } from "solid-js";
2
+ import type { HelpDocumentManifest, HelpDocumentPayload } from "../shared/help";
3
+ import { focusHelpArticleHeading } from "./layout-help-navigation";
4
+
5
+ export type HelpPanelData = {
6
+ documents: HelpDocumentManifest[];
7
+ appId?: string;
8
+ pageBase?: string;
9
+ article?: Omit<HelpDocumentPayload, "html">;
10
+ };
11
+ const states = new WeakMap<Element, ReturnType<typeof createPanelState>>();
12
+ export const panelRoot = (element: Element): HTMLElement => {
13
+ const root = element.closest<HTMLElement>('.help-shell[data-surface="panel"]');
14
+ if (!root) throw new Error("Missing Help panel");
15
+ return root;
16
+ };
17
+ const createPanelState = (root: HTMLElement) => {
18
+ // This is our own server's inert JSON, shared once instead of repeating it in island props.
19
+ const data: HelpPanelData = JSON.parse(root.querySelector("[data-help-panel-data]")?.textContent ?? "{}");
20
+ const [view, setView] = createSignal<"hub" | "article">(root.dataset.helpInitialTopic ? "article" : "hub");
21
+ const [activeId, setActiveId] = createSignal<string | null>(data.article?.id ?? root.dataset.helpInitialTopic ?? null);
22
+ const [query, setQuery] = createSignal("");
23
+ const [searchReturnId, setSearchReturnId] = createSignal<string | null>(null);
24
+ const [navigation, setNavigation] = createSignal({ hash: "", version: 0 });
25
+ const [announcement, setAnnouncement] = createSignal("");
26
+ const cache = new Map<string, HelpDocumentPayload>();
27
+ const [cacheVersion, setCacheVersion] = createSignal(0);
28
+ const cacheKey = (topic: HelpDocumentManifest) => `${topic.locale ?? "en"}:${topic.url}`;
29
+ if (data.article) {
30
+ const topic = data.documents.find((topic) => topic.id === data.article?.id);
31
+ if (topic) cache.set(cacheKey(topic), { ...data.article, html: "" });
32
+ }
33
+ const cachedDocument = (topic: HelpDocumentManifest) => {
34
+ cacheVersion();
35
+ return cache.get(cacheKey(topic));
36
+ };
37
+ const storeDocument = (topic: HelpDocumentManifest, payload: HelpDocumentPayload) => {
38
+ cache.set(cacheKey(topic), payload);
39
+ setCacheVersion((value) => value + 1);
40
+ };
41
+ const results = () => root.querySelector<HTMLElement>("[data-help-results]");
42
+ const scroller = () => root.querySelector<HTMLElement>("[data-scroll-fade-mode]");
43
+ const sync = () => {
44
+ const hub = view() === "hub";
45
+ const start = results();
46
+ if (start) start.hidden = !hub;
47
+ const server = root.querySelector<HTMLElement>("[data-help-server-article]");
48
+ if (server) server.hidden = hub || activeId() !== data.article?.id;
49
+ };
50
+ const back = () => {
51
+ setSearchReturnId(null);
52
+ setView("hub");
53
+ sync();
54
+ };
55
+ /** Where the article stood when a search started from it; clearing the search returns there. */
56
+ let articleScrollTop = 0;
57
+ const changeQuery = (value: string) => {
58
+ const searching = !!value.trim();
59
+ let returned = false;
60
+ batch(() => {
61
+ if (searching && view() === "article") {
62
+ articleScrollTop = scroller()?.scrollTop ?? 0;
63
+ setSearchReturnId(activeId());
64
+ const region = results();
65
+ if (region) region.style.minHeight = "";
66
+ setView("hub");
67
+ }
68
+ setQuery(value);
69
+ if (!searching) {
70
+ const region = results();
71
+ if (region) region.style.minHeight = "";
72
+ const id = searchReturnId();
73
+ setSearchReturnId(null);
74
+ if (id && data.documents.some((topic) => topic.id === id)) {
75
+ setActiveId(id);
76
+ setView("article");
77
+ returned = true;
78
+ }
79
+ }
80
+ });
81
+ sync();
82
+ // The article stayed in the document while results showed; give it back its place once it has its height again.
83
+ if (returned) {
84
+ const top = articleScrollTop;
85
+ requestAnimationFrame(() => {
86
+ const scroll = scroller();
87
+ if (scroll) scroll.scrollTop = top;
88
+ });
89
+ }
90
+ };
91
+ const open = (id: string, hash = "") => {
92
+ if (!data.documents.some((topic) => topic.id === id)) return;
93
+ try {
94
+ localStorage.setItem("cloud.layoutHelp.activeTopic", id);
95
+ } catch {
96
+ /* Storage is optional. */
97
+ }
98
+ batch(() => {
99
+ setSearchReturnId(null);
100
+ setActiveId(id);
101
+ setView("article");
102
+ setNavigation((value) => ({ hash, version: value.version + 1 }));
103
+ });
104
+ const scroll = scroller();
105
+ if (scroll) scroll.scrollTop = 0;
106
+ sync();
107
+ };
108
+ const focusHeading = () =>
109
+ focusHelpArticleHeading(root.querySelector<HTMLHeadingElement>("article:not([hidden]) .help-article-title") ?? undefined);
110
+ return {
111
+ root,
112
+ data,
113
+ view,
114
+ activeId,
115
+ query,
116
+ changeQuery,
117
+ back,
118
+ open,
119
+ navigation,
120
+ cache,
121
+ cacheKey,
122
+ cachedDocument,
123
+ storeDocument,
124
+ sync,
125
+ results,
126
+ scroller,
127
+ focusHeading,
128
+ announcement,
129
+ setAnnouncement,
130
+ };
131
+ };
132
+ export type HelpPanelState = ReturnType<typeof createPanelState>;
133
+ export function helpPanelState(element: Element): HelpPanelState {
134
+ const root = panelRoot(element);
135
+ let state = states.get(root);
136
+ if (!state) {
137
+ state = createPanelState(root);
138
+ states.set(root, state);
139
+ }
140
+ return state;
141
+ }
@@ -0,0 +1,19 @@
1
+ import type { HelpDocumentManifest } from "@k2b/cloud/shared";
2
+ import type { KeyboardPlatform } from "@k2b/ui";
3
+ import type { LayoutHelpArticle } from "./help";
4
+
5
+ export type LayoutHelpPanelProps = {
6
+ documents: readonly HelpDocumentManifest[];
7
+ /** Absent when the context selected Help the reader may not see. */
8
+ app?: { id: string; name: string; icon?: string };
9
+ /** appAppearanceStyle() of the app, from the server. */
10
+ appearance?: string;
11
+ /** Server-filtered For this view ids. */
12
+ view?: readonly string[];
13
+ initialTopic?: string;
14
+ /** Authorized article and server-parsed parts for an identical first frame. */
15
+ initialArticle?: LayoutHelpArticle;
16
+ pageBase?: string;
17
+ platform: KeyboardPlatform;
18
+ state?: "failed" | "unavailable";
19
+ };
@@ -0,0 +1,484 @@
1
+ import type { HelpAccessLevel, HelpArticleLink, HelpDocumentPayload } from "@k2b/cloud/shared";
2
+ import { Button, IconButton, Kbd, type KeyboardPlatform, NoticeCard, Placeholder, TextInput, useLocale } from "@k2b/ui";
3
+ import { For, type JSX, Show } from "solid-js";
4
+ import { appAccentStyle } from "./app-appearance";
5
+ import { helpMessages } from "./help-messages";
6
+ import type { HelpArticleParts } from "./layout-help-article";
7
+ import type { HelpTopic } from "./layout-help-registry";
8
+ import type { HelpShortcutScope, HelpStart, HelpStartGroupKind, helpShortcutGroups } from "./layout-help-start";
9
+
10
+ /**
11
+ * The Help reader's markup. Each view takes data and callbacks only, so the same view renders on the server and in the
12
+ * browser; state, effects, requests, the clipboard, and heavy controls stay with the reader that composes them.
13
+ */
14
+
15
+ export type HelpSurface = "modal" | "page" | "embedded";
16
+
17
+ const useHelpMessages = () => {
18
+ const locale = useLocale();
19
+ return () => helpMessages.resolve([locale()]).t;
20
+ };
21
+
22
+ /** An article without its own icon gets a neutral page tile. */
23
+ const helpIconClass = (icon?: string) => (icon?.startsWith("ti ") ? icon : `ti ${icon ?? "ti-file-text"}`);
24
+ const GROUP_ICONS: Record<HelpStartGroupKind, string> = {
25
+ start: "ti ti-rocket",
26
+ reference: "ti ti-book-2",
27
+ troubleshoot: "ti ti-lifebuoy",
28
+ };
29
+
30
+ /**
31
+ * One head in every view: the search never moves between the start, an article, and results. An app's embedded reader
32
+ * sits in the app's own chrome, which names it and picks its article, so it keeps only the search of its start.
33
+ */
34
+ export function HelpHead(props: {
35
+ /** The row with the tile, the title, and the actions; the embedded reader has none. */
36
+ row: boolean;
37
+ /** In an article, the back button takes the app tile's place. */
38
+ back: boolean;
39
+ /** A provider can mount only the interactive lead. */
40
+ lead?: JSX.Element;
41
+ tile: string;
42
+ title: string;
43
+ /** The ⋯ menu. */
44
+ menu: JSX.Element;
45
+ closable: boolean;
46
+ onBack: () => void;
47
+ onClose: () => void;
48
+ query: string;
49
+ onQuery: (value: string) => void;
50
+ searchRef: (element: HTMLInputElement) => void;
51
+ }) {
52
+ const t = useHelpMessages();
53
+ return (
54
+ <header class="help-head" data-help-head="">
55
+ <div class="help-head-inner help-measure">
56
+ <Show when={props.row}>
57
+ <div class="help-head-row">
58
+ {/* The back button takes the app tile's place in an article, so nothing beside it moves. */}
59
+ {props.lead ?? (
60
+ <Show
61
+ when={props.back}
62
+ fallback={
63
+ <span class="help-head-tile" aria-hidden="true">
64
+ <i class={props.tile} />
65
+ </span>
66
+ }
67
+ >
68
+ <IconButton class="help-head-back" label={t().backToTopics} title={t().back} onClick={props.onBack}>
69
+ <i class="ti ti-arrow-left" />
70
+ </IconButton>
71
+ </Show>
72
+ )}
73
+ <h2 id="layout-help-title" class="help-head-title">
74
+ {props.title}
75
+ </h2>
76
+ <div class="help-head-actions" role="group" aria-label={t().actions}>
77
+ {props.menu}
78
+ <Show when={props.closable}>
79
+ <IconButton label={t().closeHelp} title={t().close} onClick={props.onClose}>
80
+ <i class="ti ti-x" />
81
+ </IconButton>
82
+ </Show>
83
+ </div>
84
+ </div>
85
+ </Show>
86
+ <TextInput
87
+ ref={props.searchRef}
88
+ type="search"
89
+ class="help-head-search"
90
+ icon="ti ti-search"
91
+ aria-label={t().searchHelp}
92
+ data-help-search=""
93
+ value={props.query}
94
+ placeholder={t().searchPlaceholder}
95
+ onValueChange={props.onQuery}
96
+ />
97
+ </div>
98
+ </header>
99
+ );
100
+ }
101
+
102
+ /** Rows: icon tile, title, and description; the start's groups, its further tasks, and search results use them. */
103
+ function HelpTopicRows(props: { items: readonly HelpTopic[]; marker?: string; onOpen: (id: string) => void }) {
104
+ return (
105
+ <ul class="help-start-rows" {...(props.marker ? { [props.marker]: "" } : {})}>
106
+ <For each={props.items}>
107
+ {(topic) => (
108
+ <li>
109
+ <button type="button" class="help-start-row focus-ui" data-help-topic={topic.id} onClick={() => props.onOpen(topic.id)}>
110
+ <span class="help-topic-icon k2b-icon-tile help-start-row-icon app-accent-text" aria-hidden="true">
111
+ <i class={helpIconClass(topic.icon)} />
112
+ </span>
113
+ <span class="help-article-link-copy">
114
+ <span class="help-article-link-title">{topic.title}</span>
115
+ <Show when={topic.description}>{(description) => <span class="help-article-link-description">{description()}</span>}</Show>
116
+ </span>
117
+ <i class="ti ti-chevron-right help-start-row-arrow" aria-hidden="true" />
118
+ </button>
119
+ </li>
120
+ )}
121
+ </For>
122
+ </ul>
123
+ );
124
+ }
125
+
126
+ /** At most eight tasks as tiles; the app colour shows only in the icon. */
127
+ function HelpTaskTiles(props: { items: readonly HelpTopic[]; onOpen: (id: string) => void }) {
128
+ return (
129
+ <ul class="help-start-tiles" data-help-tiles="">
130
+ <For each={props.items}>
131
+ {(topic) => (
132
+ <li>
133
+ <button type="button" class="help-start-tile focus-ui" data-help-topic={topic.id} onClick={() => props.onOpen(topic.id)}>
134
+ <span class="help-topic-icon k2b-icon-tile help-start-tile-icon app-accent-text" aria-hidden="true">
135
+ <i class={helpIconClass(topic.icon)} />
136
+ </span>
137
+ <span class="help-start-tile-title">{topic.title}</span>
138
+ <Show when={topic.description}>{(description) => <span class="help-start-tile-description">{description()}</span>}</Show>
139
+ </button>
140
+ </li>
141
+ )}
142
+ </For>
143
+ </ul>
144
+ );
145
+ }
146
+
147
+ export function HelpViewSection(props: { items: readonly HelpTopic[]; onOpen: (id: string) => void }) {
148
+ const t = useHelpMessages();
149
+ return (
150
+ <Show when={props.items.length > 0}>
151
+ <section class="help-start-section" data-help-view="" aria-labelledby="help-start-view-title">
152
+ <h3 id="help-start-view-title" class="help-start-heading">
153
+ <i class="ti ti-focus-2" aria-hidden="true" />
154
+ {t().forThisView}
155
+ </h3>
156
+ <HelpTopicRows items={props.items} onOpen={props.onOpen} />
157
+ </section>
158
+ </Show>
159
+ );
160
+ }
161
+
162
+ /** The start: For this view, What to do, the kind groups, and the shortcuts last. */
163
+ export function HelpStartView(props: {
164
+ start: HelpStart<HelpTopic>;
165
+ /** The app has no Help articles. */
166
+ empty: boolean;
167
+ /** Reading the app's Help failed. */
168
+ failed: boolean;
169
+ onOpen: (id: string) => void;
170
+ onReload: () => void;
171
+ shortcuts?: JSX.Element;
172
+ }) {
173
+ const t = useHelpMessages();
174
+ const groupTitle = (kind: HelpStartGroupKind) =>
175
+ ({ start: t().groupStart, reference: t().groupReference, troubleshoot: t().groupTroubleshoot })[kind];
176
+ return (
177
+ <>
178
+ <HelpViewSection items={props.start.view} onOpen={props.onOpen} />
179
+ <Show when={props.start.tiles.length > 0}>
180
+ <section class="help-start-section" data-help-tasks="" aria-labelledby="help-start-tasks-title">
181
+ <h3 id="help-start-tasks-title" class="help-start-heading">
182
+ <i class="ti ti-checklist" aria-hidden="true" />
183
+ {t().whatToDo}
184
+ </h3>
185
+ <HelpTaskTiles items={props.start.tiles} onOpen={props.onOpen} />
186
+ <Show when={props.start.moreTasks.length > 0}>
187
+ <HelpTopicRows items={props.start.moreTasks} marker="data-help-more-tasks" onOpen={props.onOpen} />
188
+ </Show>
189
+ </section>
190
+ </Show>
191
+ <Show when={props.start.groups.length > 0}>
192
+ <div class="help-start-groups">
193
+ <For each={props.start.groups}>
194
+ {(group) => (
195
+ <section class="help-start-section" data-help-group={group.kind} aria-labelledby={`help-start-${group.kind}-title`}>
196
+ <h3 id={`help-start-${group.kind}-title`} class="help-start-heading">
197
+ <i class={GROUP_ICONS[group.kind]} aria-hidden="true" />
198
+ {groupTitle(group.kind)}
199
+ </h3>
200
+ <HelpTopicRows items={group.topics} onOpen={props.onOpen} />
201
+ </section>
202
+ )}
203
+ </For>
204
+ </div>
205
+ </Show>
206
+ {/* An app without Help says so plainly; only a failed read of its Help offers a way to try again. */}
207
+ <Show when={props.empty}>
208
+ <Show
209
+ when={props.failed}
210
+ fallback={<Placeholder icon="ti ti-book-off" title={t().noArticles} description={t().noArticlesDetail} />}
211
+ >
212
+ <NoticeCard tone="danger" title={t().helpUnavailable} detail={t().helpUnavailableDetail} data-help-unavailable="">
213
+ <Button size="sm" variant="secondary" onClick={() => props.onReload()}>
214
+ {t().reload}
215
+ </Button>
216
+ </NoticeCard>
217
+ </Show>
218
+ </Show>
219
+ {props.shortcuts}
220
+ </>
221
+ );
222
+ }
223
+
224
+ /**
225
+ * Keyboard shortcuts come last in the start. The groups reserve their height and place in CSS, so the ones the browser
226
+ * adds fill space that is already there.
227
+ */
228
+ export function HelpShortcutsView(props: { groups: ReturnType<typeof helpShortcutGroups>; appName?: string; platform?: KeyboardPlatform }) {
229
+ const t = useHelpMessages();
230
+ const label = (scope: HelpShortcutScope) =>
231
+ scope === "everywhere"
232
+ ? t().shortcutsEverywhere
233
+ : scope === "selection"
234
+ ? t().shortcutsWithSelection
235
+ : props.appName
236
+ ? t().shortcutsInApp({ app: props.appName })
237
+ : t().shortcutsInThisApp;
238
+ return (
239
+ <section class="help-start-section help-start-shortcuts" data-help-shortcuts="" aria-labelledby="help-start-shortcuts-title">
240
+ <h3 id="help-start-shortcuts-title" class="help-start-heading">
241
+ <i class="ti ti-keyboard" aria-hidden="true" />
242
+ {t().keyboardShortcuts}
243
+ </h3>
244
+ <div class="help-shortcut-groups">
245
+ <For each={props.groups}>
246
+ {(group) => (
247
+ <section class="help-shortcut-group" data-help-shortcut-group={group.scope} aria-label={label(group.scope)}>
248
+ <h4 class="help-shortcut-group-title" aria-hidden="true">
249
+ {label(group.scope)}
250
+ </h4>
251
+ <ul>
252
+ <For each={group.shortcuts}>
253
+ {(command) => (
254
+ <li class="help-shortcut" title={command.description}>
255
+ <span class="help-shortcut-title">{command.title}</span>
256
+ <Kbd keys={command.shortcut} platform={props.platform} />
257
+ </li>
258
+ )}
259
+ </For>
260
+ </ul>
261
+ </section>
262
+ )}
263
+ </For>
264
+ </div>
265
+ </section>
266
+ );
267
+ }
268
+
269
+ /** Search results replace the start below the field. The count takes the place of the first heading, so the first row stays where it was. */
270
+ export function HelpResultsView(props: {
271
+ results: readonly HelpTopic[];
272
+ searching: boolean;
273
+ /** Full-text search failed; the results match titles only. */
274
+ failed: boolean;
275
+ onRetry: () => void;
276
+ onOpen: (id: string) => void;
277
+ }) {
278
+ const t = useHelpMessages();
279
+ return (
280
+ <section class="help-start-section" data-help-search-results="">
281
+ {/* A failed full-text search says so in the same line, with a way to try again; the results do not move. */}
282
+ <div class="help-start-status">
283
+ <span aria-live="polite">{t().results({ count: props.results.length })}</span>
284
+ <Show when={props.searching}>
285
+ <i class="ti ti-loader-2 animate-spin" aria-hidden="true" />
286
+ </Show>
287
+ <Show when={props.failed}>
288
+ <span class="help-start-status-warning" data-help-search-failed="" title={t().searchTitlesOnly} role="status">
289
+ <i class="ti ti-alert-triangle" aria-hidden="true" />
290
+ <span aria-hidden="true">{t().titlesOnly}</span>
291
+ <span class="sr-only">{t().searchTitlesOnly}</span>
292
+ </span>
293
+ <Button size="xs" variant="ghost" class="help-start-status-retry" onClick={() => props.onRetry()}>
294
+ {t().tryAgain}
295
+ </Button>
296
+ </Show>
297
+ </div>
298
+ <Show
299
+ when={props.results.length > 0}
300
+ fallback={
301
+ <Show when={!props.searching}>
302
+ <Placeholder icon="ti ti-search-off" title={t().noResults} />
303
+ </Show>
304
+ }
305
+ >
306
+ <HelpTopicRows items={props.results} onOpen={props.onOpen} />
307
+ </Show>
308
+ </section>
309
+ );
310
+ }
311
+
312
+ /** Help the reader may not see. */
313
+ export function HelpNotAvailableView() {
314
+ const t = useHelpMessages();
315
+ return <Placeholder icon="ti ti-book-off" title={t().helpNotAvailable} description={t().helpNotAvailableDetail} />;
316
+ }
317
+
318
+ const ArticleLinkTitle = (props: { link: HelpArticleLink }) => (
319
+ <span class="help-article-link-copy">
320
+ <span class="help-article-link-title">{props.link.title}</span>
321
+ <Show when={props.link.description}>{(description) => <span class="help-article-link-description">{description()}</span>}</Show>
322
+ </span>
323
+ );
324
+
325
+ /** "Next" leads on; related articles keep their own app's colour, and another app says whose article it is. */
326
+ const ArticleEnd = (props: { document: HelpDocumentPayload; appId?: string }) => {
327
+ const t = useHelpMessages();
328
+ return (
329
+ <Show when={props.document.next || props.document.related.length > 0}>
330
+ <footer class="help-article-end">
331
+ <Show when={props.document.next}>
332
+ {(next) => (
333
+ <a class="help-article-next app-accent-scope focus-ui" href={next().url} style={appAccentStyle(next().accent)}>
334
+ <span class="help-article-next-label">{t().next}</span>
335
+ <span class="help-article-next-row">
336
+ <i class={`${helpIconClass(next().icon)} help-article-next-icon`} aria-hidden="true" />
337
+ <ArticleLinkTitle link={next()} />
338
+ <i class="ti ti-arrow-right help-article-next-arrow" aria-hidden="true" />
339
+ </span>
340
+ </a>
341
+ )}
342
+ </Show>
343
+ <Show when={props.document.related.length > 0}>
344
+ <section class="help-article-related" aria-labelledby="help-article-related-title">
345
+ <h4 id="help-article-related-title">{t().related}</h4>
346
+ <ul>
347
+ <For each={props.document.related}>
348
+ {(link) => (
349
+ <li>
350
+ <a class="help-article-related-link app-accent-scope focus-ui" href={link.url} style={appAccentStyle(link.accent)}>
351
+ <span class="help-topic-icon k2b-icon-tile help-article-related-icon app-accent-text" aria-hidden="true">
352
+ <i class={helpIconClass(link.icon)} />
353
+ </span>
354
+ <ArticleLinkTitle link={link} />
355
+ <Show when={link.appId !== props.appId}>
356
+ <span class="help-article-related-app app-accent-text">{link.appName}</span>
357
+ </Show>
358
+ </a>
359
+ </li>
360
+ )}
361
+ </For>
362
+ </ul>
363
+ </section>
364
+ </Show>
365
+ </footer>
366
+ </Show>
367
+ );
368
+ };
369
+
370
+ /** The inside of an article: its head, the status of its document, the body, its end, and the aside. */
371
+ export function HelpArticleView(props: {
372
+ surface: HelpSurface | "panel";
373
+ topic: HelpTopic;
374
+ appName?: string;
375
+ appId?: string;
376
+ /** The article's lead, body, sections, and step count; `null` while its document loads. */
377
+ parts: HelpArticleParts | null;
378
+ document: HelpDocumentPayload | null;
379
+ /** Renders trusted article HTML. */
380
+ Markdown: (props: { trustedHtml: string; class: string }) => JSX.Element;
381
+ showTools: boolean;
382
+ /** The article's tools under its facts: the "On this page" menu and the copy button. */
383
+ sectionMenu: JSX.Element;
384
+ copyButton: JSX.Element;
385
+ /** The article's document loads or failed to load; both sit between the article's head and its body. */
386
+ loading: JSX.Element;
387
+ failure: JSX.Element;
388
+ /** The sticky aside beside the article: its table of contents and the copy action. */
389
+ toc: JSX.Element;
390
+ copyAction: JSX.Element;
391
+ headingRef: (element: HTMLHeadingElement) => void;
392
+ contentRef: (element: HTMLDivElement) => void;
393
+ onBack: () => void;
394
+ }) {
395
+ const t = useHelpMessages();
396
+ const accessLabel = (level: HelpAccessLevel) => ({ view: t().accessView, edit: t().accessEdit, manage: t().accessManage })[level];
397
+ return (
398
+ <div class="help-article-layout">
399
+ <div class="help-article-main">
400
+ <header class="help-article-head" data-help-article-head="">
401
+ <div class="help-article-head-copy">
402
+ {/* In the panel, its head already names the app and leads back to the start. */}
403
+ <Show when={props.surface !== "panel"}>
404
+ <p class="help-article-path">
405
+ <Show when={props.appName}>
406
+ {(name) => (
407
+ <>
408
+ <span class="app-accent-text">{name()}</span>
409
+ <i class="ti ti-chevron-right" aria-hidden="true" />
410
+ </>
411
+ )}
412
+ </Show>
413
+ <button type="button" class="help-article-path-link focus-ui" onClick={props.onBack}>
414
+ {t().help}
415
+ </button>
416
+ </p>
417
+ </Show>
418
+ <h3 ref={props.headingRef} tabindex="-1" class="help-article-title focus-ui">
419
+ {props.topic.title}
420
+ </h3>
421
+ <Show
422
+ when={props.parts?.lead}
423
+ fallback={
424
+ // An article without a leading paragraph, or a built-in topic, shows its description; a loading article shows neither yet.
425
+ <Show when={props.parts && props.topic.description}>
426
+ {(description) => <p class="help-article-lead">{description()}</p>}
427
+ </Show>
428
+ }
429
+ >
430
+ {(lead) => <props.Markdown trustedHtml={lead()} class="help-document help-article-lead" />}
431
+ </Show>
432
+ <Show when={props.document?.needs || props.parts?.steps}>
433
+ {/* One row of facts; the access level appears here and nowhere else in the reader's chrome. */}
434
+ <ul class="help-article-facts" aria-label={t().articleFacts}>
435
+ <Show when={props.document?.needs}>
436
+ {(level) => (
437
+ <li class="help-article-fact" data-fact="needs">
438
+ <i class="ti ti-key" aria-hidden="true" />
439
+ <span>
440
+ {t().youNeed} <strong>{accessLabel(level())}</strong>
441
+ </span>
442
+ </li>
443
+ )}
444
+ </Show>
445
+ <Show when={props.parts?.steps}>
446
+ {(count) => (
447
+ <li class="help-article-fact" data-fact="steps">
448
+ <i class="ti ti-list-numbers" aria-hidden="true" />
449
+ <span>{t().steps({ count: count() })}</span>
450
+ </li>
451
+ )}
452
+ </Show>
453
+ </ul>
454
+ </Show>
455
+ <Show when={props.showTools}>
456
+ <div class="help-article-tools">
457
+ {props.sectionMenu}
458
+ {props.copyButton}
459
+ </div>
460
+ </Show>
461
+ </div>
462
+ {/* The embedded readers sit in the app's own chrome and keep the flat head. */}
463
+ <Show when={props.surface !== "embedded"}>
464
+ <span class="help-article-icon k2b-icon-tile app-accent-text" aria-hidden="true">
465
+ <i class={helpIconClass(props.topic.icon)} />
466
+ </span>
467
+ </Show>
468
+ </header>
469
+ {props.loading}
470
+ {props.failure}
471
+ <div ref={props.contentRef}>
472
+ <Show when={props.parts}>{(parts) => <props.Markdown trustedHtml={parts().body} class="help-document" />}</Show>
473
+ </div>
474
+ <Show when={props.document}>{(document) => <ArticleEnd document={document()} appId={props.appId} />}</Show>
475
+ </div>
476
+ <aside class="help-article-aside">
477
+ <div class="help-article-aside-sticky">
478
+ {props.toc}
479
+ {props.copyAction}
480
+ </div>
481
+ </aside>
482
+ </div>
483
+ );
484
+ }
@@ -30,6 +30,7 @@ export function openMobileMenu(view: (beforeSelect: () => Promise<boolean>) => D
30
30
  abort.abort();
31
31
  };
32
32
  window.addEventListener("popstate", pop);
33
+ window.addEventListener("ssr:nav-popstate", pop);
33
34
  window.addEventListener("pagehide", leaving);
34
35
  mobile.addEventListener("change", resized);
35
36
  try {
@@ -44,6 +45,7 @@ export function openMobileMenu(view: (beforeSelect: () => Promise<boolean>) => D
44
45
  );
45
46
  } finally {
46
47
  window.removeEventListener("popstate", pop);
48
+ window.removeEventListener("ssr:nav-popstate", pop);
47
49
  window.removeEventListener("pagehide", leaving);
48
50
  mobile.removeEventListener("change", resized);
49
51
  if (!leavingPage && history.state?.[marker] === id) {
@@ -51,9 +53,11 @@ export function openMobileMenu(view: (beforeSelect: () => Promise<boolean>) => D
51
53
  const returned = () => {
52
54
  window.removeEventListener("pagehide", returned);
53
55
  window.removeEventListener("popstate", returned);
56
+ window.removeEventListener("ssr:nav-popstate", returned);
54
57
  resolve();
55
58
  };
56
59
  window.addEventListener("popstate", returned, { once: true });
60
+ window.addEventListener("ssr:nav-popstate", returned, { once: true });
57
61
  window.addEventListener("pagehide", returned, { once: true });
58
62
  history.back();
59
63
  });