import { css, html, nothing } from 'lit'; import { customElement } from 'lit/decorators.js'; import { SIGN_GLYPH } from '../tokens/index.js'; import type { FixedStarsResponse } from '../types/index.js'; import { RoxyDataElement } from '../utils/base-element.js'; import { baseStyles } from '../utils/base-styles.js'; import { formatDegreeInSign } from '../utils/degree.js'; import { chevron, disclosureStyles } from '../utils/disclosure.js'; import { formatNumber } from '../utils/format.js'; import { capitalize } from '../utils/string.js'; type Star = FixedStarsResponse['stars'][number]; /** * Fixed stars table. Leads with the high-value view from a * /astrology/fixed-stars response: every star-to-natal-point conjunction sorted * tightest first, each with its reading. The full precessed star catalog * (position, magnitude, traditional nature, keywords) sits in a secondary * disclosure so the contacts stay front and center. */ @customElement('roxy-fixed-stars') export class RoxyFixedStars extends RoxyDataElement { static styles = [ baseStyles, disclosureStyles, css` .wrap { width: 100%; 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); } header { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem); } .title { font-size: var(--roxy-text-lg, 1.125rem); font-weight: var(--roxy-weight-bold, 600); margin: 0; color: var(--roxy-primary, #0f172a); } .badge { padding: 2px 8px; border-radius: var(--roxy-radius-full, 9999px); font-size: var(--roxy-text-xs, 0.75rem); background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 14%, transparent); color: var(--roxy-fg, #0a0a0a); } .badge b { color: var(--roxy-accent-ink, #b45309); font-weight: 600; } .summary { color: var(--roxy-fg, #0a0a0a); font-size: var(--roxy-text-sm, 0.875rem); margin: 0; } .empty-note { color: var(--roxy-muted, #71717a); font-size: var(--roxy-text-sm, 0.875rem); margin: 0; } .subhead { font-size: var(--roxy-text-sm, 0.875rem); font-weight: 600; color: var(--roxy-muted, #71717a); text-transform: uppercase; letter-spacing: 0.06em; margin: 0 0 var(--roxy-space-sm, 0.5rem); } .interp-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); } .interp-card summary { cursor: pointer; font-weight: 500; color: var(--roxy-fg, #0a0a0a); display: flex; align-items: center; justify-content: space-between; gap: var(--roxy-space-md, 1rem); } .contact { display: inline-flex; align-items: baseline; gap: 0.4rem; } .contact .point { color: var(--roxy-accent-ink, #b45309); font-weight: 600; } .interp-aside { display: inline-flex; align-items: center; gap: 0.5rem; } .interp-aside small { color: var(--roxy-muted, #71717a); font-weight: 400; font-variant-numeric: tabular-nums; } .interp-body { margin-top: var(--roxy-space-xs, 0.25rem); color: var(--roxy-fg, #0a0a0a); font-size: var(--roxy-text-sm, 0.875rem); } .catalog summary { cursor: pointer; font-weight: 600; color: var(--roxy-fg, #0a0a0a); display: flex; align-items: center; gap: 0.5rem; } .scroll { overflow-x: auto; margin-top: var(--roxy-space-sm, 0.5rem); } table { width: 100%; border-collapse: collapse; font-size: var(--roxy-text-sm, 0.875rem); } th, td { text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--roxy-border, #e4e4e7); white-space: nowrap; vertical-align: top; } th { color: var(--roxy-muted, #71717a); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; font-size: var(--roxy-text-xs, 0.75rem); } td.num { text-align: right; font-variant-numeric: tabular-nums; } .sg { color: var(--roxy-secondary, #475569); margin-right: 0.3rem; } .kw { display: flex; flex-wrap: wrap; gap: 0.2rem; white-space: normal; max-width: 18rem; } .kw span { padding: 0 6px; border-radius: var(--roxy-radius-full, 9999px); font-size: var(--roxy-text-xs, 0.75rem); background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 45%, transparent); color: var(--roxy-fg, #0a0a0a); } `, ]; protected renderEmpty() { return html`
No fixed star data
`; } protected renderData(data: FixedStarsResponse) { const conjunctions = data.conjunctions ?? []; const stars = data.stars ?? []; return html`

Fixed stars

${ typeof data.orb === 'number' ? html`Orb ${formatNumber(data.orb, 1)}°` : nothing }
${data.summary ? html`

${data.summary}

` : nothing} ${ conjunctions.length ? html`

Conjunctions to the chart

${conjunctions.map((c, i) => { return html`
${c.point} conjunct ${c.star} orb ${formatNumber(c.orb, 2)}° ${chevron()} ${c.interpretation ? html`
${c.interpretation}
` : nothing}
`; })}
` : html`

No star sits within the orb of a natal point.

` } ${stars.length ? this.renderCatalog(stars) : nothing}
`; } private renderCatalog(stars: Star[]) { return html`
${chevron()} Star catalog (${stars.length})
${stars.map((s) => { const g = SIGN_GLYPH[capitalize(s.sign)]; return html``; })}
Precessed positions for the chart date
Star Position Mag Nature Keywords
${s.name} ${g ? html`${g}` : nothing}${formatDegreeInSign(s.degree)} ${s.sign} ${formatNumber(s.magnitude, 1)} ${s.nature}
${(s.keywords ?? []).map((k) => html`${k}`)}
`; } } declare global { interface HTMLElementTagNameMap { 'roxy-fixed-stars': RoxyFixedStars; } }