mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-22 20:03:07 -05:00
Makeover sweep across the tools screens: glass panels, pills, chips
- Hardware: every panel is a glass card with an icon disc header, metric rows with the hint under the value, and more room between cards. - Notifications: unread rows are raised glass with a coral dot instead of a solid accent tint; the unread bar matches. - Settings: text size and theme pickers are chips/glass pills; Backup presets and Profile's Crop/Fit are chips; the roster's Save/Add are pills. - Captures: the count sits beside the description. - Changelog/FAQ: a wrapped bullet stays one list item (was split into a stray paragraph). - Sweep of my screens, components and layout: old rounded-md/lg bordered boxes become glass panels or radius tokens, small buttons become pills, single-line fields are glass pills; undefined colour tokens (--color-error/-danger/-surface-1) and Tailwind palette colours replaced with theme tokens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ub1mez9eaVtTF8pMEFXvA9
This commit is contained in:
1 parent
b5f5e6dfc1
commit
b41f76240a
47 files changed
+268
-220
No files matched your search
@@ -115,7 +115,7 @@ export function Drawer({
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label={tr("close", "Close")}
|
||||
className="shrink-0 rounded p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text)]"
|
||||
className="shrink-0 rounded-full p-1.5 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text)]"
|
||||
>
|
||||
<X size={16} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function EditSessionBanner({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 rounded-md border border-[var(--color-accent)] bg-[var(--color-accent-soft)] p-3">
|
||||
<div className="flex flex-col gap-2 rounded-[var(--radius-card)] border border-[var(--color-accent)] bg-[var(--color-accent-soft)] p-3">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<FilePenLine
|
||||
size={18}
|
||||
|
||||
@@ -116,14 +116,14 @@ export class RootErrorBoundary extends Component<Props, State> {
|
||||
onClick={() =>
|
||||
this.setState({ err: null, componentStack: "" })
|
||||
}
|
||||
className="rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1.5 text-sm font-medium hover:bg-[var(--color-surface-3)]"
|
||||
className="chip min-h-9 px-4 text-sm font-medium"
|
||||
>
|
||||
{this.tr("errorboundary_try_again", "Try again")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.location.reload()}
|
||||
className="rounded-md bg-[var(--color-bad)] px-3 py-1.5 text-sm font-medium text-[var(--color-accent-contrast)] hover:opacity-90"
|
||||
className="rounded-full border border-[color-mix(in_srgb,var(--color-bad)_45%,transparent)] bg-[var(--color-surface-raised)] px-4 py-1.5 text-sm font-medium text-[var(--color-bad)] hover:bg-[var(--color-bad-soft)]"
|
||||
>
|
||||
{this.tr("errorboundary_reload_window", "Reload window")}
|
||||
</button>
|
||||
@@ -145,7 +145,7 @@ export class RootErrorBoundary extends Component<Props, State> {
|
||||
}
|
||||
});
|
||||
}}
|
||||
className="rounded-md border border-[var(--color-accent)] px-3 py-1.5 text-sm font-medium text-[var(--color-accent)] hover:bg-[var(--color-surface)]"
|
||||
className="chip min-h-9 px-4 text-sm font-medium text-[var(--color-accent)]"
|
||||
>
|
||||
{this.tr("errorboundary_report_crash", "Report this crash")}
|
||||
</button>
|
||||
|
||||
@@ -111,7 +111,7 @@ export function PickerView(p: PickerViewProps) {
|
||||
<button
|
||||
type="button"
|
||||
aria-label={tr("cancel", undefined, "Cancel")}
|
||||
className="rounded p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface)]"
|
||||
className="rounded-full p-1.5 text-[var(--color-muted)] hover:bg-[var(--color-surface)]"
|
||||
onClick={p.onCancel}
|
||||
>
|
||||
<X size={18} />
|
||||
@@ -156,7 +156,7 @@ export function PickerView(p: PickerViewProps) {
|
||||
<span className="flex-1 truncate text-xs text-[var(--color-muted)]">{p.cwdLabel}</span>
|
||||
{!p.remote && p.roots.length > 1 && (
|
||||
<select
|
||||
className="max-w-[40%] truncate rounded border border-[var(--color-border)] bg-[var(--color-surface)] px-1 py-0.5 text-xs"
|
||||
className="max-w-[40%] truncate rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-1 py-0.5 text-xs"
|
||||
value={p.currentRoot ?? ""}
|
||||
onChange={(e) => e.target.value && p.onRoot(e.target.value)}
|
||||
>
|
||||
|
||||
@@ -47,3 +47,27 @@ describe("MarkdownView tables", () => {
|
||||
expect((out.match(/<li[ >]/g) ?? []).length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MarkdownView lists", () => {
|
||||
it("keeps a wrapped bullet's indented lines in the same item", () => {
|
||||
const out = html(
|
||||
[
|
||||
"- **The limit was too high.** It held each connection to",
|
||||
" the limit instead of the whole transfer.",
|
||||
"- Second item",
|
||||
"",
|
||||
"After the list.",
|
||||
].join("\n"),
|
||||
);
|
||||
expect((out.match(/<li[ >]/g) ?? []).length).toBe(2);
|
||||
expect(out).toContain("connection to the limit instead");
|
||||
// The continuation is not a paragraph of its own.
|
||||
expect((out.match(/<p[ >]/g) ?? []).length).toBe(1);
|
||||
});
|
||||
|
||||
it("joins wrapped lines in numbered lists too", () => {
|
||||
const out = html(["1. First step that", " wraps", "2. Second"].join("\n"));
|
||||
expect((out.match(/<li[ >]/g) ?? []).length).toBe(2);
|
||||
expect(out).toContain("First step that wraps");
|
||||
});
|
||||
});
|
||||
@@ -170,6 +170,12 @@ function parseBlocks(md: string): Block[] {
|
||||
while (i < lines.length && /^\s*[-*+]\s+/.test(lines[i])) {
|
||||
items.push(lines[i].replace(/^\s*[-*+]\s+/, ""));
|
||||
i += 1;
|
||||
// An indented line under an item continues it (a wrapped bullet in
|
||||
// CHANGELOG.md); it isn't a new paragraph.
|
||||
while (i < lines.length && isContinuation(lines[i])) {
|
||||
items[items.length - 1] += " " + lines[i].trim();
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
blocks.push({ kind: "ul", content: "", items });
|
||||
continue;
|
||||
@@ -179,6 +185,12 @@ function parseBlocks(md: string): Block[] {
|
||||
while (i < lines.length && /^\s*\d+\.\s+/.test(lines[i])) {
|
||||
items.push(lines[i].replace(/^\s*\d+\.\s+/, ""));
|
||||
i += 1;
|
||||
// An indented line under an item continues it (a wrapped bullet in
|
||||
// CHANGELOG.md); it isn't a new paragraph.
|
||||
while (i < lines.length && isContinuation(lines[i])) {
|
||||
items[items.length - 1] += " " + lines[i].trim();
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
blocks.push({ kind: "ol", content: "", items });
|
||||
continue;
|
||||
@@ -206,6 +218,11 @@ function parseBlocks(md: string): Block[] {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/** A list item's wrapped line: indented, not blank, not a new item. */
|
||||
function isContinuation(line: string): boolean {
|
||||
return /^\s{2,}\S/.test(line) && !/^\s*([-*+]|\d+\.)\s+/.test(line);
|
||||
}
|
||||
|
||||
export function MarkdownView({ source }: { source: string }) {
|
||||
const blocks = useMemo(() => parseBlocks(source), [source]);
|
||||
return (
|
||||
@@ -333,7 +350,7 @@ export function MarkdownView({ source }: { source: string }) {
|
||||
return (
|
||||
<pre
|
||||
key={key}
|
||||
className="mb-5 overflow-x-auto rounded-md border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4 font-mono text-[13px] leading-relaxed shadow-sm"
|
||||
className="mb-5 overflow-x-auto rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4 font-mono text-[13px] leading-relaxed shadow-sm"
|
||||
>
|
||||
<code>{b.content}</code>
|
||||
</pre>
|
||||
|
||||
@@ -222,7 +222,7 @@ export function PackagePanelView({
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 p-4">
|
||||
<header className="relative overflow-hidden rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<header className="relative overflow-hidden rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
{backdropUrl && (
|
||||
<img
|
||||
src={backdropUrl}
|
||||
@@ -335,7 +335,7 @@ export function PackagePanelView({
|
||||
{notes && (
|
||||
<div className="text-sm">
|
||||
<div className="mb-1 text-[var(--color-muted)]">{tr("viewer_whats_new", undefined, "What's new")}</div>
|
||||
<p className="whitespace-pre-wrap rounded-md bg-[var(--color-surface-2)] p-3 text-xs">
|
||||
<p className="whitespace-pre-wrap rounded-[var(--radius-field)] bg-[var(--color-surface-2)] p-3 text-xs">
|
||||
{notes}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -411,7 +411,7 @@ export function TaskList({ maxFinished = 20 }: { maxFinished?: number }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearFinished}
|
||||
className="ml-auto rounded-md border border-[var(--color-border)] px-2 py-0.5 text-xs normal-case tracking-normal hover:bg-[var(--color-surface-3)]"
|
||||
className="ml-auto rounded-full border border-[var(--color-border)] px-2 py-0.5 text-xs normal-case tracking-normal hover:bg-[var(--color-surface-3)]"
|
||||
>
|
||||
{tr("task_clear_finished", undefined, "Clear finished")}
|
||||
</button>
|
||||
|
||||
@@ -70,7 +70,7 @@ function ToastItem({ toast }: { toast: Toast }) {
|
||||
type="button"
|
||||
onClick={() => dismiss(toast.id)}
|
||||
aria-label={tr("dismiss", "Dismiss")}
|
||||
className="shrink-0 rounded p-0.5 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)]"
|
||||
className="shrink-0 rounded-full p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)]"
|
||||
>
|
||||
<X size={14} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
@@ -32,7 +32,7 @@ export function FilesListView(props: {
|
||||
value={props.query}
|
||||
onChange={(e) => props.onQuery(e.target.value)}
|
||||
placeholder={tr("viewer_files_search", undefined, "Search files")}
|
||||
className="rounded-md border border-[var(--color-border)] bg-[var(--color-surface-2)] px-3 py-1.5 text-sm"
|
||||
className="rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-3 py-1.5 text-sm"
|
||||
/>
|
||||
<div className="text-xs text-[var(--color-muted)]">
|
||||
{tr(
|
||||
|
||||
@@ -22,7 +22,7 @@ function ImageCard({ token, name, size }: { token: string; name: string; size: n
|
||||
};
|
||||
}, [token, name]);
|
||||
return (
|
||||
<figure className="grid gap-1.5 rounded-md border border-[var(--color-border)] p-2">
|
||||
<figure className="grid gap-1.5 rounded-[var(--radius-card)] border border-[var(--color-border)] p-3">
|
||||
<div className="flex aspect-video items-center justify-center overflow-hidden rounded bg-[var(--color-surface-2)]">
|
||||
{url ? (
|
||||
<img src={url} alt={name} loading="lazy" className="max-h-full max-w-full object-contain" />
|
||||
|
||||
@@ -32,7 +32,7 @@ export function RelatedTab(props: {
|
||||
{g.items.map((i) => (
|
||||
<li
|
||||
key={`${i.where}:${i.path ?? i.name}`}
|
||||
className="flex items-center gap-2 rounded-md border border-[var(--color-border)] px-3 py-2 text-sm"
|
||||
className="flex items-center gap-2 rounded-[var(--radius-card)] border border-[var(--color-border)] px-3 py-2 text-sm"
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">{i.name}</span>
|
||||
{i.version && <span className="font-mono text-xs text-[var(--color-muted)]">{i.version}</span>}
|
||||
|
||||
@@ -463,6 +463,19 @@ body {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
/* A panel header's icon: a small coral-on-blush disc, the same language as
|
||||
* the Home tiles' icon buttons. */
|
||||
.icon-disc {
|
||||
display: inline-grid;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
border-radius: 9999px;
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent-bright);
|
||||
}
|
||||
|
||||
/* Chip: a pill filter. Active = white filled, inactive = hairline outline.
|
||||
* Colour and shape only: size comes from the caller's utilities (this sheet
|
||||
* is unlayered, so anything set here would beat them). */
|
||||
|
||||
@@ -163,7 +163,7 @@ export default function NotificationInbox({
|
||||
<Bell size={variant === "orb" ? 18 : 14} />
|
||||
{unread > 0 && (
|
||||
<span
|
||||
className="absolute -right-0.5 -top-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-[var(--color-bad)] px-1 text-[10px] font-semibold tabular-nums text-white"
|
||||
className="absolute -right-0.5 -top-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-[var(--color-bad)] px-1 text-[10px] font-semibold tabular-nums text-[var(--color-accent-contrast)]"
|
||||
title={tr(
|
||||
unread === 1
|
||||
? "notifications_unread_one"
|
||||
@@ -214,7 +214,7 @@ export default function NotificationInbox({
|
||||
type="button"
|
||||
onClick={clear}
|
||||
aria-label={tr("notifications_clear", undefined, "Clear all")}
|
||||
className="rounded p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface-2)] hover:text-[var(--color-text)]"
|
||||
className="rounded-full p-1.5 text-[var(--color-muted)] hover:bg-[var(--color-surface-2)] hover:text-[var(--color-text)]"
|
||||
title={tr("notifications_clear", undefined, "Clear all")}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
@@ -225,7 +225,7 @@ export default function NotificationInbox({
|
||||
onClick={() => setOpen(false)}
|
||||
aria-label={tr("close", undefined, "Close")}
|
||||
title={tr("close", undefined, "Close")}
|
||||
className="rounded p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface-2)] hover:text-[var(--color-text)]"
|
||||
className="rounded-full p-1.5 text-[var(--color-muted)] hover:bg-[var(--color-surface-2)] hover:text-[var(--color-text)]"
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
|
||||
@@ -251,14 +251,14 @@ function RosterManageModal({ onClose }: { onClose: () => void }) {
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
placeholder={tr("roster_edit_name_placeholder", "Name")}
|
||||
className="flex-1 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 py-1"
|
||||
className="flex-1 rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-2 py-1"
|
||||
/>
|
||||
<input
|
||||
value={editHost}
|
||||
onChange={(e) => setEditHost(e.target.value)}
|
||||
placeholder="192.168.1.50"
|
||||
inputMode="decimal"
|
||||
className="flex-1 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 py-1"
|
||||
className="flex-1 rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-2 py-1"
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
@@ -271,13 +271,13 @@ function RosterManageModal({ onClose }: { onClose: () => void }) {
|
||||
undefined,
|
||||
"Notes (e.g. firmware quirks, do-not-update warnings)",
|
||||
)}
|
||||
className="w-full resize-y rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 py-1 text-xs"
|
||||
className="w-full resize-y rounded-[var(--radius-field)] border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-2 py-1 text-xs"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={commitEdit}
|
||||
className="rounded-md bg-[var(--color-accent)] px-2 py-1 text-[var(--color-accent-contrast)]"
|
||||
className="inline-flex items-center gap-1 rounded-full bg-[image:var(--accent-fill)] px-3 py-1 text-xs font-semibold text-[var(--color-accent-contrast)] shadow-[var(--accent-glow)] hover:brightness-105 disabled:opacity-50"
|
||||
>
|
||||
{tr("save", undefined, "Save")}
|
||||
</button>
|
||||
@@ -319,7 +319,7 @@ function RosterManageModal({ onClose }: { onClose: () => void }) {
|
||||
onClick={() => startEdit(p)}
|
||||
aria-label={tr("roster_edit", undefined, "Edit PS5")}
|
||||
title={tr("roster_edit", undefined, "Edit PS5")}
|
||||
className="rounded-md p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface)] hover:text-[var(--color-text)]"
|
||||
className="rounded-full p-1.5 text-[var(--color-muted)] hover:bg-[var(--color-surface)] hover:text-[var(--color-text)]"
|
||||
>
|
||||
<Pencil size={12} />
|
||||
</button>
|
||||
@@ -333,7 +333,7 @@ function RosterManageModal({ onClose }: { onClose: () => void }) {
|
||||
`Move ${p.name} up`,
|
||||
)}
|
||||
title={tr("roster_move_up", undefined, "Move up")}
|
||||
className="rounded-md p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface)] hover:text-[var(--color-text)] disabled:cursor-not-allowed disabled:opacity-30"
|
||||
className="rounded-full p-1.5 text-[var(--color-muted)] hover:bg-[var(--color-surface)] hover:text-[var(--color-text)] disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
<ChevronUp size={12} />
|
||||
</button>
|
||||
@@ -347,7 +347,7 @@ function RosterManageModal({ onClose }: { onClose: () => void }) {
|
||||
`Move ${p.name} down`,
|
||||
)}
|
||||
title={tr("roster_move_down", undefined, "Move down")}
|
||||
className="rounded-md p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface)] hover:text-[var(--color-text)] disabled:cursor-not-allowed disabled:opacity-30"
|
||||
className="rounded-full p-1.5 text-[var(--color-muted)] hover:bg-[var(--color-surface)] hover:text-[var(--color-text)] disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
<ChevronDownIcon size={12} />
|
||||
</button>
|
||||
@@ -376,7 +376,7 @@ function RosterManageModal({ onClose }: { onClose: () => void }) {
|
||||
`Remove ${p.name}`,
|
||||
)}
|
||||
title={tr("remove", undefined, "Remove")}
|
||||
className="rounded-md p-1 text-[var(--color-muted)] hover:bg-[var(--color-bad-soft)] hover:text-[var(--color-bad)]"
|
||||
className="rounded-full p-1.5 text-[var(--color-muted)] hover:bg-[var(--color-bad-soft)] hover:text-[var(--color-bad)]"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
@@ -399,20 +399,20 @@ function RosterManageModal({ onClose }: { onClose: () => void }) {
|
||||
undefined,
|
||||
"Living-room PS5",
|
||||
)}
|
||||
className="flex-1 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 py-1 text-xs"
|
||||
className="flex-1 rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-2 py-1 text-xs"
|
||||
/>
|
||||
<input
|
||||
value={newHost}
|
||||
onChange={(e) => setNewHost(e.target.value)}
|
||||
placeholder="192.168.1.50"
|
||||
inputMode="decimal"
|
||||
className="flex-1 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 py-1 text-xs"
|
||||
className="flex-1 rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-2 py-1 text-xs"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAdd}
|
||||
disabled={!newHost.trim()}
|
||||
className="inline-flex items-center gap-1 rounded-md bg-[var(--color-accent)] px-2 py-1 text-xs text-[var(--color-accent-contrast)] disabled:opacity-50"
|
||||
className="inline-flex items-center gap-1 rounded-full bg-[image:var(--accent-fill)] px-3 py-1 text-xs font-semibold text-[var(--color-accent-contrast)] shadow-[var(--accent-glow)] hover:brightness-105 disabled:opacity-50"
|
||||
>
|
||||
<Plus size={11} />
|
||||
{tr("add", undefined, "Add")}
|
||||
|
||||
@@ -78,7 +78,7 @@ export function UpdateToast() {
|
||||
type="button"
|
||||
disabled={downloading}
|
||||
onClick={() => void download()}
|
||||
className="inline-flex shrink-0 items-center gap-1.5 rounded-md border border-[var(--color-accent)] px-2.5 py-1 text-xs font-medium text-[var(--color-accent)] disabled:opacity-60"
|
||||
className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-[var(--color-accent)] px-2.5 py-1 text-xs font-medium text-[var(--color-accent)] disabled:opacity-60"
|
||||
>
|
||||
{downloading ? (
|
||||
<Spinner size={14} />
|
||||
@@ -96,7 +96,7 @@ export function UpdateToast() {
|
||||
type="button"
|
||||
aria-label={tr("update_toast_dismiss", undefined, "Dismiss")}
|
||||
onClick={() => setDismissedVersion(version)}
|
||||
className="shrink-0 rounded p-1 text-[var(--color-muted)] hover:text-[var(--color-text)]"
|
||||
className="shrink-0 rounded-full p-1.5 text-[var(--color-muted)] hover:text-[var(--color-text)]"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
|
||||
@@ -248,7 +248,7 @@ function FeatureTile({
|
||||
body: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<div className="surface-panel flex flex-col gap-2 p-5">
|
||||
<Icon size={18} className="text-[var(--color-accent)]" />
|
||||
<div className="text-sm font-semibold">{title}</div>
|
||||
<div className="text-xs leading-relaxed text-[var(--color-muted)]">
|
||||
|
||||
@@ -73,7 +73,7 @@ export function StatsPanel({ entries }: { entries: ActivityEntry[] }) {
|
||||
</Button>
|
||||
</div>
|
||||
{entries.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed border-[var(--color-border)] p-6 text-center text-xs text-[var(--color-muted)]">
|
||||
<div className="rounded-[var(--radius-card)] border border-dashed border-[var(--color-border)] p-6 text-center text-xs text-[var(--color-muted)]">
|
||||
{tr(
|
||||
"stats_empty",
|
||||
undefined,
|
||||
@@ -143,7 +143,7 @@ function KpiCard({
|
||||
sub?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-3">
|
||||
<div className="rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface-2)] p-3">
|
||||
<div className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-[var(--color-muted)]">
|
||||
{icon}
|
||||
{label}
|
||||
@@ -166,7 +166,7 @@ function DailyChart({
|
||||
const tr = useTr();
|
||||
const maxCount = Math.max(...days.map((d) => d.count), 1);
|
||||
return (
|
||||
<section className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<section className="surface-panel p-5">
|
||||
<h3 className="mb-3 text-sm font-semibold">
|
||||
{tr("stats_daily", undefined, "Operations per day (last 30)")}
|
||||
</h3>
|
||||
@@ -180,7 +180,7 @@ function DailyChart({
|
||||
title={`${d.date}: ${d.count} operations · ${formatBytes(d.bytes)}`}
|
||||
>
|
||||
<div
|
||||
className="rounded-t bg-[var(--color-accent)] transition-all hover:opacity-80"
|
||||
className="rounded-t-md bg-[image:var(--accent-fill)] transition-all hover:opacity-80"
|
||||
style={{
|
||||
height: `${Math.max(heightPct, 2)}%`,
|
||||
minHeight: d.count > 0 ? "2px" : "0",
|
||||
@@ -209,7 +209,7 @@ function KindBreakdown({
|
||||
const tr = useTr();
|
||||
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
||||
return (
|
||||
<section className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<section className="surface-panel p-5">
|
||||
<h3 className="mb-3 text-sm font-semibold">
|
||||
{tr("stats_breakdown", undefined, "Operation breakdown")}
|
||||
</h3>
|
||||
@@ -245,7 +245,7 @@ function TopTransfers({
|
||||
}) {
|
||||
const tr = useTr();
|
||||
return (
|
||||
<section className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<section className="surface-panel p-5">
|
||||
<h3 className="mb-3 text-sm font-semibold">
|
||||
{tr("stats_top_transfers", undefined, "Fastest transfers")}
|
||||
</h3>
|
||||
|
||||
@@ -419,7 +419,7 @@ function ActivityRow({ entry }: { entry: ActivityEntry }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDetailOpen(true)}
|
||||
className="rounded-md border border-[var(--color-border)] p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text)]"
|
||||
className="rounded-full border border-[var(--color-border)] p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text)]"
|
||||
title={tr("activity_view_tooltip", undefined, "View details")}
|
||||
>
|
||||
<Eye size={13} />
|
||||
@@ -429,7 +429,7 @@ function ActivityRow({ entry }: { entry: ActivityEntry }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleStop()}
|
||||
className="rounded-md border border-[var(--color-border)] px-2 py-0.5 text-xs hover:bg-[var(--color-surface-3)]"
|
||||
className="chip min-h-7 px-3 text-xs"
|
||||
title={
|
||||
stopAction === "cancel"
|
||||
? tr(
|
||||
@@ -453,7 +453,7 @@ function ActivityRow({ entry }: { entry: ActivityEntry }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => remove(entry.id)}
|
||||
className="rounded-md border border-[var(--color-border)] p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)] hover:text-[var(--color-bad)]"
|
||||
className="rounded-full border border-[var(--color-border)] p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)] hover:text-[var(--color-bad)]"
|
||||
title={tr(
|
||||
"activity_delete_tooltip",
|
||||
undefined,
|
||||
@@ -697,7 +697,7 @@ function ActivityDetailModal({
|
||||
))}
|
||||
</dl>
|
||||
{entry.error && (
|
||||
<div className="mt-3 rounded-md border border-[var(--color-border)] bg-[var(--color-surface-2)] p-2 text-xs">
|
||||
<div className="mt-3 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface-2)] p-3 text-xs">
|
||||
<div className="mb-1 font-medium text-[var(--color-bad)]">
|
||||
{tr("activity_detail_error", undefined, "Error")}
|
||||
</div>
|
||||
@@ -793,7 +793,7 @@ function ActivityTimeline({ entries }: { entries: ActivityEntry[] }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<section className="surface-panel p-5">
|
||||
<header className="mb-3 flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold">
|
||||
{tr(
|
||||
|
||||
@@ -249,7 +249,7 @@ export default function BackupScreen() {
|
||||
{error && <div className="mb-4"><ErrorCard title={error} onRetry={() => void refresh()} onDismiss={() => setError(null)} /></div>}
|
||||
|
||||
{actionMsg && (
|
||||
<div className="rounded-lg border border-[var(--color-good)]/30 bg-[var(--color-good)]/10 px-4 py-3 text-sm text-[var(--color-good)]">
|
||||
<div className="rounded-[var(--radius-card)] border border-[var(--color-good)]/30 bg-[var(--color-good)]/10 px-4 py-3 text-sm text-[var(--color-good)]">
|
||||
{actionMsg}
|
||||
</div>
|
||||
)}
|
||||
@@ -283,11 +283,8 @@ export default function BackupScreen() {
|
||||
setTag(defaultTagFor(preset.path));
|
||||
}}
|
||||
title={preset.path}
|
||||
className={`rounded-md border px-2 py-1 text-xs ${
|
||||
path === preset.path
|
||||
? "border-[var(--color-accent)] bg-[var(--color-accent)]/10"
|
||||
: "border-[var(--color-border)] hover:bg-[var(--color-surface)]"
|
||||
}`}
|
||||
aria-pressed={path === preset.path}
|
||||
className="chip min-h-8 px-3.5 text-xs max-md:min-h-11"
|
||||
>
|
||||
{tr(preset.labelKey, undefined, preset.label)}
|
||||
</button>
|
||||
@@ -356,7 +353,7 @@ export default function BackupScreen() {
|
||||
{entries.map((entry) => (
|
||||
<div
|
||||
key={`${entry.tag}-${entry.timestamp}`}
|
||||
className="flex items-center justify-between rounded-md border border-[var(--color-border)] bg-[var(--color-surface-2)] px-3 py-2.5"
|
||||
className="flex items-center justify-between rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface-2)] px-3 py-2.5"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -409,7 +406,7 @@ export default function BackupScreen() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-start gap-2 rounded-lg border border-[var(--color-warn)]/20 bg-[var(--color-warn)]/5 px-4 py-3 text-xs text-[var(--color-warn)]">
|
||||
<div className="flex items-start gap-2 rounded-[var(--radius-card)] border border-[var(--color-warn)]/20 bg-[var(--color-warn)]/5 px-4 py-3 text-xs text-[var(--color-warn)]">
|
||||
<AlertTriangle size={14} className="mt-0.5 shrink-0" />
|
||||
<div className="space-y-1">
|
||||
<p>
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function LogsSection({
|
||||
value={draft.customStart ? toLocalInput(draft.customStart) : ""}
|
||||
max={latest}
|
||||
onChange={(e) => update({ customStart: e.target.value ? new Date(e.target.value).getTime() : null })}
|
||||
className="rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-sm font-normal text-[var(--color-text)]"
|
||||
className="rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-3 py-2 text-sm font-normal text-[var(--color-text)]"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
@@ -118,7 +118,7 @@ export default function ScreenshotsSection({
|
||||
aria-label={s.name}
|
||||
onClick={() => toggle(s)}
|
||||
className={
|
||||
"relative block w-full rounded-lg border-2 p-0.5 transition-colors " +
|
||||
"relative block w-full rounded-[var(--radius-field)] border-2 p-0.5 transition-colors " +
|
||||
(on
|
||||
? "border-[var(--color-accent)]"
|
||||
: "border-transparent opacity-75 hover:border-[var(--color-border-strong)] hover:opacity-100")
|
||||
@@ -130,7 +130,7 @@ export default function ScreenshotsSection({
|
||||
"absolute right-1.5 top-1.5 flex h-5 w-5 items-center justify-center rounded-full border " +
|
||||
(on
|
||||
? "border-[var(--color-accent)] bg-[var(--color-accent)] text-[var(--color-accent-contrast)]"
|
||||
: "border-white/70 bg-black/40")
|
||||
: "border-[var(--glass-edge)] bg-[var(--color-float)]")
|
||||
}
|
||||
>
|
||||
{on && <Check size={12} strokeWidth={3} />}
|
||||
@@ -154,7 +154,7 @@ export default function ScreenshotsSection({
|
||||
type="button"
|
||||
aria-label={`${tr("br_remove", undefined, "Remove")} ${f.name}`}
|
||||
onClick={() => setAttached((a) => ({ ...a, files: a.files.filter((x) => x !== f) }))}
|
||||
className="absolute right-1.5 top-1.5 rounded-full bg-black/70 p-1 text-white"
|
||||
className="glass-float absolute right-1.5 top-1.5 rounded-full p-1 text-[var(--color-text)]"
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function SendPanel({
|
||||
value={discordFallback}
|
||||
rows={8}
|
||||
onFocus={(e) => e.currentTarget.select()}
|
||||
className="w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-2 font-mono text-xs"
|
||||
className="w-full rounded-[var(--radius-field)] border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] p-2 font-mono text-xs"
|
||||
/>
|
||||
<div>
|
||||
<Button size="sm" variant="secondary" leftIcon={<Copy size={12} />} onClick={() => void writeClipboard(discordFallback)}>
|
||||
|
||||
@@ -38,14 +38,14 @@ export default function WhatSection({
|
||||
{f.pinned.map((e) => (
|
||||
<div
|
||||
key={`${e.src}-${e.ts}-${e.code}`}
|
||||
className="flex items-start gap-2 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2"
|
||||
className="flex items-start gap-2 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2"
|
||||
>
|
||||
<span className="min-w-0 flex-1 break-words font-mono text-xs">{formatLine(e)}</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={tr("br_remove", undefined, "Remove")}
|
||||
onClick={() => updateForm({ pinned: f.pinned.filter((p) => p !== e) })}
|
||||
className="shrink-0 rounded p-0.5 text-[var(--color-muted)] hover:text-[var(--color-text)]"
|
||||
className="shrink-0 rounded-full p-1 text-[var(--color-muted)] hover:text-[var(--color-text)]"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
|
||||
@@ -21,14 +21,14 @@ export function CaptureToolbar({
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<p className="flex min-w-0 max-w-3xl flex-wrap items-center gap-2 text-sm leading-relaxed text-[var(--color-muted)]">
|
||||
<p className="flex min-w-0 max-w-3xl items-start gap-2.5 text-sm leading-relaxed text-[var(--color-muted)]">
|
||||
{count !== undefined && (
|
||||
<span className="shrink-0 rounded-full bg-[var(--color-surface-3)] px-2 py-0.5 text-xs tabular-nums">
|
||||
<span className="mt-0.5 shrink-0 rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-2.5 py-0.5 text-xs font-semibold tabular-nums text-[var(--color-text)] shadow-[var(--edge-highlight)]">
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
{loading && <Spinner size={14} tone="accent" />}
|
||||
<span className="min-w-0">{description}</span>
|
||||
{loading && <Spinner size={14} tone="accent" className="mt-1 shrink-0" />}
|
||||
<span className="min-w-0 flex-1">{description}</span>
|
||||
</p>
|
||||
{right && <div className="flex shrink-0 flex-wrap items-center gap-2">{right}</div>}
|
||||
</div>
|
||||
|
||||
@@ -191,7 +191,7 @@ export function CheatGameDetail({
|
||||
</button>
|
||||
|
||||
{/* Who the game is. */}
|
||||
<div className="flex flex-col gap-4 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4 sm:flex-row">
|
||||
<div className="flex flex-col gap-4 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4 sm:flex-row">
|
||||
<GameIcon host={hostOf(host)} titleId={game.titleId} size={112} rounded="rounded-lg" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -230,7 +230,7 @@ export function CheatGameDetail({
|
||||
|
||||
{/* Step: switch cheats on. Only once some are on the console. */}
|
||||
{game.downloaded && (
|
||||
<section className="rounded-lg border border-[var(--color-border)] p-4">
|
||||
<section className="surface-panel p-5">
|
||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">
|
||||
@@ -312,7 +312,7 @@ export function CheatGameDetail({
|
||||
)}
|
||||
|
||||
{/* Step: get cheats. */}
|
||||
<section className="rounded-lg border border-[var(--color-border)] p-4">
|
||||
<section className="surface-panel p-5">
|
||||
<h3 className="text-sm font-semibold">
|
||||
{game.downloaded
|
||||
? tr("cheats_more_title", undefined, "More cheats for this game")
|
||||
|
||||
@@ -258,7 +258,7 @@ export default function CheatsScreen() {
|
||||
].map((text, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="flex items-start gap-2 rounded-md border border-[var(--color-border)] bg-[var(--color-surface-2)] px-3 py-2"
|
||||
className="flex items-start gap-2 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface-2)] px-3 py-2"
|
||||
>
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent)] text-[11px] font-semibold text-[var(--color-accent-contrast)]">
|
||||
{i + 1}
|
||||
|
||||
@@ -77,7 +77,7 @@ function Question({
|
||||
return (
|
||||
<li
|
||||
id={item.id}
|
||||
className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)]"
|
||||
className="surface-panel"
|
||||
data-testid="faq-item"
|
||||
>
|
||||
<button
|
||||
@@ -237,7 +237,7 @@ export default function FAQScreen() {
|
||||
/>
|
||||
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="mb-4 flex items-center gap-2 rounded-md border border-[var(--color-border)] bg-[var(--color-surface-2)] px-3 py-2">
|
||||
<div className="mb-4 flex items-center gap-2 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface-2)] px-3 py-2">
|
||||
<Search size={14} className="shrink-0 text-[var(--color-muted)]" />
|
||||
<input
|
||||
value={query}
|
||||
@@ -254,7 +254,7 @@ export default function FAQScreen() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setQuery("")}
|
||||
className="rounded p-0.5 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)]"
|
||||
className="rounded-full p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)]"
|
||||
aria-label={tr("faq_clear_search", "Clear search")}
|
||||
>
|
||||
<X size={12} />
|
||||
@@ -318,7 +318,7 @@ export default function FAQScreen() {
|
||||
) : (
|
||||
<li
|
||||
key={h.topic.id}
|
||||
className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)]"
|
||||
className="surface-panel"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -116,39 +116,39 @@ export default function FwSpoofScreen() {
|
||||
{tr("fw_spoof_details", undefined, "Details")}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-3 text-sm md:grid-cols-2">
|
||||
<div className="flex items-center justify-between gap-2 rounded-md bg-[var(--color-surface-3)] px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-2 rounded-[var(--radius-field)] bg-[var(--color-surface-3)] px-3 py-2">
|
||||
<span className="text-[var(--color-muted)]">
|
||||
{tr("fw_spoof_sw_version", undefined, "System SW Version")}
|
||||
</span>
|
||||
<code className="font-mono font-semibold">{swVersionDisplay}</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 rounded-md bg-[var(--color-surface-3)] px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-2 rounded-[var(--radius-field)] bg-[var(--color-surface-3)] px-3 py-2">
|
||||
<span className="text-[var(--color-muted)]">
|
||||
{tr("fw_spoof_sw_raw", undefined, "Raw SW Value")}
|
||||
</span>
|
||||
<code className="font-mono">{status.system_sw_raw || "—"}</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 rounded-md bg-[var(--color-surface-3)] px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-2 rounded-[var(--radius-field)] bg-[var(--color-surface-3)] px-3 py-2">
|
||||
<span className="text-[var(--color-muted)]">
|
||||
{tr("fw_spoof_kernel", undefined, "Kernel Release")}
|
||||
</span>
|
||||
<code className="font-mono">{status.kernel_release || "—"}</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 rounded-md bg-[var(--color-surface-3)] px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-2 rounded-[var(--radius-field)] bg-[var(--color-surface-3)] px-3 py-2">
|
||||
<span className="text-[var(--color-muted)]">
|
||||
{tr("fw_spoof_kernel_fw", undefined, "Kernel FW Version")}
|
||||
</span>
|
||||
<code className="font-mono">{status.kernel_fw_version || "—"}</code>
|
||||
</div>
|
||||
{status.kernel_version && (
|
||||
<div className="flex items-center justify-between gap-2 rounded-md bg-[var(--color-surface-3)] px-3 py-2 md:col-span-2">
|
||||
<div className="flex items-center justify-between gap-2 rounded-[var(--radius-field)] bg-[var(--color-surface-3)] px-3 py-2 md:col-span-2">
|
||||
<span className="text-[var(--color-muted)]">
|
||||
{tr("fw_spoof_kern_version", undefined, "Kernel Build")}
|
||||
</span>
|
||||
<code className="font-mono text-xs">{status.kernel_version}</code>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between gap-2 rounded-md bg-[var(--color-surface-3)] px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-2 rounded-[var(--radius-field)] bg-[var(--color-surface-3)] px-3 py-2">
|
||||
<span className="text-[var(--color-muted)]">
|
||||
{tr("fw_spoof_flag", undefined, "Spoofed")}
|
||||
</span>
|
||||
|
||||
@@ -46,9 +46,9 @@ export default function NetworkPanel({ mgmtAddr }: { mgmtAddr: string }) {
|
||||
}, [mgmtAddr]);
|
||||
|
||||
return (
|
||||
<section className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<header className="mb-3 flex items-center gap-2">
|
||||
<Network size={14} />
|
||||
<section className="surface-panel min-w-0 p-5 sm:p-6">
|
||||
<header className="mb-4 flex items-center gap-2.5">
|
||||
<span className="icon-disc" aria-hidden><Network size={14} /></span>
|
||||
<h3 className="flex-1 text-sm font-semibold">
|
||||
{tr("network_panel_title", undefined, "Network interfaces")}
|
||||
</h3>
|
||||
|
||||
@@ -108,11 +108,11 @@ export default function PeripheralPanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<section className="surface-panel min-w-0 p-5 sm:p-6">
|
||||
{drive !== "no" && (
|
||||
<div data-testid="disc-controls" className="mb-4">
|
||||
<header className="mb-3 flex items-center gap-2">
|
||||
<Disc3 size={14} />
|
||||
<header className="mb-4 flex items-center gap-2.5">
|
||||
<span className="icon-disc" aria-hidden><Disc3 size={14} /></span>
|
||||
<h3 className="text-sm font-semibold">
|
||||
{tr("peripheral_title", undefined, "Disc drive")}
|
||||
</h3>
|
||||
|
||||
@@ -51,9 +51,9 @@ export default function PowerTelemetryPanel({ mgmtAddr }: { mgmtAddr: string })
|
||||
}, [mgmtAddr]);
|
||||
|
||||
return (
|
||||
<section className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<header className="mb-3 flex items-center gap-2">
|
||||
<Battery size={14} />
|
||||
<section className="surface-panel min-w-0 p-5 sm:p-6">
|
||||
<header className="mb-4 flex items-center gap-2.5">
|
||||
<span className="icon-disc" aria-hidden><Battery size={14} /></span>
|
||||
<h3 className="flex-1 text-sm font-semibold">
|
||||
{tr("power_telemetry_title_v2", undefined, "Lifetime counters")}
|
||||
</h3>
|
||||
@@ -100,7 +100,7 @@ export default function PowerTelemetryPanel({ mgmtAddr }: { mgmtAddr: string })
|
||||
which of the four ICC symbols resolved, so use it rather than
|
||||
guessing from the nulls. */}
|
||||
{data && unavailableNote(data) && (
|
||||
<p className="mt-3 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-2 text-xs text-[var(--color-muted)]">
|
||||
<p className="mt-3 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] p-3 text-xs text-[var(--color-muted)]">
|
||||
{tr(
|
||||
`power_telemetry_note_${data.status}`,
|
||||
undefined,
|
||||
|
||||
@@ -437,7 +437,7 @@ export default function HardwareScreen() {
|
||||
)}
|
||||
|
||||
<ConnectionGate require="payload">
|
||||
<div className="grid gap-4 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<div className="grid gap-5 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<SensorCard
|
||||
icon={<Thermometer size={14} />}
|
||||
title={tr("hardware_temperatures", undefined, "Temperatures")}
|
||||
@@ -770,14 +770,14 @@ function SensorCard({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<header className="mb-2 flex items-center gap-2 text-xs uppercase tracking-wide text-[var(--color-muted)]">
|
||||
{icon}
|
||||
<span className="font-semibold">{title}</span>
|
||||
<section className="surface-panel min-w-0 p-5 sm:p-6">
|
||||
<header className="mb-3 flex items-center gap-2.5 text-sm font-semibold">
|
||||
<span className="icon-disc" aria-hidden>
|
||||
{icon}
|
||||
</span>
|
||||
<span>{title}</span>
|
||||
</header>
|
||||
<dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm">
|
||||
{children}
|
||||
</dl>
|
||||
<dl className="flex flex-col">{children}</dl>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -791,18 +791,20 @@ function StatRow({
|
||||
value: string;
|
||||
hint?: string;
|
||||
}) {
|
||||
// One metric row: label left, value right, the hint (if any) under the
|
||||
// value in small muted type rather than squeezed in beside it.
|
||||
return (
|
||||
<>
|
||||
<dt className="text-[var(--color-muted)]">{label}</dt>
|
||||
<dd className="font-mono tabular-nums" title={hint}>
|
||||
{value}
|
||||
<div className="metric-row min-w-0 py-2">
|
||||
<dt className="shrink-0 text-[var(--color-muted)]">{label}</dt>
|
||||
<dd className="min-w-0 text-right" title={hint}>
|
||||
<div className="font-mono tabular-nums">{value}</div>
|
||||
{hint && (
|
||||
<span className="ml-2 text-xs text-[var(--color-muted)] font-sans">
|
||||
· {hint}
|
||||
</span>
|
||||
<div className="mt-0.5 text-xs leading-snug text-[var(--color-muted)]">
|
||||
{hint}
|
||||
</div>
|
||||
)}
|
||||
</dd>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -903,10 +905,10 @@ function FanThresholdCard({
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<section className="surface-panel min-w-0 p-5 sm:p-6">
|
||||
{confirmDialog}
|
||||
<header className="mb-3 flex items-center gap-2 text-xs uppercase tracking-wide text-[var(--color-muted)]">
|
||||
<Fan size={14} />
|
||||
<header className="mb-4 flex items-center gap-2.5 text-sm font-semibold text-[var(--color-text)]">
|
||||
<span className="icon-disc" aria-hidden><Fan size={14} /></span>
|
||||
<span className="font-semibold">
|
||||
{tr("hardware_fan_threshold", "Fan threshold")}
|
||||
</span>
|
||||
@@ -962,7 +964,7 @@ function FanThresholdCard({
|
||||
type="button"
|
||||
onClick={() => applyThreshold(draftC)}
|
||||
disabled={!canSet || lastSetC === draftC}
|
||||
className="rounded-md border border-[var(--color-border)] px-3 py-1 text-xs hover:bg-[var(--color-surface-3)] disabled:opacity-50"
|
||||
className="chip min-h-8 px-3.5 text-xs disabled:opacity-50"
|
||||
>
|
||||
{tr("hardware_apply", "Apply")}
|
||||
</button>
|
||||
@@ -994,7 +996,7 @@ function FanThresholdCard({
|
||||
type="button"
|
||||
onClick={restoreConsole}
|
||||
disabled={!canSet || (pinnedC === 0 && lastSetC === null)}
|
||||
className="rounded-md border border-[var(--color-border)] px-3 py-1 text-xs hover:bg-[var(--color-surface-3)] disabled:opacity-50"
|
||||
className="chip min-h-8 px-3.5 text-xs disabled:opacity-50"
|
||||
>
|
||||
{tr("hardware_fan_restore", undefined, "Use the console's own setting")}
|
||||
</button>
|
||||
@@ -1074,7 +1076,7 @@ function FanCurvePreview({ thresholdC }: { thresholdC: number }) {
|
||||
.map(([t, p]) => `${xFor(t).toFixed(1)},${yFor(p).toFixed(1)}`)
|
||||
.join(" ");
|
||||
return (
|
||||
<div className="mb-3 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-2">
|
||||
<div className="mb-3 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] p-3">
|
||||
<div className="mb-1 flex items-center justify-between text-xs text-[var(--color-muted)]">
|
||||
<span>
|
||||
{tr("hardware_fan_curve_preview", "Fan curve preview (approximate)")}
|
||||
@@ -1266,9 +1268,9 @@ function SystemTimeCard({
|
||||
const outcome = lastResult ? classifySyncResult(lastResult) : null;
|
||||
|
||||
return (
|
||||
<section className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<header className="mb-3 flex items-center gap-2 text-xs uppercase tracking-wide text-[var(--color-muted)]">
|
||||
<CalendarClock size={14} />
|
||||
<section className="surface-panel min-w-0 p-5 sm:p-6">
|
||||
<header className="mb-4 flex items-center gap-2.5 text-sm font-semibold text-[var(--color-text)]">
|
||||
<span className="icon-disc" aria-hidden><CalendarClock size={14} /></span>
|
||||
<span>{tr("hardware_systime_title", "System time")}</span>
|
||||
</header>
|
||||
|
||||
@@ -1347,7 +1349,7 @@ function SystemTimeCard({
|
||||
|
||||
{/* Result — success / success-via-fallback / no-op / failure */}
|
||||
{lastResult && !busy && (
|
||||
<div className="mt-3 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-2 text-[11px]">
|
||||
<div className="mt-3 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] p-3 text-[11px]">
|
||||
{outcome === "stub_no_op" ? (
|
||||
<div className="text-[var(--color-bad)]">
|
||||
{tr(
|
||||
@@ -1541,9 +1543,9 @@ function SmpMetaCard({
|
||||
: tr("smp_meta_never_run", "never");
|
||||
|
||||
return (
|
||||
<section className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<header className="mb-3 flex items-center gap-2 text-xs uppercase tracking-wide text-[var(--color-muted)]">
|
||||
<ImageIcon size={14} />
|
||||
<section className="surface-panel min-w-0 p-5 sm:p-6">
|
||||
<header className="mb-4 flex items-center gap-2.5 text-sm font-semibold text-[var(--color-text)]">
|
||||
<span className="icon-disc" aria-hidden><ImageIcon size={14} /></span>
|
||||
<span className="font-semibold">
|
||||
{tr("smp_meta_title", "SMP appmeta heal")}
|
||||
</span>
|
||||
|
||||
@@ -101,7 +101,7 @@ export function SpeedTestCard({ host }: { host: string }) {
|
||||
value={sizeMib}
|
||||
disabled={running}
|
||||
onChange={(e) => setSizeMib(Number(e.currentTarget.value))}
|
||||
className="rounded border border-[var(--color-border)] bg-[var(--color-surface-1)] px-2 py-1 text-sm text-[var(--color-text)]"
|
||||
className="rounded border border-[var(--color-border)] bg-[var(--color-surface)] px-2 py-1 text-sm text-[var(--color-text)]"
|
||||
>
|
||||
{SIZES.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
@@ -155,7 +155,7 @@ export function SpeedTestCard({ host }: { host: string }) {
|
||||
className="mt-3 grid gap-3 sm:grid-cols-2"
|
||||
data-testid="speed-test-result"
|
||||
>
|
||||
<div className="rounded-md border border-[var(--color-border)] bg-[var(--color-surface-1)] p-3">
|
||||
<div className="rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] p-3">
|
||||
<dt className="text-xs text-[var(--color-muted)]">
|
||||
{tr("speedtest.up", undefined, "To the PS5 (upload)")}
|
||||
</dt>
|
||||
@@ -163,7 +163,7 @@ export function SpeedTestCard({ host }: { host: string }) {
|
||||
{st.uploadBps !== null ? perSec(st.uploadBps) : "—"}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="rounded-md border border-[var(--color-border)] bg-[var(--color-surface-1)] p-3">
|
||||
<div className="rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] p-3">
|
||||
<dt className="text-xs text-[var(--color-muted)]">
|
||||
{tr("speedtest.down", undefined, "From the PS5 (download)")}
|
||||
</dt>
|
||||
|
||||
@@ -236,7 +236,7 @@ export default function NanoDnsScreen() {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-col gap-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<div className="surface-panel flex flex-col gap-2 p-5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge
|
||||
tone={
|
||||
@@ -305,7 +305,7 @@ export default function NanoDnsScreen() {
|
||||
) : null}
|
||||
|
||||
{version.generation === "modern" && text !== null ? (
|
||||
<div className="grid gap-4 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4 md:grid-cols-2">
|
||||
<div className="grid gap-4 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4 md:grid-cols-2">
|
||||
<Toggle
|
||||
checked={quietEnabled}
|
||||
disabled={saving}
|
||||
@@ -340,7 +340,7 @@ export default function NanoDnsScreen() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-col gap-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<div className="surface-panel flex flex-col gap-2 p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<code className="text-xs text-[var(--color-muted)]">
|
||||
{NANODNS_INI_PATH}
|
||||
@@ -371,7 +371,7 @@ export default function NanoDnsScreen() {
|
||||
value={text ?? ""}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
spellCheck={false}
|
||||
className="h-[28rem] w-full resize-y rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-3 font-mono text-xs leading-relaxed outline-none focus:border-[var(--color-accent)]"
|
||||
className="h-[28rem] w-full resize-y rounded-[var(--radius-field)] border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] p-3 font-mono text-xs leading-relaxed outline-none focus:border-[var(--color-accent)]"
|
||||
/>
|
||||
<p className="text-xs text-[var(--color-muted)]">
|
||||
{tr(
|
||||
|
||||
@@ -159,12 +159,14 @@ export default function NotificationsScreen() {
|
||||
/>
|
||||
|
||||
<ConnectionGate>
|
||||
{error && <ErrorCard title={error} />}
|
||||
{error && <ErrorCard title={error} onRetry={() => void refresh()} />}
|
||||
|
||||
{unreadCount > 0 && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-[var(--color-accent)]/40 bg-[var(--color-accent-soft)] px-4 py-2 text-sm">
|
||||
<span className="flex items-center gap-2 text-[var(--color-accent)]">
|
||||
<Mail size={14} />
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 rounded-[var(--radius-card)] border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-4 py-2.5 text-sm shadow-[var(--edge-highlight)]">
|
||||
<span className="flex items-center gap-2.5 font-medium">
|
||||
<span className="icon-disc !h-7 !w-7" aria-hidden>
|
||||
<Mail size={13} />
|
||||
</span>
|
||||
{tr(
|
||||
"notifications_unread",
|
||||
{ count: unreadCount },
|
||||
@@ -203,17 +205,17 @@ export default function NotificationsScreen() {
|
||||
return (
|
||||
<div
|
||||
key={n.seq}
|
||||
className={`rounded-md border px-3 py-2.5 transition-colors ${
|
||||
className={`rounded-[var(--radius-card)] border px-4 py-3 transition-colors ${
|
||||
read
|
||||
? "border-[var(--color-border)] bg-[var(--color-surface-2)]"
|
||||
: "border-[var(--color-accent)]/40 bg-[var(--color-accent-soft)]"
|
||||
? "border-[var(--color-border)] bg-[var(--color-surface)]"
|
||||
: "border-[var(--glass-edge)] bg-[var(--color-surface-raised)] shadow-[var(--edge-highlight),var(--shadow-1)]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<span
|
||||
aria-hidden
|
||||
className={`mt-1.5 h-2 w-2 shrink-0 rounded-full ${
|
||||
read ? "bg-transparent" : "bg-[var(--color-accent)]"
|
||||
read ? "bg-transparent" : "bg-[var(--color-accent-bright)]"
|
||||
}`}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -237,7 +239,7 @@ export default function NotificationsScreen() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => host && setRead(host, n.seq, !read)}
|
||||
className="flex shrink-0 items-center gap-1 rounded px-2 py-1 text-xs text-[var(--color-muted)] hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text)]"
|
||||
className="flex shrink-0 items-center gap-1 rounded-full px-2.5 py-1 text-xs text-[var(--color-muted)] hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text)] max-md:min-h-11"
|
||||
>
|
||||
{read ? <Mail size={12} /> : <MailOpen size={12} />}
|
||||
{read
|
||||
|
||||
@@ -360,7 +360,7 @@ export default function CatalogPanel() {
|
||||
with ps5upload is the guidance that was missing — kept as plain
|
||||
text rather than buttons so it never implies these are installed
|
||||
or endorsed beyond "start here". */}
|
||||
<div className="rounded-md border border-[var(--color-accent)] bg-[var(--color-surface)] p-3">
|
||||
<div className="rounded-[var(--radius-card)] border border-[var(--color-accent)] bg-[var(--color-surface)] p-3">
|
||||
<p className="text-xs font-semibold">
|
||||
{tr("payloads_essentials_title", undefined, "Recommended alongside ps5upload")}
|
||||
</p>
|
||||
@@ -372,7 +372,7 @@ export default function CatalogPanel() {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] p-3">
|
||||
<div className="text-xs text-[var(--color-muted)]">
|
||||
{tr(
|
||||
"payloads_curated_blurb",
|
||||
@@ -420,7 +420,7 @@ export default function CatalogPanel() {
|
||||
</div>
|
||||
</div>
|
||||
{addOpen && (
|
||||
<div className="rounded-md border border-[var(--color-border)] bg-[var(--color-surface-2)] p-3">
|
||||
<div className="rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface-2)] p-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-[var(--color-text)]">
|
||||
{tr("payloads_add_repo_title", undefined, "Track a custom payload repo")}
|
||||
@@ -470,7 +470,7 @@ export default function CatalogPanel() {
|
||||
undefined,
|
||||
"Used only when you enter a bare owner/name. GitHub or a Gitea/Forgejo host.",
|
||||
)}
|
||||
className="w-36 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 py-1.5 text-xs outline-none focus:border-[var(--color-accent)]"
|
||||
className="w-36 rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-2 py-1.5 text-xs outline-none focus:border-[var(--color-accent)]"
|
||||
/>
|
||||
</label>
|
||||
<Button
|
||||
@@ -484,7 +484,7 @@ export default function CatalogPanel() {
|
||||
</Button>
|
||||
</div>
|
||||
{addError && (
|
||||
<div className="mt-2 text-xs text-[var(--color-danger)]">{addError}</div>
|
||||
<div className="mt-2 text-xs text-[var(--color-bad)]">{addError}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -514,7 +514,7 @@ export default function CatalogPanel() {
|
||||
/>
|
||||
)}
|
||||
{query.trim() && visibleCatalog.length === 0 && (
|
||||
<div className="rounded-md border border-dashed border-[var(--color-border)] p-6 text-center text-xs text-[var(--color-muted)]">
|
||||
<div className="rounded-[var(--radius-card)] border border-dashed border-[var(--color-border)] p-6 text-center text-xs text-[var(--color-muted)]">
|
||||
{tr(
|
||||
"payloads_search_no_match",
|
||||
{ query: query.trim() },
|
||||
@@ -600,7 +600,7 @@ function PayloadCard({
|
||||
const updateAvailable =
|
||||
!!local && !!release && !!release.tag && local.version !== release.tag;
|
||||
return (
|
||||
<section className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<section className="surface-panel p-5">
|
||||
<div className="flex flex-wrap items-start gap-3">
|
||||
{/* Floor the text column's min-width to min(100%,16rem). With `flex-1`
|
||||
(basis 0%) + `min-w-0`, the name column shrank to nothing at large
|
||||
@@ -630,7 +630,7 @@ function PayloadCard({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
className="inline-flex items-center gap-1 text-xs text-[var(--color-muted)] hover:text-[var(--color-danger)]"
|
||||
className="inline-flex items-center gap-1 text-xs text-[var(--color-muted)] hover:text-[var(--color-bad)]"
|
||||
title={tr("payloads_custom_remove", undefined, "Remove this custom repo")}
|
||||
>
|
||||
<Trash2 size={11} />
|
||||
@@ -705,7 +705,7 @@ function PayloadCard({
|
||||
value={selectedTag}
|
||||
onChange={(e) => onSelectTag(e.target.value)}
|
||||
disabled={busy}
|
||||
className="max-w-[12rem] rounded border border-[var(--color-border)] bg-[var(--color-surface)] px-1.5 py-1 text-xs"
|
||||
className="max-w-[12rem] rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-1.5 py-1 text-xs"
|
||||
title={tr(
|
||||
"payloads_version_tooltip",
|
||||
undefined,
|
||||
@@ -768,7 +768,7 @@ function PayloadCard({
|
||||
</div>
|
||||
|
||||
{release && (
|
||||
<div className="mt-3 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-2 text-xs">
|
||||
<div className="mt-3 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] p-3 text-xs">
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-[var(--color-muted)]">
|
||||
<span>
|
||||
{tr(
|
||||
@@ -809,7 +809,7 @@ function PayloadCard({
|
||||
// user has no way to tell live data from N-day-stale
|
||||
// cache, and might wonder why the "latest" version
|
||||
// doesn't match a release they just saw on GitHub.
|
||||
<div className="mt-2 flex items-start gap-1.5 rounded-md border border-[var(--color-warn)] bg-[var(--color-surface)] p-2 text-xs text-[var(--color-warn)]">
|
||||
<div className="mt-2 flex items-start gap-1.5 rounded-[var(--radius-card)] border border-[var(--color-warn)] bg-[var(--color-surface)] p-3 text-xs text-[var(--color-warn)]">
|
||||
<AlertTriangle size={11} className="mt-0.5 shrink-0" />
|
||||
<span>
|
||||
{tr(
|
||||
@@ -836,7 +836,7 @@ function PayloadCard({
|
||||
// (a renamed/deleted repo → 404, or a down Forgejo host) is not a
|
||||
// user error — the row stays present and usable later. The loud red
|
||||
// card made users think the payload was "broken" or "removed".
|
||||
<div className="mt-3 flex items-start gap-2 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-2 text-xs text-[var(--color-muted)]">
|
||||
<div className="mt-3 flex items-start gap-2 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] p-3 text-xs text-[var(--color-muted)]">
|
||||
<AlertTriangle size={11} className="mt-0.5 shrink-0" />
|
||||
<span className="min-w-0 break-words">
|
||||
{tr(
|
||||
|
||||
@@ -192,7 +192,7 @@ export function PlaylistsPanel({ host, port }: { host: string; port: number }) {
|
||||
.slice(0, 5);
|
||||
|
||||
return (
|
||||
<section className="mt-8 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-5">
|
||||
<section className="mt-8 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface-2)] p-5">
|
||||
<header className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="flex items-center gap-2 text-sm font-semibold">
|
||||
@@ -341,7 +341,7 @@ export function PlaylistsPanel({ host, port }: { host: string; port: number }) {
|
||||
)}
|
||||
|
||||
{playlists.length === 0 && (
|
||||
<div className="rounded-md border border-dashed border-[var(--color-border)] bg-[var(--color-surface)] p-4 text-center text-xs text-[var(--color-muted)]">
|
||||
<div className="rounded-[var(--radius-card)] border border-dashed border-[var(--color-border)] bg-[var(--color-surface)] p-4 text-center text-xs text-[var(--color-muted)]">
|
||||
{tr(
|
||||
"playlists_empty",
|
||||
undefined,
|
||||
@@ -389,7 +389,7 @@ function AutoLoaderCard() {
|
||||
const warn = autoLoader.enabled && (!selected || selected.steps.length === 0);
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-3">
|
||||
<div className="mb-4 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] p-3">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between">
|
||||
<div className="flex min-w-0 items-start gap-2">
|
||||
<Zap
|
||||
@@ -511,7 +511,7 @@ function RunStatusBanner({ host }: { host: string }) {
|
||||
if (runStatus.kind === "running") {
|
||||
const step = playlist?.steps[runStatus.stepIndex];
|
||||
return (
|
||||
<div className="mb-3 flex items-center gap-2 rounded-md border border-[var(--color-accent)] bg-[var(--color-surface)] p-2 text-xs">
|
||||
<div className="mb-3 flex items-center gap-2 rounded-[var(--radius-card)] border border-[var(--color-accent)] bg-[var(--color-surface)] p-3 text-xs">
|
||||
<Spinner size={14} tone="accent" />
|
||||
<span className="font-medium">{name}</span>
|
||||
<ConsoleChip addr={runStatus.host} />
|
||||
@@ -534,7 +534,7 @@ function RunStatusBanner({ host }: { host: string }) {
|
||||
const elapsed = now - runStatus.sleepStartedAtMs;
|
||||
const remaining = Math.max(0, runStatus.sleepDurationMs - elapsed);
|
||||
return (
|
||||
<div className="mb-3 flex items-center gap-2 rounded-md border border-[var(--color-accent)] bg-[var(--color-surface)] p-2 text-xs">
|
||||
<div className="mb-3 flex items-center gap-2 rounded-[var(--radius-card)] border border-[var(--color-accent)] bg-[var(--color-surface)] p-3 text-xs">
|
||||
<Clock size={14} className="text-[var(--color-accent)]" />
|
||||
<span className="font-medium">{name}</span>
|
||||
<ConsoleChip addr={runStatus.host} />
|
||||
@@ -715,7 +715,7 @@ function PlaylistCard({
|
||||
const canRun = playlist.steps.length > 0 && !!host?.trim() && !anyRunning;
|
||||
|
||||
return (
|
||||
<article className="rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-4">
|
||||
<article className="rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] p-4">
|
||||
<header className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
{/* Reorder this playlist within the list. Mirrors the per-step
|
||||
@@ -726,7 +726,7 @@ function PlaylistCard({
|
||||
onClick={() => movePlaylistUp(playlist.id)}
|
||||
disabled={anyRunning || index === 0}
|
||||
aria-label={tr("playlist_move_up", undefined, "Move playlist up")}
|
||||
className="rounded p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)] disabled:opacity-30"
|
||||
className="rounded-full p-1.5 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)] disabled:opacity-30"
|
||||
title={tr("playlist_move_up", undefined, "Move playlist up")}
|
||||
>
|
||||
<ArrowUp size={12} />
|
||||
@@ -736,7 +736,7 @@ function PlaylistCard({
|
||||
onClick={() => movePlaylistDown(playlist.id)}
|
||||
disabled={anyRunning || index === total - 1}
|
||||
aria-label={tr("playlist_move_down", undefined, "Move playlist down")}
|
||||
className="rounded p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)] disabled:opacity-30"
|
||||
className="rounded-full p-1.5 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)] disabled:opacity-30"
|
||||
title={tr("playlist_move_down", undefined, "Move playlist down")}
|
||||
>
|
||||
<ArrowDown size={12} />
|
||||
@@ -757,7 +757,7 @@ function PlaylistCard({
|
||||
setRenaming(false);
|
||||
}
|
||||
}}
|
||||
className="rounded-md border border-[var(--color-accent)] bg-[var(--color-surface)] px-2 py-0.5 text-sm font-semibold outline-none"
|
||||
className="rounded-full border border-[var(--color-accent)] bg-[var(--color-surface)] px-2 py-0.5 text-sm font-semibold outline-none"
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
@@ -974,7 +974,7 @@ function PlaylistCard({
|
||||
})
|
||||
}
|
||||
disabled={anyRunning}
|
||||
className="w-28 rounded border border-[var(--color-border)] bg-[var(--color-surface)] px-1 py-0.5 text-xs"
|
||||
className="w-28 rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-1 py-0.5 text-xs"
|
||||
/>
|
||||
</label>
|
||||
{/* Per-step port override. Empty / 0 = use the
|
||||
@@ -1019,7 +1019,7 @@ function PlaylistCard({
|
||||
})
|
||||
}
|
||||
disabled={anyRunning}
|
||||
className="w-16 rounded border border-[var(--color-border)] bg-[var(--color-surface)] px-1 py-0.5 text-right text-xs tabular-nums"
|
||||
className="w-16 rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-1 py-0.5 text-right text-xs tabular-nums"
|
||||
/>
|
||||
<span>ms</span>
|
||||
</label>
|
||||
@@ -1029,7 +1029,7 @@ function PlaylistCard({
|
||||
onClick={() => moveStepUp(playlist.id, i)}
|
||||
disabled={anyRunning || i === 0}
|
||||
aria-label={tr("playlist_step_up", undefined, "Move up")}
|
||||
className="rounded p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)] disabled:opacity-30"
|
||||
className="rounded-full p-1.5 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)] disabled:opacity-30"
|
||||
title={tr("playlist_step_up", undefined, "Move up")}
|
||||
>
|
||||
<ArrowUp size={12} />
|
||||
@@ -1039,7 +1039,7 @@ function PlaylistCard({
|
||||
onClick={() => moveStepDown(playlist.id, i)}
|
||||
disabled={anyRunning || i === playlist.steps.length - 1}
|
||||
aria-label={tr("playlist_step_down", undefined, "Move down")}
|
||||
className="rounded p-1 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)] disabled:opacity-30"
|
||||
className="rounded-full p-1.5 text-[var(--color-muted)] hover:bg-[var(--color-surface-3)] disabled:opacity-30"
|
||||
title={tr("playlist_step_down", undefined, "Move down")}
|
||||
>
|
||||
<ArrowDown size={12} />
|
||||
@@ -1049,7 +1049,7 @@ function PlaylistCard({
|
||||
onClick={() => removeStep(playlist.id, i)}
|
||||
disabled={anyRunning}
|
||||
aria-label={tr("playlist_step_remove", undefined, "Remove step")}
|
||||
className="rounded p-1 text-[var(--color-muted)] hover:bg-[var(--color-bad)] hover:text-[var(--color-accent-contrast)] disabled:opacity-30"
|
||||
className="rounded-full p-1.5 text-[var(--color-muted)] hover:bg-[var(--color-bad)] hover:text-[var(--color-accent-contrast)] disabled:opacity-30"
|
||||
title={tr("playlist_step_remove", undefined, "Remove step")}
|
||||
>
|
||||
<X size={12} />
|
||||
@@ -1134,7 +1134,7 @@ function PlaylistCard({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAddRepoStep(p)}
|
||||
className="flex w-full items-center gap-2 rounded-md border border-[var(--color-border)] bg-[var(--color-surface-2)] p-2 text-left text-xs hover:border-[var(--color-accent)]"
|
||||
className="flex w-full items-center gap-2 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface-2)] p-3 text-left text-xs hover:border-[var(--color-accent)]"
|
||||
>
|
||||
<PackageIcon
|
||||
size={14}
|
||||
|
||||
@@ -388,7 +388,7 @@ export default function SendPanel() {
|
||||
instead. `min(100%,24rem)` keeps a lone column from overflowing a
|
||||
narrow viewport. */}
|
||||
<div className="grid gap-6 grid-cols-[repeat(auto-fit,minmax(min(100%,24rem),1fr))]">
|
||||
<section className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-5">
|
||||
<section className="rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface-2)] p-5">
|
||||
{/* IP + port. Stacks vertically by default and only goes
|
||||
side-by-side at sm+, with a FLOORED IP column (minmax 10rem) and
|
||||
an elastic port column. The old fixed `1fr_7rem` collapsed the IP
|
||||
@@ -575,7 +575,7 @@ export default function SendPanel() {
|
||||
</div>
|
||||
|
||||
{status.kind === "sent" && (
|
||||
<div className="mt-4 flex items-start gap-2 rounded-md border border-[var(--color-good)] bg-[var(--color-surface-3)] p-3 text-xs">
|
||||
<div className="mt-4 flex items-start gap-2 rounded-[var(--radius-card)] border border-[var(--color-good)] bg-[var(--color-surface-3)] p-3 text-xs">
|
||||
<CheckCircle2
|
||||
size={14}
|
||||
className="mt-0.5 text-[var(--color-good)]"
|
||||
@@ -601,7 +601,7 @@ export default function SendPanel() {
|
||||
)}
|
||||
|
||||
{status.kind === "failed" && (
|
||||
<div className="mt-4 flex items-start gap-2 rounded-md border border-[var(--color-bad)] bg-[var(--color-surface-3)] p-3 text-xs">
|
||||
<div className="mt-4 flex items-start gap-2 rounded-[var(--radius-card)] border border-[var(--color-bad)] bg-[var(--color-surface-3)] p-3 text-xs">
|
||||
<XCircle size={14} className="mt-0.5 text-[var(--color-bad)]" />
|
||||
<div>
|
||||
<div className="font-medium text-[var(--color-bad)]">
|
||||
@@ -649,7 +649,7 @@ function HistoryPanel({
|
||||
}) {
|
||||
const tr = useTr();
|
||||
return (
|
||||
<aside className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<aside className="surface-panel p-5">
|
||||
<header className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<History size={14} />
|
||||
@@ -662,7 +662,7 @@ function HistoryPanel({
|
||||
onClick={onClear}
|
||||
disabled={records.length === 0}
|
||||
title={tr("sendpayload_clear_history", undefined, "Clear history")}
|
||||
className="flex items-center gap-1 rounded-md border border-[var(--color-border)] px-2 py-1 text-xs hover:bg-[var(--color-surface-3)] disabled:opacity-40"
|
||||
className="flex items-center gap-1 rounded-full border border-[var(--color-border)] px-2 py-1 text-xs hover:bg-[var(--color-surface-3)] disabled:opacity-40"
|
||||
>
|
||||
<Trash2 size={11} />
|
||||
{tr("sendpayload_clear", undefined, "Clear")}
|
||||
@@ -670,7 +670,7 @@ function HistoryPanel({
|
||||
</header>
|
||||
|
||||
{records.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed border-[var(--color-border)] p-4 text-center text-xs text-[var(--color-muted)]">
|
||||
<div className="rounded-[var(--radius-card)] border border-dashed border-[var(--color-border)] p-4 text-center text-xs text-[var(--color-muted)]">
|
||||
{tr(
|
||||
"sendpayload_no_sends_yet",
|
||||
undefined,
|
||||
@@ -685,7 +685,7 @@ function HistoryPanel({
|
||||
type="button"
|
||||
onClick={() => onReplay(rec)}
|
||||
title={rec.path}
|
||||
className="group flex w-full items-start gap-2 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-2 text-left text-xs hover:border-[var(--color-accent)] hover:bg-[var(--color-surface-3)]"
|
||||
className="group flex w-full items-start gap-2 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] p-3 text-left text-xs hover:border-[var(--color-accent)] hover:bg-[var(--color-surface-3)]"
|
||||
>
|
||||
<StatusDot ok={rec.status === "success"} />
|
||||
<div className="min-w-0 flex-1">
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function UsbAutoloaderModal({
|
||||
>
|
||||
<div className="space-y-5 p-5 text-sm">
|
||||
{mobile ? (
|
||||
<div className="flex flex-col items-center gap-2 rounded-md border border-dashed border-[var(--color-border)] p-6 text-center">
|
||||
<div className="flex flex-col items-center gap-2 rounded-[var(--radius-card)] border border-dashed border-[var(--color-border)] p-6 text-center">
|
||||
<HardDrive size={24} className="text-[var(--color-muted)]" />
|
||||
<div className="text-sm font-medium">
|
||||
{tr(
|
||||
@@ -161,7 +161,7 @@ export default function UsbAutoloaderModal({
|
||||
{tr("usb_wizard_scanning", undefined, "Scanning…")}
|
||||
</div>
|
||||
) : drives.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed border-[var(--color-border)] p-3 text-xs text-[var(--color-muted)]">
|
||||
<div className="rounded-[var(--radius-card)] border border-dashed border-[var(--color-border)] p-3 text-xs text-[var(--color-muted)]">
|
||||
{tr(
|
||||
"usb_wizard_no_drives",
|
||||
undefined,
|
||||
@@ -321,7 +321,7 @@ export default function UsbAutoloaderModal({
|
||||
)}
|
||||
{result && (
|
||||
<div className="mt-2 space-y-2 text-xs">
|
||||
<div className="rounded-md border border-[var(--color-good)] bg-[var(--color-surface)] p-2">
|
||||
<div className="rounded-[var(--radius-card)] border border-[var(--color-good)] bg-[var(--color-surface)] p-3">
|
||||
<div className="font-medium text-[var(--color-good)]">
|
||||
{tr(
|
||||
"usb_wizard_done",
|
||||
@@ -336,7 +336,7 @@ export default function UsbAutoloaderModal({
|
||||
</ul>
|
||||
</div>
|
||||
{result.skipped.length > 0 && (
|
||||
<div className="rounded-md border border-[var(--color-warn)] bg-[var(--color-surface)] p-2">
|
||||
<div className="rounded-[var(--radius-card)] border border-[var(--color-warn)] bg-[var(--color-surface)] p-3">
|
||||
<div className="font-medium text-[var(--color-warn)]">
|
||||
{tr("usb_wizard_skipped", undefined, "Skipped")}
|
||||
</div>
|
||||
@@ -347,7 +347,7 @@ export default function UsbAutoloaderModal({
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<details className="rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-2">
|
||||
<details className="rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] p-3">
|
||||
<summary className="cursor-pointer text-[var(--color-muted)]">
|
||||
{tr(
|
||||
"usb_wizard_show_autoload_txt",
|
||||
|
||||
@@ -279,7 +279,7 @@ function AvatarSection({
|
||||
</div>
|
||||
|
||||
{noTarget && (
|
||||
<div className="mb-3 rounded-md border border-[var(--color-warn)] bg-[var(--color-surface)] p-3 text-xs text-[var(--color-muted)]">
|
||||
<div className="mb-3 rounded-[var(--radius-card)] border border-[var(--color-warn)] bg-[var(--color-surface)] p-3 text-xs text-[var(--color-muted)]">
|
||||
{tr(
|
||||
"profile.avatar.noUser",
|
||||
"No console user found. Sign in to a profile on the PS5, then refresh.",
|
||||
@@ -423,11 +423,8 @@ function ModeButton({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-xs font-medium transition ${
|
||||
active
|
||||
? "border-[var(--color-accent)] bg-[var(--color-accent)]/10 text-[var(--color-accent)]"
|
||||
: "border-[var(--color-border)] text-[var(--color-muted)] hover:bg-[var(--color-surface-3)]"
|
||||
}`}
|
||||
aria-pressed={active}
|
||||
className="chip min-h-8 gap-1.5 px-3.5 text-xs font-medium max-md:min-h-11"
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
@@ -625,7 +622,7 @@ function UserRow({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-2">
|
||||
<div className="rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] p-3">
|
||||
{confirmDialog}
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
@@ -635,7 +632,7 @@ function UserRow({
|
||||
{uidHex}
|
||||
</span>
|
||||
<input
|
||||
className="min-w-0 flex-1 rounded-md border border-[var(--color-border)] bg-[var(--color-surface-2)] px-2 py-1 text-sm"
|
||||
className="min-w-0 flex-1 rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-2 py-1 text-sm"
|
||||
value={draft}
|
||||
maxLength={16}
|
||||
placeholder={tr("profile.username.placeholder", "User name")}
|
||||
@@ -661,7 +658,7 @@ function UserRow({
|
||||
onClick={doDelete}
|
||||
disabled={deleting}
|
||||
title={tr("profile.username.deleteTitle", "Delete User")}
|
||||
className="shrink-0 rounded-md border border-[var(--color-border)] p-1.5 text-[var(--color-warn)] hover:bg-[var(--color-surface-3)] disabled:opacity-50"
|
||||
className="shrink-0 rounded-full border border-[var(--color-border)] p-1.5 text-[var(--color-warn)] hover:bg-[var(--color-surface-3)] disabled:opacity-50"
|
||||
>
|
||||
{deleting ? <Spinner size={14} /> : <Trash2 size={14} />}
|
||||
</button>
|
||||
@@ -709,12 +706,12 @@ function CreateUserRow({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-[var(--color-accent)]/40 bg-[var(--color-surface)] p-2">
|
||||
<div className="rounded-[var(--radius-card)] border border-[var(--color-accent)]/40 bg-[var(--color-surface)] p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<UserPlus size={14} className="shrink-0 text-[var(--color-accent)]" />
|
||||
<input
|
||||
autoFocus
|
||||
className="min-w-0 flex-1 rounded-md border border-[var(--color-border)] bg-[var(--color-surface-2)] px-2 py-1 text-sm"
|
||||
className="min-w-0 flex-1 rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-2 py-1 text-sm"
|
||||
value={name}
|
||||
maxLength={16}
|
||||
placeholder={tr("profile.username.newPlaceholder", "New user name")}
|
||||
@@ -980,14 +977,14 @@ function SlotRow({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-2">
|
||||
<div className="rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] p-3">
|
||||
{confirmDialog}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-12 shrink-0 text-xs tabular-nums text-[var(--color-muted)]">
|
||||
{tr("profile.username.slot", { n: slot }, `Slot ${slot}`)}
|
||||
</span>
|
||||
<input
|
||||
className="min-w-0 flex-1 rounded-md border border-[var(--color-border)] bg-[var(--color-surface-2)] px-2 py-1 text-sm"
|
||||
className="min-w-0 flex-1 rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-2 py-1 text-sm"
|
||||
value={draft}
|
||||
maxLength={31}
|
||||
onChange={(e) => {
|
||||
@@ -1023,7 +1020,7 @@ function SlotRow({
|
||||
{editingId ? (
|
||||
<>
|
||||
<input
|
||||
className="w-52 rounded-md border border-[var(--color-border)] bg-[var(--color-surface-2)] px-2 py-1 font-mono text-xs"
|
||||
className="w-52 rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-2 py-1 font-mono text-xs"
|
||||
value={idDraft}
|
||||
spellCheck={false}
|
||||
placeholder="0x0123456789abcdef"
|
||||
|
||||
@@ -17,7 +17,7 @@ export function NpSignInCard({ onOpenProfile }: { onOpenProfile: () => void }) {
|
||||
const tr = useTr();
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4"
|
||||
className="surface-panel p-5"
|
||||
data-testid="np-signin-card"
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2 text-sm font-medium">
|
||||
|
||||
@@ -153,7 +153,7 @@ function ReadinessPanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<div className="surface-panel p-5">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<span className="text-sm font-medium">
|
||||
{tr("remotePlay_readiness", undefined, "Before you pair")}
|
||||
@@ -167,7 +167,7 @@ function ReadinessPanel({
|
||||
<ul className="space-y-2">
|
||||
{rows.map((r) => (
|
||||
<li key={r.key} className="flex items-start gap-2 text-sm">
|
||||
<span className={r.ok ? "text-emerald-500" : "text-amber-500"}>
|
||||
<span className={r.ok ? "text-[var(--color-good)]" : "text-[var(--color-warn)]"}>
|
||||
{r.ok ? <Check size={16} /> : <X size={16} />}
|
||||
</span>
|
||||
<span className="flex-1">
|
||||
@@ -367,7 +367,7 @@ export default function RemotePlayScreen() {
|
||||
<NpSignInCard onOpenProfile={() => navigate("/profile")} />
|
||||
|
||||
{/* Request form */}
|
||||
<div className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<div className="surface-panel p-5">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-[var(--color-muted)]">
|
||||
@@ -386,7 +386,7 @@ export default function RemotePlayScreen() {
|
||||
undefined,
|
||||
"auto-detect",
|
||||
)}
|
||||
className="w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-sm text-[var(--color-text)] placeholder:text-[var(--color-muted)] focus:border-[var(--color-accent)] focus:outline-none"
|
||||
className="w-full rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-3 py-2 text-sm text-[var(--color-text)] placeholder:text-[var(--color-muted)] focus:border-[var(--color-accent)] focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -419,7 +419,7 @@ export default function RemotePlayScreen() {
|
||||
|
||||
{/* Status display */}
|
||||
{status && (
|
||||
<div className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4">
|
||||
<div className="surface-panel p-5">
|
||||
<h3 className="mb-3 text-sm font-medium text-[var(--color-text)]">
|
||||
{tr("remotePlay_status", undefined, "Status")}
|
||||
</h3>
|
||||
@@ -534,8 +534,8 @@ export default function RemotePlayScreen() {
|
||||
</dd>
|
||||
</dl>
|
||||
{status.err && (
|
||||
<div className="mt-3 rounded-md border border-[var(--color-error)] bg-[var(--color-error-bg,transparent)] p-3">
|
||||
<p className="text-xs text-[var(--color-error)]">
|
||||
<div className="mt-3 rounded-[var(--radius-card)] border border-[var(--color-bad)] bg-[var(--color-bad-soft)] p-3">
|
||||
<p className="text-xs text-[var(--color-bad)]">
|
||||
{status.err}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function AutoBackupCard() {
|
||||
// Same centred column as the save list below it, so the two line up on a wide window.
|
||||
return (
|
||||
<section
|
||||
className="mx-auto mb-4 max-w-4xl rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4"
|
||||
className="mx-auto mb-4 max-w-4xl rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4"
|
||||
data-testid="auto-save-backup"
|
||||
>
|
||||
<header className="mb-1 flex items-center gap-2">
|
||||
@@ -64,7 +64,7 @@ export default function AutoBackupCard() {
|
||||
max={50}
|
||||
value={keep}
|
||||
onChange={(e) => set({ keep: clampKeep(Number(e.target.value)) })}
|
||||
className="w-20 justify-self-start rounded border border-[var(--color-border)] bg-[var(--color-surface)] px-2 py-1 text-sm text-[var(--color-text)] sm:justify-self-end"
|
||||
className="w-20 justify-self-start rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-2 py-1 text-sm text-[var(--color-text)] sm:justify-self-end"
|
||||
/>
|
||||
</div>
|
||||
<label className="mt-3 flex items-center gap-2 border-t border-[var(--color-border)] pt-3 text-sm">
|
||||
|
||||
@@ -883,7 +883,7 @@ export default function SavesScreen() {
|
||||
|
||||
<div className="mx-auto max-w-4xl space-y-3">
|
||||
{saves?.some((e) => e.kind === "ps4") && (
|
||||
<div className="rounded-lg border border-[var(--color-warn)]/40 bg-[var(--color-warn)]/10 p-3 text-xs text-[var(--color-warn)]">
|
||||
<div className="rounded-[var(--radius-card)] border border-[var(--color-warn)]/40 bg-[var(--color-warn)]/10 p-3 text-xs text-[var(--color-warn)]">
|
||||
<strong>
|
||||
{tr("saves_ps4_warning_title", undefined, "PS4-format saves:")}
|
||||
</strong>{" "}
|
||||
@@ -897,7 +897,7 @@ export default function SavesScreen() {
|
||||
{grouped.map(({ title_id, entries }) => (
|
||||
<section
|
||||
key={title_id}
|
||||
className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] p-4"
|
||||
className="surface-panel p-5"
|
||||
>
|
||||
<SaveGroupHeader
|
||||
host={host}
|
||||
@@ -909,7 +909,7 @@ export default function SavesScreen() {
|
||||
{entries.map((e) => (
|
||||
<li
|
||||
key={e.path}
|
||||
className="flex items-center gap-3 rounded-md bg-[var(--color-surface)] px-2 py-1.5 text-xs"
|
||||
className="flex items-center gap-3 rounded-[var(--radius-field)] bg-[var(--color-surface)] px-2 py-1.5 text-xs"
|
||||
>
|
||||
<span className="rounded bg-[var(--color-surface-3)] px-1.5 py-0.5 text-xs uppercase">
|
||||
{e.kind}
|
||||
|
||||
@@ -343,7 +343,7 @@ function FakelibCorpusSection() {
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="text-[var(--color-muted)] hover:text-[var(--color-danger)]"
|
||||
className="text-[var(--color-muted)] hover:text-[var(--color-bad)]"
|
||||
onClick={() => void remove(set.id, set.label)}
|
||||
>
|
||||
{tr("fakelibs_set_remove", undefined, "Remove")}
|
||||
@@ -885,7 +885,7 @@ export default function SettingsScreen() {
|
||||
"The main preferences (theme, language, engine, save path, upload defaults, accessibility, sidebar) are mirrored to this JSON file. You can edit it by hand; edits take effect the next time you launch the app. It does not hold everything: use Backup / restore to carry all your preferences.",
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 truncate rounded-md bg-[var(--color-surface)] px-2 py-1 font-mono text-xs">
|
||||
<div className="mt-2 truncate rounded-[var(--radius-field)] bg-[var(--color-surface)] px-2 py-1 font-mono text-xs">
|
||||
{cfgPath ?? tr("resolving", undefined, "resolving…")}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1006,7 +1006,7 @@ function SchedulesPanel() {
|
||||
{schedules.map((s) => (
|
||||
<li
|
||||
key={s.id}
|
||||
className="flex items-center gap-2 rounded-md border border-[var(--color-border)] p-2 text-xs"
|
||||
className="flex items-center gap-2 rounded-[var(--radius-card)] border border-[var(--color-border)] p-3 text-xs"
|
||||
>
|
||||
<Toggle
|
||||
className="flex-1"
|
||||
@@ -1050,7 +1050,7 @@ function SchedulesPanel() {
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-2 rounded-md border border-dashed border-[var(--color-border)] p-2 text-xs">
|
||||
<div className="flex flex-wrap items-center gap-2 rounded-[var(--radius-card)] border border-dashed border-[var(--color-border)] p-3 text-xs">
|
||||
<Input
|
||||
type="text"
|
||||
value={labelDraft}
|
||||
@@ -1326,7 +1326,7 @@ function BackupRestorePanel() {
|
||||
type="button"
|
||||
onClick={importBundle}
|
||||
disabled={busy}
|
||||
className="inline-flex items-center gap-1 rounded-md border border-[var(--color-border)] px-3 py-1.5 text-xs hover:bg-[var(--color-surface)] disabled:opacity-50 max-md:min-h-11 max-md:px-4"
|
||||
className="inline-flex items-center gap-1 rounded-[var(--radius-card)] border border-[var(--color-border)] px-3 py-1.5 text-xs hover:bg-[var(--color-surface)] disabled:opacity-50 max-md:min-h-11 max-md:px-4"
|
||||
>
|
||||
{tr("settings_backup_import", undefined, "Import bundle…")}
|
||||
</button>
|
||||
@@ -1400,11 +1400,11 @@ function ThemePicker() {
|
||||
aria-pressed={theme === o.value}
|
||||
className={`flex w-full items-start gap-2 rounded-[var(--radius-card)] border p-3 text-left transition-colors ${
|
||||
theme === o.value
|
||||
? "border-[var(--color-accent)] bg-[var(--color-surface-raised)] shadow-[var(--shadow-1)]"
|
||||
: "border-[var(--color-border)] hover:bg-[var(--color-surface-raised)]"
|
||||
? "border-[var(--glass-edge)] bg-[var(--color-pill)] shadow-[var(--edge-highlight),var(--shadow-1)]"
|
||||
: "border-[var(--color-border-strong)] hover:bg-[var(--color-surface)]"
|
||||
}`}
|
||||
>
|
||||
<span className="mt-0.5">{o.icon}</span>
|
||||
<span className="icon-disc !h-7 !w-7" aria-hidden>{o.icon}</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium">{o.label}</div>
|
||||
<div className="text-xs text-[var(--color-muted)]">
|
||||
@@ -1412,7 +1412,7 @@ function ThemePicker() {
|
||||
</div>
|
||||
</span>
|
||||
{theme === o.value && (
|
||||
<CheckCircle2 size={12} className="text-[var(--color-good)]" />
|
||||
<CheckCircle2 size={14} className="shrink-0 text-[var(--color-accent-bright)]" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
@@ -1441,7 +1441,7 @@ function TextSizePicker() {
|
||||
{/* Stepper row: each button is a percentage of the designed size. The
|
||||
buttons themselves sit OUTSIDE the rem flow (fixed text-[13px]) so
|
||||
they stay tappable even when the user has scaled the app very small. */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<div className="flex flex-wrap gap-2" role="group" aria-label={tr("settings_section_text_size", undefined, "Text size")}>
|
||||
{UI_SCALE_STEPS.map((step) => {
|
||||
const active = Math.abs(step - scale) < 0.001;
|
||||
return (
|
||||
@@ -1450,11 +1450,7 @@ function TextSizePicker() {
|
||||
type="button"
|
||||
onClick={() => setScale(step)}
|
||||
aria-pressed={active}
|
||||
className={`min-w-[3rem] rounded-md border px-2 py-1.5 text-center text-[13px] max-md:min-h-11 ${
|
||||
active
|
||||
? "border-[var(--color-accent)] bg-[var(--color-surface)] font-semibold"
|
||||
: "border-[var(--color-border)] hover:bg-[var(--color-surface)]"
|
||||
}`}
|
||||
className="chip min-h-8 min-w-[3rem] justify-center px-3 py-1.5 text-[13px] max-md:min-h-11"
|
||||
>
|
||||
{uiScaleLabel(step)}
|
||||
{Math.abs(step - 1) < 0.001 && (
|
||||
@@ -1661,7 +1657,7 @@ function ResetPanel() {
|
||||
type="button"
|
||||
onClick={doReset}
|
||||
disabled={busy}
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-[var(--color-bad)] px-3 py-1.5 text-xs text-[var(--color-bad)] hover:bg-[var(--color-bad)] hover:text-[var(--color-accent-contrast)] disabled:opacity-50 max-md:min-h-11 max-md:px-4"
|
||||
className="inline-flex items-center gap-1.5 rounded-[var(--radius-card)] border border-[var(--color-bad)] px-3 py-1.5 text-xs text-[var(--color-bad)] hover:bg-[var(--color-bad)] hover:text-[var(--color-accent-contrast)] disabled:opacity-50 max-md:min-h-11 max-md:px-4"
|
||||
>
|
||||
{busy ? (
|
||||
<Spinner size={12} tone="inherit" />
|
||||
@@ -1747,7 +1743,7 @@ function UpdatesPanel() {
|
||||
/>
|
||||
)}
|
||||
{phase.kind === "downloaded" && (
|
||||
<div className="rounded-md border border-[var(--color-good)] bg-[var(--color-surface)] p-3 text-xs">
|
||||
<div className="rounded-[var(--radius-card)] border border-[var(--color-good)] bg-[var(--color-surface)] p-3 text-xs">
|
||||
<div className="mb-1 font-medium text-[var(--color-good)]">
|
||||
{tr(
|
||||
"update_downloaded_title",
|
||||
@@ -1823,7 +1819,7 @@ function UpdatesPanel() {
|
||||
type="button"
|
||||
onClick={handleCheck}
|
||||
disabled={busy || phase.kind === "downloading"}
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1.5 text-xs hover:bg-[var(--color-surface-3)] disabled:opacity-50 max-md:min-h-11 max-md:px-4"
|
||||
className="inline-flex items-center gap-1.5 rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1.5 text-xs hover:bg-[var(--color-surface-3)] disabled:opacity-50 max-md:min-h-11 max-md:px-4"
|
||||
>
|
||||
<RefreshCw size={12} className={busy ? "animate-spin" : ""} />
|
||||
{tr("check_updates", undefined, "Check for updates")}
|
||||
@@ -1833,7 +1829,7 @@ function UpdatesPanel() {
|
||||
type="button"
|
||||
onClick={handleDownload}
|
||||
disabled={busy}
|
||||
className="inline-flex items-center gap-1.5 rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-[var(--color-accent-contrast)] hover:opacity-90 disabled:opacity-50"
|
||||
className="inline-flex items-center gap-1.5 rounded-full bg-[image:var(--accent-fill)] px-3.5 py-1.5 text-xs font-semibold text-[var(--color-accent-contrast)] shadow-[var(--accent-glow)] hover:brightness-105 disabled:opacity-50"
|
||||
>
|
||||
<ExternalLink size={12} />
|
||||
{hasPlatformBundle
|
||||
@@ -1850,7 +1846,7 @@ function UpdatesPanel() {
|
||||
type="button"
|
||||
onClick={handleDownload}
|
||||
disabled={busy}
|
||||
className="inline-flex items-center gap-1.5 rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-[var(--color-accent-contrast)] hover:opacity-90 disabled:opacity-50"
|
||||
className="inline-flex items-center gap-1.5 rounded-full bg-[image:var(--accent-fill)] px-3.5 py-1.5 text-xs font-semibold text-[var(--color-accent-contrast)] shadow-[var(--accent-glow)] hover:brightness-105 disabled:opacity-50"
|
||||
>
|
||||
<Download size={12} />
|
||||
{tr(
|
||||
@@ -2031,7 +2027,7 @@ function ReleaseNotes({
|
||||
const tr = useTr();
|
||||
if (!notes.trim()) return null;
|
||||
return (
|
||||
<div className="rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-3">
|
||||
<div className="rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] p-3">
|
||||
<div className="mb-1 flex items-center justify-between text-xs uppercase tracking-wide text-[var(--color-muted)]">
|
||||
<span>
|
||||
{tr("whats_new_in", { version }, `What's new in v${version}`)}
|
||||
|
||||
@@ -189,7 +189,7 @@ export default function ShellScreen() {
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-3">
|
||||
<div
|
||||
ref={outputRef}
|
||||
className="flex-1 overflow-auto rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-2 font-mono text-xs"
|
||||
className="flex-1 overflow-auto rounded-[var(--radius-card)] border border-[var(--color-border)] bg-[var(--color-surface)] p-3 font-mono text-xs"
|
||||
>
|
||||
{history.length === 0 ? (
|
||||
<div className="text-[var(--color-muted)]">
|
||||
@@ -226,7 +226,7 @@ export default function ShellScreen() {
|
||||
undefined,
|
||||
"Enter shell command…",
|
||||
)}
|
||||
className="flex-1 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 font-mono text-xs outline-none focus:border-[var(--color-accent)] disabled:opacity-50"
|
||||
className="flex-1 rounded-full border border-[var(--glass-edge)] bg-[var(--color-surface-raised)] px-3 py-2 font-mono text-xs outline-none focus:border-[var(--color-accent)] disabled:opacity-50"
|
||||
autoFocus
|
||||
/>
|
||||
<Button
|
||||
|
||||
Reference in new issue
Block a user