starlight-theme-bejamas 0.2.0 → 0.2.4

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 CHANGED
@@ -1,5 +1,34 @@
1
1
  # starlight-theme-bejamas
2
2
 
3
+ ## 0.2.4
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`0cac89c`](https://github.com/bejamas/ui/commit/0cac89cf39e0414933d6cacff4e00b29cc8996e9), [`6d9ff06`](https://github.com/bejamas/ui/commit/6d9ff061ba4771e7956f5179c4b15d7e02c0bb64), [`a7eb35f`](https://github.com/bejamas/ui/commit/a7eb35fb24b5a19e4f8aa41d857e93a8005e10ae)]:
8
+ - @bejamas/semantic-icons@0.3.0
9
+
10
+ ## 0.2.3
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [[`d8abe23`](https://github.com/bejamas/ui/commit/d8abe237f179cc6a5be73823a134a6221c62cca5)]:
15
+ - @bejamas/semantic-icons@0.2.0
16
+
17
+ ## 0.2.2
18
+
19
+ ### Patch Changes
20
+
21
+ - [#159](https://github.com/bejamas/ui/pull/159) [`d9fb503`](https://github.com/bejamas/ui/commit/d9fb50331b2692cad08f1653611b952312e6d7fe) Thanks [@Tadzik-2](https://github.com/Tadzik-2)! - Point sidebar group carets down when collapsed and up when expanded.
22
+
23
+ ## 0.2.1
24
+
25
+ ### Patch Changes
26
+
27
+ - [#143](https://github.com/bejamas/ui/pull/143) [`94b6e69`](https://github.com/bejamas/ui/commit/94b6e698d22920f974d263709828cb9cfaa1bce5) Thanks [@Tadzik-2](https://github.com/Tadzik-2)! - Publish semantic icons publicly and validate packed release manifests so the theme installs without workspace protocol or private runtime dependencies.
28
+
29
+ - Updated dependencies [[`94b6e69`](https://github.com/bejamas/ui/commit/94b6e698d22920f974d263709828cb9cfaa1bce5)]:
30
+ - @bejamas/semantic-icons@0.1.1
31
+
3
32
  ## 0.2.0
4
33
 
5
34
  ### Minor Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "starlight-theme-bejamas",
3
3
  "author": "Bejamas",
4
- "version": "0.2.0",
4
+ "version": "0.2.4",
5
5
  "license": "MIT",
6
6
  "description": "A Starlight theme using bejamas/ui",
7
7
  "type": "module",
@@ -38,14 +38,13 @@
38
38
  },
39
39
  "bugs": "https://github.com/bejamas/ui/issues",
40
40
  "peerDependencies": {
41
- "@astrojs/starlight": ">=0.33.0",
42
- "astro": ">=5.5.0"
41
+ "@astrojs/starlight": "^0.42.3",
42
+ "astro": "^7.3.4"
43
43
  },
44
44
  "dependencies": {
45
- "@bejamas/semantic-icons": "workspace:*",
46
- "@expressive-code/plugin-line-numbers": "^0.41.3",
47
- "@fontsource-variable/inter": "^5.2.8",
48
- "@fontsource/inter": "^5.2.8",
49
- "@lucide/astro": "^0.545.0"
45
+ "@bejamas/semantic-icons": "0.3.0",
46
+ "@fontsource-variable/inter": "^5.3.0",
47
+ "@fontsource/inter": "^5.3.0",
48
+ "@lucide/astro": "^1.47.0"
50
49
  }
51
50
  }
package/src/index.ts CHANGED
@@ -11,7 +11,6 @@ import {
11
11
  type StarlightThemeBejamasUserConfig,
12
12
  } from "./config";
13
13
  import { vitePluginStarlightThemeBejamas } from "./lib/vite";
14
- // import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";
15
14
 
16
15
  export default function starlightThemeBejamas(
17
16
  userConfig: StarlightThemeBejamasUserConfig,
@@ -48,12 +47,6 @@ export default function starlightThemeBejamas(
48
47
  ? {}
49
48
  : starlightConfig.expressiveCode;
50
49
 
51
- // starlightConfig.plugins?.push(
52
- // pluginLineNumbers({
53
- // showLineNumbers: true,
54
- // }),
55
- // );
56
-
57
50
  const componentOverrides: typeof config.components = {};
58
51
 
59
52
  type OverridableComponent =
@@ -0,0 +1,246 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { refreshCurrentThemeStylesheet } from "./theme-stylesheet";
3
+
4
+ const ACTIVE_SELECTOR = "link[data-current-theme-stylesheet]";
5
+ const PENDING_SELECTOR = "link[data-pending-current-theme-stylesheet]";
6
+
7
+ type Listener = (event: Event) => void;
8
+
9
+ class FakeLinkElement {
10
+ href = "";
11
+ rel = "stylesheet";
12
+ private attributes = new Map<string, string>();
13
+ private listeners = new Map<string, Set<Listener>>();
14
+
15
+ constructor(
16
+ private readonly document: FakeDocument,
17
+ href = "",
18
+ ) {
19
+ this.href = href;
20
+ }
21
+
22
+ cloneNode() {
23
+ const clone = new FakeLinkElement(this.document, this.href);
24
+ clone.rel = this.rel;
25
+
26
+ for (const [name, value] of this.attributes) {
27
+ clone.attributes.set(name, value);
28
+ }
29
+
30
+ return clone;
31
+ }
32
+
33
+ setAttribute(name: string, value: string) {
34
+ this.attributes.set(name, value);
35
+ }
36
+
37
+ removeAttribute(name: string) {
38
+ this.attributes.delete(name);
39
+ }
40
+
41
+ hasAttribute(name: string) {
42
+ return this.attributes.has(name);
43
+ }
44
+
45
+ addEventListener(type: string, listener: Listener) {
46
+ const listeners = this.listeners.get(type) ?? new Set<Listener>();
47
+ listeners.add(listener);
48
+ this.listeners.set(type, listeners);
49
+ }
50
+
51
+ removeEventListener(type: string, listener: Listener) {
52
+ this.listeners.get(type)?.delete(listener);
53
+ }
54
+
55
+ dispatchEvent(event: Event) {
56
+ this.listeners.get(event.type)?.forEach((listener) => listener(event));
57
+ return true;
58
+ }
59
+
60
+ insertAdjacentElement(position: InsertPosition, element: FakeLinkElement) {
61
+ if (position !== "afterend") {
62
+ throw new Error(`Unsupported insert position: ${position}`);
63
+ }
64
+
65
+ this.document.insertAfter(this, element);
66
+ return element;
67
+ }
68
+
69
+ remove() {
70
+ this.document.remove(this);
71
+ }
72
+ }
73
+
74
+ class FakeDocument {
75
+ links: FakeLinkElement[] = [];
76
+
77
+ querySelector<T extends FakeLinkElement>(selector: string) {
78
+ return (this.querySelectorAll(selector)[0] ?? null) as T | null;
79
+ }
80
+
81
+ querySelectorAll<T extends FakeLinkElement>(selector: string) {
82
+ if (selector === ACTIVE_SELECTOR) {
83
+ return this.links.filter((link) =>
84
+ link.hasAttribute("data-current-theme-stylesheet"),
85
+ ) as T[];
86
+ }
87
+
88
+ if (selector === PENDING_SELECTOR) {
89
+ return this.links.filter((link) =>
90
+ link.hasAttribute("data-pending-current-theme-stylesheet"),
91
+ ) as T[];
92
+ }
93
+
94
+ if (selector === "link") {
95
+ return [...this.links] as T[];
96
+ }
97
+
98
+ return [] as T[];
99
+ }
100
+
101
+ append(link: FakeLinkElement) {
102
+ this.links.push(link);
103
+ }
104
+
105
+ insertAfter(target: FakeLinkElement, link: FakeLinkElement) {
106
+ const index = this.links.indexOf(target);
107
+ if (index === -1) {
108
+ this.links.push(link);
109
+ return;
110
+ }
111
+
112
+ this.links.splice(index + 1, 0, link);
113
+ }
114
+
115
+ remove(link: FakeLinkElement) {
116
+ this.links = this.links.filter((entry) => entry !== link);
117
+ }
118
+ }
119
+
120
+ const originalDocument = globalThis.document;
121
+ const originalWindow = globalThis.window;
122
+
123
+ function createEnvironment() {
124
+ const document = new FakeDocument();
125
+ const window = {
126
+ location: { origin: "https://ui.bejamas.com" },
127
+ requestAnimationFrame: (callback: FrameRequestCallback) => {
128
+ callback(0);
129
+ return 1;
130
+ },
131
+ setTimeout,
132
+ } as unknown as Window & typeof globalThis;
133
+
134
+ globalThis.document = document as unknown as Document;
135
+ globalThis.window = window;
136
+
137
+ return { document };
138
+ }
139
+
140
+ function appendActiveStylesheet(document: FakeDocument, href?: string) {
141
+ const stylesheet = new FakeLinkElement(
142
+ document,
143
+ href ?? "https://ui.bejamas.com/r/themes/current-theme.css",
144
+ );
145
+ stylesheet.setAttribute("data-current-theme-stylesheet", "");
146
+ document.append(stylesheet);
147
+ return stylesheet;
148
+ }
149
+
150
+ describe("refreshCurrentThemeStylesheet", () => {
151
+ beforeEach(() => {
152
+ createEnvironment();
153
+ });
154
+
155
+ afterEach(() => {
156
+ globalThis.document = originalDocument;
157
+ globalThis.window = originalWindow;
158
+ });
159
+
160
+ test("keeps the current stylesheet active until the replacement loads", async () => {
161
+ const document = globalThis.document as unknown as FakeDocument;
162
+ const currentStylesheet = appendActiveStylesheet(document);
163
+
164
+ const refreshPromise = refreshCurrentThemeStylesheet();
165
+ const pendingStylesheet =
166
+ document.querySelector<FakeLinkElement>(PENDING_SELECTOR);
167
+
168
+ expect(refreshPromise).toBeInstanceOf(Promise);
169
+ expect(document.querySelectorAll("link")).toHaveLength(2);
170
+ expect(document.querySelector(ACTIVE_SELECTOR)).toBe(currentStylesheet);
171
+ expect(pendingStylesheet).not.toBeNull();
172
+ expect(pendingStylesheet?.href).not.toBe(currentStylesheet.href);
173
+
174
+ pendingStylesheet?.dispatchEvent(new Event("load"));
175
+ await refreshPromise;
176
+
177
+ const activeStylesheet =
178
+ document.querySelector<FakeLinkElement>(ACTIVE_SELECTOR);
179
+ expect(document.querySelectorAll("link")).toHaveLength(1);
180
+ expect(document.querySelector(PENDING_SELECTOR)).toBeNull();
181
+ expect(activeStylesheet).toBe(pendingStylesheet);
182
+ expect(activeStylesheet?.href).toContain("/r/themes/current-theme.css?v=");
183
+ });
184
+
185
+ test("keeps the current stylesheet when the replacement fails to load", async () => {
186
+ const document = globalThis.document as unknown as FakeDocument;
187
+ const currentStylesheet = appendActiveStylesheet(document);
188
+
189
+ const refreshPromise = refreshCurrentThemeStylesheet();
190
+ const pendingStylesheet =
191
+ document.querySelector<FakeLinkElement>(PENDING_SELECTOR);
192
+
193
+ pendingStylesheet?.dispatchEvent(new Event("error"));
194
+ await refreshPromise;
195
+
196
+ expect(document.querySelectorAll("link")).toHaveLength(1);
197
+ expect(document.querySelector(ACTIVE_SELECTOR)).toBe(currentStylesheet);
198
+ expect(document.querySelector(PENDING_SELECTOR)).toBeNull();
199
+ });
200
+
201
+ test("uses a unique cache-busting version for each refresh", async () => {
202
+ const document = globalThis.document as unknown as FakeDocument;
203
+ appendActiveStylesheet(document);
204
+
205
+ const firstRefresh = refreshCurrentThemeStylesheet();
206
+ const firstHref =
207
+ document.querySelector<FakeLinkElement>(PENDING_SELECTOR)?.href;
208
+
209
+ // A second refresh in the same tick (same Date.now()) must still produce a
210
+ // distinct URL, otherwise the browser can reuse a stale cached response.
211
+ const secondRefresh = refreshCurrentThemeStylesheet();
212
+ const secondPending =
213
+ document.querySelector<FakeLinkElement>(PENDING_SELECTOR);
214
+
215
+ expect(secondPending?.href).toContain("/r/themes/current-theme.css?v=");
216
+ expect(secondPending?.href).not.toBe(firstHref);
217
+
218
+ secondPending?.dispatchEvent(new Event("load"));
219
+ await Promise.all([firstRefresh, secondRefresh]);
220
+ });
221
+
222
+ test("cancels an older pending swap when a newer one starts", async () => {
223
+ const document = globalThis.document as unknown as FakeDocument;
224
+ appendActiveStylesheet(document);
225
+
226
+ const firstRefresh = refreshCurrentThemeStylesheet();
227
+ const firstPendingStylesheet =
228
+ document.querySelector<FakeLinkElement>(PENDING_SELECTOR);
229
+
230
+ const secondRefresh = refreshCurrentThemeStylesheet();
231
+ const secondPendingStylesheet =
232
+ document.querySelector<FakeLinkElement>(PENDING_SELECTOR);
233
+
234
+ expect(secondPendingStylesheet).not.toBe(firstPendingStylesheet);
235
+ expect(document.querySelectorAll(PENDING_SELECTOR)).toHaveLength(1);
236
+
237
+ secondPendingStylesheet?.dispatchEvent(new Event("load"));
238
+ await Promise.all([firstRefresh, secondRefresh]);
239
+
240
+ expect(document.querySelectorAll("link")).toHaveLength(1);
241
+ expect(document.querySelector(ACTIVE_SELECTOR)).toBe(
242
+ secondPendingStylesheet,
243
+ );
244
+ expect(document.querySelector(PENDING_SELECTOR)).toBeNull();
245
+ });
246
+ });
@@ -1,5 +1,5 @@
1
1
  export const CURRENT_THEME_STYLESHEET_SELECTOR =
2
- 'link[data-current-theme-stylesheet]';
2
+ "link[data-current-theme-stylesheet]";
3
3
 
4
4
  export function buildCurrentThemeStylesheetHref(
5
5
  href: string,
@@ -9,5 +9,115 @@ export function buildCurrentThemeStylesheetHref(
9
9
  url.searchParams.set("v", String(version));
10
10
 
11
11
  const isAbsolute = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(href);
12
- return isAbsolute ? url.toString() : `${url.pathname}${url.search}${url.hash}`;
12
+ return isAbsolute
13
+ ? url.toString()
14
+ : `${url.pathname}${url.search}${url.hash}`;
15
+ }
16
+
17
+ const PENDING_THEME_STYLESHEET_SELECTOR =
18
+ "link[data-pending-current-theme-stylesheet]";
19
+
20
+ let stylesheetSwapToken = 0;
21
+ let stylesheetVersionCounter = 0;
22
+ let cancelPendingThemeStylesheetSwap: (() => void) | null = null;
23
+
24
+ function getAnimationFrame() {
25
+ if (typeof window !== "undefined" && window.requestAnimationFrame) {
26
+ return window.requestAnimationFrame.bind(window);
27
+ }
28
+
29
+ return (callback: FrameRequestCallback) =>
30
+ window.setTimeout(() => callback(performance.now()), 0);
31
+ }
32
+
33
+ export function refreshCurrentThemeStylesheet() {
34
+ const currentStylesheet = document.querySelector<HTMLLinkElement>(
35
+ "link[data-current-theme-stylesheet]",
36
+ );
37
+
38
+ if (!currentStylesheet) {
39
+ return Promise.resolve();
40
+ }
41
+
42
+ cancelPendingThemeStylesheetSwap?.();
43
+
44
+ // Strictly-monotonic version so two refreshes in the same millisecond can
45
+ // never collide on a cached response (which would swap in a stale theme).
46
+ const nextHref = buildCurrentThemeStylesheetHref(
47
+ currentStylesheet.href,
48
+ `${Date.now()}-${++stylesheetVersionCounter}`,
49
+ );
50
+ const nextStylesheet = currentStylesheet.cloneNode(true) as HTMLLinkElement;
51
+ const swapToken = ++stylesheetSwapToken;
52
+ const requestAnimationFrame = getAnimationFrame();
53
+
54
+ nextStylesheet.href = nextHref;
55
+ nextStylesheet.removeAttribute("data-current-theme-stylesheet");
56
+ nextStylesheet.setAttribute("data-pending-current-theme-stylesheet", "");
57
+
58
+ document
59
+ .querySelectorAll<HTMLLinkElement>(PENDING_THEME_STYLESHEET_SELECTOR)
60
+ .forEach((link) => {
61
+ if (link !== nextStylesheet) {
62
+ link.remove();
63
+ }
64
+ });
65
+
66
+ return new Promise<void>((resolve) => {
67
+ let settled = false;
68
+
69
+ const settle = () => {
70
+ if (settled) {
71
+ return;
72
+ }
73
+
74
+ settled = true;
75
+ if (cancelPendingThemeStylesheetSwap === cancel) {
76
+ cancelPendingThemeStylesheetSwap = null;
77
+ }
78
+ resolve();
79
+ };
80
+
81
+ const cleanup = () => {
82
+ nextStylesheet.removeEventListener("load", onLoad);
83
+ nextStylesheet.removeEventListener("error", onError);
84
+ };
85
+
86
+ const cancel = () => {
87
+ cleanup();
88
+ nextStylesheet.remove();
89
+ settle();
90
+ };
91
+
92
+ const onError = () => {
93
+ cancel();
94
+ };
95
+
96
+ const onLoad = () => {
97
+ cleanup();
98
+
99
+ if (swapToken !== stylesheetSwapToken) {
100
+ nextStylesheet.remove();
101
+ settle();
102
+ return;
103
+ }
104
+
105
+ currentStylesheet.removeAttribute("data-current-theme-stylesheet");
106
+ nextStylesheet.removeAttribute("data-pending-current-theme-stylesheet");
107
+ nextStylesheet.setAttribute("data-current-theme-stylesheet", "");
108
+
109
+ requestAnimationFrame(() => {
110
+ if (swapToken === stylesheetSwapToken) {
111
+ currentStylesheet.remove();
112
+ }
113
+
114
+ settle();
115
+ });
116
+ };
117
+
118
+ cancelPendingThemeStylesheetSwap = cancel;
119
+ nextStylesheet.addEventListener("load", onLoad, { once: true });
120
+ nextStylesheet.addEventListener("error", onError, { once: true });
121
+ currentStylesheet.insertAdjacentElement("afterend", nextStylesheet);
122
+ });
13
123
  }
@@ -28,83 +28,44 @@ const isSplashTemplate = template === "splash";
28
28
  aria-label={Astro.locals.t("sidebarNav.accessibleLabel")}
29
29
  >
30
30
  <MobileMenuToggle />
31
- <div id="starlight__sidebar" class="sidebar-pane">
31
+ <script>
32
+ if (!customElements.get("sl-sidebar-pane")) {
33
+ class StarlightSidebarPane extends HTMLElement {
34
+ constructor() {
35
+ super();
36
+
37
+ const desktopMediaQuery = matchMedia("(min-width: 64rem)");
38
+ const updatePopover = () => {
39
+ this.popover = desktopMediaQuery.matches ? null : "auto";
40
+ };
41
+ updatePopover();
42
+ desktopMediaQuery.addEventListener("change", updatePopover);
43
+
44
+ this.addEventListener("toggle", () => {
45
+ const isOpen = this.matches(":popover-open");
46
+ document
47
+ .querySelectorAll<HTMLElement>(".main-frame, .sl-skip-link")
48
+ .forEach((element) => element.toggleAttribute("inert", isOpen));
49
+
50
+ if (isOpen && this.closest(".sidebar-splash")) {
51
+ this.scrollTop = 0;
52
+ }
53
+ });
54
+ }
55
+ }
56
+
57
+ customElements.define("sl-sidebar-pane", StarlightSidebarPane);
58
+ }
59
+ </script>
60
+ <sl-sidebar-pane popover="" id="starlight__sidebar" class="sidebar-pane">
32
61
  <div class="sidebar-content sl-flex">
33
62
  <slot name="sidebar" />
34
63
  </div>
35
- </div>
64
+ </sl-sidebar-pane>
36
65
  </nav>
37
66
  )
38
67
  }
39
68
 
40
- {
41
- isSplashTemplate && (
42
- <script is:inline>
43
- (() => {
44
- const installReset = () => {
45
- const nav = document.querySelector("nav.sidebar-splash");
46
- if (!(nav instanceof HTMLElement)) return () => {};
47
-
48
- const pane = nav.querySelector("#starlight__sidebar");
49
- const toggle = nav.querySelector("starlight-menu-button");
50
-
51
- if (!(pane instanceof HTMLElement)) return () => {};
52
-
53
- const resetToTop = () => {
54
- pane.scrollTop = 0;
55
- };
56
-
57
- // Avoid opening splash menus at a persisted offset.
58
- resetToTop();
59
-
60
- if (!(toggle instanceof HTMLElement)) return () => {};
61
-
62
- const onToggleChange = () => {
63
- if (toggle.getAttribute("aria-expanded") === "true") {
64
- requestAnimationFrame(resetToTop);
65
- }
66
- };
67
-
68
- const onToggleClick = () => {
69
- requestAnimationFrame(onToggleChange);
70
- };
71
-
72
- toggle.addEventListener("click", onToggleClick);
73
-
74
- const observer = new MutationObserver(onToggleChange);
75
- observer.observe(toggle, {
76
- attributes: true,
77
- attributeFilter: ["aria-expanded"],
78
- });
79
-
80
- return () => {
81
- toggle.removeEventListener("click", onToggleClick);
82
- observer.disconnect();
83
- };
84
- };
85
-
86
- let cleanup = installReset();
87
-
88
- const onAstroPageLoad = () => {
89
- cleanup();
90
- cleanup = installReset();
91
- };
92
-
93
- document.addEventListener("astro:page-load", onAstroPageLoad);
94
-
95
- addEventListener(
96
- "pagehide",
97
- () => {
98
- cleanup();
99
- document.removeEventListener("astro:page-load", onAstroPageLoad);
100
- },
101
- { once: true },
102
- );
103
- })();
104
- </script>
105
- )
106
- }
107
-
108
69
  <div class:list={["main-frame", isSplashTemplate && "main-frame-splash"]}>
109
70
  {
110
71
  template === "splash" ? (
@@ -182,15 +143,17 @@ const isSplashTemplate = template === "splash";
182
143
  }
183
144
 
184
145
  .sidebar-pane {
185
- visibility: var(--sl-sidebar-visibility, hidden);
146
+ border: 0;
147
+ margin: 0;
148
+ color: inherit;
149
+ display: none;
186
150
  position: fixed;
187
151
  z-index: var(--sl-z-index-menu);
188
152
  inset-block: var(--sl-nav-height) 0;
189
153
  inset-inline-start: 0;
190
154
  width: 100%;
191
155
  background-color: var(--background);
192
- height: 100svh;
193
- overflow: scroll;
156
+ height: calc(100svh - var(--sl-nav-height));
194
157
  overflow-y: scroll;
195
158
  overflow-x: hidden;
196
159
  scrollbar-width: none;
@@ -198,12 +161,11 @@ const isSplashTemplate = template === "splash";
198
161
  -webkit-overflow-scrolling: touch;
199
162
  overscroll-behavior: contain;
200
163
  overscroll-behavior-y: contain;
201
- top: 0;
202
- padding-top: calc(var(--spacing) * 18);
164
+ padding: 0;
203
165
  }
204
166
 
205
- :global([aria-expanded="true"]) ~ .sidebar-pane {
206
- --sl-sidebar-visibility: visible;
167
+ .sidebar-pane:popover-open {
168
+ display: block;
207
169
  }
208
170
 
209
171
  .sidebar-content {
@@ -219,11 +181,6 @@ const isSplashTemplate = template === "splash";
219
181
  content: "";
220
182
  padding-bottom: 1px;
221
183
  }
222
-
223
- .sidebar-pane {
224
- padding-top: 0;
225
- margin-top: calc(var(--spacing) * 18);
226
- }
227
184
  }
228
185
 
229
186
  .main-frame {
@@ -315,7 +272,7 @@ const isSplashTemplate = template === "splash";
315
272
  padding-inline-end: var(--sl-nav-pad-x);
316
273
  }
317
274
  .sidebar-pane {
318
- --sl-sidebar-visibility: visible;
275
+ display: block;
319
276
  width: var(--sl-sidebar-width);
320
277
  background-color: var(--sl-color-bg-sidebar);
321
278
  border-inline-end: 1px solid var(--sl-color-hairline-shade);
@@ -100,7 +100,7 @@ const isRtl = dir === "rtl";
100
100
  display: flex;
101
101
  justify-content: space-between;
102
102
  flex-wrap: wrap;
103
- margin-top: 0.5rem;
103
+ margin-top: 0;
104
104
  align-items: center;
105
105
  }
106
106
  .sl-bejamas-docs-title-buttons {
@@ -128,15 +128,11 @@ const themeChoiceBootstrapScript = buildThemeChoiceBootstrapInlineScript();
128
128
 
129
129
  <script>
130
130
  import { syncSemanticIconsInRoot } from "@bejamas/semantic-icons/browser";
131
- import {
132
- buildCurrentThemeStylesheetHref,
133
- CURRENT_THEME_STYLESHEET_SELECTOR,
134
- } from "../lib/theme-stylesheet";
131
+ import { refreshCurrentThemeStylesheet } from "../lib/theme-stylesheet";
135
132
 
136
133
  const PRESET_CHANGE_EVENT = "bejamas:preset-change";
137
134
  const SETUP_KEY = "__bejamasCurrentThemeStylesheetSync";
138
135
  let reloadFrame = 0;
139
- let reloadVersion = 0;
140
136
  let pendingIconLibrary = null;
141
137
 
142
138
  function syncDocumentIconLibrary() {
@@ -152,68 +148,21 @@ const themeChoiceBootstrapScript = buildThemeChoiceBootstrapInlineScript();
152
148
  }
153
149
  }
154
150
 
155
- function refreshCurrentThemeStylesheet() {
156
- const links = Array.from(
157
- document.head.querySelectorAll(CURRENT_THEME_STYLESHEET_SELECTOR),
158
- ).filter(
159
- (link): link is HTMLLinkElement => link instanceof HTMLLinkElement,
160
- );
161
- const currentLink = links.at(-1);
162
-
163
- if (!currentLink) {
164
- window.StarlightThemeProvider?.refreshThemeColors?.();
151
+ const scheduleCurrentThemeStylesheetRefresh = () => {
152
+ if (reloadFrame) {
165
153
  return;
166
154
  }
167
155
 
168
- reloadVersion += 1;
169
-
170
- const replacement = currentLink.cloneNode(false) as HTMLLinkElement;
171
- const sourceHref = currentLink.getAttribute("href") ?? currentLink.href;
172
- replacement.href = buildCurrentThemeStylesheetHref(sourceHref, `${Date.now()}-${reloadVersion}`);
173
-
174
- replacement.addEventListener(
175
- "load",
176
- () => {
177
- links.forEach((link) => {
178
- if (link !== replacement) {
179
- link.remove();
180
- }
181
- });
182
- if (pendingIconLibrary) {
183
- document.documentElement.dataset.iconLibrary = pendingIconLibrary;
184
- syncSemanticIconsInRoot(document, pendingIconLibrary);
185
- pendingIconLibrary = null;
186
- }
187
- window.StarlightThemeProvider?.refreshThemeColors?.();
188
- },
189
- { once: true },
190
- );
191
-
192
- replacement.addEventListener(
193
- "error",
194
- () => {
195
- replacement.remove();
156
+ reloadFrame = requestAnimationFrame(() => {
157
+ reloadFrame = 0;
158
+ void refreshCurrentThemeStylesheet().then(() => {
196
159
  if (pendingIconLibrary) {
197
160
  document.documentElement.dataset.iconLibrary = pendingIconLibrary;
198
161
  syncSemanticIconsInRoot(document, pendingIconLibrary);
199
162
  pendingIconLibrary = null;
200
163
  }
201
164
  window.StarlightThemeProvider?.refreshThemeColors?.();
202
- },
203
- { once: true },
204
- );
205
-
206
- currentLink.after(replacement);
207
- }
208
-
209
- const scheduleCurrentThemeStylesheetRefresh = () => {
210
- if (reloadFrame) {
211
- return;
212
- }
213
-
214
- reloadFrame = requestAnimationFrame(() => {
215
- reloadFrame = 0;
216
- refreshCurrentThemeStylesheet();
165
+ });
217
166
  });
218
167
  };
219
168
 
@@ -89,6 +89,7 @@
89
89
  /* --sl-nav-height: auto; */
90
90
  --sl-nav-pad-y: calc(var(--spacing) * 4.5);
91
91
  --sl-content-pad-x: calc(var(--spacing) * 5);
92
+ --sl-docs-top-space: calc(var(--spacing) * 12);
92
93
  --sl-nav-pad-x: calc(var(--spacing) * 1 + var(--sl-content-pad-x));
93
94
  --sl-main-frame-inner-background: var(--background);
94
95
  --sl-main-frame-inner-border: var(--border);
@@ -150,7 +151,7 @@
150
151
 
151
152
  .right-sidebar-panel {
152
153
  --sl-sidebar-item-padding-inline: 1rem;
153
- padding-top: calc(var(--spacing) * 14);
154
+ padding-top: var(--sl-docs-top-space);
154
155
  }
155
156
 
156
157
  .right-sidebar-panel a {
@@ -195,6 +196,14 @@
195
196
  font-size: 1rem;
196
197
  }
197
198
 
199
+ .sidebar-content details > summary .caret {
200
+ transform: rotate(90deg);
201
+ }
202
+
203
+ .sidebar-content details[open] > summary .caret {
204
+ transform: rotate(-90deg);
205
+ }
206
+
198
207
  .sidebar-content a > span {
199
208
  color: var(--foreground);
200
209
  padding-inline: 0.5rem;
@@ -224,6 +233,16 @@
224
233
  margin-bottom: 1px;
225
234
  }
226
235
 
236
+ @media (min-width: 64rem) {
237
+ .sidebar-pane .sidebar-content {
238
+ padding-top: var(--sl-docs-top-space);
239
+ }
240
+
241
+ .sidebar-content::before {
242
+ display: none;
243
+ }
244
+ }
245
+
227
246
  @media (max-width: 49.99rem) {
228
247
  .sidebar-pane .sidebar-content {
229
248
  --sl-sidebar-item-padding-inline: 0.5rem;
@@ -271,7 +290,7 @@
271
290
 
272
291
  @media (min-width: 50rem) {
273
292
  .content-panel:first-child {
274
- padding-top: calc(var(--spacing) * 12);
293
+ padding-top: var(--sl-docs-top-space);
275
294
  }
276
295
  }
277
296
 
@@ -453,6 +472,19 @@
453
472
  z-index: 10;
454
473
  }
455
474
 
475
+ /* Draw the border on the wrapper so the collapse overlay and expanded
476
+ scrolling don't cover or scroll away the pre's own border. */
477
+ .expressive-code:has(> .ec-collapse) {
478
+ border: 1px solid var(--border);
479
+ border-radius: var(--radius-lg);
480
+ overflow: hidden;
481
+ }
482
+
483
+ .expressive-code:has(> .ec-collapse) pre {
484
+ border: 0;
485
+ border-radius: 0;
486
+ }
487
+
456
488
  .sl-bejamas-component-preview + .expressive-code {
457
489
  margin-top: 0;
458
490
  }
@@ -704,7 +736,7 @@
704
736
  max-width: calc(var(--spacing) * 150);
705
737
  }
706
738
 
707
- starlight-menu-button button {
739
+ .sl-menu-button {
708
740
  box-shadow: none;
709
741
  padding: 0;
710
742
  width: calc(var(--spacing) * 6);
@@ -712,18 +744,18 @@
712
744
  color: var(--foreground);
713
745
  }
714
746
 
715
- starlight-menu-button button svg {
747
+ .sl-menu-button svg {
716
748
  width: calc(var(--spacing) * 8);
717
749
  height: calc(var(--spacing) * 8);
718
750
  font-size: calc(var(--spacing) * 8);
719
751
  }
720
752
 
721
753
  @media (min-width: 50rem) and (max-width: calc(64rem - 0.01rem)) {
722
- starlight-menu-button button {
754
+ .sl-menu-button {
723
755
  display: flex;
724
756
  }
725
757
 
726
- [data-mobile-menu-expanded] {
758
+ body:has(#starlight__sidebar:popover-open) {
727
759
  overflow: hidden;
728
760
  }
729
761
  }