blume 1.0.2 → 1.0.4
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 +37 -0
- package/dist/cli/index.js +496 -295
- package/dist/cli/index.js.map +18 -17
- package/dist/types/core/config-input.d.ts +44 -22
- package/dist/types/core/config.d.ts +3 -3
- package/dist/types/core/data.d.ts +12 -0
- package/dist/types/core/i18n-ui.d.ts +136 -136
- package/dist/types/core/schema.d.ts +502 -384
- package/dist/types/core/types.d.ts +10 -0
- package/dist/types/openapi/references.d.ts +12 -7
- package/docs/advanced/api-reference.mdx +11 -3
- package/docs/advanced/custom-pages.mdx +2 -0
- package/docs/configuration/ai.mdx +6 -4
- package/docs/configuration/index.mdx +6 -8
- package/docs/configuration/seo.mdx +20 -1
- package/docs/content/components.mdx +26 -5
- package/docs/content/navigation.mdx +10 -0
- package/docs/content/syntax.mdx +116 -4
- package/package.json +1 -1
- package/skills/blume-migrate/SKILL.md +170 -0
- package/skills/blume-migrate/assets/oxfmt@0.55.0.patch +20 -0
- package/skills/blume-migrate/references/docusaurus.md +95 -0
- package/skills/blume-migrate/references/fumadocs.md +95 -0
- package/skills/blume-migrate/references/mintlify.md +155 -0
- package/skills/blume-migrate/references/monorepo.md +224 -0
- package/skills/blume-migrate/references/nextra.md +76 -0
- package/skills/blume-migrate/references/starlight.md +116 -0
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +466 -0
- package/src/ai/agent-readability.ts +3 -3
- package/src/ai/mcp/data.ts +2 -2
- package/src/astro/component-slots.ts +3 -2
- package/src/astro/generate.ts +97 -30
- package/src/astro/templates.ts +140 -53
- package/src/blume-modules.d.ts +6 -0
- package/src/components/content/Callout.astro +8 -2
- package/src/components/content/Prompt.astro +25 -13
- package/src/components/layout/Header.astro +19 -10
- package/src/components/layout/Logo.astro +13 -1
- package/src/components/layout/PageFeedback.astro +1 -1
- package/src/components/layout/PageLayout.astro +11 -11
- package/src/components/layout/Pagination.astro +6 -6
- package/src/components/layout/ReferenceLayout.astro +1 -0
- package/src/components/layout/RootLayout.astro +10 -11
- package/src/components/layout/Search.astro +1 -1
- package/src/components/layout/nav-utils.ts +9 -7
- package/src/core/config-input.ts +47 -27
- package/src/core/config.ts +3 -3
- package/src/core/data.ts +9 -1
- package/src/core/navigation.ts +55 -13
- package/src/core/schema.ts +32 -15
- package/src/core/server-features.ts +1 -1
- package/src/core/sources/watch.ts +5 -0
- package/src/core/types.ts +10 -0
- package/src/deploy/adapter-output.ts +11 -1
- package/src/markdown/index.ts +2 -0
- package/src/markdown/language-icon.ts +2 -1
- package/src/markdown/table-wrap.ts +43 -0
- package/src/og/card.ts +39 -12
- package/src/og/index.ts +1 -1
- package/src/og/logo.ts +21 -0
- package/src/openapi/references.ts +19 -16
- package/src/registry/eject.ts +11 -5
- package/src/theme/entry.ts +50 -5
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps each `<table>` in a scroll container. Satteri does not re-descend into a
|
|
3
|
+
* visitor's returned replacement, so the wrapped table is not re-visited.
|
|
4
|
+
*
|
|
5
|
+
* The wrapper carries `tabindex="0"` so a horizontally scrolling table is
|
|
6
|
+
* reachable and scrollable by keyboard, not just pointer (WCAG 2.1.1; axe's
|
|
7
|
+
* `scrollable-region-focusable`). It's added unconditionally — whether a given
|
|
8
|
+
* table overflows isn't known at build time — which costs a tab stop on tables
|
|
9
|
+
* that happen to fit; no ARIA label is set to avoid an untranslated string.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** A minimal hast node (avoids a hast type dependency). */
|
|
13
|
+
interface HastNode {
|
|
14
|
+
children?: HastNode[];
|
|
15
|
+
properties?: Record<string, unknown>;
|
|
16
|
+
tagName?: string;
|
|
17
|
+
type: string;
|
|
18
|
+
value?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A Satteri hast plugin, typed structurally to avoid a Satteri dep. */
|
|
22
|
+
export interface TableWrapPlugin {
|
|
23
|
+
name: string;
|
|
24
|
+
element: {
|
|
25
|
+
filter: string[];
|
|
26
|
+
visit: (node: HastNode) => HastNode;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const tableWrapPlugin = (): TableWrapPlugin => ({
|
|
31
|
+
element: {
|
|
32
|
+
filter: ["table"],
|
|
33
|
+
visit(node) {
|
|
34
|
+
return {
|
|
35
|
+
children: [node],
|
|
36
|
+
properties: { className: ["blume-table-scroll"], tabIndex: 0 },
|
|
37
|
+
tagName: "div",
|
|
38
|
+
type: "element",
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
name: "blume:table-wrap",
|
|
43
|
+
});
|
package/src/og/card.ts
CHANGED
|
@@ -35,6 +35,17 @@ const resolveAccent = (accent: string): string => {
|
|
|
35
35
|
return HEX_COLOR.test(accent) ? accent : "#3b82f6";
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
const resolveColor = (color: string | undefined, fallback: string): string =>
|
|
39
|
+
color && HEX_COLOR.test(color) ? color : fallback;
|
|
40
|
+
|
|
41
|
+
export interface OgCardPalette {
|
|
42
|
+
accent?: string;
|
|
43
|
+
background?: string;
|
|
44
|
+
border?: string;
|
|
45
|
+
foreground?: string;
|
|
46
|
+
muted?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
export interface OgCardOptions {
|
|
39
50
|
/** Large headline — the page title. */
|
|
40
51
|
title: string;
|
|
@@ -45,10 +56,12 @@ export interface OgCardOptions {
|
|
|
45
56
|
/** Muted subtitle under the headline (usually the site description). */
|
|
46
57
|
description?: string;
|
|
47
58
|
/**
|
|
48
|
-
* Inlined SVG markup of the configured logo
|
|
59
|
+
* Inlined SVG markup of the configured logo, painted into
|
|
49
60
|
* the brand lockup. Falls back to an accent mark when absent.
|
|
50
61
|
*/
|
|
51
62
|
logo?: string;
|
|
63
|
+
/** Optional colors for the generated card. */
|
|
64
|
+
palette?: OgCardPalette;
|
|
52
65
|
/** Footer-left repository slug, e.g. `owner/repo`. */
|
|
53
66
|
repo?: string;
|
|
54
67
|
/** Footer-right site host, e.g. `docs.acme.com`. */
|
|
@@ -67,6 +80,17 @@ const MUTED = "#737373";
|
|
|
67
80
|
const FAINT = "#a3a3a3";
|
|
68
81
|
const BORDER = "#e5e5e5";
|
|
69
82
|
|
|
83
|
+
const resolvePalette = (
|
|
84
|
+
options: OgCardOptions
|
|
85
|
+
): Required<OgCardPalette> & { faint: string } => ({
|
|
86
|
+
accent: resolveAccent(options.palette?.accent ?? options.accent ?? "blue"),
|
|
87
|
+
background: resolveColor(options.palette?.background, BG),
|
|
88
|
+
border: resolveColor(options.palette?.border, BORDER),
|
|
89
|
+
faint: resolveColor(options.palette?.muted, FAINT),
|
|
90
|
+
foreground: resolveColor(options.palette?.foreground, FOREGROUND),
|
|
91
|
+
muted: resolveColor(options.palette?.muted, MUTED),
|
|
92
|
+
});
|
|
93
|
+
|
|
70
94
|
/**
|
|
71
95
|
* Truncate to `max` code points with an ellipsis. Slices by code points, not
|
|
72
96
|
* UTF-16 units, so cutting mid-emoji doesn't leave a lone surrogate (a broken
|
|
@@ -103,8 +127,8 @@ const logoAspect = (svg: string): number | null => {
|
|
|
103
127
|
// Render the configured logo as the brand mark. A `currentColor` logo carries
|
|
104
128
|
// no intrinsic color, so it is painted in the foreground to read on the light
|
|
105
129
|
// card, then handed to Takumi as a data URI sized from the SVG's aspect ratio.
|
|
106
|
-
const logoMark = (svg: string): Node => {
|
|
107
|
-
const painted = svg.replaceAll("currentColor",
|
|
130
|
+
const logoMark = (svg: string, foreground: string): Node => {
|
|
131
|
+
const painted = svg.replaceAll("currentColor", foreground);
|
|
108
132
|
const aspect = logoAspect(painted);
|
|
109
133
|
let height = MARK_HEIGHT;
|
|
110
134
|
let width = aspect ? MARK_HEIGHT * aspect : MARK_HEIGHT;
|
|
@@ -151,7 +175,8 @@ const titleSize = (title: string): number => {
|
|
|
151
175
|
|
|
152
176
|
/** Render a 1200x630 Open Graph card to a PNG buffer. */
|
|
153
177
|
export const renderOgImage = (options: OgCardOptions): Promise<Buffer> => {
|
|
154
|
-
const accent
|
|
178
|
+
const { accent, background, border, faint, foreground, muted } =
|
|
179
|
+
resolvePalette(options);
|
|
155
180
|
const brand = options.brand?.trim();
|
|
156
181
|
const logo = options.logo?.trim();
|
|
157
182
|
// Slice by code point, not code unit — `charAt(0)` would split a leading
|
|
@@ -166,14 +191,16 @@ export const renderOgImage = (options: OgCardOptions): Promise<Buffer> => {
|
|
|
166
191
|
// ("Ultracite Ultracite"). Without a logo, the accent tile with the brand
|
|
167
192
|
// initial stands in.
|
|
168
193
|
const header = container({
|
|
169
|
-
children: [
|
|
194
|
+
children: [
|
|
195
|
+
logo ? logoMark(logo, foreground) : initialMark(accent, initial),
|
|
196
|
+
],
|
|
170
197
|
style: { alignItems: "center", display: "flex" },
|
|
171
198
|
});
|
|
172
199
|
|
|
173
200
|
const body = container({
|
|
174
201
|
children: [
|
|
175
202
|
text(truncate(options.title, 64), {
|
|
176
|
-
color:
|
|
203
|
+
color: foreground,
|
|
177
204
|
fontSize: titleSize(options.title),
|
|
178
205
|
fontWeight: 600,
|
|
179
206
|
letterSpacing: "-0.03em",
|
|
@@ -183,7 +210,7 @@ export const renderOgImage = (options: OgCardOptions): Promise<Buffer> => {
|
|
|
183
210
|
}),
|
|
184
211
|
description
|
|
185
212
|
? text(truncate(description, 140), {
|
|
186
|
-
color:
|
|
213
|
+
color: muted,
|
|
187
214
|
fontSize: 30,
|
|
188
215
|
lineHeight: 1.4,
|
|
189
216
|
marginTop: 28,
|
|
@@ -200,15 +227,15 @@ export const renderOgImage = (options: OgCardOptions): Promise<Buffer> => {
|
|
|
200
227
|
? container({
|
|
201
228
|
children: [
|
|
202
229
|
container({
|
|
203
|
-
style: { backgroundColor:
|
|
230
|
+
style: { backgroundColor: border, height: 1, width: "100%" },
|
|
204
231
|
}),
|
|
205
232
|
container({
|
|
206
233
|
children: [
|
|
207
234
|
repo
|
|
208
|
-
? text(repo, { color:
|
|
235
|
+
? text(repo, { color: muted, fontSize: 22 })
|
|
209
236
|
: container({}),
|
|
210
237
|
site
|
|
211
|
-
? text(site, { color:
|
|
238
|
+
? text(site, { color: faint, fontSize: 22 })
|
|
212
239
|
: container({}),
|
|
213
240
|
],
|
|
214
241
|
style: {
|
|
@@ -227,8 +254,8 @@ export const renderOgImage = (options: OgCardOptions): Promise<Buffer> => {
|
|
|
227
254
|
const node = container({
|
|
228
255
|
children: [header, body, footer],
|
|
229
256
|
style: {
|
|
230
|
-
backgroundColor:
|
|
231
|
-
color:
|
|
257
|
+
backgroundColor: background,
|
|
258
|
+
color: foreground,
|
|
232
259
|
display: "flex",
|
|
233
260
|
flexDirection: "column",
|
|
234
261
|
height: HEIGHT,
|
package/src/og/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { renderOgImage } from "./card.ts";
|
|
2
|
-
export type { OgCardOptions } from "./card.ts";
|
|
2
|
+
export type { OgCardOptions, OgCardPalette } from "./card.ts";
|
package/src/og/logo.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { join } from "pathe";
|
|
4
|
+
|
|
5
|
+
import type { BlumeProject } from "../core/project-graph.ts";
|
|
6
|
+
|
|
7
|
+
/** Resolve a configured local SVG for use in generated Open Graph cards. */
|
|
8
|
+
export const resolveOgLogo = (
|
|
9
|
+
project: BlumeProject,
|
|
10
|
+
source: string | undefined
|
|
11
|
+
): string | undefined => {
|
|
12
|
+
if (!source?.toLowerCase().endsWith(".svg")) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const relative = source.replace(/^\//u, "");
|
|
16
|
+
const file = [
|
|
17
|
+
join(project.context.root, "public", relative),
|
|
18
|
+
join(project.context.root, relative),
|
|
19
|
+
].find((path) => existsSync(path));
|
|
20
|
+
return file ? readFileSync(file, "utf-8") : undefined;
|
|
21
|
+
};
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { withBasePath } from "../core/base-path.ts";
|
|
2
2
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
3
3
|
import { trimChar, trimEnd } from "../core/trim.ts";
|
|
4
|
-
import type { NavTab } from "../core/types.ts";
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Pure resolution of the configured API reference blocks into concrete routes,
|
|
8
|
-
* labels, and a renderer choice — no file IO, so the content source, the
|
|
9
|
-
*
|
|
10
|
-
* one source of truth. Kept free of any Astro/template
|
|
11
|
-
* depend on it without a cycle.
|
|
7
|
+
* labels, and a renderer choice — no file IO, so the content source, the
|
|
8
|
+
* nav-target validation, the Scalar page generator, and the `blume:openapi`
|
|
9
|
+
* data module all share one source of truth. Kept free of any Astro/template
|
|
10
|
+
* imports so `core` can depend on it without a cycle.
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
export type ReferenceKind = "openapi" | "asyncapi";
|
|
@@ -155,18 +154,22 @@ export const resolveReferences = (
|
|
|
155
154
|
),
|
|
156
155
|
];
|
|
157
156
|
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Mounted route for every reference, regardless of renderer. References no
|
|
159
|
+
* longer add a header tab automatically — authors point a `navigation.tabs`
|
|
160
|
+
* entry at one of these routes to surface it (and, for Blume-rendered specs, to
|
|
161
|
+
* scope its operations sidebar). These routes are whitelisted as valid nav
|
|
162
|
+
* targets so such a tab doesn't read as a broken link.
|
|
163
|
+
*/
|
|
164
|
+
export const referenceRoutes = (config: ResolvedConfig): string[] =>
|
|
165
|
+
resolveReferences(config).map((ref) =>
|
|
162
166
|
// Blume-rendered operation pages flow through the content pipeline and are
|
|
163
|
-
// mounted under `basePath
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
ref.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}));
|
|
167
|
+
// mounted under `basePath`. Scalar references are a single embedded page
|
|
168
|
+
// injected at the raw `route`, left root-anchored.
|
|
169
|
+
ref.renderer === "blume"
|
|
170
|
+
? withBasePath(config.basePath, ref.route)
|
|
171
|
+
: ref.route
|
|
172
|
+
);
|
|
170
173
|
|
|
171
174
|
/**
|
|
172
175
|
* Accept one resolved reference into the deduped Blume-rendered set, or return
|
package/src/registry/eject.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { discoverIslands } from "../astro/islands.ts";
|
|
|
20
20
|
import { customOgRoutes, discoverPages, routeIsTaken } from "../astro/pages.ts";
|
|
21
21
|
import {
|
|
22
22
|
askEndpointTemplate,
|
|
23
|
+
askComponentTemplate,
|
|
23
24
|
astroConfigTemplate,
|
|
24
25
|
catchAllPageTemplate,
|
|
25
26
|
changelogIndexTemplate,
|
|
@@ -135,8 +136,8 @@ const hostsMcp = (
|
|
|
135
136
|
project: BlumeProject,
|
|
136
137
|
userPages: { pattern: string }[]
|
|
137
138
|
): boolean =>
|
|
138
|
-
project.config.mcp.enabled &&
|
|
139
|
-
!routeIsTaken(userPages, project.graph.pages, project.config.mcp.route);
|
|
139
|
+
project.config.ai.mcp.enabled &&
|
|
140
|
+
!routeIsTaken(userPages, project.graph.pages, project.config.ai.mcp.route);
|
|
140
141
|
|
|
141
142
|
/**
|
|
142
143
|
* The `.well-known` MCP discovery routes, injected as prerendered pages
|
|
@@ -174,7 +175,7 @@ const mcpFiles = async (
|
|
|
174
175
|
if (!hostsMcp(project, userPages)) {
|
|
175
176
|
return [];
|
|
176
177
|
}
|
|
177
|
-
const { route } = project.config.mcp;
|
|
178
|
+
const { route } = project.config.ai.mcp;
|
|
178
179
|
const data = await buildMcpData(project);
|
|
179
180
|
const discoveryInput = {
|
|
180
181
|
base: data.base,
|
|
@@ -356,6 +357,7 @@ export const eject = async (
|
|
|
356
357
|
}[] = [
|
|
357
358
|
{
|
|
358
359
|
content: astroConfigTemplate({
|
|
360
|
+
askPath: "./src/generated/Ask.astro",
|
|
359
361
|
config,
|
|
360
362
|
contentRoutes: project.manifest.routes.map((route) => route.path),
|
|
361
363
|
context: relContext,
|
|
@@ -390,7 +392,6 @@ export const eject = async (
|
|
|
390
392
|
},
|
|
391
393
|
{
|
|
392
394
|
content: catchAllPageTemplate({
|
|
393
|
-
askEnabled,
|
|
394
395
|
exportEpub,
|
|
395
396
|
exportPdf,
|
|
396
397
|
mathEnabled: usesMath,
|
|
@@ -441,6 +442,12 @@ export const eject = async (
|
|
|
441
442
|
path: join(genDir, "app.css"),
|
|
442
443
|
},
|
|
443
444
|
{ content: buildRuntimeData(project), path: join(genDir, "data.json") },
|
|
445
|
+
// The header's Ask trigger behind the `blume:ask` alias. Always written — it
|
|
446
|
+
// renders nothing when Ask is off — so the alias always resolves.
|
|
447
|
+
{
|
|
448
|
+
content: askComponentTemplate(askEnabled),
|
|
449
|
+
path: join(genDir, "Ask.astro"),
|
|
450
|
+
},
|
|
444
451
|
{
|
|
445
452
|
content: `${JSON.stringify(ejectOpenApiData(project))}\n`,
|
|
446
453
|
path: join(genDir, "openapi.json"),
|
|
@@ -475,7 +482,6 @@ export const eject = async (
|
|
|
475
482
|
files.push(
|
|
476
483
|
...(await mcpFiles(project, pages, srcDir, genDir)),
|
|
477
484
|
...changelogFiles(project, pages, srcDir, {
|
|
478
|
-
askEnabled,
|
|
479
485
|
exportEpub,
|
|
480
486
|
exportPdf,
|
|
481
487
|
needsReact,
|
package/src/theme/entry.ts
CHANGED
|
@@ -16,6 +16,15 @@ interface TailwindEntryOptions {
|
|
|
16
16
|
const DARK_VARIANT = `/* Dark mode is driven by data-theme on the <html> element. */
|
|
17
17
|
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));`;
|
|
18
18
|
|
|
19
|
+
/** Keep browser-provided UI in sync with the active theme in both sheets. */
|
|
20
|
+
const COLOR_SCHEME_DEFAULTS = `:root {
|
|
21
|
+
color-scheme: light;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
:root[data-theme="dark"] {
|
|
25
|
+
color-scheme: dark;
|
|
26
|
+
}`;
|
|
27
|
+
|
|
19
28
|
/**
|
|
20
29
|
* The default `--blume-*` design tokens (light + dark), shared by the app
|
|
21
30
|
* sheet and the isolated example-preview sheet so previews inherit the site's
|
|
@@ -141,6 +150,8 @@ ${options.sources.map((source) => `@source "${source}";`).join("\n")}
|
|
|
141
150
|
|
|
142
151
|
${DARK_VARIANT}
|
|
143
152
|
|
|
153
|
+
${COLOR_SCHEME_DEFAULTS}
|
|
154
|
+
|
|
144
155
|
${TOKEN_DEFAULTS}
|
|
145
156
|
|
|
146
157
|
${THEME_MAPPING}
|
|
@@ -386,13 +397,16 @@ blume-diff {
|
|
|
386
397
|
|
|
387
398
|
/* Language icon (simple-icons) sits at the header's left edge; the label shifts
|
|
388
399
|
right to make room. Injected at build time by the language-icon transformer.
|
|
389
|
-
|
|
390
|
-
|
|
400
|
+
The icon is gated on data-language (the header bar), not just data-icon: the
|
|
401
|
+
transformer runs whenever icons are on, but a header-less standalone
|
|
402
|
+
CodeBlock (no title) never gets data-language, so without this gate the
|
|
403
|
+
absolutely-positioned icon would overlap the first code line. Fenced code
|
|
404
|
+
and titled blocks always have a header, so the icon shows there. */
|
|
391
405
|
.blume-lang-icon {
|
|
392
406
|
display: none;
|
|
393
407
|
}
|
|
394
408
|
|
|
395
|
-
.prose > :where(pre[data-icon]) > .blume-lang-icon {
|
|
409
|
+
.prose > :where(pre[data-language][data-icon]) > .blume-lang-icon {
|
|
396
410
|
color: var(--blume-muted-foreground);
|
|
397
411
|
display: block;
|
|
398
412
|
height: 0.875rem;
|
|
@@ -402,7 +416,7 @@ blume-diff {
|
|
|
402
416
|
width: 0.875rem;
|
|
403
417
|
}
|
|
404
418
|
|
|
405
|
-
.prose > :where(pre[data-icon])::before {
|
|
419
|
+
.prose > :where(pre[data-language][data-icon])::before {
|
|
406
420
|
padding-left: 2.5rem;
|
|
407
421
|
}
|
|
408
422
|
|
|
@@ -453,11 +467,40 @@ blume-diff {
|
|
|
453
467
|
font-size: 0.8125rem;
|
|
454
468
|
}
|
|
455
469
|
|
|
470
|
+
/* Scroll wide tables in place; the wrapper is added by the blume:table-wrap hast
|
|
471
|
+
plugin. The border+radius frames the scroll viewport so a clipped wide table
|
|
472
|
+
reads as scrollable rather than cut off, and carries the table's vertical
|
|
473
|
+
rhythm (the inner table's own margin is zeroed so it fills the frame). */
|
|
474
|
+
.blume-table-scroll {
|
|
475
|
+
overflow-x: auto;
|
|
476
|
+
-webkit-overflow-scrolling: touch;
|
|
477
|
+
margin: 1.5rem 0;
|
|
478
|
+
border: 1px solid var(--blume-border);
|
|
479
|
+
border-radius: var(--blume-radius);
|
|
480
|
+
}
|
|
481
|
+
.blume-table-scroll > table {
|
|
482
|
+
margin: 0;
|
|
483
|
+
}
|
|
484
|
+
/* Restore inner padding on every cell. Typography zeroes the first/last cell's
|
|
485
|
+
inline padding so a borderless table aligns to the prose margin; inside the
|
|
486
|
+
framed wrapper that leaves edge text touching the border. The :is() selector
|
|
487
|
+
outweighs Typography's :where()-scoped rules so the outer columns get it too. */
|
|
488
|
+
.blume-table-scroll :is(th, td) {
|
|
489
|
+
padding: 0.5rem 0.75rem;
|
|
490
|
+
}
|
|
491
|
+
/* Keep column labels on one line so a two-word header does not wrap into a
|
|
492
|
+
ragged stack; the table just scrolls a little wider instead. Body cells keep
|
|
493
|
+
wrapping so a wide table stays a scroll away, not an endless horizontal run. */
|
|
494
|
+
.blume-table-scroll th {
|
|
495
|
+
white-space: nowrap;
|
|
496
|
+
}
|
|
497
|
+
|
|
456
498
|
/* GFM renders cells as <td><code> directly, which the descendant form alone
|
|
457
499
|
never matches (a cell is not its own descendant). */
|
|
458
500
|
.prose :where(td, th) > code,
|
|
459
501
|
.prose :where(td, th) :not(pre) > code {
|
|
460
|
-
|
|
502
|
+
overflow-wrap: break-word;
|
|
503
|
+
white-space: normal;
|
|
461
504
|
}
|
|
462
505
|
|
|
463
506
|
blume-tabs pre,
|
|
@@ -717,6 +760,8 @@ ${options.sources.map((source) => `@source "${source}";`).join("\n")}
|
|
|
717
760
|
|
|
718
761
|
${DARK_VARIANT}
|
|
719
762
|
|
|
763
|
+
${COLOR_SCHEME_DEFAULTS}
|
|
764
|
+
|
|
720
765
|
${TOKEN_DEFAULTS}
|
|
721
766
|
|
|
722
767
|
${THEME_MAPPING}
|