@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.
- package/README.md +8 -0
- package/package.json +11 -4
- package/src/_internal/app-favicon.ts +4 -2
- package/src/_internal/capabilities.ts +72 -48
- package/src/_internal/capability-reviews.ts +81 -0
- package/src/_internal/capability-schemas.ts +29 -0
- package/src/_internal/capability-sentences.ts +217 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +49 -18
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/heartbeat.ts +203 -14
- package/src/_internal/registry.ts +28 -1
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/setup-lock.ts +103 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +2 -2
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/background-review.ts +72 -0
- package/src/ai/capabilities.ts +29 -11
- package/src/ai/capability-execution.ts +76 -43
- package/src/ai/capability-result.ts +95 -0
- package/src/ai/chat/action-card.ts +464 -0
- package/src/ai/chat/action-messages.ts +129 -0
- package/src/ai/chat/action-question.browser-harness.tsx +245 -0
- package/src/ai/chat/action-question.fixtures.ts +733 -0
- package/src/ai/chat/action-question.tsx +680 -0
- package/src/ai/chat/action-text.ts +288 -0
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action-words.ts +31 -0
- package/src/ai/chat/tool-action.tsx +456 -0
- package/src/ai/chat/turn-view.tsx +10 -160
- package/src/ai/chat/view-block.tsx +15 -5
- package/src/ai/chat-task-contracts.ts +4 -1
- package/src/ai/chat-task-reasons-migrate.ts +7 -0
- package/src/ai/chat-task-reasons.ts +1 -1
- package/src/ai/chat-tasks.ts +55 -25
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +198 -39
- package/src/ai/firecrawl-tools.ts +15 -2
- package/src/ai/index.ts +3 -0
- package/src/ai/migrate.ts +26 -2
- package/src/ai/open-questions.ts +172 -0
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/question-text.ts +8 -0
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/routes.ts +195 -5
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +245 -24
- package/src/ai/testing.ts +36 -0
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-sentences.ts +152 -34
- package/src/ai/tool-wording.ts +36 -8
- package/src/ai/tools.ts +8 -3
- package/src/ai/types.ts +43 -4
- package/src/api/accounts-entities.ts +2 -1
- package/src/api/admin-ai-projects.ts +2 -0
- package/src/api/admin-ai-skills.ts +2 -0
- package/src/api/admin-ai-usage.ts +2 -0
- package/src/api/admin-geoip.ts +80 -0
- package/src/api/admin-identity.ts +3 -0
- package/src/api/admin-lifecycle.ts +3 -0
- package/src/api/admin-outgoing-mail.ts +2 -0
- package/src/api/app-approval-actions.ts +31 -7
- package/src/api/app-approval.ts +46 -5
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +2 -0
- package/src/api/auth/schemas.ts +3 -0
- package/src/api/auth.ts +67 -12
- package/src/api/capabilities.ts +44 -11
- package/src/api/capability-streams.ts +12 -0
- package/src/api/help.ts +4 -28
- package/src/api/index.ts +6 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +30 -3
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +159 -36
- package/src/api/search.ts +3 -1
- package/src/api/sync-ops.ts +2 -0
- package/src/api/widgets.ts +3 -0
- package/src/approvals/Approvals.tsx +166 -262
- package/src/approvals/approval-card.ts +68 -0
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/app-approval.ts +54 -5
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +45 -3
- package/src/browser/composer-insert.ts +10 -0
- package/src/browser/composer-open-messages.ts +17 -0
- package/src/browser/composer-open.ts +110 -0
- package/src/browser/discoverable-command.ts +6 -0
- package/src/browser/hints.ts +6 -0
- package/src/browser/live-connection.ts +26 -2
- package/src/browser/navigation-apps.ts +7 -0
- package/src/browser/panel-context-bridge.ts +30 -0
- package/src/browser/panel-context-receiver.ts +53 -0
- package/src/browser/panel-menu.tsx +56 -0
- package/src/browser/panel-messages.ts +11 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +412 -10
- package/src/browser/panel.ts +37 -0
- package/src/browser/search-commands.ts +6 -3
- package/src/capabilities/client.ts +8 -2
- package/src/capabilities/reviews.ts +205 -0
- package/src/capabilities/server.ts +10 -3
- package/src/capabilities/testing.ts +47 -0
- package/src/capabilities/types.ts +2 -2
- package/src/cli/account.ts +13 -3
- package/src/cli/admin/geoip.ts +115 -0
- package/src/cli/admin/index.ts +3 -0
- package/src/contracts/app-approval.ts +123 -16
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/capabilities.ts +410 -18
- package/src/contracts/geoip.ts +42 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/panel-context-plain.ts +99 -0
- package/src/contracts/panel-context.ts +93 -0
- package/src/contracts/seen-hints.ts +35 -0
- package/src/contracts/shared.ts +3 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +13 -1
- package/src/server/middleware/middleware.ts +9 -0
- package/src/server/middleware/no-transform.ts +19 -0
- package/src/server/middleware/rate-limit.ts +4 -6
- package/src/server/request-context/address.ts +45 -0
- package/src/server/request-context/geoip.ts +380 -0
- package/src/server/request-context/index.ts +33 -0
- package/src/server/request-context/user-agent.ts +95 -0
- package/src/services/accounts/app.ts +23 -3
- package/src/services/accounts/users.ts +12 -1
- package/src/services/app-approval.ts +382 -59
- package/src/services/approvals/device.ts +49 -31
- package/src/services/approvals/index.ts +7 -5
- package/src/services/approvals/list.ts +27 -6
- package/src/services/approvals/question.ts +68 -0
- package/src/services/audit/index.ts +1 -0
- package/src/services/auth-flows/ipa.ts +39 -2
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/auth-flows/password-reset.ts +32 -6
- package/src/services/geoip/index.ts +164 -0
- package/src/services/geoip/store.ts +30 -0
- package/src/services/geoip/upload.ts +150 -0
- package/src/services/identity/routes.ts +5 -1
- package/src/services/index.ts +13 -0
- package/src/services/known-browsers.ts +312 -0
- package/src/services/login-address-store.ts +51 -0
- package/src/services/login-context.ts +118 -0
- package/src/services/mandates/index.ts +3 -0
- package/src/services/mandates/policy.ts +15 -3
- package/src/services/notifications/email-frame.ts +1 -1
- package/src/services/oauth-tokens.ts +16 -2
- package/src/services/panel-context-consent.ts +58 -0
- package/src/services/pdf/markdown.ts +160 -2
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +296 -41
- package/src/services/session/recent.ts +13 -0
- package/src/services/webauthn.ts +12 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -0
- package/src/shared-text/cursor.ts +22 -0
- package/src/shared-text/documents.ts +142 -0
- package/src/shared-text/events.ts +43 -0
- package/src/shared-text/index.ts +12 -0
- package/src/shared-text/log.ts +48 -0
- package/src/shared-text/text-diff.ts +23 -0
- package/src/shared-text/types.ts +41 -0
- package/src/ssr/AdminLayout.tsx +2 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +10 -2
- 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 +25 -2
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +222 -419
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +135 -0
- package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
- package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +9 -1
- package/src/ssr/PanelRuntime.island.tsx +7 -0
- package/src/ssr/PanelSlot.tsx +53 -3
- package/src/ssr/ShellRuntime.island.tsx +11 -2
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
- package/src/ssr/app-color.ts +14 -9
- package/src/ssr/app-navigation.ts +16 -3
- package/src/ssr/compact-shell.browser-harness.ts +30 -22
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +21 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +3 -0
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -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-start.ts +33 -7
- package/src/ssr/layout-help-views.tsx +484 -0
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-idle.ts +9 -0
- package/src/ssr/panel-slot.browser-harness.tsx +53 -4
- package/src/ssr/platform-messages.ts +18 -0
- package/src/ssr/shell-data.tsx +2 -1
- package/src/styles/approvals.css +6 -0
- package/src/styles/effects.css +213 -9
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +49 -12
- package/src/styles/panel.css +47 -4
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +17 -11
- package/src/styles/utilities-buttons.css +3 -2
- package/src/styles/utilities-data.css +2 -1
- package/src/styles/utilities-feedback.css +2 -2
- package/src/styles/utilities-layout.css +3 -13
- package/src/styles/utilities-markdown-editor.css +6 -6
- package/src/styles/utilities-navigation.css +17 -22
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { Button, NoticeCard, Placeholder, prefersReducedMotion, useLocale } from "@k2b/ui";
|
|
2
|
+
import { createEffect, createSignal, onCleanup, onMount, Show } from "solid-js";
|
|
3
|
+
import type { HelpDocumentPayload } from "../shared/help";
|
|
4
|
+
import { helpMessages } from "./help-messages";
|
|
5
|
+
import { panelElement } from "./layout-help-panel-lifecycle";
|
|
6
|
+
import { helpPanelModules } from "./layout-help-panel-modules";
|
|
7
|
+
import { type HelpPanelState, helpPanelState } from "./layout-help-panel-state";
|
|
8
|
+
import type { HelpTopic } from "./layout-help-registry";
|
|
9
|
+
import { helpLinkTarget } from "./layout-help-url";
|
|
10
|
+
import { HelpArticleView } from "./layout-help-views";
|
|
11
|
+
import { panelIdle } from "./panel-idle";
|
|
12
|
+
|
|
13
|
+
export default function HelpPanelArticle(props: { topic?: HelpTopic }) {
|
|
14
|
+
const locale = useLocale();
|
|
15
|
+
const t = () => helpMessages.resolve([locale()]).t;
|
|
16
|
+
const [state, setState] = createSignal<HelpPanelState>();
|
|
17
|
+
const [reader, setReader] = createSignal<typeof import("./layout-help-panel-reader")>();
|
|
18
|
+
const [payload, setPayload] = createSignal<HelpDocumentPayload | null>(null);
|
|
19
|
+
const [error, setError] = createSignal<string | null>(null);
|
|
20
|
+
const [reloadRequired, setReloadRequired] = createSignal(false);
|
|
21
|
+
const [attempt, setAttempt] = createSignal(0);
|
|
22
|
+
let pending: Promise<typeof import("./layout-help-panel-reader")> | undefined;
|
|
23
|
+
let disposed = false;
|
|
24
|
+
let sectionPressed = false;
|
|
25
|
+
let queuedSection: { trigger: HTMLButtonElement; key: string } | undefined;
|
|
26
|
+
const sections = new Map<Element, () => void>();
|
|
27
|
+
const load = () =>
|
|
28
|
+
(pending ??= helpPanelModules
|
|
29
|
+
.reader()
|
|
30
|
+
.then((module) => {
|
|
31
|
+
if (!disposed) setReader(module);
|
|
32
|
+
return module;
|
|
33
|
+
})
|
|
34
|
+
.catch((error: unknown) => {
|
|
35
|
+
pending = undefined;
|
|
36
|
+
throw error;
|
|
37
|
+
}));
|
|
38
|
+
const warm = () => {
|
|
39
|
+
void load().catch(() => {});
|
|
40
|
+
};
|
|
41
|
+
const topic = () => (state() ? state()?.data.documents.find((topic) => topic.id === state()?.activeId()) : props.topic);
|
|
42
|
+
const clientVisible = () =>
|
|
43
|
+
state() ? state()?.view() === "article" && state()?.activeId() !== state()?.data.article?.id : !!props.topic;
|
|
44
|
+
const scrollToHash = (hash: string) => {
|
|
45
|
+
const current = state();
|
|
46
|
+
const heading = Array.from(
|
|
47
|
+
current?.root.querySelectorAll<HTMLElement>(
|
|
48
|
+
"article:not([hidden]) h1[id], article:not([hidden]) h2[id], article:not([hidden]) h3[id], article:not([hidden]) h4[id], article:not([hidden]) h5[id], article:not([hidden]) h6[id]",
|
|
49
|
+
) ?? [],
|
|
50
|
+
).find((element) => element.id === hash);
|
|
51
|
+
if (!heading) return false;
|
|
52
|
+
for (let details = heading.closest("details"); details; details = details.parentElement?.closest("details") ?? null)
|
|
53
|
+
details.open = true;
|
|
54
|
+
heading.scrollIntoView({ block: "start", behavior: prefersReducedMotion() ? "auto" : "smooth" });
|
|
55
|
+
return true;
|
|
56
|
+
};
|
|
57
|
+
onMount(() => {
|
|
58
|
+
const current = helpPanelState(panelElement());
|
|
59
|
+
setState(current);
|
|
60
|
+
const upgrade = (trigger: HTMLButtonElement, key: string) => {
|
|
61
|
+
// Safari keeps focus where it was when a button is clicked; only a focus move after the activation cancels it.
|
|
62
|
+
const start = document.activeElement;
|
|
63
|
+
const article = current.activeId();
|
|
64
|
+
void load()
|
|
65
|
+
.then((module) => {
|
|
66
|
+
// Back or another article while the chunk loads cancels the activation: the hidden article opens no menu.
|
|
67
|
+
if (disposed || !trigger.isConnected || current.view() !== "article" || current.activeId() !== article) return;
|
|
68
|
+
if (trigger.closest("[hidden]")) return;
|
|
69
|
+
// A pending chunk must not replace the trigger between pointerdown and pointerup.
|
|
70
|
+
if (sectionPressed) {
|
|
71
|
+
queuedSection = { trigger, key };
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const container = trigger.parentElement;
|
|
75
|
+
if (!container || sections.has(container)) return;
|
|
76
|
+
const focused = document.activeElement === trigger;
|
|
77
|
+
if (!focused && document.activeElement !== start && document.activeElement !== document.body) return;
|
|
78
|
+
setError(null);
|
|
79
|
+
sections.set(
|
|
80
|
+
container,
|
|
81
|
+
module.mountPanelSections(container, { locale: locale(), state: current, key, focused, onSection: scrollToHash }),
|
|
82
|
+
);
|
|
83
|
+
})
|
|
84
|
+
.catch((reason: unknown) => {
|
|
85
|
+
if (!disposed) {
|
|
86
|
+
setReloadRequired(true);
|
|
87
|
+
setError(reason instanceof Error ? reason.message : t().loadFallback);
|
|
88
|
+
trigger.title = t().loadFailed + ": " + t().reload;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
const click = (event: MouseEvent) => {
|
|
93
|
+
if (
|
|
94
|
+
event.defaultPrevented ||
|
|
95
|
+
event.button !== 0 ||
|
|
96
|
+
event.metaKey ||
|
|
97
|
+
event.ctrlKey ||
|
|
98
|
+
event.shiftKey ||
|
|
99
|
+
event.altKey ||
|
|
100
|
+
!(event.target instanceof Element)
|
|
101
|
+
)
|
|
102
|
+
return;
|
|
103
|
+
const sectionTrigger = event.target.closest<HTMLButtonElement>("[data-help-sections]");
|
|
104
|
+
if (sectionTrigger) {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
event.stopPropagation();
|
|
107
|
+
upgrade(sectionTrigger, "ArrowDown");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const button = event.target.closest<HTMLElement>("[data-help-topic]");
|
|
111
|
+
if (button) {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
current.open(button.dataset.helpTopic ?? "");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (event.target.closest("[data-help-unavailable] button")) {
|
|
117
|
+
location.reload();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const anchor = event.target.closest<HTMLAnchorElement>(".help-article a[href]");
|
|
121
|
+
if (!anchor || (anchor.target && anchor.target !== "_self") || anchor.hasAttribute("download")) return;
|
|
122
|
+
const target = helpLinkTarget(anchor.getAttribute("href") ?? "", location.href, current.data);
|
|
123
|
+
if (!target) return;
|
|
124
|
+
if (target.kind === "section") {
|
|
125
|
+
if (scrollToHash(target.hash)) event.preventDefault();
|
|
126
|
+
} else if (target.topicId === null) {
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
current.changeQuery("");
|
|
129
|
+
current.back();
|
|
130
|
+
const input = current.root.querySelector<HTMLInputElement>("[data-help-search]");
|
|
131
|
+
if (input) input.value = "";
|
|
132
|
+
} else if (current.data.documents.some((topic) => topic.id === target.topicId)) {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
current.open(target.topicId, target.hash);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const endPress = () => {
|
|
138
|
+
sectionPressed = false;
|
|
139
|
+
const next = queuedSection;
|
|
140
|
+
queuedSection = undefined;
|
|
141
|
+
if (next) upgrade(next.trigger, next.key);
|
|
142
|
+
};
|
|
143
|
+
const cancelPress = () => {
|
|
144
|
+
sectionPressed = false;
|
|
145
|
+
queuedSection = undefined;
|
|
146
|
+
};
|
|
147
|
+
const intent = (event: Event) => {
|
|
148
|
+
if (event.type === "pointerdown" && event.target instanceof Element && event.target.closest("[data-help-sections]"))
|
|
149
|
+
sectionPressed = true;
|
|
150
|
+
if (event.target instanceof Element && event.target.closest("[data-help-topic], .help-article a[href], [data-help-sections]")) warm();
|
|
151
|
+
};
|
|
152
|
+
const key = (event: KeyboardEvent) => {
|
|
153
|
+
intent(event);
|
|
154
|
+
const trigger = event.target instanceof Element ? event.target.closest<HTMLButtonElement>("[data-help-sections]") : null;
|
|
155
|
+
if (event.defaultPrevented || !trigger || !["Enter", " ", "ArrowDown", "ArrowUp"].includes(event.key)) return;
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
event.stopPropagation();
|
|
158
|
+
upgrade(trigger, event.key);
|
|
159
|
+
};
|
|
160
|
+
window.addEventListener("pointerup", endPress);
|
|
161
|
+
window.addEventListener("pointercancel", cancelPress);
|
|
162
|
+
current.root.addEventListener("click", click);
|
|
163
|
+
current.root.addEventListener("keydown", key);
|
|
164
|
+
const intents = ["pointerover", "pointerdown", "focusin"] as const;
|
|
165
|
+
for (const event of intents) current.root.addEventListener(event, intent);
|
|
166
|
+
onCleanup(panelIdle(warm));
|
|
167
|
+
onCleanup(() => {
|
|
168
|
+
window.removeEventListener("pointerup", endPress);
|
|
169
|
+
window.removeEventListener("pointercancel", cancelPress);
|
|
170
|
+
current.root.removeEventListener("click", click);
|
|
171
|
+
current.root.removeEventListener("keydown", key);
|
|
172
|
+
for (const event of intents) current.root.removeEventListener(event, intent);
|
|
173
|
+
for (const dispose of sections.values()) dispose();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
onCleanup(() => {
|
|
177
|
+
disposed = true;
|
|
178
|
+
});
|
|
179
|
+
createEffect(() => {
|
|
180
|
+
const current = state();
|
|
181
|
+
const active = topic();
|
|
182
|
+
attempt();
|
|
183
|
+
setPayload(null);
|
|
184
|
+
setError(null);
|
|
185
|
+
setReloadRequired(false);
|
|
186
|
+
if (!current || !active || !clientVisible()) return;
|
|
187
|
+
const controller = new AbortController();
|
|
188
|
+
let cancelled = false;
|
|
189
|
+
const chunkFailed = (reason: unknown): never => {
|
|
190
|
+
if (!cancelled) setReloadRequired(true);
|
|
191
|
+
throw reason;
|
|
192
|
+
};
|
|
193
|
+
void Promise.all([
|
|
194
|
+
load().catch(chunkFailed),
|
|
195
|
+
helpPanelModules
|
|
196
|
+
.document()
|
|
197
|
+
.catch(chunkFailed)
|
|
198
|
+
.then((module) => module.loadPanelDocument(current, active, controller.signal, t())),
|
|
199
|
+
])
|
|
200
|
+
.then(([, document]) => {
|
|
201
|
+
if (!cancelled) setPayload(document);
|
|
202
|
+
})
|
|
203
|
+
.catch((reason: unknown) => {
|
|
204
|
+
if (!cancelled && !(reason instanceof DOMException && reason.name === "AbortError"))
|
|
205
|
+
setError(reason instanceof Error ? reason.message : t().loadFallback);
|
|
206
|
+
});
|
|
207
|
+
onCleanup(() => {
|
|
208
|
+
cancelled = true;
|
|
209
|
+
controller.abort();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
/** The navigation whose heading focus and section jump are done; returning from search must not replay them. */
|
|
213
|
+
let settled = 0;
|
|
214
|
+
createEffect(() => {
|
|
215
|
+
const current = state();
|
|
216
|
+
const navigation = current?.navigation();
|
|
217
|
+
// The loaded reader replaces the loading frame and its heading; follow it unless the reader moved focus elsewhere.
|
|
218
|
+
const body = current?.activeId() === current?.data.article?.id || (!!payload() && !!reader());
|
|
219
|
+
if (!current || !navigation?.version || navigation.version === settled || current.view() !== "article") return;
|
|
220
|
+
const frame = requestAnimationFrame(() => {
|
|
221
|
+
const focused = document.activeElement;
|
|
222
|
+
if (!focused || focused === document.body || focused.matches(".help-article-title") || focused.closest("[hidden]"))
|
|
223
|
+
current.focusHeading();
|
|
224
|
+
if (!body) return;
|
|
225
|
+
settled = navigation.version;
|
|
226
|
+
if (navigation.hash) scrollToHash(navigation.hash);
|
|
227
|
+
});
|
|
228
|
+
onCleanup(() => cancelAnimationFrame(frame));
|
|
229
|
+
});
|
|
230
|
+
createEffect(() => {
|
|
231
|
+
const current = state();
|
|
232
|
+
const live = current?.root.querySelector("[data-help-announcement]");
|
|
233
|
+
if (live) live.textContent = current?.announcement() ?? "";
|
|
234
|
+
});
|
|
235
|
+
return (
|
|
236
|
+
<>
|
|
237
|
+
<Show when={clientVisible() && topic()}>
|
|
238
|
+
{(active) => (
|
|
239
|
+
<article class="help-article" data-surface="panel">
|
|
240
|
+
<Show
|
|
241
|
+
when={payload() ? reader() : undefined}
|
|
242
|
+
fallback={
|
|
243
|
+
<HelpArticleView
|
|
244
|
+
surface="panel"
|
|
245
|
+
topic={active()}
|
|
246
|
+
parts={null}
|
|
247
|
+
document={null}
|
|
248
|
+
Markdown={() => null}
|
|
249
|
+
showTools={false}
|
|
250
|
+
sectionMenu={null}
|
|
251
|
+
copyButton={null}
|
|
252
|
+
loading={
|
|
253
|
+
<Show when={!error()}>
|
|
254
|
+
<div class="min-h-[24rem]">
|
|
255
|
+
<Placeholder state="loading" variant="panel" title={t().loading} />
|
|
256
|
+
</div>
|
|
257
|
+
</Show>
|
|
258
|
+
}
|
|
259
|
+
failure={
|
|
260
|
+
<Show when={error()}>
|
|
261
|
+
{(message) => (
|
|
262
|
+
<NoticeCard tone="danger" title={t().loadFailed} detail={message()}>
|
|
263
|
+
<Button
|
|
264
|
+
size="sm"
|
|
265
|
+
variant="secondary"
|
|
266
|
+
onClick={() => (reloadRequired() ? location.reload() : setAttempt((value) => value + 1))}
|
|
267
|
+
>
|
|
268
|
+
{reloadRequired() ? t().reload : t().tryAgain}
|
|
269
|
+
</Button>
|
|
270
|
+
</NoticeCard>
|
|
271
|
+
)}
|
|
272
|
+
</Show>
|
|
273
|
+
}
|
|
274
|
+
toc={null}
|
|
275
|
+
copyAction={null}
|
|
276
|
+
headingRef={() => {}}
|
|
277
|
+
contentRef={() => {}}
|
|
278
|
+
onBack={() => state()?.back()}
|
|
279
|
+
/>
|
|
280
|
+
}
|
|
281
|
+
>
|
|
282
|
+
{(module) => {
|
|
283
|
+
const Reader = module().HelpPanelReader;
|
|
284
|
+
return (
|
|
285
|
+
<Show when={payload()}>
|
|
286
|
+
{(document) => {
|
|
287
|
+
const current = state();
|
|
288
|
+
return current && <Reader state={current} topic={active()} document={document()} onSection={scrollToHash} />;
|
|
289
|
+
}}
|
|
290
|
+
</Show>
|
|
291
|
+
);
|
|
292
|
+
}}
|
|
293
|
+
</Show>
|
|
294
|
+
</article>
|
|
295
|
+
)}
|
|
296
|
+
</Show>
|
|
297
|
+
<Show when={state()?.activeId() === state()?.data.article?.id && error()}>
|
|
298
|
+
{(message) => (
|
|
299
|
+
<NoticeCard tone="danger" title={t().loadFailed} detail={message()}>
|
|
300
|
+
<Button size="sm" variant="secondary" onClick={() => location.reload()}>
|
|
301
|
+
{t().reload}
|
|
302
|
+
</Button>
|
|
303
|
+
</NoticeCard>
|
|
304
|
+
)}
|
|
305
|
+
</Show>
|
|
306
|
+
</>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { IconButton, useLocale } from "@k2b/ui";
|
|
2
|
+
import { createSignal, onMount, Show } from "solid-js";
|
|
3
|
+
import { helpMessages } from "./help-messages";
|
|
4
|
+
import { capturePanelFocus, panelElement, restorePanelFocus } from "./layout-help-panel-lifecycle";
|
|
5
|
+
import { type HelpPanelState, helpPanelState } from "./layout-help-panel-state";
|
|
6
|
+
|
|
7
|
+
capturePanelFocus();
|
|
8
|
+
export default function HelpPanelBack(props: { back: boolean; tile: string }) {
|
|
9
|
+
const locale = useLocale();
|
|
10
|
+
const t = () => helpMessages.resolve([locale()]).t;
|
|
11
|
+
const [state, setState] = createSignal<HelpPanelState>();
|
|
12
|
+
onMount(() => {
|
|
13
|
+
const wrapper = panelElement();
|
|
14
|
+
setState(helpPanelState(wrapper));
|
|
15
|
+
const button = wrapper.querySelector(".help-head-back");
|
|
16
|
+
if (button) restorePanelFocus(button);
|
|
17
|
+
});
|
|
18
|
+
return (
|
|
19
|
+
<Show
|
|
20
|
+
when={state()?.view() === "article" || (!state() && props.back)}
|
|
21
|
+
fallback={
|
|
22
|
+
<span class="help-head-tile" aria-hidden="true">
|
|
23
|
+
<i class={props.tile} />
|
|
24
|
+
</span>
|
|
25
|
+
}
|
|
26
|
+
>
|
|
27
|
+
<IconButton class="help-head-back" label={t().backToTopics} title={t().back} onClick={() => state()?.back()}>
|
|
28
|
+
<i class="ti ti-arrow-left" />
|
|
29
|
+
</IconButton>
|
|
30
|
+
</Show>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Button, NoticeCard, useLocale } from "@k2b/ui";
|
|
2
|
+
import { createSignal, onCleanup, onMount, Show } from "solid-js";
|
|
3
|
+
import { helpMessages } from "./help-messages";
|
|
4
|
+
import { HelpPanelMenuTrigger } from "./layout-help-panel-controls";
|
|
5
|
+
import { capturePanelFocus, panelElement, restorePanelFocus } from "./layout-help-panel-lifecycle";
|
|
6
|
+
import type { MenuActivation } from "./layout-help-panel-menu";
|
|
7
|
+
import { helpPanelModules } from "./layout-help-panel-modules";
|
|
8
|
+
import { type HelpPanelState, helpPanelState } from "./layout-help-panel-state";
|
|
9
|
+
import { panelIdle } from "./panel-idle";
|
|
10
|
+
|
|
11
|
+
capturePanelFocus();
|
|
12
|
+
export default function HelpPanelMenu() {
|
|
13
|
+
const locale = useLocale();
|
|
14
|
+
const t = () => helpMessages.resolve([locale()]).t;
|
|
15
|
+
const [menu, setMenu] = createSignal<typeof import("./layout-help-panel-menu")>();
|
|
16
|
+
const [error, setError] = createSignal(false);
|
|
17
|
+
let state: HelpPanelState | undefined;
|
|
18
|
+
let loaded: typeof import("./layout-help-panel-menu") | undefined;
|
|
19
|
+
let pending: Promise<typeof import("./layout-help-panel-menu")> | undefined;
|
|
20
|
+
let activation: MenuActivation = { key: "ArrowDown", focus: false };
|
|
21
|
+
let disposed = false;
|
|
22
|
+
let pressed = false;
|
|
23
|
+
let queued: { key: string; trigger: HTMLButtonElement; pointer: boolean } | undefined;
|
|
24
|
+
const load = () =>
|
|
25
|
+
(pending ??= helpPanelModules
|
|
26
|
+
.menu()
|
|
27
|
+
.then((module) => {
|
|
28
|
+
loaded = module;
|
|
29
|
+
return module;
|
|
30
|
+
})
|
|
31
|
+
.catch((error: unknown) => {
|
|
32
|
+
pending = undefined;
|
|
33
|
+
throw error;
|
|
34
|
+
}));
|
|
35
|
+
const warm = () => {
|
|
36
|
+
void load().catch(() => {});
|
|
37
|
+
};
|
|
38
|
+
const activate = (key: string, trigger: HTMLButtonElement, pointer = false) => {
|
|
39
|
+
if (pressed) {
|
|
40
|
+
queued = { key, trigger, pointer };
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Safari keeps focus where it was when a button is clicked; only a focus move after the activation cancels it.
|
|
44
|
+
const start = document.activeElement;
|
|
45
|
+
const swap = (module: typeof import("./layout-help-panel-menu")) => {
|
|
46
|
+
const focused = document.activeElement;
|
|
47
|
+
if (focused !== start && focused !== trigger && focused !== document.body) return;
|
|
48
|
+
activation = { key, focus: focused === trigger, pointer };
|
|
49
|
+
setMenu(module);
|
|
50
|
+
};
|
|
51
|
+
setError(false);
|
|
52
|
+
if (loaded) {
|
|
53
|
+
swap(loaded);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
void load()
|
|
57
|
+
.then((module) => {
|
|
58
|
+
if (disposed) return;
|
|
59
|
+
if (pressed) {
|
|
60
|
+
queued = { key, trigger, pointer };
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
swap(module);
|
|
64
|
+
})
|
|
65
|
+
.catch(() => {
|
|
66
|
+
if (!disposed) setError(true);
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
const endPress = () => {
|
|
70
|
+
pressed = false;
|
|
71
|
+
const next = queued;
|
|
72
|
+
queued = undefined;
|
|
73
|
+
if (next) activate(next.key, next.trigger, next.pointer);
|
|
74
|
+
};
|
|
75
|
+
const cancelPress = () => {
|
|
76
|
+
pressed = false;
|
|
77
|
+
queued = undefined;
|
|
78
|
+
};
|
|
79
|
+
onMount(() => {
|
|
80
|
+
const wrapper = panelElement();
|
|
81
|
+
state = helpPanelState(wrapper);
|
|
82
|
+
const button = wrapper.querySelector("[data-help-menu]");
|
|
83
|
+
if (button) restorePanelFocus(button);
|
|
84
|
+
onCleanup(panelIdle(warm));
|
|
85
|
+
window.addEventListener("pointerup", endPress);
|
|
86
|
+
window.addEventListener("pointercancel", cancelPress);
|
|
87
|
+
onCleanup(() => {
|
|
88
|
+
window.removeEventListener("pointerup", endPress);
|
|
89
|
+
window.removeEventListener("pointercancel", cancelPress);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
onCleanup(() => {
|
|
93
|
+
disposed = true;
|
|
94
|
+
});
|
|
95
|
+
return (
|
|
96
|
+
<Show
|
|
97
|
+
when={menu()}
|
|
98
|
+
fallback={
|
|
99
|
+
<>
|
|
100
|
+
<HelpPanelMenuTrigger
|
|
101
|
+
label={t().moreActions}
|
|
102
|
+
onPointerEnter={warm}
|
|
103
|
+
onPointerDown={() => {
|
|
104
|
+
pressed = true;
|
|
105
|
+
warm();
|
|
106
|
+
}}
|
|
107
|
+
onFocus={warm}
|
|
108
|
+
onClick={(event) => activate("ArrowDown", event.currentTarget, event.detail > 0)}
|
|
109
|
+
onKeyDown={(event) => {
|
|
110
|
+
warm();
|
|
111
|
+
if (!["Enter", " ", "ArrowDown", "ArrowUp"].includes(event.key)) return;
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
event.stopPropagation();
|
|
114
|
+
activate(event.key, event.currentTarget);
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
<Show when={error()}>
|
|
118
|
+
<NoticeCard class="absolute z-50" tone="danger" title={t().helpUnavailable} detail={t().helpUnavailableDetail}>
|
|
119
|
+
<Button size="sm" variant="secondary" onClick={() => location.reload()}>
|
|
120
|
+
{t().reload}
|
|
121
|
+
</Button>
|
|
122
|
+
</NoticeCard>
|
|
123
|
+
</Show>
|
|
124
|
+
</>
|
|
125
|
+
}
|
|
126
|
+
>
|
|
127
|
+
{(module) => {
|
|
128
|
+
const Menu = module().HelpPanelMenu;
|
|
129
|
+
return state && <Menu state={state} activation={activation} />;
|
|
130
|
+
}}
|
|
131
|
+
</Show>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Button, NoticeCard, useLocale } from "@k2b/ui";
|
|
2
|
+
import { createEffect, createSignal, onCleanup, onMount, Show } from "solid-js";
|
|
3
|
+
import { helpMessages } from "./help-messages";
|
|
4
|
+
import { panelElement } from "./layout-help-panel-lifecycle";
|
|
5
|
+
import { helpPanelModules } from "./layout-help-panel-modules";
|
|
6
|
+
import { type HelpPanelState, helpPanelState } from "./layout-help-panel-state";
|
|
7
|
+
import { panelIdle } from "./panel-idle";
|
|
8
|
+
|
|
9
|
+
export default function HelpPanelSearch() {
|
|
10
|
+
const locale = useLocale();
|
|
11
|
+
const t = () => helpMessages.resolve([locale()]).t;
|
|
12
|
+
const [state, setState] = createSignal<HelpPanelState>();
|
|
13
|
+
const [engine, setEngine] = createSignal<typeof import("./layout-help-panel-search")>();
|
|
14
|
+
const [failed, setFailed] = createSignal(false);
|
|
15
|
+
let input: HTMLInputElement | null = null;
|
|
16
|
+
let pending: Promise<void> | undefined;
|
|
17
|
+
let disposed = false;
|
|
18
|
+
let lastValue = "";
|
|
19
|
+
const reserve = () => {
|
|
20
|
+
if (!input) return;
|
|
21
|
+
const region = state()?.results();
|
|
22
|
+
if (input.value.trim() && !lastValue.trim() && region) region.style.minHeight = `${region.offsetHeight}px`;
|
|
23
|
+
if (!input.value.trim() && region) region.style.minHeight = "";
|
|
24
|
+
lastValue = input.value;
|
|
25
|
+
};
|
|
26
|
+
const adopt = () => {
|
|
27
|
+
reserve();
|
|
28
|
+
// An empty field shows the start again, also after the search code failed to load.
|
|
29
|
+
if (input && !input.value.trim()) setFailed(false);
|
|
30
|
+
if (input && (engine() || failed() || !input.value.trim())) state()?.changeQuery(input.value);
|
|
31
|
+
};
|
|
32
|
+
const load = () =>
|
|
33
|
+
(pending ??= helpPanelModules
|
|
34
|
+
.search()
|
|
35
|
+
.then((module) => {
|
|
36
|
+
if (disposed) return;
|
|
37
|
+
setFailed(false);
|
|
38
|
+
setEngine(module);
|
|
39
|
+
adopt();
|
|
40
|
+
})
|
|
41
|
+
.catch(() => {
|
|
42
|
+
pending = undefined;
|
|
43
|
+
if (!disposed && input?.value.trim()) {
|
|
44
|
+
state()?.changeQuery(input.value);
|
|
45
|
+
setFailed(true);
|
|
46
|
+
}
|
|
47
|
+
}));
|
|
48
|
+
const intent = () => {
|
|
49
|
+
void load();
|
|
50
|
+
};
|
|
51
|
+
onMount(() => {
|
|
52
|
+
const current = helpPanelState(panelElement());
|
|
53
|
+
setState(current);
|
|
54
|
+
input = current.root.querySelector<HTMLInputElement>("[data-help-search]");
|
|
55
|
+
if (!input) return;
|
|
56
|
+
const typed = () => {
|
|
57
|
+
reserve();
|
|
58
|
+
intent();
|
|
59
|
+
adopt();
|
|
60
|
+
};
|
|
61
|
+
// TextInput publishes every input event, including IME input; keep that existing behavior.
|
|
62
|
+
input.addEventListener("input", typed);
|
|
63
|
+
input.addEventListener("compositionend", typed);
|
|
64
|
+
const intents = ["focus", "pointerdown", "keydown", "compositionstart"] as const;
|
|
65
|
+
for (const event of intents) input.addEventListener(event, intent);
|
|
66
|
+
const key = (event: KeyboardEvent) => {
|
|
67
|
+
if (event.defaultPrevented || event.isComposing) return;
|
|
68
|
+
if (event.key === "Escape" && event.target === input && input?.value) {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
input.value = "";
|
|
71
|
+
adopt();
|
|
72
|
+
setFailed(false);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
|
76
|
+
if (
|
|
77
|
+
event.key === "/" &&
|
|
78
|
+
!(
|
|
79
|
+
event.target instanceof Element &&
|
|
80
|
+
event.target.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])')
|
|
81
|
+
)
|
|
82
|
+
) {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
input?.focus();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
document.addEventListener("keydown", key);
|
|
88
|
+
// A user can type before even the eager island arrives. The field itself has never been replaced.
|
|
89
|
+
if (input.value) typed();
|
|
90
|
+
onCleanup(panelIdle(intent));
|
|
91
|
+
onCleanup(() => {
|
|
92
|
+
input?.removeEventListener("input", typed);
|
|
93
|
+
input?.removeEventListener("compositionend", typed);
|
|
94
|
+
for (const event of intents) input?.removeEventListener(event, intent);
|
|
95
|
+
document.removeEventListener("keydown", key);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
onCleanup(() => {
|
|
99
|
+
disposed = true;
|
|
100
|
+
});
|
|
101
|
+
createEffect(() => {
|
|
102
|
+
const current = state();
|
|
103
|
+
const content = current?.root.querySelector<HTMLElement>("[data-help-start-content]");
|
|
104
|
+
if (content) content.hidden = !!current?.query().trim() || failed();
|
|
105
|
+
});
|
|
106
|
+
return (
|
|
107
|
+
<>
|
|
108
|
+
<Show when={engine()}>
|
|
109
|
+
{(module) => {
|
|
110
|
+
const Results = module().HelpPanelSearchResults;
|
|
111
|
+
return <Show when={state()}>{(current) => <Results state={current()} />}</Show>;
|
|
112
|
+
}}
|
|
113
|
+
</Show>
|
|
114
|
+
<Show when={failed()}>
|
|
115
|
+
<NoticeCard tone="danger" title={t().helpUnavailable} detail={t().helpUnavailableDetail}>
|
|
116
|
+
<Button size="sm" variant="secondary" onClick={() => location.reload()}>
|
|
117
|
+
{t().reload}
|
|
118
|
+
</Button>
|
|
119
|
+
</NoticeCard>
|
|
120
|
+
</Show>
|
|
121
|
+
</>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type KeyboardPlatform, useLocale } from "@k2b/ui";
|
|
2
|
+
import { createSignal, onCleanup, onMount } from "solid-js";
|
|
3
|
+
import { onContextChange } from "../browser/panel";
|
|
4
|
+
import { onPanelShortcutsChange } from "../browser/panel-shortcuts";
|
|
5
|
+
import { readHelpPanelShortcuts } from "./layout-help-panel-shortcuts";
|
|
6
|
+
import { type HelpShortcutCommand, helpShortcutGroups } from "./layout-help-start";
|
|
7
|
+
import { HelpShortcutsView } from "./layout-help-views";
|
|
8
|
+
import { platformMessages } from "./platform-messages";
|
|
9
|
+
import { shellShortcuts } from "./shell-shortcuts";
|
|
10
|
+
|
|
11
|
+
export default function HelpPanelShortcuts(props: { appName?: string; platform: KeyboardPlatform }) {
|
|
12
|
+
const locale = useLocale();
|
|
13
|
+
const seed = shellShortcuts(platformMessages.resolve([locale()]).t, { panel: true });
|
|
14
|
+
const [commands, setCommands] = createSignal<HelpShortcutCommand[]>(seed);
|
|
15
|
+
onMount(() => {
|
|
16
|
+
const update = () => setCommands(readHelpPanelShortcuts(seed));
|
|
17
|
+
onCleanup(onPanelShortcutsChange(update));
|
|
18
|
+
onCleanup(onContextChange(update));
|
|
19
|
+
update();
|
|
20
|
+
});
|
|
21
|
+
return <HelpShortcutsView groups={helpShortcutGroups(commands())} appName={props.appName} platform={props.platform} />;
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createSignal, onCleanup, onMount } from "solid-js";
|
|
2
|
+
import { getContext, onContextChange } from "../browser/panel";
|
|
3
|
+
import { capturePanelFocus, panelElement, restorePanelFocus } from "./layout-help-panel-lifecycle";
|
|
4
|
+
import { helpPanelState } from "./layout-help-panel-state";
|
|
5
|
+
import type { HelpTopic } from "./layout-help-registry";
|
|
6
|
+
import { helpStart } from "./layout-help-start";
|
|
7
|
+
import { HelpViewSection } from "./layout-help-views";
|
|
8
|
+
|
|
9
|
+
capturePanelFocus();
|
|
10
|
+
export default function HelpPanelView(props: { topics: readonly HelpTopic[] }) {
|
|
11
|
+
const [topics, setTopics] = createSignal(props.topics);
|
|
12
|
+
onMount(() => {
|
|
13
|
+
const wrapper = panelElement();
|
|
14
|
+
const state = helpPanelState(wrapper);
|
|
15
|
+
const update = (context: ReturnType<typeof getContext>) => {
|
|
16
|
+
const next = helpStart(state.data.documents, context && context.app === state.data.appId ? (context.help ?? []) : []).view;
|
|
17
|
+
// Retain row identity when the blob and island props name the same topic, including the first host snapshot.
|
|
18
|
+
setTopics((previous) => next.map((topic) => previous.find((candidate) => candidate.id === topic.id) ?? topic));
|
|
19
|
+
};
|
|
20
|
+
onCleanup(onContextChange(update));
|
|
21
|
+
const section = wrapper.querySelector("[data-help-view]");
|
|
22
|
+
if (section) restorePanelFocus(section);
|
|
23
|
+
const context = getContext();
|
|
24
|
+
if (context) update(context);
|
|
25
|
+
});
|
|
26
|
+
return <HelpViewSection items={topics()} onOpen={() => {}} />;
|
|
27
|
+
}
|