blume 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +380 -157
- package/dist/cli/index.js.map +23 -22
- package/dist/types/core/data.d.ts +4 -0
- package/dist/types/core/i18n-ui.d.ts +50 -0
- package/dist/types/core/schema.d.ts +328 -39
- package/dist/types/core/types.d.ts +8 -0
- package/docs/configuration/ai.mdx +56 -0
- package/docs/configuration/seo.mdx +59 -1
- package/docs/configuration/theming.mdx +14 -9
- package/docs/content/meta.mdx +3 -17
- package/docs/content/navigation.mdx +41 -4
- package/package.json +3 -1
- package/src/ai/agent-readability.ts +97 -0
- package/src/ai/ask-context.ts +131 -8
- package/src/ai/ask-data.ts +4 -1
- package/src/astro/generate.ts +4 -0
- package/src/astro/templates.ts +24 -5
- package/src/cli/commands/build.ts +15 -0
- package/src/cli/commands/dev.ts +31 -14
- package/src/cli/dev-lock.ts +94 -21
- package/src/components/content/GithubInfo.astro +11 -10
- package/src/components/content/TypeTable.astro +8 -3
- package/src/components/islands/AskAI.astro +66 -2
- package/src/components/islands/ask-ai.tsx +289 -53
- package/src/components/layout/Header.astro +1 -1
- package/src/components/layout/NavTree.astro +1 -1
- package/src/components/layout/PageActions.astro +73 -30
- package/src/components/layout/RootLayout.astro +48 -2
- package/src/core/data.ts +4 -0
- package/src/core/graph.ts +7 -2
- package/src/core/i18n-ui.ts +5 -0
- package/src/core/nav-diagnostics.ts +7 -0
- package/src/core/navigation.ts +38 -12
- package/src/core/schema.ts +124 -9
- package/src/core/sources/filesystem.ts +5 -1
- package/src/core/sources/watch.ts +43 -12
- package/src/core/types.ts +9 -0
- package/src/deploy/robots.ts +37 -4
- package/src/openapi/scalar.ts +1 -1
- package/src/search/documents.ts +9 -2
- package/src/theme/palette.ts +21 -14
package/src/astro/templates.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, isAbsolute, join, relative } from "pathe";
|
|
|
5
5
|
import { askBackendRuntimeDep } from "../ai/ask.ts";
|
|
6
6
|
import type { AskBackend } from "../ai/ask.ts";
|
|
7
7
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
8
|
+
import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
|
|
8
9
|
import type { ProjectContext } from "../core/types.ts";
|
|
9
10
|
import { hasScalarReferences } from "../openapi/references.ts";
|
|
10
11
|
import { searchProviderMeta } from "../search/providers.ts";
|
|
@@ -354,9 +355,17 @@ export default defineConfig({
|
|
|
354
355
|
// native bindings resolve at runtime and isolated linkers don't bundle
|
|
355
356
|
// symlinked store copies (which would surface their children as unresolvable
|
|
356
357
|
// imports). See RENDER_EXTERNAL_DEPS / prerenderDepsPlugin.
|
|
358
|
+
//
|
|
359
|
+
// The SSR externals go through the legacy \`ssr.external\` key rather than
|
|
360
|
+
// \`environments.ssr\`: defining a user-owned \`environments.ssr\` block
|
|
361
|
+
// collides with the internal environment Astro 7 builds the server under and
|
|
362
|
+
// detaches the adapter's server entrypoint from the rolldown input, so the
|
|
363
|
+
// SSR entry is emitted as \`index.mjs\` instead of the \`entry.mjs\` the
|
|
364
|
+
// Vercel adapter's \`astro:build:done\` hook then fails to find. \`prerender\`
|
|
365
|
+
// is Astro-only and has no legacy equivalent, so it stays under \`environments\`.
|
|
366
|
+
ssr: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} },
|
|
357
367
|
environments: {
|
|
358
368
|
prerender: { resolve: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} } },
|
|
359
|
-
ssr: { resolve: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} } },
|
|
360
369
|
},
|
|
361
370
|
resolve: {
|
|
362
371
|
alias: {
|
|
@@ -432,7 +441,15 @@ export const contentConfigTemplate = (options: {
|
|
|
432
441
|
? [
|
|
433
442
|
...config.content.include,
|
|
434
443
|
...(config.content.exclude ?? []).map((pattern) => `!${pattern}`),
|
|
435
|
-
|
|
444
|
+
// Mirror the filesystem scan's baseline ignores (see BLUME_IGNORE_DIRS):
|
|
445
|
+
// Astro's content layer roots at the project dir, so a `.`-wide content
|
|
446
|
+
// root would otherwise re-ingest dependency trees and build output —
|
|
447
|
+
// e.g. a prior `dist/*.mdx` render — and crash the content-module graph.
|
|
448
|
+
// The runtime dir (`.blume`, or a custom distDir) is excluded precisely
|
|
449
|
+
// by `outDirIgnore` instead, so it's left out of this baseline.
|
|
450
|
+
...BLUME_IGNORE_DIRS.filter((dir) => dir !== ".blume").map(
|
|
451
|
+
(dir) => `!**/${dir}/**`
|
|
452
|
+
),
|
|
436
453
|
...outDirIgnore,
|
|
437
454
|
]
|
|
438
455
|
: [];
|
|
@@ -866,7 +883,7 @@ const siteHost = (() => {
|
|
|
866
883
|
|
|
867
884
|
export async function GET({ props }) {
|
|
868
885
|
const png = await renderOgImage({
|
|
869
|
-
accent: data.config.theme.accent,
|
|
886
|
+
accent: data.config.theme.accent.light,
|
|
870
887
|
brand: data.config.title,
|
|
871
888
|
description: data.config.description,
|
|
872
889
|
logo: data.config.logo?.svg,
|
|
@@ -939,7 +956,7 @@ export const catchAllPageTemplate = (options: {
|
|
|
939
956
|
? 'import AskAI from "blume/components/islands/AskAI.astro";\n'
|
|
940
957
|
: "";
|
|
941
958
|
const askSlot = options.askEnabled
|
|
942
|
-
? '\n <AskAI slot="ask" strings={ui.ask} />'
|
|
959
|
+
? '\n <AskAI slot="ask" strings={ui.ask} suggestions={data.config.ask?.suggestions ?? []} />'
|
|
943
960
|
: "";
|
|
944
961
|
const mathImport = options.mathEnabled
|
|
945
962
|
? 'import Math from "blume/components/content/Math.astro";\n'
|
|
@@ -1210,7 +1227,9 @@ export const changelogIndexTemplate = (options: {
|
|
|
1210
1227
|
const askImport = options.askEnabled
|
|
1211
1228
|
? 'import AskAI from "blume/components/islands/AskAI.astro";\n'
|
|
1212
1229
|
: "";
|
|
1213
|
-
const askSlot = options.askEnabled
|
|
1230
|
+
const askSlot = options.askEnabled
|
|
1231
|
+
? '\n <AskAI slot="ask" strings={data.ui.ask} suggestions={data.config.ask?.suggestions ?? []} />'
|
|
1232
|
+
: "";
|
|
1214
1233
|
const clientData = options.needsReact
|
|
1215
1234
|
? '\n clientData={{ config: data.config, navigation: data.navigation, page: { route: "/changelog", title: data.config.title + " changelog" } }}'
|
|
1216
1235
|
: "";
|
|
@@ -5,6 +5,7 @@ import { build } from "astro";
|
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
import { join } from "pathe";
|
|
7
7
|
|
|
8
|
+
import { buildAgentReadability } from "../../ai/agent-readability.ts";
|
|
8
9
|
import { buildLlmsFiles } from "../../ai/llms.ts";
|
|
9
10
|
import { ensureGitignore } from "../../core/gitignore.ts";
|
|
10
11
|
import type { BlumeProject } from "../../core/project-graph.ts";
|
|
@@ -218,6 +219,19 @@ const publishBuildArtifacts = async (
|
|
|
218
219
|
logger.success("Generated robots.txt");
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
const agentReadability = buildAgentReadability(project);
|
|
223
|
+
if (
|
|
224
|
+
agentReadability &&
|
|
225
|
+
!existsSync(join(distDir, "agent-readability.json"))
|
|
226
|
+
) {
|
|
227
|
+
await writeFile(
|
|
228
|
+
join(distDir, "agent-readability.json"),
|
|
229
|
+
`${JSON.stringify(agentReadability, null, 2)}\n`,
|
|
230
|
+
"utf-8"
|
|
231
|
+
);
|
|
232
|
+
logger.success("Generated agent-readability.json");
|
|
233
|
+
}
|
|
234
|
+
|
|
221
235
|
await emitRedirectFiles(project.config, distDir);
|
|
222
236
|
|
|
223
237
|
const { config } = project;
|
|
@@ -231,6 +245,7 @@ const publishBuildArtifacts = async (
|
|
|
231
245
|
`Redirects ${config.redirects.length}`,
|
|
232
246
|
`Sitemap ${sitemap ? "yes" : "no (set deployment.site)"}`,
|
|
233
247
|
`Robots ${robots ? "yes" : "no"}`,
|
|
248
|
+
`Agent JSON ${agentReadability ? "yes" : "no"}`,
|
|
234
249
|
`LLM files ${config.ai.llmsTxt ? "yes" : "no"}`,
|
|
235
250
|
`Server features ${features.length > 0 ? features.join(", ") : "none"}`,
|
|
236
251
|
].join("\n")
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -6,9 +6,15 @@ import { defineCommand } from "citty";
|
|
|
6
6
|
import { generateRuntime } from "../../astro/generate.ts";
|
|
7
7
|
import { showBlumeErrorOverlay } from "../../astro/integration.ts";
|
|
8
8
|
import { scanProject } from "../../core/project-graph.ts";
|
|
9
|
+
import { resolveRuntimeDir } from "../../core/project.ts";
|
|
9
10
|
import { parsePort } from "../args.ts";
|
|
10
11
|
import { coalescedRunner } from "../coalesce.ts";
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
acquireDevLock,
|
|
14
|
+
describeDevLock,
|
|
15
|
+
readDevLock,
|
|
16
|
+
updateDevLockPort,
|
|
17
|
+
} from "../dev-lock.ts";
|
|
12
18
|
import { logger } from "../log.ts";
|
|
13
19
|
import { prepareProject } from "../prepare.ts";
|
|
14
20
|
|
|
@@ -47,6 +53,23 @@ export const devCommand = defineCommand({
|
|
|
47
53
|
const explicitPort = parsePort(args.port);
|
|
48
54
|
const port = explicitPort ?? 4321;
|
|
49
55
|
const devServerUrl = `http://localhost:${port}`;
|
|
56
|
+
|
|
57
|
+
// Claim the shared `.blume` dir BEFORE preparing: `prepareProject`
|
|
58
|
+
// regenerates the runtime, so even a refused second dev server would
|
|
59
|
+
// otherwise clobber the running one's generated tree (with this
|
|
60
|
+
// invocation's port baked in) on its way out. Dev never relocates the
|
|
61
|
+
// runtime dir, so the lock always lives at `<root>/.blume`.
|
|
62
|
+
const outDir = resolveRuntimeDir(root);
|
|
63
|
+
const running = readDevLock(outDir);
|
|
64
|
+
if (running) {
|
|
65
|
+
logger.error(
|
|
66
|
+
`A \`blume dev\` server is already running${describeDevLock(running)} in this project. Reuse that server instead of starting a second one — two dev servers would corrupt the shared .blume dir. If it crashed, delete .blume/dev.lock.`
|
|
67
|
+
);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
const releaseLock = acquireDevLock(outDir, port);
|
|
71
|
+
process.on("exit", releaseLock);
|
|
72
|
+
|
|
50
73
|
const project = await prepareProject({
|
|
51
74
|
devServerUrl,
|
|
52
75
|
mode: "dev",
|
|
@@ -56,19 +79,6 @@ export const devCommand = defineCommand({
|
|
|
56
79
|
strict: args.strict,
|
|
57
80
|
});
|
|
58
81
|
|
|
59
|
-
// Claim the shared `.blume` dir so a concurrent build/eject/sync refuses
|
|
60
|
-
// rather than regenerating or deleting it out from under this server. A
|
|
61
|
-
// second dev server would fight over the same generated tree the same
|
|
62
|
-
// way, so it must refuse too instead of silently clobbering the lock.
|
|
63
|
-
if (isDevLocked(project.context.outDir)) {
|
|
64
|
-
logger.error(
|
|
65
|
-
"Another `blume dev` is already running in this project; two dev servers would corrupt the shared .blume dir. Stop the other one first (or delete .blume/dev.lock if it crashed)."
|
|
66
|
-
);
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
const releaseLock = acquireDevLock(project.context.outDir);
|
|
70
|
-
process.on("exit", releaseLock);
|
|
71
|
-
|
|
72
82
|
const server = await dev({
|
|
73
83
|
logLevel: args.debug ? "debug" : "info",
|
|
74
84
|
root: project.context.outDir,
|
|
@@ -79,6 +89,13 @@ export const devCommand = defineCommand({
|
|
|
79
89
|
},
|
|
80
90
|
});
|
|
81
91
|
|
|
92
|
+
// Vite bumps to the next free port when the default is taken, so record
|
|
93
|
+
// the port the server actually bound — the lock's URL is what a refused
|
|
94
|
+
// second invocation tells its caller to reuse.
|
|
95
|
+
if (server.address.port !== port) {
|
|
96
|
+
updateDevLockPort(outDir, server.address.port);
|
|
97
|
+
}
|
|
98
|
+
|
|
82
99
|
// Mirror any initial diagnostics into the browser overlay now the server
|
|
83
100
|
// (and its HMR channel) is up.
|
|
84
101
|
showBlumeErrorOverlay(project.diagnostics);
|
package/src/cli/dev-lock.ts
CHANGED
|
@@ -14,26 +14,48 @@ import { logger } from "./log.ts";
|
|
|
14
14
|
/**
|
|
15
15
|
* A best-effort PID lock in the shared `.blume/` runtime dir. `blume dev`
|
|
16
16
|
* regenerates and serves `.blume` continuously, so a concurrent `build`,
|
|
17
|
-
* `eject`, or
|
|
17
|
+
* `eject`, or second `dev` that regenerates or deletes it out from under the
|
|
18
18
|
* running Vite server corrupts the dev session. The lock lets those commands
|
|
19
|
-
* detect a live dev server and refuse
|
|
19
|
+
* detect a live dev server and refuse — and, because it records the server's
|
|
20
|
+
* port, point the caller (often an agent that just tried to start its own
|
|
21
|
+
* server) at the URL to reuse instead.
|
|
20
22
|
*/
|
|
21
23
|
|
|
24
|
+
export interface DevLockInfo {
|
|
25
|
+
pid: number;
|
|
26
|
+
/** Port the dev server is bound to, when known. */
|
|
27
|
+
port?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
const lockPath = (outDir: string): string => join(outDir, "dev.lock");
|
|
23
31
|
|
|
32
|
+
const isValidPid = (pid: unknown): pid is number =>
|
|
33
|
+
typeof pid === "number" && Number.isInteger(pid) && pid > 0;
|
|
34
|
+
|
|
24
35
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
36
|
+
* Parse a lock file body. Current locks are JSON (`{"pid":123,"port":3001}`);
|
|
37
|
+
* a bare integer (the pre-port format) still parses as a pid-only lock.
|
|
27
38
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
const parseLock = (raw: string): DevLockInfo | null => {
|
|
40
|
+
let data: unknown;
|
|
41
|
+
try {
|
|
42
|
+
data = JSON.parse(raw.trim());
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
32
45
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return false;
|
|
46
|
+
if (isValidPid(data)) {
|
|
47
|
+
return { pid: data };
|
|
36
48
|
}
|
|
49
|
+
if (typeof data === "object" && data !== null) {
|
|
50
|
+
const { pid, port } = data as { pid?: unknown; port?: unknown };
|
|
51
|
+
if (isValidPid(pid)) {
|
|
52
|
+
return typeof port === "number" ? { pid, port } : { pid };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const isProcessAlive = (pid: number): boolean => {
|
|
37
59
|
try {
|
|
38
60
|
// Signal 0 probes liveness without actually signaling the process.
|
|
39
61
|
process.kill(pid, 0);
|
|
@@ -45,15 +67,53 @@ export const isDevLocked = (outDir: string): boolean => {
|
|
|
45
67
|
}
|
|
46
68
|
};
|
|
47
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Read the lock on `outDir` held by a live `blume dev`, or null. A lock left
|
|
72
|
+
* by a process that has since exited (stale) is treated as absent.
|
|
73
|
+
*/
|
|
74
|
+
export const readDevLock = (outDir: string): DevLockInfo | null => {
|
|
75
|
+
const path = lockPath(outDir);
|
|
76
|
+
if (!existsSync(path)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const lock = parseLock(readFileSync(path, "utf-8"));
|
|
80
|
+
return lock && isProcessAlive(lock.pid) ? lock : null;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Whether another live `blume dev` holds the lock on `outDir`. */
|
|
84
|
+
export const isDevLocked = (outDir: string): boolean =>
|
|
85
|
+
readDevLock(outDir) !== null;
|
|
86
|
+
|
|
87
|
+
const writeLock = (outDir: string, port?: number): void => {
|
|
88
|
+
writeFileSync(
|
|
89
|
+
lockPath(outDir),
|
|
90
|
+
JSON.stringify({
|
|
91
|
+
pid: process.pid,
|
|
92
|
+
...(port === undefined ? {} : { port }),
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const ownsLock = (outDir: string): boolean => {
|
|
98
|
+
const path = lockPath(outDir);
|
|
99
|
+
if (!existsSync(path)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
return parseLock(readFileSync(path, "utf-8"))?.pid === process.pid;
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
48
109
|
/**
|
|
49
110
|
* Write the current process's dev lock into `outDir` and return a release
|
|
50
111
|
* function. The release only removes the file if it's still ours, so a newer
|
|
51
112
|
* dev server's lock is never clobbered.
|
|
52
113
|
*/
|
|
53
|
-
export const acquireDevLock = (outDir: string): (() => void) => {
|
|
54
|
-
const path = lockPath(outDir);
|
|
114
|
+
export const acquireDevLock = (outDir: string, port?: number): (() => void) => {
|
|
55
115
|
mkdirSync(outDir, { recursive: true });
|
|
56
|
-
|
|
116
|
+
writeLock(outDir, port);
|
|
57
117
|
let released = false;
|
|
58
118
|
return () => {
|
|
59
119
|
if (released) {
|
|
@@ -61,11 +121,8 @@ export const acquireDevLock = (outDir: string): (() => void) => {
|
|
|
61
121
|
}
|
|
62
122
|
released = true;
|
|
63
123
|
try {
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
readFileSync(path, "utf-8").trim() === String(process.pid)
|
|
67
|
-
) {
|
|
68
|
-
rmSync(path, { force: true });
|
|
124
|
+
if (ownsLock(outDir)) {
|
|
125
|
+
rmSync(lockPath(outDir), { force: true });
|
|
69
126
|
}
|
|
70
127
|
} catch {
|
|
71
128
|
// Best-effort cleanup; a stale lock is handled by the liveness check.
|
|
@@ -73,6 +130,21 @@ export const acquireDevLock = (outDir: string): (() => void) => {
|
|
|
73
130
|
};
|
|
74
131
|
};
|
|
75
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Rewrite this process's lock with the port the server actually bound (the
|
|
135
|
+
* lock is acquired before the server starts, and Vite may bump a busy port).
|
|
136
|
+
* A lock owned by another process is left alone.
|
|
137
|
+
*/
|
|
138
|
+
export const updateDevLockPort = (outDir: string, port: number): void => {
|
|
139
|
+
if (ownsLock(outDir)) {
|
|
140
|
+
writeLock(outDir, port);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** Human-readable location of a locked dev server, e.g. " at http://localhost:3001". */
|
|
145
|
+
export const describeDevLock = (lock: DevLockInfo): string =>
|
|
146
|
+
lock.port === undefined ? "" : ` at http://localhost:${lock.port}`;
|
|
147
|
+
|
|
76
148
|
/**
|
|
77
149
|
* Exit with an error when a live `blume dev` owns the runtime dir under `root`.
|
|
78
150
|
* `action` names the operation being refused (e.g. "building"). `runtimeDir`
|
|
@@ -85,9 +157,10 @@ export const refuseIfDevRunning = (
|
|
|
85
157
|
action: string,
|
|
86
158
|
runtimeDir?: string
|
|
87
159
|
): void => {
|
|
88
|
-
|
|
160
|
+
const lock = readDevLock(resolveRuntimeDir(root, runtimeDir));
|
|
161
|
+
if (lock) {
|
|
89
162
|
logger.error(
|
|
90
|
-
`A \`blume dev\` server is running
|
|
163
|
+
`A \`blume dev\` server is running${describeDevLock(lock)}; ${action} would corrupt its .blume runtime. Reuse that server, stop it first, or re-run with --isolated to build/verify against .blume-verify without touching it.`
|
|
91
164
|
);
|
|
92
165
|
process.exit(1);
|
|
93
166
|
}
|
|
@@ -3,10 +3,17 @@
|
|
|
3
3
|
// at build time (no client JS). Pass `owner`/`repo`, or omit them to use the
|
|
4
4
|
// repo from blume.config. A `GITHUB_TOKEN` env var lifts the API rate limit.
|
|
5
5
|
// If the API is unreachable the card still renders, just without counts.
|
|
6
|
+
import { resolveIcon } from "../../theme/icons.ts";
|
|
6
7
|
import data from "blume:data";
|
|
7
8
|
import { GITHUB_MARK } from "../github-mark.ts";
|
|
8
9
|
import { fetchRepositoryInfo } from "./github-info.ts";
|
|
9
10
|
|
|
11
|
+
// Star/fork glyphs resolve from Lucide at build time; the `<svg>` wrappers below
|
|
12
|
+
// supply size and the shared `statIcon` class. (The GitHub octocat above is a
|
|
13
|
+
// brand mark, not a Lucide icon, so it stays as GITHUB_MARK.)
|
|
14
|
+
const starIcon = resolveIcon("star")?.body ?? "";
|
|
15
|
+
const forkIcon = resolveIcon("git-fork")?.body ?? "";
|
|
16
|
+
|
|
10
17
|
interface Props {
|
|
11
18
|
owner?: string;
|
|
12
19
|
repo?: string;
|
|
@@ -77,9 +84,8 @@ const statIcon =
|
|
|
77
84
|
stroke-width="2"
|
|
78
85
|
viewBox="0 0 24 24"
|
|
79
86
|
width="14"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
</svg>
|
|
87
|
+
set:html={starIcon}
|
|
88
|
+
/>
|
|
83
89
|
<span>{numbers.format(info.stars)}</span>
|
|
84
90
|
</span>
|
|
85
91
|
<span class="flex items-center gap-1.5">
|
|
@@ -94,13 +100,8 @@ const statIcon =
|
|
|
94
100
|
stroke-width="2"
|
|
95
101
|
viewBox="0 0 24 24"
|
|
96
102
|
width="14"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
<circle cx="6" cy="6" r="3" />
|
|
100
|
-
<circle cx="18" cy="6" r="3" />
|
|
101
|
-
<path d="M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9" />
|
|
102
|
-
<path d="M12 12v3" />
|
|
103
|
-
</svg>
|
|
103
|
+
set:html={forkIcon}
|
|
104
|
+
/>
|
|
104
105
|
<span>{numbers.format(info.forks)}</span>
|
|
105
106
|
</span>
|
|
106
107
|
</span>
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
// the accent-colored prop name (with a `?` suffix when optional) and its type;
|
|
5
5
|
// expanded, the row lifts into a card showing the description and a Type/Default
|
|
6
6
|
// detail grid. Uses native <details>/<summary> so the core stays React-free.
|
|
7
|
+
import { resolveIcon } from "../../theme/icons.ts";
|
|
8
|
+
|
|
9
|
+
// The row disclosure caret; resolved from Lucide at build time and injected into
|
|
10
|
+
// the summary's `<svg>` (which supplies size and the rotate-on-open transition).
|
|
11
|
+
const chevronDown = resolveIcon("chevron-down")?.body ?? "";
|
|
12
|
+
|
|
7
13
|
interface TypeEntry {
|
|
8
14
|
default?: string;
|
|
9
15
|
description?: string;
|
|
@@ -64,9 +70,8 @@ const columns = "grid grid-cols-[1fr_2fr_auto] items-center gap-4";
|
|
|
64
70
|
stroke-width="2"
|
|
65
71
|
viewBox="0 0 24 24"
|
|
66
72
|
width="16"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
</svg>
|
|
73
|
+
set:html={chevronDown}
|
|
74
|
+
/>
|
|
70
75
|
</summary>
|
|
71
76
|
|
|
72
77
|
<div class="border-border border-t px-3 py-3 text-xs">
|
|
@@ -1,12 +1,76 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
3
|
+
import { resolveIcon } from "../../theme/icons.ts";
|
|
3
4
|
import AskAI from "./ask-ai.tsx";
|
|
4
5
|
|
|
6
|
+
interface Suggestion {
|
|
7
|
+
icon?: string;
|
|
8
|
+
label: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
interface Props {
|
|
6
12
|
strings?: UIStrings["ask"];
|
|
13
|
+
suggestions?: Suggestion[];
|
|
7
14
|
}
|
|
8
15
|
|
|
9
|
-
const { strings } = Astro.props;
|
|
16
|
+
const { strings, suggestions = [] } = Astro.props;
|
|
17
|
+
|
|
18
|
+
// Icons resolve to inline SVG here (server-only module), so the client island
|
|
19
|
+
// gets ready-to-render markup rather than a name it can't resolve. Bare Lucide
|
|
20
|
+
// bodies carry no styling, so wrap them in a stroke-styled root like Icon.astro.
|
|
21
|
+
const svgFor = (name: string | undefined): string | null => {
|
|
22
|
+
const resolved = resolveIcon(name ?? "sparkles") ?? resolveIcon("sparkles");
|
|
23
|
+
return resolved
|
|
24
|
+
? `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${resolved.viewBox}" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${resolved.body}</svg>`
|
|
25
|
+
: null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const items = suggestions.map((suggestion) => ({
|
|
29
|
+
icon: svgFor(suggestion.icon),
|
|
30
|
+
label: suggestion.label,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
// The panel's own chrome glyphs resolve here too, so the client island carries
|
|
34
|
+
// no icon data. Each value is the bare Lucide body; the island's <Glyph> wraps
|
|
35
|
+
// it in a sized, stroke-styled <svg>. Keyed by role → Lucide icon name.
|
|
36
|
+
const bodyFor = (name: string): string => resolveIcon(name)?.body ?? "";
|
|
37
|
+
const icons = {
|
|
38
|
+
arrowUp: bodyFor("arrow-up"),
|
|
39
|
+
chat: bodyFor("message-circle"),
|
|
40
|
+
clear: bodyFor("trash-2"),
|
|
41
|
+
close: bodyFor("chevrons-right"),
|
|
42
|
+
copy: bodyFor("copy"),
|
|
43
|
+
};
|
|
10
44
|
---
|
|
11
45
|
|
|
12
|
-
<AskAI client:load strings={strings} />
|
|
46
|
+
<AskAI client:load icons={icons} strings={strings} suggestions={items} />
|
|
47
|
+
|
|
48
|
+
<style is:global>
|
|
49
|
+
:root {
|
|
50
|
+
--blume-ask-width: min(30rem, 100vw);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Desktop docks the panel and shrinks the page to fit, like vercel.com/docs.
|
|
54
|
+
On smaller screens the panel is a full-width overlay (no push). */
|
|
55
|
+
@media (min-width: 1024px) {
|
|
56
|
+
body {
|
|
57
|
+
transition: padding-inline-end 0.2s ease-out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
body[data-blume-ask="open"] {
|
|
61
|
+
padding-inline-end: var(--blume-ask-width);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* The table of contents only shows at xl; reclaim its column for the article
|
|
66
|
+
while the panel is open so the shrunken content still has room to breathe. */
|
|
67
|
+
@media (min-width: 1280px) {
|
|
68
|
+
body[data-blume-ask="open"] [data-blume-toc] {
|
|
69
|
+
display: none;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
body[data-blume-ask="open"] [data-blume-doc-grid] {
|
|
73
|
+
grid-template-columns: 17.5rem minmax(0, 1fr);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
</style>
|