blume 1.0.3 → 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 +29 -0
- package/dist/cli/index.js +441 -259
- package/dist/cli/index.js.map +12 -11
- package/dist/types/core/config-input.d.ts +27 -8
- 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 +420 -350
- 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/configuration/seo.mdx +20 -1
- package/docs/content/components.mdx +1 -2
- 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/astro/component-slots.ts +3 -2
- package/src/astro/generate.ts +82 -23
- package/src/astro/templates.ts +93 -34
- package/src/components/content/Callout.astro +8 -2
- package/src/components/content/Prompt.astro +25 -13
- package/src/components/layout/Header.astro +4 -8
- package/src/components/layout/Logo.astro +13 -1
- package/src/components/layout/PageFeedback.astro +1 -1
- package/src/components/layout/PageLayout.astro +4 -8
- package/src/components/layout/Pagination.astro +6 -6
- package/src/components/layout/RootLayout.astro +4 -8
- package/src/components/layout/Search.astro +1 -1
- package/src/components/layout/nav-utils.ts +9 -7
- package/src/core/config-input.ts +29 -8
- package/src/core/data.ts +9 -1
- package/src/core/navigation.ts +55 -13
- package/src/core/schema.ts +17 -1
- package/src/core/sources/watch.ts +5 -0
- package/src/core/types.ts +10 -0
- 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/theme/entry.ts +50 -5
package/src/astro/generate.ts
CHANGED
|
@@ -37,7 +37,8 @@ import { resolveDocsCollection } from "../core/sources/resolve.ts";
|
|
|
37
37
|
import { resolveTsconfigAliases } from "../core/tsconfig-aliases.ts";
|
|
38
38
|
import type { Navigation } from "../core/types.ts";
|
|
39
39
|
import { buildRssFeeds, renderRssFeed } from "../deploy/rss.ts";
|
|
40
|
-
import {
|
|
40
|
+
import { resolveOgLogo } from "../og/logo.ts";
|
|
41
|
+
import { hasScalarReferences, referenceRoutes } from "../openapi/references.ts";
|
|
41
42
|
import { buildReferenceFiles } from "../openapi/scalar.ts";
|
|
42
43
|
import { isOpenApiSource } from "../openapi/source.ts";
|
|
43
44
|
import { registry } from "../registry/registry.ts";
|
|
@@ -626,7 +627,56 @@ const writeStagedContent = async (
|
|
|
626
627
|
}
|
|
627
628
|
};
|
|
628
629
|
|
|
629
|
-
|
|
630
|
+
interface LogoDimensions {
|
|
631
|
+
height: number;
|
|
632
|
+
width: number;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const SVG_ROOT = /<svg\b(?<attributes>[^>]*)>/u;
|
|
636
|
+
const SVG_WIDTH = /\bwidth\s*=\s*["'](?<value>[^"']+)["']/u;
|
|
637
|
+
const SVG_HEIGHT = /\bheight\s*=\s*["'](?<value>[^"']+)["']/u;
|
|
638
|
+
const SVG_LENGTH = /^\s*(?<value>[\d.]+)(?:px)?\s*$/u;
|
|
639
|
+
const SVG_VIEW_BOX =
|
|
640
|
+
/\bviewBox\s*=\s*["'][\d.-]+[\s,]+[\d.-]+[\s,]+(?<width>[\d.]+)[\s,]+(?<height>[\d.]+)["']/u;
|
|
641
|
+
|
|
642
|
+
const parseSvgLength = (value: string | undefined): number | undefined => {
|
|
643
|
+
const length = Number(value?.match(SVG_LENGTH)?.groups?.value);
|
|
644
|
+
return length > 0 ? length : undefined;
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
/** Read dimensions from an SVG's explicit size or its view box. */
|
|
648
|
+
const svgDimensions = (svg: string | undefined): LogoDimensions | undefined => {
|
|
649
|
+
const attributes = svg?.match(SVG_ROOT)?.groups?.attributes;
|
|
650
|
+
const width = parseSvgLength(attributes?.match(SVG_WIDTH)?.groups?.value);
|
|
651
|
+
const height = parseSvgLength(attributes?.match(SVG_HEIGHT)?.groups?.value);
|
|
652
|
+
if (width && height) {
|
|
653
|
+
return { height, width };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const viewBox = attributes?.match(SVG_VIEW_BOX);
|
|
657
|
+
const viewBoxWidth = Number(viewBox?.groups?.width);
|
|
658
|
+
const viewBoxHeight = Number(viewBox?.groups?.height);
|
|
659
|
+
return viewBoxWidth > 0 && viewBoxHeight > 0
|
|
660
|
+
? { height: viewBoxHeight, width: viewBoxWidth }
|
|
661
|
+
: undefined;
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
/** Read a local SVG logo from the project root or public directory. */
|
|
665
|
+
const readLogoSvg = (
|
|
666
|
+
project: BlumeProject,
|
|
667
|
+
source: string | undefined
|
|
668
|
+
): string | undefined => {
|
|
669
|
+
if (!source?.toLowerCase().endsWith(".svg")) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const rel = source.replace(/^\//u, "");
|
|
673
|
+
const file = [
|
|
674
|
+
join(project.context.root, "public", rel),
|
|
675
|
+
join(project.context.root, rel),
|
|
676
|
+
].find((path) => existsSync(path));
|
|
677
|
+
return file ? readFileSync(file, "utf-8") : undefined;
|
|
678
|
+
};
|
|
679
|
+
|
|
630
680
|
/**
|
|
631
681
|
* Resolve the configured logo. A single SVG is read and inlined so a
|
|
632
682
|
* `currentColor` logo follows the theme; other images keep their URL for an
|
|
@@ -647,18 +697,20 @@ const resolveLogo = (project: BlumeProject): BlumeLogo | null => {
|
|
|
647
697
|
const dark = image?.dark ?? image?.light;
|
|
648
698
|
const alt = image?.alt ?? "";
|
|
649
699
|
const brandHref = href ?? "/";
|
|
700
|
+
const lightSvg = readLogoSvg(project, light);
|
|
701
|
+
const darkSvg = dark === light ? lightSvg : readLogoSvg(project, dark);
|
|
650
702
|
|
|
651
|
-
if (light && light === dark &&
|
|
652
|
-
|
|
653
|
-
const file = [
|
|
654
|
-
join(project.context.root, "public", rel),
|
|
655
|
-
join(project.context.root, rel),
|
|
656
|
-
].find((path) => existsSync(path));
|
|
657
|
-
if (file) {
|
|
658
|
-
return { alt, href: brandHref, svg: readFileSync(file, "utf-8"), text };
|
|
659
|
-
}
|
|
703
|
+
if (light && light === dark && lightSvg) {
|
|
704
|
+
return { alt, href: brandHref, svg: lightSvg, text };
|
|
660
705
|
}
|
|
661
|
-
|
|
706
|
+
|
|
707
|
+
const lightDimensions = svgDimensions(lightSvg);
|
|
708
|
+
const darkDimensions = svgDimensions(darkSvg);
|
|
709
|
+
const dimensions =
|
|
710
|
+
lightDimensions || darkDimensions
|
|
711
|
+
? { dark: darkDimensions, light: lightDimensions }
|
|
712
|
+
: undefined;
|
|
713
|
+
return { alt, dark, dimensions, href: brandHref, light, text };
|
|
662
714
|
};
|
|
663
715
|
|
|
664
716
|
/**
|
|
@@ -779,6 +831,10 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
779
831
|
? `https://github.com/${github.owner}/${github.repo}`
|
|
780
832
|
: null;
|
|
781
833
|
const editBase = github ? `${repoUrl}/edit/${github.branch}` : null;
|
|
834
|
+
const logo = resolveLogo(project);
|
|
835
|
+
const ogLogo = config.seo.og.logo
|
|
836
|
+
? resolveOgLogo(project, config.seo.og.logo)
|
|
837
|
+
: logo?.svg;
|
|
782
838
|
|
|
783
839
|
const editUrlFor = (sourcePath?: string): string | null => {
|
|
784
840
|
if (!(editBase && sourcePath)) {
|
|
@@ -791,13 +847,12 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
791
847
|
|
|
792
848
|
const { i18n } = config;
|
|
793
849
|
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
const
|
|
850
|
+
// Resolve the header repo link per locale. API references no longer add a tab
|
|
851
|
+
// automatically — authors point a `navigation.tabs` entry at the reference
|
|
852
|
+
// route to surface it (see `referenceRoutes`).
|
|
853
|
+
const withRepoUrl = (nav: Navigation): Navigation => ({
|
|
798
854
|
...nav,
|
|
799
855
|
repoUrl: config.navigation.repo && repoUrl ? repoUrl : null,
|
|
800
|
-
tabs: [...nav.tabs, ...referenceTabs(config)],
|
|
801
856
|
});
|
|
802
857
|
|
|
803
858
|
// Resolved UI dictionaries: one per locale under i18n, English baseline
|
|
@@ -824,7 +879,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
824
879
|
? Object.fromEntries(
|
|
825
880
|
i18n.locales.map(({ code }) => [
|
|
826
881
|
code,
|
|
827
|
-
|
|
882
|
+
withRepoUrl(
|
|
828
883
|
graph.navigationByLocale[code] ?? {
|
|
829
884
|
featured: [],
|
|
830
885
|
selectors: [],
|
|
@@ -865,7 +920,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
865
920
|
}
|
|
866
921
|
: null,
|
|
867
922
|
imageZoom: config.markdown.imageZoom,
|
|
868
|
-
logo
|
|
923
|
+
logo,
|
|
869
924
|
mcp: config.ai.mcp.enabled
|
|
870
925
|
? {
|
|
871
926
|
name: config.ai.mcp.name ?? config.title,
|
|
@@ -874,7 +929,11 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
874
929
|
: null,
|
|
875
930
|
// `og.enabled` is resolved to a definite boolean in `loadConfig`; coerce
|
|
876
931
|
// the optional schema type so the serialized shape stays `boolean`.
|
|
877
|
-
og: {
|
|
932
|
+
og: {
|
|
933
|
+
enabled: config.seo.og.enabled ?? false,
|
|
934
|
+
logo: ogLogo,
|
|
935
|
+
palette: config.seo.og.palette,
|
|
936
|
+
},
|
|
878
937
|
repoUrl,
|
|
879
938
|
search: {
|
|
880
939
|
enabled: config.search.provider !== "none",
|
|
@@ -894,7 +953,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
894
953
|
// CSS variables for Astro's <Font> component; matches the astro.config
|
|
895
954
|
// `fonts:` entries derived from the same theme.fonts config.
|
|
896
955
|
fontCssVars: configuredCssVars(config.theme.fonts),
|
|
897
|
-
navigation:
|
|
956
|
+
navigation: withRepoUrl(graph.navigation),
|
|
898
957
|
// Per-locale navigation; the catch-all selects the active locale's tree.
|
|
899
958
|
navigationByLocale,
|
|
900
959
|
routes: manifest.routes.map((route) => ({
|
|
@@ -1441,11 +1500,11 @@ export const generateRuntime = async (
|
|
|
1441
1500
|
|
|
1442
1501
|
// Missing-navigation-target check, now that every servable route is known:
|
|
1443
1502
|
// content routes, custom `.astro` pages, the generated changelog, and any
|
|
1444
|
-
// OpenAPI reference
|
|
1503
|
+
// OpenAPI reference routes (so a tab an author points at one still validates).
|
|
1445
1504
|
const navTargetRoutes = new Set<string>([
|
|
1446
1505
|
...project.graph.routes.keys(),
|
|
1447
1506
|
...pages.map((page) => page.pattern),
|
|
1448
|
-
...
|
|
1507
|
+
...referenceRoutes(config),
|
|
1449
1508
|
]);
|
|
1450
1509
|
if (hasGeneratedChangelog(project, pages)) {
|
|
1451
1510
|
navTargetRoutes.add("/changelog");
|
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
|
|
@@ -263,10 +287,15 @@ export const astroConfigTemplate = (options: {
|
|
|
263
287
|
server && deployment.adapter
|
|
264
288
|
? `import adapter from "${ADAPTER_IMPORTS[deployment.adapter]}";\n`
|
|
265
289
|
: "";
|
|
266
|
-
const adapterArgs =
|
|
267
|
-
server
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
})();
|
|
270
299
|
const adapterOption =
|
|
271
300
|
server && deployment.adapter ? `\n adapter: adapter(${adapterArgs}),` : "";
|
|
272
301
|
|
|
@@ -410,6 +439,18 @@ export default defineConfig({
|
|
|
410
439
|
devToolbar: { enabled: false },
|
|
411
440
|
vite: {
|
|
412
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"] },
|
|
413
454
|
// Blume's render-time deps are forced external on both build environments so
|
|
414
455
|
// native bindings resolve at runtime and isolated linkers don't bundle
|
|
415
456
|
// symlinked store copies (which would surface their children as unresolvable
|
|
@@ -909,10 +950,11 @@ export function getStaticPaths() {
|
|
|
909
950
|
}));
|
|
910
951
|
}
|
|
911
952
|
|
|
912
|
-
export function GET({ props }) {
|
|
913
|
-
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];
|
|
914
956
|
return new Response(entry ? ${
|
|
915
|
-
kind === "md" ?
|
|
957
|
+
kind === "md" ? '(entry.md ?? entry.mdx ?? "")' : '(entry.mdx ?? "")'
|
|
916
958
|
} : "", {
|
|
917
959
|
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
918
960
|
});
|
|
@@ -976,8 +1018,9 @@ export function getStaticPaths() {
|
|
|
976
1018
|
}));
|
|
977
1019
|
}
|
|
978
1020
|
|
|
979
|
-
export function GET({ props }) {
|
|
980
|
-
|
|
1021
|
+
export function GET({ props }: { props: { section: string } }) {
|
|
1022
|
+
const bySection = feeds as Record<string, string>;
|
|
1023
|
+
return new Response(bySection[props.section] ?? "", {
|
|
981
1024
|
headers: { "Content-Type": "application/rss+xml; charset=utf-8" },
|
|
982
1025
|
});
|
|
983
1026
|
}
|
|
@@ -989,7 +1032,7 @@ export const ogEndpointTemplate = (
|
|
|
989
1032
|
): string =>
|
|
990
1033
|
`// Generated by Blume. Do not edit.
|
|
991
1034
|
import { renderOgImage } from "blume/og";
|
|
992
|
-
import data from "
|
|
1035
|
+
import data from "blume:data";
|
|
993
1036
|
|
|
994
1037
|
export const prerender = true;
|
|
995
1038
|
|
|
@@ -997,9 +1040,9 @@ export const prerender = true;
|
|
|
997
1040
|
const customRoutes = ${JSON.stringify(customRoutes)};
|
|
998
1041
|
|
|
999
1042
|
export function getStaticPaths() {
|
|
1000
|
-
const seen = new Set();
|
|
1001
|
-
const paths = [];
|
|
1002
|
-
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) => {
|
|
1003
1046
|
if (seen.has(slug)) {
|
|
1004
1047
|
return;
|
|
1005
1048
|
}
|
|
@@ -1032,17 +1075,18 @@ const siteHost = (() => {
|
|
|
1032
1075
|
}
|
|
1033
1076
|
})();
|
|
1034
1077
|
|
|
1035
|
-
export async function GET({ props }) {
|
|
1078
|
+
export async function GET({ props }: { props: { title: string } }) {
|
|
1036
1079
|
const png = await renderOgImage({
|
|
1037
|
-
accent: data.config.theme.accent.light,
|
|
1080
|
+
accent: data.config.og.palette?.accent ?? data.config.theme.accent.light,
|
|
1038
1081
|
brand: data.config.title,
|
|
1039
1082
|
description: data.config.description,
|
|
1040
|
-
logo: data.config.logo
|
|
1083
|
+
logo: data.config.og.logo,
|
|
1084
|
+
palette: data.config.og.palette,
|
|
1041
1085
|
repo: repoSlug,
|
|
1042
1086
|
site: siteHost,
|
|
1043
1087
|
title: props.title,
|
|
1044
1088
|
});
|
|
1045
|
-
return new Response(png, {
|
|
1089
|
+
return new Response(new Uint8Array(png), {
|
|
1046
1090
|
headers: {
|
|
1047
1091
|
"Cache-Control": "public, max-age=31536000, immutable",
|
|
1048
1092
|
"Content-Type": "image/png",
|
|
@@ -1127,6 +1171,7 @@ export const catchAllPageTemplate = (options: {
|
|
|
1127
1171
|
return `---
|
|
1128
1172
|
// Generated by Blume. Do not edit.
|
|
1129
1173
|
import { getEntry, render } from "astro:content";
|
|
1174
|
+
import type { CollectionKey } from "astro:content";
|
|
1130
1175
|
import RootLayout from "blume/components/layout/RootLayout.astro";
|
|
1131
1176
|
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1132
1177
|
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
@@ -1170,7 +1215,7 @@ import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
|
|
|
1170
1215
|
import Operation from "blume/components/openapi/Operation.astro";
|
|
1171
1216
|
${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../generated/components.ts";
|
|
1172
1217
|
import { islandComponents } from "../generated/islands.ts";
|
|
1173
|
-
import data from "
|
|
1218
|
+
import data from "blume:data";
|
|
1174
1219
|
|
|
1175
1220
|
const Color = Object.assign(ColorRoot, { Item: ColorItem, Row: ColorRow });
|
|
1176
1221
|
const Tree = Object.assign(TreeRoot, { File: TreeFile, Folder: TreeFolder });
|
|
@@ -1239,7 +1284,7 @@ export function getStaticPaths() {
|
|
|
1239
1284
|
}
|
|
1240
1285
|
|
|
1241
1286
|
const { entryId, collection, route, title, indexable, editUrl, lastModified, locale, alternates, fallback } = Astro.props;
|
|
1242
|
-
const entry = await getEntry(collection, entryId);
|
|
1287
|
+
const entry = await getEntry(collection as CollectionKey, entryId);
|
|
1243
1288
|
if (!entry) {
|
|
1244
1289
|
return new Response(null, { status: 404 });
|
|
1245
1290
|
}
|
|
@@ -1274,18 +1319,18 @@ const canonical =
|
|
|
1274
1319
|
// Locale resolution. With i18n on, pick the active locale's nav + dictionary,
|
|
1275
1320
|
// build hreflang alternates, and derive the language-switcher targets.
|
|
1276
1321
|
const i18n = data.config.i18n;
|
|
1277
|
-
const localePrefix = (codeArg) =>
|
|
1322
|
+
const localePrefix = (codeArg: string) =>
|
|
1278
1323
|
i18n && codeArg === i18n.defaultLocale && i18n.hideDefaultLocalePrefix
|
|
1279
1324
|
? ""
|
|
1280
1325
|
: \`/\${codeArg}\`;
|
|
1281
|
-
const localizeRoute = (logical, codeArg) => {
|
|
1326
|
+
const localizeRoute = (logical: string, codeArg: string) => {
|
|
1282
1327
|
const prefix = localePrefix(codeArg);
|
|
1283
1328
|
if (!prefix) {
|
|
1284
1329
|
return logical;
|
|
1285
1330
|
}
|
|
1286
1331
|
return logical === "/" ? prefix : \`\${prefix}\${logical}\`;
|
|
1287
1332
|
};
|
|
1288
|
-
const stripLocale = (path, codeArg) => {
|
|
1333
|
+
const stripLocale = (path: string, codeArg: string) => {
|
|
1289
1334
|
const prefix = localePrefix(codeArg);
|
|
1290
1335
|
return prefix && path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
|
|
1291
1336
|
};
|
|
@@ -1302,7 +1347,7 @@ const contentLocale =
|
|
|
1302
1347
|
const contentDir = i18n
|
|
1303
1348
|
? (i18n.locales.find((l) => l.code === contentLocale)?.dir ?? "ltr")
|
|
1304
1349
|
: "ltr";
|
|
1305
|
-
const absolute = (path) => {
|
|
1350
|
+
const absolute = (path: string) => {
|
|
1306
1351
|
const p = withBase(path);
|
|
1307
1352
|
return base + (p === "/" ? "" : p);
|
|
1308
1353
|
};
|
|
@@ -1414,14 +1459,15 @@ import Update from "blume/components/content/Update.astro";
|
|
|
1414
1459
|
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1415
1460
|
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
1416
1461
|
import { layoutOverrides } from "../generated/components.ts";
|
|
1417
|
-
import data from "
|
|
1462
|
+
import data from "blume:data";
|
|
1418
1463
|
|
|
1419
1464
|
export const prerender = true;
|
|
1420
1465
|
|
|
1421
|
-
const entryDate = (entry
|
|
1422
|
-
|
|
1466
|
+
const entryDate = (entry: {
|
|
1467
|
+
data: { date?: string | null; changelog?: { date?: string | null } | null };
|
|
1468
|
+
}) => entry.data.date ?? entry.data.changelog?.date ?? null;
|
|
1423
1469
|
|
|
1424
|
-
const toTime = (value) => {
|
|
1470
|
+
const toTime = (value: string | null | undefined) => {
|
|
1425
1471
|
if (!value) {
|
|
1426
1472
|
return 0;
|
|
1427
1473
|
}
|
|
@@ -1429,7 +1475,7 @@ const toTime = (value) => {
|
|
|
1429
1475
|
return Number.isNaN(date.getTime()) ? 0 : date.getTime();
|
|
1430
1476
|
};
|
|
1431
1477
|
|
|
1432
|
-
const formatDate = (value) => {
|
|
1478
|
+
const formatDate = (value: string | null | undefined) => {
|
|
1433
1479
|
if (!value) {
|
|
1434
1480
|
return;
|
|
1435
1481
|
}
|
|
@@ -1442,14 +1488,14 @@ const formatDate = (value) => {
|
|
|
1442
1488
|
}).format(date);
|
|
1443
1489
|
};
|
|
1444
1490
|
|
|
1445
|
-
const slugify = (text) =>
|
|
1491
|
+
const slugify = (text: string) =>
|
|
1446
1492
|
text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ||
|
|
1447
1493
|
"update";
|
|
1448
1494
|
|
|
1449
1495
|
// The major of a version's embedded semver (\`1.2.3\` -> 1, \`pkg@2.0.0\` -> 2), or
|
|
1450
1496
|
// null when there is no full major.minor.patch to key on. Drives the changelog's
|
|
1451
1497
|
// group-by-major pagination, so it tolerates the scoped tags monorepos publish.
|
|
1452
|
-
const majorVersion = (version) => {
|
|
1498
|
+
const majorVersion = (version: string | null | undefined) => {
|
|
1453
1499
|
const match = /(\\d+)\\.\\d+\\.\\d+/.exec(String(version ?? ""));
|
|
1454
1500
|
return match ? Number(match[1]) : null;
|
|
1455
1501
|
};
|
|
@@ -1481,7 +1527,7 @@ const items = await Promise.all(
|
|
|
1481
1527
|
return {
|
|
1482
1528
|
Content: (await render(entry)).Content,
|
|
1483
1529
|
date: formatDate(entryDate(entry)),
|
|
1484
|
-
href: routeByEntry.get(entry.id) ??
|
|
1530
|
+
href: routeByEntry.get(entry.id) ?? undefined,
|
|
1485
1531
|
id: slugify(label),
|
|
1486
1532
|
label,
|
|
1487
1533
|
major: majorVersion(entry.data.changelog?.version),
|
|
@@ -1510,7 +1556,9 @@ for (const item of items) {
|
|
|
1510
1556
|
// semver and they span more than one major line. Older majors then collapse
|
|
1511
1557
|
// into groups the reader reveals one at a time; otherwise the timeline is flat.
|
|
1512
1558
|
const majors = items.every((item) => item.major !== null)
|
|
1513
|
-
? [...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)
|
|
1514
1562
|
: [];
|
|
1515
1563
|
const paginate = majors.length > 1;
|
|
1516
1564
|
const majorGroups = majors.map((major) => ({
|
|
@@ -1658,7 +1706,7 @@ export const notFoundPageTemplate = (): string => `---
|
|
|
1658
1706
|
// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
|
|
1659
1707
|
import PageLayout from "blume/components/layout/PageLayout.astro";
|
|
1660
1708
|
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1661
|
-
import data from "
|
|
1709
|
+
import data from "blume:data";
|
|
1662
1710
|
|
|
1663
1711
|
export const prerender = true;
|
|
1664
1712
|
|
|
@@ -1867,7 +1915,10 @@ export const getStaticPaths = () =>
|
|
|
1867
1915
|
Object.keys(examples).map((path) => ({ params: { path } }));
|
|
1868
1916
|
|
|
1869
1917
|
const { path } = Astro.params;
|
|
1870
|
-
const entry = examples[path];
|
|
1918
|
+
const entry = path ? examples[path] : undefined;
|
|
1919
|
+
if (!entry) {
|
|
1920
|
+
return new Response(null, { status: 404 });
|
|
1921
|
+
}
|
|
1871
1922
|
const Example = entry.Component;
|
|
1872
1923
|
---
|
|
1873
1924
|
|
|
@@ -1924,6 +1975,14 @@ declare module "blume:data" {
|
|
|
1924
1975
|
export default data;
|
|
1925
1976
|
}
|
|
1926
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
|
+
|
|
1927
1986
|
declare module "blume:openapi" {
|
|
1928
1987
|
const specs: import("blume/openapi/model.ts").OpenApiData;
|
|
1929
1988
|
export default specs;
|
|
@@ -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
|
}
|
|
@@ -10,7 +10,7 @@ import { GITHUB_MARK } from "../github-mark.ts";
|
|
|
10
10
|
import Icon from "../Icon.astro";
|
|
11
11
|
import LanguageSwitcher from "./LanguageSwitcher.astro";
|
|
12
12
|
import Logo from "./Logo.astro";
|
|
13
|
-
import {
|
|
13
|
+
import { activeTabForRoute } from "./nav-utils.ts";
|
|
14
14
|
import NavSelector from "./NavSelector.astro";
|
|
15
15
|
import { resolveSlot } from "./overrides.ts";
|
|
16
16
|
import Search from "./Search.astro";
|
|
@@ -94,6 +94,7 @@ const SearchSlot = resolveSlot(layout.Search, Search);
|
|
|
94
94
|
// PageLayout) it's a tabs-only drawer the layout renders — so the button is also
|
|
95
95
|
// needed whenever there are tabs to reveal.
|
|
96
96
|
const showNavToggle = hasDrawer && (hasSidebar || navigation.tabs.length > 0);
|
|
97
|
+
const activeTab = activeTabForRoute(navigation.tabs, route);
|
|
97
98
|
// Where the header's inline tab bar appears. With a sidebar it shares the `md`
|
|
98
99
|
// breakpoint with the docs drawer; without one, the tabs-only drawer is the sole
|
|
99
100
|
// mobile nav below `lg`, so the inline tabs wait until `lg` to avoid duplicating
|
|
@@ -155,14 +156,9 @@ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-b
|
|
|
155
156
|
<nav aria-label={n.sections} class={tabsNavClass}>
|
|
156
157
|
{navigation.tabs.map((tab) => (
|
|
157
158
|
<a
|
|
158
|
-
aria-current={
|
|
159
|
-
route === tab.path ||
|
|
160
|
-
(tab.path !== "/" && isUnderPath(route, tab.path))
|
|
161
|
-
? "page"
|
|
162
|
-
: undefined
|
|
163
|
-
}
|
|
159
|
+
aria-current={tab === activeTab ? "page" : undefined}
|
|
164
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"
|
|
165
|
-
href={withBase(tab.path)}
|
|
161
|
+
href={withBase(tab.href ?? tab.path)}
|
|
166
162
|
>
|
|
167
163
|
{tab.label}
|
|
168
164
|
</a>
|
|
@@ -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
|
|
@@ -39,7 +39,7 @@ import Favicon from "./Favicon.astro";
|
|
|
39
39
|
import Fonts from "./Fonts.astro";
|
|
40
40
|
import { BANNER_INIT_SCRIPT, THEME_INIT_SCRIPT } from "./head-scripts.ts";
|
|
41
41
|
import Header from "./Header.astro";
|
|
42
|
-
import {
|
|
42
|
+
import { activeTabForRoute } from "./nav-utils.ts";
|
|
43
43
|
|
|
44
44
|
interface Props {
|
|
45
45
|
site: { title: string; description?: string };
|
|
@@ -137,6 +137,7 @@ const searchLocale =
|
|
|
137
137
|
const pageTitle = page?.title ?? site.title;
|
|
138
138
|
const description = page?.description ?? site.description;
|
|
139
139
|
const route = page?.route ?? "/";
|
|
140
|
+
const activeTab = activeTabForRoute(navigation.tabs, route);
|
|
140
141
|
|
|
141
142
|
// Derive canonical + og:image from the site URL the same way the catch-all does
|
|
142
143
|
// for content pages, so a custom page gets both for free. The matching OG card
|
|
@@ -267,14 +268,9 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
267
268
|
{navigation.tabs.map((tab) => (
|
|
268
269
|
<li>
|
|
269
270
|
<a
|
|
270
|
-
aria-current={
|
|
271
|
-
route === tab.path ||
|
|
272
|
-
(tab.path !== "/" && isUnderPath(route, tab.path))
|
|
273
|
-
? "page"
|
|
274
|
-
: undefined
|
|
275
|
-
}
|
|
271
|
+
aria-current={tab === activeTab ? "page" : undefined}
|
|
276
272
|
class="block rounded-[0.65rem] px-2.5 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:text-foreground"
|
|
277
|
-
href={withBase(tab.path)}
|
|
273
|
+
href={withBase(tab.href ?? tab.path)}
|
|
278
274
|
>
|
|
279
275
|
{tab.label}
|
|
280
276
|
</a>
|