diff --git a/bun.lock b/bun.lock index fa607cf..38e27c4 100644 --- a/bun.lock +++ b/bun.lock @@ -18,7 +18,7 @@ "preact": "10.22.0", }, "devDependencies": { - "@types/bun": "latest", + "@types/bun": "^1.3.11", "@types/html-minifier": "4.0.5", "@types/inquirer": "9.0.7", "@types/web-bluetooth": "0.0.21", diff --git a/package.json b/package.json index 86e3e62..3fc5d13 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "preact": "10.22.0" }, "devDependencies": { - "@types/bun": "latest", + "@types/bun": "^1.3.11", "@types/html-minifier": "4.0.5", "@types/inquirer": "9.0.7", "@types/web-bluetooth": "0.0.21", diff --git a/src/common/assets/ui.module.css b/src/common/assets/ui.module.css new file mode 100644 index 0000000..a94ba44 --- /dev/null +++ b/src/common/assets/ui.module.css @@ -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; +} diff --git a/src/games/horde/assets/manage-workers-modal.module.css b/src/games/horde/assets/manage-workers-modal.module.css index 22c385f..3a62fcf 100644 --- a/src/games/horde/assets/manage-workers-modal.module.css +++ b/src/games/horde/assets/manage-workers-modal.module.css @@ -58,10 +58,24 @@ } .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 { - 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; + transition: all var(--transition); + + &:hover { background: var(--accent); border-color: var(--accent); color: var(--bg); } } diff --git a/src/games/horde/assets/options-modal.module.css b/src/games/horde/assets/options-modal.module.css index 70c8578..512896c 100644 --- a/src/games/horde/assets/options-modal.module.css +++ b/src/games/horde/assets/options-modal.module.css @@ -15,5 +15,23 @@ } .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); } } diff --git a/src/games/horde/components/modals/leaderboard-modal.tsx b/src/games/horde/components/modals/leaderboard-modal.tsx index 70a1040..445e9a5 100644 --- a/src/games/horde/components/modals/leaderboard-modal.tsx +++ b/src/games/horde/components/modals/leaderboard-modal.tsx @@ -1,8 +1,9 @@ +import clsx from "clsx"; import { useEffect, useState } from "preact/hooks"; -import { X } from "lucide-preact"; import { useHordeState } from "../../contexts/state"; import { fetchLeaderboard, type LeaderboardEntry } from "../../utils/api"; import { formatNumber, formatTime } from "@common/utils"; +import { Modal } from "@common/components/modal/Modal"; import modalStyles from "../../assets/modal.module.css"; import styles from "../../assets/leaderboard-modal.module.css"; @@ -93,67 +94,55 @@ export const LeaderboardModal = ({ open, onClose }: Props) => { return () => { cancelled = true; }; }, [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 ( -
{ if (e.target === e.currentTarget) onClose(); }}> -
-
- Leaderboard - -
-
- {loading &&

Loading…

} - {error &&

{error}

} - {!loading && !error && rows.length > 0 && ( - - - - - - - {user && } - - - - {rows.map(r => ( - - - - - {user && ( - - )} - - ))} - -
#UsernameKudosDiff
{r.rank}{r.username}{formatNumber(r.kudos)} 0 ? styles.above : styles.below) : undefined}> - {r.diff !== undefined ? `+${formatNumber(r.diff)}` : '—'} -
- )} -
- {(() => { - 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 ( -
- - Time to #{nextRow.rank}: {formatTime(secs)} - ({formatNumber(Math.round(kudosPerHour))} kudos/h) - -
- ); - })()} + +
+ Leaderboard
-
+
+ {loading &&

Loading…

} + {error &&

{error}

} + {!loading && !error && rows.length > 0 && ( + + + + + + + {user && } + + + + {rows.map(r => ( + + + + + {user && ( + + )} + + ))} + +
#UsernameKudosDiff
{r.rank}{r.username}{formatNumber(r.kudos)} 0 ? styles.above : styles.below))}> + {r.diff !== undefined ? `+${formatNumber(r.diff)}` : '—'} +
+ )} +
+ {(() => { + 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 ( +
+ + Time to #{nextRow.rank}: {formatTime(secs)} + ({formatNumber(Math.round(kudosPerHour))} kudos/h) + +
+ ); + })()} + ); }; diff --git a/src/games/horde/components/modals/manage-workers-modal.tsx b/src/games/horde/components/modals/manage-workers-modal.tsx index 6183ae9..734e744 100644 --- a/src/games/horde/components/modals/manage-workers-modal.tsx +++ b/src/games/horde/components/modals/manage-workers-modal.tsx @@ -1,7 +1,8 @@ +import clsx from "clsx"; import { useEffect, useState } from "preact/hooks"; -import { X } from "lucide-preact"; import { useHordeState } from "../../contexts/state"; import { fetchWorker, updateWorker, deleteWorker, type WorkerData } from "../../utils/api"; +import { Modal } from "@common/components/modal/Modal"; import modalStyles from "../../assets/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; }; }, [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) => { setEdits(prev => ({ ...prev, [id]: { ...prev[id], ...patch } })); }; @@ -79,7 +71,7 @@ export const ManageWorkersModal = ({ open, onClose }: Props) => { try { await deleteWorker(id, apiKey); 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) { console.error('[horde] delete worker error:', e); } finally { @@ -88,56 +80,53 @@ export const ManageWorkersModal = ({ open, onClose }: Props) => { }; return ( -
{ if (e.target === e.currentTarget) onClose(); }}> -
-
- Manage Workers - -
-
- {loading &&

Loading…

} - {!loading && workerDetails.map(w => { - const edit = edits[w.id] ?? { name: w.name, info: w.info ?? '', maintenance_mode: w.maintenance_mode }; - return ( -
-
{w.name}
- -