{
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: hidden;
box-shadow: var(--roxy-shadow-sm);
}
.head {
padding: var(--roxy-space-md, 1rem);
border-bottom: 1px solid var(--roxy-border, #e4e4e7);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--roxy-space-sm, 0.5rem);
}
.title {
margin: 0;
font-size: var(--roxy-text-lg, 1.125rem);
font-weight: var(--roxy-weight-bold, 600);
}
.date {
color: var(--roxy-muted, #71717a);
font-size: var(--roxy-text-sm, 0.875rem);
}
table {
width: 100%;
border-collapse: collapse;
font-size: var(--roxy-text-sm, 0.875rem);
}
tbody tr:nth-child(odd) {
background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 24%, transparent);
}
th,
td {
padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem);
text-align: left;
vertical-align: top;
}
th {
color: var(--roxy-muted, #71717a);
font-weight: var(--roxy-weight-bold, 600);
width: 38%;
text-transform: capitalize;
}
td {
color: var(--roxy-fg, #0a0a0a);
font-variant-numeric: tabular-nums;
}
td small {
display: block;
margin-top: 2px;
color: var(--roxy-muted, #71717a);
font-size: var(--roxy-text-xs, 0.75rem);
font-variant-numeric: normal;
line-height: 1.5;
}
.section {
border-top: 1px solid var(--roxy-border, #e4e4e7);
padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem);
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;
}
.quiet {
color: var(--roxy-muted, #71717a);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: var(--roxy-space-xs, 0.25rem);
margin-top: 4px;
}
.chip {
padding: 1px 8px;
border-radius: var(--roxy-radius-full, 9999px);
font-size: var(--roxy-text-xs, 0.75rem);
background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 55%, transparent);
}
.chip.good {
background: color-mix(in srgb, var(--roxy-success, #16a34a) 14%, transparent);
color: var(--roxy-success-fg, #166534);
}
.chip.bad {
background: color-mix(in srgb, var(--roxy-danger, #dc2626) 14%, transparent);
color: var(--roxy-danger-fg, #991b1b);
}
`,
];
@property({ type: String, reflect: true })
detail: 'basic' | 'detailed' = 'detailed';
protected renderData(d: PanchangData) {
const detailed = 'sunrise' in d ? d : null;
const basic = 'sunLongitude' in d ? d : null;
const muhurtas: Array<[string, PanchangTime | null | undefined]> = detailed
? [
['Brahma Muhurta', detailed.brahmaMuhurta],
['Abhijit Muhurta', detailed.abhijitMuhurta],
['Vijaya Muhurta', detailed.vijayaMuhurta],
['Godhuli Muhurta', detailed.godhuliMuhurta],
['Nishita Muhurta', detailed.nishitaMuhurta],
['Pratah Sandhya', detailed.pratahSandhya],
['Sayahna Sandhya', detailed.sayahnaSandhya],
]
: [];
const auspiciousWindows: Array<[string, PanchangTime]> = detailed
? this.expandWindows(this.t('Amrit Kalam'), detailed.amritKalam)
: [];
const inauspicious: Array<[string, PanchangTime | undefined]> = detailed
? [
['Rahu Kaal', detailed.rahuKaal],
['Yamaganda', detailed.yamaganda],
['Gulika', detailed.gulika],
]
: [];
const inauspiciousWindows: Array<[string, PanchangTime]> = detailed
? [
...this.expandWindows(this.t('Dur Muhurta'), detailed.durMuhurta),
...this.expandWindows(this.t('Varjyam'), detailed.varjyam),
]
: [];
const showTimings = this.detail === 'detailed' && detailed !== null;
return html`
${this.t('Panchang')}
${detailed ? formatDate(this.effectiveLang(), detailed.date) : ''}
${this.limbs(d, detailed).map((l) => this.renderLimbRow(l))}
${
detailed
? html`
${this.renderRow(this.t('Sunrise'), formatTime(this.effectiveLang(), detailed.sunrise))}
${this.renderRow(this.t('Sunset'), formatTime(this.effectiveLang(), detailed.sunset))}
${this.renderRow(this.t('Moonrise'), formatTime(this.effectiveLang(), detailed.moonrise))}
${this.renderRow(this.t('Moonset'), formatTime(this.effectiveLang(), detailed.moonset))}
${this.renderRow(this.t('Moon sign'), this.formatRashi(detailed.moonSign))}
${this.renderRow(this.t('Sun sign'), this.formatRashi(detailed.sunSign))}
${this.renderRow(this.t('Sun nakshatra'), this.formatSunNakshatra(detailed.sunNakshatra))}
${this.renderRow(this.t('Hora'), this.formatHora(detailed.hora))}
`
: basic
? html`
${this.renderRow(this.t('Sun'), this.formatLongitude(basic.sunLongitude))}
${this.renderRow(this.t('Moon'), this.formatLongitude(basic.moonLongitude))}
`
: nothing
}
${detailed ? this.renderTransitions(detailed.transitions) : nothing}
${
showTimings
? html`
${this.t('Auspicious muhurtas')}
${this.renderPeriodRows([
...muhurtas.filter(
(m): m is [string, PanchangTime] => !!m[1],
),
...auspiciousWindows,
])}
${this.t('Inauspicious periods')}
${this.renderPeriodRows([
...inauspicious.filter(
(m): m is [string, PanchangTime] => !!m[1],
),
...inauspiciousWindows,
])}
${this.renderBhadraRow(detailed.bhadra)}
${this.renderPanchakaRow(detailed.panchaka)}
${this.renderBalams(detailed.chandrabalam, detailed.tarabalam)}
`
: nothing
}
`;
}
/**
* The five limbs, each with its own reading. Basic and detailed carry the
* same limb shapes; only detailed carries the vara.
*/
private limbs(d: PanchangData, detailed: Detailed | null): Limb[] {
const t = d.tithi;
const n = d.nakshatra;
const y = d.yoga;
const k = d.karana;
const rows: Limb[] = [
{
label: this.t('Tithi'),
value: name(t?.name, t?.paksha ? `${t.paksha} paksha` : undefined),
meta: meta(
t?.rulingPlanet ? `Ruled by ${t.rulingPlanet}` : undefined,
t?.deity ? `Deity ${t.deity}` : undefined,
t?.element,
typeof t?.percent === 'number'
? `${formatPercent(this.effectiveLang(), t.percent, 0)} elapsed`
: undefined,
),
},
{
label: this.t('Nakshatra'),
value: name(
n?.name,
typeof n?.pada === 'number' ? `pada ${n.pada}` : undefined,
),
meta: meta(
n?.lord ? `Lord ${n.lord}` : undefined,
n?.deity ? `Deity ${n.deity}` : undefined,
n?.symbol ? `Symbol ${n.symbol}` : undefined,
n?.characteristics,
),
},
{
label: this.t('Yoga'),
value: y?.name ?? '',
meta: y?.characteristics ?? '',
},
{
label: this.t('Karana'),
value: name(k?.name, k?.type),
meta: k?.characteristics ?? '',
},
];
if (detailed?.vara) {
rows.push({
label: this.t('Vara'),
value: formatWithSanskrit(
detailed.vara.name,
detailed.vara.sanskritName,
),
meta: detailed.vara.lord ? `Lord ${detailed.vara.lord}` : '',
});
}
return rows.filter((l) => Boolean(l.value));
}
private renderLimbRow(l: Limb) {
return html`
| ${l.label} |
${l.value}${l.meta ? html`${l.meta}` : nothing} |
`;
}
/** One label/value row, skipped when the value is empty. */
private renderRow(
label: string,
value: string,
): TemplateResult | typeof nothing {
if (!value) return nothing;
return html`
| ${label} |
${value} |
`;
}
private renderTransitions(t: Detailed['transitions'] | undefined) {
if (!t) return nothing;
return html`
${this.t('Next transitions')}
${this.renderTransitionRow(this.t('Tithi'), t.tithi)}
${this.renderTransitionRow(this.t('Nakshatra'), t.nakshatra)}
${this.renderTransitionRow(this.t('Yoga'), t.yoga)}
${this.renderTransitionRow(this.t('Karana'), t.karana)}
${this.renderMoonSignRow(t.moonSign)}
`;
}
/** Renders one row per [label, period] pair, dropping any with no range. */
private renderPeriodRows(rows: Array<[string, PanchangTime]>) {
return rows.map(([k, v]) => {
const range = formatTimeRange(this.effectiveLang(), v);
return range
? html`
| ${k} |
${range} |
`
: nothing;
});
}
/** Expands an array of periods into labeled rows, numbering when more than one. */
private expandWindows(
label: string,
windows: PanchangTime[] | undefined,
): Array<[string, PanchangTime]> {
if (!windows || windows.length === 0) return [];
return windows.map((w, i) => [
windows.length > 1 ? `${label} ${i + 1}` : label,
w,
]);
}
/**
* Bhadra (Vishti karana) is avoided for every auspicious act, so "none today"
* is as much of an answer as a window and is stated rather than left blank.
*/
private renderBhadraRow(b: Bhadra | undefined) {
if (!b) return nothing;
const span = this.formatSpan(b.startsAt, b.endsAt);
return html`
| ${this.t('Bhadra (Vishti)')} |
${b.active && span ? span : html`${this.t('None today')}`}
|
`;
}
/**
* Panchaka runs about five days, so the window commonly starts before or ends
* after this date; the dosha type names which of the five it is.
*/
private renderPanchakaRow(p: Panchaka | undefined) {
if (!p) return nothing;
const span = this.formatSpan(p.startsAt, p.endsAt);
return html`
| ${this.t('Panchaka')} |
${
p.active
? html`${p.type ? `${p.type} Panchaka` : 'Panchaka'}${
span ? html`${span}` : nothing
}`
: html`${this.t('None today')}`
}
|
`;
}
/**
* Chandrabalam and Tarabalam are read against the querent's own birth rashi
* and birth nakshatra, so both lists are shown in full rather than summarized.
*/
private renderBalams(c: Chandrabalam | undefined, t: Tarabalam | undefined) {
if (!c && !t) return nothing;
return html`
${this.t('Chandrabalam and Tarabalam')}
${
c
? html`
| ${this.t('Favorable Moon signs')} |
${this.renderChips(c.favorableRashis, 'good')} |
${this.renderRow(this.t('Ashtama Chandra rashi'), c.ashtamaChandraRashi ?? '')}`
: nothing
}
${
t
? html`
| ${this.t('Favorable birth nakshatras')} |
${this.renderChips(t.favorableNakshatras, 'good')} |
| ${this.t('Unfavorable birth nakshatras')} |
${this.renderChips(t.unfavorableNakshatras, 'bad')} |
`
: nothing
}
`;
}
private renderChips(items: string[] | undefined, tone: 'good' | 'bad') {
if (!items?.length)
return html`${this.t('None')}`;
return html`
${items.map((i) => html`${i}`)}
`;
}
private renderTransitionRow(
label: string,
t: { endsAt?: string; next?: string; nextPada?: number } | undefined,
) {
if (!t?.endsAt) return nothing;
const time = formatTime(this.effectiveLang(), t.endsAt);
// One whole sentence per shape, so each language orders the parts its own way.
const body = !t.next
? this.t('ends {{time}}', { time })
: typeof t.nextPada === 'number'
? this.t('ends {{time}} to {{next}} pada {{pada}}', {
time,
next: t.next,
pada: t.nextPada,
})
: this.t('ends {{time}} to {{next}}', { time, next: t.next });
return html`
| ${label} |
${body} |
`;
}
private renderMoonSignRow(m: MoonSignTransition | undefined) {
if (!m?.changesAt) return nothing;
return html`
| ${this.t('Moon sign')} |
${
m.next
? this.t('{{sign}} until {{time}}, then {{next}}', {
sign: m.current ?? '',
time: formatTime(this.effectiveLang(), m.changesAt),
next: m.next,
})
: this.t('{{sign}} until {{time}}', {
sign: m.current ?? '',
time: formatTime(this.effectiveLang(), m.changesAt),
})
}
|
`;
}
/** A window that may end on the following day carries that date so it cannot be misread. */
private formatSpan(
start: string | null | undefined,
end: string | null | undefined,
): string {
const range = formatTimeRange(this.effectiveLang(), {
start: start ?? undefined,
end: end ?? undefined,
});
if (!range || !start || !end) return range;
return start.slice(0, 10) === end.slice(0, 10)
? range
: this.t('{{range}} (ends {{date}})', {
range,
date: formatDate(this.effectiveLang(), end),
});
}
/** "English (Sanskrit)" label for the Moon or Sun rashi at sunrise. */
private formatRashi(r: RashiPlacement | undefined): string {
return formatWithSanskrit(r?.name, r?.sanskritName);
}
/** Sun nakshatra with pada and lord, the form a panchang reader expects. */
private formatSunNakshatra(n: SunNakshatra | undefined): string {
if (!n?.name) return '';
return meta(
n.name,
typeof n.pada === 'number' ? `pada ${n.pada}` : undefined,
n.lord ? `lord ${n.lord}` : undefined,
);
}
/** Current planetary hora with its active window. */
private formatHora(h: Hora | undefined): string {
if (!h?.current) return '';
const range = formatTimeRange(this.effectiveLang(), h);
return range ? `${h.current} (${range})` : h.current;
}
/** Basic mode returns raw sidereal longitudes rather than named placements. */
private formatLongitude(lon: number | undefined): string {
return typeof lon === 'number' ? formatSignPosition(lon) : '';
}
}
declare global {
interface HTMLElementTagNameMap {
'roxy-panchang-table': RoxyPanchangTable;
}
}