import { css, html, nothing } from 'lit'; import { property } from 'lit/decorators.js'; import { mayanDaySignEndpoints } from '../generated/endpoint-defaults.js'; import type { ChromeString } from '../i18n/chrome-strings.js'; import type { CalculateTzolkinResponse, GenerateMayanChartResponse, } from '../types/index.js'; import { RoxyDataElement } from '../utils/base-element.js'; import { baseStyles } from '../utils/base-styles.js'; import { customElement } from '../utils/define.js'; import { formatDate, formatInteger } from '../utils/format.js'; import type { InterpSection } from '../utils/interp-accordion.js'; import { interpAccordionStyles } from '../utils/interp-accordion.js'; import { displayOption } from '../utils/localized.js'; import { stackedTableStyles } from '../utils/stacked-table.js'; import { humanize } from '../utils/string.js'; type MayanData = CalculateTzolkinResponse | GenerateMayanChartResponse; type Tzolkin = CalculateTzolkinResponse; type MayanChart = GenerateMayanChartResponse; /** * What the recorded character of a coefficient is called. * * @remarks * A closed three-member set of machine identifiers with no localized partner on the response, so the * words are the component's own and go through the catalogue. Typed `ChromeString`, which is what * keeps a dynamic lookup inside it. * * **The word names the CHARACTER the tradition records for the number, not the number.** Only nine of * the thirteen coefficients carry one, which is why an unbanded day prints no such fact at all rather * than a blank, and the fact is labelled for what it is: a bare adjective under a heading naming the * coefficient reads as the coefficient itself, which is the numeral in the hero. */ const BAND_LABEL: Record = { gentle: 'Gentle', indifferent: 'Indifferent', violent: 'Violent', }; /** * What each arm of the four-fold cross is called. * * @remarks * Five machine identifiers, closed, with no localized partner on the response, so the words are the * component's own. Same reason and same typing as {@link BAND_LABEL}. */ const CROSS_LABEL: Record = { center: 'Center', conception: 'Conception arm', destiny: 'Destiny arm', left: 'Left arm', right: 'Right arm', }; /** A trecena is thirteen days by definition, which is what makes the coefficient and the day inside it the same number. */ const TRECENA_DAYS = 13; /** True for the fuller payload, which nests the day sign under a Calendar Round. */ function isChart(d: MayanData): d is MayanChart { return 'tzolkin' in d; } /** * Mayan day sign. Pass `data` from POST /mesoamerican-astrology/mayan/tzolkin, or set * `mode="chart"` and pass POST /mesoamerican-astrology/mayan/chart. * * @remarks * **The sign is printed in all three of the orthographies the response carries, because a reader * arrives holding one of them.** The standard Maya spelling is the headline, the sixteenth century * Yucatec form is what printed reference tables use, and the highland form is the vocabulary a * daykeeper works in. Showing one and dropping the others makes the card unreconcilable with * whichever book or teacher the reader already has. * * **The coefficient is drawn beside the sign and never underneath it.** A Tzolkin day is a number * and a sign together, and neither half names the day on its own, so the pair reads as one unit and * the trecena that contains it sits in the facts below. * * **The attribute says which read was asked for and the shape check narrows under it.** The Calendar * Round payload adds the Haab date, the Long Count, the Lord of the Night, the year bearer and the * four-fold cross, and `mode="chart"` is what draws them, so a page that asked for the day sign * alone never grows a second calendar its endpoint did not return. The day sign half reads the * SHAPE either way, so the fuller payload still renders correctly in day mode. * * `hide-readings` keeps every calculated value: both calendar dates, the coefficient, the trecena, * the Long Count, the Calendar Round, the correlation and the four positions of the cross. The * keynote, the coefficient reading, the guidance, the strengths and challenges and every per * position reading go. */ @customElement('roxy-mayan-day-sign') export class RoxyMayanDaySign extends RoxyDataElement { static endpoints = mayanDaySignEndpoints; static styles = [ baseStyles, interpAccordionStyles, stackedTableStyles, css` .card { 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; /* Never an implicit auto column: it floors at min-content, so one long * unbreakable string widens the track past the padded card. */ grid-template-columns: minmax(0, 1fr); gap: var(--roxy-space-md, 1rem); } .hero { display: flex; align-items: center; gap: var(--roxy-space-md, 1rem); } /* Only the text column may shrink: a flex item keeps min-width auto, so a * long sign name would otherwise push the numeral out of the card. */ .coefficient { flex-shrink: 0; font-size: 2.75rem; line-height: 1; font-weight: var(--roxy-weight-bold, 600); font-variant-numeric: tabular-nums; color: var(--roxy-accent-ink, #b45309); } .hero > div:not(.coefficient) { min-width: 0; } .title { margin: 0; font-size: var(--roxy-text-xl, 1.25rem); font-weight: var(--roxy-weight-bold, 600); letter-spacing: var(--roxy-tracking-tight); } .spellings { margin: 0.15rem 0 0; display: flex; flex-wrap: wrap; gap: 0.15rem var(--roxy-space-md, 1rem); font-size: var(--roxy-text-sm, 0.875rem); color: var(--roxy-secondary, #475569); } .facts { display: flex; flex-wrap: wrap; gap: 0.35rem var(--roxy-space-md, 1rem); font-size: var(--roxy-text-sm, 0.875rem); color: var(--roxy-secondary, #475569); } .lbl { color: var(--roxy-muted, #71717a); font-size: var(--roxy-text-xs, 0.75rem); text-transform: uppercase; letter-spacing: 0.06em; font-weight: var(--roxy-weight-bold, 600); margin-right: 0.35rem; } .facts b { color: var(--roxy-fg, #0a0a0a); font-weight: var(--roxy-weight-bold, 600); } .band { display: inline-block; padding: 0.1rem 0.4rem; border-radius: var(--roxy-radius-sm, 4px); font-size: var(--roxy-text-xs, 0.75rem); background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 12%, var(--roxy-surface, #fff)); color: var(--roxy-fg, #0a0a0a); } .keynote { margin: 0; font-size: var(--roxy-text-base, 1rem); line-height: var(--roxy-leading-relaxed, 1.65); } .summary { margin: 0; font-size: var(--roxy-text-sm, 0.875rem); line-height: var(--roxy-leading-relaxed, 1.65); color: var(--roxy-secondary, #475569); } .block-title { margin: 0 0 var(--roxy-space-sm, 0.5rem); font-size: var(--roxy-text-xs, 0.75rem); color: var(--roxy-muted, #71717a); font-weight: var(--roxy-weight-bold, 600); text-transform: uppercase; letter-spacing: 0.06em; } .lists { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); gap: var(--roxy-space-md, 1rem); } ul.plain { margin: 0; padding-left: 1.1rem; font-size: var(--roxy-text-sm, 0.875rem); line-height: var(--roxy-leading-relaxed, 1.65); } .scroll { overflow-x: auto; min-width: 0; } table { border-collapse: collapse; width: 100%; font-size: var(--roxy-text-sm, 0.875rem); } th, td { text-align: left; padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--roxy-border, #e4e4e7); vertical-align: top; } th { color: var(--roxy-muted, #71717a); font-size: var(--roxy-text-xs, 0.75rem); text-transform: uppercase; letter-spacing: 0.06em; white-space: nowrap; } td.num { font-variant-numeric: tabular-nums; white-space: nowrap; } td p { margin: 0.15rem 0 0; font-size: var(--roxy-text-xs, 0.75rem); color: var(--roxy-secondary, #475569); } `, ]; /** Which read the response is: the day sign alone, or the full Calendar Round chart. */ @property({ type: String, reflect: true }) mode: 'day' | 'chart' = 'day'; protected renderData(d: MayanData) { const locale = this.effectiveLang(); const chart = this.mode === 'chart' && isChart(d) ? d : undefined; const tz = isChart(d) ? d.tzolkin : d; const reading = tz?.reading; const band = reading?.numberBand ? BAND_LABEL[reading.numberBand] : undefined; return html`
${ typeof tz?.number === 'number' ? html`
${formatInteger(locale, tz.number)}
` : nothing }

${this.t('Mayan day sign')}

${tz?.daySignName ?? ''}

${ tz?.daySignClassic ? html`${this.t('Classic spelling')}${tz.daySignClassic}` : nothing } ${ tz?.daySignKiche ? html`${this.t('Highland spelling')}${tz.daySignKiche}` : nothing }

${ d.date ? html`${this.t('Date')}${formatDate(locale, d.date)}` : nothing } ${this.renderTrecena(locale, tz?.trecena)} ${chart ? this.renderHaabFact(locale, chart.haab) : nothing} ${ chart?.longCount?.formatted ? html`${this.t('Long Count')}${chart.longCount.formatted}` : nothing } ${ chart?.calendarRound ? html`${this.t('Calendar Round')}${chart.calendarRound}` : nothing } ${ chart?.yearBearer?.daySignName ? html`${this.t('Year Bearer')}${formatInteger(locale, chart.yearBearer.number)} ${chart.yearBearer.daySignName}` : nothing } ${ chart?.lordOfNight?.label ? html`${this.t('Lord of the Night')}${chart.lordOfNight.label}` : nothing } ${ band ? html`${this.t('Character of the number')}${this.t(band)}` : nothing } ${ // The constant is a request enum, so the published field-label payload // already names it in the reader's language. The identifier is what a // caller stores and what a printed table is checked against, so it // rides in the title and is what an unlabelled language prints. d.conventions?.correlation ? html`${this.t('Correlation')}${displayOption(locale, 'correlation', d.conventions.correlation, d.conventions.correlation)}` : nothing }
${ reading?.keynote && !this.hideReadings ? html`

${reading.keynote}

` : nothing } ${this.renderLists(reading)} ${this.renderInterpretation(this.sections(chart, reading), 'mayan-day')} ${this.renderCross(locale, chart?.cross)} ${ chart?.summary && !this.hideReadings ? html`

${chart.summary}

` : nothing }
`; } /** The thirteen day period the day sits in, and where in it. */ private renderTrecena( locale: string | undefined, trecena: Tzolkin['trecena'] | undefined, ) { if (!trecena) return nothing; return html`${this.t('Trecena')}${formatInteger(locale, trecena.number)} ${ typeof trecena.dayOfTrecena === 'number' ? this.t('Day {{n}} of {{total}}', { n: formatInteger(locale, trecena.dayOfTrecena), total: formatInteger(locale, TRECENA_DAYS), }) : nothing } ${ trecena.rulingSignName ? html`· ${this.t('Ruling sign')}${trecena.rulingSignName}` : nothing }`; } /** The Haab date: the month name, the day inside it, and where the day falls in the 365 day year. */ private renderHaabFact( locale: string | undefined, haab: MayanChart['haab'] | undefined, ) { if (!haab?.monthName) return nothing; return html`${this.t('Haab')}${formatInteger(locale, haab.day)} ${haab.monthName} ${haab.monthClassic ? html`(${haab.monthClassic})` : nothing} ${ typeof haab.dayOfYear === 'number' ? html`· ${this.t('Day of year')}${formatInteger(locale, haab.dayOfYear)}` : nothing }`; } /** What the sign does well and where the same temperament costs it something. */ private renderLists(reading: Tzolkin['reading'] | undefined) { const strengths = reading?.strengths ?? []; const challenges = reading?.challenges ?? []; if ( this.hideReadings || (strengths.length === 0 && challenges.length === 0) ) return nothing; return html`
${ strengths.length > 0 ? html`

${this.t('Strengths')}

    ${strengths.map((s) => html`
  • ${s}
  • `)}
` : nothing } ${ challenges.length > 0 ? html`

${this.t('Challenges')}

    ${challenges.map((s) => html`
  • ${s}
  • `)}
` : nothing }
`; } /** The prose that belongs behind a disclosure: what the coefficient contributes, and what to do about it. */ private sections( chart: MayanChart | undefined, reading: Tzolkin['reading'] | undefined, ): InterpSection[] { const sections: InterpSection[] = []; if (reading?.numberReading) sections.push({ label: this.t('Coefficient'), body: reading.numberReading, }); if (reading?.guidance) sections.push({ label: this.t('Guidance'), body: reading.guidance }); if (chart?.haab?.reading) sections.push({ label: this.t('Haab'), body: chart.haab.reading }); if (chart?.lordOfNight?.reading) sections.push({ label: this.t('Lord of the Night'), body: chart.lordOfNight.reading, }); if (chart?.yearBearer?.reading) sections.push({ label: this.t('Year Bearer'), body: chart.yearBearer.reading, }); return sections; } /** The name of one arm, falling back to the wire id for a position the API might add. */ private crossLabel(position: string | undefined): string { const source = CROSS_LABEL[position ?? '']; return source ? this.t(source) : humanize(position ?? ''); } /** * The four-fold cross: the day sign standing at each quarter around this one. * * @remarks * The positions and their day offsets are calculated values and stay under `hide-readings`, so a * page showing the map without the report keeps the whole cross and loses only the sentences. */ private renderCross( locale: string | undefined, cross: MayanChart['cross'] | undefined, ) { const rows = cross ?? []; if (rows.length === 0) return nothing; return html`

${this.t('Mayan cross')}

${rows.map( (r) => html``, )}
${this.t('Position')} ${this.t('Offset in days')} ${this.t('Day sign')}
${this.crossLabel(r.position)} ${formatInteger(locale, r.offsetDays)} ${formatInteger(locale, r.number)} ${r.daySignName ?? ''} ${r.daySignKiche ? html`(${r.daySignKiche})` : nothing} ${ r.reading && !this.hideReadings ? html`

${r.reading}

` : nothing }
`; } } declare global { interface HTMLElementTagNameMap { 'roxy-mayan-day-sign': RoxyMayanDaySign; } }