index.astro (4057B)
1 --- 2 import Base from '../../layouts/Base.astro'; 3 import SectionBanner from '../../components/SectionBanner.astro'; 4 import allTools from '../../data/tools.json'; 5 import allTechniques from '../../data/techniques.json'; 6 import { toolSlug, routeSourceOf } from '../../lib/techniques'; 7 import type { Tool, Technique } from '../../lib/techniques'; 8 import { SOURCE_ROUTE, SOURCE_META, SOURCES, PLATFORM_META } from '../../lib/taxonomy'; 9 import { url } from '../../lib/url'; 10 import type { SourceId } from '../../lib/taxonomy'; 11 12 export async function getStaticPaths() { 13 const tools = allTools as unknown as Tool[]; 14 const techs = allTechniques as unknown as Technique[]; 15 return SOURCES.map((sourceId, i) => { 16 // Group by toolId namespace (deck), not per-technique source, so a 17 // daemon-authored WADComs addition lists under the WADComs deck. 18 const srcTools = tools.filter((t) => routeSourceOf(t.id) === sourceId).sort((a, b) => a.name.localeCompare(b.name)); 19 const techCount = techs.filter((t) => routeSourceOf(t.toolId) === sourceId).length; 20 return { 21 params: { source: SOURCE_ROUTE[sourceId] }, 22 props: { sourceId, tools: srcTools, techCount, n: String(i + 1).padStart(2, '0') }, 23 }; 24 }); 25 } 26 27 interface Props { sourceId: SourceId; tools: Tool[]; techCount: number; n: string } 28 const { sourceId, tools, techCount, n } = Astro.props; 29 const src = SOURCE_META[sourceId]; 30 const stats = [ 31 { label: 'Tools', value: tools.length, accent: true }, 32 { label: 'Techniques', value: techCount }, 33 { label: 'License', value: src.license }, 34 ]; 35 --- 36 <Base title={`${src.label} — DÆMONBins`} description={`${src.label}: ${src.blurb}`}> 37 <SectionBanner n={n} label={src.tag} title={src.label} blurb={src.blurb} stats={stats} accent={src.accent} /> 38 39 <div class="wrap src" style="padding:2.5rem 0 4rem;"> 40 <p class="src__lead"> 41 <a href={url('catalog')}>← Full catalog</a> 42 <span aria-hidden="true"> · </span> 43 <a href={src.homepage} target="_blank" rel="noopener">{src.homepage.replace(/^https?:\/\//, '')}</a> 44 <span aria-hidden="true"> · </span> 45 <a href={src.repo} target="_blank" rel="noopener">source</a> 46 </p> 47 48 <div class="src__grid"> 49 {tools.map((t) => ( 50 <a class="src__tool" href={url(`${SOURCE_ROUTE[sourceId]}/${toolSlug(t.id)}`)} style={`--acc: var(--${src.accent});`}> 51 <span class="src__tname">{t.name}</span> 52 <span class="src__tmeta"> 53 <span class="src__tcount">{t.count}</span> 54 {t.platform?.map((p) => <span class="src__tplat">{PLATFORM_META[p]?.short || p}</span>)} 55 </span> 56 </a> 57 ))} 58 </div> 59 </div> 60 </Base> 61 62 <style> 63 .src__lead { font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.04em; color: var(--fg-dim); margin: 0 0 1.5rem; } 64 .src__lead a { color: var(--accent); text-decoration: none; } 65 .src__lead a:hover { text-decoration: underline; text-underline-offset: 3px; } 66 .src__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 1px; background: var(--rule); border: 1px solid var(--rule); } 67 .src__tool { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; padding: 0.7rem 0.9rem; background: var(--bg); text-decoration: none; color: var(--fg); } 68 .src__tool:hover { background: var(--acc); color: var(--base); } 69 .src__tool:hover .src__tcount, .src__tool:hover .src__tplat { color: var(--base); border-color: color-mix(in srgb, var(--base) 40%, transparent); opacity: 0.85; } 70 .src__tname { font-family: var(--font-archivo); font-weight: 600; font-size: 14px; letter-spacing: -0.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 71 .src__tmeta { display: flex; align-items: center; gap: 0.3rem; flex-shrink: 0; } 72 .src__tcount { font-family: var(--font-mono); font-size: 11px; color: var(--fg-dim); } 73 .src__tplat { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.06em; color: var(--fg-faint); border: 1px solid var(--rule); padding: 0.05rem 0.25rem; } 74 </style>