UI refactor
This commit is contained in:
parent
3f675966a6
commit
663f102cd1
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -1,5 +1,6 @@
|
|||
.modal {
|
||||
width: 480px;
|
||||
max-width: 480px;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.loading,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
.modal {
|
||||
width: 480px;
|
||||
max-width: 480px;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.loading {
|
||||
|
|
@ -57,31 +58,10 @@
|
|||
gap: 8px;
|
||||
}
|
||||
|
||||
.saveBtn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
transition: opacity var(--transition);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.saveBtn {
|
||||
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);
|
||||
}
|
||||
.deleteBtn {
|
||||
composes: buttonDanger from '@common/assets/ui.module.css';
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 }) => (
|
||||
|
|
|
|||
Loading…
Reference in New Issue