@@ -88,7 +88,10 @@ async function loadList(resource) {
8888 let v = obj [ k ] ;
8989 if ( v && typeof v === "object" && v . name ) v = v . name ; // {name,slug} → name
9090 if ( k === "display" && v ) v = { size_inch : v . size_inch , refresh_hz : v . refresh_hz } ;
91- if ( k === "score" && v ) v = { overall : v . overall , performance : v . performance , camera : v . camera , cpu : v . cpu , gpu : v . gpu } ;
91+ if ( k === "score" && v ) {
92+ const ax = v . perf || v . multi || v . graphics || v . cpu || { } ;
93+ v = { overall : v . overall , tier : ax . tier , index : ax . index , source : ax . source } ;
94+ }
9295 out [ k ] = v ;
9396 }
9497 return out ;
@@ -563,63 +566,110 @@ if (resSel) populateSlugs("smartphones").then((items) => {
563566/* ============================================================
564567 FEATURED DEVICES
565568 ============================================================ */
566- const PREFERRED = [ "galaxy-s26-ultra" , "iphone-17-pro-max" , "pixel-10-pro" ,
567- "oneplus-14" , "xiaomi-15-ultra" , "galaxy-z-fold-7" ] ;
568569
569570function bar ( label , v ) {
570571 const w = v == null ? 0 : Math . round ( v ) ;
571572 return `<div class="sb"><span class="sb-l">${ label } </span>
572573 <span class="sb-track"><span class="sb-fill" data-w="${ w } "></span></span>
573574 <span class="sb-v">${ v == null ? "—" : v } </span></div>` ;
574575}
575- function deviceCard ( d ) {
576+
577+ // Per-category card shape: spec chips, score bars (label + value), subtitle, and the
578+ // primary axis whose hybrid tier/era/source is surfaced as a badge + provenance line.
579+ const withUnit = ( v , suffix = "" ) => ( v == null ? null : `${ v } ${ suffix } ` ) ;
580+ const CARD = {
581+ smartphones : {
582+ sub : ( d ) => d . soc ?. name || "" ,
583+ specs : ( d ) => [ withUnit ( d . ram_gb , "GB" ) , withUnit ( d . battery_mah , "mAh" ) ,
584+ d . display ?. size_inch ? `${ d . display . size_inch } "` : null , withUnit ( d . display ?. refresh_hz , "Hz" ) ] ,
585+ bars : ( sc ) => [ [ "Perf" , sc . performance ] , [ "Cam" , sc . camera ] , [ "Batt" , sc . battery ] , [ "Disp" , sc . display ] ] ,
586+ axis : ( sc ) => sc . perf ,
587+ } ,
588+ cpus : {
589+ sub : ( d ) => d . architecture || d . segment || "" ,
590+ specs : ( d ) => [ d . cores ? `${ d . cores } C/${ d . threads } T` : null , withUnit ( d . boost_clock_ghz , "GHz" ) ,
591+ withUnit ( d . tdp_w , "W" ) , d . process_node ] ,
592+ bars : ( sc ) => [ [ "Single" , sc . single ?. index ] , [ "Multi" , sc . multi ?. index ] ] ,
593+ axis : ( sc ) => sc . multi ,
594+ } ,
595+ gpus : {
596+ sub : ( d ) => d . architecture || "" ,
597+ specs : ( d ) => [ withUnit ( d . memory_gb , "GB" ) , d . memory_type , withUnit ( d . tdp_w , "W" ) , withUnit ( d . boost_clock_mhz , "MHz" ) ] ,
598+ bars : ( sc ) => [ [ "Graphics" , sc . graphics ?. index ] ] ,
599+ axis : ( sc ) => sc . graphics ,
600+ } ,
601+ socs : {
602+ sub : ( d ) => d . gpu_name || "" ,
603+ specs : ( d ) => [ withUnit ( d . process_nm , "nm" ) , d . gpu_name , withUnit ( d . gpu_cores , " GPU" ) , withUnit ( d . npu_tops , " TOPS" ) ] ,
604+ bars : ( sc ) => [ [ "CPU" , sc . cpu ?. index ] , [ "System" , sc . system ?. index ] ] ,
605+ axis : ( sc ) => sc . cpu ,
606+ } ,
607+ } ;
608+ const prettyBench = ( s ) => s ? s . replace ( / _ / g, " " ) . replace ( / \b ( c p u | g p u | g 3 d | f p 3 2 | r 2 3 | r 1 5 | r 1 0 | r 1 1 5 | 2 0 2 4 ) \b / gi,
609+ ( m ) => m . toUpperCase ( ) ) . replace ( / \b c i n e b e n c h \b / i, "Cinebench" ) . replace ( / \b g e e k b e n c h \b / i, "Geekbench" )
610+ . replace ( / \b p a s s m a r k \b / i, "PassMark" ) . replace ( / \b a n t u t u s c o r e \b / i, "AnTuTu" ) . replace ( / \b t i m e s p y \b / i, "Time Spy" ) : "" ;
611+
612+ function deviceCard ( d , category = "smartphones" ) {
613+ const cfg = CARD [ category ] || CARD . smartphones ;
576614 const sc = d . score || { } ;
577615 const overall = sc . overall == null ? "—" : Math . round ( sc . overall ) ;
578- const initial = ( d . brand ?. name || d . name || "?" ) . charAt ( 0 ) . toUpperCase ( ) ;
579- const specs = [
580- d . ram_gb ? ` ${ d . ram_gb } GB` : null ,
581- d . battery_mah ? ` ${ d . battery_mah } mAh` : null ,
582- d . display ?. size_inch ? `${ d . display . size_inch } " ` : null ,
583- d . display ?. refresh_hz ? `${ d . display . refresh_hz } Hz ` : null ,
584- ] . filter ( Boolean ) ;
616+ const brandName = d . brand ?. name || d . manufacturer ?. name || "" ;
617+ const initial = ( brandName || d . name || "?" ) . charAt ( 0 ) . toUpperCase ( ) ;
618+ const specs = cfg . specs ( d ) . filter ( Boolean ) ;
619+ const axis = cfg . axis ( sc ) || { } ;
620+ const tier = axis . tier ? `<span class="tier tier- ${ esc ( axis . tier ) } "> ${ esc ( axis . tier ) } </span> ` : "" ;
621+ const era = axis . era ? `<span class="chip chip-era"> ${ esc ( axis . era ) } </span> ` : "" ;
622+ const src = axis . source ? `<div class="card-src">via ${ esc ( prettyBench ( axis . source ) ) } </div>` : "" ;
585623 const el = document . createElement ( "article" ) ;
586624 el . className = "card" ; el . dataset . slug = d . slug ;
587625 el . innerHTML = `
588626 <div class="card-top">
589627 <div class="thumb"><span class="thumb-fallback">${ esc ( initial ) } </span></div>
590628 <div class="card-id">
591- <div class="card-brand">${ esc ( d . brand ?. name || "" ) } </div>
629+ <div class="card-brand">${ esc ( brandName ) } </div>
592630 <div class="card-name">${ esc ( d . name ) } </div>
593- <div class="card-soc">${ esc ( d . soc ?. name || "" ) } </div>
631+ <div class="card-soc">${ esc ( cfg . sub ( d ) ) } </div>
594632 </div>
595633 <div class="ring" style="--p:${ sc . overall || 0 } "><b>${ overall } </b><i>score</i></div>
596634 </div>
597- <div class="chips">${ specs . map ( ( s ) => `<span class="chip">${ esc ( s ) } </span>` ) . join ( "" ) } </div>
598- <div class="bars">${ bar ( "Perf" , sc . performance ) } ${ bar ( "Cam" , sc . camera ) } ${ bar ( "Batt" , sc . battery ) } ${ bar ( "Disp" , sc . display ) } </div>` ;
635+ <div class="chips">${ tier } ${ era } ${ specs . map ( ( s ) => `<span class="chip">${ esc ( s ) } </span>` ) . join ( "" ) } </div>
636+ <div class="bars">${ cfg . bars ( sc ) . map ( ( [ l , v ] ) => bar ( l , v ) ) . join ( "" ) } </div>${ src } ` ;
599637 if ( d . image_url ) {
600638 const img = new Image ( ) ;
601639 img . src = d . image_url ; img . alt = d . name ; img . loading = "lazy" ; img . className = "thumb-img" ;
602640 img . onload = ( ) => el . querySelector ( ".thumb" ) . appendChild ( img ) ;
603641 }
604642 el . addEventListener ( "click" , ( ) => {
605- resSel . value = "smartphones" ; slugIn . value = d . slug ; run ( "smartphones" , d . slug ) ;
643+ resSel . value = category ; slugIn . value = d . slug ; run ( category , d . slug ) ;
606644 document . getElementById ( "playground" ) . scrollIntoView ( { behavior : "smooth" } ) ;
607645 } ) ;
608646 return el ;
609647}
610648
649+ // A cross-category showcase so the scoring is visible across phones + CPU + GPU + SoC.
650+ const FEATURED = [
651+ { cat : "smartphones" , slug : "galaxy-s26-ultra" } ,
652+ { cat : "cpus" , slug : "core-i9-14900k" } ,
653+ { cat : "gpus" , slug : "geforce-rtx-5090" } ,
654+ { cat : "smartphones" , slug : "iphone-17-pro-max" } ,
655+ { cat : "socs" , slug : "snapdragon-8-elite" } ,
656+ { cat : "cpus" , slug : "ryzen-9-7950x" } ,
657+ ] ;
611658( async function featured ( ) {
612659 const cards = document . getElementById ( "cards" ) ;
613660 if ( ! cards ) return ;
614661 try {
615- const items = await loadList ( "smartphones" ) ;
616- const have = new Set ( items . map ( ( i ) => i . slug ) ) ;
617- let slugs = PREFERRED . filter ( ( s ) => have . has ( s ) ) ;
618- for ( const it of items ) { if ( slugs . length >= 6 ) break ; if ( ! slugs . includes ( it . slug ) ) slugs . push ( it . slug ) ; }
619- const details = await Promise . all ( slugs . slice ( 0 , 6 ) . map ( ( s ) =>
620- getJSON ( `v1/smartphones/${ s } /index.json` ) . catch ( ( ) => null ) ) ) ;
662+ let picks = await Promise . all ( FEATURED . map ( ( f ) =>
663+ getJSON ( `v1/${ f . cat } /${ f . slug } /index.json` ) . then ( ( d ) => ( { d, cat : f . cat } ) ) . catch ( ( ) => null ) ) ) ;
664+ picks = picks . filter ( Boolean ) ;
665+ if ( ! picks . length ) { // fallback: first few phones if the curated slugs are absent
666+ const items = await loadList ( "smartphones" ) ;
667+ const details = await Promise . all ( items . slice ( 0 , 6 ) . map ( ( it ) =>
668+ getJSON ( `v1/smartphones/${ it . slug } /index.json` ) . then ( ( d ) => ( { d, cat : "smartphones" } ) ) . catch ( ( ) => null ) ) ) ;
669+ picks = details . filter ( Boolean ) ;
670+ }
621671 cards . innerHTML = "" ;
622- details . filter ( Boolean ) . forEach ( ( d ) => cards . appendChild ( deviceCard ( d ) ) ) ;
672+ picks . forEach ( ( { d , cat } ) => cards . appendChild ( deviceCard ( d , cat ) ) ) ;
623673 if ( ! cards . children . length ) cards . innerHTML = '<p class="muted">Build the dataset to see featured devices.</p>' ;
624674 const obs = new IntersectionObserver ( ( es ) => es . forEach ( ( e ) => {
625675 if ( ! e . isIntersecting ) return ;
0 commit comments