feat(web): add search to Rules page
Filter sections by typing in the search box — matches against both section titles and content. Shows result count and a "no results" message. Expand/Collapse All buttons respect the filtered set. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -542,12 +542,34 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Rules Page */
|
/* Rules Page */
|
||||||
|
.rules-search {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--color-surface);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-search::placeholder {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.rules-actions {
|
.rules-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-sm);
|
||||||
margin-bottom: var(--spacing-md);
|
margin-bottom: var(--spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rules-count {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.rules-sections {
|
.rules-sections {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import Markdown from 'react-markdown';
|
import Markdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import TopBar from '../components/layout/TopBar';
|
import TopBar from '../components/layout/TopBar';
|
||||||
@@ -7,6 +7,15 @@ import { battleReferenceSections } from '../rules/battle-reference';
|
|||||||
|
|
||||||
export default function RulesPage() {
|
export default function RulesPage() {
|
||||||
const [openSections, setOpenSections] = useState<Set<number>>(new Set());
|
const [openSections, setOpenSections] = useState<Set<number>>(new Set());
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
if (!search.trim()) return battleReferenceSections.map((s, i) => ({ ...s, originalIndex: i }));
|
||||||
|
const q = search.toLowerCase();
|
||||||
|
return battleReferenceSections
|
||||||
|
.map((s, i) => ({ ...s, originalIndex: i }))
|
||||||
|
.filter((s) => s.title.toLowerCase().includes(q) || s.content.toLowerCase().includes(q));
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
const toggle = (index: number) => {
|
const toggle = (index: number) => {
|
||||||
setOpenSections((prev) => {
|
setOpenSections((prev) => {
|
||||||
@@ -21,7 +30,7 @@ export default function RulesPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const expandAll = () => {
|
const expandAll = () => {
|
||||||
setOpenSections(new Set(battleReferenceSections.map((_, i) => i)));
|
setOpenSections(new Set(filtered.map((s) => s.originalIndex)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const collapseAll = () => {
|
const collapseAll = () => {
|
||||||
@@ -32,6 +41,14 @@ export default function RulesPage() {
|
|||||||
<>
|
<>
|
||||||
<TopBar title="Ship Reference" />
|
<TopBar title="Ship Reference" />
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
className="rules-search"
|
||||||
|
placeholder="Search rules..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="rules-actions">
|
<div className="rules-actions">
|
||||||
<button className="btn-secondary btn-sm" onClick={expandAll}>
|
<button className="btn-secondary btn-sm" onClick={expandAll}>
|
||||||
Expand All
|
Expand All
|
||||||
@@ -39,12 +56,15 @@ export default function RulesPage() {
|
|||||||
<button className="btn-secondary btn-sm" onClick={collapseAll}>
|
<button className="btn-secondary btn-sm" onClick={collapseAll}>
|
||||||
Collapse All
|
Collapse All
|
||||||
</button>
|
</button>
|
||||||
|
{search.trim() && (
|
||||||
|
<span className="rules-count">{filtered.length} result{filtered.length !== 1 ? 's' : ''}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rules-sections">
|
<div className="rules-sections">
|
||||||
{battleReferenceSections.map((section, i) => (
|
{filtered.map((section) => (
|
||||||
<details key={i} className="rules-section" open={openSections.has(i)}>
|
<details key={section.originalIndex} className="rules-section" open={openSections.has(section.originalIndex)}>
|
||||||
<summary className="rules-summary" onClick={(e) => { e.preventDefault(); toggle(i); }}>
|
<summary className="rules-summary" onClick={(e) => { e.preventDefault(); toggle(section.originalIndex); }}>
|
||||||
{section.title}
|
{section.title}
|
||||||
</summary>
|
</summary>
|
||||||
<div className="rules-content">
|
<div className="rules-content">
|
||||||
@@ -59,6 +79,11 @@ export default function RulesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
))}
|
))}
|
||||||
|
{filtered.length === 0 && (
|
||||||
|
<p className="empty-text" style={{ padding: 'var(--spacing-md) 0' }}>
|
||||||
|
No sections match "{search}".
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user