import { css, html, nothing } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { planetGlyph, signGlyph } from '../tokens/index.js'; import type { TransitsResponse } from '../types/index.js'; import { RoxyDataElement } from '../utils/base-element.js'; import { baseStyles } from '../utils/base-styles.js'; import { chevron, disclosureStyles } from '../utils/disclosure.js'; import { formatAspectName, formatDate, formatNumber, formatTime, } from '../utils/format.js'; import { readingDetailStyles, renderReadingDetail, } from '../utils/interp-accordion.js'; import { renderTablist, tablistStyles } from '../utils/tablist.js'; /** * Transit positions and aspect table. Pass `data` from /astrology/transits. * When natalChart is included in the request, `data.transitAspects` and * `data.summary` are present and rendered automatically. */ @customElement('roxy-transits-table') export class RoxyTransitsTable extends RoxyDataElement { static styles = [ baseStyles, tablistStyles, disclosureStyles, readingDetailStyles, 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; /* minmax(0, 1fr), not the implicit auto column. An auto grid column takes * its MINIMUM from min-content, so a nowrap table wider than the card blows * the column out and drags every sibling with it, clipped on the right. This * is what lets the scroll container inside actually scroll. */ grid-template-columns: minmax(0, 1fr); 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; } .summary-pills { display: flex; flex-wrap: wrap; gap: var(--roxy-space-sm, 0.5rem); } .pill { display: inline-flex; align-items: center; gap: 4px; padding: 2px var(--roxy-space-sm, 0.5rem); border-radius: var(--roxy-radius-full, 9999px); font-size: var(--roxy-text-xs, 0.75rem); font-weight: var(--roxy-weight-bold, 600); border: 1px solid currentColor; } .pill--muted { color: var(--roxy-fg, #0a0a0a); background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 60%, transparent); } .pill--success { color: var(--roxy-success-fg, #166534); background: color-mix(in srgb, var(--roxy-success, #16a34a) 10%, transparent); } .pill--danger { color: var(--roxy-danger-fg, #991b1b); background: color-mix(in srgb, var(--roxy-danger, #dc2626) 10%, transparent); } table { width: 100%; border-collapse: collapse; font-size: var(--roxy-text-sm, 0.875rem); } th, td { padding: var(--roxy-space-sm, 0.5rem); border-bottom: 1px solid var(--roxy-border, #e4e4e7); text-align: left; } th { color: var(--roxy-muted, #71717a); font-weight: var(--roxy-weight-bold, 600); text-transform: uppercase; font-size: var(--roxy-text-xs, 0.75rem); letter-spacing: 0.06em; } .section-label { font-size: var(--roxy-text-xs, 0.75rem); color: var(--roxy-muted, #71717a); text-transform: uppercase; letter-spacing: 0.06em; font-weight: var(--roxy-weight-bold, 600); margin: 0 0 var(--roxy-space-xs, 0.25rem) 0; } .glyph { font-size: 1.1em; margin-right: 2px; line-height: 1; } .planet-cell { display: flex; align-items: center; gap: 4px; white-space: nowrap; } .retro-badge { display: inline-block; font-size: 0.7em; padding: 1px 4px; border-radius: var(--roxy-radius-sm, 4px); background: color-mix(in srgb, var(--roxy-warning, #ea580c) 12%, transparent); color: var(--roxy-warning-fg, #9a3412); font-weight: var(--roxy-weight-bold, 600); margin-left: 2px; vertical-align: middle; } .speed { font-variant-numeric: tabular-nums; color: var(--roxy-muted, #71717a); white-space: nowrap; } .speed-arrow { font-size: 0.85em; } td.num { font-variant-numeric: tabular-nums; color: var(--roxy-muted, #71717a); } .overflow-scroll { overflow-x: auto; min-width: 0; -webkit-overflow-scrolling: touch; } .aspect-card { border: 1px solid var(--roxy-border, #e4e4e7); border-radius: var(--roxy-radius-md, 8px); padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem); margin-bottom: var(--roxy-space-xs, 0.25rem); } .aspect-card summary { cursor: pointer; font-weight: 500; color: var(--roxy-fg, #0a0a0a); display: flex; flex-wrap: wrap; align-items: center; gap: 0.5em; } .aspect-card summary .meta { color: var(--roxy-muted, #71717a); font-weight: 400; font-size: var(--roxy-text-xs, 0.75rem); margin-left: auto; font-variant-numeric: tabular-nums; } .aspect-card .interp-body { margin-top: var(--roxy-space-xs, 0.25rem); color: var(--roxy-fg, #0a0a0a); font-size: var(--roxy-text-sm, 0.875rem); line-height: 1.45; } .aspect-card .interp-body p { margin: 0 0 var(--roxy-space-xs, 0.25rem); } .nature-badge { display: inline-block; padding: 1px 8px; border-radius: 9999px; font-size: var(--roxy-text-xs, 0.75rem); font-weight: 600; } .nature-badge.harmonious { background: color-mix(in srgb, var(--roxy-success, #16a34a) 12%, transparent); color: var(--roxy-success-fg, #166534); } .nature-badge.challenging { background: color-mix(in srgb, var(--roxy-danger, #dc2626) 12%, transparent); color: var(--roxy-danger-fg, #991b1b); } .nature-badge.neutral { background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 60%, transparent); color: var(--roxy-fg, #0a0a0a); } `, ]; /** Which panel is showing: planet positions or the transit-to-natal aspects. */ @state() private tab: 'positions' | 'aspects' = 'positions'; protected renderData(d: TransitsResponse) { if (!d.transitPlanets?.length) return this.renderEmpty(); const { transitDate, transitTime, transitPlanets, transitAspects, summary, } = d; const dateStr = [ formatDate(this.effectiveLang(), transitDate), formatTime(this.effectiveLang(), transitTime), ] .filter(Boolean) .join(' '); const aspectCount = transitAspects?.length ?? 0; const tab = this.tab; return html`

${this.t('Transits')}

${dateStr ? html`

${dateStr}

` : nothing}
${ aspectCount > 0 ? html`${renderTablist({ items: [ { id: 'positions', label: this.t('Positions') }, { id: 'aspects', label: this.t('Aspects ({{count}})', { count: aspectCount }), }, ], active: tab, onSelect: (v) => { this.tab = v; }, label: this.t('Transit views'), idPrefix: 'transits', controls: true, })}
${ tab === 'positions' ? html`
${this.renderPlanetsTable(transitPlanets)}
` : html`${summary ? this.renderSummaryPills(summary) : nothing}
${this.renderAspectsList(transitAspects ?? [])}
` }
` : html`
${this.renderPlanetsTable(transitPlanets)}
` }
`; } private renderSummaryPills( summary: NonNullable, ) { return html`
${this.t('Total')}: ${summary.totalAspects} ${this.t('Harmonious')}: ${summary.harmonious} ${this.t('Challenging')}: ${summary.challenging} ${this.t('Neutral')}: ${summary.neutral}
`; } private renderPlanetsTable(planets: TransitsResponse['transitPlanets']) { return html` ${planets.map((p) => { const pGlyph = planetGlyph(p.name) ?? ''; const sGlyph = signGlyph(p.sign) ?? ''; const speedArrow = p.speed >= 0 ? '↑' : '↓'; return html``; })}
${this.t( 'Transiting planets: each planet with its current sign, degree and daily speed.', )}
${this.t('Planet')} ${this.t('Sign')} ${this.t('Degree')} ${this.t('Speed')}
${p.name} ${ p.isRetrograde ? html`R` : nothing }
${p.sign}
${formatNumber(this.effectiveLang(), p.degree, 2)} ${formatNumber(this.effectiveLang(), Math.abs(p.speed), 2)}${this.t('°/day')}
`; } private renderAspectsList( aspects: NonNullable, ) { return html`
${aspects.map((a, idx) => { const tGlyph = planetGlyph(a.transitPlanet) ?? ''; const nGlyph = planetGlyph(a.natalPlanet) ?? ''; const nature = (a.nature ?? 'neutral').toLowerCase(); const interp = a.interpretation; const type = formatAspectName(a); const status = a.isApplying ? this.t('Applying') : this.t('Separating'); return html`
${a.transitPlanet} ${type} ${a.natalPlanet} ${this.t('{{status}} · orb {{orb}}° · str {{strength}}', { status, orb: formatNumber(this.effectiveLang(), a.orb, 2), strength: formatNumber(this.effectiveLang(), a.strength, 1), })} ${chevron()}
${renderReadingDetail(interp, this.translator)}
`; })}
`; } } declare global { interface HTMLElementTagNameMap { 'roxy-transits-table': RoxyTransitsTable; } }