cortena-ui 1.4.2 → 1.6.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/CHANGELOG.md +107 -0
- package/LICENSE +7 -0
- package/README.md +235 -3
- package/dist/a2ui/views.js +2 -2
- package/dist/agent-chat/a2ui-block.d.ts +60 -0
- package/dist/agent-chat/a2ui-block.js +69 -0
- package/dist/agent-chat/a2ui-block.js.map +1 -0
- package/dist/agent-chat/agui-client.d.ts +40 -0
- package/dist/agent-chat/agui-client.js +251 -0
- package/dist/agent-chat/agui-client.js.map +1 -0
- package/dist/agent-chat/bridge.d.ts +109 -0
- package/dist/agent-chat/bridge.js +353 -0
- package/dist/agent-chat/bridge.js.map +1 -0
- package/dist/agent-chat/session.d.ts +79 -0
- package/dist/agent-chat/session.js +391 -0
- package/dist/agent-chat/session.js.map +1 -0
- package/dist/agent-chat/step-label.d.ts +99 -0
- package/dist/agent-chat/step-label.js +116 -0
- package/dist/agent-chat/step-label.js.map +1 -0
- package/dist/agent-chat/store.d.ts +102 -0
- package/dist/agent-chat/store.js +876 -0
- package/dist/agent-chat/store.js.map +1 -0
- package/dist/agent-chat/types.d.ts +277 -0
- package/dist/agent-chat/types.js +17 -0
- package/dist/agent-chat/types.js.map +1 -0
- package/dist/agent-chat.d.ts +11 -0
- package/dist/agent-chat.js +11 -0
- package/dist/components/admin-permissions/admin-permissions.d.ts +66 -0
- package/dist/components/admin-permissions/admin-permissions.js +101 -0
- package/dist/components/admin-permissions/admin-permissions.js.map +1 -0
- package/dist/components/admin-permissions/context.d.ts +70 -0
- package/dist/components/admin-permissions/context.js +258 -0
- package/dist/components/admin-permissions/context.js.map +1 -0
- package/dist/components/admin-permissions/index.d.ts +10 -0
- package/dist/components/admin-permissions/licence.d.ts +15 -0
- package/dist/components/admin-permissions/licence.js +78 -0
- package/dist/components/admin-permissions/licence.js.map +1 -0
- package/dist/components/admin-permissions/matrix.d.ts +20 -0
- package/dist/components/admin-permissions/matrix.js +191 -0
- package/dist/components/admin-permissions/matrix.js.map +1 -0
- package/dist/components/admin-permissions/members.d.ts +18 -0
- package/dist/components/admin-permissions/members.js +185 -0
- package/dist/components/admin-permissions/members.js.map +1 -0
- package/dist/components/admin-permissions/role-assignment.d.ts +35 -0
- package/dist/components/admin-permissions/role-assignment.js +174 -0
- package/dist/components/admin-permissions/role-assignment.js.map +1 -0
- package/dist/components/admin-permissions/roles.d.ts +25 -0
- package/dist/components/admin-permissions/roles.js +168 -0
- package/dist/components/admin-permissions/roles.js.map +1 -0
- package/dist/components/admin-permissions/types.d.ts +152 -0
- package/dist/components/admin-permissions/types.js +63 -0
- package/dist/components/admin-permissions/types.js.map +1 -0
- package/dist/components/agent-chat-popup.d.ts +29 -0
- package/dist/components/agent-chat-popup.js +188 -0
- package/dist/components/agent-chat-popup.js.map +1 -0
- package/dist/components/agent-chat.d.ts +163 -0
- package/dist/components/agent-chat.js +673 -0
- package/dist/components/agent-chat.js.map +1 -0
- package/dist/components/app-shell.d.ts +126 -0
- package/dist/components/app-shell.js +297 -0
- package/dist/components/app-shell.js.map +1 -0
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button-link.js +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/checkbox.d.ts +1 -1
- package/dist/components/combobox.d.ts +1 -1
- package/dist/components/combobox.js +1 -1
- package/dist/components/consent-screen.d.ts +65 -0
- package/dist/components/consent-screen.js +123 -0
- package/dist/components/consent-screen.js.map +1 -0
- package/dist/components/data-table/data-table.d.ts +15 -1
- package/dist/components/data-table/data-table.js +18 -4
- package/dist/components/data-table/data-table.js.map +1 -1
- package/dist/components/data-table/index.d.ts +4 -4
- package/dist/components/data-table/parts.d.ts +27 -3
- package/dist/components/data-table/parts.js +175 -55
- package/dist/components/data-table/parts.js.map +1 -1
- package/dist/components/data-table/types.d.ts +61 -0
- package/dist/components/data-table/use-data-table.js +91 -6
- package/dist/components/data-table/use-data-table.js.map +1 -1
- package/dist/components/data-table/use-server-source.js +119 -28
- package/dist/components/data-table/use-server-source.js.map +1 -1
- package/dist/components/help-panel.d.ts +131 -0
- package/dist/components/help-panel.js +545 -0
- package/dist/components/help-panel.js.map +1 -0
- package/dist/components/login-screen.d.ts +127 -0
- package/dist/components/login-screen.js +339 -0
- package/dist/components/login-screen.js.map +1 -0
- package/dist/components/session-guard.d.ts +268 -0
- package/dist/components/session-guard.js +632 -0
- package/dist/components/session-guard.js.map +1 -0
- package/dist/components/toast.d.ts +1 -1
- package/dist/core.d.ts +5 -1
- package/dist/core.js +11 -7
- package/dist/data-table.d.ts +13 -4
- package/dist/data-table.js +10 -2
- package/dist/hooks/use-cortena-theme.js +49 -3
- package/dist/hooks/use-cortena-theme.js.map +1 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.js +21 -8
- package/dist/markdown.d.ts +2 -1
- package/dist/markdown.js +2 -1
- package/package.json +18 -5
- package/src/agent-chat/a2ui-block.ts +118 -0
- package/src/agent-chat/agui-client.ts +405 -0
- package/src/agent-chat/bridge.ts +445 -0
- package/src/agent-chat/session.ts +549 -0
- package/src/agent-chat/step-label.ts +177 -0
- package/src/agent-chat/store.ts +1234 -0
- package/src/agent-chat/types.ts +308 -0
- package/src/components/admin-permissions/admin-permissions.tsx +130 -0
- package/src/components/admin-permissions/context.tsx +376 -0
- package/src/components/admin-permissions/index.tsx +32 -0
- package/src/components/admin-permissions/licence.tsx +84 -0
- package/src/components/admin-permissions/matrix.tsx +257 -0
- package/src/components/admin-permissions/members.tsx +204 -0
- package/src/components/admin-permissions/role-assignment.tsx +239 -0
- package/src/components/admin-permissions/roles.tsx +169 -0
- package/src/components/admin-permissions/types.ts +231 -0
- package/src/components/agent-chat-popup.tsx +289 -0
- package/src/components/agent-chat.tsx +1006 -0
- package/src/components/app-shell.tsx +502 -0
- package/src/components/consent-screen.tsx +239 -0
- package/src/components/data-table/data-table.tsx +36 -0
- package/src/components/data-table/index.tsx +6 -1
- package/src/components/data-table/parts.tsx +223 -47
- package/src/components/data-table/types.ts +68 -0
- package/src/components/data-table/use-data-table.ts +152 -4
- package/src/components/data-table/use-server-source.ts +150 -12
- package/src/components/help-panel.tsx +765 -0
- package/src/components/login-screen.tsx +479 -0
- package/src/components/session-guard.tsx +1071 -0
- package/src/entries/agent-chat.ts +137 -0
- package/src/entries/core.ts +8 -0
- package/src/entries/data-table.ts +41 -0
- package/src/entries/markdown.ts +25 -0
- package/src/hooks/use-cortena-theme.ts +63 -4
- package/src/index.ts +6 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
//#region src/components/help-panel.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The markdown, or a loader for it. A function is called the first time the
|
|
6
|
+
* panel is opened, which is what keeps a long document out of the initial
|
|
7
|
+
* payload; keep its identity stable (module scope, or `useCallback`) or it is
|
|
8
|
+
* called again on every parent render.
|
|
9
|
+
*/
|
|
10
|
+
export type HelpSource = string | (() => Promise<string>);
|
|
11
|
+
export interface HelpSection {
|
|
12
|
+
/** Slug of the heading — the `#anchor` the outline links to. `""` for the preamble. */
|
|
13
|
+
id: string;
|
|
14
|
+
/** 1–6 for a heading; 0 for the text before the first heading. */
|
|
15
|
+
depth: number;
|
|
16
|
+
/** Heading text with inline markdown removed. */
|
|
17
|
+
title: string;
|
|
18
|
+
/** The heading and everything under it, verbatim, so it can be re-rendered alone. */
|
|
19
|
+
markdown: string;
|
|
20
|
+
}
|
|
21
|
+
export interface HelpRoute {
|
|
22
|
+
/** A route the extension serves, e.g. `/board`. */
|
|
23
|
+
route: string;
|
|
24
|
+
/** The section it is about: a heading, or that heading's slug. */
|
|
25
|
+
target: string;
|
|
26
|
+
}
|
|
27
|
+
export interface HelpDocument {
|
|
28
|
+
/** `title:` from the front matter, else the first `#` heading. */
|
|
29
|
+
title?: string;
|
|
30
|
+
/** Every scalar key in the front matter. */
|
|
31
|
+
frontMatter: Record<string, string>;
|
|
32
|
+
/** The `route:` / `routes:` block, longest route first. */
|
|
33
|
+
routes: HelpRoute[];
|
|
34
|
+
/** The document without its front matter. */
|
|
35
|
+
body: string;
|
|
36
|
+
sections: HelpSection[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parse the document once: front matter, then sections split at ATX headings.
|
|
40
|
+
*
|
|
41
|
+
* Exported because the route map and the outline are worth asserting on
|
|
42
|
+
* without a browser, and because an extension that wants its own navigation
|
|
43
|
+
* over the same document should read it the same way rather than write a
|
|
44
|
+
* second parser.
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseHelpDocument(source: string): HelpDocument;
|
|
47
|
+
/**
|
|
48
|
+
* The section a route is about: the front matter's map first (exact, then the
|
|
49
|
+
* longest matching prefix, so `/tasks/123` finds `/tasks`), then a heading
|
|
50
|
+
* whose slug is the route or its last segment.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveHelpTarget(doc: HelpDocument, context?: string): string | undefined;
|
|
53
|
+
export interface HelpHotkeysOptions {
|
|
54
|
+
/** Whether the panel is open. Escape only closes when it is. */
|
|
55
|
+
open?: boolean;
|
|
56
|
+
/** Defaults to pressing the shell's help button. */
|
|
57
|
+
onOpen?: () => void;
|
|
58
|
+
onClose?: () => void;
|
|
59
|
+
/** Turn the shortcuts off without unmounting. */
|
|
60
|
+
disabled?: boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Escape closes, `?` opens — the second only when focus is not in an input,
|
|
64
|
+
* because `?` is a character before it is a shortcut and stealing it from
|
|
65
|
+
* someone typing a question into a filter box is worse than having no
|
|
66
|
+
* shortcut at all.
|
|
67
|
+
*
|
|
68
|
+
* `HelpPanel` calls this itself for Escape. Mount `<HelpHotkeys />` in the
|
|
69
|
+
* shell's children for `?`, which has to be listening while the panel is
|
|
70
|
+
* closed and therefore unmounted.
|
|
71
|
+
*/
|
|
72
|
+
export declare function useHelpHotkeys({ open, onOpen, onClose, disabled }?: HelpHotkeysOptions): void;
|
|
73
|
+
export interface HelpHotkeysProps extends HelpHotkeysOptions {}
|
|
74
|
+
/** `useHelpHotkeys` as a component, for a shell whose children are markup. */
|
|
75
|
+
export declare function HelpHotkeys(props: HelpHotkeysProps): null;
|
|
76
|
+
/**
|
|
77
|
+
* `documentUrl` when it is safe to hang off an `href`, `undefined` otherwise.
|
|
78
|
+
*
|
|
79
|
+
* A relative URL is resolved against the current page first, so "/docs/x.md"
|
|
80
|
+
* — the ordinary case — passes on any http(s) origin and is not special-cased
|
|
81
|
+
* into a hole. `javascript:`, `data:`, `blob:` and `vbscript:` all fall out of
|
|
82
|
+
* the same allow-list rather than each needing to be thought of.
|
|
83
|
+
*/
|
|
84
|
+
export declare function httpDocumentUrl(documentUrl: string | undefined): string | undefined;
|
|
85
|
+
export interface HelpPanelProps extends Omit<React.ComponentProps<"div">, "title" | "onSelect"> {
|
|
86
|
+
/** The functional document, as markdown. A function is loaded on first open. */
|
|
87
|
+
source: HelpSource;
|
|
88
|
+
/** From the shell's help slot. `false` renders nothing and loads nothing. */
|
|
89
|
+
open?: boolean;
|
|
90
|
+
/** Closes the panel: the header button and Escape. */
|
|
91
|
+
onClose?: () => void;
|
|
92
|
+
/** The current route. Opens the panel on the section that route is about. */
|
|
93
|
+
context?: string;
|
|
94
|
+
/** Header title. Defaults to the front matter's `title:`, then the first `#`. */
|
|
95
|
+
title?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Where "open in new tab" goes — the document's own URL, so the tab can be
|
|
98
|
+
* shared. Without one the panel opens the text it already has.
|
|
99
|
+
*
|
|
100
|
+
* **http(s) only.** The value reaches an `href` the user clicks, and the
|
|
101
|
+
* panel's source is the extension's functional document, which an extension
|
|
102
|
+
* may well be fetching from somewhere configurable. A `javascript:` URL in
|
|
103
|
+
* that slot is script execution one click away, and `data:` opens a document
|
|
104
|
+
* of the author's choosing on a tab the user believes is the help page.
|
|
105
|
+
*/
|
|
106
|
+
documentUrl?: string;
|
|
107
|
+
/** Deepest heading level in the outline. 3 by default; 6 shows every heading. */
|
|
108
|
+
outlineDepth?: number;
|
|
109
|
+
/** Start with the search box filled. For the guide and for tests. */
|
|
110
|
+
defaultQuery?: string;
|
|
111
|
+
}
|
|
112
|
+
declare function HelpPanel({ source, open, onClose, context, title, documentUrl, outlineDepth, defaultQuery, className, ...props }: HelpPanelProps): React.JSX.Element | null;
|
|
113
|
+
export interface HelpPanelSlotConfig extends Omit<HelpPanelProps, "open" | "onClose"> {}
|
|
114
|
+
/** What `AppShell` hands its `helpPanel` render function. */
|
|
115
|
+
interface ShellHelpSlot {
|
|
116
|
+
open: boolean;
|
|
117
|
+
source?: string;
|
|
118
|
+
onClose: () => void;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* `AppShell`'s `helpPanel` slot, filled.
|
|
122
|
+
*
|
|
123
|
+
* The shell renders whatever the function returns, open or closed, so this
|
|
124
|
+
* returns `null` while closed — a panel that rendered an empty box would draw
|
|
125
|
+
* the shell's bordered aside over the corner with nothing in it. `help.source`
|
|
126
|
+
* from the shell is used when no `source` is passed here.
|
|
127
|
+
*/
|
|
128
|
+
export declare function helpPanelSlot(config: HelpPanelSlotConfig): (slot: ShellHelpSlot) => React.ReactNode;
|
|
129
|
+
//#endregion
|
|
130
|
+
export { HelpPanel };
|
|
131
|
+
//# sourceMappingURL=help-panel.d.ts.map
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
4
|
+
import { Spinner } from "./spinner.js";
|
|
5
|
+
import { Markdown, markdownSanitizeSchema } from "./markdown.js";
|
|
6
|
+
import { ExternalLink, Search, X } from "lucide-react";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
//#region src/components/help-panel.tsx
|
|
10
|
+
/**
|
|
11
|
+
* HelpPanel — the extension's own help, answered from its own functional
|
|
12
|
+
* document.
|
|
13
|
+
*
|
|
14
|
+
* §12 of how-to-create-a-cortena-extension, audit rule P-13. Every extension
|
|
15
|
+
* with a UI ships one: anchored bottom-right, collapsed by default, expanding
|
|
16
|
+
* into a right-hand vertical panel. It fills `AppShell`'s help slot, so the
|
|
17
|
+
* shell keeps the button, the open state and the corner geometry (the panel
|
|
18
|
+
* stops 12px above the cluster so the button that opened it stays clickable).
|
|
19
|
+
*
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <AppShell
|
|
22
|
+
* extension={{ id: "tasks", name: "Tasks" }}
|
|
23
|
+
* user={user}
|
|
24
|
+
* help={{ source: "/docs/functional.md" }}
|
|
25
|
+
* helpPanel={helpPanelSlot({
|
|
26
|
+
* // lazy: the document is fetched the first time help is opened
|
|
27
|
+
* source: () => fetch("/docs/functional.md").then((r) => r.text()),
|
|
28
|
+
* documentUrl: "/docs/functional.md",
|
|
29
|
+
* context: location.pathname,
|
|
30
|
+
* })}
|
|
31
|
+
* >
|
|
32
|
+
* <HelpHotkeys />
|
|
33
|
+
* …
|
|
34
|
+
* </AppShell>
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* It is NOT the agent pop-up (§17) and the two must not be merged. Help
|
|
38
|
+
* answers *about* the extension from a document; the agent *acts* in the
|
|
39
|
+
* extension through its tools. Same corner, different affordances.
|
|
40
|
+
*
|
|
41
|
+
* Giving the functional document a runtime consumer is the whole point: a
|
|
42
|
+
* stale document becomes a visibly wrong help panel rather than a file nobody
|
|
43
|
+
* opens. So the panel renders the document itself — outline from its headings,
|
|
44
|
+
* search over its text, `#heading` deep links, and "open in new tab" for the
|
|
45
|
+
* whole thing — rather than a hand-written copy of it that can drift.
|
|
46
|
+
*
|
|
47
|
+
* `context` is the current route. On open the panel jumps to the section that
|
|
48
|
+
* route is about, matched against the front matter's `routes:` map first and
|
|
49
|
+
* against the headings second, so help opens on the part of the document the
|
|
50
|
+
* user is looking at rather than at the top of it.
|
|
51
|
+
*
|
|
52
|
+
* Lives on the `cortena-ui/markdown` entry: it renders markdown, so it carries
|
|
53
|
+
* react-markdown, and no extension that only draws a Button may pay for that.
|
|
54
|
+
*/
|
|
55
|
+
/** Lowercase, punctuation to hyphens. The same slug the rendered heading gets. */
|
|
56
|
+
function slugify(text) {
|
|
57
|
+
return text.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "section";
|
|
58
|
+
}
|
|
59
|
+
/** Slugs in document order, with `-1`, `-2` for repeats, as GitHub numbers them. */
|
|
60
|
+
function createSlugger() {
|
|
61
|
+
const seen = /* @__PURE__ */ new Map();
|
|
62
|
+
return (text) => {
|
|
63
|
+
const base = slugify(text);
|
|
64
|
+
const n = seen.get(base) ?? 0;
|
|
65
|
+
seen.set(base, n + 1);
|
|
66
|
+
return n === 0 ? base : `${base}-${n}`;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Heading text with the inline markdown taken off, so the outline entry and
|
|
71
|
+
* the slug match what the rendered heading actually says. Without this a
|
|
72
|
+
* `## [Reports](/reports)` heading slugs to `reports-reports` in the outline
|
|
73
|
+
* and to `reports` in the document, and the deep link lands nowhere.
|
|
74
|
+
*/
|
|
75
|
+
function plainText(md) {
|
|
76
|
+
return md.replace(/`([^`]*)`/g, "$1").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/<[^>]*>/g, "").replace(/[*_~]+/g, "").replace(/\s+/g, " ").trim();
|
|
77
|
+
}
|
|
78
|
+
const FRONT_MATTER = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
79
|
+
const HEADING = /^(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/;
|
|
80
|
+
const FENCE = /^[ \t]*(```|~~~)/;
|
|
81
|
+
function unquote(value) {
|
|
82
|
+
const trimmed = value.trim();
|
|
83
|
+
const quoted = /^(['"])([\s\S]*)\1$/.exec(trimmed);
|
|
84
|
+
return quoted ? quoted[2] ?? "" : trimmed;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parse the document once: front matter, then sections split at ATX headings.
|
|
88
|
+
*
|
|
89
|
+
* Exported because the route map and the outline are worth asserting on
|
|
90
|
+
* without a browser, and because an extension that wants its own navigation
|
|
91
|
+
* over the same document should read it the same way rather than write a
|
|
92
|
+
* second parser.
|
|
93
|
+
*/
|
|
94
|
+
function parseHelpDocument(source) {
|
|
95
|
+
const matter = FRONT_MATTER.exec(source);
|
|
96
|
+
const body = matter ? source.slice(matter[0].length) : source;
|
|
97
|
+
const frontMatter = {};
|
|
98
|
+
const routes = [];
|
|
99
|
+
if (matter) {
|
|
100
|
+
let inRoutes = false;
|
|
101
|
+
for (const line of (matter[1] ?? "").split(/\r?\n/)) {
|
|
102
|
+
if (!line.trim() || line.trim().startsWith("#")) continue;
|
|
103
|
+
const indented = /^[ \t]+/.test(line);
|
|
104
|
+
const pair = /^[ \t]*([^:]+):[ \t]*(.*)$/.exec(line);
|
|
105
|
+
if (!pair) continue;
|
|
106
|
+
const key = unquote(pair[1] ?? "");
|
|
107
|
+
const value = unquote(pair[2] ?? "");
|
|
108
|
+
if (inRoutes && indented) {
|
|
109
|
+
if (key && value) routes.push({
|
|
110
|
+
route: key,
|
|
111
|
+
target: value
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
inRoutes = !indented && (key === "route" || key === "routes") && value === "";
|
|
116
|
+
if (!inRoutes && !indented && key) frontMatter[key] = value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
routes.sort((a, b) => b.route.length - a.route.length);
|
|
120
|
+
const slug = createSlugger();
|
|
121
|
+
const sections = [];
|
|
122
|
+
let current;
|
|
123
|
+
let fence;
|
|
124
|
+
for (const line of body.split(/\r?\n/)) {
|
|
125
|
+
const fenced = FENCE.exec(line);
|
|
126
|
+
if (fenced) {
|
|
127
|
+
const mark = fenced[1] ?? "";
|
|
128
|
+
fence = fence === void 0 ? mark : fence === mark ? void 0 : fence;
|
|
129
|
+
}
|
|
130
|
+
const heading = fence === void 0 ? HEADING.exec(line) : null;
|
|
131
|
+
if (heading) {
|
|
132
|
+
const title = plainText(heading[2] ?? "");
|
|
133
|
+
current = {
|
|
134
|
+
id: slug(title),
|
|
135
|
+
depth: (heading[1] ?? "").length,
|
|
136
|
+
title,
|
|
137
|
+
markdown: ""
|
|
138
|
+
};
|
|
139
|
+
sections.push(current);
|
|
140
|
+
} else if (!current) {
|
|
141
|
+
current = {
|
|
142
|
+
id: "",
|
|
143
|
+
depth: 0,
|
|
144
|
+
title: "",
|
|
145
|
+
markdown: ""
|
|
146
|
+
};
|
|
147
|
+
sections.push(current);
|
|
148
|
+
}
|
|
149
|
+
current.markdown += `${line}\n`;
|
|
150
|
+
}
|
|
151
|
+
const preamble = sections[0];
|
|
152
|
+
if (preamble && preamble.depth === 0 && !preamble.markdown.trim()) sections.shift();
|
|
153
|
+
return {
|
|
154
|
+
title: frontMatter.title ?? sections.find((s) => s.depth === 1)?.title,
|
|
155
|
+
frontMatter,
|
|
156
|
+
routes,
|
|
157
|
+
body,
|
|
158
|
+
sections
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function normalizeRoute(route) {
|
|
162
|
+
const path = (route.split(/[?#]/)[0] ?? "").trim().toLowerCase();
|
|
163
|
+
return path.length > 1 ? path.replace(/\/+$/, "") : path;
|
|
164
|
+
}
|
|
165
|
+
function findSection(doc, target) {
|
|
166
|
+
const wanted = target.trim().toLowerCase();
|
|
167
|
+
return doc.sections.find((s) => s.id !== "" && (s.id === wanted || s.id === slugify(wanted) || s.title.toLowerCase() === wanted));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The section a route is about: the front matter's map first (exact, then the
|
|
171
|
+
* longest matching prefix, so `/tasks/123` finds `/tasks`), then a heading
|
|
172
|
+
* whose slug is the route or its last segment.
|
|
173
|
+
*/
|
|
174
|
+
function resolveHelpTarget(doc, context) {
|
|
175
|
+
if (!context) return void 0;
|
|
176
|
+
const path = normalizeRoute(context);
|
|
177
|
+
if (!path) return void 0;
|
|
178
|
+
for (const { route, target } of doc.routes) {
|
|
179
|
+
const candidate = normalizeRoute(route);
|
|
180
|
+
const prefix = candidate === "/" ? "/" : `${candidate}/`;
|
|
181
|
+
if (candidate === path || path.startsWith(prefix)) {
|
|
182
|
+
const section = findSection(doc, target);
|
|
183
|
+
if (section) return section.id;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const segments = path.split("/").filter(Boolean);
|
|
187
|
+
for (const candidate of [path, segments.at(-1)]) {
|
|
188
|
+
if (!candidate) continue;
|
|
189
|
+
const section = findSection(doc, candidate);
|
|
190
|
+
if (section) return section.id;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function nodeText(node) {
|
|
194
|
+
if (node.type === "text") return node.value ?? "";
|
|
195
|
+
return (node.children ?? []).map(nodeText).join("");
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Give every heading the slug the outline links to. It runs AFTER sanitize —
|
|
199
|
+
* the ids are ours, not the document's, so they are added to trusted nodes
|
|
200
|
+
* rather than smuggled through the sanitizer.
|
|
201
|
+
*/
|
|
202
|
+
function rehypeHelpAnchors() {
|
|
203
|
+
return (tree) => {
|
|
204
|
+
const slug = createSlugger();
|
|
205
|
+
const walk = (node) => {
|
|
206
|
+
const tag = node.tagName ?? "";
|
|
207
|
+
if (/^h[1-6]$/.test(tag)) node.properties = {
|
|
208
|
+
...node.properties,
|
|
209
|
+
id: slug(plainText(nodeText(node)))
|
|
210
|
+
};
|
|
211
|
+
for (const child of node.children ?? []) walk(child);
|
|
212
|
+
};
|
|
213
|
+
walk(tree);
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function isTypingTarget(target) {
|
|
217
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
218
|
+
if (target.isContentEditable) return true;
|
|
219
|
+
const tag = target.tagName;
|
|
220
|
+
if (tag === "TEXTAREA" || tag === "SELECT") return true;
|
|
221
|
+
if (tag !== "INPUT") return target.closest("[role=\"textbox\"], [role=\"searchbox\"]") !== null;
|
|
222
|
+
const type = target.type;
|
|
223
|
+
return type !== "checkbox" && type !== "radio" && type !== "button" && type !== "submit";
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The shell owns the open state and exposes only its button, so with no
|
|
227
|
+
* `onOpen` the shortcut presses exactly the control a pointer would. Already
|
|
228
|
+
* open (the button says so) means `?` is a no-op rather than a close.
|
|
229
|
+
*/
|
|
230
|
+
function pressHelpButton() {
|
|
231
|
+
const button = document.querySelector("[data-slot=\"help-button\"]");
|
|
232
|
+
if (!button || button.getAttribute("aria-expanded") === "true") return;
|
|
233
|
+
button.click();
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Escape closes, `?` opens — the second only when focus is not in an input,
|
|
237
|
+
* because `?` is a character before it is a shortcut and stealing it from
|
|
238
|
+
* someone typing a question into a filter box is worse than having no
|
|
239
|
+
* shortcut at all.
|
|
240
|
+
*
|
|
241
|
+
* `HelpPanel` calls this itself for Escape. Mount `<HelpHotkeys />` in the
|
|
242
|
+
* shell's children for `?`, which has to be listening while the panel is
|
|
243
|
+
* closed and therefore unmounted.
|
|
244
|
+
*/
|
|
245
|
+
function useHelpHotkeys({ open, onOpen, onClose, disabled } = {}) {
|
|
246
|
+
const state = React.useRef({
|
|
247
|
+
open,
|
|
248
|
+
onOpen,
|
|
249
|
+
onClose
|
|
250
|
+
});
|
|
251
|
+
state.current = {
|
|
252
|
+
open,
|
|
253
|
+
onOpen,
|
|
254
|
+
onClose
|
|
255
|
+
};
|
|
256
|
+
React.useEffect(() => {
|
|
257
|
+
if (disabled) return;
|
|
258
|
+
const onKeyDown = (event) => {
|
|
259
|
+
const { open: isOpen, onOpen: openIt, onClose: closeIt } = state.current;
|
|
260
|
+
if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
261
|
+
if (event.key === "Escape") {
|
|
262
|
+
if (isOpen && closeIt) closeIt();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (event.key !== "?" || isOpen || isTypingTarget(event.target)) return;
|
|
266
|
+
event.preventDefault();
|
|
267
|
+
(openIt ?? pressHelpButton)();
|
|
268
|
+
};
|
|
269
|
+
document.addEventListener("keydown", onKeyDown);
|
|
270
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
271
|
+
}, [disabled]);
|
|
272
|
+
}
|
|
273
|
+
/** `useHelpHotkeys` as a component, for a shell whose children are markup. */
|
|
274
|
+
function HelpHotkeys(props) {
|
|
275
|
+
useHelpHotkeys(props);
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* `documentUrl` when it is safe to hang off an `href`, `undefined` otherwise.
|
|
280
|
+
*
|
|
281
|
+
* A relative URL is resolved against the current page first, so "/docs/x.md"
|
|
282
|
+
* — the ordinary case — passes on any http(s) origin and is not special-cased
|
|
283
|
+
* into a hole. `javascript:`, `data:`, `blob:` and `vbscript:` all fall out of
|
|
284
|
+
* the same allow-list rather than each needing to be thought of.
|
|
285
|
+
*/
|
|
286
|
+
function httpDocumentUrl(documentUrl) {
|
|
287
|
+
if (!documentUrl) return void 0;
|
|
288
|
+
try {
|
|
289
|
+
const base = typeof location === "undefined" ? void 0 : location.href;
|
|
290
|
+
const { protocol } = base === void 0 ? new URL(documentUrl) : new URL(documentUrl, base);
|
|
291
|
+
return protocol === "http:" || protocol === "https:" ? documentUrl : void 0;
|
|
292
|
+
} catch {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const headerButton = [
|
|
297
|
+
"inline-flex size-8 shrink-0 items-center justify-center rounded-[var(--ds-radius-md)]",
|
|
298
|
+
"text-[color:var(--ds-muted-foreground)] outline-none",
|
|
299
|
+
"transition-colors duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]",
|
|
300
|
+
"hover:bg-[var(--ds-hover)] hover:text-[color:var(--ds-foreground)]",
|
|
301
|
+
"focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)]",
|
|
302
|
+
"[&_svg]:size-4"
|
|
303
|
+
].join(" ");
|
|
304
|
+
/**
|
|
305
|
+
* Documents already loaded, by the loader that produced them.
|
|
306
|
+
*
|
|
307
|
+
* `helpPanelSlot` returns `null` while closed, so closing the panel UNMOUNTS
|
|
308
|
+
* it and takes its state with it. Every reopen therefore re-ran the loader:
|
|
309
|
+
* on a `fetch` that is a network round trip per open, and on a slow one the
|
|
310
|
+
* panel shows a skeleton for a document the user read a minute ago. The cache
|
|
311
|
+
* is keyed by the loader itself and held weakly, so a caller that memoises its
|
|
312
|
+
* loader — the documented shape — gets the document back instantly, and one
|
|
313
|
+
* that builds a new closure each render is simply not cached rather than
|
|
314
|
+
* leaking.
|
|
315
|
+
*/
|
|
316
|
+
const loadedDocuments = /* @__PURE__ */ new WeakMap();
|
|
317
|
+
function HelpPanel({ source, open = true, onClose, context, title, documentUrl, outlineDepth = 3, defaultQuery = "", className, ...props }) {
|
|
318
|
+
const [text, setText] = React.useState(typeof source === "string" ? source : loadedDocuments.get(source));
|
|
319
|
+
const [failed, setFailed] = React.useState(false);
|
|
320
|
+
const [query, setQuery] = React.useState(defaultQuery);
|
|
321
|
+
const [activeId, setActiveId] = React.useState();
|
|
322
|
+
const bodyRef = React.useRef(null);
|
|
323
|
+
const panelRef = React.useRef(null);
|
|
324
|
+
useHelpHotkeys({
|
|
325
|
+
open,
|
|
326
|
+
onClose
|
|
327
|
+
});
|
|
328
|
+
/**
|
|
329
|
+
* Focus moves into the panel when it opens.
|
|
330
|
+
*
|
|
331
|
+
* The shell's help button stays where it is, so without this, focus is
|
|
332
|
+
* still on the button: Escape works (the handler is on the document) but
|
|
333
|
+
* Tab walks the page BEHIND the panel, and a screen reader announces
|
|
334
|
+
* nothing at all — the panel is simply not where the user is. The search
|
|
335
|
+
* box is the target, because searching is what the panel is for.
|
|
336
|
+
*/
|
|
337
|
+
React.useEffect(() => {
|
|
338
|
+
if (!open) return;
|
|
339
|
+
const frame = requestAnimationFrame(() => {
|
|
340
|
+
const root = panelRef.current;
|
|
341
|
+
if (!root || root.contains(document.activeElement)) return;
|
|
342
|
+
root.querySelector("[data-slot=\"help-panel-search-input\"]")?.focus({ preventScroll: true });
|
|
343
|
+
});
|
|
344
|
+
return () => cancelAnimationFrame(frame);
|
|
345
|
+
}, [open]);
|
|
346
|
+
React.useEffect(() => {
|
|
347
|
+
if (!open) return;
|
|
348
|
+
if (typeof source === "string") {
|
|
349
|
+
setText(source);
|
|
350
|
+
setFailed(false);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const cached = loadedDocuments.get(source);
|
|
354
|
+
if (cached !== void 0) {
|
|
355
|
+
setText(cached);
|
|
356
|
+
setFailed(false);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
let cancelled = false;
|
|
360
|
+
setFailed(false);
|
|
361
|
+
source().then((loaded) => {
|
|
362
|
+
loadedDocuments.set(source, loaded);
|
|
363
|
+
if (!cancelled) setText(loaded);
|
|
364
|
+
}, () => {
|
|
365
|
+
if (!cancelled) setFailed(true);
|
|
366
|
+
});
|
|
367
|
+
return () => {
|
|
368
|
+
cancelled = true;
|
|
369
|
+
};
|
|
370
|
+
}, [open, source]);
|
|
371
|
+
const doc = React.useMemo(() => parseHelpDocument(text ?? ""), [text]);
|
|
372
|
+
const needle = query.trim().toLowerCase();
|
|
373
|
+
const matches = React.useMemo(() => needle ? doc.sections.filter((section) => section.markdown.toLowerCase().includes(needle)) : doc.sections, [doc, needle]);
|
|
374
|
+
const markdown = needle ? matches.map((section) => section.markdown).join("\n") : doc.body;
|
|
375
|
+
const outline = matches.filter((s) => s.depth >= 1 && s.depth <= outlineDepth);
|
|
376
|
+
const scrolledTo = React.useRef(void 0);
|
|
377
|
+
const scrollTo = React.useCallback((id, onlyIfUntouched = false) => {
|
|
378
|
+
const scroller = bodyRef.current;
|
|
379
|
+
const heading = scroller?.querySelector(`#${CSS.escape(id)}`);
|
|
380
|
+
if (!scroller || !heading) return false;
|
|
381
|
+
if (onlyIfUntouched && scrolledTo.current !== scroller.scrollTop) return false;
|
|
382
|
+
scroller.scrollTop += heading.getBoundingClientRect().top - scroller.getBoundingClientRect().top;
|
|
383
|
+
scrolledTo.current = scroller.scrollTop;
|
|
384
|
+
setActiveId(id);
|
|
385
|
+
return true;
|
|
386
|
+
}, []);
|
|
387
|
+
const target = React.useMemo(() => resolveHelpTarget(doc, context), [doc, context]);
|
|
388
|
+
React.useEffect(() => {
|
|
389
|
+
if (!open || !target || needle) return;
|
|
390
|
+
let cancelled = false;
|
|
391
|
+
scrollTo(target);
|
|
392
|
+
const again = () => {
|
|
393
|
+
if (!cancelled) scrollTo(target, scrolledTo.current !== void 0);
|
|
394
|
+
};
|
|
395
|
+
const frame = requestAnimationFrame(again);
|
|
396
|
+
document.fonts?.ready.then(again);
|
|
397
|
+
return () => {
|
|
398
|
+
cancelled = true;
|
|
399
|
+
cancelAnimationFrame(frame);
|
|
400
|
+
};
|
|
401
|
+
}, [
|
|
402
|
+
open,
|
|
403
|
+
target,
|
|
404
|
+
needle,
|
|
405
|
+
scrollTo
|
|
406
|
+
]);
|
|
407
|
+
const safeDocumentUrl = httpDocumentUrl(documentUrl);
|
|
408
|
+
const [fallbackUrl, setFallbackUrl] = React.useState();
|
|
409
|
+
React.useEffect(() => {
|
|
410
|
+
if (safeDocumentUrl || text === void 0) return;
|
|
411
|
+
const url = URL.createObjectURL(new Blob([text], { type: "text/markdown;charset=utf-8" }));
|
|
412
|
+
setFallbackUrl(url);
|
|
413
|
+
return () => URL.revokeObjectURL(url);
|
|
414
|
+
}, [safeDocumentUrl, text]);
|
|
415
|
+
const href = safeDocumentUrl ?? fallbackUrl;
|
|
416
|
+
if (!open) return null;
|
|
417
|
+
const heading = title ?? doc.title ?? "Help";
|
|
418
|
+
const loading = text === void 0 && !failed;
|
|
419
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
420
|
+
ref: panelRef,
|
|
421
|
+
"data-slot": "help-panel",
|
|
422
|
+
"data-loading": loading || void 0,
|
|
423
|
+
className: cn("flex h-full min-h-0 w-full flex-col overflow-hidden", "bg-[var(--ds-card)] text-[color:var(--ds-foreground)]", className),
|
|
424
|
+
...props,
|
|
425
|
+
children: [
|
|
426
|
+
/* @__PURE__ */ jsxs("header", {
|
|
427
|
+
"data-slot": "help-panel-header",
|
|
428
|
+
className: "flex shrink-0 items-center gap-2 border-b border-[var(--ds-border-subtle)] px-4 py-3",
|
|
429
|
+
children: [
|
|
430
|
+
/* @__PURE__ */ jsx("h2", {
|
|
431
|
+
"data-slot": "help-panel-title",
|
|
432
|
+
className: cn("min-w-0 flex-1 truncate font-display font-bold", "text-[length:var(--ds-text-body-sm)] leading-[var(--ds-text-body-sm--line-height)]", "text-[color:var(--ds-foreground)]"),
|
|
433
|
+
children: heading
|
|
434
|
+
}),
|
|
435
|
+
href ? /* @__PURE__ */ jsx("a", {
|
|
436
|
+
"data-slot": "help-panel-open-full",
|
|
437
|
+
href,
|
|
438
|
+
target: "_blank",
|
|
439
|
+
rel: "noreferrer",
|
|
440
|
+
"aria-label": "Open the full document in a new tab",
|
|
441
|
+
className: headerButton,
|
|
442
|
+
children: /* @__PURE__ */ jsx(ExternalLink, { "aria-hidden": true })
|
|
443
|
+
}) : null,
|
|
444
|
+
onClose ? /* @__PURE__ */ jsx("button", {
|
|
445
|
+
type: "button",
|
|
446
|
+
"data-slot": "help-panel-close",
|
|
447
|
+
"aria-label": "Close help",
|
|
448
|
+
onClick: onClose,
|
|
449
|
+
className: headerButton,
|
|
450
|
+
children: /* @__PURE__ */ jsx(X, { "aria-hidden": true })
|
|
451
|
+
}) : null
|
|
452
|
+
]
|
|
453
|
+
}),
|
|
454
|
+
/* @__PURE__ */ jsx("div", {
|
|
455
|
+
"data-slot": "help-panel-search",
|
|
456
|
+
className: "shrink-0 border-b border-[var(--ds-border-subtle)] px-4 py-3",
|
|
457
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
458
|
+
className: "relative",
|
|
459
|
+
children: [/* @__PURE__ */ jsx(Search, {
|
|
460
|
+
"aria-hidden": true,
|
|
461
|
+
className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-[color:var(--ds-text-tertiary)]"
|
|
462
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
463
|
+
type: "search",
|
|
464
|
+
"data-slot": "help-panel-search-input",
|
|
465
|
+
"aria-label": `Search ${heading}`,
|
|
466
|
+
placeholder: "Search this document",
|
|
467
|
+
value: query,
|
|
468
|
+
onChange: (event) => setQuery(event.target.value),
|
|
469
|
+
className: cn("h-9 w-full rounded-[var(--ds-radius-md)] border border-[var(--ds-border)]", "bg-[var(--ds-muted)] py-2 pr-3 pl-8", "text-[length:var(--ds-text-caption-lg)] leading-[var(--ds-text-caption-lg--line-height)]", "text-[color:var(--ds-foreground)] placeholder:text-[color:var(--ds-text-tertiary)]", "transition-[border-color,box-shadow] duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]", "outline-none hover:border-[var(--ds-border-strong)]", "focus-visible:border-[var(--ds-ring)] focus-visible:ring-[3px] focus-visible:ring-[var(--ds-ring)]/40", "[&::-webkit-search-cancel-button]:hidden")
|
|
470
|
+
})]
|
|
471
|
+
})
|
|
472
|
+
}),
|
|
473
|
+
outline.length > 0 ? /* @__PURE__ */ jsx("nav", {
|
|
474
|
+
"data-slot": "help-panel-outline",
|
|
475
|
+
"aria-label": `${heading} sections`,
|
|
476
|
+
className: "max-h-40 shrink-0 overflow-y-auto border-b border-[var(--ds-border-subtle)] px-2 py-2",
|
|
477
|
+
children: /* @__PURE__ */ jsx("ol", {
|
|
478
|
+
className: "flex flex-col",
|
|
479
|
+
children: outline.map((section) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("a", {
|
|
480
|
+
"data-slot": "help-panel-outline-item",
|
|
481
|
+
"data-depth": section.depth,
|
|
482
|
+
"data-active": section.id === activeId || void 0,
|
|
483
|
+
href: `#${section.id}`,
|
|
484
|
+
onClick: (event) => {
|
|
485
|
+
event.preventDefault();
|
|
486
|
+
scrollTo(section.id);
|
|
487
|
+
},
|
|
488
|
+
className: cn("block truncate rounded-[var(--ds-radius-sm)] px-2 py-1 no-underline", "text-[length:var(--ds-text-caption)] leading-[var(--ds-text-caption--line-height)]", "text-[color:var(--ds-muted-foreground)] outline-none", "transition-colors duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]", "hover:bg-[var(--ds-hover)] hover:text-[color:var(--ds-foreground)]", "focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)]", "data-[active]:bg-[var(--ds-primary-soft)] data-[active]:text-[color:var(--ds-primary-soft-foreground)]", section.depth === 2 && "pl-5", section.depth >= 3 && "pl-8"),
|
|
489
|
+
children: section.title
|
|
490
|
+
}) }, section.id))
|
|
491
|
+
})
|
|
492
|
+
}) : null,
|
|
493
|
+
/* @__PURE__ */ jsx("div", {
|
|
494
|
+
ref: bodyRef,
|
|
495
|
+
"data-slot": "help-panel-body",
|
|
496
|
+
className: "min-h-0 flex-1 overflow-y-auto px-4 py-3",
|
|
497
|
+
children: loading ? /* @__PURE__ */ jsxs("div", {
|
|
498
|
+
"data-slot": "help-panel-loading",
|
|
499
|
+
className: "flex items-center gap-2 py-6",
|
|
500
|
+
children: [/* @__PURE__ */ jsx(Spinner, {}), /* @__PURE__ */ jsx("span", {
|
|
501
|
+
className: "text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]",
|
|
502
|
+
children: "Loading help…"
|
|
503
|
+
})]
|
|
504
|
+
}) : failed ? /* @__PURE__ */ jsx("p", {
|
|
505
|
+
"data-slot": "help-panel-error",
|
|
506
|
+
className: "py-6 text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-destructive)]",
|
|
507
|
+
children: "The help document could not be loaded."
|
|
508
|
+
}) : needle && matches.length === 0 ? /* @__PURE__ */ jsxs("p", {
|
|
509
|
+
"data-slot": "help-panel-empty",
|
|
510
|
+
className: "py-6 text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]",
|
|
511
|
+
children: [
|
|
512
|
+
"No section matches “",
|
|
513
|
+
query.trim(),
|
|
514
|
+
"”."
|
|
515
|
+
]
|
|
516
|
+
}) : /* @__PURE__ */ jsx(Markdown, {
|
|
517
|
+
compact: true,
|
|
518
|
+
sanitizeSchema: markdownSanitizeSchema,
|
|
519
|
+
rehypePlugins: [rehypeHelpAnchors],
|
|
520
|
+
children: markdown
|
|
521
|
+
})
|
|
522
|
+
})
|
|
523
|
+
]
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* `AppShell`'s `helpPanel` slot, filled.
|
|
528
|
+
*
|
|
529
|
+
* The shell renders whatever the function returns, open or closed, so this
|
|
530
|
+
* returns `null` while closed — a panel that rendered an empty box would draw
|
|
531
|
+
* the shell's bordered aside over the corner with nothing in it. `help.source`
|
|
532
|
+
* from the shell is used when no `source` is passed here.
|
|
533
|
+
*/
|
|
534
|
+
function helpPanelSlot(config) {
|
|
535
|
+
return ({ open, source, onClose }) => open ? /* @__PURE__ */ jsx(HelpPanel, {
|
|
536
|
+
...config,
|
|
537
|
+
source: config.source ?? source ?? "",
|
|
538
|
+
open: true,
|
|
539
|
+
onClose
|
|
540
|
+
}) : null;
|
|
541
|
+
}
|
|
542
|
+
//#endregion
|
|
543
|
+
export { HelpHotkeys, HelpPanel, helpPanelSlot, httpDocumentUrl, parseHelpDocument, resolveHelpTarget, useHelpHotkeys };
|
|
544
|
+
|
|
545
|
+
//# sourceMappingURL=help-panel.js.map
|