ux: build the Findings region (12 card variants + StoneRegion + run-health)
Browse filesFirst substantial commit of the v0.4.4 design pass. Ports
docs/design_handoff/design_files/findings.jsx (the centerpiece of the
handoff) into idiomatic Svelte 5 — runes only, scoped styles, no React
or Tailwind.
lib/types/card.ts
Card schema mirroring the handoff's TypeScript-spec'd shape: 12
body variants, register rows, scalar cells, forecast bands,
comparison sides, meta rows, plus StoneTrace + StoneMember for
provenance.
lib/components/findings/cards/ (12 body variants)
HeadlineBody · TabularBody · ScalarsBody · SparkBody (covers spark
+ histogram) · TimeseriesBody · ForecastBody · RasterBody (+
RasterThumb hand-drawn SVGs for stormwater / stormwater-dry /
prithvi / lulc / buildings) · RegisterBody · ComparisonBody ·
MetaBody. CardBody.svelte dispatches by variant.
lib/components/findings/FindingCard.svelte
Chrome wrapper. Header (tier glyph + source · vintage), title,
body slot, footer (docId + tier badge + cite arrow). Synthetic /
illustrative / comparison cards get a 1px dashed top-rule.
Cards with mapLayer render as <button role=button> for keyboard
+ hover linking; others as <article>.
lib/components/findings/StoneRegion.svelte
Stone header (serif italic name + role · tag · run-tally chip),
smart-default provenance toggle (collapsed if all-ok, expanded on
warn/error/silent), 12-col card grid. Capstone uses a 6-col
capstone-rail variant.
lib/components/findings/StoneTally.svelte
Run-tally chip: card count, fired count, silent / warn / error
(when nonzero), heaviest-specialist runtime.
lib/components/findings/ProvenanceTrace.svelte
Indented specialist tree, recursive via self-import (Svelte 5
deprecates svelte:self). Status pip in tier-color or warn/error.
lib/components/findings/RunHealthStrip.svelte
Top-of-Findings status row: Stones · functions fired · cards ·
wall-clock · cache-hit (when supplied) · silent / warn / error.
lib/components/findings/CardGrammarReference.svelte
Dev-only catalog of all 12 card variants. Gated on caller-passed
showGrammar prop.
lib/components/findings/FindingsRegion.svelte
Composer: RunHealthStrip + 5 StoneRegions in canonical order
(cornerstone → keystone → touchstone → lodestone → capstone) +
optional CardGrammarReference. Lifts linkedKey through props so
the page route owns the cross-link state.
lib/data/findingsSample.ts
Red Hook sample fixture mirroring findings.jsx CARDS table.
Drives routes/q/sample so the design surface can be reviewed
without a running FSM.
Pre-existing type errors in Briefing.svelte / RipMap.svelte /
q/[queryId]/+page.svelte / TraceRow.svelte are unrelated to this
branch and not addressed here. Zero new errors / warnings introduced.
Wiring into the sample + live routes lands in the next commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- web/sveltekit/src/lib/components/findings/CardGrammarReference.svelte +223 -0
- web/sveltekit/src/lib/components/findings/FindingCard.svelte +228 -0
- web/sveltekit/src/lib/components/findings/FindingsRegion.svelte +116 -0
- web/sveltekit/src/lib/components/findings/ProvenanceTrace.svelte +97 -0
- web/sveltekit/src/lib/components/findings/RunHealthStrip.svelte +81 -0
- web/sveltekit/src/lib/components/findings/StoneRegion.svelte +233 -0
- web/sveltekit/src/lib/components/findings/StoneTally.svelte +70 -0
- web/sveltekit/src/lib/components/findings/cards/CardBody.svelte +52 -0
- web/sveltekit/src/lib/components/findings/cards/ComparisonBody.svelte +92 -0
- web/sveltekit/src/lib/components/findings/cards/ForecastBody.svelte +51 -0
- web/sveltekit/src/lib/components/findings/cards/HeadlineBody.svelte +41 -0
- web/sveltekit/src/lib/components/findings/cards/MetaBody.svelte +56 -0
- web/sveltekit/src/lib/components/findings/cards/RasterBody.svelte +56 -0
- web/sveltekit/src/lib/components/findings/cards/RasterThumb.svelte +106 -0
- web/sveltekit/src/lib/components/findings/cards/RegisterBody.svelte +83 -0
- web/sveltekit/src/lib/components/findings/cards/ScalarsBody.svelte +48 -0
- web/sveltekit/src/lib/components/findings/cards/SparkBody.svelte +63 -0
- web/sveltekit/src/lib/components/findings/cards/TabularBody.svelte +55 -0
- web/sveltekit/src/lib/components/findings/cards/TimeseriesBody.svelte +108 -0
- web/sveltekit/src/lib/data/findingsSample.ts +235 -0
- web/sveltekit/src/lib/types/card.ts +201 -0
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { Card, Density } from '$lib/types/card';
|
| 3 |
+
import FindingCard from './FindingCard.svelte';
|
| 4 |
+
|
| 5 |
+
/** Dev-only catalog of all 12 card variants. Gated on import.meta.env.DEV
|
| 6 |
+
* + ?grammar=1 in the page URL. Useful for spot-checking visual fidelity
|
| 7 |
+
* while iterating on the variant components. */
|
| 8 |
+
let { density = 'comfortable' as Density }: { density?: Density } = $props();
|
| 9 |
+
|
| 10 |
+
// One stub per variant. Mirrors findings.jsx GRAMMAR_STUBS.
|
| 11 |
+
const STUBS: Card[] = [
|
| 12 |
+
{
|
| 13 |
+
id: 'grm-headline', stone: 'cornerstone', tier: 'modeled', variant: 'headline',
|
| 14 |
+
source: 'FEMA', agency: 'spec', vintage: 'spec',
|
| 15 |
+
title: 'Single big number, scenario-tagged',
|
| 16 |
+
headline: 'Zone AE', subhead: 'preliminary FIRM, panel ID',
|
| 17 |
+
sub: 'Use when the answer is one categorical state.',
|
| 18 |
+
docId: 'DS-HEADLINE',
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
id: 'grm-tabular', stone: 'cornerstone', tier: 'empirical', variant: 'tabular',
|
| 22 |
+
source: 'USGS', agency: 'spec', vintage: 'spec',
|
| 23 |
+
title: 'Small table of observations',
|
| 24 |
+
columns: ['id', 'value', 'dist.'],
|
| 25 |
+
rows: [
|
| 26 |
+
['ROW-001', '1.2 m', '0.18 mi'],
|
| 27 |
+
['ROW-002', '0.9 m', '0.32 mi'],
|
| 28 |
+
['ROW-003', '0.7 m', '0.41 mi'],
|
| 29 |
+
],
|
| 30 |
+
sub: 'Use when 3–8 records each carry the same fields.',
|
| 31 |
+
docId: 'DS-TABULAR',
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
id: 'grm-scalars', stone: 'touchstone', tier: 'empirical', variant: 'scalars',
|
| 35 |
+
source: 'NWS', agency: 'spec', vintage: 'spec',
|
| 36 |
+
title: 'Trio of scalar readings',
|
| 37 |
+
scalars: [
|
| 38 |
+
{ value: '0.02 in', label: 'precip · 24h' },
|
| 39 |
+
{ value: '11 mph', label: 'wind' },
|
| 40 |
+
{ value: '63°F', label: 'temp' },
|
| 41 |
+
],
|
| 42 |
+
sub: 'Use for current-state dashboards.',
|
| 43 |
+
docId: 'DS-SCALARS',
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
id: 'grm-spark', stone: 'touchstone', tier: 'empirical', variant: 'spark',
|
| 47 |
+
source: 'FloodNet', agency: 'spec', vintage: 'spec',
|
| 48 |
+
title: 'Sparkline of recent events',
|
| 49 |
+
headline: 'n events', subhead: 'window · peak',
|
| 50 |
+
spark: [1, 2, 4, 3, 7, 12, 8, 5, 3, 2, 4, 9, 6],
|
| 51 |
+
docId: 'DS-SPARK',
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
id: 'grm-histogram', stone: 'touchstone', tier: 'proxy', variant: 'histogram',
|
| 55 |
+
source: 'NYC 311', agency: 'spec', vintage: 'spec',
|
| 56 |
+
title: 'Histogram of binned counts',
|
| 57 |
+
headline: 'n calls', subhead: 'window · seasonal note',
|
| 58 |
+
histogram: [3, 2, 1, 0, 1, 4, 7, 12, 18, 11, 5, 3, 4, 2, 1, 0, 2, 3, 8, 9, 4, 2, 1, 0],
|
| 59 |
+
docId: 'DS-HIST',
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
id: 'grm-timeseries', stone: 'lodestone', tier: 'modeled', variant: 'timeseries',
|
| 63 |
+
source: 'Granite TTM', agency: 'spec', vintage: 'spec',
|
| 64 |
+
title: 'Forecast curve with horizon',
|
| 65 |
+
headline: '+0.41 m peak', subhead: '+38h · 90% CI',
|
| 66 |
+
timeseries: { hours: 96, peak: { x: 38, y: 41 }, peakLabel: '+0.41 m' },
|
| 67 |
+
spatialNote: 'regional',
|
| 68 |
+
sub: 'Spatial-index callout when station ≠ point-of-query.',
|
| 69 |
+
docId: 'DS-TS',
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
id: 'grm-forecast', stone: 'lodestone', tier: 'modeled', variant: 'forecast',
|
| 73 |
+
source: 'NPCC4', agency: 'spec', vintage: 'spec',
|
| 74 |
+
title: 'Long-horizon scenario projections',
|
| 75 |
+
forecast: [
|
| 76 |
+
{ year: 2030, low: 4, mid: 6, high: 9 },
|
| 77 |
+
{ year: 2050, low: 13, mid: 22, high: 30 },
|
| 78 |
+
{ year: 2100, low: 38, mid: 71, high: 114 },
|
| 79 |
+
],
|
| 80 |
+
sub: 'Use for decadal+ uncertainty cones.',
|
| 81 |
+
docId: 'DS-FCST',
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
id: 'grm-raster', stone: 'cornerstone', tier: 'modeled', variant: 'raster',
|
| 85 |
+
source: 'NYC DEP', agency: 'spec', vintage: 'spec',
|
| 86 |
+
title: 'Raster snapshot, mapped layer',
|
| 87 |
+
rasterKind: 'stormwater',
|
| 88 |
+
headline: 'ponding', subhead: 'scenario · pixel summary',
|
| 89 |
+
sub: 'Use for any 2D model output.',
|
| 90 |
+
docId: 'DS-RASTER',
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
id: 'grm-rasterpred', stone: 'touchstone', tier: 'modeled', variant: 'raster-pred',
|
| 94 |
+
source: 'Prithvi-NYC', agency: 'spec', vintage: 'spec',
|
| 95 |
+
title: 'Raster prediction, illustrative',
|
| 96 |
+
rasterKind: 'prithvi',
|
| 97 |
+
headline: 'n% flooded', subhead: 'model · scene id',
|
| 98 |
+
illustrative: true,
|
| 99 |
+
sub: 'Same chrome as raster + illustrative tag.',
|
| 100 |
+
docId: 'DS-RASTERPRED',
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
id: 'grm-register', stone: 'keystone', tier: 'empirical', variant: 'register',
|
| 104 |
+
source: 'NYC OpenData', agency: 'spec', vintage: 'spec',
|
| 105 |
+
title: 'Composite register list',
|
| 106 |
+
registers: [
|
| 107 |
+
{ reg: 'MTA', tier: 'empirical', label: 'Station entrance', detail: '0.18 mi · 5', sourceId: 'MTA-X', note: null },
|
| 108 |
+
{ reg: 'NYCHA', tier: 'empirical', label: 'Development', detail: '0.41 mi · 2,878 res.', sourceId: 'NYCHA-Y', note: null },
|
| 109 |
+
{ reg: 'DOH', tier: 'empirical', label: null, detail: null, sourceId: null, note: 'no acute-care hospital within 1.0 mi' },
|
| 110 |
+
],
|
| 111 |
+
sub: 'Use when many specialists join into one Stone.',
|
| 112 |
+
docId: 'DS-REGISTER',
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
id: 'grm-comparison', stone: 'keystone', tier: 'synthetic', variant: 'comparison',
|
| 116 |
+
source: 'EMP × SYN', agency: 'spec', vintage: 'spec',
|
| 117 |
+
title: 'Documented vs. interpreted',
|
| 118 |
+
left: { tier: 'empirical', label: 'documented', value: '31.4%', aux: 'n polygons' },
|
| 119 |
+
right: { tier: 'synthetic', label: 'interpreted', value: '29.8%', aux: 'n polygons' },
|
| 120 |
+
delta: 'Δ = −1.6 pp · agreement strong',
|
| 121 |
+
sub: 'Use to surface model–ground-truth deltas.',
|
| 122 |
+
docId: 'DS-CMP',
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
id: 'grm-meta', stone: 'capstone', tier: 'modeled', variant: 'meta',
|
| 126 |
+
source: 'Mellea', agency: 'spec', vintage: 'spec',
|
| 127 |
+
title: 'Capstone reconciliation',
|
| 128 |
+
metaRows: [
|
| 129 |
+
{ k: 'claims', v: '12 / 12 grounded' },
|
| 130 |
+
{ k: 'tier mix', v: 'EMP 5 · MOD 4 · PRX 2 · SYN 1' },
|
| 131 |
+
{ k: 'tier-1 freshness', v: 'median 38 d' },
|
| 132 |
+
{ k: 'warnings', v: '0' },
|
| 133 |
+
],
|
| 134 |
+
sub: "Use to expose the synthesis layer's audit.",
|
| 135 |
+
docId: 'DS-META',
|
| 136 |
+
},
|
| 137 |
+
];
|
| 138 |
+
</script>
|
| 139 |
+
|
| 140 |
+
<section class="region region-grammar" aria-label="Card grammar reference">
|
| 141 |
+
<header class="region-head">
|
| 142 |
+
<div class="region-head-left">
|
| 143 |
+
<span class="region-num">SPEC</span>
|
| 144 |
+
<h3 class="region-name">Card grammar</h3>
|
| 145 |
+
<span class="region-role">· every body variant in the system</span>
|
| 146 |
+
<span class="region-tag">stubs, not findings</span>
|
| 147 |
+
</div>
|
| 148 |
+
<span class="grammar-count">{STUBS.length} variants</span>
|
| 149 |
+
</header>
|
| 150 |
+
<div class="rail">
|
| 151 |
+
{#each STUBS as stub (stub.id)}
|
| 152 |
+
<FindingCard card={stub} {density} />
|
| 153 |
+
{/each}
|
| 154 |
+
</div>
|
| 155 |
+
</section>
|
| 156 |
+
|
| 157 |
+
<style>
|
| 158 |
+
.region-grammar {
|
| 159 |
+
border-top: 2px solid var(--ink);
|
| 160 |
+
padding: var(--s-5) 0;
|
| 161 |
+
}
|
| 162 |
+
.region-head {
|
| 163 |
+
display: flex;
|
| 164 |
+
justify-content: space-between;
|
| 165 |
+
align-items: baseline;
|
| 166 |
+
flex-wrap: wrap;
|
| 167 |
+
gap: var(--s-3);
|
| 168 |
+
margin-bottom: var(--s-3);
|
| 169 |
+
}
|
| 170 |
+
.region-head-left {
|
| 171 |
+
display: flex;
|
| 172 |
+
align-items: baseline;
|
| 173 |
+
gap: var(--s-2);
|
| 174 |
+
flex-wrap: wrap;
|
| 175 |
+
}
|
| 176 |
+
.region-num {
|
| 177 |
+
font-family: var(--font-mono);
|
| 178 |
+
font-size: 11px;
|
| 179 |
+
color: var(--ink-tertiary);
|
| 180 |
+
letter-spacing: 0.1em;
|
| 181 |
+
}
|
| 182 |
+
.region-name {
|
| 183 |
+
margin: 0;
|
| 184 |
+
font-family: var(--font-serif);
|
| 185 |
+
font-style: italic;
|
| 186 |
+
font-size: 22px;
|
| 187 |
+
font-weight: 500;
|
| 188 |
+
}
|
| 189 |
+
.region-role {
|
| 190 |
+
font-family: var(--font-sans);
|
| 191 |
+
font-size: 13px;
|
| 192 |
+
color: var(--ink-secondary);
|
| 193 |
+
}
|
| 194 |
+
.region-tag {
|
| 195 |
+
font-family: var(--font-mono);
|
| 196 |
+
font-size: 11px;
|
| 197 |
+
color: var(--ink-tertiary);
|
| 198 |
+
letter-spacing: 0.05em;
|
| 199 |
+
}
|
| 200 |
+
.grammar-count {
|
| 201 |
+
font-family: var(--font-mono);
|
| 202 |
+
font-size: 11px;
|
| 203 |
+
color: var(--ink-tertiary);
|
| 204 |
+
}
|
| 205 |
+
.rail {
|
| 206 |
+
display: grid;
|
| 207 |
+
grid-template-columns: repeat(12, 1fr);
|
| 208 |
+
gap: var(--s-3);
|
| 209 |
+
}
|
| 210 |
+
.rail :global(> .fc) { grid-column: span 4; }
|
| 211 |
+
.rail :global(> .fc.fc-register),
|
| 212 |
+
.rail :global(> .fc.fc-timeseries),
|
| 213 |
+
.rail :global(> .fc.fc-forecast),
|
| 214 |
+
.rail :global(> .fc.fc-raster),
|
| 215 |
+
.rail :global(> .fc.fc-raster-pred),
|
| 216 |
+
.rail :global(> .fc.fc-comparison) {
|
| 217 |
+
grid-column: span 6;
|
| 218 |
+
}
|
| 219 |
+
@media (max-width: 920px) {
|
| 220 |
+
.rail { grid-template-columns: repeat(6, 1fr); }
|
| 221 |
+
.rail :global(> .fc) { grid-column: span 6; }
|
| 222 |
+
}
|
| 223 |
+
</style>
|
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { Card, Density } from '$lib/types/card';
|
| 3 |
+
import { TIER_META } from '$lib/types/tier';
|
| 4 |
+
import TierGlyph from '$lib/components/glyphs/TierGlyph.svelte';
|
| 5 |
+
import CardBody from './cards/CardBody.svelte';
|
| 6 |
+
|
| 7 |
+
/** Findings card chrome: header (tier glyph + source + vintage), title,
|
| 8 |
+
* body (variant dispatch), footer (docId + tier badge + cite arrow).
|
| 9 |
+
*
|
| 10 |
+
* Synthetic-tier cards get a 1px dashed top-rule (telegraph "no
|
| 11 |
+
* observed data here"), per the v0.4.4 spec. Comparison and
|
| 12 |
+
* raster-pred always render synthetic.
|
| 13 |
+
*
|
| 14 |
+
* Hover linking: card sets `linkedKey` to its `mapLayer` on
|
| 15 |
+
* pointerenter / focus and clears on pointerleave / blur. The map
|
| 16 |
+
* reads `linkedKey` and lights up the matching layer.
|
| 17 |
+
*/
|
| 18 |
+
interface Props {
|
| 19 |
+
card: Card;
|
| 20 |
+
density?: Density;
|
| 21 |
+
linkedKey?: string | null;
|
| 22 |
+
onCite?: (citeId: string) => void;
|
| 23 |
+
onLink?: (key: string | null) => void;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
let {
|
| 27 |
+
card,
|
| 28 |
+
density = 'comfortable',
|
| 29 |
+
linkedKey = null,
|
| 30 |
+
onCite,
|
| 31 |
+
onLink,
|
| 32 |
+
}: Props = $props();
|
| 33 |
+
|
| 34 |
+
let isLinked = $derived(linkedKey != null && card.mapLayer != null && card.mapLayer === linkedKey);
|
| 35 |
+
let tierShort = $derived(TIER_META[card.tier].short);
|
| 36 |
+
|
| 37 |
+
let interactive = $derived(card.mapLayer != null);
|
| 38 |
+
|
| 39 |
+
function handleEnter() { if (card.mapLayer) onLink?.(card.mapLayer); }
|
| 40 |
+
function handleLeave() { if (card.mapLayer) onLink?.(null); }
|
| 41 |
+
function handleCite(e: Event) {
|
| 42 |
+
e.stopPropagation();
|
| 43 |
+
if (card.citeId) onCite?.(card.citeId);
|
| 44 |
+
}
|
| 45 |
+
function handleKey(e: KeyboardEvent) {
|
| 46 |
+
if (!interactive) return;
|
| 47 |
+
if (e.key === 'Enter' || e.key === ' ') {
|
| 48 |
+
e.preventDefault();
|
| 49 |
+
onLink?.(card.mapLayer ?? null);
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
</script>
|
| 53 |
+
|
| 54 |
+
<svelte:element
|
| 55 |
+
this={interactive ? 'button' : 'article'}
|
| 56 |
+
type={interactive ? 'button' : undefined}
|
| 57 |
+
role={interactive ? 'button' : 'article'}
|
| 58 |
+
class="fc fc-{card.variant} fc-tier-{card.tier}"
|
| 59 |
+
class:is-compact={density === 'compact'}
|
| 60 |
+
class:is-linked={isLinked}
|
| 61 |
+
class:is-interactive={interactive}
|
| 62 |
+
class:has-illustrative={card.illustrative || card.tier === 'synthetic' || card.variant === 'comparison'}
|
| 63 |
+
aria-labelledby={`fc-${card.id}-title`}
|
| 64 |
+
aria-label={`${TIER_META[card.tier].label} card · ${card.title} · ${card.source}`}
|
| 65 |
+
onpointerenter={handleEnter}
|
| 66 |
+
onpointerleave={handleLeave}
|
| 67 |
+
onfocus={handleEnter}
|
| 68 |
+
onblur={handleLeave}
|
| 69 |
+
onkeydown={handleKey}
|
| 70 |
+
>
|
| 71 |
+
<header class="fc-head">
|
| 72 |
+
<div class="fc-head-source">
|
| 73 |
+
<TierGlyph tier={card.tier} size={11} color="var(--tier-{card.tier})" />
|
| 74 |
+
<span class="fc-head-source-label" title={card.agency}>{card.source}</span>
|
| 75 |
+
</div>
|
| 76 |
+
<span class="fc-head-vintage">v. {card.vintage}</span>
|
| 77 |
+
</header>
|
| 78 |
+
|
| 79 |
+
<h4 id={`fc-${card.id}-title`} class="fc-title">{card.title}</h4>
|
| 80 |
+
|
| 81 |
+
<CardBody {card} />
|
| 82 |
+
|
| 83 |
+
<footer class="fc-foot">
|
| 84 |
+
{#if card.citeId}
|
| 85 |
+
<button
|
| 86 |
+
type="button"
|
| 87 |
+
class="fc-foot-cite"
|
| 88 |
+
onclick={handleCite}
|
| 89 |
+
title={`Open ${card.docId} in citation drawer`}
|
| 90 |
+
>
|
| 91 |
+
<span class="fc-foot-docid">{card.docId}</span>
|
| 92 |
+
<span class="fc-foot-arrow" aria-hidden="true">→</span>
|
| 93 |
+
</button>
|
| 94 |
+
{:else}
|
| 95 |
+
<span class="fc-foot-docid fc-foot-docid-mute">{card.docId}</span>
|
| 96 |
+
{/if}
|
| 97 |
+
<span class="fc-tier-badge fc-tier-badge-{card.tier}" aria-label={`epistemic tier ${tierShort}`}>
|
| 98 |
+
<TierGlyph tier={card.tier} size={9} color="var(--tier-{card.tier})" />
|
| 99 |
+
<span>{tierShort}</span>
|
| 100 |
+
</span>
|
| 101 |
+
</footer>
|
| 102 |
+
</svelte:element>
|
| 103 |
+
|
| 104 |
+
<style>
|
| 105 |
+
.fc {
|
| 106 |
+
background: var(--paper);
|
| 107 |
+
border: 1px solid var(--rule-soft);
|
| 108 |
+
display: flex;
|
| 109 |
+
flex-direction: column;
|
| 110 |
+
transition: background-color 200ms ease, border-color 200ms ease, outline-color 200ms ease;
|
| 111 |
+
outline: 0 solid transparent;
|
| 112 |
+
outline-offset: 0;
|
| 113 |
+
/* When the article is rendered as a button (interactive cards), strip
|
| 114 |
+
the default browser button chrome so it looks like the article. */
|
| 115 |
+
color: inherit;
|
| 116 |
+
text-align: left;
|
| 117 |
+
font: inherit;
|
| 118 |
+
padding: 0;
|
| 119 |
+
width: 100%;
|
| 120 |
+
}
|
| 121 |
+
.fc.is-interactive { cursor: pointer; }
|
| 122 |
+
.fc:hover { background: var(--paper-deep); }
|
| 123 |
+
.fc.is-linked {
|
| 124 |
+
outline: 2px solid var(--accent-graphical);
|
| 125 |
+
outline-offset: 0;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
/* Synthetic + comparison + illustrative cards get a dashed top rule. */
|
| 129 |
+
.has-illustrative { border-top: 1px dashed var(--tier-synthetic-line); }
|
| 130 |
+
.fc-tier-synthetic { border-top: 1px dashed var(--tier-synthetic-line); }
|
| 131 |
+
|
| 132 |
+
.fc-head {
|
| 133 |
+
display: flex;
|
| 134 |
+
justify-content: space-between;
|
| 135 |
+
align-items: center;
|
| 136 |
+
padding: var(--s-2) var(--s-4);
|
| 137 |
+
border-bottom: 1px solid var(--rule-soft);
|
| 138 |
+
background: var(--paper-deep);
|
| 139 |
+
}
|
| 140 |
+
:global(.fc.is-compact) .fc-head { padding: 6px var(--s-3); }
|
| 141 |
+
.fc-head-source {
|
| 142 |
+
display: inline-flex;
|
| 143 |
+
align-items: center;
|
| 144 |
+
gap: var(--s-2);
|
| 145 |
+
font-family: var(--font-mono);
|
| 146 |
+
font-size: 11px;
|
| 147 |
+
font-weight: 600;
|
| 148 |
+
letter-spacing: 0.06em;
|
| 149 |
+
text-transform: uppercase;
|
| 150 |
+
color: var(--ink);
|
| 151 |
+
}
|
| 152 |
+
.fc-head-source-label {
|
| 153 |
+
cursor: help;
|
| 154 |
+
}
|
| 155 |
+
.fc-head-vintage {
|
| 156 |
+
font-family: var(--font-mono);
|
| 157 |
+
font-size: 10px;
|
| 158 |
+
color: var(--ink-tertiary);
|
| 159 |
+
letter-spacing: 0.05em;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.fc-title {
|
| 163 |
+
margin: 0;
|
| 164 |
+
padding: var(--s-3) var(--s-4) 0;
|
| 165 |
+
font-family: var(--font-sans);
|
| 166 |
+
font-size: 14px;
|
| 167 |
+
font-weight: 600;
|
| 168 |
+
line-height: 1.35;
|
| 169 |
+
color: var(--ink);
|
| 170 |
+
}
|
| 171 |
+
:global(.fc.is-compact) .fc-title { padding: var(--s-2) var(--s-3) 0; font-size: 13px; }
|
| 172 |
+
|
| 173 |
+
.fc-foot {
|
| 174 |
+
display: flex;
|
| 175 |
+
justify-content: space-between;
|
| 176 |
+
align-items: center;
|
| 177 |
+
padding: var(--s-2) var(--s-4);
|
| 178 |
+
border-top: 1px solid var(--rule-soft);
|
| 179 |
+
background: var(--paper-deep);
|
| 180 |
+
gap: var(--s-3);
|
| 181 |
+
margin-top: auto;
|
| 182 |
+
}
|
| 183 |
+
:global(.fc.is-compact) .fc-foot { padding: 6px var(--s-3); }
|
| 184 |
+
|
| 185 |
+
.fc-foot-cite {
|
| 186 |
+
display: inline-flex;
|
| 187 |
+
align-items: center;
|
| 188 |
+
gap: 4px;
|
| 189 |
+
background: transparent;
|
| 190 |
+
border: 0;
|
| 191 |
+
padding: 0;
|
| 192 |
+
cursor: pointer;
|
| 193 |
+
font-family: var(--font-mono);
|
| 194 |
+
font-size: 10px;
|
| 195 |
+
letter-spacing: 0.05em;
|
| 196 |
+
color: var(--accent);
|
| 197 |
+
}
|
| 198 |
+
.fc-foot-cite:hover { color: var(--ink); }
|
| 199 |
+
.fc-foot-docid {
|
| 200 |
+
text-transform: uppercase;
|
| 201 |
+
}
|
| 202 |
+
.fc-foot-docid-mute {
|
| 203 |
+
font-family: var(--font-mono);
|
| 204 |
+
font-size: 10px;
|
| 205 |
+
color: var(--ink-tertiary);
|
| 206 |
+
letter-spacing: 0.05em;
|
| 207 |
+
text-transform: uppercase;
|
| 208 |
+
}
|
| 209 |
+
.fc-foot-arrow {
|
| 210 |
+
font-family: var(--font-mono);
|
| 211 |
+
font-size: 11px;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.fc-tier-badge {
|
| 215 |
+
display: inline-flex;
|
| 216 |
+
align-items: center;
|
| 217 |
+
gap: 4px;
|
| 218 |
+
font-family: var(--font-mono);
|
| 219 |
+
font-size: 10px;
|
| 220 |
+
font-weight: 500;
|
| 221 |
+
letter-spacing: 0.08em;
|
| 222 |
+
text-transform: uppercase;
|
| 223 |
+
}
|
| 224 |
+
.fc-tier-badge-empirical { color: var(--tier-empirical); }
|
| 225 |
+
.fc-tier-badge-modeled { color: var(--tier-modeled); }
|
| 226 |
+
.fc-tier-badge-proxy { color: var(--tier-proxy); }
|
| 227 |
+
.fc-tier-badge-synthetic { color: var(--tier-synthetic); }
|
| 228 |
+
</style>
|
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type {
|
| 3 |
+
Card, Density, FindingsData, ProvenanceMode, StoneKey, StoneTrace
|
| 4 |
+
} from '$lib/types/card';
|
| 5 |
+
import { STONE_ORDER } from '$lib/types/card';
|
| 6 |
+
import RunHealthStrip from './RunHealthStrip.svelte';
|
| 7 |
+
import StoneRegion from './StoneRegion.svelte';
|
| 8 |
+
import CardGrammarReference from './CardGrammarReference.svelte';
|
| 9 |
+
|
| 10 |
+
/** Findings region: 5 Stones in canonical order, top-banner run-health
|
| 11 |
+
* strip, optional dev-only card-grammar catalog. linkedKey is owned by
|
| 12 |
+
* the parent route (`q/[queryId]/+page.svelte` or `q/sample/+page.svelte`)
|
| 13 |
+
* so the briefing's map can read it without a store. */
|
| 14 |
+
interface Props {
|
| 15 |
+
data: FindingsData;
|
| 16 |
+
density?: Density;
|
| 17 |
+
provenanceMode?: ProvenanceMode;
|
| 18 |
+
showGrammar?: boolean;
|
| 19 |
+
linkedKey?: string | null;
|
| 20 |
+
onCite?: (citeId: string) => void;
|
| 21 |
+
onLink?: (key: string | null) => void;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
let {
|
| 25 |
+
data,
|
| 26 |
+
density = 'comfortable',
|
| 27 |
+
provenanceMode = 'smart',
|
| 28 |
+
showGrammar = false,
|
| 29 |
+
linkedKey = null,
|
| 30 |
+
onCite,
|
| 31 |
+
onLink,
|
| 32 |
+
}: Props = $props();
|
| 33 |
+
|
| 34 |
+
// Index cards by Stone, keep order from `data.cards`.
|
| 35 |
+
let cardsByStone = $derived.by<Record<StoneKey, Card[]>>(() => {
|
| 36 |
+
const out: Record<StoneKey, Card[]> = {
|
| 37 |
+
cornerstone: [], keystone: [], touchstone: [], lodestone: [], capstone: [],
|
| 38 |
+
};
|
| 39 |
+
for (const c of data.cards) out[c.stone].push(c);
|
| 40 |
+
return out;
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
// Index traces by Stone with safe defaults so a missing Stone still
|
| 44 |
+
// renders an empty region rather than crashing.
|
| 45 |
+
let tracesByStone = $derived.by<Record<StoneKey, StoneTrace>>(() => {
|
| 46 |
+
const out: Record<StoneKey, StoneTrace> = {
|
| 47 |
+
cornerstone: { key: 'cornerstone', members: [] },
|
| 48 |
+
keystone: { key: 'keystone', members: [] },
|
| 49 |
+
touchstone: { key: 'touchstone', members: [] },
|
| 50 |
+
lodestone: { key: 'lodestone', members: [] },
|
| 51 |
+
capstone: { key: 'capstone', members: [] },
|
| 52 |
+
};
|
| 53 |
+
for (const t of data.stones) out[t.key] = t;
|
| 54 |
+
return out;
|
| 55 |
+
});
|
| 56 |
+
</script>
|
| 57 |
+
|
| 58 |
+
<section class="findings" aria-label="Findings, grouped by Stone">
|
| 59 |
+
<header class="findings-head">
|
| 60 |
+
<h2 class="findings-h2">Findings · grouped by Stone</h2>
|
| 61 |
+
<span class="findings-tagline">cards = what each Stone found · provenance collapses below</span>
|
| 62 |
+
</header>
|
| 63 |
+
|
| 64 |
+
<RunHealthStrip
|
| 65 |
+
cards={data.cards}
|
| 66 |
+
stones={data.stones}
|
| 67 |
+
wallSeconds={data.wallSeconds}
|
| 68 |
+
cacheHit={data.cacheHit}
|
| 69 |
+
/>
|
| 70 |
+
|
| 71 |
+
{#each STONE_ORDER as key (key)}
|
| 72 |
+
<StoneRegion
|
| 73 |
+
stone={key}
|
| 74 |
+
cards={cardsByStone[key]}
|
| 75 |
+
trace={tracesByStone[key]}
|
| 76 |
+
{density}
|
| 77 |
+
{provenanceMode}
|
| 78 |
+
{linkedKey}
|
| 79 |
+
{onCite}
|
| 80 |
+
{onLink}
|
| 81 |
+
/>
|
| 82 |
+
{/each}
|
| 83 |
+
|
| 84 |
+
{#if showGrammar}
|
| 85 |
+
<CardGrammarReference {density} />
|
| 86 |
+
{/if}
|
| 87 |
+
</section>
|
| 88 |
+
|
| 89 |
+
<style>
|
| 90 |
+
.findings {
|
| 91 |
+
background: var(--paper);
|
| 92 |
+
color: var(--ink);
|
| 93 |
+
}
|
| 94 |
+
.findings-head {
|
| 95 |
+
display: flex;
|
| 96 |
+
align-items: baseline;
|
| 97 |
+
justify-content: space-between;
|
| 98 |
+
flex-wrap: wrap;
|
| 99 |
+
gap: var(--s-3);
|
| 100 |
+
padding: var(--s-3) 0 var(--s-2);
|
| 101 |
+
}
|
| 102 |
+
.findings-h2 {
|
| 103 |
+
margin: 0;
|
| 104 |
+
font-family: var(--font-serif);
|
| 105 |
+
font-style: italic;
|
| 106 |
+
font-size: 22px;
|
| 107 |
+
font-weight: 500;
|
| 108 |
+
color: var(--ink);
|
| 109 |
+
}
|
| 110 |
+
.findings-tagline {
|
| 111 |
+
font-family: var(--font-mono);
|
| 112 |
+
font-size: 11px;
|
| 113 |
+
color: var(--ink-tertiary);
|
| 114 |
+
letter-spacing: 0.05em;
|
| 115 |
+
}
|
| 116 |
+
</style>
|
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { StoneMember } from '$lib/types/card';
|
| 3 |
+
import TierGlyph from '$lib/components/glyphs/TierGlyph.svelte';
|
| 4 |
+
import Self from './ProvenanceTrace.svelte';
|
| 5 |
+
|
| 6 |
+
/** Indented specialist tree. Each row: status pip · mono id · italic-
|
| 7 |
+
* serif name · note. Recursive via self-import (svelte:self is
|
| 8 |
+
* deprecated in Svelte 5). */
|
| 9 |
+
let { members, depth = 0 }: { members: StoneMember[]; depth?: number } = $props();
|
| 10 |
+
|
| 11 |
+
function pip(status: StoneMember['status']): string {
|
| 12 |
+
return ({ ok: '●', warn: '▲', error: '■', silent: '○' } as const)[status];
|
| 13 |
+
}
|
| 14 |
+
function pipColorVar(m: StoneMember): string {
|
| 15 |
+
if (m.status === 'warn') return '#B7791F';
|
| 16 |
+
if (m.status === 'error') return '#B91C1C';
|
| 17 |
+
if (m.status === 'silent') return 'var(--ink-tertiary)';
|
| 18 |
+
if (m.tier) return `var(--tier-${m.tier})`;
|
| 19 |
+
return 'var(--ink)';
|
| 20 |
+
}
|
| 21 |
+
</script>
|
| 22 |
+
|
| 23 |
+
<ul class="prov-tree" style="--depth: {depth};">
|
| 24 |
+
{#each members as m (m.id)}
|
| 25 |
+
<li class="prov-row prov-status-{m.status}">
|
| 26 |
+
<span class="prov-pip" style="color: {pipColorVar(m)};" aria-hidden="true">{pip(m.status)}</span>
|
| 27 |
+
<span class="prov-id">{m.id}</span>
|
| 28 |
+
{#if m.tier}
|
| 29 |
+
<span class="prov-tier">
|
| 30 |
+
<TierGlyph tier={m.tier} size={9} color={`var(--tier-${m.tier})`} />
|
| 31 |
+
</span>
|
| 32 |
+
{/if}
|
| 33 |
+
<span class="prov-name">{m.name}</span>
|
| 34 |
+
{#if m.note}<span class="prov-note">— {m.note}</span>{/if}
|
| 35 |
+
{#if m.ms != null}<span class="prov-ms">{m.ms < 1000 ? `${m.ms}ms` : `${(m.ms / 1000).toFixed(1)}s`}</span>{/if}
|
| 36 |
+
</li>
|
| 37 |
+
{#if m.children?.length}
|
| 38 |
+
<li class="prov-children">
|
| 39 |
+
<Self members={m.children} depth={depth + 1} />
|
| 40 |
+
</li>
|
| 41 |
+
{/if}
|
| 42 |
+
{/each}
|
| 43 |
+
</ul>
|
| 44 |
+
|
| 45 |
+
<style>
|
| 46 |
+
.prov-tree {
|
| 47 |
+
list-style: none;
|
| 48 |
+
margin: 0;
|
| 49 |
+
padding: 0;
|
| 50 |
+
padding-left: calc(var(--depth, 0) * 16px);
|
| 51 |
+
}
|
| 52 |
+
.prov-row {
|
| 53 |
+
display: grid;
|
| 54 |
+
grid-template-columns: 14px max-content max-content 1fr auto;
|
| 55 |
+
gap: var(--s-2);
|
| 56 |
+
align-items: baseline;
|
| 57 |
+
padding: 3px 0;
|
| 58 |
+
font-family: var(--font-mono);
|
| 59 |
+
font-size: 11px;
|
| 60 |
+
border-bottom: 1px dotted var(--rule-soft);
|
| 61 |
+
}
|
| 62 |
+
.prov-row:last-child { border-bottom: 0; }
|
| 63 |
+
.prov-pip {
|
| 64 |
+
text-align: center;
|
| 65 |
+
font-size: 10px;
|
| 66 |
+
line-height: 1;
|
| 67 |
+
}
|
| 68 |
+
.prov-id {
|
| 69 |
+
color: var(--ink);
|
| 70 |
+
letter-spacing: 0.04em;
|
| 71 |
+
text-transform: lowercase;
|
| 72 |
+
}
|
| 73 |
+
.prov-tier {
|
| 74 |
+
display: inline-flex;
|
| 75 |
+
align-items: center;
|
| 76 |
+
}
|
| 77 |
+
.prov-name {
|
| 78 |
+
font-family: var(--font-serif);
|
| 79 |
+
font-style: italic;
|
| 80 |
+
font-size: 13px;
|
| 81 |
+
color: var(--ink);
|
| 82 |
+
}
|
| 83 |
+
.prov-note {
|
| 84 |
+
font-family: var(--font-sans);
|
| 85 |
+
font-size: 12px;
|
| 86 |
+
color: var(--ink-tertiary);
|
| 87 |
+
}
|
| 88 |
+
.prov-ms {
|
| 89 |
+
font-family: var(--font-mono);
|
| 90 |
+
font-size: 10px;
|
| 91 |
+
color: var(--ink-tertiary);
|
| 92 |
+
}
|
| 93 |
+
.prov-status-silent .prov-name { color: var(--ink-tertiary); }
|
| 94 |
+
.prov-status-warn .prov-name { color: #B7791F; }
|
| 95 |
+
.prov-status-error .prov-name { color: #B91C1C; }
|
| 96 |
+
.prov-children { padding: 0; }
|
| 97 |
+
</style>
|
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { Card, StoneTrace } from '$lib/types/card';
|
| 3 |
+
|
| 4 |
+
/** Top-of-Findings status row. Mirrors findings.jsx RunHealth44:
|
| 5 |
+
* Stones · functions fired · evidence cards · wall-clock · silent /
|
| 6 |
+
* warn / error chips when nonzero. */
|
| 7 |
+
interface Props {
|
| 8 |
+
cards: Card[];
|
| 9 |
+
stones: StoneTrace[];
|
| 10 |
+
wallSeconds?: number;
|
| 11 |
+
cacheHit?: number;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
let { cards, stones, wallSeconds, cacheHit }: Props = $props();
|
| 15 |
+
|
| 16 |
+
function flatten(ms: StoneTrace['members']): StoneTrace['members'] {
|
| 17 |
+
return ms.flatMap((m) => (m.children ? [m, ...flatten(m.children)] : [m]));
|
| 18 |
+
}
|
| 19 |
+
let allMembers = $derived(stones.flatMap((s) => flatten(s.members)));
|
| 20 |
+
let total = $derived(allMembers.length);
|
| 21 |
+
let fired = $derived(allMembers.filter((m) => m.status === 'ok').length);
|
| 22 |
+
let silent = $derived(allMembers.filter((m) => m.status === 'silent').length);
|
| 23 |
+
let warn = $derived(allMembers.filter((m) => m.status === 'warn').length);
|
| 24 |
+
let err = $derived(allMembers.filter((m) => m.status === 'error').length);
|
| 25 |
+
|
| 26 |
+
let wall = $derived(wallSeconds == null
|
| 27 |
+
? '—'
|
| 28 |
+
: wallSeconds < 1 ? `${Math.round(wallSeconds * 1000)}ms` : `${wallSeconds.toFixed(1)}s`);
|
| 29 |
+
</script>
|
| 30 |
+
|
| 31 |
+
<div class="rh">
|
| 32 |
+
<span class="rh-item"><strong>{stones.length}</strong> Stones</span>
|
| 33 |
+
<span class="rh-sep">·</span>
|
| 34 |
+
<span class="rh-item"><strong>{fired}/{total}</strong> functions fired</span>
|
| 35 |
+
<span class="rh-sep">·</span>
|
| 36 |
+
<span class="rh-item"><strong>{cards.length}</strong> evidence card{cards.length === 1 ? '' : 's'}</span>
|
| 37 |
+
<span class="rh-sep">·</span>
|
| 38 |
+
<span class="rh-item"><strong>{wall}</strong> wall-clock</span>
|
| 39 |
+
{#if cacheHit != null}
|
| 40 |
+
<span class="rh-sep">·</span>
|
| 41 |
+
<span class="rh-item"><strong>{Math.round(cacheHit * 100)}%</strong> cache</span>
|
| 42 |
+
{/if}
|
| 43 |
+
{#if silent > 0}
|
| 44 |
+
<span class="rh-sep">·</span>
|
| 45 |
+
<span class="rh-item rh-silent">{silent} silent</span>
|
| 46 |
+
{/if}
|
| 47 |
+
{#if warn > 0}
|
| 48 |
+
<span class="rh-sep">·</span>
|
| 49 |
+
<span class="rh-item rh-warn">{warn} warn</span>
|
| 50 |
+
{/if}
|
| 51 |
+
{#if err > 0}
|
| 52 |
+
<span class="rh-sep">·</span>
|
| 53 |
+
<span class="rh-item rh-err">{err} error</span>
|
| 54 |
+
{/if}
|
| 55 |
+
</div>
|
| 56 |
+
|
| 57 |
+
<style>
|
| 58 |
+
.rh {
|
| 59 |
+
display: flex;
|
| 60 |
+
align-items: center;
|
| 61 |
+
flex-wrap: wrap;
|
| 62 |
+
gap: var(--s-2);
|
| 63 |
+
padding: var(--s-2) var(--s-4);
|
| 64 |
+
background: var(--paper-deep);
|
| 65 |
+
border-top: 1px solid var(--rule-soft);
|
| 66 |
+
border-bottom: 1px solid var(--rule-soft);
|
| 67 |
+
font-family: var(--font-mono);
|
| 68 |
+
font-size: 11px;
|
| 69 |
+
color: var(--ink-tertiary);
|
| 70 |
+
letter-spacing: 0.04em;
|
| 71 |
+
}
|
| 72 |
+
.rh-item strong {
|
| 73 |
+
font-weight: 600;
|
| 74 |
+
color: var(--ink);
|
| 75 |
+
margin-right: 2px;
|
| 76 |
+
}
|
| 77 |
+
.rh-sep { opacity: 0.5; }
|
| 78 |
+
.rh-silent { color: var(--ink-tertiary); }
|
| 79 |
+
.rh-warn { color: #B7791F; }
|
| 80 |
+
.rh-err { color: #B91C1C; }
|
| 81 |
+
</style>
|
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { Card, Density, ProvenanceMode, StoneKey, StoneTrace } from '$lib/types/card';
|
| 3 |
+
import { STONE_META, STONE_ORDER } from '$lib/types/card';
|
| 4 |
+
import FindingCard from './FindingCard.svelte';
|
| 5 |
+
import StoneTally from './StoneTally.svelte';
|
| 6 |
+
import ProvenanceTrace from './ProvenanceTrace.svelte';
|
| 7 |
+
|
| 8 |
+
/** A Stone group: header (serif italic name + role · tag · run-tally),
|
| 9 |
+
* card grid, smart provenance toggle. */
|
| 10 |
+
interface Props {
|
| 11 |
+
stone: StoneKey;
|
| 12 |
+
cards: Card[];
|
| 13 |
+
trace: StoneTrace;
|
| 14 |
+
density?: Density;
|
| 15 |
+
provenanceMode?: ProvenanceMode;
|
| 16 |
+
linkedKey?: string | null;
|
| 17 |
+
onCite?: (citeId: string) => void;
|
| 18 |
+
onLink?: (key: string | null) => void;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
let {
|
| 22 |
+
stone,
|
| 23 |
+
cards,
|
| 24 |
+
trace,
|
| 25 |
+
density = 'comfortable',
|
| 26 |
+
provenanceMode = 'smart',
|
| 27 |
+
linkedKey = null,
|
| 28 |
+
onCite,
|
| 29 |
+
onLink,
|
| 30 |
+
}: Props = $props();
|
| 31 |
+
|
| 32 |
+
let meta = $derived(STONE_META[stone]);
|
| 33 |
+
let stoneNum = $derived(`${STONE_ORDER.indexOf(stone) + 1}`.padStart(2, '0'));
|
| 34 |
+
let isCapstone = $derived(stone === 'capstone');
|
| 35 |
+
|
| 36 |
+
function flatten(ms: StoneTrace['members']): StoneTrace['members'] {
|
| 37 |
+
return ms.flatMap((m) => (m.children ? [m, ...flatten(m.children)] : [m]));
|
| 38 |
+
}
|
| 39 |
+
let flat = $derived(flatten(trace.members));
|
| 40 |
+
let traceCount = $derived(flat.length);
|
| 41 |
+
let hasAnomaly = $derived(
|
| 42 |
+
flat.some((m) => m.status === 'warn' || m.status === 'error' || m.status === 'silent')
|
| 43 |
+
);
|
| 44 |
+
let smartOpen = $derived(
|
| 45 |
+
provenanceMode === 'all-expanded' ? true :
|
| 46 |
+
provenanceMode === 'all-collapsed' ? false :
|
| 47 |
+
hasAnomaly
|
| 48 |
+
);
|
| 49 |
+
|
| 50 |
+
let userOpen = $state<boolean | null>(null);
|
| 51 |
+
let traceOpen = $derived(userOpen ?? smartOpen);
|
| 52 |
+
|
| 53 |
+
// Reset user override when the mode changes.
|
| 54 |
+
$effect(() => {
|
| 55 |
+
provenanceMode;
|
| 56 |
+
userOpen = null;
|
| 57 |
+
});
|
| 58 |
+
</script>
|
| 59 |
+
|
| 60 |
+
<section class="region region-{stone}" aria-labelledby={`region-h-${stone}`} data-stone={stone}>
|
| 61 |
+
<header class="region-head">
|
| 62 |
+
<div class="region-head-left">
|
| 63 |
+
<span class="region-num">{stoneNum}</span>
|
| 64 |
+
<h3 id={`region-h-${stone}`} class="region-name">{meta.name}</h3>
|
| 65 |
+
<span class="region-role">· {meta.role}</span>
|
| 66 |
+
<span class="region-tag">{meta.tag}</span>
|
| 67 |
+
</div>
|
| 68 |
+
<StoneTally cardCount={cards.length} members={trace.members} />
|
| 69 |
+
</header>
|
| 70 |
+
|
| 71 |
+
{#if cards.length === 0}
|
| 72 |
+
<div class="silent">
|
| 73 |
+
<span class="silent-tag">silent</span>
|
| 74 |
+
<p class="silent-prose">
|
| 75 |
+
{#if stone === 'lodestone'}
|
| 76 |
+
No projection cards landed for this query. Atomic functions still ran (see provenance) and returned silence rather than confabulation.
|
| 77 |
+
{:else}
|
| 78 |
+
No cards for this Stone on this query.
|
| 79 |
+
{/if}
|
| 80 |
+
</p>
|
| 81 |
+
</div>
|
| 82 |
+
{:else}
|
| 83 |
+
<div class="rail" class:rail-capstone={isCapstone}>
|
| 84 |
+
{#each cards as card (card.id)}
|
| 85 |
+
<FindingCard
|
| 86 |
+
{card}
|
| 87 |
+
{density}
|
| 88 |
+
{linkedKey}
|
| 89 |
+
{onCite}
|
| 90 |
+
{onLink}
|
| 91 |
+
/>
|
| 92 |
+
{/each}
|
| 93 |
+
</div>
|
| 94 |
+
{/if}
|
| 95 |
+
|
| 96 |
+
<div class="prov">
|
| 97 |
+
<button
|
| 98 |
+
type="button"
|
| 99 |
+
class="prov-toggle"
|
| 100 |
+
aria-expanded={traceOpen}
|
| 101 |
+
aria-controls={`prov-body-${stone}`}
|
| 102 |
+
onclick={() => (userOpen = !traceOpen)}
|
| 103 |
+
>
|
| 104 |
+
<span class="prov-caret" aria-hidden="true">{traceOpen ? '▾' : '▸'}</span>
|
| 105 |
+
<span class="prov-label">{traceOpen ? 'Hide' : 'Show'} provenance</span>
|
| 106 |
+
<span class="prov-meta">
|
| 107 |
+
· {traceCount} function{traceCount === 1 ? '' : 's'}{hasAnomaly ? ' · anomaly' : ''}
|
| 108 |
+
</span>
|
| 109 |
+
</button>
|
| 110 |
+
{#if traceOpen}
|
| 111 |
+
<div id={`prov-body-${stone}`} class="prov-body">
|
| 112 |
+
<ProvenanceTrace members={trace.members} />
|
| 113 |
+
</div>
|
| 114 |
+
{/if}
|
| 115 |
+
</div>
|
| 116 |
+
</section>
|
| 117 |
+
|
| 118 |
+
<style>
|
| 119 |
+
.region {
|
| 120 |
+
border-top: 1px solid var(--rule-soft);
|
| 121 |
+
padding: var(--s-5) 0 var(--s-5);
|
| 122 |
+
background: transparent;
|
| 123 |
+
}
|
| 124 |
+
.region:first-of-type { border-top: 0; }
|
| 125 |
+
.region-head {
|
| 126 |
+
display: flex;
|
| 127 |
+
align-items: baseline;
|
| 128 |
+
justify-content: space-between;
|
| 129 |
+
gap: var(--s-4);
|
| 130 |
+
margin-bottom: var(--s-3);
|
| 131 |
+
flex-wrap: wrap;
|
| 132 |
+
}
|
| 133 |
+
.region-head-left {
|
| 134 |
+
display: flex;
|
| 135 |
+
align-items: baseline;
|
| 136 |
+
gap: var(--s-2);
|
| 137 |
+
flex-wrap: wrap;
|
| 138 |
+
}
|
| 139 |
+
.region-num {
|
| 140 |
+
font-family: var(--font-mono);
|
| 141 |
+
font-size: 11px;
|
| 142 |
+
color: var(--ink-tertiary);
|
| 143 |
+
letter-spacing: 0.1em;
|
| 144 |
+
}
|
| 145 |
+
.region-name {
|
| 146 |
+
margin: 0;
|
| 147 |
+
font-family: var(--font-serif);
|
| 148 |
+
font-style: italic;
|
| 149 |
+
font-size: 26px;
|
| 150 |
+
font-weight: 500;
|
| 151 |
+
color: var(--ink);
|
| 152 |
+
letter-spacing: -0.005em;
|
| 153 |
+
line-height: 1.1;
|
| 154 |
+
}
|
| 155 |
+
.region-role {
|
| 156 |
+
font-family: var(--font-sans);
|
| 157 |
+
font-size: 14px;
|
| 158 |
+
color: var(--ink-secondary);
|
| 159 |
+
}
|
| 160 |
+
.region-tag {
|
| 161 |
+
font-family: var(--font-mono);
|
| 162 |
+
font-size: 11px;
|
| 163 |
+
color: var(--ink-tertiary);
|
| 164 |
+
letter-spacing: 0.05em;
|
| 165 |
+
margin-left: var(--s-2);
|
| 166 |
+
}
|
| 167 |
+
.rail {
|
| 168 |
+
display: grid;
|
| 169 |
+
grid-template-columns: repeat(12, 1fr);
|
| 170 |
+
gap: var(--s-3);
|
| 171 |
+
}
|
| 172 |
+
.rail :global(> .fc) { grid-column: span 4; }
|
| 173 |
+
.rail :global(> .fc.fc-register),
|
| 174 |
+
.rail :global(> .fc.fc-timeseries),
|
| 175 |
+
.rail :global(> .fc.fc-forecast),
|
| 176 |
+
.rail :global(> .fc.fc-raster),
|
| 177 |
+
.rail :global(> .fc.fc-raster-pred),
|
| 178 |
+
.rail :global(> .fc.fc-comparison) {
|
| 179 |
+
grid-column: span 6;
|
| 180 |
+
}
|
| 181 |
+
.rail-capstone :global(> .fc) { grid-column: span 6; }
|
| 182 |
+
@media (max-width: 920px) {
|
| 183 |
+
.rail { grid-template-columns: repeat(6, 1fr); }
|
| 184 |
+
.rail :global(> .fc) { grid-column: span 6; }
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.silent {
|
| 188 |
+
border: 1px dashed var(--rule-soft);
|
| 189 |
+
padding: var(--s-4);
|
| 190 |
+
display: flex;
|
| 191 |
+
flex-direction: column;
|
| 192 |
+
gap: var(--s-2);
|
| 193 |
+
background: var(--paper-deep);
|
| 194 |
+
}
|
| 195 |
+
.silent-tag {
|
| 196 |
+
font-family: var(--font-mono);
|
| 197 |
+
font-size: 10px;
|
| 198 |
+
color: var(--ink-tertiary);
|
| 199 |
+
letter-spacing: 0.1em;
|
| 200 |
+
text-transform: uppercase;
|
| 201 |
+
}
|
| 202 |
+
.silent-prose {
|
| 203 |
+
margin: 0;
|
| 204 |
+
font-size: 13px;
|
| 205 |
+
color: var(--ink-secondary);
|
| 206 |
+
line-height: 1.5;
|
| 207 |
+
max-width: var(--measure);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.prov { margin-top: var(--s-3); }
|
| 211 |
+
.prov-toggle {
|
| 212 |
+
background: transparent;
|
| 213 |
+
border: 0;
|
| 214 |
+
padding: 4px 0;
|
| 215 |
+
cursor: pointer;
|
| 216 |
+
display: inline-flex;
|
| 217 |
+
align-items: baseline;
|
| 218 |
+
gap: var(--s-1);
|
| 219 |
+
font-family: var(--font-mono);
|
| 220 |
+
font-size: 11px;
|
| 221 |
+
color: var(--ink-secondary);
|
| 222 |
+
letter-spacing: 0.05em;
|
| 223 |
+
}
|
| 224 |
+
.prov-toggle:hover { color: var(--ink); }
|
| 225 |
+
.prov-caret { font-size: 10px; color: var(--ink-tertiary); }
|
| 226 |
+
.prov-meta { color: var(--ink-tertiary); }
|
| 227 |
+
.prov-body {
|
| 228 |
+
margin-top: var(--s-2);
|
| 229 |
+
padding: var(--s-2) 0;
|
| 230 |
+
border-top: 1px solid var(--rule-soft);
|
| 231 |
+
border-bottom: 1px solid var(--rule-soft);
|
| 232 |
+
}
|
| 233 |
+
</style>
|
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { StoneMember } from '$lib/types/card';
|
| 3 |
+
|
| 4 |
+
/** Run-tally chip in the Stone-region header. Mirrors findings.jsx
|
| 5 |
+
* StoneTally44: shows card count, fired count, silent / warn / error
|
| 6 |
+
* counts (when nonzero), and the heaviest-specialist runtime. */
|
| 7 |
+
interface Props {
|
| 8 |
+
cardCount: number;
|
| 9 |
+
members: StoneMember[];
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
let { cardCount, members }: Props = $props();
|
| 13 |
+
|
| 14 |
+
function flatten(ms: StoneMember[]): StoneMember[] {
|
| 15 |
+
return ms.flatMap((m) => (m.children ? [m, ...flatten(m.children)] : [m]));
|
| 16 |
+
}
|
| 17 |
+
let flat = $derived(flatten(members));
|
| 18 |
+
let fired = $derived(flat.filter((m) => m.status === 'ok').length);
|
| 19 |
+
let silent = $derived(flat.filter((m) => m.status === 'silent').length);
|
| 20 |
+
let warn = $derived(flat.filter((m) => m.status === 'warn').length);
|
| 21 |
+
let err = $derived(flat.filter((m) => m.status === 'error').length);
|
| 22 |
+
let ms = $derived(members.reduce((a, m) => Math.max(a, m.ms ?? 0), 0));
|
| 23 |
+
|
| 24 |
+
function fmtMs(x: number): string {
|
| 25 |
+
if (x === 0) return '—';
|
| 26 |
+
if (x < 1000) return `${x}ms`;
|
| 27 |
+
return `${(x / 1000).toFixed(1)}s`;
|
| 28 |
+
}
|
| 29 |
+
</script>
|
| 30 |
+
|
| 31 |
+
<span class="tally">
|
| 32 |
+
<span class="cards">{cardCount} card{cardCount === 1 ? '' : 's'}</span>
|
| 33 |
+
<span class="sep">·</span>
|
| 34 |
+
<span class="fired"><strong>{fired}</strong> fired</span>
|
| 35 |
+
{#if silent > 0}
|
| 36 |
+
<span class="sep">·</span>
|
| 37 |
+
<span class="silent"><strong>{silent}</strong> silent</span>
|
| 38 |
+
{/if}
|
| 39 |
+
{#if warn > 0}
|
| 40 |
+
<span class="sep">·</span>
|
| 41 |
+
<span class="warn"><strong>{warn}</strong> warn</span>
|
| 42 |
+
{/if}
|
| 43 |
+
{#if err > 0}
|
| 44 |
+
<span class="sep">·</span>
|
| 45 |
+
<span class="err"><strong>{err}</strong> error</span>
|
| 46 |
+
{/if}
|
| 47 |
+
<span class="sep">·</span>
|
| 48 |
+
<span class="ms"><strong>{fmtMs(ms)}</strong></span>
|
| 49 |
+
</span>
|
| 50 |
+
|
| 51 |
+
<style>
|
| 52 |
+
.tally {
|
| 53 |
+
display: inline-flex;
|
| 54 |
+
align-items: center;
|
| 55 |
+
gap: 4px;
|
| 56 |
+
font-family: var(--font-mono);
|
| 57 |
+
font-size: 11px;
|
| 58 |
+
color: var(--ink-tertiary);
|
| 59 |
+
letter-spacing: 0.04em;
|
| 60 |
+
flex-wrap: wrap;
|
| 61 |
+
}
|
| 62 |
+
strong {
|
| 63 |
+
font-weight: 600;
|
| 64 |
+
color: var(--ink);
|
| 65 |
+
}
|
| 66 |
+
.silent strong { color: var(--ink-tertiary); }
|
| 67 |
+
.warn strong { color: #B7791F; }
|
| 68 |
+
.err strong { color: #B91C1C; }
|
| 69 |
+
.sep { color: var(--ink-tertiary); opacity: 0.6; }
|
| 70 |
+
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { Card } from '$lib/types/card';
|
| 3 |
+
import HeadlineBody from './HeadlineBody.svelte';
|
| 4 |
+
import TabularBody from './TabularBody.svelte';
|
| 5 |
+
import ScalarsBody from './ScalarsBody.svelte';
|
| 6 |
+
import SparkBody from './SparkBody.svelte';
|
| 7 |
+
import TimeseriesBody from './TimeseriesBody.svelte';
|
| 8 |
+
import ForecastBody from './ForecastBody.svelte';
|
| 9 |
+
import RasterBody from './RasterBody.svelte';
|
| 10 |
+
import RegisterBody from './RegisterBody.svelte';
|
| 11 |
+
import ComparisonBody from './ComparisonBody.svelte';
|
| 12 |
+
import MetaBody from './MetaBody.svelte';
|
| 13 |
+
|
| 14 |
+
/** Dispatcher: picks the body component for a card variant.
|
| 15 |
+
* Spark + histogram share SparkBody (same shape, different source field).
|
| 16 |
+
* Raster + raster-pred share RasterBody (synthetic chrome carries the
|
| 17 |
+
* illustrative tag + dashed top-rule from the parent FindingCard). */
|
| 18 |
+
let { card }: { card: Card } = $props();
|
| 19 |
+
</script>
|
| 20 |
+
|
| 21 |
+
{#if card.variant === 'headline'}
|
| 22 |
+
<HeadlineBody {card} />
|
| 23 |
+
{:else if card.variant === 'tabular'}
|
| 24 |
+
<TabularBody {card} />
|
| 25 |
+
{:else if card.variant === 'scalars'}
|
| 26 |
+
<ScalarsBody {card} />
|
| 27 |
+
{:else if card.variant === 'spark' || card.variant === 'histogram'}
|
| 28 |
+
<SparkBody {card} />
|
| 29 |
+
{:else if card.variant === 'timeseries'}
|
| 30 |
+
<TimeseriesBody {card} />
|
| 31 |
+
{:else if card.variant === 'forecast'}
|
| 32 |
+
<ForecastBody {card} />
|
| 33 |
+
{:else if card.variant === 'raster' || card.variant === 'raster-pred'}
|
| 34 |
+
<RasterBody {card} />
|
| 35 |
+
{:else if card.variant === 'register'}
|
| 36 |
+
<RegisterBody {card} />
|
| 37 |
+
{:else if card.variant === 'comparison'}
|
| 38 |
+
<ComparisonBody {card} />
|
| 39 |
+
{:else if card.variant === 'meta'}
|
| 40 |
+
<MetaBody {card} />
|
| 41 |
+
{:else}
|
| 42 |
+
<div class="unknown">unknown variant: {card.variant}</div>
|
| 43 |
+
{/if}
|
| 44 |
+
|
| 45 |
+
<style>
|
| 46 |
+
.unknown {
|
| 47 |
+
padding: var(--s-3) var(--s-4);
|
| 48 |
+
font-family: var(--font-mono);
|
| 49 |
+
font-size: 11px;
|
| 50 |
+
color: var(--ink-tertiary);
|
| 51 |
+
}
|
| 52 |
+
</style>
|
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { Card } from '$lib/types/card';
|
| 3 |
+
import TierGlyph from '$lib/components/glyphs/TierGlyph.svelte';
|
| 4 |
+
let { card }: { card: Card } = $props();
|
| 5 |
+
</script>
|
| 6 |
+
|
| 7 |
+
<div class="body body-comparison">
|
| 8 |
+
<div class="cmp-grid">
|
| 9 |
+
{#if card.left}
|
| 10 |
+
<div class="cell">
|
| 11 |
+
<div class="cell-tier">
|
| 12 |
+
<TierGlyph tier={card.left.tier} size={10} color="var(--tier-{card.left.tier})" />
|
| 13 |
+
<span class="cell-label">{card.left.label}</span>
|
| 14 |
+
</div>
|
| 15 |
+
<div class="cell-value" style="color: var(--tier-{card.left.tier});">{card.left.value}</div>
|
| 16 |
+
{#if card.left.aux}<div class="cell-aux">{card.left.aux}</div>{/if}
|
| 17 |
+
</div>
|
| 18 |
+
{/if}
|
| 19 |
+
<div class="divider" aria-hidden="true">vs</div>
|
| 20 |
+
{#if card.right}
|
| 21 |
+
<div class="cell">
|
| 22 |
+
<div class="cell-tier">
|
| 23 |
+
<TierGlyph tier={card.right.tier} size={10} color="var(--tier-{card.right.tier})" />
|
| 24 |
+
<span class="cell-label">{card.right.label}</span>
|
| 25 |
+
</div>
|
| 26 |
+
<div class="cell-value" style="color: var(--tier-{card.right.tier});">{card.right.value}</div>
|
| 27 |
+
{#if card.right.aux}<div class="cell-aux">{card.right.aux}</div>{/if}
|
| 28 |
+
</div>
|
| 29 |
+
{/if}
|
| 30 |
+
</div>
|
| 31 |
+
{#if card.delta}<div class="cmp-delta">{card.delta}</div>{/if}
|
| 32 |
+
{#if card.sub}<div class="body-sub">{card.sub}</div>{/if}
|
| 33 |
+
</div>
|
| 34 |
+
|
| 35 |
+
<style>
|
| 36 |
+
.body-comparison {
|
| 37 |
+
padding: var(--s-3) var(--s-4) var(--s-3);
|
| 38 |
+
display: flex;
|
| 39 |
+
flex-direction: column;
|
| 40 |
+
gap: var(--s-2);
|
| 41 |
+
}
|
| 42 |
+
:global(.fc.is-compact) .body-comparison { padding: var(--s-2) var(--s-3); }
|
| 43 |
+
.cmp-grid {
|
| 44 |
+
display: grid;
|
| 45 |
+
grid-template-columns: 1fr auto 1fr;
|
| 46 |
+
gap: var(--s-3);
|
| 47 |
+
align-items: stretch;
|
| 48 |
+
}
|
| 49 |
+
.cell { display: flex; flex-direction: column; gap: 4px; }
|
| 50 |
+
.cell-tier {
|
| 51 |
+
display: inline-flex;
|
| 52 |
+
align-items: center;
|
| 53 |
+
gap: 4px;
|
| 54 |
+
font-family: var(--font-mono);
|
| 55 |
+
font-size: 10px;
|
| 56 |
+
color: var(--ink-tertiary);
|
| 57 |
+
letter-spacing: 0.05em;
|
| 58 |
+
text-transform: lowercase;
|
| 59 |
+
}
|
| 60 |
+
.cell-value {
|
| 61 |
+
font-family: var(--font-serif);
|
| 62 |
+
font-style: italic;
|
| 63 |
+
font-size: 22px;
|
| 64 |
+
font-weight: 500;
|
| 65 |
+
}
|
| 66 |
+
.cell-aux {
|
| 67 |
+
font-family: var(--font-mono);
|
| 68 |
+
font-size: 10px;
|
| 69 |
+
color: var(--ink-tertiary);
|
| 70 |
+
}
|
| 71 |
+
.divider {
|
| 72 |
+
align-self: center;
|
| 73 |
+
font-family: var(--font-serif);
|
| 74 |
+
font-style: italic;
|
| 75 |
+
font-size: 14px;
|
| 76 |
+
color: var(--ink-tertiary);
|
| 77 |
+
padding-top: 18px;
|
| 78 |
+
}
|
| 79 |
+
.cmp-delta {
|
| 80 |
+
font-family: var(--font-mono);
|
| 81 |
+
font-size: 11px;
|
| 82 |
+
color: var(--ink);
|
| 83 |
+
border-top: 1px solid var(--rule-soft);
|
| 84 |
+
padding-top: var(--s-2);
|
| 85 |
+
}
|
| 86 |
+
.body-sub {
|
| 87 |
+
font-family: var(--font-mono);
|
| 88 |
+
font-size: 11px;
|
| 89 |
+
color: var(--ink-tertiary);
|
| 90 |
+
line-height: 1.5;
|
| 91 |
+
}
|
| 92 |
+
</style>
|
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { Card, ForecastBand } from '$lib/types/card';
|
| 3 |
+
let { card }: { card: Card } = $props();
|
| 4 |
+
|
| 5 |
+
const W = 240, H = 88, PAD = 6;
|
| 6 |
+
|
| 7 |
+
let data = $derived<ForecastBand[]>(card.forecast ?? []);
|
| 8 |
+
let xs = $derived(data.map((_, i) => PAD + (i / Math.max(data.length - 1, 1)) * (W - PAD * 2)));
|
| 9 |
+
let max = $derived(Math.max(...data.map((d) => d.high), 1));
|
| 10 |
+
|
| 11 |
+
function y(v: number): number {
|
| 12 |
+
return H - PAD - (v / max) * (H - PAD * 2 - 12);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
let midD = $derived(xs.map((x, i) => `${i ? 'L' : 'M'} ${x} ${y(data[i].mid)}`).join(' '));
|
| 16 |
+
|
| 17 |
+
let areaD = $derived.by(() => {
|
| 18 |
+
if (!data.length) return '';
|
| 19 |
+
const lows = xs.map((x, i) => `${x} ${y(data[i].low)}`).join(' L ');
|
| 20 |
+
const highs = [...xs].reverse()
|
| 21 |
+
.map((x, idx) => `${x} ${y(data[data.length - 1 - idx].high)}`)
|
| 22 |
+
.join(' L ');
|
| 23 |
+
return `M ${lows} L ${highs} Z`;
|
| 24 |
+
});
|
| 25 |
+
</script>
|
| 26 |
+
|
| 27 |
+
<div class="body body-forecast">
|
| 28 |
+
<svg viewBox="0 0 {W} {H}" width="100%" height={H} aria-hidden="true">
|
| 29 |
+
<path d={areaD} fill="var(--tier-{card.tier})" fill-opacity="0.18" />
|
| 30 |
+
<path d={midD} fill="none" stroke="var(--tier-{card.tier})" stroke-width="1.5" />
|
| 31 |
+
{#each data as d, i}
|
| 32 |
+
<circle cx={xs[i]} cy={y(d.mid)} r="2.2" fill="var(--tier-{card.tier})" />
|
| 33 |
+
<text x={xs[i]} y={H - 1} font-size="9" font-family="IBM Plex Mono"
|
| 34 |
+
text-anchor="middle" fill="#6B6B6B">{d.year}</text>
|
| 35 |
+
{/each}
|
| 36 |
+
</svg>
|
| 37 |
+
{#if card.sub}<div class="body-sub">{card.sub}</div>{/if}
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
<style>
|
| 41 |
+
.body-forecast { padding: var(--s-3) var(--s-4) var(--s-3); }
|
| 42 |
+
:global(.fc.is-compact) .body-forecast { padding: var(--s-2) var(--s-3); }
|
| 43 |
+
svg { display: block; }
|
| 44 |
+
.body-sub {
|
| 45 |
+
margin-top: var(--s-2);
|
| 46 |
+
font-family: var(--font-mono);
|
| 47 |
+
font-size: 11px;
|
| 48 |
+
color: var(--ink-tertiary);
|
| 49 |
+
line-height: 1.5;
|
| 50 |
+
}
|
| 51 |
+
</style>
|
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { Card } from '$lib/types/card';
|
| 3 |
+
let { card }: { card: Card } = $props();
|
| 4 |
+
</script>
|
| 5 |
+
|
| 6 |
+
<div class="body body-headline">
|
| 7 |
+
<div class="headline" style="color: var(--tier-{card.tier});">{card.headline ?? ''}</div>
|
| 8 |
+
{#if card.subhead}<div class="subhead">{card.subhead}</div>{/if}
|
| 9 |
+
{#if card.body}<p class="body-prose">{card.body}</p>{/if}
|
| 10 |
+
</div>
|
| 11 |
+
|
| 12 |
+
<style>
|
| 13 |
+
.body-headline {
|
| 14 |
+
padding: var(--s-3) var(--s-4) var(--s-2);
|
| 15 |
+
display: flex;
|
| 16 |
+
flex-direction: column;
|
| 17 |
+
gap: var(--s-1);
|
| 18 |
+
}
|
| 19 |
+
:global(.fc.is-compact) .body-headline { padding: var(--s-2) var(--s-3); }
|
| 20 |
+
.headline {
|
| 21 |
+
font-family: var(--font-serif);
|
| 22 |
+
font-style: italic;
|
| 23 |
+
font-size: 28px;
|
| 24 |
+
font-weight: 500;
|
| 25 |
+
line-height: 1.1;
|
| 26 |
+
letter-spacing: -0.01em;
|
| 27 |
+
}
|
| 28 |
+
:global(.fc.is-compact) .headline { font-size: 22px; }
|
| 29 |
+
.subhead {
|
| 30 |
+
font-family: var(--font-mono);
|
| 31 |
+
font-size: 11px;
|
| 32 |
+
color: var(--ink-tertiary);
|
| 33 |
+
letter-spacing: 0.04em;
|
| 34 |
+
}
|
| 35 |
+
.body-prose {
|
| 36 |
+
margin: var(--s-2) 0 0;
|
| 37 |
+
font-size: 13px;
|
| 38 |
+
line-height: 1.45;
|
| 39 |
+
color: var(--ink-secondary);
|
| 40 |
+
}
|
| 41 |
+
</style>
|
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { Card } from '$lib/types/card';
|
| 3 |
+
let { card }: { card: Card } = $props();
|
| 4 |
+
</script>
|
| 5 |
+
|
| 6 |
+
<div class="body body-meta">
|
| 7 |
+
<dl class="meta-list">
|
| 8 |
+
{#each card.metaRows ?? [] as r}
|
| 9 |
+
<div class="meta-row">
|
| 10 |
+
<dt>{r.k}</dt>
|
| 11 |
+
<dd>{r.v}</dd>
|
| 12 |
+
</div>
|
| 13 |
+
{/each}
|
| 14 |
+
</dl>
|
| 15 |
+
{#if card.sub}<div class="body-sub">{card.sub}</div>{/if}
|
| 16 |
+
</div>
|
| 17 |
+
|
| 18 |
+
<style>
|
| 19 |
+
.body-meta { padding: var(--s-2) var(--s-4) var(--s-3); }
|
| 20 |
+
:global(.fc.is-compact) .body-meta { padding: var(--s-2) var(--s-3); }
|
| 21 |
+
.meta-list {
|
| 22 |
+
margin: 0;
|
| 23 |
+
display: grid;
|
| 24 |
+
grid-template-columns: 1fr;
|
| 25 |
+
gap: 4px;
|
| 26 |
+
}
|
| 27 |
+
.meta-row {
|
| 28 |
+
display: grid;
|
| 29 |
+
grid-template-columns: minmax(110px, max-content) 1fr;
|
| 30 |
+
gap: var(--s-3);
|
| 31 |
+
padding: 3px 0;
|
| 32 |
+
border-bottom: 1px solid var(--rule-soft);
|
| 33 |
+
align-items: baseline;
|
| 34 |
+
}
|
| 35 |
+
.meta-row:last-child { border-bottom: 0; }
|
| 36 |
+
dt {
|
| 37 |
+
font-family: var(--font-mono);
|
| 38 |
+
font-size: 11px;
|
| 39 |
+
color: var(--ink-tertiary);
|
| 40 |
+
text-transform: lowercase;
|
| 41 |
+
letter-spacing: 0.04em;
|
| 42 |
+
}
|
| 43 |
+
dd {
|
| 44 |
+
margin: 0;
|
| 45 |
+
font-family: var(--font-mono);
|
| 46 |
+
font-size: 12px;
|
| 47 |
+
color: var(--ink);
|
| 48 |
+
}
|
| 49 |
+
.body-sub {
|
| 50 |
+
margin-top: var(--s-2);
|
| 51 |
+
font-family: var(--font-mono);
|
| 52 |
+
font-size: 11px;
|
| 53 |
+
color: var(--ink-tertiary);
|
| 54 |
+
line-height: 1.5;
|
| 55 |
+
}
|
| 56 |
+
</style>
|
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { Card } from '$lib/types/card';
|
| 3 |
+
import RasterThumb from './RasterThumb.svelte';
|
| 4 |
+
let { card }: { card: Card } = $props();
|
| 5 |
+
</script>
|
| 6 |
+
|
| 7 |
+
<div class="body body-raster">
|
| 8 |
+
<div class="frame">
|
| 9 |
+
<RasterThumb kind={card.rasterKind} />
|
| 10 |
+
{#if card.illustrative || card.tier === 'synthetic'}
|
| 11 |
+
<span class="illustrative" title="Illustrative rendering, not source pixels">illustrative</span>
|
| 12 |
+
{/if}
|
| 13 |
+
</div>
|
| 14 |
+
{#if card.headline}
|
| 15 |
+
<div class="raster-headline">
|
| 16 |
+
<span style="color: var(--tier-{card.tier});">{card.headline}</span>
|
| 17 |
+
{#if card.subhead}<span> · {card.subhead}</span>{/if}
|
| 18 |
+
</div>
|
| 19 |
+
{/if}
|
| 20 |
+
{#if card.sub}<div class="body-sub">{card.sub}</div>{/if}
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<style>
|
| 24 |
+
.body-raster { padding: var(--s-2) var(--s-4) var(--s-3); display: flex; flex-direction: column; gap: var(--s-2); }
|
| 25 |
+
:global(.fc.is-compact) .body-raster { padding: var(--s-2) var(--s-3); }
|
| 26 |
+
.frame { position: relative; border: 1px solid var(--rule-soft); }
|
| 27 |
+
.illustrative {
|
| 28 |
+
position: absolute;
|
| 29 |
+
top: 6px;
|
| 30 |
+
right: 6px;
|
| 31 |
+
background: rgba(26, 26, 26, 0.7);
|
| 32 |
+
color: var(--paper);
|
| 33 |
+
font-family: var(--font-mono);
|
| 34 |
+
font-size: 9px;
|
| 35 |
+
padding: 2px 6px;
|
| 36 |
+
letter-spacing: 0.05em;
|
| 37 |
+
text-transform: lowercase;
|
| 38 |
+
}
|
| 39 |
+
.raster-headline {
|
| 40 |
+
font-family: var(--font-mono);
|
| 41 |
+
font-size: 12px;
|
| 42 |
+
color: var(--ink);
|
| 43 |
+
}
|
| 44 |
+
.raster-headline span:first-child {
|
| 45 |
+
font-family: var(--font-serif);
|
| 46 |
+
font-style: italic;
|
| 47 |
+
font-size: 18px;
|
| 48 |
+
font-weight: 500;
|
| 49 |
+
}
|
| 50 |
+
.body-sub {
|
| 51 |
+
font-family: var(--font-mono);
|
| 52 |
+
font-size: 11px;
|
| 53 |
+
color: var(--ink-tertiary);
|
| 54 |
+
line-height: 1.5;
|
| 55 |
+
}
|
| 56 |
+
</style>
|
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { RasterKind } from '$lib/types/card';
|
| 3 |
+
|
| 4 |
+
/** Hand-drawn SVG approximations of each raster layer's conventional
|
| 5 |
+
* palette. Replace with real MapLibre tile snapshots when the tile
|
| 6 |
+
* pipeline is wired up. Mirrors findings.jsx → RasterThumb exactly. */
|
| 7 |
+
let { kind }: { kind?: RasterKind } = $props();
|
| 8 |
+
|
| 9 |
+
const W = 240, H = 120;
|
| 10 |
+
</script>
|
| 11 |
+
|
| 12 |
+
{#if kind === 'stormwater'}
|
| 13 |
+
<svg viewBox="0 0 {W} {H}" width="100%" height={H} aria-hidden="true">
|
| 14 |
+
<rect width={W} height={H} fill="#F2F2EE" />
|
| 15 |
+
<g stroke="#D9D6CC" stroke-width="0.6">
|
| 16 |
+
<line x1="0" y1="40" x2={W} y2="40" />
|
| 17 |
+
<line x1="0" y1="80" x2={W} y2="80" />
|
| 18 |
+
<line x1="60" y1="0" x2="60" y2={H} />
|
| 19 |
+
<line x1="160" y1="0" x2="160" y2={H} />
|
| 20 |
+
</g>
|
| 21 |
+
<path d="M20 50 Q 60 38 90 56 Q 120 76 150 64 Q 180 50 180 86 Q 130 100 70 96 Q 30 92 20 76 Z"
|
| 22 |
+
fill="rgba(42,111,168,0.32)" stroke="#2A6FA8" stroke-width="0.7" />
|
| 23 |
+
<path d="M40 60 Q 80 54 110 70 Q 140 84 160 78 Q 165 90 130 92 Q 80 90 50 82 Z"
|
| 24 |
+
fill="rgba(11,83,148,0.36)" stroke="#0B5394" stroke-width="0.6" />
|
| 25 |
+
<circle cx="120" cy="74" r="3.2" fill="#D17C00" stroke="#FAFAF7" stroke-width="1.3" />
|
| 26 |
+
<text x={W - 6} y={H - 5} font-size="8" font-family="IBM Plex Mono" text-anchor="end" fill="#6B6B6B">2.13 in/hr · MOD</text>
|
| 27 |
+
</svg>
|
| 28 |
+
{:else if kind === 'stormwater-dry'}
|
| 29 |
+
<svg viewBox="0 0 {W} {H}" width="100%" height={H} aria-hidden="true">
|
| 30 |
+
<rect width={W} height={H} fill="#F2F2EE" />
|
| 31 |
+
<g stroke="#D9D6CC" stroke-width="0.6">
|
| 32 |
+
<line x1="0" y1="40" x2={W} y2="40" />
|
| 33 |
+
<line x1="0" y1="80" x2={W} y2="80" />
|
| 34 |
+
<line x1="60" y1="0" x2="60" y2={H} />
|
| 35 |
+
<line x1="160" y1="0" x2="160" y2={H} />
|
| 36 |
+
</g>
|
| 37 |
+
<path d="M180 92 Q 200 88 215 96 Q 220 105 200 104 Q 185 102 180 96 Z"
|
| 38 |
+
fill="rgba(42,111,168,0.18)" stroke="#2A6FA8" stroke-width="0.5" stroke-dasharray="2 2" />
|
| 39 |
+
<circle cx="120" cy="60" r="3.2" fill="#D17C00" stroke="#FAFAF7" stroke-width="1.3" />
|
| 40 |
+
<text x={W - 6} y={H - 5} font-size="8" font-family="IBM Plex Mono" text-anchor="end" fill="#6B6B6B">no ponding · MOD</text>
|
| 41 |
+
</svg>
|
| 42 |
+
{:else if kind === 'prithvi'}
|
| 43 |
+
<svg viewBox="0 0 {W} {H}" width="100%" height={H} aria-hidden="true">
|
| 44 |
+
<defs>
|
| 45 |
+
<pattern id="rt-s2-rgb" x="0" y="0" width="6" height="6" patternUnits="userSpaceOnUse">
|
| 46 |
+
<rect width="6" height="6" fill="#7A8E6A" />
|
| 47 |
+
<rect x="0" y="0" width="3" height="3" fill="#8D9C7A" />
|
| 48 |
+
<rect x="3" y="3" width="3" height="3" fill="#69795D" />
|
| 49 |
+
</pattern>
|
| 50 |
+
</defs>
|
| 51 |
+
<rect width={W} height={H} fill="url(#rt-s2-rgb)" />
|
| 52 |
+
<rect x="0" y="55" width={W} height="6" fill="#A8A496" />
|
| 53 |
+
<rect x="115" y="0" width="8" height={H} fill="#A8A496" />
|
| 54 |
+
<ellipse cx="50" cy="92" rx="6" ry="3" fill="#2A6FA8" fill-opacity="0.65" />
|
| 55 |
+
<text x="6" y="14" font-size="9" font-family="IBM Plex Mono" fill="#FAFAF7">PRITHVI · 0.3%</text>
|
| 56 |
+
<text x={W - 6} y={H - 5} font-size="8" font-family="IBM Plex Mono" text-anchor="end" fill="#FAFAF7">scene 2026-05-02</text>
|
| 57 |
+
</svg>
|
| 58 |
+
{:else if kind === 'lulc'}
|
| 59 |
+
<svg viewBox="0 0 {W} {H}" width="100%" height={H} aria-hidden="true">
|
| 60 |
+
<rect width={W} height={H} fill="#F2F2EE" />
|
| 61 |
+
<rect x="0" y="0" width="80" height="60" fill="#C66" />
|
| 62 |
+
<rect x="80" y="0" width="60" height="60" fill="#C66" />
|
| 63 |
+
<rect x="140" y="0" width="100" height="38" fill="#C66" />
|
| 64 |
+
<rect x="140" y="38" width="100" height="22" fill="#5B7FB4" />
|
| 65 |
+
<rect x="0" y="60" width="100" height="60" fill="#C66" />
|
| 66 |
+
<rect x="100" y="60" width="50" height="40" fill="#5B8A4A" />
|
| 67 |
+
<rect x="150" y="60" width="50" height="60" fill="#D9C75A" />
|
| 68 |
+
<rect x="200" y="60" width="40" height="60" fill="#C66" />
|
| 69 |
+
<rect x="100" y="100" width="50" height="20" fill="#A89A78" />
|
| 70 |
+
<text x="6" y="14" font-size="9" font-family="IBM Plex Mono" fill="#FAFAF7">LULC · TerraMind</text>
|
| 71 |
+
<text x={W - 6} y={H - 5} font-size="8" font-family="IBM Plex Mono" text-anchor="end" fill="#FAFAF7">scene 2026-05-02</text>
|
| 72 |
+
</svg>
|
| 73 |
+
{:else if kind === 'buildings'}
|
| 74 |
+
<svg viewBox="0 0 {W} {H}" width="100%" height={H} aria-hidden="true">
|
| 75 |
+
<rect width={W} height={H} fill="#3A3A38" />
|
| 76 |
+
{#each [
|
| 77 |
+
[10,10,28,18],[42,10,30,16],[78,10,40,22],[124,10,32,18],[162,10,30,18],[198,10,32,18],
|
| 78 |
+
[10,32,28,16],[42,30,30,18],[124,32,32,16],[162,32,30,16],[198,32,32,16],
|
| 79 |
+
[10,55,28,18],[42,55,30,18],[78,55,40,18],[124,55,32,18],[162,55,30,18],[198,55,32,18],
|
| 80 |
+
[10,80,28,16],[42,80,30,16],[78,80,40,16],[124,80,32,16],[162,80,30,16],
|
| 81 |
+
[10,100,28,12],[42,100,30,12],[78,100,40,12]
|
| 82 |
+
] as r}
|
| 83 |
+
<rect x={r[0]} y={r[1]} width={r[2]} height={r[3]}
|
| 84 |
+
fill="rgba(42,111,168,0.55)" stroke="#2A6FA8" stroke-width="0.4" />
|
| 85 |
+
{/each}
|
| 86 |
+
<text x="6" y="14" font-size="9" font-family="IBM Plex Mono" fill="#FAFAF7">BLDG · TerraMind</text>
|
| 87 |
+
<text x={W - 6} y={H - 5} font-size="8" font-family="IBM Plex Mono" text-anchor="end" fill="#FAFAF7">36.2% built</text>
|
| 88 |
+
</svg>
|
| 89 |
+
{:else}
|
| 90 |
+
<div class="thumb-placeholder">raster preview</div>
|
| 91 |
+
{/if}
|
| 92 |
+
|
| 93 |
+
<style>
|
| 94 |
+
svg { display: block; }
|
| 95 |
+
.thumb-placeholder {
|
| 96 |
+
height: 120px;
|
| 97 |
+
background: var(--paper-deep);
|
| 98 |
+
display: flex;
|
| 99 |
+
align-items: center;
|
| 100 |
+
justify-content: center;
|
| 101 |
+
color: var(--ink-tertiary);
|
| 102 |
+
font-family: var(--font-mono);
|
| 103 |
+
font-size: 11px;
|
| 104 |
+
border: 1px dashed var(--rule-soft);
|
| 105 |
+
}
|
| 106 |
+
</style>
|
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { Card } from '$lib/types/card';
|
| 3 |
+
import TierGlyph from '$lib/components/glyphs/TierGlyph.svelte';
|
| 4 |
+
let { card }: { card: Card } = $props();
|
| 5 |
+
</script>
|
| 6 |
+
|
| 7 |
+
<div class="body body-register">
|
| 8 |
+
<ul class="reg-list">
|
| 9 |
+
{#each card.registers ?? [] as r}
|
| 10 |
+
<li class="reg-row" class:silent={!r.label}>
|
| 11 |
+
<span class="reg-tag" title={r.tier}>
|
| 12 |
+
<TierGlyph tier={r.tier} size={9} color="var(--tier-{r.tier})" />
|
| 13 |
+
<span>{r.reg}</span>
|
| 14 |
+
</span>
|
| 15 |
+
{#if r.label}
|
| 16 |
+
<span class="reg-label" title={r.detail ? `${r.label} — ${r.detail}` : r.label}>{r.label}</span>
|
| 17 |
+
<span class="reg-source">{r.sourceId ?? ''}</span>
|
| 18 |
+
{:else}
|
| 19 |
+
<span class="reg-silent">{r.note}</span>
|
| 20 |
+
{/if}
|
| 21 |
+
</li>
|
| 22 |
+
{/each}
|
| 23 |
+
</ul>
|
| 24 |
+
{#if card.sub}<div class="body-sub">{card.sub}</div>{/if}
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
<style>
|
| 28 |
+
.body-register { padding: var(--s-2) var(--s-4) var(--s-3); }
|
| 29 |
+
:global(.fc.is-compact) .body-register { padding: var(--s-2) var(--s-3); }
|
| 30 |
+
.reg-list {
|
| 31 |
+
list-style: none;
|
| 32 |
+
margin: 0;
|
| 33 |
+
padding: 0;
|
| 34 |
+
display: flex;
|
| 35 |
+
flex-direction: column;
|
| 36 |
+
}
|
| 37 |
+
.reg-row {
|
| 38 |
+
display: grid;
|
| 39 |
+
grid-template-columns: 70px 1fr auto;
|
| 40 |
+
gap: var(--s-2);
|
| 41 |
+
align-items: baseline;
|
| 42 |
+
padding: 5px 0;
|
| 43 |
+
border-bottom: 1px solid var(--rule-soft);
|
| 44 |
+
font-family: var(--font-mono);
|
| 45 |
+
font-size: 12px;
|
| 46 |
+
line-height: 1.4;
|
| 47 |
+
}
|
| 48 |
+
.reg-row:last-child { border-bottom: 0; }
|
| 49 |
+
:global(.fc.is-compact) .reg-row { padding: 3px 0; font-size: 11px; }
|
| 50 |
+
.reg-tag {
|
| 51 |
+
display: inline-flex;
|
| 52 |
+
gap: 4px;
|
| 53 |
+
align-items: center;
|
| 54 |
+
color: var(--ink-tertiary);
|
| 55 |
+
text-transform: uppercase;
|
| 56 |
+
letter-spacing: 0.05em;
|
| 57 |
+
font-weight: 500;
|
| 58 |
+
}
|
| 59 |
+
.reg-label {
|
| 60 |
+
color: var(--ink);
|
| 61 |
+
overflow: hidden;
|
| 62 |
+
text-overflow: ellipsis;
|
| 63 |
+
white-space: nowrap;
|
| 64 |
+
}
|
| 65 |
+
.reg-source {
|
| 66 |
+
color: var(--ink-tertiary);
|
| 67 |
+
font-size: 10px;
|
| 68 |
+
letter-spacing: 0.05em;
|
| 69 |
+
}
|
| 70 |
+
.reg-silent {
|
| 71 |
+
grid-column: 2 / span 2;
|
| 72 |
+
color: var(--ink-tertiary);
|
| 73 |
+
font-style: italic;
|
| 74 |
+
}
|
| 75 |
+
.reg-row.silent { opacity: 0.65; }
|
| 76 |
+
.body-sub {
|
| 77 |
+
margin-top: var(--s-2);
|
| 78 |
+
font-family: var(--font-mono);
|
| 79 |
+
font-size: 11px;
|
| 80 |
+
color: var(--ink-tertiary);
|
| 81 |
+
line-height: 1.5;
|
| 82 |
+
}
|
| 83 |
+
</style>
|
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { Card } from '$lib/types/card';
|
| 3 |
+
let { card }: { card: Card } = $props();
|
| 4 |
+
</script>
|
| 5 |
+
|
| 6 |
+
<div class="body body-scalars">
|
| 7 |
+
<div class="row">
|
| 8 |
+
{#each card.scalars ?? [] as s}
|
| 9 |
+
<div class="cell">
|
| 10 |
+
<div class="value" style="color: var(--tier-{card.tier});">{s.value}</div>
|
| 11 |
+
<div class="label">{s.label}</div>
|
| 12 |
+
</div>
|
| 13 |
+
{/each}
|
| 14 |
+
</div>
|
| 15 |
+
{#if card.sub}<div class="body-sub">{card.sub}</div>{/if}
|
| 16 |
+
</div>
|
| 17 |
+
|
| 18 |
+
<style>
|
| 19 |
+
.body-scalars { padding: var(--s-3) var(--s-4) var(--s-3); }
|
| 20 |
+
:global(.fc.is-compact) .body-scalars { padding: var(--s-2) var(--s-3); }
|
| 21 |
+
.row {
|
| 22 |
+
display: grid;
|
| 23 |
+
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
|
| 24 |
+
gap: var(--s-3);
|
| 25 |
+
}
|
| 26 |
+
.cell { display: flex; flex-direction: column; gap: 2px; }
|
| 27 |
+
.value {
|
| 28 |
+
font-family: var(--font-serif);
|
| 29 |
+
font-style: italic;
|
| 30 |
+
font-size: 22px;
|
| 31 |
+
font-weight: 500;
|
| 32 |
+
line-height: 1.1;
|
| 33 |
+
}
|
| 34 |
+
.label {
|
| 35 |
+
font-family: var(--font-mono);
|
| 36 |
+
font-size: 10px;
|
| 37 |
+
color: var(--ink-tertiary);
|
| 38 |
+
letter-spacing: 0.06em;
|
| 39 |
+
text-transform: lowercase;
|
| 40 |
+
}
|
| 41 |
+
.body-sub {
|
| 42 |
+
margin-top: var(--s-3);
|
| 43 |
+
font-family: var(--font-mono);
|
| 44 |
+
font-size: 11px;
|
| 45 |
+
color: var(--ink-tertiary);
|
| 46 |
+
line-height: 1.5;
|
| 47 |
+
}
|
| 48 |
+
</style>
|
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { Card } from '$lib/types/card';
|
| 3 |
+
let { card }: { card: Card } = $props();
|
| 4 |
+
|
| 5 |
+
/** spark + histogram both render the same shape — bars filling 240×38. */
|
| 6 |
+
const W = 240, H = 38;
|
| 7 |
+
let data = $derived(card.spark ?? card.histogram ?? []);
|
| 8 |
+
let max = $derived(Math.max(...data, 1));
|
| 9 |
+
let n = $derived(data.length);
|
| 10 |
+
let barW = $derived(Math.max(2, W / Math.max(n, 1) - 1.5));
|
| 11 |
+
</script>
|
| 12 |
+
|
| 13 |
+
<div class="body body-spark">
|
| 14 |
+
{#if card.headline}
|
| 15 |
+
<div class="headline" style="color: var(--tier-{card.tier});">{card.headline}</div>
|
| 16 |
+
{/if}
|
| 17 |
+
{#if card.subhead}<div class="subhead">{card.subhead}</div>{/if}
|
| 18 |
+
<svg viewBox="0 0 {W} {H}" width="100%" height={H} preserveAspectRatio="none" aria-hidden="true">
|
| 19 |
+
{#each data as v, i}
|
| 20 |
+
<rect
|
| 21 |
+
x={(i / n) * W + 0.5}
|
| 22 |
+
y={H - (v / max) * H}
|
| 23 |
+
width={barW}
|
| 24 |
+
height={(v / max) * H}
|
| 25 |
+
fill="var(--tier-{card.tier})"
|
| 26 |
+
/>
|
| 27 |
+
{/each}
|
| 28 |
+
</svg>
|
| 29 |
+
{#if card.sparkSub}<div class="body-sub">{card.sparkSub}</div>{/if}
|
| 30 |
+
{#if !card.sparkSub && card.sub}<div class="body-sub">{card.sub}</div>{/if}
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<style>
|
| 34 |
+
.body-spark {
|
| 35 |
+
padding: var(--s-3) var(--s-4) var(--s-3);
|
| 36 |
+
display: flex;
|
| 37 |
+
flex-direction: column;
|
| 38 |
+
gap: var(--s-1);
|
| 39 |
+
}
|
| 40 |
+
:global(.fc.is-compact) .body-spark { padding: var(--s-2) var(--s-3); }
|
| 41 |
+
.headline {
|
| 42 |
+
font-family: var(--font-serif);
|
| 43 |
+
font-style: italic;
|
| 44 |
+
font-size: 22px;
|
| 45 |
+
font-weight: 500;
|
| 46 |
+
line-height: 1.1;
|
| 47 |
+
}
|
| 48 |
+
.subhead {
|
| 49 |
+
font-family: var(--font-mono);
|
| 50 |
+
font-size: 11px;
|
| 51 |
+
color: var(--ink-tertiary);
|
| 52 |
+
letter-spacing: 0.04em;
|
| 53 |
+
margin-bottom: var(--s-1);
|
| 54 |
+
}
|
| 55 |
+
svg { display: block; }
|
| 56 |
+
.body-sub {
|
| 57 |
+
margin-top: var(--s-2);
|
| 58 |
+
font-family: var(--font-mono);
|
| 59 |
+
font-size: 11px;
|
| 60 |
+
color: var(--ink-tertiary);
|
| 61 |
+
line-height: 1.5;
|
| 62 |
+
}
|
| 63 |
+
</style>
|
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { Card } from '$lib/types/card';
|
| 3 |
+
let { card }: { card: Card } = $props();
|
| 4 |
+
</script>
|
| 5 |
+
|
| 6 |
+
<div class="body body-tabular">
|
| 7 |
+
<table class="t">
|
| 8 |
+
<thead>
|
| 9 |
+
<tr>
|
| 10 |
+
{#each card.columns ?? [] as h}<th>{h}</th>{/each}
|
| 11 |
+
</tr>
|
| 12 |
+
</thead>
|
| 13 |
+
<tbody>
|
| 14 |
+
{#each card.rows ?? [] as row}
|
| 15 |
+
<tr>{#each row as cell}<td>{cell}</td>{/each}</tr>
|
| 16 |
+
{/each}
|
| 17 |
+
</tbody>
|
| 18 |
+
</table>
|
| 19 |
+
{#if card.sub}<div class="body-sub">{card.sub}</div>{/if}
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<style>
|
| 23 |
+
.body-tabular { padding: var(--s-2) var(--s-4) var(--s-3); }
|
| 24 |
+
:global(.fc.is-compact) .body-tabular { padding: var(--s-2) var(--s-3); }
|
| 25 |
+
.t {
|
| 26 |
+
width: 100%;
|
| 27 |
+
border-collapse: collapse;
|
| 28 |
+
font-family: var(--font-mono);
|
| 29 |
+
font-size: 12px;
|
| 30 |
+
}
|
| 31 |
+
.t th {
|
| 32 |
+
text-align: left;
|
| 33 |
+
font-weight: 500;
|
| 34 |
+
color: var(--ink-tertiary);
|
| 35 |
+
padding: 4px 8px 4px 0;
|
| 36 |
+
border-bottom: 1px solid var(--rule-soft);
|
| 37 |
+
text-transform: lowercase;
|
| 38 |
+
letter-spacing: 0.04em;
|
| 39 |
+
}
|
| 40 |
+
.t td {
|
| 41 |
+
padding: 4px 8px 4px 0;
|
| 42 |
+
border-bottom: 1px solid var(--rule-soft);
|
| 43 |
+
color: var(--ink);
|
| 44 |
+
}
|
| 45 |
+
.t tr:last-child td { border-bottom: 0; }
|
| 46 |
+
:global(.fc.is-compact) .t th,
|
| 47 |
+
:global(.fc.is-compact) .t td { padding: 2px 6px 2px 0; font-size: 11px; }
|
| 48 |
+
.body-sub {
|
| 49 |
+
margin-top: var(--s-2);
|
| 50 |
+
font-family: var(--font-mono);
|
| 51 |
+
font-size: 11px;
|
| 52 |
+
color: var(--ink-tertiary);
|
| 53 |
+
line-height: 1.5;
|
| 54 |
+
}
|
| 55 |
+
</style>
|
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { Card } from '$lib/types/card';
|
| 3 |
+
let { card }: { card: Card } = $props();
|
| 4 |
+
|
| 5 |
+
const W = 240, H = 84, PAD = 6;
|
| 6 |
+
|
| 7 |
+
// Synthetic surge curve: harmonic baseline + storm pulse around peak.
|
| 8 |
+
// Mirrors findings.jsx exactly so the visual matches the prototype.
|
| 9 |
+
const ts = $derived(card.timeseries ?? { hours: 96, peak: { x: 38, y: 47 }, peakLabel: '' });
|
| 10 |
+
const points = $derived(buildPoints(ts));
|
| 11 |
+
const yScale = $derived(buildScale(points, ts));
|
| 12 |
+
const pathD = $derived(buildPath(points, yScale));
|
| 13 |
+
|
| 14 |
+
type Pt = { x: number; y: number };
|
| 15 |
+
|
| 16 |
+
function buildPoints(t: { hours: number; peak: { x: number; y: number } }): Pt[] {
|
| 17 |
+
const out: Pt[] = [];
|
| 18 |
+
for (let i = 0; i <= t.hours; i++) {
|
| 19 |
+
const harmonic = 6 * Math.sin((i / 12.42) * Math.PI * 2);
|
| 20 |
+
const pulse = 38 * Math.exp(-Math.pow((i - t.peak.x) / 12, 2));
|
| 21 |
+
out.push({ x: i, y: harmonic + pulse + 4 });
|
| 22 |
+
}
|
| 23 |
+
return out;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function buildScale(pts: Pt[], t: { hours: number; peak: { x: number; y: number } }) {
|
| 27 |
+
const maxY = Math.max(...pts.map(p => p.y), t.peak.y);
|
| 28 |
+
const minY = Math.min(...pts.map(p => p.y), -10);
|
| 29 |
+
return {
|
| 30 |
+
sx: (i: number) => PAD + (i / t.hours) * (W - PAD * 2),
|
| 31 |
+
sy: (v: number) => H - PAD - 14 - ((v - minY) / (maxY - minY)) * (H - PAD * 2 - 14),
|
| 32 |
+
};
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
function buildPath(pts: Pt[], s: { sx: (i: number) => number; sy: (v: number) => number }): string {
|
| 36 |
+
return pts.map((p, i) => `${i ? 'L' : 'M'} ${s.sx(p.x)} ${s.sy(p.y)}`).join(' ');
|
| 37 |
+
}
|
| 38 |
+
</script>
|
| 39 |
+
|
| 40 |
+
<div class="body body-timeseries">
|
| 41 |
+
<div class="ts-header">
|
| 42 |
+
{#if card.headline}
|
| 43 |
+
<span class="headline" style="color: var(--tier-{card.tier});">{card.headline}</span>
|
| 44 |
+
{/if}
|
| 45 |
+
{#if card.subhead}<span class="subhead">{card.subhead}</span>{/if}
|
| 46 |
+
</div>
|
| 47 |
+
<svg viewBox="0 0 {W} {H}" width="100%" height={H} aria-hidden="true">
|
| 48 |
+
<line x1={PAD} y1={yScale.sy(0)} x2={W - PAD} y2={yScale.sy(0)}
|
| 49 |
+
stroke="#C9C9C5" stroke-width="0.5" stroke-dasharray="2 2" />
|
| 50 |
+
<path d={pathD} fill="none" stroke="var(--tier-{card.tier})" stroke-width="1.4" />
|
| 51 |
+
<circle cx={yScale.sx(ts.peak.x)} cy={yScale.sy(ts.peak.y)} r="3" fill="var(--tier-{card.tier})" />
|
| 52 |
+
<text x={yScale.sx(ts.peak.x)} y={yScale.sy(ts.peak.y) - 6}
|
| 53 |
+
font-size="9" font-family="IBM Plex Mono"
|
| 54 |
+
text-anchor="middle" fill="var(--tier-{card.tier})">{ts.peakLabel}</text>
|
| 55 |
+
<text x={PAD} y={H - 2} font-size="8" font-family="IBM Plex Mono" fill="#6B6B6B">now</text>
|
| 56 |
+
<text x={W - PAD} y={H - 2} font-size="8" font-family="IBM Plex Mono"
|
| 57 |
+
text-anchor="end" fill="#6B6B6B">+{ts.hours}h</text>
|
| 58 |
+
</svg>
|
| 59 |
+
{#if card.spatialNote || card.sub}
|
| 60 |
+
<div class="body-sub">
|
| 61 |
+
{#if card.spatialNote}<span class="spatial-note">{card.spatialNote}</span>{/if}
|
| 62 |
+
{#if card.sub}<span>{card.sub}</span>{/if}
|
| 63 |
+
</div>
|
| 64 |
+
{/if}
|
| 65 |
+
</div>
|
| 66 |
+
|
| 67 |
+
<style>
|
| 68 |
+
.body-timeseries {
|
| 69 |
+
padding: var(--s-3) var(--s-4) var(--s-3);
|
| 70 |
+
display: flex;
|
| 71 |
+
flex-direction: column;
|
| 72 |
+
gap: var(--s-2);
|
| 73 |
+
}
|
| 74 |
+
:global(.fc.is-compact) .body-timeseries { padding: var(--s-2) var(--s-3); }
|
| 75 |
+
.ts-header {
|
| 76 |
+
display: flex;
|
| 77 |
+
align-items: baseline;
|
| 78 |
+
flex-wrap: wrap;
|
| 79 |
+
gap: var(--s-2);
|
| 80 |
+
}
|
| 81 |
+
.headline {
|
| 82 |
+
font-family: var(--font-serif);
|
| 83 |
+
font-style: italic;
|
| 84 |
+
font-size: 22px;
|
| 85 |
+
font-weight: 500;
|
| 86 |
+
line-height: 1.1;
|
| 87 |
+
}
|
| 88 |
+
.subhead {
|
| 89 |
+
font-family: var(--font-mono);
|
| 90 |
+
font-size: 11px;
|
| 91 |
+
color: var(--ink-tertiary);
|
| 92 |
+
letter-spacing: 0.04em;
|
| 93 |
+
}
|
| 94 |
+
svg { display: block; }
|
| 95 |
+
.body-sub {
|
| 96 |
+
font-family: var(--font-mono);
|
| 97 |
+
font-size: 11px;
|
| 98 |
+
color: var(--ink-tertiary);
|
| 99 |
+
line-height: 1.5;
|
| 100 |
+
display: flex;
|
| 101 |
+
flex-direction: column;
|
| 102 |
+
gap: 2px;
|
| 103 |
+
}
|
| 104 |
+
.spatial-note {
|
| 105 |
+
color: var(--accent);
|
| 106 |
+
font-style: italic;
|
| 107 |
+
}
|
| 108 |
+
</style>
|
|
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Findings v0.4.4 sample data — Red Hook query.
|
| 3 |
+
*
|
| 4 |
+
* Mirrors the CARDS / CARDS_BY_QUERY tables in
|
| 5 |
+
* docs/design_handoff/design_files/findings.jsx so the SvelteKit Findings
|
| 6 |
+
* region can be rendered without a live FSM. Use as fixture for
|
| 7 |
+
* `routes/q/sample/+page.svelte` and Playwright snapshots.
|
| 8 |
+
*
|
| 9 |
+
* Stone-trace member ids match the FSM step names where possible so the
|
| 10 |
+
* adapter can replace this with live data without changing card copy.
|
| 11 |
+
*/
|
| 12 |
+
import type { FindingsData } from '$lib/types/card';
|
| 13 |
+
|
| 14 |
+
export const SAMPLE_FINDINGS: FindingsData = {
|
| 15 |
+
wallSeconds: 14.0,
|
| 16 |
+
cacheHit: 0.92,
|
| 17 |
+
cards: [
|
| 18 |
+
/* ── Cornerstone ── */
|
| 19 |
+
{
|
| 20 |
+
id: 'fc-fema',
|
| 21 |
+
stone: 'cornerstone', tier: 'modeled', variant: 'headline',
|
| 22 |
+
source: 'FEMA', agency: 'Federal Emergency Management Agency',
|
| 23 |
+
vintage: '2024-09',
|
| 24 |
+
title: 'Preliminary FIRM, panel 36047C0207G',
|
| 25 |
+
headline: 'Zone AE',
|
| 26 |
+
subhead: 'BFE 11 ft NAVD88 · freeboard +4.8 ft',
|
| 27 |
+
body: 'Address sits within the regulatory 1% annual-chance floodplain. Base Flood Elevation 11.0 ft NAVD88; first floor must be at or above this datum for NFIP rating.',
|
| 28 |
+
docId: 'FEMA-FIRM-36047C0207G', citeId: 'c4',
|
| 29 |
+
mapLayer: 'fema-ae',
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
id: 'fc-hwm',
|
| 33 |
+
stone: 'cornerstone', tier: 'empirical', variant: 'tabular',
|
| 34 |
+
source: 'USGS', agency: 'U.S. Geological Survey',
|
| 35 |
+
vintage: '2013-05',
|
| 36 |
+
title: 'Post-Sandy high-water marks within 500 ft',
|
| 37 |
+
columns: ['id', 'elev.', 'dist.'],
|
| 38 |
+
rows: [
|
| 39 |
+
['HWM-NY-3081', '7.4 ft NAVD88', '0.18 mi'],
|
| 40 |
+
['HWM-NY-3082', '8.1 ft NAVD88', '0.22 mi'],
|
| 41 |
+
['HWM-NY-3105', '6.8 ft NAVD88', '0.31 mi'],
|
| 42 |
+
],
|
| 43 |
+
sub: '3 marks · max 8.1 ft · surveyed Nov 2012',
|
| 44 |
+
docId: 'USGS-OFR-2013-1234', citeId: 'c1',
|
| 45 |
+
mapLayer: 'hwm',
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
id: 'fc-stormwater',
|
| 49 |
+
stone: 'cornerstone', tier: 'modeled', variant: 'raster',
|
| 50 |
+
source: 'NYC DEP', agency: 'NYC Dept. of Environmental Protection',
|
| 51 |
+
vintage: '2024-06',
|
| 52 |
+
title: 'Stormwater Flood Map · moderate scenario',
|
| 53 |
+
rasterKind: 'stormwater',
|
| 54 |
+
sub: '2.13 in/hr · ponding ≥4 in W half of lot · routed toward Imlay St',
|
| 55 |
+
docId: 'NYCDEP-SWFM-2024', citeId: 'c5',
|
| 56 |
+
mapLayer: 'stormwater',
|
| 57 |
+
},
|
| 58 |
+
|
| 59 |
+
/* ── Keystone ── */
|
| 60 |
+
{
|
| 61 |
+
id: 'fc-register-rh',
|
| 62 |
+
stone: 'keystone', tier: 'empirical', variant: 'register',
|
| 63 |
+
source: 'NYC OpenData', agency: 'NYC OpenData · multi-agency join',
|
| 64 |
+
vintage: '2026-05',
|
| 65 |
+
title: 'Nearby exposed assets',
|
| 66 |
+
registers: [
|
| 67 |
+
{ reg: 'MTA', tier: 'empirical', label: 'Smith–9 St subway entrance', detail: '0.34 mi · F · G', sourceId: 'MTA-ENT-N048', vintage: '2025-11', note: null },
|
| 68 |
+
{ reg: 'NYCHA', tier: 'empirical', label: 'Red Hook East Houses', detail: '0.41 mi · 2,878 res.', sourceId: 'NYCHA-RHE', vintage: '2025-Q3', note: null },
|
| 69 |
+
{ reg: 'NYCHA', tier: 'empirical', label: 'Red Hook West Houses', detail: '0.52 mi · 3,142 res.', sourceId: 'NYCHA-RHW', vintage: '2025-Q3', note: null },
|
| 70 |
+
{ reg: 'DOE', tier: 'empirical', label: 'PS 27 Agnes Y. Humphrey', detail: '0.29 mi · 271 K-5', sourceId: 'DOE-K027', vintage: '2024-25', note: null },
|
| 71 |
+
{ reg: 'DOH', tier: 'empirical', label: null, detail: null, sourceId: null, vintage: null, note: 'no acute-care hospital within 1.0 mi (silent)' },
|
| 72 |
+
{ reg: 'PLUTO', tier: 'empirical', label: 'Lot 36047 / 521 / 7', detail: 'BIN 3018472 · MX-1', sourceId: 'PLUTO-2024v2', vintage: '2024-12', note: null },
|
| 73 |
+
],
|
| 74 |
+
sub: '5 of 6 registers fired · 1 silent · joined within 1.0 mi',
|
| 75 |
+
docId: 'RIPRAP-EXP-RH80', citeId: 'c-reg-rh',
|
| 76 |
+
mapLayer: 'registers',
|
| 77 |
+
},
|
| 78 |
+
|
| 79 |
+
/* ── Touchstone ── */
|
| 80 |
+
{
|
| 81 |
+
id: 'fc-floodnet',
|
| 82 |
+
stone: 'touchstone', tier: 'empirical', variant: 'spark',
|
| 83 |
+
source: 'FloodNet', agency: 'FloodNet NYC sensor network',
|
| 84 |
+
vintage: '2026-04',
|
| 85 |
+
title: 'Sensor BK-RH-002, monthly above-curb events',
|
| 86 |
+
headline: '7 events',
|
| 87 |
+
subhead: 'Jun 2024 → Apr 2026 · peak 14.3 cm',
|
| 88 |
+
spark: [0,0,1,0,2,1,0,0,3,0,1,0,0,0,2,1,0,0,1,0,2,4,1,1],
|
| 89 |
+
sparkSub: 'Sensor located 0.21 mi N at Coffey & Van Brunt. Above-curb depth in cm; events ≥2 cm.',
|
| 90 |
+
docId: 'FN-BK-RH-002', citeId: 'c3',
|
| 91 |
+
mapLayer: 'floodnet',
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
id: 'fc-311',
|
| 95 |
+
stone: 'touchstone', tier: 'proxy', variant: 'histogram',
|
| 96 |
+
source: 'NYC 311', agency: 'NYC 311 service requests',
|
| 97 |
+
vintage: '2025-12',
|
| 98 |
+
title: 'Recent 311 flood complaints, BK CB6',
|
| 99 |
+
headline: '89 calls',
|
| 100 |
+
subhead: '2019–2025 · seasonal cluster Aug–Oct',
|
| 101 |
+
histogram: [3,2,1,0,1,4,7,12,18,11,5,3,4,2,1,0,2,3,8,9,4,2,1,0],
|
| 102 |
+
sparkSub: 'Filtered to complaint types: Sewer (Backup), Street Flooding, Catch Basin Clogged. Within 200 m of address.',
|
| 103 |
+
docId: 'NYC311-FLD-CB6', citeId: 'c7',
|
| 104 |
+
mapLayer: 'complaints',
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
id: 'fc-prithvi',
|
| 108 |
+
stone: 'touchstone', tier: 'modeled', variant: 'raster-pred',
|
| 109 |
+
source: 'Prithvi-NYC', agency: 'Prithvi-NYC-Pluvial v2 · IBM/NASA × Riprap',
|
| 110 |
+
vintage: '2026-05-02',
|
| 111 |
+
title: 'Pluvial flood prediction, current Sentinel-2 chip',
|
| 112 |
+
rasterKind: 'prithvi',
|
| 113 |
+
headline: '0.3% flooded',
|
| 114 |
+
subhead: 'no flooding apparent · scene 2026-05-02',
|
| 115 |
+
sub: 'Model interpretation of imagery, not real-time observation. Confidence-mean 0.84 across non-flooded pixels.',
|
| 116 |
+
docId: 'PRITHVI-NYC-PLUV-V2-20260502', citeId: 'c-prithvi',
|
| 117 |
+
illustrative: true,
|
| 118 |
+
mapLayer: 'prithvi',
|
| 119 |
+
},
|
| 120 |
+
{
|
| 121 |
+
id: 'fc-nws',
|
| 122 |
+
stone: 'touchstone', tier: 'empirical', variant: 'scalars',
|
| 123 |
+
source: 'NWS KNYC', agency: 'NOAA · National Weather Service',
|
| 124 |
+
vintage: '2026-05-05',
|
| 125 |
+
title: 'Current weather, station KNYC',
|
| 126 |
+
scalars: [
|
| 127 |
+
{ value: '0.02 in', label: 'precip · last 24h' },
|
| 128 |
+
{ value: '67°F', label: 'temp · current' },
|
| 129 |
+
{ value: 'PC', label: 'conditions' },
|
| 130 |
+
],
|
| 131 |
+
sub: 'Observation timestamp 2026-05-05 14:18 ET. Central Park station; not point-of-query.',
|
| 132 |
+
docId: 'NWS-KNYC', citeId: 'c-nws',
|
| 133 |
+
mapLayer: 'nws',
|
| 134 |
+
},
|
| 135 |
+
|
| 136 |
+
/* ── Lodestone ── */
|
| 137 |
+
{
|
| 138 |
+
id: 'fc-ttm-surge',
|
| 139 |
+
stone: 'lodestone', tier: 'modeled', variant: 'timeseries',
|
| 140 |
+
source: 'Granite TTM r2', agency: 'IBM Granite-TimeSeries · Riprap fine-tune',
|
| 141 |
+
vintage: '2026-05-05 12:00 ET',
|
| 142 |
+
title: 'Storm surge nowcast at The Battery, 96-hour horizon',
|
| 143 |
+
timeseries: { hours: 96, peak: { x: 38, y: 47 }, peakLabel: '+47 cm @ +38h' },
|
| 144 |
+
headline: '+47 cm',
|
| 145 |
+
subhead: 'peak surge residual · Wed 04:00 ET',
|
| 146 |
+
sub: 'Nowcast applies city-wide via NOAA station 8518750. Not localized to query address. Residual above harmonic tide.',
|
| 147 |
+
spatialNote: 'regional · The Battery, not point-of-query',
|
| 148 |
+
docId: 'ttm_battery_surge_v2', citeId: 'c-ttm',
|
| 149 |
+
mapLayer: null,
|
| 150 |
+
},
|
| 151 |
+
{
|
| 152 |
+
id: 'fc-npcc4',
|
| 153 |
+
stone: 'lodestone', tier: 'modeled', variant: 'forecast',
|
| 154 |
+
source: 'NPCC4', agency: 'NYC Panel on Climate Change, 4th Assessment',
|
| 155 |
+
vintage: '2024-03',
|
| 156 |
+
title: 'Sea-level rise projections, Lower NY Harbor',
|
| 157 |
+
forecast: [
|
| 158 |
+
{ year: 2030, low: 4, mid: 6, high: 9 },
|
| 159 |
+
{ year: 2050, low: 13, mid: 22, high: 30 },
|
| 160 |
+
{ year: 2080, low: 28, mid: 49, high: 75 },
|
| 161 |
+
{ year: 2100, low: 38, mid: 71, high: 114 },
|
| 162 |
+
],
|
| 163 |
+
sub: 'inches MSL · 17th–83rd %ile range, median line. Battery tide-gauge baseline.',
|
| 164 |
+
docId: 'NPCC4-Ch3-Tbl3.2', citeId: 'c6',
|
| 165 |
+
mapLayer: null,
|
| 166 |
+
},
|
| 167 |
+
|
| 168 |
+
/* ── Capstone ── */
|
| 169 |
+
{
|
| 170 |
+
id: 'fc-mellea-meta',
|
| 171 |
+
stone: 'capstone', tier: 'modeled', variant: 'meta',
|
| 172 |
+
source: 'Mellea', agency: 'Capstone synthesis · grounding check',
|
| 173 |
+
vintage: '2026-05-05 14:22 ET',
|
| 174 |
+
title: 'Briefing reconciliation',
|
| 175 |
+
metaRows: [
|
| 176 |
+
{ k: 'Mellea reroll', v: '1 attempt' },
|
| 177 |
+
{ k: 'Grounding checks', v: '4 / 4 passed' },
|
| 178 |
+
{ k: 'Citations resolved',v: '11 / 11' },
|
| 179 |
+
{ k: 'RAG → GLiNER', v: '9 entities · 0 unresolved' },
|
| 180 |
+
],
|
| 181 |
+
sub: 'Capstone produces prose, not cards. This meta-card summarizes the reconciler chain that wrote the four-section briefing above.',
|
| 182 |
+
docId: 'RIPRAP-CAP-RH80', citeId: null,
|
| 183 |
+
mapLayer: null,
|
| 184 |
+
},
|
| 185 |
+
],
|
| 186 |
+
|
| 187 |
+
stones: [
|
| 188 |
+
{
|
| 189 |
+
key: 'cornerstone',
|
| 190 |
+
members: [
|
| 191 |
+
{ id: 'CORN-001', name: 'pull FEMA NFHL panel 36047C0207G', status: 'ok', tier: 'modeled', ms: 412 },
|
| 192 |
+
{ id: 'CORN-002', name: 'parse panel index for AE / VE bands', status: 'ok', tier: 'modeled', ms: 88 },
|
| 193 |
+
{ id: 'CORN-003', name: 'USGS STN: post-Sandy HWM survey within 500 ft', status: 'ok', tier: 'empirical', ms: 612 },
|
| 194 |
+
{ id: 'CORN-004', name: 'NYC DEP stormwater flood map 2024', status: 'ok', tier: 'modeled', ms: 980 },
|
| 195 |
+
{ id: 'CORN-005', name: 'microtopo: 3DEP DEM + HAND + TWI', status: 'ok', tier: 'proxy', ms: 1240 },
|
| 196 |
+
],
|
| 197 |
+
},
|
| 198 |
+
{
|
| 199 |
+
key: 'keystone',
|
| 200 |
+
members: [
|
| 201 |
+
{ id: 'KEY-001', name: 'MTA subway entrance proximity', status: 'ok', tier: 'empirical', ms: 220 },
|
| 202 |
+
{ id: 'KEY-002', name: 'NYCHA developments in 1 mi', status: 'ok', tier: 'empirical', ms: 410 },
|
| 203 |
+
{ id: 'KEY-003', name: 'DOE schools in 1 mi', status: 'ok', tier: 'empirical', ms: 360 },
|
| 204 |
+
{ id: 'KEY-004', name: 'NYS DOH hospitals in 1 mi', status: 'silent', tier: 'empirical', ms: 95, note: 'no acute-care within 1 mi' },
|
| 205 |
+
{ id: 'KEY-005', name: 'PLUTO BBL fetch', status: 'ok', tier: 'empirical', ms: 130 },
|
| 206 |
+
],
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
key: 'touchstone',
|
| 210 |
+
members: [
|
| 211 |
+
{ id: 'TCH-001', name: 'FloodNet sensor lookup', status: 'ok', tier: 'empirical', ms: 285 },
|
| 212 |
+
{ id: 'TCH-002', name: 'NYC 311 flood complaints', status: 'ok', tier: 'proxy', ms: 410 },
|
| 213 |
+
{ id: 'TCH-003', name: 'NWS station KNYC observation', status: 'ok', tier: 'empirical', ms: 240 },
|
| 214 |
+
{ id: 'TCH-004', name: 'NOAA tide gauge water level', status: 'ok', tier: 'empirical', ms: 196 },
|
| 215 |
+
{ id: 'TCH-005', name: 'Prithvi-EO 2.0 NYC-Pluvial v2', status: 'ok', tier: 'modeled', ms: 4920 },
|
| 216 |
+
],
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
key: 'lodestone',
|
| 220 |
+
members: [
|
| 221 |
+
{ id: 'LOD-001', name: 'Granite TTM r2 surge fine-tune', status: 'ok', tier: 'modeled', ms: 1820 },
|
| 222 |
+
{ id: 'LOD-002', name: 'NPCC4 SLR projection table', status: 'ok', tier: 'modeled', ms: 38 },
|
| 223 |
+
{ id: 'LOD-003', name: 'NWS active flood alerts', status: 'silent', tier: 'modeled', ms: 110 },
|
| 224 |
+
],
|
| 225 |
+
},
|
| 226 |
+
{
|
| 227 |
+
key: 'capstone',
|
| 228 |
+
members: [
|
| 229 |
+
{ id: 'CAP-001', name: 'Granite Embedding RAG retrieval', status: 'ok', tier: 'proxy', ms: 410 },
|
| 230 |
+
{ id: 'CAP-002', name: 'GLiNER typed extraction', status: 'ok', tier: 'proxy', ms: 280 },
|
| 231 |
+
{ id: 'CAP-003', name: 'Granite 4.1 reconcile (Mellea)', status: 'ok', tier: 'modeled', ms: 6240 },
|
| 232 |
+
],
|
| 233 |
+
},
|
| 234 |
+
],
|
| 235 |
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Findings card schema — v0.4.4.
|
| 3 |
+
*
|
| 4 |
+
* The Findings region renders a stack of cards grouped by Stone. Each card
|
| 5 |
+
* is one specialist's structured output, with explicit epistemic tiering
|
| 6 |
+
* and a citation fan-out that ties back into the briefing prose.
|
| 7 |
+
*
|
| 8 |
+
* Schema mirrors `docs/design_handoff/design_files/findings.jsx` exactly.
|
| 9 |
+
* Body fields are variant-specific — only the fields a given variant
|
| 10 |
+
* needs are populated. The renderer dispatches on `variant`.
|
| 11 |
+
*/
|
| 12 |
+
import type { Tier } from './tier';
|
| 13 |
+
|
| 14 |
+
/** Stone keys, fixed order. Mirrors `app/stones/__init__.py`. */
|
| 15 |
+
export type StoneKey =
|
| 16 |
+
| 'cornerstone'
|
| 17 |
+
| 'keystone'
|
| 18 |
+
| 'touchstone'
|
| 19 |
+
| 'lodestone'
|
| 20 |
+
| 'capstone';
|
| 21 |
+
|
| 22 |
+
export const STONE_ORDER: StoneKey[] = [
|
| 23 |
+
'cornerstone', 'keystone', 'touchstone', 'lodestone', 'capstone',
|
| 24 |
+
];
|
| 25 |
+
|
| 26 |
+
export type StoneMeta = { name: string; role: string; tag: string };
|
| 27 |
+
|
| 28 |
+
export const STONE_META: Record<StoneKey, StoneMeta> = {
|
| 29 |
+
cornerstone: { name: 'Cornerstone', role: 'the hazard reader', tag: "what NYC's ground remembers" },
|
| 30 |
+
keystone: { name: 'Keystone', role: 'the asset register', tag: "what's exposed" },
|
| 31 |
+
touchstone: { name: 'Touchstone', role: 'the live observer', tag: "what's happening now" },
|
| 32 |
+
lodestone: { name: 'Lodestone', role: 'the projector', tag: "what's coming" },
|
| 33 |
+
capstone: { name: 'Capstone', role: 'the synthesizer', tag: 'writes it all down with citations' },
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
/** 12 card body variants — one renderer per shape. */
|
| 37 |
+
export type CardVariant =
|
| 38 |
+
| 'headline'
|
| 39 |
+
| 'tabular'
|
| 40 |
+
| 'scalars'
|
| 41 |
+
| 'spark'
|
| 42 |
+
| 'histogram'
|
| 43 |
+
| 'timeseries'
|
| 44 |
+
| 'forecast'
|
| 45 |
+
| 'raster'
|
| 46 |
+
| 'raster-pred'
|
| 47 |
+
| 'register'
|
| 48 |
+
| 'comparison'
|
| 49 |
+
| 'meta';
|
| 50 |
+
|
| 51 |
+
export type Citation = { id: string; label: string; href?: string };
|
| 52 |
+
|
| 53 |
+
export type RegisterRow = {
|
| 54 |
+
reg: string; // "MTA" | "NYCHA" | "DOE" | "DOH" | "PLUTO" | ...
|
| 55 |
+
tier: Tier;
|
| 56 |
+
/** When `label` is null the row renders as a silent — register fired but
|
| 57 |
+
had no hits. The note carries the silent reason. */
|
| 58 |
+
label: string | null;
|
| 59 |
+
detail: string | null;
|
| 60 |
+
sourceId: string | null;
|
| 61 |
+
vintage?: string | null;
|
| 62 |
+
note?: string | null;
|
| 63 |
+
};
|
| 64 |
+
|
| 65 |
+
export type ScalarCell = { value: string; label: string; unit?: string };
|
| 66 |
+
|
| 67 |
+
export type ForecastBand = {
|
| 68 |
+
year: number;
|
| 69 |
+
low: number;
|
| 70 |
+
mid: number;
|
| 71 |
+
high: number;
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
export type ComparisonSide = {
|
| 75 |
+
tier: Tier;
|
| 76 |
+
label: string;
|
| 77 |
+
value: string;
|
| 78 |
+
aux?: string;
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
export type MetaRow = { k: string; v: string };
|
| 82 |
+
|
| 83 |
+
export type RasterKind =
|
| 84 |
+
| 'stormwater'
|
| 85 |
+
| 'stormwater-dry'
|
| 86 |
+
| 'fema-ae'
|
| 87 |
+
| 'hwm'
|
| 88 |
+
| 'prithvi'
|
| 89 |
+
| 'lulc'
|
| 90 |
+
| 'buildings'
|
| 91 |
+
| 'floodnet-density';
|
| 92 |
+
|
| 93 |
+
/** A single Findings card. Most fields are variant-specific. */
|
| 94 |
+
export type Card = {
|
| 95 |
+
/** Stable id used as Svelte key + linkedKey target. */
|
| 96 |
+
id: string;
|
| 97 |
+
stone: StoneKey;
|
| 98 |
+
tier: Tier;
|
| 99 |
+
variant: CardVariant;
|
| 100 |
+
|
| 101 |
+
/** Header chrome — always shown. */
|
| 102 |
+
source: string; // short label, e.g. "FEMA"
|
| 103 |
+
agency: string; // long form, e.g. "Federal Emergency Management Agency"
|
| 104 |
+
vintage: string; // e.g. "2024-09" or "2024-Q3"
|
| 105 |
+
|
| 106 |
+
/** Title row. */
|
| 107 |
+
title: string;
|
| 108 |
+
|
| 109 |
+
/** Footer chrome — always shown. */
|
| 110 |
+
docId: string;
|
| 111 |
+
citeId?: string | null;
|
| 112 |
+
cites?: Citation[];
|
| 113 |
+
|
| 114 |
+
/** Map cross-link key. Hovering this card lights up the matching map
|
| 115 |
+
* layer; hovering the layer outlines this card. */
|
| 116 |
+
mapLayer?: string | null;
|
| 117 |
+
|
| 118 |
+
/** Marks the card visually as illustrative (dashed top-rule on synthetic
|
| 119 |
+
* / preview cards). Always implied true for tier=synthetic. */
|
| 120 |
+
illustrative?: boolean;
|
| 121 |
+
|
| 122 |
+
/** Optional spatial-index callout (e.g. "regional · The Battery, not
|
| 123 |
+
* point-of-query") rendered next to the body sub. */
|
| 124 |
+
spatialNote?: string;
|
| 125 |
+
|
| 126 |
+
/** Variant-specific body fields. Only the relevant ones are populated. */
|
| 127 |
+
headline?: string;
|
| 128 |
+
subhead?: string;
|
| 129 |
+
body?: string;
|
| 130 |
+
sub?: string;
|
| 131 |
+
sparkSub?: string;
|
| 132 |
+
|
| 133 |
+
// tabular
|
| 134 |
+
columns?: string[];
|
| 135 |
+
rows?: (string | number)[][];
|
| 136 |
+
|
| 137 |
+
// scalars
|
| 138 |
+
scalars?: ScalarCell[];
|
| 139 |
+
|
| 140 |
+
// spark / histogram
|
| 141 |
+
spark?: number[];
|
| 142 |
+
histogram?: number[];
|
| 143 |
+
|
| 144 |
+
// timeseries
|
| 145 |
+
timeseries?: {
|
| 146 |
+
hours: number;
|
| 147 |
+
peak: { x: number; y: number };
|
| 148 |
+
peakLabel: string;
|
| 149 |
+
};
|
| 150 |
+
|
| 151 |
+
// forecast
|
| 152 |
+
forecast?: ForecastBand[];
|
| 153 |
+
|
| 154 |
+
// raster / raster-pred
|
| 155 |
+
rasterKind?: RasterKind;
|
| 156 |
+
|
| 157 |
+
// register
|
| 158 |
+
registers?: RegisterRow[];
|
| 159 |
+
|
| 160 |
+
// comparison (always synthetic-tier)
|
| 161 |
+
left?: ComparisonSide;
|
| 162 |
+
right?: ComparisonSide;
|
| 163 |
+
delta?: string;
|
| 164 |
+
|
| 165 |
+
// meta
|
| 166 |
+
metaRows?: MetaRow[];
|
| 167 |
+
};
|
| 168 |
+
|
| 169 |
+
/** Per-Stone provenance member (specialist) summary used by the trace. */
|
| 170 |
+
export type StoneMember = {
|
| 171 |
+
id: string;
|
| 172 |
+
name: string;
|
| 173 |
+
status: 'ok' | 'warn' | 'error' | 'silent';
|
| 174 |
+
tier?: Tier | null;
|
| 175 |
+
ms?: number;
|
| 176 |
+
note?: string;
|
| 177 |
+
children?: StoneMember[];
|
| 178 |
+
};
|
| 179 |
+
|
| 180 |
+
/** A Stone's provenance + counts, fed by the FSM trace. */
|
| 181 |
+
export type StoneTrace = {
|
| 182 |
+
key: StoneKey;
|
| 183 |
+
members: StoneMember[];
|
| 184 |
+
};
|
| 185 |
+
|
| 186 |
+
/** What the page loader hands the FindingsRegion. */
|
| 187 |
+
export type FindingsData = {
|
| 188 |
+
cards: Card[];
|
| 189 |
+
stones: StoneTrace[];
|
| 190 |
+
/** Wall-clock seconds for the run; surfaced in RunHealthStrip. */
|
| 191 |
+
wallSeconds?: number;
|
| 192 |
+
/** Optional cache-hit ratio, dev-mode surfaced. */
|
| 193 |
+
cacheHit?: number;
|
| 194 |
+
};
|
| 195 |
+
|
| 196 |
+
/** Density toggle — affects card padding + register row height. */
|
| 197 |
+
export type Density = 'comfortable' | 'compact';
|
| 198 |
+
|
| 199 |
+
/** Provenance-trace expansion mode. Smart = collapsed if all-ok, expanded
|
| 200 |
+
* if any specialist warned / errored / went silent. */
|
| 201 |
+
export type ProvenanceMode = 'smart' | 'all-expanded' | 'all-collapsed';
|