blume 1.5.2 → 1.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.
Files changed (194) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/dist/cli/index.js +3639 -1377
  3. package/dist/cli/index.js.map +103 -91
  4. package/dist/types/ai/component-markdown.d.ts +79 -0
  5. package/dist/types/components/layout/nav-utils.d.ts +60 -0
  6. package/dist/types/core/base-path.d.ts +9 -0
  7. package/dist/types/core/config-input.d.ts +206 -4
  8. package/dist/types/core/config.d.ts +6 -4
  9. package/dist/types/core/data.d.ts +23 -1
  10. package/dist/types/core/github.d.ts +35 -0
  11. package/dist/types/core/i18n-ui.d.ts +8 -0
  12. package/dist/types/core/navigation.d.ts +69 -0
  13. package/dist/types/core/schema.d.ts +117 -1
  14. package/dist/types/core/sources/types.d.ts +31 -6
  15. package/dist/types/core/types.d.ts +23 -2
  16. package/dist/types/markdown/features.d.ts +21 -0
  17. package/dist/types/openapi/references.d.ts +21 -1
  18. package/dist/types/seo/jsonld.d.ts +105 -0
  19. package/dist/types/theme/fonts.d.ts +34 -4
  20. package/docs/_snippets/include-demo.mdx +7 -0
  21. package/docs/advanced/api-reference.mdx +3 -3
  22. package/docs/advanced/custom-pages.mdx +1 -1
  23. package/docs/advanced/graphql.mdx +84 -0
  24. package/docs/advanced/meta.ts +8 -1
  25. package/docs/configuration/ai.mdx +21 -3
  26. package/docs/configuration/index.mdx +24 -0
  27. package/docs/configuration/search.mdx +13 -1
  28. package/docs/configuration/seo.mdx +27 -0
  29. package/docs/configuration/theming.mdx +17 -0
  30. package/docs/content/components.mdx +7 -0
  31. package/docs/content/includes.mdx +68 -0
  32. package/docs/content/meta.ts +1 -0
  33. package/docs/content/navigation.mdx +25 -0
  34. package/docs/content/sources.mdx +42 -1
  35. package/docs/content/syntax.mdx +69 -1
  36. package/docs/content/versioning.mdx +15 -9
  37. package/docs/reference/cli.mdx +2 -1
  38. package/package.json +23 -14
  39. package/skills/blume-migrate/SKILL.md +16 -7
  40. package/skills/blume-migrate/references/docusaurus.md +5 -3
  41. package/skills/blume-migrate/references/fumadocs.md +10 -2
  42. package/skills/blume-migrate/references/mintlify.md +3 -2
  43. package/skills/blume-migrate/references/nextra.md +2 -2
  44. package/skills/blume-migrate/references/starlight.md +1 -1
  45. package/src/ai/agent-readability.ts +2 -1
  46. package/src/ai/ask-data.ts +2 -1
  47. package/src/ai/component-markdown.ts +199 -36
  48. package/src/ai/llms.ts +93 -6
  49. package/src/ai/markdown.ts +2 -2
  50. package/src/ai/mcp/discovery.ts +10 -2
  51. package/src/ai/mcp/server.ts +74 -2
  52. package/src/astro/generate.ts +183 -116
  53. package/src/astro/include-hmr.ts +81 -0
  54. package/src/astro/include-refresh.ts +0 -0
  55. package/src/astro/index.ts +3 -5
  56. package/src/astro/templates.ts +125 -76
  57. package/src/cli/commands/build.ts +84 -15
  58. package/src/cli/init/questions.ts +1 -0
  59. package/src/cli/init/scaffold.ts +27 -4
  60. package/src/components/colors.ts +142 -0
  61. package/src/components/content/Badge.astro +5 -12
  62. package/src/components/content/Callout.astro +19 -36
  63. package/src/components/content/Card.astro +15 -21
  64. package/src/components/content/Component.astro +10 -1
  65. package/src/components/content/GithubInfo.astro +28 -9
  66. package/src/components/content/Tabs.astro +27 -5
  67. package/src/components/content/github-info.ts +20 -5
  68. package/src/components/dropdown-dismiss.ts +122 -0
  69. package/src/components/layout/Fonts.astro +15 -8
  70. package/src/components/layout/Header.astro +44 -0
  71. package/src/components/layout/LanguageSwitcher.astro +9 -1
  72. package/src/components/layout/NavSelector.astro +12 -3
  73. package/src/components/layout/NavTree.astro +6 -18
  74. package/src/components/layout/PageActions.astro +29 -8
  75. package/src/components/layout/PageLayout.astro +10 -1
  76. package/src/components/layout/ReferenceLayout.astro +6 -1
  77. package/src/components/layout/RootLayout.astro +46 -15
  78. package/src/components/layout/Search.astro +36 -4
  79. package/src/components/layout/TableOfContents.astro +8 -2
  80. package/src/components/layout/head-scripts.ts +53 -1
  81. package/src/components/openapi/ApiOverview.astro +13 -3
  82. package/src/components/openapi/AsyncApiOperation.astro +7 -14
  83. package/src/components/openapi/GraphqlChip.astro +33 -0
  84. package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
  85. package/src/components/openapi/GraphqlOperation.astro +186 -0
  86. package/src/components/openapi/GraphqlType.astro +154 -0
  87. package/src/components/openapi/MethodBadge.astro +3 -14
  88. package/src/components/openapi/Operation.astro +12 -5
  89. package/src/components/openapi/OperationPanel.astro +43 -0
  90. package/src/components/openapi/RequestPanel.astro +5 -10
  91. package/src/components/openapi/Responses.astro +1 -16
  92. package/src/components/openapi/graphql-helpers.ts +466 -0
  93. package/src/components/openapi/playground-client.ts +15 -0
  94. package/src/components/openapi/sample-panels.ts +45 -0
  95. package/src/components/openapi/snippets.ts +13 -35
  96. package/src/core/base-path.ts +11 -0
  97. package/src/core/config-input.ts +209 -2
  98. package/src/core/config.ts +6 -4
  99. package/src/core/content-assets.ts +15 -4
  100. package/src/core/data.ts +18 -2
  101. package/src/core/diagnostics.ts +8 -0
  102. package/src/core/frontmatter.ts +20 -8
  103. package/src/core/github.ts +71 -0
  104. package/src/core/graph.ts +22 -8
  105. package/src/core/heading-markers.ts +96 -0
  106. package/src/core/i18n-ui.ts +11 -0
  107. package/src/core/includes.ts +632 -0
  108. package/src/core/last-modified.ts +36 -11
  109. package/src/core/links.ts +79 -13
  110. package/src/core/meta.ts +2 -1
  111. package/src/core/nav-diagnostics.ts +11 -2
  112. package/src/core/navigation.ts +27 -6
  113. package/src/core/project-graph.ts +61 -9
  114. package/src/core/schema.ts +226 -35
  115. package/src/core/server-features.ts +5 -9
  116. package/src/core/sources/github-releases.ts +2 -2
  117. package/src/core/sources/normalize.ts +502 -115
  118. package/src/core/sources/notion.ts +43 -8
  119. package/src/core/sources/obsidian.ts +1038 -0
  120. package/src/core/sources/read.ts +36 -1
  121. package/src/core/sources/resolve.ts +34 -1
  122. package/src/core/sources/types.ts +28 -6
  123. package/src/core/sources/watch.ts +12 -8
  124. package/src/core/tsconfig-aliases.ts +48 -35
  125. package/src/core/types.ts +25 -2
  126. package/src/core/ui-packs/ar.ts +1 -0
  127. package/src/core/ui-packs/bg.ts +2 -0
  128. package/src/core/ui-packs/bn.ts +1 -0
  129. package/src/core/ui-packs/ca.ts +2 -0
  130. package/src/core/ui-packs/cs.ts +1 -0
  131. package/src/core/ui-packs/da.ts +1 -0
  132. package/src/core/ui-packs/de.ts +2 -0
  133. package/src/core/ui-packs/el.ts +2 -0
  134. package/src/core/ui-packs/es.ts +2 -0
  135. package/src/core/ui-packs/fa.ts +1 -0
  136. package/src/core/ui-packs/fi.ts +1 -0
  137. package/src/core/ui-packs/fr.ts +2 -0
  138. package/src/core/ui-packs/he.ts +1 -0
  139. package/src/core/ui-packs/hi.ts +1 -0
  140. package/src/core/ui-packs/hr.ts +2 -0
  141. package/src/core/ui-packs/hu.ts +2 -0
  142. package/src/core/ui-packs/id.ts +2 -0
  143. package/src/core/ui-packs/it.ts +1 -0
  144. package/src/core/ui-packs/ja.ts +2 -0
  145. package/src/core/ui-packs/ko.ts +2 -0
  146. package/src/core/ui-packs/nl.ts +2 -0
  147. package/src/core/ui-packs/no.ts +2 -0
  148. package/src/core/ui-packs/pl.ts +2 -0
  149. package/src/core/ui-packs/pt-br.ts +2 -0
  150. package/src/core/ui-packs/pt.ts +2 -0
  151. package/src/core/ui-packs/ro.ts +2 -0
  152. package/src/core/ui-packs/ru.ts +2 -0
  153. package/src/core/ui-packs/sk.ts +1 -0
  154. package/src/core/ui-packs/sr.ts +1 -0
  155. package/src/core/ui-packs/sv.ts +2 -0
  156. package/src/core/ui-packs/th.ts +1 -0
  157. package/src/core/ui-packs/tr.ts +2 -0
  158. package/src/core/ui-packs/uk.ts +2 -0
  159. package/src/core/ui-packs/vi.ts +1 -0
  160. package/src/core/ui-packs/zh-tw.ts +1 -0
  161. package/src/core/ui-packs/zh.ts +1 -0
  162. package/src/core/version-cut.ts +21 -3
  163. package/src/core/yaml.ts +26 -0
  164. package/src/deploy/function-bundle.ts +251 -0
  165. package/src/eval/schema.ts +3 -1
  166. package/src/markdown/code-title.ts +22 -16
  167. package/src/markdown/features.ts +21 -0
  168. package/src/markdown/fence-meta.ts +50 -0
  169. package/src/markdown/heading-anchors.ts +198 -37
  170. package/src/markdown/include.ts +247 -0
  171. package/src/markdown/index.ts +43 -34
  172. package/src/markdown/language-icon.ts +2 -2
  173. package/src/markdown/mdast.ts +7 -3
  174. package/src/markdown/ts2js.ts +264 -0
  175. package/src/openapi/asyncapi.ts +4 -1
  176. package/src/openapi/graphql-build.ts +293 -0
  177. package/src/openapi/graphql.ts +212 -0
  178. package/src/openapi/model.ts +38 -5
  179. package/src/openapi/parse.ts +34 -0
  180. package/src/openapi/proxy.ts +30 -5
  181. package/src/openapi/references.ts +89 -13
  182. package/src/openapi/render-mdx.ts +48 -8
  183. package/src/openapi/scalar.ts +5 -12
  184. package/src/openapi/source.ts +91 -23
  185. package/src/registry/eject.ts +11 -0
  186. package/src/search/documents.ts +229 -37
  187. package/src/search/orama-index.ts +9 -5
  188. package/src/seo/jsonld.ts +293 -51
  189. package/src/theme/code-block-padding.ts +16 -0
  190. package/src/theme/entry.ts +65 -11
  191. package/src/theme/fonts.ts +189 -16
  192. package/src/translate/prompts.ts +2 -0
  193. package/src/translate/run.ts +7 -0
  194. package/src/translate/work-list.ts +0 -0
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Color classes shared by every component that draws a label over a tint of
3
+ * its own hue — method badges, response status chips, `<Badge>`, the sidebar's
4
+ * method badges — and by the typed callout/card icons. One table so the pairs
5
+ * can't drift between components.
6
+ *
7
+ * Contrast rule (light mode): text over a 15% tint of its hue must clear 4.5:1,
8
+ * the WCAG AA bar for text this size. `-700` does for most hues; green and
9
+ * orange need `-800`, the way yellow already does over its 20% tint. Dark mode
10
+ * clears the bar at `-300` throughout. Check any hue added here.
11
+ */
12
+
13
+ export type Hue =
14
+ | "blue"
15
+ | "green"
16
+ | "orange"
17
+ | "purple"
18
+ | "red"
19
+ | "teal"
20
+ | "violet"
21
+ | "yellow";
22
+
23
+ /** Label over a translucent tint of the same hue. */
24
+ export const TINT = {
25
+ blue: "bg-blue-500/15 text-blue-700 dark:text-blue-300",
26
+ green: "bg-green-500/15 text-green-800 dark:text-green-300",
27
+ orange: "bg-orange-500/15 text-orange-800 dark:text-orange-300",
28
+ purple: "bg-purple-500/15 text-purple-700 dark:text-purple-300",
29
+ red: "bg-red-500/15 text-red-700 dark:text-red-300",
30
+ teal: "bg-teal-500/15 text-teal-700 dark:text-teal-300",
31
+ violet: "bg-violet-500/15 text-violet-700 dark:text-violet-300",
32
+ yellow: "bg-yellow-500/20 text-yellow-800 dark:text-yellow-300",
33
+ } satisfies Record<Hue, string>;
34
+
35
+ /**
36
+ * Label with a translucent border of the same hue and no fill. The label
37
+ * inherits whatever surface it sits on (a callout, a card, the muted panel), so
38
+ * it is held to the same values as `TINT`: green at `-700` is 4.47:1 on the
39
+ * default muted surface.
40
+ */
41
+ export const STROKE = {
42
+ blue: "border-blue-500/40 text-blue-700 dark:text-blue-300",
43
+ green: "border-green-500/40 text-green-800 dark:text-green-300",
44
+ orange: "border-orange-500/40 text-orange-800 dark:text-orange-300",
45
+ purple: "border-purple-500/40 text-purple-700 dark:text-purple-300",
46
+ red: "border-red-500/40 text-red-700 dark:text-red-300",
47
+ teal: "border-teal-500/40 text-teal-700 dark:text-teal-300",
48
+ violet: "border-violet-500/40 text-violet-700 dark:text-violet-300",
49
+ yellow: "border-yellow-500/40 text-yellow-800 dark:text-yellow-300",
50
+ } satisfies Record<Hue, string>;
51
+
52
+ /** The neutral fallback every table falls through to. */
53
+ export const MUTED = "bg-muted text-muted-foreground";
54
+
55
+ /**
56
+ * HTTP methods, AsyncAPI actions, and GraphQL root-field kinds, as shown on an
57
+ * operation's badge and in a reference's sidebar. Type-page kinds fall through
58
+ * to the muted default so operation badges stay the loud ones.
59
+ */
60
+ export const METHOD_COLORS = {
61
+ DELETE: TINT.red,
62
+ GET: TINT.green,
63
+ HEAD: MUTED,
64
+ MUTATION: TINT.blue,
65
+ OPTIONS: MUTED,
66
+ PATCH: TINT.yellow,
67
+ POST: TINT.blue,
68
+ PUT: TINT.orange,
69
+ QUERY: TINT.green,
70
+ RECEIVE: TINT.teal,
71
+ SEND: TINT.violet,
72
+ SUBSCRIPTION: TINT.violet,
73
+ } satisfies Record<string, string>;
74
+
75
+ const isMethod = (key: string): key is keyof typeof METHOD_COLORS =>
76
+ Object.hasOwn(METHOD_COLORS, key);
77
+
78
+ export const methodColor = (method: string): string => {
79
+ const key = method.toUpperCase();
80
+ return isMethod(key) ? METHOD_COLORS[key] : MUTED;
81
+ };
82
+
83
+ /** Response status chips, by the status code's class. */
84
+ export const statusColor = (status: string): string => {
85
+ if (status.startsWith("2")) {
86
+ return TINT.green;
87
+ }
88
+ if (status.startsWith("3")) {
89
+ return TINT.blue;
90
+ }
91
+ if (status.startsWith("4")) {
92
+ return TINT.orange;
93
+ }
94
+ if (status.startsWith("5")) {
95
+ return TINT.red;
96
+ }
97
+ return MUTED;
98
+ };
99
+
100
+ /**
101
+ * The small `deprecated` label beside an operation's path or field name. It
102
+ * draws on the page background, where orange needs `-700` for 4.5:1.
103
+ */
104
+ export const DEPRECATED_LABEL_CLASS =
105
+ "font-medium text-[0.625rem] text-orange-700 uppercase tracking-wide dark:text-orange-400";
106
+
107
+ /** The typed admonitions `<Callout>` and `<Card>` render; `check` is an alias of `success`. */
108
+ export type AdmonitionType =
109
+ | "info"
110
+ | "note"
111
+ | "tip"
112
+ | "success"
113
+ | "warning"
114
+ | "danger";
115
+
116
+ export const admonitionType = (
117
+ type: AdmonitionType | "check"
118
+ ): AdmonitionType => (type === "check" ? "success" : type);
119
+
120
+ export const ADMONITION_ICON = {
121
+ danger: "circle-x",
122
+ info: "info",
123
+ note: "info",
124
+ success: "circle-check",
125
+ tip: "lightbulb",
126
+ warning: "triangle-alert",
127
+ } satisfies Record<AdmonitionType, string>;
128
+
129
+ /**
130
+ * An admonition's icon names its type, so it is meaningful UI held to the 3:1
131
+ * non-text bar against the tint behind it: green and amber need `-700` over
132
+ * their 10% tint, and the note icon is full-strength muted-foreground (at 70%
133
+ * it fell under the bar on the muted surface).
134
+ */
135
+ export const ADMONITION_ICON_CLASS = {
136
+ danger: "text-red-600 dark:text-red-400",
137
+ info: "text-blue-600 dark:text-blue-400",
138
+ note: "text-muted-foreground",
139
+ success: "text-green-700 dark:text-green-400",
140
+ tip: "text-accent",
141
+ warning: "text-amber-700 dark:text-amber-400",
142
+ } satisfies Record<AdmonitionType, string>;
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { STROKE, TINT } from "../colors.ts";
2
3
  import Icon from "../Icon.astro";
3
4
 
4
5
  type BadgeVariant = "default" | "accent" | "success" | "warning" | "danger";
@@ -44,34 +45,26 @@ const colorName =
44
45
  }[variant];
45
46
  const customColor = colorName.startsWith("#") ? colorName : null;
46
47
 
48
+ // The hue rows come from the shared table (see colors.ts for the contrast
49
+ // rule); the neutral and surface rows are Badge's own.
47
50
  const filledColorClass: Record<string, string> = {
48
- blue: "bg-blue-500/15 text-blue-700 dark:text-blue-300",
51
+ ...TINT,
49
52
  gray: "bg-muted text-muted-foreground",
50
- green: "bg-green-500/15 text-green-700 dark:text-green-300",
51
- orange: "bg-orange-500/15 text-orange-700 dark:text-orange-300",
52
- purple: "bg-purple-500/15 text-purple-700 dark:text-purple-300",
53
- red: "bg-red-500/15 text-red-700 dark:text-red-300",
54
53
  surface: "bg-background text-muted-foreground ring-1 ring-border",
55
54
  "surface-destructive":
56
55
  "bg-background text-red-700 ring-1 ring-red-500/30 dark:text-red-300",
57
56
  white: "bg-white text-muted-foreground ring-1 ring-border dark:bg-white/10",
58
57
  "white-destructive":
59
58
  "bg-white text-red-700 ring-1 ring-red-500/30 dark:bg-white/10 dark:text-red-300",
60
- yellow: "bg-yellow-500/20 text-yellow-800 dark:text-yellow-300",
61
59
  };
62
60
 
63
61
  const strokeColorClass: Record<string, string> = {
64
- blue: "border-blue-500/40 text-blue-700 dark:text-blue-300",
62
+ ...STROKE,
65
63
  gray: "border-border text-muted-foreground",
66
- green: "border-green-500/40 text-green-700 dark:text-green-300",
67
- orange: "border-orange-500/40 text-orange-700 dark:text-orange-300",
68
- purple: "border-purple-500/40 text-purple-700 dark:text-purple-300",
69
- red: "border-red-500/40 text-red-700 dark:text-red-300",
70
64
  surface: "border-border text-muted-foreground",
71
65
  "surface-destructive": "border-red-500/40 text-red-700 dark:text-red-300",
72
66
  white: "border-border text-muted-foreground",
73
67
  "white-destructive": "border-red-500/40 text-red-700 dark:text-red-300",
74
- yellow: "border-yellow-500/40 text-yellow-800 dark:text-yellow-300",
75
68
  };
76
69
 
77
70
  const sizeClass: Record<BadgeSize, string> = {
@@ -1,64 +1,47 @@
1
1
  ---
2
+ import {
3
+ ADMONITION_ICON,
4
+ ADMONITION_ICON_CLASS,
5
+ type AdmonitionType,
6
+ admonitionType,
7
+ } from "../colors.ts";
2
8
  import Icon from "../Icon.astro";
3
9
 
4
- type CalloutType =
5
- | "info"
6
- | "note"
7
- | "tip"
8
- | "success"
9
- | "check"
10
- | "warning"
11
- | "danger";
12
-
13
10
  interface Props {
14
- type?: CalloutType;
11
+ type?: AdmonitionType | "check";
15
12
  title?: string;
16
13
  icon?: unknown;
17
14
  color?: string;
18
15
  }
19
16
 
20
17
  const { color, icon, type = "info", title } = Astro.props;
18
+ // `check` is an alias of `success`; fold it once so the tables need one row.
19
+ const kind = admonitionType(type);
21
20
 
22
- const iconByType: Record<CalloutType, string> = {
23
- check: "circle-check",
24
- danger: "circle-x",
25
- info: "info",
26
- note: "info",
27
- success: "circle-check",
28
- tip: "lightbulb",
29
- warning: "triangle-alert",
30
- };
31
-
32
- const variantClass: Record<CalloutType, string> = {
33
- check: "border-green-500/25 bg-green-500/10",
21
+ const variantClass: Record<AdmonitionType, string> = {
34
22
  danger: "border-red-500/25 bg-red-500/10",
35
23
  info: "border-blue-500/25 bg-blue-500/10",
36
24
  note: "border-border bg-muted",
37
25
  success: "border-green-500/25 bg-green-500/10",
38
- tip: "border-accent/25 bg-accent/10",
26
+ // 6% rather than the 10% the other variants use: this tint comes from the
27
+ // user-configurable accent, whose default is near black, and muted body
28
+ // text on a 10% black tint sits at 4.25:1. 6% clears WCAG AA (4.5:1)
29
+ // against any accent — 4.62:1 worst case on pure black.
30
+ tip: "border-accent/25 bg-accent/6",
39
31
  warning: "border-amber-500/25 bg-amber-500/10",
40
32
  };
41
-
42
- const iconClass: Record<CalloutType, string> = {
43
- check: "text-green-600 dark:text-green-400",
44
- danger: "text-red-600 dark:text-red-400",
45
- info: "text-blue-600 dark:text-blue-400",
46
- note: "text-muted-foreground/70",
47
- success: "text-green-600 dark:text-green-400",
48
- tip: "text-accent",
49
- warning: "text-amber-600 dark:text-amber-400",
50
- };
51
33
  ---
52
34
 
53
35
  <aside
54
36
  class:list={[
55
37
  "not-prose my-5 flex flex-row items-start gap-2 rounded-blume border px-4 py-3 font-sans text-muted-foreground text-sm leading-6 [&_a]:underline [&_a]:decoration-current/50 [&_code]:font-mono",
56
- color ? "bg-background" : variantClass[type],
38
+ color ? "bg-background" : variantClass[kind],
57
39
  ]}
40
+ data-blume-callout={kind}
58
41
  style={color ? `border:1px solid ${color};color:${color}` : undefined}
59
42
  >
60
- <span class:list={["mt-0.5 shrink-0", color ? "" : iconClass[type]]}>
61
- <Icon color={color} icon={icon ?? iconByType[type]} size={16} />
43
+ <span class:list={["mt-0.5 shrink-0", color ? "" : ADMONITION_ICON_CLASS[kind]]}>
44
+ <Icon color={color} icon={icon ?? ADMONITION_ICON[kind]} size={16} />
62
45
  </span>
63
46
  {/* The global prose rule leaks a 1rem margin onto these paragraphs/lists even
64
47
  though the callout is not-prose; with a title the body isn't the first
@@ -1,4 +1,9 @@
1
1
  ---
2
+ import {
3
+ ADMONITION_ICON,
4
+ ADMONITION_ICON_CLASS,
5
+ admonitionType,
6
+ } from "../colors.ts";
2
7
  import Icon from "../Icon.astro";
3
8
  import { withBase } from "../islands/base-path.ts";
4
9
  import { contentHref } from "./base-href.ts";
@@ -22,31 +27,20 @@ const external = href?.startsWith("http");
22
27
  const isHorizontal = horizontal === true || horizontal === "true";
23
28
  const showArrow =
24
29
  href && arrow === undefined ? external : arrow === true || arrow === "true";
25
- const iconByType = {
26
- check: "circle-check",
27
- danger: "circle-x",
28
- info: "info",
29
- note: "info",
30
- tip: "lightbulb",
31
- warning: "triangle-alert",
32
- };
33
- const iconClass = {
34
- check: "text-green-600 dark:text-green-400",
35
- danger: "text-red-600 dark:text-red-400",
36
- info: "text-blue-600 dark:text-blue-400",
37
- note: "text-muted-foreground/70",
38
- tip: "text-accent",
39
- warning: "text-amber-600 dark:text-amber-400",
40
- };
41
- const iconName = icon ?? (type ? iconByType[type] : undefined);
30
+ // The typed icon shares Callout's table (and its contrast rule); the surface
31
+ // tints are Card's own, a shade lighter than a callout's.
32
+ const kind = type ? admonitionType(type) : undefined;
33
+ const iconName = icon ?? (kind ? ADMONITION_ICON[kind] : undefined);
42
34
  const variantClass = {
43
- check: "border-green-500/30 bg-green-500/10",
44
35
  danger: "border-red-500/30 bg-red-500/10",
45
36
  info: "border-blue-500/30 bg-blue-500/10",
46
37
  note: "border-border bg-muted/40",
47
- tip: "border-accent/30 bg-accent/10",
38
+ success: "border-green-500/30 bg-green-500/10",
39
+ // 6% for the same reason as Callout: the accent defaults to near black, and
40
+ // muted body text on a 10% black tint misses WCAG AA.
41
+ tip: "border-accent/30 bg-accent/6",
48
42
  warning: "border-amber-500/30 bg-amber-500/10",
49
- }[type ?? "note"];
43
+ }[kind ?? "note"];
50
44
  ---
51
45
 
52
46
  <Tag
@@ -75,7 +69,7 @@ const variantClass = {
75
69
  <div class="p-5">
76
70
  {
77
71
  iconName && (
78
- <div class:list={["mb-2.5", color ? "" : type ? iconClass[type] : "text-accent"]}>
72
+ <div class:list={["mb-2.5", color ? "" : kind ? ADMONITION_ICON_CLASS[kind] : "text-accent"]}>
79
73
  <Icon color={color} name={iconName} size={20} />
80
74
  </div>
81
75
  )
@@ -10,6 +10,10 @@
10
10
  // examples are all supported. The source is highlighted with the same Shiki
11
11
  // setup as ordinary code fences.
12
12
  import data from "blume:data";
13
+ import {
14
+ CODE_PADDING_BLOCK_REM,
15
+ FLUSH_CODE_PADDING_TOP_REM,
16
+ } from "../../theme/code-block-padding.ts";
13
17
 
14
18
  import { highlightCode } from "../../markdown/index.ts";
15
19
  import { withBase } from "../islands/base-path.ts";
@@ -58,7 +62,12 @@ const codeHtml = entry
58
62
  // whose collapse to the measured height no transition could hide. No-JS
59
63
  // readers aren't hurt by the cap, since the source scrolls at any height.
60
64
  const LINE_PX = 21;
61
- const PADDING_PX = 36;
65
+ const REM_PX = 16;
66
+ // The pre's vertical padding, from the same constants the theme emits: the
67
+ // copy-button strip on top (the pane is a flush block inside tabs) and the
68
+ // plain inset below. The tab panel and the pre carry no border of their own.
69
+ const PADDING_PX =
70
+ (FLUSH_CODE_PADDING_TOP_REM + CODE_PADDING_BLOCK_REM) * REM_PX;
62
71
  const ESTIMATE_MAX_PX = 400;
63
72
  // The floor also clamps the measured height client-side; it rides along on the
64
73
  // iframe as `data-blume-min-pane` so the script and this estimate can't drift.
@@ -3,6 +3,7 @@
3
3
  // at build time (no client JS). Pass `owner`/`repo`, or omit them to use the
4
4
  // repo from blume.config. A `GITHUB_TOKEN` env var lifts the API rate limit.
5
5
  // If the API is unreachable the card still renders, just without counts.
6
+ import { apiUrl, PUBLIC_HOST_URL } from "../../core/github.ts";
6
7
  import { resolveIcon } from "../../theme/icons.ts";
7
8
  import data from "blume:data";
8
9
  import { GITHUB_MARK } from "../github-mark.ts";
@@ -15,22 +16,40 @@ const starIcon = resolveIcon("star")?.body ?? "";
15
16
  const forkIcon = resolveIcon("git-fork")?.body ?? "";
16
17
 
17
18
  interface Props {
19
+ /**
20
+ * Origin of the GitHub instance the card's repo lives on. Defaults to the
21
+ * configured `github.host`, so a card on an Enterprise site reads that
22
+ * instance; set it to point one card elsewhere — `https://github.com` for a
23
+ * public project from an Enterprise-hosted docs site, say. The REST base is
24
+ * derived from it the same way `github.api` is derived from `github.host`.
25
+ */
26
+ host?: string;
18
27
  owner?: string;
19
28
  repo?: string;
20
29
  token?: string;
21
30
  }
22
31
 
23
- // The resolved config exposes the repo as `repoUrl`, not `github`; parse the
24
- // owner/repo back out so <GithubInfo /> can default to the configured repo.
25
- const repoMatch = data.config.repoUrl?.match(
26
- /github\.com\/(?<owner>[^/]+)\/(?<repo>[^/]+)/u
27
- );
28
- const owner = Astro.props.owner ?? repoMatch?.groups?.owner;
29
- const repo = Astro.props.repo ?? repoMatch?.groups?.repo;
32
+ // `data.config.github` carries the configured repo's coordinates, so the card
33
+ // defaults to it and addresses the right instance — an Enterprise host serves
34
+ // both the link and the API from somewhere other than github.com. Explicit
35
+ // `owner`/`repo` props are read against that same instance unless `host`
36
+ // points them somewhere else; with no `github` configured at all, they fall
37
+ // back to the public one.
38
+ const configured = data.config.github;
39
+ const owner = Astro.props.owner ?? configured?.owner;
40
+ const repo = Astro.props.repo ?? configured?.repo;
30
41
  const token = Astro.props.token ?? process.env.GITHUB_TOKEN;
42
+ // A `host` prop is reduced to its origin, as the config field is, so a
43
+ // trailing slash or a path never lands mid-link.
44
+ const host = Astro.props.host
45
+ ? new URL(Astro.props.host).origin
46
+ : (configured?.host ?? PUBLIC_HOST_URL);
47
+ const baseUrl = Astro.props.host ? apiUrl({ host }) : configured?.api;
31
48
 
32
49
  const info =
33
- owner && repo ? await fetchRepositoryInfo({ owner, repo, token }) : null;
50
+ owner && repo
51
+ ? await fetchRepositoryInfo({ baseUrl, owner, repo, token })
52
+ : null;
34
53
 
35
54
  // Compact notation matches GitHub's own counts (e.g. 1.2k, 34.5k).
36
55
  const numbers = new Intl.NumberFormat("en", {
@@ -46,7 +65,7 @@ const statIcon =
46
65
  owner && repo && (
47
66
  <a
48
67
  class="not-prose my-6 flex flex-col gap-2 rounded-blume border border-border p-4 no-underline! transition-colors hover:border-foreground/30 hover:bg-muted/40"
49
- href={`https://github.com/${owner}/${repo}`}
68
+ href={`${host}/${owner}/${repo}`}
50
69
  rel="noreferrer"
51
70
  target="_blank"
52
71
  >
@@ -19,6 +19,13 @@ interface Props {
19
19
  */
20
20
  param?: string;
21
21
  sync?: boolean;
22
+ /**
23
+ * Sync tab selection only with other groups sharing the same key. Groups
24
+ * without a key form one page-wide pool (the default); keyed groups — like
25
+ * the generated ts2js dialect pairs — sync among themselves, so picking a
26
+ * tab there can't drag along an authored group with a same-titled tab.
27
+ */
28
+ syncKey?: string;
22
29
  }
23
30
 
24
31
  const {
@@ -29,6 +36,7 @@ const {
29
36
  inline = false,
30
37
  param,
31
38
  sync = true,
39
+ syncKey,
32
40
  } = Astro.props;
33
41
 
34
42
  // MDX string attributes (`hash="false"` from generated markup) must read as
@@ -52,6 +60,7 @@ const useDropdown = dropdown && !inline;
52
60
  data-hash={hashEnabled ? "true" : "false"}
53
61
  data-param={param}
54
62
  data-sync={sync ? "true" : "false"}
63
+ data-sync-key={syncKey}
55
64
  >
56
65
  <div
57
66
  class:list={[
@@ -116,9 +125,12 @@ const useDropdown = dropdown && !inline;
116
125
 
117
126
  connectedCallback() {
118
127
  const list = this.querySelector<HTMLElement>("[data-blume-tablist]");
128
+ // Nested groups (a generated ts2js pair inside a CodeGroup, say) own
129
+ // their panels; adopting a descendant group's panels here would leave
130
+ // both groups toggling the same elements.
119
131
  let panels = Array.from(
120
132
  this.querySelectorAll<HTMLElement>("[data-blume-tab-panel]")
121
- );
133
+ ).filter((panel) => panel.closest("blume-tabs") === this);
122
134
  // CodeGroup passes raw code fences instead of <Tab> elements, so adopt
123
135
  // the content wrapper's direct code blocks as panels. The tab label comes
124
136
  // from each block's title (the text after the language in the fence).
@@ -251,7 +263,10 @@ const useDropdown = dropdown && !inline;
251
263
  if (sync && this.#syncEnabled()) {
252
264
  document.dispatchEvent(
253
265
  new CustomEvent(SYNC_EVENT, {
254
- detail: { title: panelTitle(activePanel, index) },
266
+ detail: {
267
+ key: this.#syncKey(),
268
+ title: panelTitle(activePanel, index),
269
+ },
255
270
  })
256
271
  );
257
272
  }
@@ -339,6 +354,10 @@ const useDropdown = dropdown && !inline;
339
354
  return this.dataset.sync !== "false";
340
355
  }
341
356
 
357
+ #syncKey() {
358
+ return this.dataset.syncKey ?? "";
359
+ }
360
+
342
361
  #hashEnabled() {
343
362
  return this.dataset.hash !== "false";
344
363
  }
@@ -358,9 +377,12 @@ const useDropdown = dropdown && !inline;
358
377
  }
359
378
 
360
379
  #sync = (event: Event) => {
361
- const title = (event as CustomEvent<{ title?: string }>).detail?.title;
362
- if (title) {
363
- this.activateByTitle(title, false);
380
+ const detail = (event as CustomEvent<{ key?: string; title?: string }>)
381
+ .detail;
382
+ // Only same-key groups sync: keyless groups form one page-wide pool,
383
+ // keyed groups (generated dialect pairs) their own.
384
+ if (detail?.title && (detail.key ?? "") === this.#syncKey()) {
385
+ this.activateByTitle(detail.title, false);
364
386
  }
365
387
  };
366
388
 
@@ -7,6 +7,8 @@
7
7
  * than throwing, so a card never breaks the build when the API is unreachable.
8
8
  */
9
9
 
10
+ import { PUBLIC_API_URL } from "../../core/github.ts";
11
+
10
12
  export interface RepositoryInfo {
11
13
  description: string | null;
12
14
  forks: number;
@@ -20,18 +22,31 @@ export interface FetchRepositoryOptions {
20
22
  token?: string;
21
23
  }
22
24
 
23
- const DEFAULT_BASE_URL = "https://api.github.com";
24
-
25
25
  /** In-process dedupe so the same repo is fetched once per build. */
26
26
  const cache = new Map<string, Promise<RepositoryInfo | null>>();
27
27
 
28
+ /** Cleartext bases already warned about, so a site of cards logs once. */
29
+ const warnedCleartext = new Set<string>();
30
+
28
31
  const load = async (
29
32
  options: FetchRepositoryOptions
30
33
  ): Promise<RepositoryInfo | null> => {
31
- const { baseUrl = DEFAULT_BASE_URL, owner, repo, token } = options;
34
+ const { baseUrl = PUBLIC_API_URL, owner, repo, token } = options;
32
35
  const headers = new Headers({ Accept: "application/vnd.github+json" });
36
+ // An Enterprise instance can be configured on plain HTTP; a bearer token is
37
+ // never worth putting on the wire in cleartext, so it is dropped rather than
38
+ // sent. The request still goes out — a public repo's counts render either way
39
+ // — but a private repo's card comes back bare, which looks exactly like a
40
+ // network failure, so say why once per base.
33
41
  if (token) {
34
- headers.set("Authorization", `Bearer ${token}`);
42
+ if (new URL(baseUrl).protocol === "https:") {
43
+ headers.set("Authorization", `Bearer ${token}`);
44
+ } else if (!warnedCleartext.has(baseUrl)) {
45
+ warnedCleartext.add(baseUrl);
46
+ console.warn(
47
+ `[blume] <GithubInfo> is not sending its token to ${baseUrl}: the API base is plain HTTP, so the bearer token would travel in cleartext. Counts for a private repository will be missing; serve the API over https to authenticate.`
48
+ );
49
+ }
35
50
  }
36
51
 
37
52
  const response = await fetch(`${baseUrl}/repos/${owner}/${repo}`, {
@@ -69,7 +84,7 @@ const loadSafe = async (
69
84
  export const fetchRepositoryInfo = (
70
85
  options: FetchRepositoryOptions
71
86
  ): Promise<RepositoryInfo | null> => {
72
- const key = `${options.baseUrl ?? DEFAULT_BASE_URL}/${options.owner}/${options.repo}`;
87
+ const key = `${options.baseUrl ?? PUBLIC_API_URL}/${options.owner}/${options.repo}`;
73
88
  const existing = cache.get(key);
74
89
  if (existing) {
75
90
  return existing;