Controls: separate Categories filter toggle, standalone buttons, full-width category headers
- FilterBar split into CategoryFilterBar / StatesFilterBar / StatFilterBar — each independently toggleable from the controls bar (Categories Filter / States Filter / Stats Filter), visibility persisted to localStorage - Control buttons are now individual rounded buttons with a 6px gap (removed the merged/segmented styling that left a missing border) - Category group headers span the full width with top/bottom borders only; 37px side padding keeps the header text aligned with room card content Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a4c1f9bce5
commit
2ec629cb5b
3 changed files with 42 additions and 50 deletions
|
|
@ -4,15 +4,6 @@ import type { FilterMode, FilterState, StatFilters, StatFilterMode, SemanticFlow
|
||||||
import { SEMANTIC_FLOW_MAP } from '../types'
|
import { SEMANTIC_FLOW_MAP } from '../types'
|
||||||
import type { RoomData } from '../types'
|
import type { RoomData } from '../types'
|
||||||
|
|
||||||
interface FilterBarProps {
|
|
||||||
filters: FilterState
|
|
||||||
categories: Array<{ id: string; name: string }>
|
|
||||||
rooms: RoomData[]
|
|
||||||
viewDate: string
|
|
||||||
onToggleCategory: (id: string) => void
|
|
||||||
onToggleFlow: (flow: SemanticFlowKey) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const SEMANTIC_KEYS: SemanticFlowKey[] = ['arriving', 'departing', 'back-to-back', 'staying', 'vacant', 'blocked']
|
const SEMANTIC_KEYS: SemanticFlowKey[] = ['arriving', 'departing', 'back-to-back', 'staying', 'vacant', 'blocked']
|
||||||
const SEMANTIC_LABELS: Record<SemanticFlowKey, string> = {
|
const SEMANTIC_LABELS: Record<SemanticFlowKey, string> = {
|
||||||
arriving: 'Arriving', departing: 'Departing', 'back-to-back': 'B2B',
|
arriving: 'Arriving', departing: 'Departing', 'back-to-back': 'B2B',
|
||||||
|
|
@ -34,7 +25,14 @@ function countBySemantic(rooms: RoomData[], key: SemanticFlowKey) {
|
||||||
return rooms.filter(r => flowTypes.includes(r.flow_type)).length
|
return rooms.filter(r => flowTypes.includes(r.flow_type)).length
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FilterBar({ filters, categories, rooms, viewDate, onToggleCategory, onToggleFlow }: FilterBarProps) {
|
interface CategoryFilterBarProps {
|
||||||
|
filters: FilterState
|
||||||
|
categories: Array<{ id: string; name: string }>
|
||||||
|
rooms: RoomData[]
|
||||||
|
onToggleCategory: (id: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CategoryFilterBar({ filters, categories, rooms, onToggleCategory }: CategoryFilterBarProps) {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const scroll = (dir: 'left' | 'right') => {
|
const scroll = (dir: 'left' | 'right') => {
|
||||||
|
|
@ -43,7 +41,6 @@ export function FilterBar({ filters, categories, rooms, viewDate, onToggleCatego
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="filter-section">
|
<div className="filter-section">
|
||||||
{/* Category filter row */}
|
|
||||||
<div className="filter-bar">
|
<div className="filter-bar">
|
||||||
<button className="filter-arrow left" onClick={() => scroll('left')}>
|
<button className="filter-arrow left" onClick={() => scroll('left')}>
|
||||||
<ChevronLeft size={14} strokeWidth={1.75} />
|
<ChevronLeft size={14} strokeWidth={1.75} />
|
||||||
|
|
@ -68,9 +65,20 @@ export function FilterBar({ filters, categories, rooms, viewDate, onToggleCatego
|
||||||
<ChevronRight size={14} strokeWidth={1.75} />
|
<ChevronRight size={14} strokeWidth={1.75} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{/* Flow type filter row */}
|
interface StatesFilterBarProps {
|
||||||
<div className="filter-bar" style={{ borderTop: '1px solid var(--border)' }}>
|
filters: FilterState
|
||||||
|
rooms: RoomData[]
|
||||||
|
onToggleFlow: (flow: SemanticFlowKey) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatesFilterBar({ filters, rooms, onToggleFlow }: StatesFilterBarProps) {
|
||||||
|
return (
|
||||||
|
<div className="filter-section">
|
||||||
|
<div className="filter-bar">
|
||||||
<div className="filter-bar-scroll">
|
<div className="filter-bar-scroll">
|
||||||
{SEMANTIC_KEYS.map(key => {
|
{SEMANTIC_KEYS.map(key => {
|
||||||
const mode = filters.flowTypes[key] ?? 'off'
|
const mode = filters.flowTypes[key] ?? 'off'
|
||||||
|
|
@ -111,7 +119,7 @@ const STAT_LABELS: Record<keyof StatFilters, [string, string, string]> = {
|
||||||
|
|
||||||
export function StatFilterBar({ statFilters, onChange }: StatFilterBarProps) {
|
export function StatFilterBar({ statFilters, onChange }: StatFilterBarProps) {
|
||||||
return (
|
return (
|
||||||
<div className="filter-section" style={{ borderBottom: 'none', borderTop: '1px solid var(--border)' }}>
|
<div className="filter-section">
|
||||||
<div className="filter-bar">
|
<div className="filter-bar">
|
||||||
<div className="filter-bar-scroll">
|
<div className="filter-bar-scroll">
|
||||||
{(Object.keys(statFilters) as Array<keyof StatFilters>).map(key => {
|
{(Object.keys(statFilters) as Array<keyof StatFilters>).map(key => {
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ html, body, #root {
|
||||||
}
|
}
|
||||||
.control-group {
|
.control-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
.control-btn {
|
.control-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -238,9 +238,6 @@ html, body, #root {
|
||||||
color: var(--app-primary);
|
color: var(--app-primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.control-group .control-btn:not(:first-child) { border-left: none; border-radius: 0 6px 6px 0; }
|
|
||||||
.control-group .control-btn:first-child:not(:last-child) { border-radius: 6px 0 0 6px; }
|
|
||||||
|
|
||||||
/* ── Filter bars ─────────────────────────────────────────────── */
|
/* ── Filter bars ─────────────────────────────────────────────── */
|
||||||
.filter-section {
|
.filter-section {
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
|
|
@ -347,38 +344,16 @@ html, body, #root {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
background: #e5e7eb;
|
background: #e5e7eb;
|
||||||
border: 2px solid #d1d5db;
|
border-top: 2px solid #d1d5db;
|
||||||
border-radius: 8px;
|
border-bottom: 2px solid #d1d5db;
|
||||||
margin: 8px 23px 5px;
|
margin: 8px 0 5px;
|
||||||
padding: 6px 12px;
|
padding: 6px 37px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
.category-header::before,
|
|
||||||
.category-header::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -2px;
|
|
||||||
bottom: -2px;
|
|
||||||
width: 16px;
|
|
||||||
background: #e5e7eb;
|
|
||||||
border-top: 2px solid #d1d5db;
|
|
||||||
border-bottom: 2px solid #d1d5db;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.category-header::before {
|
|
||||||
left: -16px;
|
|
||||||
border-right: 2px solid #d1d5db;
|
|
||||||
border-radius: 0 6px 6px 0;
|
|
||||||
}
|
|
||||||
.category-header::after {
|
|
||||||
right: -16px;
|
|
||||||
border-left: 2px solid #d1d5db;
|
|
||||||
border-radius: 6px 0 0 6px;
|
|
||||||
}
|
|
||||||
.category-name {
|
.category-name {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
|
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
|
||||||
import { RefreshCw, Settings, List, LayoutList, Filter, BarChart2, RotateCcw } from 'lucide-react'
|
import { RefreshCw, Settings, List, LayoutList, Filter, BarChart2, RotateCcw, Tag } from 'lucide-react'
|
||||||
import type { RoomData, AppConfig, Category, FilterState, StatFilters, SemanticFlowKey, ActivityEntry } from '../types'
|
import type { RoomData, AppConfig, Category, FilterState, StatFilters, SemanticFlowKey, ActivityEntry } from '../types'
|
||||||
import { SEMANTIC_FLOW_MAP } from '../types'
|
import { SEMANTIC_FLOW_MAP } from '../types'
|
||||||
import { useAuth } from '../components/AuthGate'
|
import { useAuth } from '../components/AuthGate'
|
||||||
import CategoryGroup from '../components/CategoryGroup'
|
import CategoryGroup from '../components/CategoryGroup'
|
||||||
import RoomCard from '../components/RoomCard'
|
import RoomCard from '../components/RoomCard'
|
||||||
import { FilterBar, StatFilterBar } from '../components/FilterBar'
|
import { CategoryFilterBar, StatesFilterBar, StatFilterBar } from '../components/FilterBar'
|
||||||
import ActivityPanel from '../components/ActivityPanel'
|
import ActivityPanel from '../components/ActivityPanel'
|
||||||
import RoomModal from '../components/RoomModal'
|
import RoomModal from '../components/RoomModal'
|
||||||
import CheckoutNotification, { useToasts } from '../components/CheckoutNotification'
|
import CheckoutNotification, { useToasts } from '../components/CheckoutNotification'
|
||||||
|
|
@ -49,6 +49,9 @@ export default function Planner() {
|
||||||
const [viewMode, setViewMode] = useState<'flat' | 'grouped'>(() =>
|
const [viewMode, setViewMode] = useState<'flat' | 'grouped'>(() =>
|
||||||
(localStorage.getItem('rp-view-mode') ?? 'grouped') as 'flat' | 'grouped'
|
(localStorage.getItem('rp-view-mode') ?? 'grouped') as 'flat' | 'grouped'
|
||||||
)
|
)
|
||||||
|
const [categoriesFilterVisible, setCategoriesFilterVisible] = useState(() =>
|
||||||
|
JSON.parse(localStorage.getItem('rp-categories-visible') ?? 'true')
|
||||||
|
)
|
||||||
const [statesFilterVisible, setStatesFilterVisible] = useState(() =>
|
const [statesFilterVisible, setStatesFilterVisible] = useState(() =>
|
||||||
JSON.parse(localStorage.getItem('rp-states-visible') ?? 'true')
|
JSON.parse(localStorage.getItem('rp-states-visible') ?? 'true')
|
||||||
)
|
)
|
||||||
|
|
@ -57,6 +60,7 @@ export default function Planner() {
|
||||||
)
|
)
|
||||||
useEffect(() => { localStorage.setItem('rp-controls-visible', JSON.stringify(controlsVisible)) }, [controlsVisible])
|
useEffect(() => { localStorage.setItem('rp-controls-visible', JSON.stringify(controlsVisible)) }, [controlsVisible])
|
||||||
useEffect(() => { localStorage.setItem('rp-view-mode', viewMode) }, [viewMode])
|
useEffect(() => { localStorage.setItem('rp-view-mode', viewMode) }, [viewMode])
|
||||||
|
useEffect(() => { localStorage.setItem('rp-categories-visible', JSON.stringify(categoriesFilterVisible)) }, [categoriesFilterVisible])
|
||||||
useEffect(() => { localStorage.setItem('rp-states-visible', JSON.stringify(statesFilterVisible)) }, [statesFilterVisible])
|
useEffect(() => { localStorage.setItem('rp-states-visible', JSON.stringify(statesFilterVisible)) }, [statesFilterVisible])
|
||||||
useEffect(() => { localStorage.setItem('rp-stats-visible', JSON.stringify(statsFilterVisible)) }, [statsFilterVisible])
|
useEffect(() => { localStorage.setItem('rp-stats-visible', JSON.stringify(statsFilterVisible)) }, [statsFilterVisible])
|
||||||
|
|
||||||
|
|
@ -178,6 +182,7 @@ export default function Planner() {
|
||||||
setFilters({ categories: {}, flowTypes: {} })
|
setFilters({ categories: {}, flowTypes: {} })
|
||||||
setStatFilters({ newbookTasks: 'off', cleanDirty: 'off' })
|
setStatFilters({ newbookTasks: 'off', cleanDirty: 'off' })
|
||||||
setViewMode('grouped')
|
setViewMode('grouped')
|
||||||
|
setCategoriesFilterVisible(true)
|
||||||
setStatesFilterVisible(true)
|
setStatesFilterVisible(true)
|
||||||
setStatsFilterVisible(true)
|
setStatsFilterVisible(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
@ -312,6 +317,9 @@ export default function Planner() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="control-group">
|
<div className="control-group">
|
||||||
|
<button className={`control-btn${categoriesFilterVisible ? ' active' : ''}`} onClick={() => setCategoriesFilterVisible((v: boolean) => !v)}>
|
||||||
|
<Tag size={13} strokeWidth={1.75} /> Categories Filter
|
||||||
|
</button>
|
||||||
<button className={`control-btn${statesFilterVisible ? ' active' : ''}`} onClick={() => setStatesFilterVisible((v: boolean) => !v)}>
|
<button className={`control-btn${statesFilterVisible ? ' active' : ''}`} onClick={() => setStatesFilterVisible((v: boolean) => !v)}>
|
||||||
<Filter size={13} strokeWidth={1.75} /> States Filter
|
<Filter size={13} strokeWidth={1.75} /> States Filter
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -328,16 +336,17 @@ export default function Planner() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
{statesFilterVisible && (
|
{categoriesFilterVisible && (
|
||||||
<FilterBar
|
<CategoryFilterBar
|
||||||
filters={filters}
|
filters={filters}
|
||||||
categories={orderedCategories}
|
categories={orderedCategories}
|
||||||
rooms={rooms}
|
rooms={rooms}
|
||||||
viewDate={viewDate}
|
|
||||||
onToggleCategory={toggleCategory}
|
onToggleCategory={toggleCategory}
|
||||||
onToggleFlow={toggleFlow}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{statesFilterVisible && (
|
||||||
|
<StatesFilterBar filters={filters} rooms={rooms} onToggleFlow={toggleFlow} />
|
||||||
|
)}
|
||||||
{statsFilterVisible && <StatFilterBar statFilters={statFilters} onChange={toggleStatFilter} />}
|
{statsFilterVisible && <StatFilterBar statFilters={statFilters} onChange={toggleStatFilter} />}
|
||||||
|
|
||||||
{/* Main area */}
|
{/* Main area */}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue