import { css, html, nothing } from 'lit'; import { customElement } from 'lit/decorators.js'; import type { ChromeString } from '../i18n/chrome-strings.js'; import { planetGlyph } from '../tokens/index.js'; import type { HeliacalResponse } from '../types/index.js'; import { RoxyDataElement } from '../utils/base-element.js'; import { baseStyles } from '../utils/base-styles.js'; import { formatDateTime, formatNumber } from '../utils/format.js'; type Graha = HeliacalResponse['grahas'][number]; type Event = NonNullable; /** Udaya is the graha re-emerging from the Sun rays, asta is it disappearing into them. Sanskrit keys, never translated by the API, so the display word is ours to choose. */ const EVENT_WORD: Record = { udaya: 'Rose', asta: 'Set', }; /** Forward-looking form of the same two events, for the NEXT event rather than the last one. */ const NEXT_EVENT_WORD: Record = { udaya: 'rises', asta: 'sets', }; /** * Heliacal rising and setting (udaya and asta) for the six grahas that rise and set. * * @remarks * This is the muhurta surface: Guru Asta and Shukra Asta are the periods classical muhurta withholds marriage, so what a practitioner opens this for is a WINDOW, not a status. The window is therefore the headline of every row and the present verdict is the supporting line, because "invisible until 12 August" answers the question and "invisible" alone does not. * * Three things here are easy to render wrongly, and each is deliberate: * * `lastEvent` and `nextEvent` are genuinely NULLABLE, not merely optional. A graha far from conjunction can have no event inside the search horizon at all, which is normal for Mars, so a missing event is reported as its own state rather than as a blank date. * * The `kalamsa` limit on an EVENT can differ from the graha's CURRENT `kalamsa`, because the Surya Siddhanta gives Mercury and Venus a tighter limit while retrograde. Rendering one and labelling it the other would be a quiet lie, so each is shown against the event it belongs to. * * `horizon` is a real distinction a reader needs, not an internal detail: east means the graha is read before sunrise and is a morning object, west means after sunset and an evening one. A graha crosses to the other horizon as it passes the Sun, which is why an asta and the udaya after it usually sit on opposite horizons. */ @customElement('roxy-heliacal-table') export class RoxyHeliacalTable extends RoxyDataElement { static styles = [ baseStyles, 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); display: grid; gap: var(--roxy-space-xs, 0.25rem); } .title { margin: 0; font-size: var(--roxy-text-lg, 1.125rem); font-weight: var(--roxy-weight-bold, 600); } .sub { margin: 0; color: var(--roxy-muted, #71717a); font-size: var(--roxy-text-xs, 0.75rem); } .rows { display: grid; } .row { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--roxy-space-xs, 0.25rem); padding: var(--roxy-space-md, 1rem); border-top: 1px solid var(--roxy-border, #e4e4e7); } .row-top { display: flex; align-items: baseline; gap: var(--roxy-space-sm, 0.5rem); flex-wrap: wrap; } .graha { font-weight: var(--roxy-weight-bold, 600); color: var(--roxy-fg, #0a0a0a); } .glyph { margin-right: 0.35em; color: var(--roxy-muted, #71717a); } .state { font-size: var(--roxy-text-xs, 0.75rem); padding: 0.1rem 0.45rem; border-radius: var(--roxy-radius-sm, 4px); border: 1px solid var(--roxy-border, #e4e4e7); color: var(--roxy-fg, #0a0a0a); } .state.invisible { background: color-mix( in srgb, var(--roxy-warning, #f59e0b) 14%, transparent ); } .retro { color: var(--roxy-warning-fg, #9a3412); font-size: var(--roxy-text-xs, 0.75rem); font-weight: var(--roxy-weight-bold, 600); } .window { margin: 0; font-size: var(--roxy-text-sm, 0.875rem); color: var(--roxy-fg, #0a0a0a); } .meta { margin: 0; color: var(--roxy-muted, #71717a); font-size: var(--roxy-text-xs, 0.75rem); line-height: var(--roxy-leading-normal, 1.5); } .num { font-variant-numeric: tabular-nums; } `, ]; protected renderData(d: HeliacalResponse) { const grahas = d.grahas ?? []; if (!grahas.length) return this.renderEmpty(); return html`

${this.t('Heliacal rising and setting')}

${this.t( 'Whether each graha stands far enough from the Sun to be seen, for {{date}}. The Sun and the nodes never appear here: they have no heliacal event.', { date: d.date }, )}

${grahas.map((g) => this.renderGraha(g))}
`; } private renderGraha(g: Graha) { const glyph = planetGlyph(g.graha) ?? ''; return html`
${glyph ? html`${glyph}` : nothing}${g.graha} ${g.visible ? this.t('Visible') : this.t('Invisible')} ${ g.retrograde ? html`R` : nothing }

${this.windowLine(g)}

${this.metaLine(g)}

`; } /** * The headline: what a reader came for. Leads with the NEXT event, since that is the date a muhurta decision turns on, and falls back to the last one when nothing lies ahead inside the search horizon. */ private windowLine(g: Graha) { const next = g.nextEvent; const last = g.lastEvent; if (next) { const vars = { event: this.t(NEXT_EVENT_WORD[next.type]), where: next.horizon === 'east' ? this.t('in the east') : this.t('in the west'), when: formatDateTime(this.effectiveLang(), next.datetime), }; return html`${ g.visible ? this.t('Visible until it {{event}} {{where}} on {{when}}', vars) : this.t('Invisible until it {{event}} {{where}} on {{when}}', vars) }`; } if (last) { return html`${this.t( '{{event}} {{where}} on {{when}}, with no further event inside the search window', { event: this.t(EVENT_WORD[last.type]), where: last.horizon === 'east' ? this.t('in the east') : this.t('in the west'), when: formatDateTime(this.effectiveLang(), last.datetime), }, )}`; } return html`${this.t( 'No rising or setting inside the search window, which is normal for a graha far from the Sun', )}`; } /** * The supporting line: the measured separation against the limit it is judged by, plus which horizon the graha belongs to. * * Both numbers are degrees of TIME in oblique ascension rather than a plain longitude gap, which is the measure the Surya Siddhanta defines these limits in and the reason the verdict depends on latitude at all. * * @remarks * The differing-limit note is derived by COMPARING the two numbers the response carries, never by naming which grahas have a retrograde variant. Only Mercury and Venus do: ix.7-8 gives them 12 or 14 and 8 or 10, while Jupiter, Saturn, Mars and the Moon each have one limit whatever their motion. Tying the note to retrograde motion instead would claim a variant limit for a retrograde Jupiter, which does not exist, and a practitioner reads that as the component not knowing the rule. Comparing the data cannot make that mistake and needs no copy of the table. */ private metaLine(g: Graha) { const side = g.horizon === 'east' ? this.t('a morning graha, read before sunrise') : this.t('an evening graha, read after sunset'); const at = g.nextEvent ?? g.lastEvent; const shifts = at && at.kalamsa !== g.kalamsa; const vars = { degrees: formatNumber(this.effectiveLang(), g.timeDegrees, 2), limit: formatNumber(this.effectiveLang(), g.kalamsa, 0), shifted: shifts ? formatNumber(this.effectiveLang(), at.kalamsa, 0) : '', }; // Two whole sentences rather than a stem and an appended clause: the shift // only applies to Mercury and Venus, and a fragment cannot be translated. return html`${ shifts ? this.t( '{{degrees}}° of time from the Sun against a limit of {{limit}}°, becoming {{shifted}}° at that event', vars, ) : this.t( '{{degrees}}° of time from the Sun against a limit of {{limit}}°', vars, ) } · ${side}`; } } declare global { interface HTMLElementTagNameMap { 'roxy-heliacal-table': RoxyHeliacalTable; } }