feat(frontend): /?view=inventory route + inventory page scaffold + dark CSS

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-15 06:16:12 +02:00
parent 6fd87a3751
commit 9a9b33a528
7 changed files with 149 additions and 1 deletions

View file

@ -1,20 +1,25 @@
import { MapLayout } from './components/map/MapLayout';
import { PlayerDashboardFullPage } from './components/PlayerDashboardFullPage';
import { InventorySearchPage } from './components/inventory/InventorySearchPage';
import { MidsummerProvider } from './hooks/useMidsummer';
import { useLiveData } from './hooks/useLiveData';
import './styles/map-layout.css';
import './styles/midsummer.css';
import './styles/inventory.css';
/**
* Single SPA entry. Branches on `?view=` query param:
* /?view=dashboard fullscreen PlayerDashboardFullPage (new-tab target)
* /?view=inventory fullscreen InventorySearchPage (new-tab target)
* / default map + sidebar layout
*/
export default function App() {
const view = new URLSearchParams(window.location.search).get('view');
return (
<MidsummerProvider>
{view === 'dashboard' ? <PlayerDashboardFullPage /> : <DefaultApp />}
{view === 'dashboard' ? <PlayerDashboardFullPage />
: view === 'inventory' ? <InventorySearchPage />
: <DefaultApp />}
</MidsummerProvider>
);
}

View file

@ -0,0 +1,3 @@
// Stub — replaced in Task 5
import type { InventorySearch } from './useInventorySearch';
export function ActiveChips(_: { search: InventorySearch }) { return null; }

View file

@ -0,0 +1,3 @@
// Stub — replaced in Task 6
import type { InvItem } from './types';
export function DetailPanel(_: { item: InvItem; onClose: () => void }) { return null; }

View file

@ -0,0 +1,3 @@
// Stub — replaced in Task 4
import type { InventorySearch } from './useInventorySearch';
export function FilterSidebar(_: { search: InventorySearch }) { return <div className="inv-sidebar" />; }

View file

@ -0,0 +1,40 @@
import { useState } from 'react';
import { useInventorySearch } from './useInventorySearch';
import { FilterSidebar } from './FilterSidebar';
import { ActiveChips } from './ActiveChips';
import { ResultsTable } from './ResultsTable';
import { DetailPanel } from './DetailPanel';
import type { InvItem } from './types';
export function InventorySearchPage() {
const search = useInventorySearch();
const [selected, setSelected] = useState<InvItem | null>(null);
return (
<div className="inv-page">
<div className="inv-topbar">
<span className="inv-title"> Inventory Search</span>
<input
className="inv-searchbox"
placeholder="Search name or material…"
value={search.filters.text}
onChange={e => search.update({ text: e.target.value })}
/>
<button className="inv-btn" onClick={() => { search.reset(); setSelected(null); }}>Reset</button>
<span className="inv-count">
{search.error ? <span className="inv-error">{search.error}</span>
: search.result ? <><b>{search.result.total_count.toLocaleString()}</b> items
{search.queryMs != null && <> · {search.queryMs} ms</>}
{search.loading && ' · …'}</>
: 'loading…'}
</span>
</div>
<ActiveChips search={search} />
<div className="inv-main">
<FilterSidebar search={search} />
<ResultsTable search={search} selected={selected} onSelect={setSelected} />
{selected && <DetailPanel item={selected} onClose={() => setSelected(null)} />}
</div>
</div>
);
}

View file

@ -0,0 +1,6 @@
// Stub — replaced in Task 5
import type { InventorySearch } from './useInventorySearch';
import type { InvItem } from './types';
export function ResultsTable(_: { search: InventorySearch; selected: InvItem | null; onSelect: (i: InvItem | null) => void }) {
return <div className="inv-results" />;
}

View file

@ -0,0 +1,88 @@
/* Inventory Search (/?view=inventory) dark theme ported from
docs/superpowers/specs/2026-07-15-inventory-search-redesign-mockup.html
All rules are scoped under .inv-* classes (this SPA also renders the
map/dashboard views, so no bare element selectors at file scope). */
.inv-page, .inv-page * { box-sizing: border-box; margin: 0; padding: 0; }
.inv-page {
display: flex;
flex-direction: column;
height: 100vh;
background: #111;
color: #eee;
font-family: "Segoe UI", sans-serif;
font-size: 13px;
}
.inv-main { display: flex; flex: 1; overflow: hidden; }
.inv-error { color: #c66; }
/* ── Top bar ── */
.inv-topbar { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: #1a1a1a; border-bottom: 2px solid #333; }
.inv-title { color: #88f; font-weight: 600; font-size: 15px; white-space: nowrap; }
.inv-searchbox { flex: 1; max-width: 520px; background: #222; border: 1px solid #444; border-radius: 4px; padding: 7px 12px; color: #eee; font-size: 13px; outline: none; }
.inv-searchbox:focus { border-color: #88f; }
.inv-searchbox::placeholder { color: #666; }
.inv-btn { background: #333; border: 1px solid #555; color: #ccc; border-radius: 4px; padding: 6px 14px; font-size: 12px; cursor: pointer; }
.inv-btn:hover { background: #444; color: #fff; }
.inv-count { margin-left: auto; color: #888; font-size: 12px; white-space: nowrap; }
.inv-count b { color: #eee; }
/* ── Chips ── */
.inv-chipsrow { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; padding: 8px 14px; background: #151515; border-bottom: 1px solid #2a2a2a; min-height: 37px; }
.inv-chips-lbl { color: #666; font-size: 10px; letter-spacing: 1px; }
.inv-chip { display: inline-flex; align-items: center; gap: 6px; background: #23233a; border: 1px solid #55f; border-radius: 12px; padding: 2px 10px; font-size: 11px; color: #bbd; cursor: default; }
.inv-chip-x { color: #c66; cursor: pointer; font-weight: bold; }
.inv-chip-x:hover { color: #f88; }
.inv-chip-gold { border-color: #a80; color: #fc6; background: #2a2418; }
/* ── Sidebar ── */
.inv-sidebar { width: 210px; min-width: 210px; background: #1a1a1a; border-right: 2px solid #333; overflow-y: auto; padding: 8px 10px; }
.inv-grp { border-bottom: 1px solid #262626; padding: 6px 0; }
.inv-grp-head { display: flex; align-items: center; gap: 6px; cursor: pointer; color: #999; text-transform: uppercase; font-size: 10px; letter-spacing: 1px; padding: 3px 0; user-select: none; }
.inv-grp-head:hover { color: #ccc; }
.inv-arrow { font-size: 9px; transition: transform .15s; }
.inv-grp.inv-open .inv-arrow { transform: rotate(90deg); }
.inv-badge { margin-left: auto; background: #3a3a6e; color: #cce; border-radius: 8px; padding: 0 7px; font-size: 9px; }
.inv-grp-body { padding: 5px 0 3px; }
.inv-minisearch { width: 100%; background: #222; border: 1px solid #3a3a3a; border-radius: 3px; padding: 4px 8px; color: #ccc; font-size: 11px; outline: none; margin-bottom: 5px; }
.inv-minisearch::placeholder { color: #555; }
.inv-fitem { display: flex; align-items: center; gap: 6px; padding: 2px 0; font-size: 12px; color: #bbb; cursor: pointer; }
.inv-fitem:hover { color: #fff; }
.inv-fitem input { accent-color: #88f; }
.inv-subhead { color: #666; font-size: 9px; text-transform: uppercase; letter-spacing: 1px; margin: 6px 0 2px; }
.inv-linky { color: #88f; font-size: 10px; cursor: pointer; }
.inv-linky:hover { text-decoration: underline; }
.inv-range { display: flex; gap: 4px; align-items: center; margin: 2px 0; }
.inv-range label { flex: 1; color: #999; font-size: 11px; }
.inv-range input { width: 52px; background: #222; border: 1px solid #3a3a3a; border-radius: 3px; color: #ccc; padding: 2px 5px; font-size: 11px; }
/* ── Results ── */
.inv-results { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
.inv-results table { width: 100%; border-collapse: collapse; }
.inv-results thead { position: sticky; top: 0; background: #191919; z-index: 1; }
.inv-results th { text-align: left; color: #88f; font-size: 11px; font-weight: 600; padding: 8px 10px; border-bottom: 2px solid #333; cursor: pointer; white-space: nowrap; user-select: none; }
.inv-results th:hover { color: #aaf; }
.inv-results td { padding: 6px 10px; border-bottom: 1px solid #1e1e1e; font-size: 12px; color: #bbb; vertical-align: top; }
.inv-results tbody tr { cursor: pointer; }
.inv-results tbody tr:hover td { background: #191922; }
.inv-results tbody tr.inv-sel td { background: #20203a; }
.inv-results td.inv-num { text-align: right; font-variant-numeric: tabular-nums; }
.inv-leg { color: #fc6; }
.inv-spells { color: #889; font-size: 11px; }
.inv-equipped { color: #4c4; font-size: 10px; }
.inv-pager { display: flex; align-items: center; gap: 8px; padding: 8px 14px; color: #888; font-size: 12px; border-top: 1px solid #262626; background: #151515; margin-top: auto; }
.inv-pg { padding: 2px 8px; border: 1px solid #333; border-radius: 3px; cursor: pointer; }
.inv-pg.inv-cur { background: #3a3a6e; border-color: #88f; color: #fff; }
/* ── Detail panel ── */
.inv-detail { width: 250px; min-width: 250px; background: #161620; border-left: 2px solid #333; overflow-y: auto; padding: 12px; }
.inv-detail h3 { color: #88f; font-size: 14px; margin-bottom: 2px; }
.inv-detail-sub { color: #777; font-size: 11px; margin-bottom: 10px; }
.inv-kv { display: flex; justify-content: space-between; padding: 2px 0; font-size: 12px; color: #999; }
.inv-kv b { color: #ddd; font-weight: normal; text-align: right; }
.inv-detail hr { border: none; border-top: 1px solid #2a2a35; margin: 9px 0; }
.inv-sphead { color: #666; font-size: 10px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; }
.inv-sp { padding: 2px 0; font-size: 12px; color: #bbb; }
.inv-hint { color: #555; font-size: 10px; margin-top: 12px; }
.inv-closex { float: right; color: #666; cursor: pointer; font-size: 14px; }
.inv-closex:hover { color: #f88; }