@@ -9,12 +9,10 @@ import {
99 type SelectedFacets
1010} from "@/lib/task-filter" ;
1111import { TagChip } from "@/components/tag-chip" ;
12- import { TaskCard } from "@/components/task-card" ;
1312import { TaskRow } from "@/components/task-row" ;
1413import { TaskDrawer } from "@/components/task-drawer" ;
15- import clsx from "@/components/utils/clsx" ;
16- import { IconViewGrid , IconViewList } from "@/components/icons" ;
1714import { formatMaturityLabel } from "@/components/maturity-badge" ;
15+ import { formatIsoDateTime } from "@/lib/format" ;
1816
1917function FacetSection ( {
2018 title,
@@ -66,55 +64,15 @@ function FacetSection({
6664 ) ;
6765}
6866
69- function ViewToggle ( {
70- view ,
71- setView
67+ export function GalleryClient ( {
68+ tasks ,
69+ generatedAt
7270} : {
73- view : "list" | "cards" ;
74- setView : ( view : "list" | "cards" ) => void ;
71+ tasks : TaskIndexItem [ ] ;
72+ generatedAt : string ;
7573} ) {
76- return (
77- < div
78- className = "inline-flex rounded-xl border border-slate-200 bg-white p-1 shadow-sm"
79- role = "group"
80- aria-label = "Gallery view"
81- >
82- < button
83- type = "button"
84- aria-pressed = { view === "list" }
85- className = { clsx (
86- "tb-focus-ring inline-flex items-center gap-2 rounded-lg px-3 py-2 text-xs font-semibold transition-colors" ,
87- view === "list"
88- ? "bg-brand-700 text-white"
89- : "text-slate-800 hover:bg-brand-50 hover:text-brand-900"
90- ) }
91- onClick = { ( ) => setView ( "list" ) }
92- >
93- < IconViewList className = "size-4" />
94- List
95- </ button >
96- < button
97- type = "button"
98- aria-pressed = { view === "cards" }
99- className = { clsx (
100- "tb-focus-ring inline-flex items-center gap-2 rounded-lg px-3 py-2 text-xs font-semibold transition-colors" ,
101- view === "cards"
102- ? "bg-brand-700 text-white"
103- : "text-slate-800 hover:bg-brand-50 hover:text-brand-900"
104- ) }
105- onClick = { ( ) => setView ( "cards" ) }
106- >
107- < IconViewGrid className = "size-4" />
108- Cards
109- </ button >
110- </ div >
111- ) ;
112- }
113-
114- export function GalleryClient ( { tasks } : { tasks : TaskIndexItem [ ] } ) {
11574 const [ query , setQuery ] = useState < string > ( "" ) ;
11675 const [ selected , setSelected ] = useState < SelectedFacets > ( ( ) => emptySelectedFacets ( ) ) ;
117- const [ view , setView ] = useState < "list" | "cards" > ( "list" ) ;
11876 const [ activeRepo , setActiveRepo ] = useState < string | null > ( null ) ;
11977 const [ openFacets , setOpenFacets ] = useState < { maturity : boolean ; paradigm : boolean } > ( {
12078 maturity : true ,
@@ -124,6 +82,14 @@ export function GalleryClient({ tasks }: { tasks: TaskIndexItem[] }) {
12482 const deferredQuery = useDeferredValue ( query ) ;
12583 const allMaturities = useMemo ( ( ) => facetValues ( tasks , "maturity" ) , [ tasks ] ) ;
12684 const allParadigms = useMemo ( ( ) => facetValues ( tasks , "paradigm" ) , [ tasks ] ) ;
85+ const previewCount = useMemo ( ( ) => tasks . filter ( ( task ) => task . web_variant ) . length , [ tasks ] ) ;
86+ const paradigmCount = useMemo (
87+ ( ) =>
88+ new Set (
89+ tasks . flatMap ( ( task ) => task . tags ?. paradigm ?? [ ] ) . map ( ( value ) => value . toLowerCase ( ) )
90+ ) . size ,
91+ [ tasks ]
92+ ) ;
12793 const filtered = useMemo (
12894 ( ) => filterTasks ( tasks , deferredQuery , selected ) ,
12995 [ deferredQuery , selected , tasks ]
@@ -162,55 +128,102 @@ export function GalleryClient({ tasks }: { tasks: TaskIndexItem[] }) {
162128 }
163129
164130 return (
165- < section id = "explorer" className = "space-y-5 " >
131+ < section className = "space-y-6 " >
166132 < div className = "rounded-[32px] border border-slate-200 bg-white/90 p-5 shadow-sm" >
167- < div className = "flex flex-col gap-5 " >
168- < div className = "flex flex-col gap-4 lg :flex-row lg :items-end lg :justify-between" >
169- < div className = "min-w-0 flex-1" >
133+ < div className = "flex flex-col gap-6 " >
134+ < div className = "flex flex-col gap-4 xl :flex-row xl :items-end xl :justify-between" >
135+ < div >
170136 < div className = "text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500" >
171- Task explorer
137+ Tasks
172138 </ div >
173- < div className = "mt-2 font-heading text-2xl font-semibold tracking-tight text-slate-900" >
174- Browse canonical local tasks and attached browser previews.
139+ < div className = "mt-2 font-heading text-3xl font-semibold tracking-tight text-slate-900" >
140+ Browse canonical local tasks and aligned previews.
175141 </ div >
176142 < div className = "mt-2 max-w-3xl text-sm leading-6 text-slate-700" >
177- Search by task name, repo, paradigm, or ID. Switch between a dense list and a flow-oriented card browser, then expand any task to load its README snapshot on demand.
143+ This page keeps the denser explorer layout: filter from the left, scan the list on
144+ the right, and open README-backed details only when needed.
178145 </ div >
179146 </ div >
180147
181- < div className = "flex flex-wrap items-center gap-2" >
182- { anyFilters ? (
183- < button
184- type = "button"
185- className = "tb-focus-ring rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-800 shadow-sm hover:border-brand-200 hover:bg-brand-50"
186- onClick = { clearAll }
187- >
188- Clear filters
189- </ button >
190- ) : null }
191- < ViewToggle view = { view } setView = { setView } />
148+ < div className = "rounded-2xl border border-slate-200 bg-slate-50/85 px-4 py-3 text-sm text-slate-700 shadow-sm" >
149+ Index updated{ " " }
150+ < span className = "font-semibold text-slate-900" >
151+ { formatIsoDateTime ( generatedAt ) }
152+ </ span >
192153 </ div >
193154 </ div >
194155
195- < div className = "grid gap-4 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-end" >
196- < label className = "block" htmlFor = "task-explorer-search" >
197- < span className = "text-xs font-semibold uppercase tracking-wide text-slate-600" > Search</ span >
198- < input
199- id = "task-explorer-search"
200- value = { query }
201- onChange = { ( event ) => setQuery ( event . target . value ) }
202- placeholder = "e.g. stroop, T000012, H000006, EEG"
203- className = "tb-focus-ring mt-2 w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 placeholder:text-slate-400"
204- />
205- </ label >
206-
207- < div className = "rounded-2xl border border-slate-200 bg-slate-50/85 px-4 py-3 text-sm text-slate-700 shadow-sm" >
208- Showing < span className = "font-semibold text-slate-900" > { filtered . length } </ span > of{ " " }
209- < span className = "font-semibold text-slate-900" > { tasks . length } </ span > tasks
156+ < div className = "grid gap-3 md:grid-cols-2 xl:grid-cols-4" >
157+ < div className = "rounded-2xl border border-slate-200 bg-slate-50/85 p-4 shadow-sm" >
158+ < div className = "text-[11px] font-semibold uppercase tracking-wide text-slate-500" >
159+ Total tasks
160+ </ div >
161+ < div className = "mt-2 font-heading text-2xl font-semibold text-slate-900" >
162+ { tasks . length }
163+ </ div >
164+ </ div >
165+ < div className = "rounded-2xl border border-slate-200 bg-slate-50/85 p-4 shadow-sm" >
166+ < div className = "text-[11px] font-semibold uppercase tracking-wide text-slate-500" >
167+ Matching now
168+ </ div >
169+ < div className = "mt-2 font-heading text-2xl font-semibold text-slate-900" >
170+ { filtered . length }
171+ </ div >
172+ </ div >
173+ < div className = "rounded-2xl border border-slate-200 bg-slate-50/85 p-4 shadow-sm" >
174+ < div className = "text-[11px] font-semibold uppercase tracking-wide text-slate-500" >
175+ Previews
176+ </ div >
177+ < div className = "mt-2 font-heading text-2xl font-semibold text-slate-900" >
178+ { previewCount }
179+ </ div >
180+ </ div >
181+ < div className = "rounded-2xl border border-slate-200 bg-slate-50/85 p-4 shadow-sm" >
182+ < div className = "text-[11px] font-semibold uppercase tracking-wide text-slate-500" >
183+ Paradigms
184+ </ div >
185+ < div className = "mt-2 font-heading text-2xl font-semibold text-slate-900" >
186+ { paradigmCount }
187+ </ div >
210188 </ div >
211189 </ div >
190+ </ div >
191+ </ div >
192+
193+ < div className = "grid grid-cols-1 gap-6 lg:grid-cols-12" >
194+ < aside className = "lg:col-span-4 xl:col-span-3" >
195+ < div className = "sticky top-24 space-y-4" >
196+ < section className = "rounded-2xl border border-slate-200 bg-white/90 p-4 shadow-sm" >
197+ < label className = "block" htmlFor = "task-explorer-search" >
198+ < span className = "text-xs font-semibold uppercase tracking-wide text-slate-600" >
199+ Search
200+ </ span >
201+ < input
202+ id = "task-explorer-search"
203+ value = { query }
204+ onChange = { ( event ) => setQuery ( event . target . value ) }
205+ placeholder = "e.g. stroop, T000012, H000006, EEG"
206+ className = "tb-focus-ring mt-2 w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 placeholder:text-slate-400"
207+ />
208+ </ label >
209+
210+ < div className = "mt-4 flex flex-wrap items-center justify-between gap-3" >
211+ < div className = "text-sm text-slate-700" >
212+ Showing < span className = "font-semibold text-slate-900" > { filtered . length } </ span > { " " }
213+ of < span className = "font-semibold text-slate-900" > { tasks . length } </ span >
214+ </ div >
215+ { anyFilters ? (
216+ < button
217+ type = "button"
218+ className = "tb-focus-ring rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-800 shadow-sm hover:border-brand-200 hover:bg-brand-50"
219+ onClick = { clearAll }
220+ >
221+ Clear
222+ </ button >
223+ ) : null }
224+ </ div >
225+ </ section >
212226
213- < div className = "grid gap-3 lg:grid-cols-2" >
214227 < FacetSection
215228 title = "Maturity"
216229 facet = "maturity"
@@ -234,50 +247,39 @@ export function GalleryClient({ tasks }: { tasks: TaskIndexItem[] }) {
234247 }
235248 />
236249 </ div >
237- </ div >
238- </ div >
250+ </ aside >
239251
240- { filtered . length === 0 ? (
241- < div className = "rounded-[32px] border border-slate-200 bg-white/90 p-10 text-center shadow-sm" >
242- < div className = "font-heading text-lg font-semibold tracking-tight text-slate-900" >
243- No matches
244- </ div >
245- < div className = "mt-2 text-sm text-slate-700" >
246- Try clearing filters or searching by paradigm name, repo handle, or task ID.
247- </ div >
248- < div className = "mt-5" >
249- < button
250- type = "button"
251- className = "tb-focus-ring rounded-lg bg-cta-500 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-cta-600"
252- onClick = { clearAll }
253- >
254- Reset explorer
255- </ button >
256- </ div >
257- </ div >
258- ) : view === "list" ? (
259- < div className = "space-y-3" >
260- { filtered . map ( ( task ) => (
261- < TaskRow
262- key = { task . repo }
263- task = { task }
264- onTagClick = { ( facet : TaskTagFacet , value ) => toggleFacet ( facet , value ) }
265- onOpen = { ( nextTask ) => setActiveRepo ( nextTask . repo ) }
266- />
267- ) ) }
268- </ div >
269- ) : (
270- < div className = "grid grid-cols-1 gap-4 sm:grid-cols-2" >
271- { filtered . map ( ( task ) => (
272- < TaskCard
273- key = { task . repo }
274- task = { task }
275- onTagClick = { ( facet : TaskTagFacet , value ) => toggleFacet ( facet , value ) }
276- onOpen = { ( nextTask ) => setActiveRepo ( nextTask . repo ) }
277- />
278- ) ) }
279- </ div >
280- ) }
252+ < section className = "space-y-3 lg:col-span-8 xl:col-span-9" >
253+ { filtered . length === 0 ? (
254+ < div className = "rounded-[32px] border border-slate-200 bg-white/90 p-10 text-center shadow-sm" >
255+ < div className = "font-heading text-lg font-semibold tracking-tight text-slate-900" >
256+ No matches
257+ </ div >
258+ < div className = "mt-2 text-sm text-slate-700" >
259+ Try clearing filters or searching by paradigm name, repo handle, or task ID.
260+ </ div >
261+ < div className = "mt-5" >
262+ < button
263+ type = "button"
264+ className = "tb-focus-ring rounded-lg bg-cta-500 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-cta-600"
265+ onClick = { clearAll }
266+ >
267+ Reset explorer
268+ </ button >
269+ </ div >
270+ </ div >
271+ ) : (
272+ filtered . map ( ( task ) => (
273+ < TaskRow
274+ key = { task . repo }
275+ task = { task }
276+ onTagClick = { ( facet : TaskTagFacet , value ) => toggleFacet ( facet , value ) }
277+ onOpen = { ( nextTask ) => setActiveRepo ( nextTask . repo ) }
278+ />
279+ ) )
280+ ) }
281+ </ section >
282+ </ div >
281283
282284 < TaskDrawer task = { activeTask } onClose = { ( ) => setActiveRepo ( null ) } />
283285 </ section >
0 commit comments