blume 0.0.0 → 0.1.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/bin/blume.mjs +18 -0
- package/dist/cli/index.js +11989 -0
- package/dist/cli/index.js.map +141 -0
- package/docs/01-quickstart.mdx +99 -0
- package/docs/02-deployment.mdx +129 -0
- package/docs/advanced/api-reference.mdx +114 -0
- package/docs/advanced/blog.mdx +121 -0
- package/docs/advanced/changelog.mdx +113 -0
- package/docs/advanced/custom-pages.mdx +187 -0
- package/docs/advanced/meta.ts +7 -0
- package/docs/changelog/v0-1-0.mdx +12 -0
- package/docs/changelog/v0-2-0.mdx +16 -0
- package/docs/configuration/ai.mdx +228 -0
- package/docs/configuration/analytics.mdx +98 -0
- package/docs/configuration/customization.mdx +91 -0
- package/docs/configuration/export.mdx +70 -0
- package/docs/configuration/index.mdx +271 -0
- package/docs/configuration/meta.ts +15 -0
- package/docs/configuration/search.mdx +172 -0
- package/docs/configuration/seo.mdx +196 -0
- package/docs/configuration/theming.mdx +178 -0
- package/docs/content/components.mdx +565 -0
- package/docs/content/i18n.mdx +205 -0
- package/docs/content/index.mdx +161 -0
- package/docs/content/islands.mdx +94 -0
- package/docs/content/meta.mdx +119 -0
- package/docs/content/meta.ts +15 -0
- package/docs/content/navigation.mdx +168 -0
- package/docs/content/sources.mdx +216 -0
- package/docs/content/syntax.mdx +445 -0
- package/docs/index.mdx +112 -0
- package/docs/reference/cli.mdx +43 -0
- package/docs/reference/frontmatter.mdx +74 -0
- package/docs/reference/meta.ts +7 -0
- package/package.json +140 -6
- package/src/ai/ask.ts +93 -0
- package/src/ai/llms.ts +65 -0
- package/src/ai/markdown.ts +31 -0
- package/src/ai/mcp/data.ts +74 -0
- package/src/ai/mcp/discovery.ts +49 -0
- package/src/ai/mcp/server.ts +225 -0
- package/src/ai/mcp/tools.ts +47 -0
- package/src/assets/icon.png +0 -0
- package/src/astro/generate.ts +878 -0
- package/src/astro/index.ts +4 -0
- package/src/astro/integration.ts +74 -0
- package/src/astro/islands.ts +131 -0
- package/src/astro/markdown-negotiation.ts +68 -0
- package/src/astro/pages.ts +28 -0
- package/src/astro/templates.ts +1199 -0
- package/src/cli/commands/add.ts +81 -0
- package/src/cli/commands/build.ts +103 -0
- package/src/cli/commands/dev.ts +108 -0
- package/src/cli/commands/doctor.ts +74 -0
- package/src/cli/commands/eject.ts +57 -0
- package/src/cli/commands/init.ts +98 -0
- package/src/cli/commands/migrate.ts +39 -0
- package/src/cli/commands/preview.ts +39 -0
- package/src/cli/commands/sync.ts +52 -0
- package/src/cli/commands/validate.ts +60 -0
- package/src/cli/index.ts +35 -0
- package/src/cli/log.ts +37 -0
- package/src/cli/prepare.ts +80 -0
- package/src/components/Icon.astro +99 -0
- package/src/components/content/Accordion.astro +8 -0
- package/src/components/content/AccordionItem.astro +121 -0
- package/src/components/content/AutoTypeTable.astro +51 -0
- package/src/components/content/Badge.astro +124 -0
- package/src/components/content/Callout.astro +73 -0
- package/src/components/content/Card.astro +104 -0
- package/src/components/content/CardGroup.astro +14 -0
- package/src/components/content/CodeGroup.astro +13 -0
- package/src/components/content/Color.astro +15 -0
- package/src/components/content/ColorItem.astro +87 -0
- package/src/components/content/ColorRow.astro +10 -0
- package/src/components/content/Column.astro +6 -0
- package/src/components/content/Columns.astro +9 -0
- package/src/components/content/Expandable.astro +11 -0
- package/src/components/content/FileTree.astro +8 -0
- package/src/components/content/Frame.astro +70 -0
- package/src/components/content/GithubInfo.astro +110 -0
- package/src/components/content/Math.astro +24 -0
- package/src/components/content/Panel.astro +20 -0
- package/src/components/content/Prompt.astro +129 -0
- package/src/components/content/Step.astro +34 -0
- package/src/components/content/Steps.astro +20 -0
- package/src/components/content/Tab.astro +40 -0
- package/src/components/content/Tabs.astro +273 -0
- package/src/components/content/Tile.astro +42 -0
- package/src/components/content/Tooltip.astro +68 -0
- package/src/components/content/Tree.astro +300 -0
- package/src/components/content/TreeFile.astro +15 -0
- package/src/components/content/TreeFolder.astro +62 -0
- package/src/components/content/TypeTable.astro +106 -0
- package/src/components/content/Update.astro +66 -0
- package/src/components/content/Visibility.astro +12 -0
- package/src/components/content/Warning.astro +9 -0
- package/src/components/content/auto-type-table.ts +141 -0
- package/src/components/content/github-info.ts +79 -0
- package/src/components/content/mermaid-element.ts +68 -0
- package/src/components/github-mark.ts +9 -0
- package/src/components/index.ts +14 -0
- package/src/components/islands/AskAI.astro +12 -0
- package/src/components/islands/ask-ai.tsx +156 -0
- package/src/components/layout/Analytics.astro +63 -0
- package/src/components/layout/Banner.astro +50 -0
- package/src/components/layout/Breadcrumbs.astro +31 -0
- package/src/components/layout/Favicon.astro +15 -0
- package/src/components/layout/Fonts.astro +14 -0
- package/src/components/layout/Header.astro +188 -0
- package/src/components/layout/LanguageSwitcher.astro +56 -0
- package/src/components/layout/NavTree.astro +462 -0
- package/src/components/layout/PageActions.astro +438 -0
- package/src/components/layout/PageFeedback.astro +58 -0
- package/src/components/layout/Pagination.astro +56 -0
- package/src/components/layout/ReferenceLayout.astro +102 -0
- package/src/components/layout/RootLayout.astro +533 -0
- package/src/components/layout/Search.astro +608 -0
- package/src/components/layout/TableOfContents.astro +68 -0
- package/src/components/layout/analytics-client.ts +38 -0
- package/src/components/layout/nav-utils.ts +87 -0
- package/src/components/layout/overrides.ts +32 -0
- package/src/components/layout/search/algolia.ts +43 -0
- package/src/components/layout/search/endpoint.ts +22 -0
- package/src/components/layout/search/flexsearch.ts +52 -0
- package/src/components/layout/search/orama-cloud.ts +41 -0
- package/src/components/layout/search/orama.ts +26 -0
- package/src/components/layout/search/pagefind.ts +43 -0
- package/src/components/layout/search/types.ts +163 -0
- package/src/components/layout/search/typesense.ts +60 -0
- package/src/components/layout/toc-element.ts +108 -0
- package/src/core/bridge.ts +92 -0
- package/src/core/config.ts +112 -0
- package/src/core/content.ts +50 -0
- package/src/core/define-components.ts +34 -0
- package/src/core/define-meta.ts +20 -0
- package/src/core/deployment-env.ts +73 -0
- package/src/core/diagnostics.ts +104 -0
- package/src/core/graph.ts +128 -0
- package/src/core/i18n-ui.ts +171 -0
- package/src/core/i18n.ts +169 -0
- package/src/core/last-modified.ts +88 -0
- package/src/core/links.ts +336 -0
- package/src/core/load-module.ts +15 -0
- package/src/core/manifest.ts +126 -0
- package/src/core/meta.ts +97 -0
- package/src/core/navigation.ts +392 -0
- package/src/core/package-root.ts +37 -0
- package/src/core/project-graph.ts +153 -0
- package/src/core/project.ts +56 -0
- package/src/core/schema.ts +1057 -0
- package/src/core/server-features.ts +23 -0
- package/src/core/sources/assets.ts +77 -0
- package/src/core/sources/cache.ts +122 -0
- package/src/core/sources/filesystem.ts +99 -0
- package/src/core/sources/mdx-remote.ts +216 -0
- package/src/core/sources/mintlify.ts +161 -0
- package/src/core/sources/normalize.ts +227 -0
- package/src/core/sources/notion.ts +440 -0
- package/src/core/sources/portable-text.ts +143 -0
- package/src/core/sources/read.ts +36 -0
- package/src/core/sources/resolve.ts +158 -0
- package/src/core/sources/sanity.ts +218 -0
- package/src/core/sources/types.ts +105 -0
- package/src/core/types.ts +261 -0
- package/src/core/ui-packs/ar.ts +47 -0
- package/src/core/ui-packs/bg.ts +47 -0
- package/src/core/ui-packs/bn.ts +47 -0
- package/src/core/ui-packs/ca.ts +47 -0
- package/src/core/ui-packs/cs.ts +47 -0
- package/src/core/ui-packs/da.ts +47 -0
- package/src/core/ui-packs/de.ts +47 -0
- package/src/core/ui-packs/el.ts +47 -0
- package/src/core/ui-packs/es.ts +47 -0
- package/src/core/ui-packs/fa.ts +47 -0
- package/src/core/ui-packs/fi.ts +47 -0
- package/src/core/ui-packs/fr.ts +47 -0
- package/src/core/ui-packs/he.ts +47 -0
- package/src/core/ui-packs/hi.ts +47 -0
- package/src/core/ui-packs/hr.ts +47 -0
- package/src/core/ui-packs/hu.ts +47 -0
- package/src/core/ui-packs/id.ts +47 -0
- package/src/core/ui-packs/index.ts +87 -0
- package/src/core/ui-packs/it.ts +47 -0
- package/src/core/ui-packs/ja.ts +47 -0
- package/src/core/ui-packs/ko.ts +47 -0
- package/src/core/ui-packs/nl.ts +47 -0
- package/src/core/ui-packs/no.ts +47 -0
- package/src/core/ui-packs/pl.ts +47 -0
- package/src/core/ui-packs/pt-br.ts +47 -0
- package/src/core/ui-packs/pt.ts +47 -0
- package/src/core/ui-packs/ro.ts +47 -0
- package/src/core/ui-packs/ru.ts +47 -0
- package/src/core/ui-packs/sk.ts +47 -0
- package/src/core/ui-packs/sr.ts +47 -0
- package/src/core/ui-packs/sv.ts +47 -0
- package/src/core/ui-packs/th.ts +47 -0
- package/src/core/ui-packs/tr.ts +47 -0
- package/src/core/ui-packs/uk.ts +47 -0
- package/src/core/ui-packs/vi.ts +47 -0
- package/src/core/ui-packs/zh-tw.ts +47 -0
- package/src/core/ui-packs/zh.ts +47 -0
- package/src/core/version.ts +23 -0
- package/src/deploy/robots.ts +20 -0
- package/src/deploy/rss.ts +128 -0
- package/src/deploy/sitemap.ts +28 -0
- package/src/index.ts +27 -0
- package/src/markdown/code-title.ts +71 -0
- package/src/markdown/directives.ts +83 -0
- package/src/markdown/heading-anchors.ts +137 -0
- package/src/markdown/index.ts +159 -0
- package/src/markdown/inline-code.ts +108 -0
- package/src/markdown/language-icon.ts +172 -0
- package/src/markdown/math.ts +32 -0
- package/src/markdown/mdast.ts +48 -0
- package/src/markdown/mermaid.ts +37 -0
- package/src/markdown/package-commands.ts +159 -0
- package/src/markdown/package-install.ts +40 -0
- package/src/migrate/fumadocs/config.ts +106 -0
- package/src/migrate/fumadocs/content.ts +365 -0
- package/src/migrate/fumadocs/frontmatter.ts +18 -0
- package/src/migrate/fumadocs/index.ts +252 -0
- package/src/migrate/fumadocs/meta.ts +114 -0
- package/src/migrate/migrate.ts +53 -0
- package/src/migrate/mintlify/config.ts +1040 -0
- package/src/migrate/mintlify/content.ts +98 -0
- package/src/migrate/mintlify/frontmatter.ts +126 -0
- package/src/migrate/mintlify/i18n.ts +51 -0
- package/src/migrate/mintlify/icons.ts +128 -0
- package/src/migrate/mintlify/index.ts +266 -0
- package/src/migrate/mintlify/snippets.ts +305 -0
- package/src/migrate/mintlify/transform.ts +81 -0
- package/src/migrate/nextra/content.ts +46 -0
- package/src/migrate/nextra/frontmatter.ts +40 -0
- package/src/migrate/nextra/index.ts +374 -0
- package/src/migrate/nextra/meta.ts +266 -0
- package/src/migrate/shared.ts +623 -0
- package/src/migrate/starlight/config.ts +459 -0
- package/src/migrate/starlight/content.ts +78 -0
- package/src/migrate/starlight/frontmatter.ts +111 -0
- package/src/migrate/starlight/i18n.ts +54 -0
- package/src/migrate/starlight/index.ts +131 -0
- package/src/og/card.ts +92 -0
- package/src/og/index.ts +2 -0
- package/src/openapi/scalar.ts +246 -0
- package/src/registry/eject.ts +263 -0
- package/src/registry/registry.ts +100 -0
- package/src/registry/rewrite-imports.ts +39 -0
- package/src/runtime/index.ts +14 -0
- package/src/search/build.ts +23 -0
- package/src/search/documents.ts +165 -0
- package/src/search/orama-index.ts +66 -0
- package/src/search/providers.ts +91 -0
- package/src/search/sync/algolia.ts +30 -0
- package/src/search/sync/index.ts +50 -0
- package/src/search/sync/orama-cloud.ts +40 -0
- package/src/search/sync/typesense.ts +65 -0
- package/src/seo/jsonld.ts +113 -0
- package/src/theme/entry.ts +608 -0
- package/src/theme/fonts.ts +198 -0
- package/src/theme/icons.ts +184 -0
- package/src/theme/palette.ts +143 -0
- package/src/theme/twoslash.ts +81 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript type extraction for `<AutoTypeTable>`.
|
|
3
|
+
*
|
|
4
|
+
* Reads an interface or type alias — from a file path or inline source — via the
|
|
5
|
+
* TypeScript compiler API and returns one row per property: the shape
|
|
6
|
+
* `<TypeTable>` renders. `typescript` is loaded lazily (dynamic import) so the
|
|
7
|
+
* dependency is only pulled into the build when a page actually uses
|
|
8
|
+
* `<AutoTypeTable>`.
|
|
9
|
+
*/
|
|
10
|
+
import nodePath from "node:path";
|
|
11
|
+
|
|
12
|
+
import type * as TypeScriptApi from "typescript";
|
|
13
|
+
|
|
14
|
+
/** A single documented property — one generated row of a type table. */
|
|
15
|
+
export interface TypeTableProperty {
|
|
16
|
+
default?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
name: string;
|
|
19
|
+
required: boolean;
|
|
20
|
+
type: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ExtractOptions {
|
|
24
|
+
/** Named interface or type alias to document. */
|
|
25
|
+
name: string;
|
|
26
|
+
/** File to read the type from, resolved relative to {@link ExtractOptions.root}. */
|
|
27
|
+
path?: string;
|
|
28
|
+
/** Base directory for resolving a relative {@link ExtractOptions.path}. */
|
|
29
|
+
root?: string;
|
|
30
|
+
/** Inline TypeScript source. Used when {@link ExtractOptions.path} is omitted. */
|
|
31
|
+
source?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Virtual file name backing the in-memory program when `source` is supplied. */
|
|
35
|
+
const VIRTUAL_FILE = "__blume_auto_type_table__.ts";
|
|
36
|
+
|
|
37
|
+
/** Build a compiler host that serves a single in-memory file plus the real libs. */
|
|
38
|
+
const inMemoryHost = (
|
|
39
|
+
ts: typeof TypeScriptApi,
|
|
40
|
+
options: TypeScriptApi.CompilerOptions,
|
|
41
|
+
source: string
|
|
42
|
+
): TypeScriptApi.CompilerHost => {
|
|
43
|
+
const host = ts.createCompilerHost(options, true);
|
|
44
|
+
const getSourceFile = host.getSourceFile.bind(host);
|
|
45
|
+
host.getSourceFile = (requested, languageVersion, onError, shouldCreate) =>
|
|
46
|
+
requested === VIRTUAL_FILE
|
|
47
|
+
? ts.createSourceFile(VIRTUAL_FILE, source, languageVersion, true)
|
|
48
|
+
: getSourceFile(requested, languageVersion, onError, shouldCreate);
|
|
49
|
+
const fileExists = host.fileExists.bind(host);
|
|
50
|
+
host.fileExists = (requested) =>
|
|
51
|
+
requested === VIRTUAL_FILE || fileExists(requested);
|
|
52
|
+
const readFile = host.readFile.bind(host);
|
|
53
|
+
host.readFile = (requested) =>
|
|
54
|
+
requested === VIRTUAL_FILE ? source : readFile(requested);
|
|
55
|
+
return host;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract the documented properties of `name` from a TypeScript file or inline
|
|
60
|
+
* source. Property rows carry the declared type text, JSDoc description,
|
|
61
|
+
* `@default` tag, and whether the property is required.
|
|
62
|
+
*/
|
|
63
|
+
export const extractTypeTable = async (
|
|
64
|
+
options: ExtractOptions
|
|
65
|
+
): Promise<TypeTableProperty[]> => {
|
|
66
|
+
const { name, path, root = process.cwd(), source } = options;
|
|
67
|
+
const tsModule = await import("typescript");
|
|
68
|
+
const ts = (tsModule.default ?? tsModule) as typeof TypeScriptApi;
|
|
69
|
+
|
|
70
|
+
const compilerOptions: TypeScriptApi.CompilerOptions = {
|
|
71
|
+
allowJs: true,
|
|
72
|
+
module: ts.ModuleKind.ESNext,
|
|
73
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
74
|
+
noEmit: true,
|
|
75
|
+
skipLibCheck: true,
|
|
76
|
+
strict: true,
|
|
77
|
+
target: ts.ScriptTarget.ESNext,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
let fileName: string;
|
|
81
|
+
let program: TypeScriptApi.Program;
|
|
82
|
+
if (source === undefined) {
|
|
83
|
+
if (path === undefined) {
|
|
84
|
+
throw new Error("AutoTypeTable needs a `path` or inline `type` source.");
|
|
85
|
+
}
|
|
86
|
+
fileName = nodePath.isAbsolute(path) ? path : nodePath.join(root, path);
|
|
87
|
+
program = ts.createProgram([fileName], compilerOptions);
|
|
88
|
+
} else {
|
|
89
|
+
fileName = VIRTUAL_FILE;
|
|
90
|
+
program = ts.createProgram(
|
|
91
|
+
[fileName],
|
|
92
|
+
compilerOptions,
|
|
93
|
+
inMemoryHost(ts, compilerOptions, source)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sourceFile = program.getSourceFile(fileName);
|
|
98
|
+
if (!sourceFile) {
|
|
99
|
+
throw new Error(`Could not read "${path ?? VIRTUAL_FILE}".`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const declaration = sourceFile.statements.find(
|
|
103
|
+
(statement): statement is TypeScriptApi.DeclarationStatement =>
|
|
104
|
+
(ts.isInterfaceDeclaration(statement) ||
|
|
105
|
+
ts.isTypeAliasDeclaration(statement)) &&
|
|
106
|
+
statement.name?.text === name
|
|
107
|
+
);
|
|
108
|
+
if (!declaration) {
|
|
109
|
+
throw new Error(`No interface or type named "${name}" found.`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const checker = program.getTypeChecker();
|
|
113
|
+
const type = checker.getTypeAtLocation(declaration);
|
|
114
|
+
|
|
115
|
+
return checker.getPropertiesOfType(type).map((symbol) => {
|
|
116
|
+
const decl = symbol.declarations?.[0];
|
|
117
|
+
const signature = decl && ts.isPropertySignature(decl) ? decl : undefined;
|
|
118
|
+
const typeText = signature?.type
|
|
119
|
+
? signature.type.getText(sourceFile)
|
|
120
|
+
: checker.typeToString(
|
|
121
|
+
checker.getTypeOfSymbolAtLocation(symbol, decl ?? declaration)
|
|
122
|
+
);
|
|
123
|
+
const description = ts
|
|
124
|
+
.displayPartsToString(symbol.getDocumentationComment(checker))
|
|
125
|
+
.trim();
|
|
126
|
+
const defaultTag = symbol
|
|
127
|
+
.getJsDocTags(checker)
|
|
128
|
+
.find((tag) => tag.name === "default" || tag.name === "defaultValue");
|
|
129
|
+
const defaultValue = defaultTag
|
|
130
|
+
? ts.displayPartsToString(defaultTag.text).trim()
|
|
131
|
+
: "";
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
default: defaultValue || undefined,
|
|
135
|
+
description: description || undefined,
|
|
136
|
+
name: symbol.getName(),
|
|
137
|
+
required: !signature?.questionToken,
|
|
138
|
+
type: typeText,
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time GitHub repository lookup for `<GithubInfo>`.
|
|
3
|
+
*
|
|
4
|
+
* Fetches a repo's star and fork counts (plus description) from the GitHub REST
|
|
5
|
+
* API. Results are cached per repo for the lifetime of the build, and any
|
|
6
|
+
* failure — network error, rate limit, missing repo — resolves to `null` rather
|
|
7
|
+
* than throwing, so a card never breaks the build when the API is unreachable.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface RepositoryInfo {
|
|
11
|
+
description: string | null;
|
|
12
|
+
forks: number;
|
|
13
|
+
stars: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FetchRepositoryOptions {
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
owner: string;
|
|
19
|
+
repo: string;
|
|
20
|
+
token?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DEFAULT_BASE_URL = "https://api.github.com";
|
|
24
|
+
|
|
25
|
+
/** In-process dedupe so the same repo is fetched once per build. */
|
|
26
|
+
const cache = new Map<string, Promise<RepositoryInfo | null>>();
|
|
27
|
+
|
|
28
|
+
const load = async (
|
|
29
|
+
options: FetchRepositoryOptions
|
|
30
|
+
): Promise<RepositoryInfo | null> => {
|
|
31
|
+
const { baseUrl = DEFAULT_BASE_URL, owner, repo, token } = options;
|
|
32
|
+
const headers = new Headers({ Accept: "application/vnd.github+json" });
|
|
33
|
+
if (token) {
|
|
34
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const response = await fetch(`${baseUrl}/repos/${owner}/${repo}`, {
|
|
38
|
+
headers,
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const data = (await response.json()) as {
|
|
45
|
+
description: string | null;
|
|
46
|
+
forks_count: number;
|
|
47
|
+
stargazers_count: number;
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
description: data.description,
|
|
51
|
+
forks: data.forks_count,
|
|
52
|
+
stars: data.stargazers_count,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const loadSafe = async (
|
|
57
|
+
options: FetchRepositoryOptions
|
|
58
|
+
): Promise<RepositoryInfo | null> => {
|
|
59
|
+
try {
|
|
60
|
+
return await load(options);
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Fetch repo info, deduped per `owner/repo` and never throwing. */
|
|
67
|
+
export const fetchRepositoryInfo = (
|
|
68
|
+
options: FetchRepositoryOptions
|
|
69
|
+
): Promise<RepositoryInfo | null> => {
|
|
70
|
+
const key = `${options.baseUrl ?? DEFAULT_BASE_URL}/${options.owner}/${options.repo}`;
|
|
71
|
+
const existing = cache.get(key);
|
|
72
|
+
if (existing) {
|
|
73
|
+
return existing;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const promise = loadSafe(options);
|
|
77
|
+
cache.set(key, promise);
|
|
78
|
+
return promise;
|
|
79
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client behaviour for the `<blume-mermaid>` custom element emitted by the
|
|
3
|
+
* Mermaid markdown plugin. Mermaid is lazy-loaded — it needs a DOM and is large,
|
|
4
|
+
* so the dependency only downloads on pages that actually contain a diagram —
|
|
5
|
+
* and each diagram re-renders when the color theme flips so it tracks light/dark.
|
|
6
|
+
*
|
|
7
|
+
* Imported for its side effect (registers the element) from RootLayout's script.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const importMermaid = async () => {
|
|
11
|
+
const mod = await import("mermaid");
|
|
12
|
+
return mod.default;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Memoize the import so a page with several diagrams loads Mermaid once.
|
|
16
|
+
let loader: ReturnType<typeof importMermaid> | null = null;
|
|
17
|
+
const loadMermaid = () => {
|
|
18
|
+
loader ??= importMermaid();
|
|
19
|
+
return loader;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const prefersDark = () => document.documentElement.dataset.theme === "dark";
|
|
23
|
+
|
|
24
|
+
let counter = 0;
|
|
25
|
+
|
|
26
|
+
class BlumeMermaid extends HTMLElement {
|
|
27
|
+
connectedCallback() {
|
|
28
|
+
const source = this.dataset.source ?? "";
|
|
29
|
+
if (!source.trim()) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const output = document.createElement("div");
|
|
34
|
+
output.setAttribute("aria-busy", "true");
|
|
35
|
+
this.replaceChildren(output);
|
|
36
|
+
|
|
37
|
+
const render = async () => {
|
|
38
|
+
const mermaid = await loadMermaid();
|
|
39
|
+
mermaid.initialize({
|
|
40
|
+
securityLevel: "strict",
|
|
41
|
+
startOnLoad: false,
|
|
42
|
+
theme: prefersDark() ? "dark" : "default",
|
|
43
|
+
});
|
|
44
|
+
try {
|
|
45
|
+
counter += 1;
|
|
46
|
+
const { svg } = await mermaid.render(
|
|
47
|
+
`blume-mermaid-${counter}`,
|
|
48
|
+
source
|
|
49
|
+
);
|
|
50
|
+
output.innerHTML = svg;
|
|
51
|
+
} catch {
|
|
52
|
+
output.textContent = "Could not render this diagram.";
|
|
53
|
+
}
|
|
54
|
+
output.removeAttribute("aria-busy");
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
render();
|
|
58
|
+
|
|
59
|
+
// Re-render on color-theme changes so the diagram tracks light and dark.
|
|
60
|
+
new MutationObserver(() => render()).observe(document.documentElement, {
|
|
61
|
+
attributeFilter: ["data-theme"],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!customElements.get("blume-mermaid")) {
|
|
67
|
+
customElements.define("blume-mermaid", BlumeMermaid);
|
|
68
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inner markup of the official GitHub mark — a filled glyph on a 16×16 viewBox.
|
|
3
|
+
* Shared by the header repo link, the "Edit on GitHub" action, and
|
|
4
|
+
* `<GithubInfo>`; each wraps it in its own sized `<svg>` (via `set:html`). Kept
|
|
5
|
+
* out of the stroke-based `icons` set, which renders at 24×24 and can't express
|
|
6
|
+
* a filled mark.
|
|
7
|
+
*/
|
|
8
|
+
export const GITHUB_MARK =
|
|
9
|
+
'<path fill-rule="evenodd" clip-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z"/>';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public component contracts.
|
|
3
|
+
*
|
|
4
|
+
* Prop types for built-in components are exported here so users can type their
|
|
5
|
+
* overrides (`import type { CalloutProps } from "blume/components"`). Concrete
|
|
6
|
+
* component types are added as components land; the override descriptor types
|
|
7
|
+
* below are stable today.
|
|
8
|
+
*/
|
|
9
|
+
export type {
|
|
10
|
+
ComponentOverride,
|
|
11
|
+
ComponentOverrides,
|
|
12
|
+
IslandDescriptor,
|
|
13
|
+
} from "../core/define-components.ts";
|
|
14
|
+
export type { HydrationMode } from "../core/schema.ts";
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import type { FormEvent } from "react";
|
|
3
|
+
|
|
4
|
+
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
5
|
+
|
|
6
|
+
interface ChatMessage {
|
|
7
|
+
id: number;
|
|
8
|
+
role: "user" | "assistant";
|
|
9
|
+
content: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// English fallback so the island renders even if no dictionary is passed.
|
|
13
|
+
const DEFAULT_ASK: UIStrings["ask"] = {
|
|
14
|
+
empty: "Ask a question about the docs.",
|
|
15
|
+
error: "Sorry, something went wrong.",
|
|
16
|
+
label: "Ask a question",
|
|
17
|
+
placeholder: "Ask a question…",
|
|
18
|
+
send: "Send",
|
|
19
|
+
title: "Ask AI",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let idCounter = 0;
|
|
23
|
+
const nextId = (): number => {
|
|
24
|
+
idCounter += 1;
|
|
25
|
+
return idCounter;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const BUTTON_CLASS =
|
|
29
|
+
"inline-flex h-9 cursor-pointer items-center gap-2 rounded-blume border border-border bg-muted px-2.5 text-muted-foreground text-sm hover:border-accent disabled:opacity-50";
|
|
30
|
+
|
|
31
|
+
const AskAI = ({ strings }: { strings?: UIStrings["ask"] }) => {
|
|
32
|
+
const t = strings ?? DEFAULT_ASK;
|
|
33
|
+
const [open, setOpen] = useState(false);
|
|
34
|
+
const [input, setInput] = useState("");
|
|
35
|
+
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
36
|
+
const [busy, setBusy] = useState(false);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const handler = (event: Event) => {
|
|
40
|
+
// The search modal forwards the typed query so "Ask AI: <query>" carries
|
|
41
|
+
// straight into the chat input.
|
|
42
|
+
const query = (event as CustomEvent<{ query?: string }>).detail?.query;
|
|
43
|
+
if (query) {
|
|
44
|
+
setInput(query);
|
|
45
|
+
}
|
|
46
|
+
setOpen(true);
|
|
47
|
+
};
|
|
48
|
+
window.addEventListener("blume:open-ask-ai", handler);
|
|
49
|
+
return () => window.removeEventListener("blume:open-ask-ai", handler);
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
const send = async (event: FormEvent) => {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
const question = input.trim();
|
|
55
|
+
if (!question || busy) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const userMessage: ChatMessage = {
|
|
60
|
+
content: question,
|
|
61
|
+
id: nextId(),
|
|
62
|
+
role: "user",
|
|
63
|
+
};
|
|
64
|
+
const history = [...messages, userMessage];
|
|
65
|
+
const assistant: ChatMessage = {
|
|
66
|
+
content: "",
|
|
67
|
+
id: nextId(),
|
|
68
|
+
role: "assistant",
|
|
69
|
+
};
|
|
70
|
+
setMessages([...history, assistant]);
|
|
71
|
+
setInput("");
|
|
72
|
+
setBusy(true);
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetch("/api/ask", {
|
|
76
|
+
body: JSON.stringify({
|
|
77
|
+
messages: history.map((m) => ({ content: m.content, role: m.role })),
|
|
78
|
+
}),
|
|
79
|
+
headers: { "content-type": "application/json" },
|
|
80
|
+
method: "POST",
|
|
81
|
+
});
|
|
82
|
+
const reader = response.body?.getReader();
|
|
83
|
+
const decoder = new TextDecoder();
|
|
84
|
+
if (reader) {
|
|
85
|
+
let done = false;
|
|
86
|
+
while (!done) {
|
|
87
|
+
// oxlint-disable-next-line no-await-in-loop -- sequential stream reads
|
|
88
|
+
const chunk = await reader.read();
|
|
89
|
+
({ done } = chunk);
|
|
90
|
+
if (chunk.value) {
|
|
91
|
+
assistant.content += decoder.decode(chunk.value);
|
|
92
|
+
setMessages((current) => [
|
|
93
|
+
...current.slice(0, -1),
|
|
94
|
+
{ ...assistant },
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
assistant.content = t.error;
|
|
101
|
+
setMessages((current) => [...current.slice(0, -1), { ...assistant }]);
|
|
102
|
+
} finally {
|
|
103
|
+
setBusy(false);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className="relative">
|
|
109
|
+
<button
|
|
110
|
+
className={BUTTON_CLASS}
|
|
111
|
+
onClick={() => setOpen(!open)}
|
|
112
|
+
type="button"
|
|
113
|
+
>
|
|
114
|
+
{t.title}
|
|
115
|
+
</button>
|
|
116
|
+
{open && (
|
|
117
|
+
<div className="absolute top-[calc(100%+0.5rem)] end-0 z-50 flex max-h-[70vh] w-[min(24rem,90vw)] flex-col overflow-hidden rounded-blume border border-border bg-background shadow-xl">
|
|
118
|
+
<div className="flex flex-1 flex-col gap-2.5 overflow-y-auto p-3.5">
|
|
119
|
+
{messages.length === 0 && (
|
|
120
|
+
<p className="m-0 text-muted-foreground text-sm">{t.empty}</p>
|
|
121
|
+
)}
|
|
122
|
+
{messages.map((message) => (
|
|
123
|
+
<div
|
|
124
|
+
className={`whitespace-pre-wrap rounded-blume px-2.5 py-2 text-sm ${
|
|
125
|
+
message.role === "user"
|
|
126
|
+
? "self-end bg-accent/15"
|
|
127
|
+
: "self-start bg-muted"
|
|
128
|
+
}`}
|
|
129
|
+
key={message.id}
|
|
130
|
+
>
|
|
131
|
+
{message.content}
|
|
132
|
+
</div>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
<form
|
|
136
|
+
className="flex gap-2 border-border border-t p-2.5"
|
|
137
|
+
onSubmit={send}
|
|
138
|
+
>
|
|
139
|
+
<input
|
|
140
|
+
aria-label={t.label}
|
|
141
|
+
className="flex-1 rounded-blume border border-border bg-transparent px-2.5 py-1.5 text-sm"
|
|
142
|
+
onChange={(event) => setInput(event.target.value)}
|
|
143
|
+
placeholder={t.placeholder}
|
|
144
|
+
value={input}
|
|
145
|
+
/>
|
|
146
|
+
<button className={BUTTON_CLASS} disabled={busy} type="submit">
|
|
147
|
+
{t.send}
|
|
148
|
+
</button>
|
|
149
|
+
</form>
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export default AskAI;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
import VercelAnalytics from "@vercel/analytics/astro";
|
|
3
|
+
|
|
4
|
+
// Analytics scripts injected into <head>. PostHog and custom providers use the
|
|
5
|
+
// inline-snippet house style (banner/theme/JSON-LD); Vercel uses its official
|
|
6
|
+
// Astro component. Loads ONLY in production builds so `blume dev` stays clean
|
|
7
|
+
// and local traffic never reaches your analytics.
|
|
8
|
+
interface AnalyticsScript {
|
|
9
|
+
attributes?: Record<string, string>;
|
|
10
|
+
content?: string;
|
|
11
|
+
src?: string;
|
|
12
|
+
strategy?: "async" | "defer";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
analytics?: {
|
|
17
|
+
posthog?: { host?: string; key: string };
|
|
18
|
+
scripts?: AnalyticsScript[];
|
|
19
|
+
vercel?: boolean;
|
|
20
|
+
} | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { analytics } = Astro.props;
|
|
24
|
+
const enabled = import.meta.env.PROD && Boolean(analytics);
|
|
25
|
+
|
|
26
|
+
// Vercel Web Analytics: rendered via the official @vercel/analytics/astro
|
|
27
|
+
// component, which injects the first-party script Vercel serves at
|
|
28
|
+
// /_vercel/insights once Web Analytics is enabled for the project.
|
|
29
|
+
const vercelEnabled = enabled && analytics?.vercel === true;
|
|
30
|
+
|
|
31
|
+
// PostHog: the official array.js loader snippet, keyed off config. The host
|
|
32
|
+
// defaults to PostHog Cloud US; EU/self-hosted users override `posthog.host`.
|
|
33
|
+
const POSTHOG_LOADER =
|
|
34
|
+
'!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug getPageViewId captureTraceFeedback captureTraceMetric".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);';
|
|
35
|
+
const posthog = enabled ? analytics?.posthog : undefined;
|
|
36
|
+
const posthogSnippet = posthog
|
|
37
|
+
? `${POSTHOG_LOADER}posthog.init(${JSON.stringify(posthog.key)},{api_host:${JSON.stringify(posthog.host ?? "https://us.i.posthog.com")}});`
|
|
38
|
+
: null;
|
|
39
|
+
|
|
40
|
+
// Custom scripts: any other provider. Each is either external (`src`) or inline
|
|
41
|
+
// (`content`). Explicit fields win over spread `attributes`.
|
|
42
|
+
const customScripts = enabled ? (analytics?.scripts ?? []) : [];
|
|
43
|
+
const externalScripts = customScripts
|
|
44
|
+
.filter((script) => script.src)
|
|
45
|
+
.map((script) => ({
|
|
46
|
+
...script.attributes,
|
|
47
|
+
...(script.strategy === "async" ? { async: true } : {}),
|
|
48
|
+
...(script.strategy === "defer" ? { defer: true } : {}),
|
|
49
|
+
src: script.src,
|
|
50
|
+
}));
|
|
51
|
+
const inlineScripts = customScripts
|
|
52
|
+
.filter((script) => script.content)
|
|
53
|
+
.map((script) => ({ attributes: script.attributes ?? {}, content: script.content }));
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
{vercelEnabled && <VercelAnalytics />}
|
|
57
|
+
{posthogSnippet && <script is:inline set:html={posthogSnippet} />}
|
|
58
|
+
{externalScripts.map((attrs) => <script is:inline {...attrs} />)}
|
|
59
|
+
{
|
|
60
|
+
inlineScripts.map((script) => (
|
|
61
|
+
<script is:inline set:html={script.content} {...script.attributes} />
|
|
62
|
+
))
|
|
63
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Icon from "../Icon.astro";
|
|
3
|
+
|
|
4
|
+
// The site-wide announcement banner, shared by RootLayout and ReferenceLayout so
|
|
5
|
+
// every shell shows the same bar. The pre-paint hide for a dismissed banner and
|
|
6
|
+
// the dismiss click handler live in the layout/Header, not here.
|
|
7
|
+
interface Props {
|
|
8
|
+
banner:
|
|
9
|
+
| {
|
|
10
|
+
content: string;
|
|
11
|
+
link?: { text: string; href: string };
|
|
12
|
+
dismissible: boolean;
|
|
13
|
+
key: string;
|
|
14
|
+
}
|
|
15
|
+
| null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { banner } = Astro.props;
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
banner && (
|
|
23
|
+
<div
|
|
24
|
+
class="relative flex items-center justify-center gap-x-2 gap-y-0.5 bg-foreground px-10 py-3 text-center text-background text-sm max-sm:flex-wrap dark:border-border dark:border-b dark:bg-background dark:text-foreground [:where([data-blume-banner-hidden])_&]:hidden"
|
|
25
|
+
data-banner-key={banner.key}
|
|
26
|
+
data-blume-banner
|
|
27
|
+
>
|
|
28
|
+
<span>{banner.content}</span>
|
|
29
|
+
{banner.link && (
|
|
30
|
+
<a
|
|
31
|
+
class="inline-flex items-center gap-1 font-medium underline underline-offset-2"
|
|
32
|
+
href={banner.link.href}
|
|
33
|
+
>
|
|
34
|
+
{banner.link.text}
|
|
35
|
+
<Icon name="arrow-right" size={14} />
|
|
36
|
+
</a>
|
|
37
|
+
)}
|
|
38
|
+
{banner.dismissible && (
|
|
39
|
+
<button
|
|
40
|
+
aria-label="Dismiss announcement"
|
|
41
|
+
class="absolute end-1.5 inline-flex size-7 items-center justify-center rounded-full text-background/80 transition-colors hover:bg-white/10 hover:text-background dark:text-foreground/70 dark:hover:bg-foreground/10 dark:hover:text-foreground"
|
|
42
|
+
data-blume-banner-dismiss
|
|
43
|
+
type="button"
|
|
44
|
+
>
|
|
45
|
+
<Icon name="x" size={16} />
|
|
46
|
+
</button>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Crumb } from "./nav-utils.ts";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Full breadcrumb trail from the site root to the current page. */
|
|
6
|
+
crumbs: Crumb[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { crumbs } = Astro.props;
|
|
10
|
+
|
|
11
|
+
// The built-in shows a single "eyebrow" crumb — the parent group — rather than
|
|
12
|
+
// the whole trail. An override receives the complete list and can render more.
|
|
13
|
+
const eyebrowCrumb = crumbs.length > 1 ? crumbs[crumbs.length - 2] : null;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
eyebrowCrumb && (
|
|
18
|
+
<nav
|
|
19
|
+
aria-label="Breadcrumb"
|
|
20
|
+
class="mx-auto mb-2 max-w-[42rem] text-muted-foreground text-sm"
|
|
21
|
+
>
|
|
22
|
+
{eyebrowCrumb.route ? (
|
|
23
|
+
<a class="hover:text-foreground" href={eyebrowCrumb.route}>
|
|
24
|
+
{eyebrowCrumb.label}
|
|
25
|
+
</a>
|
|
26
|
+
) : (
|
|
27
|
+
<span>{eyebrowCrumb.label}</span>
|
|
28
|
+
)}
|
|
29
|
+
</nav>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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.
|
|
5
|
+
interface Props {
|
|
6
|
+
favicon?: {
|
|
7
|
+
href: string;
|
|
8
|
+
type?: string;
|
|
9
|
+
} | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { favicon } = Astro.props;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{favicon && <link href={favicon.href} rel="icon" type={favicon.type} />}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Emits the optimized @font-face declarations + preload links for each
|
|
3
|
+
// configured font (Astro's Fonts API). Renders nothing when no fonts are set;
|
|
4
|
+
// the CSS variables match the astro.config `fonts:` entries.
|
|
5
|
+
import { Font } from "astro:assets";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
cssVars: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { cssVars } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{cssVars.map((cssVar) => <Font cssVariable={cssVar} preload />)}
|