idcmd 0.0.5 → 0.0.7
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 +29 -9
- package/package.json +2 -2
- package/src/build.ts +6 -5
- package/src/cli/commands/build.ts +11 -7
- package/src/cli/commands/client.ts +328 -0
- package/src/cli/commands/dev.ts +92 -34
- package/src/cli/commands/init.ts +93 -2
- package/src/cli/main.ts +12 -0
- package/src/cli/runtime-assets.ts +89 -0
- package/src/cli.ts +0 -0
- package/src/client/index.ts +7 -1
- package/src/content/icons.ts +1 -1
- package/src/content/paths.ts +1 -1
- package/src/project/paths.ts +26 -30
- package/src/render/layout-loader.ts +7 -4
- 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/src/server/live-reload.ts +2 -6
- package/src/server/static.ts +1 -1
- package/src/server.ts +0 -1
- package/src/site/config.ts +2 -10
- package/templates/default/.github/workflows/ci.yml +24 -0
- package/templates/default/README.md +31 -5
- package/templates/default/package.json +2 -1
- package/templates/default/scripts/check-internal.ts +56 -0
- package/templates/default/scripts/check.ts +332 -0
- package/templates/default/scripts/smoke.ts +223 -0
- package/templates/default/site/{public/_idcmd/llm-menu.js → code/runtime/llm-menu.ts} +27 -18
- package/templates/default/site/{public/_idcmd/nav-prefetch.js → code/runtime/nav-prefetch.ts} +3 -3
- package/templates/default/site/{public/_idcmd/right-rail-scrollspy.js → code/runtime/right-rail-scrollspy.ts} +73 -32
- package/templates/default/site/{server → code}/server.ts +1 -1
- package/templates/default/site/code/ui/layout.tsx +237 -0
- package/templates/default/site/code/ui/right-rail.tsx +246 -0
- package/templates/default/site/code/ui/search-page.tsx +87 -0
- package/templates/default/tsconfig.json +1 -1
- package/templates/default/site/client/layout.tsx +0 -2
- package/templates/default/site/client/right-rail.tsx +0 -1
- package/templates/default/site/client/search-page.tsx +0 -1
- /package/templates/default/site/{public → assets}/anthropic-white.svg +0 -0
- /package/templates/default/site/{public → assets}/favicon.svg +0 -0
- /package/templates/default/site/{icons → assets/icons}/file.svg +0 -0
- /package/templates/default/site/{icons → assets/icons}/home.svg +0 -0
- /package/templates/default/site/{icons → assets/icons}/info.svg +0 -0
- /package/templates/default/site/{public → assets}/openai-white.svg +0 -0
- /package/templates/default/site/{server → code}/routes/api/hello.ts +0 -0
- /package/templates/default/site/{public/_idcmd/live-reload.js → code/runtime/live-reload.ts} +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
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} />)}`;
|
|
@@ -0,0 +1,246 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { SearchPageProps } from "idcmd/client";
|
|
2
|
+
import type { JSX } from "preact";
|
|
3
|
+
|
|
4
|
+
import { render as renderToString } from "preact-render-to-string";
|
|
5
|
+
|
|
6
|
+
const ResultItem = ({
|
|
7
|
+
result,
|
|
8
|
+
}: {
|
|
9
|
+
result: SearchPageProps["results"][number];
|
|
10
|
+
}): JSX.Element => (
|
|
11
|
+
<li class="rounded-md border border-border p-3">
|
|
12
|
+
<a
|
|
13
|
+
href={result.slug}
|
|
14
|
+
class="font-medium underline decoration-border underline-offset-4"
|
|
15
|
+
>
|
|
16
|
+
{result.title}
|
|
17
|
+
</a>
|
|
18
|
+
<p class="mt-1 text-sm text-muted-foreground">{result.description}</p>
|
|
19
|
+
</li>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const EmptyState = ({
|
|
23
|
+
minQueryLength,
|
|
24
|
+
topPages,
|
|
25
|
+
}: {
|
|
26
|
+
minQueryLength: number;
|
|
27
|
+
topPages: SearchPageProps["topPages"];
|
|
28
|
+
}): JSX.Element => (
|
|
29
|
+
<div class="text-sm text-muted-foreground">
|
|
30
|
+
<p>{`Type at least ${minQueryLength} characters to search.`}</p>
|
|
31
|
+
{topPages.length > 0 ? (
|
|
32
|
+
<>
|
|
33
|
+
<p class="mt-4 font-medium text-foreground">Popular pages</p>
|
|
34
|
+
<ul class="mt-2 space-y-1">
|
|
35
|
+
{topPages.map((page) => (
|
|
36
|
+
<li key={page.href}>
|
|
37
|
+
<a
|
|
38
|
+
href={page.href}
|
|
39
|
+
class="underline decoration-border underline-offset-4"
|
|
40
|
+
>
|
|
41
|
+
{page.title}
|
|
42
|
+
</a>
|
|
43
|
+
</li>
|
|
44
|
+
))}
|
|
45
|
+
</ul>
|
|
46
|
+
</>
|
|
47
|
+
) : null}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const SearchPage = ({
|
|
52
|
+
query,
|
|
53
|
+
minQueryLength,
|
|
54
|
+
results,
|
|
55
|
+
topPages,
|
|
56
|
+
}: SearchPageProps): JSX.Element => {
|
|
57
|
+
const trimmed = query.trim();
|
|
58
|
+
const showResults = trimmed.length >= minQueryLength;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<section class="not-prose rounded-lg border border-border bg-card/30 p-4">
|
|
62
|
+
<h1 class="text-lg font-semibold">Search</h1>
|
|
63
|
+
{showResults ? (
|
|
64
|
+
<p class="mt-2 text-sm text-muted-foreground">
|
|
65
|
+
{results.length === 0
|
|
66
|
+
? `No matches for "${trimmed}".`
|
|
67
|
+
: `Found ${results.length} result(s) for "${trimmed}".`}
|
|
68
|
+
</p>
|
|
69
|
+
) : (
|
|
70
|
+
<div class="mt-2">
|
|
71
|
+
<EmptyState minQueryLength={minQueryLength} topPages={topPages} />
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{showResults ? (
|
|
76
|
+
<ul class="mt-4 space-y-2">
|
|
77
|
+
{results.map((result) => (
|
|
78
|
+
<ResultItem key={result.slug} result={result} />
|
|
79
|
+
))}
|
|
80
|
+
</ul>
|
|
81
|
+
) : null}
|
|
82
|
+
</section>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const renderSearchPageContent = (props: SearchPageProps): string =>
|
|
87
|
+
renderToString(<SearchPage {...props} />);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { RightRail } from "idcmd/client";
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { renderSearchPageContent } from "idcmd/client";
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/templates/default/site/{public/_idcmd/live-reload.js → code/runtime/live-reload.ts}
RENAMED
|
File without changes
|