@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
@@ -1,4 +1,5 @@
1
- import type { HelpAccessLevel, HelpArticleLink, HelpDocumentManifest, HelpDocumentPayload } from "@k2b/cloud/shared";
1
+ import type { HelpArticleLink, HelpDocumentManifest, HelpDocumentPayload } from "@k2b/cloud/shared";
2
+ import { listenPopState } from "@k2b/ssr/nav";
2
3
  import { clipboard } from "@k2b/stdlib/solid";
3
4
  import {
4
5
  BottomSheet,
@@ -7,8 +8,6 @@ import {
7
8
  Dropdown,
8
9
  dialogCore,
9
10
  IconButton,
10
- Kbd,
11
- type KeyboardPlatform,
12
11
  MarkdownView,
13
12
  NoticeCard,
14
13
  Placeholder,
@@ -16,14 +15,12 @@ import {
16
15
  prompts,
17
16
  ScrollArea,
18
17
  TableOfContents,
19
- TextInput,
20
18
  toast,
21
19
  useLocale,
22
20
  } from "@k2b/ui";
23
- import { createEffect, createMemo, createSignal, For, type JSX, onCleanup, onMount, Show, untrack } from "solid-js";
21
+ import { createEffect, createMemo, createSignal, type JSX, onCleanup, onMount, Show, untrack } from "solid-js";
24
22
  import { COMMANDS_CHANGED, contextCommandsWithShortcuts } from "../browser/command-shortcuts";
25
- import { getContext, onContextChange, openPanel } from "../browser/panel";
26
- import { onPanelShortcutsChange, panelShortcutsReceived, readPanelShortcuts } from "../browser/panel-shortcuts";
23
+ import { openPanel } from "../browser/panel";
27
24
  import { HELP_ACCESS_LEVELS, HELP_KINDS } from "../shared/help";
28
25
  import { appAccentStyle } from "./app-appearance";
29
26
  import type { LayoutHelpArticle } from "./help";
@@ -33,14 +30,9 @@ import { formatHelpBundleMarkdown, formatHelpDocumentMarkdown } from "./layout-h
33
30
  import { focusHelpArticleHeading, resetHelpArticleScroll } from "./layout-help-navigation";
34
31
  import { HELP_TOPICS_EVENT, type HelpTopic, registry } from "./layout-help-registry";
35
32
  import { searchHelpTopics } from "./layout-help-search";
36
- import {
37
- type HelpShortcutCommand,
38
- type HelpShortcutScope,
39
- type HelpStartGroupKind,
40
- helpShortcutGroups,
41
- helpStart,
42
- } from "./layout-help-start";
33
+ import { type HelpShortcutCommand, helpShortcutGroups, helpStart } from "./layout-help-start";
43
34
  import { helpLinkTarget, layoutHelpTopicHref } from "./layout-help-url";
35
+ import { HelpArticleView, HelpHead, HelpResultsView, HelpShortcutsView, HelpStartView, type HelpSurface } from "./layout-help-views";
44
36
  import { platformMessages } from "./platform-messages";
45
37
  import { readShellData, useShellData } from "./shell-data";
46
38
  import { shellShortcuts } from "./shell-shortcuts";
@@ -58,22 +50,6 @@ export type LayoutHelpPageProps = {
58
50
  pageBase?: string;
59
51
  };
60
52
 
61
- export type LayoutHelpPanelProps = {
62
- documents: readonly HelpDocumentManifest[];
63
- /** Absent when the context selected Help the reader may not see. */
64
- app?: { id: string; name: string; icon?: string };
65
- /** appAppearanceStyle() of the app, from the server. */
66
- appearance?: string;
67
- /** Server-filtered For this view ids. */
68
- view?: readonly string[];
69
- initialTopic?: string;
70
- /** Authorized article and server-parsed parts for an identical first frame. */
71
- initialArticle?: LayoutHelpArticle;
72
- pageBase?: string;
73
- platform: KeyboardPlatform;
74
- state?: "failed" | "unavailable";
75
- };
76
-
77
53
  type HelpView = "hub" | "article";
78
54
  type HelpSession = {
79
55
  view: HelpView;
@@ -101,8 +77,6 @@ const articleLink = (value: unknown): HelpArticleLink | undefined => {
101
77
  return { appId, appName, accent, documentId, title, description, icon, url };
102
78
  };
103
79
 
104
- /** An article without its own icon gets a neutral page tile. */
105
- const iconClass = (icon?: string) => (icon?.startsWith("ti ") ? icon : `ti ${icon ?? "ti-file-text"}`);
106
80
  const byOrder = (a: HelpTopic, b: HelpTopic) => (a.order ?? 100) - (b.order ?? 100) || a.title.localeCompare(b.title);
107
81
  const sortedTopics = () => [...(registry()?.values() ?? [])].sort(byOrder);
108
82
  const mergeTopics = (registered: readonly HelpTopic[], documents: readonly HelpDocumentManifest[] = []): HelpTopic[] => {
@@ -110,11 +84,6 @@ const mergeTopics = (registered: readonly HelpTopic[], documents: readonly HelpD
110
84
  for (const topic of documents) topics.set(topic.id, topic);
111
85
  return [...topics.values()].sort(byOrder);
112
86
  };
113
- const GROUP_ICONS: Record<HelpStartGroupKind, string> = {
114
- start: "ti ti-rocket",
115
- reference: "ti ti-book-2",
116
- troubleshoot: "ti ti-lifebuoy",
117
- };
118
87
 
119
88
  /**
120
89
  * Keyboard shortcuts come last in the start. The browser knows the page's commands. On a Layout page (it has shell
@@ -122,68 +91,22 @@ const GROUP_ICONS: Record<HelpStartGroupKind, string> = {
122
91
  * first client render on, also before the shell has registered them. The groups reserve their height and place in CSS,
123
92
  * so the ones the browser adds fill space that is already there.
124
93
  */
125
- const StartShortcuts = (props: { appName?: string; panel?: boolean; platform?: KeyboardPlatform }) => {
94
+ const StartShortcuts = (props: { appName?: string }) => {
126
95
  const locale = useLocale();
127
- const t = () => helpMessages.resolve([locale()]).t;
128
- const shell = props.panel ? undefined : useShellData();
129
- const seed: HelpShortcutCommand[] =
130
- props.panel || shell ? shellShortcuts(platformMessages.resolve([locale()]).t, { panel: props.panel || !!shell?.panel }) : [];
96
+ const shell = useShellData();
97
+ const seed: HelpShortcutCommand[] = shell ? shellShortcuts(platformMessages.resolve([locale()]).t, { panel: !!shell.panel }) : [];
131
98
  const read = (): HelpShortcutCommand[] => {
132
- // Once received, the host list is authoritative: omitted shortcuts must stay omitted.
133
- if (props.panel) return panelShortcutsReceived() && getContext() !== null ? readPanelShortcuts() : seed;
134
99
  const commands = contextCommandsWithShortcuts();
135
100
  return [...seed.filter((command) => !commands.some((other) => other.id === command.id)), ...commands];
136
101
  };
137
- const [commands, setCommands] = createSignal<HelpShortcutCommand[]>(props.panel ? seed : read());
102
+ const [commands, setCommands] = createSignal<HelpShortcutCommand[]>(read());
138
103
  onMount(() => {
139
104
  const update = () => setCommands(read());
140
- if (props.panel) {
141
- onCleanup(onPanelShortcutsChange(update));
142
- onCleanup(onContextChange(update));
143
- }
144
105
  update();
145
- if (!props.panel) {
146
- window.addEventListener(COMMANDS_CHANGED, update);
147
- onCleanup(() => window.removeEventListener(COMMANDS_CHANGED, update));
148
- }
106
+ window.addEventListener(COMMANDS_CHANGED, update);
107
+ onCleanup(() => window.removeEventListener(COMMANDS_CHANGED, update));
149
108
  });
150
- const label = (scope: HelpShortcutScope) =>
151
- scope === "everywhere"
152
- ? t().shortcutsEverywhere
153
- : scope === "selection"
154
- ? t().shortcutsWithSelection
155
- : props.appName
156
- ? t().shortcutsInApp({ app: props.appName })
157
- : t().shortcutsInThisApp;
158
- return (
159
- <section class="help-start-section help-start-shortcuts" data-help-shortcuts="" aria-labelledby="help-start-shortcuts-title">
160
- <h3 id="help-start-shortcuts-title" class="help-start-heading">
161
- <i class="ti ti-keyboard" aria-hidden="true" />
162
- {t().keyboardShortcuts}
163
- </h3>
164
- <div class="help-shortcut-groups">
165
- <For each={helpShortcutGroups(commands())}>
166
- {(group) => (
167
- <section class="help-shortcut-group" data-help-shortcut-group={group.scope} aria-label={label(group.scope)}>
168
- <h4 class="help-shortcut-group-title" aria-hidden="true">
169
- {label(group.scope)}
170
- </h4>
171
- <ul>
172
- <For each={group.shortcuts}>
173
- {(command) => (
174
- <li class="help-shortcut" title={command.description}>
175
- <span class="help-shortcut-title">{command.title}</span>
176
- <Kbd keys={command.shortcut} platform={props.platform ?? shell?.platform} />
177
- </li>
178
- )}
179
- </For>
180
- </ul>
181
- </section>
182
- )}
183
- </For>
184
- </div>
185
- </section>
186
- );
109
+ return <HelpShortcutsView groups={helpShortcutGroups(commands())} appName={props.appName} platform={shell?.platform} />;
187
110
  };
188
111
 
189
112
  const HelpShell = (props: {
@@ -196,29 +119,29 @@ const HelpShell = (props: {
196
119
  accent?: string;
197
120
  /** The app's canvas colours for a reader outside the app canvas. */
198
121
  appearance?: string;
199
- surface?: "modal" | "page" | "embedded" | "panel";
200
- panel?: LayoutHelpPanelProps;
122
+ surface: HelpSurface;
201
123
  syncPageUrl?: boolean;
202
124
  }) => {
203
125
  const locale = useLocale();
204
126
  const t = () => helpMessages.resolve([locale()]).t;
205
127
  // Links to this app's Help, by its own route or Core's `/help/apps/<id>`, open in place.
206
- const shell = props.surface === "panel" ? undefined : useShellData();
207
- const appId = props.panel?.app?.id ?? shell?.appId;
128
+ const shell = useShellData();
129
+ const appId = shell?.appId;
208
130
  const app = shell?.apps.find((candidate) => candidate.id === appId);
209
- const appName = props.panel?.app?.name ?? app?.label;
210
- const [viewIds, setViewIds] = createSignal(props.panel?.view ?? []);
211
- const registeredTopics = () => (props.surface === "panel" ? [] : sortedTopics());
212
- const accessLabel = (level: HelpAccessLevel) => ({ view: t().accessView, edit: t().accessEdit, manage: t().accessManage })[level];
131
+ const appName = app?.label;
132
+ const registeredTopics = () => sortedTopics();
213
133
  const [externalTopics, setExternalTopics] = createSignal(mergeTopics(registeredTopics(), props.documents));
214
134
  const [view, setView] = createSignal<HelpView>(props.session.view);
215
135
  const [query, setQuery] = createSignal(props.session.query);
216
136
  const [activeId, setActiveId] = createSignal<string | null>(props.session.activeId);
217
137
  const initialTopic = props.documents?.find((topic) => topic.id === props.session.activeId);
218
138
  const initialArticle = initialTopic && props.initialArticle?.payload.id === initialTopic.id ? props.initialArticle : undefined;
219
- if (initialArticle && initialTopic)
220
- props.session.articleCache.set(`${initialTopic.locale ?? "en"}:${initialTopic.url}`, initialArticle.payload);
221
- const [payload, setPayload] = createSignal<HelpDocumentPayload | null>(initialArticle?.payload ?? null);
139
+ // HTML travels once as parts; rebuild it for the cache and the article settle effect.
140
+ const seeded = initialArticle
141
+ ? { ...initialArticle.payload, html: (initialArticle.parts.lead ?? "") + initialArticle.parts.body }
142
+ : undefined;
143
+ if (seeded && initialTopic) props.session.articleCache.set(`${initialTopic.locale ?? "en"}:${initialTopic.url}`, seeded);
144
+ const [payload, setPayload] = createSignal<HelpDocumentPayload | null>(seeded ?? null);
222
145
  const [loading, setLoading] = createSignal(props.session.view === "article" && !initialArticle);
223
146
  const [loadError, setLoadError] = createSignal<string | null>(null);
224
147
  const [loadAttempt, setLoadAttempt] = createSignal(0);
@@ -266,7 +189,7 @@ const HelpShell = (props: {
266
189
  };
267
190
 
268
191
  const restoreArticleScroll = () => {
269
- if ((props.surface !== "modal" && props.surface !== "panel") || pendingNavigation || pendingHash) return;
192
+ if (props.surface !== "modal" || pendingNavigation || pendingHash) return;
270
193
  requestAnimationFrame(() => {
271
194
  if (scrollArea && view() === "article") scrollArea.scrollTop = props.session.articleScrollTop;
272
195
  });
@@ -311,14 +234,12 @@ const HelpShell = (props: {
311
234
  };
312
235
 
313
236
  const topics = externalTopics;
314
- const start = createMemo(() =>
315
- helpStart(topics(), props.surface === "panel" ? viewIds() : props.surface === "modal" ? shell?.help : undefined),
316
- );
237
+ const start = createMemo(() => helpStart(topics(), props.surface === "modal" ? shell?.help : undefined));
317
238
  const activeTopic = createMemo(() => topics().find((topic) => topic.id === activeId()) ?? null);
318
239
  // Reuse the serialized parts for the initial object on both sides of the island boundary.
319
240
  const articleParts = createMemo(() => {
320
241
  const article = payload();
321
- if (initialArticle && article === initialArticle.payload) return initialArticle.parts;
242
+ if (initialArticle && article === seeded) return initialArticle.parts;
322
243
  return article?.html ? helpArticleParts(article.html) : null;
323
244
  });
324
245
  const articleSections = (): HelpSection[] => (view() === "article" ? articleParts()?.sections : undefined) ?? [];
@@ -409,10 +330,7 @@ const HelpShell = (props: {
409
330
  createEffect(syncSession);
410
331
 
411
332
  onMount(() => {
412
- if (props.surface === "panel") {
413
- onCleanup(onContextChange((context) => setViewIds(context && context.app === appId ? (context.help ?? []) : [])));
414
- }
415
- if (props.surface === "panel" || props.surface === "modal") {
333
+ if (props.surface === "modal") {
416
334
  const key = (event: KeyboardEvent) => {
417
335
  if (event.defaultPrevented || event.isComposing) return;
418
336
  const target = event.target;
@@ -445,7 +363,7 @@ const HelpShell = (props: {
445
363
  const onScroll = () => {
446
364
  if (view() === "article" && scrollArea) props.session.articleScrollTop = scrollArea.scrollTop;
447
365
  };
448
- if (props.surface === "modal" || props.surface === "panel") scrollArea?.addEventListener("scroll", onScroll, { passive: true });
366
+ if (props.surface === "modal") scrollArea?.addEventListener("scroll", onScroll, { passive: true });
449
367
  // Layout's breakpoints move page scrolling between its main column and the window; the contents follow.
450
368
  let resizeFrame: number | undefined;
451
369
  const onResize = () => {
@@ -466,12 +384,12 @@ const HelpShell = (props: {
466
384
  // The hub keeps its search, as the reader's own Back button does. An unknown topic shows the hub, as the server does.
467
385
  else setView("hub");
468
386
  };
469
- if (props.syncPageUrl) window.addEventListener("popstate", onPopState);
387
+ const stopPopState = props.syncPageUrl ? listenPopState(onPopState) : undefined;
470
388
  onCleanup(() => {
471
389
  scrollArea?.removeEventListener("scroll", onScroll);
472
390
  window.removeEventListener("resize", onResize);
473
391
  if (resizeFrame !== undefined) cancelAnimationFrame(resizeFrame);
474
- window.removeEventListener("popstate", onPopState);
392
+ stopPopState?.();
475
393
  });
476
394
  });
477
395
  onCleanup(() => copyAllController?.abort());
@@ -704,99 +622,6 @@ const HelpShell = (props: {
704
622
  }
705
623
  };
706
624
 
707
- /** Rows: icon tile, title, and description; the start's groups, its further tasks, and search results use them. */
708
- const TopicList = (listProps: { items: HelpTopic[]; marker?: string }) => (
709
- <ul class="help-start-rows" {...(listProps.marker ? { [listProps.marker]: "" } : {})}>
710
- <For each={listProps.items}>
711
- {(topic) => (
712
- <li>
713
- <button type="button" class="help-start-row focus-ui" data-help-topic={topic.id} onClick={() => openTopic(topic.id)}>
714
- <span class="help-topic-icon k2b-icon-tile help-start-row-icon app-accent-text" aria-hidden="true">
715
- <i class={iconClass(topic.icon)} />
716
- </span>
717
- <span class="help-article-link-copy">
718
- <span class="help-article-link-title">{topic.title}</span>
719
- <Show when={topic.description}>{(description) => <span class="help-article-link-description">{description()}</span>}</Show>
720
- </span>
721
- <i class="ti ti-chevron-right help-start-row-arrow" aria-hidden="true" />
722
- </button>
723
- </li>
724
- )}
725
- </For>
726
- </ul>
727
- );
728
-
729
- /** At most eight tasks as tiles; the app colour shows only in the icon. */
730
- const TaskTiles = (tilesProps: { items: HelpTopic[] }) => (
731
- <ul class="help-start-tiles" data-help-tiles="">
732
- <For each={tilesProps.items}>
733
- {(topic) => (
734
- <li>
735
- <button type="button" class="help-start-tile focus-ui" data-help-topic={topic.id} onClick={() => openTopic(topic.id)}>
736
- <span class="help-topic-icon k2b-icon-tile help-start-tile-icon app-accent-text" aria-hidden="true">
737
- <i class={iconClass(topic.icon)} />
738
- </span>
739
- <span class="help-start-tile-title">{topic.title}</span>
740
- <Show when={topic.description}>{(description) => <span class="help-start-tile-description">{description()}</span>}</Show>
741
- </button>
742
- </li>
743
- )}
744
- </For>
745
- </ul>
746
- );
747
-
748
- const groupTitle = (kind: HelpStartGroupKind) =>
749
- ({ start: t().groupStart, reference: t().groupReference, troubleshoot: t().groupTroubleshoot })[kind];
750
-
751
- const ArticleLinkTitle = (link: { link: HelpArticleLink }) => (
752
- <span class="help-article-link-copy">
753
- <span class="help-article-link-title">{link.link.title}</span>
754
- <Show when={link.link.description}>{(description) => <span class="help-article-link-description">{description()}</span>}</Show>
755
- </span>
756
- );
757
-
758
- /** "Next" leads on; related articles keep their own app's colour, and another app says whose article it is. */
759
- const ArticleEnd = (end: { document: HelpDocumentPayload }) => (
760
- <Show when={end.document.next || end.document.related.length > 0}>
761
- <footer class="help-article-end">
762
- <Show when={end.document.next}>
763
- {(next) => (
764
- <a class="help-article-next app-accent-scope focus-ui" href={next().url} style={appAccentStyle(next().accent)}>
765
- <span class="help-article-next-label">{t().next}</span>
766
- <span class="help-article-next-row">
767
- <i class={`${iconClass(next().icon)} help-article-next-icon`} aria-hidden="true" />
768
- <ArticleLinkTitle link={next()} />
769
- <i class="ti ti-arrow-right help-article-next-arrow" aria-hidden="true" />
770
- </span>
771
- </a>
772
- )}
773
- </Show>
774
- <Show when={end.document.related.length > 0}>
775
- <section class="help-article-related" aria-labelledby="help-article-related-title">
776
- <h4 id="help-article-related-title">{t().related}</h4>
777
- <ul>
778
- <For each={end.document.related}>
779
- {(link) => (
780
- <li>
781
- <a class="help-article-related-link app-accent-scope focus-ui" href={link.url} style={appAccentStyle(link.accent)}>
782
- <span class="help-topic-icon k2b-icon-tile help-article-related-icon app-accent-text" aria-hidden="true">
783
- <i class={iconClass(link.icon)} />
784
- </span>
785
- <ArticleLinkTitle link={link} />
786
- <Show when={link.appId !== appId}>
787
- <span class="help-article-related-app app-accent-text">{link.appName}</span>
788
- </Show>
789
- </a>
790
- </li>
791
- )}
792
- </For>
793
- </ul>
794
- </section>
795
- </Show>
796
- </footer>
797
- </Show>
798
- );
799
-
800
625
  const ReaderContent = (content: { children: JSX.Element }) =>
801
626
  props.surface === "page" ? (
802
627
  <div class="p-4 sm:p-5">{content.children}</div>
@@ -806,8 +631,8 @@ const HelpShell = (props: {
806
631
  </ScrollArea>
807
632
  );
808
633
 
809
- // The dialog and the panel copy from their ⋯ menu; only the page repeats the copy action beside the article.
810
- const copyInMenu = props.surface === "modal" || props.surface === "panel";
634
+ // The dialog copies from its ⋯ menu; only the page repeats the copy action beside the article.
635
+ const copyInMenu = props.surface === "modal";
811
636
  const pageHref = () => (props.pageBase ? layoutHelpTopicHref(props.pageBase, view() === "article" ? activeId() : null) : undefined);
812
637
  const menuItems = () => [
813
638
  {
@@ -823,335 +648,169 @@ const HelpShell = (props: {
823
648
  return (
824
649
  <div
825
650
  ref={root}
826
- class={
827
- props.surface === "panel"
828
- ? "help-shell app-accent-scope flex h-full min-h-0 flex-col"
829
- : `help-shell app-accent-scope flex min-h-0 flex-col ${props.surface === "page" ? "" : "bg-[var(--ui-surface-raised)]"} ${
830
- props.surface === "modal"
831
- ? "h-[min(48rem,var(--ui-dialog-available-height))] overflow-hidden panel-dialog-shell [box-shadow:var(--ui-shadow-float)]"
832
- : props.surface === "page"
833
- ? ""
834
- : "h-full"
835
- }`
836
- }
651
+ class={`help-shell app-accent-scope flex min-h-0 flex-col ${props.surface === "page" ? "" : "bg-[var(--ui-surface-raised)]"} ${
652
+ props.surface === "modal"
653
+ ? "h-[min(48rem,var(--ui-dialog-available-height))] overflow-hidden panel-dialog-shell [box-shadow:var(--ui-shadow-float)]"
654
+ : props.surface === "page"
655
+ ? ""
656
+ : "h-full"
657
+ }`}
837
658
  data-surface={props.surface}
838
- style={
839
- props.surface === "panel"
840
- ? props.panel?.appearance
841
- : [appAccentStyle(props.accent), props.appearance].filter(Boolean).join(";") || undefined
842
- }
659
+ style={[appAccentStyle(props.accent), props.appearance].filter(Boolean).join(";") || undefined}
843
660
  >
844
- {/* One head in every view: the search never moves between the start, an article, and results. An app's embedded
845
- reader sits in the app's own chrome, which names it and picks its article, so it keeps only the search of its start. */}
846
- {/* Help the reader may not see gets no head either: the slot already names the panel, and there is nothing to search. */}
847
- <Show when={(props.surface !== "embedded" || view() === "hub") && props.panel?.state !== "unavailable"}>
848
- <header class="help-head" data-help-head="">
849
- <div class="help-head-inner help-measure">
850
- <Show when={props.surface !== "embedded"}>
851
- <div class="help-head-row">
852
- {/* The back button takes the app tile's place in an article, so nothing beside it moves. */}
853
- <Show
854
- when={view() === "article"}
855
- fallback={
856
- <span class="help-head-tile" aria-hidden="true">
857
- <i class={props.panel?.app?.icon ?? app?.iconClass ?? "ti ti-help-circle"} />
858
- </span>
859
- }
860
- >
861
- <IconButton class="help-head-back" label={t().backToTopics} title={t().back} onClick={goBack}>
862
- <i class="ti ti-arrow-left" />
863
- </IconButton>
864
- </Show>
865
- <h2 id="layout-help-title" class="help-head-title">
866
- {appName ? (props.surface === "panel" ? appName : t().helpFor({ app: appName })) : t().help}
867
- </h2>
868
- <div class="help-head-actions" role="group" aria-label={t().actions}>
869
- <Dropdown.Root label={t().moreActions} position="bottom-left" items={menuItems()}>
870
- <Dropdown.Trigger iconOnly label={t().moreActions} variant="ghost" data-help-menu="">
871
- <i class="ti ti-dots" aria-hidden="true" />
872
- </Dropdown.Trigger>
873
- </Dropdown.Root>
874
- <Show when={props.surface === "modal"}>
875
- <IconButton label={t().closeHelp} title={t().close} onClick={props.close}>
876
- <i class="ti ti-x" />
877
- </IconButton>
878
- </Show>
879
- </div>
880
- </div>
881
- </Show>
882
- <TextInput
883
- ref={searchInput}
884
- type="search"
885
- class="help-head-search"
886
- icon="ti ti-search"
887
- aria-label={t().searchHelp}
888
- data-help-search=""
889
- value={query()}
890
- placeholder={t().searchPlaceholder}
891
- onValueChange={changeQuery}
892
- />
893
- </div>
894
- </header>
661
+ <Show when={props.surface !== "embedded" || view() === "hub"}>
662
+ <HelpHead
663
+ row={props.surface !== "embedded"}
664
+ back={view() === "article"}
665
+ tile={app?.iconClass ?? "ti ti-help-circle"}
666
+ title={appName ? t().helpFor({ app: appName }) : t().help}
667
+ menu={
668
+ <Dropdown.Root label={t().moreActions} position="bottom-left" items={menuItems()}>
669
+ <Dropdown.Trigger iconOnly label={t().moreActions} variant="ghost" data-help-menu="">
670
+ <i class="ti ti-dots" aria-hidden="true" />
671
+ </Dropdown.Trigger>
672
+ </Dropdown.Root>
673
+ }
674
+ closable={props.surface === "modal"}
675
+ onBack={goBack}
676
+ onClose={props.close}
677
+ query={query()}
678
+ onQuery={changeQuery}
679
+ searchRef={(element) => {
680
+ searchInput = element;
681
+ }}
682
+ />
895
683
  </Show>
896
684
 
897
685
  <ReaderContent>
898
- <Show
899
- when={props.panel?.state !== "unavailable"}
900
- fallback={<Placeholder icon="ti ti-book-off" title={t().helpNotAvailable} description={t().helpNotAvailableDetail} />}
901
- >
902
- <Show when={view() === "hub"}>
903
- <div ref={resultsRegion} data-help-results="" class="help-start help-measure" style={{ "min-height": resultsMinHeight() }}>
904
- <Show
905
- when={normalizedQuery()}
906
- fallback={
907
- <>
908
- <Show when={start().view.length > 0}>
909
- <section class="help-start-section" data-help-view="" aria-labelledby="help-start-view-title">
910
- <h3 id="help-start-view-title" class="help-start-heading">
911
- <i class="ti ti-focus-2" aria-hidden="true" />
912
- {t().forThisView}
913
- </h3>
914
- <TopicList items={start().view} />
915
- </section>
916
- </Show>
917
- <Show when={start().tiles.length > 0}>
918
- <section class="help-start-section" data-help-tasks="" aria-labelledby="help-start-tasks-title">
919
- <h3 id="help-start-tasks-title" class="help-start-heading">
920
- <i class="ti ti-checklist" aria-hidden="true" />
921
- {t().whatToDo}
922
- </h3>
923
- <TaskTiles items={start().tiles} />
924
- <Show when={start().moreTasks.length > 0}>
925
- <TopicList items={start().moreTasks} marker="data-help-more-tasks" />
926
- </Show>
927
- </section>
928
- </Show>
929
- <Show when={start().groups.length > 0}>
930
- <div class="help-start-groups">
931
- <For each={start().groups}>
932
- {(group) => (
933
- <section
934
- class="help-start-section"
935
- data-help-group={group.kind}
936
- aria-labelledby={`help-start-${group.kind}-title`}
937
- >
938
- <h3 id={`help-start-${group.kind}-title`} class="help-start-heading">
939
- <i class={GROUP_ICONS[group.kind]} aria-hidden="true" />
940
- {groupTitle(group.kind)}
941
- </h3>
942
- <TopicList items={group.topics} />
943
- </section>
944
- )}
945
- </For>
946
- </div>
947
- </Show>
948
- {/* An app without Help says so plainly; only a failed read of its Help offers a way to try again. */}
949
- <Show when={topics().length === 0}>
950
- <Show
951
- when={props.panel?.state === "failed" || shell?.helpFailed}
952
- fallback={<Placeholder icon="ti ti-book-off" title={t().noArticles} description={t().noArticlesDetail} />}
953
- >
954
- <NoticeCard tone="danger" title={t().helpUnavailable} detail={t().helpUnavailableDetail} data-help-unavailable="">
955
- <Button size="sm" variant="secondary" onClick={() => location.reload()}>
956
- {t().reload}
957
- </Button>
958
- </NoticeCard>
959
- </Show>
960
- </Show>
686
+ <Show when={view() === "hub"}>
687
+ <div ref={resultsRegion} data-help-results="" class="help-start help-measure" style={{ "min-height": resultsMinHeight() }}>
688
+ <Show
689
+ when={normalizedQuery()}
690
+ fallback={
691
+ <HelpStartView
692
+ start={start()}
693
+ empty={topics().length === 0}
694
+ failed={!!shell?.helpFailed}
695
+ onOpen={openTopic}
696
+ onReload={() => location.reload()}
697
+ shortcuts={
961
698
  <Show when={props.includeShortcuts !== false}>
962
- <StartShortcuts appName={appName} panel={props.surface === "panel"} platform={props.panel?.platform} />
699
+ <StartShortcuts appName={appName} />
963
700
  </Show>
964
- </>
701
+ }
702
+ />
703
+ }
704
+ >
705
+ <HelpResultsView
706
+ results={results()}
707
+ searching={searching()}
708
+ failed={searchFailed()}
709
+ onRetry={() => setSearchAttempt((value) => value + 1)}
710
+ onOpen={openTopic}
711
+ />
712
+ </Show>
713
+ </div>
714
+ </Show>
715
+
716
+ <Show when={view() === "article" && activeTopic()}>
717
+ {(topic) => (
718
+ <article
719
+ class="help-article"
720
+ data-surface={props.surface}
721
+ ref={(element) => {
722
+ // Delegated from the article: its body, table of contents, and end hold the links.
723
+ element.addEventListener("click", followHelpLink);
724
+ onCleanup(() => element.removeEventListener("click", followHelpLink));
725
+ }}
726
+ >
727
+ <HelpArticleView
728
+ surface={props.surface}
729
+ topic={topic()}
730
+ appName={appName}
731
+ appId={appId}
732
+ parts={articleParts()}
733
+ document={payload()}
734
+ Markdown={MarkdownView}
735
+ showTools={articleSections().length > 0 || !copyInMenu}
736
+ sectionMenu={
737
+ <Show when={articleSections().length > 0}>
738
+ <Dropdown.Root
739
+ label={t().onThisPage}
740
+ position="bottom-left"
741
+ items={articleSections().map((section) => ({
742
+ label: section.label,
743
+ choice: "radio" as const,
744
+ checked: () => activeSectionId() === section.id,
745
+ action: () => openArticleSection(section),
746
+ }))}
747
+ >
748
+ <Dropdown.Trigger label={t().onThisPage} size="sm" tooltip={false} variant="secondary">
749
+ <i class="ti ti-list" aria-hidden="true" />
750
+ <span>{t().onThisPage}</span>
751
+ <i class="ti ti-chevron-down" aria-hidden="true" />
752
+ </Dropdown.Trigger>
753
+ </Dropdown.Root>
754
+ </Show>
965
755
  }
966
- >
967
- {/* The count takes the place of the first heading, so the first row stays where it was. */}
968
- <section class="help-start-section" data-help-search-results="">
969
- {/* A failed full-text search says so in the same line, with a way to try again; the results do not move. */}
970
- <div class="help-start-status">
971
- <span aria-live="polite">{t().results({ count: results().length })}</span>
972
- <Show when={searching()}>
973
- <i class="ti ti-loader-2 animate-spin" aria-hidden="true" />
974
- </Show>
975
- <Show when={searchFailed()}>
976
- <span class="help-start-status-warning" data-help-search-failed="" title={t().searchTitlesOnly} role="status">
977
- <i class="ti ti-alert-triangle" aria-hidden="true" />
978
- <span aria-hidden="true">{t().titlesOnly}</span>
979
- <span class="sr-only">{t().searchTitlesOnly}</span>
980
- </span>
981
- <Button
982
- size="xs"
983
- variant="ghost"
984
- class="help-start-status-retry"
985
- onClick={() => setSearchAttempt((value) => value + 1)}
986
- >
987
- {t().tryAgain}
988
- </Button>
989
- </Show>
990
- </div>
991
- <Show
992
- when={results().length > 0}
993
- fallback={
994
- <Show when={!searching()}>
995
- <Placeholder icon="ti ti-search-off" title={t().noResults} />
996
- </Show>
997
- }
998
- >
999
- <TopicList items={results()} />
756
+ copyButton={
757
+ <Show when={!copyInMenu}>
758
+ <IconButton
759
+ class="help-article-tools-copy"
760
+ disabled={!payload()}
761
+ label={articleClipboard.wasCopied() ? t().articleCopied : t().copyArticle}
762
+ title={articleClipboard.wasCopied() ? t().copied : t().copyArticle}
763
+ onClick={() => void copyArticle(topic(), payload())}
764
+ >
765
+ <i class={articleClipboard.wasCopied() ? "ti ti-check" : "ti ti-markdown"} />
766
+ </IconButton>
1000
767
  </Show>
1001
- </section>
1002
- </Show>
1003
- </div>
1004
- </Show>
1005
-
1006
- <Show when={view() === "article" && activeTopic()}>
1007
- {(topic) => (
1008
- <article
1009
- class="help-article"
1010
- data-surface={props.surface}
1011
- ref={(element) => {
1012
- // Delegated from the article: its body, table of contents, and end hold the links.
1013
- element.addEventListener("click", followHelpLink);
1014
- onCleanup(() => element.removeEventListener("click", followHelpLink));
1015
- }}
1016
- >
1017
- <div class="help-article-layout">
1018
- <div class="help-article-main">
1019
- <header class="help-article-head" data-help-article-head="">
1020
- <div class="help-article-head-copy">
1021
- {/* In the panel, its head already names the app and leads back to the start. */}
1022
- <Show when={props.surface !== "panel"}>
1023
- <p class="help-article-path">
1024
- <Show when={appName}>
1025
- {(name) => (
1026
- <>
1027
- <span class="app-accent-text">{name()}</span>
1028
- <i class="ti ti-chevron-right" aria-hidden="true" />
1029
- </>
1030
- )}
1031
- </Show>
1032
- <button type="button" class="help-article-path-link focus-ui" onClick={goBack}>
1033
- {t().help}
1034
- </button>
1035
- </p>
1036
- </Show>
1037
- <h3 ref={articleHeading} tabindex="-1" class="help-article-title focus-ui">
1038
- {topic().title}
1039
- </h3>
1040
- <Show
1041
- when={articleParts()?.lead}
1042
- fallback={
1043
- // An article without a leading paragraph, or a built-in topic, shows its description; a loading article shows neither yet.
1044
- <Show when={articleParts() && topic().description}>
1045
- {(description) => <p class="help-article-lead">{description()}</p>}
1046
- </Show>
1047
- }
1048
- >
1049
- {(lead) => <MarkdownView trustedHtml={lead()} class="help-document help-article-lead" />}
1050
- </Show>
1051
- <Show when={payload()?.needs || articleParts()?.steps}>
1052
- {/* One row of facts; the access level appears here and nowhere else in the reader's chrome. */}
1053
- <ul class="help-article-facts" aria-label={t().articleFacts}>
1054
- <Show when={payload()?.needs}>
1055
- {(level) => (
1056
- <li class="help-article-fact" data-fact="needs">
1057
- <i class="ti ti-key" aria-hidden="true" />
1058
- <span>
1059
- {t().youNeed} <strong>{accessLabel(level())}</strong>
1060
- </span>
1061
- </li>
1062
- )}
1063
- </Show>
1064
- <Show when={articleParts()?.steps}>
1065
- {(count) => (
1066
- <li class="help-article-fact" data-fact="steps">
1067
- <i class="ti ti-list-numbers" aria-hidden="true" />
1068
- <span>{t().steps({ count: count() })}</span>
1069
- </li>
1070
- )}
1071
- </Show>
1072
- </ul>
1073
- </Show>
1074
- <Show when={articleSections().length > 0 || !copyInMenu}>
1075
- <div class="help-article-tools">
1076
- <Show when={articleSections().length > 0}>
1077
- <Dropdown.Root
1078
- label={t().onThisPage}
1079
- position="bottom-left"
1080
- items={articleSections().map((section) => ({
1081
- label: section.label,
1082
- choice: "radio" as const,
1083
- checked: () => activeSectionId() === section.id,
1084
- action: () => openArticleSection(section),
1085
- }))}
1086
- >
1087
- <Dropdown.Trigger label={t().onThisPage} size="sm" tooltip={false} variant="secondary">
1088
- <i class="ti ti-list" aria-hidden="true" />
1089
- <span>{t().onThisPage}</span>
1090
- <i class="ti ti-chevron-down" aria-hidden="true" />
1091
- </Dropdown.Trigger>
1092
- </Dropdown.Root>
1093
- </Show>
1094
- <Show when={!copyInMenu}>
1095
- <IconButton
1096
- class="help-article-tools-copy"
1097
- disabled={!payload()}
1098
- label={articleClipboard.wasCopied() ? t().articleCopied : t().copyArticle}
1099
- title={articleClipboard.wasCopied() ? t().copied : t().copyArticle}
1100
- onClick={() => void copyArticle(topic(), payload())}
1101
- >
1102
- <i class={articleClipboard.wasCopied() ? "ti ti-check" : "ti ti-markdown"} />
1103
- </IconButton>
1104
- </Show>
1105
- </div>
1106
- </Show>
1107
- </div>
1108
- {/* The embedded readers sit in the app's own chrome and keep the flat head. */}
1109
- <Show when={props.surface !== "embedded"}>
1110
- <span class="help-article-icon k2b-icon-tile app-accent-text" aria-hidden="true">
1111
- <i class={iconClass(topic().icon)} />
1112
- </span>
1113
- </Show>
1114
- </header>
1115
- <Show when={loading()}>
1116
- <div class="min-h-[24rem]">
1117
- <Placeholder state="loading" variant="panel" title={t().loading} />
1118
- </div>
1119
- </Show>
1120
- <Show when={loadError()}>
1121
- {(message) => (
1122
- <NoticeCard tone="danger" title={t().loadFailed} detail={message()}>
1123
- <Button size="sm" variant="secondary" onClick={() => setLoadAttempt((value) => value + 1)}>
1124
- {t().tryAgain}
1125
- </Button>
1126
- </NoticeCard>
1127
- )}
1128
- </Show>
1129
- <div ref={articleContent}>
1130
- <Show when={articleParts()}>{(parts) => <MarkdownView trustedHtml={parts().body} class="help-document" />}</Show>
768
+ }
769
+ loading={
770
+ <Show when={loading()}>
771
+ <div class="min-h-[24rem]">
772
+ <Placeholder state="loading" variant="panel" title={t().loading} />
1131
773
  </div>
1132
- <Show when={payload()}>{(document) => <ArticleEnd document={document()} />}</Show>
1133
- </div>
1134
- <aside class="help-article-aside">
1135
- <div class="help-article-aside-sticky">
1136
- <TableOfContents
1137
- items={articleSections()}
1138
- title={t().onThisPage}
1139
- scrollRoot={tocScrollRoot}
1140
- contentRoot={() => articleContent}
1141
- onActiveChange={(id) => setActiveSectionId(id ?? null)}
1142
- />
1143
- <Show when={!copyInMenu}>
1144
- <Button size="sm" variant="ghost" disabled={!payload()} onClick={() => void copyArticle(topic(), payload())}>
1145
- <i class={articleClipboard.wasCopied() ? "ti ti-check" : "ti ti-markdown"} />
1146
- {articleClipboard.wasCopied() ? t().copied : t().copyMarkdown}
774
+ </Show>
775
+ }
776
+ failure={
777
+ <Show when={loadError()}>
778
+ {(message) => (
779
+ <NoticeCard tone="danger" title={t().loadFailed} detail={message()}>
780
+ <Button size="sm" variant="secondary" onClick={() => setLoadAttempt((value) => value + 1)}>
781
+ {t().tryAgain}
1147
782
  </Button>
1148
- </Show>
1149
- </div>
1150
- </aside>
1151
- </div>
1152
- </article>
1153
- )}
1154
- </Show>
783
+ </NoticeCard>
784
+ )}
785
+ </Show>
786
+ }
787
+ toc={
788
+ <TableOfContents
789
+ items={articleSections()}
790
+ title={t().onThisPage}
791
+ scrollRoot={tocScrollRoot}
792
+ contentRoot={() => articleContent}
793
+ onActiveChange={(id) => setActiveSectionId(id ?? null)}
794
+ />
795
+ }
796
+ copyAction={
797
+ <Show when={!copyInMenu}>
798
+ <Button size="sm" variant="ghost" disabled={!payload()} onClick={() => void copyArticle(topic(), payload())}>
799
+ <i class={articleClipboard.wasCopied() ? "ti ti-check" : "ti ti-markdown"} />
800
+ {articleClipboard.wasCopied() ? t().copied : t().copyMarkdown}
801
+ </Button>
802
+ </Show>
803
+ }
804
+ headingRef={(element) => {
805
+ articleHeading = element;
806
+ }}
807
+ contentRef={(element) => {
808
+ articleContent = element;
809
+ }}
810
+ onBack={goBack}
811
+ />
812
+ </article>
813
+ )}
1155
814
  </Show>
1156
815
  <span class="sr-only" aria-live="polite">
1157
816
  {articleClipboard.wasCopied() ? t().articleCopiedAnnouncement : ""}
@@ -1201,22 +860,6 @@ export function LayoutHelpPage(props: LayoutHelpPageProps) {
1201
860
  );
1202
861
  }
1203
862
 
1204
- /** Help inside a provider document; the server snapshot also owns the first browser render. */
1205
- export function LayoutHelpPanel(props: LayoutHelpPanelProps) {
1206
- const session = createSession(props.initialTopic);
1207
- return (
1208
- <HelpShell
1209
- session={session}
1210
- initialArticle={props.initialTopic ? props.initialArticle : undefined}
1211
- close={() => {}}
1212
- documents={props.documents}
1213
- pageBase={props.pageBase}
1214
- surface="panel"
1215
- panel={props}
1216
- />
1217
- );
1218
- }
1219
-
1220
863
  /** The dialog renders outside the app canvas; it takes the app's canvas colours along for the article head. */
1221
864
  const canvasAppearance = () => {
1222
865
  const canvas = document.querySelector(".cloud-app-canvas");