idcmd 0.0.4 → 0.0.6
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 +23 -4
- package/package.json +2 -2
- package/src/build.ts +4 -3
- package/src/cli/commands/build.ts +7 -0
- package/src/cli/commands/client.ts +317 -0
- package/src/cli/commands/dev.ts +89 -24
- package/src/cli/commands/init.ts +93 -2
- package/src/cli/main.ts +12 -0
- package/src/cli/runtime-assets.ts +92 -0
- package/src/client/index.ts +7 -1
- package/src/render/layout-loader.ts +6 -3
- package/src/render/layout.tsx +10 -2
- package/src/render/page-renderer.ts +12 -2
- package/src/render/right-rail-loader.ts +49 -0
- package/src/render/right-rail.tsx +10 -6
- package/src/search/page.tsx +4 -2
- package/src/search/search-page-loader.ts +51 -0
- package/src/search/server-page.ts +52 -18
- package/templates/default/.github/workflows/ci.yml +24 -0
- package/templates/default/README.md +23 -0
- package/templates/default/package.json +2 -1
- package/templates/default/scripts/check-internal.ts +56 -0
- package/templates/default/scripts/check.ts +318 -0
- package/templates/default/scripts/smoke.ts +193 -0
- package/templates/default/site/client/layout.tsx +237 -2
- package/templates/default/site/client/right-rail.tsx +246 -1
- package/templates/default/site/{public/_idcmd/llm-menu.js → client/runtime/llm-menu.ts} +27 -18
- package/templates/default/site/{public/_idcmd/nav-prefetch.js → client/runtime/nav-prefetch.ts} +3 -3
- package/templates/default/site/{public/_idcmd/right-rail-scrollspy.js → client/runtime/right-rail-scrollspy.ts} +73 -32
- package/templates/default/site/client/search-page.tsx +87 -1
- package/templates/default/tsconfig.json +1 -1
- /package/templates/default/site/{public/_idcmd/live-reload.js → client/runtime/live-reload.ts} +0 -0
|
@@ -1,2 +1,237 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type { LayoutProps } from "idcmd/client";
|
|
2
|
+
/* eslint-disable react/no-danger */
|
|
3
|
+
import type { JSX } from "preact";
|
|
4
|
+
|
|
5
|
+
import { render } from "preact-render-to-string";
|
|
6
|
+
|
|
7
|
+
import { RightRail } from "./right-rail";
|
|
8
|
+
|
|
9
|
+
type NavItem = LayoutProps["navigation"][number]["items"][number];
|
|
10
|
+
|
|
11
|
+
const Icon = ({ svg }: { svg: string }): JSX.Element => (
|
|
12
|
+
<span
|
|
13
|
+
class="inline-flex h-[18px] w-[18px]"
|
|
14
|
+
dangerouslySetInnerHTML={{ __html: svg }}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const isActiveLink = (item: NavItem, currentPath: string): boolean =>
|
|
19
|
+
currentPath === item.href ||
|
|
20
|
+
(item.href !== "/" && currentPath.startsWith(item.href));
|
|
21
|
+
|
|
22
|
+
const Sidebar = ({
|
|
23
|
+
siteName,
|
|
24
|
+
navigation,
|
|
25
|
+
currentPath,
|
|
26
|
+
}: {
|
|
27
|
+
siteName: LayoutProps["siteName"];
|
|
28
|
+
navigation: LayoutProps["navigation"];
|
|
29
|
+
currentPath: LayoutProps["currentPath"];
|
|
30
|
+
}): JSX.Element => (
|
|
31
|
+
<aside class="sidebar">
|
|
32
|
+
<div class="sidebar-header">
|
|
33
|
+
<a
|
|
34
|
+
href="/"
|
|
35
|
+
class="text-sm font-medium tracking-tight"
|
|
36
|
+
data-prefetch="hover"
|
|
37
|
+
>
|
|
38
|
+
<span class="text-muted-foreground">~/</span>
|
|
39
|
+
{siteName}
|
|
40
|
+
</a>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="sidebar-content">
|
|
43
|
+
{navigation.map((group) => (
|
|
44
|
+
<div key={group.id} class="py-2">
|
|
45
|
+
<p class="px-3 py-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
46
|
+
{group.label}
|
|
47
|
+
</p>
|
|
48
|
+
<nav class="space-y-1">
|
|
49
|
+
{group.items.map((item) => (
|
|
50
|
+
<a
|
|
51
|
+
key={item.href}
|
|
52
|
+
href={item.href}
|
|
53
|
+
data-prefetch="hover"
|
|
54
|
+
class={`flex items-center gap-3 px-3 py-1.5 text-sm transition-colors hover:text-sidebar-foreground ${
|
|
55
|
+
isActiveLink(item, currentPath)
|
|
56
|
+
? "border-l-2 border-sidebar-primary font-medium text-sidebar-foreground"
|
|
57
|
+
: "border-l-2 border-transparent"
|
|
58
|
+
}`}
|
|
59
|
+
>
|
|
60
|
+
<Icon svg={item.iconSvg} />
|
|
61
|
+
<span>{item.title}</span>
|
|
62
|
+
</a>
|
|
63
|
+
))}
|
|
64
|
+
</nav>
|
|
65
|
+
</div>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
</aside>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const SearchForm = ({ query }: { query?: string }): JSX.Element => (
|
|
72
|
+
<form
|
|
73
|
+
method="get"
|
|
74
|
+
action="/search/"
|
|
75
|
+
class="flex w-full items-center"
|
|
76
|
+
role="search"
|
|
77
|
+
noValidate
|
|
78
|
+
>
|
|
79
|
+
<label htmlFor="site-search" class="sr-only">
|
|
80
|
+
Search pages
|
|
81
|
+
</label>
|
|
82
|
+
<input
|
|
83
|
+
id="site-search"
|
|
84
|
+
name="q"
|
|
85
|
+
type="search"
|
|
86
|
+
autoComplete="off"
|
|
87
|
+
spellcheck={false}
|
|
88
|
+
placeholder="Search..."
|
|
89
|
+
defaultValue={query ?? ""}
|
|
90
|
+
class="w-full border-b border-input bg-transparent px-1 py-1.5 text-sm placeholder:text-muted-foreground focus:border-foreground focus:outline-none transition-colors"
|
|
91
|
+
/>
|
|
92
|
+
</form>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const TopNavbar = ({
|
|
96
|
+
query,
|
|
97
|
+
siteName,
|
|
98
|
+
}: {
|
|
99
|
+
query?: LayoutProps["searchQuery"];
|
|
100
|
+
siteName: LayoutProps["siteName"];
|
|
101
|
+
}): JSX.Element => (
|
|
102
|
+
<header class="sticky top-0 z-30 border-b border-border bg-background/80 backdrop-blur-sm">
|
|
103
|
+
<div class="mx-auto max-w-6xl px-8 py-3">
|
|
104
|
+
<div class="flex items-center gap-4">
|
|
105
|
+
<a
|
|
106
|
+
href="/"
|
|
107
|
+
class="text-sm font-mono font-medium tracking-tight md:hidden"
|
|
108
|
+
data-prefetch="hover"
|
|
109
|
+
>
|
|
110
|
+
<span class="text-muted-foreground">~/</span>
|
|
111
|
+
{siteName}
|
|
112
|
+
</a>
|
|
113
|
+
<div class="not-prose ml-auto w-full max-w-xs">
|
|
114
|
+
<SearchForm query={query} />
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</header>
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const buildHtmlClass = (
|
|
122
|
+
smoothScroll: LayoutProps["rightRail"]["smoothScroll"]
|
|
123
|
+
): string => (smoothScroll ? "dark smooth-scroll" : "dark");
|
|
124
|
+
|
|
125
|
+
const buildScrollSpyDataset = (props: {
|
|
126
|
+
isScrollSpyEnabled: boolean;
|
|
127
|
+
rightRail: LayoutProps["rightRail"];
|
|
128
|
+
}): {
|
|
129
|
+
scrollspy?: string;
|
|
130
|
+
scrollspyCenter?: string;
|
|
131
|
+
scrollspyUpdateHash?: string;
|
|
132
|
+
} =>
|
|
133
|
+
props.isScrollSpyEnabled
|
|
134
|
+
? {
|
|
135
|
+
scrollspy: "1",
|
|
136
|
+
scrollspyCenter: props.rightRail.scrollSpy.centerActiveItem
|
|
137
|
+
? "1"
|
|
138
|
+
: undefined,
|
|
139
|
+
scrollspyUpdateHash: props.rightRail.scrollSpy.updateHash,
|
|
140
|
+
}
|
|
141
|
+
: {};
|
|
142
|
+
|
|
143
|
+
const Layout = ({
|
|
144
|
+
title,
|
|
145
|
+
siteName,
|
|
146
|
+
description,
|
|
147
|
+
canonicalUrl,
|
|
148
|
+
content,
|
|
149
|
+
cssPath,
|
|
150
|
+
inlineCss,
|
|
151
|
+
currentPath,
|
|
152
|
+
navigation,
|
|
153
|
+
scriptPaths = [],
|
|
154
|
+
searchQuery,
|
|
155
|
+
showRightRail = true,
|
|
156
|
+
rightRail,
|
|
157
|
+
tocItems,
|
|
158
|
+
}: LayoutProps): JSX.Element => {
|
|
159
|
+
const resolvedCssPath = inlineCss ? undefined : (cssPath ?? "/styles.css");
|
|
160
|
+
const shouldShowRightRail = showRightRail && rightRail.enabled;
|
|
161
|
+
const isScrollSpyEnabled =
|
|
162
|
+
shouldShowRightRail && rightRail.scrollSpy.enabled && tocItems.length > 0;
|
|
163
|
+
const scrollSpyDataset = buildScrollSpyDataset({
|
|
164
|
+
isScrollSpyEnabled,
|
|
165
|
+
rightRail,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<html lang="en" class={buildHtmlClass(rightRail.smoothScroll)}>
|
|
170
|
+
<head>
|
|
171
|
+
<meta charset="utf-8" />
|
|
172
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
173
|
+
<title>{title}</title>
|
|
174
|
+
{description ? <meta name="description" content={description} /> : null}
|
|
175
|
+
{canonicalUrl ? <link rel="canonical" href={canonicalUrl} /> : null}
|
|
176
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
177
|
+
<link
|
|
178
|
+
rel="preconnect"
|
|
179
|
+
href="https://fonts.gstatic.com"
|
|
180
|
+
crossOrigin="anonymous"
|
|
181
|
+
/>
|
|
182
|
+
<link
|
|
183
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
|
|
184
|
+
rel="stylesheet"
|
|
185
|
+
/>
|
|
186
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
187
|
+
{inlineCss ? <style>{inlineCss}</style> : null}
|
|
188
|
+
{resolvedCssPath ? (
|
|
189
|
+
<link rel="stylesheet" href={resolvedCssPath} />
|
|
190
|
+
) : null}
|
|
191
|
+
</head>
|
|
192
|
+
<body
|
|
193
|
+
class="bg-background font-sans text-foreground"
|
|
194
|
+
data-scrollspy={scrollSpyDataset.scrollspy}
|
|
195
|
+
data-scrollspy-center={scrollSpyDataset.scrollspyCenter}
|
|
196
|
+
data-scrollspy-update-hash={scrollSpyDataset.scrollspyUpdateHash}
|
|
197
|
+
>
|
|
198
|
+
<Sidebar
|
|
199
|
+
siteName={siteName}
|
|
200
|
+
navigation={navigation}
|
|
201
|
+
currentPath={currentPath}
|
|
202
|
+
/>
|
|
203
|
+
<div class="main-wrapper">
|
|
204
|
+
<TopNavbar query={searchQuery} siteName={siteName} />
|
|
205
|
+
<main class="main-content">
|
|
206
|
+
<div class="mx-auto flex w-full max-w-6xl items-start gap-10">
|
|
207
|
+
<article
|
|
208
|
+
class={`prose min-w-0 flex-1${
|
|
209
|
+
currentPath === "/" ? " prose-home" : ""
|
|
210
|
+
}`}
|
|
211
|
+
dangerouslySetInnerHTML={{ __html: content }}
|
|
212
|
+
/>
|
|
213
|
+
{shouldShowRightRail ? (
|
|
214
|
+
<RightRail
|
|
215
|
+
canonicalUrl={canonicalUrl}
|
|
216
|
+
currentPath={currentPath}
|
|
217
|
+
tocItems={tocItems}
|
|
218
|
+
rightRailConfig={rightRail}
|
|
219
|
+
/>
|
|
220
|
+
) : null}
|
|
221
|
+
</div>
|
|
222
|
+
</main>
|
|
223
|
+
<footer class="site-footer">
|
|
224
|
+
Built with Preact SSR + Tailwind | Zero JavaScript on
|
|
225
|
+
content pages
|
|
226
|
+
</footer>
|
|
227
|
+
</div>
|
|
228
|
+
{scriptPaths.map((scriptPath) => (
|
|
229
|
+
<script key={scriptPath} defer src={scriptPath} />
|
|
230
|
+
))}
|
|
231
|
+
</body>
|
|
232
|
+
</html>
|
|
233
|
+
);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export const renderLayout = (props: LayoutProps): string =>
|
|
237
|
+
`<!DOCTYPE html>${render(<Layout {...props} />)}`;
|
|
@@ -1 +1,246 @@
|
|
|
1
|
-
|
|
1
|
+
import type { RightRailProps } from "idcmd/client";
|
|
2
|
+
import type { JSX } from "preact";
|
|
3
|
+
|
|
4
|
+
const CaretDownIcon = (): JSX.Element => (
|
|
5
|
+
<svg
|
|
6
|
+
width="16"
|
|
7
|
+
height="16"
|
|
8
|
+
viewBox="0 0 24 24"
|
|
9
|
+
fill="none"
|
|
10
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
11
|
+
aria-hidden="true"
|
|
12
|
+
>
|
|
13
|
+
<path
|
|
14
|
+
d="M7 10l5 5 5-5"
|
|
15
|
+
stroke="currentColor"
|
|
16
|
+
stroke-width="2"
|
|
17
|
+
stroke-linecap="round"
|
|
18
|
+
stroke-linejoin="round"
|
|
19
|
+
/>
|
|
20
|
+
</svg>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const CopyIcon = (): JSX.Element => (
|
|
24
|
+
<svg
|
|
25
|
+
width="18"
|
|
26
|
+
height="18"
|
|
27
|
+
viewBox="0 0 24 24"
|
|
28
|
+
fill="none"
|
|
29
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
30
|
+
aria-hidden="true"
|
|
31
|
+
>
|
|
32
|
+
<path
|
|
33
|
+
d="M9 9h10v12H9V9z"
|
|
34
|
+
stroke="currentColor"
|
|
35
|
+
stroke-width="2"
|
|
36
|
+
stroke-linecap="round"
|
|
37
|
+
stroke-linejoin="round"
|
|
38
|
+
/>
|
|
39
|
+
<path
|
|
40
|
+
d="M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1"
|
|
41
|
+
stroke="currentColor"
|
|
42
|
+
stroke-width="2"
|
|
43
|
+
stroke-linecap="round"
|
|
44
|
+
stroke-linejoin="round"
|
|
45
|
+
/>
|
|
46
|
+
</svg>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const buildSlugFromCurrentPath = (currentPath: string): string => {
|
|
50
|
+
if (currentPath === "/") {
|
|
51
|
+
return "index";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const trimmed = currentPath.replaceAll(/^\/+|\/+$/g, "");
|
|
55
|
+
return trimmed || "index";
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const buildAskUrls = ({
|
|
59
|
+
canonicalUrl,
|
|
60
|
+
currentPath,
|
|
61
|
+
}: {
|
|
62
|
+
canonicalUrl?: string;
|
|
63
|
+
currentPath: string;
|
|
64
|
+
}): { chatgptUrl: string; claudeUrl: string; markdownPath: string } => {
|
|
65
|
+
const slug = buildSlugFromCurrentPath(currentPath);
|
|
66
|
+
const markdownPath = `/${slug}.md`;
|
|
67
|
+
const markdownUrl = canonicalUrl
|
|
68
|
+
? new URL(markdownPath, canonicalUrl).toString()
|
|
69
|
+
: markdownPath;
|
|
70
|
+
|
|
71
|
+
const llmsTxtUrl = canonicalUrl
|
|
72
|
+
? new URL("/llms.txt", canonicalUrl).toString()
|
|
73
|
+
: "/llms.txt";
|
|
74
|
+
|
|
75
|
+
const prompt = `Investigate this document and explain it to the user: ${markdownUrl}\ndirectory for further exploration: ${llmsTxtUrl}`;
|
|
76
|
+
|
|
77
|
+
const chatgpt = new URL("https://chatgpt.com/");
|
|
78
|
+
chatgpt.searchParams.set("prompt", prompt);
|
|
79
|
+
|
|
80
|
+
const claude = new URL("https://claude.ai/new");
|
|
81
|
+
claude.searchParams.set("q", prompt);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
chatgptUrl: chatgpt.toString(),
|
|
85
|
+
claudeUrl: claude.toString(),
|
|
86
|
+
markdownPath,
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const AskInDropdown = ({
|
|
91
|
+
claudeUrl,
|
|
92
|
+
chatgptUrl,
|
|
93
|
+
markdownPath,
|
|
94
|
+
}: {
|
|
95
|
+
claudeUrl: string;
|
|
96
|
+
chatgptUrl: string;
|
|
97
|
+
markdownPath: string;
|
|
98
|
+
}): JSX.Element => (
|
|
99
|
+
<details class="llm-menu relative">
|
|
100
|
+
<summary class="flex w-full cursor-pointer select-none items-center justify-between gap-3 rounded-full border border-white/20 bg-card/30 px-4 py-2 text-sm shadow-sm hover:border-white/30 hover:bg-card/40">
|
|
101
|
+
<span class="flex items-center gap-2">
|
|
102
|
+
<img
|
|
103
|
+
src="/openai-white.svg"
|
|
104
|
+
alt=""
|
|
105
|
+
width={18}
|
|
106
|
+
height={18}
|
|
107
|
+
class="shrink-0"
|
|
108
|
+
/>
|
|
109
|
+
<span>Ask in ChatGPT</span>
|
|
110
|
+
</span>
|
|
111
|
+
<span class="text-muted-foreground">
|
|
112
|
+
<CaretDownIcon />
|
|
113
|
+
</span>
|
|
114
|
+
</summary>
|
|
115
|
+
|
|
116
|
+
<div class="absolute left-0 right-0 z-50 mt-2 rounded-xl border border-border bg-popover p-1 shadow-sm">
|
|
117
|
+
<a
|
|
118
|
+
href={chatgptUrl}
|
|
119
|
+
target="_blank"
|
|
120
|
+
rel="noopener noreferrer"
|
|
121
|
+
class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm hover:bg-muted"
|
|
122
|
+
>
|
|
123
|
+
<img
|
|
124
|
+
src="/openai-white.svg"
|
|
125
|
+
alt=""
|
|
126
|
+
width={18}
|
|
127
|
+
height={18}
|
|
128
|
+
class="shrink-0"
|
|
129
|
+
/>
|
|
130
|
+
<span>Ask in ChatGPT</span>
|
|
131
|
+
</a>
|
|
132
|
+
<a
|
|
133
|
+
href={claudeUrl}
|
|
134
|
+
target="_blank"
|
|
135
|
+
rel="noopener noreferrer"
|
|
136
|
+
class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm hover:bg-muted"
|
|
137
|
+
>
|
|
138
|
+
<img
|
|
139
|
+
src="/anthropic-white.svg"
|
|
140
|
+
alt=""
|
|
141
|
+
width={18}
|
|
142
|
+
height={18}
|
|
143
|
+
class="shrink-0"
|
|
144
|
+
/>
|
|
145
|
+
<span>Ask in Claude</span>
|
|
146
|
+
</a>
|
|
147
|
+
<a
|
|
148
|
+
href={markdownPath}
|
|
149
|
+
target="_blank"
|
|
150
|
+
rel="noopener noreferrer"
|
|
151
|
+
data-copy-markdown="1"
|
|
152
|
+
class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm hover:bg-muted"
|
|
153
|
+
>
|
|
154
|
+
<span class="shrink-0 text-muted-foreground">
|
|
155
|
+
<CopyIcon />
|
|
156
|
+
</span>
|
|
157
|
+
<span data-copy-markdown-label="1">Copy Markdown to Clipboard</span>
|
|
158
|
+
</a>
|
|
159
|
+
</div>
|
|
160
|
+
</details>
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const OnThisPage = ({
|
|
164
|
+
items,
|
|
165
|
+
}: {
|
|
166
|
+
items: RightRailProps["tocItems"];
|
|
167
|
+
}): JSX.Element => (
|
|
168
|
+
<section class="flex min-h-0 flex-1 flex-col" data-toc-root="1">
|
|
169
|
+
<div class="px-0.5 pb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
170
|
+
On this page
|
|
171
|
+
</div>
|
|
172
|
+
<nav aria-label="Table of contents" class="min-h-0 flex flex-1 flex-col">
|
|
173
|
+
<div class="toc-scroll min-h-0 flex-1" data-toc-scroll-container="1">
|
|
174
|
+
<ul class="space-y-2 text-sm text-muted-foreground">
|
|
175
|
+
{items.map((item) => (
|
|
176
|
+
<li key={item.id} class={item.level >= 3 ? "pl-3" : ""}>
|
|
177
|
+
<a
|
|
178
|
+
href={`#${encodeURIComponent(item.id)}`}
|
|
179
|
+
class="hover:text-foreground"
|
|
180
|
+
data-toc-link="1"
|
|
181
|
+
>
|
|
182
|
+
{item.text}
|
|
183
|
+
</a>
|
|
184
|
+
</li>
|
|
185
|
+
))}
|
|
186
|
+
</ul>
|
|
187
|
+
</div>
|
|
188
|
+
</nav>
|
|
189
|
+
</section>
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const getVisibilityClass = (
|
|
193
|
+
visibleFrom: RightRailProps["rightRailConfig"]["visibleFrom"]
|
|
194
|
+
): string => {
|
|
195
|
+
switch (visibleFrom) {
|
|
196
|
+
case "always": {
|
|
197
|
+
return "block";
|
|
198
|
+
}
|
|
199
|
+
case "never": {
|
|
200
|
+
return "hidden";
|
|
201
|
+
}
|
|
202
|
+
case "md": {
|
|
203
|
+
return "hidden md:block";
|
|
204
|
+
}
|
|
205
|
+
case "lg": {
|
|
206
|
+
return "hidden lg:block";
|
|
207
|
+
}
|
|
208
|
+
default: {
|
|
209
|
+
return "hidden xl:block";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const getPanelClass = (
|
|
215
|
+
placement: RightRailProps["rightRailConfig"]["placement"]
|
|
216
|
+
): string =>
|
|
217
|
+
placement === "viewport"
|
|
218
|
+
? "fixed top-24 bottom-0 right-8 z-20 w-64 flex flex-col gap-6 min-h-0"
|
|
219
|
+
: "sticky top-24 h-[calc(100vh-6rem)] flex flex-col gap-6 min-h-0";
|
|
220
|
+
|
|
221
|
+
export const RightRail = ({
|
|
222
|
+
canonicalUrl,
|
|
223
|
+
currentPath,
|
|
224
|
+
tocItems,
|
|
225
|
+
rightRailConfig,
|
|
226
|
+
}: RightRailProps): JSX.Element => {
|
|
227
|
+
const { chatgptUrl, claudeUrl, markdownPath } = buildAskUrls({
|
|
228
|
+
canonicalUrl,
|
|
229
|
+
currentPath,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<aside
|
|
234
|
+
class={`${getVisibilityClass(rightRailConfig.visibleFrom)} w-64 shrink-0`}
|
|
235
|
+
>
|
|
236
|
+
<div class={getPanelClass(rightRailConfig.placement)}>
|
|
237
|
+
<AskInDropdown
|
|
238
|
+
chatgptUrl={chatgptUrl}
|
|
239
|
+
claudeUrl={claudeUrl}
|
|
240
|
+
markdownPath={markdownPath}
|
|
241
|
+
/>
|
|
242
|
+
{tocItems.length > 0 ? <OnThisPage items={tocItems} /> : null}
|
|
243
|
+
</div>
|
|
244
|
+
</aside>
|
|
245
|
+
);
|
|
246
|
+
};
|
|
@@ -2,7 +2,7 @@ const COPY_SELECTOR = 'a[data-copy-markdown="1"]';
|
|
|
2
2
|
const LABEL_SELECTOR = '[data-copy-markdown-label="1"]';
|
|
3
3
|
const RESET_DELAY_MS = 2000;
|
|
4
4
|
|
|
5
|
-
const setLinkDisabled = (link, disabled) => {
|
|
5
|
+
const setLinkDisabled = (link: HTMLAnchorElement, disabled: boolean): void => {
|
|
6
6
|
if (disabled) {
|
|
7
7
|
link.setAttribute("aria-disabled", "true");
|
|
8
8
|
link.style.pointerEvents = "none";
|
|
@@ -15,14 +15,14 @@ const setLinkDisabled = (link, disabled) => {
|
|
|
15
15
|
link.style.opacity = "";
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
const setLinkLabel = (link, next) => {
|
|
18
|
+
const setLinkLabel = (link: HTMLAnchorElement, next: string): void => {
|
|
19
19
|
const label = link.querySelector(LABEL_SELECTOR);
|
|
20
20
|
if (label) {
|
|
21
21
|
label.textContent = next;
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
const toAbsoluteUrl = (href) => {
|
|
25
|
+
const toAbsoluteUrl = (href: string): string => {
|
|
26
26
|
try {
|
|
27
27
|
return new URL(href, window.location.href).toString();
|
|
28
28
|
} catch {
|
|
@@ -30,7 +30,7 @@ const toAbsoluteUrl = (href) => {
|
|
|
30
30
|
}
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
const createHiddenTextarea = (text) => {
|
|
33
|
+
const createHiddenTextarea = (text: string): HTMLTextAreaElement => {
|
|
34
34
|
const textarea = document.createElement("textarea");
|
|
35
35
|
textarea.value = text;
|
|
36
36
|
textarea.setAttribute("readonly", "true");
|
|
@@ -40,7 +40,7 @@ const createHiddenTextarea = (text) => {
|
|
|
40
40
|
return textarea;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
const safeExecCommandCopy = () => {
|
|
43
|
+
const safeExecCommandCopy = (): boolean => {
|
|
44
44
|
try {
|
|
45
45
|
return document.execCommand("copy");
|
|
46
46
|
} catch {
|
|
@@ -48,7 +48,7 @@ const safeExecCommandCopy = () => {
|
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
-
const copyViaExecCommand = (text) => {
|
|
51
|
+
const copyViaExecCommand = (text: string): boolean => {
|
|
52
52
|
const textarea = createHiddenTextarea(text);
|
|
53
53
|
document.body.append(textarea);
|
|
54
54
|
textarea.focus();
|
|
@@ -58,7 +58,7 @@ const copyViaExecCommand = (text) => {
|
|
|
58
58
|
return ok;
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
const copyText = async (text) => {
|
|
61
|
+
const copyText = async (text: string): Promise<boolean> => {
|
|
62
62
|
const { clipboard } = navigator;
|
|
63
63
|
if (clipboard?.writeText) {
|
|
64
64
|
try {
|
|
@@ -72,18 +72,18 @@ const copyText = async (text) => {
|
|
|
72
72
|
return copyViaExecCommand(text);
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
-
const closeMenuIfPresent = (link) => {
|
|
75
|
+
const closeMenuIfPresent = (link: HTMLAnchorElement): void => {
|
|
76
76
|
const details = link.closest("details");
|
|
77
77
|
if (details instanceof HTMLDetailsElement) {
|
|
78
78
|
details.open = false;
|
|
79
79
|
}
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
-
const getOriginalLabel = (link) =>
|
|
82
|
+
const getOriginalLabel = (link: HTMLAnchorElement): string =>
|
|
83
83
|
link.querySelector(LABEL_SELECTOR)?.textContent ??
|
|
84
84
|
"Copy Markdown to Clipboard";
|
|
85
85
|
|
|
86
|
-
const fetchMarkdownText = async (href) => {
|
|
86
|
+
const fetchMarkdownText = async (href: string): Promise<string | null> => {
|
|
87
87
|
const res = await fetch(toAbsoluteUrl(href), { credentials: "same-origin" });
|
|
88
88
|
if (!res.ok) {
|
|
89
89
|
return null;
|
|
@@ -91,7 +91,7 @@ const fetchMarkdownText = async (href) => {
|
|
|
91
91
|
return res.text();
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
-
const copyMarkdownFromHref = async (href) => {
|
|
94
|
+
const copyMarkdownFromHref = async (href: string): Promise<boolean> => {
|
|
95
95
|
try {
|
|
96
96
|
const text = await fetchMarkdownText(href);
|
|
97
97
|
if (!text) {
|
|
@@ -103,24 +103,30 @@ const copyMarkdownFromHref = async (href) => {
|
|
|
103
103
|
}
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
-
const startCopyOperation = (link) => {
|
|
106
|
+
const startCopyOperation = (link: HTMLAnchorElement): void => {
|
|
107
107
|
setLinkDisabled(link, true);
|
|
108
108
|
setLinkLabel(link, "Copying...");
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
-
const finishCopyOperation = (link, ok) => {
|
|
111
|
+
const finishCopyOperation = (link: HTMLAnchorElement, ok: boolean): void => {
|
|
112
112
|
setLinkLabel(link, ok ? "Copied" : "Copy failed");
|
|
113
113
|
closeMenuIfPresent(link);
|
|
114
114
|
};
|
|
115
115
|
|
|
116
|
-
const scheduleResetOperation = (
|
|
116
|
+
const scheduleResetOperation = (
|
|
117
|
+
link: HTMLAnchorElement,
|
|
118
|
+
originalLabel: string
|
|
119
|
+
): void => {
|
|
117
120
|
window.setTimeout(() => {
|
|
118
121
|
setLinkLabel(link, originalLabel);
|
|
119
122
|
setLinkDisabled(link, false);
|
|
120
123
|
}, RESET_DELAY_MS);
|
|
121
124
|
};
|
|
122
125
|
|
|
123
|
-
const handleCopyClick = async (
|
|
126
|
+
const handleCopyClick = async (
|
|
127
|
+
link: HTMLAnchorElement,
|
|
128
|
+
originalLabel: string
|
|
129
|
+
): Promise<void> => {
|
|
124
130
|
const href = link.getAttribute("href");
|
|
125
131
|
if (!href) {
|
|
126
132
|
return;
|
|
@@ -132,7 +138,7 @@ const handleCopyClick = async (link, originalLabel) => {
|
|
|
132
138
|
scheduleResetOperation(link, originalLabel);
|
|
133
139
|
};
|
|
134
140
|
|
|
135
|
-
const attachCopyHandler = (link) => {
|
|
141
|
+
const attachCopyHandler = (link: HTMLAnchorElement): void => {
|
|
136
142
|
const originalLabel = getOriginalLabel(link);
|
|
137
143
|
link.addEventListener("click", async (event) => {
|
|
138
144
|
event.preventDefault();
|
|
@@ -143,8 +149,11 @@ const attachCopyHandler = (link) => {
|
|
|
143
149
|
});
|
|
144
150
|
};
|
|
145
151
|
|
|
146
|
-
const initCopyMarkdownButtons = () => {
|
|
147
|
-
const links = [...document.querySelectorAll(COPY_SELECTOR)]
|
|
152
|
+
const initCopyMarkdownButtons = (): void => {
|
|
153
|
+
const links = [...document.querySelectorAll(COPY_SELECTOR)].filter(
|
|
154
|
+
(link): link is HTMLAnchorElement => link instanceof HTMLAnchorElement
|
|
155
|
+
);
|
|
156
|
+
|
|
148
157
|
for (const link of links) {
|
|
149
158
|
attachCopyHandler(link);
|
|
150
159
|
}
|
package/templates/default/site/{public/_idcmd/nav-prefetch.js → client/runtime/nav-prefetch.ts}
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
const selector = 'a[data-prefetch="hover"][href]';
|
|
3
|
-
const prefetched = new Set();
|
|
3
|
+
const prefetched = new Set<string>();
|
|
4
4
|
|
|
5
|
-
const prefetch = (href) => {
|
|
5
|
+
const prefetch = (href: string | null | undefined): void => {
|
|
6
6
|
if (!href || prefetched.has(href)) {
|
|
7
7
|
return;
|
|
8
8
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
document.head.append(link);
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
const onOver = (event) => {
|
|
17
|
+
const onOver = (event: Event): void => {
|
|
18
18
|
const { target } = event;
|
|
19
19
|
if (!(target instanceof Element)) {
|
|
20
20
|
return;
|