From 9a9b33a5280ed7aa94a11795bf3002a60f365524 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 15 Jul 2026 06:16:12 +0200 Subject: [PATCH] feat(frontend): /?view=inventory route + inventory page scaffold + dark CSS Co-Authored-By: Claude Sonnet 5 --- frontend/src/App.tsx | 7 +- .../src/components/inventory/ActiveChips.tsx | 3 + .../src/components/inventory/DetailPanel.tsx | 3 + .../components/inventory/FilterSidebar.tsx | 3 + .../inventory/InventorySearchPage.tsx | 40 +++++++++ .../src/components/inventory/ResultsTable.tsx | 6 ++ frontend/src/styles/inventory.css | 88 +++++++++++++++++++ 7 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/inventory/ActiveChips.tsx create mode 100644 frontend/src/components/inventory/DetailPanel.tsx create mode 100644 frontend/src/components/inventory/FilterSidebar.tsx create mode 100644 frontend/src/components/inventory/InventorySearchPage.tsx create mode 100644 frontend/src/components/inventory/ResultsTable.tsx create mode 100644 frontend/src/styles/inventory.css diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4e3ff977..c9c00853 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 ( - {view === 'dashboard' ? : } + {view === 'dashboard' ? + : view === 'inventory' ? + : } ); } diff --git a/frontend/src/components/inventory/ActiveChips.tsx b/frontend/src/components/inventory/ActiveChips.tsx new file mode 100644 index 00000000..0c7080e8 --- /dev/null +++ b/frontend/src/components/inventory/ActiveChips.tsx @@ -0,0 +1,3 @@ +// Stub — replaced in Task 5 +import type { InventorySearch } from './useInventorySearch'; +export function ActiveChips(_: { search: InventorySearch }) { return null; } diff --git a/frontend/src/components/inventory/DetailPanel.tsx b/frontend/src/components/inventory/DetailPanel.tsx new file mode 100644 index 00000000..13b9bbd7 --- /dev/null +++ b/frontend/src/components/inventory/DetailPanel.tsx @@ -0,0 +1,3 @@ +// Stub — replaced in Task 6 +import type { InvItem } from './types'; +export function DetailPanel(_: { item: InvItem; onClose: () => void }) { return null; } diff --git a/frontend/src/components/inventory/FilterSidebar.tsx b/frontend/src/components/inventory/FilterSidebar.tsx new file mode 100644 index 00000000..006eed02 --- /dev/null +++ b/frontend/src/components/inventory/FilterSidebar.tsx @@ -0,0 +1,3 @@ +// Stub — replaced in Task 4 +import type { InventorySearch } from './useInventorySearch'; +export function FilterSidebar(_: { search: InventorySearch }) { return
; } diff --git a/frontend/src/components/inventory/InventorySearchPage.tsx b/frontend/src/components/inventory/InventorySearchPage.tsx new file mode 100644 index 00000000..88e55cd4 --- /dev/null +++ b/frontend/src/components/inventory/InventorySearchPage.tsx @@ -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(null); + + return ( +
+
+ ⚔ Inventory Search + search.update({ text: e.target.value })} + /> + + + {search.error ? {search.error} + : search.result ? <>{search.result.total_count.toLocaleString()} items + {search.queryMs != null && <> · {search.queryMs} ms} + {search.loading && ' · …'} + : 'loading…'} + +
+ +
+ + + {selected && setSelected(null)} />} +
+
+ ); +} diff --git a/frontend/src/components/inventory/ResultsTable.tsx b/frontend/src/components/inventory/ResultsTable.tsx new file mode 100644 index 00000000..ba6c35e1 --- /dev/null +++ b/frontend/src/components/inventory/ResultsTable.tsx @@ -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
; +} diff --git a/frontend/src/styles/inventory.css b/frontend/src/styles/inventory.css new file mode 100644 index 00000000..5df15221 --- /dev/null +++ b/frontend/src/styles/inventory.css @@ -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; }