@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.
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@nu-appdev/northwestern-starlight-theme",
3
+ "version": "1.0.0",
4
+ "description": "A Northwestern-branded theme for Astro Starlight",
5
+ "license": "MIT",
6
+ "author": "Danny Foster <danny@northwestern.edu>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme.git",
10
+ "directory": "packages/northwestern-starlight-theme"
11
+ },
12
+ "homepage": "https://starlight-theme.entapp.northwestern.edu",
13
+ "bugs": {
14
+ "url": "https://github.com/NIT-Administrative-Systems/northwestern-starlight-theme/issues"
15
+ },
16
+ "keywords": [
17
+ "astro",
18
+ "starlight",
19
+ "theme",
20
+ "northwestern",
21
+ "documentation",
22
+ "plugin"
23
+ ],
24
+ "type": "module",
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "provenance": true
28
+ },
29
+ "exports": {
30
+ ".": "./index.ts",
31
+ "./expressive-code": "./expressive-code.mjs",
32
+ "./mermaid": "./mermaid.ts",
33
+ "./src/styles/components/*": "./src/styles/components/*",
34
+ "./src/components/*": "./src/components/*",
35
+ "./src/scripts/*": "./src/scripts/*",
36
+ "./src/styles/*": "./src/styles/*"
37
+ },
38
+ "files": [
39
+ "CHANGELOG.md",
40
+ "expressive-code.mjs",
41
+ "index.ts",
42
+ "mermaid.ts",
43
+ "src/"
44
+ ],
45
+ "dependencies": {
46
+ "@expressive-code/plugin-line-numbers": "^0.41.7",
47
+ "khroma": "^2.1.0"
48
+ },
49
+ "peerDependencies": {
50
+ "@astrojs/starlight": ">=0.32.0",
51
+ "astro": ">=5.1.5",
52
+ "astro-mermaid": ">=1.3.0",
53
+ "mermaid": ">=11.0.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "astro-mermaid": {
57
+ "optional": true
58
+ },
59
+ "mermaid": {
60
+ "optional": true
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,7 @@
1
+ ---
2
+ import EditLink from "@astrojs/starlight/components/EditLink.astro";
3
+
4
+ const isHomepage = Astro.locals.starlightRoute.id === "";
5
+ ---
6
+
7
+ {isHomepage ? <div /> : <EditLink {...Astro.props} />}
@@ -0,0 +1,252 @@
1
+ ---
2
+ import { Image } from "astro:assets";
3
+ import themeConfig from "virtual:northwestern-theme/config";
4
+ import { LinkButton } from "@astrojs/starlight/components";
5
+
6
+ const PAGE_TITLE_ID = "_top";
7
+ const { data } = Astro.locals.starlightRoute.entry;
8
+ const { title = data.title, tagline, image, actions = [] } = data.hero || {};
9
+
10
+ const imageAttrs = {
11
+ loading: "eager" as const,
12
+ decoding: "async" as const,
13
+ width: 400,
14
+ height: 400,
15
+ alt: image?.alt || "",
16
+ };
17
+
18
+ let darkImage: ImageMetadata | undefined;
19
+ let lightImage: ImageMetadata | undefined;
20
+ let rawHtml: string | undefined;
21
+ if (image) {
22
+ if ("file" in image) {
23
+ darkImage = image.file;
24
+ } else if ("dark" in image) {
25
+ darkImage = image.dark;
26
+ lightImage = image.light;
27
+ } else {
28
+ rawHtml = image.html;
29
+ }
30
+ }
31
+
32
+ const hasImage = Boolean(darkImage || lightImage || rawHtml);
33
+ const isSplit = hasImage && themeConfig.homepage.layout === "split";
34
+ const showTitle = themeConfig.homepage.showTitle;
35
+ ---
36
+
37
+ <div class:list={["hero", { "hero--split": isSplit }]}>
38
+ {
39
+ isSplit ? (
40
+ <>
41
+ <div class="sl-flex stack">
42
+ <div class="sl-flex copy">
43
+ {showTitle && <h1 id={PAGE_TITLE_ID} data-page-title set:html={title} />}
44
+ {tagline && <div class="tagline" set:html={tagline} />}
45
+ </div>
46
+ {actions.length > 0 && (
47
+ <div class="sl-flex actions">
48
+ {actions.map(({ icon, link: href, text, variant }) => (
49
+ <LinkButton href={href} variant={variant} icon={icon?.name || icon}>
50
+ {text}
51
+ </LinkButton>
52
+ ))}
53
+ </div>
54
+ )}
55
+ </div>
56
+ <div class="hero-image-container">
57
+ {darkImage && (
58
+ <Image
59
+ src={darkImage}
60
+ {...imageAttrs}
61
+ class:list={["hero-image", { "light:sl-hidden": Boolean(lightImage) }]}
62
+ />
63
+ )}
64
+ {lightImage && <Image src={lightImage} {...imageAttrs} class="hero-image dark:sl-hidden" />}
65
+ {rawHtml && <div class="hero-html sl-flex" set:html={rawHtml} />}
66
+ </div>
67
+ </>
68
+ ) : (
69
+ <div class="sl-flex stack">
70
+ {hasImage && (
71
+ <div class="hero-image-centered">
72
+ {darkImage && (
73
+ <Image
74
+ src={darkImage}
75
+ {...imageAttrs}
76
+ class:list={["hero-image", { "light:sl-hidden": Boolean(lightImage) }]}
77
+ />
78
+ )}
79
+ {lightImage && <Image src={lightImage} {...imageAttrs} class="hero-image dark:sl-hidden" />}
80
+ {rawHtml && <div class="hero-html sl-flex" set:html={rawHtml} />}
81
+ </div>
82
+ )}
83
+ <div class="sl-flex copy">
84
+ <h1 id={PAGE_TITLE_ID} data-page-title set:html={title} />
85
+ {tagline && <div class="tagline" set:html={tagline} />}
86
+ </div>
87
+ {actions.length > 0 && (
88
+ <div class="sl-flex actions">
89
+ {actions.map(({ icon, link: href, text, variant }) => (
90
+ <LinkButton href={href} variant={variant} icon={icon?.name || icon}>
91
+ {text}
92
+ </LinkButton>
93
+ ))}
94
+ </div>
95
+ )}
96
+ </div>
97
+ )
98
+ }
99
+ </div>
100
+
101
+ <style>
102
+ /* ---- Base / Centered layout ---- */
103
+ .hero {
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ padding-block: clamp(2rem, calc(1rem + 10vmin), 8rem);
108
+ }
109
+
110
+ .stack {
111
+ flex-direction: column;
112
+ gap: clamp(1.5rem, calc(1.5rem + 1vw), 2.5rem);
113
+ text-align: center;
114
+ align-items: center;
115
+ }
116
+
117
+ .copy {
118
+ flex-direction: column;
119
+ gap: 1rem;
120
+ align-items: center;
121
+ }
122
+
123
+ .copy > * {
124
+ max-width: 50ch;
125
+ }
126
+
127
+ h1 {
128
+ font-size: clamp(var(--sl-text-3xl), calc(0.25rem + 5vw), var(--sl-text-6xl));
129
+ line-height: var(--sl-line-height-headings);
130
+ font-weight: 600;
131
+ color: var(--nu-purple-100);
132
+ }
133
+
134
+ :root:not([data-theme="light"]) h1 {
135
+ color: var(--sl-color-white);
136
+ }
137
+
138
+ .tagline {
139
+ font-size: clamp(var(--sl-text-base), calc(0.0625rem + 1.5vw), var(--sl-text-lg));
140
+ color: var(--sl-color-gray-2);
141
+ max-width: 90vw;
142
+ }
143
+
144
+ .actions {
145
+ gap: 1rem;
146
+ flex-direction: column;
147
+ align-items: stretch;
148
+ width: 100%;
149
+ max-width: 300px;
150
+ }
151
+
152
+ .actions :global(a) {
153
+ width: auto;
154
+ justify-content: center;
155
+ }
156
+
157
+ .hero-image-centered {
158
+ display: flex;
159
+ align-items: center;
160
+ justify-content: center;
161
+ }
162
+
163
+ .hero-image-centered .hero-image,
164
+ .hero-image-centered .hero-html {
165
+ object-fit: contain;
166
+ width: min(90%, 500px);
167
+ height: auto;
168
+ }
169
+
170
+ @media (min-width: 50rem) {
171
+ .hero:not(.hero--split) .tagline {
172
+ white-space: nowrap;
173
+ max-width: none;
174
+ }
175
+
176
+ .actions {
177
+ flex-direction: row;
178
+ align-items: center;
179
+ width: auto;
180
+ max-width: none;
181
+ }
182
+
183
+ .actions :global(a) {
184
+ width: auto;
185
+ }
186
+ }
187
+
188
+ /* ---- Split layout ---- */
189
+ .hero--split {
190
+ display: grid;
191
+ grid-template-columns: 1fr;
192
+ gap: 2rem;
193
+ padding-block: clamp(2rem, calc(1rem + 8vmin), 6rem);
194
+ }
195
+
196
+ .hero--split .stack {
197
+ text-align: center;
198
+ align-items: center;
199
+ order: 2;
200
+ }
201
+
202
+ .hero--split .copy {
203
+ align-items: center;
204
+ }
205
+
206
+ .hero-image-container {
207
+ display: flex;
208
+ align-items: center;
209
+ justify-content: center;
210
+ order: 1;
211
+ }
212
+
213
+ .hero-image-container .hero-image,
214
+ .hero-image-container .hero-html {
215
+ object-fit: contain;
216
+ width: min(60%, 240px);
217
+ height: auto;
218
+ }
219
+
220
+ @media (min-width: 50rem) {
221
+ .hero--split {
222
+ grid-template-columns: 3fr 2fr;
223
+ gap: 3rem;
224
+ align-items: center;
225
+ }
226
+
227
+ .hero--split .stack {
228
+ text-align: start;
229
+ align-items: flex-start;
230
+ order: unset;
231
+ }
232
+
233
+ .hero-image-container {
234
+ order: unset;
235
+ padding: 2rem;
236
+ }
237
+
238
+ .hero--split .copy {
239
+ align-items: flex-start;
240
+ }
241
+
242
+ .hero--split .actions {
243
+ justify-content: flex-start;
244
+ }
245
+
246
+ .hero-image-container .hero-image,
247
+ .hero-image-container .hero-html {
248
+ width: 100%;
249
+ max-width: 280px;
250
+ }
251
+ }
252
+ </style>
@@ -0,0 +1,199 @@
1
+ ---
2
+ import crypto from "node:crypto";
3
+
4
+ const id = `northwestern-theme-mask-${crypto.randomBytes(4).toString("hex")}`;
5
+ const accessibleLabel = Astro.locals.t("themeSelect.accessibleLabel");
6
+ ---
7
+
8
+ <northwestern-theme-toggle>
9
+ <button aria-label={accessibleLabel} aria-live="polite" title={accessibleLabel} type="button">
10
+ <svg aria-hidden="true" height="16" viewBox="0 0 24 24" width="16">
11
+ <mask class="moon" id={id}>
12
+ <rect fill="white" height="100%" width="100%" x="0" y="0"></rect>
13
+ <circle cx="24" cy="10" fill="black" r="6"></circle>
14
+ </mask>
15
+ <circle class="sun" cx="12" cy="12" mask={`url(#${id})`} r="6"></circle>
16
+ <g class="sun-beams" stroke="currentColor">
17
+ <line x1="12" x2="12" y1="1" y2="3"></line>
18
+ <line x1="12" x2="12" y1="21" y2="23"></line>
19
+ <line x1="4.22" x2="5.64" y1="4.22" y2="5.64"></line>
20
+ <line x1="18.36" x2="19.78" y1="18.36" y2="19.78"></line>
21
+ <line x1="1" x2="3" y1="12" y2="12"></line>
22
+ <line x1="21" x2="23" y1="12" y2="12"></line>
23
+ <line x1="4.22" x2="5.64" y1="19.78" y2="18.36"></line>
24
+ <line x1="18.36" x2="19.78" y1="5.64" y2="4.22"></line>
25
+ </g>
26
+ </svg>
27
+ </button>
28
+ </northwestern-theme-toggle>
29
+
30
+ <script is:inline>
31
+ StarlightThemeProvider.updatePickers();
32
+ </script>
33
+
34
+ <script>
35
+ type Theme = "auto" | "dark" | "light";
36
+
37
+ const storageKey = "starlight-theme";
38
+
39
+ function parseTheme(theme: unknown): Theme {
40
+ return theme === "auto" || theme === "dark" || theme === "light" ? theme : "auto";
41
+ }
42
+
43
+ function loadTheme(): Theme {
44
+ return parseTheme(typeof localStorage !== "undefined" && localStorage.getItem(storageKey));
45
+ }
46
+
47
+ function storeTheme(theme: Theme): void {
48
+ if (typeof localStorage !== "undefined") {
49
+ localStorage.setItem(storageKey, theme === "light" || theme === "dark" ? theme : "");
50
+ }
51
+ }
52
+
53
+ function getPreferredColorScheme(): Theme {
54
+ return matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
55
+ }
56
+
57
+ function onThemeChange(theme: Theme): void {
58
+ StarlightThemeProvider.updatePickers(theme);
59
+ document.documentElement.dataset.theme = theme === "auto" ? getPreferredColorScheme() : theme;
60
+ storeTheme(theme);
61
+ syncToggleButtons();
62
+ }
63
+
64
+ function syncToggleButtons(): void {
65
+ const currentTheme = parseTheme(document.documentElement.dataset.theme);
66
+ const nextTheme = currentTheme === "dark" ? "light" : "dark";
67
+
68
+ document.querySelectorAll("northwestern-theme-toggle button").forEach((button) => {
69
+ if (button instanceof HTMLButtonElement) {
70
+ button.setAttribute("aria-label", `${button.title}: ${nextTheme}`);
71
+ button.setAttribute("aria-pressed", String(currentTheme === "dark"));
72
+ }
73
+ });
74
+ }
75
+
76
+ matchMedia("(prefers-color-scheme: light)").addEventListener("change", () => {
77
+ if (loadTheme() === "auto") onThemeChange("auto");
78
+ });
79
+
80
+ class NorthwesternThemeToggle extends HTMLElement {
81
+ constructor() {
82
+ super();
83
+ onThemeChange(loadTheme());
84
+
85
+ this.querySelector("button")?.addEventListener("click", () => {
86
+ const theme = parseTheme(document.documentElement.dataset.theme);
87
+ const newTheme = theme === "dark" ? "light" : "dark";
88
+ onThemeChange(newTheme);
89
+ });
90
+ }
91
+ }
92
+
93
+ if (!customElements.get("northwestern-theme-toggle")) {
94
+ customElements.define("northwestern-theme-toggle", NorthwesternThemeToggle);
95
+ }
96
+ </script>
97
+
98
+ <style>
99
+ @layer northwestern {
100
+ northwestern-theme-toggle {
101
+ --nu-theme-toggle-duration: 400ms;
102
+ --nu-theme-toggle-ease: cubic-bezier(0.5, 1.25, 0.75, 1.25);
103
+ align-self: stretch;
104
+ display: inline-flex;
105
+ }
106
+
107
+ northwestern-theme-toggle button {
108
+ display: inline-flex;
109
+ align-items: center;
110
+ justify-content: center;
111
+ min-width: auto;
112
+ height: 100%;
113
+ padding: 0.5rem;
114
+ background-color: transparent;
115
+ border: 1px solid currentcolor;
116
+ color: inherit;
117
+ cursor: pointer;
118
+ }
119
+
120
+ northwestern-theme-toggle svg {
121
+ stroke-linecap: round;
122
+ }
123
+
124
+ northwestern-theme-toggle svg :is(.moon, .sun, .sun-beams) {
125
+ transform-origin: center;
126
+ }
127
+
128
+ northwestern-theme-toggle svg :is(.moon, .sun) {
129
+ fill: currentcolor;
130
+ }
131
+
132
+ northwestern-theme-toggle svg .sun-beams {
133
+ stroke: currentcolor;
134
+ stroke-width: 2px;
135
+ }
136
+
137
+ northwestern-theme-toggle button:focus-visible {
138
+ outline: 2px solid var(--nu-focus-ring);
139
+ outline-offset: 2px;
140
+ }
141
+
142
+ :global([data-theme="dark"]) northwestern-theme-toggle svg .sun {
143
+ transform: scale(1.75);
144
+ }
145
+
146
+ :global([data-theme="dark"]) northwestern-theme-toggle svg .sun-beams {
147
+ opacity: 0;
148
+ }
149
+
150
+ :global([data-theme="dark"]) northwestern-theme-toggle svg .moon circle {
151
+ transform: translateX(-7px);
152
+ }
153
+
154
+ @supports (cx: 1) {
155
+ :global([data-theme="dark"]) northwestern-theme-toggle svg .moon circle {
156
+ cx: 17;
157
+ transform: translateX(0);
158
+ }
159
+ }
160
+
161
+ @media (prefers-reduced-motion: no-preference) {
162
+ northwestern-theme-toggle svg .sun {
163
+ transition: transform var(--nu-theme-toggle-duration) var(--nu-theme-toggle-ease);
164
+ }
165
+
166
+ northwestern-theme-toggle svg .sun-beams {
167
+ transition:
168
+ opacity var(--nu-theme-toggle-duration) ease,
169
+ transform var(--nu-theme-toggle-duration) var(--nu-theme-toggle-ease);
170
+ }
171
+
172
+ northwestern-theme-toggle svg .moon circle {
173
+ transition: transform calc(var(--nu-theme-toggle-duration) / 2) ease-out;
174
+ }
175
+
176
+ @supports (cx: 1) {
177
+ northwestern-theme-toggle svg .moon circle {
178
+ transition: cx calc(var(--nu-theme-toggle-duration) / 2) ease-out;
179
+ }
180
+ }
181
+
182
+ :global([data-theme="dark"]) northwestern-theme-toggle svg .sun {
183
+ transform: scale(1.75);
184
+ transition-duration: calc(var(--nu-theme-toggle-duration) / 2);
185
+ transition-timing-function: ease;
186
+ }
187
+
188
+ :global([data-theme="dark"]) northwestern-theme-toggle svg .sun-beams {
189
+ transform: rotateZ(-25deg);
190
+ transition-duration: calc(var(--nu-theme-toggle-duration) / 4);
191
+ }
192
+
193
+ :global([data-theme="dark"]) northwestern-theme-toggle svg .moon circle {
194
+ transition-delay: calc(var(--nu-theme-toggle-duration) / 4);
195
+ transition-duration: var(--nu-theme-toggle-duration);
196
+ }
197
+ }
198
+ }
199
+ </style>