@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
package/src/ssr/help.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Context } from "hono";
2
2
  import { buildHelpDocumentPayload } from "../server/help-payload";
3
3
  import { getLocale } from "../server/locale";
4
+ import { appAccessAuthority } from "../services/app-access";
4
5
  import { createHelpReader } from "../services/help";
5
6
  import type { HelpManifestResult } from "../services/help/types";
6
7
  import { logger } from "../services/logging";
@@ -23,7 +24,7 @@ export const preloadLayoutHelp = async (c: Context, appId?: string): Promise<Hel
23
24
  const id = appId ?? resolveCurrentApp(getRuntimeContext(c).apps, c.req.path)?.id;
24
25
  if (!id) return null;
25
26
  try {
26
- const manifest = await createHelpReader(getLocale(c), c.get("actor")).manifest(id);
27
+ const manifest = await createHelpReader(getLocale(c), c.get("actor"), { authority: appAccessAuthority(c) }).manifest(id);
27
28
  manifests.set(c, manifest);
28
29
  return manifest;
29
30
  } catch (error) {
@@ -35,16 +36,17 @@ export const preloadLayoutHelp = async (c: Context, appId?: string): Promise<Hel
35
36
  }
36
37
  };
37
38
 
38
- export type LayoutHelpArticle = { payload: HelpDocumentPayload; parts: HelpArticleParts };
39
+ /** Article HTML crosses the island boundary once, as parts. */
40
+ export type LayoutHelpArticle = { payload: Omit<HelpDocumentPayload, "html">; parts: HelpArticleParts };
39
41
 
40
42
  /** An authorized initial article; failures leave the browser's on-demand reader available. */
41
43
  export const preloadLayoutHelpArticle = async (c: Context, appId: string, documentId: string): Promise<LayoutHelpArticle | null> => {
42
44
  if (!c.get("user")) return null;
43
45
  try {
44
- const article = await createHelpReader(getLocale(c), c.get("actor")).read({ appId, documentId });
46
+ const article = await createHelpReader(getLocale(c), c.get("actor"), { authority: appAccessAuthority(c) }).read({ appId, documentId });
45
47
  if (!article) return null;
46
- const payload = buildHelpDocumentPayload(article, c.req.header("User-Agent") ?? "");
47
- return { payload, parts: helpArticlePartsServer(payload.html) };
48
+ const { html, ...payload } = buildHelpDocumentPayload(article, c.req.header("User-Agent") ?? "");
49
+ return { payload, parts: helpArticlePartsServer(html) };
48
50
  } catch (error) {
49
51
  log.warn("Help article unavailable", { appId, documentId, error: error instanceof Error ? error.message : String(error) });
50
52
  return null;
package/src/ssr/index.ts CHANGED
@@ -13,6 +13,7 @@ export {
13
13
  } from "./app-navigation";
14
14
  export { hintOpen, markHintSeen } from "./hints";
15
15
  export { default as Layout } from "./Layout";
16
+ export { LayoutHelpPanel, type LayoutHelpPanelProps } from "./LayoutHelpPanel.server";
16
17
  export {
17
18
  default as MinimalLayout,
18
19
  type MinimalLayoutPreferencePosition,
@@ -22,6 +23,6 @@ export { default as PanelLayout, type PanelLayoutProps } from "./PanelLayout";
22
23
  export { default as PwaLayout, type PwaLayoutProps } from "./PwaLayout";
23
24
  export { readPanelContext } from "./panel-context";
24
25
  export { requestPath } from "./request-path";
25
- export { getLocalizedRuntimeContext, getRuntimeContext, type RuntimeContext } from "./runtime";
26
+ export { getDisplayRuntimeContext, getLocalizedRuntimeContext, getRuntimeContext, type RuntimeContext } from "./runtime";
26
27
  export type { UrlFilterField } from "./url-filter";
27
28
  export { createUrlFilter, flag, list, oneOf, page, text } from "./url-filter";
@@ -4,20 +4,19 @@ export { provideWorkspaceNavigation } from "../workspace-navigation";
4
4
  export { default as SearchBar } from "./SearchBar";
5
5
 
6
6
  import { AppLaunchpadButton, AppLaunchpadProvider } from "../app-launchpad";
7
- import { LayoutHelpPage, LayoutHelpPanel } from "../LayoutHelp";
7
+ import { LayoutHelpPage } from "../LayoutHelp";
8
8
  import { LayoutHelpDocuments } from "../layout-help-registry";
9
9
 
10
10
  export type { AppLaunchpadApp, AppLaunchpadLegalLink } from "../app-launchpad";
11
11
  export { AppLaunchpadButton, AppLaunchpadProvider, openAppLaunchpad, setAppLaunchpadContext } from "../app-launchpad";
12
- export type { LayoutHelpPageProps, LayoutHelpPanelProps } from "../LayoutHelp";
13
- export { LayoutHelpPage, LayoutHelpPanel, openLayoutHelpDialog } from "../LayoutHelp";
12
+ export type { LayoutHelpPageProps } from "../LayoutHelp";
13
+ export { LayoutHelpPage, openLayoutHelpDialog } from "../LayoutHelp";
14
14
  export type { LayoutHelpDocumentsProps } from "../layout-help-registry";
15
15
  export { LayoutHelpDocuments } from "../layout-help-registry";
16
16
 
17
17
  export const Layout = {
18
18
  HelpDocuments: LayoutHelpDocuments,
19
19
  HelpPage: LayoutHelpPage,
20
- HelpPanel: LayoutHelpPanel,
21
20
  AppLaunchpadButton,
22
21
  AppLaunchpadProvider,
23
22
  };
@@ -1,10 +1,12 @@
1
1
  import type { PanelConsentSnapshot } from "../contracts/panel-context";
2
2
  import type { RailSnapshot } from "../contracts/rail-preferences";
3
3
  import type { User } from "../contracts/shared";
4
+ import type { AuthContext } from "../server/middleware/auth";
4
5
  import type { LayoutAnnouncementsState } from "../server/middleware/settings";
5
6
  import type { RuntimeContext } from "./runtime";
6
7
 
7
8
  export type LayoutContext = {
9
+ get(key: "actor"): AuthContext["Variables"]["actor"] | undefined;
8
10
  get(key: "user"): User | undefined;
9
11
  get(key: "panelConsent"): PanelConsentSnapshot | undefined;
10
12
  get(key: "railPreferences"): RailSnapshot | undefined;
@@ -0,0 +1,43 @@
1
+ import { Button, IconButton } from "@k2b/ui";
2
+ import type { JSX } from "solid-js";
3
+
4
+ /**
5
+ * The same button and tooltip as Dropdown.Trigger, without loading the menu implementation. It names no aria-controls:
6
+ * the menu it would point to exists only once the real Dropdown replaces it on activation.
7
+ */
8
+ export function HelpPanelMenuTrigger(props: {
9
+ label: string;
10
+ onClick?: JSX.EventHandler<HTMLButtonElement, MouseEvent>;
11
+ onKeyDown?: JSX.EventHandler<HTMLButtonElement, KeyboardEvent>;
12
+ onPointerEnter?: JSX.EventHandler<HTMLButtonElement, PointerEvent>;
13
+ onPointerDown?: JSX.EventHandler<HTMLButtonElement, PointerEvent>;
14
+ onFocus?: JSX.EventHandler<HTMLButtonElement, FocusEvent>;
15
+ }) {
16
+ return (
17
+ <span class="k2b-dropdown ">
18
+ <IconButton {...props} class="k2b-dropdown__trigger " aria-expanded="false" aria-haspopup="menu" data-help-menu="">
19
+ <i class="ti ti-dots" aria-hidden="true" />
20
+ </IconButton>
21
+ </span>
22
+ );
23
+ }
24
+ export function HelpPanelSectionTrigger(props: { label: string }) {
25
+ return (
26
+ <span class="k2b-dropdown ">
27
+ <Button
28
+ aria-expanded="false"
29
+ aria-haspopup="menu"
30
+ aria-label={props.label}
31
+ class=" k2b-dropdown__trigger "
32
+ size="sm"
33
+ tooltip={false}
34
+ variant="secondary"
35
+ data-help-sections=""
36
+ >
37
+ <i class="ti ti-list" aria-hidden="true" />
38
+ <span>{props.label}</span>
39
+ <i class="ti ti-chevron-down" aria-hidden="true" />
40
+ </Button>
41
+ </span>
42
+ );
43
+ }
@@ -0,0 +1,67 @@
1
+ import type { HelpArticleLink, HelpDocumentManifest, HelpDocumentPayload } from "../shared/help";
2
+ import { HELP_ACCESS_LEVELS, HELP_KINDS } from "../shared/help";
3
+ import type { HelpPanelState } from "./layout-help-panel-state";
4
+
5
+ const optionalString = (value: object, key: string): string | undefined => {
6
+ const field = Reflect.get(value, key);
7
+ return typeof field === "string" ? field : undefined;
8
+ };
9
+ /** A linked article from the Help API, or nothing when the server sent something else. */
10
+ const articleLink = (value: unknown): HelpArticleLink | undefined => {
11
+ if (!value || typeof value !== "object") return undefined;
12
+ const [appId, appName, documentId, title, url] = ["appId", "appName", "documentId", "title", "url"].map((key) =>
13
+ optionalString(value, key),
14
+ );
15
+ if (!appId || !appName || !documentId || !title || !url?.startsWith("/")) return undefined;
16
+ const [accent, description, icon] = ["accent", "description", "icon"].map((key) => optionalString(value, key));
17
+ return { appId, appName, accent, documentId, title, description, icon, url };
18
+ };
19
+
20
+ export const loadPanelDocument = async (
21
+ state: HelpPanelState,
22
+ topic: HelpDocumentManifest,
23
+ signal: AbortSignal,
24
+ t: { requestFailed: string; invalidDocument: string },
25
+ ) => {
26
+ const cacheKey = `${topic.locale ?? "en"}:${topic.url}`;
27
+ const cached = state.cache.get(cacheKey);
28
+ if (cached) return cached;
29
+
30
+ const response = await fetch(topic.url, { signal, headers: { Accept: "application/json" } });
31
+ if (!response.ok) throw new Error(t.requestFailed);
32
+ const value: unknown = await response.json();
33
+ if (
34
+ !value ||
35
+ typeof value !== "object" ||
36
+ !("id" in value) ||
37
+ value.id !== topic.id ||
38
+ !("html" in value) ||
39
+ !("markdown" in value) ||
40
+ !("title" in value) ||
41
+ ("locale" in value && value.locale !== undefined && typeof value.locale !== "string") ||
42
+ (topic.locale !== undefined && "locale" in value && value.locale !== undefined && value.locale !== topic.locale) ||
43
+ typeof value.html !== "string" ||
44
+ typeof value.markdown !== "string" ||
45
+ typeof value.title !== "string"
46
+ ) {
47
+ throw new Error(t.invalidDocument);
48
+ }
49
+ const kind = Reflect.get(value, "kind");
50
+ const needs = Reflect.get(value, "needs");
51
+ const related = Reflect.get(value, "related");
52
+ const next = articleLink(Reflect.get(value, "next"));
53
+ const document: HelpDocumentPayload = {
54
+ id: topic.id,
55
+ title: value.title,
56
+ html: value.html,
57
+ markdown: value.markdown,
58
+ locale: "locale" in value && typeof value.locale === "string" ? value.locale : (topic.locale ?? "en"),
59
+ // A Core older than the app's Help reader sends no article facts; the article then shows none.
60
+ kind: HELP_KINDS.find((candidate) => candidate === kind) ?? "task",
61
+ needs: HELP_ACCESS_LEVELS.find((level) => level === needs),
62
+ ...(next ? { next } : {}),
63
+ related: Array.isArray(related) ? related.flatMap((link) => articleLink(link) ?? []) : [],
64
+ };
65
+ state.storeDocument(topic, document);
66
+ return document;
67
+ };
@@ -0,0 +1,32 @@
1
+ const focused = new WeakMap<Element, string>();
2
+ /** Module evaluation precedes SSR's destructive island mount. */
3
+ export function capturePanelFocus() {
4
+ if (typeof document === "undefined") return;
5
+ const element = document.activeElement;
6
+ const wrapper = element?.closest("solid-island");
7
+ if (!wrapper || !(element instanceof HTMLElement)) return;
8
+ const topic = element.dataset.helpTopic;
9
+ const selector =
10
+ topic !== undefined
11
+ ? `[data-help-topic="${CSS.escape(topic)}"]`
12
+ : element.matches("[data-help-menu]")
13
+ ? "[data-help-menu]"
14
+ : element.matches(".help-head-back")
15
+ ? ".help-head-back"
16
+ : undefined;
17
+ if (selector) focused.set(wrapper, selector);
18
+ }
19
+ export function restorePanelFocus(element: Element) {
20
+ const wrapper = element.closest("solid-island");
21
+ const selector = wrapper && focused.get(wrapper);
22
+ if (!wrapper || !selector) return;
23
+ focused.delete(wrapper);
24
+ wrapper.querySelector<HTMLElement>(selector)?.focus({ preventScroll: true });
25
+ }
26
+
27
+ /** One provider reader per document; production island wrappers intentionally omit data-file. */
28
+ export function panelElement(): HTMLElement {
29
+ const element = document.querySelector<HTMLElement>('.help-shell[data-surface="panel"]');
30
+ if (!element) throw new Error("Missing Help panel");
31
+ return element;
32
+ }
@@ -0,0 +1,113 @@
1
+ import { clipboard } from "@k2b/stdlib/solid";
2
+ import { Dropdown, toast, useLocale } from "@k2b/ui";
3
+ import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
4
+ import { helpMessages } from "./help-messages";
5
+ import { formatHelpBundleMarkdown, formatHelpDocumentMarkdown } from "./layout-help-markdown";
6
+ import { loadPanelDocument } from "./layout-help-panel-document";
7
+ import type { HelpPanelState } from "./layout-help-panel-state";
8
+ import { layoutHelpTopicHref } from "./layout-help-url";
9
+
10
+ export type MenuActivation = { key: string; focus: boolean; pointer?: boolean };
11
+ export function HelpPanelMenu(props: { state: HelpPanelState; activation: MenuActivation }) {
12
+ const state = props.state;
13
+ const locale = useLocale();
14
+ const t = () => helpMessages.resolve([locale()]).t;
15
+ const write = (text: string) => navigator.clipboard.writeText(text);
16
+ const writer = clipboard.createWriter({ write, copiedFor: 1800 });
17
+ const allWriter = clipboard.createWriter({ write, copiedFor: 1800 });
18
+ createEffect(() => state.setAnnouncement(writer.wasCopied() ? t().articleCopiedAnnouncement : ""));
19
+ onCleanup(() => state.setAnnouncement(""));
20
+ const [copying, setCopying] = createSignal(false);
21
+ let controller: AbortController | undefined;
22
+ let trigger: HTMLButtonElement | undefined;
23
+ const topic = () => state.data.documents.find((topic) => topic.id === state.activeId());
24
+ const payload = () => {
25
+ const active = topic();
26
+ return active ? state.cachedDocument(active) : undefined;
27
+ };
28
+ const copyArticle = async () => {
29
+ const active = topic();
30
+ const document = payload();
31
+ if (!active || !document) return;
32
+ // Invoke the clipboard writer before the first await: Safari needs the menu item's user activation.
33
+ if (await writer.copy(formatHelpDocumentMarkdown({ ...active, markdown: document.markdown }))) {
34
+ toast.success(t().articleCopied);
35
+ } else
36
+ toast.error(t().copyFailed({ message: writer.error()?.message ?? t().copyFallback }), {
37
+ action: { label: t().tryAgain, onClick: () => void copyArticle() },
38
+ });
39
+ };
40
+ const copyAll = async () => {
41
+ if (!state.data.documents.length || copying()) return;
42
+ controller?.abort();
43
+ const current = new AbortController();
44
+ controller = current;
45
+ setCopying(true);
46
+ const progress = toast(t().preparing, { progress: "indeterminate", duration: 0, iconClass: "ti ti-markdown" });
47
+ try {
48
+ const documents = await Promise.all(
49
+ state.data.documents.map(async (topic) => ({
50
+ ...topic,
51
+ markdown: (await loadPanelDocument(state, topic, current.signal, t())).markdown,
52
+ })),
53
+ );
54
+ if (!(await allWriter.copy(formatHelpBundleMarkdown(documents)))) throw allWriter.error() ?? new Error(t().copyFallback);
55
+ progress.dismiss();
56
+ toast.success(t().helpCopied);
57
+ } catch (error) {
58
+ progress.dismiss();
59
+ if (!(error instanceof DOMException && error.name === "AbortError"))
60
+ toast.error(t().copyAllFailed({ message: error instanceof Error ? error.message : t().copyFallback }), {
61
+ action: { label: t().tryAgain, onClick: () => void copyAll() },
62
+ });
63
+ } finally {
64
+ if (controller === current) {
65
+ controller = undefined;
66
+ setCopying(false);
67
+ }
68
+ }
69
+ };
70
+ const [quiet, setQuiet] = createSignal(!!props.activation.pointer);
71
+ onCleanup(() => controller?.abort());
72
+ onMount(() => {
73
+ if (props.activation.focus) trigger?.focus({ preventScroll: true });
74
+ trigger?.dispatchEvent(new KeyboardEvent("keydown", { key: props.activation.key, bubbles: true, cancelable: true }));
75
+ });
76
+ const items = () => [
77
+ {
78
+ label: t().copyAsMarkdown,
79
+ icon: "ti ti-markdown",
80
+ ...(copying() || (state.view() === "article" ? !payload() : !state.data.documents.length)
81
+ ? { disabled: true as const }
82
+ : { action: () => void (state.view() === "article" ? copyArticle() : copyAll()) }),
83
+ },
84
+ ...(state.data.pageBase
85
+ ? [
86
+ {
87
+ label: t().openAsPage,
88
+ icon: "ti ti-app-window",
89
+ href: layoutHelpTopicHref(state.data.pageBase, state.view() === "article" ? state.activeId() : null),
90
+ },
91
+ ]
92
+ : []),
93
+ ];
94
+ return (
95
+ <Dropdown.Root label={t().moreActions} position="bottom-left" items={items()}>
96
+ <Dropdown.Trigger
97
+ ref={(element) => {
98
+ trigger = element;
99
+ }}
100
+ iconOnly
101
+ label={t().moreActions}
102
+ variant="ghost"
103
+ data-help-menu=""
104
+ // A press on the light trigger counts as a press here: like Dropdown.Trigger's own, its hint stays closed until
105
+ // the pointer leaves.
106
+ tooltip={quiet() ? false : undefined}
107
+ onPointerLeave={() => setQuiet(false)}
108
+ >
109
+ <i class="ti ti-dots" aria-hidden="true" />
110
+ </Dropdown.Trigger>
111
+ </Dropdown.Root>
112
+ );
113
+ }
@@ -0,0 +1,7 @@
1
+ // Keep importOnDemand() from ../browser/reload out: its eager toast import would break the panel bundle budget.
2
+ export const helpPanelModules = {
3
+ menu: () => import("./layout-help-panel-menu"),
4
+ search: () => import("./layout-help-panel-search"),
5
+ reader: () => import("./layout-help-panel-reader"),
6
+ document: () => import("./layout-help-panel-document"),
7
+ };
@@ -0,0 +1,159 @@
1
+ import { Dropdown, LocaleProvider, MarkdownView, TableOfContents, useLocale } from "@k2b/ui";
2
+ import { type Accessor, createMemo, createSignal, onMount } from "solid-js";
3
+ import { render } from "solid-js/web";
4
+ import type { HelpDocumentPayload } from "../shared/help";
5
+ import { helpMessages } from "./help-messages";
6
+ import { helpArticleParts } from "./layout-help-article";
7
+ import type { HelpPanelState } from "./layout-help-panel-state";
8
+ import type { HelpTopic } from "./layout-help-registry";
9
+ import { HelpArticleView } from "./layout-help-views";
10
+
11
+ function Sections(props: {
12
+ sections: { id: string; label: string }[];
13
+ onSection: (id: string) => void;
14
+ active: Accessor<string | null>;
15
+ activation?: { key: string; focused: boolean };
16
+ }) {
17
+ const locale = useLocale();
18
+ const t = () => helpMessages.resolve([locale()]).t;
19
+ let trigger: HTMLButtonElement | undefined;
20
+ onMount(() => {
21
+ if (!props.activation) return;
22
+ if (props.activation.focused) trigger?.focus({ preventScroll: true });
23
+ trigger?.dispatchEvent(new KeyboardEvent("keydown", { key: props.activation.key, bubbles: true, cancelable: true }));
24
+ });
25
+ return (
26
+ <Dropdown.Root
27
+ label={t().onThisPage}
28
+ position="bottom-left"
29
+ items={props.sections.map((section) => ({
30
+ label: section.label,
31
+ choice: "radio" as const,
32
+ checked: () => props.active() === section.id,
33
+ action: () => {
34
+ props.onSection(section.id);
35
+ },
36
+ }))}
37
+ >
38
+ <Dropdown.Trigger
39
+ ref={(element) => {
40
+ trigger = element;
41
+ }}
42
+ label={t().onThisPage}
43
+ size="sm"
44
+ tooltip={false}
45
+ variant="secondary"
46
+ >
47
+ <i class="ti ti-list" aria-hidden="true" />
48
+ <span>{t().onThisPage}</span>
49
+ <i class="ti ti-chevron-down" aria-hidden="true" />
50
+ </Dropdown.Trigger>
51
+ </Dropdown.Root>
52
+ );
53
+ }
54
+ export function HelpPanelReader(props: {
55
+ state: HelpPanelState;
56
+ topic: HelpTopic;
57
+ document: HelpDocumentPayload;
58
+ onSection: (id: string) => void;
59
+ }) {
60
+ const locale = useLocale();
61
+ const t = () => helpMessages.resolve([locale()]).t;
62
+ const parts = createMemo(() => helpArticleParts(props.document.html));
63
+ const [active, setActive] = createSignal<string | null>(parts().sections[0]?.id ?? null);
64
+ let content: HTMLDivElement | undefined;
65
+ return (
66
+ <HelpArticleView
67
+ surface="panel"
68
+ topic={props.topic}
69
+ appId={props.state.data.appId}
70
+ parts={parts()}
71
+ document={props.document}
72
+ Markdown={MarkdownView}
73
+ showTools={parts().sections.length > 0}
74
+ sectionMenu={
75
+ parts().sections.length ? (
76
+ <Sections
77
+ sections={parts().sections}
78
+ active={active}
79
+ onSection={(id) => {
80
+ setActive(id);
81
+ props.onSection(id);
82
+ }}
83
+ />
84
+ ) : null
85
+ }
86
+ copyButton={null}
87
+ loading={null}
88
+ failure={null}
89
+ toc={
90
+ <TableOfContents
91
+ items={parts().sections}
92
+ title={t().onThisPage}
93
+ scrollRoot={() => props.state.scroller() ?? undefined}
94
+ contentRoot={() => content}
95
+ onActiveChange={(id) => setActive(id ?? null)}
96
+ />
97
+ }
98
+ copyAction={null}
99
+ headingRef={() => {}}
100
+ contentRef={(element) => {
101
+ content = element;
102
+ }}
103
+ onBack={props.state.back}
104
+ />
105
+ );
106
+ }
107
+ export function mountPanelSections(
108
+ container: HTMLElement,
109
+ props: { locale: string; state: HelpPanelState; key: string; focused: boolean; onSection: (id: string) => void },
110
+ ): () => void {
111
+ const article = container.closest("article");
112
+ const sections = Array.from(article?.querySelectorAll<HTMLAnchorElement>(".help-article-aside a[href^='#']") ?? []).map((link) => ({
113
+ id: decodeURIComponent(link.getAttribute("href")!.slice(1)),
114
+ label: link.textContent ?? "",
115
+ }));
116
+ const [active, setActive] = createSignal<string | null>(sections[0]?.id ?? null);
117
+ const aside = article?.querySelector<HTMLElement>(".help-article-aside-sticky");
118
+ let disposeToc = () => {};
119
+ // The aside is hidden at every panel width. Upgrade its scroll spy only when the section menu needs it.
120
+ if (aside) {
121
+ aside.textContent = "";
122
+ disposeToc = render(
123
+ () => (
124
+ <LocaleProvider locale={props.locale}>
125
+ <TableOfContents
126
+ items={sections}
127
+ title={helpMessages.resolve([props.locale]).t.onThisPage}
128
+ scrollRoot={() => props.state.scroller() ?? undefined}
129
+ contentRoot={() => article ?? undefined}
130
+ onActiveChange={(id) => setActive(id ?? null)}
131
+ />
132
+ </LocaleProvider>
133
+ ),
134
+ aside,
135
+ );
136
+ }
137
+ container.textContent = "";
138
+ container.style.display = "contents";
139
+ const disposeMenu = render(
140
+ () => (
141
+ <LocaleProvider locale={props.locale}>
142
+ <Sections
143
+ sections={sections}
144
+ active={active}
145
+ onSection={(id) => {
146
+ setActive(id);
147
+ props.onSection(id);
148
+ }}
149
+ activation={{ key: props.key, focused: props.focused }}
150
+ />
151
+ </LocaleProvider>
152
+ ),
153
+ container,
154
+ );
155
+ return () => {
156
+ disposeMenu();
157
+ disposeToc();
158
+ };
159
+ }
@@ -0,0 +1,69 @@
1
+ import { useLocale } from "@k2b/ui";
2
+ import { createEffect, createMemo, createSignal, onCleanup, Show } from "solid-js";
3
+ import { helpMessages } from "./help-messages";
4
+ import type { HelpPanelState } from "./layout-help-panel-state";
5
+ import { searchHelpTopics } from "./layout-help-search";
6
+ import { HelpResultsView } from "./layout-help-views";
7
+
8
+ export function HelpPanelSearchResults(props: { state: HelpPanelState }) {
9
+ const locale = useLocale();
10
+ const t = () => helpMessages.resolve([locale()]).t;
11
+ const query = createMemo(() => props.state.query().trim().toLocaleLowerCase());
12
+ const [ids, setIds] = createSignal<readonly string[] | null>(null);
13
+ const [searching, setSearching] = createSignal(false);
14
+ const [failed, setFailed] = createSignal(false);
15
+ const [attempt, setAttempt] = createSignal(0);
16
+ let version = 0;
17
+ createEffect(() => {
18
+ const value = query();
19
+ attempt();
20
+ const current = ++version;
21
+ setIds(null);
22
+ setSearching(false);
23
+ setFailed(false);
24
+ const urls = [...new Set(props.state.data.documents.map((topic) => topic.searchUrl))];
25
+ if (!value || !urls.length) return;
26
+ const controller = new AbortController();
27
+ const timer = setTimeout(() => {
28
+ setSearching(true);
29
+ void Promise.all(
30
+ urls.map(async (url) => {
31
+ const response = await fetch(`${url}?q=${encodeURIComponent(value)}`, {
32
+ signal: controller.signal,
33
+ headers: { Accept: "application/json" },
34
+ });
35
+ if (!response.ok) throw new Error(t().searchFailed);
36
+ const payload: unknown = await response.json();
37
+ return payload && typeof payload === "object" && "ids" in payload && Array.isArray(payload.ids)
38
+ ? payload.ids.filter((id): id is string => typeof id === "string")
39
+ : [];
40
+ }),
41
+ )
42
+ .then((groups) => {
43
+ if (current === version) setIds([...new Set(groups.flat())]);
44
+ })
45
+ .catch((error) => {
46
+ if (!(error instanceof DOMException && error.name === "AbortError") && current === version) setFailed(true);
47
+ })
48
+ .finally(() => {
49
+ if (current === version) setSearching(false);
50
+ });
51
+ }, 120);
52
+ onCleanup(() => {
53
+ clearTimeout(timer);
54
+ controller.abort();
55
+ if (current === version) version++;
56
+ });
57
+ });
58
+ return (
59
+ <Show when={query()}>
60
+ <HelpResultsView
61
+ results={searchHelpTopics(props.state.data.documents, query(), ids())}
62
+ searching={searching()}
63
+ failed={failed()}
64
+ onRetry={() => setAttempt((value) => value + 1)}
65
+ onOpen={() => {}}
66
+ />
67
+ </Show>
68
+ );
69
+ }
@@ -0,0 +1,7 @@
1
+ import { getContext } from "../browser/panel";
2
+ import { panelShortcutsReceived, readPanelShortcuts } from "../browser/panel-shortcuts";
3
+ import type { HelpShortcutCommand } from "./layout-help-start";
4
+
5
+ /** An empty host list is authoritative too, including when a reader remounts. */
6
+ export const readHelpPanelShortcuts = (seed: HelpShortcutCommand[]): HelpShortcutCommand[] =>
7
+ panelShortcutsReceived() && getContext() !== null ? readPanelShortcuts() : seed;