1
0
Fork 0

UI refactor

This commit is contained in:
Pabloader 2026-04-10 09:31:22 +00:00
parent 3f675966a6
commit 663f102cd1
17 changed files with 156 additions and 298 deletions

View File

@ -26,3 +26,19 @@
scrollbar-width: thin;
scrollbar-color: var(--bg-active) transparent;
}
html {
padding: 0;
margin: 0;
}
body {
background: var(--bg);
color: var(--text);
font-family: 'Georgia', serif;
font-size: 14px;
line-height: 1.5;
width: 100dvw;
height: 100dvh;
overflow: hidden;
}

View File

@ -7,7 +7,7 @@
max-width: 100%;
height: 100%;
max-height: 100%;
display: flex;
display: none;
align-items: center;
justify-content: center;
background: transparent;
@ -17,6 +17,11 @@
&::backdrop {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
}
&[open] {
display: flex;
}
}
@ -59,6 +64,7 @@
border: none;
border-radius: var(--radius);
cursor: pointer;
padding: 0;
&:hover {
color: var(--text);

View File

@ -2,7 +2,7 @@
/* ─── Form Fields ─────────────────────────────────────────── */
.input {
.input, input, textarea {
width: 100%;
padding: 8px 12px;
background: var(--bg);
@ -37,7 +37,7 @@
/* ─── Buttons ─────────────────────────────────────────────── */
.button {
.button, button {
padding: 8px 16px;
border-radius: var(--radius);
font-size: 14px;
@ -46,6 +46,7 @@
border: none;
background: transparent;
transition: all var(--transition);
color: var(--text);
}
.buttonPrimary {
@ -59,6 +60,17 @@
}
}
.buttonDanger {
composes: button;
background: var(--danger, #dc2626);
color: var(--bg);
font-weight: 500;
&:hover {
background: var(--danger-alt, #b91c1c);
}
}
.buttonSecondary {
composes: button;
border: 1px solid var(--border);

View File

@ -23,6 +23,7 @@ export const Modal = ({ children, open, title, onClose, sidebar, footer, ['class
if (open) {
ref.current?.showModal();
} else {
console.log(ref.current);
ref.current?.close();
}
}, [open]);

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,5 +1,6 @@
.modal {
width: 480px;
max-width: 480px;
height: fit-content;
}
.loading,

View File

@ -1,5 +1,6 @@
.modal {
width: 480px;
max-width: 480px;
height: fit-content;
}
.loading {
@ -58,30 +59,9 @@
}
.saveBtn {
padding: 8px 16px;
border: none;
border-radius: var(--radius);
background: var(--accent);
color: var(--bg);
transition: opacity var(--transition);
&:hover {
opacity: 0.85;
}
composes: buttonSecondary from '@common/assets/ui.module.css';
}
.deleteBtn {
padding: 8px 16px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: transparent;
color: var(--text-muted);
margin-left: auto;
transition: all var(--transition);
&:hover {
background: var(--accent);
border-color: var(--accent);
color: var(--bg);
}
composes: buttonDanger from '@common/assets/ui.module.css';
}

View File

@ -1,72 +0,0 @@
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
max-height: 80vh;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.title {
font-size: 16px;
font-weight: bold;
color: var(--text);
}
.closeButton {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
color: var(--text-muted);
background: transparent;
border: none;
border-radius: var(--radius);
cursor: pointer;
&:hover {
color: var(--text);
background: var(--bg-hover);
}
}
.body {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 14px;
min-height: 0;
}
.footer {
padding: 12px 16px;
border-top: 1px solid var(--border);
display: flex;
flex-direction: row;
gap: 8px;
flex-shrink: 0;
align-items: center;
}

View File

@ -1,5 +1,6 @@
.modal {
width: 380px;
max-width: 380px;
height: fit-content;
}
.label {
@ -15,27 +16,9 @@
}
.saveBtn {
padding: 8px 16px;
border: none;
border-radius: var(--radius);
background: var(--accent);
color: var(--bg);
transition: opacity var(--transition);
&:hover {
opacity: 0.85;
}
composes: buttonSecondary from '@common/assets/ui.module.css';
}
.clearBtn {
padding: 8px 16px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: transparent;
color: var(--text);
transition: background var(--transition);
&:hover {
background: var(--bg-hover);
}
composes: buttonSecondary from '@common/assets/ui.module.css';
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -1,50 +0,0 @@
@import "@common/assets/global.css";
body {
background: var(--bg);
color: var(--text);
font-family: 'Georgia', serif;
font-size: 14px;
line-height: 1.5;
width: 100dvw;
height: 100dvh;
overflow: hidden;
}
button {
cursor: pointer;
background: none;
border: none;
color: var(--text-muted);
font-family: inherit;
font-size: inherit;
transition: color var(--transition), background var(--transition);
border-radius: var(--radius);
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 4px;
padding: 4px 8px;
&:hover {
color: var(--text);
background: var(--bg-hover);
}
}
input,
textarea {
background: var(--bg-active);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-family: inherit;
font-size: inherit;
padding: 6px 10px;
outline: none;
transition: border-color var(--transition);
&:focus {
border-color: var(--accent);
}
}

View File

@ -1,10 +1,8 @@
import clsx from "clsx";
import { useEffect, useState } from "preact/hooks";
import { useHordeState } from "../../contexts/state";
import { fetchLeaderboard, type LeaderboardEntry } from "../../utils/api";
import { formatNumber, formatTime } from "@common/utils";
import { Modal } from "@common/components/Modal";
import modalStyles from "../../assets/modal.module.css";
import styles from "../../assets/leaderboard-modal.module.css";
interface Props {
@ -94,55 +92,49 @@ export const LeaderboardModal = ({ open, onClose }: Props) => {
return () => { cancelled = true; };
}, [open, user?.username]);
const nextRow = rows.length >= 2 ? rows[rows.length - 2] : null;
const footer = user && nextRow?.diff && nextRow.diff > 0 && kudosPerHour > 0
? (() => {
const secs = Math.ceil((nextRow.diff / kudosPerHour) * 3600);
return (
<span class={styles.nextPlace}>
Time to #{nextRow.rank}: <strong>{formatTime(secs)}</strong>
<span class={styles.nextPlaceMeta}> ({formatNumber(Math.round(kudosPerHour))} kudos/h)</span>
</span>
);
})()
: undefined;
return (
<Modal open={open} onClose={onClose} class={clsx(modalStyles.modal, styles.modal)}>
<div class={modalStyles.header}>
<span class={modalStyles.title}>Leaderboard</span>
</div>
<div class={modalStyles.body}>
{loading && <p class={styles.loading}>Loading</p>}
{error && <p class={styles.error}>{error}</p>}
{!loading && !error && rows.length > 0 && (
<table class={styles.table}>
<thead>
<tr>
<th>#</th>
<th>Username</th>
<th>Kudos</th>
{user && <th>Diff</th>}
<Modal open={open} onClose={onClose} title="Leaderboard" class={styles.modal} footer={footer}>
{loading && <p class={styles.loading}>Loading</p>}
{error && <p class={styles.error}>{error}</p>}
{!loading && !error && rows.length > 0 && (
<table class={styles.table}>
<thead>
<tr>
<th>#</th>
<th>Username</th>
<th>Kudos</th>
{user && <th>Diff</th>}
</tr>
</thead>
<tbody>
{rows.map(r => (
<tr key={r.rank} class={r.username === user?.username ? styles.selfRow : undefined}>
<td>{r.rank}</td>
<td>{r.username}</td>
<td>{formatNumber(r.kudos)}</td>
{user && (
<td class={r.diff !== undefined ? (r.diff > 0 ? styles.above : styles.below) : undefined}>
{r.diff !== undefined ? `+${formatNumber(r.diff)}` : '—'}
</td>
)}
</tr>
</thead>
<tbody>
{rows.map(r => (
<tr key={r.rank} class={r.username === user?.username ? styles.selfRow : undefined}>
<td>{r.rank}</td>
<td>{r.username}</td>
<td>{formatNumber(r.kudos)}</td>
{user && (
<td class={clsx(r.diff !== undefined && (r.diff > 0 ? styles.above : styles.below))}>
{r.diff !== undefined ? `+${formatNumber(r.diff)}` : '—'}
</td>
)}
</tr>
))}
</tbody>
</table>
)}
</div>
{(() => {
if (!user || rows.length < 2 || kudosPerHour <= 0) return null;
const nextRow = rows[rows.length - 2];
if (!nextRow.diff || nextRow.diff <= 0) return null;
const secs = Math.ceil((nextRow.diff / kudosPerHour) * 3600);
return (
<div class={modalStyles.footer}>
<span class={styles.nextPlace}>
Time to #{nextRow.rank}: <strong>{formatTime(secs)}</strong>
<span class={styles.nextPlaceMeta}> ({formatNumber(Math.round(kudosPerHour))} kudos/h)</span>
</span>
</div>
);
})()}
))}
</tbody>
</table>
)}
</Modal>
);
};

View File

@ -1,9 +1,7 @@
import clsx from "clsx";
import { useEffect, useState } from "preact/hooks";
import { useHordeState } from "../../contexts/state";
import { fetchWorker, updateWorker, deleteWorker, type WorkerData } from "../../utils/api";
import { Modal } from "@common/components/Modal";
import modalStyles from "../../assets/modal.module.css";
import styles from "../../assets/manage-workers-modal.module.css";
interface Props {
@ -80,53 +78,48 @@ export const ManageWorkersModal = ({ open, onClose }: Props) => {
};
return (
<Modal open={open} onClose={onClose} class={clsx(modalStyles.modal, styles.modal)}>
<div class={modalStyles.header}>
<span class={modalStyles.title}>Manage Workers</span>
</div>
<div class={modalStyles.body}>
{loading && <p class={styles.loading}>Loading</p>}
{!loading && user && workerDetails.map(w => {
const edit = edits[w.id] ?? { name: w.name, info: w.info ?? '', maintenance_mode: w.maintenance_mode };
return (
<div key={w.id} class={styles.workerForm}>
<div class={styles.workerName}>{w.name}</div>
<label class={styles.label}>
<span>Name</span>
<input
value={edit.name}
onInput={e => setEdit(w.id, { name: (e.target as HTMLInputElement).value })}
/>
</label>
<label class={styles.label}>
<span>Info</span>
<textarea
class={styles.textarea}
value={edit.info}
onInput={e => setEdit(w.id, { info: (e.target as HTMLTextAreaElement).value })}
rows={3}
/>
</label>
<label class={styles.checkboxLabel}>
<input
type="checkbox"
checked={edit.maintenance_mode}
onChange={e => setEdit(w.id, { maintenance_mode: (e.target as HTMLInputElement).checked })}
/>
<span>Maintenance mode</span>
</label>
<div class={styles.formActions}>
<button class={styles.saveBtn} onClick={() => save(w.id)} disabled={saving[w.id]}>
{saving[w.id] ? 'Saving…' : 'Save'}
</button>
<button class={styles.deleteBtn} onClick={() => confirmDelete(w.id)} disabled={deleting === w.id}>
{deleting === w.id ? 'Deleting…' : 'Delete'}
</button>
</div>
<Modal open={open && Boolean(user)} onClose={onClose} title="Manage Workers" class={styles.modal}>
{loading && <p class={styles.loading}>Loading</p>}
{!loading && user && workerDetails.map(w => {
const edit = edits[w.id] ?? { name: w.name, info: w.info ?? '', maintenance_mode: w.maintenance_mode };
return (
<div key={w.id} class={styles.workerForm}>
<div class={styles.workerName}>{w.name}</div>
<label class={styles.label}>
<span>Name</span>
<input
value={edit.name}
onInput={e => setEdit(w.id, { name: (e.target as HTMLInputElement).value })}
/>
</label>
<label class={styles.label}>
<span>Info</span>
<textarea
class={styles.textarea}
value={edit.info}
onInput={e => setEdit(w.id, { info: (e.target as HTMLTextAreaElement).value })}
rows={3}
/>
</label>
<label class={styles.checkboxLabel}>
<input
type="checkbox"
checked={edit.maintenance_mode}
onChange={e => setEdit(w.id, { maintenance_mode: (e.target as HTMLInputElement).checked })}
/>
<span>Maintenance mode</span>
</label>
<div class={styles.formActions}>
<button class={styles.saveBtn} onClick={() => save(w.id)} disabled={saving[w.id]}>
{saving[w.id] ? 'Saving…' : 'Save'}
</button>
<button class={styles.deleteBtn} onClick={() => confirmDelete(w.id)} disabled={deleting === w.id}>
{deleting === w.id ? 'Deleting…' : 'Delete'}
</button>
</div>
);
})}
</div>
</div>
);
})}
</Modal>
);
};

View File

@ -1,9 +1,7 @@
import clsx from "clsx";
import { useEffect } from "preact/hooks";
import { useHordeState } from "../../contexts/state";
import { useInputState } from "@common/hooks/useInputState";
import { Modal } from "@common/components/Modal";
import modalStyles from "../../assets/modal.module.css";
import styles from "../../assets/options-modal.module.css";
interface Props {
@ -31,27 +29,27 @@ export const OptionsModal = ({ open, onClose }: Props) => {
};
return (
<Modal open={open} onClose={onClose} class={clsx(modalStyles.modal, styles.modal)}>
<div class={modalStyles.header}>
<span class={modalStyles.title}>Options</span>
</div>
<div class={modalStyles.body}>
<label class={styles.label}>
<span>API Key</span>
<input
type="password"
value={draft}
onInput={setDraft}
onKeyDown={e => { if (e.key === 'Enter') save(); }}
placeholder="Enter your AI Horde API key"
autoFocus
/>
</label>
</div>
<div class={modalStyles.footer}>
<Modal
open={open}
onClose={onClose}
title="Options"
class={styles.modal}
footer={<>
<button class={styles.saveBtn} onClick={save}>Save</button>
{state.apiKey && <button class={styles.clearBtn} onClick={clear}>Clear</button>}
</div>
</>}
>
<label class={styles.label}>
<span>API Key</span>
<input
type="password"
value={draft}
onInput={setDraft}
onKeyDown={e => { if (e.key === 'Enter') save(); }}
placeholder="Enter your AI Horde API key"
autoFocus
/>
</label>
</Modal>
);
};

View File

@ -2,8 +2,6 @@ import { render } from "preact";
import { App } from "./components/app";
import { HordeStateProvider } from "./contexts/state";
import './assets/style.css';
export default function main() {
render(
<HordeStateProvider>

View File

@ -322,9 +322,8 @@ export const Menu = ({ visible }: { visible: boolean }) => {
<Settings size={16} /> Settings
</button>
</div>
{isSettingsOpen.value && (
<SettingsModal onClose={isSettingsOpen.toggle} />
)}
<SettingsModal open={isSettingsOpen.value} onClose={isSettingsOpen.setFalse} />
</div>
);
};

View File

@ -10,6 +10,7 @@ import { SystemInstructionSettings } from "./settings/system-instruction";
import { UserSettings } from "./settings/user";
interface Props {
open: boolean;
onClose: () => void;
}
@ -24,12 +25,12 @@ const TABS: { id: Tab; label: string }[] = [
{ id: "banned-tokens", label: "Banned Tokens" },
];
export const SettingsModal = ({ onClose }: Props) => {
export const SettingsModal = ({ open, onClose }: Props) => {
const [activeTab, setActiveTab] = useState<Tab>("connection");
return (
<Modal
open
open={open}
onClose={onClose}
title="Settings"
sidebar={TABS.map(({ id, label }) => (