blume 0.5.3 → 0.6.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/dist/cli/index.js +3349 -7024
- package/dist/cli/index.js.map +39 -69
- package/dist/types/core/config.d.ts +0 -8
- package/dist/types/core/data.d.ts +6 -2
- package/dist/types/core/i18n-ui.d.ts +50 -0
- package/dist/types/core/schema.d.ts +379 -485
- package/dist/types/core/types.d.ts +8 -6
- package/docs/advanced/meta.ts +1 -8
- package/docs/advanced/skills.mdx +28 -0
- package/docs/configuration/ai.mdx +58 -0
- package/docs/configuration/index.mdx +13 -17
- package/docs/configuration/seo.mdx +59 -1
- package/docs/configuration/theming.mdx +15 -18
- package/docs/content/components.mdx +2 -53
- package/docs/content/i18n.mdx +0 -4
- package/docs/content/meta.mdx +3 -17
- package/docs/content/navigation.mdx +41 -4
- package/docs/content/syntax.mdx +1 -1
- package/docs/index.mdx +0 -1
- package/docs/reference/cli.mdx +12 -13
- package/package.json +6 -6
- package/skills/blume/SKILL.md +71 -0
- package/skills/blume-update-docs/SKILL.md +52 -0
- package/skills/blume-update-docs/references/audit-checklist.md +46 -0
- package/src/ai/agent-readability.ts +97 -0
- package/src/ai/ask-context.ts +131 -8
- package/src/ai/ask-data.ts +4 -1
- package/src/astro/generate.ts +19 -12
- package/src/astro/integration.ts +0 -21
- package/src/astro/templates.ts +33 -21
- package/src/cli/commands/build.ts +15 -0
- package/src/cli/commands/dev.ts +31 -20
- package/src/cli/commands/validate.ts +0 -2
- package/src/cli/dev-lock.ts +94 -21
- package/src/cli/index.ts +0 -2
- package/src/components/BlumePage.astro +0 -6
- package/src/components/Icon.astro +1 -12
- package/src/components/content/AccordionItem.astro +3 -6
- package/src/components/content/Badge.astro +1 -3
- package/src/components/content/Callout.astro +3 -9
- package/src/components/content/Card.astro +2 -3
- package/src/components/content/ColorItem.astro +2 -2
- package/src/components/content/Column.astro +1 -1
- package/src/components/content/GithubInfo.astro +11 -10
- package/src/components/content/Prompt.astro +1 -1
- package/src/components/content/Step.astro +3 -4
- package/src/components/content/Tab.astro +2 -3
- package/src/components/content/TypeTable.astro +13 -8
- package/src/components/content/Update.astro +1 -1
- package/src/components/islands/AskAI.astro +66 -2
- package/src/components/islands/ask-ai.tsx +289 -53
- package/src/components/layout/Header.astro +27 -4
- package/src/components/layout/Logo.astro +5 -1
- package/src/components/layout/NavSelector.astro +1 -1
- package/src/components/layout/NavTree.astro +15 -15
- package/src/components/layout/PageActions.astro +73 -30
- package/src/components/layout/PageLayout.astro +42 -0
- package/src/components/layout/ReferenceLayout.astro +1 -0
- package/src/components/layout/RootLayout.astro +79 -4
- package/src/components/layout/Search.astro +5 -5
- package/src/components/layout/nav-utils.ts +9 -4
- package/src/components/openapi/ApiOverview.astro +4 -50
- package/src/components/openapi/ApiTagOperations.astro +42 -0
- package/src/core/builtin-tags.ts +1 -3
- package/src/core/config.ts +5 -28
- package/src/core/data.ts +6 -2
- package/src/core/graph.ts +8 -6
- package/src/core/i18n-ui.ts +5 -0
- package/src/core/links.ts +5 -19
- package/src/core/meta.ts +1 -1
- package/src/core/nav-diagnostics.ts +7 -0
- package/src/core/navigation.ts +38 -17
- package/src/core/project-graph.ts +0 -5
- package/src/core/schema.ts +133 -95
- package/src/core/sources/filesystem.ts +5 -1
- package/src/core/sources/resolve.ts +0 -13
- package/src/core/sources/watch.ts +43 -11
- package/src/core/types.ts +8 -6
- package/src/deploy/robots.ts +37 -4
- package/src/openapi/parse.ts +197 -14
- package/src/openapi/render-mdx.ts +44 -10
- package/src/openapi/scalar.ts +1 -1
- package/src/openapi/source.ts +19 -2
- package/src/search/documents.ts +9 -2
- package/src/theme/entry.ts +45 -17
- package/src/theme/icons.ts +18 -109
- package/src/theme/palette.ts +25 -51
- package/src/theme/twoslash.ts +6 -1
- package/dist/types/core/bridge.d.ts +0 -24
- package/dist/types/core/package-json.d.ts +0 -12
- package/dist/types/migrate/mintlify/assets.d.ts +0 -8
- package/dist/types/migrate/mintlify/config.d.ts +0 -16
- package/dist/types/migrate/mintlify/i18n.d.ts +0 -7
- package/dist/types/migrate/shared.d.ts +0 -153
- package/docs/advanced/bridge.mdx +0 -76
- package/docs/advanced/migrate.mdx +0 -124
- package/src/astro/static-assets.ts +0 -124
- package/src/cli/commands/migrate.ts +0 -39
- package/src/components/content/ApiField.astro +0 -75
- package/src/components/content/ParamField.astro +0 -39
- package/src/components/content/RequestField.astro +0 -23
- package/src/components/content/ResponseField.astro +0 -23
- package/src/components/content/Warning.astro +0 -9
- package/src/core/assets.ts +0 -31
- package/src/core/bridge.ts +0 -102
- package/src/core/sources/mintlify.ts +0 -190
- package/src/migrate/fumadocs/config.ts +0 -155
- package/src/migrate/fumadocs/content.ts +0 -376
- package/src/migrate/fumadocs/frontmatter.ts +0 -18
- package/src/migrate/fumadocs/groups.ts +0 -237
- package/src/migrate/fumadocs/index.ts +0 -355
- package/src/migrate/fumadocs/meta.ts +0 -244
- package/src/migrate/migrate.ts +0 -53
- package/src/migrate/mintlify/assets.ts +0 -46
- package/src/migrate/mintlify/config.ts +0 -954
- package/src/migrate/mintlify/content.ts +0 -120
- package/src/migrate/mintlify/frontmatter.ts +0 -126
- package/src/migrate/mintlify/i18n.ts +0 -51
- package/src/migrate/mintlify/icons.ts +0 -128
- package/src/migrate/mintlify/index.ts +0 -459
- package/src/migrate/mintlify/snippets.ts +0 -315
- package/src/migrate/mintlify/transform.ts +0 -82
- package/src/migrate/nextra/content.ts +0 -46
- package/src/migrate/nextra/frontmatter.ts +0 -40
- package/src/migrate/nextra/index.ts +0 -389
- package/src/migrate/nextra/meta.ts +0 -266
- package/src/migrate/shared.ts +0 -801
- package/src/migrate/starlight/config.ts +0 -455
- package/src/migrate/starlight/content.ts +0 -75
- package/src/migrate/starlight/frontmatter.ts +0 -111
- package/src/migrate/starlight/i18n.ts +0 -54
- package/src/migrate/starlight/index.ts +0 -131
|
@@ -161,9 +161,14 @@ const withoutTabSections = (nodes: NavNode[], tabs: NavTab[]): NavNode[] => {
|
|
|
161
161
|
* Adapters / API / AI tabs) drills each tab into its own pages instead of one
|
|
162
162
|
* global tree, the way Fumadocs' root folders do. On a route under no tab (or
|
|
163
163
|
* the root `/` tab), the tab-owned groups are hidden so the root sidebar shows
|
|
164
|
-
* only pages that don't belong to a tab.
|
|
165
|
-
*
|
|
166
|
-
* sidebar
|
|
164
|
+
* only pages that don't belong to a tab.
|
|
165
|
+
*
|
|
166
|
+
* When a matched tab owns no sidebar group — a standalone page like the
|
|
167
|
+
* generated changelog timeline (`/changelog`), or a tab whose source produced
|
|
168
|
+
* no pages — the sidebar is empty. It must not fall back to the full tree: that
|
|
169
|
+
* would leak every *other* tab's section (e.g. the OpenAPI operations) onto the
|
|
170
|
+
* page. On a route under no tab, hiding the tab sections falls back to the full
|
|
171
|
+
* sidebar only when it would otherwise blank, so an un-tabbed route stays full.
|
|
167
172
|
*/
|
|
168
173
|
export const sidebarForRoute = (
|
|
169
174
|
sidebar: NavNode[],
|
|
@@ -172,7 +177,7 @@ export const sidebarForRoute = (
|
|
|
172
177
|
): NavNode[] => {
|
|
173
178
|
const tab = activeTab(tabs, route);
|
|
174
179
|
if (tab) {
|
|
175
|
-
return sectionChildren(sidebar, tab.path) ??
|
|
180
|
+
return sectionChildren(sidebar, tab.path) ?? [];
|
|
176
181
|
}
|
|
177
182
|
const scoped = withoutTabSections(sidebar, tabs);
|
|
178
183
|
return scoped.length > 0 ? scoped : sidebar;
|
|
@@ -1,28 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
import specs from "blume:openapi";
|
|
3
|
-
import MethodBadge from "./MethodBadge.astro";
|
|
4
3
|
|
|
4
|
+
// The spec-level metadata block (version + base URLs) at the top of an API
|
|
5
|
+
// overview page. The tag sections that follow are emitted by `overviewMdx` as
|
|
6
|
+
// markdown headings plus `<ApiTagOperations>` lists, so they land in the
|
|
7
|
+
// table of contents.
|
|
5
8
|
interface Props {
|
|
6
9
|
source: string;
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
const { source } = Astro.props;
|
|
10
13
|
const spec = specs[source];
|
|
11
|
-
const operations = Object.values(spec?.operations ?? {});
|
|
12
14
|
const servers =
|
|
13
15
|
((spec?.document ?? {}) as { servers?: { url?: string }[] }).servers ?? [];
|
|
14
|
-
|
|
15
|
-
// Tags in declared order, then any operation tag not listed under `tags`.
|
|
16
|
-
const declaredSlugs = new Set(spec?.tags.map((tag) => tag.slug));
|
|
17
|
-
const extraSlugs = new Set<string>();
|
|
18
|
-
const extraTags: { slug: string; name: string; description: string }[] = [];
|
|
19
|
-
for (const operation of operations) {
|
|
20
|
-
if (!(declaredSlugs.has(operation.tagSlug) || extraSlugs.has(operation.tagSlug))) {
|
|
21
|
-
extraSlugs.add(operation.tagSlug);
|
|
22
|
-
extraTags.push({ description: "", name: operation.tag, slug: operation.tagSlug });
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
const sections = [...(spec?.tags ?? []), ...extraTags];
|
|
26
16
|
---
|
|
27
17
|
|
|
28
18
|
{
|
|
@@ -43,42 +33,6 @@ const sections = [...(spec?.tags ?? []), ...extraTags];
|
|
|
43
33
|
))}
|
|
44
34
|
</div>
|
|
45
35
|
)}
|
|
46
|
-
{sections.map((tag) => {
|
|
47
|
-
const ops = operations.filter(
|
|
48
|
-
(operation) => operation.tagSlug === tag.slug
|
|
49
|
-
);
|
|
50
|
-
if (ops.length === 0) {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
return (
|
|
54
|
-
<section class="mb-8">
|
|
55
|
-
<h2 class="mb-1 font-semibold text-foreground text-xl">
|
|
56
|
-
{tag.name}
|
|
57
|
-
</h2>
|
|
58
|
-
{tag.description && (
|
|
59
|
-
<div class="mb-3 text-muted-foreground text-sm" set:text={tag.description} />
|
|
60
|
-
)}
|
|
61
|
-
<ul class="not-prose flex list-none flex-col gap-2 p-0">
|
|
62
|
-
{ops.map((operation) => (
|
|
63
|
-
<li>
|
|
64
|
-
<a
|
|
65
|
-
class="flex items-center gap-3 rounded-blume border border-border p-3 text-inherit no-underline! transition-colors hover:border-accent hover:bg-muted hover:no-underline!"
|
|
66
|
-
href={operation.route}
|
|
67
|
-
>
|
|
68
|
-
<MethodBadge method={operation.method} />
|
|
69
|
-
<span class="font-medium text-foreground text-sm">
|
|
70
|
-
{operation.summary || operation.path}
|
|
71
|
-
</span>
|
|
72
|
-
<code class="ml-auto hidden text-muted-foreground text-xs sm:inline">
|
|
73
|
-
{operation.path}
|
|
74
|
-
</code>
|
|
75
|
-
</a>
|
|
76
|
-
</li>
|
|
77
|
-
))}
|
|
78
|
-
</ul>
|
|
79
|
-
</section>
|
|
80
|
-
);
|
|
81
|
-
})}
|
|
82
36
|
</div>
|
|
83
37
|
)
|
|
84
38
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
import specs from "blume:openapi";
|
|
3
|
+
import MethodBadge from "./MethodBadge.astro";
|
|
4
|
+
|
|
5
|
+
// The operation-link list for one tag section of an API overview page. The
|
|
6
|
+
// section heading itself is emitted by `overviewMdx` as a markdown `##` — so it
|
|
7
|
+
// flows into the table of contents — and this component renders only the list
|
|
8
|
+
// of operations beneath it.
|
|
9
|
+
interface Props {
|
|
10
|
+
source: string;
|
|
11
|
+
/** Tag slug (`ApiOperationRef.tagSlug`) selecting this section's operations. */
|
|
12
|
+
tag: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { source, tag } = Astro.props;
|
|
16
|
+
const operations = Object.values(specs[source]?.operations ?? {}).filter(
|
|
17
|
+
(operation) => operation.tagSlug === tag
|
|
18
|
+
);
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
operations.length > 0 && (
|
|
23
|
+
<ul class="not-prose my-4 flex list-none flex-col gap-2 p-0">
|
|
24
|
+
{operations.map((operation) => (
|
|
25
|
+
<li>
|
|
26
|
+
<a
|
|
27
|
+
class="flex items-center gap-3 rounded-blume border border-border p-3 text-inherit no-underline! transition-colors hover:border-accent hover:bg-muted hover:no-underline!"
|
|
28
|
+
href={operation.route}
|
|
29
|
+
>
|
|
30
|
+
<MethodBadge method={operation.method} />
|
|
31
|
+
<span class="font-medium text-foreground text-sm">
|
|
32
|
+
{operation.summary || operation.path}
|
|
33
|
+
</span>
|
|
34
|
+
<code class="ml-auto hidden text-muted-foreground text-xs sm:inline">
|
|
35
|
+
{operation.path}
|
|
36
|
+
</code>
|
|
37
|
+
</a>
|
|
38
|
+
</li>
|
|
39
|
+
))}
|
|
40
|
+
</ul>
|
|
41
|
+
)
|
|
42
|
+
}
|
package/src/core/builtin-tags.ts
CHANGED
|
@@ -8,6 +8,7 @@ export const BUILTIN_MDX_TAGS = new Set<string>([
|
|
|
8
8
|
"Accordion",
|
|
9
9
|
"AccordionItem",
|
|
10
10
|
"ApiOverview",
|
|
11
|
+
"ApiTagOperations",
|
|
11
12
|
"AutoTypeTable",
|
|
12
13
|
"Badge",
|
|
13
14
|
"Callout",
|
|
@@ -28,10 +29,7 @@ export const BUILTIN_MDX_TAGS = new Set<string>([
|
|
|
28
29
|
"Math",
|
|
29
30
|
"Operation",
|
|
30
31
|
"Panel",
|
|
31
|
-
"ParamField",
|
|
32
32
|
"Prompt",
|
|
33
|
-
"RequestField",
|
|
34
|
-
"ResponseField",
|
|
35
33
|
"Step",
|
|
36
34
|
"Steps",
|
|
37
35
|
"Tab",
|
package/src/core/config.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
|
-
import { detectMintlifyBridge } from "./bridge.ts";
|
|
4
|
-
import type { BridgeDetection } from "./bridge.ts";
|
|
5
3
|
import { applyDeploymentEnv } from "./deployment-env.ts";
|
|
6
4
|
import { BlumeError, diagnosticsFromZod } from "./diagnostics.ts";
|
|
7
5
|
import { createModuleLoader } from "./load-module.ts";
|
|
@@ -16,20 +14,11 @@ import type { Diagnostic } from "./types.ts";
|
|
|
16
14
|
*/
|
|
17
15
|
export const defineConfig = (config: BlumeConfig): BlumeConfig => config;
|
|
18
16
|
|
|
19
|
-
/** Bridge mode info: a foreign docs tool Blume is serving without migrating. */
|
|
20
|
-
export interface ConfigBridge {
|
|
21
|
-
tool: "mintlify";
|
|
22
|
-
/** Absolute path of the foreign config file (`docs.json`/`mint.json`). */
|
|
23
|
-
configFile: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
17
|
/** Result of loading + validating a project config. */
|
|
27
18
|
export interface ConfigLoadResult {
|
|
28
19
|
config: ResolvedConfig;
|
|
29
20
|
/** Absolute path of the config file used, or null when defaults were used. */
|
|
30
21
|
configFile: string | null;
|
|
31
|
-
/** Set when a foreign docs config (e.g. Mintlify) is being bridged. */
|
|
32
|
-
bridge: ConfigBridge | null;
|
|
33
22
|
diagnostics: Diagnostic[];
|
|
34
23
|
}
|
|
35
24
|
|
|
@@ -50,9 +39,6 @@ export const loadConfig = async (
|
|
|
50
39
|
): Promise<ConfigLoadResult> => {
|
|
51
40
|
const configFile = findConfigFile(root);
|
|
52
41
|
|
|
53
|
-
// With no Blume config, a Mintlify `docs.json` activates bridge mode: serve
|
|
54
|
-
// the unconverted project by synthesizing config + a `mintlify` content source.
|
|
55
|
-
let bridge: BridgeDetection | null = null;
|
|
56
42
|
let raw: unknown = {};
|
|
57
43
|
if (configFile) {
|
|
58
44
|
try {
|
|
@@ -65,30 +51,24 @@ export const loadConfig = async (
|
|
|
65
51
|
severity: "error",
|
|
66
52
|
});
|
|
67
53
|
}
|
|
68
|
-
} else {
|
|
69
|
-
bridge = await detectMintlifyBridge(root);
|
|
70
|
-
if (bridge) {
|
|
71
|
-
({ raw } = bridge);
|
|
72
|
-
}
|
|
73
54
|
}
|
|
74
55
|
|
|
75
|
-
const sourceFile = bridge?.configFile ?? configFile;
|
|
76
56
|
const parsed = blumeConfigSchema.safeParse(raw ?? {});
|
|
77
57
|
if (!parsed.success) {
|
|
78
58
|
// Read the raw config text (when on disk) so errors carry a line/column.
|
|
79
59
|
const source =
|
|
80
|
-
|
|
81
|
-
? readFileSync(
|
|
60
|
+
configFile && existsSync(configFile)
|
|
61
|
+
? readFileSync(configFile, "utf-8")
|
|
82
62
|
: undefined;
|
|
83
63
|
const diagnostics = diagnosticsFromZod(parsed.error, {
|
|
84
64
|
code: "BLUME_CONFIG_INVALID",
|
|
85
|
-
file:
|
|
65
|
+
file: configFile ?? undefined,
|
|
86
66
|
source,
|
|
87
67
|
});
|
|
88
68
|
const [first, ...rest] = diagnostics;
|
|
89
69
|
const primary = first ?? {
|
|
90
70
|
code: "BLUME_CONFIG_INVALID",
|
|
91
|
-
file:
|
|
71
|
+
file: configFile ?? undefined,
|
|
92
72
|
message: "Invalid Blume config.",
|
|
93
73
|
severity: "error" as const,
|
|
94
74
|
};
|
|
@@ -115,15 +95,12 @@ export const loadConfig = async (
|
|
|
115
95
|
const ogEnabled = config.seo.og.enabled ?? Boolean(site);
|
|
116
96
|
|
|
117
97
|
return {
|
|
118
|
-
bridge: bridge
|
|
119
|
-
? { configFile: bridge.configFile, tool: bridge.tool }
|
|
120
|
-
: null,
|
|
121
98
|
config: {
|
|
122
99
|
...config,
|
|
123
100
|
deployment: { ...config.deployment, site },
|
|
124
101
|
seo: { ...config.seo, og: { ...config.seo.og, enabled: ogEnabled } },
|
|
125
102
|
},
|
|
126
|
-
configFile
|
|
103
|
+
configFile,
|
|
127
104
|
diagnostics: [],
|
|
128
105
|
};
|
|
129
106
|
};
|
package/src/core/data.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface BlumeLogo {
|
|
|
17
17
|
dark?: string;
|
|
18
18
|
alt: string;
|
|
19
19
|
href: string;
|
|
20
|
+
/** Wordmark text beside the mark; `undefined` falls back to the site title. */
|
|
21
|
+
text?: string;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/** A favicon or apple-touch-icon: a link href plus an optional MIME type. */
|
|
@@ -86,6 +88,10 @@ export interface BlumeDataConfig {
|
|
|
86
88
|
analytics: NonNullable<ResolvedConfig["analytics"]> | null;
|
|
87
89
|
/** Apple touch icon, or `null` when none is configured/detected. */
|
|
88
90
|
appleIcon: BlumeFavicon | null;
|
|
91
|
+
/** Ask AI empty-state suggestions, or `null` when Ask AI is off. */
|
|
92
|
+
ask: {
|
|
93
|
+
suggestions: NonNullable<ResolvedConfig["ai"]["ask"]>["suggestions"];
|
|
94
|
+
} | null;
|
|
89
95
|
banner: BlumeBanner | null;
|
|
90
96
|
/** `markdown.code.wrap`: wrap long code lines instead of scrolling. */
|
|
91
97
|
codeWrap: boolean;
|
|
@@ -93,8 +99,6 @@ export interface BlumeDataConfig {
|
|
|
93
99
|
favicon: BlumeFavicon;
|
|
94
100
|
feedback: boolean;
|
|
95
101
|
i18n: BlumeDataI18n | null;
|
|
96
|
-
/** Default icon library for bare `icon` names. */
|
|
97
|
-
icons: ResolvedConfig["icons"];
|
|
98
102
|
/** `markdown.imageZoom`: click-to-zoom content images. */
|
|
99
103
|
imageZoom: boolean;
|
|
100
104
|
logo: BlumeLogo | null;
|
package/src/core/graph.ts
CHANGED
|
@@ -60,7 +60,7 @@ export const buildContentGraph = (
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// Each locale gets an independent tree from its own pages and folder meta,
|
|
63
|
-
// so navigation may diverge per language
|
|
63
|
+
// so navigation may diverge per language.
|
|
64
64
|
for (const { code } of i18n.locales) {
|
|
65
65
|
// Localize internal tab paths so a header tab points to its in-locale
|
|
66
66
|
// route (e.g. `/docs` -> `/fr/docs`); external paths pass through.
|
|
@@ -89,7 +89,8 @@ export const buildContentGraph = (
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
navigationByLocale[code] = buildNavigation(localePages, {
|
|
92
|
-
|
|
92
|
+
display: options.navigation.sidebar.display,
|
|
93
|
+
featured: options.navigation.featured,
|
|
93
94
|
folderMeta: options.folderMeta,
|
|
94
95
|
// Meta files live in locale directories only under the `dir` parser
|
|
95
96
|
// (`fr/guides/meta.ts` -> key `fr/guides`). Under `dot`, translations
|
|
@@ -100,23 +101,24 @@ export const buildContentGraph = (
|
|
|
100
101
|
refByLogical: true,
|
|
101
102
|
selectors: options.navigation.selectors,
|
|
102
103
|
sharedFolderMeta: options.sharedFolderMeta,
|
|
103
|
-
sidebar: options.navigation.sidebar,
|
|
104
|
+
sidebar: options.navigation.sidebar.items,
|
|
104
105
|
tabs,
|
|
105
106
|
});
|
|
106
107
|
}
|
|
107
108
|
navigation = navigationByLocale[i18n.defaultLocale] ?? {
|
|
108
|
-
|
|
109
|
+
featured: [],
|
|
109
110
|
selectors: [],
|
|
110
111
|
sidebar: [],
|
|
111
112
|
tabs: [],
|
|
112
113
|
};
|
|
113
114
|
} else {
|
|
114
115
|
navigation = buildNavigation(pages, {
|
|
115
|
-
|
|
116
|
+
display: options.navigation.sidebar.display,
|
|
117
|
+
featured: options.navigation.featured,
|
|
116
118
|
folderMeta: options.folderMeta,
|
|
117
119
|
selectors: options.navigation.selectors,
|
|
118
120
|
sharedFolderMeta: options.sharedFolderMeta,
|
|
119
|
-
sidebar: options.navigation.sidebar,
|
|
121
|
+
sidebar: options.navigation.sidebar.items,
|
|
120
122
|
tabs: options.navigation.tabs,
|
|
121
123
|
});
|
|
122
124
|
}
|
package/src/core/i18n-ui.ts
CHANGED
|
@@ -19,6 +19,7 @@ const uiStringsObject = z.object({
|
|
|
19
19
|
connectMcp: z.string().default("Connect to MCP"),
|
|
20
20
|
copied: z.string().default("Copied!"),
|
|
21
21
|
copyClaudeCode: z.string().default("Copy Claude Code command"),
|
|
22
|
+
copyCodex: z.string().default("Copy Codex command"),
|
|
22
23
|
copyMarkdown: z.string().default("Copy as Markdown"),
|
|
23
24
|
copyServerUrl: z.string().default("Copy server URL"),
|
|
24
25
|
edit: z.string().default("Edit on GitHub"),
|
|
@@ -28,11 +29,15 @@ const uiStringsObject = z.object({
|
|
|
28
29
|
.default({}),
|
|
29
30
|
ask: z
|
|
30
31
|
.object({
|
|
32
|
+
clear: z.string().default("Clear conversation"),
|
|
33
|
+
close: z.string().default("Close"),
|
|
34
|
+
copy: z.string().default("Copy conversation"),
|
|
31
35
|
empty: z.string().default("Ask a question about the docs."),
|
|
32
36
|
error: z.string().default("Sorry, something went wrong."),
|
|
33
37
|
label: z.string().default("Ask a question"),
|
|
34
38
|
placeholder: z.string().default("Ask a question…"),
|
|
35
39
|
send: z.string().default("Send"),
|
|
40
|
+
tip: z.string().default("Tip: You can open and close chat with"),
|
|
36
41
|
title: z.string().default("Ask AI"),
|
|
37
42
|
})
|
|
38
43
|
.default({}),
|
package/src/core/links.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
|
|
3
3
|
import { basename, join } from "pathe";
|
|
4
4
|
|
|
5
|
-
import type { AssetMount } from "./assets.ts";
|
|
6
5
|
import type {
|
|
7
6
|
ContentGraph,
|
|
8
7
|
Diagnostic,
|
|
@@ -38,25 +37,15 @@ interface ExternalRef extends LinkSite {
|
|
|
38
37
|
/** Lookups derived once from the content graph. */
|
|
39
38
|
interface LinkContext {
|
|
40
39
|
anchors: Map<string, Set<string>>;
|
|
41
|
-
/** `content.assets` mounts served alongside `public/` (checked in place). */
|
|
42
|
-
assetMounts: AssetMount[];
|
|
43
40
|
publicDir: string | null;
|
|
44
41
|
/** Normalized `redirect.from` paths — valid targets that resolve at runtime. */
|
|
45
42
|
redirects: Set<string>;
|
|
46
43
|
routes: Set<string>;
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
/** Whether a resolved asset path exists under `public
|
|
50
|
-
const assetIsPresent = (resolved: string, ctx: LinkContext): boolean =>
|
|
51
|
-
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
return ctx.assetMounts.some(
|
|
55
|
-
(mount) =>
|
|
56
|
-
(resolved === mount.url || resolved.startsWith(`${mount.url}/`)) &&
|
|
57
|
-
existsSync(join(mount.dir, resolved.slice(mount.url.length)))
|
|
58
|
-
);
|
|
59
|
-
};
|
|
46
|
+
/** Whether a resolved asset path exists under `public/`. */
|
|
47
|
+
const assetIsPresent = (resolved: string, ctx: LinkContext): boolean =>
|
|
48
|
+
ctx.publicDir !== null && existsSync(join(ctx.publicDir, resolved));
|
|
60
49
|
|
|
61
50
|
/** Outcome of classifying one link target. */
|
|
62
51
|
type LinkResult = Diagnostic | "asset-unchecked" | null;
|
|
@@ -165,8 +154,8 @@ const checkPathLink = (
|
|
|
165
154
|
if (assetIsPresent(resolved, ctx)) {
|
|
166
155
|
return null;
|
|
167
156
|
}
|
|
168
|
-
// Nowhere to look: no `public/`
|
|
169
|
-
if (ctx.publicDir === null
|
|
157
|
+
// Nowhere to look: no `public/` directory.
|
|
158
|
+
if (ctx.publicDir === null) {
|
|
170
159
|
return "asset-unchecked";
|
|
171
160
|
}
|
|
172
161
|
return {
|
|
@@ -347,15 +336,12 @@ export const validateLinks = async (
|
|
|
347
336
|
options: {
|
|
348
337
|
publicDir: string | null;
|
|
349
338
|
checkExternal?: boolean;
|
|
350
|
-
/** `content.assets` mounts served alongside `public/`. */
|
|
351
|
-
assetMounts?: AssetMount[];
|
|
352
339
|
/** Configured redirects; their `from` paths count as valid link targets. */
|
|
353
340
|
redirects?: { from: string }[];
|
|
354
341
|
}
|
|
355
342
|
): Promise<Diagnostic[]> => {
|
|
356
343
|
const ctx: LinkContext = {
|
|
357
344
|
anchors: buildAnchorIndex(graph.pages),
|
|
358
|
-
assetMounts: options.assetMounts ?? [],
|
|
359
345
|
publicDir: options.publicDir,
|
|
360
346
|
redirects: new Set(
|
|
361
347
|
(options.redirects ?? []).map((redirect) => toRoute(redirect.from))
|
package/src/core/meta.ts
CHANGED
|
@@ -39,7 +39,7 @@ export const discoverFolderMeta = async (
|
|
|
39
39
|
absolute: true,
|
|
40
40
|
cwd: contentRoot,
|
|
41
41
|
// Never descend into dependencies or build output — relevant when the
|
|
42
|
-
// content root is the project root (e.g. a
|
|
42
|
+
// content root is the project root (e.g. a `.`-rooted or all-staged project).
|
|
43
43
|
ignore: ["**/node_modules/**", "**/.blume/**", "**/dist/**"],
|
|
44
44
|
onlyFiles: true,
|
|
45
45
|
});
|
|
@@ -42,6 +42,9 @@ const collectIcons = (
|
|
|
42
42
|
push(item.icon, `selector "${item.label}"`);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
for (const link of navigation.featured) {
|
|
46
|
+
push(link.icon, `featured link "${link.label}"`);
|
|
47
|
+
}
|
|
45
48
|
const sidebars = [navigation.sidebar];
|
|
46
49
|
for (const sidebar of sidebars) {
|
|
47
50
|
for (const node of flattenNodes(sidebar)) {
|
|
@@ -90,6 +93,10 @@ export const validateNavTargets = (
|
|
|
90
93
|
...navigation.selectors.flatMap((selector) =>
|
|
91
94
|
selector.items.map((item) => ({ label: item.label, path: item.path }))
|
|
92
95
|
),
|
|
96
|
+
...navigation.featured.map((link) => ({
|
|
97
|
+
label: link.label,
|
|
98
|
+
path: link.href,
|
|
99
|
+
})),
|
|
93
100
|
];
|
|
94
101
|
const diagnostics: Diagnostic[] = [];
|
|
95
102
|
const seen = new Set<string>();
|
package/src/core/navigation.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
SidebarItemConfig,
|
|
7
7
|
} from "./schema.ts";
|
|
8
8
|
import type {
|
|
9
|
-
|
|
9
|
+
FeaturedLink,
|
|
10
10
|
NavNode,
|
|
11
11
|
Navigation,
|
|
12
12
|
NavSelector,
|
|
@@ -59,7 +59,6 @@ interface MutableGroup {
|
|
|
59
59
|
label: string;
|
|
60
60
|
icon?: string;
|
|
61
61
|
collapsed?: boolean;
|
|
62
|
-
display?: SidebarDisplay;
|
|
63
62
|
order: number;
|
|
64
63
|
children: MutableNode[];
|
|
65
64
|
index: Map<string, MutableGroup>;
|
|
@@ -141,7 +140,6 @@ const applyFolderMeta = (
|
|
|
141
140
|
group.icon = meta.icon ?? group.icon;
|
|
142
141
|
group.order = meta.order ?? group.order;
|
|
143
142
|
group.collapsed = meta.collapsed ?? group.collapsed;
|
|
144
|
-
group.display = meta.display ?? group.display;
|
|
145
143
|
|
|
146
144
|
if (meta.pages) {
|
|
147
145
|
const rank = new Map(meta.pages.map((key, i) => [key, i]));
|
|
@@ -175,7 +173,21 @@ const sortNodes = (nodes: MutableNode[]): void => {
|
|
|
175
173
|
}
|
|
176
174
|
};
|
|
177
175
|
|
|
178
|
-
|
|
176
|
+
/**
|
|
177
|
+
* In flat display a group renders as a plain section header, so a loose page
|
|
178
|
+
* sorted after a group would visually read as that group's last child. Hoist
|
|
179
|
+
* pages above groups at every level (relative order otherwise preserved).
|
|
180
|
+
*/
|
|
181
|
+
const hoistPages = (nodes: MutableNode[]): void => {
|
|
182
|
+
const pages = nodes.filter((node) => node.kind === "page");
|
|
183
|
+
const groups = nodes.filter((node) => node.kind === "group");
|
|
184
|
+
nodes.splice(0, nodes.length, ...pages, ...groups);
|
|
185
|
+
for (const group of groups) {
|
|
186
|
+
hoistPages(group.children);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const toNavNode = (node: MutableNode, display: SidebarDisplay): NavNode => {
|
|
179
191
|
if (node.kind === "page") {
|
|
180
192
|
return {
|
|
181
193
|
badge: node.badge,
|
|
@@ -189,9 +201,9 @@ const toNavNode = (node: MutableNode): NavNode => {
|
|
|
189
201
|
};
|
|
190
202
|
}
|
|
191
203
|
return {
|
|
192
|
-
children: node.children.map(toNavNode),
|
|
204
|
+
children: node.children.map((child) => toNavNode(child, display)),
|
|
193
205
|
collapsed: node.collapsed,
|
|
194
|
-
display
|
|
206
|
+
display,
|
|
195
207
|
icon: node.icon,
|
|
196
208
|
kind: "group",
|
|
197
209
|
label: node.label,
|
|
@@ -204,7 +216,8 @@ const buildFileSystemSidebar = (
|
|
|
204
216
|
pages: PageRecord[],
|
|
205
217
|
folderMeta: Map<string, FolderMeta>,
|
|
206
218
|
sharedMeta: Map<string, FolderMeta>,
|
|
207
|
-
metaPrefix: string
|
|
219
|
+
metaPrefix: string,
|
|
220
|
+
display: SidebarDisplay
|
|
208
221
|
): NavNode[] => {
|
|
209
222
|
const root = createGroup("", "", "", 0);
|
|
210
223
|
|
|
@@ -247,7 +260,10 @@ const buildFileSystemSidebar = (
|
|
|
247
260
|
|
|
248
261
|
applyFolderMeta(root, folderMeta, sharedMeta, metaPrefix);
|
|
249
262
|
sortNodes(root.children);
|
|
250
|
-
|
|
263
|
+
if (display === "flat") {
|
|
264
|
+
hoistPages(root.children);
|
|
265
|
+
}
|
|
266
|
+
return root.children.map((child) => toNavNode(child, display));
|
|
251
267
|
};
|
|
252
268
|
|
|
253
269
|
const normalizeRef = (ref: string): string => {
|
|
@@ -276,7 +292,8 @@ const routeForRef = (
|
|
|
276
292
|
/** Build the sidebar tree from an explicit config spec. */
|
|
277
293
|
const buildConfigSidebar = (
|
|
278
294
|
items: SidebarItemConfig[],
|
|
279
|
-
byRoute: Map<string, PageRecord
|
|
295
|
+
byRoute: Map<string, PageRecord>,
|
|
296
|
+
display: SidebarDisplay
|
|
280
297
|
): NavNode[] => {
|
|
281
298
|
const nodes: NavNode[] = [];
|
|
282
299
|
|
|
@@ -301,10 +318,10 @@ const buildConfigSidebar = (
|
|
|
301
318
|
if (item.items) {
|
|
302
319
|
nodes.push({
|
|
303
320
|
badge: item.badge,
|
|
304
|
-
children: buildConfigSidebar(item.items, byRoute),
|
|
321
|
+
children: buildConfigSidebar(item.items, byRoute, display),
|
|
305
322
|
collapsed: item.collapsed,
|
|
306
323
|
directory: item.directory,
|
|
307
|
-
display: item.display,
|
|
324
|
+
display: item.display ?? display,
|
|
308
325
|
icon: item.icon,
|
|
309
326
|
kind: "group",
|
|
310
327
|
label: item.label,
|
|
@@ -346,8 +363,10 @@ const buildConfigSidebar = (
|
|
|
346
363
|
export const buildNavigation = (
|
|
347
364
|
pages: PageRecord[],
|
|
348
365
|
options: {
|
|
349
|
-
chromeVariants?: NavChromeVariant[];
|
|
350
366
|
folderMeta: Map<string, FolderMeta>;
|
|
367
|
+
/** Global display mode for every sidebar group (default `flat`). */
|
|
368
|
+
display?: SidebarDisplay;
|
|
369
|
+
featured?: FeaturedLink[];
|
|
351
370
|
selectors?: NavSelector[];
|
|
352
371
|
tabs?: NavTab[];
|
|
353
372
|
sidebar?: SidebarItemConfig[];
|
|
@@ -363,9 +382,10 @@ export const buildNavigation = (
|
|
|
363
382
|
sharedFolderMeta?: Map<string, FolderMeta>;
|
|
364
383
|
}
|
|
365
384
|
): Navigation => {
|
|
366
|
-
const
|
|
385
|
+
const featured = options.featured ?? [];
|
|
367
386
|
const selectors = options.selectors ?? [];
|
|
368
387
|
const tabs = options.tabs ?? [];
|
|
388
|
+
const display = options.display ?? "flat";
|
|
369
389
|
const metaPrefix = options.metaPrefix ?? "";
|
|
370
390
|
const sharedFolderMeta = options.sharedFolderMeta ?? new Map();
|
|
371
391
|
const byRoute = new Map(
|
|
@@ -377,21 +397,22 @@ export const buildNavigation = (
|
|
|
377
397
|
|
|
378
398
|
if (options.sidebar) {
|
|
379
399
|
return {
|
|
380
|
-
|
|
400
|
+
featured,
|
|
381
401
|
selectors,
|
|
382
|
-
sidebar: buildConfigSidebar(options.sidebar, byRoute),
|
|
402
|
+
sidebar: buildConfigSidebar(options.sidebar, byRoute, display),
|
|
383
403
|
tabs,
|
|
384
404
|
};
|
|
385
405
|
}
|
|
386
406
|
|
|
387
407
|
return {
|
|
388
|
-
|
|
408
|
+
featured,
|
|
389
409
|
selectors,
|
|
390
410
|
sidebar: buildFileSystemSidebar(
|
|
391
411
|
pages,
|
|
392
412
|
options.folderMeta,
|
|
393
413
|
sharedFolderMeta,
|
|
394
|
-
metaPrefix
|
|
414
|
+
metaPrefix,
|
|
415
|
+
display
|
|
395
416
|
),
|
|
396
417
|
tabs,
|
|
397
418
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { loadConfig } from "./config.ts";
|
|
2
|
-
import type { ConfigBridge } from "./config.ts";
|
|
3
2
|
import { buildContentGraph } from "./graph.ts";
|
|
4
3
|
import { i18nDiagnostics } from "./i18n.ts";
|
|
5
4
|
import {
|
|
@@ -69,8 +68,6 @@ export interface BlumeProject {
|
|
|
69
68
|
diagnostics: Diagnostic[];
|
|
70
69
|
/** The instantiated content sources, for lazy entry reads (search/AI/raw). */
|
|
71
70
|
sources: ContentSource[];
|
|
72
|
-
/** Set when serving a foreign docs config (e.g. Mintlify) without migrating. */
|
|
73
|
-
bridge: ConfigBridge | null;
|
|
74
71
|
}
|
|
75
72
|
|
|
76
73
|
/**
|
|
@@ -97,7 +94,6 @@ export const scanProject = async (
|
|
|
97
94
|
const configResult = await loadConfig(root, {
|
|
98
95
|
devServerUrl: options.devServerUrl,
|
|
99
96
|
});
|
|
100
|
-
const { bridge } = configResult;
|
|
101
97
|
const config = applyConfigOverrides(configResult.config, options.overrides);
|
|
102
98
|
const context = resolveProjectContext(root, config, {
|
|
103
99
|
runtimeDir: options.runtimeDir,
|
|
@@ -179,7 +175,6 @@ export const scanProject = async (
|
|
|
179
175
|
const i18nWarnings = config.i18n ? i18nDiagnostics(pages, config.i18n) : [];
|
|
180
176
|
|
|
181
177
|
return {
|
|
182
|
-
bridge,
|
|
183
178
|
config,
|
|
184
179
|
context,
|
|
185
180
|
diagnostics: [
|