@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,159 @@
|
|
|
1
|
+
import { Dropdown, LocaleProvider, MarkdownView, TableOfContents, useLocale } from "@k2b/ui";
|
|
2
|
+
import { type Accessor, createMemo, createSignal, onMount } from "solid-js";
|
|
3
|
+
import { render } from "solid-js/web";
|
|
4
|
+
import type { HelpDocumentPayload } from "../shared/help";
|
|
5
|
+
import { helpMessages } from "./help-messages";
|
|
6
|
+
import { helpArticleParts } from "./layout-help-article";
|
|
7
|
+
import type { HelpPanelState } from "./layout-help-panel-state";
|
|
8
|
+
import type { HelpTopic } from "./layout-help-registry";
|
|
9
|
+
import { HelpArticleView } from "./layout-help-views";
|
|
10
|
+
|
|
11
|
+
function Sections(props: {
|
|
12
|
+
sections: { id: string; label: string }[];
|
|
13
|
+
onSection: (id: string) => void;
|
|
14
|
+
active: Accessor<string | null>;
|
|
15
|
+
activation?: { key: string; focused: boolean };
|
|
16
|
+
}) {
|
|
17
|
+
const locale = useLocale();
|
|
18
|
+
const t = () => helpMessages.resolve([locale()]).t;
|
|
19
|
+
let trigger: HTMLButtonElement | undefined;
|
|
20
|
+
onMount(() => {
|
|
21
|
+
if (!props.activation) return;
|
|
22
|
+
if (props.activation.focused) trigger?.focus({ preventScroll: true });
|
|
23
|
+
trigger?.dispatchEvent(new KeyboardEvent("keydown", { key: props.activation.key, bubbles: true, cancelable: true }));
|
|
24
|
+
});
|
|
25
|
+
return (
|
|
26
|
+
<Dropdown.Root
|
|
27
|
+
label={t().onThisPage}
|
|
28
|
+
position="bottom-left"
|
|
29
|
+
items={props.sections.map((section) => ({
|
|
30
|
+
label: section.label,
|
|
31
|
+
choice: "radio" as const,
|
|
32
|
+
checked: () => props.active() === section.id,
|
|
33
|
+
action: () => {
|
|
34
|
+
props.onSection(section.id);
|
|
35
|
+
},
|
|
36
|
+
}))}
|
|
37
|
+
>
|
|
38
|
+
<Dropdown.Trigger
|
|
39
|
+
ref={(element) => {
|
|
40
|
+
trigger = element;
|
|
41
|
+
}}
|
|
42
|
+
label={t().onThisPage}
|
|
43
|
+
size="sm"
|
|
44
|
+
tooltip={false}
|
|
45
|
+
variant="secondary"
|
|
46
|
+
>
|
|
47
|
+
<i class="ti ti-list" aria-hidden="true" />
|
|
48
|
+
<span>{t().onThisPage}</span>
|
|
49
|
+
<i class="ti ti-chevron-down" aria-hidden="true" />
|
|
50
|
+
</Dropdown.Trigger>
|
|
51
|
+
</Dropdown.Root>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
export function HelpPanelReader(props: {
|
|
55
|
+
state: HelpPanelState;
|
|
56
|
+
topic: HelpTopic;
|
|
57
|
+
document: HelpDocumentPayload;
|
|
58
|
+
onSection: (id: string) => void;
|
|
59
|
+
}) {
|
|
60
|
+
const locale = useLocale();
|
|
61
|
+
const t = () => helpMessages.resolve([locale()]).t;
|
|
62
|
+
const parts = createMemo(() => helpArticleParts(props.document.html));
|
|
63
|
+
const [active, setActive] = createSignal<string | null>(parts().sections[0]?.id ?? null);
|
|
64
|
+
let content: HTMLDivElement | undefined;
|
|
65
|
+
return (
|
|
66
|
+
<HelpArticleView
|
|
67
|
+
surface="panel"
|
|
68
|
+
topic={props.topic}
|
|
69
|
+
appId={props.state.data.appId}
|
|
70
|
+
parts={parts()}
|
|
71
|
+
document={props.document}
|
|
72
|
+
Markdown={MarkdownView}
|
|
73
|
+
showTools={parts().sections.length > 0}
|
|
74
|
+
sectionMenu={
|
|
75
|
+
parts().sections.length ? (
|
|
76
|
+
<Sections
|
|
77
|
+
sections={parts().sections}
|
|
78
|
+
active={active}
|
|
79
|
+
onSection={(id) => {
|
|
80
|
+
setActive(id);
|
|
81
|
+
props.onSection(id);
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
) : null
|
|
85
|
+
}
|
|
86
|
+
copyButton={null}
|
|
87
|
+
loading={null}
|
|
88
|
+
failure={null}
|
|
89
|
+
toc={
|
|
90
|
+
<TableOfContents
|
|
91
|
+
items={parts().sections}
|
|
92
|
+
title={t().onThisPage}
|
|
93
|
+
scrollRoot={() => props.state.scroller() ?? undefined}
|
|
94
|
+
contentRoot={() => content}
|
|
95
|
+
onActiveChange={(id) => setActive(id ?? null)}
|
|
96
|
+
/>
|
|
97
|
+
}
|
|
98
|
+
copyAction={null}
|
|
99
|
+
headingRef={() => {}}
|
|
100
|
+
contentRef={(element) => {
|
|
101
|
+
content = element;
|
|
102
|
+
}}
|
|
103
|
+
onBack={props.state.back}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
export function mountPanelSections(
|
|
108
|
+
container: HTMLElement,
|
|
109
|
+
props: { locale: string; state: HelpPanelState; key: string; focused: boolean; onSection: (id: string) => void },
|
|
110
|
+
): () => void {
|
|
111
|
+
const article = container.closest("article");
|
|
112
|
+
const sections = Array.from(article?.querySelectorAll<HTMLAnchorElement>(".help-article-aside a[href^='#']") ?? []).map((link) => ({
|
|
113
|
+
id: decodeURIComponent(link.getAttribute("href")!.slice(1)),
|
|
114
|
+
label: link.textContent ?? "",
|
|
115
|
+
}));
|
|
116
|
+
const [active, setActive] = createSignal<string | null>(sections[0]?.id ?? null);
|
|
117
|
+
const aside = article?.querySelector<HTMLElement>(".help-article-aside-sticky");
|
|
118
|
+
let disposeToc = () => {};
|
|
119
|
+
// The aside is hidden at every panel width. Upgrade its scroll spy only when the section menu needs it.
|
|
120
|
+
if (aside) {
|
|
121
|
+
aside.textContent = "";
|
|
122
|
+
disposeToc = render(
|
|
123
|
+
() => (
|
|
124
|
+
<LocaleProvider locale={props.locale}>
|
|
125
|
+
<TableOfContents
|
|
126
|
+
items={sections}
|
|
127
|
+
title={helpMessages.resolve([props.locale]).t.onThisPage}
|
|
128
|
+
scrollRoot={() => props.state.scroller() ?? undefined}
|
|
129
|
+
contentRoot={() => article ?? undefined}
|
|
130
|
+
onActiveChange={(id) => setActive(id ?? null)}
|
|
131
|
+
/>
|
|
132
|
+
</LocaleProvider>
|
|
133
|
+
),
|
|
134
|
+
aside,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
container.textContent = "";
|
|
138
|
+
container.style.display = "contents";
|
|
139
|
+
const disposeMenu = render(
|
|
140
|
+
() => (
|
|
141
|
+
<LocaleProvider locale={props.locale}>
|
|
142
|
+
<Sections
|
|
143
|
+
sections={sections}
|
|
144
|
+
active={active}
|
|
145
|
+
onSection={(id) => {
|
|
146
|
+
setActive(id);
|
|
147
|
+
props.onSection(id);
|
|
148
|
+
}}
|
|
149
|
+
activation={{ key: props.key, focused: props.focused }}
|
|
150
|
+
/>
|
|
151
|
+
</LocaleProvider>
|
|
152
|
+
),
|
|
153
|
+
container,
|
|
154
|
+
);
|
|
155
|
+
return () => {
|
|
156
|
+
disposeMenu();
|
|
157
|
+
disposeToc();
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useLocale } from "@k2b/ui";
|
|
2
|
+
import { createEffect, createMemo, createSignal, onCleanup, Show } from "solid-js";
|
|
3
|
+
import { helpMessages } from "./help-messages";
|
|
4
|
+
import type { HelpPanelState } from "./layout-help-panel-state";
|
|
5
|
+
import { searchHelpTopics } from "./layout-help-search";
|
|
6
|
+
import { HelpResultsView } from "./layout-help-views";
|
|
7
|
+
|
|
8
|
+
export function HelpPanelSearchResults(props: { state: HelpPanelState }) {
|
|
9
|
+
const locale = useLocale();
|
|
10
|
+
const t = () => helpMessages.resolve([locale()]).t;
|
|
11
|
+
const query = createMemo(() => props.state.query().trim().toLocaleLowerCase());
|
|
12
|
+
const [ids, setIds] = createSignal<readonly string[] | null>(null);
|
|
13
|
+
const [searching, setSearching] = createSignal(false);
|
|
14
|
+
const [failed, setFailed] = createSignal(false);
|
|
15
|
+
const [attempt, setAttempt] = createSignal(0);
|
|
16
|
+
let version = 0;
|
|
17
|
+
createEffect(() => {
|
|
18
|
+
const value = query();
|
|
19
|
+
attempt();
|
|
20
|
+
const current = ++version;
|
|
21
|
+
setIds(null);
|
|
22
|
+
setSearching(false);
|
|
23
|
+
setFailed(false);
|
|
24
|
+
const urls = [...new Set(props.state.data.documents.map((topic) => topic.searchUrl))];
|
|
25
|
+
if (!value || !urls.length) return;
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
setSearching(true);
|
|
29
|
+
void Promise.all(
|
|
30
|
+
urls.map(async (url) => {
|
|
31
|
+
const response = await fetch(`${url}?q=${encodeURIComponent(value)}`, {
|
|
32
|
+
signal: controller.signal,
|
|
33
|
+
headers: { Accept: "application/json" },
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) throw new Error(t().searchFailed);
|
|
36
|
+
const payload: unknown = await response.json();
|
|
37
|
+
return payload && typeof payload === "object" && "ids" in payload && Array.isArray(payload.ids)
|
|
38
|
+
? payload.ids.filter((id): id is string => typeof id === "string")
|
|
39
|
+
: [];
|
|
40
|
+
}),
|
|
41
|
+
)
|
|
42
|
+
.then((groups) => {
|
|
43
|
+
if (current === version) setIds([...new Set(groups.flat())]);
|
|
44
|
+
})
|
|
45
|
+
.catch((error) => {
|
|
46
|
+
if (!(error instanceof DOMException && error.name === "AbortError") && current === version) setFailed(true);
|
|
47
|
+
})
|
|
48
|
+
.finally(() => {
|
|
49
|
+
if (current === version) setSearching(false);
|
|
50
|
+
});
|
|
51
|
+
}, 120);
|
|
52
|
+
onCleanup(() => {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
controller.abort();
|
|
55
|
+
if (current === version) version++;
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
return (
|
|
59
|
+
<Show when={query()}>
|
|
60
|
+
<HelpResultsView
|
|
61
|
+
results={searchHelpTopics(props.state.data.documents, query(), ids())}
|
|
62
|
+
searching={searching()}
|
|
63
|
+
failed={failed()}
|
|
64
|
+
onRetry={() => setAttempt((value) => value + 1)}
|
|
65
|
+
onOpen={() => {}}
|
|
66
|
+
/>
|
|
67
|
+
</Show>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getContext } from "../browser/panel";
|
|
2
|
+
import { panelShortcutsReceived, readPanelShortcuts } from "../browser/panel-shortcuts";
|
|
3
|
+
import type { HelpShortcutCommand } from "./layout-help-start";
|
|
4
|
+
|
|
5
|
+
/** An empty host list is authoritative too, including when a reader remounts. */
|
|
6
|
+
export const readHelpPanelShortcuts = (seed: HelpShortcutCommand[]): HelpShortcutCommand[] =>
|
|
7
|
+
panelShortcutsReceived() && getContext() !== null ? readPanelShortcuts() : seed;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { batch, createSignal } from "solid-js";
|
|
2
|
+
import type { HelpDocumentManifest, HelpDocumentPayload } from "../shared/help";
|
|
3
|
+
import { focusHelpArticleHeading } from "./layout-help-navigation";
|
|
4
|
+
|
|
5
|
+
export type HelpPanelData = {
|
|
6
|
+
documents: HelpDocumentManifest[];
|
|
7
|
+
appId?: string;
|
|
8
|
+
pageBase?: string;
|
|
9
|
+
article?: Omit<HelpDocumentPayload, "html">;
|
|
10
|
+
};
|
|
11
|
+
const states = new WeakMap<Element, ReturnType<typeof createPanelState>>();
|
|
12
|
+
export const panelRoot = (element: Element): HTMLElement => {
|
|
13
|
+
const root = element.closest<HTMLElement>('.help-shell[data-surface="panel"]');
|
|
14
|
+
if (!root) throw new Error("Missing Help panel");
|
|
15
|
+
return root;
|
|
16
|
+
};
|
|
17
|
+
const createPanelState = (root: HTMLElement) => {
|
|
18
|
+
// This is our own server's inert JSON, shared once instead of repeating it in island props.
|
|
19
|
+
const data: HelpPanelData = JSON.parse(root.querySelector("[data-help-panel-data]")?.textContent ?? "{}");
|
|
20
|
+
const [view, setView] = createSignal<"hub" | "article">(root.dataset.helpInitialTopic ? "article" : "hub");
|
|
21
|
+
const [activeId, setActiveId] = createSignal<string | null>(data.article?.id ?? root.dataset.helpInitialTopic ?? null);
|
|
22
|
+
const [query, setQuery] = createSignal("");
|
|
23
|
+
const [searchReturnId, setSearchReturnId] = createSignal<string | null>(null);
|
|
24
|
+
const [navigation, setNavigation] = createSignal({ hash: "", version: 0 });
|
|
25
|
+
const [announcement, setAnnouncement] = createSignal("");
|
|
26
|
+
const cache = new Map<string, HelpDocumentPayload>();
|
|
27
|
+
const [cacheVersion, setCacheVersion] = createSignal(0);
|
|
28
|
+
const cacheKey = (topic: HelpDocumentManifest) => `${topic.locale ?? "en"}:${topic.url}`;
|
|
29
|
+
if (data.article) {
|
|
30
|
+
const topic = data.documents.find((topic) => topic.id === data.article?.id);
|
|
31
|
+
if (topic) cache.set(cacheKey(topic), { ...data.article, html: "" });
|
|
32
|
+
}
|
|
33
|
+
const cachedDocument = (topic: HelpDocumentManifest) => {
|
|
34
|
+
cacheVersion();
|
|
35
|
+
return cache.get(cacheKey(topic));
|
|
36
|
+
};
|
|
37
|
+
const storeDocument = (topic: HelpDocumentManifest, payload: HelpDocumentPayload) => {
|
|
38
|
+
cache.set(cacheKey(topic), payload);
|
|
39
|
+
setCacheVersion((value) => value + 1);
|
|
40
|
+
};
|
|
41
|
+
const results = () => root.querySelector<HTMLElement>("[data-help-results]");
|
|
42
|
+
const scroller = () => root.querySelector<HTMLElement>("[data-scroll-fade-mode]");
|
|
43
|
+
const sync = () => {
|
|
44
|
+
const hub = view() === "hub";
|
|
45
|
+
const start = results();
|
|
46
|
+
if (start) start.hidden = !hub;
|
|
47
|
+
const server = root.querySelector<HTMLElement>("[data-help-server-article]");
|
|
48
|
+
if (server) server.hidden = hub || activeId() !== data.article?.id;
|
|
49
|
+
};
|
|
50
|
+
const back = () => {
|
|
51
|
+
setSearchReturnId(null);
|
|
52
|
+
setView("hub");
|
|
53
|
+
sync();
|
|
54
|
+
};
|
|
55
|
+
/** Where the article stood when a search started from it; clearing the search returns there. */
|
|
56
|
+
let articleScrollTop = 0;
|
|
57
|
+
const changeQuery = (value: string) => {
|
|
58
|
+
const searching = !!value.trim();
|
|
59
|
+
let returned = false;
|
|
60
|
+
batch(() => {
|
|
61
|
+
if (searching && view() === "article") {
|
|
62
|
+
articleScrollTop = scroller()?.scrollTop ?? 0;
|
|
63
|
+
setSearchReturnId(activeId());
|
|
64
|
+
const region = results();
|
|
65
|
+
if (region) region.style.minHeight = "";
|
|
66
|
+
setView("hub");
|
|
67
|
+
}
|
|
68
|
+
setQuery(value);
|
|
69
|
+
if (!searching) {
|
|
70
|
+
const region = results();
|
|
71
|
+
if (region) region.style.minHeight = "";
|
|
72
|
+
const id = searchReturnId();
|
|
73
|
+
setSearchReturnId(null);
|
|
74
|
+
if (id && data.documents.some((topic) => topic.id === id)) {
|
|
75
|
+
setActiveId(id);
|
|
76
|
+
setView("article");
|
|
77
|
+
returned = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
sync();
|
|
82
|
+
// The article stayed in the document while results showed; give it back its place once it has its height again.
|
|
83
|
+
if (returned) {
|
|
84
|
+
const top = articleScrollTop;
|
|
85
|
+
requestAnimationFrame(() => {
|
|
86
|
+
const scroll = scroller();
|
|
87
|
+
if (scroll) scroll.scrollTop = top;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const open = (id: string, hash = "") => {
|
|
92
|
+
if (!data.documents.some((topic) => topic.id === id)) return;
|
|
93
|
+
try {
|
|
94
|
+
localStorage.setItem("cloud.layoutHelp.activeTopic", id);
|
|
95
|
+
} catch {
|
|
96
|
+
/* Storage is optional. */
|
|
97
|
+
}
|
|
98
|
+
batch(() => {
|
|
99
|
+
setSearchReturnId(null);
|
|
100
|
+
setActiveId(id);
|
|
101
|
+
setView("article");
|
|
102
|
+
setNavigation((value) => ({ hash, version: value.version + 1 }));
|
|
103
|
+
});
|
|
104
|
+
const scroll = scroller();
|
|
105
|
+
if (scroll) scroll.scrollTop = 0;
|
|
106
|
+
sync();
|
|
107
|
+
};
|
|
108
|
+
const focusHeading = () =>
|
|
109
|
+
focusHelpArticleHeading(root.querySelector<HTMLHeadingElement>("article:not([hidden]) .help-article-title") ?? undefined);
|
|
110
|
+
return {
|
|
111
|
+
root,
|
|
112
|
+
data,
|
|
113
|
+
view,
|
|
114
|
+
activeId,
|
|
115
|
+
query,
|
|
116
|
+
changeQuery,
|
|
117
|
+
back,
|
|
118
|
+
open,
|
|
119
|
+
navigation,
|
|
120
|
+
cache,
|
|
121
|
+
cacheKey,
|
|
122
|
+
cachedDocument,
|
|
123
|
+
storeDocument,
|
|
124
|
+
sync,
|
|
125
|
+
results,
|
|
126
|
+
scroller,
|
|
127
|
+
focusHeading,
|
|
128
|
+
announcement,
|
|
129
|
+
setAnnouncement,
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
export type HelpPanelState = ReturnType<typeof createPanelState>;
|
|
133
|
+
export function helpPanelState(element: Element): HelpPanelState {
|
|
134
|
+
const root = panelRoot(element);
|
|
135
|
+
let state = states.get(root);
|
|
136
|
+
if (!state) {
|
|
137
|
+
state = createPanelState(root);
|
|
138
|
+
states.set(root, state);
|
|
139
|
+
}
|
|
140
|
+
return state;
|
|
141
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HelpDocumentManifest } from "@k2b/cloud/shared";
|
|
2
|
+
import type { KeyboardPlatform } from "@k2b/ui";
|
|
3
|
+
import type { LayoutHelpArticle } from "./help";
|
|
4
|
+
|
|
5
|
+
export type LayoutHelpPanelProps = {
|
|
6
|
+
documents: readonly HelpDocumentManifest[];
|
|
7
|
+
/** Absent when the context selected Help the reader may not see. */
|
|
8
|
+
app?: { id: string; name: string; icon?: string };
|
|
9
|
+
/** appAppearanceStyle() of the app, from the server. */
|
|
10
|
+
appearance?: string;
|
|
11
|
+
/** Server-filtered For this view ids. */
|
|
12
|
+
view?: readonly string[];
|
|
13
|
+
initialTopic?: string;
|
|
14
|
+
/** Authorized article and server-parsed parts for an identical first frame. */
|
|
15
|
+
initialArticle?: LayoutHelpArticle;
|
|
16
|
+
pageBase?: string;
|
|
17
|
+
platform: KeyboardPlatform;
|
|
18
|
+
state?: "failed" | "unavailable";
|
|
19
|
+
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { HELP_VIEW_LIMIT, type HelpKind } from "../shared/help";
|
|
2
2
|
import { SHELL_SHORTCUT_ORDER } from "./shell-shortcuts";
|
|
3
3
|
|
|
4
4
|
/** The concept asks for at most eight tasks as tiles; more task articles follow as a list. */
|
|
5
5
|
export const HELP_START_TILE_LIMIT = 8;
|
|
6
6
|
/** "For this view" shows two or three articles the page names. */
|
|
7
|
-
export
|
|
7
|
+
export { HELP_VIEW_LIMIT } from "../shared/help";
|
|
8
8
|
export const HELP_START_GROUP_KINDS = ["start", "reference", "troubleshoot"] as const;
|
|
9
9
|
export type HelpStartGroupKind = (typeof HELP_START_GROUP_KINDS)[number];
|
|
10
10
|
export type HelpStartTopic = { id: string; title: string; order?: number; kind?: HelpKind };
|
|
@@ -41,19 +41,42 @@ export const helpStart = <T extends HelpStartTopic>(topics: readonly T[], viewId
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
export type HelpShortcutScope = "everywhere" | "app" | "selection";
|
|
44
|
+
// Three groups, each as scannable as HELP_START_TILE_LIMIT (8) task tiles: 3 x 8 rows.
|
|
45
|
+
// Today's largest real page is well below this (shell 3 + Spaces about 8).
|
|
46
|
+
export const HELP_START_SHORTCUT_LIMIT = 24;
|
|
47
|
+
// Qualified command ids share CAPABILITY_QUALIFIED_ID_MAX_LENGTH.
|
|
48
|
+
export const HELP_SHORTCUT_ID_MAX_LENGTH = 180;
|
|
49
|
+
// One row label: the platform's CapabilityLink title / human-readable label bound.
|
|
50
|
+
export const HELP_SHORTCUT_TITLE_MAX_LENGTH = 120;
|
|
51
|
+
// Row tooltip: the platform's plain-text Action review message bound.
|
|
52
|
+
export const HELP_SHORTCUT_DESCRIPTION_MAX_LENGTH = 1000;
|
|
53
|
+
// command-shortcuts grammar: five distinct modifier tokens "ctrl+alt+shift+meta+mod+" (24 chars)
|
|
54
|
+
// plus the longest standard KeyboardEvent.key name "TVAudioDescriptionMixDown" (25 chars).
|
|
55
|
+
export const HELP_SHORTCUT_KEYS_MAX_LENGTH = 49;
|
|
56
|
+
export type HelpShortcut = { id: string; title: string; description?: string; shortcut: string; scope?: "selection" | "page" };
|
|
44
57
|
export type HelpShortcutCommand = { id: string; title: string; description?: string; shortcut?: string; scope?: "selection" | "page" };
|
|
45
58
|
/**
|
|
46
59
|
* Group commands with a shortcut: scope "selection" -> "selection"; id starting with "cloud." -> "everywhere"; the rest -> "app".
|
|
47
60
|
* Group order everywhere, app, selection; empty groups are absent. Inside "everywhere", the ids of SHELL_SHORTCUT_ORDER come first in that
|
|
48
61
|
* 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.
|
|
62
|
+
* Commands without a shortcut, with empty ids/titles or exceeding the string bounds are dropped.
|
|
63
|
+
* Keep at most HELP_START_SHORTCUT_LIMIT rows across groups in group order.
|
|
50
64
|
*/
|
|
51
65
|
export const helpShortcutGroups = <T extends HelpShortcutCommand>(
|
|
52
66
|
commands: readonly T[],
|
|
53
67
|
): { scope: HelpShortcutScope; shortcuts: (T & { shortcut: string })[] }[] => {
|
|
54
68
|
const groups: Record<HelpShortcutScope, (T & { shortcut: string })[]> = { everywhere: [], app: [], selection: [] };
|
|
55
69
|
for (const command of commands) {
|
|
56
|
-
if (
|
|
70
|
+
if (
|
|
71
|
+
!command.shortcut ||
|
|
72
|
+
!command.id ||
|
|
73
|
+
!command.title ||
|
|
74
|
+
command.id.length > HELP_SHORTCUT_ID_MAX_LENGTH ||
|
|
75
|
+
command.title.length > HELP_SHORTCUT_TITLE_MAX_LENGTH ||
|
|
76
|
+
(command.description?.length ?? 0) > HELP_SHORTCUT_DESCRIPTION_MAX_LENGTH ||
|
|
77
|
+
command.shortcut.length > HELP_SHORTCUT_KEYS_MAX_LENGTH
|
|
78
|
+
)
|
|
79
|
+
continue;
|
|
57
80
|
const scope = command.scope === "selection" ? "selection" : command.id.startsWith("cloud.") ? "everywhere" : "app";
|
|
58
81
|
groups[scope].push({ ...command, shortcut: command.shortcut });
|
|
59
82
|
}
|
|
@@ -62,7 +85,10 @@ export const helpShortcutGroups = <T extends HelpShortcutCommand>(
|
|
|
62
85
|
return index < 0 ? SHELL_SHORTCUT_ORDER.length : index;
|
|
63
86
|
};
|
|
64
87
|
groups.everywhere.sort((a, b) => rank(a.id) - rank(b.id));
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
88
|
+
let remaining = HELP_START_SHORTCUT_LIMIT;
|
|
89
|
+
return (["everywhere", "app", "selection"] as const).flatMap((scope) => {
|
|
90
|
+
const shortcuts = groups[scope].slice(0, remaining);
|
|
91
|
+
remaining -= shortcuts.length;
|
|
92
|
+
return shortcuts.length ? [{ scope, shortcuts }] : [];
|
|
93
|
+
});
|
|
68
94
|
};
|