@nu-appdev/northwestern-starlight-theme 1.2.0 → 1.3.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 (45) hide show
  1. package/CHANGELOG.md +53 -2
  2. package/index.ts +84 -5
  3. package/mermaid.ts +196 -80
  4. package/package.json +19 -4
  5. package/src/components/Expandable.astro +19 -0
  6. package/src/components/Glossary.astro +11 -0
  7. package/src/components/Hero.astro +0 -2
  8. package/src/components/Kbd.astro +147 -0
  9. package/src/components/Property.astro +64 -0
  10. package/src/components/PropertyGroup.astro +13 -0
  11. package/src/components/PropertyTable.astro +54 -0
  12. package/src/components/Term.astro +29 -0
  13. package/src/components/Tooltip.astro +138 -0
  14. package/src/components/glossary-store.ts +12 -0
  15. package/src/components/index.ts +8 -0
  16. package/src/rehype-table-scroll.ts +35 -0
  17. package/src/scripts/mermaid/focus.ts +100 -0
  18. package/src/scripts/mermaid/fullscreen.ts +120 -0
  19. package/src/scripts/mermaid/index.ts +2 -0
  20. package/src/scripts/mermaid/overlay.ts +132 -0
  21. package/src/scripts/mermaid/pan-zoom.ts +285 -0
  22. package/src/scripts/mermaid/render.ts +143 -0
  23. package/src/scripts/mermaid/toolbar.ts +137 -0
  24. package/src/scripts/mermaid/ui.ts +265 -0
  25. package/src/styles/a11y.css +69 -0
  26. package/src/styles/components/blockquotes.css +37 -0
  27. package/src/styles/components/code.css +8 -5
  28. package/src/styles/components/footnotes.css +76 -0
  29. package/src/styles/components/kbd.css +69 -0
  30. package/src/styles/components/mermaid.css +4 -4
  31. package/src/styles/components/property-table.css +569 -0
  32. package/src/styles/components/steps.css +0 -3
  33. package/src/styles/components/tables.css +60 -0
  34. package/src/styles/components/tabs.css +0 -3
  35. package/src/styles/components/tooltip.css +145 -0
  36. package/src/styles/content.css +4 -160
  37. package/src/styles/layers.css +0 -7
  38. package/src/styles/mermaid-toolbar.css +62 -13
  39. package/src/styles/navigation.css +66 -17
  40. package/src/styles/openapi.css +0 -3
  41. package/src/styles/theme.css +0 -4
  42. package/src/styles/typography.css +4 -0
  43. package/src/styles/variables.css +0 -3
  44. package/src/virtual.d.ts +11 -0
  45. package/src/scripts/mermaid-toolbar.ts +0 -659
package/CHANGELOG.md CHANGED
@@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.3.0] - 2026-03-26
11
+
12
+ ### Added
13
+
14
+ - **Property Table component suite**: `<PropertyTable>`, `<Property>`, `<PropertyGroup>`, `<Expandable>`.
15
+ - See the [documentation](https://starlight-theme.entapp.northwestern.edu/components/property-table) for more information.
16
+ - **Tooltip component suite**: `<Tooltip>`, `<Glossary>`, `<Term>`.
17
+ - See the [documentation](https://starlight-theme.entapp.northwestern.edu/components/tooltip) for more information.
18
+ - **Keyboard component**: `<Kbd>`.
19
+ - See the [documentation](https://starlight-theme.entapp.northwestern.edu/components/kbd) for more information.
20
+ - Fullscreen Mermaid overlay: scale animation, dot grid background, pan momentum on drag release, toast notifications on copy.
21
+ - `@media (prefers-contrast: more)`: heavier borders, system colors for focus rings.
22
+ - `@media (prefers-reduced-transparency: reduce)`: solid surfaces replace translucent backgrounds.
23
+ - `CONTRIBUTING.md` with development setup, conventions, and PR process.
24
+ - JSDoc comments on public TypeScript interfaces.
25
+
26
+ ### Fixed
27
+
28
+ - Code block line numbers in dark mode had 2.85:1 contrast. Bumped from `#6e6e6e` to `#999` (4.6:1).
29
+ - Code block copy button icon in dark mode used `--nu-purple-100` on hover. Switched to `--nu-purple-40`.
30
+ - Wide tables overflowed into the sidebar. A [rehype](https://github.com/rehypejs/rehype) plugin now wraps each `<table>` in a scrollable `<div>` at build time.
31
+ - Reopening the fullscreen viewer after Escape showed a blue focus ring around the entire viewport.
32
+ - Mermaid hover toolbar sat unevenly relative to the diagram container border.
33
+ - Fullscreen close button used `rgb(255 255 255 / 20%)` while the theme toggle used `15%`. Both use `15%` now.
34
+ - Mobile sidebar: theme toggle and GitHub icon were white-on-white in light mode.
35
+ - Mobile sidebar: theme toggle was taller than wide; GitHub icon sat below it.
36
+ - Copy buttons did nothing on HTTP (insecure contexts). Added `document.execCommand("copy")` fallback.
37
+ - Clicking copy twice during the success animation duplicated the check icon. Debounced per button.
38
+ - iOS Safari toggled the URL bar when copying in the fullscreen viewer. The fallback textarea now appends inside the overlay.
39
+ - Mouse-opening the fullscreen viewer put a focus ring on the first control. Keyboard opens focus the first button; mouse opens focus the overlay container (no visible ring, Tab still works).
40
+ - Zoom in/out/reset buttons hidden on mobile (pinch-to-zoom covers it).
41
+ - Mermaid user overrides dropped on client-side theme toggle. Runtime configs now include merged user config.
42
+ - Mermaid lazy-rendered diagrams used stale theme after toggle. Observer callback now reads live theme mode.
43
+ - Mermaid pinch-to-zoom anchored to viewport center instead of pinch midpoint.
44
+ - Mermaid `MutationObservers` accumulated on view transitions. Previous observer now disconnected before creating a new one.
45
+ - Mermaid render race on rapid theme toggles. Per-container version tracking discards stale async completions.
46
+ - Mermaid fullscreen close could fire twice during animation. Added guard against double-close.
47
+ - Mermaid `diagramSources` `Map` leaked detached DOM nodes across view transitions. Switched to `WeakMap`.
48
+ - Tooltip event listeners duplicated on Astro view transitions. Added `WeakSet` idempotency guard.
49
+ - Tooltip Popover API called without feature detection. Added fallback for unsupported browsers.
50
+ - Tooltip positioning drifted on scroll. Now repositions via scroll listener while visible.
51
+ - Sidebar active item border shifted text. All sidebar links now reserve space with a transparent left border.
52
+ - Search modal focus ring used browser default blue. Now uses `--nu-focus-ring`.
53
+ - Site title clipped on mobile. Added fluid font sizing with `clamp()` and ellipsis overflow.
54
+
55
+ ### Changed
56
+
57
+ - Fullscreen controls bar uses Starlight's `--sl-nav-pad-x` and `--sl-menu-button-size` so the close button aligns with the hamburger menu on mobile.
58
+ - Removed right padding on active sidebar headings that broke nested heading alignment.
59
+
10
60
  ## [1.2.0] - 2026-03-25
11
61
 
12
62
  ### Added
@@ -40,7 +90,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
40
90
 
41
91
  ### Added
42
92
 
43
- - Fullscreen mermaid viewer supports touch: one-finger pan, pinch-to-zoom, two-finger pan.
93
+ - Fullscreen Mermaid viewer supports touch: one-finger pan, pinch-to-zoom, two-finger pan.
44
94
  - Mermaid toolbar stays visible on touch devices (`hover: none`) since hover is unavailable.
45
95
  - SVG downloads use descriptive filenames derived from the site title, page slug, and diagram type (e.g., `northwestern-starlight-theme_examples-mermaid-flowchart.svg`).
46
96
 
@@ -71,7 +121,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
71
121
  - OpenAPI plugin compatibility with method badge preservation
72
122
  - Reduced motion support for transitions
73
123
 
74
- [Unreleased]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.2.0...HEAD
124
+ [Unreleased]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.3.0...HEAD
125
+ [1.3.0]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.2.0...v1.3.0
75
126
  [1.2.0]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.1.1...v1.2.0
76
127
  [1.1.1]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.1.0...v1.1.1
77
128
  [1.1.0]: https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/compare/v1.0.0...v1.1.0
package/index.ts CHANGED
@@ -3,7 +3,25 @@ import type { IncomingMessage, ServerResponse } from "node:http";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import type { StarlightPlugin } from "@astrojs/starlight/types";
6
+ import rehypeTableScroll from "./src/rehype-table-scroll.ts";
6
7
 
8
+ /**
9
+ * Configuration for the homepage hero section.
10
+ *
11
+ * Controls hero layout, title visibility, and image sizing. Passed as the
12
+ * `homepage` property of {@link NorthwesternThemeConfig}.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * northwesternTheme({
17
+ * homepage: {
18
+ * layout: "split",
19
+ * showTitle: false,
20
+ * imageWidth: "750px",
21
+ * },
22
+ * })
23
+ * ```
24
+ */
7
25
  export interface NorthwesternHomepageConfig {
8
26
  /**
9
27
  * Hero layout style.
@@ -32,19 +50,46 @@ export interface NorthwesternHomepageConfig {
32
50
  imageWidth?: string;
33
51
  }
34
52
 
53
+ /**
54
+ * Top-level configuration for the Northwestern Starlight theme plugin.
55
+ *
56
+ * Pass to {@link northwesternTheme} when registering the plugin in your
57
+ * Starlight config. All properties are optional and have sensible defaults.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * // astro.config.ts
62
+ * import northwesternTheme from "@nu-appdev/northwestern-starlight-theme";
63
+ *
64
+ * export default defineConfig({
65
+ * integrations: [
66
+ * starlight({
67
+ * plugins: [northwesternTheme({ homepage: { layout: "split" } })],
68
+ * }),
69
+ * ],
70
+ * });
71
+ * ```
72
+ *
73
+ * @see {@link NorthwesternHomepageConfig} for homepage hero options
74
+ */
35
75
  export interface NorthwesternThemeConfig {
36
76
  /**
37
77
  * Homepage hero layout configuration.
78
+ *
79
+ * @see {@link NorthwesternHomepageConfig}
38
80
  */
39
81
  homepage?: NorthwesternHomepageConfig;
40
82
 
41
83
  /**
42
84
  * Mermaid diagram support.
43
85
  *
44
- * - `true` (default) — auto-detect: enables mermaid if `astro-mermaid` and `mermaid`
86
+ * - `true` (default) — auto-detect: enables Mermaid if `astro-mermaid` and `mermaid`
45
87
  * are installed, skips silently if they are not
46
- * - `false` — disables mermaid entirely
47
- * - `object` — enables mermaid with custom config (merged with Northwestern defaults)
88
+ * - `false` — disables Mermaid entirely
89
+ * - `object` — enables Mermaid with custom {@link https://mermaid.js.org/config/schema-docs/config.html | MermaidConfig}
90
+ * merged with Northwestern defaults
91
+ *
92
+ * @default true
48
93
  */
49
94
  mermaid?: boolean | Record<string, unknown>;
50
95
  }
@@ -64,6 +109,33 @@ async function canResolve(id: string): Promise<boolean> {
64
109
  const VIRTUAL_MODULE_ID = "virtual:northwestern-theme/config";
65
110
  const RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
66
111
 
112
+ /**
113
+ * Create a Northwestern-branded Starlight theme plugin.
114
+ *
115
+ * Registers Northwestern typography (Akkurat Pro, Poppins), purple color palette,
116
+ * component overrides (Hero, ThemeToggle, EditLink), and optional Mermaid diagram
117
+ * support with branded color schemes.
118
+ *
119
+ * @param config - Theme configuration. All properties optional.
120
+ * @returns A Starlight plugin to pass to `plugins` in your Starlight config.
121
+ *
122
+ * @example Basic usage (all defaults)
123
+ * ```ts
124
+ * starlight({ plugins: [northwesternTheme()] })
125
+ * ```
126
+ *
127
+ * @example Split hero with Mermaid disabled
128
+ * ```ts
129
+ * starlight({
130
+ * plugins: [
131
+ * northwesternTheme({
132
+ * homepage: { layout: "split", showTitle: false },
133
+ * mermaid: false,
134
+ * }),
135
+ * ],
136
+ * })
137
+ * ```
138
+ */
67
139
  export default function northwesternTheme(config: NorthwesternThemeConfig = {}): StarlightPlugin {
68
140
  const { homepage = {}, mermaid = true } = config;
69
141
  const themeConfig = {
@@ -92,6 +164,9 @@ export default function northwesternTheme(config: NorthwesternThemeConfig = {}):
92
164
  hooks: {
93
165
  "astro:config:setup": ({ updateConfig: updateAstroConfig }) => {
94
166
  updateAstroConfig({
167
+ markdown: {
168
+ rehypePlugins: [rehypeTableScroll],
169
+ },
95
170
  vite: {
96
171
  plugins: [
97
172
  {
@@ -151,7 +226,7 @@ export default function northwesternTheme(config: NorthwesternThemeConfig = {}):
151
226
  },
152
227
  },
153
228
  });
154
- // Detect mermaid availability
229
+ // Detect Mermaid availability
155
230
  let mermaidEnabled = mermaid;
156
231
 
157
232
  if (mermaidEnabled) {
@@ -159,7 +234,7 @@ export default function northwesternTheme(config: NorthwesternThemeConfig = {}):
159
234
 
160
235
  if (!hasMermaid) {
161
236
  if (typeof mermaid === "object") {
162
- // User explicitly configured mermaid but it's not installed
237
+ // User explicitly configured Mermaid but it's not installed
163
238
  logger.warn(
164
239
  'Mermaid config was provided but "astro-mermaid" and "mermaid" are not installed.\n' +
165
240
  " Run: pnpm add astro-mermaid mermaid",
@@ -190,6 +265,9 @@ export default function northwesternTheme(config: NorthwesternThemeConfig = {}):
190
265
  "@nu-appdev/northwestern-starlight-theme/src/styles/theme.css",
191
266
  "@nu-appdev/northwestern-starlight-theme/src/styles/navigation.css",
192
267
  "@nu-appdev/northwestern-starlight-theme/src/styles/content.css",
268
+ "@nu-appdev/northwestern-starlight-theme/src/styles/components/blockquotes.css",
269
+ "@nu-appdev/northwestern-starlight-theme/src/styles/components/footnotes.css",
270
+ "@nu-appdev/northwestern-starlight-theme/src/styles/components/tables.css",
193
271
  "@nu-appdev/northwestern-starlight-theme/src/styles/components/cards.css",
194
272
  "@nu-appdev/northwestern-starlight-theme/src/styles/components/utility-surfaces.css",
195
273
  "@nu-appdev/northwestern-starlight-theme/src/styles/components/steps.css",
@@ -198,6 +276,7 @@ export default function northwesternTheme(config: NorthwesternThemeConfig = {}):
198
276
  "@nu-appdev/northwestern-starlight-theme/src/styles/components/mermaid.css",
199
277
  "@nu-appdev/northwestern-starlight-theme/src/styles/mermaid-toolbar.css",
200
278
  "@nu-appdev/northwestern-starlight-theme/src/styles/openapi.css",
279
+ "@nu-appdev/northwestern-starlight-theme/src/styles/a11y.css",
201
280
  ...(starlightConfig.customCss ?? []),
202
281
  ],
203
282
  expressiveCode: {
package/mermaid.ts CHANGED
@@ -2,14 +2,41 @@ import type { AstroIntegration } from "astro";
2
2
  import mermaid, { type AstroMermaidOptions } from "astro-mermaid";
3
3
  import { darken, isDark, lighten, mix, transparentize } from "khroma";
4
4
 
5
+ /**
6
+ * Configuration options for the Northwestern Mermaid integration.
7
+ *
8
+ * Extends `astro-mermaid` options with a `toolbar` toggle that controls
9
+ * the hover toolbar (fullscreen, download, copy) on rendered diagrams.
10
+ *
11
+ * @see {@link northwesternMermaid} for the integration factory
12
+ */
5
13
  export interface NorthwesternMermaidOptions extends AstroMermaidOptions {
14
+ /**
15
+ * Show the hover toolbar (fullscreen, download SVG, copy source) on diagrams.
16
+ *
17
+ * @default true
18
+ */
6
19
  toolbar?: boolean;
7
20
  }
8
21
 
22
+ /**
23
+ * Theme mode for Mermaid color palette generation.
24
+ *
25
+ * Maps to the `data-theme` attribute on `<html>`: `"light"` for the default
26
+ * palette, `"dark"` for the inverted palette with lighter primary colors
27
+ * and darker canvas backgrounds.
28
+ */
9
29
  export type NorthwesternMermaidMode = "light" | "dark";
10
30
 
31
+ /** Flat key-value map passed to Mermaid's `themeVariables`. */
11
32
  type ThemeVariables = Record<string, unknown>;
12
33
 
34
+ /**
35
+ * Resolved brand palette for a single theme mode.
36
+ *
37
+ * All values are hex strings. Derived from Northwestern's brand primaries
38
+ * with light/dark adjustments via `khroma`.
39
+ */
13
40
  type BrandPalette = {
14
41
  purple: string;
15
42
  blue: string;
@@ -31,6 +58,15 @@ type BrandPalette = {
31
58
 
32
59
  const chartPalette = ["#4e2a84", "#836eaa", "#5091cd", "#008656", "#ffc520", "#ef553f", "#7fcecd", "#d85820"];
33
60
 
61
+ /** Generate `{ prefix0: fn(0), prefix1: fn(1), ... }` from an array. */
62
+ function indexedVars(prefix: string, values: string[], startAt = 0): ThemeVariables {
63
+ const out: ThemeVariables = {};
64
+ for (let i = 0; i < values.length; i++) {
65
+ out[`${prefix}${i + startAt}`] = values[i];
66
+ }
67
+ return out;
68
+ }
69
+
34
70
  function onColor(color: string, darkText = "#342f2e", lightText = "#fff") {
35
71
  return isDark(color) ? lightText : darkText;
36
72
  }
@@ -216,32 +252,16 @@ function createThemeVariables(mode: NorthwesternMermaidMode): ThemeVariables {
216
252
  altSectionBkgColor: rowEven,
217
253
  sectionBkgColor2: compositeAltBackground,
218
254
 
219
- git0: chartPalette[0],
220
- git1: chartPalette[1],
221
- git2: chartPalette[2],
222
- git3: chartPalette[3],
223
- git4: chartPalette[4],
224
- git5: chartPalette[5],
225
- git6: chartPalette[6],
226
- git7: chartPalette[7],
227
- gitBranchLabel0: onColor(chartPalette[0]),
228
- gitBranchLabel1: onColor(chartPalette[1]),
229
- gitBranchLabel2: onColor(chartPalette[2]),
230
- gitBranchLabel3: onColor(chartPalette[3]),
231
- gitBranchLabel4: onColor(chartPalette[4]),
232
- gitBranchLabel5: onColor(chartPalette[5]),
233
- gitBranchLabel6: onColor(chartPalette[6]),
234
- gitBranchLabel7: onColor(chartPalette[7]),
255
+ // Git graph — one color per branch from the chart palette
256
+ ...indexedVars("git", chartPalette),
257
+ ...indexedVars(
258
+ "gitBranchLabel",
259
+ chartPalette.map((c) => onColor(c)),
260
+ ),
235
261
  gitInv0: palette.white,
236
262
 
237
- pie1: chartPalette[0],
238
- pie2: chartPalette[1],
239
- pie3: chartPalette[2],
240
- pie4: chartPalette[3],
241
- pie5: chartPalette[4],
242
- pie6: chartPalette[5],
243
- pie7: chartPalette[6],
244
- pie8: chartPalette[7],
263
+ // Pie chart — 8 base slices + 4 extended with tint shift
264
+ ...indexedVars("pie", chartPalette, 1),
245
265
  pie9: dark ? lighten(chartPalette[0], 16) : darken(chartPalette[0], 10),
246
266
  pie10: dark ? lighten(chartPalette[2], 16) : darken(chartPalette[2], 10),
247
267
  pie11: dark ? lighten(chartPalette[3], 16) : darken(chartPalette[3], 10),
@@ -255,35 +275,64 @@ function createThemeVariables(mode: NorthwesternMermaidMode): ThemeVariables {
255
275
  pieLegendTextSize: "14px",
256
276
  pieStrokeWidth: "2px",
257
277
  pieOuterStrokeWidth: "1px",
258
- pieOuterStrokeColor: dark ? palette.border : palette.border,
278
+ pieOuterStrokeColor: palette.border,
259
279
  pieOpacity: "0.85",
260
280
 
261
- cScale0: dark ? mix(primary, palette.canvas, 40) : primary,
262
- cScale1: dark ? mix(palette.blue, palette.canvas, 40) : palette.blue,
263
- cScale2: dark ? mix(palette.green, palette.canvas, 40) : palette.green,
264
- cScale3: dark ? mix(palette.orange, palette.canvas, 35) : palette.orange,
265
- cScale4: dark ? mix(palette.teal, palette.canvas, 35) : darken(palette.teal, 15),
266
- cScale5: dark ? mix(palette.yellow, palette.canvas, 30) : darken(palette.yellow, 20),
267
- cScale6: dark ? mix(palette.red, palette.canvas, 35) : palette.red,
268
- cScale7: dark ? mix(primary, palette.canvas, 30) : lighten(primary, 10),
269
-
270
- cScaleLabel0: palette.white,
271
- cScaleLabel1: palette.white,
272
- cScaleLabel2: palette.white,
273
- cScaleLabel3: palette.white,
274
- cScaleLabel4: dark ? palette.white : palette.black,
275
- cScaleLabel5: palette.black,
276
- cScaleLabel6: palette.white,
277
- cScaleLabel7: palette.white,
278
-
279
- quadrant1Fill: dark ? mix(primary, palette.canvas, 35) : mix(primary, palette.white, 25),
280
- quadrant2Fill: dark ? mix(primary, palette.canvas, 28) : mix(primary, palette.white, 18),
281
- quadrant3Fill: dark ? mix(primary, palette.canvas, 22) : mix(primary, palette.white, 12),
282
- quadrant4Fill: dark ? mix(primary, palette.canvas, 15) : mix(primary, palette.white, 8),
283
- quadrant1TextFill: onColor(dark ? mix(primary, palette.canvas, 35) : mix(primary, palette.white, 25)),
284
- quadrant2TextFill: onColor(dark ? mix(primary, palette.canvas, 28) : mix(primary, palette.white, 18)),
285
- quadrant3TextFill: onColor(dark ? mix(primary, palette.canvas, 22) : mix(primary, palette.white, 12)),
286
- quadrant4TextFill: onColor(dark ? mix(primary, palette.canvas, 15) : mix(primary, palette.white, 8)),
281
+ // C4/journey scale — each color mixed toward canvas in dark mode
282
+ ...(() => {
283
+ const colors = [
284
+ primary,
285
+ palette.blue,
286
+ palette.green,
287
+ palette.orange,
288
+ palette.teal,
289
+ palette.yellow,
290
+ palette.red,
291
+ primary,
292
+ ];
293
+ const darkMix = [40, 40, 40, 35, 35, 30, 35, 30];
294
+ const lightFallback = [
295
+ primary,
296
+ palette.blue,
297
+ palette.green,
298
+ palette.orange,
299
+ darken(palette.teal, 15),
300
+ darken(palette.yellow, 20),
301
+ palette.red,
302
+ lighten(primary, 10),
303
+ ];
304
+ return indexedVars(
305
+ "cScale",
306
+ colors.map((c, i) => (dark ? mix(c, palette.canvas, darkMix[i]) : lightFallback[i])),
307
+ );
308
+ })(),
309
+ ...indexedVars("cScaleLabel", [
310
+ palette.white,
311
+ palette.white,
312
+ palette.white,
313
+ palette.white,
314
+ dark ? palette.white : palette.black,
315
+ palette.black,
316
+ palette.white,
317
+ palette.white,
318
+ ]),
319
+
320
+ // Quadrant chart — progressive primary tints
321
+ ...(() => {
322
+ const darkRatios = [35, 28, 22, 15];
323
+ const lightRatios = [25, 18, 12, 8];
324
+ const fills = darkRatios.map((dr, i) =>
325
+ dark ? mix(primary, palette.canvas, dr) : mix(primary, palette.white, lightRatios[i]),
326
+ );
327
+ return {
328
+ ...indexedVars("quadrantFill", fills, 1),
329
+ ...indexedVars(
330
+ "quadrantTextFill",
331
+ fills.map((f) => onColor(f)),
332
+ 1,
333
+ ),
334
+ };
335
+ })(),
287
336
  quadrantPointFill: primary,
288
337
  quadrantPointTextFill: dark ? palette.white : palette.black,
289
338
  quadrantXAxisTextFill: palette.text,
@@ -294,17 +343,36 @@ function createThemeVariables(mode: NorthwesternMermaidMode): ThemeVariables {
294
343
 
295
344
  xyChart: {
296
345
  plotColorPalette: chartPalette.join(","),
297
- titleColor: dark ? "#342f2e" : palette.text,
298
- xAxisLabelColor: dark ? "#342f2e" : palette.text,
299
- yAxisLabelColor: dark ? "#342f2e" : palette.text,
300
- xAxisTitleColor: dark ? "#342f2e" : palette.text,
301
- yAxisTitleColor: dark ? "#342f2e" : palette.text,
302
- xAxisLineColor: "#ccc",
303
- yAxisLineColor: "#ccc",
346
+ titleColor: palette.text,
347
+ xAxisLabelColor: palette.text,
348
+ yAxisLabelColor: palette.text,
349
+ xAxisTitleColor: palette.text,
350
+ yAxisTitleColor: palette.text,
351
+ xAxisLineColor: dark ? palette.border : "#ccc",
352
+ yAxisLineColor: dark ? palette.border : "#ccc",
304
353
  },
305
354
  };
306
355
  }
307
356
 
357
+ /**
358
+ * Generate a complete `astro-mermaid` config with Northwestern-branded colors
359
+ * for the given theme mode.
360
+ *
361
+ * Builds a Mermaid `themeVariables` object from the Northwestern brand palette,
362
+ * deriving all node, edge, label, chart, and diagram-specific colors from a
363
+ * small set of brand primaries via `khroma` color manipulation.
364
+ *
365
+ * @param mode - `"light"` or `"dark"`. Controls canvas, text, and primary color values.
366
+ * @returns A complete `AstroMermaidOptions` object ready to pass to `astro-mermaid`.
367
+ *
368
+ * @example
369
+ * ```ts
370
+ * import { createNorthwesternMermaidConfig } from "@nu-appdev/northwestern-starlight-theme/mermaid";
371
+ *
372
+ * const darkConfig = createNorthwesternMermaidConfig("dark");
373
+ * // darkConfig.mermaidConfig.themeVariables contains all colors
374
+ * ```
375
+ */
308
376
  export function createNorthwesternMermaidConfig(mode: NorthwesternMermaidMode): AstroMermaidOptions {
309
377
  const palette = createPalette(mode);
310
378
  return {
@@ -314,45 +382,93 @@ export function createNorthwesternMermaidConfig(mode: NorthwesternMermaidMode):
314
382
  themeVariables: createThemeVariables(mode),
315
383
  xyChart: {
316
384
  plotColorPalette: chartPalette.join(","),
317
- titleColor: mode === "dark" ? "#342f2e" : palette.text,
318
- xAxisLabelColor: mode === "dark" ? "#342f2e" : palette.text,
319
- yAxisLabelColor: mode === "dark" ? "#342f2e" : palette.text,
320
- xAxisTitleColor: mode === "dark" ? "#342f2e" : palette.text,
321
- yAxisTitleColor: mode === "dark" ? "#342f2e" : palette.text,
322
- xAxisLineColor: "#ccc",
323
- yAxisLineColor: "#ccc",
385
+ titleColor: palette.text,
386
+ xAxisLabelColor: palette.text,
387
+ yAxisLabelColor: palette.text,
388
+ xAxisTitleColor: palette.text,
389
+ yAxisTitleColor: palette.text,
390
+ xAxisLineColor: mode === "dark" ? palette.border : "#ccc",
391
+ yAxisLineColor: mode === "dark" ? palette.border : "#ccc",
324
392
  },
325
393
  },
326
394
  };
327
395
  }
328
396
 
329
397
  /**
330
- * Northwestern-branded Mermaid configuration.
398
+ * Pre-built light-mode Mermaid config with Northwestern brand colors.
331
399
  *
332
- * The theme is derived from a small brand palette per mode instead of a large
333
- * hand-authored variable table. Mermaid renders with a dedicated light or dark
334
- * config, while CSS remains limited to structural SVG tweaks.
400
+ * Used as the default when Mermaid is auto-detected. Override individual
401
+ * `themeVariables` by passing a `mermaid` object to {@link northwesternTheme}
402
+ * in `index.ts`, or use {@link createNorthwesternMermaidConfig} for full control.
335
403
  */
336
404
  export const defaultMermaidConfig: AstroMermaidOptions = createNorthwesternMermaidConfig("light");
405
+
406
+ /**
407
+ * Pre-built dark-mode Mermaid config with Northwestern brand colors.
408
+ *
409
+ * Applied at runtime when the user switches to dark mode. The toolbar script
410
+ * re-renders diagrams with this config via `window.__NU_MERMAID_CONFIGS__.dark`.
411
+ */
337
412
  export const darkMermaidConfig: AstroMermaidOptions = createNorthwesternMermaidConfig("dark");
338
413
 
414
+ /**
415
+ * Create an Astro integration that registers Northwestern-branded Mermaid diagrams.
416
+ *
417
+ * Wraps `astro-mermaid` with Northwestern color palettes for both light and dark
418
+ * modes, and injects the toolbar script (fullscreen viewer, download, copy).
419
+ *
420
+ * **Must be added before `starlight()` in the `integrations` array.** The
421
+ * `astro-mermaid` remark plugin needs to register before Starlight's rehype
422
+ * processing, and Astro processes integrations in order. Placing it after
423
+ * `starlight()` causes Mermaid code blocks to be treated as plain code.
424
+ *
425
+ * @param options - Merged with Northwestern defaults. Set `toolbar: false` to
426
+ * disable the hover toolbar.
427
+ * @returns An Astro integration to add to `integrations` in your Astro config.
428
+ *
429
+ * @example
430
+ * ```ts
431
+ * import { northwesternMermaid } from "@nu-appdev/northwestern-starlight-theme/mermaid";
432
+ * import northwesternTheme from "@nu-appdev/northwestern-starlight-theme";
433
+ *
434
+ * export default defineConfig({
435
+ * integrations: [
436
+ * northwesternMermaid(), // Must come before starlight()
437
+ * starlight({
438
+ * plugins: [northwesternTheme()],
439
+ * title: "My Docs",
440
+ * }),
441
+ * ],
442
+ * });
443
+ * ```
444
+ */
339
445
  export function northwesternMermaid(options: NorthwesternMermaidOptions = {}): AstroIntegration {
340
446
  const { toolbar = true, ...overrides } = options;
341
447
  const lightConfig = createNorthwesternMermaidConfig("light");
342
448
  const darkConfig = createNorthwesternMermaidConfig("dark");
343
449
 
450
+ const userMermaidConfig = overrides.mermaidConfig ?? {};
451
+ const userThemeVars = (userMermaidConfig as Record<string, unknown>)?.themeVariables ?? {};
452
+
453
+ function mergeWithOverrides(base: Record<string, unknown>): Record<string, unknown> {
454
+ return {
455
+ ...base,
456
+ ...userMermaidConfig,
457
+ themeVariables: {
458
+ ...(base.themeVariables ?? {}),
459
+ ...userThemeVars,
460
+ },
461
+ };
462
+ }
463
+
464
+ const mergedLightMermaidConfig = mergeWithOverrides(lightConfig.mermaidConfig as Record<string, unknown>);
465
+ const mergedDarkMermaidConfig = mergeWithOverrides(darkConfig.mermaidConfig as Record<string, unknown>);
466
+
344
467
  const mermaidIntegration = mermaid({
345
468
  ...lightConfig,
346
469
  ...overrides,
347
470
  enableLog: false,
348
- mermaidConfig: {
349
- ...(lightConfig.mermaidConfig ?? {}),
350
- ...(overrides.mermaidConfig ?? {}),
351
- themeVariables: {
352
- ...((lightConfig.mermaidConfig as Record<string, unknown>)?.themeVariables ?? {}),
353
- ...((overrides.mermaidConfig as Record<string, unknown>)?.themeVariables ?? {}),
354
- },
355
- },
471
+ mermaidConfig: mergedLightMermaidConfig,
356
472
  });
357
473
 
358
474
  return {
@@ -365,10 +481,10 @@ export function northwesternMermaid(options: NorthwesternMermaidOptions = {}): A
365
481
  "page",
366
482
  `window.__NU_MERMAID_TOOLBAR__ = ${toolbar ? "true" : "false"}; window.__NU_MERMAID_CONFIGS__ = ${JSON.stringify(
367
483
  {
368
- light: lightConfig.mermaidConfig,
369
- dark: darkConfig.mermaidConfig,
484
+ light: mergedLightMermaidConfig,
485
+ dark: mergedDarkMermaidConfig,
370
486
  },
371
- )}; import "@nu-appdev/northwestern-starlight-theme/src/scripts/mermaid-toolbar.ts";`,
487
+ )}; import "@nu-appdev/northwestern-starlight-theme/src/scripts/mermaid/toolbar.ts";`,
372
488
  );
373
489
  },
374
490
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nu-appdev/northwestern-starlight-theme",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "A Northwestern-branded theme for Astro Starlight",
5
5
  "license": "MIT",
6
6
  "author": "Danny Foster <danny@northwestern.edu>",
@@ -27,9 +27,21 @@
27
27
  "provenance": true
28
28
  },
29
29
  "exports": {
30
- ".": "./index.ts",
31
- "./expressive-code": "./expressive-code.mjs",
32
- "./mermaid": "./mermaid.ts",
30
+ ".": {
31
+ "types": "./index.ts",
32
+ "import": "./index.ts"
33
+ },
34
+ "./expressive-code": {
35
+ "import": "./expressive-code.mjs"
36
+ },
37
+ "./mermaid": {
38
+ "types": "./mermaid.ts",
39
+ "import": "./mermaid.ts"
40
+ },
41
+ "./components": {
42
+ "types": "./src/components/index.ts",
43
+ "import": "./src/components/index.ts"
44
+ },
33
45
  "./src/styles/components/*": "./src/styles/components/*",
34
46
  "./src/components/*": "./src/components/*",
35
47
  "./src/scripts/*": "./src/scripts/*",
@@ -59,5 +71,8 @@
59
71
  "mermaid": {
60
72
  "optional": true
61
73
  }
74
+ },
75
+ "devDependencies": {
76
+ "@types/hast": "^3.0.4"
62
77
  }
63
78
  }
@@ -0,0 +1,19 @@
1
+ ---
2
+ interface Props {
3
+ /** Label for the toggle. */
4
+ title?: string;
5
+ /** Whether to start expanded. */
6
+ open?: boolean;
7
+ }
8
+
9
+ const { title = "Show properties", open = false } = Astro.props;
10
+ ---
11
+
12
+ <details class="nu-expandable" open={open || undefined}>
13
+ <summary class="nu-expandable__toggle">{title}</summary>
14
+ <div class="nu-expandable__content">
15
+ <div class="nu-expandable__nested">
16
+ <slot />
17
+ </div>
18
+ </div>
19
+ </details>
@@ -0,0 +1,11 @@
1
+ ---
2
+ import { setTerms } from "./glossary-store.ts";
3
+
4
+ interface Props {
5
+ /** Map of term keys to definitions. */
6
+ terms: Record<string, string>;
7
+ }
8
+
9
+ const { terms } = Astro.props;
10
+ setTerms(terms);
11
+ ---