blume 0.1.0 → 0.1.2

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 (106) hide show
  1. package/dist/cli/index.js +1213 -473
  2. package/dist/cli/index.js.map +33 -29
  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 +3461 -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 +103 -1
  62. package/package.json +13 -3
  63. package/src/ai/llms.ts +1 -2
  64. package/src/astro/examples.ts +106 -0
  65. package/src/astro/generate.ts +300 -75
  66. package/src/astro/index.ts +1 -1
  67. package/src/astro/islands.ts +1 -1
  68. package/src/astro/pages.ts +59 -0
  69. package/src/astro/templates.ts +180 -22
  70. package/src/cli/commands/validate.ts +1 -0
  71. package/src/components/content/CodeBlock.astro +28 -0
  72. package/src/components/content/Component.astro +65 -0
  73. package/src/components/content/Diff.astro +44 -0
  74. package/src/components/content/Tab.astro +8 -2
  75. package/src/components/content/diff.ts +95 -0
  76. package/src/components/layout/Favicon.astro +11 -4
  77. package/src/components/layout/PageLayout.astro +173 -0
  78. package/src/components/layout/ReferenceLayout.astro +6 -1
  79. package/src/components/layout/RootLayout.astro +9 -5
  80. package/src/components/layout/head-scripts.ts +19 -0
  81. package/src/core/data.ts +128 -0
  82. package/src/core/frontmatter.ts +43 -0
  83. package/src/core/links.ts +26 -10
  84. package/src/core/schema.ts +8 -0
  85. package/src/core/sources/filesystem.ts +1 -1
  86. package/src/core/sources/mdx-remote.ts +1 -2
  87. package/src/core/sources/mintlify.ts +1 -1
  88. package/src/core/sources/normalize.ts +19 -3
  89. package/src/core/sources/notion.ts +1 -1
  90. package/src/core/sources/sanity.ts +1 -1
  91. package/src/core/tsconfig-aliases.ts +201 -0
  92. package/src/index.ts +12 -0
  93. package/src/markdown/index.ts +69 -0
  94. package/src/migrate/fumadocs/config.ts +53 -4
  95. package/src/migrate/fumadocs/content.ts +1 -1
  96. package/src/migrate/fumadocs/groups.ts +230 -0
  97. package/src/migrate/fumadocs/index.ts +104 -19
  98. package/src/migrate/fumadocs/meta.ts +140 -10
  99. package/src/migrate/mintlify/snippets.ts +2 -1
  100. package/src/migrate/mintlify/transform.ts +1 -2
  101. package/src/migrate/nextra/index.ts +1 -1
  102. package/src/migrate/shared.ts +98 -1
  103. package/src/migrate/starlight/index.ts +1 -1
  104. package/src/registry/eject.ts +57 -10
  105. package/src/search/documents.ts +1 -2
  106. 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,59 @@ 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
+ /**
134
+ * Blume's render-time dependencies, forced external on the build's SSR and
135
+ * static-prerender Vite environments.
136
+ *
137
+ * Two reasons a dep lands here:
138
+ * - `@takumi-rs/core` (OG image rendering) is a native NAPI addon that loads a
139
+ * platform-specific `.node` binding via `createRequire(import.meta.url)`.
140
+ * Bundling it relocates `import.meta.url` and breaks the binding lookup
141
+ * ("Cannot find native binding") on other platforms (e.g. the Linux CI
142
+ * runner), so it must resolve from `node_modules` at runtime instead.
143
+ * - The rest are pure-JS packages kept external so an isolated linker (Bun's
144
+ * `isolated` mode, pnpm) doesn't bundle their symlinked store copies. When
145
+ * Vite bundles such a package but leaves its own `node_modules` child
146
+ * external, that child surfaces as an unresolvable bare import in the
147
+ * prerender chunk (e.g. `batchwork` via `@astrojs/markdown-satteri`). Kept
148
+ * external, each package's transitive imports resolve relative to its real
149
+ * store location — reachable through the `node_modules` junction {@link
150
+ * prerenderDepsPlugin} drops beside the prerender bundle.
151
+ *
152
+ * Astro 7 configures externalization per Vite environment, so this must be
153
+ * applied to both `prerender` (static) and `ssr` (server) — a top-level
154
+ * `ssr.external` only reaches the latter.
155
+ */
156
+ const RENDER_EXTERNAL_DEPS = [
157
+ "@astrojs/markdown-satteri",
158
+ "@pierre/diffs",
159
+ "@shikijs/transformers",
160
+ "@takumi-rs/core",
161
+ "@takumi-rs/helpers",
162
+ "github-slugger",
163
+ "katex",
164
+ "shiki",
165
+ "simple-icons",
166
+ "zod",
167
+ ];
168
+
169
+ const renderUserAliases = (
170
+ aliases: Record<string, string> | undefined
171
+ ): string =>
172
+ Object.entries(aliases ?? {})
173
+ .toSorted(([a], [b]) => b.length - a.length)
174
+ .map(
175
+ ([find, replacement]) =>
176
+ `\n ${JSON.stringify(find)}: ${JSON.stringify(replacement)},`
177
+ )
178
+ .join("");
179
+
125
180
  export const astroConfigTemplate = (options: {
126
181
  context: ProjectContext;
127
182
  config: ResolvedConfig;
@@ -131,12 +186,22 @@ export const astroConfigTemplate = (options: {
131
186
  pages: BlumePageRoute[];
132
187
  contentRoutes: string[];
133
188
  dataPath: string;
189
+ examplesPath: string;
134
190
  themePath: string;
135
191
  searchClientPath: string;
192
+ /** Project tsconfig path aliases (`find` -> absolute dir), e.g. `@` -> src. */
193
+ aliases?: Record<string, string>;
136
194
  }): string => {
137
195
  const { context, config, needsReact, pages, dataPath, themePath } = options;
138
- const { contentRoutes, needsSvelte, needsVue, searchClientPath } = options;
196
+ const {
197
+ contentRoutes,
198
+ examplesPath,
199
+ needsSvelte,
200
+ needsVue,
201
+ searchClientPath,
202
+ } = options;
139
203
  const { deployment } = config;
204
+ const userAliasLines = renderUserAliases(options.aliases);
140
205
  const server = deployment.output === "server";
141
206
 
142
207
  // The project root plus the workspace root, so hoisted dependencies (e.g.
@@ -214,7 +279,7 @@ export const astroConfigTemplate = (options: {
214
279
  const svelteImport = needsSvelte
215
280
  ? `import svelte from "@astrojs/svelte";\n`
216
281
  : "";
217
- const blumeImport = `import { blumeIntegration } from "blume/astro";\n`;
282
+ const blumeImport = `import { blumeIntegration, prerenderDepsPlugin } from "blume/astro";\n`;
218
283
 
219
284
  // Twoslash runs first, before the always-on transformers, but only on fences
220
285
  // with the `twoslash` meta (explicitTrigger) — so it's opt-in per block with
@@ -276,25 +341,21 @@ export default defineConfig({
276
341
  },
277
342
  devToolbar: { enabled: false },
278
343
  vite: {
279
- plugins: [tailwindcss()],
280
- // @takumi-rs/core (OG image rendering) is a native NAPI addon that loads a
281
- // platform-specific .node binding via createRequire(import.meta.url). Astro's
282
- // build bundles it into the per-environment output by default, which
283
- // relocates import.meta.url and breaks the binding lookup ("Cannot find
284
- // native binding") on other platforms (e.g. the Linux CI runner). Astro 7
285
- // configures externalization per Vite environment, so it must be forced
286
- // external on the prerender (static) and ssr (server) environments -- a
287
- // top-level ssr.external only reaches the latter -- so the binding resolves
288
- // from node_modules at runtime instead.
344
+ plugins: [tailwindcss(), prerenderDepsPlugin()],
345
+ // Blume's render-time deps are forced external on both build environments so
346
+ // native bindings resolve at runtime and isolated linkers don't bundle
347
+ // symlinked store copies (which would surface their children as unresolvable
348
+ // imports). See RENDER_EXTERNAL_DEPS / prerenderDepsPlugin.
289
349
  environments: {
290
- prerender: { resolve: { external: ["@takumi-rs/core"] } },
291
- ssr: { resolve: { external: ["@takumi-rs/core"] } },
350
+ prerender: { resolve: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} } },
351
+ ssr: { resolve: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} } },
292
352
  },
293
353
  resolve: {
294
354
  alias: {
295
355
  "blume:data": ${JSON.stringify(dataPath)},
356
+ "blume:examples": ${JSON.stringify(examplesPath)},
296
357
  "blume:search-client": ${JSON.stringify(searchClientPath)},
297
- "blume:theme": ${JSON.stringify(themePath)},
358
+ "blume:theme": ${JSON.stringify(themePath)},${userAliasLines}
298
359
  },
299
360
  },
300
361
  server: {
@@ -645,25 +706,47 @@ export function GET({ props }) {
645
706
  `;
646
707
 
647
708
  /** Generate the OG image endpoint (`.blume/src/pages/_og/[...slug].png.ts`). */
648
- export const ogEndpointTemplate = (): string =>
709
+ export const ogEndpointTemplate = (
710
+ customRoutes: OgCustomRoute[] = []
711
+ ): string =>
649
712
  `// Generated by Blume. Do not edit.
650
713
  import { renderOgImage } from "blume/og";
651
714
  import data from "../../generated/data.json";
652
715
 
653
716
  export const prerender = true;
654
717
 
718
+ // Custom (non-content) pages opted into a generated card, baked in at build.
719
+ const customRoutes = ${JSON.stringify(customRoutes)};
720
+
655
721
  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
- }));
722
+ const seen = new Set();
723
+ const paths = [];
724
+ const add = (slug, title, eyebrow) => {
725
+ if (seen.has(slug)) {
726
+ return;
727
+ }
728
+ seen.add(slug);
729
+ paths.push({ params: { slug }, props: { eyebrow, title } });
730
+ };
731
+ // A custom page wins over a content route sharing its path, so add it first.
732
+ for (const route of customRoutes) {
733
+ add(route.slug, route.title, route.eyebrow);
734
+ }
735
+ for (const route of data.routes) {
736
+ add(
737
+ route.path === "/" ? "index" : route.path.slice(1),
738
+ route.title,
739
+ data.config.title
740
+ );
741
+ }
742
+ return paths;
660
743
  }
661
744
 
662
745
  export async function GET({ props }) {
663
746
  const png = await renderOgImage({
664
- title: props.title,
665
- eyebrow: data.config.title,
666
747
  accent: data.config.theme.accent,
748
+ eyebrow: props.eyebrow,
749
+ title: props.title,
667
750
  });
668
751
  return new Response(png, {
669
752
  headers: {
@@ -706,6 +789,7 @@ const configuration = ${JSON.stringify(options.configuration, null, 2)};
706
789
  fontCssVars={data.fontCssVars}
707
790
  logo={data.config.logo}
708
791
  favicon={data.config.favicon}
792
+ appleIcon={data.config.appleIcon}
709
793
  navigation={data.navigation}
710
794
  pageTitle={${JSON.stringify(options.title)}}
711
795
  route={${JSON.stringify(options.route)}}
@@ -746,12 +830,15 @@ import Badge from "blume/components/content/Badge.astro";
746
830
  import Callout from "blume/components/content/Callout.astro";
747
831
  import Card from "blume/components/content/Card.astro";
748
832
  import CardGroup from "blume/components/content/CardGroup.astro";
833
+ import CodeBlock from "blume/components/content/CodeBlock.astro";
749
834
  import CodeGroup from "blume/components/content/CodeGroup.astro";
750
835
  import ColorRoot from "blume/components/content/Color.astro";
751
836
  import ColorItem from "blume/components/content/ColorItem.astro";
752
837
  import ColorRow from "blume/components/content/ColorRow.astro";
753
838
  import Column from "blume/components/content/Column.astro";
754
839
  import Columns from "blume/components/content/Columns.astro";
840
+ import Component from "blume/components/content/Component.astro";
841
+ import Diff from "blume/components/content/Diff.astro";
755
842
  import Expandable from "blume/components/content/Expandable.astro";
756
843
  import FileTree from "blume/components/content/FileTree.astro";
757
844
  import Frame from "blume/components/content/Frame.astro";
@@ -791,10 +878,13 @@ const components = {
791
878
  Callout,
792
879
  Card,
793
880
  CardGroup,
881
+ CodeBlock,
794
882
  CodeGroup,
795
883
  Color,
796
884
  Column,
797
885
  Columns,
886
+ Component,
887
+ Diff,
798
888
  Expandable,
799
889
  FileTree,
800
890
  Frame,
@@ -915,6 +1005,7 @@ const localeSwitch = i18n
915
1005
  logo={data.config.logo}
916
1006
  mcp={data.config.mcp}
917
1007
  favicon={data.config.favicon}
1008
+ appleIcon={data.config.appleIcon}
918
1009
  banner={data.config.banner}
919
1010
  analytics={data.config.analytics}
920
1011
  imageZoom={data.config.imageZoom}
@@ -1053,6 +1144,7 @@ const canonical = base ? base + "/changelog" : null;
1053
1144
  logo={data.config.logo}
1054
1145
  mcp={data.config.mcp}
1055
1146
  favicon={data.config.favicon}
1147
+ appleIcon={data.config.appleIcon}
1056
1148
  banner={data.config.banner}
1057
1149
  analytics={data.config.analytics}
1058
1150
  imageZoom={data.config.imageZoom}
@@ -1164,10 +1256,76 @@ ${entries}
1164
1256
  `;
1165
1257
  };
1166
1258
 
1259
+ /** The literal Astro hydration directive for an example's framework/client. */
1260
+ const exampleDirective = (spec: ExampleSpec): string => {
1261
+ if (spec.framework === "astro" || !spec.client) {
1262
+ return "";
1263
+ }
1264
+ return spec.client === "only"
1265
+ ? `client:only="${spec.framework}" `
1266
+ : `client:${spec.client} `;
1267
+ };
1268
+
1269
+ /** Filesystem-safe slug for an example's generated wrapper file. */
1270
+ export const exampleSlug = (path: string): string =>
1271
+ path.replaceAll("/", "__").replaceAll(/[^a-zA-Z0-9_]+/gu, "-");
1272
+
1273
+ /**
1274
+ * Generate `.blume/src/generated/examples/<slug>.astro` — a wrapper that renders
1275
+ * one example live, with its hydration directive applied (none for `.astro`).
1276
+ * Mirrors {@link islandWrapperTemplate}; `<Component>` resolves these by path.
1277
+ */
1278
+ export const exampleWrapperTemplate = (spec: ExampleSpec): string =>
1279
+ `---
1280
+ // Generated by Blume. Do not edit.
1281
+ import Example from ${JSON.stringify(spec.file)};
1282
+ ---
1283
+ <Example ${exampleDirective(spec)}{...Astro.props}><slot /></Example>
1284
+ `;
1285
+
1286
+ /**
1287
+ * Generate `.blume/src/generated/examples.ts` — a map of example path to its live
1288
+ * wrapper component plus raw source and language for the code tab. Reached by the
1289
+ * shipped `Component.astro` via the `blume:examples` alias. Always written (an
1290
+ * empty object when there are no examples) so the alias resolves.
1291
+ */
1292
+ export const exampleMapTemplate = (specs: ExampleSpec[]): string => {
1293
+ if (specs.length === 0) {
1294
+ return `// Generated by Blume. Do not edit.
1295
+ export const examples = {};
1296
+ `;
1297
+ }
1298
+ const imports = specs
1299
+ .map(
1300
+ (spec, index) =>
1301
+ `import E${index} from "./examples/${exampleSlug(spec.path)}.astro";`
1302
+ )
1303
+ .join("\n");
1304
+ const entries = specs
1305
+ .map(
1306
+ (spec, index) =>
1307
+ ` ${JSON.stringify(spec.path)}: { Component: E${index}, code: ${JSON.stringify(
1308
+ spec.source
1309
+ )}, lang: ${JSON.stringify(spec.lang)} },`
1310
+ )
1311
+ .join("\n");
1312
+ return `// Generated by Blume. Do not edit.
1313
+ ${imports}
1314
+ export const examples = {
1315
+ ${entries}
1316
+ };
1317
+ `;
1318
+ };
1319
+
1167
1320
  /** Generate `.blume/src/env.d.ts`. */
1168
1321
  export const envTemplate =
1169
1322
  (): string => `/// <reference path="../.astro/types.d.ts" />
1170
1323
  /// <reference types="astro/client" />
1324
+
1325
+ declare module "blume:data" {
1326
+ const data: import("blume").BlumeData;
1327
+ export default data;
1328
+ }
1171
1329
  `;
1172
1330
 
1173
1331
  /** 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" />}