blume 1.0.2 → 1.0.4
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/CHANGELOG.md +37 -0
- package/dist/cli/index.js +496 -295
- package/dist/cli/index.js.map +18 -17
- package/dist/types/core/config-input.d.ts +44 -22
- package/dist/types/core/config.d.ts +3 -3
- package/dist/types/core/data.d.ts +12 -0
- package/dist/types/core/i18n-ui.d.ts +136 -136
- package/dist/types/core/schema.d.ts +502 -384
- package/dist/types/core/types.d.ts +10 -0
- package/dist/types/openapi/references.d.ts +12 -7
- package/docs/advanced/api-reference.mdx +11 -3
- package/docs/advanced/custom-pages.mdx +2 -0
- package/docs/configuration/ai.mdx +6 -4
- package/docs/configuration/index.mdx +6 -8
- package/docs/configuration/seo.mdx +20 -1
- package/docs/content/components.mdx +26 -5
- package/docs/content/navigation.mdx +10 -0
- package/docs/content/syntax.mdx +116 -4
- package/package.json +1 -1
- package/skills/blume-migrate/SKILL.md +170 -0
- package/skills/blume-migrate/assets/oxfmt@0.55.0.patch +20 -0
- package/skills/blume-migrate/references/docusaurus.md +95 -0
- package/skills/blume-migrate/references/fumadocs.md +95 -0
- package/skills/blume-migrate/references/mintlify.md +155 -0
- package/skills/blume-migrate/references/monorepo.md +224 -0
- package/skills/blume-migrate/references/nextra.md +76 -0
- package/skills/blume-migrate/references/starlight.md +116 -0
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +466 -0
- package/src/ai/agent-readability.ts +3 -3
- package/src/ai/mcp/data.ts +2 -2
- package/src/astro/component-slots.ts +3 -2
- package/src/astro/generate.ts +97 -30
- package/src/astro/templates.ts +140 -53
- package/src/blume-modules.d.ts +6 -0
- package/src/components/content/Callout.astro +8 -2
- package/src/components/content/Prompt.astro +25 -13
- package/src/components/layout/Header.astro +19 -10
- package/src/components/layout/Logo.astro +13 -1
- package/src/components/layout/PageFeedback.astro +1 -1
- package/src/components/layout/PageLayout.astro +11 -11
- package/src/components/layout/Pagination.astro +6 -6
- package/src/components/layout/ReferenceLayout.astro +1 -0
- package/src/components/layout/RootLayout.astro +10 -11
- package/src/components/layout/Search.astro +1 -1
- package/src/components/layout/nav-utils.ts +9 -7
- package/src/core/config-input.ts +47 -27
- package/src/core/config.ts +3 -3
- package/src/core/data.ts +9 -1
- package/src/core/navigation.ts +55 -13
- package/src/core/schema.ts +32 -15
- package/src/core/server-features.ts +1 -1
- package/src/core/sources/watch.ts +5 -0
- package/src/core/types.ts +10 -0
- package/src/deploy/adapter-output.ts +11 -1
- package/src/markdown/index.ts +2 -0
- package/src/markdown/language-icon.ts +2 -1
- package/src/markdown/table-wrap.ts +43 -0
- package/src/og/card.ts +39 -12
- package/src/og/index.ts +1 -1
- package/src/og/logo.ts +21 -0
- package/src/openapi/references.ts +19 -16
- package/src/registry/eject.ts +11 -5
- package/src/theme/entry.ts +50 -5
package/src/astro/templates.ts
CHANGED
|
@@ -80,6 +80,30 @@ const ADAPTER_OPTIONS: Record<string, string> = {
|
|
|
80
80
|
node: '{ mode: "standalone" }',
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
const WRANGLER_CONFIG_FILES = [
|
|
84
|
+
"wrangler.jsonc",
|
|
85
|
+
"wrangler.json",
|
|
86
|
+
"wrangler.toml",
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const resolveCloudflareAdapterArgs = (context: ProjectContext): string => {
|
|
90
|
+
const args: string[] = ['prerenderEnvironment: "node"'];
|
|
91
|
+
const wranglerPath = WRANGLER_CONFIG_FILES.map((file) =>
|
|
92
|
+
join(context.root, file)
|
|
93
|
+
).find((file) => existsSync(file));
|
|
94
|
+
if (wranglerPath) {
|
|
95
|
+
let configPath = relative(context.outDir, wranglerPath);
|
|
96
|
+
// The wrangler config always lives at the project root, above the `.blume`
|
|
97
|
+
// runtime, so `relative` yields a `../…` path; normalize the theoretical
|
|
98
|
+
// sibling case to an explicit `./` so it reads as a relative import.
|
|
99
|
+
if (!configPath.startsWith(".") && !configPath.startsWith("/")) {
|
|
100
|
+
configPath = `./${configPath}`;
|
|
101
|
+
}
|
|
102
|
+
args.push(`configPath: ${JSON.stringify(configPath)}`);
|
|
103
|
+
}
|
|
104
|
+
return `{ ${args.join(", ")} }`;
|
|
105
|
+
};
|
|
106
|
+
|
|
83
107
|
/**
|
|
84
108
|
* Integration packages the generated runtime imports. Declaring them in
|
|
85
109
|
* `.blume/package.json` lets Astro's framework-package crawl discover and bundle
|
|
@@ -222,6 +246,8 @@ export const astroConfigTemplate = (options: {
|
|
|
222
246
|
needsSvelte?: boolean;
|
|
223
247
|
pages: BlumePageRoute[];
|
|
224
248
|
contentRoutes: string[];
|
|
249
|
+
/** The generated Ask trigger (`blume:ask`); renders nothing when Ask is off. */
|
|
250
|
+
askPath: string;
|
|
225
251
|
dataPath: string;
|
|
226
252
|
examplesPath: string;
|
|
227
253
|
/** The example-preview Tailwind entry (`blume:examples-theme`). */
|
|
@@ -240,6 +266,7 @@ export const astroConfigTemplate = (options: {
|
|
|
240
266
|
}): string => {
|
|
241
267
|
const { context, config, needsReact, pages, dataPath, themePath } = options;
|
|
242
268
|
const {
|
|
269
|
+
askPath,
|
|
243
270
|
contentRoutes,
|
|
244
271
|
examplesPath,
|
|
245
272
|
examplesThemePath,
|
|
@@ -260,10 +287,15 @@ export const astroConfigTemplate = (options: {
|
|
|
260
287
|
server && deployment.adapter
|
|
261
288
|
? `import adapter from "${ADAPTER_IMPORTS[deployment.adapter]}";\n`
|
|
262
289
|
: "";
|
|
263
|
-
const adapterArgs =
|
|
264
|
-
server
|
|
265
|
-
|
|
266
|
-
|
|
290
|
+
const adapterArgs = (() => {
|
|
291
|
+
if (!server || !deployment.adapter) {
|
|
292
|
+
return "";
|
|
293
|
+
}
|
|
294
|
+
if (deployment.adapter === "cloudflare") {
|
|
295
|
+
return resolveCloudflareAdapterArgs(context);
|
|
296
|
+
}
|
|
297
|
+
return ADAPTER_OPTIONS[deployment.adapter] ?? "";
|
|
298
|
+
})();
|
|
267
299
|
const adapterOption =
|
|
268
300
|
server && deployment.adapter ? `\n adapter: adapter(${adapterArgs}),` : "";
|
|
269
301
|
|
|
@@ -407,6 +439,18 @@ export default defineConfig({
|
|
|
407
439
|
devToolbar: { enabled: false },
|
|
408
440
|
vite: {
|
|
409
441
|
plugins: [tailwindcss(), prerenderDepsPlugin(), serverAppResolvePlugin()],
|
|
442
|
+
// Mermaid (lazy-loaded client-side for diagrams) statically imports dayjs as
|
|
443
|
+
// CJS (\`dayjs/dayjs.min.js\`). In dev, an un-pre-bundled dependency is served
|
|
444
|
+
// as raw ESM, and that UMD file exposes no \`default\` export, so mermaid
|
|
445
|
+
// throws on load and diagrams render blank. Forcing mermaid through the dep
|
|
446
|
+
// optimizer bundles dayjs with correct CJS interop. In a standalone install
|
|
447
|
+
// Blume's dynamic \`import("mermaid")\` lives inside \`node_modules/blume\`,
|
|
448
|
+
// which Vite's optimizer scan doesn't crawl, so mermaid is never discovered
|
|
449
|
+
// on its own — hence the explicit include. mermaid resolves through the
|
|
450
|
+
// \`blume\` package (it isn't a direct dep of the generated project), so the
|
|
451
|
+
// nested \`blume > mermaid\` form is required. Production (Rollup) already
|
|
452
|
+
// handles the interop, so this only affects dev.
|
|
453
|
+
optimizeDeps: { include: ["blume > mermaid"] },
|
|
410
454
|
// Blume's render-time deps are forced external on both build environments so
|
|
411
455
|
// native bindings resolve at runtime and isolated linkers don't bundle
|
|
412
456
|
// symlinked store copies (which would surface their children as unresolvable
|
|
@@ -425,6 +469,7 @@ export default defineConfig({
|
|
|
425
469
|
},
|
|
426
470
|
resolve: {
|
|
427
471
|
alias: {
|
|
472
|
+
"blume:ask": ${JSON.stringify(askPath)},
|
|
428
473
|
"blume:data": ${JSON.stringify(dataPath)},
|
|
429
474
|
"blume:examples": ${JSON.stringify(examplesPath)},
|
|
430
475
|
"blume:examples-theme": ${JSON.stringify(examplesThemePath)},
|
|
@@ -694,6 +739,42 @@ ${handler}
|
|
|
694
739
|
`;
|
|
695
740
|
};
|
|
696
741
|
|
|
742
|
+
/**
|
|
743
|
+
* Generate `.blume/src/generated/Ask.astro` — the component behind the
|
|
744
|
+
* `blume:ask` alias that the shared header renders in place of a per-page slot.
|
|
745
|
+
*
|
|
746
|
+
* The header can't import the Ask AI island directly: it's a React component, so
|
|
747
|
+
* the import alone would drag the JSX renderer into the module graph of every
|
|
748
|
+
* project — including the ones that never enable Ask AI and therefore have no
|
|
749
|
+
* React integration wired into their generated Astro config (see `needsReact`).
|
|
750
|
+
* Routing the import through a generated component keeps that dependency behind
|
|
751
|
+
* the config switch: enabled projects get the island, disabled ones get a
|
|
752
|
+
* component that renders nothing and imports no React.
|
|
753
|
+
*
|
|
754
|
+
* `strings` comes from the header (the active locale's dictionary); the empty-
|
|
755
|
+
* state suggestions are read straight from the data snapshot, which is why no
|
|
756
|
+
* page has to pass them.
|
|
757
|
+
*/
|
|
758
|
+
export const askComponentTemplate = (askEnabled: boolean): string =>
|
|
759
|
+
askEnabled
|
|
760
|
+
? `---
|
|
761
|
+
// Generated by Blume. Do not edit.
|
|
762
|
+
import AskAI from "blume/components/islands/AskAI.astro";
|
|
763
|
+
import data from "blume:data";
|
|
764
|
+
|
|
765
|
+
const { strings } = Astro.props;
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
<AskAI strings={strings ?? data.ui.ask} suggestions={data.config.ask?.suggestions ?? []} />
|
|
769
|
+
`
|
|
770
|
+
: `---
|
|
771
|
+
// Generated by Blume. Do not edit.
|
|
772
|
+
// Ask AI is off (\`ai.ask.enabled\`), so the header's Ask trigger renders nothing.
|
|
773
|
+
// Deliberately imports no React island, keeping the JSX renderer out of projects
|
|
774
|
+
// that don't need it.
|
|
775
|
+
---
|
|
776
|
+
`;
|
|
777
|
+
|
|
697
778
|
/** Generate the static search index endpoint (`/blume-search.json`). */
|
|
698
779
|
export const searchEndpointTemplate = (): string =>
|
|
699
780
|
`// Generated by Blume. Do not edit.
|
|
@@ -869,10 +950,11 @@ export function getStaticPaths() {
|
|
|
869
950
|
}));
|
|
870
951
|
}
|
|
871
952
|
|
|
872
|
-
export function GET({ props }) {
|
|
873
|
-
const
|
|
953
|
+
export function GET({ props }: { props: { route: string } }) {
|
|
954
|
+
const entries = raw as Record<string, { md?: string; mdx?: string }>;
|
|
955
|
+
const entry = entries[props.route];
|
|
874
956
|
return new Response(entry ? ${
|
|
875
|
-
kind === "md" ?
|
|
957
|
+
kind === "md" ? '(entry.md ?? entry.mdx ?? "")' : '(entry.mdx ?? "")'
|
|
876
958
|
} : "", {
|
|
877
959
|
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
878
960
|
});
|
|
@@ -936,8 +1018,9 @@ export function getStaticPaths() {
|
|
|
936
1018
|
}));
|
|
937
1019
|
}
|
|
938
1020
|
|
|
939
|
-
export function GET({ props }) {
|
|
940
|
-
|
|
1021
|
+
export function GET({ props }: { props: { section: string } }) {
|
|
1022
|
+
const bySection = feeds as Record<string, string>;
|
|
1023
|
+
return new Response(bySection[props.section] ?? "", {
|
|
941
1024
|
headers: { "Content-Type": "application/rss+xml; charset=utf-8" },
|
|
942
1025
|
});
|
|
943
1026
|
}
|
|
@@ -949,7 +1032,7 @@ export const ogEndpointTemplate = (
|
|
|
949
1032
|
): string =>
|
|
950
1033
|
`// Generated by Blume. Do not edit.
|
|
951
1034
|
import { renderOgImage } from "blume/og";
|
|
952
|
-
import data from "
|
|
1035
|
+
import data from "blume:data";
|
|
953
1036
|
|
|
954
1037
|
export const prerender = true;
|
|
955
1038
|
|
|
@@ -957,9 +1040,9 @@ export const prerender = true;
|
|
|
957
1040
|
const customRoutes = ${JSON.stringify(customRoutes)};
|
|
958
1041
|
|
|
959
1042
|
export function getStaticPaths() {
|
|
960
|
-
const seen = new Set();
|
|
961
|
-
const paths = [];
|
|
962
|
-
const add = (slug, title) => {
|
|
1043
|
+
const seen = new Set<string>();
|
|
1044
|
+
const paths: { params: { slug: string }; props: { title: string } }[] = [];
|
|
1045
|
+
const add = (slug: string, title: string) => {
|
|
963
1046
|
if (seen.has(slug)) {
|
|
964
1047
|
return;
|
|
965
1048
|
}
|
|
@@ -992,17 +1075,18 @@ const siteHost = (() => {
|
|
|
992
1075
|
}
|
|
993
1076
|
})();
|
|
994
1077
|
|
|
995
|
-
export async function GET({ props }) {
|
|
1078
|
+
export async function GET({ props }: { props: { title: string } }) {
|
|
996
1079
|
const png = await renderOgImage({
|
|
997
|
-
accent: data.config.theme.accent.light,
|
|
1080
|
+
accent: data.config.og.palette?.accent ?? data.config.theme.accent.light,
|
|
998
1081
|
brand: data.config.title,
|
|
999
1082
|
description: data.config.description,
|
|
1000
|
-
logo: data.config.logo
|
|
1083
|
+
logo: data.config.og.logo,
|
|
1084
|
+
palette: data.config.og.palette,
|
|
1001
1085
|
repo: repoSlug,
|
|
1002
1086
|
site: siteHost,
|
|
1003
1087
|
title: props.title,
|
|
1004
1088
|
});
|
|
1005
|
-
return new Response(png, {
|
|
1089
|
+
return new Response(new Uint8Array(png), {
|
|
1006
1090
|
headers: {
|
|
1007
1091
|
"Cache-Control": "public, max-age=31536000, immutable",
|
|
1008
1092
|
"Content-Type": "image/png",
|
|
@@ -1069,19 +1153,12 @@ const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
|
1069
1153
|
`;
|
|
1070
1154
|
|
|
1071
1155
|
export const catchAllPageTemplate = (options: {
|
|
1072
|
-
askEnabled: boolean;
|
|
1073
1156
|
exportEpub: boolean;
|
|
1074
1157
|
exportPdf: boolean;
|
|
1075
1158
|
mathEnabled: boolean;
|
|
1076
1159
|
/** Serialize the island-hooks snapshot; only needed when React is enabled. */
|
|
1077
1160
|
needsReact: boolean;
|
|
1078
1161
|
}): string => {
|
|
1079
|
-
const askImport = options.askEnabled
|
|
1080
|
-
? 'import AskAI from "blume/components/islands/AskAI.astro";\n'
|
|
1081
|
-
: "";
|
|
1082
|
-
const askSlot = options.askEnabled
|
|
1083
|
-
? '\n <AskAI slot="ask" strings={ui.ask} suggestions={data.config.ask?.suggestions ?? []} />'
|
|
1084
|
-
: "";
|
|
1085
1162
|
const mathImport = options.mathEnabled
|
|
1086
1163
|
? 'import Math from "blume/components/content/Math.astro";\n'
|
|
1087
1164
|
: "";
|
|
@@ -1094,10 +1171,10 @@ export const catchAllPageTemplate = (options: {
|
|
|
1094
1171
|
return `---
|
|
1095
1172
|
// Generated by Blume. Do not edit.
|
|
1096
1173
|
import { getEntry, render } from "astro:content";
|
|
1174
|
+
import type { CollectionKey } from "astro:content";
|
|
1097
1175
|
import RootLayout from "blume/components/layout/RootLayout.astro";
|
|
1098
1176
|
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1099
1177
|
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
1100
|
-
${askImport}
|
|
1101
1178
|
import Accordion from "blume/components/content/Accordion.astro";
|
|
1102
1179
|
import AccordionItem from "blume/components/content/AccordionItem.astro";
|
|
1103
1180
|
import AutoTypeTable from "blume/components/content/AutoTypeTable.astro";
|
|
@@ -1138,7 +1215,7 @@ import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
|
|
|
1138
1215
|
import Operation from "blume/components/openapi/Operation.astro";
|
|
1139
1216
|
${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../generated/components.ts";
|
|
1140
1217
|
import { islandComponents } from "../generated/islands.ts";
|
|
1141
|
-
import data from "
|
|
1218
|
+
import data from "blume:data";
|
|
1142
1219
|
|
|
1143
1220
|
const Color = Object.assign(ColorRoot, { Item: ColorItem, Row: ColorRow });
|
|
1144
1221
|
const Tree = Object.assign(TreeRoot, { File: TreeFile, Folder: TreeFolder });
|
|
@@ -1207,7 +1284,7 @@ export function getStaticPaths() {
|
|
|
1207
1284
|
}
|
|
1208
1285
|
|
|
1209
1286
|
const { entryId, collection, route, title, indexable, editUrl, lastModified, locale, alternates, fallback } = Astro.props;
|
|
1210
|
-
const entry = await getEntry(collection, entryId);
|
|
1287
|
+
const entry = await getEntry(collection as CollectionKey, entryId);
|
|
1211
1288
|
if (!entry) {
|
|
1212
1289
|
return new Response(null, { status: 404 });
|
|
1213
1290
|
}
|
|
@@ -1242,18 +1319,18 @@ const canonical =
|
|
|
1242
1319
|
// Locale resolution. With i18n on, pick the active locale's nav + dictionary,
|
|
1243
1320
|
// build hreflang alternates, and derive the language-switcher targets.
|
|
1244
1321
|
const i18n = data.config.i18n;
|
|
1245
|
-
const localePrefix = (codeArg) =>
|
|
1322
|
+
const localePrefix = (codeArg: string) =>
|
|
1246
1323
|
i18n && codeArg === i18n.defaultLocale && i18n.hideDefaultLocalePrefix
|
|
1247
1324
|
? ""
|
|
1248
1325
|
: \`/\${codeArg}\`;
|
|
1249
|
-
const localizeRoute = (logical, codeArg) => {
|
|
1326
|
+
const localizeRoute = (logical: string, codeArg: string) => {
|
|
1250
1327
|
const prefix = localePrefix(codeArg);
|
|
1251
1328
|
if (!prefix) {
|
|
1252
1329
|
return logical;
|
|
1253
1330
|
}
|
|
1254
1331
|
return logical === "/" ? prefix : \`\${prefix}\${logical}\`;
|
|
1255
1332
|
};
|
|
1256
|
-
const stripLocale = (path, codeArg) => {
|
|
1333
|
+
const stripLocale = (path: string, codeArg: string) => {
|
|
1257
1334
|
const prefix = localePrefix(codeArg);
|
|
1258
1335
|
return prefix && path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
|
|
1259
1336
|
};
|
|
@@ -1270,7 +1347,7 @@ const contentLocale =
|
|
|
1270
1347
|
const contentDir = i18n
|
|
1271
1348
|
? (i18n.locales.find((l) => l.code === contentLocale)?.dir ?? "ltr")
|
|
1272
1349
|
: "ltr";
|
|
1273
|
-
const absolute = (path) => {
|
|
1350
|
+
const absolute = (path: string) => {
|
|
1274
1351
|
const p = withBase(path);
|
|
1275
1352
|
return base + (p === "/" ? "" : p);
|
|
1276
1353
|
};
|
|
@@ -1334,7 +1411,6 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
1334
1411
|
canonical={canonical}
|
|
1335
1412
|
editUrl={editUrl}
|
|
1336
1413
|
feedback={data.config.feedback}
|
|
1337
|
-
askEnabled={${options.askEnabled}}
|
|
1338
1414
|
exportPdf={${options.exportPdf}}
|
|
1339
1415
|
exportEpub={${options.exportEpub}}
|
|
1340
1416
|
feeds={data.feeds}
|
|
@@ -1344,7 +1420,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
1344
1420
|
lastModified={lastModified}
|
|
1345
1421
|
noindex={seo.noindex}
|
|
1346
1422
|
structuredDataEnabled={data.config.structuredData}
|
|
1347
|
-
|
|
1423
|
+
>
|
|
1348
1424
|
<h1>{title}</h1>
|
|
1349
1425
|
{frontmatter.description && <p class="text-lg text-muted-foreground">{frontmatter.description}</p>}
|
|
1350
1426
|
<Content components={components} />
|
|
@@ -1359,7 +1435,6 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
1359
1435
|
* by {@link generateAstroProject} when changelog entries exist.
|
|
1360
1436
|
*/
|
|
1361
1437
|
export const changelogIndexTemplate = (options: {
|
|
1362
|
-
askEnabled: boolean;
|
|
1363
1438
|
exportEpub: boolean;
|
|
1364
1439
|
exportPdf: boolean;
|
|
1365
1440
|
/** Serialize the island-hooks snapshot; only needed when React is enabled. */
|
|
@@ -1367,12 +1442,6 @@ export const changelogIndexTemplate = (options: {
|
|
|
1367
1442
|
/** Whether a `staged` collection exists (non-filesystem changelog sources). */
|
|
1368
1443
|
staged: boolean;
|
|
1369
1444
|
}): string => {
|
|
1370
|
-
const askImport = options.askEnabled
|
|
1371
|
-
? 'import AskAI from "blume/components/islands/AskAI.astro";\n'
|
|
1372
|
-
: "";
|
|
1373
|
-
const askSlot = options.askEnabled
|
|
1374
|
-
? '\n <AskAI slot="ask" strings={data.ui.ask} suggestions={data.config.ask?.suggestions ?? []} />'
|
|
1375
|
-
: "";
|
|
1376
1445
|
const clientData = options.needsReact
|
|
1377
1446
|
? '\n clientData={{ config: data.config, navigation: data.navigation, page: { route: "/changelog", title: pageTitle } }}'
|
|
1378
1447
|
: "";
|
|
@@ -1390,14 +1459,15 @@ import Update from "blume/components/content/Update.astro";
|
|
|
1390
1459
|
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1391
1460
|
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
1392
1461
|
import { layoutOverrides } from "../generated/components.ts";
|
|
1393
|
-
|
|
1462
|
+
import data from "blume:data";
|
|
1394
1463
|
|
|
1395
1464
|
export const prerender = true;
|
|
1396
1465
|
|
|
1397
|
-
const entryDate = (entry
|
|
1398
|
-
|
|
1466
|
+
const entryDate = (entry: {
|
|
1467
|
+
data: { date?: string | null; changelog?: { date?: string | null } | null };
|
|
1468
|
+
}) => entry.data.date ?? entry.data.changelog?.date ?? null;
|
|
1399
1469
|
|
|
1400
|
-
const toTime = (value) => {
|
|
1470
|
+
const toTime = (value: string | null | undefined) => {
|
|
1401
1471
|
if (!value) {
|
|
1402
1472
|
return 0;
|
|
1403
1473
|
}
|
|
@@ -1405,7 +1475,7 @@ const toTime = (value) => {
|
|
|
1405
1475
|
return Number.isNaN(date.getTime()) ? 0 : date.getTime();
|
|
1406
1476
|
};
|
|
1407
1477
|
|
|
1408
|
-
const formatDate = (value) => {
|
|
1478
|
+
const formatDate = (value: string | null | undefined) => {
|
|
1409
1479
|
if (!value) {
|
|
1410
1480
|
return;
|
|
1411
1481
|
}
|
|
@@ -1418,14 +1488,14 @@ const formatDate = (value) => {
|
|
|
1418
1488
|
}).format(date);
|
|
1419
1489
|
};
|
|
1420
1490
|
|
|
1421
|
-
const slugify = (text) =>
|
|
1491
|
+
const slugify = (text: string) =>
|
|
1422
1492
|
text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ||
|
|
1423
1493
|
"update";
|
|
1424
1494
|
|
|
1425
1495
|
// The major of a version's embedded semver (\`1.2.3\` -> 1, \`pkg@2.0.0\` -> 2), or
|
|
1426
1496
|
// null when there is no full major.minor.patch to key on. Drives the changelog's
|
|
1427
1497
|
// group-by-major pagination, so it tolerates the scoped tags monorepos publish.
|
|
1428
|
-
const majorVersion = (version) => {
|
|
1498
|
+
const majorVersion = (version: string | null | undefined) => {
|
|
1429
1499
|
const match = /(\\d+)\\.\\d+\\.\\d+/.exec(String(version ?? ""));
|
|
1430
1500
|
return match ? Number(match[1]) : null;
|
|
1431
1501
|
};
|
|
@@ -1457,7 +1527,7 @@ const items = await Promise.all(
|
|
|
1457
1527
|
return {
|
|
1458
1528
|
Content: (await render(entry)).Content,
|
|
1459
1529
|
date: formatDate(entryDate(entry)),
|
|
1460
|
-
href: routeByEntry.get(entry.id) ??
|
|
1530
|
+
href: routeByEntry.get(entry.id) ?? undefined,
|
|
1461
1531
|
id: slugify(label),
|
|
1462
1532
|
label,
|
|
1463
1533
|
major: majorVersion(entry.data.changelog?.version),
|
|
@@ -1486,7 +1556,9 @@ for (const item of items) {
|
|
|
1486
1556
|
// semver and they span more than one major line. Older majors then collapse
|
|
1487
1557
|
// into groups the reader reveals one at a time; otherwise the timeline is flat.
|
|
1488
1558
|
const majors = items.every((item) => item.major !== null)
|
|
1489
|
-
? [...new Set(items.map((item) => item.major))]
|
|
1559
|
+
? [...new Set(items.map((item) => item.major))]
|
|
1560
|
+
.filter((major): major is number => major !== null)
|
|
1561
|
+
.toSorted((a, b) => b - a)
|
|
1490
1562
|
: [];
|
|
1491
1563
|
const paginate = majors.length > 1;
|
|
1492
1564
|
const majorGroups = majors.map((major) => ({
|
|
@@ -1559,14 +1631,13 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
1559
1631
|
ogImage={null}
|
|
1560
1632
|
x={data.config.x}
|
|
1561
1633
|
canonical={canonical}
|
|
1562
|
-
askEnabled={${options.askEnabled}}
|
|
1563
1634
|
exportPdf={${options.exportPdf}}
|
|
1564
1635
|
exportEpub={${options.exportEpub}}
|
|
1565
1636
|
feeds={data.feeds}
|
|
1566
1637
|
siteUrl={data.config.site}
|
|
1567
1638
|
noindex={false}
|
|
1568
1639
|
structuredDataEnabled={data.config.structuredData}
|
|
1569
|
-
|
|
1640
|
+
>
|
|
1570
1641
|
<h1>{changelogTitle}</h1>
|
|
1571
1642
|
{
|
|
1572
1643
|
items.length === 0 ? (
|
|
@@ -1635,7 +1706,7 @@ export const notFoundPageTemplate = (): string => `---
|
|
|
1635
1706
|
// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
|
|
1636
1707
|
import PageLayout from "blume/components/layout/PageLayout.astro";
|
|
1637
1708
|
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1638
|
-
import data from "
|
|
1709
|
+
import data from "blume:data";
|
|
1639
1710
|
|
|
1640
1711
|
export const prerender = true;
|
|
1641
1712
|
|
|
@@ -1844,7 +1915,10 @@ export const getStaticPaths = () =>
|
|
|
1844
1915
|
Object.keys(examples).map((path) => ({ params: { path } }));
|
|
1845
1916
|
|
|
1846
1917
|
const { path } = Astro.params;
|
|
1847
|
-
const entry = examples[path];
|
|
1918
|
+
const entry = path ? examples[path] : undefined;
|
|
1919
|
+
if (!entry) {
|
|
1920
|
+
return new Response(null, { status: 404 });
|
|
1921
|
+
}
|
|
1848
1922
|
const Example = entry.Component;
|
|
1849
1923
|
---
|
|
1850
1924
|
|
|
@@ -1891,11 +1965,24 @@ export const envTemplate =
|
|
|
1891
1965
|
(): string => `/// <reference path="../.astro/types.d.ts" />
|
|
1892
1966
|
/// <reference types="astro/client" />
|
|
1893
1967
|
|
|
1968
|
+
declare module "blume:ask" {
|
|
1969
|
+
const Ask: typeof import("blume/components/islands/AskAI.astro").default;
|
|
1970
|
+
export default Ask;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1894
1973
|
declare module "blume:data" {
|
|
1895
1974
|
const data: import("blume").BlumeData;
|
|
1896
1975
|
export default data;
|
|
1897
1976
|
}
|
|
1898
1977
|
|
|
1978
|
+
declare module "blume:examples" {
|
|
1979
|
+
type Examples = typeof import("./generated/examples.ts").examples;
|
|
1980
|
+
export const examples: Record<string, Examples[keyof Examples]>;
|
|
1981
|
+
export const examplesBase: string;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
declare module "blume:examples-theme";
|
|
1985
|
+
|
|
1899
1986
|
declare module "blume:openapi" {
|
|
1900
1987
|
const specs: import("blume/openapi/model.ts").OpenApiData;
|
|
1901
1988
|
export default specs;
|
package/src/blume-modules.d.ts
CHANGED
|
@@ -15,6 +15,12 @@ declare module "blume:search-client" {
|
|
|
15
15
|
export const createSearch: () => Fn | Promise<Fn>;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
declare module "blume:ask" {
|
|
19
|
+
/** The generated Ask trigger (see `askComponentTemplate`); empty when Ask is off. */
|
|
20
|
+
const Ask: (props: Record<string, unknown>) => unknown;
|
|
21
|
+
export default Ask;
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
declare module "blume:data" {
|
|
19
25
|
/** The generated per-project data snapshot (see `core/data.ts`). */
|
|
20
26
|
// biome-ignore lint/style/useImportType: ambient module must stay a global script
|
|
@@ -60,8 +60,14 @@ const iconClass: Record<CalloutType, string> = {
|
|
|
60
60
|
<span class:list={["mt-0.5 shrink-0", color ? "" : iconClass[type]]}>
|
|
61
61
|
<Icon color={color} icon={icon ?? iconByType[type]} size={16} />
|
|
62
62
|
</span>
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
{/* The global prose rule leaks a 1rem margin onto these paragraphs/lists even
|
|
64
|
+
though the callout is not-prose; with a title the body isn't the first
|
|
65
|
+
child, so that margin stacks under the title's own gap and reads as too
|
|
66
|
+
much space. Override it here for a uniform, compact gap (important beats
|
|
67
|
+
the unlayered prose rule): every child a small top margin, none on the
|
|
68
|
+
first, and no trailing bottom margin. */}
|
|
69
|
+
<div class="flex-1 [&>*]:mt-2! [&>*]:mb-0! [&>:first-child]:mt-0!">
|
|
70
|
+
{title && <p class="font-semibold text-foreground">{title}</p>}
|
|
65
71
|
<slot />
|
|
66
72
|
</div>
|
|
67
73
|
</aside>
|
|
@@ -59,7 +59,15 @@ const secondaryButton =
|
|
|
59
59
|
data-blume-prompt-copy
|
|
60
60
|
type="button"
|
|
61
61
|
>
|
|
62
|
-
|
|
62
|
+
{/* Both labels share one grid cell so the button is always sized
|
|
63
|
+
to the wider ("Copy prompt") — swapping to "Copied" on copy
|
|
64
|
+
can't resize it and shift the description beside it. */}
|
|
65
|
+
<span class="grid *:col-start-1 *:row-start-1 *:text-center">
|
|
66
|
+
<span data-blume-prompt-copy-idle>Copy prompt</span>
|
|
67
|
+
<span aria-hidden="true" class="invisible" data-blume-prompt-copy-done>
|
|
68
|
+
Copied
|
|
69
|
+
</span>
|
|
70
|
+
</span>
|
|
63
71
|
</button>
|
|
64
72
|
)}
|
|
65
73
|
{enabledActions.has("cursor") && (
|
|
@@ -99,6 +107,13 @@ const secondaryButton =
|
|
|
99
107
|
const copy = this.querySelector<HTMLButtonElement>(
|
|
100
108
|
"[data-blume-prompt-copy]"
|
|
101
109
|
);
|
|
110
|
+
const idle = copy?.querySelector<HTMLElement>(
|
|
111
|
+
"[data-blume-prompt-copy-idle]"
|
|
112
|
+
);
|
|
113
|
+
const done = copy?.querySelector<HTMLElement>(
|
|
114
|
+
"[data-blume-prompt-copy-done]"
|
|
115
|
+
);
|
|
116
|
+
let resetTimer: ReturnType<typeof setTimeout> | undefined;
|
|
102
117
|
copy?.addEventListener("click", async () => {
|
|
103
118
|
const text = promptText();
|
|
104
119
|
if (!text) {
|
|
@@ -110,18 +125,15 @@ const secondaryButton =
|
|
|
110
125
|
return;
|
|
111
126
|
}
|
|
112
127
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
label.textContent = "Copied";
|
|
123
|
-
setTimeout(() => {
|
|
124
|
-
label.textContent = label.dataset.blumeLabel ?? "Copy prompt";
|
|
128
|
+
// Toggle visibility rather than swapping text: both labels occupy the
|
|
129
|
+
// same grid cell, so the button width never changes. Clearing the timer
|
|
130
|
+
// keeps the "Copied" state 1.5s past the last of several rapid clicks.
|
|
131
|
+
clearTimeout(resetTimer);
|
|
132
|
+
idle?.classList.add("invisible");
|
|
133
|
+
done?.classList.remove("invisible");
|
|
134
|
+
resetTimer = setTimeout(() => {
|
|
135
|
+
idle?.classList.remove("invisible");
|
|
136
|
+
done?.classList.add("invisible");
|
|
125
137
|
}, 1500);
|
|
126
138
|
});
|
|
127
139
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
+
import Ask from "blume:ask";
|
|
3
|
+
import data from "blume:data";
|
|
2
4
|
import { withBase } from "../islands/base-path.ts";
|
|
3
5
|
import type { ComponentOverride } from "../../core/define-components.ts";
|
|
4
6
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
@@ -8,7 +10,7 @@ import { GITHUB_MARK } from "../github-mark.ts";
|
|
|
8
10
|
import Icon from "../Icon.astro";
|
|
9
11
|
import LanguageSwitcher from "./LanguageSwitcher.astro";
|
|
10
12
|
import Logo from "./Logo.astro";
|
|
11
|
-
import {
|
|
13
|
+
import { activeTabForRoute } from "./nav-utils.ts";
|
|
12
14
|
import NavSelector from "./NavSelector.astro";
|
|
13
15
|
import { resolveSlot } from "./overrides.ts";
|
|
14
16
|
import Search from "./Search.astro";
|
|
@@ -26,7 +28,14 @@ interface Props {
|
|
|
26
28
|
navigation: Navigation;
|
|
27
29
|
route: string;
|
|
28
30
|
searchEnabled: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Whether the search modal offers an "Ask AI" hand-off. Defaults to whether
|
|
33
|
+
* Ask AI is configured, so no page has to pass it; a layout can still opt a
|
|
34
|
+
* shell out explicitly.
|
|
35
|
+
*/
|
|
29
36
|
askEnabled?: boolean;
|
|
37
|
+
/** Localized Ask AI strings for the active locale. */
|
|
38
|
+
askStrings?: UIStrings["ask"];
|
|
30
39
|
// The mobile menu button toggles the docs sidebar drawer; custom pages
|
|
31
40
|
// without a sidebar (e.g. a landing page) pass `false` to hide it.
|
|
32
41
|
hasSidebar?: boolean;
|
|
@@ -57,7 +66,11 @@ const {
|
|
|
57
66
|
navigation,
|
|
58
67
|
route,
|
|
59
68
|
searchEnabled,
|
|
60
|
-
|
|
69
|
+
// `config.ask` is null whenever Ask AI is off, so the header is the one place
|
|
70
|
+
// that has to know — the Ask trigger below and the search modal's hand-off to
|
|
71
|
+
// it both switch on this, and every page gets both for free.
|
|
72
|
+
askEnabled = Boolean(data.config.ask),
|
|
73
|
+
askStrings,
|
|
61
74
|
hasSidebar = true,
|
|
62
75
|
hasDrawer = true,
|
|
63
76
|
searchStrings,
|
|
@@ -81,6 +94,7 @@ const SearchSlot = resolveSlot(layout.Search, Search);
|
|
|
81
94
|
// PageLayout) it's a tabs-only drawer the layout renders — so the button is also
|
|
82
95
|
// needed whenever there are tabs to reveal.
|
|
83
96
|
const showNavToggle = hasDrawer && (hasSidebar || navigation.tabs.length > 0);
|
|
97
|
+
const activeTab = activeTabForRoute(navigation.tabs, route);
|
|
84
98
|
// Where the header's inline tab bar appears. With a sidebar it shares the `md`
|
|
85
99
|
// breakpoint with the docs drawer; without one, the tabs-only drawer is the sole
|
|
86
100
|
// mobile nav below `lg`, so the inline tabs wait until `lg` to avoid duplicating
|
|
@@ -142,14 +156,9 @@ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-b
|
|
|
142
156
|
<nav aria-label={n.sections} class={tabsNavClass}>
|
|
143
157
|
{navigation.tabs.map((tab) => (
|
|
144
158
|
<a
|
|
145
|
-
aria-current={
|
|
146
|
-
route === tab.path ||
|
|
147
|
-
(tab.path !== "/" && isUnderPath(route, tab.path))
|
|
148
|
-
? "page"
|
|
149
|
-
: undefined
|
|
150
|
-
}
|
|
159
|
+
aria-current={tab === activeTab ? "page" : undefined}
|
|
151
160
|
class="rounded-full px-3 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:text-foreground"
|
|
152
|
-
href={withBase(tab.path)}
|
|
161
|
+
href={withBase(tab.href ?? tab.path)}
|
|
153
162
|
>
|
|
154
163
|
{tab.label}
|
|
155
164
|
</a>
|
|
@@ -208,7 +217,7 @@ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-b
|
|
|
208
217
|
<span class="inline-flex dark:hidden"><Icon name="sun" size={18} /></span>
|
|
209
218
|
<span class="hidden dark:inline-flex"><Icon name="moon" size={18} /></span>
|
|
210
219
|
</button>
|
|
211
|
-
<
|
|
220
|
+
{askEnabled && <Ask strings={askStrings} />}
|
|
212
221
|
</div>
|
|
213
222
|
</header>
|
|
214
223
|
<script is:inline set:html={clickScript} />
|
|
@@ -18,6 +18,8 @@ const { site, logo } = Astro.props;
|
|
|
18
18
|
const logoSvg = logo?.svg;
|
|
19
19
|
const logoLight = logo?.light;
|
|
20
20
|
const logoDark = logo?.dark ?? logo?.light;
|
|
21
|
+
const logoLightDimensions = logo?.dimensions?.light;
|
|
22
|
+
const logoDarkDimensions = logo?.dimensions?.dark ?? logo?.dimensions?.light;
|
|
21
23
|
const logoAlt = logo?.alt ?? "";
|
|
22
24
|
const brandHref = logo?.href ?? "/";
|
|
23
25
|
// Wordmark beside the mark: the configured `logo.text`, or the site title when
|
|
@@ -43,18 +45,28 @@ const brandText = logo?.text ?? site.title;
|
|
|
43
45
|
!logoSvg &&
|
|
44
46
|
logoLight &&
|
|
45
47
|
(logoLight === logoDark ? (
|
|
46
|
-
<img
|
|
48
|
+
<img
|
|
49
|
+
alt={logoAlt}
|
|
50
|
+
class="h-5 w-auto"
|
|
51
|
+
height={logoLightDimensions?.height}
|
|
52
|
+
src={withBase(logoLight)}
|
|
53
|
+
width={logoLightDimensions?.width}
|
|
54
|
+
/>
|
|
47
55
|
) : (
|
|
48
56
|
<>
|
|
49
57
|
<img
|
|
50
58
|
alt={logoAlt}
|
|
51
59
|
class="h-5 w-auto dark:hidden"
|
|
60
|
+
height={logoLightDimensions?.height}
|
|
52
61
|
src={withBase(logoLight)}
|
|
62
|
+
width={logoLightDimensions?.width}
|
|
53
63
|
/>
|
|
54
64
|
<img
|
|
55
65
|
alt={logoAlt}
|
|
56
66
|
class="hidden h-5 w-auto dark:block"
|
|
67
|
+
height={logoDarkDimensions?.height}
|
|
57
68
|
src={withBase(logoDark)}
|
|
69
|
+
width={logoDarkDimensions?.width}
|
|
58
70
|
/>
|
|
59
71
|
</>
|
|
60
72
|
))
|
|
@@ -15,7 +15,7 @@ const { strings } = Astro.props;
|
|
|
15
15
|
const f = { ...EN_UI.feedback, ...strings };
|
|
16
16
|
|
|
17
17
|
const buttonClass =
|
|
18
|
-
"inline-flex items-center gap-2 rounded-
|
|
18
|
+
"inline-flex items-center gap-2 rounded-blume border border-border px-4 py-2 text-foreground text-sm transition-colors hover:border-foreground";
|
|
19
19
|
---
|
|
20
20
|
|
|
21
21
|
<section
|