@k2b/cloud 0.37.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/src/_internal/help-catalog.ts +123 -4
- package/src/_internal/help.ts +27 -6
- package/src/account/Avatar.tsx +20 -2
- package/src/account/ui.ts +1 -1
- package/src/ai/browser.ts +1 -0
- package/src/ai/chart-block.ts +44 -13
- package/src/ai/chat/chart-block.tsx +2 -11
- package/src/ai/chat/messages.ts +10 -0
- package/src/ai/chat/web-tools.tsx +23 -6
- package/src/ai/chat-tasks.ts +65 -1
- package/src/ai/default-tool-contracts.ts +30 -13
- package/src/ai/fetch-file-tool.ts +37 -3
- package/src/ai/migrate.ts +19 -2
- package/src/ai/readable-html.ts +1057 -0
- package/src/api/help.ts +23 -4
- package/src/api/mcp.ts +8 -2
- package/src/browser/FileChooser.tsx +11 -3
- package/src/browser/choose-files.browser-harness.tsx +4 -1
- package/src/browser/file-chooser-messages.ts +4 -0
- package/src/browser/file-providers.ts +14 -1
- package/src/browser/provider-folder.tsx +58 -5
- package/src/browser/resource-picker.tsx +5 -1
- package/src/cli/admin/workflows.ts +1 -0
- package/src/config/env.ts +14 -0
- package/src/contracts/file-provider.ts +48 -1
- package/src/contracts/outgoing-mail.ts +8 -1
- package/src/server/help.ts +136 -10
- package/src/server/services/geo.ts +21 -4
- package/src/services/allowed-network-fetch.ts +189 -0
- package/src/services/help/index.ts +46 -5
- package/src/services/help/store.ts +26 -5
- package/src/services/help/types.ts +10 -2
- package/src/services/index.ts +1 -0
- package/src/services/outgoing-mail/dispatcher.ts +7 -2
- package/src/services/outgoing-mail/resolve-host.ts +39 -0
- package/src/services/outgoing-mail/test-send.ts +18 -3
- package/src/services/outgoing-mail/transport.ts +24 -13
- package/src/services/settings/index.ts +48 -26
- package/src/shared/help.ts +41 -0
- package/src/shared/index.ts +9 -1
- package/src/shared/markdown/extensions/guided-help.ts +208 -64
- package/src/shared/markdown/formula.ts +8 -8
- package/src/shared/markdown/index.ts +72 -11
- package/src/ssr/Layout.tsx +20 -2
- package/src/ssr/LayoutHelp.browser-harness.tsx +130 -27
- package/src/ssr/LayoutHelp.tsx +569 -410
- package/src/ssr/ShellRuntime.island.tsx +17 -25
- package/src/ssr/help-messages.ts +60 -38
- package/src/ssr/help.ts +3 -0
- package/src/ssr/layout-help-article.ts +41 -0
- package/src/ssr/layout-help-navigation.ts +0 -12
- package/src/ssr/layout-help-registry.tsx +4 -11
- package/src/ssr/layout-help-search.ts +9 -12
- package/src/ssr/layout-help-start.ts +68 -0
- package/src/ssr/platform-messages.ts +4 -0
- package/src/ssr/shell-data.tsx +7 -0
- package/src/ssr/shell-shortcuts.ts +8 -0
- package/src/styles/effects.css +688 -185
- package/src/styles/resource-search.css +25 -0
- package/src/workflows/store/events.ts +24 -0
- package/src/workflows/store/index.ts +8 -0
- package/src/workflows/store/observability.ts +6 -1
- package/src/workflows/store/retention.ts +157 -0
|
@@ -2,6 +2,7 @@ import type { HelpDocumentManifest } from "@k2b/cloud/shared";
|
|
|
2
2
|
import {
|
|
3
3
|
followPageTrail,
|
|
4
4
|
installAppWorkspaceController,
|
|
5
|
+
installInputModeController,
|
|
5
6
|
installPageTitleController,
|
|
6
7
|
serializeAppWorkspaceLayoutState,
|
|
7
8
|
useLocale,
|
|
@@ -27,6 +28,7 @@ import { LayoutHelpDocuments } from "./layout-help-registry";
|
|
|
27
28
|
import { installPageTrailRuntime } from "./page-trail-runtime";
|
|
28
29
|
import { platformMessages } from "./platform-messages";
|
|
29
30
|
import { readShellData } from "./shell-data";
|
|
31
|
+
import { shellShortcuts } from "./shell-shortcuts";
|
|
30
32
|
import { syncTimezoneCookie } from "./timezone-cookie";
|
|
31
33
|
|
|
32
34
|
export type ShellRuntimeProps = {
|
|
@@ -66,6 +68,7 @@ export default function ShellRuntime(props: ShellRuntimeProps) {
|
|
|
66
68
|
const locale = useLocale();
|
|
67
69
|
const t = () => platformMessages.resolve([locale()]).t;
|
|
68
70
|
onMount(() => syncTimezoneCookie(true));
|
|
71
|
+
onMount(() => onCleanup(installInputModeController()));
|
|
69
72
|
|
|
70
73
|
if (props.workspace) {
|
|
71
74
|
onMount(() => {
|
|
@@ -142,31 +145,20 @@ export default function ShellRuntime(props: ShellRuntimeProps) {
|
|
|
142
145
|
),
|
|
143
146
|
),
|
|
144
147
|
);
|
|
145
|
-
createEffect(() =>
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
registerContextAwareCommand({
|
|
160
|
-
id: "cloud.help",
|
|
161
|
-
title: t().openShortcutHelp,
|
|
162
|
-
description: t().shortcutHelpDescription,
|
|
163
|
-
icon: "ti ti-help-circle",
|
|
164
|
-
shortcut: "shift+/",
|
|
165
|
-
// Returning the load keeps the Command busy until the dialog is open; a second press opens no second dialog.
|
|
166
|
-
action: () => importOnDemand(() => import("./LayoutHelp")).then((help) => help?.openLayoutHelpDialog()),
|
|
167
|
-
}),
|
|
168
|
-
),
|
|
169
|
-
);
|
|
148
|
+
createEffect(() => {
|
|
149
|
+
for (const command of shellShortcuts(t())) {
|
|
150
|
+
onCleanup(
|
|
151
|
+
registerContextAwareCommand({
|
|
152
|
+
...command,
|
|
153
|
+
action:
|
|
154
|
+
command.id === "cloud.search"
|
|
155
|
+
? { search: {} }
|
|
156
|
+
: // Returning the load keeps the Command busy until the dialog is open; a second press opens no second dialog.
|
|
157
|
+
() => importOnDemand(() => import("./LayoutHelp")).then((help) => help?.openLayoutHelpDialog()),
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
170
162
|
}
|
|
171
163
|
onMount(() => {
|
|
172
164
|
if (props.push) {
|
package/src/ssr/help-messages.ts
CHANGED
|
@@ -5,38 +5,50 @@ export const helpMessages = i18n.define({
|
|
|
5
5
|
baseLocale: "en",
|
|
6
6
|
messages: {
|
|
7
7
|
en: {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
helpFor: ({ app }: { app: string }) => `Help for ${app}`,
|
|
9
|
+
moreActions: "More Help actions",
|
|
10
|
+
copyAsMarkdown: "Copy as Markdown",
|
|
11
|
+
openAsPage: "Open as page",
|
|
12
|
+
copyFailed: ({ message }: { message: string }) => `Could not copy the article: ${message}`,
|
|
13
|
+
forThisView: "For this view",
|
|
14
|
+
whatToDo: "What do you want to do?",
|
|
15
|
+
groupStart: "Get started",
|
|
16
|
+
groupReference: "Look up",
|
|
17
|
+
groupTroubleshoot: "Solve problems",
|
|
18
|
+
keyboardShortcuts: "Keyboard shortcuts",
|
|
19
|
+
shortcutsEverywhere: "Everywhere",
|
|
20
|
+
shortcutsInApp: ({ app }: { app: string }) => `In ${app}`,
|
|
21
|
+
shortcutsInThisApp: "In this app",
|
|
22
|
+
shortcutsWithSelection: "With a selection",
|
|
23
|
+
noArticles: "No Help articles here yet",
|
|
24
|
+
reload: "Reload",
|
|
25
|
+
noArticlesDetail: "This app has not published Help yet.",
|
|
26
|
+
helpUnavailable: "Help could not be loaded",
|
|
27
|
+
helpUnavailableDetail: "Reload the page to try again.",
|
|
28
|
+
titlesOnly: "Titles only",
|
|
29
|
+
searchTitlesOnly: "Full-text search is not available right now. These results match titles and descriptions only.",
|
|
11
30
|
searchHelp: "Search help",
|
|
12
|
-
section: "Section",
|
|
13
31
|
help: "Help",
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
32
|
+
youNeed: "You need",
|
|
33
|
+
accessView: "View",
|
|
34
|
+
accessEdit: "Edit",
|
|
35
|
+
accessManage: "Manage",
|
|
36
|
+
steps: ({ count }: { count: number }) => `${count} ${count === 1 ? "step" : "steps"}`,
|
|
37
|
+
articleFacts: "About this article",
|
|
38
|
+
next: "Next",
|
|
39
|
+
related: "Related",
|
|
19
40
|
back: "Back",
|
|
20
|
-
backToHelp: "Back to help",
|
|
21
41
|
backToTopics: "Back to help topics",
|
|
22
42
|
actions: "Help actions",
|
|
23
43
|
copied: "Copied",
|
|
24
44
|
helpCopied: "Help copied as Markdown",
|
|
25
|
-
copyAllHelp: "Copy all help as Markdown",
|
|
26
45
|
articleCopied: "Article copied as Markdown",
|
|
27
46
|
copyArticle: "Copy article as Markdown",
|
|
28
|
-
openWindow: "Open help in a new browser window",
|
|
29
|
-
openFullPage: "Open full-page help",
|
|
30
47
|
closeHelp: "Close help",
|
|
31
48
|
close: "Close",
|
|
32
|
-
heading: "How can we help?",
|
|
33
|
-
headingDescription: "Find a task, concept, or shortcut for the current app.",
|
|
34
49
|
preparing: "Preparing…",
|
|
35
|
-
copyAllShort: "Copy all as Markdown",
|
|
36
50
|
copyAllFailed: ({ message }: { message: string }) => `Could not copy all help: ${message}`,
|
|
37
51
|
searchPlaceholder: "Search help…",
|
|
38
|
-
startHere: "Start here",
|
|
39
|
-
allTopics: "All topics",
|
|
40
52
|
results: ({ count }: { count: number }) => `${count} ${count === 1 ? "result" : "results"}`,
|
|
41
53
|
noResults: "No help topic matches this search.",
|
|
42
54
|
copyMarkdown: "Copy Markdown",
|
|
@@ -45,7 +57,6 @@ export const helpMessages = i18n.define({
|
|
|
45
57
|
tryAgain: "Try again",
|
|
46
58
|
onThisPage: "On this page",
|
|
47
59
|
articleCopiedAnnouncement: "Article copied as Markdown.",
|
|
48
|
-
allCopiedAnnouncement: "All help copied as Markdown.",
|
|
49
60
|
searchFailed: "Help could not be searched. Try again.",
|
|
50
61
|
requestFailed: "Help could not be loaded. Try again.",
|
|
51
62
|
invalidDocument: "Help server returned an invalid document",
|
|
@@ -53,38 +64,50 @@ export const helpMessages = i18n.define({
|
|
|
53
64
|
copyFallback: "Could not copy help",
|
|
54
65
|
},
|
|
55
66
|
de: {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
helpFor: ({ app }) => `Hilfe zu ${app}`,
|
|
68
|
+
moreActions: "Weitere Hilfeaktionen",
|
|
69
|
+
copyAsMarkdown: "Als Markdown kopieren",
|
|
70
|
+
openAsPage: "Als Seite öffnen",
|
|
71
|
+
copyFailed: ({ message }) => `Der Artikel konnte nicht kopiert werden: ${message}`,
|
|
72
|
+
forThisView: "Für diese Ansicht",
|
|
73
|
+
whatToDo: "Was möchtest du tun?",
|
|
74
|
+
groupStart: "Loslegen",
|
|
75
|
+
groupReference: "Nachschlagen",
|
|
76
|
+
groupTroubleshoot: "Probleme lösen",
|
|
77
|
+
keyboardShortcuts: "Tastaturkürzel",
|
|
78
|
+
shortcutsEverywhere: "Überall",
|
|
79
|
+
shortcutsInApp: ({ app }) => `In ${app}`,
|
|
80
|
+
shortcutsInThisApp: "In dieser App",
|
|
81
|
+
shortcutsWithSelection: "Mit Auswahl",
|
|
82
|
+
noArticles: "Hier gibt es noch keine Hilfeartikel",
|
|
83
|
+
reload: "Neu laden",
|
|
84
|
+
noArticlesDetail: "Diese App hat noch keine Hilfe veröffentlicht.",
|
|
85
|
+
helpUnavailable: "Die Hilfe konnte nicht geladen werden",
|
|
86
|
+
helpUnavailableDetail: "Lade die Seite neu, um es erneut zu versuchen.",
|
|
87
|
+
titlesOnly: "Nur Titel",
|
|
88
|
+
searchTitlesOnly: "Die Volltextsuche ist gerade nicht verfügbar. Diese Treffer passen nur zu Titeln und Beschreibungen.",
|
|
59
89
|
searchHelp: "Hilfe durchsuchen",
|
|
60
|
-
section: "Abschnitt",
|
|
61
90
|
help: "Hilfe",
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
91
|
+
youNeed: "Du brauchst",
|
|
92
|
+
accessView: "Ansehen",
|
|
93
|
+
accessEdit: "Bearbeiten",
|
|
94
|
+
accessManage: "Verwalten",
|
|
95
|
+
steps: ({ count }) => i18n.plural(count, "de", { one: "1 Schritt", other: `${count} Schritte` }),
|
|
96
|
+
articleFacts: "Zu diesem Artikel",
|
|
97
|
+
next: "Als Nächstes",
|
|
98
|
+
related: "Passt dazu",
|
|
67
99
|
back: "Zurück",
|
|
68
|
-
backToHelp: "Zurück zur Hilfe",
|
|
69
100
|
backToTopics: "Zurück zu den Hilfethemen",
|
|
70
101
|
actions: "Hilfeaktionen",
|
|
71
102
|
copied: "Kopiert",
|
|
72
103
|
helpCopied: "Hilfe als Markdown kopiert",
|
|
73
|
-
copyAllHelp: "Gesamte Hilfe als Markdown kopieren",
|
|
74
104
|
articleCopied: "Artikel als Markdown kopiert",
|
|
75
105
|
copyArticle: "Artikel als Markdown kopieren",
|
|
76
|
-
openWindow: "Hilfe in einem neuen Browserfenster öffnen",
|
|
77
|
-
openFullPage: "Hilfe als ganze Seite öffnen",
|
|
78
106
|
closeHelp: "Hilfe schließen",
|
|
79
107
|
close: "Schließen",
|
|
80
|
-
heading: "Wobei brauchst du Hilfe?",
|
|
81
|
-
headingDescription: "Finde eine Aufgabe, ein Konzept oder ein Tastaturkürzel für die aktuelle App.",
|
|
82
108
|
preparing: "Wird vorbereitet…",
|
|
83
|
-
copyAllShort: "Alles als Markdown kopieren",
|
|
84
109
|
copyAllFailed: ({ message }) => `Die gesamte Hilfe konnte nicht kopiert werden: ${message}`,
|
|
85
110
|
searchPlaceholder: "Hilfe durchsuchen…",
|
|
86
|
-
startHere: "Hier starten",
|
|
87
|
-
allTopics: "Alle Themen",
|
|
88
111
|
results: ({ count }) => i18n.plural(count, "de", { one: "1 Ergebnis", other: `${count} Ergebnisse` }),
|
|
89
112
|
noResults: "Kein Hilfethema passt zu dieser Suche.",
|
|
90
113
|
copyMarkdown: "Markdown kopieren",
|
|
@@ -93,7 +116,6 @@ export const helpMessages = i18n.define({
|
|
|
93
116
|
tryAgain: "Erneut versuchen",
|
|
94
117
|
onThisPage: "Auf dieser Seite",
|
|
95
118
|
articleCopiedAnnouncement: "Artikel als Markdown kopiert.",
|
|
96
|
-
allCopiedAnnouncement: "Gesamte Hilfe als Markdown kopiert.",
|
|
97
119
|
searchFailed: "Die Hilfe konnte nicht durchsucht werden. Versuche es erneut.",
|
|
98
120
|
requestFailed: "Die Hilfe konnte nicht geladen werden. Versuche es erneut.",
|
|
99
121
|
invalidDocument: "Der Hilfeserver hat ein ungültiges Dokument zurückgegeben",
|
package/src/ssr/help.ts
CHANGED
|
@@ -7,8 +7,10 @@ import { resolveCurrentApp } from "./app-appearance";
|
|
|
7
7
|
import { getRuntimeContext } from "./runtime";
|
|
8
8
|
|
|
9
9
|
const manifests = new WeakMap<object, HelpManifestResult | null>();
|
|
10
|
+
const failedContexts = new WeakSet<object>();
|
|
10
11
|
const log = logger("help:ssr");
|
|
11
12
|
export const getLayoutHelp = (c: object): HelpManifestResult | null => manifests.get(c) ?? null;
|
|
13
|
+
export const layoutHelpFailed = (c: object): boolean => failedContexts.has(c);
|
|
12
14
|
|
|
13
15
|
/** Help metadata for the current or an explicit app; `null` without a user or when the user may not see the app. */
|
|
14
16
|
export const preloadLayoutHelp = async (c: Context, appId?: string): Promise<HelpManifestResult | null> => {
|
|
@@ -22,6 +24,7 @@ export const preloadLayoutHelp = async (c: Context, appId?: string): Promise<Hel
|
|
|
22
24
|
return manifest;
|
|
23
25
|
} catch (error) {
|
|
24
26
|
// An unavailable Help store must not prevent an unrelated application page from rendering.
|
|
27
|
+
failedContexts.add(c);
|
|
25
28
|
log.warn("Help metadata unavailable", { appId: id, error: error instanceof Error ? error.message : String(error) });
|
|
26
29
|
manifests.set(c, null);
|
|
27
30
|
return null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** What the article head, the table of contents, and the step count read from the rendered Help HTML. */
|
|
2
|
+
export type HelpArticleParts = {
|
|
3
|
+
/** The first paragraph: the article's short answer. */
|
|
4
|
+
lead?: string;
|
|
5
|
+
body: string;
|
|
6
|
+
sections: { id: string; label: string }[];
|
|
7
|
+
steps: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** The text a screen reader reads: key names instead of key caps, no icons or decorative separators. */
|
|
11
|
+
const spokenText = (element: Element): string => {
|
|
12
|
+
const copy = element.cloneNode(true) as Element;
|
|
13
|
+
for (const hidden of copy.querySelectorAll('[aria-hidden="true"]')) hidden.remove();
|
|
14
|
+
return copy.textContent?.replace(/\s+/g, " ").trim() ?? "";
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Splits trusted Help HTML before it renders, so the head shows the short answer, the step count, and the
|
|
19
|
+
* section menu in the same frame as the body and nothing moves afterwards.
|
|
20
|
+
*/
|
|
21
|
+
export const helpArticleParts = (html: string): HelpArticleParts => {
|
|
22
|
+
// A template parses inert markup: nothing in it loads or runs.
|
|
23
|
+
const template = document.createElement("template");
|
|
24
|
+
template.innerHTML = html;
|
|
25
|
+
const body = template.content;
|
|
26
|
+
const first = body.firstElementChild;
|
|
27
|
+
// A bold-only paragraph is an eyebrow (`help-eyebrow`), not the short answer.
|
|
28
|
+
const lead = first?.tagName === "P" && !first.className ? first.outerHTML : undefined;
|
|
29
|
+
if (lead) first?.remove();
|
|
30
|
+
return {
|
|
31
|
+
lead,
|
|
32
|
+
body: template.innerHTML,
|
|
33
|
+
// A heading inside collapsed details is not a section of the page.
|
|
34
|
+
sections: Array.from(body.querySelectorAll("h2[id]"))
|
|
35
|
+
.filter((heading) => !heading.closest("details"))
|
|
36
|
+
.map((heading) => ({ id: heading.id, label: spokenText(heading) }))
|
|
37
|
+
.filter((section) => section.label),
|
|
38
|
+
// Steps inside a collapsed `details` block are an aside, not the task.
|
|
39
|
+
steps: Array.from(body.querySelectorAll(".help-steps > ol > li")).filter((step) => !step.closest("details")).length,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
import type { HelpDocumentManifest } from "@k2b/cloud/shared";
|
|
2
|
-
|
|
3
1
|
type HelpArticleSession = { articleScrollTop: number };
|
|
4
2
|
type HelpArticleScroller = { scrollTop: number };
|
|
5
3
|
type HelpArticleHeading = { focus: (options?: FocusOptions) => void };
|
|
6
4
|
|
|
7
|
-
export const adjacentHelpDocuments = (documents: readonly HelpDocumentManifest[], activeId: string | null) => {
|
|
8
|
-
const index = documents.findIndex((document) => document.id === activeId);
|
|
9
|
-
if (index < 0) return { previous: null, next: null };
|
|
10
|
-
|
|
11
|
-
return {
|
|
12
|
-
previous: documents[index - 1] ?? null,
|
|
13
|
-
next: documents[index + 1] ?? null,
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
|
|
17
5
|
export const resetHelpArticleScroll = (session: HelpArticleSession, scroller?: HelpArticleScroller) => {
|
|
18
6
|
session.articleScrollTop = 0;
|
|
19
7
|
if (scroller) scroller.scrollTop = 0;
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import type { HelpDocumentManifest } from "@k2b/cloud/shared";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
type HelpTopicBase = {
|
|
5
|
-
id: string;
|
|
6
|
-
title: string;
|
|
7
|
-
icon?: string;
|
|
8
|
-
description?: string;
|
|
9
|
-
order?: number;
|
|
10
|
-
};
|
|
2
|
+
import { onCleanup, onMount } from "solid-js";
|
|
11
3
|
|
|
12
4
|
export type LayoutHelpDocumentsProps = {
|
|
13
5
|
documents: readonly HelpDocumentManifest[];
|
|
14
6
|
/** Canonical standalone Help route owned by the current app. */
|
|
15
7
|
pageBase: string;
|
|
16
8
|
};
|
|
17
|
-
|
|
9
|
+
/** One article of the current app's Help, as the page registered it. */
|
|
10
|
+
export type HelpTopic = HelpDocumentManifest;
|
|
18
11
|
|
|
19
12
|
export const HELP_TOPICS_EVENT = "cloud:layout-help-topics";
|
|
20
13
|
|
|
@@ -50,7 +43,7 @@ export function LayoutHelpDocuments(props: LayoutHelpDocumentsProps) {
|
|
|
50
43
|
// Cleanup runs while this unmounts, when a parent `<Show>` may already have dropped the props' source.
|
|
51
44
|
const pageBase = props.pageBase;
|
|
52
45
|
window.__cloudLayoutHelpPageBase = pageBase;
|
|
53
|
-
const disposers = props.documents.map((document) => registerTopic(
|
|
46
|
+
const disposers = props.documents.map((document) => registerTopic(document));
|
|
54
47
|
onCleanup(() => {
|
|
55
48
|
disposers.forEach((dispose) => dispose());
|
|
56
49
|
if (window.__cloudLayoutHelpPageBase === pageBase) delete window.__cloudLayoutHelpPageBase;
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
type SearchTopic = { id: string; title: string; description?: string
|
|
1
|
+
type SearchTopic = { id: string; title: string; description?: string };
|
|
2
2
|
|
|
3
|
-
/** Use server ranking for
|
|
3
|
+
/** Use server ranking for articles; metadata matches remain usable during loading or outages. */
|
|
4
4
|
export const searchHelpTopics = <T extends SearchTopic>(topics: readonly T[], query: string, remoteIds: readonly string[] | null): T[] => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
return
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}),
|
|
13
|
-
...local.filter((topic) => topic.kind === "content"),
|
|
14
|
-
];
|
|
5
|
+
if (remoteIds === null)
|
|
6
|
+
return topics.filter((topic) => [topic.title, topic.description].some((part) => part?.toLocaleLowerCase().includes(query)));
|
|
7
|
+
const byId = new Map(topics.map((topic) => [topic.id, topic]));
|
|
8
|
+
return remoteIds.flatMap((id) => {
|
|
9
|
+
const topic = byId.get(id);
|
|
10
|
+
return topic ? [topic] : [];
|
|
11
|
+
});
|
|
15
12
|
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { HelpKind } from "../shared/help";
|
|
2
|
+
import { SHELL_SHORTCUT_ORDER } from "./shell-shortcuts";
|
|
3
|
+
|
|
4
|
+
/** The concept asks for at most eight tasks as tiles; more task articles follow as a list. */
|
|
5
|
+
export const HELP_START_TILE_LIMIT = 8;
|
|
6
|
+
/** "For this view" shows two or three articles the page names. */
|
|
7
|
+
export const HELP_VIEW_LIMIT = 3;
|
|
8
|
+
export const HELP_START_GROUP_KINDS = ["start", "reference", "troubleshoot"] as const;
|
|
9
|
+
export type HelpStartGroupKind = (typeof HELP_START_GROUP_KINDS)[number];
|
|
10
|
+
export type HelpStartTopic = { id: string; title: string; order?: number; kind?: HelpKind };
|
|
11
|
+
export type HelpStart<T> = {
|
|
12
|
+
/** Declared ids in declared order: only ids of known topics, without duplicates, at most HELP_VIEW_LIMIT. */
|
|
13
|
+
view: T[];
|
|
14
|
+
/** Articles of kind task (missing kind = task), by order (missing = 100) then title, at most HELP_START_TILE_LIMIT. */
|
|
15
|
+
tiles: T[];
|
|
16
|
+
/** The remaining task articles in the same order, so no task becomes unreachable. */
|
|
17
|
+
moreTasks: T[];
|
|
18
|
+
/** Fixed order start, reference, troubleshoot; a kind without articles is absent. Each list sorted like tiles. */
|
|
19
|
+
groups: { kind: HelpStartGroupKind; topics: T[] }[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const helpStart = <T extends HelpStartTopic>(topics: readonly T[], viewIds: readonly string[] = []): HelpStart<T> => {
|
|
23
|
+
const byId = new Map(topics.map((topic) => [topic.id, topic]));
|
|
24
|
+
const view = [...new Set(viewIds)]
|
|
25
|
+
.flatMap((id) => {
|
|
26
|
+
const topic = byId.get(id);
|
|
27
|
+
return topic ? [topic] : [];
|
|
28
|
+
})
|
|
29
|
+
.slice(0, HELP_VIEW_LIMIT);
|
|
30
|
+
const sorted = [...topics].sort((a, b) => (a.order ?? 100) - (b.order ?? 100) || a.title.localeCompare(b.title));
|
|
31
|
+
const tasks = sorted.filter((topic) => (topic.kind ?? "task") === "task");
|
|
32
|
+
return {
|
|
33
|
+
view,
|
|
34
|
+
tiles: tasks.slice(0, HELP_START_TILE_LIMIT),
|
|
35
|
+
moreTasks: tasks.slice(HELP_START_TILE_LIMIT),
|
|
36
|
+
groups: HELP_START_GROUP_KINDS.flatMap((kind) => {
|
|
37
|
+
const topics = sorted.filter((topic) => topic.kind === kind);
|
|
38
|
+
return topics.length ? [{ kind, topics }] : [];
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type HelpShortcutScope = "everywhere" | "app" | "selection";
|
|
44
|
+
export type HelpShortcutCommand = { id: string; title: string; description?: string; shortcut?: string; scope?: "selection" | "page" };
|
|
45
|
+
/**
|
|
46
|
+
* Group commands with a shortcut: scope "selection" -> "selection"; id starting with "cloud." -> "everywhere"; the rest -> "app".
|
|
47
|
+
* Group order everywhere, app, selection; empty groups are absent. Inside "everywhere", the ids of SHELL_SHORTCUT_ORDER come first in that
|
|
48
|
+
* order (so rows rendered on the server keep their place when the browser adds more), then the rest in input order; other groups keep input order.
|
|
49
|
+
* Commands without a shortcut are dropped.
|
|
50
|
+
*/
|
|
51
|
+
export const helpShortcutGroups = <T extends HelpShortcutCommand>(
|
|
52
|
+
commands: readonly T[],
|
|
53
|
+
): { scope: HelpShortcutScope; shortcuts: (T & { shortcut: string })[] }[] => {
|
|
54
|
+
const groups: Record<HelpShortcutScope, (T & { shortcut: string })[]> = { everywhere: [], app: [], selection: [] };
|
|
55
|
+
for (const command of commands) {
|
|
56
|
+
if (!command.shortcut) continue;
|
|
57
|
+
const scope = command.scope === "selection" ? "selection" : command.id.startsWith("cloud.") ? "everywhere" : "app";
|
|
58
|
+
groups[scope].push({ ...command, shortcut: command.shortcut });
|
|
59
|
+
}
|
|
60
|
+
const rank = (id: string) => {
|
|
61
|
+
const index = SHELL_SHORTCUT_ORDER.findIndex((shellId) => shellId === id);
|
|
62
|
+
return index < 0 ? SHELL_SHORTCUT_ORDER.length : index;
|
|
63
|
+
};
|
|
64
|
+
groups.everywhere.sort((a, b) => rank(a.id) - rank(b.id));
|
|
65
|
+
return (["everywhere", "app", "selection"] as const).flatMap((scope) =>
|
|
66
|
+
groups[scope].length ? [{ scope, shortcuts: groups[scope] }] : [],
|
|
67
|
+
);
|
|
68
|
+
};
|
|
@@ -4,6 +4,8 @@ export const platformMessages = i18n.define({
|
|
|
4
4
|
baseLocale: "en",
|
|
5
5
|
messages: {
|
|
6
6
|
en: {
|
|
7
|
+
printFallbackTitle: "This view does not print well.",
|
|
8
|
+
printFallbackHint: "Open the item you want to print, or use the app's export.",
|
|
7
9
|
admin: "Admin",
|
|
8
10
|
platformAdministration: "Platform administration.",
|
|
9
11
|
start: "Start",
|
|
@@ -76,6 +78,8 @@ export const platformMessages = i18n.define({
|
|
|
76
78
|
clearSearch: "Clear search",
|
|
77
79
|
},
|
|
78
80
|
de: {
|
|
81
|
+
printFallbackTitle: "Diese Ansicht lässt sich nicht gut drucken.",
|
|
82
|
+
printFallbackHint: "Öffne den Eintrag, den du drucken möchtest, oder nutze den Export der App.",
|
|
79
83
|
admin: "Administration",
|
|
80
84
|
platformAdministration: "Plattform verwalten.",
|
|
81
85
|
start: "Start",
|
package/src/ssr/shell-data.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { KeyboardPlatform } from "@k2b/ui";
|
|
1
2
|
import { createContext, type JSX, useContext } from "solid-js";
|
|
2
3
|
import type { AppSearchLink } from "../contracts/app";
|
|
3
4
|
import type { RailSnapshot } from "../contracts/rail-preferences";
|
|
@@ -20,6 +21,12 @@ export type ShellData = {
|
|
|
20
21
|
searchLinks: Record<string, readonly AppSearchLink[]>;
|
|
21
22
|
/** The current app, for links inside its Help. */
|
|
22
23
|
appId?: string;
|
|
24
|
+
/** The reader's keyboard platform, resolved from the request User-Agent for server-rendered key caps. */
|
|
25
|
+
platform?: KeyboardPlatform;
|
|
26
|
+
/** "For this view" article ids, already filtered for the reader. */
|
|
27
|
+
help?: string[];
|
|
28
|
+
/** The current app's Help metadata could not be read for this request; Help offers a reload. */
|
|
29
|
+
helpFailed?: true;
|
|
23
30
|
};
|
|
24
31
|
|
|
25
32
|
const ShellDataContext = createContext<() => ShellData>();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { platformMessages } from "./platform-messages";
|
|
2
|
+
|
|
3
|
+
export const SHELL_SHORTCUT_ORDER = ["cloud.search", "cloud.help"] as const;
|
|
4
|
+
/** The shell's always-present shortcuts, without their actions; ShellRuntime adds the actions, Help renders them before the browser reports commands. */
|
|
5
|
+
export const shellShortcuts = (t: ReturnType<typeof platformMessages.resolve>["t"]) => [
|
|
6
|
+
{ id: "cloud.search", title: t.openGlobalSearch, description: t.globalSearchDescription, icon: "ti ti-search", shortcut: "mod+k" },
|
|
7
|
+
{ id: "cloud.help", title: t.openShortcutHelp, description: t.shortcutHelpDescription, icon: "ti ti-help-circle", shortcut: "shift+/" },
|
|
8
|
+
];
|