import { css, html, nothing } from 'lit'; import { customElement } from 'lit/decorators.js'; import { PLANET_GLYPH, SIGN_GLYPH } from '../tokens/index.js'; import type { BirthChartResponse } from '../types/index.js'; import { RoxyDataElement } from '../utils/base-element.js'; import { baseStyles } from '../utils/base-styles.js'; import { formatSignPosition } from '../utils/degree.js'; import { chevron, disclosureStyles } from '../utils/disclosure.js'; import { formatNumber } from '../utils/format.js'; import { capitalize } from '../utils/string.js'; /** * Fixed display order: Lagna pinned first as the chart frame, then the nine * grahas in classical sequence. Any graha not in this list is appended. */ const GRAHA_ORDER = [ 'Lagna', 'Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu', ]; type MetaEntry = BirthChartResponse['meta'][string]; /** * Vedic planetary positions table. Renders /vedic-astrology/birth-chart `meta` * as the full reference-grade positions grid a practitioner reads alongside * the kundli wheel: graha, rashi, exact degree, nakshatra and pada, nakshatra * lord, bhava (house), Baladi avastha, and retrograde. * * @remarks * The positions grid is the default view. The same birth-chart response also * carries chart-wide conditions and readings, surfaced as collapsed accordions * below the grid so they never crowd the table: combust grahas (astangata), * planetary wars (graha yuddha), per-graha rashi and nakshatra interpretations, * the active classical yogas (present === true), and the twelve bhava * significations. Each accordion renders only when its source array or map is * non-empty. */ @customElement('roxy-vedic-planets-table') export class RoxyVedicPlanetsTable extends RoxyDataElement { static styles = [ baseStyles, disclosureStyles, css` .wrap { border: 1px solid var(--roxy-border, #e4e4e7); border-radius: var(--roxy-radius-md, 8px); background: var(--roxy-surface, #fff); box-shadow: var(--roxy-shadow-sm); overflow: hidden; } .head { padding: var(--roxy-space-md, 1rem); border-bottom: 1px solid var(--roxy-border, #e4e4e7); } .title { margin: 0; font-size: var(--roxy-text-lg, 1.125rem); font-weight: var(--roxy-weight-bold, 600); } .scroll { overflow-x: auto; } table { width: 100%; border-collapse: collapse; font-size: var(--roxy-text-sm, 0.875rem); min-width: 620px; } thead { background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 20%, transparent); } th, td { padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem); text-align: left; white-space: nowrap; } 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.04em; } tbody tr { border-top: 1px solid var(--roxy-border, #e4e4e7); } tbody tr.lagna { background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 10%, transparent); } td.graha { font-weight: var(--roxy-weight-bold, 600); color: var(--roxy-fg, #0a0a0a); } .glyph { margin-right: 0.4em; color: var(--roxy-muted, #71717a); } /* On the tinted Lagna row the muted glyph drops below the WCAG AA contrast floor, so use the accent foreground there instead. */ tbody tr.lagna .glyph { color: var(--roxy-accent-ink, #b45309); } .retro { color: var(--roxy-warning-fg, #9a3412); font-size: var(--roxy-text-xs, 0.75rem); font-weight: var(--roxy-weight-bold, 600); } .num { font-variant-numeric: tabular-nums; } details.panel { border-top: 1px solid var(--roxy-border, #e4e4e7); } details.panel > summary { display: flex; align-items: center; justify-content: space-between; gap: var(--roxy-space-sm, 0.5rem); padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem); cursor: pointer; font-size: var(--roxy-text-sm, 0.875rem); font-weight: var(--roxy-weight-bold, 600); color: var(--roxy-fg, #0a0a0a); } details.panel > summary:focus-visible { outline: 2px solid var(--roxy-ring, rgba(245, 158, 11, 0.4)); outline-offset: -2px; } .summary-count { margin-left: auto; margin-right: var(--roxy-space-xs, 0.25rem); font-weight: var(--roxy-weight-normal, 400); color: var(--roxy-muted, #71717a); font-variant-numeric: tabular-nums; } .panel-body { padding: 0 var(--roxy-space-md, 1rem) var(--roxy-space-md, 1rem); display: grid; gap: var(--roxy-space-sm, 0.5rem); } .condition { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4em; font-size: var(--roxy-text-sm, 0.875rem); } .condition .planet { font-weight: var(--roxy-weight-bold, 600); } .condition .detail { color: var(--roxy-muted, #71717a); font-variant-numeric: tabular-nums; } .condition .winner { color: var(--roxy-success-fg, #166534); font-weight: var(--roxy-weight-bold, 600); } .interp { display: grid; gap: 0.15em; } .interp .planet { font-weight: var(--roxy-weight-bold, 600); font-size: var(--roxy-text-sm, 0.875rem); } .interp p { margin: 0; font-size: var(--roxy-text-sm, 0.875rem); line-height: var(--roxy-leading-normal, 1.5); } .interp .label { color: var(--roxy-muted, #71717a); font-weight: var(--roxy-weight-bold, 600); } .bhava { display: grid; gap: 0.15em; } .bhava .name { font-weight: var(--roxy-weight-bold, 600); font-size: var(--roxy-text-sm, 0.875rem); } .bhava .desc { margin: 0; font-size: var(--roxy-text-sm, 0.875rem); color: var(--roxy-muted, #71717a); line-height: var(--roxy-leading-normal, 1.5); } .quality { font-size: var(--roxy-text-xs, 0.75rem); font-weight: var(--roxy-weight-bold, 600); text-transform: uppercase; letter-spacing: 0.04em; } .quality.positive { color: var(--roxy-success-fg, #166534); } .quality.negative { color: var(--roxy-danger-fg, #991b1b); } .quality.both { color: var(--roxy-muted, #71717a); } `, ]; /** Ordered [name, entry] pairs: GRAHA_ORDER first, then any extras. */ private orderedRows(): Array<[string, MetaEntry]> { const meta = this.data?.meta ?? {}; const seen = new Set(); const rows: Array<[string, MetaEntry]> = []; for (const name of GRAHA_ORDER) { const entry = meta[name]; if (entry) { rows.push([name, entry]); seen.add(name); } } for (const [name, entry] of Object.entries(meta)) { if (!seen.has(name)) rows.push([name, entry]); } return rows; } protected renderEmpty() { return html`
No chart data
`; } protected renderData(d: BirthChartResponse) { if (!d.meta) return this.renderEmpty(); const rows = this.orderedRows(); return html`

Planetary positions

${rows.map(([name, p]) => { const isLagna = (p.graha ?? name) === 'Lagna'; const glyph = PLANET_GLYPH[capitalize(p.graha ?? name)] ?? ''; const signGlyph = SIGN_GLYPH[capitalize(p.rashi ?? '')] ?? ''; return html``; })}
Graha Rashi Degree Nakshatra Pada Nak. lord House Avastha Retro
${glyph ? html`${glyph}` : nothing}${p.graha ?? name} ${signGlyph ? html`${signGlyph}` : nothing}${p.rashi ?? ''} ${typeof p.longitude === 'number' ? formatSignPosition(p.longitude) : ''} ${p.nakshatra?.name ?? ''} ${p.nakshatra?.pada ?? ''} ${p.nakshatra?.lord ?? ''} ${typeof p.house === 'number' ? p.house : ''} ${p.awastha ?? ''} ${p.isRetrograde ? html`R` : nothing}
${this.renderCombustion()} ${this.renderPlanetaryWar()} ${this.renderInterpretations()} ${this.renderYogas()} ${this.renderHouses()}
`; } private renderCombustion() { const combust = this.data?.combustion ?? []; if (combust.length === 0) return nothing; return html`
Combust grahas${combust.length}${chevron()}
${combust.map((c) => { const glyph = PLANET_GLYPH[capitalize(c.planet)] ?? ''; const dist = formatNumber(c.distanceFromSun, 2); const orb = formatNumber(c.orb, 1); return html`
${glyph ? `${glyph} ` : ''}${c.planet} ${dist} deg from Sun, within ${orb} deg orb
`; })}
`; } private renderPlanetaryWar() { const wars = this.data?.planetaryWar ?? []; if (wars.length === 0) return nothing; return html`
Planetary wars${wars.length}${chevron()}
${wars.map((w) => { const dist = formatNumber(w.distance, 2); return html`
${w.planet1} vs ${w.planet2} ${dist} deg apart ${w.winner} wins
`; })}
`; } private renderInterpretations() { const interp = this.data?.interpretations ?? {}; const entries = this.orderedRows() .map(([name, p]) => [p.graha ?? name, interp[p.graha ?? name]] as const) .filter(([, v]) => v != null); if (entries.length === 0) return nothing; return html`
Interpretations${entries.length}${chevron()}
${entries.map(([name, v]) => { const glyph = PLANET_GLYPH[capitalize(name)] ?? ''; return html`
${glyph ? `${glyph} ` : ''}${name} ${v.rashi ? html`

Rashi. ${v.rashi}

` : nothing} ${v.nakshatra ? html`

Nakshatra. ${v.nakshatra}

` : nothing}
`; })}
`; } private renderHouses() { const houses = (this.data?.houses ?? []).filter( (h) => h.name || h.description, ); if (houses.length === 0) return nothing; return html`
Bhava significations${houses.length}${chevron()}
${houses.map( (h) => html`
${h.number}. ${h.name ?? ''} ${h.description ? html`

${h.description}

` : nothing}
`, )}
`; } private renderYogas() { const yogas = (this.data?.yogas ?? []).filter((y) => y.present); if (yogas.length === 0) return nothing; return html`
Yogas${yogas.length}${chevron()}
${yogas.map( (y) => html`
${y.name} ${y.quality ? html`${y.quality}` : nothing} ${y.result ? html`

${y.result}

` : nothing}
`, )}
`; } } declare global { interface HTMLElementTagNameMap { 'roxy-vedic-planets-table': RoxyVedicPlanetsTable; } }