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; }