blume 1.6.6 → 1.7.0
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 +17 -0
- package/dist/cli/{chunk-nyqzjdhj.js → chunk-0qhq7b8q.js} +5 -5
- package/dist/cli/{chunk-cnvm6k3e.js → chunk-18tjv4f7.js} +11 -11
- package/dist/cli/{chunk-62qsssnh.js → chunk-5d4q7121.js} +401 -145
- package/dist/cli/chunk-5d4q7121.js.map +40 -0
- package/dist/cli/{chunk-ag1zyr5x.js → chunk-9qs6acpw.js} +11 -11
- package/dist/cli/{chunk-aerwpe14.js → chunk-agy5rzxy.js} +98 -15
- package/dist/cli/chunk-agy5rzxy.js.map +15 -0
- package/dist/cli/{chunk-x1vrdjyk.js → chunk-cfw6x4rm.js} +5 -5
- package/dist/cli/{chunk-bawgnt8x.js → chunk-ckh3a410.js} +3 -3
- package/dist/cli/{chunk-j00ezcg5.js → chunk-drke6t0h.js} +9 -9
- package/dist/cli/{chunk-3k0kzs6d.js → chunk-j6pxe0dt.js} +2 -2
- package/dist/cli/{chunk-n0y172hf.js → chunk-jk1zwka1.js} +4 -4
- package/dist/cli/{chunk-f75cqye8.js → chunk-jxkxjsc1.js} +10 -10
- package/dist/cli/{chunk-s4k1pnvf.js → chunk-kwx90v78.js} +11 -11
- package/dist/cli/{chunk-9sh49q0h.js → chunk-n0nyat6g.js} +2 -2
- package/dist/cli/{chunk-wkq5tbtq.js → chunk-qq9nm3qd.js} +3 -3
- package/dist/cli/{chunk-etsqspj6.js → chunk-s102bysw.js} +2 -2
- package/dist/cli/{chunk-wb067mv3.js → chunk-s5dsk8bj.js} +18 -7
- package/dist/cli/chunk-s5dsk8bj.js.map +13 -0
- package/dist/cli/{chunk-m3p3wahd.js → chunk-tnskyrej.js} +4 -4
- package/dist/cli/{chunk-vv237fp3.js → chunk-v2ymm99c.js} +26 -12
- package/dist/cli/{chunk-vv237fp3.js.map → chunk-v2ymm99c.js.map} +3 -3
- package/dist/cli/{chunk-5yvt556e.js → chunk-v5mm027v.js} +2 -2
- package/dist/cli/{chunk-vv3f8mb6.js → chunk-xv91q4nm.js} +24 -24
- package/dist/cli/{chunk-vv3f8mb6.js.map → chunk-xv91q4nm.js.map} +3 -3
- package/dist/cli/{chunk-0ewz4trd.js → chunk-y3g15rvv.js} +6 -6
- package/dist/cli/{chunk-tc89yh2r.js → chunk-ye9zdkgv.js} +2 -2
- package/dist/cli/{chunk-n4qjabmt.js → chunk-ynacq3ev.js} +4 -4
- package/dist/cli/{chunk-s4jn7f1q.js → chunk-zr3ygrq3.js} +2 -2
- package/dist/cli/index.js +13 -13
- package/dist/types/components/layout/nav-utils.d.ts +33 -1
- package/dist/types/theme/fonts.d.ts +22 -22
- package/docs/02-deployment.mdx +21 -0
- package/docs/content/navigation.mdx +2 -0
- package/docs/content/syntax.mdx +1 -1
- package/docs/discoverability/open-graph.mdx +4 -0
- package/package.json +1 -1
- package/src/astro/generate.ts +141 -5
- package/src/astro/integration.ts +12 -1
- package/src/astro/module-types.ts +9 -0
- package/src/astro/templates.ts +171 -11
- package/src/cli/commands/build.ts +28 -0
- package/src/components/Icon.astro +24 -0
- package/src/components/icon-sprite-middleware.ts +41 -0
- package/src/components/icon-sprite.ts +93 -0
- package/src/components/layout/IconSprite.astro +11 -0
- package/src/components/layout/NavTree.astro +156 -188
- package/src/components/layout/NavTreeCache.astro +45 -0
- package/src/components/layout/NavTreeScript.astro +256 -0
- package/src/components/layout/PageActions.astro +11 -5
- package/src/components/layout/PageLayout.astro +7 -0
- package/src/components/layout/ReferenceLayout.astro +7 -0
- package/src/components/layout/RootLayout.astro +30 -2
- package/src/components/layout/nav-cache.ts +49 -0
- package/src/components/layout/nav-utils.ts +69 -1
- package/src/markdown/language-icon.ts +64 -20
- package/src/markdown/mermaid.ts +11 -0
- package/src/og/cache.ts +236 -0
- package/src/og/card.ts +12 -4
- package/src/og/index.ts +8 -1
- package/src/registry/eject.ts +23 -8
- package/src/theme/entry.ts +41 -7
- package/src/theme/fonts.ts +30 -23
- package/dist/cli/chunk-62qsssnh.js.map +0 -36
- package/dist/cli/chunk-aerwpe14.js.map +0 -15
- package/dist/cli/chunk-wb067mv3.js.map +0 -13
- /package/dist/cli/{chunk-nyqzjdhj.js.map → chunk-0qhq7b8q.js.map} +0 -0
- /package/dist/cli/{chunk-cnvm6k3e.js.map → chunk-18tjv4f7.js.map} +0 -0
- /package/dist/cli/{chunk-ag1zyr5x.js.map → chunk-9qs6acpw.js.map} +0 -0
- /package/dist/cli/{chunk-x1vrdjyk.js.map → chunk-cfw6x4rm.js.map} +0 -0
- /package/dist/cli/{chunk-bawgnt8x.js.map → chunk-ckh3a410.js.map} +0 -0
- /package/dist/cli/{chunk-j00ezcg5.js.map → chunk-drke6t0h.js.map} +0 -0
- /package/dist/cli/{chunk-3k0kzs6d.js.map → chunk-j6pxe0dt.js.map} +0 -0
- /package/dist/cli/{chunk-n0y172hf.js.map → chunk-jk1zwka1.js.map} +0 -0
- /package/dist/cli/{chunk-f75cqye8.js.map → chunk-jxkxjsc1.js.map} +0 -0
- /package/dist/cli/{chunk-s4k1pnvf.js.map → chunk-kwx90v78.js.map} +0 -0
- /package/dist/cli/{chunk-9sh49q0h.js.map → chunk-n0nyat6g.js.map} +0 -0
- /package/dist/cli/{chunk-wkq5tbtq.js.map → chunk-qq9nm3qd.js.map} +0 -0
- /package/dist/cli/{chunk-etsqspj6.js.map → chunk-s102bysw.js.map} +0 -0
- /package/dist/cli/{chunk-m3p3wahd.js.map → chunk-tnskyrej.js.map} +0 -0
- /package/dist/cli/{chunk-5yvt556e.js.map → chunk-v5mm027v.js.map} +0 -0
- /package/dist/cli/{chunk-0ewz4trd.js.map → chunk-y3g15rvv.js.map} +0 -0
- /package/dist/cli/{chunk-tc89yh2r.js.map → chunk-ye9zdkgv.js.map} +0 -0
- /package/dist/cli/{chunk-n4qjabmt.js.map → chunk-ynacq3ev.js.map} +0 -0
- /package/dist/cli/{chunk-s4jn7f1q.js.map → chunk-zr3ygrq3.js.map} +0 -0
package/src/markdown/mermaid.ts
CHANGED
|
@@ -13,6 +13,17 @@ interface CodeNode extends MdastNode {
|
|
|
13
13
|
* rendered on the client (Mermaid needs a DOM), so the source rides on a string
|
|
14
14
|
* attribute rather than as child text (which MDX would try to parse).
|
|
15
15
|
*/
|
|
16
|
+
/**
|
|
17
|
+
* A ```mermaid (or ~~~mermaid) fence opener at the start of a line. Used to
|
|
18
|
+
* decide, at generation time, whether the site needs the Mermaid client
|
|
19
|
+
* library at all — see `featuresTemplate`.
|
|
20
|
+
*/
|
|
21
|
+
const MERMAID_FENCE = /^[ \t]*(?:`{3,}|~{3,})[ \t]*mermaid\b/mu;
|
|
22
|
+
|
|
23
|
+
/** Whether a page's Markdown/MDX source contains a mermaid fence. */
|
|
24
|
+
export const hasMermaidFence = (text: string): boolean =>
|
|
25
|
+
MERMAID_FENCE.test(text);
|
|
26
|
+
|
|
16
27
|
export const mermaidPlugin = () => ({
|
|
17
28
|
code(node: CodeNode, ctx: MdastVisitorContext) {
|
|
18
29
|
if (node.lang !== "mermaid") {
|
package/src/og/cache.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
mkdir,
|
|
5
|
+
readdir,
|
|
6
|
+
readFile,
|
|
7
|
+
rename,
|
|
8
|
+
rm,
|
|
9
|
+
writeFile,
|
|
10
|
+
} from "node:fs/promises";
|
|
11
|
+
|
|
12
|
+
import { dirname, join } from "pathe";
|
|
13
|
+
|
|
14
|
+
import type { ProjectContext } from "../core/types.ts";
|
|
15
|
+
import type { OgCardOptions, OgFont } from "./card.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Where a build keeps rendered OG cards between runs (see {@link cardCacheKey}
|
|
19
|
+
* for what invalidates one). Baked into the generated OG endpoint alongside
|
|
20
|
+
* the Blume version that renders the cards.
|
|
21
|
+
*/
|
|
22
|
+
export interface OgCache {
|
|
23
|
+
/** Absolute directory holding `<key>.png` files. */
|
|
24
|
+
dir: string;
|
|
25
|
+
/** The Blume version rendering the cards; part of every key. */
|
|
26
|
+
version: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The card cache directory for a project: `node_modules/.cache/blume/og`,
|
|
31
|
+
* the conventional build-cache location. Vercel and Netlify restore
|
|
32
|
+
* `node_modules` from their build caches, so a deploy there re-renders only
|
|
33
|
+
* the cards whose inputs changed; Cloudflare Workers Builds keeps only
|
|
34
|
+
* package-manager caches (and `node_modules/.astro` for a detected Astro
|
|
35
|
+
* project), and a self-managed runner needs a cache step for the directory.
|
|
36
|
+
* A project with no `node_modules` of its own falls back to the runtime's
|
|
37
|
+
* cache dir next to Astro's and Vite's.
|
|
38
|
+
*/
|
|
39
|
+
export const ogCacheDir = (
|
|
40
|
+
context: Pick<ProjectContext, "outDir" | "root">
|
|
41
|
+
): string =>
|
|
42
|
+
existsSync(join(context.root, "node_modules"))
|
|
43
|
+
? join(context.root, "node_modules", ".cache", "blume", "og")
|
|
44
|
+
: join(context.outDir, ".cache", "og");
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Per-process tally of cache hits and misses plus the keys this build asked
|
|
48
|
+
* for, read back by the CLI after `build()` for the summary line and the
|
|
49
|
+
* prune. On `globalThis` for the same reason as the integration registry: the
|
|
50
|
+
* endpoint renders in the copy of this module Vite bundled for the prerender,
|
|
51
|
+
* while the CLI reads from its own bundled copy.
|
|
52
|
+
*/
|
|
53
|
+
interface OgCacheRegistry {
|
|
54
|
+
hits: number;
|
|
55
|
+
misses: number;
|
|
56
|
+
used: Set<string>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const REGISTRY_KEY = Symbol.for("blume.og-cache");
|
|
60
|
+
|
|
61
|
+
type RegistryHost = typeof globalThis & { [REGISTRY_KEY]?: OgCacheRegistry };
|
|
62
|
+
|
|
63
|
+
const registry = (): OgCacheRegistry => {
|
|
64
|
+
// SAFETY: the registry is stashed on globalThis under a well-known symbol so
|
|
65
|
+
// every copy of this module in the process shares it; the intersection only
|
|
66
|
+
// names that slot.
|
|
67
|
+
const host = globalThis as RegistryHost;
|
|
68
|
+
host[REGISTRY_KEY] ??= { hits: 0, misses: 0, used: new Set() };
|
|
69
|
+
return host[REGISTRY_KEY];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Type guard: is this OG font a local file entry? */
|
|
73
|
+
export const isLocalOgFont = (
|
|
74
|
+
font: OgFont
|
|
75
|
+
): font is Extract<OgFont, { src: string }> =>
|
|
76
|
+
typeof font !== "string" && "src" in font;
|
|
77
|
+
|
|
78
|
+
// A local font file's contents digest, computed once per path per process: the
|
|
79
|
+
// key must follow the file's bytes, not its mtime (a fresh CI checkout resets
|
|
80
|
+
// every mtime, which would miss the whole cache on each build).
|
|
81
|
+
const localFontDigests = new Map<string, Promise<string>>();
|
|
82
|
+
|
|
83
|
+
const digestFile = async (path: string): Promise<string> =>
|
|
84
|
+
createHash("sha256")
|
|
85
|
+
.update(await readFile(path))
|
|
86
|
+
.digest("hex");
|
|
87
|
+
|
|
88
|
+
const localFontDigest = (path: string): Promise<string> => {
|
|
89
|
+
let digest = localFontDigests.get(path);
|
|
90
|
+
if (!digest) {
|
|
91
|
+
digest = digestFile(path);
|
|
92
|
+
localFontDigests.set(path, digest);
|
|
93
|
+
}
|
|
94
|
+
return digest;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The cache key of a card: a digest of everything that decides its pixels —
|
|
99
|
+
* the card options (title, description, brand, logo markup, palette, footer
|
|
100
|
+
* text, font families), the fonts (a local file by its contents, a Google
|
|
101
|
+
* family by its request), and the Blume version, since the layout and the
|
|
102
|
+
* renderer it pins ship with the package. Pre-fetched `images` are left out:
|
|
103
|
+
* the endpoint never passes them.
|
|
104
|
+
*/
|
|
105
|
+
export const cardCacheKey = async (
|
|
106
|
+
version: string,
|
|
107
|
+
options: OgCardOptions
|
|
108
|
+
): Promise<string> => {
|
|
109
|
+
const fonts = await Promise.all(
|
|
110
|
+
(options.fonts ?? []).map(async (font) =>
|
|
111
|
+
isLocalOgFont(font)
|
|
112
|
+
? { ...font, digest: await localFontDigest(font.src) }
|
|
113
|
+
: font
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
const card = { ...options, fonts: undefined, images: undefined };
|
|
117
|
+
return createHash("sha256")
|
|
118
|
+
.update(JSON.stringify({ card, fonts, version }))
|
|
119
|
+
.digest("hex");
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const cardPath = (cache: OgCache, key: string): string =>
|
|
123
|
+
join(cache.dir, `${key}.png`);
|
|
124
|
+
|
|
125
|
+
/** A cached card's bytes, or `null` when there is none. */
|
|
126
|
+
const readCard = async (file: string): Promise<Uint8Array | null> => {
|
|
127
|
+
try {
|
|
128
|
+
return await readFile(file);
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Written to a sibling temp file and renamed into place: concurrent page
|
|
135
|
+
// renders may store the same key, and a reader must never see a half-written
|
|
136
|
+
// card.
|
|
137
|
+
const storeCard = async (file: string, png: Uint8Array): Promise<void> => {
|
|
138
|
+
await mkdir(dirname(file), { recursive: true });
|
|
139
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
140
|
+
await writeFile(tmp, png);
|
|
141
|
+
await rename(tmp, file);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** The in-flight renders of this process, so duplicate titles render once. */
|
|
145
|
+
const inflight = new Map<string, Promise<Uint8Array>>();
|
|
146
|
+
|
|
147
|
+
const renderAndStore = async (
|
|
148
|
+
key: string,
|
|
149
|
+
file: string,
|
|
150
|
+
options: OgCardOptions,
|
|
151
|
+
render: (options: OgCardOptions) => Promise<Uint8Array>
|
|
152
|
+
): Promise<Uint8Array> => {
|
|
153
|
+
try {
|
|
154
|
+
const png = await render(options);
|
|
155
|
+
try {
|
|
156
|
+
await storeCard(file, png);
|
|
157
|
+
} catch {
|
|
158
|
+
// An unwritable cache (a read-only workspace) never fails the build.
|
|
159
|
+
}
|
|
160
|
+
return png;
|
|
161
|
+
} finally {
|
|
162
|
+
inflight.delete(key);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Serve a card from `cache`, rendering it with `render` on a miss and storing
|
|
168
|
+
* the result for the next build. Without a cache every card renders; the
|
|
169
|
+
* cache is only ever a shortcut.
|
|
170
|
+
*/
|
|
171
|
+
export const throughCardCache = async (
|
|
172
|
+
cache: OgCache | undefined,
|
|
173
|
+
options: OgCardOptions,
|
|
174
|
+
render: (options: OgCardOptions) => Promise<Uint8Array>
|
|
175
|
+
): Promise<Uint8Array> => {
|
|
176
|
+
if (!cache) {
|
|
177
|
+
return render(options);
|
|
178
|
+
}
|
|
179
|
+
const key = await cardCacheKey(cache.version, options);
|
|
180
|
+
const state = registry();
|
|
181
|
+
state.used.add(key);
|
|
182
|
+
const file = cardPath(cache, key);
|
|
183
|
+
const hit = await readCard(file);
|
|
184
|
+
if (hit) {
|
|
185
|
+
state.hits += 1;
|
|
186
|
+
return hit;
|
|
187
|
+
}
|
|
188
|
+
let pending = inflight.get(key);
|
|
189
|
+
if (!pending) {
|
|
190
|
+
state.misses += 1;
|
|
191
|
+
pending = renderAndStore(key, file, options, render);
|
|
192
|
+
inflight.set(key, pending);
|
|
193
|
+
}
|
|
194
|
+
return pending;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Remove the cards this build never asked for — a renamed page, a changed
|
|
199
|
+
* description, a previous Blume version — plus any temp file a crashed build
|
|
200
|
+
* left behind, so a persisted cache holds exactly the current site's cards.
|
|
201
|
+
* Returns how many files were removed; a missing directory removes nothing.
|
|
202
|
+
*/
|
|
203
|
+
export const pruneCardCache = async (dir: string): Promise<number> => {
|
|
204
|
+
const { used } = registry();
|
|
205
|
+
let entries: string[];
|
|
206
|
+
try {
|
|
207
|
+
entries = await readdir(dir);
|
|
208
|
+
} catch {
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
const stale = entries.filter(
|
|
212
|
+
(name) =>
|
|
213
|
+
name.endsWith(".tmp") ||
|
|
214
|
+
(name.endsWith(".png") && !used.has(name.slice(0, -".png".length)))
|
|
215
|
+
);
|
|
216
|
+
await Promise.all(stale.map((name) => rm(join(dir, name), { force: true })));
|
|
217
|
+
return stale.length;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* This process's card cache tally — how many cards a build reused and how many
|
|
222
|
+
* it rendered — or `null` when no card was requested (OG cards off, or an
|
|
223
|
+
* endpoint-free build).
|
|
224
|
+
*/
|
|
225
|
+
export const cardCacheTally = (): { hits: number; misses: number } | null => {
|
|
226
|
+
const { hits, misses } = registry();
|
|
227
|
+
return hits + misses === 0 ? null : { hits, misses };
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/** Reset the tally and the used-key set (tests). */
|
|
231
|
+
export const resetCardCacheTally = (): void => {
|
|
232
|
+
const state = registry();
|
|
233
|
+
state.hits = 0;
|
|
234
|
+
state.misses = 0;
|
|
235
|
+
state.used.clear();
|
|
236
|
+
};
|
package/src/og/card.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type { FontSubset, GoogleFontFamily, Node } from "takumi-js/helpers";
|
|
|
7
7
|
|
|
8
8
|
import { svgDimensions } from "../core/svg-dimensions.ts";
|
|
9
9
|
import { ACCENTS, isAccentPreset } from "../theme/palette.ts";
|
|
10
|
+
import { isLocalOgFont, throughCardCache } from "./cache.ts";
|
|
11
|
+
import type { OgCache } from "./cache.ts";
|
|
10
12
|
import { OG_IMAGE_HEIGHT, OG_IMAGE_WIDTH } from "./dimensions.ts";
|
|
11
13
|
|
|
12
14
|
/** A local font file registered with the OG card renderer, read at build. */
|
|
@@ -40,10 +42,6 @@ export type OgFont =
|
|
|
40
42
|
}
|
|
41
43
|
| OgLocalFont;
|
|
42
44
|
|
|
43
|
-
/** Type guard: is this OG font a local file entry? */
|
|
44
|
-
const isLocalOgFont = (font: OgFont): font is OgLocalFont =>
|
|
45
|
-
typeof font !== "string" && "src" in font;
|
|
46
|
-
|
|
47
45
|
/**
|
|
48
46
|
* Which loaded family each card role renders in. Takumi still falls back
|
|
49
47
|
* across every loaded font per glyph, so a family that misses a script
|
|
@@ -418,3 +416,13 @@ export const renderOgImage = async (
|
|
|
418
416
|
width: WIDTH,
|
|
419
417
|
});
|
|
420
418
|
};
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* {@link renderOgImage} through the on-disk card cache: a card whose inputs
|
|
422
|
+
* match one rendered by a previous build (or an earlier page of this one) is
|
|
423
|
+
* read back instead of rendered. `cache` undefined renders every card.
|
|
424
|
+
*/
|
|
425
|
+
export const cachedOgImage = (
|
|
426
|
+
cache: OgCache | undefined,
|
|
427
|
+
options: OgCardOptions
|
|
428
|
+
): Promise<Uint8Array> => throughCardCache(cache, options, renderOgImage);
|
package/src/og/index.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
cardCacheKey,
|
|
3
|
+
cardCacheTally,
|
|
4
|
+
ogCacheDir,
|
|
5
|
+
pruneCardCache,
|
|
6
|
+
} from "./cache.ts";
|
|
7
|
+
export type { OgCache } from "./cache.ts";
|
|
8
|
+
export { cachedOgImage, renderOgImage } from "./card.ts";
|
|
2
9
|
export type {
|
|
3
10
|
OgCardOptions,
|
|
4
11
|
OgCardPalette,
|
package/src/registry/eject.ts
CHANGED
|
@@ -17,23 +17,26 @@ import {
|
|
|
17
17
|
} from "../astro/examples.ts";
|
|
18
18
|
import {
|
|
19
19
|
buildRuntimeData,
|
|
20
|
+
clientFeaturesFor,
|
|
20
21
|
collectStaged,
|
|
21
22
|
detectNeedsReact,
|
|
22
23
|
detectUsesMath,
|
|
24
|
+
languageIconCssFor,
|
|
23
25
|
} from "../astro/generate.ts";
|
|
24
26
|
import { discoverIslands } from "../astro/islands.ts";
|
|
25
27
|
import { customOgRoutes, discoverPages, routeIsTaken } from "../astro/pages.ts";
|
|
26
28
|
import {
|
|
27
|
-
askEndpointTemplate,
|
|
28
29
|
askComponentTemplate,
|
|
30
|
+
askEndpointTemplate,
|
|
29
31
|
astroConfigTemplate,
|
|
30
32
|
catchAllPageTemplate,
|
|
31
33
|
changelogIndexTemplate,
|
|
32
34
|
contentConfigTemplate,
|
|
33
35
|
exampleMapTemplate,
|
|
34
|
-
exampleWrapperTemplate,
|
|
35
|
-
examplesPageTemplate,
|
|
36
36
|
exampleSlug,
|
|
37
|
+
examplesPageTemplate,
|
|
38
|
+
exampleWrapperTemplate,
|
|
39
|
+
featuresTemplate,
|
|
37
40
|
islandMapTemplate,
|
|
38
41
|
islandWrapperTemplate,
|
|
39
42
|
mcpEndpointTemplate,
|
|
@@ -387,6 +390,8 @@ export const eject = async (
|
|
|
387
390
|
const staged = collectStaged(project);
|
|
388
391
|
const hasStaged = staged.size > 0;
|
|
389
392
|
const stagedDir = "blume-staged";
|
|
393
|
+
const features = await clientFeaturesFor(project);
|
|
394
|
+
const languageIcons = await languageIconCssFor(project);
|
|
390
395
|
|
|
391
396
|
const files: {
|
|
392
397
|
path: string;
|
|
@@ -403,6 +408,8 @@ export const eject = async (
|
|
|
403
408
|
context: relContext,
|
|
404
409
|
examplesPath: "./src/generated/examples.ts",
|
|
405
410
|
examplesThemePath: "./src/generated/examples.css",
|
|
411
|
+
features,
|
|
412
|
+
featuresPath: "./src/generated/features.ts",
|
|
406
413
|
// No CLI publishes the runtime data modules in memory after eject, so
|
|
407
414
|
// the config aliases each to the JSON snapshot written below.
|
|
408
415
|
generatedModulesDir: "./src/generated",
|
|
@@ -476,6 +483,7 @@ export const eject = async (
|
|
|
476
483
|
{
|
|
477
484
|
content: tailwindEntryTemplate({
|
|
478
485
|
configTokens: buildThemeCss(config.theme),
|
|
486
|
+
languageIcons,
|
|
479
487
|
// Relative paths from src/generated/app.css keep the ejected app
|
|
480
488
|
// portable; the blume glob resolves the real install location when
|
|
481
489
|
// the package is hoisted out of the project's own node_modules.
|
|
@@ -558,11 +566,18 @@ export const eject = async (
|
|
|
558
566
|
});
|
|
559
567
|
}
|
|
560
568
|
|
|
561
|
-
// The
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
569
|
+
// The client-feature loaders behind the `blume:features` alias, and the
|
|
570
|
+
// provider-specific client loader behind `blume:search-client`.
|
|
571
|
+
files.push(
|
|
572
|
+
{
|
|
573
|
+
content: featuresTemplate(features),
|
|
574
|
+
path: join(genDir, "features.ts"),
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
content: searchClientTemplate(config),
|
|
578
|
+
path: join(genDir, "search-client.ts"),
|
|
579
|
+
}
|
|
580
|
+
);
|
|
566
581
|
|
|
567
582
|
if (servesStaticIndex(config.search.provider)) {
|
|
568
583
|
const documents = await buildSearchDocuments(project);
|
package/src/theme/entry.ts
CHANGED
|
@@ -20,6 +20,8 @@ interface TailwindEntryOptions {
|
|
|
20
20
|
userTheme: string;
|
|
21
21
|
/** Twoslash rich-renderer styles (for fences with the `twoslash` meta). */
|
|
22
22
|
twoslashCss?: string;
|
|
23
|
+
/** The code-block language icon rules (`languageIconCss`), if any. */
|
|
24
|
+
languageIcons?: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/** Dark mode is driven by `data-theme` on the root element (both sheets). */
|
|
@@ -244,6 +246,31 @@ ${THEME_MAPPING}
|
|
|
244
246
|
unicode-bidi: isolate;
|
|
245
247
|
}
|
|
246
248
|
|
|
249
|
+
/* The sidebar's rows. A docs site renders every sidebar row on every page,
|
|
250
|
+
so a large site's HTML is mostly this markup — one utility per row kind
|
|
251
|
+
instead of the dozen classes each expands to keeps every page (and the
|
|
252
|
+
build's HTML output) small. Kept in sync with NavTree.astro: a page row,
|
|
253
|
+
a page-mode group's drill-in button, a collapsible group's summary and
|
|
254
|
+
the link inside it, and a flat group's heading and the link inside it. */
|
|
255
|
+
@utility blume-nav-link {
|
|
256
|
+
@apply block rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:font-medium aria-[current=page]:text-foreground;
|
|
257
|
+
}
|
|
258
|
+
@utility blume-nav-drill {
|
|
259
|
+
@apply flex w-full items-center gap-2 rounded-[0.65rem] px-2.5 py-1.5 text-left font-medium text-foreground text-sm transition-colors hover:bg-muted;
|
|
260
|
+
}
|
|
261
|
+
@utility blume-nav-summary {
|
|
262
|
+
@apply flex cursor-pointer list-none items-center gap-1.5 rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden;
|
|
263
|
+
}
|
|
264
|
+
@utility blume-nav-summary-link {
|
|
265
|
+
@apply -my-1 flex flex-1 items-center gap-1.5 rounded py-1 transition-colors aria-[current=page]:text-foreground aria-[current=page]:font-semibold;
|
|
266
|
+
}
|
|
267
|
+
@utility blume-nav-heading {
|
|
268
|
+
@apply mb-2 flex items-center gap-1.5 px-2.5 font-medium text-foreground text-sm;
|
|
269
|
+
}
|
|
270
|
+
@utility blume-nav-heading-link {
|
|
271
|
+
@apply -ml-1 flex flex-1 items-center gap-1.5 rounded-[0.65rem] px-1 py-0.5 text-foreground transition-colors hover:bg-muted aria-[current=page]:bg-muted;
|
|
272
|
+
}
|
|
273
|
+
|
|
247
274
|
/* Theme Tailwind Typography (prose) with Blume tokens. */
|
|
248
275
|
.prose {
|
|
249
276
|
--tw-prose-body: var(--blume-foreground);
|
|
@@ -459,15 +486,17 @@ blume-diff {
|
|
|
459
486
|
CodeBlock (no title) never gets data-language, so without this gate the
|
|
460
487
|
absolutely-positioned icon would overlap the first code line. Fenced code
|
|
461
488
|
and titled blocks always have a header, so the icon shows there. */
|
|
462
|
-
.
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
.prose > :where(pre[data-language][data-icon]) > .blume-lang-icon {
|
|
467
|
-
color: var(--blume-muted-foreground);
|
|
468
|
-
display: block;
|
|
489
|
+
.prose > :where(pre[data-language][data-icon])::after {
|
|
490
|
+
content: "";
|
|
469
491
|
height: 0.875rem;
|
|
470
492
|
left: 1rem;
|
|
493
|
+
-webkit-mask-position: center;
|
|
494
|
+
-webkit-mask-repeat: no-repeat;
|
|
495
|
+
-webkit-mask-size: contain;
|
|
496
|
+
mask-position: center;
|
|
497
|
+
mask-repeat: no-repeat;
|
|
498
|
+
mask-size: contain;
|
|
499
|
+
pointer-events: none;
|
|
471
500
|
position: absolute;
|
|
472
501
|
top: 0.875rem;
|
|
473
502
|
width: 0.875rem;
|
|
@@ -477,6 +506,11 @@ blume-diff {
|
|
|
477
506
|
padding-left: 2.5rem;
|
|
478
507
|
}
|
|
479
508
|
|
|
509
|
+
/* One mask rule per language the site's Markdown uses (see
|
|
510
|
+
markdown/language-icon.ts); the ::after above only gets a color and a mask
|
|
511
|
+
from these, so a block whose language has no rule paints nothing. */
|
|
512
|
+
${options.languageIcons ?? ""}
|
|
513
|
+
|
|
480
514
|
.prose :where(pre code) {
|
|
481
515
|
background: transparent;
|
|
482
516
|
border-radius: 0;
|
package/src/theme/fonts.ts
CHANGED
|
@@ -63,7 +63,14 @@ export type FontValue = string | RemoteFontConfig | LocalFontConfig;
|
|
|
63
63
|
interface FontDef {
|
|
64
64
|
category: FontCategory;
|
|
65
65
|
family: string;
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Discrete weights, or one `"min..max"` range for a family Google Fonts
|
|
68
|
+
* serves as a variable font: the range loads the same variable file the
|
|
69
|
+
* discrete weights did, declared as one `@font-face` per style instead of
|
|
70
|
+
* one per weight (16 rules down to 4 for Inter on every page). The three
|
|
71
|
+
* static families in the table keep discrete weights.
|
|
72
|
+
*/
|
|
73
|
+
weights: (number | string)[];
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
/** Resolved theme fonts (a validated value per role, all optional). */
|
|
@@ -96,22 +103,22 @@ const FALLBACKS = {
|
|
|
96
103
|
|
|
97
104
|
/** Slug -> Google family + weights + fallback category. Keep keys alphabetical. */
|
|
98
105
|
export const GOOGLE_FONTS = {
|
|
99
|
-
"dm-sans": { category: "sans", family: "DM Sans", weights: [400
|
|
106
|
+
"dm-sans": { category: "sans", family: "DM Sans", weights: ["400..700"] },
|
|
100
107
|
figtree: {
|
|
101
108
|
category: "sans",
|
|
102
109
|
family: "Figtree",
|
|
103
|
-
weights: [400
|
|
110
|
+
weights: ["400..700"],
|
|
104
111
|
},
|
|
105
112
|
"fira-code": {
|
|
106
113
|
category: "mono",
|
|
107
114
|
family: "Fira Code",
|
|
108
|
-
weights: [400
|
|
115
|
+
weights: ["400..700"],
|
|
109
116
|
},
|
|
110
|
-
geist: { category: "sans", family: "Geist", weights: [400
|
|
117
|
+
geist: { category: "sans", family: "Geist", weights: ["400..700"] },
|
|
111
118
|
"geist-mono": {
|
|
112
119
|
category: "mono",
|
|
113
120
|
family: "Geist Mono",
|
|
114
|
-
weights: [400
|
|
121
|
+
weights: ["400..600"],
|
|
115
122
|
},
|
|
116
123
|
"ibm-plex-mono": {
|
|
117
124
|
category: "mono",
|
|
@@ -121,81 +128,81 @@ export const GOOGLE_FONTS = {
|
|
|
121
128
|
"ibm-plex-sans": {
|
|
122
129
|
category: "sans",
|
|
123
130
|
family: "IBM Plex Sans",
|
|
124
|
-
weights: [400
|
|
131
|
+
weights: ["400..700"],
|
|
125
132
|
},
|
|
126
133
|
"ibm-plex-serif": {
|
|
127
134
|
category: "serif",
|
|
128
135
|
family: "IBM Plex Serif",
|
|
129
136
|
weights: [400, 500, 600],
|
|
130
137
|
},
|
|
131
|
-
inter: { category: "sans", family: "Inter", weights: [400
|
|
138
|
+
inter: { category: "sans", family: "Inter", weights: ["400..700"] },
|
|
132
139
|
"inter-tight": {
|
|
133
140
|
category: "sans",
|
|
134
141
|
family: "Inter Tight",
|
|
135
|
-
weights: [400
|
|
142
|
+
weights: ["400..700"],
|
|
136
143
|
},
|
|
137
144
|
"jetbrains-mono": {
|
|
138
145
|
category: "mono",
|
|
139
146
|
family: "JetBrains Mono",
|
|
140
|
-
weights: [400
|
|
147
|
+
weights: ["400..700"],
|
|
141
148
|
},
|
|
142
|
-
lora: { category: "serif", family: "Lora", weights: [400
|
|
149
|
+
lora: { category: "serif", family: "Lora", weights: ["400..700"] },
|
|
143
150
|
manrope: {
|
|
144
151
|
category: "sans",
|
|
145
152
|
family: "Manrope",
|
|
146
|
-
weights: [400
|
|
153
|
+
weights: ["400..700"],
|
|
147
154
|
},
|
|
148
155
|
merriweather: {
|
|
149
156
|
category: "serif",
|
|
150
157
|
family: "Merriweather",
|
|
151
|
-
weights: [400
|
|
158
|
+
weights: ["400..700"],
|
|
152
159
|
},
|
|
153
160
|
"open-sans": {
|
|
154
161
|
category: "sans",
|
|
155
162
|
family: "Open Sans",
|
|
156
|
-
weights: [400
|
|
163
|
+
weights: ["400..700"],
|
|
157
164
|
},
|
|
158
165
|
"playfair-display": {
|
|
159
166
|
category: "serif",
|
|
160
167
|
family: "Playfair Display",
|
|
161
|
-
weights: [400
|
|
168
|
+
weights: ["400..700"],
|
|
162
169
|
},
|
|
163
170
|
"plus-jakarta-sans": {
|
|
164
171
|
category: "sans",
|
|
165
172
|
family: "Plus Jakarta Sans",
|
|
166
|
-
weights: [400
|
|
173
|
+
weights: ["400..700"],
|
|
167
174
|
},
|
|
168
|
-
roboto: { category: "sans", family: "Roboto", weights: [400
|
|
175
|
+
roboto: { category: "sans", family: "Roboto", weights: ["400..700"] },
|
|
169
176
|
"roboto-mono": {
|
|
170
177
|
category: "mono",
|
|
171
178
|
family: "Roboto Mono",
|
|
172
|
-
weights: [400
|
|
179
|
+
weights: ["400..700"],
|
|
173
180
|
},
|
|
174
181
|
"source-code-pro": {
|
|
175
182
|
category: "mono",
|
|
176
183
|
family: "Source Code Pro",
|
|
177
|
-
weights: [400
|
|
184
|
+
weights: ["400..600"],
|
|
178
185
|
},
|
|
179
186
|
"source-sans-3": {
|
|
180
187
|
category: "sans",
|
|
181
188
|
family: "Source Sans 3",
|
|
182
|
-
weights: [400
|
|
189
|
+
weights: ["400..700"],
|
|
183
190
|
},
|
|
184
191
|
"source-serif-4": {
|
|
185
192
|
category: "serif",
|
|
186
193
|
family: "Source Serif 4",
|
|
187
|
-
weights: [400
|
|
194
|
+
weights: ["400..700"],
|
|
188
195
|
},
|
|
189
196
|
"space-grotesk": {
|
|
190
197
|
category: "sans",
|
|
191
198
|
family: "Space Grotesk",
|
|
192
|
-
weights: [400
|
|
199
|
+
weights: ["400..700"],
|
|
193
200
|
},
|
|
194
201
|
"space-mono": { category: "mono", family: "Space Mono", weights: [400, 700] },
|
|
195
202
|
"work-sans": {
|
|
196
203
|
category: "sans",
|
|
197
204
|
family: "Work Sans",
|
|
198
|
-
weights: [400
|
|
205
|
+
weights: ["400..600"],
|
|
199
206
|
},
|
|
200
207
|
} satisfies Record<string, FontDef>;
|
|
201
208
|
|