starlight-theme-bejamas 0.0.0-canary.0138fd3
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 +132 -0
- package/package.json +50 -0
- package/src/config.test.ts +40 -0
- package/src/config.ts +51 -0
- package/src/env.d.ts +16 -0
- package/src/global.d.ts +20 -0
- package/src/index.ts +152 -0
- package/src/lib/theme-choice.test.ts +251 -0
- package/src/lib/theme-choice.ts +346 -0
- package/src/lib/theme-stylesheet-runtime.test.ts +246 -0
- package/src/lib/theme-stylesheet.test.ts +16 -0
- package/src/lib/theme-stylesheet.ts +123 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/vite.ts +62 -0
- package/src/overrides/Footer.astro +143 -0
- package/src/overrides/Header.astro +166 -0
- package/src/overrides/Hero.astro +175 -0
- package/src/overrides/MobileTableOfContents.astro +275 -0
- package/src/overrides/PageFrame.astro +287 -0
- package/src/overrides/PageTitle.astro +142 -0
- package/src/overrides/Pagination.astro +57 -0
- package/src/overrides/Sidebar.astro +54 -0
- package/src/overrides/SiteTitle.astro +59 -0
- package/src/overrides/TableOfContents/TableOfContentsList.astro +88 -0
- package/src/overrides/TableOfContents/starlight-toc.ts +125 -0
- package/src/overrides/ThemeProvider.astro +198 -0
- package/src/overrides/ThemeSelect.astro +110 -0
- package/src/styles/theme.css +776 -0
- package/src/virtual-internal.d.ts +40 -0
- package/src/virtual-modules.d.ts +28 -0
- package/src/virtual.d.ts +12 -0
- package/tsconfig.json +11 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { buildCurrentThemeStylesheetHref } from "./theme-stylesheet";
|
|
3
|
+
|
|
4
|
+
describe("current theme stylesheet helpers", () => {
|
|
5
|
+
test("adds a cache-busting query param to relative hrefs", () => {
|
|
6
|
+
expect(
|
|
7
|
+
buildCurrentThemeStylesheetHref("/r/themes/current-theme.css", 123),
|
|
8
|
+
).toBe("/r/themes/current-theme.css?v=123");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("preserves existing query params when cache-busting", () => {
|
|
12
|
+
expect(
|
|
13
|
+
buildCurrentThemeStylesheetHref("/r/themes/current-theme.css?foo=bar", 123),
|
|
14
|
+
).toBe("/r/themes/current-theme.css?foo=bar&v=123");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export const CURRENT_THEME_STYLESHEET_SELECTOR =
|
|
2
|
+
"link[data-current-theme-stylesheet]";
|
|
3
|
+
|
|
4
|
+
export function buildCurrentThemeStylesheetHref(
|
|
5
|
+
href: string,
|
|
6
|
+
version: string | number,
|
|
7
|
+
) {
|
|
8
|
+
const url = new URL(href, "https://ui.bejamas.com");
|
|
9
|
+
url.searchParams.set("v", String(version));
|
|
10
|
+
|
|
11
|
+
const isAbsolute = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(href);
|
|
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
|
+
});
|
|
123
|
+
}
|
package/src/lib/utils.ts
ADDED
package/src/lib/vite.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ViteUserConfig } from "astro";
|
|
2
|
+
import type { StarlightThemeBejamasConfig } from "../config";
|
|
3
|
+
|
|
4
|
+
export function vitePluginStarlightThemeBejamas(
|
|
5
|
+
config: StarlightThemeBejamasConfig,
|
|
6
|
+
): VitePlugin {
|
|
7
|
+
const moduleId = "virtual:starlight-theme-bejamas/user-config";
|
|
8
|
+
const resolvedModuleId = `\0${moduleId}`;
|
|
9
|
+
const moduleContent = `export default ${JSON.stringify(config)}`;
|
|
10
|
+
|
|
11
|
+
const componentsPrefix = "virtual:starlight-theme-bejamas/components/";
|
|
12
|
+
const resolvedComponentsPrefix = `\0${componentsPrefix}`;
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
name: "vite-plugin-starlight-theme-bejamas",
|
|
16
|
+
load(id) {
|
|
17
|
+
if (id === resolvedModuleId) {
|
|
18
|
+
return moduleContent;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (id.startsWith(resolvedComponentsPrefix)) {
|
|
22
|
+
const componentId = id.slice(resolvedComponentsPrefix.length);
|
|
23
|
+
const componentName = componentId.replace(/\.astro$/, "");
|
|
24
|
+
const target = config.components?.[componentName];
|
|
25
|
+
|
|
26
|
+
if (!target) {
|
|
27
|
+
return `export default (()=>{ throw new Error(\"starlight-theme-bejamas: No component mapping found for '${componentName}'. Add it under 'components' in your starlightThemeBejamas config.\"); })()`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check if target is a barrel import (no .astro extension) or old-style file import
|
|
31
|
+
const isBarrelImport = !target.endsWith(".astro");
|
|
32
|
+
|
|
33
|
+
if (isBarrelImport) {
|
|
34
|
+
// For barrel imports, the component is exported as a named export in PascalCase
|
|
35
|
+
// Convert componentName (e.g., 'button') to PascalCase (e.g., 'Button')
|
|
36
|
+
const pascalCaseName = componentName.charAt(0).toUpperCase() + componentName.slice(1);
|
|
37
|
+
return `export { ${pascalCaseName} as default, ${pascalCaseName} } from "${target}"; export * from "${target}";`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Old pattern: .astro file with default export
|
|
41
|
+
return `export { default } from "${target}"; export * from "${target}";`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return undefined;
|
|
45
|
+
},
|
|
46
|
+
resolveId(id) {
|
|
47
|
+
if (id === moduleId) {
|
|
48
|
+
return resolvedModuleId;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (id.startsWith(componentsPrefix)) {
|
|
52
|
+
const componentId = id.slice(componentsPrefix.length);
|
|
53
|
+
const componentName = componentId.replace(/\.astro$/, "");
|
|
54
|
+
return `${resolvedComponentsPrefix}${componentName}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return undefined;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type VitePlugin = NonNullable<ViteUserConfig["plugins"]>[number];
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
import EditLink from "virtual:starlight/components/EditLink";
|
|
3
|
+
import LastUpdated from "virtual:starlight/components/LastUpdated";
|
|
4
|
+
import Pagination from "virtual:starlight/components/Pagination";
|
|
5
|
+
import config from "virtual:starlight/user-config";
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<footer class="sl-flex footer-wide">
|
|
9
|
+
<div class="meta"><EditLink /><LastUpdated /></div>
|
|
10
|
+
<Pagination />
|
|
11
|
+
<div class="fade"></div>
|
|
12
|
+
</footer>
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
config.credits && (
|
|
16
|
+
<div class="kudos sl-flex footer-wide">
|
|
17
|
+
<p>
|
|
18
|
+
Built at <a href="https://bejamas.com?utm_source=ui">Bejamas</a>. The
|
|
19
|
+
source code is available on{" "}
|
|
20
|
+
<a href="https://github.com/bejamas/ui">GitHub</a>.
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
@layer starlight.core {
|
|
28
|
+
.footer-wide {
|
|
29
|
+
width: calc(
|
|
30
|
+
100% + calc(var(--sl-content-pad-x) * 2) + calc(var(--spacing) * 2)
|
|
31
|
+
);
|
|
32
|
+
margin-inline: calc(var(--spacing) * -1 - var(--sl-content-pad-x));
|
|
33
|
+
padding-inline: calc(var(--sl-content-pad-x) + var(--spacing));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
footer {
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
gap: 1.5rem;
|
|
39
|
+
position: sticky;
|
|
40
|
+
bottom: 0;
|
|
41
|
+
background: linear-gradient(to top, var(--background), transparent);
|
|
42
|
+
padding-block: calc(var(--spacing) * 3);
|
|
43
|
+
z-index: 10;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@media (min-width: 50rem) {
|
|
47
|
+
footer {
|
|
48
|
+
position: relative;
|
|
49
|
+
background: transparent;
|
|
50
|
+
}
|
|
51
|
+
.meta {
|
|
52
|
+
display: flex;
|
|
53
|
+
}
|
|
54
|
+
.kudos {
|
|
55
|
+
margin-bottom: calc(var(--spacing) * -10);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.meta {
|
|
60
|
+
gap: 0.75rem 3rem;
|
|
61
|
+
justify-content: space-between;
|
|
62
|
+
flex-wrap: wrap;
|
|
63
|
+
margin-top: 3rem;
|
|
64
|
+
font-size: var(--sl-text-sm);
|
|
65
|
+
color: var(--sl-color-gray-3);
|
|
66
|
+
display: flex;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.meta:empty {
|
|
70
|
+
display: none;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.meta > :global(p:only-child) {
|
|
74
|
+
margin-inline-start: auto;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.kudos {
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 0.5em;
|
|
80
|
+
margin-block: 0;
|
|
81
|
+
font-size: var(--sl-text-xs);
|
|
82
|
+
color: var(--muted-foreground);
|
|
83
|
+
justify-content: center;
|
|
84
|
+
padding-top: calc(var(--spacing) * 12);
|
|
85
|
+
padding-bottom: 0;
|
|
86
|
+
background-color: var(--background);
|
|
87
|
+
justify-content: center;
|
|
88
|
+
}
|
|
89
|
+
.kudos a {
|
|
90
|
+
color: var(--muted-foreground);
|
|
91
|
+
text-decoration: underline;
|
|
92
|
+
text-underline-offset: 0.1875rem;
|
|
93
|
+
}
|
|
94
|
+
.kudos a:hover {
|
|
95
|
+
color: var(--foreground);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@media (min-width: 50rem) {
|
|
99
|
+
.kudos {
|
|
100
|
+
background-color: transparent;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.fade {
|
|
105
|
+
--blur: 4px;
|
|
106
|
+
--stop: 50%;
|
|
107
|
+
--height: 72px;
|
|
108
|
+
position: absolute;
|
|
109
|
+
pointer-events: none;
|
|
110
|
+
width: 100%;
|
|
111
|
+
height: var(--height);
|
|
112
|
+
user-select: none;
|
|
113
|
+
-webkit-user-select: none;
|
|
114
|
+
left: 0;
|
|
115
|
+
-webkit-backdrop-filter: blur(var(--blur));
|
|
116
|
+
backdrop-filter: blur(var(--blur));
|
|
117
|
+
|
|
118
|
+
background: linear-gradient(
|
|
119
|
+
to bottom,
|
|
120
|
+
transparent,
|
|
121
|
+
var(--sl-page-background)
|
|
122
|
+
);
|
|
123
|
+
mask-image: linear-gradient(
|
|
124
|
+
to top,
|
|
125
|
+
var(--sl-page-background) var(--stop),
|
|
126
|
+
transparent
|
|
127
|
+
);
|
|
128
|
+
bottom: 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@media (min-width: 50rem) {
|
|
132
|
+
.fade {
|
|
133
|
+
display: none;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@layer starlight.components {
|
|
139
|
+
.kudos :global(svg) {
|
|
140
|
+
color: var(--sl-color-orange);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
</style>
|