feat(frontend): /?view=inventory route + inventory page scaffold + dark CSS
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
6fd87a3751
commit
9a9b33a528
7 changed files with 149 additions and 1 deletions
|
|
@ -1,20 +1,25 @@
|
||||||
import { MapLayout } from './components/map/MapLayout';
|
import { MapLayout } from './components/map/MapLayout';
|
||||||
import { PlayerDashboardFullPage } from './components/PlayerDashboardFullPage';
|
import { PlayerDashboardFullPage } from './components/PlayerDashboardFullPage';
|
||||||
|
import { InventorySearchPage } from './components/inventory/InventorySearchPage';
|
||||||
import { MidsummerProvider } from './hooks/useMidsummer';
|
import { MidsummerProvider } from './hooks/useMidsummer';
|
||||||
import { useLiveData } from './hooks/useLiveData';
|
import { useLiveData } from './hooks/useLiveData';
|
||||||
import './styles/map-layout.css';
|
import './styles/map-layout.css';
|
||||||
import './styles/midsummer.css';
|
import './styles/midsummer.css';
|
||||||
|
import './styles/inventory.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single SPA entry. Branches on `?view=` query param:
|
* Single SPA entry. Branches on `?view=` query param:
|
||||||
* /?view=dashboard → fullscreen PlayerDashboardFullPage (new-tab target)
|
* /?view=dashboard → fullscreen PlayerDashboardFullPage (new-tab target)
|
||||||
|
* /?view=inventory → fullscreen InventorySearchPage (new-tab target)
|
||||||
* / → default map + sidebar layout
|
* / → default map + sidebar layout
|
||||||
*/
|
*/
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const view = new URLSearchParams(window.location.search).get('view');
|
const view = new URLSearchParams(window.location.search).get('view');
|
||||||
return (
|
return (
|
||||||
<MidsummerProvider>
|
<MidsummerProvider>
|
||||||
{view === 'dashboard' ? <PlayerDashboardFullPage /> : <DefaultApp />}
|
{view === 'dashboard' ? <PlayerDashboardFullPage />
|
||||||
|
: view === 'inventory' ? <InventorySearchPage />
|
||||||
|
: <DefaultApp />}
|
||||||
</MidsummerProvider>
|
</MidsummerProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
frontend/src/components/inventory/ActiveChips.tsx
Normal file
3
frontend/src/components/inventory/ActiveChips.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
// Stub — replaced in Task 5
|
||||||
|
import type { InventorySearch } from './useInventorySearch';
|
||||||
|
export function ActiveChips(_: { search: InventorySearch }) { return null; }
|
||||||
3
frontend/src/components/inventory/DetailPanel.tsx
Normal file
3
frontend/src/components/inventory/DetailPanel.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
// Stub — replaced in Task 6
|
||||||
|
import type { InvItem } from './types';
|
||||||
|
export function DetailPanel(_: { item: InvItem; onClose: () => void }) { return null; }
|
||||||
3
frontend/src/components/inventory/FilterSidebar.tsx
Normal file
3
frontend/src/components/inventory/FilterSidebar.tsx
Normal 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" />; }
|
||||||
40
frontend/src/components/inventory/InventorySearchPage.tsx
Normal file
40
frontend/src/components/inventory/InventorySearchPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
frontend/src/components/inventory/ResultsTable.tsx
Normal file
6
frontend/src/components/inventory/ResultsTable.tsx
Normal 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" />;
|
||||||
|
}
|
||||||
88
frontend/src/styles/inventory.css
Normal file
88
frontend/src/styles/inventory.css
Normal 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; }
|
||||||
Loading…
Add table
Add a link
Reference in a new issue