@yaakdoc/react 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +232 -2
- package/dist/client.d.ts +9 -0
- package/dist/client.js +26 -0
- package/dist/color-mode-B-vJCVkP.js +77 -0
- package/dist/index-CoQ7pnuB.d.ts +471 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +551 -0
- package/dist/island-BWyuL4_6.d.ts +29 -0
- package/dist/islands.d.ts +170 -0
- package/dist/islands.js +152 -0
- package/dist/llm-Ddn5hWnS.js +205 -0
- package/dist/run-in-yaak-CMMcDS7S.js +164 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.js +139 -0
- package/dist/site-DyK7ZcqQ.js +371 -0
- package/dist/theme.d.ts +45 -0
- package/dist/theme.js +7 -0
- package/dist/types-BY2lOvlK.d.ts +101 -0
- package/package.json +85 -3
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { n as createIsland, t as IslandComponent } from "./island-BWyuL4_6.js";
|
|
2
|
+
import { i as EnvironmentOption, n as ColorMode, r as ColorModePreference, s as LlmProvider } from "./types-BY2lOvlK.js";
|
|
3
|
+
import { $ as useTabs, J as TabList, Q as tabPanelProps, X as tabElementIds, Y as TabListProps, Z as tabGroupIds, et as visibleTabPanelSelector, q as TAB_STORAGE_KEY } from "./index-CoQ7pnuB.js";
|
|
4
|
+
//#region src/islands/browser.d.ts
|
|
5
|
+
/** Browser helpers shared by the islands. */
|
|
6
|
+
/** `localStorage` access that never throws (private mode, blocked storage...). */
|
|
7
|
+
export declare const storage: {
|
|
8
|
+
get(key: string): string | null;
|
|
9
|
+
set(key: string, value: string): void;
|
|
10
|
+
};
|
|
11
|
+
/** Resolves a (possibly relative) URL against the current page. */
|
|
12
|
+
export declare const resolveUrl: (url: string) => string;
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/islands/color-mode.d.ts
|
|
15
|
+
export declare const COLOR_MODE_STORAGE_KEY = "yaakdoc-color-mode";
|
|
16
|
+
/** Applies a preference to <html data-theme> and persists it. */
|
|
17
|
+
export declare function applyColorMode(preference: ColorModePreference): void;
|
|
18
|
+
/**
|
|
19
|
+
* Headless color mode state. Hydration-safe: renders `fallback` on the server and during hydration,
|
|
20
|
+
* then switches to the real value.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useColorMode(fallback?: ColorModePreference): {
|
|
23
|
+
preference: "dark" | "light" | "system";
|
|
24
|
+
resolved: ColorMode;
|
|
25
|
+
setPreference: (next: ColorModePreference) => void;
|
|
26
|
+
toggle: () => void;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Inline script for <head> that applies the stored/system color mode before first paint, and keeps
|
|
30
|
+
* following the system setting while the preference is "system".
|
|
31
|
+
*/
|
|
32
|
+
export declare function colorModeScript(defaultMode: ColorModePreference): string;
|
|
33
|
+
type ColorModeToggleProps = {
|
|
34
|
+
className?: string;
|
|
35
|
+
label?: string;
|
|
36
|
+
};
|
|
37
|
+
/** Unstyled light/dark toggle. Both icons are rendered; show one via `[data-theme]` CSS. */
|
|
38
|
+
export declare const ColorModeToggle: IslandComponent<ColorModeToggleProps>;
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/islands/copy.d.ts
|
|
41
|
+
type CopySource = {
|
|
42
|
+
/** Literal text to copy. */
|
|
43
|
+
text?: string;
|
|
44
|
+
/** CSS selector of an element whose text content is copied. */
|
|
45
|
+
target?: string;
|
|
46
|
+
/** URL whose response body is copied (e.g. a page's Markdown twin). */
|
|
47
|
+
url?: string;
|
|
48
|
+
};
|
|
49
|
+
/** Headless copy-to-clipboard state. `copied` resets after `timeout` ms. */
|
|
50
|
+
export declare function useCopy(timeout?: number): {
|
|
51
|
+
state: "copied" | "error" | "idle";
|
|
52
|
+
copied: boolean;
|
|
53
|
+
copy: (source: CopySource) => Promise<void>;
|
|
54
|
+
};
|
|
55
|
+
type CopyButtonProps = CopySource & {
|
|
56
|
+
label?: string;
|
|
57
|
+
copiedLabel?: string;
|
|
58
|
+
className?: string;
|
|
59
|
+
};
|
|
60
|
+
/** Unstyled copy button. Copies `text`, the text of `target`, or the body of `url`. */
|
|
61
|
+
export declare const CopyButton: IslandComponent<CopyButtonProps>;
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/islands/environment.d.ts
|
|
64
|
+
export declare const ENVIRONMENT_STORAGE_KEY = "yaakdoc-environment";
|
|
65
|
+
/**
|
|
66
|
+
* Shows the content variants of one environment. Variants are elements with
|
|
67
|
+
* `data-yd-env="<environment id>"`; `<html data-environment>` drives the CSS (see
|
|
68
|
+
* {@link environmentCss}) and the `hidden` attributes are kept in sync for accessibility.
|
|
69
|
+
*/
|
|
70
|
+
export declare function applyEnvironment(id: string): void;
|
|
71
|
+
/** Headless environment switcher state, synced across switchers and remembered. */
|
|
72
|
+
export declare function useEnvironment(environments: readonly EnvironmentOption[], defaultId: string): {
|
|
73
|
+
selected: string;
|
|
74
|
+
select: (id: string) => void;
|
|
75
|
+
current: import("@yaakdoc/core").EnvironmentRef | undefined;
|
|
76
|
+
};
|
|
77
|
+
/** Inline <head> script: applies the remembered environment before first paint. */
|
|
78
|
+
export declare function environmentScript(ids: readonly string[]): string;
|
|
79
|
+
/** CSS that shows the variants of the environment set on <html> (falls back to `hidden`). */
|
|
80
|
+
export declare function environmentCss(ids: readonly string[]): string;
|
|
81
|
+
type EnvironmentSwitcherProps = {
|
|
82
|
+
environments: EnvironmentOption[];
|
|
83
|
+
defaultId: string;
|
|
84
|
+
className?: string;
|
|
85
|
+
label?: string;
|
|
86
|
+
};
|
|
87
|
+
/** Unstyled environment switcher (native select). */
|
|
88
|
+
export declare const EnvironmentSwitcher: IslandComponent<EnvironmentSwitcherProps>;
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/islands/llm.d.ts
|
|
91
|
+
export declare const LLM_PROVIDERS: Record<LlmProvider, {
|
|
92
|
+
label: string;
|
|
93
|
+
url: (prompt: string) => string;
|
|
94
|
+
}>;
|
|
95
|
+
export declare const llmPrompt: (markdownUrl: string) => string;
|
|
96
|
+
/** URL that opens an assistant with a prompt pointing at the (absolute) Markdown URL. */
|
|
97
|
+
export declare function openInUrl(provider: LlmProvider, markdownUrl: string): string;
|
|
98
|
+
type OpenInLinkProps = {
|
|
99
|
+
provider: LlmProvider;
|
|
100
|
+
/** Markdown URL; relative URLs are made absolute on click using the current location. */
|
|
101
|
+
markdownUrl: string;
|
|
102
|
+
className?: string;
|
|
103
|
+
label?: string;
|
|
104
|
+
};
|
|
105
|
+
/** Link that opens ChatGPT/Claude/... with the page's Markdown as context. */
|
|
106
|
+
export declare const OpenInLink: IslandComponent<OpenInLinkProps>;
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/islands/nav.d.ts
|
|
109
|
+
/** Headless open/close state for a navigation element (`data-open` is set on the target). */
|
|
110
|
+
export declare function useNavToggle(target: string): {
|
|
111
|
+
open: boolean;
|
|
112
|
+
setOpen: (next: boolean) => void;
|
|
113
|
+
toggle: () => void;
|
|
114
|
+
};
|
|
115
|
+
type NavToggleProps = {
|
|
116
|
+
target: string;
|
|
117
|
+
label?: string;
|
|
118
|
+
className?: string;
|
|
119
|
+
};
|
|
120
|
+
/** Unstyled menu button for small screens. */
|
|
121
|
+
export declare const NavToggle: IslandComponent<NavToggleProps>;
|
|
122
|
+
/**
|
|
123
|
+
* Filters `[data-yd-nav-item]` elements inside `target` by their text. Folders
|
|
124
|
+
* (`[data-yd-nav-folder]`) without visible matches are hidden; matching folders are expanded.
|
|
125
|
+
*/
|
|
126
|
+
export declare function filterNav(target: string, query: string): number;
|
|
127
|
+
type NavFilterProps = {
|
|
128
|
+
target: string;
|
|
129
|
+
placeholder?: string;
|
|
130
|
+
className?: string;
|
|
131
|
+
shortcut?: string;
|
|
132
|
+
};
|
|
133
|
+
/** Unstyled filter input for the navigation. Press `/` to focus. */
|
|
134
|
+
export declare const NavFilter: IslandComponent<NavFilterProps>;
|
|
135
|
+
/**
|
|
136
|
+
* Highlights the navigation link of the section currently in view (single-page mode). Sections are
|
|
137
|
+
* elements with `data-yd-section`; links are `a[href="#<id>"]` inside `target`.
|
|
138
|
+
*/
|
|
139
|
+
export declare function useScrollSpy(target: string): void;
|
|
140
|
+
/** Renders nothing; marks the active nav link while scrolling. */
|
|
141
|
+
export declare const ScrollSpy: IslandComponent<{
|
|
142
|
+
target: string;
|
|
143
|
+
}>;
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/islands/run-in-yaak.d.ts
|
|
146
|
+
/** Official "Run in Yaak" badge image. */
|
|
147
|
+
export declare const RUN_IN_YAAK_BADGE = "https://yaak.app/static/button.svg";
|
|
148
|
+
/** Link that opens Yaak and prompts to import the export/spec at `dataUrl` (must be absolute). */
|
|
149
|
+
export declare function runInYaakUrl(name: string, dataUrl: string): string;
|
|
150
|
+
/**
|
|
151
|
+
* "Run in Yaak" link for an export URL. Relative URLs (sites without a configured public URL) are
|
|
152
|
+
* made absolute on the client, after hydration.
|
|
153
|
+
*/
|
|
154
|
+
export declare function useRunInYaakHref(name: string, dataUrl: string): string;
|
|
155
|
+
type RunInYaakLinkProps = {
|
|
156
|
+
name: string;
|
|
157
|
+
dataUrl: string;
|
|
158
|
+
/** Render the official badge image instead of text. */
|
|
159
|
+
badge?: boolean;
|
|
160
|
+
label?: string;
|
|
161
|
+
className?: string;
|
|
162
|
+
};
|
|
163
|
+
/** Unstyled "Run in Yaak" link (or the official badge with `badge`). */
|
|
164
|
+
export declare const RunInYaakLink: IslandComponent<RunInYaakLinkProps>;
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/islands/index.d.ts
|
|
167
|
+
/** All built-in islands. Pass to `hydrateIslands()` (alongside your own) in a theme's client entry. */
|
|
168
|
+
export declare const builtinIslands: ReadonlyArray<IslandComponent<any>>;
|
|
169
|
+
//#endregion
|
|
170
|
+
export { type ColorMode, type ColorModePreference, type ColorModeToggleProps, type CopyButtonProps, type CopySource, type EnvironmentOption, type EnvironmentSwitcherProps, type IslandComponent, type LlmProvider, type NavFilterProps, type NavToggleProps, type OpenInLinkProps, type RunInYaakLinkProps, TAB_STORAGE_KEY, TabList, type TabListProps, createIsland, tabElementIds, tabGroupIds, tabPanelProps, useTabs, visibleTabPanelSelector };
|
package/dist/islands.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { a as ENVIRONMENT_STORAGE_KEY, c as environmentCss, d as resolveUrl, f as storage, i as useRunInYaakHref, l as environmentScript, m as createIsland, n as RunInYaakLink, o as EnvironmentSwitcher, r as runInYaakUrl, s as applyEnvironment, t as RUN_IN_YAAK_BADGE, u as useEnvironment } from "./run-in-yaak-CMMcDS7S.js";
|
|
2
|
+
import { a as CopyButton, c as TabList, d as tabPanelProps, f as useTabs, i as openInUrl, l as tabElementIds, n as OpenInLink, o as useCopy, p as visibleTabPanelSelector, r as llmPrompt, s as TAB_STORAGE_KEY, t as LLM_PROVIDERS, u as tabGroupIds } from "./llm-Ddn5hWnS.js";
|
|
3
|
+
import { a as useColorMode, i as colorModeScript, n as ColorModeToggle, r as applyColorMode, t as COLOR_MODE_STORAGE_KEY } from "./color-mode-B-vJCVkP.js";
|
|
4
|
+
import { useCallback, useEffect, useState } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
//#region src/islands/nav.tsx
|
|
7
|
+
/** Headless open/close state for a navigation element (`data-open` is set on the target). */
|
|
8
|
+
function useNavToggle(target) {
|
|
9
|
+
const [open, setOpen] = useState(false);
|
|
10
|
+
const set = useCallback((next) => {
|
|
11
|
+
setOpen(next);
|
|
12
|
+
const element = document.querySelector(target);
|
|
13
|
+
if (element) element.dataset.open = String(next);
|
|
14
|
+
document.documentElement.dataset.navOpen = String(next);
|
|
15
|
+
}, [target]);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const element = document.querySelector(target);
|
|
18
|
+
const onKey = (event) => event.key === "Escape" && set(false);
|
|
19
|
+
const onClick = (event) => {
|
|
20
|
+
if (event.target.closest("a")) set(false);
|
|
21
|
+
};
|
|
22
|
+
window.addEventListener("keydown", onKey);
|
|
23
|
+
element?.addEventListener("click", onClick);
|
|
24
|
+
return () => {
|
|
25
|
+
window.removeEventListener("keydown", onKey);
|
|
26
|
+
element?.removeEventListener("click", onClick);
|
|
27
|
+
};
|
|
28
|
+
}, [target, set]);
|
|
29
|
+
return {
|
|
30
|
+
open,
|
|
31
|
+
setOpen: set,
|
|
32
|
+
toggle: () => set(!open)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function NavToggleImpl({ target, label = "Toggle navigation", className }) {
|
|
36
|
+
const { open, toggle } = useNavToggle(target);
|
|
37
|
+
return /* @__PURE__ */ jsx("button", {
|
|
38
|
+
type: "button",
|
|
39
|
+
"data-yd": "nav-toggle",
|
|
40
|
+
"data-state": open ? "open" : "closed",
|
|
41
|
+
"aria-expanded": open,
|
|
42
|
+
"aria-label": label,
|
|
43
|
+
className,
|
|
44
|
+
onClick: toggle,
|
|
45
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
46
|
+
"aria-hidden": "true",
|
|
47
|
+
children: "☰"
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Unstyled menu button for small screens. */
|
|
52
|
+
const NavToggle = createIsland("yd:nav-toggle", NavToggleImpl);
|
|
53
|
+
/**
|
|
54
|
+
* Filters `[data-yd-nav-item]` elements inside `target` by their text. Folders
|
|
55
|
+
* (`[data-yd-nav-folder]`) without visible matches are hidden; matching folders are expanded.
|
|
56
|
+
*/
|
|
57
|
+
function filterNav(target, query) {
|
|
58
|
+
const root = document.querySelector(target);
|
|
59
|
+
if (!root) return 0;
|
|
60
|
+
const needle = query.trim().toLowerCase();
|
|
61
|
+
let visible = 0;
|
|
62
|
+
for (const item of root.querySelectorAll("[data-yd-nav-item]")) {
|
|
63
|
+
const text = (item.dataset.ydNavLabel ?? item.textContent ?? "").toLowerCase();
|
|
64
|
+
const match = !needle || text.includes(needle);
|
|
65
|
+
item.hidden = !match;
|
|
66
|
+
if (match) visible++;
|
|
67
|
+
}
|
|
68
|
+
const folders = [...root.querySelectorAll("[data-yd-nav-folder]")].reverse();
|
|
69
|
+
for (const folder of folders) {
|
|
70
|
+
const label = (folder.dataset.ydNavLabel ?? "").toLowerCase();
|
|
71
|
+
const hasMatch = !needle || label.includes(needle) || folder.querySelector("[data-yd-nav-item]:not([hidden])") !== null;
|
|
72
|
+
folder.hidden = !hasMatch;
|
|
73
|
+
if (needle && hasMatch) {
|
|
74
|
+
folder.querySelector("details")?.setAttribute("open", "");
|
|
75
|
+
if (label.includes(needle)) for (const child of folder.querySelectorAll("[hidden]")) child.hidden = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return visible;
|
|
79
|
+
}
|
|
80
|
+
function NavFilterImpl({ target, placeholder = "Filter...", className, shortcut = "/" }) {
|
|
81
|
+
const [query, setQuery] = useState("");
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const onKey = (event) => {
|
|
84
|
+
const active = document.activeElement;
|
|
85
|
+
if (event.key !== shortcut || active?.matches("input, textarea, [contenteditable]")) return;
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
document.querySelector("[data-yd=\"nav-filter\"]")?.focus();
|
|
88
|
+
};
|
|
89
|
+
window.addEventListener("keydown", onKey);
|
|
90
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
91
|
+
}, [shortcut]);
|
|
92
|
+
return /* @__PURE__ */ jsx("input", {
|
|
93
|
+
type: "search",
|
|
94
|
+
"data-yd": "nav-filter",
|
|
95
|
+
className,
|
|
96
|
+
placeholder,
|
|
97
|
+
"aria-label": placeholder,
|
|
98
|
+
value: query,
|
|
99
|
+
onChange: (event) => {
|
|
100
|
+
setQuery(event.target.value);
|
|
101
|
+
filterNav(target, event.target.value);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** Unstyled filter input for the navigation. Press `/` to focus. */
|
|
106
|
+
const NavFilter = createIsland("yd:nav-filter", NavFilterImpl);
|
|
107
|
+
/**
|
|
108
|
+
* Highlights the navigation link of the section currently in view (single-page mode). Sections are
|
|
109
|
+
* elements with `data-yd-section`; links are `a[href="#<id>"]` inside `target`.
|
|
110
|
+
*/
|
|
111
|
+
function useScrollSpy(target) {
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const nav = document.querySelector(target);
|
|
114
|
+
const sections = document.querySelectorAll("[data-yd-section]");
|
|
115
|
+
if (!nav || sections.length === 0 || typeof IntersectionObserver === "undefined") return;
|
|
116
|
+
let current = null;
|
|
117
|
+
const observer = new IntersectionObserver((entries) => {
|
|
118
|
+
const top = entries.filter((entry) => entry.isIntersecting).sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)[0];
|
|
119
|
+
if (!top) return;
|
|
120
|
+
const link = nav.querySelector(`a[href$="#${CSS.escape(top.target.id)}"]`);
|
|
121
|
+
if (!link || link === current) return;
|
|
122
|
+
current?.removeAttribute("aria-current");
|
|
123
|
+
link.setAttribute("aria-current", "location");
|
|
124
|
+
current = link;
|
|
125
|
+
for (let el = link.parentElement; el && el !== nav; el = el.parentElement) if (el instanceof HTMLDetailsElement && !el.open) el.open = true;
|
|
126
|
+
}, { rootMargin: "-10% 0px -70% 0px" });
|
|
127
|
+
for (const section of sections) observer.observe(section);
|
|
128
|
+
return () => observer.disconnect();
|
|
129
|
+
}, [target]);
|
|
130
|
+
}
|
|
131
|
+
function ScrollSpyImpl({ target }) {
|
|
132
|
+
useScrollSpy(target);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
/** Renders nothing; marks the active nav link while scrolling. */
|
|
136
|
+
const ScrollSpy = createIsland("yd:scroll-spy", ScrollSpyImpl);
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/islands/index.ts
|
|
139
|
+
/** All built-in islands. Pass to `hydrateIslands()` (alongside your own) in a theme's client entry. */
|
|
140
|
+
const builtinIslands = [
|
|
141
|
+
ColorModeToggle,
|
|
142
|
+
CopyButton,
|
|
143
|
+
EnvironmentSwitcher,
|
|
144
|
+
TabList,
|
|
145
|
+
NavToggle,
|
|
146
|
+
NavFilter,
|
|
147
|
+
ScrollSpy,
|
|
148
|
+
OpenInLink,
|
|
149
|
+
RunInYaakLink
|
|
150
|
+
];
|
|
151
|
+
//#endregion
|
|
152
|
+
export { COLOR_MODE_STORAGE_KEY, ColorModeToggle, CopyButton, ENVIRONMENT_STORAGE_KEY, EnvironmentSwitcher, LLM_PROVIDERS, NavFilter, NavToggle, OpenInLink, RUN_IN_YAAK_BADGE, RunInYaakLink, ScrollSpy, TAB_STORAGE_KEY, TabList, applyColorMode, applyEnvironment, builtinIslands, colorModeScript, createIsland, environmentCss, environmentScript, filterNav, llmPrompt, openInUrl, resolveUrl, runInYaakUrl, storage, tabElementIds, tabGroupIds, tabPanelProps, useColorMode, useCopy, useEnvironment, useNavToggle, useRunInYaakHref, useScrollSpy, useTabs, visibleTabPanelSelector };
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { d as resolveUrl, f as storage, m as createIsland } from "./run-in-yaak-CMMcDS7S.js";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
//#region src/islands/tabs.tsx
|
|
5
|
+
const TAB_STORAGE_KEY = "yaakdoc-tab";
|
|
6
|
+
/** Tab group names of a request's code examples and example responses. */
|
|
7
|
+
const tabGroupIds = {
|
|
8
|
+
examples: (anchor) => `ex-${anchor}`,
|
|
9
|
+
responses: (anchor) => `res-${anchor}`
|
|
10
|
+
};
|
|
11
|
+
/** Ids linking a tab to its panel. `variant` tells apart copies of a panel (one per environment). */
|
|
12
|
+
function tabElementIds(group, id, variant = 0) {
|
|
13
|
+
return {
|
|
14
|
+
tab: `${group}-tab-${id}`,
|
|
15
|
+
panel: `${group}-panel-${id}${variant ? `-${variant}` : ""}`
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Props for a server-rendered panel controlled by {@link TabList}. */
|
|
19
|
+
function tabPanelProps(group, id, options) {
|
|
20
|
+
const ids = tabElementIds(group, id, options.variant);
|
|
21
|
+
return {
|
|
22
|
+
role: "tabpanel",
|
|
23
|
+
id: ids.panel,
|
|
24
|
+
"aria-labelledby": ids.tab,
|
|
25
|
+
"data-yd-tab-panel": id,
|
|
26
|
+
hidden: !options.selected
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Selector of an element (default: the code) inside the visible panel of a tab group. */
|
|
30
|
+
const visibleTabPanelSelector = (group, inner = "pre") => `[data-yd-tab-group="${group}"] [data-yd-tab-panel]:not([hidden]) ${inner}`;
|
|
31
|
+
const TAB_EVENT = "yaakdoc:tab";
|
|
32
|
+
function showPanel(group, id) {
|
|
33
|
+
const container = document.querySelector(`[data-yd-tab-group="${CSS.escape(group)}"]`);
|
|
34
|
+
if (!container) return;
|
|
35
|
+
for (const panel of container.querySelectorAll("[data-yd-tab-panel]")) panel.hidden = panel.dataset.ydTabPanel !== id;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Headless tab state for a panel group rendered on the server. Panels are elements with
|
|
39
|
+
* `data-yd-tab-panel="<id>"` inside `[data-yd-tab-group="<group>"]`. The selection is synced across
|
|
40
|
+
* all groups on the page (pick "Python" once, see Python everywhere) and remembered.
|
|
41
|
+
*/
|
|
42
|
+
function useTabs(group, ids, sync = true) {
|
|
43
|
+
const [selected, setSelected] = useState(ids[0] ?? "");
|
|
44
|
+
const select = useCallback((id, broadcast = true) => {
|
|
45
|
+
if (!ids.includes(id)) return;
|
|
46
|
+
setSelected(id);
|
|
47
|
+
showPanel(group, id);
|
|
48
|
+
if (sync && broadcast) {
|
|
49
|
+
storage.set(TAB_STORAGE_KEY, id);
|
|
50
|
+
window.dispatchEvent(new CustomEvent(TAB_EVENT, { detail: {
|
|
51
|
+
id,
|
|
52
|
+
group
|
|
53
|
+
} }));
|
|
54
|
+
}
|
|
55
|
+
}, [
|
|
56
|
+
group,
|
|
57
|
+
ids,
|
|
58
|
+
sync
|
|
59
|
+
]);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!sync) return;
|
|
62
|
+
const stored = storage.get(TAB_STORAGE_KEY);
|
|
63
|
+
if (stored) select(stored, false);
|
|
64
|
+
const onTab = (event) => {
|
|
65
|
+
const detail = event.detail;
|
|
66
|
+
if (detail.group !== group) select(detail.id, false);
|
|
67
|
+
};
|
|
68
|
+
window.addEventListener(TAB_EVENT, onTab);
|
|
69
|
+
return () => window.removeEventListener(TAB_EVENT, onTab);
|
|
70
|
+
}, [
|
|
71
|
+
group,
|
|
72
|
+
select,
|
|
73
|
+
sync
|
|
74
|
+
]);
|
|
75
|
+
return {
|
|
76
|
+
selected,
|
|
77
|
+
select
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function TabListImpl({ group, tabs, sync = true, className, tabClassName, label = "Code examples" }) {
|
|
81
|
+
const { selected, select } = useTabs(group, tabs.map((tab) => tab.id), sync);
|
|
82
|
+
const onKeyDown = (event, index) => {
|
|
83
|
+
const delta = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
|
|
84
|
+
if (!delta) return;
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
const next = tabs[(index + delta + tabs.length) % tabs.length];
|
|
87
|
+
select(next.id);
|
|
88
|
+
(event.currentTarget.parentElement?.children[tabs.indexOf(next)])?.focus();
|
|
89
|
+
};
|
|
90
|
+
return /* @__PURE__ */ jsx("div", {
|
|
91
|
+
role: "tablist",
|
|
92
|
+
"aria-label": label,
|
|
93
|
+
"data-yd": "tab-list",
|
|
94
|
+
className,
|
|
95
|
+
children: tabs.map((tab, index) => /* @__PURE__ */ jsx("button", {
|
|
96
|
+
type: "button",
|
|
97
|
+
role: "tab",
|
|
98
|
+
id: tabElementIds(group, tab.id).tab,
|
|
99
|
+
"aria-selected": selected === tab.id,
|
|
100
|
+
"aria-controls": tabElementIds(group, tab.id).panel,
|
|
101
|
+
tabIndex: selected === tab.id ? 0 : -1,
|
|
102
|
+
"data-state": selected === tab.id ? "active" : "inactive",
|
|
103
|
+
className: [tabClassName, tab.className].filter(Boolean).join(" ") || void 0,
|
|
104
|
+
onClick: () => select(tab.id),
|
|
105
|
+
onKeyDown: (event) => onKeyDown(event, index),
|
|
106
|
+
children: tab.label
|
|
107
|
+
}, tab.id))
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** Accessible tab list controlling server-rendered panels (see {@link useTabs}). */
|
|
111
|
+
const TabList = createIsland("yd:tab-list", TabListImpl);
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/islands/copy.tsx
|
|
114
|
+
async function resolveCopyText(source) {
|
|
115
|
+
if (source.text !== void 0) return source.text;
|
|
116
|
+
if (source.url) {
|
|
117
|
+
const response = await fetch(source.url);
|
|
118
|
+
if (!response.ok) throw new Error(`Failed to fetch ${source.url}: ${response.status}`);
|
|
119
|
+
return response.text();
|
|
120
|
+
}
|
|
121
|
+
if (source.target) {
|
|
122
|
+
const matches = [...document.querySelectorAll(source.target)];
|
|
123
|
+
return (matches.find((el) => el.getClientRects().length > 0) ?? matches[0])?.textContent ?? "";
|
|
124
|
+
}
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
127
|
+
/** Headless copy-to-clipboard state. `copied` resets after `timeout` ms. */
|
|
128
|
+
function useCopy(timeout = 2e3) {
|
|
129
|
+
const [state, setState] = useState("idle");
|
|
130
|
+
const timer = useRef(void 0);
|
|
131
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
132
|
+
const copy = useCallback(async (source) => {
|
|
133
|
+
try {
|
|
134
|
+
if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) await navigator.clipboard.write([new ClipboardItem({ "text/plain": resolveCopyText(source).then((text) => new Blob([text], { type: "text/plain" })) })]);
|
|
135
|
+
else await navigator.clipboard.writeText(await resolveCopyText(source));
|
|
136
|
+
setState("copied");
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error("[yaakdoc] Copy failed", error);
|
|
139
|
+
setState("error");
|
|
140
|
+
}
|
|
141
|
+
clearTimeout(timer.current);
|
|
142
|
+
timer.current = setTimeout(() => setState("idle"), timeout);
|
|
143
|
+
}, [timeout]);
|
|
144
|
+
return {
|
|
145
|
+
state,
|
|
146
|
+
copied: state === "copied",
|
|
147
|
+
copy
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function CopyButtonImpl({ label = "Copy", copiedLabel = "Copied!", className, ...source }) {
|
|
151
|
+
const { state, copy } = useCopy();
|
|
152
|
+
return /* @__PURE__ */ jsx("button", {
|
|
153
|
+
type: "button",
|
|
154
|
+
"data-yd": "copy-button",
|
|
155
|
+
"data-state": state,
|
|
156
|
+
className,
|
|
157
|
+
onClick: () => copy(source),
|
|
158
|
+
"aria-live": "polite",
|
|
159
|
+
children: state === "copied" ? copiedLabel : label
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/** Unstyled copy button. Copies `text`, the text of `target`, or the body of `url`. */
|
|
163
|
+
const CopyButton = createIsland("yd:copy-button", CopyButtonImpl);
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/islands/llm.tsx
|
|
166
|
+
const LLM_PROVIDERS = {
|
|
167
|
+
chatgpt: {
|
|
168
|
+
label: "ChatGPT",
|
|
169
|
+
url: (q) => `https://chatgpt.com/?hints=search&q=${encodeURIComponent(q)}`
|
|
170
|
+
},
|
|
171
|
+
claude: {
|
|
172
|
+
label: "Claude",
|
|
173
|
+
url: (q) => `https://claude.ai/new?q=${encodeURIComponent(q)}`
|
|
174
|
+
},
|
|
175
|
+
perplexity: {
|
|
176
|
+
label: "Perplexity",
|
|
177
|
+
url: (q) => `https://www.perplexity.ai/search?q=${encodeURIComponent(q)}`
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const llmPrompt = (markdownUrl) => `Read ${markdownUrl} so I can ask questions about this API documentation.`;
|
|
181
|
+
/** URL that opens an assistant with a prompt pointing at the (absolute) Markdown URL. */
|
|
182
|
+
function openInUrl(provider, markdownUrl) {
|
|
183
|
+
return LLM_PROVIDERS[provider].url(llmPrompt(markdownUrl));
|
|
184
|
+
}
|
|
185
|
+
function OpenInLinkImpl({ provider, markdownUrl, className, label }) {
|
|
186
|
+
return /* @__PURE__ */ jsx("a", {
|
|
187
|
+
"data-yd": "open-in",
|
|
188
|
+
"data-provider": provider,
|
|
189
|
+
className,
|
|
190
|
+
href: openInUrl(provider, markdownUrl),
|
|
191
|
+
target: "_blank",
|
|
192
|
+
rel: "noopener noreferrer",
|
|
193
|
+
onClick: (event) => {
|
|
194
|
+
const absolute = resolveUrl(markdownUrl);
|
|
195
|
+
if (absolute === markdownUrl) return;
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
window.open(openInUrl(provider, absolute), "_blank", "noopener,noreferrer");
|
|
198
|
+
},
|
|
199
|
+
children: label ?? `Open in ${LLM_PROVIDERS[provider].label}`
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/** Link that opens ChatGPT/Claude/... with the page's Markdown as context. */
|
|
203
|
+
const OpenInLink = createIsland("yd:open-in", OpenInLinkImpl);
|
|
204
|
+
//#endregion
|
|
205
|
+
export { CopyButton as a, TabList as c, tabPanelProps as d, useTabs as f, openInUrl as i, tabElementIds as l, OpenInLink as n, useCopy as o, visibleTabPanelSelector as p, llmPrompt as r, TAB_STORAGE_KEY as s, LLM_PROVIDERS as t, tabGroupIds as u };
|