File size: 10,988 Bytes
e8a6c67
 
 
 
1184305
 
e8a6c67
1184305
 
 
 
 
 
 
 
 
 
 
 
 
e8a6c67
1184305
 
 
 
 
 
e8a6c67
 
 
 
1184305
 
 
 
 
 
 
 
 
 
 
 
 
b2f95f6
1184305
 
 
 
 
 
 
5bbf2fe
1184305
 
b2f95f6
 
 
5bbf2fe
1184305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8a6c67
 
1184305
 
 
e8a6c67
 
1184305
 
 
e8a6c67
1184305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8a6c67
1184305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
<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';

  /** v0.4.5 §7 — LAYERS panel restructured to mirror Findings Stones.
   *
   *  Each Stone is its own collapsed-but-visible group with one row per
   *  map layer keyed to that Stone. Master tier toggles (the live
   *  empirical / modeled / synthetic / proxy switches the map respects
   *  today) sit at the bottom of the panel; per-Stone rows inherit
   *  their tier's master state and display the resolved ON/OFF.
   *
   *  Rows for layers that aren't yet wired into the map visibility
   *  pipeline render with a dimmed "off (not yet wired)" caption so the
   *  reader sees the catalog without thinking the toggle is broken. */

  type MasterKey = 'empirical' | 'modeled' | 'synthetic' | 'proxy';
  interface Props {
    active: Record<MasterKey, boolean>;
    /** Per-tier feature counts. Used to surface "no features" inline
     *  on each Stone row. `null` means caller wants the full catalog
     *  shown regardless of data-driven counts. */
    featureCounts?: Record<MasterKey, number> | null;
    onToggle: (key: MasterKey) => void;
  }

  let { active, featureCounts, onToggle }: Props = $props();

  type LayerRow = {
    label: string;
    source: string;
    tier: Tier;
    /** When false, the row is purely catalog — the master tier toggle
     *  doesn't yet drive a real map source. Surfaced as "not yet wired". */
    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: [],   // intentional — surfaced as the explicit absence row
    capstone:  [],   // not a map layer; surfaced as "not a map layer"
  };

  /** Resolve a row's ON state from the master tier toggle. */
  function isOn(row: LayerRow): boolean {
    return !!active[row.tier];
  }

  function tally(stone: StoneKey): number {
    return STONE_LAYERS[stone].length;
  }

  // Active tier toggles — rendered as small chips at the bottom of the
  // panel so the user can still flip the four master switches.
  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' },
  ];

  // featureCounts is intentionally accepted but not used in the catalog
  // view — the catalog shows every row regardless of live counts. Kept
  // in the prop signature so callers don't have to change.
</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}

  <!-- Master tier toggles. These are the actual switches the map honours
       today; each Stone row above resolves ON/OFF from these. -->
  <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);
    /* Stone-tinted left rule (v0.4.5 §9 sibling treatment). */
    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>