@nu-appdev/northwestern-starlight-theme 1.0.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.
@@ -0,0 +1,503 @@
1
+ /**
2
+ * Mermaid Toolbar — Fullscreen viewer with pan/zoom, download, and copy.
3
+ *
4
+ * Features:
5
+ * - Hover toolbar: fullscreen, download SVG, copy mermaid source
6
+ * - Fullscreen overlay: pan/zoom, download, copy, zoom indicator, diagram title
7
+ * - Keyboard: +/- zoom, 0 reset, arrows pan, Escape close
8
+ * - Double-click to zoom in
9
+ * - Smooth open/close animation
10
+ */
11
+
12
+ // Force module scope so `declare global` works correctly
13
+ export {};
14
+
15
+ declare global {
16
+ interface Window {
17
+ __NU_MERMAID_TOOLBAR__?: boolean;
18
+ __NU_MERMAID_CONFIGS__?: {
19
+ light?: Record<string, unknown>;
20
+ dark?: Record<string, unknown>;
21
+ };
22
+ }
23
+ }
24
+
25
+ // ---- Icon paths ----
26
+ const ICONS = {
27
+ 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"],
28
+ download: ["M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4", "M7 10l5 5 5-5", "M12 15V3"],
29
+ close: ["M18 6L6 18M6 6l12 12"],
30
+ zoomIn: ["M12 5v14", "M5 12h14"],
31
+ zoomOut: ["M5 12h14"],
32
+ reset: ["M3 12a9 9 0 1018 0 9 9 0 00-18 0z", "M12 8v4l2 2"],
33
+ copy: [
34
+ "M8 4H6a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2v-2",
35
+ "M16 4h2a2 2 0 012 2v6a2 2 0 01-2 2h-8a2 2 0 01-2-2V6a2 2 0 012-2",
36
+ ],
37
+ code: ["M16 18l6-6-6-6", "M8 6l-6 6 6 6"],
38
+ fitWidth: ["M21 3H3v18h18V3z", "M9 3v18", "M15 3v18"],
39
+ fitHeight: ["M21 3H3v18h18V3z", "M3 9h18", "M3 15h18"],
40
+ };
41
+
42
+ function createSvgIcon(paths: string[], size = 14): SVGElement {
43
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
44
+ svg.setAttribute("width", String(size));
45
+ svg.setAttribute("height", String(size));
46
+ svg.setAttribute("viewBox", "0 0 24 24");
47
+ svg.setAttribute("fill", "none");
48
+ svg.setAttribute("stroke", "currentColor");
49
+ svg.setAttribute("stroke-width", "2");
50
+ svg.setAttribute("stroke-linecap", "round");
51
+ svg.setAttribute("stroke-linejoin", "round");
52
+ for (const d of paths) {
53
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
54
+ path.setAttribute("d", d);
55
+ svg.appendChild(path);
56
+ }
57
+ return svg;
58
+ }
59
+
60
+ function createButton(title: string, action: string, iconPaths: string[], cls = "nu-mermaid-btn"): HTMLButtonElement {
61
+ const btn = document.createElement("button");
62
+ btn.className = cls;
63
+ btn.dataset.action = action;
64
+ btn.title = title;
65
+ btn.setAttribute("aria-label", title);
66
+ btn.appendChild(createSvgIcon(iconPaths));
67
+ return btn;
68
+ }
69
+
70
+ function createZoomIcon(extra: string[]): SVGElement {
71
+ const svg = createSvgIcon(extra, 14);
72
+ const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
73
+ circle.setAttribute("cx", "11");
74
+ circle.setAttribute("cy", "11");
75
+ circle.setAttribute("r", "8");
76
+ svg.insertBefore(circle, svg.firstChild);
77
+ const line = document.createElementNS("http://www.w3.org/2000/svg", "path");
78
+ line.setAttribute("d", "M21 21l-4.35-4.35");
79
+ svg.insertBefore(line, svg.children[1]);
80
+ return svg;
81
+ }
82
+
83
+ function createZoomButton(
84
+ title: string,
85
+ action: string,
86
+ iconExtra: string[],
87
+ cls = "nu-mermaid-btn",
88
+ ): HTMLButtonElement {
89
+ const btn = document.createElement("button");
90
+ btn.className = cls;
91
+ btn.dataset.action = action;
92
+ btn.title = title;
93
+ btn.setAttribute("aria-label", title);
94
+ btn.appendChild(createZoomIcon(iconExtra));
95
+ return btn;
96
+ }
97
+
98
+ type MermaidThemeMode = "light" | "dark";
99
+
100
+ // ---- Store original mermaid source before rendering ----
101
+ const mermaidSources = new Map<Element, string>();
102
+ const renderedThemes = new WeakMap<HTMLElement, MermaidThemeMode>();
103
+ let mermaidRuntimePromise: Promise<typeof import("mermaid")["default"]> | undefined;
104
+
105
+ function currentThemeMode(): MermaidThemeMode {
106
+ return document.documentElement.dataset.theme === "light" ? "light" : "dark";
107
+ }
108
+
109
+ function getRuntimeMermaidConfig(mode: MermaidThemeMode): Record<string, unknown> {
110
+ return window.__NU_MERMAID_CONFIGS__?.[mode] ?? {};
111
+ }
112
+
113
+ async function getMermaidRuntime() {
114
+ mermaidRuntimePromise ??= import("mermaid").then((module) => module.default);
115
+ return mermaidRuntimePromise;
116
+ }
117
+
118
+ function captureSource() {
119
+ document.querySelectorAll<HTMLElement>("pre.mermaid").forEach((pre) => {
120
+ if (mermaidSources.has(pre)) return;
121
+ const source = pre.textContent?.trim() || "";
122
+ if (source) mermaidSources.set(pre, source);
123
+ });
124
+ }
125
+
126
+ async function renderDiagram(container: HTMLElement, mode: MermaidThemeMode, index: number) {
127
+ const source = mermaidSources.get(container) ?? container.textContent?.trim();
128
+ if (!source) return;
129
+ const mermaid = await getMermaidRuntime();
130
+
131
+ mermaid.initialize({
132
+ startOnLoad: false,
133
+ ...(getRuntimeMermaidConfig(mode) as Record<string, unknown>),
134
+ });
135
+
136
+ const renderId = `nu-mermaid-${mode}-${index}-${Math.random().toString(36).slice(2, 8)}`;
137
+ const { svg, bindFunctions } = await mermaid.render(renderId, source);
138
+ container.innerHTML = svg;
139
+ bindFunctions?.(container);
140
+ renderedThemes.set(container, mode);
141
+ }
142
+
143
+ async function renderMermaidDiagrams(force = false) {
144
+ captureSource();
145
+ const mode = currentThemeMode();
146
+ const diagrams = [...document.querySelectorAll<HTMLElement>("pre.mermaid:not(.nu-mermaid-fullscreen)")];
147
+ if (!diagrams.length) return;
148
+
149
+ // If force (theme change), re-render all visible diagrams immediately
150
+ if (force) {
151
+ for (const [index, container] of diagrams.entries()) {
152
+ if (renderedThemes.get(container) === mode && container.querySelector("svg")) continue;
153
+ await renderDiagram(container, mode, index);
154
+ }
155
+ return;
156
+ }
157
+
158
+ // Lazy render: only render diagrams when they scroll into view
159
+ const observer = new IntersectionObserver(
160
+ (entries) => {
161
+ for (const entry of entries) {
162
+ if (!entry.isIntersecting) continue;
163
+ const container = entry.target as HTMLElement;
164
+ const index = diagrams.indexOf(container);
165
+ if (renderedThemes.get(container) === mode && container.querySelector("svg")) continue;
166
+ observer.unobserve(container);
167
+ void renderDiagram(container, mode, index);
168
+ }
169
+ },
170
+ { rootMargin: "200px" },
171
+ );
172
+
173
+ for (const container of diagrams) {
174
+ if (renderedThemes.get(container) === mode && container.querySelector("svg")) continue;
175
+ observer.observe(container);
176
+ }
177
+ }
178
+
179
+ // ---- Success feedback (green check) ----
180
+ function showSuccess(btn: HTMLButtonElement, label = "Copied!") {
181
+ const originalTitle = btn.title;
182
+ const originalIcon = btn.querySelector("svg");
183
+ const check = createSvgIcon(["M20 6L9 17l-5-5"], 14);
184
+
185
+ btn.title = label;
186
+ btn.classList.add("nu-mermaid-success");
187
+ if (originalIcon) {
188
+ originalIcon.style.display = "none";
189
+ btn.insertBefore(check, btn.firstChild);
190
+ }
191
+
192
+ setTimeout(() => {
193
+ btn.title = originalTitle;
194
+ btn.classList.remove("nu-mermaid-success");
195
+ check.remove();
196
+ if (originalIcon) originalIcon.style.display = "";
197
+ }, 1500);
198
+ }
199
+
200
+ // ---- Clipboard helper ----
201
+ async function copyToClipboard(text: string, btn: HTMLButtonElement) {
202
+ try {
203
+ await navigator.clipboard.writeText(text);
204
+ showSuccess(btn, "Copied!");
205
+ } catch {
206
+ // Fallback
207
+ }
208
+ }
209
+
210
+ // ---- Inline toolbar injection ----
211
+ function initMermaidToolbar(): number {
212
+ const diagrams = document.querySelectorAll<HTMLElement>(".mermaid");
213
+ if (!diagrams.length) return 0;
214
+ let injected = 0;
215
+
216
+ diagrams.forEach((container, index) => {
217
+ if (container.classList.contains("nu-mermaid-fullscreen")) return;
218
+ if (!container.querySelector("svg")) return;
219
+ if (container.querySelector(".nu-mermaid-toolbar")) return;
220
+
221
+ const toolbar = document.createElement("div");
222
+ toolbar.className = "nu-mermaid-toolbar";
223
+
224
+ toolbar.appendChild(createButton("Fullscreen", "fullscreen", ICONS.fullscreen));
225
+ toolbar.appendChild(createButton("Download SVG", "download-svg", ICONS.download));
226
+
227
+ const source = mermaidSources.get(container);
228
+ if (source) {
229
+ toolbar.appendChild(createButton("Copy Mermaid", "copy-source", ICONS.code));
230
+ }
231
+
232
+ container.style.position = "relative";
233
+ container.appendChild(toolbar);
234
+ injected++;
235
+
236
+ toolbar.addEventListener("click", (e) => {
237
+ const btn = (e.target as HTMLElement).closest<HTMLButtonElement>(".nu-mermaid-btn");
238
+ if (!btn) return;
239
+ e.stopPropagation();
240
+
241
+ const svg = container.querySelector("svg");
242
+ if (!svg) return;
243
+
244
+ const action = btn.dataset.action;
245
+ if (action === "fullscreen") openFullscreen(svg, container, index);
246
+ else if (action === "download-svg") {
247
+ downloadSvg(svg, index);
248
+ showSuccess(btn, "Downloaded!");
249
+ } else if (action === "copy-source" && source) copyToClipboard(source, btn);
250
+ });
251
+ });
252
+ return injected;
253
+ }
254
+
255
+ // ---- Fullscreen overlay ----
256
+ function openFullscreen(svg: SVGElement, container: HTMLElement, index: number) {
257
+ const overlay = document.createElement("div");
258
+ overlay.className = "nu-mermaid-overlay";
259
+
260
+ // Controls
261
+ const controls = document.createElement("div");
262
+ controls.className = "nu-mermaid-overlay-controls";
263
+
264
+ const btnCls = "nu-mermaid-btn nu-mermaid-overlay-btn";
265
+ controls.appendChild(createZoomButton("Zoom In (+)", "zoom-in", ICONS.zoomIn, btnCls));
266
+ controls.appendChild(createZoomButton("Zoom Out (-)", "zoom-out", ICONS.zoomOut, btnCls));
267
+ controls.appendChild(createButton("Reset (0)", "zoom-reset", ICONS.reset, btnCls));
268
+
269
+ const sep1 = document.createElement("span");
270
+ sep1.className = "nu-mermaid-separator";
271
+ controls.appendChild(sep1);
272
+
273
+ controls.appendChild(createButton("Download SVG", "download-svg", ICONS.download, btnCls));
274
+ controls.appendChild(createButton("Copy SVG", "copy-svg", ICONS.copy, btnCls));
275
+
276
+ const source = mermaidSources.get(container);
277
+ if (source) {
278
+ controls.appendChild(createButton("Copy Mermaid", "copy-source", ICONS.code, btnCls));
279
+ }
280
+
281
+ // Spacer pushes close button to the right
282
+ const spacer = document.createElement("span");
283
+ spacer.className = "nu-mermaid-spacer";
284
+ controls.appendChild(spacer);
285
+
286
+ controls.appendChild(createButton("Close (Esc)", "close", ICONS.close, `${btnCls} nu-mermaid-close`));
287
+
288
+ overlay.appendChild(controls);
289
+
290
+ // Viewport
291
+ const viewport = document.createElement("div");
292
+ viewport.className = "nu-mermaid-viewport";
293
+
294
+ const wrapper = document.createElement("div");
295
+ wrapper.className = "mermaid nu-mermaid-fullscreen";
296
+
297
+ const cloned = svg.cloneNode(true) as SVGElement;
298
+ cloned.removeAttribute("style");
299
+
300
+ // Calculate initial fit
301
+ const viewBox = svg.getAttribute("viewBox");
302
+ let vbWidth = 800;
303
+ let vbHeight = 600;
304
+ if (viewBox) {
305
+ const parts = viewBox.split(/[\s,]+/).map(Number);
306
+ vbWidth = parts[2] || 800;
307
+ vbHeight = parts[3] || 600;
308
+ }
309
+
310
+ function calcFitScale(mode: "both" | "width" | "height" = "both"): number {
311
+ const maxW = window.innerWidth * 0.92;
312
+ const maxH = (window.innerHeight - 120) * 0.92; // account for title + controls
313
+ if (mode === "width") return maxW / vbWidth;
314
+ if (mode === "height") return maxH / vbHeight;
315
+ return Math.min(maxW / vbWidth, maxH / vbHeight);
316
+ }
317
+
318
+ const initialScale = calcFitScale();
319
+ cloned.setAttribute("width", String(Math.round(vbWidth * initialScale)));
320
+ cloned.setAttribute("height", String(Math.round(vbHeight * initialScale)));
321
+
322
+ wrapper.appendChild(cloned);
323
+ viewport.appendChild(wrapper);
324
+ overlay.appendChild(viewport);
325
+
326
+ // Add to DOM with animation
327
+ overlay.style.opacity = "0";
328
+ document.body.appendChild(overlay);
329
+ document.body.style.overflow = "hidden";
330
+ requestAnimationFrame(() => {
331
+ overlay.style.opacity = "1";
332
+ });
333
+
334
+ // Pan + zoom state
335
+ let scale = 1; // relative to initial fit
336
+ let panX = 0;
337
+ let panY = 0;
338
+ let isPanning = false;
339
+ let startX = 0;
340
+ let startY = 0;
341
+
342
+ function updateTransform() {
343
+ wrapper.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
344
+ wrapper.style.transformOrigin = "center center";
345
+ }
346
+
347
+ function zoomTo(newScale: number) {
348
+ scale = Math.min(Math.max(0.1, newScale), 20);
349
+ updateTransform();
350
+ }
351
+
352
+ function resetView() {
353
+ scale = 1;
354
+ panX = 0;
355
+ panY = 0;
356
+ updateTransform();
357
+ }
358
+
359
+ // Mouse wheel zoom
360
+ viewport.addEventListener(
361
+ "wheel",
362
+ (e) => {
363
+ e.preventDefault();
364
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
365
+ zoomTo(scale * delta);
366
+ },
367
+ { passive: false },
368
+ );
369
+
370
+ // Pan with mouse drag
371
+ viewport.addEventListener("mousedown", (e) => {
372
+ isPanning = true;
373
+ startX = e.clientX - panX;
374
+ startY = e.clientY - panY;
375
+ viewport.style.cursor = "grabbing";
376
+ e.preventDefault();
377
+ });
378
+
379
+ const onMouseMove = (e: MouseEvent) => {
380
+ if (!isPanning) return;
381
+ panX = e.clientX - startX;
382
+ panY = e.clientY - startY;
383
+ updateTransform();
384
+ };
385
+
386
+ const onMouseUp = () => {
387
+ isPanning = false;
388
+ viewport.style.cursor = "grab";
389
+ };
390
+
391
+ window.addEventListener("mousemove", onMouseMove);
392
+ window.addEventListener("mouseup", onMouseUp);
393
+
394
+ // Double-click to zoom in
395
+ viewport.addEventListener("dblclick", (e) => {
396
+ e.preventDefault();
397
+ zoomTo(scale * 1.5);
398
+ });
399
+
400
+ // Control buttons
401
+ controls.addEventListener("click", (e) => {
402
+ const btn = (e.target as HTMLElement).closest<HTMLButtonElement>(".nu-mermaid-overlay-btn");
403
+ if (!btn) return;
404
+
405
+ const action = btn.dataset.action;
406
+ if (action === "close") close();
407
+ else if (action === "zoom-in") zoomTo(scale * 1.3);
408
+ else if (action === "zoom-out") zoomTo(scale * 0.7);
409
+ else if (action === "zoom-reset") resetView();
410
+ else if (action === "download-svg") {
411
+ downloadSvg(svg, index);
412
+ showSuccess(btn, "Downloaded!");
413
+ } else if (action === "copy-svg") copyToClipboard(svg.outerHTML, btn);
414
+ else if (action === "copy-source" && source) copyToClipboard(source, btn);
415
+ });
416
+
417
+ // Keyboard shortcuts
418
+ const onKeydown = (e: KeyboardEvent) => {
419
+ if (e.key === "Escape") close();
420
+ else if (e.key === "+" || e.key === "=") zoomTo(scale * 1.2);
421
+ else if (e.key === "-" || e.key === "_") zoomTo(scale * 0.8);
422
+ else if (e.key === "0") resetView();
423
+ else if (e.key === "ArrowUp") {
424
+ panY += 50;
425
+ updateTransform();
426
+ } else if (e.key === "ArrowDown") {
427
+ panY -= 50;
428
+ updateTransform();
429
+ } else if (e.key === "ArrowLeft") {
430
+ panX += 50;
431
+ updateTransform();
432
+ } else if (e.key === "ArrowRight") {
433
+ panX -= 50;
434
+ updateTransform();
435
+ } else return;
436
+ e.preventDefault();
437
+ };
438
+ window.addEventListener("keydown", onKeydown);
439
+
440
+ // Close only on overlay backdrop click (not viewport/svg)
441
+ overlay.addEventListener("click", (e) => {
442
+ if (e.target === overlay) close();
443
+ });
444
+
445
+ function close() {
446
+ overlay.style.opacity = "0";
447
+ setTimeout(() => {
448
+ window.removeEventListener("keydown", onKeydown);
449
+ window.removeEventListener("mousemove", onMouseMove);
450
+ window.removeEventListener("mouseup", onMouseUp);
451
+ document.body.style.overflow = "";
452
+ overlay.remove();
453
+ }, 150);
454
+ }
455
+ }
456
+
457
+ // ---- Download SVG ----
458
+ function downloadSvg(svg: SVGElement, index: number) {
459
+ const clone = svg.cloneNode(true) as SVGElement;
460
+ clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
461
+ const blob = new Blob([clone.outerHTML], { type: "image/svg+xml" });
462
+ const url = URL.createObjectURL(blob);
463
+ const a = document.createElement("a");
464
+ a.href = url;
465
+ a.download = `diagram-${index + 1}.svg`;
466
+ a.click();
467
+ URL.revokeObjectURL(url);
468
+ }
469
+
470
+ // ---- Watch for mermaid renders ----
471
+ function watchForMermaidRender() {
472
+ const toolbarEnabled = window.__NU_MERMAID_TOOLBAR__ !== false;
473
+ // Initial load: lazy render (only diagrams in/near viewport)
474
+ void renderMermaidDiagrams(false).then(() => {
475
+ if (toolbarEnabled) initMermaidToolbar();
476
+
477
+ // Re-check for toolbars as lazy diagrams render
478
+ const toolbarObserver = new MutationObserver(() => {
479
+ if (toolbarEnabled) initMermaidToolbar();
480
+ });
481
+ toolbarObserver.observe(document.body, { childList: true, subtree: true });
482
+ setTimeout(() => toolbarObserver.disconnect(), 30000);
483
+ });
484
+ }
485
+
486
+ const themeObserver = new MutationObserver(() => {
487
+ void renderMermaidDiagrams(true).then(() => {
488
+ if (window.__NU_MERMAID_TOOLBAR__ !== false) initMermaidToolbar();
489
+ });
490
+ });
491
+
492
+ themeObserver.observe(document.documentElement, {
493
+ attributes: true,
494
+ attributeFilter: ["data-theme"],
495
+ });
496
+
497
+ if (document.readyState === "loading") {
498
+ document.addEventListener("DOMContentLoaded", watchForMermaidRender);
499
+ } else {
500
+ watchForMermaidRender();
501
+ }
502
+
503
+ document.addEventListener("astro:page-load", watchForMermaidRender);
@@ -0,0 +1,31 @@
1
+ /* ==========================================================================
2
+ Cards — Card Surface Components
3
+
4
+ Encapsulates shared card-like surfaces including generic cards and Starlight
5
+ link cards, along with their hover border treatment.
6
+ ========================================================================== */
7
+
8
+ @layer northwestern {
9
+ :root,
10
+ ::backdrop {
11
+ --nu-card-bg: #292929;
12
+ }
13
+
14
+ :root[data-theme="light"],
15
+ [data-theme="light"] ::backdrop {
16
+ --nu-card-bg: var(--nu-sidebar-bg);
17
+ }
18
+
19
+ .card,
20
+ .sl-link-card {
21
+ background-color: var(--nu-card-bg) !important;
22
+ }
23
+
24
+ .sl-link-card:hover {
25
+ border-color: var(--nu-purple-60) !important;
26
+ }
27
+
28
+ :root:not([data-theme="light"]) .sl-link-card:hover {
29
+ border-color: var(--nu-purple-40) !important;
30
+ }
31
+ }
@@ -0,0 +1,168 @@
1
+ /* ==========================================================================
2
+ Code — Expressive Code Frames and Controls
3
+
4
+ Encapsulates standalone code block and terminal styling, including frame
5
+ chrome, headers, copy controls, and dark-mode variants.
6
+ ========================================================================== */
7
+
8
+ @layer northwestern {
9
+ /* stylelint-disable custom-property-pattern, no-duplicate-selectors, declaration-block-no-duplicate-properties */
10
+
11
+ .expressive-code .frame {
12
+ overflow: hidden;
13
+ border: 1px solid var(--nu-border-color);
14
+ border-top: 4px solid var(--nu-purple-100);
15
+ box-shadow: none;
16
+ }
17
+
18
+ .expressive-code .frame.has-title:not(.is-terminal) {
19
+ border-top-width: 3px;
20
+ }
21
+
22
+ .expressive-code .header {
23
+ min-height: 0;
24
+ }
25
+
26
+ .expressive-code .frame.has-title:not(.is-terminal) .header,
27
+ .expressive-code .frame.is-terminal .header {
28
+ background: var(--nu-purple-surface);
29
+ border-bottom: 1px solid var(--nu-border-color);
30
+ }
31
+
32
+ .expressive-code .frame.has-title:not(.is-terminal) .title,
33
+ .expressive-code .frame.is-terminal .header .title {
34
+ font-family: var(--__sl-font-mono);
35
+ font-weight: 600;
36
+ }
37
+
38
+ .expressive-code .copy button {
39
+ border-radius: 0;
40
+ }
41
+
42
+ .expressive-code .frame.has-title:not(.is-terminal) .header {
43
+ display: flex;
44
+ align-items: end;
45
+ padding-inline-start: 0;
46
+ }
47
+
48
+ .expressive-code .frame.has-title:not(.is-terminal) .header::before {
49
+ content: none;
50
+ }
51
+
52
+ .expressive-code .frame.has-title:not(.is-terminal) .title {
53
+ padding: 0.5rem 0.9rem 0.45rem;
54
+ margin-block-start: 0;
55
+ font-size: 0.8rem;
56
+ color: var(--nu-black-80);
57
+ background: transparent;
58
+ border: 0;
59
+ }
60
+
61
+ .expressive-code .frame.has-title:not(.is-terminal) .title::after {
62
+ content: none;
63
+ }
64
+
65
+ .expressive-code .frame.is-terminal .header {
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ padding: 0.45rem 3.25rem 0.45rem 0.9rem;
70
+ }
71
+
72
+ .expressive-code .frame.is-terminal .header::before {
73
+ position: absolute;
74
+ top: 50%;
75
+ left: 0.9rem;
76
+ width: 6px;
77
+ height: 6px;
78
+ content: "";
79
+ background-color: #bbb4c7;
80
+ border-radius: 999px;
81
+ box-shadow:
82
+ 10px 0 0 #bbb4c7,
83
+ 20px 0 0 #bbb4c7;
84
+ opacity: 1;
85
+ mask-image: none;
86
+ mask-image: none;
87
+ transform: translateY(-50%);
88
+ }
89
+
90
+ .expressive-code .frame.is-terminal .header::after {
91
+ content: none;
92
+ }
93
+
94
+ .expressive-code .frame.is-terminal .header .title {
95
+ margin-left: 0;
96
+ font-size: 0.78rem;
97
+ letter-spacing: 0.02em;
98
+ }
99
+
100
+ .expressive-code .copy {
101
+ top: 0.45rem;
102
+ right: 0.45rem;
103
+ bottom: auto;
104
+ z-index: 2;
105
+ }
106
+
107
+ .expressive-code .frame.has-title .copy,
108
+ .expressive-code .frame.is-terminal .copy {
109
+ top: calc(0.45rem + 2.2rem);
110
+ }
111
+
112
+ .expressive-code .copy button {
113
+ width: 2rem;
114
+ height: 2rem;
115
+ background: rgb(255 255 255 / 92%);
116
+ border: 1px solid var(--nu-border-color);
117
+ }
118
+
119
+ .expressive-code .copy button:hover {
120
+ background: #fff;
121
+ border-color: var(--nu-black-50);
122
+ }
123
+
124
+ .expressive-code .copy button:hover::after {
125
+ background-color: var(--nu-purple-100);
126
+ }
127
+
128
+ .expressive-code pre {
129
+ padding-block: 0.2rem;
130
+ margin-block: 0 !important;
131
+ }
132
+
133
+ .expressive-code .gutter .ln {
134
+ color: #6e6e6e;
135
+ }
136
+
137
+ :root:not([data-theme="light"]) .expressive-code .frame {
138
+ border-top-color: var(--nu-purple-40);
139
+ box-shadow: none;
140
+ }
141
+
142
+ :root:not([data-theme="light"]) .expressive-code .header,
143
+ :root:not([data-theme="light"]) .expressive-code .frame.has-title:not(.is-terminal) .header {
144
+ background: rgb(78 42 132 / 14%);
145
+ border-bottom-color: var(--nu-border-color);
146
+ }
147
+
148
+ :root:not([data-theme="light"]) .expressive-code .frame.has-title:not(.is-terminal) .title {
149
+ color: var(--nu-color-text);
150
+ }
151
+
152
+ :root:not([data-theme="light"]) .expressive-code .frame.is-terminal .header::before {
153
+ background-color: #74668b;
154
+ box-shadow:
155
+ 10px 0 0 #74668b,
156
+ 20px 0 0 #74668b;
157
+ }
158
+
159
+ :root:not([data-theme="light"]) .expressive-code .copy button {
160
+ background: rgb(24 24 27 / 88%);
161
+ border-color: var(--nu-border-color);
162
+ }
163
+
164
+ :root:not([data-theme="light"]) .expressive-code .copy button:hover {
165
+ background: rgb(41 41 41 / 95%);
166
+ border-color: var(--nu-purple-40);
167
+ }
168
+ }