mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
Merge pull request #473 from Jaxx7594/bring-back-the-cat
feat(bring back the cat!): Brings back the legacy 404 page
This commit is contained in:
@@ -14,6 +14,8 @@ export default {
|
|||||||
'/node_modules/(?!(color|color-string|color-convert|color-name)/)',
|
'/node_modules/(?!(color|color-string|color-convert|color-name)/)',
|
||||||
],
|
],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
|
'\\.png$': '<rootDir>/src/test/mocks/assetStub.ts',
|
||||||
|
'\\?inline$': '<rootDir>/src/test/mocks/inlineStub.ts',
|
||||||
'^@/(.*)$': '<rootDir>/src/$1',
|
'^@/(.*)$': '<rootDir>/src/$1',
|
||||||
'^color$': '<rootDir>/src/test/mocks/color.ts',
|
'^color$': '<rootDir>/src/test/mocks/color.ts',
|
||||||
'^webextension-polyfill$': '<rootDir>/src/test/mocks/webextension-polyfill.ts',
|
'^webextension-polyfill$': '<rootDir>/src/test/mocks/webextension-polyfill.ts',
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ if (document.childNodes[1]) {
|
|||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The 404 page is a bare document with no second childNode, so it must be
|
||||||
|
// booted unconditionally (outside the gate above). It self-guards on title.
|
||||||
|
bootErrorPage();
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
window.addEventListener("unhandledrejection", (event) => {
|
window.addEventListener("unhandledrejection", (event) => {
|
||||||
recoverFromStaleDevModuleGraph(event.reason);
|
recoverFromStaleDevModuleGraph(event.reason);
|
||||||
@@ -138,6 +142,65 @@ async function init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The 404 page is a standalone document that never passes the SPA gate above,
|
||||||
|
// so the normal plugin path never boots there; start just the classic kitten
|
||||||
|
// 404 plugin so it renders without any SPA work.
|
||||||
|
async function bootErrorPage() {
|
||||||
|
if (IsSEQTAPage) return;
|
||||||
|
|
||||||
|
// Runs at document_start, so <title> and body are not available yet; wait
|
||||||
|
// for the DOM before testing the 404 title.
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
if (document.readyState !== "loading") {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.addEventListener("DOMContentLoaded", () => resolve(), {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const is404Page =
|
||||||
|
/404/.test(document.title) || /not found/i.test(document.title);
|
||||||
|
if (!is404Page) return;
|
||||||
|
|
||||||
|
const stored = await browser.storage.local.get([
|
||||||
|
"onoff",
|
||||||
|
"plugin.error-page-kitten.settings",
|
||||||
|
]);
|
||||||
|
if ((stored.onoff ?? true) === false) return;
|
||||||
|
const kittenEnabled = (
|
||||||
|
stored["plugin.error-page-kitten.settings"] as
|
||||||
|
| { enabled?: boolean }
|
||||||
|
| undefined
|
||||||
|
)?.enabled;
|
||||||
|
if (kittenEnabled === false) return;
|
||||||
|
|
||||||
|
hideRebranded404();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { pluginManager } = await import("@/plugins/index");
|
||||||
|
await pluginManager.startPlugin("error-page-kitten");
|
||||||
|
} catch (error) {
|
||||||
|
// Restore the rebranded page so the user is not left with a blank document.
|
||||||
|
document
|
||||||
|
.querySelectorAll<HTMLElement>(".bsplus-kitten-404-hidden")
|
||||||
|
.forEach((el) => {
|
||||||
|
el.classList.remove("bsplus-kitten-404-hidden");
|
||||||
|
el.style.display = "";
|
||||||
|
});
|
||||||
|
console.error("[BetterSEQTA+] Failed to boot 404 page:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideRebranded404() {
|
||||||
|
const message = document.querySelector<HTMLElement>(".message");
|
||||||
|
if (message) {
|
||||||
|
message.classList.add("bsplus-kitten-404-hidden");
|
||||||
|
message.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function replaceIcons() {
|
function replaceIcons() {
|
||||||
document
|
document
|
||||||
.querySelectorAll<HTMLLinkElement>('link[rel*="icon"]')
|
.querySelectorAll<HTMLLinkElement>('link[rel*="icon"]')
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
"resources": [
|
"resources": [
|
||||||
|
"resources/error-page/*",
|
||||||
"resources/icons/*",
|
"resources/icons/*",
|
||||||
"resources/update-image.webp",
|
"resources/update-image.webp",
|
||||||
"resources/pdfjs/pdf.worker.min.mjs",
|
"resources/pdfjs/pdf.worker.min.mjs",
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*/
|
||||||
|
/// <reference types="jest" />
|
||||||
|
import type { PluginAPI } from "@/plugins/core/types";
|
||||||
|
import errorPageKittenPlugin from "./index";
|
||||||
|
|
||||||
|
const REBRANDED_404_BODY = `
|
||||||
|
<div class="message">
|
||||||
|
<h1>Page not found</h1>
|
||||||
|
<p>We can't find the page you're looking for.</p>
|
||||||
|
<p>It may have been moved, deleted or the link might be out of date.</p>
|
||||||
|
<p class="error-ref">Ref: 404</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
settings: { loaded: Promise.resolve() },
|
||||||
|
} as unknown as PluginAPI<{}>;
|
||||||
|
|
||||||
|
async function startPlugin(): Promise<() => void> {
|
||||||
|
const cleanup = (await errorPageKittenPlugin.run(api)) ?? (() => {});
|
||||||
|
return cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCard(): HTMLElement | null {
|
||||||
|
// The card container shares its class with <html>, so scope to body.
|
||||||
|
return document.body.querySelector<HTMLElement>(".bsplus-kitten-404");
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectKittenRendered(): void {
|
||||||
|
const card = getCard();
|
||||||
|
expect(card).not.toBeNull();
|
||||||
|
expect(document.documentElement.classList).toContain("bsplus-kitten-404");
|
||||||
|
expect(card?.querySelector("h1")?.textContent).toBe("404 Not Found");
|
||||||
|
// The original document's title.
|
||||||
|
expect(document.title).toBe("404 Not Found");
|
||||||
|
// The reference page's exact vendor-prefixed banner CSS is shipped at runtime.
|
||||||
|
const stripeStyle = Array.from(
|
||||||
|
document.head.querySelectorAll("style"),
|
||||||
|
).find((s) => s.textContent?.includes("-webkit-repeating-linear-gradient"));
|
||||||
|
expect(stripeStyle?.textContent).toContain(
|
||||||
|
"repeating-linear-gradient(45deg",
|
||||||
|
);
|
||||||
|
const img = card?.querySelector(".kitten img") as HTMLImageElement | null;
|
||||||
|
expect(img?.getAttribute("alt")).toBeNull();
|
||||||
|
expect(img?.src).toContain("kitten");
|
||||||
|
const flickrLink = card?.querySelector(
|
||||||
|
".attrib a",
|
||||||
|
) as HTMLAnchorElement | null;
|
||||||
|
expect(flickrLink?.textContent).toBe("by storyvillegirl");
|
||||||
|
expect(flickrLink?.href).toBe(
|
||||||
|
"http://www.flickr.com/photos/bibbit/2756165489/",
|
||||||
|
);
|
||||||
|
const seqtaLink = card?.querySelector("a[href='http://www.seqta.com.au']");
|
||||||
|
expect(seqtaLink?.textContent).toBe("SEQTA");
|
||||||
|
// Original rebranded message is hidden.
|
||||||
|
expect(
|
||||||
|
document.querySelector(".message")?.classList.contains(
|
||||||
|
"bsplus-kitten-404-hidden",
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("errorPageKitten", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
document.title = "";
|
||||||
|
document.documentElement.classList.remove("bsplus-kitten-404");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the classic kitten 404 card over the rebranded page", async () => {
|
||||||
|
document.title = "Page not found";
|
||||||
|
document.body.innerHTML = REBRANDED_404_BODY;
|
||||||
|
|
||||||
|
const cleanup = await startPlugin();
|
||||||
|
|
||||||
|
expectKittenRendered();
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
expect(getCard()).toBeNull();
|
||||||
|
expect(document.title).toBe("Page not found");
|
||||||
|
expect(
|
||||||
|
Array.from(document.head.querySelectorAll("style")).some((s) =>
|
||||||
|
s.textContent?.includes("-webkit-repeating-linear-gradient"),
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
document.documentElement.classList.contains("bsplus-kitten-404"),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
document
|
||||||
|
.querySelector(".message")
|
||||||
|
?.classList.contains("bsplus-kitten-404-hidden"),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render inside the SPA (no .message, has #container)", async () => {
|
||||||
|
document.title = "SEQTA";
|
||||||
|
document.body.innerHTML = '<div id="container"></div>';
|
||||||
|
|
||||||
|
await startPlugin();
|
||||||
|
|
||||||
|
expect(getCard()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render on an unrelated page", async () => {
|
||||||
|
document.title = "Home";
|
||||||
|
document.body.innerHTML = '<div id="app"></div>';
|
||||||
|
|
||||||
|
await startPlugin();
|
||||||
|
|
||||||
|
expect(getCard()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import type { Plugin } from "@/plugins/core/types";
|
||||||
|
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
|
||||||
|
import kittenPng from "@/resources/error-page/kitten.png";
|
||||||
|
import styles from "./styles.css?inline";
|
||||||
|
|
||||||
|
const KITTEN_IMG = resolveExtensionAssetUrl(kittenPng);
|
||||||
|
const KITTEN_CLASS = "bsplus-kitten-404";
|
||||||
|
|
||||||
|
// The reference page's exact h1 background declarations, in the original
|
||||||
|
// order. Each engine keeps the last declaration it understands (-webkit in
|
||||||
|
// Blink, -moz in Gecko, the legacy standard otherwise), so shipping them
|
||||||
|
// byte-for-byte reproduces the reference banner on every engine. Lives in a
|
||||||
|
// JS string because the CSS minifier drops vendor-prefixed declarations.
|
||||||
|
const H1_STRIPE_CSS =
|
||||||
|
"html.bsplus-kitten-404 .bsplus-kitten-404>h1{background-image:-moz-repeating-linear-gradient(135deg, rgba(0,0,0,0), rgba(0,0,0,0) 8px, rgba(0,0,0,0.05) 8px, rgba(0,0,0,0.05) 16px), -moz-linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.1));background-image:-webkit-repeating-linear-gradient(135deg, rgba(0,0,0,0), rgba(0,0,0,0) 8px, rgba(0,0,0,0.05) 8px, rgba(0,0,0,0.05) 16px), -webkit-linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.1));background-image:-o-repeating-linear-gradient(135deg, rgba(0,0,0,0), rgba(0,0,0,0) 8px, rgba(0,0,0,0.05) 8px, rgba(0,0,0,0.05) 16px), -o-linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.1));background-image:repeating-linear-gradient(45deg, rgba(0,0,0,0), rgba(0,0,0,0) 8px, rgba(0,0,0,0.05) 8px, rgba(0,0,0,0.05) 16px), linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.1))}";
|
||||||
|
|
||||||
|
// The 404 page (legacy and rebranded) is a standalone document built around a
|
||||||
|
// single `.message` block; the real app mounts a `#container`. Require the
|
||||||
|
// former and rule out the latter so the plugin never renders inside the app.
|
||||||
|
function is404Page(): boolean {
|
||||||
|
if (document.getElementById("container")) return false;
|
||||||
|
const heading = document.querySelector(".message > h1");
|
||||||
|
const text = heading?.textContent ?? document.title;
|
||||||
|
return /not found/i.test(text) || /404/.test(document.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCard(): HTMLElement {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = KITTEN_CLASS;
|
||||||
|
|
||||||
|
const h1 = document.createElement("h1");
|
||||||
|
h1.textContent = "404 Not Found";
|
||||||
|
card.appendChild(h1);
|
||||||
|
|
||||||
|
const p1 = document.createElement("p");
|
||||||
|
p1.textContent =
|
||||||
|
"Sorry — the resource you are looking for could not be found.";
|
||||||
|
card.appendChild(p1);
|
||||||
|
|
||||||
|
const p2 = document.createElement("p");
|
||||||
|
p2.textContent =
|
||||||
|
"If you believe you are seeing this message in error, please contact your IT department.";
|
||||||
|
card.appendChild(p2);
|
||||||
|
|
||||||
|
const kitten = document.createElement("div");
|
||||||
|
kitten.className = "kitten";
|
||||||
|
|
||||||
|
const kittenLine = document.createElement("p");
|
||||||
|
kittenLine.textContent = "I did find this picture of a kitten for you, though!";
|
||||||
|
kitten.appendChild(kittenLine);
|
||||||
|
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = KITTEN_IMG;
|
||||||
|
kitten.appendChild(img);
|
||||||
|
|
||||||
|
const attrib = document.createElement("div");
|
||||||
|
attrib.className = "attrib";
|
||||||
|
attrib.append("CC-BY-SA ");
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = "http://www.flickr.com/photos/bibbit/2756165489/";
|
||||||
|
link.textContent = "by storyvillegirl";
|
||||||
|
attrib.appendChild(link);
|
||||||
|
kitten.appendChild(attrib);
|
||||||
|
|
||||||
|
card.appendChild(kitten);
|
||||||
|
|
||||||
|
const seqtaLink = document.createElement("a");
|
||||||
|
seqtaLink.href = "http://www.seqta.com.au";
|
||||||
|
seqtaLink.textContent = "SEQTA";
|
||||||
|
card.appendChild(seqtaLink);
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorPageKittenPlugin: Plugin = {
|
||||||
|
id: "error-page-kitten",
|
||||||
|
name: "Classic 404 Page",
|
||||||
|
description: "Brings back SEQTA's old kitten 404 page",
|
||||||
|
version: "1.0.0",
|
||||||
|
settings: {},
|
||||||
|
disableToggle: true,
|
||||||
|
defaultEnabled: true,
|
||||||
|
styles,
|
||||||
|
|
||||||
|
run: async (_api) => {
|
||||||
|
let card: HTMLElement | null = null;
|
||||||
|
let hasBuilt = false;
|
||||||
|
let originalTitle = document.title;
|
||||||
|
let stripeStyle: HTMLStyleElement | null = null;
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
if (!is404Page()) return;
|
||||||
|
|
||||||
|
document.querySelector(".message")?.classList.add("bsplus-kitten-404-hidden");
|
||||||
|
|
||||||
|
if (hasBuilt) return;
|
||||||
|
const body = document.body;
|
||||||
|
if (!body) return;
|
||||||
|
|
||||||
|
hasBuilt = true;
|
||||||
|
document.documentElement.classList.add(KITTEN_CLASS);
|
||||||
|
document.title = "404 Not Found";
|
||||||
|
// Appended after the manager-injected styles so these same-specificity
|
||||||
|
// declarations win over the compiled baseline.
|
||||||
|
stripeStyle = document.createElement("style");
|
||||||
|
stripeStyle.textContent = H1_STRIPE_CSS;
|
||||||
|
document.head.appendChild(stripeStyle);
|
||||||
|
card = buildCard();
|
||||||
|
body.appendChild(card);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Body may not be parsed yet at document_start; render now and again on
|
||||||
|
// DOM ready as a safety net.
|
||||||
|
render();
|
||||||
|
document.addEventListener("DOMContentLoaded", render, { once: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("DOMContentLoaded", render);
|
||||||
|
document.documentElement.classList.remove(KITTEN_CLASS);
|
||||||
|
document
|
||||||
|
.querySelectorAll(".bsplus-kitten-404-hidden")
|
||||||
|
.forEach((el) => el.classList.remove("bsplus-kitten-404-hidden"));
|
||||||
|
document.title = originalTitle;
|
||||||
|
stripeStyle?.remove();
|
||||||
|
stripeStyle = null;
|
||||||
|
card?.remove();
|
||||||
|
card = null;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default errorPageKittenPlugin;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineLazyPlugin } from "../../core/dynamicLoader";
|
||||||
|
import styles from "./styles.css?inline";
|
||||||
|
|
||||||
|
export default defineLazyPlugin({
|
||||||
|
id: "error-page-kitten",
|
||||||
|
name: "Classic 404 Page",
|
||||||
|
description: "Brings back SEQTA's old kitten 404 page",
|
||||||
|
version: "1.0.0",
|
||||||
|
settings: {},
|
||||||
|
disableToggle: true,
|
||||||
|
defaultEnabled: true,
|
||||||
|
styles,
|
||||||
|
loader: () => import("./index"),
|
||||||
|
});
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/* Classic SEQTA 404 page, restored one-to-one over the rebranded document.
|
||||||
|
Scoped under html.bsplus-kitten-404 so it can never leak into the SPA;
|
||||||
|
the card element itself carries the .bsplus-kitten-404 class. */
|
||||||
|
|
||||||
|
/* The rebranded page styles <body> (Arial / flex / 25vh top / line-height
|
||||||
|
1.5 / letter-spacing). line-height and letter-spacing are reset because
|
||||||
|
the rebranded body sets them and the classic page uses browser defaults. */
|
||||||
|
html.bsplus-kitten-404 {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
html.bsplus-kitten-404 body {
|
||||||
|
background: #333;
|
||||||
|
font-family: 'Trebuchet MS', sans-serif;
|
||||||
|
font-size: 12pt;
|
||||||
|
color: #aaa;
|
||||||
|
margin: 8px;
|
||||||
|
padding: 0;
|
||||||
|
display: block;
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the rebranded page content while the classic card is shown. */
|
||||||
|
html.bsplus-kitten-404 .bsplus-kitten-404-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.bsplus-kitten-404 .bsplus-kitten-404 {
|
||||||
|
width: 600px;
|
||||||
|
margin: 64px auto;
|
||||||
|
background: #222;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: -1px -1px rgba(0, 0, 0, 0.5),
|
||||||
|
1px 1px rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 0 0 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.bsplus-kitten-404 .bsplus-kitten-404 > h1 {
|
||||||
|
background: #612;
|
||||||
|
/* Valid modern baseline; the reference's exact vendor-prefixed declarations
|
||||||
|
are injected at runtime (H1_STRIPE_CSS in index.ts), which win over this.
|
||||||
|
The vendor-prefixed lines can't live in this file because the CSS
|
||||||
|
minifier drops them. */
|
||||||
|
background-image: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(0, 0, 0, 0),
|
||||||
|
rgba(0, 0, 0, 0) 8px,
|
||||||
|
rgba(0, 0, 0, 0.05) 8px,
|
||||||
|
rgba(0, 0, 0, 0.05) 16px
|
||||||
|
),
|
||||||
|
linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.1));
|
||||||
|
color: #fff;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
text-shadow: 0 1px rgba(0, 0, 0, 0.5);
|
||||||
|
font-size: 150%;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.bsplus-kitten-404 .bsplus-kitten-404 > p {
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 0 1px rgba(0, 0, 0, 0.5);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.bsplus-kitten-404 .bsplus-kitten-404 > p > a {
|
||||||
|
color: #729fcf;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.bsplus-kitten-404 .bsplus-kitten-404 > .kitten {
|
||||||
|
font-size: 70%;
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.bsplus-kitten-404 .bsplus-kitten-404 > .kitten > p {
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.bsplus-kitten-404 .bsplus-kitten-404 > .kitten > .attrib {
|
||||||
|
color: #333;
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.bsplus-kitten-404 .bsplus-kitten-404 > .kitten > .attrib > a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.bsplus-kitten-404 .bsplus-kitten-404 > .kitten > img {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 0 4px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.bsplus-kitten-404 .bsplus-kitten-404 > a {
|
||||||
|
display: block;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0 8px 0 auto;
|
||||||
|
width: 100px;
|
||||||
|
text-align: center;
|
||||||
|
color: #444;
|
||||||
|
background: #111;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 80%;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: inset 1px 1px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
@@ -26,6 +26,9 @@ interface StorageChange<T = any> {
|
|||||||
|
|
||||||
/** Phased plugin startup: critical UI first, light DOM next, heavy plugins last. */
|
/** Phased plugin startup: critical UI first, light DOM next, heavy plugins last. */
|
||||||
const PLUGIN_START_PHASES: readonly string[][] = [
|
const PLUGIN_START_PHASES: readonly string[][] = [
|
||||||
|
// 404 page boot is a tiny standalone path; start it first so the kitten
|
||||||
|
// card renders before any SPA work. A no-op on normal SEQTA pages.
|
||||||
|
["error-page-kitten"],
|
||||||
["themes", "animated-background"],
|
["themes", "animated-background"],
|
||||||
[
|
[
|
||||||
"timetable",
|
"timetable",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import messageFoldersPluginLazy from "./built-in/messageFolders/lazy";
|
|||||||
import enhancedNavigationPluginLazy from "./built-in/enhancedNavigation/lazy";
|
import enhancedNavigationPluginLazy from "./built-in/enhancedNavigation/lazy";
|
||||||
import globalSearchPluginLazy from "./built-in/globalSearch/lazy";
|
import globalSearchPluginLazy from "./built-in/globalSearch/lazy";
|
||||||
import gradeAnalyticsPluginLazy from "./built-in/gradeAnalytics/lazy";
|
import gradeAnalyticsPluginLazy from "./built-in/gradeAnalytics/lazy";
|
||||||
|
import errorPageKittenPluginLazy from "./built-in/errorPageKitten/lazy";
|
||||||
|
|
||||||
// Initialize plugin manager
|
// Initialize plugin manager
|
||||||
const pluginManager = PluginManager.getInstance();
|
const pluginManager = PluginManager.getInstance();
|
||||||
@@ -36,6 +37,7 @@ pluginManager.registerPlugin(messageFoldersPluginLazy);
|
|||||||
pluginManager.registerPlugin(enhancedNavigationPluginLazy);
|
pluginManager.registerPlugin(enhancedNavigationPluginLazy);
|
||||||
pluginManager.registerPlugin(globalSearchPluginLazy);
|
pluginManager.registerPlugin(globalSearchPluginLazy);
|
||||||
pluginManager.registerPlugin(gradeAnalyticsPluginLazy);
|
pluginManager.registerPlugin(gradeAnalyticsPluginLazy);
|
||||||
|
pluginManager.registerPlugin(errorPageKittenPluginLazy);
|
||||||
|
|
||||||
export async function initializePlugins(): Promise<void> {
|
export async function initializePlugins(): Promise<void> {
|
||||||
await pluginManager.startAllPlugins();
|
await pluginManager.startAllPlugins();
|
||||||
|
|||||||
@@ -37,4 +37,5 @@ export const SYNCABLE_PLUGIN_SETTING_DEFAULTS: Record<
|
|||||||
passiveIndexing: true,
|
passiveIndexing: true,
|
||||||
},
|
},
|
||||||
"grade-analytics": { cacheTtlHours: 24 },
|
"grade-analytics": { cacheTtlHours: 24 },
|
||||||
|
"error-page-kitten": {},
|
||||||
};
|
};
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
// Jest stub for Vite asset imports (png, etc.): Vite returns a URL string.
|
||||||
|
export default "/resources/error-page/kitten.png";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Jest stub for Vite's `?inline` css imports: returns a CSS string.
|
||||||
|
export default "/* test inline css */";
|
||||||
@@ -32,6 +32,9 @@ export default {
|
|||||||
storage: { local, onChanged },
|
storage: { local, onChanged },
|
||||||
runtime: {
|
runtime: {
|
||||||
sendMessage: jest.fn(async () => undefined),
|
sendMessage: jest.fn(async () => undefined),
|
||||||
|
getURL: jest.fn(
|
||||||
|
(path: string) => `chrome-extension://test/${String(path).replace(/^\/+/, "")}`,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user