@k2b/cloud 0.39.0 → 0.41.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 (237) hide show
  1. package/README.md +8 -0
  2. package/package.json +11 -4
  3. package/src/_internal/app-favicon.ts +4 -2
  4. package/src/_internal/capabilities.ts +72 -48
  5. package/src/_internal/capability-reviews.ts +81 -0
  6. package/src/_internal/capability-schemas.ts +29 -0
  7. package/src/_internal/capability-sentences.ts +217 -1
  8. package/src/_internal/capability-streams.ts +7 -0
  9. package/src/_internal/define-app.ts +49 -18
  10. package/src/_internal/font-preloads.ts +2 -1
  11. package/src/_internal/heartbeat.ts +203 -14
  12. package/src/_internal/registry.ts +28 -1
  13. package/src/_internal/runtime-watcher.ts +6 -0
  14. package/src/_internal/setup-lock.ts +103 -0
  15. package/src/_internal/static-assets.ts +52 -3
  16. package/src/_internal/status-preserving-ssr.ts +2 -0
  17. package/src/access/PermissionEditor.tsx +2 -2
  18. package/src/account/EntitySearch.tsx +1 -1
  19. package/src/ai/action-review.ts +101 -0
  20. package/src/ai/background-review.ts +72 -0
  21. package/src/ai/capabilities.ts +29 -11
  22. package/src/ai/capability-execution.ts +76 -43
  23. package/src/ai/capability-result.ts +95 -0
  24. package/src/ai/chat/action-card.ts +464 -0
  25. package/src/ai/chat/action-messages.ts +129 -0
  26. package/src/ai/chat/action-question.browser-harness.tsx +245 -0
  27. package/src/ai/chat/action-question.fixtures.ts +733 -0
  28. package/src/ai/chat/action-question.tsx +680 -0
  29. package/src/ai/chat/action-text.ts +288 -0
  30. package/src/ai/chat/blocks.tsx +19 -25
  31. package/src/ai/chat/message-actions.tsx +6 -0
  32. package/src/ai/chat/tool-action-words.ts +31 -0
  33. package/src/ai/chat/tool-action.tsx +456 -0
  34. package/src/ai/chat/turn-view.tsx +10 -160
  35. package/src/ai/chat/view-block.tsx +15 -5
  36. package/src/ai/chat-task-contracts.ts +4 -1
  37. package/src/ai/chat-task-reasons-migrate.ts +7 -0
  38. package/src/ai/chat-task-reasons.ts +1 -1
  39. package/src/ai/chat-tasks.ts +55 -25
  40. package/src/ai/client/controller.ts +59 -0
  41. package/src/ai/default-tools.ts +9 -1
  42. package/src/ai/executor.ts +198 -39
  43. package/src/ai/firecrawl-tools.ts +15 -2
  44. package/src/ai/index.ts +3 -0
  45. package/src/ai/migrate.ts +26 -2
  46. package/src/ai/open-questions.ts +172 -0
  47. package/src/ai/projects-routes.ts +3 -0
  48. package/src/ai/protocol.ts +14 -2
  49. package/src/ai/question-text.ts +8 -0
  50. package/src/ai/recent-resources.ts +240 -0
  51. package/src/ai/recent-sent-refs.ts +38 -0
  52. package/src/ai/routes.ts +195 -5
  53. package/src/ai/runtime.ts +1 -0
  54. package/src/ai/skills-routes.ts +3 -0
  55. package/src/ai/skills.ts +10 -1
  56. package/src/ai/store.ts +245 -24
  57. package/src/ai/testing.ts +36 -0
  58. package/src/ai/tool-audit.ts +8 -0
  59. package/src/ai/tool-sentences.ts +152 -34
  60. package/src/ai/tool-wording.ts +36 -8
  61. package/src/ai/tools.ts +8 -3
  62. package/src/ai/types.ts +43 -4
  63. package/src/api/accounts-entities.ts +2 -1
  64. package/src/api/admin-ai-projects.ts +2 -0
  65. package/src/api/admin-ai-skills.ts +2 -0
  66. package/src/api/admin-ai-usage.ts +2 -0
  67. package/src/api/admin-geoip.ts +80 -0
  68. package/src/api/admin-identity.ts +3 -0
  69. package/src/api/admin-lifecycle.ts +3 -0
  70. package/src/api/admin-outgoing-mail.ts +2 -0
  71. package/src/api/app-approval-actions.ts +31 -7
  72. package/src/api/app-approval.ts +46 -5
  73. package/src/api/approvals-loader.ts +23 -1
  74. package/src/api/approvals.ts +2 -0
  75. package/src/api/auth/schemas.ts +3 -0
  76. package/src/api/auth.ts +67 -12
  77. package/src/api/capabilities.ts +44 -11
  78. package/src/api/capability-streams.ts +12 -0
  79. package/src/api/help.ts +4 -28
  80. package/src/api/index.ts +6 -1
  81. package/src/api/lasting-access-messages.ts +17 -0
  82. package/src/api/mcp.ts +30 -3
  83. package/src/api/me-panel-consent.ts +71 -0
  84. package/src/api/me.ts +159 -36
  85. package/src/api/search.ts +3 -1
  86. package/src/api/sync-ops.ts +2 -0
  87. package/src/api/widgets.ts +3 -0
  88. package/src/approvals/Approvals.tsx +166 -262
  89. package/src/approvals/approval-card.ts +68 -0
  90. package/src/approvals/messages.ts +12 -24
  91. package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
  92. package/src/browser/app-approval.ts +54 -5
  93. package/src/browser/commands.ts +22 -6
  94. package/src/browser/composer-catalog.ts +45 -3
  95. package/src/browser/composer-insert.ts +10 -0
  96. package/src/browser/composer-open-messages.ts +17 -0
  97. package/src/browser/composer-open.ts +110 -0
  98. package/src/browser/discoverable-command.ts +6 -0
  99. package/src/browser/hints.ts +6 -0
  100. package/src/browser/live-connection.ts +26 -2
  101. package/src/browser/navigation-apps.ts +7 -0
  102. package/src/browser/panel-context-bridge.ts +30 -0
  103. package/src/browser/panel-context-receiver.ts +53 -0
  104. package/src/browser/panel-menu.tsx +56 -0
  105. package/src/browser/panel-messages.ts +11 -0
  106. package/src/browser/panel-shortcuts.ts +31 -0
  107. package/src/browser/panel-slot.ts +412 -10
  108. package/src/browser/panel.ts +37 -0
  109. package/src/browser/search-commands.ts +6 -3
  110. package/src/capabilities/client.ts +8 -2
  111. package/src/capabilities/reviews.ts +205 -0
  112. package/src/capabilities/server.ts +10 -3
  113. package/src/capabilities/testing.ts +47 -0
  114. package/src/capabilities/types.ts +2 -2
  115. package/src/cli/account.ts +13 -3
  116. package/src/cli/admin/geoip.ts +115 -0
  117. package/src/cli/admin/index.ts +3 -0
  118. package/src/contracts/app-approval.ts +123 -16
  119. package/src/contracts/approvals.ts +2 -2
  120. package/src/contracts/capabilities.ts +410 -18
  121. package/src/contracts/geoip.ts +42 -0
  122. package/src/contracts/index.ts +3 -0
  123. package/src/contracts/panel-context-plain.ts +99 -0
  124. package/src/contracts/panel-context.ts +93 -0
  125. package/src/contracts/seen-hints.ts +35 -0
  126. package/src/contracts/shared.ts +3 -0
  127. package/src/server/client-ip.ts +5 -0
  128. package/src/server/help-payload.ts +38 -0
  129. package/src/server/middleware/auth.ts +13 -1
  130. package/src/server/middleware/middleware.ts +9 -0
  131. package/src/server/middleware/no-transform.ts +19 -0
  132. package/src/server/middleware/rate-limit.ts +4 -6
  133. package/src/server/request-context/address.ts +45 -0
  134. package/src/server/request-context/geoip.ts +380 -0
  135. package/src/server/request-context/index.ts +33 -0
  136. package/src/server/request-context/user-agent.ts +95 -0
  137. package/src/services/accounts/app.ts +23 -3
  138. package/src/services/accounts/users.ts +12 -1
  139. package/src/services/app-approval.ts +382 -59
  140. package/src/services/approvals/device.ts +49 -31
  141. package/src/services/approvals/index.ts +7 -5
  142. package/src/services/approvals/list.ts +27 -6
  143. package/src/services/approvals/question.ts +68 -0
  144. package/src/services/audit/index.ts +1 -0
  145. package/src/services/auth-flows/ipa.ts +39 -2
  146. package/src/services/auth-flows/notification-sender.ts +3 -0
  147. package/src/services/auth-flows/password-reset.ts +32 -6
  148. package/src/services/geoip/index.ts +164 -0
  149. package/src/services/geoip/store.ts +30 -0
  150. package/src/services/geoip/upload.ts +150 -0
  151. package/src/services/identity/routes.ts +5 -1
  152. package/src/services/index.ts +13 -0
  153. package/src/services/known-browsers.ts +312 -0
  154. package/src/services/login-address-store.ts +51 -0
  155. package/src/services/login-context.ts +118 -0
  156. package/src/services/mandates/index.ts +3 -0
  157. package/src/services/mandates/policy.ts +15 -3
  158. package/src/services/notifications/email-frame.ts +1 -1
  159. package/src/services/oauth-tokens.ts +16 -2
  160. package/src/services/panel-context-consent.ts +58 -0
  161. package/src/services/pdf/markdown.ts +160 -2
  162. package/src/services/service-account-credentials.ts +21 -0
  163. package/src/services/session/index.ts +296 -41
  164. package/src/services/session/recent.ts +13 -0
  165. package/src/services/webauthn.ts +12 -0
  166. package/src/shared/email-html.ts +2 -0
  167. package/src/shared/help.ts +3 -0
  168. package/src/shared-text/cursor.ts +22 -0
  169. package/src/shared-text/documents.ts +142 -0
  170. package/src/shared-text/events.ts +43 -0
  171. package/src/shared-text/index.ts +12 -0
  172. package/src/shared-text/log.ts +48 -0
  173. package/src/shared-text/text-diff.ts +23 -0
  174. package/src/shared-text/types.ts +41 -0
  175. package/src/ssr/AdminLayout.tsx +2 -0
  176. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  177. package/src/ssr/CloudShell.island.tsx +10 -2
  178. package/src/ssr/HelpPanelArticle.island.tsx +308 -0
  179. package/src/ssr/HelpPanelBack.island.tsx +32 -0
  180. package/src/ssr/HelpPanelMenu.island.tsx +133 -0
  181. package/src/ssr/HelpPanelSearch.island.tsx +123 -0
  182. package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
  183. package/src/ssr/HelpPanelView.island.tsx +27 -0
  184. package/src/ssr/Layout.tsx +25 -2
  185. package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
  186. package/src/ssr/LayoutHelp.tsx +222 -419
  187. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +135 -0
  188. package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
  189. package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
  190. package/src/ssr/LayoutHelpTrigger.tsx +27 -2
  191. package/src/ssr/MobileNavigation.tsx +2 -2
  192. package/src/ssr/PanelLayout.tsx +9 -1
  193. package/src/ssr/PanelRuntime.island.tsx +7 -0
  194. package/src/ssr/PanelSlot.tsx +53 -3
  195. package/src/ssr/ShellRuntime.island.tsx +11 -2
  196. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
  197. package/src/ssr/app-color.ts +14 -9
  198. package/src/ssr/app-navigation.ts +16 -3
  199. package/src/ssr/compact-shell.browser-harness.ts +30 -22
  200. package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
  201. package/src/ssr/help-messages.ts +4 -0
  202. package/src/ssr/help.ts +21 -0
  203. package/src/ssr/hints.ts +25 -0
  204. package/src/ssr/index.ts +3 -0
  205. package/src/ssr/layout-context.ts +2 -0
  206. package/src/ssr/layout-help-article.server.ts +50 -0
  207. package/src/ssr/layout-help-panel-controls.tsx +43 -0
  208. package/src/ssr/layout-help-panel-document.ts +67 -0
  209. package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
  210. package/src/ssr/layout-help-panel-menu.tsx +113 -0
  211. package/src/ssr/layout-help-panel-modules.ts +7 -0
  212. package/src/ssr/layout-help-panel-reader.tsx +159 -0
  213. package/src/ssr/layout-help-panel-search.tsx +69 -0
  214. package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
  215. package/src/ssr/layout-help-panel-state.ts +141 -0
  216. package/src/ssr/layout-help-panel-types.ts +19 -0
  217. package/src/ssr/layout-help-start.ts +33 -7
  218. package/src/ssr/layout-help-views.tsx +484 -0
  219. package/src/ssr/panel-consent.ts +29 -0
  220. package/src/ssr/panel-context.ts +10 -0
  221. package/src/ssr/panel-idle.ts +9 -0
  222. package/src/ssr/panel-slot.browser-harness.tsx +53 -4
  223. package/src/ssr/platform-messages.ts +18 -0
  224. package/src/ssr/shell-data.tsx +2 -1
  225. package/src/styles/approvals.css +6 -0
  226. package/src/styles/effects.css +213 -9
  227. package/src/styles/file-chooser.css +2 -1
  228. package/src/styles/global.css +49 -12
  229. package/src/styles/panel.css +47 -4
  230. package/src/styles/resource-search.css +6 -2
  231. package/src/styles/tokens.css +17 -11
  232. package/src/styles/utilities-buttons.css +3 -2
  233. package/src/styles/utilities-data.css +2 -1
  234. package/src/styles/utilities-feedback.css +2 -2
  235. package/src/styles/utilities-layout.css +3 -13
  236. package/src/styles/utilities-markdown-editor.css +6 -6
  237. package/src/styles/utilities-navigation.css +17 -22
@@ -1,10 +1,12 @@
1
- import type { HelpAccessLevel, HelpArticleLink, HelpDocumentManifest, HelpDocumentPayload } from "@k2b/cloud/shared";
1
+ import type { HelpArticleLink, HelpDocumentManifest, HelpDocumentPayload } from "@k2b/cloud/shared";
2
2
  import { clipboard } from "@k2b/stdlib/solid";
3
3
  import {
4
+ BottomSheet,
4
5
  Button,
6
+ bottomSheetOptions,
5
7
  Dropdown,
8
+ dialogCore,
6
9
  IconButton,
7
- Kbd,
8
10
  MarkdownView,
9
11
  NoticeCard,
10
12
  Placeholder,
@@ -12,28 +14,24 @@ import {
12
14
  prompts,
13
15
  ScrollArea,
14
16
  TableOfContents,
15
- TextInput,
16
17
  toast,
17
18
  useLocale,
18
19
  } from "@k2b/ui";
19
- import { createEffect, createMemo, createSignal, For, type JSX, onCleanup, onMount, Show } from "solid-js";
20
+ import { createEffect, createMemo, createSignal, type JSX, onCleanup, onMount, Show, untrack } from "solid-js";
20
21
  import { COMMANDS_CHANGED, contextCommandsWithShortcuts } from "../browser/command-shortcuts";
22
+ import { openPanel } from "../browser/panel";
21
23
  import { HELP_ACCESS_LEVELS, HELP_KINDS } from "../shared/help";
22
24
  import { appAccentStyle } from "./app-appearance";
25
+ import type { LayoutHelpArticle } from "./help";
23
26
  import { helpMessages } from "./help-messages";
24
27
  import { helpArticleParts } from "./layout-help-article";
25
28
  import { formatHelpBundleMarkdown, formatHelpDocumentMarkdown } from "./layout-help-markdown";
26
29
  import { focusHelpArticleHeading, resetHelpArticleScroll } from "./layout-help-navigation";
27
30
  import { HELP_TOPICS_EVENT, type HelpTopic, registry } from "./layout-help-registry";
28
31
  import { searchHelpTopics } from "./layout-help-search";
29
- import {
30
- type HelpShortcutCommand,
31
- type HelpShortcutScope,
32
- type HelpStartGroupKind,
33
- helpShortcutGroups,
34
- helpStart,
35
- } from "./layout-help-start";
32
+ import { type HelpShortcutCommand, helpShortcutGroups, helpStart } from "./layout-help-start";
36
33
  import { helpLinkTarget, layoutHelpTopicHref } from "./layout-help-url";
34
+ import { HelpArticleView, HelpHead, HelpResultsView, HelpShortcutsView, HelpStartView, type HelpSurface } from "./layout-help-views";
37
35
  import { platformMessages } from "./platform-messages";
38
36
  import { readShellData, useShellData } from "./shell-data";
39
37
  import { shellShortcuts } from "./shell-shortcuts";
@@ -41,6 +39,8 @@ import { shellShortcuts } from "./shell-shortcuts";
41
39
  export type LayoutHelpPageProps = {
42
40
  documents: readonly HelpDocumentManifest[];
43
41
  initialTopic?: string;
42
+ /** Authorized article and server-parsed parts for an identical first frame. */
43
+ initialArticle?: LayoutHelpArticle;
44
44
  includeShortcuts?: boolean;
45
45
  accent?: string;
46
46
  /** Render inside an app-owned pane instead of occupying a full page. */
@@ -76,8 +76,6 @@ const articleLink = (value: unknown): HelpArticleLink | undefined => {
76
76
  return { appId, appName, accent, documentId, title, description, icon, url };
77
77
  };
78
78
 
79
- /** An article without its own icon gets a neutral page tile. */
80
- const iconClass = (icon?: string) => (icon?.startsWith("ti ") ? icon : `ti ${icon ?? "ti-file-text"}`);
81
79
  const byOrder = (a: HelpTopic, b: HelpTopic) => (a.order ?? 100) - (b.order ?? 100) || a.title.localeCompare(b.title);
82
80
  const sortedTopics = () => [...(registry()?.values() ?? [])].sort(byOrder);
83
81
  const mergeTopics = (registered: readonly HelpTopic[], documents: readonly HelpDocumentManifest[] = []): HelpTopic[] => {
@@ -85,11 +83,6 @@ const mergeTopics = (registered: readonly HelpTopic[], documents: readonly HelpD
85
83
  for (const topic of documents) topics.set(topic.id, topic);
86
84
  return [...topics.values()].sort(byOrder);
87
85
  };
88
- const GROUP_ICONS: Record<HelpStartGroupKind, string> = {
89
- start: "ti ti-rocket",
90
- reference: "ti ti-book-2",
91
- troubleshoot: "ti ti-lifebuoy",
92
- };
93
86
 
94
87
  /**
95
88
  * Keyboard shortcuts come last in the start. The browser knows the page's commands. On a Layout page (it has shell
@@ -99,7 +92,6 @@ const GROUP_ICONS: Record<HelpStartGroupKind, string> = {
99
92
  */
100
93
  const StartShortcuts = (props: { appName?: string }) => {
101
94
  const locale = useLocale();
102
- const t = () => helpMessages.resolve([locale()]).t;
103
95
  const shell = useShellData();
104
96
  const seed: HelpShortcutCommand[] = shell ? shellShortcuts(platformMessages.resolve([locale()]).t, { panel: !!shell.panel }) : [];
105
97
  const read = (): HelpShortcutCommand[] => {
@@ -113,53 +105,20 @@ const StartShortcuts = (props: { appName?: string }) => {
113
105
  window.addEventListener(COMMANDS_CHANGED, update);
114
106
  onCleanup(() => window.removeEventListener(COMMANDS_CHANGED, update));
115
107
  });
116
- const label = (scope: HelpShortcutScope) =>
117
- scope === "everywhere"
118
- ? t().shortcutsEverywhere
119
- : scope === "selection"
120
- ? t().shortcutsWithSelection
121
- : props.appName
122
- ? t().shortcutsInApp({ app: props.appName })
123
- : t().shortcutsInThisApp;
124
- return (
125
- <section class="help-start-section help-start-shortcuts" data-help-shortcuts="" aria-labelledby="help-start-shortcuts-title">
126
- <h3 id="help-start-shortcuts-title" class="help-start-heading">
127
- <i class="ti ti-keyboard" aria-hidden="true" />
128
- {t().keyboardShortcuts}
129
- </h3>
130
- <div class="help-shortcut-groups">
131
- <For each={helpShortcutGroups(commands())}>
132
- {(group) => (
133
- <section class="help-shortcut-group" data-help-shortcut-group={group.scope} aria-label={label(group.scope)}>
134
- <h4 class="help-shortcut-group-title" aria-hidden="true">
135
- {label(group.scope)}
136
- </h4>
137
- <ul>
138
- <For each={group.shortcuts}>
139
- {(command) => (
140
- <li class="help-shortcut" title={command.description}>
141
- <span class="help-shortcut-title">{command.title}</span>
142
- <Kbd keys={command.shortcut} platform={shell?.platform} />
143
- </li>
144
- )}
145
- </For>
146
- </ul>
147
- </section>
148
- )}
149
- </For>
150
- </div>
151
- </section>
152
- );
108
+ return <HelpShortcutsView groups={helpShortcutGroups(commands())} appName={props.appName} platform={shell?.platform} />;
153
109
  };
154
110
 
155
111
  const HelpShell = (props: {
156
112
  session: HelpSession;
113
+ initialArticle?: LayoutHelpArticle;
157
114
  close: () => void;
158
115
  pageBase?: string;
159
116
  documents?: readonly HelpDocumentManifest[];
160
117
  includeShortcuts?: boolean;
161
118
  accent?: string;
162
- surface?: "modal" | "page" | "embedded";
119
+ /** The app's canvas colours for a reader outside the app canvas. */
120
+ appearance?: string;
121
+ surface: HelpSurface;
163
122
  syncPageUrl?: boolean;
164
123
  }) => {
165
124
  const locale = useLocale();
@@ -169,13 +128,20 @@ const HelpShell = (props: {
169
128
  const appId = shell?.appId;
170
129
  const app = shell?.apps.find((candidate) => candidate.id === appId);
171
130
  const appName = app?.label;
172
- const accessLabel = (level: HelpAccessLevel) => ({ view: t().accessView, edit: t().accessEdit, manage: t().accessManage })[level];
173
- const [externalTopics, setExternalTopics] = createSignal(mergeTopics(sortedTopics(), props.documents));
131
+ const registeredTopics = () => sortedTopics();
132
+ const [externalTopics, setExternalTopics] = createSignal(mergeTopics(registeredTopics(), props.documents));
174
133
  const [view, setView] = createSignal<HelpView>(props.session.view);
175
134
  const [query, setQuery] = createSignal(props.session.query);
176
135
  const [activeId, setActiveId] = createSignal<string | null>(props.session.activeId);
177
- const [payload, setPayload] = createSignal<HelpDocumentPayload | null>(null);
178
- const [loading, setLoading] = createSignal(props.session.view === "article");
136
+ const initialTopic = props.documents?.find((topic) => topic.id === props.session.activeId);
137
+ const initialArticle = initialTopic && props.initialArticle?.payload.id === initialTopic.id ? props.initialArticle : undefined;
138
+ // HTML travels once as parts; rebuild it for the cache and the article settle effect.
139
+ const seeded = initialArticle
140
+ ? { ...initialArticle.payload, html: (initialArticle.parts.lead ?? "") + initialArticle.parts.body }
141
+ : undefined;
142
+ if (seeded && initialTopic) props.session.articleCache.set(`${initialTopic.locale ?? "en"}:${initialTopic.url}`, seeded);
143
+ const [payload, setPayload] = createSignal<HelpDocumentPayload | null>(seeded ?? null);
144
+ const [loading, setLoading] = createSignal(props.session.view === "article" && !initialArticle);
179
145
  const [loadError, setLoadError] = createSignal<string | null>(null);
180
146
  const [loadAttempt, setLoadAttempt] = createSignal(0);
181
147
  const [remoteIds, setRemoteIds] = createSignal<readonly string[] | null>(null);
@@ -269,10 +235,11 @@ const HelpShell = (props: {
269
235
  const topics = externalTopics;
270
236
  const start = createMemo(() => helpStart(topics(), props.surface === "modal" ? shell?.help : undefined));
271
237
  const activeTopic = createMemo(() => topics().find((topic) => topic.id === activeId()) ?? null);
272
- // Article bodies load in the browser only; the server renders the head and the loading state.
238
+ // Reuse the serialized parts for the initial object on both sides of the island boundary.
273
239
  const articleParts = createMemo(() => {
274
- const html = payload()?.html;
275
- return html ? helpArticleParts(html) : null;
240
+ const article = payload();
241
+ if (initialArticle && article === seeded) return initialArticle.parts;
242
+ return article?.html ? helpArticleParts(article.html) : null;
276
243
  });
277
244
  const articleSections = (): HelpSection[] => (view() === "article" ? articleParts()?.sections : undefined) ?? [];
278
245
  const normalizedQuery = createMemo(() => query().trim().toLocaleLowerCase());
@@ -362,9 +329,31 @@ const HelpShell = (props: {
362
329
  createEffect(syncSession);
363
330
 
364
331
  onMount(() => {
332
+ if (props.surface === "modal") {
333
+ const key = (event: KeyboardEvent) => {
334
+ if (event.defaultPrevented || event.isComposing) return;
335
+ const target = event.target;
336
+ if (event.key === "Escape" && target === searchInput && query()) {
337
+ event.preventDefault();
338
+ changeQuery("");
339
+ return;
340
+ }
341
+ // Some layouts type "/" with Shift (German: Shift+7), so Shift does not disqualify it.
342
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
343
+ if (
344
+ event.key === "/" &&
345
+ !(target instanceof Element && target.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])'))
346
+ ) {
347
+ event.preventDefault();
348
+ searchInput?.focus();
349
+ }
350
+ };
351
+ document.addEventListener("keydown", key);
352
+ onCleanup(() => document.removeEventListener("keydown", key));
353
+ }
365
354
  const dialog = root?.closest("dialog");
366
355
  dialog?.setAttribute("aria-labelledby", "layout-help-title");
367
- const update = () => setExternalTopics(mergeTopics(sortedTopics(), props.documents));
356
+ const update = () => setExternalTopics(mergeTopics(registeredTopics(), props.documents));
368
357
  window.addEventListener(HELP_TOPICS_EVENT, update);
369
358
  update();
370
359
  onCleanup(() => window.removeEventListener(HELP_TOPICS_EVENT, update));
@@ -452,16 +441,19 @@ const HelpShell = (props: {
452
441
  loadAttempt();
453
442
  const topic = activeTopic();
454
443
  const version = ++requestVersion;
455
- setPayload(null);
444
+ const cached = topic && view() === "article" ? props.session.articleCache.get(`${topic.locale ?? "en"}:${topic.url}`) : undefined;
456
445
  setLoadError(null);
457
446
  setLoading(false);
458
- if (!topic || view() !== "article") return;
459
- const cached = props.session.articleCache.get(`${topic.locale ?? "en"}:${topic.url}`);
447
+ // Keep a seeded payload in place on mount: no empty frame or duplicate scroll restoration.
460
448
  if (cached) {
461
- setPayload(cached);
462
- restoreArticleScroll();
449
+ if (untrack(payload) !== cached) {
450
+ setPayload(cached);
451
+ restoreArticleScroll();
452
+ }
463
453
  return;
464
454
  }
455
+ setPayload(null);
456
+ if (!topic || view() !== "article") return;
465
457
 
466
458
  const controller = new AbortController();
467
459
  setLoading(true);
@@ -629,99 +621,6 @@ const HelpShell = (props: {
629
621
  }
630
622
  };
631
623
 
632
- /** Rows: icon tile, title, and description; the start's groups, its further tasks, and search results use them. */
633
- const TopicList = (listProps: { items: HelpTopic[]; marker?: string }) => (
634
- <ul class="help-start-rows" {...(listProps.marker ? { [listProps.marker]: "" } : {})}>
635
- <For each={listProps.items}>
636
- {(topic) => (
637
- <li>
638
- <button type="button" class="help-start-row focus-ui" data-help-topic={topic.id} onClick={() => openTopic(topic.id)}>
639
- <span class="help-topic-icon help-start-row-icon app-accent-text" aria-hidden="true">
640
- <i class={iconClass(topic.icon)} />
641
- </span>
642
- <span class="help-article-link-copy">
643
- <span class="help-article-link-title">{topic.title}</span>
644
- <Show when={topic.description}>{(description) => <span class="help-article-link-description">{description()}</span>}</Show>
645
- </span>
646
- <i class="ti ti-chevron-right help-start-row-arrow" aria-hidden="true" />
647
- </button>
648
- </li>
649
- )}
650
- </For>
651
- </ul>
652
- );
653
-
654
- /** At most eight tasks as tiles; the app colour shows only in the icon. */
655
- const TaskTiles = (tilesProps: { items: HelpTopic[] }) => (
656
- <ul class="help-start-tiles" data-help-tiles="">
657
- <For each={tilesProps.items}>
658
- {(topic) => (
659
- <li>
660
- <button type="button" class="help-start-tile focus-ui" data-help-topic={topic.id} onClick={() => openTopic(topic.id)}>
661
- <span class="help-topic-icon help-start-tile-icon app-accent-text" aria-hidden="true">
662
- <i class={iconClass(topic.icon)} />
663
- </span>
664
- <span class="help-start-tile-title">{topic.title}</span>
665
- <Show when={topic.description}>{(description) => <span class="help-start-tile-description">{description()}</span>}</Show>
666
- </button>
667
- </li>
668
- )}
669
- </For>
670
- </ul>
671
- );
672
-
673
- const groupTitle = (kind: HelpStartGroupKind) =>
674
- ({ start: t().groupStart, reference: t().groupReference, troubleshoot: t().groupTroubleshoot })[kind];
675
-
676
- const ArticleLinkTitle = (link: { link: HelpArticleLink }) => (
677
- <span class="help-article-link-copy">
678
- <span class="help-article-link-title">{link.link.title}</span>
679
- <Show when={link.link.description}>{(description) => <span class="help-article-link-description">{description()}</span>}</Show>
680
- </span>
681
- );
682
-
683
- /** "Next" leads on; related articles keep their own app's colour, and another app says whose article it is. */
684
- const ArticleEnd = (end: { document: HelpDocumentPayload }) => (
685
- <Show when={end.document.next || end.document.related.length > 0}>
686
- <footer class="help-article-end">
687
- <Show when={end.document.next}>
688
- {(next) => (
689
- <a class="help-article-next app-accent-scope focus-ui" href={next().url} style={appAccentStyle(next().accent)}>
690
- <span class="help-article-next-label">{t().next}</span>
691
- <span class="help-article-next-row">
692
- <i class={`${iconClass(next().icon)} help-article-next-icon`} aria-hidden="true" />
693
- <ArticleLinkTitle link={next()} />
694
- <i class="ti ti-arrow-right help-article-next-arrow" aria-hidden="true" />
695
- </span>
696
- </a>
697
- )}
698
- </Show>
699
- <Show when={end.document.related.length > 0}>
700
- <section class="help-article-related" aria-labelledby="help-article-related-title">
701
- <h4 id="help-article-related-title">{t().related}</h4>
702
- <ul>
703
- <For each={end.document.related}>
704
- {(link) => (
705
- <li>
706
- <a class="help-article-related-link app-accent-scope focus-ui" href={link.url} style={appAccentStyle(link.accent)}>
707
- <span class="help-topic-icon help-article-related-icon app-accent-text" aria-hidden="true">
708
- <i class={iconClass(link.icon)} />
709
- </span>
710
- <ArticleLinkTitle link={link} />
711
- <Show when={link.appId !== appId}>
712
- <span class="help-article-related-app app-accent-text">{link.appName}</span>
713
- </Show>
714
- </a>
715
- </li>
716
- )}
717
- </For>
718
- </ul>
719
- </section>
720
- </Show>
721
- </footer>
722
- </Show>
723
- );
724
-
725
624
  const ReaderContent = (content: { children: JSX.Element }) =>
726
625
  props.surface === "page" ? (
727
626
  <div class="p-4 sm:p-5">{content.children}</div>
@@ -731,6 +630,8 @@ const HelpShell = (props: {
731
630
  </ScrollArea>
732
631
  );
733
632
 
633
+ // The dialog copies from its ⋯ menu; only the page repeats the copy action beside the article.
634
+ const copyInMenu = props.surface === "modal";
734
635
  const pageHref = () => (props.pageBase ? layoutHelpTopicHref(props.pageBase, view() === "article" ? activeId() : null) : undefined);
735
636
  const menuItems = () => [
736
637
  {
@@ -740,7 +641,7 @@ const HelpShell = (props: {
740
641
  ? { disabled: true as const }
741
642
  : { action: () => void copyFromMenu() }),
742
643
  },
743
- ...(props.surface === "modal" && pageHref() ? [{ label: t().openAsPage, icon: "ti ti-app-window", href: pageHref()! }] : []),
644
+ ...(copyInMenu && pageHref() ? [{ label: t().openAsPage, icon: "ti ti-app-window", href: pageHref()! }] : []),
744
645
  ];
745
646
 
746
647
  return (
@@ -754,58 +655,30 @@ const HelpShell = (props: {
754
655
  : "h-full"
755
656
  }`}
756
657
  data-surface={props.surface}
757
- style={appAccentStyle(props.accent)}
658
+ style={[appAccentStyle(props.accent), props.appearance].filter(Boolean).join(";") || undefined}
758
659
  >
759
- {/* One head in every view: the search never moves between the start, an article, and results. An app's embedded
760
- reader sits in the app's own chrome, which names it and picks its article, so it keeps only the search of its start. */}
761
660
  <Show when={props.surface !== "embedded" || view() === "hub"}>
762
- <header class="help-head" data-help-head="">
763
- <div class="help-head-inner help-measure">
764
- <Show when={props.surface !== "embedded"}>
765
- <div class="help-head-row">
766
- {/* The back button takes the app tile's place in an article, so nothing beside it moves. */}
767
- <Show
768
- when={view() === "article"}
769
- fallback={
770
- <span class="help-head-tile" aria-hidden="true">
771
- <i class={app?.iconClass ?? "ti ti-help"} />
772
- </span>
773
- }
774
- >
775
- <IconButton class="help-head-back" label={t().backToTopics} title={t().back} onClick={goBack}>
776
- <i class="ti ti-arrow-left" />
777
- </IconButton>
778
- </Show>
779
- <h2 id="layout-help-title" class="help-head-title">
780
- {appName ? t().helpFor({ app: appName }) : t().help}
781
- </h2>
782
- <div class="help-head-actions" role="group" aria-label={t().actions}>
783
- <Dropdown.Root label={t().moreActions} position="bottom-left" items={menuItems()}>
784
- <Dropdown.Trigger iconOnly label={t().moreActions} variant="ghost" data-help-menu="">
785
- <i class="ti ti-dots" aria-hidden="true" />
786
- </Dropdown.Trigger>
787
- </Dropdown.Root>
788
- <Show when={props.surface === "modal"}>
789
- <IconButton label={t().closeHelp} title={t().close} onClick={props.close}>
790
- <i class="ti ti-x" />
791
- </IconButton>
792
- </Show>
793
- </div>
794
- </div>
795
- </Show>
796
- <TextInput
797
- ref={searchInput}
798
- type="search"
799
- class="help-head-search"
800
- icon="ti ti-search"
801
- aria-label={t().searchHelp}
802
- data-help-search=""
803
- value={query()}
804
- placeholder={t().searchPlaceholder}
805
- onValueChange={changeQuery}
806
- />
807
- </div>
808
- </header>
661
+ <HelpHead
662
+ row={props.surface !== "embedded"}
663
+ back={view() === "article"}
664
+ tile={app?.iconClass ?? "ti ti-help-circle"}
665
+ title={appName ? t().helpFor({ app: appName }) : t().help}
666
+ menu={
667
+ <Dropdown.Root label={t().moreActions} position="bottom-left" items={menuItems()}>
668
+ <Dropdown.Trigger iconOnly label={t().moreActions} variant="ghost" data-help-menu="">
669
+ <i class="ti ti-dots" aria-hidden="true" />
670
+ </Dropdown.Trigger>
671
+ </Dropdown.Root>
672
+ }
673
+ closable={props.surface === "modal"}
674
+ onBack={goBack}
675
+ onClose={props.close}
676
+ query={query()}
677
+ onQuery={changeQuery}
678
+ searchRef={(element) => {
679
+ searchInput = element;
680
+ }}
681
+ />
809
682
  </Show>
810
683
 
811
684
  <ReaderContent>
@@ -814,101 +687,27 @@ const HelpShell = (props: {
814
687
  <Show
815
688
  when={normalizedQuery()}
816
689
  fallback={
817
- <>
818
- <Show when={start().view.length > 0}>
819
- <section class="help-start-section" data-help-view="" aria-labelledby="help-start-view-title">
820
- <h3 id="help-start-view-title" class="help-start-heading">
821
- <i class="ti ti-focus-2" aria-hidden="true" />
822
- {t().forThisView}
823
- </h3>
824
- <TopicList items={start().view} />
825
- </section>
826
- </Show>
827
- <Show when={start().tiles.length > 0}>
828
- <section class="help-start-section" data-help-tasks="" aria-labelledby="help-start-tasks-title">
829
- <h3 id="help-start-tasks-title" class="help-start-heading">
830
- <i class="ti ti-checklist" aria-hidden="true" />
831
- {t().whatToDo}
832
- </h3>
833
- <TaskTiles items={start().tiles} />
834
- <Show when={start().moreTasks.length > 0}>
835
- <TopicList items={start().moreTasks} marker="data-help-more-tasks" />
836
- </Show>
837
- </section>
838
- </Show>
839
- <Show when={start().groups.length > 0}>
840
- <div class="help-start-groups">
841
- <For each={start().groups}>
842
- {(group) => (
843
- <section
844
- class="help-start-section"
845
- data-help-group={group.kind}
846
- aria-labelledby={`help-start-${group.kind}-title`}
847
- >
848
- <h3 id={`help-start-${group.kind}-title`} class="help-start-heading">
849
- <i class={GROUP_ICONS[group.kind]} aria-hidden="true" />
850
- {groupTitle(group.kind)}
851
- </h3>
852
- <TopicList items={group.topics} />
853
- </section>
854
- )}
855
- </For>
856
- </div>
857
- </Show>
858
- {/* An app without Help says so plainly; only a failed read of its Help offers a way to try again. */}
859
- <Show when={topics().length === 0}>
860
- <Show
861
- when={shell?.helpFailed}
862
- fallback={<Placeholder icon="ti ti-book-off" title={t().noArticles} description={t().noArticlesDetail} />}
863
- >
864
- <NoticeCard tone="danger" title={t().helpUnavailable} detail={t().helpUnavailableDetail} data-help-unavailable="">
865
- <Button size="sm" variant="secondary" onClick={() => location.reload()}>
866
- {t().reload}
867
- </Button>
868
- </NoticeCard>
690
+ <HelpStartView
691
+ start={start()}
692
+ empty={topics().length === 0}
693
+ failed={!!shell?.helpFailed}
694
+ onOpen={openTopic}
695
+ onReload={() => location.reload()}
696
+ shortcuts={
697
+ <Show when={props.includeShortcuts !== false}>
698
+ <StartShortcuts appName={appName} />
869
699
  </Show>
870
- </Show>
871
- <Show when={props.includeShortcuts !== false}>
872
- <StartShortcuts appName={appName} />
873
- </Show>
874
- </>
700
+ }
701
+ />
875
702
  }
876
703
  >
877
- {/* The count takes the place of the first heading, so the first row stays where it was. */}
878
- <section class="help-start-section" data-help-search-results="">
879
- {/* A failed full-text search says so in the same line, with a way to try again; the results do not move. */}
880
- <div class="help-start-status">
881
- <span aria-live="polite">{t().results({ count: results().length })}</span>
882
- <Show when={searching()}>
883
- <i class="ti ti-loader-2 animate-spin" aria-hidden="true" />
884
- </Show>
885
- <Show when={searchFailed()}>
886
- <span class="help-start-status-warning" data-help-search-failed="" title={t().searchTitlesOnly} role="status">
887
- <i class="ti ti-alert-triangle" aria-hidden="true" />
888
- <span aria-hidden="true">{t().titlesOnly}</span>
889
- <span class="sr-only">{t().searchTitlesOnly}</span>
890
- </span>
891
- <Button
892
- size="xs"
893
- variant="ghost"
894
- class="help-start-status-retry"
895
- onClick={() => setSearchAttempt((value) => value + 1)}
896
- >
897
- {t().tryAgain}
898
- </Button>
899
- </Show>
900
- </div>
901
- <Show
902
- when={results().length > 0}
903
- fallback={
904
- <Show when={!searching()}>
905
- <Placeholder icon="ti ti-search-off" title={t().noResults} />
906
- </Show>
907
- }
908
- >
909
- <TopicList items={results()} />
910
- </Show>
911
- </section>
704
+ <HelpResultsView
705
+ results={results()}
706
+ searching={searching()}
707
+ failed={searchFailed()}
708
+ onRetry={() => setSearchAttempt((value) => value + 1)}
709
+ onOpen={openTopic}
710
+ />
912
711
  </Show>
913
712
  </div>
914
713
  </Show>
@@ -924,98 +723,56 @@ const HelpShell = (props: {
924
723
  onCleanup(() => element.removeEventListener("click", followHelpLink));
925
724
  }}
926
725
  >
927
- <div class="help-article-layout">
928
- <div class="help-article-main">
929
- <header class="help-article-head">
930
- <p class="help-article-path">
931
- <Show when={appName}>
932
- {(name) => (
933
- <>
934
- <span class="app-accent-text">{name()}</span>
935
- <i class="ti ti-chevron-right" aria-hidden="true" />
936
- </>
937
- )}
938
- </Show>
939
- <button type="button" class="help-article-path-link focus-ui" onClick={goBack}>
940
- {t().help}
941
- </button>
942
- </p>
943
- <h3 ref={articleHeading} tabindex="-1" class="help-article-title focus-ui">
944
- {topic().title}
945
- </h3>
946
- <Show
947
- when={articleParts()?.lead}
948
- fallback={
949
- // An article without a leading paragraph, or a built-in topic, shows its description; a loading article shows neither yet.
950
- <Show when={articleParts() && topic().description}>
951
- {(description) => <p class="help-article-lead">{description()}</p>}
952
- </Show>
953
- }
726
+ <HelpArticleView
727
+ surface={props.surface}
728
+ topic={topic()}
729
+ appName={appName}
730
+ appId={appId}
731
+ parts={articleParts()}
732
+ document={payload()}
733
+ Markdown={MarkdownView}
734
+ showTools={articleSections().length > 0 || !copyInMenu}
735
+ sectionMenu={
736
+ <Show when={articleSections().length > 0}>
737
+ <Dropdown.Root
738
+ label={t().onThisPage}
739
+ position="bottom-left"
740
+ items={articleSections().map((section) => ({
741
+ label: section.label,
742
+ choice: "radio" as const,
743
+ checked: () => activeSectionId() === section.id,
744
+ action: () => openArticleSection(section),
745
+ }))}
954
746
  >
955
- {(lead) => <MarkdownView trustedHtml={lead()} class="help-document help-article-lead" />}
956
- </Show>
957
- <Show when={payload()?.needs || articleParts()?.steps}>
958
- {/* One row of facts; the access level appears here and nowhere else in the reader's chrome. */}
959
- <ul class="help-article-facts" aria-label={t().articleFacts}>
960
- <Show when={payload()?.needs}>
961
- {(level) => (
962
- <li class="help-article-fact" data-fact="needs">
963
- <i class="ti ti-key" aria-hidden="true" />
964
- <span>
965
- {t().youNeed} <strong>{accessLabel(level())}</strong>
966
- </span>
967
- </li>
968
- )}
969
- </Show>
970
- <Show when={articleParts()?.steps}>
971
- {(count) => (
972
- <li class="help-article-fact" data-fact="steps">
973
- <i class="ti ti-list-numbers" aria-hidden="true" />
974
- <span>{t().steps({ count: count() })}</span>
975
- </li>
976
- )}
977
- </Show>
978
- </ul>
979
- </Show>
980
- <Show when={articleSections().length > 0 || props.surface !== "modal"}>
981
- <div class="help-article-tools">
982
- <Show when={articleSections().length > 0}>
983
- <Dropdown.Root
984
- label={t().onThisPage}
985
- position="bottom-left"
986
- items={articleSections().map((section) => ({
987
- label: section.label,
988
- choice: "radio" as const,
989
- checked: () => activeSectionId() === section.id,
990
- action: () => openArticleSection(section),
991
- }))}
992
- >
993
- <Dropdown.Trigger label={t().onThisPage} size="sm" tooltip={false} variant="secondary">
994
- <i class="ti ti-list" aria-hidden="true" />
995
- <span>{t().onThisPage}</span>
996
- <i class="ti ti-chevron-down" aria-hidden="true" />
997
- </Dropdown.Trigger>
998
- </Dropdown.Root>
999
- </Show>
1000
- <Show when={props.surface !== "modal"}>
1001
- <IconButton
1002
- class="help-article-tools-copy"
1003
- disabled={!payload()}
1004
- label={articleClipboard.wasCopied() ? t().articleCopied : t().copyArticle}
1005
- title={articleClipboard.wasCopied() ? t().copied : t().copyArticle}
1006
- onClick={() => void copyArticle(topic(), payload())}
1007
- >
1008
- <i class={articleClipboard.wasCopied() ? "ti ti-check" : "ti ti-markdown"} />
1009
- </IconButton>
1010
- </Show>
1011
- </div>
1012
- </Show>
1013
- </header>
747
+ <Dropdown.Trigger label={t().onThisPage} size="sm" tooltip={false} variant="secondary">
748
+ <i class="ti ti-list" aria-hidden="true" />
749
+ <span>{t().onThisPage}</span>
750
+ <i class="ti ti-chevron-down" aria-hidden="true" />
751
+ </Dropdown.Trigger>
752
+ </Dropdown.Root>
753
+ </Show>
754
+ }
755
+ copyButton={
756
+ <Show when={!copyInMenu}>
757
+ <IconButton
758
+ class="help-article-tools-copy"
759
+ disabled={!payload()}
760
+ label={articleClipboard.wasCopied() ? t().articleCopied : t().copyArticle}
761
+ title={articleClipboard.wasCopied() ? t().copied : t().copyArticle}
762
+ onClick={() => void copyArticle(topic(), payload())}
763
+ >
764
+ <i class={articleClipboard.wasCopied() ? "ti ti-check" : "ti ti-markdown"} />
765
+ </IconButton>
766
+ </Show>
767
+ }
768
+ loading={
1014
769
  <Show when={loading()}>
1015
770
  <div class="min-h-[24rem]">
1016
771
  <Placeholder state="loading" variant="panel" title={t().loading} />
1017
772
  </div>
1018
773
  </Show>
774
+ }
775
+ failure={
1019
776
  <Show when={loadError()}>
1020
777
  {(message) => (
1021
778
  <NoticeCard tone="danger" title={t().loadFailed} detail={message()}>
@@ -1025,29 +782,32 @@ const HelpShell = (props: {
1025
782
  </NoticeCard>
1026
783
  )}
1027
784
  </Show>
1028
- <div ref={articleContent}>
1029
- <Show when={articleParts()}>{(parts) => <MarkdownView trustedHtml={parts().body} class="help-document" />}</Show>
1030
- </div>
1031
- <Show when={payload()}>{(document) => <ArticleEnd document={document()} />}</Show>
1032
- </div>
1033
- <aside class="help-article-aside">
1034
- <div class="help-article-aside-sticky">
1035
- <TableOfContents
1036
- items={articleSections()}
1037
- title={t().onThisPage}
1038
- scrollRoot={tocScrollRoot}
1039
- contentRoot={() => articleContent}
1040
- onActiveChange={(id) => setActiveSectionId(id ?? null)}
1041
- />
1042
- <Show when={props.surface !== "modal"}>
1043
- <Button size="sm" variant="ghost" disabled={!payload()} onClick={() => void copyArticle(topic(), payload())}>
1044
- <i class={articleClipboard.wasCopied() ? "ti ti-check" : "ti ti-markdown"} />
1045
- {articleClipboard.wasCopied() ? t().copied : t().copyMarkdown}
1046
- </Button>
1047
- </Show>
1048
- </div>
1049
- </aside>
1050
- </div>
785
+ }
786
+ toc={
787
+ <TableOfContents
788
+ items={articleSections()}
789
+ title={t().onThisPage}
790
+ scrollRoot={tocScrollRoot}
791
+ contentRoot={() => articleContent}
792
+ onActiveChange={(id) => setActiveSectionId(id ?? null)}
793
+ />
794
+ }
795
+ copyAction={
796
+ <Show when={!copyInMenu}>
797
+ <Button size="sm" variant="ghost" disabled={!payload()} onClick={() => void copyArticle(topic(), payload())}>
798
+ <i class={articleClipboard.wasCopied() ? "ti ti-check" : "ti ti-markdown"} />
799
+ {articleClipboard.wasCopied() ? t().copied : t().copyMarkdown}
800
+ </Button>
801
+ </Show>
802
+ }
803
+ headingRef={(element) => {
804
+ articleHeading = element;
805
+ }}
806
+ contentRef={(element) => {
807
+ articleContent = element;
808
+ }}
809
+ onBack={goBack}
810
+ />
1051
811
  </article>
1052
812
  )}
1053
813
  </Show>
@@ -1079,14 +839,15 @@ const createSession = (initialTopic?: string): HelpSession => {
1079
839
 
1080
840
  /**
1081
841
  * Render the shared Help experience as a full page. Apps pass the same
1082
- * manifest used by `Layout.HelpDocuments`; article bodies still load lazily
1083
- * from the central Help API.
842
+ * manifest used by `Layout.HelpDocuments`; initialArticle supplies the first
843
+ * article, while other articles load from the central Help API.
1084
844
  */
1085
845
  export function LayoutHelpPage(props: LayoutHelpPageProps) {
1086
846
  const session = createSession(props.initialTopic);
1087
847
  return (
1088
848
  <HelpShell
1089
849
  session={session}
850
+ initialArticle={props.initialTopic ? props.initialArticle : undefined}
1090
851
  close={() => {}}
1091
852
  documents={props.documents}
1092
853
  includeShortcuts={props.includeShortcuts}
@@ -1098,12 +859,54 @@ export function LayoutHelpPage(props: LayoutHelpPageProps) {
1098
859
  );
1099
860
  }
1100
861
 
1101
- /** Open the current app's Help over the page; the dialog sits outside the app canvas, so it takes the app's accent along. */
862
+ /** The dialog renders outside the app canvas; it takes the app's canvas colours along for the article head. */
863
+ const canvasAppearance = () => {
864
+ const canvas = document.querySelector(".cloud-app-canvas");
865
+ if (!canvas) return undefined;
866
+ const style = getComputedStyle(canvas);
867
+ return CANVAS_PROPERTIES.flatMap((name) => {
868
+ const value = style.getPropertyValue(name).trim();
869
+ return value ? [`${name}:${value}`] : [];
870
+ }).join(";");
871
+ };
872
+ const CANVAS_PROPERTIES = [
873
+ "--app-canvas-from",
874
+ "--app-canvas-via",
875
+ "--app-canvas-to",
876
+ "--app-canvas-angle",
877
+ "--app-canvas-strength",
878
+ "--app-canvas-dark-strength",
879
+ ];
880
+
881
+ /**
882
+ * Open Core's Help provider in the panel slot, or fall back to the current app's dialog when no slot accepts it
883
+ * (anonymous pages, `Layout panel={false}`, an app on an older @k2b/cloud). On a phone the fallback is the shared
884
+ * bottom sheet at full height, like the panel's phone sheet.
885
+ */
1102
886
  export function openLayoutHelpDialog() {
887
+ if (openPanel("core")) return;
1103
888
  const session = createSession();
1104
889
  const accent = readShellData()?.accent;
890
+ const appearance = canvasAppearance();
891
+ const pageBase = window.__cloudLayoutHelpPageBase;
892
+ if (window.matchMedia("(max-width: 47.999rem)").matches) {
893
+ void dialogCore.open<void>(
894
+ (close, context) => (
895
+ <BottomSheet onDismiss={context.requestDismiss}>
896
+ <HelpShell session={session} close={() => close()} accent={accent} appearance={appearance} surface="modal" pageBase={pageBase} />
897
+ </BottomSheet>
898
+ ),
899
+ {
900
+ ...bottomSheetOptions,
901
+ panelClassName: `${bottomSheetOptions.panelClassName} cloud-help-sheet`,
902
+ ariaLabel: helpMessages.resolve([document.documentElement.lang || "en"]).t.help,
903
+ history: true,
904
+ },
905
+ );
906
+ return;
907
+ }
1105
908
  void prompts.dialog<void>(
1106
- (close) => <HelpShell session={session} close={close} accent={accent} surface="modal" pageBase={window.__cloudLayoutHelpPageBase} />,
909
+ (close) => <HelpShell session={session} close={close} accent={accent} appearance={appearance} surface="modal" pageBase={pageBase} />,
1107
910
  { surface: "bare", header: false, size: "wide" },
1108
911
  );
1109
912
  }