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.
Files changed (104) hide show
  1. package/dist/cli/index.js +1162 -455
  2. package/dist/cli/index.js.map +32 -28
  3. package/dist/types/core/bridge.d.ts +24 -0
  4. package/dist/types/core/config.d.ts +35 -0
  5. package/dist/types/core/data.d.ts +129 -0
  6. package/dist/types/core/define-components.d.ts +27 -0
  7. package/dist/types/core/define-meta.d.ts +16 -0
  8. package/dist/types/core/deployment-env.d.ts +9 -0
  9. package/dist/types/core/diagnostics.d.ts +17 -0
  10. package/dist/types/core/i18n-ui.d.ts +500 -0
  11. package/dist/types/core/load-module.d.ts +7 -0
  12. package/dist/types/core/package-root.d.ts +17 -0
  13. package/dist/types/core/project.d.ts +9 -0
  14. package/dist/types/core/schema.d.ts +3451 -0
  15. package/dist/types/core/sources/types.d.ts +107 -0
  16. package/dist/types/core/types.d.ts +245 -0
  17. package/dist/types/core/ui-packs/ar.d.ts +3 -0
  18. package/dist/types/core/ui-packs/bg.d.ts +3 -0
  19. package/dist/types/core/ui-packs/bn.d.ts +3 -0
  20. package/dist/types/core/ui-packs/ca.d.ts +3 -0
  21. package/dist/types/core/ui-packs/cs.d.ts +3 -0
  22. package/dist/types/core/ui-packs/da.d.ts +3 -0
  23. package/dist/types/core/ui-packs/de.d.ts +3 -0
  24. package/dist/types/core/ui-packs/el.d.ts +3 -0
  25. package/dist/types/core/ui-packs/es.d.ts +3 -0
  26. package/dist/types/core/ui-packs/fa.d.ts +3 -0
  27. package/dist/types/core/ui-packs/fi.d.ts +3 -0
  28. package/dist/types/core/ui-packs/fr.d.ts +3 -0
  29. package/dist/types/core/ui-packs/he.d.ts +3 -0
  30. package/dist/types/core/ui-packs/hi.d.ts +3 -0
  31. package/dist/types/core/ui-packs/hr.d.ts +3 -0
  32. package/dist/types/core/ui-packs/hu.d.ts +3 -0
  33. package/dist/types/core/ui-packs/id.d.ts +3 -0
  34. package/dist/types/core/ui-packs/index.d.ts +13 -0
  35. package/dist/types/core/ui-packs/it.d.ts +3 -0
  36. package/dist/types/core/ui-packs/ja.d.ts +3 -0
  37. package/dist/types/core/ui-packs/ko.d.ts +3 -0
  38. package/dist/types/core/ui-packs/nl.d.ts +3 -0
  39. package/dist/types/core/ui-packs/no.d.ts +3 -0
  40. package/dist/types/core/ui-packs/pl.d.ts +3 -0
  41. package/dist/types/core/ui-packs/pt-br.d.ts +3 -0
  42. package/dist/types/core/ui-packs/pt.d.ts +3 -0
  43. package/dist/types/core/ui-packs/ro.d.ts +3 -0
  44. package/dist/types/core/ui-packs/ru.d.ts +3 -0
  45. package/dist/types/core/ui-packs/sk.d.ts +3 -0
  46. package/dist/types/core/ui-packs/sr.d.ts +3 -0
  47. package/dist/types/core/ui-packs/sv.d.ts +3 -0
  48. package/dist/types/core/ui-packs/th.d.ts +3 -0
  49. package/dist/types/core/ui-packs/tr.d.ts +3 -0
  50. package/dist/types/core/ui-packs/uk.d.ts +3 -0
  51. package/dist/types/core/ui-packs/vi.d.ts +3 -0
  52. package/dist/types/core/ui-packs/zh-tw.d.ts +3 -0
  53. package/dist/types/core/ui-packs/zh.d.ts +3 -0
  54. package/dist/types/core/version.d.ts +8 -0
  55. package/dist/types/index.d.ts +10 -0
  56. package/dist/types/migrate/mintlify/config.d.ts +2 -0
  57. package/dist/types/migrate/mintlify/i18n.d.ts +7 -0
  58. package/dist/types/theme/fonts.d.ts +163 -0
  59. package/docs/advanced/custom-pages.mdx +91 -10
  60. package/docs/configuration/index.mdx +19 -0
  61. package/docs/content/components.mdx +87 -1
  62. package/package.json +13 -3
  63. package/src/ai/llms.ts +1 -2
  64. package/src/astro/examples.ts +104 -0
  65. package/src/astro/generate.ts +197 -69
  66. package/src/astro/islands.ts +1 -1
  67. package/src/astro/pages.ts +59 -0
  68. package/src/astro/templates.ts +136 -9
  69. package/src/cli/commands/validate.ts +1 -0
  70. package/src/components/content/CodeBlock.astro +28 -0
  71. package/src/components/content/Component.astro +65 -0
  72. package/src/components/content/Diff.astro +44 -0
  73. package/src/components/content/Tab.astro +8 -2
  74. package/src/components/content/diff.ts +95 -0
  75. package/src/components/layout/Favicon.astro +11 -4
  76. package/src/components/layout/PageLayout.astro +173 -0
  77. package/src/components/layout/ReferenceLayout.astro +6 -1
  78. package/src/components/layout/RootLayout.astro +9 -5
  79. package/src/components/layout/head-scripts.ts +19 -0
  80. package/src/core/data.ts +128 -0
  81. package/src/core/frontmatter.ts +43 -0
  82. package/src/core/links.ts +26 -10
  83. package/src/core/sources/filesystem.ts +1 -1
  84. package/src/core/sources/mdx-remote.ts +1 -2
  85. package/src/core/sources/mintlify.ts +1 -1
  86. package/src/core/sources/normalize.ts +19 -3
  87. package/src/core/sources/notion.ts +1 -1
  88. package/src/core/sources/sanity.ts +1 -1
  89. package/src/core/tsconfig-aliases.ts +201 -0
  90. package/src/index.ts +12 -0
  91. package/src/markdown/index.ts +69 -0
  92. package/src/migrate/fumadocs/config.ts +53 -4
  93. package/src/migrate/fumadocs/content.ts +1 -1
  94. package/src/migrate/fumadocs/groups.ts +230 -0
  95. package/src/migrate/fumadocs/index.ts +104 -19
  96. package/src/migrate/fumadocs/meta.ts +140 -10
  97. package/src/migrate/mintlify/snippets.ts +2 -1
  98. package/src/migrate/mintlify/transform.ts +1 -2
  99. package/src/migrate/nextra/index.ts +1 -1
  100. package/src/migrate/shared.ts +98 -1
  101. package/src/migrate/starlight/index.ts +1 -1
  102. package/src/registry/eject.ts +57 -10
  103. package/src/search/documents.ts +1 -2
  104. package/src/theme/entry.ts +29 -0
@@ -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 { contentRoutes, needsSvelte, needsVue, searchClientPath } = options;
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 = (): string =>
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
- return data.routes.map((route) => ({
657
- params: { slug: route.path === "/" ? "index" : route.path.slice(1) },
658
- props: { title: route.title },
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="px-4 py-3 [&:has(>pre)]:p-0 [&>:first-child]:mt-0! [&>:last-child]:mb-0!"
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 favicon <link> for the document <head>. The href is either a URL to
3
- // a file the project ships in public/, or a data URI (a root-level icon, or the
4
- // bundled Blume default) — both resolved by the generator's resolveFavicon.
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" />}