@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.
- package/package.json +9 -5
- package/src/_internal/bounded-json.ts +14 -2
- package/src/_internal/capabilities.ts +87 -8
- package/src/_internal/capability-reviews.ts +6 -0
- package/src/_internal/capability-sentences.ts +2 -1
- package/src/_internal/capability-timeout.ts +71 -0
- package/src/_internal/define-app.ts +158 -92
- package/src/_internal/heartbeat.ts +203 -14
- package/src/_internal/http-drain.ts +56 -0
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/registry.ts +28 -1
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/setup-lock.ts +103 -0
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +16 -17
- package/src/access/PrincipalPicker.tsx +45 -10
- package/src/access/messages.ts +8 -0
- package/src/access/principal-label.ts +18 -0
- package/src/access/ui.ts +1 -0
- package/src/account/EntitySearch.tsx +5 -4
- package/src/ai/action-review.ts +2 -1
- package/src/ai/app-skill-store.ts +1 -1
- package/src/ai/capabilities.ts +21 -3
- package/src/ai/capability-execution.ts +14 -12
- package/src/ai/chat/action-card.ts +76 -4
- package/src/ai/chat/action-messages.ts +14 -0
- package/src/ai/chat/action-question.browser-harness.tsx +12 -0
- package/src/ai/chat/action-question.tsx +214 -23
- package/src/ai/chat/action-text.ts +288 -0
- package/src/ai/chat/tool-action-words.ts +31 -0
- package/src/ai/chat/tool-action.tsx +1 -26
- package/src/ai/chat-tasks.ts +2 -1
- package/src/ai/client/controller.ts +150 -32
- package/src/ai/client/transport.ts +14 -2
- package/src/ai/code-capability-routes.ts +27 -23
- package/src/ai/code-runtime-tools.ts +14 -16
- package/src/ai/code-source-tools.ts +16 -18
- package/src/ai/executor.ts +64 -21
- package/src/ai/fetch-file-tool.ts +67 -76
- package/src/ai/firecrawl-tools.ts +15 -2
- package/src/ai/http.ts +1 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/migrate.ts +28 -0
- package/src/ai/open-questions.ts +168 -0
- package/src/ai/projects.ts +1 -2
- package/src/ai/protocol.ts +2 -2
- package/src/ai/question-text.ts +8 -0
- package/src/ai/recent-resources.ts +12 -5
- package/src/ai/review-content.ts +77 -0
- package/src/ai/routes.ts +119 -35
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skill-tool.ts +17 -6
- package/src/ai/skills-routes.ts +72 -27
- package/src/ai/skills.ts +139 -60
- package/src/ai/store.ts +184 -31
- package/src/ai/testing.ts +36 -0
- package/src/ai/tool-sentences.ts +152 -34
- package/src/ai/tool-wording.ts +12 -6
- package/src/ai/tools.ts +8 -3
- package/src/ai/types.ts +25 -4
- package/src/ai/usage.ts +2 -2
- package/src/api/admin-core-settings.ts +15 -2
- package/src/api/admin-geo.ts +54 -0
- package/src/api/admin-linux-identities.ts +6 -3
- package/src/api/admin-outgoing-mail.ts +29 -15
- package/src/api/app-approval-actions.ts +31 -7
- package/src/api/approvals-loader.ts +3 -3
- package/src/api/approvals.ts +51 -0
- package/src/api/apps.ts +4 -2
- package/src/api/auth/schemas.ts +3 -0
- package/src/api/auth.ts +54 -18
- package/src/api/capabilities.ts +258 -181
- package/src/api/capability-streams.ts +21 -13
- package/src/api/cli-plugins.ts +4 -1
- package/src/api/geo-glyphs.ts +11 -0
- package/src/api/geo.ts +128 -0
- package/src/api/help.ts +12 -4
- package/src/api/index.ts +5 -0
- package/src/api/mcp.ts +73 -41
- package/src/api/me-panel-consent.ts +8 -5
- package/src/api/me.ts +79 -3
- package/src/api/search.ts +21 -14
- package/src/api/sync-ops.ts +19 -13
- package/src/api/widgets.ts +42 -18
- package/src/approvals/Approvals.tsx +30 -66
- package/src/approvals/approval-card.ts +68 -0
- package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
- package/src/browser/CloudResourceSearch.tsx +3 -13
- package/src/browser/PanelLeaveNotice.tsx +18 -0
- package/src/browser/navigation-search.ts +78 -25
- package/src/browser/panel-leave.ts +69 -0
- package/src/browser/panel-messages.ts +12 -0
- package/src/browser/panel-slot.ts +173 -24
- package/src/browser/panel.ts +3 -0
- package/src/browser/resource-search-messages.ts +2 -0
- package/src/capabilities/client.ts +28 -1
- package/src/capabilities/executions.ts +22 -3
- package/src/capabilities/geo-client.ts +57 -0
- package/src/capabilities/response.ts +2 -1
- package/src/capabilities/review-storage.ts +24 -0
- package/src/capabilities/reviews.ts +17 -3
- package/src/capabilities/server.ts +36 -16
- package/src/cli/access.ts +15 -23
- package/src/cli/admin/linux.ts +13 -2
- package/src/cli/admin/outgoing-mail.ts +26 -4
- package/src/cli/capabilities.ts +19 -0
- package/src/config/env.ts +0 -6
- package/src/contracts/app-access.ts +48 -0
- package/src/contracts/app-approval.ts +3 -3
- package/src/contracts/app.ts +4 -1
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/avatar.ts +186 -0
- package/src/contracts/capabilities.ts +149 -55
- package/src/contracts/geo-admin.ts +127 -0
- package/src/contracts/geo-map.ts +19 -0
- package/src/contracts/geo-route.ts +168 -0
- package/src/contracts/geo.ts +248 -0
- package/src/contracts/index.ts +4 -0
- package/src/contracts/outgoing-mail.ts +17 -4
- package/src/contracts/registry.ts +2 -0
- package/src/contracts/shared.ts +3 -0
- package/src/events/app-access.ts +68 -0
- package/src/events/live-delivery.ts +128 -0
- package/src/events/live.ts +14 -130
- package/src/geo/CloudMap.tsx +95 -0
- package/src/geo/map-messages.ts +17 -0
- package/src/geo/ui.ts +1 -0
- package/src/index.ts +2 -0
- package/src/server/client-ip.ts +36 -0
- package/src/server/index.ts +3 -4
- package/src/server/middleware/app-access.ts +312 -0
- package/src/server/middleware/auth.ts +5 -3
- package/src/server/middleware/invocation.ts +11 -0
- package/src/server/middleware/rate-limit.ts +4 -4
- package/src/server/services/access.ts +13 -9
- package/src/server/services/index.ts +0 -2
- package/src/server/services/services.ts +0 -2
- package/src/services/accounts/app.ts +23 -3
- package/src/services/accounts/posix.ts +21 -7
- package/src/services/accounts/users.ts +12 -1
- package/src/services/allowed-network-fetch.ts +16 -10
- package/src/services/app-access-metrics.ts +24 -0
- package/src/services/app-access.ts +123 -0
- package/src/services/app-discovery.ts +27 -0
- package/src/services/approvals/content.ts +40 -0
- package/src/services/approvals/device.ts +49 -33
- package/src/services/approvals/index.ts +15 -6
- package/src/services/approvals/list.ts +6 -11
- package/src/services/approvals/question.ts +72 -0
- package/src/services/audit/index.ts +1 -0
- package/src/services/auth-flows/ipa.ts +39 -2
- package/src/services/auth-flows/password-reset.ts +32 -6
- package/src/services/downstream-budget.ts +87 -0
- package/src/services/geo/admin.ts +262 -0
- package/src/services/geo/bucket.ts +67 -0
- package/src/services/geo/cache.ts +57 -0
- package/src/services/geo/costs.ts +5 -0
- package/src/services/geo/errors.ts +41 -0
- package/src/services/geo/fixtures/README.md +17 -0
- package/src/services/geo/fixtures/along.json +355 -0
- package/src/services/geo/fixtures/autocomplete.json +218 -0
- package/src/services/geo/fixtures/geocode.json +129 -0
- package/src/services/geo/fixtures/places.json +355 -0
- package/src/services/geo/fixtures/reverse.json +61 -0
- package/src/services/geo/fixtures/routing.json +57 -0
- package/src/services/geo/geoapify-route.ts +93 -0
- package/src/services/geo/geoapify.ts +281 -0
- package/src/services/geo/ledger.ts +122 -0
- package/src/services/geo/map.ts +160 -0
- package/src/services/geo/migrate.ts +25 -0
- package/src/services/geo/polyline.ts +184 -0
- package/src/services/geo/route-chain.ts +145 -0
- package/src/services/geo/route-costs.ts +9 -0
- package/src/services/geo/route-geometry.ts +39 -0
- package/src/services/geo/route-plan.ts +172 -0
- package/src/services/geo/route-service.ts +433 -0
- package/src/services/geo/service.ts +300 -0
- package/src/services/geo/settings.ts +49 -0
- package/src/services/help/index.ts +16 -7
- package/src/services/help/types.ts +6 -1
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/issue-invocation.ts +67 -0
- package/src/services/identity/key-ring.ts +5 -3
- package/src/services/identity/runtime-config.ts +23 -0
- package/src/services/index.ts +11 -0
- package/src/services/mandates/index.ts +40 -3
- package/src/services/network-security.ts +6 -3
- package/src/services/network-transport.ts +99 -0
- package/src/services/notifications/catalog.ts +3 -2
- package/src/services/notifications/runtime.ts +1 -0
- package/src/services/notifications/web-push-transport.ts +45 -38
- package/src/services/outgoing-mail/dispatcher.ts +6 -1
- package/src/services/outgoing-mail/index.ts +55 -1
- package/src/services/outgoing-mail/messages.ts +82 -3
- package/src/services/outgoing-mail/resolve-host.ts +2 -2
- package/src/services/outgoing-mail/retention.ts +13 -1
- package/src/services/outgoing-mail/store.ts +36 -6
- package/src/services/outgoing-mail/test-send.ts +34 -4
- package/src/services/outgoing-mail/transport.ts +1 -1
- package/src/services/pdf/gotenberg.ts +11 -38
- package/src/services/pdf/markdown.ts +160 -2
- package/src/services/pdf/template-preview.ts +1 -1
- package/src/services/public-http.ts +28 -40
- package/src/services/session/index.ts +203 -29
- package/src/services/settings/app.ts +4 -1
- package/src/services/settings/core-settings.ts +88 -0
- package/src/services/settings/defaults.ts +8 -2
- package/src/services/settings/errors.ts +11 -0
- package/src/services/settings/geo-budget.ts +40 -0
- package/src/services/settings/index.ts +4 -3
- package/src/services/settings/store.ts +25 -17
- package/src/services/weather/geo.ts +29 -111
- package/src/services/weather/index.ts +4 -3
- package/src/services/weather/locations.ts +8 -5
- package/src/services/weather/migrate.ts +1 -0
- package/src/services/weather/settings.ts +0 -16
- package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
- package/src/shared/avatar-art.ts +400 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/network-address.ts +22 -3
- package/src/shared/template-rendering.ts +36 -4
- package/src/shared-text/documents.ts +1 -1
- package/src/shared-text/index.ts +2 -0
- package/src/shared-text/log.ts +51 -3
- package/src/shared-text/snapshots.ts +340 -0
- package/src/ssr/AdminLayout.tsx +4 -2
- package/src/ssr/CloudShell.island.tsx +6 -0
- package/src/ssr/HelpPanelArticle.island.tsx +308 -0
- package/src/ssr/HelpPanelBack.island.tsx +32 -0
- package/src/ssr/HelpPanelMenu.island.tsx +133 -0
- package/src/ssr/HelpPanelSearch.island.tsx +123 -0
- package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
- package/src/ssr/HelpPanelView.island.tsx +27 -0
- package/src/ssr/Layout.tsx +65 -15
- package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
- package/src/ssr/LayoutHelp.tsx +185 -542
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
- package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
- package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
- package/src/ssr/PanelRuntime.island.tsx +11 -7
- package/src/ssr/PwaLayout.tsx +2 -2
- package/src/ssr/RailApps.tsx +6 -0
- package/src/ssr/ShellRuntime.island.tsx +23 -7
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
- package/src/ssr/admin-navigation.ts +1 -0
- package/src/ssr/app-badges.ts +6 -1
- package/src/ssr/compact-shell.browser-harness.ts +44 -5
- package/src/ssr/help.ts +7 -5
- package/src/ssr/index.ts +2 -1
- package/src/ssr/islands/index.ts +3 -4
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-panel-controls.tsx +43 -0
- package/src/ssr/layout-help-panel-document.ts +67 -0
- package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
- package/src/ssr/layout-help-panel-menu.tsx +113 -0
- package/src/ssr/layout-help-panel-modules.ts +7 -0
- package/src/ssr/layout-help-panel-reader.tsx +159 -0
- package/src/ssr/layout-help-panel-search.tsx +69 -0
- package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
- package/src/ssr/layout-help-panel-state.ts +141 -0
- package/src/ssr/layout-help-panel-types.ts +19 -0
- package/src/ssr/layout-help-views.tsx +484 -0
- package/src/ssr/mobile-menu-history.ts +4 -0
- package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
- package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
- package/src/ssr/page-swap.browser-harness.tsx +177 -0
- package/src/ssr/page-swap.ts +28 -0
- package/src/ssr/page-trail-runtime.ts +32 -13
- package/src/ssr/panel-consent.ts +2 -2
- package/src/ssr/panel-idle.ts +9 -0
- package/src/ssr/panel-slot.browser-harness.tsx +3 -0
- package/src/ssr/platform-messages.ts +8 -0
- package/src/ssr/runtime.ts +17 -0
- package/src/ssr/shell-data.tsx +10 -2
- package/src/styles/approvals.css +6 -0
- package/src/styles/effects.css +15 -0
- package/src/styles/global.css +70 -6
- 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
|
-
|
|
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(
|
|
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";
|
package/src/ssr/islands/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
13
|
-
export { LayoutHelpPage,
|
|
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;
|