@k2b/cloud 0.38.0 → 0.40.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 +12 -5
- package/scripts/build-canvas-workers.ts +109 -29
- package/scripts/build.ts +3 -5
- package/src/_internal/app-favicon.ts +4 -2
- package/src/_internal/app-path.ts +25 -0
- package/src/_internal/app-presentation.ts +4 -0
- package/src/_internal/canvas-worker.ts +5 -3
- 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 +216 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +63 -16
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/panel-cookie.ts +39 -0
- package/src/_internal/registry-validation.ts +21 -26
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.browser-harness.tsx +57 -38
- package/src/access/PermissionEditor.tsx +32 -7
- package/src/access/ResourceApiKeys.tsx +5 -1
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/approval-routes.ts +5 -2
- package/src/ai/approvals.ts +139 -28
- package/src/ai/background-review.ts +72 -0
- package/src/ai/browser.ts +10 -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 +398 -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 +669 -0
- package/src/ai/chat/action-sentences.ts +1 -44
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action.tsx +481 -0
- package/src/ai/chat/turn-view.tsx +45 -163
- package/src/ai/chat/view-block.tsx +22 -7
- package/src/ai/chat-task-contracts.ts +31 -3
- package/src/ai/chat-task-reasons-migrate.ts +75 -0
- package/src/ai/chat-task-reasons.ts +92 -0
- package/src/ai/chat-tasks.ts +438 -78
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +190 -39
- package/src/ai/index.ts +14 -0
- package/src/ai/migrate.ts +39 -4
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/request-context.ts +7 -1
- package/src/ai/routes.ts +172 -2
- package/src/ai/runtime.ts +13 -27
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +232 -27
- package/src/ai/timeline.ts +9 -1
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-wording.ts +67 -0
- package/src/ai/turn-abort.ts +33 -0
- package/src/ai/types.ts +40 -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-core-settings.ts +190 -139
- 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 +6 -2
- package/src/api/app-approval.ts +54 -6
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +3 -1
- package/src/api/auth.ts +25 -6
- 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 +7 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +3 -0
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +80 -33
- 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 +180 -238
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/CloudResourceSearch.tsx +10 -6
- package/src/browser/app-approval.ts +60 -6
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +145 -0
- package/src/browser/composer-insert-messages.ts +31 -0
- package/src/browser/composer-insert.ts +160 -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 +35 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +756 -0
- package/src/browser/panel.ts +65 -0
- package/src/browser/resource-picker.tsx +12 -1
- 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 +159 -12
- package/src/contracts/app.ts +14 -0
- 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/registry.ts +3 -1
- package/src/contracts/seen-hints.ts +35 -0
- package/src/contracts/shared.ts +2 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +11 -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/server/services/access.ts +5 -1
- package/src/services/accounts/posix.ts +2 -1
- package/src/services/app-approval.ts +533 -90
- package/src/services/approvals/device.ts +9 -5
- package/src/services/approvals/index.ts +6 -5
- package/src/services/approvals/list.ts +26 -5
- package/src/services/auth-flows/notification-sender.ts +3 -0
- 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 +32 -3
- 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/browser.ts +16 -5
- 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/image-protocol.ts +135 -0
- package/src/services/pdf/image-variants.ts +124 -0
- package/src/services/pdf/image-worker.ts +31 -0
- package/src/services/pdf/image.ts +109 -0
- package/src/services/pdf/index.ts +9 -0
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +97 -16
- package/src/services/session/recent.ts +13 -0
- package/src/services/settings/api.ts +11 -6
- package/src/services/settings/app.ts +5 -4
- package/src/services/settings/core-settings.ts +4 -3
- package/src/services/settings/index.ts +19 -17
- package/src/services/settings/namespace.ts +2 -1
- package/src/services/settings/store.ts +83 -11
- package/src/services/webauthn.ts +12 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/app-presentation.ts +1 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -0
- package/src/shared/redirect.ts +12 -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 +95 -10
- package/src/ssr/Layout.tsx +66 -10
- package/src/ssr/LayoutHeader.tsx +15 -9
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +435 -274
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/LayoutRail.tsx +2 -2
- package/src/ssr/LayoutTabBar.tsx +29 -0
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +35 -0
- package/src/ssr/PanelRuntime.island.tsx +94 -0
- package/src/ssr/PanelSlot.tsx +158 -0
- package/src/ssr/PanelTrigger.tsx +49 -0
- package/src/ssr/ShellRuntime.island.tsx +69 -5
- package/src/ssr/app-appearance.ts +3 -2
- package/src/ssr/app-color.ts +105 -0
- package/src/ssr/app-launchpad.tsx +18 -1
- package/src/ssr/app-navigation.ts +28 -0
- package/src/ssr/compact-shell.baseline.json +5270 -0
- package/src/ssr/compact-shell.browser-harness.ts +439 -0
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +20 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +5 -0
- package/src/ssr/islands/index.ts +4 -3
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-slot.browser-harness.tsx +159 -0
- package/src/ssr/platform-messages.ts +48 -0
- package/src/ssr/rail-navigation.ts +44 -0
- package/src/ssr/shell-data.tsx +5 -0
- package/src/ssr/shell-shortcuts.ts +13 -2
- package/src/styles/effects.css +201 -11
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +314 -63
- package/src/styles/panel.css +416 -0
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +36 -13
- 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 +22 -43
package/src/ssr/LayoutHelp.tsx
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { HelpAccessLevel, HelpArticleLink, HelpDocumentManifest, HelpDocumentPayload } from "@k2b/cloud/shared";
|
|
2
2
|
import { clipboard } from "@k2b/stdlib/solid";
|
|
3
3
|
import {
|
|
4
|
+
BottomSheet,
|
|
4
5
|
Button,
|
|
6
|
+
bottomSheetOptions,
|
|
5
7
|
Dropdown,
|
|
8
|
+
dialogCore,
|
|
6
9
|
IconButton,
|
|
7
10
|
Kbd,
|
|
11
|
+
type KeyboardPlatform,
|
|
8
12
|
MarkdownView,
|
|
9
13
|
NoticeCard,
|
|
10
14
|
Placeholder,
|
|
@@ -16,10 +20,13 @@ import {
|
|
|
16
20
|
toast,
|
|
17
21
|
useLocale,
|
|
18
22
|
} from "@k2b/ui";
|
|
19
|
-
import { createEffect, createMemo, createSignal, For, type JSX, onCleanup, onMount, Show } from "solid-js";
|
|
23
|
+
import { createEffect, createMemo, createSignal, For, type JSX, onCleanup, onMount, Show, untrack } from "solid-js";
|
|
20
24
|
import { COMMANDS_CHANGED, contextCommandsWithShortcuts } from "../browser/command-shortcuts";
|
|
25
|
+
import { getContext, onContextChange, openPanel } from "../browser/panel";
|
|
26
|
+
import { onPanelShortcutsChange, panelShortcutsReceived, readPanelShortcuts } from "../browser/panel-shortcuts";
|
|
21
27
|
import { HELP_ACCESS_LEVELS, HELP_KINDS } from "../shared/help";
|
|
22
28
|
import { appAccentStyle } from "./app-appearance";
|
|
29
|
+
import type { LayoutHelpArticle } from "./help";
|
|
23
30
|
import { helpMessages } from "./help-messages";
|
|
24
31
|
import { helpArticleParts } from "./layout-help-article";
|
|
25
32
|
import { formatHelpBundleMarkdown, formatHelpDocumentMarkdown } from "./layout-help-markdown";
|
|
@@ -41,6 +48,8 @@ import { shellShortcuts } from "./shell-shortcuts";
|
|
|
41
48
|
export type LayoutHelpPageProps = {
|
|
42
49
|
documents: readonly HelpDocumentManifest[];
|
|
43
50
|
initialTopic?: string;
|
|
51
|
+
/** Authorized article and server-parsed parts for an identical first frame. */
|
|
52
|
+
initialArticle?: LayoutHelpArticle;
|
|
44
53
|
includeShortcuts?: boolean;
|
|
45
54
|
accent?: string;
|
|
46
55
|
/** Render inside an app-owned pane instead of occupying a full page. */
|
|
@@ -49,6 +58,22 @@ export type LayoutHelpPageProps = {
|
|
|
49
58
|
pageBase?: string;
|
|
50
59
|
};
|
|
51
60
|
|
|
61
|
+
export type LayoutHelpPanelProps = {
|
|
62
|
+
documents: readonly HelpDocumentManifest[];
|
|
63
|
+
/** Absent when the context selected Help the reader may not see. */
|
|
64
|
+
app?: { id: string; name: string; icon?: string };
|
|
65
|
+
/** appAppearanceStyle() of the app, from the server. */
|
|
66
|
+
appearance?: string;
|
|
67
|
+
/** Server-filtered For this view ids. */
|
|
68
|
+
view?: readonly string[];
|
|
69
|
+
initialTopic?: string;
|
|
70
|
+
/** Authorized article and server-parsed parts for an identical first frame. */
|
|
71
|
+
initialArticle?: LayoutHelpArticle;
|
|
72
|
+
pageBase?: string;
|
|
73
|
+
platform: KeyboardPlatform;
|
|
74
|
+
state?: "failed" | "unavailable";
|
|
75
|
+
};
|
|
76
|
+
|
|
52
77
|
type HelpView = "hub" | "article";
|
|
53
78
|
type HelpSession = {
|
|
54
79
|
view: HelpView;
|
|
@@ -97,21 +122,30 @@ const GROUP_ICONS: Record<HelpStartGroupKind, string> = {
|
|
|
97
122
|
* first client render on, also before the shell has registered them. The groups reserve their height and place in CSS,
|
|
98
123
|
* so the ones the browser adds fill space that is already there.
|
|
99
124
|
*/
|
|
100
|
-
const StartShortcuts = (props: { appName?: string }) => {
|
|
125
|
+
const StartShortcuts = (props: { appName?: string; panel?: boolean; platform?: KeyboardPlatform }) => {
|
|
101
126
|
const locale = useLocale();
|
|
102
127
|
const t = () => helpMessages.resolve([locale()]).t;
|
|
103
|
-
const shell = useShellData();
|
|
104
|
-
const seed: HelpShortcutCommand[] =
|
|
128
|
+
const shell = props.panel ? undefined : useShellData();
|
|
129
|
+
const seed: HelpShortcutCommand[] =
|
|
130
|
+
props.panel || shell ? shellShortcuts(platformMessages.resolve([locale()]).t, { panel: props.panel || !!shell?.panel }) : [];
|
|
105
131
|
const read = (): HelpShortcutCommand[] => {
|
|
132
|
+
// Once received, the host list is authoritative: omitted shortcuts must stay omitted.
|
|
133
|
+
if (props.panel) return panelShortcutsReceived() && getContext() !== null ? readPanelShortcuts() : seed;
|
|
106
134
|
const commands = contextCommandsWithShortcuts();
|
|
107
135
|
return [...seed.filter((command) => !commands.some((other) => other.id === command.id)), ...commands];
|
|
108
136
|
};
|
|
109
|
-
const [commands, setCommands] = createSignal<HelpShortcutCommand[]>(read());
|
|
137
|
+
const [commands, setCommands] = createSignal<HelpShortcutCommand[]>(props.panel ? seed : read());
|
|
110
138
|
onMount(() => {
|
|
111
139
|
const update = () => setCommands(read());
|
|
140
|
+
if (props.panel) {
|
|
141
|
+
onCleanup(onPanelShortcutsChange(update));
|
|
142
|
+
onCleanup(onContextChange(update));
|
|
143
|
+
}
|
|
112
144
|
update();
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
if (!props.panel) {
|
|
146
|
+
window.addEventListener(COMMANDS_CHANGED, update);
|
|
147
|
+
onCleanup(() => window.removeEventListener(COMMANDS_CHANGED, update));
|
|
148
|
+
}
|
|
115
149
|
});
|
|
116
150
|
const label = (scope: HelpShortcutScope) =>
|
|
117
151
|
scope === "everywhere"
|
|
@@ -139,7 +173,7 @@ const StartShortcuts = (props: { appName?: string }) => {
|
|
|
139
173
|
{(command) => (
|
|
140
174
|
<li class="help-shortcut" title={command.description}>
|
|
141
175
|
<span class="help-shortcut-title">{command.title}</span>
|
|
142
|
-
<Kbd keys={command.shortcut} platform={shell?.platform} />
|
|
176
|
+
<Kbd keys={command.shortcut} platform={props.platform ?? shell?.platform} />
|
|
143
177
|
</li>
|
|
144
178
|
)}
|
|
145
179
|
</For>
|
|
@@ -154,28 +188,38 @@ const StartShortcuts = (props: { appName?: string }) => {
|
|
|
154
188
|
|
|
155
189
|
const HelpShell = (props: {
|
|
156
190
|
session: HelpSession;
|
|
191
|
+
initialArticle?: LayoutHelpArticle;
|
|
157
192
|
close: () => void;
|
|
158
193
|
pageBase?: string;
|
|
159
194
|
documents?: readonly HelpDocumentManifest[];
|
|
160
195
|
includeShortcuts?: boolean;
|
|
161
196
|
accent?: string;
|
|
162
|
-
|
|
197
|
+
/** The app's canvas colours for a reader outside the app canvas. */
|
|
198
|
+
appearance?: string;
|
|
199
|
+
surface?: "modal" | "page" | "embedded" | "panel";
|
|
200
|
+
panel?: LayoutHelpPanelProps;
|
|
163
201
|
syncPageUrl?: boolean;
|
|
164
202
|
}) => {
|
|
165
203
|
const locale = useLocale();
|
|
166
204
|
const t = () => helpMessages.resolve([locale()]).t;
|
|
167
205
|
// Links to this app's Help, by its own route or Core's `/help/apps/<id>`, open in place.
|
|
168
|
-
const shell = useShellData();
|
|
169
|
-
const appId = shell?.appId;
|
|
206
|
+
const shell = props.surface === "panel" ? undefined : useShellData();
|
|
207
|
+
const appId = props.panel?.app?.id ?? shell?.appId;
|
|
170
208
|
const app = shell?.apps.find((candidate) => candidate.id === appId);
|
|
171
|
-
const appName = app?.label;
|
|
209
|
+
const appName = props.panel?.app?.name ?? app?.label;
|
|
210
|
+
const [viewIds, setViewIds] = createSignal(props.panel?.view ?? []);
|
|
211
|
+
const registeredTopics = () => (props.surface === "panel" ? [] : sortedTopics());
|
|
172
212
|
const accessLabel = (level: HelpAccessLevel) => ({ view: t().accessView, edit: t().accessEdit, manage: t().accessManage })[level];
|
|
173
|
-
const [externalTopics, setExternalTopics] = createSignal(mergeTopics(
|
|
213
|
+
const [externalTopics, setExternalTopics] = createSignal(mergeTopics(registeredTopics(), props.documents));
|
|
174
214
|
const [view, setView] = createSignal<HelpView>(props.session.view);
|
|
175
215
|
const [query, setQuery] = createSignal(props.session.query);
|
|
176
216
|
const [activeId, setActiveId] = createSignal<string | null>(props.session.activeId);
|
|
177
|
-
const
|
|
178
|
-
const
|
|
217
|
+
const initialTopic = props.documents?.find((topic) => topic.id === props.session.activeId);
|
|
218
|
+
const initialArticle = initialTopic && props.initialArticle?.payload.id === initialTopic.id ? props.initialArticle : undefined;
|
|
219
|
+
if (initialArticle && initialTopic)
|
|
220
|
+
props.session.articleCache.set(`${initialTopic.locale ?? "en"}:${initialTopic.url}`, initialArticle.payload);
|
|
221
|
+
const [payload, setPayload] = createSignal<HelpDocumentPayload | null>(initialArticle?.payload ?? null);
|
|
222
|
+
const [loading, setLoading] = createSignal(props.session.view === "article" && !initialArticle);
|
|
179
223
|
const [loadError, setLoadError] = createSignal<string | null>(null);
|
|
180
224
|
const [loadAttempt, setLoadAttempt] = createSignal(0);
|
|
181
225
|
const [remoteIds, setRemoteIds] = createSignal<readonly string[] | null>(null);
|
|
@@ -222,7 +266,7 @@ const HelpShell = (props: {
|
|
|
222
266
|
};
|
|
223
267
|
|
|
224
268
|
const restoreArticleScroll = () => {
|
|
225
|
-
if (props.surface !== "modal" || pendingNavigation || pendingHash) return;
|
|
269
|
+
if ((props.surface !== "modal" && props.surface !== "panel") || pendingNavigation || pendingHash) return;
|
|
226
270
|
requestAnimationFrame(() => {
|
|
227
271
|
if (scrollArea && view() === "article") scrollArea.scrollTop = props.session.articleScrollTop;
|
|
228
272
|
});
|
|
@@ -267,12 +311,15 @@ const HelpShell = (props: {
|
|
|
267
311
|
};
|
|
268
312
|
|
|
269
313
|
const topics = externalTopics;
|
|
270
|
-
const start = createMemo(() =>
|
|
314
|
+
const start = createMemo(() =>
|
|
315
|
+
helpStart(topics(), props.surface === "panel" ? viewIds() : props.surface === "modal" ? shell?.help : undefined),
|
|
316
|
+
);
|
|
271
317
|
const activeTopic = createMemo(() => topics().find((topic) => topic.id === activeId()) ?? null);
|
|
272
|
-
//
|
|
318
|
+
// Reuse the serialized parts for the initial object on both sides of the island boundary.
|
|
273
319
|
const articleParts = createMemo(() => {
|
|
274
|
-
const
|
|
275
|
-
|
|
320
|
+
const article = payload();
|
|
321
|
+
if (initialArticle && article === initialArticle.payload) return initialArticle.parts;
|
|
322
|
+
return article?.html ? helpArticleParts(article.html) : null;
|
|
276
323
|
});
|
|
277
324
|
const articleSections = (): HelpSection[] => (view() === "article" ? articleParts()?.sections : undefined) ?? [];
|
|
278
325
|
const normalizedQuery = createMemo(() => query().trim().toLocaleLowerCase());
|
|
@@ -362,9 +409,34 @@ const HelpShell = (props: {
|
|
|
362
409
|
createEffect(syncSession);
|
|
363
410
|
|
|
364
411
|
onMount(() => {
|
|
412
|
+
if (props.surface === "panel") {
|
|
413
|
+
onCleanup(onContextChange((context) => setViewIds(context && context.app === appId ? (context.help ?? []) : [])));
|
|
414
|
+
}
|
|
415
|
+
if (props.surface === "panel" || props.surface === "modal") {
|
|
416
|
+
const key = (event: KeyboardEvent) => {
|
|
417
|
+
if (event.defaultPrevented || event.isComposing) return;
|
|
418
|
+
const target = event.target;
|
|
419
|
+
if (event.key === "Escape" && target === searchInput && query()) {
|
|
420
|
+
event.preventDefault();
|
|
421
|
+
changeQuery("");
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
// Some layouts type "/" with Shift (German: Shift+7), so Shift does not disqualify it.
|
|
425
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
|
426
|
+
if (
|
|
427
|
+
event.key === "/" &&
|
|
428
|
+
!(target instanceof Element && target.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])'))
|
|
429
|
+
) {
|
|
430
|
+
event.preventDefault();
|
|
431
|
+
searchInput?.focus();
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
document.addEventListener("keydown", key);
|
|
435
|
+
onCleanup(() => document.removeEventListener("keydown", key));
|
|
436
|
+
}
|
|
365
437
|
const dialog = root?.closest("dialog");
|
|
366
438
|
dialog?.setAttribute("aria-labelledby", "layout-help-title");
|
|
367
|
-
const update = () => setExternalTopics(mergeTopics(
|
|
439
|
+
const update = () => setExternalTopics(mergeTopics(registeredTopics(), props.documents));
|
|
368
440
|
window.addEventListener(HELP_TOPICS_EVENT, update);
|
|
369
441
|
update();
|
|
370
442
|
onCleanup(() => window.removeEventListener(HELP_TOPICS_EVENT, update));
|
|
@@ -373,7 +445,7 @@ const HelpShell = (props: {
|
|
|
373
445
|
const onScroll = () => {
|
|
374
446
|
if (view() === "article" && scrollArea) props.session.articleScrollTop = scrollArea.scrollTop;
|
|
375
447
|
};
|
|
376
|
-
if (props.surface === "modal") scrollArea?.addEventListener("scroll", onScroll, { passive: true });
|
|
448
|
+
if (props.surface === "modal" || props.surface === "panel") scrollArea?.addEventListener("scroll", onScroll, { passive: true });
|
|
377
449
|
// Layout's breakpoints move page scrolling between its main column and the window; the contents follow.
|
|
378
450
|
let resizeFrame: number | undefined;
|
|
379
451
|
const onResize = () => {
|
|
@@ -452,16 +524,19 @@ const HelpShell = (props: {
|
|
|
452
524
|
loadAttempt();
|
|
453
525
|
const topic = activeTopic();
|
|
454
526
|
const version = ++requestVersion;
|
|
455
|
-
|
|
527
|
+
const cached = topic && view() === "article" ? props.session.articleCache.get(`${topic.locale ?? "en"}:${topic.url}`) : undefined;
|
|
456
528
|
setLoadError(null);
|
|
457
529
|
setLoading(false);
|
|
458
|
-
|
|
459
|
-
const cached = props.session.articleCache.get(`${topic.locale ?? "en"}:${topic.url}`);
|
|
530
|
+
// Keep a seeded payload in place on mount: no empty frame or duplicate scroll restoration.
|
|
460
531
|
if (cached) {
|
|
461
|
-
|
|
462
|
-
|
|
532
|
+
if (untrack(payload) !== cached) {
|
|
533
|
+
setPayload(cached);
|
|
534
|
+
restoreArticleScroll();
|
|
535
|
+
}
|
|
463
536
|
return;
|
|
464
537
|
}
|
|
538
|
+
setPayload(null);
|
|
539
|
+
if (!topic || view() !== "article") return;
|
|
465
540
|
|
|
466
541
|
const controller = new AbortController();
|
|
467
542
|
setLoading(true);
|
|
@@ -636,7 +711,7 @@ const HelpShell = (props: {
|
|
|
636
711
|
{(topic) => (
|
|
637
712
|
<li>
|
|
638
713
|
<button type="button" class="help-start-row focus-ui" data-help-topic={topic.id} onClick={() => openTopic(topic.id)}>
|
|
639
|
-
<span class="help-topic-icon help-start-row-icon app-accent-text" aria-hidden="true">
|
|
714
|
+
<span class="help-topic-icon k2b-icon-tile help-start-row-icon app-accent-text" aria-hidden="true">
|
|
640
715
|
<i class={iconClass(topic.icon)} />
|
|
641
716
|
</span>
|
|
642
717
|
<span class="help-article-link-copy">
|
|
@@ -658,7 +733,7 @@ const HelpShell = (props: {
|
|
|
658
733
|
{(topic) => (
|
|
659
734
|
<li>
|
|
660
735
|
<button type="button" class="help-start-tile focus-ui" data-help-topic={topic.id} onClick={() => openTopic(topic.id)}>
|
|
661
|
-
<span class="help-topic-icon help-start-tile-icon app-accent-text" aria-hidden="true">
|
|
736
|
+
<span class="help-topic-icon k2b-icon-tile help-start-tile-icon app-accent-text" aria-hidden="true">
|
|
662
737
|
<i class={iconClass(topic.icon)} />
|
|
663
738
|
</span>
|
|
664
739
|
<span class="help-start-tile-title">{topic.title}</span>
|
|
@@ -704,7 +779,7 @@ const HelpShell = (props: {
|
|
|
704
779
|
{(link) => (
|
|
705
780
|
<li>
|
|
706
781
|
<a class="help-article-related-link app-accent-scope focus-ui" href={link.url} style={appAccentStyle(link.accent)}>
|
|
707
|
-
<span class="help-topic-icon help-article-related-icon app-accent-text" aria-hidden="true">
|
|
782
|
+
<span class="help-topic-icon k2b-icon-tile help-article-related-icon app-accent-text" aria-hidden="true">
|
|
708
783
|
<i class={iconClass(link.icon)} />
|
|
709
784
|
</span>
|
|
710
785
|
<ArticleLinkTitle link={link} />
|
|
@@ -731,6 +806,8 @@ const HelpShell = (props: {
|
|
|
731
806
|
</ScrollArea>
|
|
732
807
|
);
|
|
733
808
|
|
|
809
|
+
// The dialog and the panel copy from their ⋯ menu; only the page repeats the copy action beside the article.
|
|
810
|
+
const copyInMenu = props.surface === "modal" || props.surface === "panel";
|
|
734
811
|
const pageHref = () => (props.pageBase ? layoutHelpTopicHref(props.pageBase, view() === "article" ? activeId() : null) : undefined);
|
|
735
812
|
const menuItems = () => [
|
|
736
813
|
{
|
|
@@ -740,25 +817,34 @@ const HelpShell = (props: {
|
|
|
740
817
|
? { disabled: true as const }
|
|
741
818
|
: { action: () => void copyFromMenu() }),
|
|
742
819
|
},
|
|
743
|
-
...(
|
|
820
|
+
...(copyInMenu && pageHref() ? [{ label: t().openAsPage, icon: "ti ti-app-window", href: pageHref()! }] : []),
|
|
744
821
|
];
|
|
745
822
|
|
|
746
823
|
return (
|
|
747
824
|
<div
|
|
748
825
|
ref={root}
|
|
749
|
-
class={
|
|
750
|
-
props.surface === "
|
|
751
|
-
? "
|
|
752
|
-
: props.surface === "page"
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
826
|
+
class={
|
|
827
|
+
props.surface === "panel"
|
|
828
|
+
? "help-shell app-accent-scope flex h-full min-h-0 flex-col"
|
|
829
|
+
: `help-shell app-accent-scope flex min-h-0 flex-col ${props.surface === "page" ? "" : "bg-[var(--ui-surface-raised)]"} ${
|
|
830
|
+
props.surface === "modal"
|
|
831
|
+
? "h-[min(48rem,var(--ui-dialog-available-height))] overflow-hidden panel-dialog-shell [box-shadow:var(--ui-shadow-float)]"
|
|
832
|
+
: props.surface === "page"
|
|
833
|
+
? ""
|
|
834
|
+
: "h-full"
|
|
835
|
+
}`
|
|
836
|
+
}
|
|
756
837
|
data-surface={props.surface}
|
|
757
|
-
style={
|
|
838
|
+
style={
|
|
839
|
+
props.surface === "panel"
|
|
840
|
+
? props.panel?.appearance
|
|
841
|
+
: [appAccentStyle(props.accent), props.appearance].filter(Boolean).join(";") || undefined
|
|
842
|
+
}
|
|
758
843
|
>
|
|
759
844
|
{/* One head in every view: the search never moves between the start, an article, and results. An app's embedded
|
|
760
845
|
reader sits in the app's own chrome, which names it and picks its article, so it keeps only the search of its start. */}
|
|
761
|
-
|
|
846
|
+
{/* Help the reader may not see gets no head either: the slot already names the panel, and there is nothing to search. */}
|
|
847
|
+
<Show when={(props.surface !== "embedded" || view() === "hub") && props.panel?.state !== "unavailable"}>
|
|
762
848
|
<header class="help-head" data-help-head="">
|
|
763
849
|
<div class="help-head-inner help-measure">
|
|
764
850
|
<Show when={props.surface !== "embedded"}>
|
|
@@ -768,7 +854,7 @@ const HelpShell = (props: {
|
|
|
768
854
|
when={view() === "article"}
|
|
769
855
|
fallback={
|
|
770
856
|
<span class="help-head-tile" aria-hidden="true">
|
|
771
|
-
<i class={app?.iconClass ?? "ti ti-help"} />
|
|
857
|
+
<i class={props.panel?.app?.icon ?? app?.iconClass ?? "ti ti-help-circle"} />
|
|
772
858
|
</span>
|
|
773
859
|
}
|
|
774
860
|
>
|
|
@@ -777,7 +863,7 @@ const HelpShell = (props: {
|
|
|
777
863
|
</IconButton>
|
|
778
864
|
</Show>
|
|
779
865
|
<h2 id="layout-help-title" class="help-head-title">
|
|
780
|
-
{appName ? t().helpFor({ app: appName }) : t().help}
|
|
866
|
+
{appName ? (props.surface === "panel" ? appName : t().helpFor({ app: appName })) : t().help}
|
|
781
867
|
</h2>
|
|
782
868
|
<div class="help-head-actions" role="group" aria-label={t().actions}>
|
|
783
869
|
<Dropdown.Root label={t().moreActions} position="bottom-left" items={menuItems()}>
|
|
@@ -809,247 +895,263 @@ const HelpShell = (props: {
|
|
|
809
895
|
</Show>
|
|
810
896
|
|
|
811
897
|
<ReaderContent>
|
|
812
|
-
<Show
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
<i class="ti ti-checklist" aria-hidden="true" />
|
|
831
|
-
{t().whatToDo}
|
|
832
|
-
</h3>
|
|
833
|
-
<TaskTiles items={start().tiles} />
|
|
834
|
-
<Show when={start().moreTasks.length > 0}>
|
|
835
|
-
<TopicList items={start().moreTasks} marker="data-help-more-tasks" />
|
|
836
|
-
</Show>
|
|
837
|
-
</section>
|
|
838
|
-
</Show>
|
|
839
|
-
<Show when={start().groups.length > 0}>
|
|
840
|
-
<div class="help-start-groups">
|
|
841
|
-
<For each={start().groups}>
|
|
842
|
-
{(group) => (
|
|
843
|
-
<section
|
|
844
|
-
class="help-start-section"
|
|
845
|
-
data-help-group={group.kind}
|
|
846
|
-
aria-labelledby={`help-start-${group.kind}-title`}
|
|
847
|
-
>
|
|
848
|
-
<h3 id={`help-start-${group.kind}-title`} class="help-start-heading">
|
|
849
|
-
<i class={GROUP_ICONS[group.kind]} aria-hidden="true" />
|
|
850
|
-
{groupTitle(group.kind)}
|
|
851
|
-
</h3>
|
|
852
|
-
<TopicList items={group.topics} />
|
|
853
|
-
</section>
|
|
854
|
-
)}
|
|
855
|
-
</For>
|
|
856
|
-
</div>
|
|
857
|
-
</Show>
|
|
858
|
-
{/* An app without Help says so plainly; only a failed read of its Help offers a way to try again. */}
|
|
859
|
-
<Show when={topics().length === 0}>
|
|
860
|
-
<Show
|
|
861
|
-
when={shell?.helpFailed}
|
|
862
|
-
fallback={<Placeholder icon="ti ti-book-off" title={t().noArticles} description={t().noArticlesDetail} />}
|
|
863
|
-
>
|
|
864
|
-
<NoticeCard tone="danger" title={t().helpUnavailable} detail={t().helpUnavailableDetail} data-help-unavailable="">
|
|
865
|
-
<Button size="sm" variant="secondary" onClick={() => location.reload()}>
|
|
866
|
-
{t().reload}
|
|
867
|
-
</Button>
|
|
868
|
-
</NoticeCard>
|
|
869
|
-
</Show>
|
|
870
|
-
</Show>
|
|
871
|
-
<Show when={props.includeShortcuts !== false}>
|
|
872
|
-
<StartShortcuts appName={appName} />
|
|
873
|
-
</Show>
|
|
874
|
-
</>
|
|
875
|
-
}
|
|
876
|
-
>
|
|
877
|
-
{/* The count takes the place of the first heading, so the first row stays where it was. */}
|
|
878
|
-
<section class="help-start-section" data-help-search-results="">
|
|
879
|
-
{/* A failed full-text search says so in the same line, with a way to try again; the results do not move. */}
|
|
880
|
-
<div class="help-start-status">
|
|
881
|
-
<span aria-live="polite">{t().results({ count: results().length })}</span>
|
|
882
|
-
<Show when={searching()}>
|
|
883
|
-
<i class="ti ti-loader-2 animate-spin" aria-hidden="true" />
|
|
884
|
-
</Show>
|
|
885
|
-
<Show when={searchFailed()}>
|
|
886
|
-
<span class="help-start-status-warning" data-help-search-failed="" title={t().searchTitlesOnly} role="status">
|
|
887
|
-
<i class="ti ti-alert-triangle" aria-hidden="true" />
|
|
888
|
-
<span aria-hidden="true">{t().titlesOnly}</span>
|
|
889
|
-
<span class="sr-only">{t().searchTitlesOnly}</span>
|
|
890
|
-
</span>
|
|
891
|
-
<Button
|
|
892
|
-
size="xs"
|
|
893
|
-
variant="ghost"
|
|
894
|
-
class="help-start-status-retry"
|
|
895
|
-
onClick={() => setSearchAttempt((value) => value + 1)}
|
|
896
|
-
>
|
|
897
|
-
{t().tryAgain}
|
|
898
|
-
</Button>
|
|
899
|
-
</Show>
|
|
900
|
-
</div>
|
|
901
|
-
<Show
|
|
902
|
-
when={results().length > 0}
|
|
903
|
-
fallback={
|
|
904
|
-
<Show when={!searching()}>
|
|
905
|
-
<Placeholder icon="ti ti-search-off" title={t().noResults} />
|
|
898
|
+
<Show
|
|
899
|
+
when={props.panel?.state !== "unavailable"}
|
|
900
|
+
fallback={<Placeholder icon="ti ti-book-off" title={t().helpNotAvailable} description={t().helpNotAvailableDetail} />}
|
|
901
|
+
>
|
|
902
|
+
<Show when={view() === "hub"}>
|
|
903
|
+
<div ref={resultsRegion} data-help-results="" class="help-start help-measure" style={{ "min-height": resultsMinHeight() }}>
|
|
904
|
+
<Show
|
|
905
|
+
when={normalizedQuery()}
|
|
906
|
+
fallback={
|
|
907
|
+
<>
|
|
908
|
+
<Show when={start().view.length > 0}>
|
|
909
|
+
<section class="help-start-section" data-help-view="" aria-labelledby="help-start-view-title">
|
|
910
|
+
<h3 id="help-start-view-title" class="help-start-heading">
|
|
911
|
+
<i class="ti ti-focus-2" aria-hidden="true" />
|
|
912
|
+
{t().forThisView}
|
|
913
|
+
</h3>
|
|
914
|
+
<TopicList items={start().view} />
|
|
915
|
+
</section>
|
|
906
916
|
</Show>
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
<Show when={view() === "article" && activeTopic()}>
|
|
917
|
-
{(topic) => (
|
|
918
|
-
<article
|
|
919
|
-
class="help-article"
|
|
920
|
-
data-surface={props.surface}
|
|
921
|
-
ref={(element) => {
|
|
922
|
-
// Delegated from the article: its body, table of contents, and end hold the links.
|
|
923
|
-
element.addEventListener("click", followHelpLink);
|
|
924
|
-
onCleanup(() => element.removeEventListener("click", followHelpLink));
|
|
925
|
-
}}
|
|
926
|
-
>
|
|
927
|
-
<div class="help-article-layout">
|
|
928
|
-
<div class="help-article-main">
|
|
929
|
-
<header class="help-article-head">
|
|
930
|
-
<p class="help-article-path">
|
|
931
|
-
<Show when={appName}>
|
|
932
|
-
{(name) => (
|
|
933
|
-
<>
|
|
934
|
-
<span class="app-accent-text">{name()}</span>
|
|
935
|
-
<i class="ti ti-chevron-right" aria-hidden="true" />
|
|
936
|
-
</>
|
|
937
|
-
)}
|
|
938
|
-
</Show>
|
|
939
|
-
<button type="button" class="help-article-path-link focus-ui" onClick={goBack}>
|
|
940
|
-
{t().help}
|
|
941
|
-
</button>
|
|
942
|
-
</p>
|
|
943
|
-
<h3 ref={articleHeading} tabindex="-1" class="help-article-title focus-ui">
|
|
944
|
-
{topic().title}
|
|
945
|
-
</h3>
|
|
946
|
-
<Show
|
|
947
|
-
when={articleParts()?.lead}
|
|
948
|
-
fallback={
|
|
949
|
-
// An article without a leading paragraph, or a built-in topic, shows its description; a loading article shows neither yet.
|
|
950
|
-
<Show when={articleParts() && topic().description}>
|
|
951
|
-
{(description) => <p class="help-article-lead">{description()}</p>}
|
|
917
|
+
<Show when={start().tiles.length > 0}>
|
|
918
|
+
<section class="help-start-section" data-help-tasks="" aria-labelledby="help-start-tasks-title">
|
|
919
|
+
<h3 id="help-start-tasks-title" class="help-start-heading">
|
|
920
|
+
<i class="ti ti-checklist" aria-hidden="true" />
|
|
921
|
+
{t().whatToDo}
|
|
922
|
+
</h3>
|
|
923
|
+
<TaskTiles items={start().tiles} />
|
|
924
|
+
<Show when={start().moreTasks.length > 0}>
|
|
925
|
+
<TopicList items={start().moreTasks} marker="data-help-more-tasks" />
|
|
952
926
|
</Show>
|
|
953
|
-
|
|
954
|
-
>
|
|
955
|
-
{(lead) => <MarkdownView trustedHtml={lead()} class="help-document help-article-lead" />}
|
|
927
|
+
</section>
|
|
956
928
|
</Show>
|
|
957
|
-
<Show when={
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
929
|
+
<Show when={start().groups.length > 0}>
|
|
930
|
+
<div class="help-start-groups">
|
|
931
|
+
<For each={start().groups}>
|
|
932
|
+
{(group) => (
|
|
933
|
+
<section
|
|
934
|
+
class="help-start-section"
|
|
935
|
+
data-help-group={group.kind}
|
|
936
|
+
aria-labelledby={`help-start-${group.kind}-title`}
|
|
937
|
+
>
|
|
938
|
+
<h3 id={`help-start-${group.kind}-title`} class="help-start-heading">
|
|
939
|
+
<i class={GROUP_ICONS[group.kind]} aria-hidden="true" />
|
|
940
|
+
{groupTitle(group.kind)}
|
|
941
|
+
</h3>
|
|
942
|
+
<TopicList items={group.topics} />
|
|
943
|
+
</section>
|
|
968
944
|
)}
|
|
945
|
+
</For>
|
|
946
|
+
</div>
|
|
947
|
+
</Show>
|
|
948
|
+
{/* An app without Help says so plainly; only a failed read of its Help offers a way to try again. */}
|
|
949
|
+
<Show when={topics().length === 0}>
|
|
950
|
+
<Show
|
|
951
|
+
when={props.panel?.state === "failed" || shell?.helpFailed}
|
|
952
|
+
fallback={<Placeholder icon="ti ti-book-off" title={t().noArticles} description={t().noArticlesDetail} />}
|
|
953
|
+
>
|
|
954
|
+
<NoticeCard tone="danger" title={t().helpUnavailable} detail={t().helpUnavailableDetail} data-help-unavailable="">
|
|
955
|
+
<Button size="sm" variant="secondary" onClick={() => location.reload()}>
|
|
956
|
+
{t().reload}
|
|
957
|
+
</Button>
|
|
958
|
+
</NoticeCard>
|
|
959
|
+
</Show>
|
|
960
|
+
</Show>
|
|
961
|
+
<Show when={props.includeShortcuts !== false}>
|
|
962
|
+
<StartShortcuts appName={appName} panel={props.surface === "panel"} platform={props.panel?.platform} />
|
|
963
|
+
</Show>
|
|
964
|
+
</>
|
|
965
|
+
}
|
|
966
|
+
>
|
|
967
|
+
{/* The count takes the place of the first heading, so the first row stays where it was. */}
|
|
968
|
+
<section class="help-start-section" data-help-search-results="">
|
|
969
|
+
{/* A failed full-text search says so in the same line, with a way to try again; the results do not move. */}
|
|
970
|
+
<div class="help-start-status">
|
|
971
|
+
<span aria-live="polite">{t().results({ count: results().length })}</span>
|
|
972
|
+
<Show when={searching()}>
|
|
973
|
+
<i class="ti ti-loader-2 animate-spin" aria-hidden="true" />
|
|
974
|
+
</Show>
|
|
975
|
+
<Show when={searchFailed()}>
|
|
976
|
+
<span class="help-start-status-warning" data-help-search-failed="" title={t().searchTitlesOnly} role="status">
|
|
977
|
+
<i class="ti ti-alert-triangle" aria-hidden="true" />
|
|
978
|
+
<span aria-hidden="true">{t().titlesOnly}</span>
|
|
979
|
+
<span class="sr-only">{t().searchTitlesOnly}</span>
|
|
980
|
+
</span>
|
|
981
|
+
<Button
|
|
982
|
+
size="xs"
|
|
983
|
+
variant="ghost"
|
|
984
|
+
class="help-start-status-retry"
|
|
985
|
+
onClick={() => setSearchAttempt((value) => value + 1)}
|
|
986
|
+
>
|
|
987
|
+
{t().tryAgain}
|
|
988
|
+
</Button>
|
|
989
|
+
</Show>
|
|
990
|
+
</div>
|
|
991
|
+
<Show
|
|
992
|
+
when={results().length > 0}
|
|
993
|
+
fallback={
|
|
994
|
+
<Show when={!searching()}>
|
|
995
|
+
<Placeholder icon="ti ti-search-off" title={t().noResults} />
|
|
996
|
+
</Show>
|
|
997
|
+
}
|
|
998
|
+
>
|
|
999
|
+
<TopicList items={results()} />
|
|
1000
|
+
</Show>
|
|
1001
|
+
</section>
|
|
1002
|
+
</Show>
|
|
1003
|
+
</div>
|
|
1004
|
+
</Show>
|
|
1005
|
+
|
|
1006
|
+
<Show when={view() === "article" && activeTopic()}>
|
|
1007
|
+
{(topic) => (
|
|
1008
|
+
<article
|
|
1009
|
+
class="help-article"
|
|
1010
|
+
data-surface={props.surface}
|
|
1011
|
+
ref={(element) => {
|
|
1012
|
+
// Delegated from the article: its body, table of contents, and end hold the links.
|
|
1013
|
+
element.addEventListener("click", followHelpLink);
|
|
1014
|
+
onCleanup(() => element.removeEventListener("click", followHelpLink));
|
|
1015
|
+
}}
|
|
1016
|
+
>
|
|
1017
|
+
<div class="help-article-layout">
|
|
1018
|
+
<div class="help-article-main">
|
|
1019
|
+
<header class="help-article-head" data-help-article-head="">
|
|
1020
|
+
<div class="help-article-head-copy">
|
|
1021
|
+
{/* In the panel, its head already names the app and leads back to the start. */}
|
|
1022
|
+
<Show when={props.surface !== "panel"}>
|
|
1023
|
+
<p class="help-article-path">
|
|
1024
|
+
<Show when={appName}>
|
|
1025
|
+
{(name) => (
|
|
1026
|
+
<>
|
|
1027
|
+
<span class="app-accent-text">{name()}</span>
|
|
1028
|
+
<i class="ti ti-chevron-right" aria-hidden="true" />
|
|
1029
|
+
</>
|
|
1030
|
+
)}
|
|
1031
|
+
</Show>
|
|
1032
|
+
<button type="button" class="help-article-path-link focus-ui" onClick={goBack}>
|
|
1033
|
+
{t().help}
|
|
1034
|
+
</button>
|
|
1035
|
+
</p>
|
|
969
1036
|
</Show>
|
|
970
|
-
<
|
|
971
|
-
{(
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1037
|
+
<h3 ref={articleHeading} tabindex="-1" class="help-article-title focus-ui">
|
|
1038
|
+
{topic().title}
|
|
1039
|
+
</h3>
|
|
1040
|
+
<Show
|
|
1041
|
+
when={articleParts()?.lead}
|
|
1042
|
+
fallback={
|
|
1043
|
+
// An article without a leading paragraph, or a built-in topic, shows its description; a loading article shows neither yet.
|
|
1044
|
+
<Show when={articleParts() && topic().description}>
|
|
1045
|
+
{(description) => <p class="help-article-lead">{description()}</p>}
|
|
1046
|
+
</Show>
|
|
1047
|
+
}
|
|
1048
|
+
>
|
|
1049
|
+
{(lead) => <MarkdownView trustedHtml={lead()} class="help-document help-article-lead" />}
|
|
977
1050
|
</Show>
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1051
|
+
<Show when={payload()?.needs || articleParts()?.steps}>
|
|
1052
|
+
{/* One row of facts; the access level appears here and nowhere else in the reader's chrome. */}
|
|
1053
|
+
<ul class="help-article-facts" aria-label={t().articleFacts}>
|
|
1054
|
+
<Show when={payload()?.needs}>
|
|
1055
|
+
{(level) => (
|
|
1056
|
+
<li class="help-article-fact" data-fact="needs">
|
|
1057
|
+
<i class="ti ti-key" aria-hidden="true" />
|
|
1058
|
+
<span>
|
|
1059
|
+
{t().youNeed} <strong>{accessLabel(level())}</strong>
|
|
1060
|
+
</span>
|
|
1061
|
+
</li>
|
|
1062
|
+
)}
|
|
1063
|
+
</Show>
|
|
1064
|
+
<Show when={articleParts()?.steps}>
|
|
1065
|
+
{(count) => (
|
|
1066
|
+
<li class="help-article-fact" data-fact="steps">
|
|
1067
|
+
<i class="ti ti-list-numbers" aria-hidden="true" />
|
|
1068
|
+
<span>{t().steps({ count: count() })}</span>
|
|
1069
|
+
</li>
|
|
1070
|
+
)}
|
|
1071
|
+
</Show>
|
|
1072
|
+
</ul>
|
|
999
1073
|
</Show>
|
|
1000
|
-
<Show when={
|
|
1001
|
-
<
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1074
|
+
<Show when={articleSections().length > 0 || !copyInMenu}>
|
|
1075
|
+
<div class="help-article-tools">
|
|
1076
|
+
<Show when={articleSections().length > 0}>
|
|
1077
|
+
<Dropdown.Root
|
|
1078
|
+
label={t().onThisPage}
|
|
1079
|
+
position="bottom-left"
|
|
1080
|
+
items={articleSections().map((section) => ({
|
|
1081
|
+
label: section.label,
|
|
1082
|
+
choice: "radio" as const,
|
|
1083
|
+
checked: () => activeSectionId() === section.id,
|
|
1084
|
+
action: () => openArticleSection(section),
|
|
1085
|
+
}))}
|
|
1086
|
+
>
|
|
1087
|
+
<Dropdown.Trigger label={t().onThisPage} size="sm" tooltip={false} variant="secondary">
|
|
1088
|
+
<i class="ti ti-list" aria-hidden="true" />
|
|
1089
|
+
<span>{t().onThisPage}</span>
|
|
1090
|
+
<i class="ti ti-chevron-down" aria-hidden="true" />
|
|
1091
|
+
</Dropdown.Trigger>
|
|
1092
|
+
</Dropdown.Root>
|
|
1093
|
+
</Show>
|
|
1094
|
+
<Show when={!copyInMenu}>
|
|
1095
|
+
<IconButton
|
|
1096
|
+
class="help-article-tools-copy"
|
|
1097
|
+
disabled={!payload()}
|
|
1098
|
+
label={articleClipboard.wasCopied() ? t().articleCopied : t().copyArticle}
|
|
1099
|
+
title={articleClipboard.wasCopied() ? t().copied : t().copyArticle}
|
|
1100
|
+
onClick={() => void copyArticle(topic(), payload())}
|
|
1101
|
+
>
|
|
1102
|
+
<i class={articleClipboard.wasCopied() ? "ti ti-check" : "ti ti-markdown"} />
|
|
1103
|
+
</IconButton>
|
|
1104
|
+
</Show>
|
|
1105
|
+
</div>
|
|
1010
1106
|
</Show>
|
|
1011
1107
|
</div>
|
|
1108
|
+
{/* The embedded readers sit in the app's own chrome and keep the flat head. */}
|
|
1109
|
+
<Show when={props.surface !== "embedded"}>
|
|
1110
|
+
<span class="help-article-icon k2b-icon-tile app-accent-text" aria-hidden="true">
|
|
1111
|
+
<i class={iconClass(topic().icon)} />
|
|
1112
|
+
</span>
|
|
1113
|
+
</Show>
|
|
1114
|
+
</header>
|
|
1115
|
+
<Show when={loading()}>
|
|
1116
|
+
<div class="min-h-[24rem]">
|
|
1117
|
+
<Placeholder state="loading" variant="panel" title={t().loading} />
|
|
1118
|
+
</div>
|
|
1119
|
+
</Show>
|
|
1120
|
+
<Show when={loadError()}>
|
|
1121
|
+
{(message) => (
|
|
1122
|
+
<NoticeCard tone="danger" title={t().loadFailed} detail={message()}>
|
|
1123
|
+
<Button size="sm" variant="secondary" onClick={() => setLoadAttempt((value) => value + 1)}>
|
|
1124
|
+
{t().tryAgain}
|
|
1125
|
+
</Button>
|
|
1126
|
+
</NoticeCard>
|
|
1127
|
+
)}
|
|
1012
1128
|
</Show>
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
<div class="min-h-[24rem]">
|
|
1016
|
-
<Placeholder state="loading" variant="panel" title={t().loading} />
|
|
1129
|
+
<div ref={articleContent}>
|
|
1130
|
+
<Show when={articleParts()}>{(parts) => <MarkdownView trustedHtml={parts().body} class="help-document" />}</Show>
|
|
1017
1131
|
</div>
|
|
1018
|
-
|
|
1019
|
-
<Show when={loadError()}>
|
|
1020
|
-
{(message) => (
|
|
1021
|
-
<NoticeCard tone="danger" title={t().loadFailed} detail={message()}>
|
|
1022
|
-
<Button size="sm" variant="secondary" onClick={() => setLoadAttempt((value) => value + 1)}>
|
|
1023
|
-
{t().tryAgain}
|
|
1024
|
-
</Button>
|
|
1025
|
-
</NoticeCard>
|
|
1026
|
-
)}
|
|
1027
|
-
</Show>
|
|
1028
|
-
<div ref={articleContent}>
|
|
1029
|
-
<Show when={articleParts()}>{(parts) => <MarkdownView trustedHtml={parts().body} class="help-document" />}</Show>
|
|
1132
|
+
<Show when={payload()}>{(document) => <ArticleEnd document={document()} />}</Show>
|
|
1030
1133
|
</div>
|
|
1031
|
-
<
|
|
1134
|
+
<aside class="help-article-aside">
|
|
1135
|
+
<div class="help-article-aside-sticky">
|
|
1136
|
+
<TableOfContents
|
|
1137
|
+
items={articleSections()}
|
|
1138
|
+
title={t().onThisPage}
|
|
1139
|
+
scrollRoot={tocScrollRoot}
|
|
1140
|
+
contentRoot={() => articleContent}
|
|
1141
|
+
onActiveChange={(id) => setActiveSectionId(id ?? null)}
|
|
1142
|
+
/>
|
|
1143
|
+
<Show when={!copyInMenu}>
|
|
1144
|
+
<Button size="sm" variant="ghost" disabled={!payload()} onClick={() => void copyArticle(topic(), payload())}>
|
|
1145
|
+
<i class={articleClipboard.wasCopied() ? "ti ti-check" : "ti ti-markdown"} />
|
|
1146
|
+
{articleClipboard.wasCopied() ? t().copied : t().copyMarkdown}
|
|
1147
|
+
</Button>
|
|
1148
|
+
</Show>
|
|
1149
|
+
</div>
|
|
1150
|
+
</aside>
|
|
1032
1151
|
</div>
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
items={articleSections()}
|
|
1037
|
-
title={t().onThisPage}
|
|
1038
|
-
scrollRoot={tocScrollRoot}
|
|
1039
|
-
contentRoot={() => articleContent}
|
|
1040
|
-
onActiveChange={(id) => setActiveSectionId(id ?? null)}
|
|
1041
|
-
/>
|
|
1042
|
-
<Show when={props.surface !== "modal"}>
|
|
1043
|
-
<Button size="sm" variant="ghost" disabled={!payload()} onClick={() => void copyArticle(topic(), payload())}>
|
|
1044
|
-
<i class={articleClipboard.wasCopied() ? "ti ti-check" : "ti ti-markdown"} />
|
|
1045
|
-
{articleClipboard.wasCopied() ? t().copied : t().copyMarkdown}
|
|
1046
|
-
</Button>
|
|
1047
|
-
</Show>
|
|
1048
|
-
</div>
|
|
1049
|
-
</aside>
|
|
1050
|
-
</div>
|
|
1051
|
-
</article>
|
|
1052
|
-
)}
|
|
1152
|
+
</article>
|
|
1153
|
+
)}
|
|
1154
|
+
</Show>
|
|
1053
1155
|
</Show>
|
|
1054
1156
|
<span class="sr-only" aria-live="polite">
|
|
1055
1157
|
{articleClipboard.wasCopied() ? t().articleCopiedAnnouncement : ""}
|
|
@@ -1079,14 +1181,15 @@ const createSession = (initialTopic?: string): HelpSession => {
|
|
|
1079
1181
|
|
|
1080
1182
|
/**
|
|
1081
1183
|
* Render the shared Help experience as a full page. Apps pass the same
|
|
1082
|
-
* manifest used by `Layout.HelpDocuments`;
|
|
1083
|
-
* from the central Help API.
|
|
1184
|
+
* manifest used by `Layout.HelpDocuments`; initialArticle supplies the first
|
|
1185
|
+
* article, while other articles load from the central Help API.
|
|
1084
1186
|
*/
|
|
1085
1187
|
export function LayoutHelpPage(props: LayoutHelpPageProps) {
|
|
1086
1188
|
const session = createSession(props.initialTopic);
|
|
1087
1189
|
return (
|
|
1088
1190
|
<HelpShell
|
|
1089
1191
|
session={session}
|
|
1192
|
+
initialArticle={props.initialTopic ? props.initialArticle : undefined}
|
|
1090
1193
|
close={() => {}}
|
|
1091
1194
|
documents={props.documents}
|
|
1092
1195
|
includeShortcuts={props.includeShortcuts}
|
|
@@ -1098,12 +1201,70 @@ export function LayoutHelpPage(props: LayoutHelpPageProps) {
|
|
|
1098
1201
|
);
|
|
1099
1202
|
}
|
|
1100
1203
|
|
|
1101
|
-
/**
|
|
1204
|
+
/** Help inside a provider document; the server snapshot also owns the first browser render. */
|
|
1205
|
+
export function LayoutHelpPanel(props: LayoutHelpPanelProps) {
|
|
1206
|
+
const session = createSession(props.initialTopic);
|
|
1207
|
+
return (
|
|
1208
|
+
<HelpShell
|
|
1209
|
+
session={session}
|
|
1210
|
+
initialArticle={props.initialTopic ? props.initialArticle : undefined}
|
|
1211
|
+
close={() => {}}
|
|
1212
|
+
documents={props.documents}
|
|
1213
|
+
pageBase={props.pageBase}
|
|
1214
|
+
surface="panel"
|
|
1215
|
+
panel={props}
|
|
1216
|
+
/>
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/** The dialog renders outside the app canvas; it takes the app's canvas colours along for the article head. */
|
|
1221
|
+
const canvasAppearance = () => {
|
|
1222
|
+
const canvas = document.querySelector(".cloud-app-canvas");
|
|
1223
|
+
if (!canvas) return undefined;
|
|
1224
|
+
const style = getComputedStyle(canvas);
|
|
1225
|
+
return CANVAS_PROPERTIES.flatMap((name) => {
|
|
1226
|
+
const value = style.getPropertyValue(name).trim();
|
|
1227
|
+
return value ? [`${name}:${value}`] : [];
|
|
1228
|
+
}).join(";");
|
|
1229
|
+
};
|
|
1230
|
+
const CANVAS_PROPERTIES = [
|
|
1231
|
+
"--app-canvas-from",
|
|
1232
|
+
"--app-canvas-via",
|
|
1233
|
+
"--app-canvas-to",
|
|
1234
|
+
"--app-canvas-angle",
|
|
1235
|
+
"--app-canvas-strength",
|
|
1236
|
+
"--app-canvas-dark-strength",
|
|
1237
|
+
];
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Open Core's Help provider in the panel slot, or fall back to the current app's dialog when no slot accepts it
|
|
1241
|
+
* (anonymous pages, `Layout panel={false}`, an app on an older @k2b/cloud). On a phone the fallback is the shared
|
|
1242
|
+
* bottom sheet at full height, like the panel's phone sheet.
|
|
1243
|
+
*/
|
|
1102
1244
|
export function openLayoutHelpDialog() {
|
|
1245
|
+
if (openPanel("core")) return;
|
|
1103
1246
|
const session = createSession();
|
|
1104
1247
|
const accent = readShellData()?.accent;
|
|
1248
|
+
const appearance = canvasAppearance();
|
|
1249
|
+
const pageBase = window.__cloudLayoutHelpPageBase;
|
|
1250
|
+
if (window.matchMedia("(max-width: 47.999rem)").matches) {
|
|
1251
|
+
void dialogCore.open<void>(
|
|
1252
|
+
(close, context) => (
|
|
1253
|
+
<BottomSheet onDismiss={context.requestDismiss}>
|
|
1254
|
+
<HelpShell session={session} close={() => close()} accent={accent} appearance={appearance} surface="modal" pageBase={pageBase} />
|
|
1255
|
+
</BottomSheet>
|
|
1256
|
+
),
|
|
1257
|
+
{
|
|
1258
|
+
...bottomSheetOptions,
|
|
1259
|
+
panelClassName: `${bottomSheetOptions.panelClassName} cloud-help-sheet`,
|
|
1260
|
+
ariaLabel: helpMessages.resolve([document.documentElement.lang || "en"]).t.help,
|
|
1261
|
+
history: true,
|
|
1262
|
+
},
|
|
1263
|
+
);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1105
1266
|
void prompts.dialog<void>(
|
|
1106
|
-
(close) => <HelpShell session={session} close={close} accent={accent} surface="modal" pageBase={
|
|
1267
|
+
(close) => <HelpShell session={session} close={close} accent={accent} appearance={appearance} surface="modal" pageBase={pageBase} />,
|
|
1107
1268
|
{ surface: "bare", header: false, size: "wide" },
|
|
1108
1269
|
);
|
|
1109
1270
|
}
|