mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-10 03:30:59 +00:00
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:
parent
b8d93603f8
commit
5370a64f96
22 changed files with 2443 additions and 2682 deletions
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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" && (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 |
2
lib/crates/fabro-spa/assets/assets/app.css
generated
2
lib/crates/fabro-spa/assets/assets/app.css
generated
File diff suppressed because one or more lines are too long
1
lib/crates/fabro-spa/assets/assets/entry-4r6r51av.css
generated
Normal file
1
lib/crates/fabro-spa/assets/assets/entry-4r6r51av.css
generated
Normal file
File diff suppressed because one or more lines are too long
2399
lib/crates/fabro-spa/assets/assets/entry-78vt4q5f.js
generated
Normal file
2399
lib/crates/fabro-spa/assets/assets/entry-78vt4q5f.js
generated
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2399
lib/crates/fabro-spa/assets/assets/entry-pmqz8d3n.js
generated
2399
lib/crates/fabro-spa/assets/assets/entry-pmqz8d3n.js
generated
File diff suppressed because one or more lines are too long
9
lib/crates/fabro-spa/assets/index.html
generated
9
lib/crates/fabro-spa/assets/index.html
generated
|
|
@ -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>
|
||||
|
|
|
|||
4
lib/crates/fabro-spa/assets/logotype-light.svg
generated
4
lib/crates/fabro-spa/assets/logotype-light.svg
generated
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 7.8 KiB |
Loading…
Add table
Reference in a new issue