import { css, html, nothing } from 'lit'; import { customElement } from 'lit/decorators.js'; import type { GenerateDigestResponse } from '../types/index.js'; import { RoxyDataElement } from '../utils/base-element.js'; import { baseStyles } from '../utils/base-styles.js'; import { formatDate, formatNumber } from '../utils/format.js'; import { humanize } from '../utils/string.js'; type DigestWindow = NonNullable[number]; type DigestEvent = NonNullable[number]; /** * Forecast digest: the rolled-up reading across the next 24 hours, 7, 30, and 90 days. Renders /forecast/digest. Each window shows how many events fall in it, a per-domain breakdown (western, vedic, biorhythm), and the few highest-significance events with a significance bar coloured by domain. Use it as the at-a-glance period summary that pairs with the day-by-day roxy-forecast-timeline. */ @customElement('roxy-forecast-digest') export class RoxyForecastDigest extends RoxyDataElement { static styles = [ baseStyles, css` .wrap { background: var(--roxy-surface, #fff); color: var(--roxy-fg, #0a0a0a); border: 1px solid var(--roxy-border, #e4e4e7); border-radius: var(--roxy-radius-md, 8px); padding: var(--roxy-space-lg, 1.5rem); box-shadow: var(--roxy-shadow-sm); display: grid; gap: var(--roxy-space-md, 1rem); } .head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--roxy-space-md, 1rem); flex-wrap: wrap; } .title { font-size: var(--roxy-text-lg, 1.125rem); font-weight: var(--roxy-weight-bold, 600); margin: 0; } .subtitle { color: var(--roxy-muted, #71717a); font-size: var(--roxy-text-sm, 0.875rem); margin: 0; } .window { display: grid; gap: var(--roxy-space-sm, 0.5rem); padding-top: var(--roxy-space-md, 1rem); border-top: 1px solid var(--roxy-border, #e4e4e7); } .window:first-of-type { border-top: none; padding-top: 0; } .window-head { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; } .window-label { font-weight: var(--roxy-weight-bold, 600); } .window-count { margin-left: auto; color: var(--roxy-muted, #71717a); font-size: var(--roxy-text-xs, 0.75rem); font-variant-numeric: tabular-nums; } .domains { display: flex; flex-wrap: wrap; gap: 0.4rem; } .domain-chip { display: inline-flex; align-items: center; gap: 0.35rem; font-size: var(--roxy-text-xs, 0.75rem); color: var(--roxy-muted, #71717a); } .swatch { width: 0.65rem; height: 0.65rem; border-radius: 2px; display: inline-block; } .sw-western { background: var(--roxy-accent, #f59e0b); } .sw-vedic { background: var(--roxy-info, #2563eb); } .sw-biorhythm { background: var(--roxy-success, #16a34a); } .event { display: grid; grid-template-columns: auto 1fr; gap: 0.25rem 0.6rem; align-items: baseline; } .event-date { color: var(--roxy-muted, #71717a); font-size: var(--roxy-text-xs, 0.75rem); font-variant-numeric: tabular-nums; white-space: nowrap; } .event-desc { font-size: var(--roxy-text-sm, 0.875rem); line-height: 1.4; } .sig { grid-column: 2; height: 4px; border-radius: 2px; background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 60%, transparent); overflow: hidden; } .sig-fill { display: block; height: 100%; border-radius: 2px; } .sig-fill.western { background: var(--roxy-accent, #f59e0b); } .sig-fill.vedic { background: var(--roxy-info, #2563eb); } .sig-fill.biorhythm { background: var(--roxy-success, #16a34a); } .quiet { color: var(--roxy-muted, #71717a); font-size: var(--roxy-text-sm, 0.875rem); font-style: italic; } `, ]; protected renderEmpty() { return html`
No digest data
`; } protected renderData(d: GenerateDigestResponse) { const windows = d.windows ?? []; if (windows.length === 0) return this.renderEmpty(); const range = [formatDate(d.startDate), formatDate(d.endDate)] .filter(Boolean) .join(' – '); return html`

Forecast Digest

${range ? html`

${range}

` : nothing}
${windows.map((w) => this.renderWindow(w))}
`; } private windowLabel(days: number | undefined): string { if (days === 1) return 'Next 24 hours'; return typeof days === 'number' ? `Next ${days} days` : 'Window'; } private renderWindow(w: DigestWindow) { const top = w.top ?? []; const byDomain = w.byDomain ?? {}; const domains = Object.entries(byDomain) as Array<[string, number]>; return html`
${this.windowLabel(w.days)} ${w.count ?? 0} event${w.count === 1 ? '' : 's'}
${ domains.length > 0 ? html`
${domains.map( ([dom, n]) => html` ${humanize(dom)} ${n} `, )}
` : nothing } ${ top.length > 0 ? html`
${top.map((e) => this.renderEvent(e))}
` : html`

No notable events.

` }
`; } private renderEvent(e: DigestEvent) { const sig = typeof e.significance === 'number' ? e.significance : 0; return html`
${formatDate(e.date)} ${e.description ?? humanize(e.type ?? '')}
`; } } declare global { interface HTMLElementTagNameMap { 'roxy-forecast-digest': RoxyForecastDigest; } }