@@ -125,13 +125,23 @@ <h4 class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-400">Bun
125125 < 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 ">
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+ < 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 ">
130+ < option value ="popular "> Sort: Popular</ option >
131+ < option value ="stars "> Most stars</ option >
132+ < option value ="forks "> Most forks</ option >
133+ < option value ="updated "> Recently updated</ option >
134+ < option value ="name "> Name (A–Z)</ option >
135+ </ select >
136+ < div class ="inline-flex rounded-xl border border-gray-200 bg-white/70 p-1 shadow-sm dark:border-white/10 dark:bg-white/5 ">
137+ < button id ="layoutGrid " type ="button " title ="Grid view " aria-label ="Grid view " class ="rounded-lg px-2.5 py-1.5 ">
138+ < 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 "/> < rect x ="14 " y ="3 " width ="7 " height ="7 " rx ="1 "/> < rect x ="3 " y ="14 " width ="7 " height ="7 " rx ="1 "/> < rect x ="14 " y ="14 " width ="7 " height ="7 " rx ="1 "/> </ svg >
139+ </ button >
140+ < button id ="layoutList " type ="button " title ="List view " aria-label ="List view " class ="rounded-lg px-2.5 py-1.5 ">
141+ < 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 ="4 " cy ="6 " r ="1 "/> < circle cx ="4 " cy ="12 " r ="1 "/> < circle cx ="4 " cy ="18 " r ="1 "/> </ svg >
142+ </ button >
143+ </ div >
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,8 @@ <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" } ;
162+ try { const saved = localStorage . getItem ( 'pkgLayout' ) ; if ( saved === 'list' || saved === 'grid' ) state . layout = saved ; } catch { }
152163const REPO = "BasisVR/BasisPackageManager" ;
153164let RAW = [ ] , ALL = [ ] , RAWB = [ ] , bundlesLoaded = false ;
154165
@@ -257,6 +268,21 @@ <h4 class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-400">Bun
257268 document . getElementById ( 'tabBundles' ) . className = state . view === 'bundles' ? on : off ;
258269}
259270
271+ function layoutStyle ( ) {
272+ const on = 'rounded-lg px-2.5 py-1.5 bg-gradient-to-r from-brand to-purple-600 text-white shadow' ;
273+ const off = 'rounded-lg px-2.5 py-1.5 text-gray-500 dark:text-gray-300 hover:text-brand' ;
274+ document . getElementById ( 'layoutGrid' ) . className = state . layout === 'grid' ? on : off ;
275+ document . getElementById ( 'layoutList' ) . className = state . layout === 'list' ? on : off ;
276+ }
277+
278+ function setLayout ( layout ) {
279+ if ( state . layout === layout ) return ;
280+ state . layout = layout ;
281+ try { localStorage . setItem ( 'pkgLayout' , layout ) ; } catch { }
282+ layoutStyle ( ) ;
283+ renderGrid ( ) ; // ALL is already filtered/sorted — just re-render the current view in the new layout
284+ }
285+
260286async function switchView ( view ) {
261287 if ( state . view === view ) return ;
262288 state . view = view ;
@@ -333,12 +359,13 @@ <h4 class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-400">Bun
333359 document . querySelectorAll ( '[data-category]' ) . forEach ( el => el . onclick = ( ) => { state . category = el . dataset . category ; buildFilters ( ) ; load ( ) ; } ) ;
334360}
335361
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 => `
362+ // --- Results layout (grid cards vs. list rows), shared by packages and bundles ---
363+ const GRID_CLS = 'grid gap-4 grid-cols-[repeat(auto-fill,minmax(18rem,1fr))]' ;
364+ const LIST_CLS = 'flex flex-col gap-3' ;
365+ const gridCls = ( ) => state . layout === 'list' ? LIST_CLS : GRID_CLS ;
366+ const emptyState = ( title , sub ) => `<div class="col-span-full w-full py-20 text-center text-gray-500 dark:text-gray-400"><p class="text-lg font-semibold text-gray-900 dark:text-white">${ title } </p><p class="mt-1">${ sub } </p></div>` ;
367+
368+ const pkgCard = p => `
342369 <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">
343370 <div class="flex items-start gap-3">
344371 <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 +384,66 @@ <h3 class="flex flex-wrap items-center gap-2 font-semibold">${esc(p.name)}
357384 ${ p . license ?`<span title="License">⚖ ${ esc ( p . license ) } </span>` :'' }
358385 <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>
359386 </div>
360- </div>` ) . join ( '' ) ;
387+ </div>` ;
388+
389+ const pkgRow = p => `
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">${ esc ( p . name ) }
394+ <span class="rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide ${ badgeCls ( p . source ) } ">${ esc ( p . source ) } </span></h3>
395+ <p class="truncate text-xs text-gray-500 dark:text-gray-400">by ${ esc ( p . author ) } ${ hostOf ( p ) ?' · ' + hostOf ( p ) :'' } ${ p . basisVersion ?' · Basis ' + esc ( p . basisVersion ) :'' } </p>
396+ <p class="mt-0.5 line-clamp-1 text-sm text-gray-600 dark:text-gray-300">${ esc ( p . description ) } </p>
397+ </div>
398+ <div class="hidden shrink-0 items-center gap-3 text-xs text-gray-500 dark:text-gray-400 sm:flex">
399+ <span>⭐ ${ fmtNum ( p . stars ) } </span>
400+ <span>⑂ ${ fmtNum ( p . forks ) } </span>
401+ <span>v${ esc ( p . version ) } </span>
402+ ${ p . license ?`<span title="License">⚖ ${ esc ( p . license ) } </span>` :'' }
403+ </div>
404+ <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>
405+ </div>` ;
406+
407+ const bundleCard = b => {
408+ const n = ( b . packages || [ ] ) . length ;
409+ const basis = b . basisBranch ? `${ esc ( b . basisBranch ) } ${ b . basisCommit ?'@' + esc ( b . basisCommit ) :'' } ` : '' ;
410+ return `
411+ <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">
412+ <div class="flex items-start gap-3">
413+ <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>
414+ <div class="min-w-0">
415+ <h3 class="flex flex-wrap items-center gap-2 font-semibold">${ esc ( b . name ) }
416+ <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></h3>
417+ <p class="text-xs text-gray-500 dark:text-gray-400">by ${ esc ( b . author || '—' ) } ${ basis ?' · Basis ' + basis :'' } </p>
418+ </div>
419+ </div>
420+ <p class="mt-3 line-clamp-2 text-sm text-gray-600 dark:text-gray-300">${ esc ( b . description || '' ) } </p>
421+ <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>
422+ </div>` ;
423+ } ;
424+
425+ const bundleRow = b => {
426+ const n = ( b . packages || [ ] ) . length ;
427+ const basis = b . basisBranch ? `${ esc ( b . basisBranch ) } ${ b . basisCommit ?'@' + esc ( b . basisCommit ) :'' } ` : '' ;
428+ return `
429+ <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">
430+ <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>
431+ <div class="min-w-0 flex-1">
432+ <h3 class="flex flex-wrap items-center gap-2 font-semibold">${ esc ( b . name ) }
433+ <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></h3>
434+ <p class="truncate text-xs text-gray-500 dark:text-gray-400">by ${ esc ( b . author || '—' ) } ${ basis ?' · Basis ' + basis :'' } </p>
435+ <p class="mt-0.5 line-clamp-1 text-sm text-gray-600 dark:text-gray-300">${ esc ( b . description || '' ) } </p>
436+ </div>
437+ </div>` ;
438+ } ;
439+
440+ function renderGrid ( ) {
441+ if ( state . view === 'bundles' ) return renderBundles ( ) ;
442+ const grid = document . getElementById ( 'grid' ) ;
443+ grid . className = gridCls ( ) ;
444+ document . getElementById ( 'count' ) . innerHTML = `<b class="text-gray-900 dark:text-white">${ ALL . length } </b> package${ ALL . length === 1 ?'' :'s' } ` ;
445+ if ( ! ALL . length ) { grid . innerHTML = emptyState ( 'No packages found' , 'Try a different search or filter.' ) ; return ; }
446+ grid . innerHTML = ALL . map ( p => state . layout === 'list' ? pkgRow ( p ) : pkgCard ( p ) ) . join ( '' ) ;
361447 grid . querySelectorAll ( '[data-id]' ) . forEach ( c => c . onclick = ( ) => openDetail ( c . dataset . id ) ) ;
362448}
363449
@@ -431,25 +517,10 @@ <h2 class="flex flex-wrap items-center gap-2 text-xl font-bold">${esc(p.name)} <
431517
432518function renderBundles ( ) {
433519 const grid = document . getElementById ( 'grid' ) ;
520+ grid . className = gridCls ( ) ;
434521 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 `
440- <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">
441- <div class="flex items-start gap-3">
442- <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>
443- <div class="min-w-0">
444- <h3 class="flex flex-wrap items-center gap-2 font-semibold">${ esc ( b . name ) }
445- <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></h3>
446- <p class="text-xs text-gray-500 dark:text-gray-400">by ${ esc ( b . author || '—' ) } ${ basis ?' · Basis ' + basis :'' } </p>
447- </div>
448- </div>
449- <p class="mt-3 line-clamp-2 text-sm text-gray-600 dark:text-gray-300">${ esc ( b . description || '' ) } </p>
450- <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>
451- </div>` ;
452- } ) . join ( '' ) ;
522+ if ( ! ALL . length ) { grid . innerHTML = emptyState ( 'No bundles yet' , 'Build one from the Basis Package Manager and submit it.' ) ; return ; }
523+ grid . innerHTML = ALL . map ( b => state . layout === 'list' ? bundleRow ( b ) : bundleCard ( b ) ) . join ( '' ) ;
453524 grid . querySelectorAll ( '[data-bundle]' ) . forEach ( c => c . onclick = ( ) => openBundle ( c . dataset . bundle ) ) ;
454525}
455526
@@ -649,12 +720,14 @@ <h2 class="text-xl font-bold">${esc(b.name)}</h2>
649720document . getElementById ( 'submitBtn' ) . onclick = openSubmit ;
650721document . getElementById ( 'tabPackages' ) . onclick = ( ) => switchView ( 'packages' ) ;
651722document . getElementById ( 'tabBundles' ) . onclick = ( ) => switchView ( 'bundles' ) ;
723+ document . getElementById ( 'layoutGrid' ) . onclick = ( ) => setLayout ( 'grid' ) ;
724+ document . getElementById ( 'layoutList' ) . onclick = ( ) => setLayout ( 'list' ) ;
652725
653726let searchTimer ;
654727document . getElementById ( 'search' ) . oninput = e => { clearTimeout ( searchTimer ) ; state . search = e . target . value ; searchTimer = setTimeout ( load , 160 ) ; } ;
655728document . getElementById ( 'sort' ) . onchange = e => { state . sort = e . target . value ; load ( ) ; } ;
656729
657- ( async function init ( ) { tabStyle ( ) ; await fetchAll ( ) ; buildFilters ( ) ; load ( ) ; if ( location . hash === '#bundles' ) switchView ( 'bundles' ) ; } ) ( ) ;
730+ ( async function init ( ) { tabStyle ( ) ; layoutStyle ( ) ; await fetchAll ( ) ; buildFilters ( ) ; load ( ) ; if ( location . hash === '#bundles' ) switchView ( 'bundles' ) ; } ) ( ) ;
658731window . addEventListener ( 'hashchange' , ( ) => switchView ( location . hash === '#bundles' ?'bundles' :'packages' ) ) ;
659732</ script >
660733
0 commit comments