| <script lang="ts"> |
| import type { Tier } from '$lib/types/tier'; |
| import TierGlyph from '$lib/components/glyphs/TierGlyph.svelte'; |
| import TierBadge from '$lib/components/glyphs/TierBadge.svelte'; |
| import type { StoneKey } from '$lib/types/card'; |
| import { STONE_META, STONE_ORDER } from '$lib/types/card'; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| type MasterKey = 'empirical' | 'modeled' | 'synthetic' | 'proxy'; |
| interface Props { |
| active: Record<MasterKey, boolean>; |
| |
| |
| |
| featureCounts?: Record<MasterKey, number> | null; |
| onToggle: (key: MasterKey) => void; |
| } |
|
|
| let { active, featureCounts, onToggle }: Props = $props(); |
|
|
| type LayerRow = { |
| label: string; |
| source: string; |
| tier: Tier; |
| |
| |
| wired: boolean; |
| }; |
|
|
| const STONE_LAYERS: Record<StoneKey, LayerRow[]> = { |
| cornerstone: [ |
| { label: 'Sandy Inundation Zone (2012)', source: 'NYC OEM', tier: 'empirical', wired: true }, |
| { label: 'FEMA / DEP scenarios', source: 'FEMA · NYC DEP', tier: 'modeled', wired: true }, |
| { label: 'Ida HWM points (2021)', source: 'USGS STN', tier: 'empirical', wired: true }, |
| { label: 'Microtopography (HAND/TWI)', source: 'USGS 3DEP', tier: 'proxy', wired: false }, |
| ], |
| keystone: [ |
| { label: 'MTA subway entrances', source: 'MTA Open Data', tier: 'empirical', wired: true }, |
| { label: 'NYCHA developments', source: 'NYC OD phvi-damg', tier: 'empirical', wired: true }, |
| { label: 'DOE schools', source: 'NYC DOE Locations', tier: 'empirical', wired: true }, |
| { label: 'DOH hospitals', source: 'NYS DOH vn5v-hh5r', tier: 'empirical', wired: true }, |
| { label: 'TerraMind Buildings (current)', source: 'msradam/TerraMind-NYC-Adapters', tier: 'synthetic', wired: true }, |
| ], |
| touchstone: [ |
| { label: '311 flood complaints', source: 'NYC 311', tier: 'proxy', wired: false }, |
| { label: 'FloodNet sensors', source: 'FloodNet NYC', tier: 'proxy', wired: true }, |
| { label: 'TerraMind LULC (current)', source: 'msradam/TerraMind-NYC-Adapters', tier: 'synthetic', wired: true }, |
| { label: 'Prithvi-NYC-Pluvial flood pred.', source: 'msradam/Prithvi-EO-2.0-NYC-Pluvial', tier: 'modeled', wired: true }, |
| ], |
| lodestone: [], |
| capstone: [], |
| }; |
|
|
| |
| function isOn(row: LayerRow): boolean { |
| return !!active[row.tier]; |
| } |
|
|
| function tally(stone: StoneKey): number { |
| return STONE_LAYERS[stone].length; |
| } |
|
|
| |
| |
| const MASTERS: { k: MasterKey; tier: Tier; label: string }[] = [ |
| { k: 'empirical', tier: 'empirical', label: 'EMP' }, |
| { k: 'modeled', tier: 'modeled', label: 'MOD' }, |
| { k: 'proxy', tier: 'proxy', label: 'PRX' }, |
| { k: 'synthetic', tier: 'synthetic', label: 'SYN' }, |
| ]; |
|
|
| |
| |
| |
| </script> |
|
|
| <aside class="layers-panel" aria-label="Map layers grouped by Stone"> |
| <div class="layers-head"> |
| <span class="section-label">Layers · grouped by Stone</span> |
| </div> |
| |
| {#each STONE_ORDER as stone (stone)} |
| <details class="layers-group region-{stone}" open> |
| <summary> |
| <span class="layers-caret" aria-hidden="true">▾</span> |
| <span class="layers-stone-name">{STONE_META[stone].name}</span> |
| <span class="layers-stone-tag">— {STONE_META[stone].tag}</span> |
| {#if tally(stone) > 0} |
| <span class="layers-count">{tally(stone)}</span> |
| {/if} |
| </summary> |
| <ul class="layers-list"> |
| {#if stone === 'lodestone'} |
| <li class="layers-row layers-row-empty"> |
| <span class="layers-empty-text">no map layers — see Findings cards</span> |
| </li> |
| {:else if stone === 'capstone'} |
| <li class="layers-row layers-row-empty"> |
| <span class="layers-empty-text">not a map layer</span> |
| </li> |
| {:else} |
| {#each STONE_LAYERS[stone] as row, i (i)} |
| <li class="layers-row" class:dim={!row.wired}> |
| <span class="layers-glyph" aria-hidden="true"> |
| <TierGlyph tier={row.tier} size={11} color="var(--tier-{row.tier})" /> |
| </span> |
| <span class="layers-text"> |
| <span class="layers-label">{row.label}</span> |
| <span class="layers-meta">{row.source} · <TierBadge tier={row.tier} compact /></span> |
| </span> |
| <span class="layers-state"> |
| {#if !row.wired} |
| <span class="layers-state-dim" title="Not yet wired to map source">off · catalog</span> |
| {:else if isOn(row)} |
| on |
| {:else} |
| off |
| {/if} |
| </span> |
| </li> |
| {/each} |
| {/if} |
| </ul> |
| </details> |
| {/each} |
| |
| |
| |
| <div class="layers-masters" role="group" aria-label="Master tier toggles"> |
| <span class="section-label">Tier toggles</span> |
| <div class="layers-master-row"> |
| {#each MASTERS as m (m.k)} |
| <button |
| type="button" |
| class="layers-master" |
| class:is-on={active[m.k]} |
| aria-pressed={active[m.k]} |
| onclick={() => onToggle(m.k)} |
| > |
| <TierGlyph tier={m.tier} size={11} color="var(--tier-{m.tier})" /> |
| <span>{m.label}</span> |
| <span class="layers-master-state">{active[m.k] ? 'ON' : 'OFF'}</span> |
| </button> |
| {/each} |
| </div> |
| </div> |
| </aside> |
|
|
| <style> |
| .layers-panel { |
| background: var(--paper); |
| border: 1px solid var(--rule-soft); |
| padding: var(--s-3) var(--s-4) var(--s-4); |
| display: flex; |
| flex-direction: column; |
| gap: var(--s-3); |
| font-family: var(--font-sans); |
| } |
| .layers-head { padding-bottom: 4px; } |
| |
| .layers-group { |
| border-top: 1px solid var(--rule-soft); |
| padding-top: var(--s-2); |
| |
| border-left: 3px solid var(--stone-tint, var(--rule-soft)); |
| padding-left: var(--s-3); |
| } |
| .layers-group.region-cornerstone { --stone-tint: var(--stone-cornerstone); } |
| .layers-group.region-keystone { --stone-tint: var(--stone-keystone); } |
| .layers-group.region-touchstone { --stone-tint: var(--stone-touchstone); } |
| .layers-group.region-lodestone { --stone-tint: var(--stone-lodestone); } |
| .layers-group.region-capstone { --stone-tint: var(--stone-capstone); } |
| |
| .layers-group summary { |
| cursor: pointer; |
| list-style: none; |
| display: flex; |
| align-items: baseline; |
| gap: var(--s-2); |
| padding: 4px 0; |
| } |
| .layers-group summary::-webkit-details-marker { display: none; } |
| .layers-caret { |
| font-size: 10px; |
| color: var(--ink-tertiary); |
| transition: transform 200ms ease; |
| } |
| .layers-group:not([open]) .layers-caret { transform: rotate(-90deg); } |
| .layers-stone-name { |
| font-family: var(--font-serif); |
| font-style: italic; |
| font-size: 16px; |
| color: var(--ink); |
| } |
| .layers-stone-tag { |
| font-family: var(--font-mono); |
| font-size: 11px; |
| color: var(--ink-tertiary); |
| letter-spacing: 0.04em; |
| } |
| .layers-count { |
| margin-left: auto; |
| font-family: var(--font-mono); |
| font-size: 10px; |
| color: var(--ink-tertiary); |
| letter-spacing: 0.05em; |
| text-transform: lowercase; |
| } |
| |
| .layers-list { |
| list-style: none; |
| margin: 4px 0 var(--s-2); |
| padding: 0; |
| display: flex; |
| flex-direction: column; |
| } |
| .layers-row { |
| display: grid; |
| grid-template-columns: 16px 1fr auto; |
| gap: var(--s-2); |
| align-items: center; |
| padding: 4px 0; |
| font-family: var(--font-mono); |
| font-size: 11px; |
| color: var(--ink); |
| border-bottom: 1px dotted var(--rule-soft); |
| } |
| .layers-row:last-child { border-bottom: 0; } |
| .layers-row.dim { opacity: 0.7; } |
| .layers-glyph { display: inline-flex; align-items: center; } |
| .layers-text { display: flex; flex-direction: column; gap: 2px; } |
| .layers-label { |
| color: var(--ink); |
| font-family: var(--font-sans); |
| font-size: 12px; |
| } |
| .layers-meta { |
| font-family: var(--font-mono); |
| font-size: 10px; |
| color: var(--ink-tertiary); |
| display: inline-flex; |
| align-items: center; |
| gap: 4px; |
| } |
| .layers-state { |
| font-family: var(--font-mono); |
| font-size: 10px; |
| letter-spacing: 0.05em; |
| color: var(--ink); |
| text-transform: uppercase; |
| } |
| .layers-state-dim { |
| color: var(--ink-tertiary); |
| text-transform: lowercase; |
| font-style: italic; |
| } |
| .layers-row-empty .layers-empty-text { |
| grid-column: 1 / -1; |
| color: var(--ink-tertiary); |
| font-style: italic; |
| font-family: var(--font-mono); |
| font-size: 11px; |
| } |
| |
| .layers-masters { |
| border-top: 1px solid var(--rule-soft); |
| padding-top: var(--s-2); |
| } |
| .layers-master-row { |
| margin-top: 4px; |
| display: flex; |
| flex-wrap: wrap; |
| gap: 6px; |
| } |
| .layers-master { |
| display: inline-flex; |
| align-items: center; |
| gap: 4px; |
| padding: 4px 8px; |
| background: var(--paper); |
| border: 1px solid var(--rule-soft); |
| cursor: pointer; |
| font-family: var(--font-mono); |
| font-size: 10px; |
| letter-spacing: 0.05em; |
| color: var(--ink); |
| } |
| .layers-master.is-on { background: var(--paper-deep); border-color: var(--ink); } |
| .layers-master-state { |
| margin-left: 4px; |
| color: var(--ink-tertiary); |
| font-size: 9px; |
| } |
| .layers-master.is-on .layers-master-state { color: var(--ink); } |
| </style> |
|
|