// LiveBlog: a story told as it happens: timestamped updates, newest first, with a
// pinned summary held at the top, the key events listed as links that jump to them,
// a switch between all updates and the key events only, the order turned round
// (oldest first, to read it as a story), and the older updates a page at a time.
//
//   <LiveBlog id="vote" status="live" updated="Updated 2 min ago" entries={[
//       { id: "p1", pinned: true, time: "21:05", title: "What we know", body: "Polls have closed in 12 states..." },
//       { id: "u9", time: "21:42", datetime: "2026-11-03T21:42:00-05:00", key: true, title: "Ohio called",
//         body: "The Ledger's decision desk calls Ohio.", author: "Ada Lovelace", tag: "Senate" },
//       { id: "u8", time: "21:30", body: "Turnout in the capital is running ahead of 2022." },
//   ]}/>
//
// entries: newest first, maps of { id, time, body, datetime?, title?, author?, tag?,
// key?, pinned? }: id is unique among them (the entry's element is `<id>-<entry id>`,
// a link can point at it); time: when, as it reads ("21:42", "2 min ago"), with
// datetime its ISO form; body: the text, a blank line between paragraphs; title: a
// heading for it; author: who wrote it; tag: a small label ("Senate", "Analysis");
// key: a key event (marked, listed at the top and kept by "Key events"); pinned:
// shown at the top, out of the stream (the first pinned one). id (required): unique on
// the page. status: "live" (default, a pulsing Live) or "ended". updated: a line
// beside it ("Updated 2 min ago"). title: the blog's name for screen readers ("Live
// updates"). page_size: the updates shown before "Show more" (10). oldest_first: start
// with the oldest at the top. Entry headings are <h3>.

import ui/theme.qwe use ui_cls

let live_blog_css = css`
.ui-lvb { display: grid; gap: 16px; min-width: 0; max-width: 720px; color: var(--ui-fg); font: 15px/1.55 var(--ui-font); }
.ui-lvb-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; color: var(--ui-muted); font-size: 13px; }
.ui-lvb-status { display: inline-flex; align-items: center; gap: 6px; padding: 2px 9px; border-radius: 4px; background: var(--ui-danger);
  color: var(--ui-bg); font-size: 12px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
.ui-lvb-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; animation: ui-lvb-pulse 1.4s ease-in-out infinite; }
@keyframes ui-lvb-pulse { 50% { opacity: .2; } }
.ui-lvb-ended .ui-lvb-status { background: var(--ui-subtle); color: var(--ui-muted); }
.ui-lvb-ended .ui-lvb-status::before { display: none; }
.ui-lvb-keys { padding: 12px 16px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-subtle); }
.ui-lvb-keys h3 { margin: 0 0 6px; font-size: 13px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--ui-muted); }
.ui-lvb-keys ul { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; }
.ui-lvb-keys a { display: grid; grid-template-columns: 52px 1fr; gap: 8px; color: var(--ui-fg); text-decoration: none; font-size: 14px; }
.ui-lvb-keys a span { color: var(--ui-danger); font-weight: 700; font-variant-numeric: tabular-nums; }
.ui-lvb-keys a:hover b { color: var(--ui-accent); text-decoration: underline; }
.ui-lvb-keys b { font-weight: 550; }
.ui-lvb-tools { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; }
.ui-lvb-seg { display: inline-flex; padding: 3px; border-radius: 999px; background: var(--ui-subtle); }
.ui-lvb-seg button, .ui-lvb-order { height: 30px; padding: 0 14px; border: 0; border-radius: 999px; background: transparent; color: var(--ui-muted);
  font: 600 13px var(--ui-font); cursor: pointer; }
.ui-lvb-seg button[aria-pressed="true"] { background: var(--ui-surface); color: var(--ui-fg); box-shadow: 0 1px 2px rgb(0 0 0 / .1); }
.ui-lvb-order { border: 1px solid var(--ui-border); }
.ui-lvb-order:hover, .ui-lvb-seg button:hover { color: var(--ui-fg); }
.ui-lvb button:focus-visible, .ui-lvb a:focus-visible { outline: none; box-shadow: var(--ui-focus); }
.ui-lvb-feed { margin: 0; padding: 0; list-style: none; }
.ui-lvb-entry { position: relative; display: grid; gap: 6px; padding: 16px 0 18px 22px; border-top: 1px solid var(--ui-border); scroll-margin-top: 80px; }
.ui-lvb-entry::before { content: ""; position: absolute; left: 0; top: 23px; width: 9px; height: 9px; border-radius: 50%; background: var(--ui-strong); }
.ui-lvb-entry:focus { outline: none; }
.ui-lvb-entry:focus-visible { box-shadow: var(--ui-focus); border-radius: var(--ui-radius-sm); }
.ui-lvb-key::before { background: var(--ui-danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-danger) 25%, transparent); }
.ui-lvb-pinned { padding: 14px 16px 16px; border: 1px solid var(--ui-border); border-left: 4px solid var(--ui-accent); border-radius: var(--ui-radius-sm);
  background: var(--ui-surface); }
.ui-lvb-pinned::before { display: none; }
.ui-lvb-top { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; margin: 0; font-size: 13px; }
.ui-lvb-time { color: var(--ui-fg); font-weight: 700; font-variant-numeric: tabular-nums; }
.ui-lvb-flag { padding: 0 7px; border-radius: 4px; font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; }
.ui-lvb-flag-key { background: var(--ui-danger-soft); color: var(--ui-danger); }
.ui-lvb-flag-pin { background: var(--ui-accent-soft); color: var(--ui-accent); }
.ui-lvb-tag { color: var(--ui-muted); }
.ui-lvb .ui-lvb-title { margin: 0; font-size: 18px; line-height: 1.3; font-weight: 700; }
.ui-lvb-entry > p:not(.ui-lvb-top):not(.ui-lvb-by) { margin: 0; }
.ui-lvb-by { margin: 0; color: var(--ui-muted); font-size: 13px; }
.ui-lvb-empty { padding: 16px 0; color: var(--ui-muted); border-top: 1px solid var(--ui-border); }
.ui-lvb-more { justify-self: center; height: 38px; padding: 0 20px; border: 1px solid var(--ui-border); border-radius: 999px; background: var(--ui-surface);
  color: var(--ui-fg); font: 600 14px var(--ui-font); cursor: pointer; }
.ui-lvb-more:hover { border-color: var(--ui-accent); color: var(--ui-accent); }
@media (prefers-reduced-motion: reduce) { .ui-lvb-status::before { animation: none; } }
`

component LiveBlog(entries: list, id: str, status: str = "live", updated: str? = null, title: str = "Live updates",
                   page_size: int = 10, oldest_first: bool = false, class: str? = null) {
    style global live_blog_css
    state only_key = false
    state oldest = oldest_first
    state shown = page_size
    state jump = null
    let pin = entries | find((e) => e.pinned == true)
    let stream = entries | filter((e) => pin == null || e.id != pin.id)
    let picked = only_key ? stream | filter((e) => e.key == true) : stream
    let ordered = oldest ? reverse(picked) : picked
    let visible = take(ordered, shown)
    let left = len(ordered) - len(visible)
    let keys = stream | filter((e) => e.key == true)
    let anchor = (e) => `${id}-${e.id}`
    // A key event's link: show the entry (it may be on a later page), then go to it.
    let go = (e) => {
        let at = index_of(ordered | map((x) => x.id), e.id)
        if at == null {
            only_key = false
            let all = oldest ? reverse(stream) : stream
            shown = max([shown, (index_of(all | map((x) => x.id), e.id) ?? 0) + 1])
        } else if at >= shown {
            shown = at + 1
        }
        jump = anchor(e)
    }
    if jump != null {
        every(16, () => {
            let el = js("document.getElementById", jump)
            jump = null
            if el != null {
                el.scrollIntoView({ block: "start", behavior: "smooth" })
                el.focus({ preventScroll: true })
            }
        })
    }
    let body = (e) => for p in split(e.body ?? "", "\n\n") | filter((p) => trim(p) != "") => <p>{trim(p)}</p>
    let entry = (e, pinned: bool) => [
        <p.ui-lvb-top>
            <time.ui-lvb-time datetime={e.datetime}>{e.time}</time>
            {pinned && <span class="ui-lvb-flag ui-lvb-flag-pin">Pinned</span>}
            {e.key == true && <span class="ui-lvb-flag ui-lvb-flag-key">Key event</span>}
            {e.tag != null && <span.ui-lvb-tag>{e.tag}</span>}
        </p>,
        e.title != null && <h3.ui-lvb-title>{e.title}</h3>,
        body(e),
        e.author != null && <p.ui-lvb-by>{e.author}</p>,
    ]
    <section class={ui_cls(["ui-lvb", status == "ended" && "ui-lvb-ended", class])} id={id} aria-label={title}>
        <div.ui-lvb-head>
            <span.ui-lvb-status>{status == "ended" ? "Ended" : "Live"}</span>
            <span data-count={len(stream)}>{len(stream)} {len(stream) == 1 ? "update" : "updates"}</span>
            {updated != null && <span>{updated}</span>}
        </div>
        {pin != null && <article class="ui-lvb-entry ui-lvb-pinned" id={anchor(pin)} tabindex="-1">{entry(pin, true)}</article>}
        {len(keys) > 0 && <nav.ui-lvb-keys aria-label="Key events">
            <h3>Key events</h3>
            <ul>
                {for e in keys => <li key={e.id}>
                    <a href={`#${anchor(e)}`} @click={(ev) => {
                        ev.preventDefault()
                        go(e)
                    }}><span>{e.time}</span><b>{e.title ?? e.body}</b></a>
                </li>}
            </ul>
        </nav>}
        <div.ui-lvb-tools>
            <div.ui-lvb-seg role="group" aria-label="Show">
                <button type="button" aria-pressed={only_key ? "false" : "true"} @click={() => { only_key = false }}>All updates</button>
                <button type="button" aria-pressed={only_key ? "true" : "false"} @click={() => { only_key = true }}>Key events</button>
            </div>
            <button.ui-lvb-order type="button" @click={() => { oldest = !oldest }}
                    aria-label={oldest ? "Oldest first; show the newest first" : "Newest first; show the oldest first"}>
                {oldest ? "Oldest first ↑" : "Newest first ↓"}
            </button>
        </div>
        <ol.ui-lvb-feed>
            {for e in visible => <li key={e.id}>
                <article class={ui_cls(["ui-lvb-entry", e.key == true && "ui-lvb-key"])} id={anchor(e)} tabindex="-1">{entry(e, false)}</article>
            </li>}
        </ol>
        {len(visible) == 0 && <p.ui-lvb-empty>{only_key ? "No key events yet." : "No updates yet."}</p>}
        {left > 0 && <button.ui-lvb-more type="button" @click={() => { shown += page_size }}>
            Show {left < page_size ? left : page_size} more {left == 1 ? "update" : "updates"}
        </button>}
    </section>
}
