1
0
Fork 0
This commit is contained in:
Pabloader 2026-04-10 08:43:14 +00:00
parent 23bc808cca
commit 810035547c
13 changed files with 477 additions and 572 deletions

View File

@ -18,7 +18,7 @@
"preact": "10.22.0", "preact": "10.22.0",
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "^1.3.11",
"@types/html-minifier": "4.0.5", "@types/html-minifier": "4.0.5",
"@types/inquirer": "9.0.7", "@types/inquirer": "9.0.7",
"@types/web-bluetooth": "0.0.21", "@types/web-bluetooth": "0.0.21",

View File

@ -22,7 +22,7 @@
"preact": "10.22.0" "preact": "10.22.0"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "^1.3.11",
"@types/html-minifier": "4.0.5", "@types/html-minifier": "4.0.5",
"@types/inquirer": "9.0.7", "@types/inquirer": "9.0.7",
"@types/web-bluetooth": "0.0.21", "@types/web-bluetooth": "0.0.21",

View File

@ -0,0 +1,255 @@
@import "./global.css";
/* ─── Form Fields ─────────────────────────────────────────── */
.input {
width: 100%;
padding: 8px 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 14px;
color: var(--text);
font-family: inherit;
&:focus {
outline: none;
border-color: var(--accent);
}
}
.textarea {
composes: input;
resize: vertical;
box-sizing: border-box;
min-height: 80px;
}
.select {
composes: input;
cursor: pointer;
}
.inputRow {
display: flex;
gap: 8px;
}
/* ─── Buttons ─────────────────────────────────────────────── */
.button {
padding: 8px 16px;
border-radius: var(--radius);
font-size: 14px;
font-family: inherit;
cursor: pointer;
border: none;
background: transparent;
transition: all var(--transition);
}
.buttonPrimary {
composes: button;
background: var(--accent);
color: var(--bg);
font-weight: 500;
&:hover {
background: var(--accent-alt);
}
}
.buttonSecondary {
composes: button;
border: 1px solid var(--border);
color: var(--text);
&:hover {
background: var(--bg-hover);
}
}
.buttonSmall {
padding: 4px 10px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 12px;
font-family: inherit;
color: var(--text);
cursor: pointer;
transition: all var(--transition);
&:hover {
border-color: var(--accent);
color: var(--accent);
}
}
.iconButton {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: transparent;
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-muted);
font-size: 16px;
cursor: pointer;
transition: all var(--transition);
flex-shrink: 0;
&:hover:not(:disabled) {
border-color: var(--accent);
color: var(--accent);
}
&:disabled {
opacity: 0.3;
cursor: not-allowed;
}
}
.deleteButton {
composes: iconButton;
&:hover:not(:disabled) {
background: var(--danger, var(--accent));
border-color: var(--danger, var(--accent));
color: var(--bg);
}
}
.confirmButton {
padding: 4px 10px;
border: 1px solid var(--accent);
border-radius: var(--radius);
font-size: 12px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
background: var(--accent);
color: var(--bg);
transition: all var(--transition);
&:hover {
background: var(--bg);
color: var(--accent);
}
}
.cancelButton {
padding: 4px 10px;
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 12px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
background: transparent;
color: var(--text-muted);
transition: all var(--transition);
&:hover {
background: var(--bg-hover);
color: var(--text);
}
}
/* ─── Form Layout ─────────────────────────────────────────── */
.form {
display: flex;
flex-direction: column;
gap: 16px;
flex: 1;
}
.formGroup {
display: flex;
flex-direction: column;
gap: 4px;
}
.label {
display: block;
font-weight: bold;
margin-bottom: 4px;
}
/* ─── Cards ───────────────────────────────────────────────── */
.card {
background: var(--bg-secondary);
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.cardHeader {
display: flex;
align-items: center;
gap: 12px;
}
/* ─── Editor Section Header ───────────────────────────────── */
.editorHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
}
/* ─── Badges ──────────────────────────────────────────────── */
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: var(--bg-active);
border-radius: 16px;
font-size: 13px;
color: var(--text);
}
.badgeRemove {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 0;
background: transparent;
border: none;
border-radius: 50%;
color: var(--text-muted);
cursor: pointer;
font-size: 14px;
line-height: 1;
&:hover {
background: var(--danger, var(--accent));
color: var(--bg);
}
}
/* ─── Utilities ───────────────────────────────────────────── */
.empty {
color: var(--text-muted);
font-style: italic;
font-size: 14px;
}
.divider {
height: 1px;
background: var(--border);
margin: 16px 0;
}

View File

@ -58,10 +58,24 @@
} }
.saveBtn { .saveBtn {
color: var(--accent-alt) !important; padding: 8px 16px;
border: none;
border-radius: var(--radius);
background: var(--accent);
color: var(--bg);
transition: opacity var(--transition);
&:hover { opacity: 0.85; }
} }
.deleteBtn { .deleteBtn {
color: var(--accent) !important; padding: 8px 16px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: transparent;
color: var(--text-muted);
margin-left: auto; margin-left: auto;
transition: all var(--transition);
&:hover { background: var(--accent); border-color: var(--accent); color: var(--bg); }
} }

View File

@ -15,5 +15,23 @@
} }
.saveBtn { .saveBtn {
color: var(--accent-alt) !important; padding: 8px 16px;
border: none;
border-radius: var(--radius);
background: var(--accent);
color: var(--bg);
transition: opacity var(--transition);
&:hover { opacity: 0.85; }
}
.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); }
} }

View File

@ -1,8 +1,9 @@
import clsx from "clsx";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { X } from "lucide-preact";
import { useHordeState } from "../../contexts/state"; import { useHordeState } from "../../contexts/state";
import { fetchLeaderboard, type LeaderboardEntry } from "../../utils/api"; import { fetchLeaderboard, type LeaderboardEntry } from "../../utils/api";
import { formatNumber, formatTime } from "@common/utils"; import { formatNumber, formatTime } from "@common/utils";
import { Modal } from "@common/components/modal/Modal";
import modalStyles from "../../assets/modal.module.css"; import modalStyles from "../../assets/modal.module.css";
import styles from "../../assets/leaderboard-modal.module.css"; import styles from "../../assets/leaderboard-modal.module.css";
@ -93,67 +94,55 @@ export const LeaderboardModal = ({ open, onClose }: Props) => {
return () => { cancelled = true; }; return () => { cancelled = true; };
}, [open, user?.username]); }, [open, user?.username]);
useEffect(() => {
if (!open) return;
const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [open]);
if (!open) return null;
return ( return (
<div class={modalStyles.overlay} onMouseDown={e => { if (e.target === e.currentTarget) onClose(); }}> <Modal open={open} onClose={onClose} class={clsx(modalStyles.modal, styles.modal)}>
<div class={`${modalStyles.modal} ${styles.modal}`}> <div class={modalStyles.header}>
<div class={modalStyles.header}> <span class={modalStyles.title}>Leaderboard</span>
<span class={modalStyles.title}>Leaderboard</span>
<button class={modalStyles.closeButton} onClick={onClose}><X size={16} /></button>
</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>}
</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>
))}
</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>
);
})()}
</div> </div>
</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>}
</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>
);
})()}
</Modal>
); );
}; };

View File

@ -1,7 +1,8 @@
import clsx from "clsx";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { X } from "lucide-preact";
import { useHordeState } from "../../contexts/state"; import { useHordeState } from "../../contexts/state";
import { fetchWorker, updateWorker, deleteWorker, type WorkerData } from "../../utils/api"; import { fetchWorker, updateWorker, deleteWorker, type WorkerData } from "../../utils/api";
import { Modal } from "@common/components/modal/Modal";
import modalStyles from "../../assets/modal.module.css"; import modalStyles from "../../assets/modal.module.css";
import styles from "../../assets/manage-workers-modal.module.css"; import styles from "../../assets/manage-workers-modal.module.css";
@ -47,15 +48,6 @@ export const ManageWorkersModal = ({ open, onClose }: Props) => {
return () => { cancelled = true; }; return () => { cancelled = true; };
}, [open, user?.worker_ids.join(',')]); }, [open, user?.worker_ids.join(',')]);
useEffect(() => {
if (!open) return;
const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [open]);
if (!open || !user) return null;
const setEdit = (id: string, patch: Partial<WorkerEdit>) => { const setEdit = (id: string, patch: Partial<WorkerEdit>) => {
setEdits(prev => ({ ...prev, [id]: { ...prev[id], ...patch } })); setEdits(prev => ({ ...prev, [id]: { ...prev[id], ...patch } }));
}; };
@ -79,7 +71,7 @@ export const ManageWorkersModal = ({ open, onClose }: Props) => {
try { try {
await deleteWorker(id, apiKey); await deleteWorker(id, apiKey);
setWorkerDetails(prev => prev.filter(w => w.id !== id)); setWorkerDetails(prev => prev.filter(w => w.id !== id));
dispatch({ type: 'SET_USER', user: { ...user, worker_ids: user.worker_ids.filter(i => i !== id) } }); dispatch({ type: 'SET_USER', user: { ...user!, worker_ids: user!.worker_ids.filter(i => i !== id) } });
} catch (e) { } catch (e) {
console.error('[horde] delete worker error:', e); console.error('[horde] delete worker error:', e);
} finally { } finally {
@ -88,56 +80,53 @@ export const ManageWorkersModal = ({ open, onClose }: Props) => {
}; };
return ( return (
<div class={modalStyles.overlay} onMouseDown={e => { if (e.target === e.currentTarget) onClose(); }}> <Modal open={open} onClose={onClose} class={clsx(modalStyles.modal, styles.modal)}>
<div class={`${modalStyles.modal} ${styles.modal}`}> <div class={modalStyles.header}>
<div class={modalStyles.header}> <span class={modalStyles.title}>Manage Workers</span>
<span class={modalStyles.title}>Manage Workers</span>
<button class={modalStyles.closeButton} onClick={onClose}><X size={16} /></button>
</div>
<div class={modalStyles.body}>
{loading && <p class={styles.loading}>Loading</p>}
{!loading && 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>
</div> </div>
</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>
</div>
);
})}
</div>
</Modal>
); );
}; };

View File

@ -1,6 +1,8 @@
import { useEffect, useState } from "preact/hooks"; import clsx from "clsx";
import { X } from "lucide-preact"; import { useEffect } from "preact/hooks";
import { useHordeState } from "../../contexts/state"; import { useHordeState } from "../../contexts/state";
import { useInputState } from "@common/hooks/useInputState";
import { Modal } from "@common/components/modal/Modal";
import modalStyles from "../../assets/modal.module.css"; import modalStyles from "../../assets/modal.module.css";
import styles from "../../assets/options-modal.module.css"; import styles from "../../assets/options-modal.module.css";
@ -11,21 +13,12 @@ interface Props {
export const OptionsModal = ({ open, onClose }: Props) => { export const OptionsModal = ({ open, onClose }: Props) => {
const { state, dispatch } = useHordeState(); const { state, dispatch } = useHordeState();
const [draft, setDraft] = useState(state.apiKey); const [draft, setDraft] = useInputState(state.apiKey);
useEffect(() => { useEffect(() => {
if (open) setDraft(state.apiKey); if (open) setDraft(state.apiKey);
}, [open]); }, [open]);
useEffect(() => {
if (!open) return;
const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [open]);
if (!open) return null;
const save = () => { const save = () => {
dispatch({ type: 'SET_API_KEY', apiKey: draft.trim() }); dispatch({ type: 'SET_API_KEY', apiKey: draft.trim() });
onClose(); onClose();
@ -38,30 +31,27 @@ export const OptionsModal = ({ open, onClose }: Props) => {
}; };
return ( return (
<div class={modalStyles.overlay} onMouseDown={e => { if (e.target === e.currentTarget) onClose(); }}> <Modal open={open} onClose={onClose} class={clsx(modalStyles.modal, styles.modal)}>
<div class={`${modalStyles.modal} ${styles.modal}`}> <div class={modalStyles.header}>
<div class={modalStyles.header}> <span class={modalStyles.title}>Options</span>
<span class={modalStyles.title}>Options</span>
<button class={modalStyles.closeButton} onClick={onClose}><X size={16} /></button>
</div>
<div class={modalStyles.body}>
<label class={styles.label}>
<span>API Key</span>
<input
type="password"
value={draft}
onInput={e => setDraft((e.target as HTMLInputElement).value)}
onKeyDown={e => { if (e.key === 'Enter') save(); }}
placeholder="Enter your AI Horde API key"
autoFocus
/>
</label>
</div>
<div class={modalStyles.footer}>
<button class={styles.saveBtn} onClick={save}>Save</button>
{state.apiKey && <button onClick={clear}>Clear</button>}
</div>
</div> </div>
</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}>
<button class={styles.saveBtn} onClick={save}>Save</button>
{state.apiKey && <button class={styles.clearBtn} onClick={clear}>Clear</button>}
</div>
</Modal>
); );
}; };

View File

@ -7,18 +7,11 @@
} }
.empty { .empty {
color: var(--text-muted); composes: empty from '@common/assets/ui.module.css';
font-style: italic;
font-size: 14px;
} }
.chapterCard { .chapterCard {
background: var(--bg-secondary); composes: card from '@common/assets/ui.module.css';
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
} }
.chapterTitle { .chapterTitle {
@ -59,24 +52,9 @@
} }
.summaryEditable { .summaryEditable {
width: 100%; composes: textarea from '@common/assets/ui.module.css';
padding: 10px 12px; padding: 10px 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
font-size: 14px;
color: var(--text);
font-family: inherit;
box-sizing: border-box;
min-height: 80px;
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
resize: vertical;
&:focus {
outline: none;
border-color: var(--accent);
}
} }

View File

@ -7,11 +7,7 @@
} }
.header { .header {
display: flex; composes: editorHeader from '@common/assets/ui.module.css';
justify-content: space-between;
align-items: center;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
} }
.header h2 { .header h2 {
@ -22,19 +18,8 @@
} }
.addButton { .addButton {
padding: 8px 16px; composes: buttonPrimary from '@common/assets/ui.module.css';
background: var(--accent);
color: var(--bg);
border: none;
border-radius: 6px; border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s;
&:hover {
background: var(--accent-alt);
}
} }
.deleteConfirm { .deleteConfirm {
@ -53,37 +38,12 @@
} }
} }
.confirmButton,
.cancelButton {
padding: 4px 10px;
border: 1px solid transparent;
border-radius: var(--radius);
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all var(--transition);
}
.confirmButton { .confirmButton {
background: var(--accent); composes: confirmButton from '@common/assets/ui.module.css';
color: var(--bg);
border-color: var(--accent);
&:hover {
background: var(--bg);
color: var(--accent);
}
} }
.cancelButton { .cancelButton {
background: transparent; composes: cancelButton from '@common/assets/ui.module.css';
color: var(--text-muted);
border-color: var(--border);
&:hover {
background: var(--bg-hover);
color: var(--text);
}
} }
.list { .list {
@ -95,24 +55,15 @@
} }
.empty { .empty {
color: var(--text-muted); composes: empty from '@common/assets/ui.module.css';
font-style: italic;
font-size: 14px;
} }
.characterCard { .characterCard {
background: var(--bg-secondary); composes: card from '@common/assets/ui.module.css';
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
} }
.cardHeader { .cardHeader {
display: flex; composes: cardHeader from '@common/assets/ui.module.css';
align-items: center;
gap: 12px;
} }
.nameInput { .nameInput {
@ -133,29 +84,15 @@
} }
.deleteButton { .deleteButton {
composes: deleteButton from '@common/assets/ui.module.css';
width: 32px; width: 32px;
height: 32px; height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
color: var(--text-muted);
font-size: 20px; font-size: 20px;
cursor: pointer;
transition: all var(--transition);
&:hover {
background: var(--accent);
border-color: var(--accent);
color: var(--bg);
}
} }
.field { .field {
display: flex; composes: formGroup from '@common/assets/ui.module.css';
flex-direction: column;
gap: 8px; gap: 8px;
} }
@ -224,70 +161,23 @@
} }
.generateButton { .generateButton {
padding: 4px 10px; composes: buttonSmall from '@common/assets/ui.module.css';
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
font-size: 12px;
color: var(--text);
cursor: pointer;
transition: all var(--transition);
&:hover {
border-color: var(--accent);
color: var(--accent);
}
} }
.textarea { .textarea {
width: 100%; composes: textarea from '@common/assets/ui.module.css';
padding: 10px 12px; padding: 10px 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
font-size: 14px;
color: var(--text);
font-family: inherit;
resize: vertical;
box-sizing: border-box;
&:focus {
outline: none;
border-color: var(--accent);
}
} }
.select { .select {
width: 100%; composes: select from '@common/assets/ui.module.css';
padding: 10px 12px; padding: 10px 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
font-size: 14px;
color: var(--text);
font-family: inherit;
cursor: pointer;
&:focus {
outline: none;
border-color: var(--accent);
}
} }
.smallButton { .smallButton {
padding: 4px 10px; composes: buttonSmall from '@common/assets/ui.module.css';
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
font-size: 12px;
color: var(--text);
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: var(--accent);
color: var(--accent);
}
} }
.nicknames { .nicknames {
@ -303,35 +193,11 @@
} }
.badge { .badge {
display: inline-flex; composes: badge from '@common/assets/ui.module.css';
align-items: center;
gap: 6px;
padding: 4px 10px;
background: var(--bg-active);
border-radius: 16px;
font-size: 13px;
color: var(--text);
} }
.badgeRemove { .badgeRemove {
display: flex; composes: badgeRemove from '@common/assets/ui.module.css';
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 0;
background: transparent;
border: none;
border-radius: 50%;
color: var(--text-muted);
cursor: pointer;
font-size: 14px;
line-height: 1;
&:hover {
background: var(--danger);
color: var(--bg);
}
} }
.relations { .relations {

View File

@ -7,11 +7,7 @@
} }
.header { .header {
display: flex; composes: editorHeader from '@common/assets/ui.module.css';
justify-content: space-between;
align-items: center;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
} }
.header h2 { .header h2 {
@ -22,19 +18,8 @@
} }
.addButton { .addButton {
padding: 8px 16px; composes: buttonPrimary from '@common/assets/ui.module.css';
background: var(--accent);
color: var(--bg);
border: none;
border-radius: 6px; border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s;
&:hover {
background: var(--accent-alt);
}
} }
.deleteConfirm { .deleteConfirm {
@ -53,37 +38,12 @@
} }
} }
.confirmButton,
.cancelButton {
padding: 4px 10px;
border: 1px solid transparent;
border-radius: var(--radius);
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all var(--transition);
}
.confirmButton { .confirmButton {
background: var(--accent); composes: confirmButton from '@common/assets/ui.module.css';
color: var(--bg);
border-color: var(--accent);
&:hover {
background: var(--bg);
color: var(--accent);
}
} }
.cancelButton { .cancelButton {
background: transparent; composes: cancelButton from '@common/assets/ui.module.css';
color: var(--text-muted);
border-color: var(--border);
&:hover {
background: var(--bg-hover);
color: var(--text);
}
} }
.list { .list {
@ -95,24 +55,15 @@
} }
.empty { .empty {
color: var(--text-muted); composes: empty from '@common/assets/ui.module.css';
font-style: italic;
font-size: 14px;
} }
.locationCard { .locationCard {
background: var(--bg-secondary); composes: card from '@common/assets/ui.module.css';
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
} }
.cardHeader { .cardHeader {
display: flex; composes: cardHeader from '@common/assets/ui.module.css';
align-items: center;
gap: 12px;
} }
.nameInput { .nameInput {
@ -133,29 +84,15 @@
} }
.deleteButton { .deleteButton {
composes: deleteButton from '@common/assets/ui.module.css';
width: 32px; width: 32px;
height: 32px; height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
color: var(--text-muted);
font-size: 20px; font-size: 20px;
cursor: pointer;
transition: all var(--transition);
&:hover {
background: var(--accent);
border-color: var(--accent);
color: var(--bg);
}
} }
.field { .field {
display: flex; composes: formGroup from '@common/assets/ui.module.css';
flex-direction: column;
gap: 8px; gap: 8px;
} }
@ -174,52 +111,17 @@
} }
.select { .select {
width: 100%; composes: select from '@common/assets/ui.module.css';
padding: 10px 12px; padding: 10px 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
font-size: 14px;
color: var(--text);
font-family: inherit;
cursor: pointer;
&:focus {
outline: none;
border-color: var(--accent);
}
} }
.generateButton { .generateButton {
padding: 4px 10px; composes: buttonSmall from '@common/assets/ui.module.css';
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
font-size: 12px;
color: var(--text);
cursor: pointer;
transition: all var(--transition);
&:hover {
border-color: var(--accent);
color: var(--accent);
}
} }
.textarea { .textarea {
width: 100%; composes: textarea from '@common/assets/ui.module.css';
padding: 10px 12px; padding: 10px 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
font-size: 14px;
color: var(--text);
font-family: inherit;
resize: vertical;
box-sizing: border-box;
&:focus {
outline: none;
border-color: var(--accent);
}
} }

View File

@ -7,11 +7,7 @@
} }
.header { .header {
display: flex; composes: editorHeader from '@common/assets/ui.module.css';
justify-content: space-between;
align-items: center;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
} }
.header h2 { .header h2 {
@ -44,19 +40,8 @@
} }
.addButton { .addButton {
padding: 8px 16px; composes: buttonPrimary from '@common/assets/ui.module.css';
background: var(--accent);
color: var(--bg);
border: none;
border-radius: 6px; border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s;
&:hover {
background: var(--accent-alt);
}
} }
.list { .list {
@ -68,25 +53,17 @@
} }
.empty { .empty {
color: var(--text-muted); composes: empty from '@common/assets/ui.module.css';
font-style: italic;
font-size: 14px;
} }
.entryCard { .entryCard {
background: var(--bg-secondary); composes: card from '@common/assets/ui.module.css';
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px; gap: 12px;
} }
.cardHeader { .cardHeader {
display: flex; composes: cardHeader from '@common/assets/ui.module.css';
align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 12px;
} }
.titleRow { .titleRow {
@ -131,49 +108,15 @@
} }
.moveButton { .moveButton {
width: 28px; composes: iconButton from '@common/assets/ui.module.css';
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg); background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text); color: var(--text);
font-size: 14px; font-size: 14px;
cursor: pointer;
transition: all var(--transition);
&:hover:not(:disabled) {
border-color: var(--accent);
color: var(--accent);
}
&:disabled {
opacity: 0.3;
cursor: not-allowed;
}
} }
.deleteButton { .deleteButton {
width: 28px; composes: deleteButton from '@common/assets/ui.module.css';
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-muted);
font-size: 18px; font-size: 18px;
cursor: pointer;
transition: all var(--transition);
&:hover {
background: var(--danger);
border-color: var(--danger);
color: var(--bg);
}
} }
.content { .content {
@ -181,20 +124,7 @@
} }
.textarea { .textarea {
width: 100%; composes: textarea from '@common/assets/ui.module.css';
padding: 10px 12px; padding: 10px 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
font-size: 14px;
color: var(--text);
font-family: inherit;
resize: vertical;
min-height: 80px;
box-sizing: border-box;
&:focus {
outline: none;
border-color: var(--accent);
}
} }

View File

@ -107,16 +107,11 @@
} }
.form { .form {
display: flex; composes: form from '@common/assets/ui.module.css';
flex-direction: column;
gap: 16px;
flex: 1;
} }
.formGroup { .formGroup {
display: flex; composes: formGroup from '@common/assets/ui.module.css';
flex-direction: column;
gap: 4px;
} }
.formGroupFill { .formGroupFill {
@ -124,9 +119,7 @@
} }
.label { .label {
display: block; composes: label from '@common/assets/ui.module.css';
margin-bottom: 4px;
font-weight: bold;
} }
.selectMultiline { .selectMultiline {
@ -143,23 +136,21 @@
padding-inline-start: 16px; padding-inline-start: 16px;
} }
.input, .input {
.select, composes: input from '@common/assets/ui.module.css';
.textarea { }
width: 100%;
padding: 8px; .select {
border-radius: 4px; composes: select from '@common/assets/ui.module.css';
border: 1px solid var(--border);
background: var(--bg);
color: var(--text);
} }
.textarea { .textarea {
composes: textarea from '@common/assets/ui.module.css';
resize: none; resize: none;
font-family: inherit;
font-size: inherit; font-size: inherit;
line-height: 1.5; line-height: 1.5;
flex: 1; flex: 1;
min-height: unset;
} }
.footer { .footer {
@ -171,26 +162,20 @@
} }
.button { .button {
padding: 8px 16px; composes: button from '@common/assets/ui.module.css';
border-radius: 4px;
cursor: pointer;
} }
.buttonSecondary { .buttonSecondary {
border: 1px solid var(--border); composes: buttonSecondary from '@common/assets/ui.module.css';
background: transparent;
color: var(--text);
} }
.buttonPrimary { .buttonPrimary {
border: none; composes: buttonPrimary from '@common/assets/ui.module.css';
background: var(--accent);
color: var(--accent-text); color: var(--accent-text);
} }
.inputRow { .inputRow {
display: flex; composes: inputRow from '@common/assets/ui.module.css';
gap: 8px;
} }
.inputRow .input { .inputRow .input {
@ -203,7 +188,7 @@
justify-content: center; justify-content: center;
padding: 8px; padding: 8px;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 4px; border-radius: var(--radius);
background: var(--bg); background: var(--bg);
color: var(--text); color: var(--text);
cursor: pointer; cursor: pointer;
@ -216,9 +201,7 @@
} }
.divider { .divider {
height: 1px; composes: divider from '@common/assets/ui.module.css';
background: var(--border);
margin: 16px 0;
} }
.tokenList { .tokenList {
@ -229,27 +212,18 @@
} }
.tokenItem { .tokenItem {
display: flex; composes: badge from '@common/assets/ui.module.css';
align-items: center;
gap: 4px;
padding: 4px 8px;
background: var(--bg-active);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 4px; border-radius: var(--radius);
color: var(--text);
} }
.tokenRemoveButton { .tokenRemoveButton {
background: none; composes: badgeRemove from '@common/assets/ui.module.css';
border: none;
cursor: pointer;
padding: 0 2px; padding: 0 2px;
font-size: 14px;
color: var(--text-muted);
} }
.emptyText { .emptyText {
color: var(--text-muted); composes: empty from '@common/assets/ui.module.css';
} }
@media (max-width: 600px) { @media (max-width: 600px) {