refactor(web): drop light mode, go dark-only

Removes the light/dark toggle infrastructure in favor of a single dark
theme. Deletes the theme context, boot script, light-mode CSS overrides,
logotype-light asset, and the pierre-light diff theme. Collapses
graph-theme into a single constant. Adds scheme-only-dark on <html> so
native controls and the server-injected Graphviz @media query render
dark.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-20 09:27:08 -04:00
parent b8d93603f8
commit 5370a64f96
No known key found for this signature in database
22 changed files with 2443 additions and 2682 deletions

View file

@ -40,7 +40,7 @@
--color-amber: #F0A45B;
--color-coral: #E86B6B;
/* ── Semantic tokens (dark defaults) ── */
/* ── Semantic tokens ── */
--color-page: #0F1729;
--color-panel: #252C3D;
--color-panel-alt: #1a2133;
@ -53,39 +53,10 @@
--color-line: rgba(255, 255, 255, 0.06);
--color-line-strong: rgba(255, 255, 255, 0.12);
--color-focus: rgba(103, 178, 215, 0.4);
/* Text color that sits on --color-teal-500 primary fills.
Dark mode: teal-500 is light (#67B2D7) so dark text reads best.
Light mode: teal-500 is dark (#0e7490) so white reads best. */
/* Text color that sits on --color-teal-500 primary fills. */
--color-on-primary: #0F1729;
}
/* ── Light theme overrides ── */
.light {
/* Semantic tokens — page is softly tinted so panels/cards (white)
read as the lighter surface, mirroring the dark-theme hierarchy. */
--color-page: #eef2f7;
--color-panel: #ffffff;
--color-panel-alt: #e4eaf1;
--color-overlay: rgba(15, 23, 41, 0.05);
--color-overlay-strong: rgba(15, 23, 41, 0.10);
--color-fg: #0f172a;
--color-fg-2: #1e293b;
--color-fg-3: #475569;
--color-fg-muted: #64748b;
--color-line: rgba(15, 23, 41, 0.10);
--color-line-strong: rgba(15, 23, 41, 0.18);
--color-focus: rgba(14, 116, 144, 0.5);
--color-on-primary: #ffffff;
/* Accent — darkened for WCAG AA on white */
--color-mint: #0d9488;
--color-amber: #d97706;
--color-coral: #dc2626;
--color-teal-300: #0891b2;
--color-teal-500: #0e7490;
}
/* ── Chart variables ── */
:root {
@ -93,11 +64,6 @@
--chart-axis-label: rgba(75, 87, 104, 1);
}
.light {
--chart-gridline: rgba(0, 0, 0, 0.06);
--chart-axis-label: rgba(71, 85, 105, 1);
}
/* ── Background atmosphere ── */
.bg-atmosphere {
@ -108,13 +74,6 @@
background-attachment: fixed;
}
.light .bg-atmosphere,
.light.bg-atmosphere {
background-image:
radial-gradient(ellipse 120% 60% at 15% 5%, rgba(14, 116, 144, 0.06) 0%, transparent 50%),
radial-gradient(ellipse 80% 50% at 85% 90%, rgba(13, 148, 136, 0.04) 0%, transparent 45%);
}
.bg-atmosphere::before {
content: "";
position: fixed;
@ -127,11 +86,6 @@
background-size: 200px 200px;
}
.light .bg-atmosphere::before,
.light.bg-atmosphere::before {
opacity: 0.03;
}
/* ── @pierre/diffs brand overrides ── */
:root {
@ -159,28 +113,16 @@
}
/* ── Server-rendered SVG graph overrides ── */
/* The server injects @media (prefers-color-scheme: dark) but the app uses
.dark/.light classes, so we apply dark-mode colors via the class. */
/* The server injects @media (prefers-color-scheme: dark) which won't reliably
fire when the OS is in light mode, so we apply dark graph colors always. */
.dark .graph-svg text { fill: #c6d4e0; }
.dark .graph-svg [stroke="#357f9e"] { stroke: #67B2D7; }
.dark .graph-svg [stroke="#666666"] { stroke: #5a7a94; }
.dark .graph-svg polygon[fill="#357f9e"] { fill: #67B2D7; }
.dark .graph-svg polygon[fill="#666666"] { fill: #5a7a94; }
.dark .graph-svg ellipse[fill="#ffffff"],
.dark .graph-svg polygon[fill="#ffffff"] { fill: #1a2b3c; }
.dark .graph-svg ellipse[stroke="#357f9e"],
.dark .graph-svg polygon[stroke="#357f9e"] { stroke: #67B2D7; }
.dark .graph-svg [fill="#1a1a1a"] { fill: #c6d4e0; }
.light {
--diffs-bg-buffer-override: #ffffff;
--diffs-bg-context-override: #f8fafc;
--diffs-bg-hover-override: #f1f5f9;
--diffs-bg-separator-override: #e2e8f0;
--diffs-fg-number-override: #94a3b8;
--diffs-addition-color-override: #0d9488;
--diffs-fg-number-addition-override: rgba(13, 148, 136, 0.5);
--diffs-deletion-color-override: #dc2626;
--diffs-fg-number-deletion-override: rgba(220, 38, 38, 0.5);
}
.graph-svg text { fill: #c6d4e0; }
.graph-svg [stroke="#357f9e"] { stroke: #67B2D7; }
.graph-svg [stroke="#666666"] { stroke: #5a7a94; }
.graph-svg polygon[fill="#357f9e"] { fill: #67B2D7; }
.graph-svg polygon[fill="#666666"] { fill: #5a7a94; }
.graph-svg ellipse[fill="#ffffff"],
.graph-svg polygon[fill="#ffffff"] { fill: #1a2b3c; }
.graph-svg ellipse[stroke="#357f9e"],
.graph-svg polygon[stroke="#357f9e"] { stroke: #67B2D7; }
.graph-svg [fill="#1a1a1a"] { fill: #c6d4e0; }

View file

@ -2,7 +2,6 @@ import { useState } from "react";
import { ChevronRightIcon } from "@heroicons/react/20/solid";
import type { FileContents } from "@pierre/diffs";
import { File } from "@pierre/diffs/react";
import { useTheme } from "../lib/theme";
export function CollapsibleFile({
file,
@ -12,7 +11,6 @@ export function CollapsibleFile({
defaultOpen?: boolean;
}) {
const [open, setOpen] = useState(defaultOpen);
const { theme } = useTheme();
const lines = file.contents.split("\n");
const lineCount = lines.length;
@ -38,7 +36,7 @@ export function CollapsibleFile({
<div className="border-t border-line" />
<File
file={file}
options={{ theme: theme === "dark" ? "pierre-dark" : "pierre-light", disableFileHeader: true }}
options={{ theme: "pierre-dark", disableFileHeader: true }}
/>
</div>
</div>

View file

@ -7,7 +7,7 @@ import { dotLanguage } from "./dot-grammar";
export async function registerDotLanguage(): Promise<void> {
const highlighter = await getSharedHighlighter({
themes: ["pierre-dark", "pierre-light"],
themes: ["pierre-dark"],
langs: [],
});
attachResolvedLanguages(

View file

@ -12,16 +12,13 @@ import {
BeakerIcon,
ChartBarIcon,
Cog6ToothIcon,
MoonIcon,
PlayIcon,
RectangleStackIcon,
SunIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { Link, Outlet, useLocation, useMatches, useRevalidator } from "react-router";
import { getAuthMe } from "../api";
import { DemoModeProvider } from "../lib/demo-mode";
import { useTheme } from "../lib/theme";
export async function loader() {
return getAuthMe();
@ -47,7 +44,6 @@ export default function AppShell({ loaderData }: any) {
const { pathname } = useLocation();
const matches = useMatches();
const revalidator = useRevalidator();
const { theme, toggle } = useTheme();
const navigation = getVisibleNavigation(demoMode);
const currentNav = navigation.find((item) => pathname.startsWith(item.href));
const title = currentNav?.name ?? "";
@ -58,8 +54,6 @@ export default function AppShell({ loaderData }: any) {
const wide = matches.some((m) => (m.handle as { wide?: boolean } | undefined)?.wide);
const maxWidth = wide ? "" : "max-w-5xl";
const ThemeIcon = theme === "dark" ? SunIcon : MoonIcon;
async function toggleDemoMode() {
await fetch("/api/v1/demo/toggle", {
method: "POST",
@ -79,7 +73,7 @@ export default function AppShell({ loaderData }: any) {
<div className="flex items-center">
<div className="shrink-0">
<Link to={demoMode ? "/start" : "/runs"}>
<img alt="Fabro" src={theme === "dark" ? "/logotype.svg" : "/logotype-light.svg"} className="h-8 w-auto" />
<img alt="Fabro" src="/logotype.svg" className="h-8 w-auto" />
</Link>
</div>
<div className="hidden md:block">
@ -120,15 +114,6 @@ export default function AppShell({ loaderData }: any) {
<BeakerIcon className="size-5" aria-hidden="true" />
<span className="sr-only">Toggle demo mode</span>
</button>
<button
type="button"
onClick={toggle}
className="rounded-full p-1.5 text-fg-muted transition-colors hover:bg-overlay hover:text-fg"
title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
>
<ThemeIcon className="size-5" aria-hidden="true" />
<span className="sr-only">Toggle theme</span>
</button>
<Menu as="div" className="relative">
<MenuButton className="relative flex max-w-xs items-center rounded-full focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500">
<span className="absolute -inset-1.5" />
@ -228,15 +213,6 @@ export default function AppShell({ loaderData }: any) {
<BeakerIcon className="size-5" aria-hidden="true" />
<span className="sr-only">Toggle demo mode</span>
</button>
<button
type="button"
onClick={toggle}
className="rounded-full p-1.5 text-fg-muted transition-colors hover:bg-overlay hover:text-fg"
title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
>
<ThemeIcon className="size-5" aria-hidden="true" />
<span className="sr-only">Toggle theme</span>
</button>
</div>
</div>
{provider !== "tailscale" && (

View file

@ -1,6 +1,4 @@
import type { Theme } from "./theme";
interface GraphTheme {
export interface GraphTheme {
fontcolor: string;
edgeColor: string;
nodeFill: string;
@ -24,7 +22,7 @@ interface GraphTheme {
failedText: string;
}
const dark: GraphTheme = {
export const graphTheme: GraphTheme = {
fontcolor: "#5a7a94",
edgeColor: "#2a3f52",
nodeFill: "#1a2b3c",
@ -47,32 +45,3 @@ const dark: GraphTheme = {
failedBorder: "#f87171",
failedText: "#fca5a5",
};
const light: GraphTheme = {
fontcolor: "#475569",
edgeColor: "#cbd5e1",
nodeFill: "#f1f5f9",
nodeText: "#1e293b",
startFill: "#ccfbf1",
startBorder: "#0d9488",
startText: "#0d9488",
gateFill: "#fef3c7",
gateBorder: "#d97706",
gateText: "#d97706",
completedFill: "#d1fae5",
completedBorder: "#059669",
completedText: "#059669",
runningFill: "#ccfbf1",
runningBorder: "#0d9488",
runningText: "#0d9488",
runningPulseFill: "#99f6e4",
runningPulseStroke: "#0d9488",
failedFill: "#fef2f2",
failedBorder: "#dc2626",
failedText: "#dc2626",
};
export function getGraphTheme(theme: Theme): GraphTheme {
return theme === "dark" ? dark : light;
}

View file

@ -1,24 +0,0 @@
import { describe, expect, test } from "bun:test";
import { buildThemeBootScript, resolveTheme } from "./theme-selection";
describe("resolveTheme", () => {
test("defaults to dark when no saved theme exists", () => {
expect(resolveTheme(null)).toBe("dark");
expect(resolveTheme("system")).toBe("dark");
});
test("uses the saved theme when it is valid", () => {
expect(resolveTheme("light")).toBe("light");
expect(resolveTheme("dark")).toBe("dark");
});
});
describe("buildThemeBootScript", () => {
test("defaults the boot script to dark without using system preference", () => {
const script = buildThemeBootScript();
expect(script).toContain('localStorage.getItem("fabro-theme")');
expect(script).toContain('t="dark"');
expect(script).not.toContain("matchMedia");
});
});

View file

@ -1,16 +0,0 @@
type Theme = "light" | "dark";
const STORAGE_KEY = "fabro-theme";
function resolveTheme(storedTheme: string | null): Theme {
return storedTheme === "light" || storedTheme === "dark"
? storedTheme
: "dark";
}
function buildThemeBootScript(storageKey = STORAGE_KEY) {
return `(function(){try{var t=localStorage.getItem("${storageKey}");if(t!=="light"&&t!=="dark")t="dark";document.documentElement.classList.add(t)}catch(e){document.documentElement.classList.add("dark")}})()`;
}
export { STORAGE_KEY, buildThemeBootScript, resolveTheme };
export type { Theme };

View file

@ -1,47 +0,0 @@
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { STORAGE_KEY, resolveTheme } from "./theme-selection";
import type { Theme } from "./theme-selection";
function getInitialTheme(): Theme {
if (typeof window === "undefined") return "dark";
return resolveTheme(localStorage.getItem(STORAGE_KEY));
}
function applyThemeClass(theme: Theme) {
const root = document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(theme);
}
const ThemeContext = createContext<{
theme: Theme;
toggle: () => void;
}>({ theme: "dark", toggle: () => {} });
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>(getInitialTheme);
useEffect(() => {
applyThemeClass(theme);
}, [theme]);
const toggle = useCallback(() => {
setTheme((prev) => {
const next = prev === "dark" ? "light" : "dark";
localStorage.setItem(STORAGE_KEY, next);
return next;
});
}, []);
return (
<ThemeContext value={{ theme, toggle }}>
{children}
</ThemeContext>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
export type { Theme };

View file

@ -1,21 +1,8 @@
import { isRouteErrorResponse, Outlet } from "react-router";
import { ThemeProvider } from "./lib/theme";
import { buildThemeBootScript } from "./lib/theme-selection";
import "./app.css";
const themeScript = buildThemeBootScript();
export default function Root() {
return (
<ThemeProvider>
<ThemeBoot />
<Outlet />
</ThemeProvider>
);
}
function ThemeBoot() {
return <script dangerouslySetInnerHTML={{ __html: themeScript }} />;
return <Outlet />;
}
export function ErrorBoundary({ error }: any) {

View file

@ -12,7 +12,6 @@ import {
useRevalidator,
} from "react-router";
import { MultiFileDiff, PatchDiff, Virtualizer } from "@pierre/diffs/react";
import { useTheme } from "../lib/theme";
import type {
FileDiff as ApiFileDiff,
PaginatedRunFileList,
@ -285,7 +284,6 @@ function resolveRunStatus(matches: ReturnType<typeof useMatches>): string | unde
}
export default function RunFiles({ loaderData }: any) {
const theme = useTheme();
const params = useParams();
const navigation = useNavigation();
const revalidator = useRevalidator();
@ -352,9 +350,6 @@ export default function RunFiles({ loaderData }: any) {
[diffStyleForced],
);
const pierreTheme =
theme.theme === "dark" ? "pierre-dark" : "pierre-light";
const refreshButtonRef = useRef<HTMLButtonElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
@ -452,14 +447,14 @@ export default function RunFiles({ loaderData }: any) {
newFile={file.new_file}
options={{
diffStyle,
theme: pierreTheme,
theme: "pierre-dark",
expandUnchanged: isDeepLinkTarget ? true : undefined,
}}
/>
</div>
);
}),
[diffStyle, pierreTheme, hashFile],
[diffStyle, hashFile],
);
if (isInitialLoading) {
@ -528,7 +523,7 @@ export default function RunFiles({ loaderData }: any) {
patch={meta.patch}
options={{
diffStyle,
theme: pierreTheme,
theme: "pierre-dark",
}}
/>
{emptyToast && <Toast>{emptyToast}</Toast>}

View file

@ -1,7 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useParams } from "react-router";
import { useTheme } from "../lib/theme";
import { getGraphTheme } from "../lib/graph-theme";
import { graphTheme } from "../lib/graph-theme";
import { apiFetch, apiJsonOrNull } from "../api";
import { isVisibleStage } from "../data/runs";
import { formatDurationSecs } from "../lib/format";
@ -34,7 +33,8 @@ export async function loader({ request, params }: any) {
type Direction = "LR" | "TB";
function buildDot(direction: Direction, gt: ReturnType<typeof getGraphTheme>) {
function buildDot(direction: Direction) {
const gt = graphTheme;
return `digraph sync {
graph [label="Sync"]
rankdir=${direction}
@ -89,7 +89,8 @@ function stripGraphTitle(svg: SVGSVGElement) {
title.remove();
}
function annotateRunningNodes(svg: SVGSVGElement, gt: ReturnType<typeof getGraphTheme>, stageList: Stage[]) {
function annotateRunningNodes(svg: SVGSVGElement, stageList: Stage[]) {
const gt = graphTheme;
const runningDotIds = new Set(
stageList.filter((s) => s.status === "running").map((s) => s.dotId),
);
@ -185,8 +186,6 @@ export default function RunGraph({ loaderData }: any) {
const [pan, setPan] = useState({ x: 0, y: 0 });
const dragState = useRef<{ startX: number; startY: number; startPanX: number; startPanY: number } | null>(null);
const zoom = GRAPH_ZOOM_STEPS[zoomIndex];
const { theme } = useTheme();
const graphTheme = getGraphTheme(theme);
useEffect(() => {
let cancelled = false;
@ -214,7 +213,7 @@ export default function RunGraph({ loaderData }: any) {
const { instance } = await import("@viz-js/viz");
const viz = await instance();
if (cancelled) return;
svg = viz.renderSVGElement(buildDot(direction, graphTheme));
svg = viz.renderSVGElement(buildDot(direction));
}
stripGraphTitle(svg);
@ -231,7 +230,7 @@ export default function RunGraph({ loaderData }: any) {
setPan({ x: 0, y: 0 });
render();
return () => { cancelled = true; };
}, [direction, graphTheme, graphSvg, id]);
}, [direction, graphSvg, id]);
const onPointerDown = useCallback((e: React.PointerEvent) => {
if ((e.target as HTMLElement).closest("button")) return;

View file

@ -3,8 +3,7 @@ import { useNavigate, useParams } from "react-router";
import { apiFetch, apiJsonOrNull } from "../api";
import { isVisibleStage } from "../data/runs";
import { formatDurationSecs } from "../lib/format";
import { useTheme } from "../lib/theme";
import { getGraphTheme } from "../lib/graph-theme";
import { graphTheme } from "../lib/graph-theme";
import { StageSidebar } from "../components/stage-sidebar";
import type { Stage } from "../components/stage-sidebar";
import {
@ -47,7 +46,6 @@ export default function RunOverview({ loaderData }: any) {
const innerRef = useRef<HTMLDivElement>(null);
const svgRef = useRef<SVGSVGElement | null>(null);
const navigate = useNavigate();
const { theme } = useTheme();
const [zoomIndex, setZoomIndex] = useState(GRAPH_DEFAULT_ZOOM_INDEX);
const [direction, setDirection] = useState<Direction>("LR");
const [pan, setPan] = useState({ x: 0, y: 0 });
@ -69,7 +67,7 @@ export default function RunOverview({ loaderData }: any) {
if (!svg) return;
svgRef.current = svg;
const gt = getGraphTheme(theme);
const gt = graphTheme;
const runningDotIds = new Set<string>(
stages.filter((s: Stage) => s.status === "running").map((s: Stage) => s.dotId ?? s.id),
);
@ -157,7 +155,7 @@ export default function RunOverview({ loaderData }: any) {
}
})();
return () => { cancelled = true; };
}, [stages, graphSvg, theme, direction, id]);
}, [stages, graphSvg, direction, id]);
const onPointerDown = useCallback((e: React.PointerEvent) => {
if ((e.target as HTMLElement).closest("button")) return;

View file

@ -1,11 +1,11 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { ArrowDownIcon, ArrowRightIcon, MinusIcon, PlusIcon } from "@heroicons/react/20/solid";
import { useTheme } from "../lib/theme";
import { getGraphTheme } from "../lib/graph-theme";
import { graphTheme } from "../lib/graph-theme";
type Direction = "LR" | "TB";
function buildDot(direction: Direction, theme: ReturnType<typeof getGraphTheme>) {
function buildDot(direction: Direction) {
const theme = graphTheme;
return `digraph fix_build {
graph [
label="Fix Build"
@ -78,8 +78,6 @@ export default function WorkflowDiagram() {
const [pan, setPan] = useState({ x: 0, y: 0 });
const dragState = useRef<{ startX: number; startY: number; startPanX: number; startPanY: number } | null>(null);
const zoom = ZOOM_STEPS[zoomIndex];
const { theme } = useTheme();
const graphTheme = getGraphTheme(theme);
useEffect(() => {
let cancelled = false;
@ -90,7 +88,7 @@ export default function WorkflowDiagram() {
if (cancelled) return;
try {
const svg = viz.renderSVGElement(buildDot(direction, graphTheme));
const svg = viz.renderSVGElement(buildDot(direction));
stripGraphTitle(svg);
svgRef.current = svg;
@ -105,7 +103,7 @@ export default function WorkflowDiagram() {
setPan({ x: 0, y: 0 });
render();
return () => { cancelled = true; };
}, [direction, graphTheme]);
}, [direction]);
const onPointerDown = useCallback((e: React.PointerEvent) => {
if ((e.target as HTMLElement).closest("button")) return;

View file

@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" class="h-full bg-atmosphere">
<html lang="en" class="h-full bg-atmosphere scheme-only-dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@ -13,9 +13,6 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=JetBrains+Mono:wght@400;500;600&display=swap"
/>
<script>
(function(){try{var t=localStorage.getItem("fabro-theme");if(t!=="light"&&t!=="dark")t="dark";document.documentElement.classList.add(t)}catch(e){document.documentElement.classList.add("dark")}})()
</script>
{{styles}}
</head>
<body class="h-full font-sans antialiased">

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.8 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" class="h-full bg-atmosphere">
<html lang="en" class="h-full bg-atmosphere scheme-only-dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@ -13,11 +13,8 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=JetBrains+Mono:wght@400;500;600&display=swap"
/>
<script>
(function(){try{var t=localStorage.getItem("fabro-theme");if(t!=="light"&&t!=="dark")t="dark";document.documentElement.classList.add(t)}catch(e){document.documentElement.classList.add("dark")}})()
</script>
<link rel="stylesheet" href="/assets/app.css" />
<link rel="stylesheet" href="/assets/entry-g7fj8z7g.css" />
<link rel="stylesheet" href="/assets/entry-4r6r51av.css" />
</head>
<body class="h-full font-sans antialiased">
<div id="root"></div>
@ -61,7 +58,7 @@
<script type="module" src="/assets/chunk-sadshphz.js"></script>
<script type="module" src="/assets/chunk-pmthkscp.js"></script>
<script type="module" src="/assets/chunk-v61ks9f7.js"></script>
<script type="module" src="/assets/entry-pmqz8d3n.js"></script>
<script type="module" src="/assets/entry-78vt4q5f.js"></script>
<script type="module" src="/assets/chunk-n1k68xa8.js"></script>
<script type="module" src="/assets/chunk-rsph5pvm.js"></script>
<script type="module" src="/assets/chunk-9t57pdty.js"></script>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.8 KiB