@nu-appdev/northwestern-starlight-theme 1.1.1 → 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.
- package/CHANGELOG.md +73 -2
- package/index.ts +84 -5
- package/mermaid.ts +196 -80
- package/package.json +19 -4
- package/src/components/Expandable.astro +19 -0
- package/src/components/Glossary.astro +11 -0
- package/src/components/Hero.astro +0 -2
- package/src/components/Kbd.astro +147 -0
- package/src/components/Property.astro +64 -0
- package/src/components/PropertyGroup.astro +13 -0
- package/src/components/PropertyTable.astro +54 -0
- package/src/components/Term.astro +29 -0
- package/src/components/Tooltip.astro +138 -0
- package/src/components/glossary-store.ts +12 -0
- package/src/components/index.ts +8 -0
- package/src/rehype-table-scroll.ts +35 -0
- package/src/scripts/mermaid/focus.ts +100 -0
- package/src/scripts/mermaid/fullscreen.ts +120 -0
- package/src/scripts/mermaid/index.ts +2 -0
- package/src/scripts/mermaid/overlay.ts +132 -0
- package/src/scripts/mermaid/pan-zoom.ts +285 -0
- package/src/scripts/mermaid/render.ts +143 -0
- package/src/scripts/mermaid/toolbar.ts +137 -0
- package/src/scripts/mermaid/ui.ts +265 -0
- package/src/styles/a11y.css +69 -0
- package/src/styles/components/blockquotes.css +37 -0
- package/src/styles/components/code.css +8 -5
- package/src/styles/components/footnotes.css +76 -0
- package/src/styles/components/kbd.css +69 -0
- package/src/styles/components/mermaid.css +4 -4
- package/src/styles/components/property-table.css +569 -0
- package/src/styles/components/steps.css +0 -3
- package/src/styles/components/tables.css +60 -0
- package/src/styles/components/tabs.css +0 -3
- package/src/styles/components/tooltip.css +145 -0
- package/src/styles/content.css +9 -94
- package/src/styles/layers.css +0 -7
- package/src/styles/mermaid-toolbar.css +91 -14
- package/src/styles/navigation.css +72 -16
- package/src/styles/openapi.css +0 -3
- package/src/styles/theme.css +0 -4
- package/src/styles/typography.css +4 -0
- package/src/styles/variables.css +0 -3
- package/src/virtual.d.ts +11 -0
- package/src/scripts/mermaid-toolbar.ts +0 -589
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid toolbar entry point.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates diagram rendering, toolbar injection, and lifecycle management
|
|
5
|
+
* across page loads, theme changes, and Astro view transitions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
interface Window {
|
|
10
|
+
/** Whether the hover toolbar is enabled (injected by the Northwestern Mermaid integration). */
|
|
11
|
+
__NU_MERMAID_TOOLBAR__?: boolean;
|
|
12
|
+
/** Northwestern Mermaid theme configs for light and dark modes. */
|
|
13
|
+
__NU_MERMAID_CONFIGS__?: {
|
|
14
|
+
light?: Record<string, unknown>;
|
|
15
|
+
dark?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
import { openFullscreen } from "./fullscreen";
|
|
21
|
+
import { diagramSources, renderAllDiagrams } from "./render";
|
|
22
|
+
import { createActionButton, downloadDiagramSvg, flashSuccess, ICON_PATHS, writeToClipboard } from "./ui";
|
|
23
|
+
|
|
24
|
+
/** Maximum time (ms) to watch for lazy-rendered diagrams before disconnecting the observer. */
|
|
25
|
+
const TOOLBAR_OBSERVER_TIMEOUT_MS = 30_000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Inject hover toolbars into rendered mermaid diagrams.
|
|
29
|
+
*
|
|
30
|
+
* Safe to call repeatedly across page loads, lazy render callbacks, theme changes,
|
|
31
|
+
* and Astro view transitions.
|
|
32
|
+
*
|
|
33
|
+
* **Idempotency guards:**
|
|
34
|
+
* - Skips fullscreen clones (`.nu-mermaid-fullscreen`)
|
|
35
|
+
* - Skips diagrams without a rendered `<svg>` child
|
|
36
|
+
* - Skips diagrams that already have a `.nu-mermaid-toolbar`
|
|
37
|
+
*/
|
|
38
|
+
function injectToolbars(): number {
|
|
39
|
+
const diagrams = document.querySelectorAll<HTMLElement>(".mermaid");
|
|
40
|
+
if (!diagrams.length) return 0;
|
|
41
|
+
|
|
42
|
+
let injectedCount = 0;
|
|
43
|
+
|
|
44
|
+
diagrams.forEach((container) => {
|
|
45
|
+
if (container.classList.contains("nu-mermaid-fullscreen")) return;
|
|
46
|
+
if (!container.querySelector("svg")) return;
|
|
47
|
+
if (container.querySelector(".nu-mermaid-toolbar")) return;
|
|
48
|
+
|
|
49
|
+
const toolbar = document.createElement("div");
|
|
50
|
+
toolbar.className = "nu-mermaid-toolbar";
|
|
51
|
+
|
|
52
|
+
toolbar.appendChild(createActionButton("Fullscreen", "fullscreen", ICON_PATHS.fullscreen));
|
|
53
|
+
toolbar.appendChild(createActionButton("Download SVG", "download-svg", ICON_PATHS.download));
|
|
54
|
+
|
|
55
|
+
const source = diagramSources.get(container);
|
|
56
|
+
if (source) {
|
|
57
|
+
toolbar.appendChild(createActionButton("Copy Mermaid", "copy-source", ICON_PATHS.code));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
container.style.position = "relative";
|
|
61
|
+
container.appendChild(toolbar);
|
|
62
|
+
injectedCount++;
|
|
63
|
+
|
|
64
|
+
toolbar.addEventListener("click", (event) => {
|
|
65
|
+
const button = (event.target as HTMLElement).closest<HTMLButtonElement>(".nu-mermaid-btn");
|
|
66
|
+
if (!button) return;
|
|
67
|
+
event.stopPropagation();
|
|
68
|
+
|
|
69
|
+
const svg = container.querySelector("svg");
|
|
70
|
+
if (!svg) return;
|
|
71
|
+
|
|
72
|
+
const isKeyboardActivated = event.detail === 0;
|
|
73
|
+
|
|
74
|
+
switch (button.dataset.action) {
|
|
75
|
+
case "fullscreen":
|
|
76
|
+
openFullscreen(svg, container, button, isKeyboardActivated);
|
|
77
|
+
break;
|
|
78
|
+
case "download-svg":
|
|
79
|
+
downloadDiagramSvg(svg, container);
|
|
80
|
+
flashSuccess(button, "Downloaded!");
|
|
81
|
+
break;
|
|
82
|
+
case "copy-source":
|
|
83
|
+
if (source) writeToClipboard(source, button);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return injectedCount;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isToolbarEnabled(): boolean {
|
|
93
|
+
return window.__NU_MERMAID_TOOLBAR__ !== false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Render diagrams and inject toolbars. Called on initial page load
|
|
98
|
+
* and after Astro view transitions.
|
|
99
|
+
*/
|
|
100
|
+
let domObserver: MutationObserver | undefined;
|
|
101
|
+
|
|
102
|
+
function onPageReady(): void {
|
|
103
|
+
domObserver?.disconnect();
|
|
104
|
+
|
|
105
|
+
void renderAllDiagrams(false).then(() => {
|
|
106
|
+
if (isToolbarEnabled()) injectToolbars();
|
|
107
|
+
|
|
108
|
+
// Watch for lazy-rendered diagrams that enter the viewport after initial load
|
|
109
|
+
domObserver = new MutationObserver(() => {
|
|
110
|
+
if (isToolbarEnabled()) injectToolbars();
|
|
111
|
+
});
|
|
112
|
+
domObserver.observe(document.body, { childList: true, subtree: true });
|
|
113
|
+
setTimeout(() => domObserver?.disconnect(), TOOLBAR_OBSERVER_TIMEOUT_MS);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Re-render all diagrams and re-inject toolbars when the theme changes
|
|
118
|
+
const themeObserver = new MutationObserver(() => {
|
|
119
|
+
void renderAllDiagrams(true).then(() => {
|
|
120
|
+
if (isToolbarEnabled()) injectToolbars();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
themeObserver.observe(document.documentElement, {
|
|
125
|
+
attributes: true,
|
|
126
|
+
attributeFilter: ["data-theme"],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Initial page load
|
|
130
|
+
if (document.readyState === "loading") {
|
|
131
|
+
document.addEventListener("DOMContentLoaded", onPageReady);
|
|
132
|
+
} else {
|
|
133
|
+
onPageReady();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Astro view transitions
|
|
137
|
+
document.addEventListener("astro:page-load", onPageReady);
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared UI primitives for the Mermaid toolbar and fullscreen viewer.
|
|
3
|
+
*
|
|
4
|
+
* Pure factories and helpers with no DOM querying or lifecycle logic.
|
|
5
|
+
* Everything here is stateless except the success timer and toast tracking,
|
|
6
|
+
* which are scoped to individual button/container instances.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { diagramSources } from "./render";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// SVG icon paths (Lucide-compatible, 24x24 viewBox)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/** SVG path data for toolbar and overlay icons. */
|
|
16
|
+
export const ICON_PATHS = {
|
|
17
|
+
fullscreen: ["M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3"],
|
|
18
|
+
download: ["M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4", "M7 10l5 5 5-5", "M12 15V3"],
|
|
19
|
+
close: ["M18 6L6 18M6 6l12 12"],
|
|
20
|
+
zoomIn: ["M12 5v14", "M5 12h14"],
|
|
21
|
+
zoomOut: ["M5 12h14"],
|
|
22
|
+
reset: ["M3 12a9 9 0 1018 0 9 9 0 00-18 0z", "M12 8v4l2 2"],
|
|
23
|
+
copy: [
|
|
24
|
+
"M8 4H6a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2v-2",
|
|
25
|
+
"M16 4h2a2 2 0 012 2v6a2 2 0 01-2 2h-8a2 2 0 01-2-2V6a2 2 0 012-2",
|
|
26
|
+
],
|
|
27
|
+
code: ["M16 18l6-6-6-6", "M8 6l-6 6 6 6"],
|
|
28
|
+
check: ["M20 6L9 17l-5-5"],
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Icon and button factories
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/** Create a stroked SVG icon from an array of path `d` attributes. */
|
|
38
|
+
export function createIcon(paths: readonly string[], size = 14): SVGElement {
|
|
39
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
40
|
+
const attrs: Record<string, string | number> = {
|
|
41
|
+
width: size,
|
|
42
|
+
height: size,
|
|
43
|
+
viewBox: "0 0 24 24",
|
|
44
|
+
fill: "none",
|
|
45
|
+
stroke: "currentColor",
|
|
46
|
+
"stroke-width": "2",
|
|
47
|
+
"stroke-linecap": "round",
|
|
48
|
+
"stroke-linejoin": "round",
|
|
49
|
+
};
|
|
50
|
+
for (const [attr, value] of Object.entries(attrs)) {
|
|
51
|
+
svg.setAttribute(attr, String(value));
|
|
52
|
+
}
|
|
53
|
+
for (const d of paths) {
|
|
54
|
+
const path = document.createElementNS(SVG_NS, "path");
|
|
55
|
+
path.setAttribute("d", d);
|
|
56
|
+
svg.appendChild(path);
|
|
57
|
+
}
|
|
58
|
+
return svg;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Create a zoom-specific icon with a magnifying glass circle + handle. */
|
|
62
|
+
function createZoomIcon(innerPaths: readonly string[]): SVGElement {
|
|
63
|
+
const svg = createIcon(innerPaths, 14);
|
|
64
|
+
const circle = document.createElementNS(SVG_NS, "circle");
|
|
65
|
+
circle.setAttribute("cx", "11");
|
|
66
|
+
circle.setAttribute("cy", "11");
|
|
67
|
+
circle.setAttribute("r", "8");
|
|
68
|
+
svg.insertBefore(circle, svg.firstChild);
|
|
69
|
+
const handle = document.createElementNS(SVG_NS, "path");
|
|
70
|
+
handle.setAttribute("d", "M21 21l-4.35-4.35");
|
|
71
|
+
svg.insertBefore(handle, svg.children[1]);
|
|
72
|
+
return svg;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Create an accessible toolbar button with an icon and a `data-action` attribute. */
|
|
76
|
+
export function createActionButton(
|
|
77
|
+
label: string,
|
|
78
|
+
action: string,
|
|
79
|
+
iconPaths: readonly string[],
|
|
80
|
+
className = "nu-mermaid-btn",
|
|
81
|
+
): HTMLButtonElement {
|
|
82
|
+
const button = document.createElement("button");
|
|
83
|
+
button.className = className;
|
|
84
|
+
button.dataset.action = action;
|
|
85
|
+
button.title = label;
|
|
86
|
+
button.setAttribute("aria-label", label);
|
|
87
|
+
button.appendChild(createIcon(iconPaths));
|
|
88
|
+
return button;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Create a zoom-specific button with a magnifying glass icon. */
|
|
92
|
+
export function createZoomButton(
|
|
93
|
+
label: string,
|
|
94
|
+
action: string,
|
|
95
|
+
innerPaths: readonly string[],
|
|
96
|
+
className = "nu-mermaid-btn",
|
|
97
|
+
): HTMLButtonElement {
|
|
98
|
+
const button = document.createElement("button");
|
|
99
|
+
button.className = className;
|
|
100
|
+
button.dataset.action = action;
|
|
101
|
+
button.title = label;
|
|
102
|
+
button.setAttribute("aria-label", label);
|
|
103
|
+
button.appendChild(createZoomIcon(innerPaths));
|
|
104
|
+
return button;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Button success feedback
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
const activeSuccessTimers = new WeakMap<HTMLButtonElement, number>();
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Temporarily replace a button's icon with a checkmark and apply the success style.
|
|
115
|
+
*
|
|
116
|
+
* Debounced per button — rapid clicks clear the previous timer and restart.
|
|
117
|
+
* The original icon and title restore after 1500ms.
|
|
118
|
+
*/
|
|
119
|
+
export function flashSuccess(button: HTMLButtonElement, label = "Copied!"): void {
|
|
120
|
+
const previousTimer = activeSuccessTimers.get(button);
|
|
121
|
+
if (previousTimer) {
|
|
122
|
+
clearTimeout(previousTimer);
|
|
123
|
+
button.querySelector(".nu-mermaid-check")?.remove();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const originalTitle = button.title;
|
|
127
|
+
const originalIcon = button.querySelector("svg:not(.nu-mermaid-check)");
|
|
128
|
+
const checkIcon = createIcon(ICON_PATHS.check, 14);
|
|
129
|
+
checkIcon.classList.add("nu-mermaid-check");
|
|
130
|
+
|
|
131
|
+
button.title = label;
|
|
132
|
+
button.classList.add("nu-mermaid-success");
|
|
133
|
+
if (originalIcon) {
|
|
134
|
+
(originalIcon as HTMLElement).style.display = "none";
|
|
135
|
+
button.insertBefore(checkIcon, button.firstChild);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const timer = window.setTimeout(() => {
|
|
139
|
+
activeSuccessTimers.delete(button);
|
|
140
|
+
button.title = originalTitle;
|
|
141
|
+
button.classList.remove("nu-mermaid-success");
|
|
142
|
+
checkIcon.remove();
|
|
143
|
+
if (originalIcon) (originalIcon as HTMLElement).style.display = "";
|
|
144
|
+
}, 1500);
|
|
145
|
+
activeSuccessTimers.set(button, timer);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Toast notifications
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
let currentToast: HTMLElement | null = null;
|
|
153
|
+
let currentToastTimer = 0;
|
|
154
|
+
|
|
155
|
+
/** Display a toast message in the bottom-right of a container. Replaces any active toast. */
|
|
156
|
+
export function showToast(container: HTMLElement, message: string): void {
|
|
157
|
+
if (currentToast) {
|
|
158
|
+
clearTimeout(currentToastTimer);
|
|
159
|
+
currentToast.remove();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const toast = document.createElement("div");
|
|
163
|
+
toast.className = "nu-mermaid-toast";
|
|
164
|
+
toast.textContent = message;
|
|
165
|
+
container.appendChild(toast);
|
|
166
|
+
currentToast = toast;
|
|
167
|
+
|
|
168
|
+
requestAnimationFrame(() => toast.classList.add("nu-mermaid-toast-visible"));
|
|
169
|
+
|
|
170
|
+
currentToastTimer = window.setTimeout(() => {
|
|
171
|
+
toast.classList.remove("nu-mermaid-toast-visible");
|
|
172
|
+
setTimeout(() => {
|
|
173
|
+
toast.remove();
|
|
174
|
+
if (currentToast === toast) currentToast = null;
|
|
175
|
+
}, 200);
|
|
176
|
+
}, 2000);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Clipboard
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Copy text to the clipboard with a visual success flash on the button.
|
|
185
|
+
*
|
|
186
|
+
* Uses the Clipboard API when available (HTTPS / localhost). Falls back to
|
|
187
|
+
* `document.execCommand("copy")` via a hidden textarea for insecure contexts
|
|
188
|
+
* (e.g., HTTP over LAN). The textarea is appended inside the closest overlay
|
|
189
|
+
* to prevent iOS Safari's URL bar from toggling.
|
|
190
|
+
*/
|
|
191
|
+
export async function writeToClipboard(text: string, triggerButton: HTMLButtonElement): Promise<void> {
|
|
192
|
+
try {
|
|
193
|
+
await navigator.clipboard.writeText(text);
|
|
194
|
+
flashSuccess(triggerButton, "Copied!");
|
|
195
|
+
} catch {
|
|
196
|
+
const host = triggerButton.closest(".nu-mermaid-overlay") ?? document.body;
|
|
197
|
+
const textarea = document.createElement("textarea");
|
|
198
|
+
textarea.value = text;
|
|
199
|
+
textarea.setAttribute("readonly", "");
|
|
200
|
+
textarea.style.cssText = "position:absolute;left:-9999px;opacity:0;height:0";
|
|
201
|
+
host.appendChild(textarea);
|
|
202
|
+
textarea.select();
|
|
203
|
+
const success = document.execCommand("copy");
|
|
204
|
+
textarea.remove();
|
|
205
|
+
if (success) flashSuccess(triggerButton, "Copied!");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// SVG download
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Download the diagram SVG with a descriptive filename derived from
|
|
215
|
+
* the site title, page slug, and diagram type.
|
|
216
|
+
*/
|
|
217
|
+
export function downloadDiagramSvg(svg: SVGElement, container: Element): void {
|
|
218
|
+
const clone = svg.cloneNode(true) as SVGElement;
|
|
219
|
+
clone.setAttribute("xmlns", SVG_NS);
|
|
220
|
+
const blob = new Blob([clone.outerHTML], { type: "image/svg+xml" });
|
|
221
|
+
const url = URL.createObjectURL(blob);
|
|
222
|
+
const anchor = document.createElement("a");
|
|
223
|
+
anchor.href = url;
|
|
224
|
+
anchor.download = buildDownloadFilename(container);
|
|
225
|
+
anchor.click();
|
|
226
|
+
URL.revokeObjectURL(url);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Filename generation (private)
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
function slugify(text: string): string {
|
|
234
|
+
return text
|
|
235
|
+
.toLowerCase()
|
|
236
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
237
|
+
.replace(/^-|-$/g, "");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function extractSiteSlug(): string {
|
|
241
|
+
const segments = document.title.split("|");
|
|
242
|
+
return slugify((segments.length > 1 ? segments.at(-1)! : segments[0]).trim());
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function extractPageSlug(): string {
|
|
246
|
+
return slugify(window.location.pathname.replace(/^\/|\/$/g, "")) || "index";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function detectDiagramType(container: Element): string {
|
|
250
|
+
const source = diagramSources.get(container) ?? "";
|
|
251
|
+
const keyword = source.trim().split(/[\s\n]/)[0];
|
|
252
|
+
return slugify(keyword.replace(/[-_](v\d+|beta)$/i, "").replace(/(diagram|chart)$/i, "")) || "diagram";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function buildDownloadFilename(container: Element): string {
|
|
256
|
+
const site = extractSiteSlug();
|
|
257
|
+
const page = extractPageSlug();
|
|
258
|
+
const type = detectDiagramType(container);
|
|
259
|
+
|
|
260
|
+
const allDiagrams = [...document.querySelectorAll<HTMLElement>(".mermaid:not(.nu-mermaid-fullscreen)")];
|
|
261
|
+
const sameType = allDiagrams.filter((d) => detectDiagramType(d) === type);
|
|
262
|
+
const disambiguator = sameType.length > 1 ? `-${sameType.indexOf(container as HTMLElement) + 1}` : "";
|
|
263
|
+
|
|
264
|
+
return `${site}_${page}-${type}${disambiguator}.svg`;
|
|
265
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
Accessibility — High contrast and reduced transparency overrides
|
|
3
|
+
========================================================================== */
|
|
4
|
+
|
|
5
|
+
@layer northwestern {
|
|
6
|
+
@media (prefers-contrast: more) {
|
|
7
|
+
:root,
|
|
8
|
+
::backdrop {
|
|
9
|
+
--nu-border-color: CanvasText;
|
|
10
|
+
--nu-color-text-muted: CanvasText;
|
|
11
|
+
--nu-focus-ring: Highlight;
|
|
12
|
+
--nu-focus-border: Highlight;
|
|
13
|
+
--nu-focus-shadow-color: transparent;
|
|
14
|
+
--nu-hover-bg: Mark;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
:root[data-theme="light"],
|
|
18
|
+
[data-theme="light"] ::backdrop {
|
|
19
|
+
--nu-border-color: CanvasText;
|
|
20
|
+
--nu-color-text-muted: CanvasText;
|
|
21
|
+
--nu-focus-ring: Highlight;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.sl-markdown-content table:not(:where(.not-content *)) > thead > tr > th,
|
|
25
|
+
.sl-markdown-content table:not(:where(.not-content *)) > tbody > tr > td {
|
|
26
|
+
border-width: 2px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.starlight-aside {
|
|
30
|
+
border-width: 2px !important;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.sl-markdown-content blockquote:not(:where(.not-content *)) {
|
|
34
|
+
border-inline-start-width: 6px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.sl-badge {
|
|
38
|
+
border-width: 2px;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
43
|
+
:root,
|
|
44
|
+
::backdrop {
|
|
45
|
+
--nu-purple-tint: var(--nu-purple-10);
|
|
46
|
+
--nu-purple-tint-strong: var(--nu-purple-20);
|
|
47
|
+
--nu-purple-surface: #f3f0f7;
|
|
48
|
+
--nu-purple-surface-subtle: #f7f5f9;
|
|
49
|
+
--nu-surface-muted: #f5f5f5;
|
|
50
|
+
--nu-hover-bg: #ebebeb;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
:root:not([data-theme="light"]),
|
|
54
|
+
:root:not([data-theme="light"]) ::backdrop {
|
|
55
|
+
--nu-purple-surface: #2a2235;
|
|
56
|
+
--nu-purple-surface-subtle: #231d2e;
|
|
57
|
+
--nu-surface-muted: #262626;
|
|
58
|
+
--nu-hover-bg: #333;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.sl-markdown-content blockquote:not(:where(.not-content *)) {
|
|
62
|
+
background: var(--nu-purple-surface);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
:root:not([data-theme="light"]) .sl-markdown-content blockquote:not(:where(.not-content *)) {
|
|
66
|
+
background: var(--nu-purple-surface);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
Blockquotes — Styled quotations with cite/footer support
|
|
3
|
+
========================================================================== */
|
|
4
|
+
|
|
5
|
+
@layer northwestern {
|
|
6
|
+
.sl-markdown-content blockquote:not(:where(.not-content *)) {
|
|
7
|
+
padding: 1rem 1.5rem;
|
|
8
|
+
margin: 2rem 0;
|
|
9
|
+
color: var(--nu-color-text);
|
|
10
|
+
background: rgb(228 224 238 / 42%);
|
|
11
|
+
border-inline-start: 10px solid var(--nu-purple-100);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.sl-markdown-content blockquote:not(:where(.not-content *)) p:last-child {
|
|
15
|
+
margin-bottom: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.sl-markdown-content blockquote:not(:where(.not-content *)) cite,
|
|
19
|
+
.sl-markdown-content blockquote:not(:where(.not-content *)) footer {
|
|
20
|
+
display: block;
|
|
21
|
+
margin-top: 0.75rem;
|
|
22
|
+
font-size: var(--sl-text-sm);
|
|
23
|
+
font-style: normal;
|
|
24
|
+
font-weight: 700;
|
|
25
|
+
color: var(--nu-color-link);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
:root:not([data-theme="light"]) .sl-markdown-content blockquote:not(:where(.not-content *)) {
|
|
29
|
+
background: rgb(48 16 78 / 35%);
|
|
30
|
+
border-inline-start-color: var(--nu-purple-40);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
:root:not([data-theme="light"]) .sl-markdown-content blockquote:not(:where(.not-content *)) cite,
|
|
34
|
+
:root:not([data-theme="light"]) .sl-markdown-content blockquote:not(:where(.not-content *)) footer {
|
|
35
|
+
color: var(--nu-purple-30);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -35,10 +35,6 @@
|
|
|
35
35
|
font-weight: 600;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
.expressive-code .copy button {
|
|
39
|
-
border-radius: 0;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
38
|
.expressive-code .frame.has-title:not(.is-terminal) .header {
|
|
43
39
|
display: flex;
|
|
44
40
|
align-items: end;
|
|
@@ -77,7 +73,6 @@
|
|
|
77
73
|
height: 6px;
|
|
78
74
|
content: "";
|
|
79
75
|
background-color: #bbb4c7;
|
|
80
|
-
border-radius: 999px;
|
|
81
76
|
box-shadow:
|
|
82
77
|
10px 0 0 #bbb4c7,
|
|
83
78
|
20px 0 0 #bbb4c7;
|
|
@@ -134,6 +129,10 @@
|
|
|
134
129
|
color: #6e6e6e;
|
|
135
130
|
}
|
|
136
131
|
|
|
132
|
+
:root:not([data-theme="light"]) .expressive-code .gutter .ln {
|
|
133
|
+
color: #999;
|
|
134
|
+
}
|
|
135
|
+
|
|
137
136
|
:root:not([data-theme="light"]) .expressive-code .frame {
|
|
138
137
|
border-top-color: var(--nu-purple-40);
|
|
139
138
|
box-shadow: none;
|
|
@@ -165,4 +164,8 @@
|
|
|
165
164
|
background: rgb(41 41 41 / 95%);
|
|
166
165
|
border-color: var(--nu-purple-40);
|
|
167
166
|
}
|
|
167
|
+
|
|
168
|
+
:root:not([data-theme="light"]) .expressive-code .copy button:hover::after {
|
|
169
|
+
background-color: var(--nu-purple-40);
|
|
170
|
+
}
|
|
168
171
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
Footnotes — GFM footnote references, section, and back-references
|
|
3
|
+
========================================================================== */
|
|
4
|
+
|
|
5
|
+
@layer northwestern {
|
|
6
|
+
.sl-markdown-content a[data-footnote-ref] {
|
|
7
|
+
font-weight: 600;
|
|
8
|
+
line-height: 1;
|
|
9
|
+
vertical-align: super;
|
|
10
|
+
color: var(--sl-color-accent);
|
|
11
|
+
text-decoration: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.sl-markdown-content a[data-footnote-ref]::before {
|
|
15
|
+
content: "[";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.sl-markdown-content a[data-footnote-ref]::after {
|
|
19
|
+
content: "]";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.sl-markdown-content a[data-footnote-ref]:hover {
|
|
23
|
+
text-decoration: underline;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Footnotes section */
|
|
27
|
+
.sl-markdown-content section[data-footnotes] {
|
|
28
|
+
padding-top: 1.5rem;
|
|
29
|
+
margin-top: 2.5rem;
|
|
30
|
+
font-size: 0.875rem;
|
|
31
|
+
color: var(--nu-color-text-muted);
|
|
32
|
+
border-top: 1px solid var(--nu-border-color);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.sl-markdown-content section[data-footnotes] > :first-child:is(hr) {
|
|
36
|
+
display: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.sl-markdown-content section[data-footnotes] > .sl-heading-wrapper {
|
|
40
|
+
position: absolute;
|
|
41
|
+
width: 1px;
|
|
42
|
+
height: 1px;
|
|
43
|
+
padding: 0;
|
|
44
|
+
margin: -1px;
|
|
45
|
+
overflow: hidden;
|
|
46
|
+
white-space: nowrap;
|
|
47
|
+
border: 0;
|
|
48
|
+
clip-path: inset(50%);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.sl-markdown-content section[data-footnotes] ol {
|
|
52
|
+
padding-left: 1.5rem;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.sl-markdown-content section[data-footnotes] li {
|
|
56
|
+
padding-left: 0.25rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.sl-markdown-content section[data-footnotes] li + li {
|
|
60
|
+
margin-top: 0.5rem;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.sl-markdown-content section[data-footnotes] a[data-footnote-backref] {
|
|
64
|
+
margin-left: 0.25em;
|
|
65
|
+
font-family: "Segoe UI Symbol", "Noto Sans Symbols 2", sans-serif;
|
|
66
|
+
font-size: 1em;
|
|
67
|
+
font-weight: 700;
|
|
68
|
+
color: var(--sl-color-accent);
|
|
69
|
+
text-decoration: none;
|
|
70
|
+
font-variant-emoji: text;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.sl-markdown-content section[data-footnotes] a[data-footnote-backref]:hover {
|
|
74
|
+
border-bottom: 1px solid var(--sl-color-accent);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
Kbd — Keyboard shortcut display
|
|
3
|
+
========================================================================== */
|
|
4
|
+
|
|
5
|
+
/* stylelint-disable selector-class-pattern */
|
|
6
|
+
|
|
7
|
+
@layer northwestern {
|
|
8
|
+
.nu-kbd {
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
gap: 0.25rem;
|
|
11
|
+
align-items: center;
|
|
12
|
+
font-family: var(--sl-font-system-mono);
|
|
13
|
+
font-size: var(--sl-text-xs);
|
|
14
|
+
line-height: 1;
|
|
15
|
+
white-space: nowrap;
|
|
16
|
+
background: none;
|
|
17
|
+
border: none;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.nu-kbd__key {
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
min-width: 1.375rem;
|
|
25
|
+
height: 1.375rem;
|
|
26
|
+
padding: 0 0.3125rem;
|
|
27
|
+
font-family: inherit;
|
|
28
|
+
font-size: inherit;
|
|
29
|
+
font-weight: 500;
|
|
30
|
+
line-height: 1;
|
|
31
|
+
color: var(--nu-color-text);
|
|
32
|
+
text-align: center;
|
|
33
|
+
background-color: var(--nu-purple-surface-subtle);
|
|
34
|
+
border: 1px solid rgb(78 42 132 / 18%);
|
|
35
|
+
border-bottom-width: 2px;
|
|
36
|
+
border-radius: 3px !important;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:root:not([data-theme="light"]) .nu-kbd__key {
|
|
40
|
+
color: var(--nu-color-text);
|
|
41
|
+
background-color: rgb(255 255 255 / 6%);
|
|
42
|
+
border-color: rgb(255 255 255 / 12%);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.nu-kbd__sep {
|
|
46
|
+
font-size: 0.625rem;
|
|
47
|
+
color: var(--nu-color-text-muted);
|
|
48
|
+
user-select: none;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.nu-kbd__mac,
|
|
52
|
+
.nu-kbd__other {
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
gap: 0.25rem;
|
|
55
|
+
align-items: center;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.nu-kbd__other {
|
|
59
|
+
display: none;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
:root[data-platform="other"] .nu-kbd__mac {
|
|
63
|
+
display: none;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
:root[data-platform="other"] .nu-kbd__other {
|
|
67
|
+
display: inline-flex;
|
|
68
|
+
}
|
|
69
|
+
}
|