import { css, html, nothing } from 'lit'; import { customElement } from 'lit/decorators.js'; import type { ChromeString } from '../i18n/chrome-strings.js'; import { planetGlyph, signGlyph } from '../tokens/index.js'; import type { NatalChartResponse } 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 { formatNumber } from '../utils/format.js'; import { display } from '../utils/localized.js'; import { capitalize } from '../utils/string.js'; /** A body or point row, normalized so planets and the four angles share a table. */ interface BodyRow { /** Canonical English name. Keyed on by the glyph table, so it never localizes. */ name: string; /** The name a reader sees. Same as {@link BodyRow.name} until the response carries a translation. */ label: string; sign?: string; /** The sign a reader sees, for the same reason. */ signLabel?: string; longitude?: number; house?: number; speed?: number; isRetrograde?: boolean; /** Essential dignity by sign. Absent for the nodes, Chiron and Lilith, which rule no sign, and an absent value is a different answer from `peregrine`, so the cell stays blank rather than reading as the neutral state. */ dignity?: string; /** True for the chart angles (ASC, MC, Part of Fortune, Vertex). */ isPoint?: boolean; } /** * Western planetary positions table. Renders a /astrology/natal-chart response * as the reference-grade positions grid astrologers read alongside the wheel: * every body with its sign, exact degree, house, and daily motion, followed by * the four chart points (Ascendant, Midheaven, Part of Fortune, Vertex). */ @customElement('roxy-western-planets-table') export class RoxyWesternPlanetsTable 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); overflow: auto; box-shadow: var(--roxy-shadow-sm); } .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); } table { width: 100%; border-collapse: collapse; font-size: var(--roxy-text-sm, 0.875rem); min-width: 460px; } 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.point { background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 8%, transparent); } td.body { font-weight: var(--roxy-weight-bold, 600); color: var(--roxy-fg, #0a0a0a); } .glyph { margin-right: 0.4em; color: var(--roxy-muted, #71717a); } .retro { color: var(--roxy-danger, #dc2626); font-weight: var(--roxy-weight-bold, 600); } .num { font-variant-numeric: tabular-nums; } `, ]; /** Build the ordered row list: the planets array, then the four chart points. */ private rows(): BodyRow[] { const d = this.data; if (!d) return []; const rows: BodyRow[] = (d.planets ?? []).map((p) => ({ name: p.name, label: display(p, 'name'), sign: p.sign, signLabel: display(p, 'sign'), longitude: p.longitude, house: p.house, speed: p.speed, isRetrograde: p.isRetrograde, dignity: p.dignity, })); // The four chart points are copy this component writes rather than values the // response names, so each is typed to its catalogue source and the compiler // rejects one no catalogue carries. const points: ReadonlyArray< readonly [ChromeString, { sign?: string; longitude?: number } | undefined] > = [ ['Ascendant', d.ascendant], ['Midheaven', d.midheaven], ['Part of Fortune', d.partOfFortune], ['Vertex', d.vertex], ]; for (const [name, point] of points) { if (point) { rows.push({ name, label: this.t(name), sign: point.sign, signLabel: display(point, 'sign'), longitude: point.longitude, isPoint: true, }); } } return rows; } protected renderData(d: NatalChartResponse) { if (!d.planets) return this.renderEmpty(); const rows = this.rows(); return html`

${this.t('Planetary positions')}

${rows.map((r) => { const glyph = planetGlyph(r.name) ?? ''; const sGlyph = signGlyph(r.sign) ?? ''; const speed = typeof r.speed === 'number' ? formatNumber(this.effectiveLang(), r.speed, 3) : ''; return html``; })}
${this.t('Western planetary positions: each body with its sign, degree, house, essential dignity and motion. The dignity cell is blank for the lunar nodes, Chiron and Lilith, which rule no sign and therefore hold no dignity at all.')}
${this.t('Body')} ${this.t('Sign')} ${this.t('Degree')} ${this.t('House')} ${this.t('Dignity')} ${this.t('Motion')}
${glyph ? html`${glyph}` : nothing}${r.label} ${sGlyph ? html`${sGlyph}` : nothing}${r.signLabel ?? ''} ${typeof r.longitude === 'number' ? formatSignPosition(r.longitude, r.signLabel) : ''} ${typeof r.house === 'number' ? r.house : ''} ${r.dignity ? capitalize(r.dignity) : ''} ${speed ? html`${speed}${this.t('°/day')}` : nothing} ${r.isRetrograde ? html`` : nothing}
`; } } declare global { interface HTMLElementTagNameMap { 'roxy-western-planets-table': RoxyWesternPlanetsTable; } }