idcmd 0.0.8 → 0.0.10
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 +13 -13
- package/package.json +3 -3
- package/src/build.ts +2 -2
- package/src/cli/commands/build.ts +2 -2
- package/src/cli/commands/client.ts +4 -6
- package/src/cli/commands/deploy.ts +3 -3
- package/src/cli/commands/dev.ts +2 -2
- package/src/cli/commands/init.ts +1 -1
- package/src/cli/runtime-assets.ts +1 -1
- package/src/content/paths.ts +1 -1
- package/src/project/paths.ts +10 -19
- package/src/render/layout-loader.ts +1 -1
- package/src/render/layout.tsx +1 -1
- package/src/render/right-rail-loader.ts +1 -1
- package/src/search/search-page-loader.ts +1 -1
- package/src/server/live-reload.ts +1 -1
- package/src/site/config.ts +1 -1
- package/templates/default/README.md +11 -11
- package/templates/default/{site/content → content}/index.md +1 -1
- package/templates/default/scripts/check-internal.ts +6 -6
- package/templates/default/scripts/check.ts +6 -6
- package/templates/default/src/runtime/live-reload.ts +18 -0
- package/templates/default/src/runtime/llm-menu.ts +162 -0
- package/templates/default/src/runtime/nav-prefetch.ts +30 -0
- package/templates/default/src/runtime/right-rail-scrollspy.ts +303 -0
- package/templates/default/{site/src → src}/server.ts +1 -1
- package/templates/default/{site/src → src}/ui/layout.tsx +1 -1
- package/templates/default/{site/styles → styles}/tailwind.css +2 -2
- /package/{templates/default/site/src → src}/runtime/live-reload.ts +0 -0
- /package/{templates/default/site/src → src}/runtime/llm-menu.ts +0 -0
- /package/{templates/default/site/src → src}/runtime/nav-prefetch.ts +0 -0
- /package/{templates/default/site/src → src}/runtime/right-rail-scrollspy.ts +0 -0
- /package/templates/default/{site/assets → assets}/anthropic-white.svg +0 -0
- /package/templates/default/{site/assets → assets}/favicon.svg +0 -0
- /package/templates/default/{site/assets → assets}/icons/file.svg +0 -0
- /package/templates/default/{site/assets → assets}/icons/home.svg +0 -0
- /package/templates/default/{site/assets → assets}/icons/info.svg +0 -0
- /package/templates/default/{site/assets → assets}/openai-white.svg +0 -0
- /package/templates/default/{site/content → content}/404.md +0 -0
- /package/templates/default/{site/content → content}/about.md +0 -0
- /package/templates/default/{site/site.jsonc → site.jsonc} +0 -0
- /package/templates/default/{site/src → src}/routes/api/hello.ts +0 -0
- /package/templates/default/{site/src → src}/ui/right-rail.tsx +0 -0
- /package/templates/default/{site/src → src}/ui/search-page.tsx +0 -0
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ bun install
|
|
|
9
9
|
bun run dev
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
Everything you edit lives
|
|
12
|
+
Everything you edit lives at the project root-level source folders (`content/`, `src/`, `styles/`, `assets/`, `site.jsonc`).
|
|
13
13
|
|
|
14
14
|
## CLI
|
|
15
15
|
|
|
@@ -19,18 +19,18 @@ idcmd dev # tailwind watch + SSR dev server
|
|
|
19
19
|
idcmd build # static public/
|
|
20
20
|
idcmd preview # serve public/ locally
|
|
21
21
|
idcmd deploy # build + validate Vercel static deploy config
|
|
22
|
-
idcmd client ... # add/update local
|
|
22
|
+
idcmd client ... # add/update local src implementations
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
## Layout (V1)
|
|
26
26
|
|
|
27
|
-
- `
|
|
28
|
-
- `
|
|
29
|
-
- `
|
|
30
|
-
- `
|
|
31
|
-
- `
|
|
32
|
-
- `
|
|
33
|
-
- `site
|
|
27
|
+
- `content/<slug>.md` -> `/<slug>/` (`index.md` -> `/`)
|
|
28
|
+
- `src/ui/*` is local UI source code (you own and edit these files)
|
|
29
|
+
- `src/runtime/*.ts` is local browser runtime code (compiled to `public/_idcmd/*.js`)
|
|
30
|
+
- `src/routes/**` file-based server routes (dev/server-host only)
|
|
31
|
+
- `styles/tailwind.css` -> `public/styles.css`
|
|
32
|
+
- `assets/` static assets
|
|
33
|
+
- `site.jsonc` site config
|
|
34
34
|
- `public/` generated output (gitignored)
|
|
35
35
|
|
|
36
36
|
## Syncing Local Client Code
|
|
@@ -45,11 +45,11 @@ idcmd client update runtime --yes
|
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
`add` creates missing files. `update` overwrites changed files and requires `--yes` unless `--dry-run` is used.
|
|
48
|
-
Runtime files in `
|
|
48
|
+
Runtime files in `src/runtime/` are compiled automatically by `idcmd dev` and `idcmd build`.
|
|
49
49
|
|
|
50
50
|
## Example: Add A Page
|
|
51
51
|
|
|
52
|
-
Create `
|
|
52
|
+
Create `content/hello.md`:
|
|
53
53
|
|
|
54
54
|
```md
|
|
55
55
|
---
|
|
@@ -68,7 +68,7 @@ It renders at `/hello/`.
|
|
|
68
68
|
|
|
69
69
|
## Custom Server Routes (V1)
|
|
70
70
|
|
|
71
|
-
Add `
|
|
71
|
+
Add `src/routes/api/hello.ts`:
|
|
72
72
|
|
|
73
73
|
```ts
|
|
74
74
|
export const GET = (): Response => Response.json({ ok: true });
|
|
@@ -94,7 +94,7 @@ It responds at `/api/hello`.
|
|
|
94
94
|
|
|
95
95
|
### Slug and path rules
|
|
96
96
|
|
|
97
|
-
- Content lives at `
|
|
97
|
+
- Content lives at `content/<slug>.md`.
|
|
98
98
|
- `slug="index"` is the home page.
|
|
99
99
|
- Canonical HTML paths are `/` for index and `/<slug>/` otherwise.
|
|
100
100
|
- Markdown download paths exist in two forms:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idcmd",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/rustydotwtf/idcmd"
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"dev": "concurrently -k -n css,server -c blue,green \"bun run dev:css\" \"bun run dev:server\"",
|
|
25
|
-
"dev:css": "bunx @tailwindcss/cli -i
|
|
25
|
+
"dev:css": "bunx @tailwindcss/cli -i styles/tailwind.css -o public/styles.css --watch",
|
|
26
26
|
"dev:server": "bun --hot src/server.ts",
|
|
27
|
-
"build:css": "bunx @tailwindcss/cli -i
|
|
27
|
+
"build:css": "bunx @tailwindcss/cli -i styles/tailwind.css -o public/styles.css --minify",
|
|
28
28
|
"build": "bun run build:css && bun src/build.ts",
|
|
29
29
|
"preview": "bunx serve public",
|
|
30
30
|
"start": "bun src/server.ts",
|
package/src/build.ts
CHANGED
|
@@ -20,7 +20,7 @@ const MIN_SEARCH_QUERY_LENGTH = 2;
|
|
|
20
20
|
|
|
21
21
|
const project = await getProjectPaths();
|
|
22
22
|
|
|
23
|
-
// Find all content files in `
|
|
23
|
+
// Find all content files in `content/` (`content/<slug>.md`).
|
|
24
24
|
const contentFiles: string[] = [];
|
|
25
25
|
|
|
26
26
|
const buildStart = performance.now();
|
|
@@ -214,7 +214,7 @@ if (siteConfig.baseUrl) {
|
|
|
214
214
|
console.log(` generated ${project.outputDir}/robots.txt`);
|
|
215
215
|
} else {
|
|
216
216
|
console.log(
|
|
217
|
-
"Warning: site
|
|
217
|
+
"Warning: site.jsonc missing baseUrl; skipping sitemap.xml and robots.txt."
|
|
218
218
|
);
|
|
219
219
|
}
|
|
220
220
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { compileRuntimeAssetsOnce } from "../runtime-assets";
|
|
2
2
|
|
|
3
3
|
const findTailwindInput = async (): Promise<string> => {
|
|
4
|
-
const path = "
|
|
4
|
+
const path = "styles/tailwind.css";
|
|
5
5
|
if (await Bun.file(path).exists()) {
|
|
6
6
|
return path;
|
|
7
7
|
}
|
|
8
8
|
throw new Error(
|
|
9
|
-
"Could not find Tailwind input. Expected
|
|
9
|
+
"Could not find Tailwind input. Expected styles/tailwind.css."
|
|
10
10
|
);
|
|
11
11
|
};
|
|
12
12
|
|
|
@@ -8,7 +8,6 @@ const TEMPLATE_UI_DIR = joinPath(
|
|
|
8
8
|
"..",
|
|
9
9
|
"templates",
|
|
10
10
|
"default",
|
|
11
|
-
"site",
|
|
12
11
|
"src",
|
|
13
12
|
"ui"
|
|
14
13
|
);
|
|
@@ -19,14 +18,13 @@ const TEMPLATE_RUNTIME_DIR = joinPath(
|
|
|
19
18
|
"..",
|
|
20
19
|
"templates",
|
|
21
20
|
"default",
|
|
22
|
-
"site",
|
|
23
21
|
"src",
|
|
24
22
|
"runtime"
|
|
25
23
|
);
|
|
26
24
|
|
|
27
|
-
const SITE_UI_DIR = joinPath("
|
|
28
|
-
const SITE_RUNTIME_DIR = joinPath("
|
|
29
|
-
const SITE_CONFIG_PATH =
|
|
25
|
+
const SITE_UI_DIR = joinPath("src", "ui");
|
|
26
|
+
const SITE_RUNTIME_DIR = joinPath("src", "runtime");
|
|
27
|
+
const SITE_CONFIG_PATH = "site.jsonc";
|
|
30
28
|
|
|
31
29
|
const CLIENT_PARTS = [
|
|
32
30
|
"layout",
|
|
@@ -144,7 +142,7 @@ const parseClientArgs = (positionals: string[]): ParsedClientArgs => {
|
|
|
144
142
|
const ensureSiteLayout = async (): Promise<void> => {
|
|
145
143
|
if (!(await Bun.file(SITE_CONFIG_PATH).exists())) {
|
|
146
144
|
throw new Error(
|
|
147
|
-
`Could not find ${SITE_CONFIG_PATH}. Run this command from an idcmd
|
|
145
|
+
`Could not find ${SITE_CONFIG_PATH}. Run this command from an idcmd project root.`
|
|
148
146
|
);
|
|
149
147
|
}
|
|
150
148
|
};
|
|
@@ -30,7 +30,7 @@ const warnIfVercelMisconfigured = async (): Promise<void> => {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
const warnIfBaseUrlMissing = async (): Promise<void> => {
|
|
33
|
-
const file = Bun.file("site
|
|
33
|
+
const file = Bun.file("site.jsonc");
|
|
34
34
|
if (!(await file.exists())) {
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
@@ -42,13 +42,13 @@ const warnIfBaseUrlMissing = async (): Promise<void> => {
|
|
|
42
42
|
if (!cfg.baseUrl) {
|
|
43
43
|
// eslint-disable-next-line no-console
|
|
44
44
|
console.warn(
|
|
45
|
-
"Warning: site
|
|
45
|
+
"Warning: site.jsonc missing baseUrl; sitemap.xml and robots.txt will be skipped."
|
|
46
46
|
);
|
|
47
47
|
}
|
|
48
48
|
} catch (error) {
|
|
49
49
|
const message = error instanceof Error ? error.message : String(error);
|
|
50
50
|
// eslint-disable-next-line no-console
|
|
51
|
-
console.warn(`Warning: Failed to parse site
|
|
51
|
+
console.warn(`Warning: Failed to parse site.jsonc: ${message}`);
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
54
|
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -11,12 +11,12 @@ export interface DevFlags {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const findTailwindInput = async (): Promise<string> => {
|
|
14
|
-
const path = "
|
|
14
|
+
const path = "styles/tailwind.css";
|
|
15
15
|
if (await Bun.file(path).exists()) {
|
|
16
16
|
return path;
|
|
17
17
|
}
|
|
18
18
|
throw new Error(
|
|
19
|
-
"Could not find Tailwind input. Expected
|
|
19
|
+
"Could not find Tailwind input. Expected styles/tailwind.css."
|
|
20
20
|
);
|
|
21
21
|
};
|
|
22
22
|
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -200,7 +200,7 @@ const applySubstitutions = async (args: {
|
|
|
200
200
|
siteName: string;
|
|
201
201
|
targetDir: string;
|
|
202
202
|
}): Promise<void> => {
|
|
203
|
-
await replaceInFile(joinPath(args.targetDir, "site
|
|
203
|
+
await replaceInFile(joinPath(args.targetDir, "site.jsonc"), (text) =>
|
|
204
204
|
fillSiteJsonc({
|
|
205
205
|
baseUrl: args.baseUrl,
|
|
206
206
|
description: args.description,
|
package/src/content/paths.ts
CHANGED
|
@@ -21,7 +21,7 @@ export const slugFromContentFile = (file: string): string => {
|
|
|
21
21
|
export const scanContentFiles =
|
|
22
22
|
async function* scanContentFiles(): AsyncGenerator<string> {
|
|
23
23
|
const { contentDir } = await getProjectPaths();
|
|
24
|
-
// `
|
|
24
|
+
// `content/<slug>.md`
|
|
25
25
|
for await (const file of flatContentGlob.scan(contentDir)) {
|
|
26
26
|
yield file;
|
|
27
27
|
}
|
package/src/project/paths.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const DEFAULT_SITE_DIR = "site";
|
|
2
1
|
const DEFAULT_OUTPUT_DIR = "public";
|
|
3
2
|
const ASSET_PREFIX = "/_idcmd" as const;
|
|
4
3
|
const CONTENT_DIR = "content";
|
|
@@ -15,13 +14,11 @@ export interface ProjectPaths {
|
|
|
15
14
|
assetsDir: string;
|
|
16
15
|
routesDir: string;
|
|
17
16
|
siteConfigPath: string;
|
|
18
|
-
siteDir: string | null;
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
export interface ResolveProjectPathsOptions {
|
|
22
20
|
cwd?: string;
|
|
23
21
|
outputDir?: string;
|
|
24
|
-
siteDir?: string;
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
const trimTrailingSlash = (value: string): string =>
|
|
@@ -45,28 +42,22 @@ const joinPath = (...parts: string[]): string => {
|
|
|
45
42
|
const buildPaths = (args: {
|
|
46
43
|
cwd: string;
|
|
47
44
|
outputDirName: string;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
routesDir: joinPath(siteRoot, ROUTES_DIR),
|
|
58
|
-
siteConfigPath: joinPath(siteRoot, SITE_CONFIG_FILE),
|
|
59
|
-
siteDir: siteRoot,
|
|
60
|
-
};
|
|
61
|
-
};
|
|
45
|
+
}): ProjectPaths => ({
|
|
46
|
+
assetPrefix: ASSET_PREFIX,
|
|
47
|
+
assetsDir: joinPath(args.cwd, ASSETS_DIR),
|
|
48
|
+
contentDir: joinPath(args.cwd, CONTENT_DIR),
|
|
49
|
+
iconsDir: joinPath(args.cwd, ICONS_DIR),
|
|
50
|
+
outputDir: joinPath(args.cwd, args.outputDirName),
|
|
51
|
+
routesDir: joinPath(args.cwd, ROUTES_DIR),
|
|
52
|
+
siteConfigPath: joinPath(args.cwd, SITE_CONFIG_FILE),
|
|
53
|
+
});
|
|
62
54
|
|
|
63
55
|
export const resolveProjectPaths = (
|
|
64
56
|
options: ResolveProjectPathsOptions = {}
|
|
65
57
|
): Promise<ProjectPaths> => {
|
|
66
58
|
const cwd = trimTrailingSlash(options.cwd ?? process.cwd());
|
|
67
59
|
const outputDirName = options.outputDir ?? DEFAULT_OUTPUT_DIR;
|
|
68
|
-
|
|
69
|
-
return Promise.resolve(buildPaths({ cwd, outputDirName, siteDirName }));
|
|
60
|
+
return Promise.resolve(buildPaths({ cwd, outputDirName }));
|
|
70
61
|
};
|
|
71
62
|
|
|
72
63
|
let cached: Promise<ProjectPaths> | null = null;
|
|
@@ -2,7 +2,7 @@ import type { RenderLayout } from "./layout";
|
|
|
2
2
|
|
|
3
3
|
import { renderLayout as defaultRenderLayout } from "./layout";
|
|
4
4
|
|
|
5
|
-
const USER_LAYOUT_PATH = "
|
|
5
|
+
const USER_LAYOUT_PATH = "src/ui/layout.tsx";
|
|
6
6
|
|
|
7
7
|
const loadUserLayout = async (
|
|
8
8
|
filePath: string
|
package/src/render/layout.tsx
CHANGED
|
@@ -151,7 +151,7 @@ const TopNavbar = ({
|
|
|
151
151
|
<div class="flex items-center gap-4">
|
|
152
152
|
<a
|
|
153
153
|
href="/"
|
|
154
|
-
class="text-sm font-medium tracking-tight font-mono
|
|
154
|
+
class="text-sm font-medium tracking-tight font-mono lg:hidden"
|
|
155
155
|
data-prefetch="hover"
|
|
156
156
|
>
|
|
157
157
|
<span class="text-muted-foreground">~/</span>
|
|
@@ -2,7 +2,7 @@ import type { RightRailComponent } from "./right-rail";
|
|
|
2
2
|
|
|
3
3
|
import { RightRail as defaultRightRail } from "./right-rail";
|
|
4
4
|
|
|
5
|
-
const USER_RIGHT_RAIL_PATH = "
|
|
5
|
+
const USER_RIGHT_RAIL_PATH = "src/ui/right-rail.tsx";
|
|
6
6
|
|
|
7
7
|
const loadUserRightRail = async (
|
|
8
8
|
filePath: string
|
|
@@ -2,7 +2,7 @@ import type { RenderSearchPageContent } from "./page";
|
|
|
2
2
|
|
|
3
3
|
import { renderSearchPageContent as defaultRenderSearchPageContent } from "./page";
|
|
4
4
|
|
|
5
|
-
const USER_SEARCH_PAGE_PATH = "
|
|
5
|
+
const USER_SEARCH_PAGE_PATH = "src/ui/search-page.tsx";
|
|
6
6
|
|
|
7
7
|
const loadUserSearchPage = async (
|
|
8
8
|
filePath: string
|
|
@@ -59,7 +59,7 @@ export const createLiveReload = (env: LiveReloadEnv): LiveReloadController => {
|
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
console.log("Watching
|
|
62
|
+
console.log("Watching content/ for changes...");
|
|
63
63
|
let snapshot = await getContentSnapshot();
|
|
64
64
|
|
|
65
65
|
const poll = async (): Promise<void> => {
|
package/src/site/config.ts
CHANGED
|
@@ -91,7 +91,7 @@ export interface ResolvedRightRailConfig {
|
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const SITE_CONFIG_PATH = "site
|
|
94
|
+
const SITE_CONFIG_PATH = "site.jsonc";
|
|
95
95
|
const LOCALHOST_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1"]);
|
|
96
96
|
|
|
97
97
|
const DEFAULT_RIGHT_RAIL_CONFIG: ResolvedRightRailConfig = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# **IDCMD_SITE_NAME**
|
|
2
2
|
|
|
3
|
-
Everything you edit lives in
|
|
3
|
+
Everything you edit lives in root-level source folders.
|
|
4
4
|
|
|
5
5
|
This starter is intentionally opinionated for AI-friendly markdown sites.
|
|
6
6
|
|
|
@@ -20,16 +20,16 @@ bun run smoke
|
|
|
20
20
|
|
|
21
21
|
## Layout
|
|
22
22
|
|
|
23
|
-
- Content: `
|
|
24
|
-
- Code: `
|
|
25
|
-
- Code: `
|
|
26
|
-
- Code: `
|
|
27
|
-
- Assets: `
|
|
28
|
-
- Styles source: `
|
|
29
|
-
- Config: `site
|
|
23
|
+
- Content: `content/` markdown pages (`index.md` -> `/`, `about.md` -> `/about/`)
|
|
24
|
+
- Code: `src/ui/` (`layout.tsx`, `right-rail.tsx`, `search-page.tsx`)
|
|
25
|
+
- Code: `src/runtime/` browser runtime TS (`*_idcmd` scripts compile from here)
|
|
26
|
+
- Code: `src/routes/` file-based server routes (dev/server-host only)
|
|
27
|
+
- Assets: `assets/` static files you own (icons, images, favicon, etc.)
|
|
28
|
+
- Styles source: `styles/tailwind.css`
|
|
29
|
+
- Config: `site.jsonc`
|
|
30
30
|
- Generated output: `public/` (`public/styles.css`, `public/_idcmd/*.js`, built pages)
|
|
31
31
|
|
|
32
|
-
The mental model is simple: edit `
|
|
32
|
+
The mental model is simple: edit `content` and `src`, treat `public/` as generated output.
|
|
33
33
|
|
|
34
34
|
## Sync Local Client Files
|
|
35
35
|
|
|
@@ -40,8 +40,8 @@ idcmd client update layout --yes
|
|
|
40
40
|
idcmd client update runtime --yes
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
These commands copy the latest baseline implementations from `idcmd` into `
|
|
44
|
-
Runtime files in `
|
|
43
|
+
These commands copy the latest baseline implementations from `idcmd` into `src/ui/` and `src/runtime/`.
|
|
44
|
+
Runtime files in `src/runtime/` are compiled automatically by `idcmd dev` and `idcmd build`.
|
|
45
45
|
|
|
46
46
|
## Deploy (Vercel static)
|
|
47
47
|
|
|
@@ -11,16 +11,16 @@ const checks: InternalCheck[] = [
|
|
|
11
11
|
run: () => fileExists("package.json"),
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
|
-
description: "site config must exist (site
|
|
15
|
-
run: () => fileExists("site
|
|
14
|
+
description: "site config must exist (site.jsonc)",
|
|
15
|
+
run: () => fileExists("site.jsonc"),
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
|
-
description: "tailwind input must exist (
|
|
19
|
-
run: () => fileExists("
|
|
18
|
+
description: "tailwind input must exist (styles/tailwind.css)",
|
|
19
|
+
run: () => fileExists("styles/tailwind.css"),
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
|
-
description: "
|
|
23
|
-
run: () => fileExists("
|
|
22
|
+
description: "source UI entry must exist (src/ui/layout.tsx)",
|
|
23
|
+
run: () => fileExists("src/ui/layout.tsx"),
|
|
24
24
|
},
|
|
25
25
|
];
|
|
26
26
|
|
|
@@ -36,11 +36,11 @@ const LINT_TARGETS = [
|
|
|
36
36
|
".oxlintrc.json",
|
|
37
37
|
".oxfmtrc.jsonc",
|
|
38
38
|
"scripts",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"site
|
|
39
|
+
"src",
|
|
40
|
+
"content",
|
|
41
|
+
"assets",
|
|
42
|
+
"styles",
|
|
43
|
+
"site.jsonc",
|
|
44
44
|
];
|
|
45
45
|
const ROOT_TEST_FILE_PATTERNS = [
|
|
46
46
|
"*.test.ts",
|
|
@@ -78,7 +78,7 @@ const NESTED_TEST_FILE_PATTERNS = [
|
|
|
78
78
|
"**/*_spec_*.js",
|
|
79
79
|
"**/*_spec_*.jsx",
|
|
80
80
|
];
|
|
81
|
-
const TEST_SOURCE_DIRS = ["
|
|
81
|
+
const TEST_SOURCE_DIRS = ["content", "scripts", "src", "tests"];
|
|
82
82
|
|
|
83
83
|
const buildScopedTestFilePatterns = (): string[] => {
|
|
84
84
|
const patterns = [...ROOT_TEST_FILE_PATTERNS];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
3
|
+
const socket = new WebSocket(
|
|
4
|
+
`${protocol}//${location.host}/_idcmd/live-reload`
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
socket.addEventListener("message", (event) => {
|
|
8
|
+
if (event.data === "reload") {
|
|
9
|
+
location.reload();
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
socket.addEventListener("close", () => {
|
|
14
|
+
setTimeout(() => {
|
|
15
|
+
location.reload();
|
|
16
|
+
}, 1000);
|
|
17
|
+
});
|
|
18
|
+
})();
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const COPY_SELECTOR = 'a[data-copy-markdown="1"]';
|
|
2
|
+
const LABEL_SELECTOR = '[data-copy-markdown-label="1"]';
|
|
3
|
+
const RESET_DELAY_MS = 2000;
|
|
4
|
+
|
|
5
|
+
const setLinkDisabled = (link: HTMLAnchorElement, disabled: boolean): void => {
|
|
6
|
+
if (disabled) {
|
|
7
|
+
link.setAttribute("aria-disabled", "true");
|
|
8
|
+
link.style.pointerEvents = "none";
|
|
9
|
+
link.style.opacity = "0.8";
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
link.removeAttribute("aria-disabled");
|
|
14
|
+
link.style.pointerEvents = "";
|
|
15
|
+
link.style.opacity = "";
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const setLinkLabel = (link: HTMLAnchorElement, next: string): void => {
|
|
19
|
+
const label = link.querySelector(LABEL_SELECTOR);
|
|
20
|
+
if (label) {
|
|
21
|
+
label.textContent = next;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const toAbsoluteUrl = (href: string): string => {
|
|
26
|
+
try {
|
|
27
|
+
return new URL(href, window.location.href).toString();
|
|
28
|
+
} catch {
|
|
29
|
+
return href;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const createHiddenTextarea = (text: string): HTMLTextAreaElement => {
|
|
34
|
+
const textarea = document.createElement("textarea");
|
|
35
|
+
textarea.value = text;
|
|
36
|
+
textarea.setAttribute("readonly", "true");
|
|
37
|
+
textarea.style.position = "fixed";
|
|
38
|
+
textarea.style.left = "-9999px";
|
|
39
|
+
textarea.style.top = "0";
|
|
40
|
+
return textarea;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const safeExecCommandCopy = (): boolean => {
|
|
44
|
+
try {
|
|
45
|
+
return document.execCommand("copy");
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const copyViaExecCommand = (text: string): boolean => {
|
|
52
|
+
const textarea = createHiddenTextarea(text);
|
|
53
|
+
document.body.append(textarea);
|
|
54
|
+
textarea.focus();
|
|
55
|
+
textarea.select();
|
|
56
|
+
const ok = safeExecCommandCopy();
|
|
57
|
+
textarea.remove();
|
|
58
|
+
return ok;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const copyText = async (text: string): Promise<boolean> => {
|
|
62
|
+
const { clipboard } = navigator;
|
|
63
|
+
if (clipboard?.writeText) {
|
|
64
|
+
try {
|
|
65
|
+
await clipboard.writeText(text);
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
// Fall back below.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return copyViaExecCommand(text);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const closeMenuIfPresent = (link: HTMLAnchorElement): void => {
|
|
76
|
+
const details = link.closest("details");
|
|
77
|
+
if (details instanceof HTMLDetailsElement) {
|
|
78
|
+
details.open = false;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const getOriginalLabel = (link: HTMLAnchorElement): string =>
|
|
83
|
+
link.querySelector(LABEL_SELECTOR)?.textContent ??
|
|
84
|
+
"Copy Markdown to Clipboard";
|
|
85
|
+
|
|
86
|
+
const fetchMarkdownText = async (href: string): Promise<string | null> => {
|
|
87
|
+
const res = await fetch(toAbsoluteUrl(href), { credentials: "same-origin" });
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return res.text();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const copyMarkdownFromHref = async (href: string): Promise<boolean> => {
|
|
95
|
+
try {
|
|
96
|
+
const text = await fetchMarkdownText(href);
|
|
97
|
+
if (!text) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return copyText(text);
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const startCopyOperation = (link: HTMLAnchorElement): void => {
|
|
107
|
+
setLinkDisabled(link, true);
|
|
108
|
+
setLinkLabel(link, "Copying...");
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const finishCopyOperation = (link: HTMLAnchorElement, ok: boolean): void => {
|
|
112
|
+
setLinkLabel(link, ok ? "Copied" : "Copy failed");
|
|
113
|
+
closeMenuIfPresent(link);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const scheduleResetOperation = (
|
|
117
|
+
link: HTMLAnchorElement,
|
|
118
|
+
originalLabel: string
|
|
119
|
+
): void => {
|
|
120
|
+
window.setTimeout(() => {
|
|
121
|
+
setLinkLabel(link, originalLabel);
|
|
122
|
+
setLinkDisabled(link, false);
|
|
123
|
+
}, RESET_DELAY_MS);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleCopyClick = async (
|
|
127
|
+
link: HTMLAnchorElement,
|
|
128
|
+
originalLabel: string
|
|
129
|
+
): Promise<void> => {
|
|
130
|
+
const href = link.getAttribute("href");
|
|
131
|
+
if (!href) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
startCopyOperation(link);
|
|
136
|
+
const ok = await copyMarkdownFromHref(href);
|
|
137
|
+
finishCopyOperation(link, ok);
|
|
138
|
+
scheduleResetOperation(link, originalLabel);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const attachCopyHandler = (link: HTMLAnchorElement): void => {
|
|
142
|
+
const originalLabel = getOriginalLabel(link);
|
|
143
|
+
link.addEventListener("click", async (event) => {
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
if (link.getAttribute("aria-disabled") === "true") {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
await handleCopyClick(link, originalLabel);
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const initCopyMarkdownButtons = (): void => {
|
|
153
|
+
const links = [...document.querySelectorAll(COPY_SELECTOR)].filter(
|
|
154
|
+
(link): link is HTMLAnchorElement => link instanceof HTMLAnchorElement
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
for (const link of links) {
|
|
158
|
+
attachCopyHandler(link);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
initCopyMarkdownButtons();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const selector = 'a[data-prefetch="hover"][href]';
|
|
3
|
+
const prefetched = new Set<string>();
|
|
4
|
+
|
|
5
|
+
const prefetch = (href: string | null | undefined): void => {
|
|
6
|
+
if (!href || prefetched.has(href)) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
prefetched.add(href);
|
|
10
|
+
|
|
11
|
+
const link = document.createElement("link");
|
|
12
|
+
link.rel = "prefetch";
|
|
13
|
+
link.href = href;
|
|
14
|
+
document.head.append(link);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const onOver = (event: Event): void => {
|
|
18
|
+
const { target } = event;
|
|
19
|
+
if (!(target instanceof Element)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const link = target.closest(selector);
|
|
23
|
+
if (!(link instanceof HTMLAnchorElement)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
prefetch(link.href);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
document.addEventListener("mouseover", onOver, { passive: true });
|
|
30
|
+
})();
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
const TOC_ROOT_SELECTOR = '[data-toc-root="1"]';
|
|
2
|
+
const TOC_LINK_SELECTOR = 'a[data-toc-link="1"][href^="#"]';
|
|
3
|
+
const TOC_SCROLL_CONTAINER_SELECTOR = '[data-toc-scroll-container="1"]';
|
|
4
|
+
|
|
5
|
+
const NAVBAR_GAP_PX = 16;
|
|
6
|
+
const clamp = (value: number, min: number, max: number): number =>
|
|
7
|
+
Math.min(Math.max(value, min), max);
|
|
8
|
+
|
|
9
|
+
const getTopOffset = (): number => {
|
|
10
|
+
const header = document.querySelector("header");
|
|
11
|
+
const headerHeight = header?.getBoundingClientRect().height ?? 0;
|
|
12
|
+
return Math.ceil(headerHeight + NAVBAR_GAP_PX);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const decodeAnchorId = (href: string | null): string | null => {
|
|
16
|
+
if (!href?.startsWith("#")) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const raw = href.slice(1);
|
|
21
|
+
if (!raw) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
return decodeURIComponent(raw);
|
|
27
|
+
} catch {
|
|
28
|
+
return raw;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
interface TocEntry {
|
|
33
|
+
heading: HTMLElement;
|
|
34
|
+
link: HTMLAnchorElement;
|
|
35
|
+
y: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const toEntry = (link: HTMLAnchorElement): TocEntry | null => {
|
|
39
|
+
const id = decodeAnchorId(link.getAttribute("href"));
|
|
40
|
+
if (!id) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ids like "11-overview" are valid HTML ids but invalid CSS selectors unless escaped.
|
|
45
|
+
// eslint-disable-next-line unicorn/prefer-query-selector
|
|
46
|
+
const heading = document.getElementById(id);
|
|
47
|
+
if (!heading) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { heading, link, y: 0 };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const buildEntries = (tocRoot: Element): TocEntry[] =>
|
|
55
|
+
[...tocRoot.querySelectorAll(TOC_LINK_SELECTOR)]
|
|
56
|
+
.filter(
|
|
57
|
+
(link): link is HTMLAnchorElement => link instanceof HTMLAnchorElement
|
|
58
|
+
)
|
|
59
|
+
.map(toEntry)
|
|
60
|
+
.filter((entry): entry is TocEntry => entry !== null);
|
|
61
|
+
|
|
62
|
+
const measureEntries = (entries: TocEntry[]): void => {
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
entry.y = entry.heading.getBoundingClientRect().top + window.scrollY;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const setScrollMarginTop = (topOffset: number): void => {
|
|
69
|
+
document.documentElement.style.setProperty(
|
|
70
|
+
"--scroll-margin-top",
|
|
71
|
+
`${topOffset}px`
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const binarySearchLastAtOrAbove = (
|
|
76
|
+
entries: TocEntry[],
|
|
77
|
+
anchorLine: number
|
|
78
|
+
): number => {
|
|
79
|
+
let lo = 0;
|
|
80
|
+
let hi = entries.length;
|
|
81
|
+
|
|
82
|
+
// Find the first entry with y > anchorLine, then step back one.
|
|
83
|
+
while (lo < hi) {
|
|
84
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
85
|
+
const midY = entries[mid]?.y ?? Number.POSITIVE_INFINITY;
|
|
86
|
+
if (midY <= anchorLine) {
|
|
87
|
+
lo = mid + 1;
|
|
88
|
+
} else {
|
|
89
|
+
hi = mid;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return lo - 1;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const findActiveIndex = (entries: TocEntry[], topOffset: number): number => {
|
|
97
|
+
const anchorLine = window.scrollY + topOffset + 1;
|
|
98
|
+
const best = binarySearchLastAtOrAbove(entries, anchorLine);
|
|
99
|
+
return Math.max(0, best);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const parseTransformValues = (transform: string, prefix: string): number[] =>
|
|
103
|
+
transform
|
|
104
|
+
.slice(prefix.length, -1)
|
|
105
|
+
.split(",")
|
|
106
|
+
.map((value) => Number.parseFloat(value.trim()));
|
|
107
|
+
|
|
108
|
+
const getNumberAtIndex = (values: number[], index: number): number => {
|
|
109
|
+
const value = values[index] ?? Number.NaN;
|
|
110
|
+
return Number.isFinite(value) ? value : 0;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const getMatrix3dTranslateY = (values: number[]): number =>
|
|
114
|
+
getNumberAtIndex(values, 13);
|
|
115
|
+
|
|
116
|
+
const getMatrixTranslateY = (values: number[]): number =>
|
|
117
|
+
getNumberAtIndex(values, 5);
|
|
118
|
+
|
|
119
|
+
const getComputedTranslateY = (element: Element): number => {
|
|
120
|
+
const { transform } = window.getComputedStyle(element);
|
|
121
|
+
if (!transform || transform === "none") {
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (transform.startsWith("matrix3d(")) {
|
|
126
|
+
const values = parseTransformValues(transform, "matrix3d(");
|
|
127
|
+
return getMatrix3dTranslateY(values);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (transform.startsWith("matrix(")) {
|
|
131
|
+
const values = parseTransformValues(transform, "matrix(");
|
|
132
|
+
return getMatrixTranslateY(values);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return 0;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const setTranslateY = (element: HTMLElement, next: number): void => {
|
|
139
|
+
element.style.transform = `translate3d(0, ${next}px, 0)`;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const getElementCenterY = (element: Element): number => {
|
|
143
|
+
const rect = element.getBoundingClientRect();
|
|
144
|
+
return rect.top + rect.height / 2;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const getCenteredTranslateY = (
|
|
148
|
+
scrollContainer: HTMLElement,
|
|
149
|
+
list: HTMLElement,
|
|
150
|
+
link: HTMLAnchorElement
|
|
151
|
+
): number => {
|
|
152
|
+
const containerHeight = scrollContainer.clientHeight;
|
|
153
|
+
const listHeight = list.scrollHeight;
|
|
154
|
+
|
|
155
|
+
if (listHeight <= containerHeight + 1) {
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const delta = getElementCenterY(scrollContainer) - getElementCenterY(link);
|
|
160
|
+
const currentTranslate = getComputedTranslateY(list);
|
|
161
|
+
const minTranslate = containerHeight - listHeight;
|
|
162
|
+
return clamp(currentTranslate + delta, minTranslate, 0);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
interface ScrollSpyState {
|
|
166
|
+
activeIndex: number;
|
|
167
|
+
centerActiveItem: boolean;
|
|
168
|
+
entries: TocEntry[];
|
|
169
|
+
isTicking: boolean;
|
|
170
|
+
scrollContainer: HTMLElement | null;
|
|
171
|
+
tocList: HTMLElement | null;
|
|
172
|
+
topOffset: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const centerLinkIfNeeded = (
|
|
176
|
+
state: ScrollSpyState,
|
|
177
|
+
link: HTMLAnchorElement
|
|
178
|
+
): void => {
|
|
179
|
+
const { scrollContainer, tocList } = state;
|
|
180
|
+
if (!scrollContainer || !tocList) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const next = getCenteredTranslateY(scrollContainer, tocList, link);
|
|
185
|
+
const current = getComputedTranslateY(tocList);
|
|
186
|
+
if (Math.abs(current - next) < 0.5) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
setTranslateY(tocList, next);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const setCurrentLocationAttr = (entry: TocEntry | undefined): void => {
|
|
194
|
+
if (!entry) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
entry.link.setAttribute("aria-current", "location");
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const setActiveLink = (state: ScrollSpyState, index: number): void => {
|
|
201
|
+
if (index === state.activeIndex) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const previous = state.entries[state.activeIndex];
|
|
206
|
+
previous?.link.removeAttribute("aria-current");
|
|
207
|
+
|
|
208
|
+
state.activeIndex = index;
|
|
209
|
+
const current = state.entries[state.activeIndex];
|
|
210
|
+
setCurrentLocationAttr(current);
|
|
211
|
+
|
|
212
|
+
if (state.centerActiveItem && current) {
|
|
213
|
+
centerLinkIfNeeded(state, current.link);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const updateActive = (state: ScrollSpyState): void => {
|
|
218
|
+
setActiveLink(state, findActiveIndex(state.entries, state.topOffset));
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const scheduleUpdate = (state: ScrollSpyState): void => {
|
|
222
|
+
if (state.isTicking) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
state.isTicking = true;
|
|
227
|
+
requestAnimationFrame(() => {
|
|
228
|
+
state.isTicking = false;
|
|
229
|
+
updateActive(state);
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const refreshLayout = (state: ScrollSpyState): void => {
|
|
234
|
+
state.topOffset = getTopOffset();
|
|
235
|
+
setScrollMarginTop(state.topOffset);
|
|
236
|
+
measureEntries(state.entries);
|
|
237
|
+
updateActive(state);
|
|
238
|
+
|
|
239
|
+
if (state.centerActiveItem) {
|
|
240
|
+
const current = state.entries[state.activeIndex];
|
|
241
|
+
if (current) {
|
|
242
|
+
centerLinkIfNeeded(state, current.link);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const createState = (): ScrollSpyState | null => {
|
|
248
|
+
const { body } = document;
|
|
249
|
+
const tocRoot = document.querySelector(TOC_ROOT_SELECTOR);
|
|
250
|
+
const entries = tocRoot ? buildEntries(tocRoot) : [];
|
|
251
|
+
if (
|
|
252
|
+
!body ||
|
|
253
|
+
body.dataset.scrollspy !== "1" ||
|
|
254
|
+
!tocRoot ||
|
|
255
|
+
entries.length === 0
|
|
256
|
+
) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const centerActiveItem = body.dataset.scrollspyCenter === "1";
|
|
261
|
+
const scrollContainer = tocRoot.querySelector(TOC_SCROLL_CONTAINER_SELECTOR);
|
|
262
|
+
const tocList = scrollContainer?.querySelector("ul") ?? null;
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
activeIndex: -1,
|
|
266
|
+
centerActiveItem,
|
|
267
|
+
entries,
|
|
268
|
+
isTicking: false,
|
|
269
|
+
scrollContainer:
|
|
270
|
+
scrollContainer instanceof HTMLElement ? scrollContainer : null,
|
|
271
|
+
tocList: tocList instanceof HTMLElement ? tocList : null,
|
|
272
|
+
topOffset: getTopOffset(),
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const start = (state: ScrollSpyState): void => {
|
|
277
|
+
// Disable independent TOC scrolling whenever scrollspy is active.
|
|
278
|
+
// The TOC list position is controlled by JS (either centered or left as-is).
|
|
279
|
+
document.body.dataset.tocFollow = "1";
|
|
280
|
+
|
|
281
|
+
window.addEventListener("scroll", () => scheduleUpdate(state), {
|
|
282
|
+
passive: true,
|
|
283
|
+
});
|
|
284
|
+
window.addEventListener("resize", () => refreshLayout(state));
|
|
285
|
+
window.addEventListener("load", () => {
|
|
286
|
+
refreshLayout(state);
|
|
287
|
+
setTimeout(() => refreshLayout(state), 250);
|
|
288
|
+
setTimeout(() => refreshLayout(state), 1000);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
refreshLayout(state);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const init = (): void => {
|
|
295
|
+
const state = createState();
|
|
296
|
+
if (!state) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
start(state);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
init();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// Optional: this file is here as the obvious place to put server-side code.
|
|
2
|
-
// V1 runs the built-in idcmd server; add custom endpoints via `
|
|
2
|
+
// V1 runs the built-in idcmd server; add custom endpoints via `src/routes/**`.
|
|
3
3
|
|
|
4
4
|
export const serverPlaceholder = true;
|
|
@@ -104,7 +104,7 @@ const TopNavbar = ({
|
|
|
104
104
|
<div class="flex items-center gap-4">
|
|
105
105
|
<a
|
|
106
106
|
href="/"
|
|
107
|
-
class="text-sm font-mono font-medium tracking-tight
|
|
107
|
+
class="text-sm font-mono font-medium tracking-tight lg:hidden"
|
|
108
108
|
data-prefetch="hover"
|
|
109
109
|
>
|
|
110
110
|
<span class="text-muted-foreground">~/</span>
|
|
@@ -151,7 +151,7 @@ header {
|
|
|
151
151
|
display: none;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
@media (min-width:
|
|
154
|
+
@media (min-width: 1024px) {
|
|
155
155
|
.sidebar {
|
|
156
156
|
position: fixed;
|
|
157
157
|
top: 0;
|
|
@@ -185,7 +185,7 @@ header {
|
|
|
185
185
|
flex-direction: column;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
@media (min-width:
|
|
188
|
+
@media (min-width: 1024px) {
|
|
189
189
|
.main-wrapper {
|
|
190
190
|
margin-left: var(--sidebar-width);
|
|
191
191
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|