feat: dual-skin system — all home components XP ↔ Sci-Fi - #2
Conversation
Restyle global CSS tokens, components, and layout for XP theme. Co-authored-by: Zihan Huang <84128702+bravohenry@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 733e976b22
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * CSS CUSTOM PROPERTIES — Windows XP Luna theme | ||
| * -------------------------------------------------------------------------- */ | ||
|
|
||
| :root { |
There was a problem hiding this comment.
Reintroduce light-theme overrides for data-theme toggling
The global theme refactor now defines only :root variables, but ThemeToggle still flips data-theme between dark and light (src/components/ui/ThemeToggle.tsx). Because the [data-theme="light"] overrides were removed, toggling persists state and changes the icon/label but no visual theme tokens actually change, so the theme switcher is effectively broken for users. Please restore a light-theme override block (or remove/disable the toggle behavior) so the control has a real effect.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR restyles the FRI dashboard UI from the current neon/glass aesthetic to a Windows XP “Luna” look-and-feel, introducing XP window chrome (titlebars, beveled borders), XP typography, and an XP taskbar.
Changes:
- Replaces global design tokens and core UI styling in
globals.cssto implement XP Luna colors, fonts, chrome, scrollbars, and controls. - Updates multiple home dashboard components to render XP-like titlebars/menu bars and CMD-style terminal presentation.
- Adds a new
XPTaskbarcomponent and mounts it on the homepage.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/styles/globals.css | Rebuilds global theme variables and adds XP chrome/control styles. |
| src/components/ui/ThemeToggle.tsx | Restyles the toggle as an XP window-control button with glyph icons. |
| src/components/ui/TechBorder.tsx | Changes TechBorder wrapper styling to XP window chrome and adds optional title. |
| src/components/home/XPTaskbar.tsx | Introduces a new XP taskbar + Start menu component. |
| src/components/home/WidgetPanel.tsx | Reworks widget panel UI into an XP window with tabs/status bar styling. |
| src/components/home/TypeWriter.tsx | Updates typography/styling to match XP console-like text. |
| src/components/home/Terminal.tsx | Converts terminal panel to CMD-style titlebar/body and XP input/button styling. |
| src/components/home/SystemHeader.tsx | Converts header to XP titlebar + menubar layout with window controls. |
| src/components/home/IdentityMatrix.tsx | Converts panel to XP titlebar and XP list/status-bar styling. |
| src/components/home/Diagnostics.tsx | Converts diagnostics to XP titlebar + groupbox/list-view styling. |
| src/components/home/CoreDirectives.tsx | Converts directives panel to XP titlebar + toolbar/footer styling. |
| src/components/home/ArcReactor.tsx | Wraps center display in an XP window layout and adds XP-like bottom controls bar. |
| src/app/page.tsx | Integrates XP taskbar and tightens grid spacing/padding. |
| src/app/layout.tsx | Updates metadata title and forces XP font styling on <body>. |
| node_modules/.package-lock.json | Updates a tracked lockfile under node_modules/ (should not be committed). |
Comments suppressed due to low confidence (1)
src/components/home/SystemHeader.tsx:46
- The clock format now only shows hours/minutes (no seconds), but the component still re-renders every 1s (
setInterval(tick, 1000)). Consider updating less frequently (e.g., every 10–60s) to reduce unnecessary renders.
const clockFmt = new Intl.DateTimeFormat("en-US", {
timeZone: "America/New_York",
hour12: true,
hour: "numeric",
minute: "2-digit",
});
/* ------------------------------------------------------------------ */
/* Component */
/* ------------------------------------------------------------------ */
export function SystemHeader({ totalEntries, totalWords, daysSinceLaunch }: SystemHeaderProps) {
const [clock, setClock] = useState("");
/* --- NYC clock (1 s) --- */
useEffect(() => {
const tick = () => setClock(clockFmt.format(new Date()));
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -521,23 +553,29 @@ | |||
| "url": "https://opencollective.com/libvips" | |||
| }, | |||
| "optionalDependencies": { | |||
| "@img/sharp-libvips-darwin-arm64": "1.2.4" | |||
| "@img/sharp-libvips-linux-x64": "1.2.4" | |||
| } | |||
| }, | |||
| "node_modules/@img/sharp-libvips-darwin-arm64": { | |||
| "version": "1.2.4", | |||
| "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", | |||
| "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", | |||
| "node_modules/@img/sharp-linuxmusl-x64": { | |||
| "version": "0.34.5", | |||
| "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", | |||
| "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", | |||
| "cpu": [ | |||
| "arm64" | |||
| "x64" | |||
| ], | |||
| "license": "LGPL-3.0-or-later", | |||
| "license": "Apache-2.0", | |||
| "optional": true, | |||
| "os": [ | |||
| "darwin" | |||
| "linux" | |||
| ], | |||
| "engines": { | |||
| "node": "^18.17.0 || ^20.3.0 || >=21.0.0" | |||
| }, | |||
| "funding": { | |||
| "url": "https://opencollective.com/libvips" | |||
| }, | |||
| "optionalDependencies": { | |||
| "@img/sharp-libvips-linuxmusl-x64": "1.2.4" | |||
| } | |||
| }, | |||
There was a problem hiding this comment.
This file lives under node_modules/ (which is gitignored) and shouldn’t be tracked/modified in the repo. Please remove node_modules/.package-lock.json from version control (and from this PR), and ensure dependency changes are reflected only in the root package-lock.json.
| return ( | ||
| <div | ||
| id="session-panel" | ||
| className={`h-1/3 min-h-[200px] md:min-h-0 glass-panel tech-border rounded-t-lg p-4 md:p-6 flex flex-col justify-end mt-4 ${ | ||
| className={`h-1/3 min-h-[200px] md:min-h-0 flex flex-col mt-4 ${ | ||
| expanded ? "expanded" : "" | ||
| }`} | ||
| style={{ | ||
| border: '1px solid #848484', | ||
| boxShadow: '1px 1px 0 #ffffff inset, -1px -1px 0 #808080 inset', | ||
| }} | ||
| > | ||
| {/* Corner decorations */} | ||
| <div className="corner-bl absolute bottom-0 left-0 w-3 h-3" /> | ||
| <div className="corner-br absolute bottom-0 right-0 w-3 h-3" /> | ||
|
|
There was a problem hiding this comment.
corner-bl / corner-br are absolutely positioned, but the #session-panel container is no longer position: relative (previously provided by .glass-panel). This will cause the corner decorations to position relative to the page instead of the terminal panel. Add a relative class (or otherwise set position: relative) on the #session-panel container, or remove the absolute corner elements.
| <div className="flex items-center gap-2"> | ||
| <span className="status-dot w-2 h-2 animate-pulse shrink-0" style={{ background: 'var(--text-status)' }} /> | ||
| <span className="hidden sm:inline">SYSTEM ONLINE</span> | ||
| <header className="flex-none z-50 xp-window" style={{ borderRadius: '0 !important' }}> |
There was a problem hiding this comment.
React inline styles don’t support !important, so style={{ borderRadius: '0 !important' }} is invalid and will be ignored by the browser. If you need to enforce no rounding here, rely on the global border-radius: 0 !important rule or move this override into a CSS class.
| <header className="flex-none z-50 xp-window" style={{ borderRadius: '0 !important' }}> | |
| <header className="flex-none z-50 xp-window" style={{ borderRadius: 0 }}> |
| interface SystemHeaderProps { | ||
| totalEntries: number; | ||
| totalWords: number; | ||
| daysSinceLaunch: number; | ||
| } | ||
|
|
||
| /* ------------------------------------------------------------------ */ | ||
| /* Constants */ | ||
| /* ------------------------------------------------------------------ */ | ||
|
|
||
| const clockFmt = new Intl.DateTimeFormat("en-US", { | ||
| timeZone: "America/New_York", | ||
| hour12: false, | ||
| hour: "2-digit", | ||
| hour12: true, | ||
| hour: "numeric", | ||
| minute: "2-digit", | ||
| second: "2-digit", | ||
| }); | ||
|
|
||
| /* ------------------------------------------------------------------ */ | ||
| /* Component */ | ||
| /* ------------------------------------------------------------------ */ | ||
|
|
||
| export function SystemHeader({ totalEntries, totalWords }: SystemHeaderProps) { | ||
| export function SystemHeader({ totalEntries, totalWords, daysSinceLaunch }: SystemHeaderProps) { | ||
| const [clock, setClock] = useState(""); | ||
|
|
There was a problem hiding this comment.
daysSinceLaunch is included in the component props/destructuring but isn’t used anywhere in the render. This will typically trigger an unused-var lint warning and makes the public props harder to reason about. Either use it in the UI or remove it from the destructured props (and possibly from the interface if no longer needed).
| return ( | ||
| <div className={`glass-panel tech-border rounded-sm relative ${className}`}> | ||
| <div className={`xp-window relative ${className}`}> | ||
| {title && ( | ||
| <div className="xp-panel-title"> | ||
| {title} | ||
| </div> | ||
| )} | ||
| <div className="corner-bl absolute bottom-0 left-0 w-3 h-3" /> | ||
| <div className="corner-br absolute bottom-0 right-0 w-3 h-3" /> | ||
| {children} |
There was a problem hiding this comment.
TechBorder no longer includes the tech-border class, but it still renders .corner-bl / .corner-br elements that are only styled under .tech-border > .corner-* in globals.css. As a result, these corner divs will have no visible styling. Either re-add the tech-border class to the wrapper or remove the corner elements (and/or the now-dead .tech-border CSS).
| export function ThemeToggle() { | ||
| const [theme, setTheme] = useState<"dark" | "light">("dark"); | ||
|
|
||
| useEffect(() => { | ||
| const saved = localStorage.getItem("fri-theme") as "dark" | "light" | null; | ||
| const initial = saved || "dark"; | ||
| setTheme(initial); | ||
| document.documentElement.setAttribute("data-theme", initial); | ||
| }, []); | ||
|
|
||
| const toggle = useCallback(() => { | ||
| const next = theme === "dark" ? "light" : "dark"; | ||
| setTheme(next); | ||
| document.documentElement.setAttribute("data-theme", next); | ||
| localStorage.setItem("fri-theme", next); | ||
| }, [theme]); | ||
|
|
||
| return ( | ||
| <button | ||
| type="button" | ||
| onClick={toggle} | ||
| className="p-1.5 transition-colors hover:opacity-80" | ||
| className="xp-btn-min" | ||
| style={{ width: '21px', height: '21px', fontSize: '10px' }} | ||
| aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} mode`} | ||
| title={`Switch to ${theme === "dark" ? "light" : "dark"} mode`} | ||
| > | ||
| <img | ||
| src={ | ||
| theme === "dark" | ||
| ? "https://unpkg.com/pixelarticons@1.8.1/svg/sun.svg" | ||
| : "https://unpkg.com/pixelarticons@1.8.1/svg/moon.svg" | ||
| } | ||
| className="pa-icon w-4 h-4 inline-block" | ||
| alt="" | ||
| aria-hidden="true" | ||
| /> | ||
| {theme === "dark" ? "☀" : "☾"} | ||
| </button> |
There was a problem hiding this comment.
Theme toggling still flips data-theme between dark and light, but globals.css no longer defines any [data-theme="light"] overrides. That makes the button state/ARIA label misleading and suggests a feature regression. Either reintroduce per-theme CSS variables, or update ThemeToggle to reflect what it actually toggles (or remove it if the XP theme is intended to be fixed).
| color: "#0a246a", | ||
| }, | ||
| { | ||
| title: "Honesty / 诚实", | ||
| body: "Truthful analysis, even when it stings a little.", | ||
| color: "#0a246a", | ||
| }, | ||
| { | ||
| title: "Privacy / 隐私", | ||
| body: "Absolute data integrity. Trust is earned, not demanded.", | ||
| color: "#0a246a", | ||
| }, | ||
| { | ||
| title: "Autonomy / 自主", | ||
| body: "Proactive when needed, reserved when not. Always present.", | ||
| color: "#0a246a", |
There was a problem hiding this comment.
Each directive now includes a color field, but the render path doesn’t use it (all colors are hard-coded in styles below). If the per-directive color is no longer needed, remove the field; otherwise, wire it into the styles so this data doesn’t become stale/dead.
| color: "#0a246a", | |
| }, | |
| { | |
| title: "Honesty / 诚实", | |
| body: "Truthful analysis, even when it stings a little.", | |
| color: "#0a246a", | |
| }, | |
| { | |
| title: "Privacy / 隐私", | |
| body: "Absolute data integrity. Trust is earned, not demanded.", | |
| color: "#0a246a", | |
| }, | |
| { | |
| title: "Autonomy / 自主", | |
| body: "Proactive when needed, reserved when not. Always present.", | |
| color: "#0a246a", | |
| }, | |
| { | |
| title: "Honesty / 诚实", | |
| body: "Truthful analysis, even when it stings a little.", | |
| }, | |
| { | |
| title: "Privacy / 隐私", | |
| body: "Absolute data integrity. Trust is earned, not demanded.", | |
| }, | |
| { | |
| title: "Autonomy / 自主", | |
| body: "Proactive when needed, reserved when not. Always present.", |
| <Link | ||
| key={entry.slug} | ||
| href={`/daily/${entry.slug}`} | ||
| className="group flex gap-2 p-2 transition-all block" |
There was a problem hiding this comment.
This Link has both flex and block display utilities in the same className (... flex ... block). These utilities conflict and whichever wins in Tailwind’s generated CSS will control layout, potentially breaking the row alignment. Remove the redundant/conflicting display class (likely drop block).
| className="group flex gap-2 p-2 transition-all block" | |
| className="group flex gap-2 p-2 transition-all" |
| import type { Metadata } from "next"; | ||
| import { GeistSans } from "geist/font/sans"; | ||
| import { GeistMono } from "geist/font/mono"; | ||
| import { GeistPixelSquare } from "geist/font/pixel"; | ||
| import "@/styles/globals.css"; | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: "FRI Interface v3.28", | ||
| title: "FRI Interface v3.28 — Windows Edition", | ||
| description: "Intelligent Assistant — Portfolio Shell for Friday", | ||
| icons: { icon: "/favicon.png" }, | ||
| }; | ||
|
|
||
| export default function RootLayout({ | ||
| children, | ||
| }: { | ||
| children: React.ReactNode; | ||
| }) { | ||
| return ( | ||
| <html lang="en" className={`${GeistSans.variable} ${GeistMono.variable} ${GeistPixelSquare.variable}`} suppressHydrationWarning> | ||
| <head> | ||
| <script dangerouslySetInnerHTML={{ __html: `try{var t=localStorage.getItem('fri-theme');if(t)document.documentElement.setAttribute('data-theme',t)}catch(e){}` }} /> | ||
| </head> | ||
| <body className="font-suse">{children}</body> | ||
| <body style={{ fontFamily: "'Tahoma', 'Trebuchet MS', sans-serif", fontSize: '11px' }}>{children}</body> | ||
| </html> |
There was a problem hiding this comment.
layout.tsx still imports Geist fonts and applies their CSS variables to <html>, but the <body> now forces a different font family via inline style. This means the Geist font preload/code is likely unused, adding bundle/FOIT overhead. Consider either removing the Geist font imports/variables if XP fonts are the new global default, or applying the font choice via Tailwind/CSS so you can keep a consistent font strategy across the app.
Implement dual-skin theme with XP and sci-fi options. Co-authored-by: Zihan Huang <84128702+bravohenry@users.noreply.github.com>
Summary
Completes the dual-skin system so every homepage component renders faithfully in both Windows XP and Sci-Fi modes. Toggling the skin button in the header switches the entire UI instantly, persisted across sessions.
Changes
New files
src/components/home/HomeGrid.tsx— client layout bridge that swapsWidgetPanel(XP) vsActiveModules(sci-fi) in the left column, and rendersXPTaskbaronly in XP modeUpdated components
src/app/page.tsxHomeGridto delegate skin-conditional layoutsrc/components/home/IdentityMatrix.tsxsrc/components/home/TypeWriter.tsxsrc/components/home/ArcReactor.tsxsrc/components/home/Diagnostics.tsxsrc/components/home/CoreDirectives.tsxsrc/components/home/ActiveModules.tsxsrc/components/home/Terminal.tsxsrc/components/home/XPTaskbar.tsxnullin sci-fi skinsrc/components/ui/TechBorder.tsxsrc/components/home/CLAUDE.mdResult
data-skin="xp"→ full Windows XP UI (all panels, taskbar, CMD terminal)data-skin="scifi"→ original dark neon sci-fi UI restoredpnpm next buildpasses with no errors