Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions dashboard/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"dependencies": {
"@fontsource-variable/inter": "^5.3.0",
"@fontsource-variable/sora": "^5.3.0",
"@gsap/react": "^2.1.2",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-popover": "^1.1.15",
Expand All @@ -33,6 +34,8 @@
"idb": "^8.0.4",
"lenis": "^1.3.27",
"lucide-react": "^1.61.0",
"motion": "^14.6.0",
"ogl": "^1.0.11",
"react": "^19.3.1",
"react-dom": "^19.3.1",
"react-router-dom": "^7.18.4",
Expand Down
23 changes: 23 additions & 0 deletions dashboard/src/animations/motion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Transition, Variants } from "framer-motion";

export const motionTiming = {
card: 0.38,
chart: 0.76,
page: 0.32,
stagger: 0.055,
} as const;

export const motionEase = {
interface: [0.23, 1, 0.32, 1] as const,
exit: [0.4, 0, 1, 1] as const,
};

export const pageVariants: Variants = {
initial: { opacity: 0, y: 9, filter: "blur(2px)" },
enter: { opacity: 1, y: 0, filter: "blur(0px)" },
};

export const pageTransition: Transition = {
duration: motionTiming.page,
ease: motionEase.interface,
};
62 changes: 62 additions & 0 deletions dashboard/src/animations/theme-transition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { flushSync } from "react-dom";

export function transitionTheme(
origin: HTMLElement | null,
next: "light" | "dark",
apply: () => void,
) {
const root = document.documentElement;
const reduced =
window.matchMedia("(prefers-reduced-motion: reduce)").matches ||
root.dataset.motion === "reduced" ||
root.dataset.performance === "on";
const start = () => {
root.dataset.theme = next;
root.style.colorScheme = next;
apply();
};

if (reduced || typeof document.startViewTransition !== "function") {
flushSync(start);
return;
}

const rect = origin?.getBoundingClientRect();
const x = rect ? rect.left + rect.width / 2 : innerWidth / 2;
const y = rect ? rect.top + rect.height / 2 : innerHeight / 2;
const radius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
);
root.dataset.themeTransition = "active";
let transition: ViewTransition;
try {
transition = document.startViewTransition(() => flushSync(start));
} catch {
delete root.dataset.themeTransition;
flushSync(start);
return;
}
transition.finished
.finally(() => {
delete root.dataset.themeTransition;
})
.catch(() => undefined);
transition.ready
.then(() => {
root.animate(
{
clipPath: [
`circle(0 at ${x}px ${y}px)`,
`circle(${radius}px at ${x}px ${y}px)`,
],
},
{
duration: 520,
easing: "cubic-bezier(0.23, 1, 0.32, 1)",
pseudoElement: "::view-transition-new(root)",
},
);
})
.catch(() => undefined);
}
62 changes: 56 additions & 6 deletions dashboard/src/components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Grid2X2,
Layers3,
Menu,
Puzzle,
Search,
Settings,
Sparkles,
Expand All @@ -22,9 +23,12 @@ import {
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
import { pageTransition, pageVariants } from "../animations/motion";
import { useDashboardStore } from "../state/useDashboardStore";
import { BranchedMenu } from "./BranchedMenu";
import { Dock } from "./Dock";
import { BellToggle } from "./react-bits/controls";
import { AnimatedThemeToggler } from "./react-bits/utilities";
import { Button } from "./ui";

const navItems = [
Expand All @@ -37,6 +41,7 @@ const navItems = [
{ label: "AI Software", to: "/software", icon: TerminalSquare },
{ label: "Tasks", to: "/tasks", icon: Activity },
{ label: "Monitoring", to: "/monitoring", icon: Gauge },
{ label: "Component Lab", to: "/component-lab", icon: Puzzle },
{ label: "Activity", to: "/activity", icon: Bell },
];
const routeNames: Record<string, string> = {
Expand All @@ -51,6 +56,7 @@ const routeNames: Record<string, string> = {
"/monitoring": "Monitoring",
"/activity": "Activity",
"/settings": "Settings",
"/component-lab": "Component Lab",
};
const branchedItems = [
{ label: "Overview", value: "/", icon: <Grid2X2 size={16} /> },
Expand All @@ -66,6 +72,11 @@ const branchedItems = [
{ value: "/nodes", label: "Nodes", icon: <Cpu size={16} /> },
{ value: "/monitoring", label: "Monitoring", icon: <Gauge size={16} /> },
{ value: "/activity", label: "Activity", icon: <Bell size={16} /> },
{
value: "/component-lab",
label: "Component Lab",
icon: <Puzzle size={16} />,
},
],
},
{
Expand Down Expand Up @@ -113,6 +124,7 @@ export function AppShell() {
const collapsed = useDashboardStore((s) => s.sidebarCollapsed);
const toggleSidebar = useDashboardStore((s) => s.toggleSidebar);
const theme = useDashboardStore((s) => s.theme);
const setTheme = useDashboardStore((s) => s.setTheme);
const demoMotion = useDashboardStore((s) => s.demoMotion);
const compactDashboard = useDashboardStore((s) => s.compactDashboard);
const performanceMode = useDashboardStore((s) => s.performanceMode);
Expand All @@ -123,12 +135,28 @@ export function AppShell() {
(s) => s.clearPersistenceError,
);
const prefersReducedMotion = useReducedMotion();
const [systemTheme, setSystemTheme] = useState<"light" | "dark">("light");
const [searchOpen, setSearchOpen] = useState(false);
const [notificationsEnabled, setNotificationsEnabled] = useState(true);
const [query, setQuery] = useState("");
const searchInput = useRef<HTMLInputElement>(null);
useEffect(() => {
const readPreference = () =>
setNotificationsEnabled(
localStorage.getItem("opencluster.notifications") !== "false",
);
readPreference();
window.addEventListener("opencluster:notifications-change", readPreference);
return () =>
window.removeEventListener(
"opencluster:notifications-change",
readPreference,
);
}, []);
useEffect(() => {
const media = window.matchMedia("(prefers-color-scheme: light)");
const updateTheme = () => {
setSystemTheme(media.matches ? "light" : "dark");
document.documentElement.dataset.theme =
theme === "system" ? (media.matches ? "light" : "dark") : theme;
};
Expand Down Expand Up @@ -292,6 +320,12 @@ export function AppShell() {
<strong>{routeNames[location.pathname] ?? "Dashboard"}</strong>
</div>
<div className="top-actions">
<AnimatedThemeToggler
theme={theme === "system" ? systemTheme : theme}
onThemeChange={setTheme}
className="top-theme-toggle"
title="Change dashboard theme"
/>
<Button
variant="secondary"
className="search-trigger"
Expand All @@ -305,12 +339,27 @@ export function AppShell() {
</Button>
<Button
variant="ghost"
aria-label="Notifications"
aria-label="Activity"
onClick={() => navigate("/activity")}
>
<Bell size={17} />
<span className="notification-dot" />
<Activity size={17} />
{notificationsEnabled && <span className="notification-dot" />}
</Button>
<BellToggle
checked={notificationsEnabled}
onCheckedChange={(enabled) => {
localStorage.setItem(
"opencluster.notifications",
String(enabled),
);
setNotificationsEnabled(enabled);
window.dispatchEvent(
new Event("opencluster:notifications-change"),
);
}}
label="Activity notifications"
className="top-notification-toggle"
/>
<span className="top-divider" />
<div className="top-avatar" title="Demo operator">
OC
Expand Down Expand Up @@ -338,11 +387,12 @@ export function AppShell() {
className="page-transition"
initial={
demoMotion && !performanceMode && !prefersReducedMotion
? { opacity: 0, y: 8 }
? "initial"
: false
}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.24, ease: "easeOut" }}
animate="enter"
variants={pageVariants}
transition={pageTransition}
>
<Outlet />
</motion.div>
Expand Down
Loading
Loading