@@ -2,17 +2,17 @@ import path from "node:path"
22import { glob } from "node:fs/promises"
33import { readFile } from "node:fs/promises"
44import matter from "gray-matter"
5-
65import type { CSSProperties } from "react"
6+
77import { Button } from "@/app/conf/_design-system/button"
88import blurCorner from "./blur-corner.webp"
99import { Eyebrow } from "@/_design-system/eyebrow"
1010import slugMap from "@/code/slug-map.json"
1111import { type Topic } from "@/resources/types"
1212import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration"
1313
14- import { ChevronRight } from "@/app/conf/_design-system/pixelarticons/chevron-right"
1514import { IconSpritesheet , IconName } from "./spritesheet"
15+ import CaretDown from "@/app/conf/_design-system/pixelarticons/caret-down.svg?svgr"
1616
1717interface LibraryEntry {
1818 name : string
@@ -72,7 +72,7 @@ export async function CategoryToolsLibrariesSection({
7272 const libraries = await librariesPromise
7373 const filtered = libraries . filter ( item => item . tags . includes ( category ) )
7474
75- const grouped = Array . from (
75+ const sortedGroups = Array . from (
7676 filtered . reduce < Map < string , LibraryEntry [ ] > > ( ( acc , item ) => {
7777 const list = acc . get ( item . group ) ?? [ ]
7878 list . push ( item )
@@ -91,6 +91,14 @@ export async function CategoryToolsLibrariesSection({
9191 } ) )
9292 . sort ( ( a , b ) => b . items . length - a . items . length )
9393
94+ const grouped : GroupData [ ] = sortedGroups . map ( ( group , index ) => {
95+ const nextLength = sortedGroups [ index + 1 ] ?. items . length ?? 0
96+ const columns =
97+ nextLength > 0 && group . items . length >= nextLength * 1.9 ? 2 : 1
98+ const breakIndex = columns === 2 ? Math . ceil ( group . items . length / 2 ) : 0
99+ return { ...group , columns, breakIndex }
100+ } )
101+
94102 if ( grouped . length === 0 ) {
95103 return null
96104 }
@@ -124,73 +132,108 @@ export async function CategoryToolsLibrariesSection({
124132 </ Button >
125133 </ div >
126134
127- < div className = "flex flex-wrap gap-4 pb-2 lg:overflow-visible" >
128- { grouped . map ( ( group , index ) => {
129- const nextLength = grouped [ index + 1 ] ?. items . length ?? 0
130- const columns =
131- nextLength > 0 && group . items . length >= nextLength * 1.9 ? 2 : 1
132- const listStyle = { "--item-columns" : columns } as CSSProperties
133- const breakIndex =
134- columns === 2 ? Math . floor ( group . items . length / 2 ) : 0
135-
136- return (
137- < div
138- key = { group . id }
139- className = "min-w-[480px] shrink-0 grow border border-neu-200 bg-neu-50 dark:border-neu-100 dark:bg-neu-50/25 lg:w-1/3 lg:min-w-0"
140- >
141- < div className = "typography-body-lg flex items-center gap-3 border-b border-inherit bg-neu-50 text-neu-900 dark:bg-transparent" >
142- < div className = "border-r border-inherit p-3" >
143- < IconSpritesheet
144- sprite = { group . id as IconName }
145- className = "size-10 text-neu-800 dark:text-neu-700"
146- />
147- </ div >
148- < div className = "px-4 py-3" > { group . name } </ div >
149- < div className = "border-l border-inherit p-3 md:hidden" >
150- { /* TODO: On mobile */ }
151- < ChevronRight className = "rotate-90" />
152- </ div >
153- </ div >
154- < ul
155- className = "gap-0 divide-y divide-neu-200 dark:divide-neu-100 lg:[column-count:var(--item-columns,1)]"
156- style = { listStyle }
157- >
158- { group . items . map ( ( item , i ) => (
159- < li
160- key = { `${ group . id } -${ item . name } ` }
161- style = {
162- breakIndex
163- ? {
164- borderTop : i === breakIndex ? "none" : "" ,
165- borderLeftWidth : i >= breakIndex ? "1px" : "" ,
166- }
167- : { }
168- }
169- >
170- { item . href ? (
171- < a
172- href = { item . href }
173- className = "flex items-center justify-between bg-neu-0/40 px-4 py-3 text-neu-900 transition-colors hover:bg-neu-0 hover:duration-0"
174- >
175- { item . name }
176- </ a >
177- ) : (
178- < span className = "flex items-center justify-between bg-neu-0/40 px-4 py-3 text-neu-900" >
179- { item . name }
180- </ span >
181- ) }
182- </ li >
183- ) ) }
184- </ ul >
185- </ div >
186- )
187- } ) }
135+ < div className = "flex flex-wrap gap-4 pb-2 max-md:flex-col md:flex-nowrap md:items-start" >
136+ { distributeToColumns ( grouped ) . map ( ( column , colIndex ) => (
137+ < div
138+ key = { colIndex }
139+ className = "flex w-full flex-col gap-4 max-md:contents"
140+ >
141+ { column . map ( group => (
142+ < Group key = { group . id } group = { group } />
143+ ) ) }
144+ </ div >
145+ ) ) }
188146 </ div >
189147 </ section >
190148 </ div >
191149 )
192150}
193151
152+ interface GroupData {
153+ id : string
154+ name : string
155+ items : LibraryEntry [ ]
156+ columns : 1 | 2
157+ breakIndex : number
158+ }
159+
160+ function distributeToColumns ( groups : GroupData [ ] ) : [ GroupData [ ] , GroupData [ ] ] {
161+ const left : GroupData [ ] = [ ]
162+ const right : GroupData [ ] = [ ]
163+
164+ let leftHeight = 0
165+ let rightHeight = 0
166+
167+ for ( const group of groups ) {
168+ const itemRows =
169+ group . columns === 2
170+ ? Math . ceil ( group . items . length / 2 )
171+ : group . items . length
172+ const height = itemRows + 1
173+ if ( leftHeight <= rightHeight ) {
174+ left . push ( group )
175+ leftHeight += height
176+ } else {
177+ right . push ( group )
178+ rightHeight += height
179+ }
180+ }
181+
182+ return [ left , right ]
183+ }
184+
185+ function Group ( { group } : { group : GroupData } ) {
186+ const listStyle = { "--item-columns" : group . columns } as CSSProperties
187+
188+ return (
189+ < div className = "shrink-0 grow border border-neu-200 bg-neu-50 dark:border-neu-100 dark:bg-neu-50/25 lg:min-w-0 xl:min-w-[480px]" >
190+ < div className = "typography-body-lg flex items-center border-b border-inherit bg-neu-50 text-neu-900 dark:bg-transparent" >
191+ < div className = "border-r border-inherit p-2 lg:p-3" >
192+ < IconSpritesheet
193+ sprite = { group . id as IconName }
194+ className = "size-8 text-neu-800 dark:text-neu-700 lg:size-10"
195+ />
196+ </ div >
197+ < div className = "p-2 lg:px-4 lg:py-3" > { group . name } </ div >
198+ < div className = "ml-auto flex aspect-square h-12 shrink-0 items-center justify-center border-l border-inherit p-2 md:hidden" >
199+ < CaretDown className = "size-6 shrink-0 fill-neu-700" />
200+ </ div >
201+ </ div >
202+ < ul
203+ className = "divide-y divide-neu-200 dark:divide-neu-100 lg:[column-count:var(--item-columns,1)]"
204+ style = { listStyle }
205+ >
206+ { group . items . map ( ( item , i ) => (
207+ < li
208+ key = { `${ group . id } -${ item . name } ` }
209+ style = {
210+ group . breakIndex
211+ ? {
212+ borderTop : i === group . breakIndex ? "none" : "" ,
213+ borderLeftWidth : i >= group . breakIndex ? "1px" : "" ,
214+ }
215+ : { }
216+ }
217+ >
218+ { item . href ? (
219+ < a
220+ href = { item . href }
221+ className = "flex items-center justify-between bg-neu-0/40 px-4 py-3 text-neu-900 transition-colors hover:bg-neu-0 hover:duration-0"
222+ >
223+ { item . name }
224+ </ a >
225+ ) : (
226+ < span className = "flex items-center justify-between bg-neu-0/40 px-4 py-3 text-neu-900" >
227+ { item . name }
228+ </ span >
229+ ) }
230+ </ li >
231+ ) ) }
232+ </ ul >
233+ </ div >
234+ )
235+ }
236+
194237function Stripes ( ) {
195238 return (
196239 < div
0 commit comments