Rebuild UI as a faithful recreation of the original Aurora-themed layout: sidebar panel nav, two-pane rename list, live naming-scheme preview

This commit is contained in:
Renamr Scaffold
2026-08-29 01:55:54 +00:00
parent e01852f292
commit 87e150f31b
14 changed files with 914 additions and 145 deletions
+12
View File
@@ -0,0 +1,12 @@
.main-column {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
background: var(--surface);
}
.main-content {
flex: 1;
min-height: 0;
}
+15 -58
View File
@@ -1,65 +1,22 @@
import { useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
const APP_NAME = "Renamr";
const APP_VERSION = "0.1.0";
const DEFAULT_EXPRESSION = "{n} - {s00e00} - {t}";
const DEFAULT_BINDINGS: Record<string, string> = {
n: "Example Show",
s00e00: "S01E01",
t: "Pilot",
};
import { useState } from "react";
import Sidebar, { PANELS } from "./components/Sidebar";
import Header from "./components/Header";
import RenamePanel from "./components/RenamePanel";
import PanelStub from "./components/PanelStub";
export default function App() {
const [expression, setExpression] = useState(DEFAULT_EXPRESSION);
const [preview, setPreview] = useState<string>("");
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
invoke<string>("render_expression", { expression, bindings: DEFAULT_BINDINGS })
.then((result) => {
if (!cancelled) {
setPreview(result);
setError(null);
}
})
.catch((err) => {
if (!cancelled) {
setPreview("");
setError(String(err));
}
});
return () => {
cancelled = true;
};
}, [expression]);
const [selected, setSelected] = useState(PANELS[0].id);
const current = PANELS.find((p) => p.id === selected) ?? PANELS[0];
return (
<main className="shell">
<header>
<h1>{APP_NAME}</h1>
<p className="tagline">A fast, cross-platform media renamer.</p>
<p className="version">v{APP_VERSION}</p>
</header>
<section className="panel">
<label htmlFor="expression">Naming scheme</label>
<input
id="expression"
value={expression}
onChange={(e) => setExpression(e.target.value)}
spellCheck={false}
/>
<div className="preview">
<span className="preview-label">Preview</span>
{error ? <span className="preview-error">{error}</span> : <span className="preview-value">{preview}</span>}
<div className="app-shell">
<Sidebar selected={selected} onSelect={setSelected} />
<div className="main-column">
<Header title={current.name} />
<div className="main-content">
{selected === "rename" ? <RenamePanel /> : <PanelStub name={current.name} />}
</div>
</div>
</div>
</section>
</main>
);
}
+27
View File
@@ -0,0 +1,27 @@
.header {
height: var(--header-height);
flex: 0 0 var(--header-height);
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, #111e26 0%, var(--surface-alt) 62%, #0a161c 100%);
border-bottom: 1px solid var(--dock-line);
position: relative;
}
.header::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -1px;
height: 1px;
background: linear-gradient(90deg, var(--aurora-cyan), transparent 70%);
}
.header h1 {
margin: 0;
font-size: 20px;
font-weight: 700;
color: var(--text);
}
+7
View File
@@ -0,0 +1,7 @@
export default function Header({ title }: { title: string }) {
return (
<header className="header">
<h1>{title}</h1>
</header>
);
}
+146
View File
@@ -0,0 +1,146 @@
// Small line-icon set for the sidebar panels. The original app used bitmap icon
// resources (ResourceManager.getIcon("panel.rename") etc.) that aren't available to
// port directly, so these are fresh SVGs matching the same one-glyph-per-panel idea.
import type { SVGProps } from "react";
const base: SVGProps<SVGSVGElement> = {
width: 18,
height: 18,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 1.8,
strokeLinecap: "round",
strokeLinejoin: "round",
};
export function RenameIcon() {
return (
<svg {...base}>
<path d="M4 7h9l3 3-3 3H4z" />
<path d="M20 17h-9l-3-3 3-3h9" />
</svg>
);
}
export function EpisodesIcon() {
return (
<svg {...base}>
<rect x="3.5" y="5" width="17" height="15" rx="2" />
<path d="M3.5 9.5h17M8 3v4M16 3v4" />
</svg>
);
}
export function SubtitlesIcon() {
return (
<svg {...base}>
<rect x="3" y="5.5" width="18" height="13" rx="2" />
<path d="M7 14h4M13 14h4M7 10.5h10" />
</svg>
);
}
export function SfvIcon() {
return (
<svg {...base}>
<path d="M12 3l7 3v6c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6z" />
<path d="M9 12l2 2 4-4" />
</svg>
);
}
export function FilterIcon() {
return (
<svg {...base}>
<path d="M4 5h16l-6 7.5V19l-4 2v-8.5z" />
</svg>
);
}
export function ListIcon() {
return (
<svg {...base}>
<path d="M8 6h12M8 12h12M8 18h12" />
<circle cx="4" cy="6" r="1.2" fill="currentColor" stroke="none" />
<circle cx="4" cy="12" r="1.2" fill="currentColor" stroke="none" />
<circle cx="4" cy="18" r="1.2" fill="currentColor" stroke="none" />
</svg>
);
}
export function GearIcon() {
return (
<svg {...{ ...base, width: 16, height: 16 }}>
<circle cx="12" cy="12" r="3" />
<path d="M12 3v2.5M12 18.5V21M21 12h-2.5M5.5 12H3M18.4 5.6l-1.8 1.8M7.4 16.6l-1.8 1.8M18.4 18.4l-1.8-1.8M7.4 7.4 5.6 5.6" />
</svg>
);
}
export function LoadIcon() {
return (
<svg {...{ ...base, width: 15, height: 15 }}>
<path d="M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3M12 3v12M7 10l5 5 5-5" />
</svg>
);
}
export function UpIcon() {
return (
<svg {...{ ...base, width: 13, height: 13 }}>
<path d="M6 15l6-6 6 6" />
</svg>
);
}
export function DownIcon() {
return (
<svg {...{ ...base, width: 13, height: 13 }}>
<path d="M6 9l6 6 6-6" />
</svg>
);
}
export function RemoveIcon() {
return (
<svg {...{ ...base, width: 15, height: 15 }}>
<path d="M5 5l14 14M19 5L5 19" />
</svg>
);
}
export function ClearIcon() {
return (
<svg {...{ ...base, width: 15, height: 15 }}>
<path d="M4 7h16M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12" />
</svg>
);
}
export function HistoryIcon() {
return (
<svg {...{ ...base, width: 15, height: 15 }}>
<path d="M4 12a8 8 0 1 0 2.6-5.9" />
<path d="M4 4v4h4M12 8v4l3 2" />
</svg>
);
}
export function PresetsIcon() {
return (
<svg {...{ ...base, width: 15, height: 15 }}>
<path d="M4 6h16M4 12h10M4 18h7" />
<circle cx="19" cy="15" r="2.5" />
</svg>
);
}
export function FetchIcon() {
return (
<svg {...{ ...base, width: 15, height: 15 }}>
<circle cx="11" cy="11" r="6.5" />
<path d="M20 20l-4.3-4.3" />
</svg>
);
}
+21
View File
@@ -0,0 +1,21 @@
.panel-stub {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
color: var(--muted-text);
}
.panel-stub-name {
margin: 0;
font-size: 18px;
font-weight: 700;
color: var(--text);
}
.panel-stub-note {
margin: 0;
font-size: 12.5px;
}
+8
View File
@@ -0,0 +1,8 @@
export default function PanelStub({ name }: { name: string }) {
return (
<div className="panel-stub">
<p className="panel-stub-name">{name}</p>
<p className="panel-stub-note">Not built yet — see the porting roadmap in the README.</p>
</div>
);
}
+218
View File
@@ -0,0 +1,218 @@
.rename-panel {
height: 100%;
display: flex;
flex-direction: column;
position: relative;
}
.rename-lists {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 14px;
padding: 18px 18px 18px 16px;
}
.list-card {
min-height: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.list-card-title {
font-size: 12px;
font-weight: 700;
color: var(--muted-text);
padding-left: 4px;
}
.list-card-body {
flex: 1;
min-height: 0;
overflow-y: auto;
background: var(--card);
border: 1px solid var(--surface-deep);
border-radius: 12px;
padding: 8px;
display: flex;
flex-direction: column;
gap: 2px;
}
.list-row {
height: 32px;
min-height: 32px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 0 10px;
border-radius: 8px;
cursor: pointer;
color: var(--text);
}
.list-row:hover {
background: rgba(255, 255, 255, 0.03);
}
.list-row.selected {
background: var(--list-selection);
}
.list-row.muted {
color: var(--muted-text);
opacity: 0.6;
}
.list-row-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12.5px;
}
.list-row-badge {
flex: 0 0 auto;
font-size: 9.5px;
font-weight: 700;
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--muted-text);
background: rgba(255, 255, 255, 0.05);
border-radius: 999px;
padding: 2px 8px;
}
.list-button-row {
display: flex;
align-items: center;
gap: 4px;
padding: 2px 4px;
}
.list-button-gap {
flex: 1;
}
.list-icon-button {
all: unset;
box-sizing: border-box;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
border: 1px solid var(--surface-deep);
background: #172a31;
color: var(--text);
cursor: pointer;
}
.list-icon-button:hover {
background: #203941;
}
.list-icon-button:active {
background: #28515a;
}
.rename-actions {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
}
.action-button {
all: unset;
box-sizing: border-box;
width: 116px;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
border: 1px solid var(--surface-deep);
background: #172a31;
color: var(--text);
font-size: 13px;
font-weight: 600;
cursor: pointer;
text-align: center;
}
.action-button:hover {
background: #203941;
}
.action-button.primary {
background: var(--aurora-green);
color: var(--dock-deep);
border-color: transparent;
font-weight: 700;
}
.action-button.primary:hover {
background: #70e3bb;
}
.format-editor {
margin: 0 18px 18px;
padding: 14px 16px;
background: var(--card);
border: 1px solid var(--surface-deep);
border-radius: 12px;
}
.format-editor label {
display: block;
font-size: 11px;
font-weight: 700;
color: var(--muted-text);
margin-bottom: 6px;
}
.format-editor input {
width: 100%;
box-sizing: border-box;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid var(--surface-deep);
background: var(--surface);
color: var(--text);
font-family: ui-monospace, "SF Mono", monospace;
font-size: 13px;
}
.format-error {
margin: 8px 0 0;
color: #ff8a8a;
font-family: ui-monospace, monospace;
font-size: 11.5px;
}
.format-hint {
margin: 8px 0 0;
color: var(--muted-text);
font-size: 11px;
line-height: 1.5;
}
.status-bar {
position: absolute;
left: 18px;
right: 18px;
bottom: 14px;
padding: 8px 14px;
background: rgba(19, 34, 41, 0.95);
border: 1px solid var(--surface-deep);
border-radius: 8px;
font-size: 11.5px;
color: var(--sunset);
text-align: center;
}
+258
View File
@@ -0,0 +1,258 @@
import { useEffect, useMemo, useState, type ReactNode } from "react";
import { invoke } from "@tauri-apps/api/core";
import {
ClearIcon,
DownIcon,
FetchIcon,
GearIcon,
HistoryIcon,
LoadIcon,
PresetsIcon,
RemoveIcon,
UpIcon,
} from "./Icons";
interface SampleItem {
id: string;
originalName: string;
ext: string;
bindings: Record<string, string> | null; // null = no match (rendered muted, like the original)
}
// Placeholder sample batch standing in for a real folder scan + online match, which
// aren't ported yet (see the porting roadmap). This is enough to show the two-list
// layout, selection sync, and live naming-scheme preview working end-to-end.
const SAMPLE_ITEMS: SampleItem[] = [
{
id: "1",
originalName: "Breaking.Bad.S05E14.720p.HDTV.x264-KILLERS.mkv",
ext: "mkv",
bindings: { n: "Breaking Bad", s00e00: "S05E14", t: "Ozymandias" },
},
{
id: "2",
originalName: "Breaking.Bad.S05E15.720p.HDTV.x264-KILLERS.mkv",
ext: "mkv",
bindings: { n: "Breaking Bad", s00e00: "S05E15", t: "Granite State" },
},
{
id: "3",
originalName: "The.Office.US.S03E01.DVDRip.XviD-SAiNTS.avi",
ext: "avi",
bindings: { n: "The Office (US)", s00e00: "S03E01", t: "Gay Witch Hunt" },
},
{
id: "4",
originalName: "random_video_file.mp4",
ext: "mp4",
bindings: null,
},
];
const DEFAULT_EXPRESSION = "{n} - {s00e00} - {t}";
function ListButton({
icon,
label,
onClick,
}: {
icon: JSX.Element;
label: string;
onClick: () => void;
}) {
return (
<button className="list-icon-button" title={label} onClick={onClick}>
{icon}
</button>
);
}
export default function RenamePanel() {
const [expression, setExpression] = useState(DEFAULT_EXPRESSION);
const [editingFormat, setEditingFormat] = useState(false);
const [selected, setSelected] = useState<string | null>(SAMPLE_ITEMS[0].id);
const [computedNames, setComputedNames] = useState<Record<string, string>>({});
const [computeError, setComputeError] = useState<string | null>(null);
const [status, setStatus] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
Promise.all(
SAMPLE_ITEMS.map(async (item) => {
if (!item.bindings) return [item.id, null] as const;
try {
const rendered = await invoke<string>("render_expression", {
expression,
bindings: item.bindings,
});
return [item.id, `${rendered}.${item.ext}`] as const;
} catch (err) {
throw err;
}
})
)
.then((entries) => {
if (cancelled) return;
const next: Record<string, string> = {};
for (const [id, name] of entries) {
if (name) next[id] = name;
}
setComputedNames(next);
setComputeError(null);
})
.catch((err) => {
if (cancelled) return;
setComputeError(String(err));
});
return () => {
cancelled = true;
};
}, [expression]);
const notImplemented = (feature: string) => {
setStatus(`${feature} isn't implemented yet — see the porting roadmap in the README.`);
window.clearTimeout((notImplemented as any)._t);
(notImplemented as any)._t = window.setTimeout(() => setStatus(null), 3200);
};
const matchedCount = useMemo(() => SAMPLE_ITEMS.filter((i) => i.bindings).length, []);
return (
<div className="rename-panel">
<div className="rename-lists">
<ListCard
title="Original Files"
rows={SAMPLE_ITEMS.map((item) => (
<Row
key={item.id}
selected={selected === item.id}
muted={!item.bindings}
onClick={() => setSelected(item.id)}
text={item.originalName}
badge={item.ext}
/>
))}
buttonRow={
<>
<ListButton icon={<DownIcon />} label="Align Down" onClick={() => notImplemented("Reordering")} />
<ListButton icon={<UpIcon />} label="Align Up" onClick={() => notImplemented("Reordering")} />
<ListButton icon={<LoadIcon />} label="Load Files" onClick={() => notImplemented("Loading files")} />
<span className="list-button-gap" />
<ListButton icon={<RemoveIcon />} label="Exclude Selected" onClick={() => notImplemented("Excluding items")} />
<ListButton icon={<ClearIcon />} label="Clear" onClick={() => notImplemented("Clearing the list")} />
<ListButton icon={<HistoryIcon />} label="History" onClick={() => notImplemented("Rename history")} />
<ListButton icon={<PresetsIcon />} label="Presets" onClick={() => notImplemented("Presets")} />
</>
}
/>
<div className="rename-actions">
<button className="action-button" onClick={() => notImplemented("Matching")}>
Match
</button>
<button className="action-button primary" onClick={() => notImplemented("Renaming")}>
Rename
</button>
</div>
<ListCard
title="New Names"
rows={SAMPLE_ITEMS.map((item) => {
const name = computedNames[item.id];
const [base, ext] = name ? splitExt(name) : [null, item.ext];
return (
<Row
key={item.id}
selected={selected === item.id}
muted={!item.bindings}
onClick={() => setSelected(item.id)}
text={item.bindings ? base ?? "…" : "No match"}
badge={ext}
/>
);
})}
buttonRow={
<>
<ListButton icon={<DownIcon />} label="Align Down" onClick={() => notImplemented("Reordering")} />
<ListButton icon={<UpIcon />} label="Align Up" onClick={() => notImplemented("Reordering")} />
<ListButton icon={<LoadIcon />} label="Load Files" onClick={() => notImplemented("Loading files")} />
<span className="list-button-gap" />
<ListButton icon={<FetchIcon />} label="Fetch Data" onClick={() => notImplemented("Online matching")} />
<ListButton icon={<GearIcon />} label="Naming Scheme" onClick={() => setEditingFormat((v) => !v)} />
</>
}
/>
</div>
{editingFormat && (
<div className="format-editor">
<label htmlFor="expression">Naming scheme</label>
<input
id="expression"
value={expression}
spellCheck={false}
onChange={(e) => setExpression(e.target.value)}
/>
{computeError && <p className="format-error">{computeError}</p>}
<p className="format-hint">
Live preview against {matchedCount} matched sample file{matchedCount === 1 ? "" : "s"}. This runs the
real Rust naming-expression engine (Rhai-backed) through Tauri — the only piece of the renamer that's
fully implemented so far.
</p>
</div>
)}
{status && <div className="status-bar">{status}</div>}
</div>
);
}
function ListCard({
title,
rows,
buttonRow,
}: {
title: string;
rows: ReactNode;
buttonRow: ReactNode;
}) {
return (
<div className="list-card">
<span className="list-card-title">{title}</span>
<div className="list-card-body">{rows}</div>
<div className="list-button-row">{buttonRow}</div>
</div>
);
}
function Row({
text,
badge,
selected,
muted,
onClick,
}: {
text: string;
badge: string | null;
selected: boolean;
muted: boolean;
onClick: () => void;
}) {
return (
<div
className={`list-row${selected ? " selected" : ""}${muted ? " muted" : ""}`}
onClick={onClick}
>
<span className="list-row-text">{text}</span>
{badge && <span className="list-row-badge">{badge}</span>}
</div>
);
}
function splitExt(name: string): [string, string] {
const i = name.lastIndexOf(".");
if (i <= 0) return [name, ""];
return [name.slice(0, i), name.slice(i + 1)];
}
+88
View File
@@ -0,0 +1,88 @@
.sidebar {
width: var(--sidebar-width);
flex: 0 0 var(--sidebar-width);
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 64px 6px 14px;
position: relative;
/* Approximates the original's diagonal LinearGradientPaint(0,0 -> width,height,
[dock-deep, dock, dock-mist] at [0, 0.72, 1]). */
background: linear-gradient(165deg, var(--dock-deep) 0%, var(--dock) 72%, var(--dock-mist) 100%);
}
.sidebar::after {
content: "";
position: absolute;
top: 0;
right: 0;
width: 1px;
height: 100%;
background: linear-gradient(180deg, var(--aurora-green) 0%, var(--aurora-cyan) 48%, var(--aurora-lilac) 100%);
}
.sidebar-list {
list-style: none;
margin: 0;
padding: 8px 9px;
display: flex;
flex-direction: column;
}
.sidebar-item {
all: unset;
box-sizing: border-box;
display: flex;
align-items: center;
gap: 10px;
height: 54px;
padding: 0 10px;
border-radius: 14px;
color: var(--muted-text);
font-size: 12px;
font-weight: 700;
cursor: pointer;
position: relative;
}
.sidebar-item:hover {
color: var(--text);
}
.sidebar-item.selected {
background: rgba(0, 0, 0, 0.13);
color: var(--text);
}
.sidebar-item-accent {
position: absolute;
left: 5px;
top: 11px;
bottom: 11px;
width: 3px;
border-radius: 6px;
background: var(--aurora-cyan);
}
.sidebar-footer {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding-bottom: 6px;
}
.sidebar-wordmark {
font-weight: 700;
font-size: 13px;
background: linear-gradient(135deg, var(--aurora-cyan), var(--aurora-lilac));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.sidebar-version {
font-size: 10px;
color: var(--muted-text);
opacity: 0.7;
}
+55
View File
@@ -0,0 +1,55 @@
import { EpisodesIcon, FilterIcon, ListIcon, RenameIcon, SfvIcon, SubtitlesIcon } from "./Icons";
export interface PanelDef {
id: string;
name: string;
icon: () => JSX.Element;
}
// Same panel set, same order, as the original's PanelBuilder.defaultSequence().
export const PANELS: PanelDef[] = [
{ id: "rename", name: "Rename", icon: RenameIcon },
{ id: "episodes", name: "Episodes", icon: EpisodesIcon },
{ id: "subtitles", name: "Subtitles", icon: SubtitlesIcon },
{ id: "sfv", name: "SFV", icon: SfvIcon },
{ id: "filter", name: "Filter", icon: FilterIcon },
{ id: "list", name: "List", icon: ListIcon },
];
const APP_VERSION = "0.1.0";
export default function Sidebar({
selected,
onSelect,
}: {
selected: string;
onSelect: (id: string) => void;
}) {
return (
<nav className="sidebar">
<ul className="sidebar-list">
{PANELS.map((panel) => {
const Icon = panel.icon;
const isSelected = panel.id === selected;
return (
<li key={panel.id}>
<button
className={`sidebar-item${isSelected ? " selected" : ""}`}
onClick={() => onSelect(panel.id)}
>
{isSelected && <span className="sidebar-item-accent" />}
<Icon />
<span>{panel.name}</span>
</button>
</li>
);
})}
</ul>
<div className="sidebar-footer">
<span className="sidebar-wordmark">Renamr</span>
<span className="sidebar-version">v{APP_VERSION}</span>
</div>
</nav>
);
}
+6 -1
View File
@@ -1,7 +1,12 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./styles.css";
import "./theme.css";
import "./App.css";
import "./components/Sidebar.css";
import "./components/Header.css";
import "./components/RenamePanel.css";
import "./components/PanelStub.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
-86
View File
@@ -1,86 +0,0 @@
:root {
color-scheme: light dark;
font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
body {
margin: 0;
background: #0f1222;
color: #f4f5fb;
}
.shell {
max-width: 640px;
margin: 0 auto;
padding: 48px 24px;
}
header h1 {
font-size: 2.25rem;
margin: 0;
background: linear-gradient(135deg, #5b4cf5, #38bdb2);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.tagline {
margin: 4px 0 0;
opacity: 0.8;
}
.version {
margin: 2px 0 0;
font-size: 0.8rem;
opacity: 0.5;
}
.panel {
margin-top: 32px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 20px;
}
label {
display: block;
font-size: 0.8rem;
opacity: 0.7;
margin-bottom: 8px;
}
input {
width: 100%;
box-sizing: border-box;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(0, 0, 0, 0.25);
color: inherit;
font-size: 1rem;
font-family: ui-monospace, monospace;
}
.preview {
margin-top: 16px;
padding: 12px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.25);
}
.preview-label {
display: block;
font-size: 0.75rem;
opacity: 0.6;
margin-bottom: 4px;
}
.preview-value {
font-family: ui-monospace, monospace;
}
.preview-error {
color: #ff8080;
font-family: ui-monospace, monospace;
}
+53
View File
@@ -0,0 +1,53 @@
/* Renamr "Aurora" theme — ported 1:1 (hex values) from the original FB-Mod fork's
AuroraTheme.java / HeaderPanel.java / MainFrame.java so the rewrite looks like a
continuation of that design rather than a random restyle. */
:root {
--dock: #08252b;
--dock-deep: #051115;
--dock-mist: #123a42;
--dock-line: #255660;
--surface: #0b151a;
--surface-alt: #101f25;
--surface-deep: #223940;
--card: #132229;
--text: #e8f4f2;
--muted-text: #9aafb0;
--aurora-green: #55d7a9;
--aurora-cyan: #52c7db;
--aurora-lilac: #a99cff;
--sunset: #f0b66b;
--list-selection: #1e4d54;
--dock-selection: #1d4850;
--sidebar-width: 158px;
--header-height: 68px;
}
* {
box-sizing: border-box;
}
html,
body,
#root {
height: 100%;
}
body {
margin: 0;
background: var(--surface);
color: var(--text);
font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 13px;
}
button,
input {
font-family: inherit;
color: inherit;
}
.app-shell {
display: flex;
height: 100vh;
overflow: hidden;
}