@@ -122,16 +122,26 @@ <h4 class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-400">Bun
122122 </ aside >
123123
124124 <!-- Results -->
125- < section class ="rounded-2xl border border-gray-200 bg-white/40 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/[0.03] sm:p-5 ">
125+ < section class ="min-w-0 rounded-2xl border border-gray-200 bg-white/40 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/[0.03] sm:p-5 ">
126126 < div class ="mb-4 flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 pb-4 dark:border-white/10 ">
127127 < div id ="count " class ="text-sm text-gray-600 dark:text-gray-300 "> </ div >
128- < select id ="sort " class ="rounded-xl border border-gray-200 bg-white/70 px-3 py-2 text-sm text-gray-800 outline-none focus:border-brand dark:border-white/10 dark:bg-white/10 dark:text-gray-100 ">
129- < option value ="popular "> Sort: Popular</ option >
130- < option value ="stars "> Most stars</ option >
131- < option value ="forks "> Most forks</ option >
132- < option value ="updated "> Recently updated</ option >
133- < option value ="name "> Name (A–Z)</ option >
134- </ select >
128+ < div class ="flex items-center gap-3 ">
129+ < div id ="viewToggle " class ="inline-flex rounded-xl border border-gray-200 bg-white/70 p-1 shadow-sm dark:border-white/10 dark:bg-white/5 " role ="group " aria-label ="View layout ">
130+ < button id ="viewGrid " type ="button " title ="Grid view " aria-label ="Grid view " class ="rounded-lg px-2.5 py-1.5 text-gray-500 dark:text-gray-400 ">
131+ < svg class ="h-4 w-4 " viewBox ="0 0 24 24 " fill ="none " stroke ="currentColor " stroke-width ="2 "> < rect x ="3 " y ="3 " width ="7 " height ="7 " rx ="1.5 "/> < rect x ="14 " y ="3 " width ="7 " height ="7 " rx ="1.5 "/> < rect x ="3 " y ="14 " width ="7 " height ="7 " rx ="1.5 "/> < rect x ="14 " y ="14 " width ="7 " height ="7 " rx ="1.5 "/> </ svg >
132+ </ button >
133+ < button id ="viewList " type ="button " title ="List view " aria-label ="List view " class ="rounded-lg px-2.5 py-1.5 text-gray-500 dark:text-gray-400 ">
134+ < svg class ="h-4 w-4 " viewBox ="0 0 24 24 " fill ="none " stroke ="currentColor " stroke-width ="2 "> < line x1 ="8 " y1 ="6 " x2 ="21 " y2 ="6 "/> < line x1 ="8 " y1 ="12 " x2 ="21 " y2 ="12 "/> < line x1 ="8 " y1 ="18 " x2 ="21 " y2 ="18 "/> < circle cx ="3.5 " cy ="6 " r ="1.2 " fill ="currentColor " stroke ="none "/> < circle cx ="3.5 " cy ="12 " r ="1.2 " fill ="currentColor " stroke ="none "/> < circle cx ="3.5 " cy ="18 " r ="1.2 " fill ="currentColor " stroke ="none "/> </ svg >
135+ </ button >
136+ </ div >
137+ < select id ="sort " class ="rounded-xl border border-gray-200 bg-white/70 px-3 py-2 text-sm text-gray-800 outline-none focus:border-brand dark:border-white/10 dark:bg-white/10 dark:text-gray-100 ">
138+ < option value ="popular "> Sort: Popular</ option >
139+ < option value ="stars "> Most stars</ option >
140+ < option value ="forks "> Most forks</ option >
141+ < option value ="updated "> Recently updated</ option >
142+ < option value ="name "> Name (A–Z)</ option >
143+ </ select >
144+ </ div >
135145 </ div >
136146 < div id ="grid " class ="grid gap-4 grid-cols-[repeat(auto-fill,minmax(18rem,1fr))] "> </ div >
137147 </ section >
@@ -148,7 +158,7 @@ <h4 class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-400">Bun
148158 < div id ="toast " class ="pointer-events-none fixed bottom-6 left-1/2 z-50 -translate-x-1/2 rounded-2xl border border-gray-200 bg-white px-5 py-3 text-sm opacity-0 shadow-lg transition dark:border-white/10 dark:bg-basisbg "> </ div >
149159
150160< script >
151- const state = { search :"" , source :"all" , category :"all" , sort :"popular" , view :"packages" } ;
161+ const state = { search :"" , source :"all" , category :"all" , sort :"popular" , view :"packages" , layout : "grid" } ;
152162const REPO = "BasisVR/BasisPackageManager" ;
153163let RAW = [ ] , ALL = [ ] , RAWB = [ ] , bundlesLoaded = false ;
154164
@@ -257,6 +267,26 @@ <h4 class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-400">Bun
257267 document . getElementById ( 'tabBundles' ) . className = state . view === 'bundles' ? on : off ;
258268}
259269
270+ // Grid vs list layout — the container class and each button's active state follow state.layout.
271+ const gridClass = ( ) => state . layout === 'list'
272+ ? 'flex flex-col gap-3'
273+ : 'grid gap-4 grid-cols-[repeat(auto-fill,minmax(18rem,1fr))]' ;
274+ function viewStyle ( ) {
275+ const on = 'rounded-lg px-2.5 py-1.5 bg-gradient-to-r from-brand to-purple-600 text-white shadow' ;
276+ const off = 'rounded-lg px-2.5 py-1.5 text-gray-500 hover:text-brand dark:text-gray-400 dark:hover:text-brand' ;
277+ document . getElementById ( 'viewGrid' ) . className = state . layout === 'grid' ? on : off ;
278+ document . getElementById ( 'viewList' ) . className = state . layout === 'list' ? on : off ;
279+ document . getElementById ( 'viewGrid' ) . setAttribute ( 'aria-pressed' , state . layout === 'grid' ) ;
280+ document . getElementById ( 'viewList' ) . setAttribute ( 'aria-pressed' , state . layout === 'list' ) ;
281+ }
282+ function setLayout ( layout ) {
283+ if ( state . layout === layout ) return ;
284+ state . layout = layout ;
285+ try { localStorage . setItem ( 'basis:pkgLayout' , layout ) ; } catch { }
286+ viewStyle ( ) ;
287+ renderGrid ( ) ;
288+ }
289+
260290async function switchView ( view ) {
261291 if ( state . view === view ) return ;
262292 state . view = view ;
@@ -333,12 +363,8 @@ <h4 class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-400">Bun
333363 document . querySelectorAll ( '[data-category]' ) . forEach ( el => el . onclick = ( ) => { state . category = el . dataset . category ; buildFilters ( ) ; load ( ) ; } ) ;
334364}
335365
336- function renderGrid ( ) {
337- if ( state . view === 'bundles' ) return renderBundles ( ) ;
338- const grid = document . getElementById ( 'grid' ) ;
339- document . getElementById ( 'count' ) . innerHTML = `<b class="text-gray-900 dark:text-white">${ ALL . length } </b> package${ ALL . length === 1 ?'' :'s' } ` ;
340- if ( ! ALL . length ) { grid . innerHTML = `<div class="col-span-full py-20 text-center text-gray-500 dark:text-gray-400"><p class="text-lg font-semibold text-gray-900 dark:text-white">No packages found</p><p class="mt-1">Try a different search or filter.</p></div>` ; return ; }
341- grid . innerHTML = ALL . map ( p => `
366+ function pkgCard ( p ) {
367+ return `
342368 <div data-id="${ esc ( p . id ) } " class="group flex cursor-pointer flex-col rounded-2xl border border-gray-200 bg-white/70 p-5 shadow-sm backdrop-blur transition hover:-translate-y-0.5 hover:shadow-lg hover:shadow-brand/20 dark:border-white/10 dark:bg-white/5">
343369 <div class="flex items-start gap-3">
344370 <div class="inline-flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-gray-100 text-xl ring-1 ring-gray-200 dark:bg-white/5 dark:ring-white/10">${ imgOrIcon ( p ) } </div>
@@ -357,7 +383,36 @@ <h3 class="flex flex-wrap items-center gap-2 font-semibold">${esc(p.name)}
357383 ${ p . license ?`<span title="License">⚖ ${ esc ( p . license ) } </span>` :'' }
358384 <button class="ml-auto rounded-xl bg-gradient-to-r from-brand to-purple-600 px-3 py-1.5 text-xs font-semibold text-white">Install</button>
359385 </div>
360- </div>` ) . join ( '' ) ;
386+ </div>` ;
387+ }
388+ function pkgRow ( p ) {
389+ return `
390+ <div data-id="${ esc ( p . id ) } " class="group flex cursor-pointer items-center gap-4 rounded-2xl border border-gray-200 bg-white/70 p-4 shadow-sm backdrop-blur transition hover:shadow-lg hover:shadow-brand/20 dark:border-white/10 dark:bg-white/5">
391+ <div class="inline-flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-gray-100 text-xl ring-1 ring-gray-200 dark:bg-white/5 dark:ring-white/10">${ imgOrIcon ( p ) } </div>
392+ <div class="min-w-0 flex-1">
393+ <h3 class="flex flex-wrap items-center gap-2 font-semibold">
394+ <span class="truncate">${ esc ( p . name ) } </span>
395+ <span class="rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide ${ badgeCls ( p . source ) } ">${ esc ( p . source ) } </span>
396+ <span class="text-xs font-normal text-gray-500 dark:text-gray-400">by ${ esc ( p . author ) } ${ hostOf ( p ) ?' · ' + hostOf ( p ) :'' } ${ p . basisVersion ?' · Basis ' + esc ( p . basisVersion ) :'' } </span>
397+ </h3>
398+ <p class="mt-0.5 truncate text-sm text-gray-600 dark:text-gray-300">${ esc ( p . description ) } </p>
399+ </div>
400+ <div class="hidden shrink-0 items-center gap-3 text-xs text-gray-500 md:flex dark:text-gray-400">
401+ <span>⭐ ${ fmtNum ( p . stars ) } </span>
402+ <span>⑂ ${ fmtNum ( p . forks ) } </span>
403+ <span>v${ esc ( p . version ) } </span>
404+ </div>
405+ <button class="shrink-0 rounded-xl bg-gradient-to-r from-brand to-purple-600 px-3 py-1.5 text-xs font-semibold text-white">Install</button>
406+ </div>` ;
407+ }
408+
409+ function renderGrid ( ) {
410+ if ( state . view === 'bundles' ) return renderBundles ( ) ;
411+ const grid = document . getElementById ( 'grid' ) ;
412+ grid . className = gridClass ( ) ;
413+ document . getElementById ( 'count' ) . innerHTML = `<b class="text-gray-900 dark:text-white">${ ALL . length } </b> package${ ALL . length === 1 ?'' :'s' } ` ;
414+ if ( ! ALL . length ) { grid . innerHTML = `<div class="col-span-full py-20 text-center text-gray-500 dark:text-gray-400"><p class="text-lg font-semibold text-gray-900 dark:text-white">No packages found</p><p class="mt-1">Try a different search or filter.</p></div>` ; return ; }
415+ grid . innerHTML = ALL . map ( state . layout === 'list' ? pkgRow : pkgCard ) . join ( '' ) ;
361416 grid . querySelectorAll ( '[data-id]' ) . forEach ( c => c . onclick = ( ) => openDetail ( c . dataset . id ) ) ;
362417}
363418
@@ -429,14 +484,10 @@ <h2 class="flex flex-wrap items-center gap-2 text-xl font-bold">${esc(p.name)} <
429484 m . querySelectorAll ( '.copy' ) . forEach ( b => b . onclick = e => { e . stopPropagation ( ) ; copy ( b . dataset . copy , b ) ; } ) ;
430485}
431486
432- function renderBundles ( ) {
433- const grid = document . getElementById ( 'grid' ) ;
434- document . getElementById ( 'count' ) . innerHTML = `<b class="text-gray-900 dark:text-white">${ ALL . length } </b> bundle${ ALL . length === 1 ?'' :'s' } ` ;
435- if ( ! ALL . length ) { grid . innerHTML = `<div class="col-span-full py-20 text-center text-gray-500 dark:text-gray-400"><p class="text-lg font-semibold text-gray-900 dark:text-white">No bundles yet</p><p class="mt-1">Build one from the Basis Package Manager and submit it.</p></div>` ; return ; }
436- grid . innerHTML = ALL . map ( b => {
437- const n = ( b . packages || [ ] ) . length ;
438- const basis = b . basisBranch ? `${ esc ( b . basisBranch ) } ${ b . basisCommit ?'@' + esc ( b . basisCommit ) :'' } ` : '' ;
439- return `
487+ function bundleCard ( b ) {
488+ const n = ( b . packages || [ ] ) . length ;
489+ const basis = b . basisBranch ? `${ esc ( b . basisBranch ) } ${ b . basisCommit ?'@' + esc ( b . basisCommit ) :'' } ` : '' ;
490+ return `
440491 <div data-bundle="${ esc ( b . id ) } " class="group flex cursor-pointer flex-col rounded-2xl border border-gray-200 bg-white/70 p-5 shadow-sm backdrop-blur transition hover:-translate-y-0.5 hover:shadow-lg hover:shadow-brand/20 dark:border-white/10 dark:bg-white/5">
441492 <div class="flex items-start gap-3">
442493 <div class="inline-flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-gray-100 text-xl ring-1 ring-gray-200 dark:bg-white/5 dark:ring-white/10">${ imgOrIcon ( { image :b . image , icon :b . icon || '🧩' } ) } </div>
@@ -449,7 +500,30 @@ <h3 class="flex flex-wrap items-center gap-2 font-semibold">${esc(b.name)}
449500 <p class="mt-3 line-clamp-2 text-sm text-gray-600 dark:text-gray-300">${ esc ( b . description || '' ) } </p>
450501 <div class="mt-3 flex flex-wrap gap-1.5">${ ( b . tags || [ ] ) . slice ( 0 , 4 ) . map ( t => `<span class="rounded-full bg-gray-100 px-2 py-0.5 text-[11px] text-gray-600 dark:bg-white/5 dark:text-gray-300">${ esc ( t ) } </span>` ) . join ( '' ) } </div>
451502 </div>` ;
452- } ) . join ( '' ) ;
503+ }
504+ function bundleRow ( b ) {
505+ const n = ( b . packages || [ ] ) . length ;
506+ const basis = b . basisBranch ? `${ esc ( b . basisBranch ) } ${ b . basisCommit ?'@' + esc ( b . basisCommit ) :'' } ` : '' ;
507+ return `
508+ <div data-bundle="${ esc ( b . id ) } " class="group flex cursor-pointer items-center gap-4 rounded-2xl border border-gray-200 bg-white/70 p-4 shadow-sm backdrop-blur transition hover:shadow-lg hover:shadow-brand/20 dark:border-white/10 dark:bg-white/5">
509+ <div class="inline-flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-gray-100 text-xl ring-1 ring-gray-200 dark:bg-white/5 dark:ring-white/10">${ imgOrIcon ( { image :b . image , icon :b . icon || '🧩' } ) } </div>
510+ <div class="min-w-0 flex-1">
511+ <h3 class="flex flex-wrap items-center gap-2 font-semibold">
512+ <span class="truncate">${ esc ( b . name ) } </span>
513+ <span class="rounded-full bg-brand/15 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-brand ring-1 ring-brand/30">${ n } pkg${ n === 1 ?'' :'s' } </span>
514+ <span class="text-xs font-normal text-gray-500 dark:text-gray-400">by ${ esc ( b . author || '—' ) } ${ basis ?' · Basis ' + basis :'' } </span>
515+ </h3>
516+ <p class="mt-0.5 truncate text-sm text-gray-600 dark:text-gray-300">${ esc ( b . description || '' ) } </p>
517+ </div>
518+ </div>` ;
519+ }
520+
521+ function renderBundles ( ) {
522+ const grid = document . getElementById ( 'grid' ) ;
523+ grid . className = gridClass ( ) ;
524+ document . getElementById ( 'count' ) . innerHTML = `<b class="text-gray-900 dark:text-white">${ ALL . length } </b> bundle${ ALL . length === 1 ?'' :'s' } ` ;
525+ if ( ! ALL . length ) { grid . innerHTML = `<div class="col-span-full py-20 text-center text-gray-500 dark:text-gray-400"><p class="text-lg font-semibold text-gray-900 dark:text-white">No bundles yet</p><p class="mt-1">Build one from the Basis Package Manager and submit it.</p></div>` ; return ; }
526+ grid . innerHTML = ALL . map ( state . layout === 'list' ? bundleRow : bundleCard ) . join ( '' ) ;
453527 grid . querySelectorAll ( '[data-bundle]' ) . forEach ( c => c . onclick = ( ) => openBundle ( c . dataset . bundle ) ) ;
454528}
455529
@@ -649,12 +723,14 @@ <h2 class="text-xl font-bold">${esc(b.name)}</h2>
649723document . getElementById ( 'submitBtn' ) . onclick = openSubmit ;
650724document . getElementById ( 'tabPackages' ) . onclick = ( ) => switchView ( 'packages' ) ;
651725document . getElementById ( 'tabBundles' ) . onclick = ( ) => switchView ( 'bundles' ) ;
726+ document . getElementById ( 'viewGrid' ) . onclick = ( ) => setLayout ( 'grid' ) ;
727+ document . getElementById ( 'viewList' ) . onclick = ( ) => setLayout ( 'list' ) ;
652728
653729let searchTimer ;
654730document . getElementById ( 'search' ) . oninput = e => { clearTimeout ( searchTimer ) ; state . search = e . target . value ; searchTimer = setTimeout ( load , 160 ) ; } ;
655731document . getElementById ( 'sort' ) . onchange = e => { state . sort = e . target . value ; load ( ) ; } ;
656732
657- ( async function init ( ) { tabStyle ( ) ; await fetchAll ( ) ; buildFilters ( ) ; load ( ) ; if ( location . hash === '#bundles' ) switchView ( 'bundles' ) ; } ) ( ) ;
733+ ( async function init ( ) { try { if ( localStorage . getItem ( 'basis:pkgLayout' ) === 'list' ) state . layout = 'list' ; } catch { } tabStyle ( ) ; viewStyle ( ) ; await fetchAll ( ) ; buildFilters ( ) ; load ( ) ; if ( location . hash === '#bundles' ) switchView ( 'bundles' ) ; } ) ( ) ;
658734window . addEventListener ( 'hashchange' , ( ) => switchView ( location . hash === '#bundles' ?'bundles' :'packages' ) ) ;
659735</ script >
660736
0 commit comments