import { css, html, LitElement, nothing } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import type { CalculateExpressionResponse, CalculateLifePathResponse, CalculatePersonalYearResponse, GenerateNumerologyChartResponse, } from '../types/index.js'; import { baseStyles } from '../utils/base-styles.js'; import { humanize } from '../utils/string.js'; type NumerologyData = | CalculateLifePathResponse | CalculateExpressionResponse | CalculatePersonalYearResponse | GenerateNumerologyChartResponse; /** * Numerology card. Renders /numerology/{life-path,expression,personal-year,chart}. * Use the `type` attribute to switch the layout. */ @customElement('roxy-numerology-card') export class RoxyNumerologyCard extends LitElement { static styles = [ baseStyles, css` .card { background: var(--roxy-bg, #fff); 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); } .hero { display: flex; align-items: center; gap: var(--roxy-space-md, 1rem); } .numeral { font-size: 4rem; line-height: 1; font-weight: var(--roxy-weight-bold, 600); color: var(--roxy-accent-fg, #b45309); font-variant-numeric: tabular-nums; } .label { margin: 0; font-size: var(--roxy-text-xs, 0.75rem); color: var(--roxy-muted, #71717a); text-transform: uppercase; letter-spacing: 0.06em; } .title { margin: 0; font-size: var(--roxy-text-lg, 1.125rem); font-weight: var(--roxy-weight-bold, 600); } .meaning { margin: 0; color: var(--roxy-fg, #0a0a0a); } .calc { margin: 0; font-family: var(--roxy-font-mono); font-size: var(--roxy-text-xs, 0.75rem); color: var(--roxy-muted, #71717a); background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 30%, transparent); padding: var(--roxy-space-sm, 0.5rem); border-radius: var(--roxy-radius-sm, 4px); white-space: pre-wrap; overflow-wrap: anywhere; } .chips { display: flex; flex-wrap: wrap; gap: var(--roxy-space-xs, 0.25rem); } .chips span { background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 14%, transparent); padding: 2px 8px; border-radius: var(--roxy-radius-full, 9999px); font-size: var(--roxy-text-xs, 0.75rem); } .cores { display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); gap: var(--roxy-space-sm, 0.5rem); border-top: 1px solid var(--roxy-border, #e4e4e7); padding-top: var(--roxy-space-md, 1rem); } .cores .item { display: flex; align-items: baseline; gap: var(--roxy-space-xs, 0.25rem); font-size: var(--roxy-text-sm, 0.875rem); } .cores .item span:first-child { color: var(--roxy-muted, #71717a); text-transform: capitalize; } .cores .item strong { color: var(--roxy-accent-fg, #b45309); font-variant-numeric: tabular-nums; font-size: var(--roxy-text-base, 1rem); font-weight: var(--roxy-weight-bold, 600); } .karmic { background: color-mix(in srgb, var(--roxy-warning, #ea580c) 12%, transparent); border: 1px solid color-mix(in srgb, var(--roxy-warning, #ea580c) 32%, transparent); padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem); border-radius: var(--roxy-radius-md, 8px); font-size: var(--roxy-text-sm, 0.875rem); color: var(--roxy-fg, #0a0a0a); } `, ]; @property({ attribute: false }) data: NumerologyData | null = null; @property({ type: String, reflect: true }) type: 'life-path' | 'expression' | 'personal-year' | 'chart' = 'life-path'; render() { const d = this.data; if (!d) return html`
No numerology data
`; const headerLabel = LABELS[this.type] ?? this.type; if ('coreNumbers' in d) return this.renderChart(d, headerLabel); if ('personalYear' in d) return this.renderPersonalYear(d, headerLabel); return this.renderNumberCard( d as CalculateLifePathResponse | CalculateExpressionResponse, headerLabel, ); } private renderNumberCard( d: CalculateLifePathResponse | CalculateExpressionResponse, headerLabel: string, ) { const keywords = d.meaning?.keywords ?? []; return html`
${typeof d.number === 'number' ? html`
${d.number}
` : nothing}

${headerLabel}

${d.meaning?.title ? html`

${d.meaning.title}

` : nothing}
${d.meaning?.description ? html`

${d.meaning.description}

` : nothing} ${d.calculation ? html`
${d.calculation}
` : nothing} ${ keywords.length > 0 ? html`
${keywords.map((k) => html`${k}`)}
` : nothing } ${ d.hasKarmicDebt && d.karmicDebtNumber ? html`
Karmic debt ${d.karmicDebtNumber}. ${karmicDebtText(d.karmicDebtMeaning)}
` : nothing }
`; } private renderPersonalYear( d: CalculatePersonalYearResponse, headerLabel: string, ) { return html`
${typeof d.personalYear === 'number' ? html`
${d.personalYear}
` : nothing}

${headerLabel}

${d.theme ? html`

${d.theme}

` : nothing}
${d.forecast ? html`

${d.forecast}

` : nothing} ${d.advice ? html`

${d.advice}

` : nothing}
`; } private renderChart(d: GenerateNumerologyChartResponse, headerLabel: string) { const cores = Object.entries(d.coreNumbers).filter( ([, v]) => v !== null && v !== undefined, ); return html`

${headerLabel}

${d.profile?.name ? html`

${d.profile.name}

` : nothing}
${ cores.length > 0 ? html`
${cores.map( ([k, v]) => html`
${humanize(k)} ${v.number ?? ''}
`, )}
` : nothing }
`; } } const LABELS: Record = { 'life-path': 'Life Path', expression: 'Expression', 'personal-year': 'Personal Year', chart: 'Numerology chart', }; type KarmicDebtMeaning = { description: string; challenge: string; resolution: string; }; function karmicDebtText(value: KarmicDebtMeaning | undefined): string { if (!value) return ''; return [value.description, value.challenge, value.resolution] .filter(Boolean) .join(' '); } declare global { interface HTMLElementTagNameMap { 'roxy-numerology-card': RoxyNumerologyCard; } }