{
static endpoints = kpFinanceCardEndpoints;
static styles = [
baseStyles,
frameCaptionStyles,
css`
.wrap {
background: var(--roxy-surface, #fff);
color: var(--roxy-fg, #0a0a0a);
border: 1px solid var(--roxy-border, #e4e4e7);
border-radius: var(--roxy-radius-md, 8px);
box-shadow: var(--roxy-shadow-sm);
display: grid;
grid-template-columns: minmax(0, 1fr);
overflow: hidden;
}
.head {
padding: var(--roxy-space-md, 1rem);
display: grid;
gap: var(--roxy-space-sm, 0.5rem);
}
.title-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--roxy-space-sm, 0.5rem);
flex-wrap: wrap;
}
.title {
margin: 0;
font-size: var(--roxy-text-lg, 1.125rem);
font-weight: var(--roxy-weight-bold, 600);
}
.when {
color: var(--roxy-muted, #71717a);
font-size: var(--roxy-text-xs, 0.75rem);
}
/* The band word leads and the number follows it: the band is the
* reading of the day and the score is the evidence for it. */
.verdict-row {
display: flex;
align-items: center;
gap: var(--roxy-space-sm, 0.5rem);
flex-wrap: wrap;
}
.verdict {
font-size: var(--roxy-text-lg, 1.125rem);
font-weight: var(--roxy-weight-bold, 600);
padding: 0.15rem 0.6rem;
border-radius: var(--roxy-radius-full, 9999px);
background: color-mix(in srgb, var(--roxy-muted, #71717a) 14%, transparent);
}
.verdict.strong,
.verdict.favourable {
background: color-mix(in srgb, var(--roxy-success, #16a34a) 16%, transparent);
color: var(--_success-fg);
}
.verdict.mixed,
.verdict.caution {
background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 18%, transparent);
color: var(--roxy-accent-ink, #b45309);
}
.verdict.unfavourable {
background: color-mix(in srgb, var(--roxy-danger, #dc2626) 14%, transparent);
color: var(--_danger-fg);
}
.evidence {
color: var(--roxy-secondary, #475569);
font-size: var(--roxy-text-sm, 0.875rem);
font-variant-numeric: tabular-nums;
}
.block {
padding: var(--roxy-space-md, 1rem);
border-top: 1px solid var(--roxy-border, #e4e4e7);
display: grid;
gap: var(--roxy-space-sm, 0.5rem);
}
.block h3 {
margin: 0;
font-size: var(--roxy-text-xs, 0.75rem);
font-weight: var(--roxy-weight-bold, 600);
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--roxy-muted, #71717a);
}
.block-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--roxy-space-sm, 0.5rem);
flex-wrap: wrap;
}
.facts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
gap: var(--roxy-space-sm, 0.5rem);
margin: 0;
}
.fact {
display: grid;
gap: 2px;
}
.fact dt {
color: var(--roxy-muted, #71717a);
font-size: var(--roxy-text-xs, 0.75rem);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.fact dd {
margin: 0;
font-size: var(--roxy-text-sm, 0.875rem);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.chip {
font-size: var(--roxy-text-xs, 0.75rem);
padding: 1px 8px;
border-radius: var(--roxy-radius-full, 9999px);
border: 1px solid var(--roxy-border, #e4e4e7);
color: var(--roxy-secondary, #475569);
}
.scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
min-width: 0;
}
table {
width: 100%;
border-collapse: collapse;
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);
white-space: nowrap;
}
th {
color: var(--roxy-muted, #71717a);
font-size: var(--roxy-text-xs, 0.75rem);
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: var(--roxy-weight-bold, 600);
}
td.num {
font-variant-numeric: tabular-nums;
}
/* A list of names wraps in place: the table has two columns, so a
* wrapped cell reads better than a scroll box on a phone. */
td.names {
white-space: normal;
}
.glyph {
margin-right: 0.35em;
color: var(--roxy-muted, #71717a);
}
.retro {
margin-left: 0.3em;
color: var(--roxy-muted, #71717a);
font-size: var(--roxy-text-xs, 0.75rem);
}
/* One chip per verdict, tinted by what it means, the same three tints
* the band above uses so a reader learns one scale for the card. */
.state {
font-size: var(--roxy-text-xs, 0.75rem);
padding: 1px 8px;
border-radius: var(--roxy-radius-full, 9999px);
background: color-mix(in srgb, var(--roxy-muted, #71717a) 14%, transparent);
}
.state.favourable {
background: color-mix(in srgb, var(--roxy-success, #16a34a) 16%, transparent);
color: var(--_success-fg);
}
.state.mixed {
background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 16%, transparent);
color: var(--roxy-accent-ink, #b45309);
}
.state.unfavourable {
background: color-mix(in srgb, var(--roxy-danger, #dc2626) 14%, transparent);
color: var(--_danger-fg);
}
.window {
display: grid;
gap: 1px;
padding: 0.3rem 0.6rem;
border: 1px solid var(--roxy-border, #e4e4e7);
border-radius: var(--roxy-radius-md, 8px);
font-size: var(--roxy-text-sm, 0.875rem);
}
.window small {
color: var(--roxy-muted, #71717a);
font-size: var(--roxy-text-xs, 0.75rem);
font-variant-numeric: tabular-nums;
}
.window.best {
border-color: color-mix(in srgb, var(--roxy-success, #16a34a) 45%, transparent);
}
.window.worst {
border-color: color-mix(in srgb, var(--roxy-danger, #dc2626) 35%, transparent);
}
`,
];
protected renderData(d: Finance) {
if (!d.layers) return this.renderEmpty();
return html`
${this.renderHead(d)}
${this.renderSignificators(d)}
${this.renderLayer('cusps', d.layers.cusps, this.renderCuspRows(d.layers.cusps.rows))}
${this.renderLayer('dasha', d.layers.dasha, this.renderDashaRows(d.layers.dasha.rows))}
${this.renderLayer(
'rulingPlanets',
d.layers.rulingPlanets,
this.renderRulingRows(d.layers.rulingPlanets.rows),
)}
${this.renderLayer(
'moonWindows',
d.layers.moonWindows,
this.renderWindowRows(d.layers.moonWindows.rows),
)}
${this.renderWindows(d)}
${renderFrameCaption(
this.effectiveLang(),
{ ayanamsa: d.ayanamsa, ayanamsaDegrees: d.ayanamsaDegrees },
this.translator,
)}
`;
}
/** The band, the score behind it, the day and the moment it was read at, and the two house sets. */
private renderHead(d: Finance) {
const locale = this.effectiveLang();
return html`
${this.t('KP daily finance')}
${formatDateTime(locale, d.date)}
${this.t(BAND_LABEL[d.band])}
${formatNumber(locale, d.score, 1)}
- ${this.t('Reading moment')}
- ${formatDateTime(locale, d.readingAt)}
- ${this.t('Gain houses')}
- ${formatList(locale, d.houses.gain.map(String))}
- ${this.t('Loss houses')}
- ${formatList(locale, d.houses.loss.map(String))}
`;
}
/**
* The two planet sets every verdict is judged against, and the per-house evidence they were built from.
*
* @remarks
* A house lists its four tier significators strongest tier first, and a planet that qualifies at more than one tier is listed once per tier. It is named once here, at its strongest, because the row answers which planets signify the house and the two sets above are built from exactly that.
*/
private renderSignificators(d: Finance) {
const s = d.significators;
const locale = this.effectiveLang();
const chips = (planets: readonly string[]) =>
html`
${planets.map((p) => html`${this.glyphName(p)}`)}
`;
return html`
${this.t('Significators')}
- ${this.t('Gain')}
- ${chips(s.gain)}
- ${this.t('Loss')}
- ${chips(s.loss)}
${
s.byHouse.length
? html``
: nothing
}
`;
}
/** One layer: its name, the weight it carries and the score it contributed, over the rows it was scored from. */
private renderLayer(
key: LayerKey,
layer: Layers[LayerKey],
rows: TemplateResult,
) {
const locale = this.effectiveLang();
return html`
${this.t(LAYER_LABEL[key])}
${this.t('Weight')} ${formatPercent(locale, layer.weight, 0)} ยท ${this.t('Score')}
${formatNumber(locale, layer.score, 1)}
${rows}
`;
}
private renderCuspRows(rows: readonly CuspRow[]) {
return html`
| ${this.t('House')} |
${this.t('Sub lord')} |
${this.t('Star lord')} |
${this.t('Sign lord')} |
${this.t('Verdict')} |
${this.t('Score')} |
${rows.map(
(r) => html`
| ${r.house} ${this.groupChip(r.inverted ? 'loss' : 'gain')} |
${this.glyphName(r.subLord)} |
${this.glyphName(r.starLord)} |
${this.glyphName(r.signLord)} |
${this.verdictChip(r.verdict)} |
${formatNumber(this.effectiveLang(), r.score, 0)} |
`,
)}
`;
}
private renderDashaRows(rows: readonly DashaRow[]) {
const locale = this.effectiveLang();
return html`
| ${this.t('Level')} |
${this.t('Lord')} |
${this.t('Start')} |
${this.t('End')} |
${this.t('Verdict')} |
${this.t('Score')} |
${this.t('Weight')} |
${rows.map(
(r) => html`
| ${this.t(LEVEL_LABEL[r.level])} |
${this.glyphName(r.lord, r.retrograde)} |
${formatDateTime(locale, r.startDate)} |
${formatDateTime(locale, r.endDate)} |
${this.verdictChip(r.verdict)} |
${formatNumber(locale, r.score, 0)} |
${formatPercent(locale, r.weight, 0)} |
`,
)}
`;
}
private renderRulingRows(rows: readonly RulingRow[]) {
return html`
| ${this.t('Planet')} |
${this.t('Verdict')} |
${this.t('Score')} |
${rows.map(
(r) => html`
| ${this.glyphName(r.planet, r.retrograde)} |
${this.verdictChip(r.verdict)} |
${formatNumber(this.effectiveLang(), r.score, 0)} |
`,
)}
`;
}
private renderWindowRows(rows: readonly WindowRow[]) {
const locale = this.effectiveLang();
return html`
| ${this.t('Start')} |
${this.t('End')} |
${this.t('Sub lord')} |
${this.t('Verdict')} |
${this.t('Score')} |
${rows.map(
(r) => html`
| ${formatTime(locale, r.from)} |
${formatTime(locale, r.to)} |
${this.glyphName(r.subLord)} |
${this.verdictChip(r.verdict)} |
${formatNumber(locale, r.score, 0)} |
`,
)}
`;
}
/**
* The favourable window of the day and every unfavourable one, lifted out of the Moon table so a reader planning the day finds them without reading nine rows.
*
* @remarks
* `bestWindow` is null on a day with no favourable window, which is a fact about the day and not a missing value, so the block prints the worst windows alone rather than an empty slot.
*/
private renderWindows(d: Finance) {
const locale = this.effectiveLang();
const best = d.bestWindow;
const worst = d.worstWindows ?? [];
if (!best && !worst.length) return nothing;
const window = (w: WindowRow, kind: 'best' | 'worst') =>
html`
${this.glyphName(w.subLord)}
${formatTimeRange(locale, { start: w.from, end: w.to })}
`;
return html`
${
best
? html`
- ${this.t('Best window')}
${window(best, 'best')}
`
: nothing
}
${
worst.length
? html`
- ${this.t('Worst windows')}
${worst.map((w) => window(w, 'worst'))}
`
: nothing
}
`;
}
/**
* A planet with its glyph in front, and the retrograde mark after it where the row says so.
*
* @remarks
* The mark is the symbol the ephemeris and transit tables draw, with the catalogued word as its accessible name rather than printed: printed after a name, an adjective has to agree with that name in the languages that inflect it, and a symbol does not.
*/
private glyphName(name: string, retrograde = false) {
const glyph = planetGlyph(name);
return html`${glyph ? html`${glyph}` : nothing}${name}${
retrograde
? html`℞`
: nothing
}`;
}
private verdictChip(verdict: Verdict) {
return html`${this.t(VERDICT_LABEL[verdict])}`;
}
/** Which of the two sets a house belongs to, beside its number. */
private groupChip(group: 'gain' | 'loss') {
return html`${this.t(GROUP_LABEL[group])}`;
}
}
declare global {
interface HTMLElementTagNameMap {
'roxy-kp-finance-card': RoxyKpFinanceCard;
}
}