blume 0.1.0 → 0.1.1
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 +1162 -455
- package/dist/cli/index.js.map +32 -28
- package/dist/types/core/bridge.d.ts +24 -0
- package/dist/types/core/config.d.ts +35 -0
- package/dist/types/core/data.d.ts +129 -0
- package/dist/types/core/define-components.d.ts +27 -0
- package/dist/types/core/define-meta.d.ts +16 -0
- package/dist/types/core/deployment-env.d.ts +9 -0
- package/dist/types/core/diagnostics.d.ts +17 -0
- package/dist/types/core/i18n-ui.d.ts +500 -0
- package/dist/types/core/load-module.d.ts +7 -0
- package/dist/types/core/package-root.d.ts +17 -0
- package/dist/types/core/project.d.ts +9 -0
- package/dist/types/core/schema.d.ts +3451 -0
- package/dist/types/core/sources/types.d.ts +107 -0
- package/dist/types/core/types.d.ts +245 -0
- package/dist/types/core/ui-packs/ar.d.ts +3 -0
- package/dist/types/core/ui-packs/bg.d.ts +3 -0
- package/dist/types/core/ui-packs/bn.d.ts +3 -0
- package/dist/types/core/ui-packs/ca.d.ts +3 -0
- package/dist/types/core/ui-packs/cs.d.ts +3 -0
- package/dist/types/core/ui-packs/da.d.ts +3 -0
- package/dist/types/core/ui-packs/de.d.ts +3 -0
- package/dist/types/core/ui-packs/el.d.ts +3 -0
- package/dist/types/core/ui-packs/es.d.ts +3 -0
- package/dist/types/core/ui-packs/fa.d.ts +3 -0
- package/dist/types/core/ui-packs/fi.d.ts +3 -0
- package/dist/types/core/ui-packs/fr.d.ts +3 -0
- package/dist/types/core/ui-packs/he.d.ts +3 -0
- package/dist/types/core/ui-packs/hi.d.ts +3 -0
- package/dist/types/core/ui-packs/hr.d.ts +3 -0
- package/dist/types/core/ui-packs/hu.d.ts +3 -0
- package/dist/types/core/ui-packs/id.d.ts +3 -0
- package/dist/types/core/ui-packs/index.d.ts +13 -0
- package/dist/types/core/ui-packs/it.d.ts +3 -0
- package/dist/types/core/ui-packs/ja.d.ts +3 -0
- package/dist/types/core/ui-packs/ko.d.ts +3 -0
- package/dist/types/core/ui-packs/nl.d.ts +3 -0
- package/dist/types/core/ui-packs/no.d.ts +3 -0
- package/dist/types/core/ui-packs/pl.d.ts +3 -0
- package/dist/types/core/ui-packs/pt-br.d.ts +3 -0
- package/dist/types/core/ui-packs/pt.d.ts +3 -0
- package/dist/types/core/ui-packs/ro.d.ts +3 -0
- package/dist/types/core/ui-packs/ru.d.ts +3 -0
- package/dist/types/core/ui-packs/sk.d.ts +3 -0
- package/dist/types/core/ui-packs/sr.d.ts +3 -0
- package/dist/types/core/ui-packs/sv.d.ts +3 -0
- package/dist/types/core/ui-packs/th.d.ts +3 -0
- package/dist/types/core/ui-packs/tr.d.ts +3 -0
- package/dist/types/core/ui-packs/uk.d.ts +3 -0
- package/dist/types/core/ui-packs/vi.d.ts +3 -0
- package/dist/types/core/ui-packs/zh-tw.d.ts +3 -0
- package/dist/types/core/ui-packs/zh.d.ts +3 -0
- package/dist/types/core/version.d.ts +8 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/migrate/mintlify/config.d.ts +2 -0
- package/dist/types/migrate/mintlify/i18n.d.ts +7 -0
- package/dist/types/theme/fonts.d.ts +163 -0
- package/docs/advanced/custom-pages.mdx +91 -10
- package/docs/configuration/index.mdx +19 -0
- package/docs/content/components.mdx +87 -1
- package/package.json +13 -3
- package/src/ai/llms.ts +1 -2
- package/src/astro/examples.ts +104 -0
- package/src/astro/generate.ts +197 -69
- package/src/astro/islands.ts +1 -1
- package/src/astro/pages.ts +59 -0
- package/src/astro/templates.ts +136 -9
- package/src/cli/commands/validate.ts +1 -0
- package/src/components/content/CodeBlock.astro +28 -0
- package/src/components/content/Component.astro +65 -0
- package/src/components/content/Diff.astro +44 -0
- package/src/components/content/Tab.astro +8 -2
- package/src/components/content/diff.ts +95 -0
- package/src/components/layout/Favicon.astro +11 -4
- package/src/components/layout/PageLayout.astro +173 -0
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +9 -5
- package/src/components/layout/head-scripts.ts +19 -0
- package/src/core/data.ts +128 -0
- package/src/core/frontmatter.ts +43 -0
- package/src/core/links.ts +26 -10
- package/src/core/sources/filesystem.ts +1 -1
- package/src/core/sources/mdx-remote.ts +1 -2
- package/src/core/sources/mintlify.ts +1 -1
- package/src/core/sources/normalize.ts +19 -3
- package/src/core/sources/notion.ts +1 -1
- package/src/core/sources/sanity.ts +1 -1
- package/src/core/tsconfig-aliases.ts +201 -0
- package/src/index.ts +12 -0
- package/src/markdown/index.ts +69 -0
- package/src/migrate/fumadocs/config.ts +53 -4
- package/src/migrate/fumadocs/content.ts +1 -1
- package/src/migrate/fumadocs/groups.ts +230 -0
- package/src/migrate/fumadocs/index.ts +104 -19
- package/src/migrate/fumadocs/meta.ts +140 -10
- package/src/migrate/mintlify/snippets.ts +2 -1
- package/src/migrate/mintlify/transform.ts +1 -2
- package/src/migrate/nextra/index.ts +1 -1
- package/src/migrate/shared.ts +98 -1
- package/src/migrate/starlight/index.ts +1 -1
- package/src/registry/eject.ts +57 -10
- package/src/search/documents.ts +1 -2
- package/src/theme/entry.ts +29 -0
package/src/astro/templates.ts
CHANGED
|
@@ -8,8 +8,10 @@ import type { ResolvedConfig } from "../core/schema.ts";
|
|
|
8
8
|
import type { ProjectContext } from "../core/types.ts";
|
|
9
9
|
import { searchProviderMeta } from "../search/providers.ts";
|
|
10
10
|
import { buildFontEntries } from "../theme/fonts.ts";
|
|
11
|
+
import type { ExampleSpec } from "./examples.ts";
|
|
11
12
|
import type { BlumePageRoute } from "./integration.ts";
|
|
12
13
|
import type { IslandSpec } from "./islands.ts";
|
|
14
|
+
import type { OgCustomRoute } from "./pages.ts";
|
|
13
15
|
|
|
14
16
|
const WORKSPACE_MARKERS = [
|
|
15
17
|
".git",
|
|
@@ -122,6 +124,23 @@ export const runtimeDependencies = (options: {
|
|
|
122
124
|
};
|
|
123
125
|
|
|
124
126
|
/** Generate `.blume/astro.config.mjs`. */
|
|
127
|
+
/**
|
|
128
|
+
* Render project tsconfig path aliases as `vite.resolve.alias` object entries.
|
|
129
|
+
* Longest find first, so a more specific prefix (`@components`) is matched
|
|
130
|
+
* before a broader one (`@`); these follow Blume's `blume:*` aliases, which
|
|
131
|
+
* never overlap with a project's.
|
|
132
|
+
*/
|
|
133
|
+
const renderUserAliases = (
|
|
134
|
+
aliases: Record<string, string> | undefined
|
|
135
|
+
): string =>
|
|
136
|
+
Object.entries(aliases ?? {})
|
|
137
|
+
.toSorted(([a], [b]) => b.length - a.length)
|
|
138
|
+
.map(
|
|
139
|
+
([find, replacement]) =>
|
|
140
|
+
`\n ${JSON.stringify(find)}: ${JSON.stringify(replacement)},`
|
|
141
|
+
)
|
|
142
|
+
.join("");
|
|
143
|
+
|
|
125
144
|
export const astroConfigTemplate = (options: {
|
|
126
145
|
context: ProjectContext;
|
|
127
146
|
config: ResolvedConfig;
|
|
@@ -131,12 +150,22 @@ export const astroConfigTemplate = (options: {
|
|
|
131
150
|
pages: BlumePageRoute[];
|
|
132
151
|
contentRoutes: string[];
|
|
133
152
|
dataPath: string;
|
|
153
|
+
examplesPath: string;
|
|
134
154
|
themePath: string;
|
|
135
155
|
searchClientPath: string;
|
|
156
|
+
/** Project tsconfig path aliases (`find` -> absolute dir), e.g. `@` -> src. */
|
|
157
|
+
aliases?: Record<string, string>;
|
|
136
158
|
}): string => {
|
|
137
159
|
const { context, config, needsReact, pages, dataPath, themePath } = options;
|
|
138
|
-
const {
|
|
160
|
+
const {
|
|
161
|
+
contentRoutes,
|
|
162
|
+
examplesPath,
|
|
163
|
+
needsSvelte,
|
|
164
|
+
needsVue,
|
|
165
|
+
searchClientPath,
|
|
166
|
+
} = options;
|
|
139
167
|
const { deployment } = config;
|
|
168
|
+
const userAliasLines = renderUserAliases(options.aliases);
|
|
140
169
|
const server = deployment.output === "server";
|
|
141
170
|
|
|
142
171
|
// The project root plus the workspace root, so hoisted dependencies (e.g.
|
|
@@ -293,8 +322,9 @@ export default defineConfig({
|
|
|
293
322
|
resolve: {
|
|
294
323
|
alias: {
|
|
295
324
|
"blume:data": ${JSON.stringify(dataPath)},
|
|
325
|
+
"blume:examples": ${JSON.stringify(examplesPath)},
|
|
296
326
|
"blume:search-client": ${JSON.stringify(searchClientPath)},
|
|
297
|
-
"blume:theme": ${JSON.stringify(themePath)}
|
|
327
|
+
"blume:theme": ${JSON.stringify(themePath)},${userAliasLines}
|
|
298
328
|
},
|
|
299
329
|
},
|
|
300
330
|
server: {
|
|
@@ -645,25 +675,47 @@ export function GET({ props }) {
|
|
|
645
675
|
`;
|
|
646
676
|
|
|
647
677
|
/** Generate the OG image endpoint (`.blume/src/pages/_og/[...slug].png.ts`). */
|
|
648
|
-
export const ogEndpointTemplate = (
|
|
678
|
+
export const ogEndpointTemplate = (
|
|
679
|
+
customRoutes: OgCustomRoute[] = []
|
|
680
|
+
): string =>
|
|
649
681
|
`// Generated by Blume. Do not edit.
|
|
650
682
|
import { renderOgImage } from "blume/og";
|
|
651
683
|
import data from "../../generated/data.json";
|
|
652
684
|
|
|
653
685
|
export const prerender = true;
|
|
654
686
|
|
|
687
|
+
// Custom (non-content) pages opted into a generated card, baked in at build.
|
|
688
|
+
const customRoutes = ${JSON.stringify(customRoutes)};
|
|
689
|
+
|
|
655
690
|
export function getStaticPaths() {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
691
|
+
const seen = new Set();
|
|
692
|
+
const paths = [];
|
|
693
|
+
const add = (slug, title, eyebrow) => {
|
|
694
|
+
if (seen.has(slug)) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
seen.add(slug);
|
|
698
|
+
paths.push({ params: { slug }, props: { eyebrow, title } });
|
|
699
|
+
};
|
|
700
|
+
// A custom page wins over a content route sharing its path, so add it first.
|
|
701
|
+
for (const route of customRoutes) {
|
|
702
|
+
add(route.slug, route.title, route.eyebrow);
|
|
703
|
+
}
|
|
704
|
+
for (const route of data.routes) {
|
|
705
|
+
add(
|
|
706
|
+
route.path === "/" ? "index" : route.path.slice(1),
|
|
707
|
+
route.title,
|
|
708
|
+
data.config.title
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
return paths;
|
|
660
712
|
}
|
|
661
713
|
|
|
662
714
|
export async function GET({ props }) {
|
|
663
715
|
const png = await renderOgImage({
|
|
664
|
-
title: props.title,
|
|
665
|
-
eyebrow: data.config.title,
|
|
666
716
|
accent: data.config.theme.accent,
|
|
717
|
+
eyebrow: props.eyebrow,
|
|
718
|
+
title: props.title,
|
|
667
719
|
});
|
|
668
720
|
return new Response(png, {
|
|
669
721
|
headers: {
|
|
@@ -706,6 +758,7 @@ const configuration = ${JSON.stringify(options.configuration, null, 2)};
|
|
|
706
758
|
fontCssVars={data.fontCssVars}
|
|
707
759
|
logo={data.config.logo}
|
|
708
760
|
favicon={data.config.favicon}
|
|
761
|
+
appleIcon={data.config.appleIcon}
|
|
709
762
|
navigation={data.navigation}
|
|
710
763
|
pageTitle={${JSON.stringify(options.title)}}
|
|
711
764
|
route={${JSON.stringify(options.route)}}
|
|
@@ -746,12 +799,15 @@ import Badge from "blume/components/content/Badge.astro";
|
|
|
746
799
|
import Callout from "blume/components/content/Callout.astro";
|
|
747
800
|
import Card from "blume/components/content/Card.astro";
|
|
748
801
|
import CardGroup from "blume/components/content/CardGroup.astro";
|
|
802
|
+
import CodeBlock from "blume/components/content/CodeBlock.astro";
|
|
749
803
|
import CodeGroup from "blume/components/content/CodeGroup.astro";
|
|
750
804
|
import ColorRoot from "blume/components/content/Color.astro";
|
|
751
805
|
import ColorItem from "blume/components/content/ColorItem.astro";
|
|
752
806
|
import ColorRow from "blume/components/content/ColorRow.astro";
|
|
753
807
|
import Column from "blume/components/content/Column.astro";
|
|
754
808
|
import Columns from "blume/components/content/Columns.astro";
|
|
809
|
+
import Component from "blume/components/content/Component.astro";
|
|
810
|
+
import Diff from "blume/components/content/Diff.astro";
|
|
755
811
|
import Expandable from "blume/components/content/Expandable.astro";
|
|
756
812
|
import FileTree from "blume/components/content/FileTree.astro";
|
|
757
813
|
import Frame from "blume/components/content/Frame.astro";
|
|
@@ -791,10 +847,13 @@ const components = {
|
|
|
791
847
|
Callout,
|
|
792
848
|
Card,
|
|
793
849
|
CardGroup,
|
|
850
|
+
CodeBlock,
|
|
794
851
|
CodeGroup,
|
|
795
852
|
Color,
|
|
796
853
|
Column,
|
|
797
854
|
Columns,
|
|
855
|
+
Component,
|
|
856
|
+
Diff,
|
|
798
857
|
Expandable,
|
|
799
858
|
FileTree,
|
|
800
859
|
Frame,
|
|
@@ -915,6 +974,7 @@ const localeSwitch = i18n
|
|
|
915
974
|
logo={data.config.logo}
|
|
916
975
|
mcp={data.config.mcp}
|
|
917
976
|
favicon={data.config.favicon}
|
|
977
|
+
appleIcon={data.config.appleIcon}
|
|
918
978
|
banner={data.config.banner}
|
|
919
979
|
analytics={data.config.analytics}
|
|
920
980
|
imageZoom={data.config.imageZoom}
|
|
@@ -1053,6 +1113,7 @@ const canonical = base ? base + "/changelog" : null;
|
|
|
1053
1113
|
logo={data.config.logo}
|
|
1054
1114
|
mcp={data.config.mcp}
|
|
1055
1115
|
favicon={data.config.favicon}
|
|
1116
|
+
appleIcon={data.config.appleIcon}
|
|
1056
1117
|
banner={data.config.banner}
|
|
1057
1118
|
analytics={data.config.analytics}
|
|
1058
1119
|
imageZoom={data.config.imageZoom}
|
|
@@ -1164,10 +1225,76 @@ ${entries}
|
|
|
1164
1225
|
`;
|
|
1165
1226
|
};
|
|
1166
1227
|
|
|
1228
|
+
/** The literal Astro hydration directive for an example's framework/client. */
|
|
1229
|
+
const exampleDirective = (spec: ExampleSpec): string => {
|
|
1230
|
+
if (spec.framework === "astro" || !spec.client) {
|
|
1231
|
+
return "";
|
|
1232
|
+
}
|
|
1233
|
+
return spec.client === "only"
|
|
1234
|
+
? `client:only="${spec.framework}" `
|
|
1235
|
+
: `client:${spec.client} `;
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
/** Filesystem-safe slug for an example's generated wrapper file. */
|
|
1239
|
+
export const exampleSlug = (path: string): string =>
|
|
1240
|
+
path.replaceAll("/", "__").replaceAll(/[^a-zA-Z0-9_]+/gu, "-");
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Generate `.blume/src/generated/examples/<slug>.astro` — a wrapper that renders
|
|
1244
|
+
* one example live, with its hydration directive applied (none for `.astro`).
|
|
1245
|
+
* Mirrors {@link islandWrapperTemplate}; `<Component>` resolves these by path.
|
|
1246
|
+
*/
|
|
1247
|
+
export const exampleWrapperTemplate = (spec: ExampleSpec): string =>
|
|
1248
|
+
`---
|
|
1249
|
+
// Generated by Blume. Do not edit.
|
|
1250
|
+
import Example from ${JSON.stringify(spec.file)};
|
|
1251
|
+
---
|
|
1252
|
+
<Example ${exampleDirective(spec)}{...Astro.props}><slot /></Example>
|
|
1253
|
+
`;
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Generate `.blume/src/generated/examples.ts` — a map of example path to its live
|
|
1257
|
+
* wrapper component plus raw source and language for the code tab. Reached by the
|
|
1258
|
+
* shipped `Component.astro` via the `blume:examples` alias. Always written (an
|
|
1259
|
+
* empty object when there are no examples) so the alias resolves.
|
|
1260
|
+
*/
|
|
1261
|
+
export const exampleMapTemplate = (specs: ExampleSpec[]): string => {
|
|
1262
|
+
if (specs.length === 0) {
|
|
1263
|
+
return `// Generated by Blume. Do not edit.
|
|
1264
|
+
export const examples = {};
|
|
1265
|
+
`;
|
|
1266
|
+
}
|
|
1267
|
+
const imports = specs
|
|
1268
|
+
.map(
|
|
1269
|
+
(spec, index) =>
|
|
1270
|
+
`import E${index} from "./examples/${exampleSlug(spec.path)}.astro";`
|
|
1271
|
+
)
|
|
1272
|
+
.join("\n");
|
|
1273
|
+
const entries = specs
|
|
1274
|
+
.map(
|
|
1275
|
+
(spec, index) =>
|
|
1276
|
+
` ${JSON.stringify(spec.path)}: { Component: E${index}, code: ${JSON.stringify(
|
|
1277
|
+
spec.source
|
|
1278
|
+
)}, lang: ${JSON.stringify(spec.lang)} },`
|
|
1279
|
+
)
|
|
1280
|
+
.join("\n");
|
|
1281
|
+
return `// Generated by Blume. Do not edit.
|
|
1282
|
+
${imports}
|
|
1283
|
+
export const examples = {
|
|
1284
|
+
${entries}
|
|
1285
|
+
};
|
|
1286
|
+
`;
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1167
1289
|
/** Generate `.blume/src/env.d.ts`. */
|
|
1168
1290
|
export const envTemplate =
|
|
1169
1291
|
(): string => `/// <reference path="../.astro/types.d.ts" />
|
|
1170
1292
|
/// <reference types="astro/client" />
|
|
1293
|
+
|
|
1294
|
+
declare module "blume:data" {
|
|
1295
|
+
const data: import("blume").BlumeData;
|
|
1296
|
+
export default data;
|
|
1297
|
+
}
|
|
1171
1298
|
`;
|
|
1172
1299
|
|
|
1173
1300
|
/** Generate `.blume/package.json`. */
|
|
@@ -39,6 +39,7 @@ export const validateCommand = defineCommand({
|
|
|
39
39
|
...(await validateLinks(project.graph, {
|
|
40
40
|
checkExternal: Boolean(args.external),
|
|
41
41
|
publicDir: existsSync(publicDir) ? publicDir : null,
|
|
42
|
+
redirects: project.config.redirects,
|
|
42
43
|
}))
|
|
43
44
|
);
|
|
44
45
|
} catch (error) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Render a code string highlighted with Blume's Markdown code-block theme — the
|
|
3
|
+
// same Shiki themes, transformers, and dual light/dark swap as fenced code.
|
|
4
|
+
// Use it to show themed code outside the Markdown pipeline (a landing page, a
|
|
5
|
+
// custom component) where there's no fence to write.
|
|
6
|
+
//
|
|
7
|
+
// <CodeBlock lang="ts" code={`export const x = 1;`} />
|
|
8
|
+
//
|
|
9
|
+
// The theme's code styles are scoped to `.prose`, so the output is wrapped in a
|
|
10
|
+
// `.prose` container (`max-w-none` so it fills its own column rather than the
|
|
11
|
+
// prose reading measure); that's what makes the light/dark swap work on a bare
|
|
12
|
+
// page without the manual `[data-theme]` CSS it would otherwise need.
|
|
13
|
+
import { highlightCode } from "../../markdown/index.ts";
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
/** The source to highlight. */
|
|
17
|
+
code: string;
|
|
18
|
+
/** Shiki language id (default `txt`); an unknown one renders plain. */
|
|
19
|
+
lang?: string;
|
|
20
|
+
/** Show the brand language icon in the header (default on, like fences). */
|
|
21
|
+
icons?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { code, lang = "txt", icons } = Astro.props;
|
|
25
|
+
const html = await highlightCode(code.replace(/\n+$/u, ""), lang, { icons });
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<div class="prose max-w-none"><Fragment set:html={html} /></div>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Render an example file (under the project's `examples/` dir) as a live,
|
|
3
|
+
// hydrated preview alongside its highlighted source, in tabs. `path` is the
|
|
4
|
+
// file's location under `examples/` without its extension, e.g.
|
|
5
|
+
// `<Component path="forms/login" />` for `examples/forms/login.tsx`.
|
|
6
|
+
//
|
|
7
|
+
// Blume discovers `examples/` at build time and generates `blume:examples`: a
|
|
8
|
+
// map of path → { Component (a static hydration wrapper), code, lang }. React,
|
|
9
|
+
// Vue, Svelte, and Astro examples are all supported. The source is highlighted
|
|
10
|
+
// with the same Shiki setup as ordinary code fences.
|
|
11
|
+
import { highlightCode } from "../../markdown/index.ts";
|
|
12
|
+
import Tab from "./Tab.astro";
|
|
13
|
+
import Tabs from "./Tabs.astro";
|
|
14
|
+
// Generated per project; resolves to an empty map when there are no examples.
|
|
15
|
+
import { examples } from "blume:examples";
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
path: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { path } = Astro.props;
|
|
22
|
+
const entry = examples[path];
|
|
23
|
+
const Preview = entry?.Component;
|
|
24
|
+
|
|
25
|
+
// Highlight the source with the shared helper so it matches Markdown fences.
|
|
26
|
+
// `blume-source` makes the inner `<code>` (not the `<pre>`) the scroll/height-
|
|
27
|
+
// capped box — see the theme entry — so the copy button, pinned to the `<pre>`,
|
|
28
|
+
// stays put. This pane already lives inside `.prose`, so the theme styles it.
|
|
29
|
+
const codeHtml = entry
|
|
30
|
+
? await highlightCode(entry.code, entry.lang, { className: "blume-source" })
|
|
31
|
+
: undefined;
|
|
32
|
+
|
|
33
|
+
// Both tabs share one height so toggling them never shifts the layout. Size it
|
|
34
|
+
// to the source (≈21px/line + padding), clamped to a comfortable 18rem floor and
|
|
35
|
+
// a 400px ceiling — taller sources scroll inside the pane.
|
|
36
|
+
const LINE_PX = 21;
|
|
37
|
+
const PADDING_PX = 36;
|
|
38
|
+
const lineCount = entry ? entry.code.replace(/\n+$/u, "").split("\n").length : 0;
|
|
39
|
+
const paneHeight = Math.min(400, Math.max(288, lineCount * LINE_PX + PADDING_PX));
|
|
40
|
+
const paneStyle = `height:${paneHeight}px`;
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
entry ? (
|
|
45
|
+
// `sync={false}`: each preview's Preview/Code tabs are independent — unlike
|
|
46
|
+
// CodeGroup, switching one Component must not switch the others.
|
|
47
|
+
<Tabs hash={false} sync={false}>
|
|
48
|
+
<Tab
|
|
49
|
+
class="flex items-center justify-center overflow-auto"
|
|
50
|
+
style={paneStyle}
|
|
51
|
+
title="Preview"
|
|
52
|
+
>
|
|
53
|
+
<Preview />
|
|
54
|
+
</Tab>
|
|
55
|
+
<Tab class="overflow-hidden" style={paneStyle} title="Code">
|
|
56
|
+
<Fragment set:html={codeHtml} />
|
|
57
|
+
</Tab>
|
|
58
|
+
</Tabs>
|
|
59
|
+
) : (
|
|
60
|
+
<div class="not-prose my-6 rounded-blume border border-border px-3 py-2 text-muted-foreground text-sm">
|
|
61
|
+
No example found at <code class="font-mono">{path}</code>. Add it under{" "}
|
|
62
|
+
<code class="font-mono">examples/</code> in your project.
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Render a git-style diff with `@pierre/diffs`, fully at build time. The library
|
|
3
|
+
// emits a self-contained blob whose styles are scoped to `:host`, so we mount it
|
|
4
|
+
// in a declarative shadow root — no client JS. `--blume-diff-color-scheme`
|
|
5
|
+
// (set in the theme entry from `data-theme`) drives the diff's `light-dark()`.
|
|
6
|
+
//
|
|
7
|
+
// Inputs (one of): `patch`/`src` (unified patch), `before`+`after` (file paths),
|
|
8
|
+
// or `old`+`new` (inline strings, with optional `lang`).
|
|
9
|
+
import { renderDiff } from "./diff.ts";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
after?: string;
|
|
13
|
+
before?: string;
|
|
14
|
+
lang?: string;
|
|
15
|
+
new?: string;
|
|
16
|
+
old?: string;
|
|
17
|
+
patch?: string;
|
|
18
|
+
src?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let html: string | undefined;
|
|
22
|
+
let error: string | undefined;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
html = await renderDiff(Astro.props);
|
|
26
|
+
} catch (cause) {
|
|
27
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const COLOR_SCHEME =
|
|
31
|
+
"<style>:host{color-scheme:var(--blume-diff-color-scheme, light dark)}</style>";
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
error ? (
|
|
36
|
+
<div class="not-prose my-6 rounded-blume border border-border px-3 py-2 text-muted-foreground text-sm">
|
|
37
|
+
Could not render diff: {error}
|
|
38
|
+
</div>
|
|
39
|
+
) : (
|
|
40
|
+
<blume-diff class="not-prose my-6 block overflow-hidden rounded-blume border border-border">
|
|
41
|
+
<template shadowrootmode="open" set:html={html + COLOR_SCHEME} />
|
|
42
|
+
</blume-diff>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
import Icon from "../Icon.astro";
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
|
+
class?: string;
|
|
5
6
|
icon?: string;
|
|
6
7
|
iconType?: string;
|
|
7
8
|
id?: string;
|
|
9
|
+
style?: string;
|
|
8
10
|
title: string;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
const { icon, iconType, id, title } = Astro.props;
|
|
13
|
+
const { class: className, icon, iconType, id, style, title } = Astro.props;
|
|
12
14
|
const slugify = (value: string): string =>
|
|
13
15
|
value
|
|
14
16
|
.toLowerCase()
|
|
@@ -21,8 +23,12 @@ const tabId = id ?? slugify(title);
|
|
|
21
23
|
---
|
|
22
24
|
|
|
23
25
|
<div
|
|
24
|
-
class=
|
|
26
|
+
class:list={[
|
|
27
|
+
"px-4 py-3 [&:has(>pre)]:p-0 [&>:first-child]:mt-0! [&>:last-child]:mb-0!",
|
|
28
|
+
className,
|
|
29
|
+
]}
|
|
25
30
|
data-blume-tab-panel
|
|
31
|
+
style={style}
|
|
26
32
|
data-icon={icon}
|
|
27
33
|
data-tab-id={tabId}
|
|
28
34
|
data-title={title}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time diff rendering for `<Diff>`.
|
|
3
|
+
*
|
|
4
|
+
* Produces a self-contained HTML string with [`@pierre/diffs`](https://diffs.com)
|
|
5
|
+
* via its `./ssr` entry — no DOM, no React, no client JS. The returned markup
|
|
6
|
+
* carries its own `:host` stylesheet and is meant to be placed inside a
|
|
7
|
+
* declarative shadow root (see `Diff.astro`). Three input shapes are supported:
|
|
8
|
+
* a unified patch (string or `.patch`/`.diff` file), a pair of file paths, or a
|
|
9
|
+
* pair of inline strings.
|
|
10
|
+
*/
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
|
|
13
|
+
import { preloadDiffHTML, preloadPatchDiff } from "@pierre/diffs/ssr";
|
|
14
|
+
import { isAbsolute, join } from "pathe";
|
|
15
|
+
|
|
16
|
+
export interface DiffOptions {
|
|
17
|
+
/** Path to the "after" file, resolved relative to {@link DiffOptions.root}. */
|
|
18
|
+
after?: string;
|
|
19
|
+
/** Path to the "before" file, resolved relative to {@link DiffOptions.root}. */
|
|
20
|
+
before?: string;
|
|
21
|
+
/** Language for inline {@link DiffOptions.old}/{@link DiffOptions.new} input. */
|
|
22
|
+
lang?: string;
|
|
23
|
+
/** The "after" contents, as an inline string. Pairs with {@link DiffOptions.old}. */
|
|
24
|
+
new?: string;
|
|
25
|
+
/** The "before" contents, as an inline string. Pairs with {@link DiffOptions.new}. */
|
|
26
|
+
old?: string;
|
|
27
|
+
/** A unified diff/patch as an inline string. */
|
|
28
|
+
patch?: string;
|
|
29
|
+
/** Base directory for resolving relative paths. Defaults to `process.cwd()`. */
|
|
30
|
+
root?: string;
|
|
31
|
+
/** Path to a `.patch`/`.diff` file, resolved relative to {@link DiffOptions.root}. */
|
|
32
|
+
src?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Dual-theme config mirroring Blume's Shiki convention (see `templates.ts`). */
|
|
36
|
+
const THEME = { dark: "github-dark", light: "github-light" } as const;
|
|
37
|
+
|
|
38
|
+
const resolvePath = (path: string, root: string): string =>
|
|
39
|
+
isAbsolute(path) ? path : join(root, path);
|
|
40
|
+
|
|
41
|
+
const readText = (path: string, root: string): Promise<string> =>
|
|
42
|
+
readFile(resolvePath(path, root), "utf-8");
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve `<Diff>` inputs to a prerendered HTML string. Throws when no input
|
|
46
|
+
* group is supplied or a pair is half-specified, so the component can degrade
|
|
47
|
+
* to an inline notice.
|
|
48
|
+
*/
|
|
49
|
+
export const renderDiff = async (options: DiffOptions): Promise<string> => {
|
|
50
|
+
const {
|
|
51
|
+
after,
|
|
52
|
+
before,
|
|
53
|
+
lang,
|
|
54
|
+
new: newText,
|
|
55
|
+
old,
|
|
56
|
+
patch,
|
|
57
|
+
root = process.cwd(),
|
|
58
|
+
src,
|
|
59
|
+
} = options;
|
|
60
|
+
|
|
61
|
+
if (patch !== undefined || src !== undefined) {
|
|
62
|
+
const text = patch ?? (await readText(src as string, root));
|
|
63
|
+
const result = await preloadPatchDiff({
|
|
64
|
+
options: { theme: THEME },
|
|
65
|
+
patch: text,
|
|
66
|
+
});
|
|
67
|
+
return result.prerenderedHTML;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (before !== undefined || after !== undefined) {
|
|
71
|
+
if (before === undefined || after === undefined) {
|
|
72
|
+
throw new Error("<Diff> needs both `before` and `after` file paths.");
|
|
73
|
+
}
|
|
74
|
+
return await preloadDiffHTML({
|
|
75
|
+
newFile: { contents: await readText(after, root), name: after },
|
|
76
|
+
oldFile: { contents: await readText(before, root), name: before },
|
|
77
|
+
options: { theme: THEME },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (old !== undefined || newText !== undefined) {
|
|
82
|
+
if (old === undefined || newText === undefined) {
|
|
83
|
+
throw new Error("<Diff> needs both `old` and `new` strings.");
|
|
84
|
+
}
|
|
85
|
+
return await preloadDiffHTML({
|
|
86
|
+
newFile: { contents: newText, lang, name: "snippet" },
|
|
87
|
+
oldFile: { contents: old, lang, name: "snippet" },
|
|
88
|
+
options: { disableFileHeader: true, theme: THEME },
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw new Error(
|
|
93
|
+
"<Diff> requires one input: `patch`/`src`, `before`+`after`, or `old`+`new`."
|
|
94
|
+
);
|
|
95
|
+
};
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
---
|
|
2
|
-
// Emits the
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Emits the icon <link>s for the document <head>: the favicon (rel="icon") and,
|
|
3
|
+
// when the project ships one, the Apple touch icon (rel="apple-touch-icon").
|
|
4
|
+
// Each href is either a URL to a file the project ships in public/, or a data
|
|
5
|
+
// URI (a root-level icon, or the bundled Blume favicon default) — both resolved
|
|
6
|
+
// by the generator's resolveFavicon / resolveAppleIcon.
|
|
5
7
|
interface Props {
|
|
6
8
|
favicon?: {
|
|
7
9
|
href: string;
|
|
8
10
|
type?: string;
|
|
9
11
|
} | null;
|
|
12
|
+
appleIcon?: {
|
|
13
|
+
href: string;
|
|
14
|
+
type?: string;
|
|
15
|
+
} | null;
|
|
10
16
|
}
|
|
11
17
|
|
|
12
|
-
const { favicon } = Astro.props;
|
|
18
|
+
const { appleIcon, favicon } = Astro.props;
|
|
13
19
|
---
|
|
14
20
|
|
|
15
21
|
{favicon && <link href={favicon.href} rel="icon" type={favicon.type} />}
|
|
22
|
+
{appleIcon && <link href={appleIcon.href} rel="apple-touch-icon" />}
|