File size: 2,934 Bytes
e8a6c67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fb54991
e8a6c67
fb54991
 
 
e8a6c67
fb54991
e8a6c67
 
 
 
 
 
 
 
 
 
 
fb54991
 
 
 
 
 
 
 
 
 
 
 
 
 
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
<script lang="ts">
  import type { BriefingBlock, Citation } from '$lib/types/claim';
  import Claim from './Claim.svelte';
  import Cite from './Cite.svelte';
  import SectionHead from './SectionHead.svelte';

  interface Props {
    blocks: BriefingBlock[];
    citations: Record<string, Citation>;
    streaming?: boolean;
    replayKey?: number;
  }

  let { blocks, citations: cites, streaming = false, replayKey = 0 }: Props = $props();

  let visibleCount = $state(blocks.length);
  let prefersReducedMotion = $state(false);

  $effect(() => {
    if (typeof window === 'undefined') return;
    prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  });

  $effect(() => {
    // re-run when replayKey changes
    void replayKey;
    if (!streaming) {
      visibleCount = blocks.length;
      return;
    }
    if (prefersReducedMotion) {
      visibleCount = blocks.length;
      return;
    }
    visibleCount = 0;
    let i = 0;
    let timer: ReturnType<typeof setTimeout>;
    const tick = () => {
      i++;
      visibleCount = i;
      if (i < blocks.length) {
        timer = setTimeout(tick, i < 2 ? 280 : 420);
      }
    };
    timer = setTimeout(tick, 240);
    return () => clearTimeout(timer);
  });
</script>

<div
  class="briefing-prose"
  role="log"
  aria-live="polite"
  aria-atomic="false"
  aria-label="Streaming flood-exposure briefing"
>
  {#each blocks.slice(0, visibleCount) as block, i (i)}
    {#if block.kind === 'status'}
      <!-- briefing-status HTML comes from either:
           (a) the static sample fixture (lib/data/sample.ts, trusted), or
           (b) the parser's preamble fallback (currently disabled).
           No user-supplied input flows here.
        -->
      <!-- eslint-disable-next-line svelte/no-at-html-tags -->
      <div class="briefing-status briefing-fade-in">{@html block.html}</div>
    {:else if block.kind === 'head'}
      <div class="briefing-fade-in">
        <SectionHead n={block.n} label={block.label} tier={block.tier} title={block.title} />
      </div>
    {:else}
      <p class="briefing-para briefing-fade-in">
        {#each block.parts as part, j (j)}
          {#if part.tier}
            <Claim tier={part.tier}>{part.text}</Claim>{#if part.cite && cites[part.cite]}<Cite c={cites[part.cite]} />{/if}
          {:else}
            <span>{part.text}</span>
          {/if}
        {/each}
      </p>
    {/if}
  {/each}
</div>

<style>
  /* Each newly-revealed block fades in over 320ms instead of the
     blinking-cursor "typing" cadence. Citation-grounded paragraphs
     should land with authority, not chatter. Respects
     prefers-reduced-motion via the global rule in tokens.css. */
  .briefing-fade-in {
    animation: briefing-fade 320ms ease-out both;
  }
  @keyframes briefing-fade {
    from { opacity: 0; transform: translateY(2px); }
    to   { opacity: 1; transform: translateY(0); }
  }
</style>