@@ -278,6 +278,136 @@ export function buildFlowChain(category) {
278278 return chain . length >= 2 ? chain : null ;
279279}
280280
281+ // ─── Use-cases page ──────────────────────────────────────────────────────────
282+
283+ const USE_CASE_FALLBACK_COLOUR = '#888' ;
284+
285+ /**
286+ * Build a lookup of repo name -> { github, npm } from the catalogue data so the
287+ * use-cases page can link each stack item back to its repo without duplicating URLs.
288+ */
289+ export function buildRepoIndex ( catalogueData ) {
290+ const index = { } ;
291+ for ( const cat of catalogueData . categories ) {
292+ for ( const repo of cat . repos ) {
293+ index [ repo . name ] = { github : repo . github , npm : repo . npm } ;
294+ }
295+ }
296+ return index ;
297+ }
298+
299+ /**
300+ * Returns { useCases, categories, repos } counts for the use-cases hero.
301+ */
302+ export function computeUseCaseStats ( useCasesData ) {
303+ const repos = new Set ( ) ;
304+ for ( const uc of useCasesData . useCases ) {
305+ for ( const item of uc . stack ) repos . add ( item . repo ) ;
306+ }
307+ return {
308+ useCases : useCasesData . useCases . length ,
309+ categories : useCasesData . categories . length ,
310+ repos : repos . size ,
311+ } ;
312+ }
313+
314+ /**
315+ * Returns HTML for <!-- USE_CASES_HERO_STATS -->
316+ */
317+ export function buildUseCasesHeroStatsHtml ( useCasesData ) {
318+ const { useCases, categories, repos } = computeUseCaseStats ( useCasesData ) ;
319+ const stat = ( value , label ) =>
320+ `<div class="stat"><span class="stat-value">${ value } </span><span class="stat-label">${ label } </span></div>` ;
321+ return [
322+ stat ( useCases , 'end-to-end workflows' ) ,
323+ stat ( categories , 'problem domains' ) ,
324+ stat ( repos , 'building blocks used' ) ,
325+ ] . join ( '\n' ) ;
326+ }
327+
328+ /**
329+ * Returns HTML for <!-- USE_CASE_FILTERS -->
330+ */
331+ export function buildUseCaseFiltersHtml ( useCasesData ) {
332+ const counts = { } ;
333+ for ( const uc of useCasesData . useCases ) {
334+ counts [ uc . category ] = ( counts [ uc . category ] || 0 ) + 1 ;
335+ }
336+ const pill = ( slug , label , count , colour ) =>
337+ `<button class="uc-filter" data-filter="${ escHtml ( slug ) } "${ colour ? ` style="--uc-colour: ${ colour } "` : '' } type="button">${ escHtml ( label ) } <span class="uc-filter-count">${ count } </span></button>` ;
338+ const all = `<button class="uc-filter is-active" data-filter="all" type="button">All <span class="uc-filter-count">${ useCasesData . useCases . length } </span></button>` ;
339+ const cats = useCasesData . categories
340+ . filter ( c => counts [ c . slug ] )
341+ . map ( c => pill ( c . slug , c . name , counts [ c . slug ] , c . colour ) ) ;
342+ return [ all , ...cats ] . join ( '\n' ) ;
343+ }
344+
345+ /**
346+ * Build one use-case card.
347+ */
348+ function buildUseCaseCard ( uc , colour , repoIndex ) {
349+ const steps = uc . steps . map ( s => ` <li>${ escHtml ( s ) } </li>` ) . join ( '\n' ) ;
350+
351+ const stack = uc . stack
352+ . map ( item => {
353+ const meta = repoIndex [ item . repo ] ;
354+ const inner = `<span class="uc-chip-name">${ escHtml ( item . repo ) } </span><span class="uc-chip-role">${ escHtml ( item . role ) } </span>` ;
355+ return meta && meta . github
356+ ? ` <a class="uc-chip" href="${ escHtml ( meta . github ) } " target="_blank" rel="noopener noreferrer">${ inner } </a>`
357+ : ` <span class="uc-chip">${ inner } </span>` ;
358+ } )
359+ . join ( '\n' ) ;
360+
361+ const protos = ( uc . protocols || [ ] )
362+ . map ( p => ` <span class="uc-proto">${ escHtml ( p ) } </span>` )
363+ . join ( '\n' ) ;
364+
365+ return `<article class="uc-card" data-category="${ escHtml ( uc . category ) } " id="${ escHtml ( uc . id ) } " style="--uc-colour: ${ colour } ">
366+ <div class="uc-card-accent"></div>
367+ <span class="uc-persona">${ escHtml ( uc . persona ) } </span>
368+ <h3 class="uc-title">${ escHtml ( uc . title ) } </h3>
369+ <p class="uc-problem">${ escHtml ( uc . problem ) } </p>
370+ <p class="uc-outcome">${ escHtml ( uc . outcome ) } </p>
371+ <div class="uc-label">Workflow</div>
372+ <ol class="uc-steps">
373+ ${ steps }
374+ </ol>
375+ <div class="uc-label">Built with</div>
376+ <div class="uc-stack">
377+ ${ stack }
378+ </div>
379+ <div class="uc-protos">
380+ ${ protos }
381+ </div>
382+ </article>` ;
383+ }
384+
385+ /**
386+ * Returns HTML for <!-- USE_CASE_SECTIONS -->
387+ */
388+ export function buildUseCaseSectionsHtml ( useCasesData , repoIndex = { } ) {
389+ const byCat = { } ;
390+ for ( const uc of useCasesData . useCases ) {
391+ ( byCat [ uc . category ] = byCat [ uc . category ] || [ ] ) . push ( uc ) ;
392+ }
393+ return useCasesData . categories
394+ . filter ( cat => byCat [ cat . slug ] && byCat [ cat . slug ] . length )
395+ . map ( cat => {
396+ const colour = cat . colour || USE_CASE_FALLBACK_COLOUR ;
397+ const cards = byCat [ cat . slug ] . map ( uc => buildUseCaseCard ( uc , colour , repoIndex ) ) . join ( '\n' ) ;
398+ return `<section class="uc-group" data-category="${ escHtml ( cat . slug ) } " style="--uc-colour: ${ colour } ">
399+ <div class="uc-group-head">
400+ <span class="section-label" style="color: ${ colour } ">${ escHtml ( cat . name ) } </span>
401+ <p class="uc-group-desc">${ escHtml ( cat . description ) } </p>
402+ </div>
403+ <div class="uc-grid">
404+ ${ cards }
405+ </div>
406+ </section>` ;
407+ } )
408+ . join ( '\n\n' ) ;
409+ }
410+
281411// ─── Main build function ───────────────────────────────────────────────────────
282412
283413export async function build ( jsonPath ) {
@@ -299,10 +429,34 @@ export async function build(jsonPath) {
299429 console . log ( `Built site/index.html -- ${ repos } repos, ${ stacks } categories, ${ npmPackages } npm packages` ) ;
300430}
301431
432+ /**
433+ * Build site/use-cases.html from the use-cases JSON + the catalogue (for repo links).
434+ */
435+ export async function buildUseCasesPage ( catalogueJsonPath , useCasesJsonPath ) {
436+ const resolvedCatalogue = catalogueJsonPath || join ( ROOT , 'forgesworn-repos.json' ) ;
437+ const resolvedUseCases = useCasesJsonPath || join ( ROOT , 'forgesworn-use-cases.json' ) ;
438+ const templatePath = join ( ROOT , 'site' , 'use-cases-template.html' ) ;
439+ const outputPath = join ( ROOT , 'site' , 'use-cases.html' ) ;
440+
441+ const catalogueData = JSON . parse ( readFileSync ( resolvedCatalogue , 'utf8' ) ) ;
442+ const useCasesData = JSON . parse ( readFileSync ( resolvedUseCases , 'utf8' ) ) ;
443+ const repoIndex = buildRepoIndex ( catalogueData ) ;
444+
445+ let template = readFileSync ( templatePath , 'utf8' ) ;
446+ template = template . replace ( '<!-- USE_CASES_HERO_STATS -->' , buildUseCasesHeroStatsHtml ( useCasesData ) ) ;
447+ template = template . replace ( '<!-- USE_CASE_FILTERS -->' , buildUseCaseFiltersHtml ( useCasesData ) ) ;
448+ template = template . replace ( '<!-- USE_CASE_SECTIONS -->' , buildUseCaseSectionsHtml ( useCasesData , repoIndex ) ) ;
449+
450+ writeFileSync ( outputPath , template , 'utf8' ) ;
451+
452+ const { useCases, categories } = computeUseCaseStats ( useCasesData ) ;
453+ console . log ( `Built site/use-cases.html -- ${ useCases } use cases, ${ categories } categories` ) ;
454+ }
455+
302456// Only run when executed directly (not when imported by tests)
303457const isMain = process . argv [ 1 ] && fileURLToPath ( import . meta. url ) === process . argv [ 1 ] ;
304458if ( isMain ) {
305- build ( process . argv [ 2 ] ) . catch ( err => {
459+ Promise . all ( [ build ( process . argv [ 2 ] ) , buildUseCasesPage ( ) ] ) . catch ( err => {
306460 console . error ( err ) ;
307461 process . exit ( 1 ) ;
308462 } ) ;
0 commit comments