blume 1.1.2 → 1.1.3

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.
@@ -1028,6 +1028,49 @@ const lastModifiedConfigSchema = z.union([
1028
1028
  z.strictObject({ type: z.enum(["git", "frontmatter"]).default("git") }),
1029
1029
  ]);
1030
1030
 
1031
+ /**
1032
+ * How the "last updated" stamp and the changelog timeline render their dates —
1033
+ * a curated pass-through to `Intl.DateTimeFormat`, shared by both surfaces so
1034
+ * they read alike. Defaults to `{ dateStyle: "long" }`. Both stamps format in
1035
+ * UTC unless a `timeZone` is given, so a date reads the same regardless of the
1036
+ * build machine's zone. `dateStyle` is a preset that can't be combined with the
1037
+ * individual component fields (`year`, `month`, …), matching `Intl`'s own rule.
1038
+ */
1039
+ const dateFormatConfigSchema = z
1040
+ .strictObject({
1041
+ /** Calendar system (e.g. `japanese`, `buddhist`). */
1042
+ calendar: z.string().optional(),
1043
+ /** Preset date length; mutually exclusive with the component fields. */
1044
+ dateStyle: z.enum(["full", "long", "medium", "short"]).optional(),
1045
+ /** Day representation. */
1046
+ day: z.enum(["numeric", "2-digit"]).optional(),
1047
+ /** Era representation (e.g. the Japanese imperial era). */
1048
+ era: z.enum(["long", "short", "narrow"]).optional(),
1049
+ /** Month representation. */
1050
+ month: z.enum(["numeric", "2-digit", "long", "short", "narrow"]).optional(),
1051
+ /** Numbering system (e.g. `latn`, `arab`). */
1052
+ numberingSystem: z.string().optional(),
1053
+ /** IANA time zone (e.g. `Asia/Tokyo`). Defaults to `UTC`. */
1054
+ timeZone: z.string().optional(),
1055
+ /** Weekday representation. */
1056
+ weekday: z.enum(["long", "short", "narrow"]).optional(),
1057
+ /** Year representation. */
1058
+ year: z.enum(["numeric", "2-digit"]).optional(),
1059
+ })
1060
+ .refine(
1061
+ (value) =>
1062
+ value.dateStyle === undefined ||
1063
+ (value.weekday === undefined &&
1064
+ value.era === undefined &&
1065
+ value.year === undefined &&
1066
+ value.month === undefined &&
1067
+ value.day === undefined),
1068
+ {
1069
+ message:
1070
+ "dateFormat.dateStyle can't be combined with weekday/era/year/month/day; use one or the other.",
1071
+ }
1072
+ );
1073
+
1031
1074
  /** Code-block rendering options (`markdown.code`). */
1032
1075
  const codeConfigSchema = z.strictObject({
1033
1076
  /**
@@ -1085,6 +1128,16 @@ const openapiSourceSchema = z.strictObject({
1085
1128
 
1086
1129
  export type OpenApiSource = z.infer<typeof openapiSourceSchema>;
1087
1130
 
1131
+ /**
1132
+ * Arbitrary Scalar API-reference options forwarded verbatim to the generated
1133
+ * `<ScalarComponent>` (Scalar renderer only). A passthrough map — Blume doesn't
1134
+ * mirror Scalar's full config surface — so keys like `localization`, `agent`,
1135
+ * `hideTestRequestButton`, or `orderSchemaPropertiesBy` all flow through. These
1136
+ * take precedence over Blume's own derived config (spec, theme), so this is a
1137
+ * full escape hatch; the dedicated `theme` field is the ergonomic shorthand.
1138
+ */
1139
+ const scalarConfigSchema = z.record(z.string(), z.unknown()).optional();
1140
+
1088
1141
  /**
1089
1142
  * OpenAPI reference. By default (`renderer: "blume"`) Blume parses the spec with
1090
1143
  * Scalar's parser and renders its own UI: one real page per operation, grouped
@@ -1102,6 +1155,8 @@ const openapiConfigSchema = z.strictObject({
1102
1155
  renderer: z.enum(["blume", "scalar"]).default("blume"),
1103
1156
  /** Where the reference mounts. */
1104
1157
  route: z.string().default("/reference"),
1158
+ /** Extra Scalar config forwarded to `<ScalarComponent>` (Scalar renderer only). */
1159
+ scalar: scalarConfigSchema,
1105
1160
  /** One or more specs; each renders on its own route by default. */
1106
1161
  sources: z.array(openapiSourceSchema).default([]),
1107
1162
  /** Shorthand for a single source: `sources: [{ spec }]`. */
@@ -1117,6 +1172,8 @@ const openapiConfigSchema = z.strictObject({
1117
1172
  const asyncapiConfigSchema = z.strictObject({
1118
1173
  enabled: z.boolean().default(false),
1119
1174
  route: z.string().default("/events"),
1175
+ /** Extra Scalar config forwarded to `<ScalarComponent>`. */
1176
+ scalar: scalarConfigSchema,
1120
1177
  sources: z.array(openapiSourceSchema).default([]),
1121
1178
  spec: z.string().optional(),
1122
1179
  theme: z.string().optional(),
@@ -1205,6 +1262,11 @@ export const blumeConfigSchema = z.strictObject({
1205
1262
  .optional()
1206
1263
  .transform((value) => normalizeBasePath(value)),
1207
1264
  content: contentConfigSchema.default({}),
1265
+ /**
1266
+ * Date presentation for the "last updated" stamp and the changelog timeline.
1267
+ * Pass-through `Intl.DateTimeFormat` options; defaults to `{ dateStyle: "long" }`.
1268
+ */
1269
+ dateFormat: dateFormatConfigSchema.default({ dateStyle: "long" }),
1208
1270
  deployment: deploymentConfigSchema.default({}),
1209
1271
  description: z.string().optional(),
1210
1272
  /**
@@ -1237,6 +1299,8 @@ export const blumeConfigSchema = z.strictObject({
1237
1299
 
1238
1300
  /** Resolved config: every field present after defaults are applied. */
1239
1301
  export type ResolvedConfig = z.infer<typeof blumeConfigSchema>;
1302
+ /** Resolved `dateFormat`: the `Intl.DateTimeFormat` options both date stamps share. */
1303
+ export type ResolvedDateFormat = z.infer<typeof dateFormatConfigSchema>;
1240
1304
  /** Resolved `frontmatter.extend`: custom key → user-supplied schema. */
1241
1305
  export type FrontmatterExtend = Record<string, StandardSchema>;
1242
1306
  /** Resolved i18n block (present only when the project opts into i18n). */
package/src/core/types.ts CHANGED
@@ -31,6 +31,22 @@ export interface Diagnostic {
31
31
  docsUrl?: string;
32
32
  }
33
33
 
34
+ /** One discovered `examples/` file reduced to what Markdown downleveling needs. */
35
+ export interface ExampleMarkdownEntry {
36
+ /** Shiki language for the fenced block — the file's extension. */
37
+ lang: string;
38
+ /** Raw example source, shown verbatim in the agent-facing code fence. */
39
+ source: string;
40
+ }
41
+
42
+ /**
43
+ * Discovered examples keyed by their `<Component path>` (the file's location
44
+ * under `examples/`, sans extension). Lets the agent-facing Markdown downlevel
45
+ * `<Component path="…" />` to the example's source, since the live preview
46
+ * can't survive the trip to plain Markdown.
47
+ */
48
+ export type ExampleLookup = Record<string, ExampleMarkdownEntry>;
49
+
34
50
  /** A heading extracted from page content, used for the TOC and search. */
35
51
  export interface Heading {
36
52
  depth: number;
@@ -42,6 +42,11 @@ export interface ReferenceSource {
42
42
  spec: string;
43
43
  /** Per-block Scalar theme name override, if any (Scalar renderer only). */
44
44
  theme?: string;
45
+ /**
46
+ * Arbitrary Scalar config forwarded to `<ScalarComponent>` (Scalar renderer
47
+ * only). Takes precedence over Blume's derived spec/theme config.
48
+ */
49
+ scalar?: Record<string, unknown>;
45
50
  /** Display options carried through to the Blume renderer. */
46
51
  display: ReferenceDisplay;
47
52
  /**
@@ -117,6 +122,7 @@ const referencesFor = (
117
122
  label,
118
123
  renderer,
119
124
  route,
125
+ scalar: block.scalar,
120
126
  slug: routeSlug(route),
121
127
  spec: source.spec,
122
128
  theme: block.theme,
@@ -161,6 +161,10 @@ export const buildReferenceFiles = async (options: {
161
161
  configuration: {
162
162
  ...spec.config,
163
163
  ...themeConfiguration(config, ref.theme),
164
+ // Author-supplied Scalar options win outright — a full escape hatch
165
+ // over Blume's derived spec/theme config (localization, agent,
166
+ // hideTestRequestButton, orderSchemaPropertiesBy, and the rest).
167
+ ...ref.scalar,
164
168
  },
165
169
  dataImport: `${"../".repeat(depth + 1)}generated/data.json`,
166
170
  route: ref.route,
@@ -9,7 +9,7 @@ import { buildRawMarkdown } from "../ai/markdown.ts";
9
9
  import { buildMcpData } from "../ai/mcp/data.ts";
10
10
  import { buildMcpDiscovery, buildMcpServerCard } from "../ai/mcp/discovery.ts";
11
11
  import { planComponentSlots } from "../astro/component-slots.ts";
12
- import { discoverExamples } from "../astro/examples.ts";
12
+ import { discoverExamples, exampleMarkdownLookup } from "../astro/examples.ts";
13
13
  import {
14
14
  buildRuntimeData,
15
15
  collectStaged,
@@ -292,6 +292,11 @@ export const eject = async (
292
292
  const exportPdf = config.export.pdf;
293
293
  const exportEpub = config.export.epub;
294
294
 
295
+ // Discover examples first and expose them on the project, so the agent-facing
296
+ // Markdown built below (raw `.md`, MCP) downlevels `<Component>` to its source.
297
+ const examples = await discoverExamples(root, config.examples.source);
298
+ project.examples = exampleMarkdownLookup(examples.examples);
299
+
295
300
  const [
296
301
  pages,
297
302
  needsReactRaw,
@@ -300,7 +305,6 @@ export const eject = async (
300
305
  userExamplesCss,
301
306
  rawMarkdown,
302
307
  islands,
303
- examples,
304
308
  ] = await Promise.all([
305
309
  context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
306
310
  detectNeedsReact(root),
@@ -311,7 +315,6 @@ export const eject = async (
311
315
  readExamplesCss(root, config.examples.css),
312
316
  buildRawMarkdown(project),
313
317
  discoverIslands(root),
314
- discoverExamples(root, config.examples.source),
315
318
  ]);
316
319
  // Island/example frameworks drive which Astro renderers the ejected config
317
320
  // wires in; React also switches on for project `.tsx`/`.jsx` and Ask AI.
@@ -247,6 +247,13 @@ ${THEME_MAPPING}
247
247
  letter-spacing: 0;
248
248
  }
249
249
 
250
+ /* A heading can carry one long unbreakable token — an OpenAPI operation's title
251
+ is \`METHOD /very/long/{path}\` when the spec sets no summary — which would run
252
+ off the content column. Break it across lines instead of overflowing. */
253
+ .prose :where(h1, h2, h3, h4, h5, h6) {
254
+ overflow-wrap: break-word;
255
+ }
256
+
250
257
  .prose :where(h1) {
251
258
  font-size: 3rem;
252
259
  line-height: 1.1;
@@ -43,6 +43,16 @@ const OVERRIDES = `
43
43
  padding-right: 1.25rem;
44
44
  }
45
45
 
46
+ /* Twoslash blocks keep \`overflow: visible\` so popups can escape, which means
47
+ long lines can't scroll — they'd push past the viewport on narrow screens.
48
+ Wrap them on mobile instead; hover popups still position against the token. */
49
+ @media (max-width: 640px) {
50
+ .prose pre.twoslash code {
51
+ white-space: pre-wrap;
52
+ overflow-wrap: anywhere;
53
+ }
54
+ }
55
+
46
56
  /* The rich renderer renders each popup's type signature as a nested Shiki
47
57
  pre. Strip the code-block chrome (border, radius, padding, background) so it
48
58
  sits flush inside the popup, which owns the frame. */