@roxyapi/ui 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/AGENTS.md +18 -1
  2. package/README.md +31 -2
  3. package/THEMING.md +1 -1
  4. package/dist/cdn/components/ashtakavarga-grid.js +1 -1
  5. package/dist/cdn/components/ashtakavarga-grid.js.map +2 -2
  6. package/dist/cdn/components/biorhythm-chart.js +3 -3
  7. package/dist/cdn/components/biorhythm-chart.js.map +2 -2
  8. package/dist/cdn/components/bodygraph.js +54 -0
  9. package/dist/cdn/components/bodygraph.js.map +7 -0
  10. package/dist/cdn/components/choghadiya-grid.js +1 -1
  11. package/dist/cdn/components/choghadiya-grid.js.map +2 -2
  12. package/dist/cdn/components/compatibility-card.js +1 -1
  13. package/dist/cdn/components/compatibility-card.js.map +2 -2
  14. package/dist/cdn/components/dasha-timeline.js +2 -2
  15. package/dist/cdn/components/dasha-timeline.js.map +2 -2
  16. package/dist/cdn/components/divisional-chart.js +2 -2
  17. package/dist/cdn/components/divisional-chart.js.map +2 -2
  18. package/dist/cdn/components/endpoint-form.js +2 -2
  19. package/dist/cdn/components/endpoint-form.js.map +2 -2
  20. package/dist/cdn/components/forecast-timeline.js +45 -0
  21. package/dist/cdn/components/forecast-timeline.js.map +7 -0
  22. package/dist/cdn/components/guna-milan.js +1 -1
  23. package/dist/cdn/components/guna-milan.js.map +2 -2
  24. package/dist/cdn/components/hexagram.js +1 -1
  25. package/dist/cdn/components/hexagram.js.map +2 -2
  26. package/dist/cdn/components/horoscope-card.js +1 -1
  27. package/dist/cdn/components/horoscope-card.js.map +2 -2
  28. package/dist/cdn/components/kp-chart.js +1 -1
  29. package/dist/cdn/components/kp-chart.js.map +2 -2
  30. package/dist/cdn/components/kp-ruling-planets.js +1 -1
  31. package/dist/cdn/components/kp-ruling-planets.js.map +2 -2
  32. package/dist/cdn/components/location-search.js +1 -1
  33. package/dist/cdn/components/location-search.js.map +2 -2
  34. package/dist/cdn/components/nakshatra-card.js +1 -1
  35. package/dist/cdn/components/nakshatra-card.js.map +2 -2
  36. package/dist/cdn/components/natal-chart.js +4 -4
  37. package/dist/cdn/components/natal-chart.js.map +2 -2
  38. package/dist/cdn/components/numerology-card.js +2 -2
  39. package/dist/cdn/components/numerology-card.js.map +2 -2
  40. package/dist/cdn/components/panchang-table.js +3 -3
  41. package/dist/cdn/components/panchang-table.js.map +3 -3
  42. package/dist/cdn/components/shadbala-table.js +1 -1
  43. package/dist/cdn/components/shadbala-table.js.map +2 -2
  44. package/dist/cdn/components/synastry-chart.js +3 -3
  45. package/dist/cdn/components/synastry-chart.js.map +2 -2
  46. package/dist/cdn/components/transits-table.js +2 -2
  47. package/dist/cdn/components/transits-table.js.map +2 -2
  48. package/dist/cdn/components/vedic-kundli.js +10 -10
  49. package/dist/cdn/components/vedic-kundli.js.map +2 -2
  50. package/dist/cdn/components/vedic-planets-table.js +3 -3
  51. package/dist/cdn/components/vedic-planets-table.js.map +4 -4
  52. package/dist/cdn/components/yoga-list.js +2 -2
  53. package/dist/cdn/components/yoga-list.js.map +2 -2
  54. package/dist/cdn/roxy-ui.js +55 -46
  55. package/dist/cdn/roxy-ui.js.map +4 -4
  56. package/dist/components/ashtakavarga-grid.js +1 -1
  57. package/dist/components/ashtakavarga-grid.js.map +3 -3
  58. package/dist/components/biorhythm-chart.js +1 -1
  59. package/dist/components/biorhythm-chart.js.map +2 -2
  60. package/dist/components/bodygraph.d.ts +27 -0
  61. package/dist/components/bodygraph.d.ts.map +1 -0
  62. package/dist/components/bodygraph.js +11 -0
  63. package/dist/components/bodygraph.js.map +7 -0
  64. package/dist/components/choghadiya-grid.js +1 -1
  65. package/dist/components/choghadiya-grid.js.map +2 -2
  66. package/dist/components/compatibility-card.js +1 -1
  67. package/dist/components/compatibility-card.js.map +2 -2
  68. package/dist/components/dasha-timeline.js +1 -1
  69. package/dist/components/dasha-timeline.js.map +2 -2
  70. package/dist/components/divisional-chart.js +2 -2
  71. package/dist/components/divisional-chart.js.map +2 -2
  72. package/dist/components/endpoint-form.js +1 -1
  73. package/dist/components/endpoint-form.js.map +2 -2
  74. package/dist/components/forecast-timeline.d.ts +38 -0
  75. package/dist/components/forecast-timeline.d.ts.map +1 -0
  76. package/dist/components/forecast-timeline.js +2 -0
  77. package/dist/components/forecast-timeline.js.map +7 -0
  78. package/dist/components/guna-milan.js +1 -1
  79. package/dist/components/guna-milan.js.map +2 -2
  80. package/dist/components/hexagram.js +1 -1
  81. package/dist/components/hexagram.js.map +2 -2
  82. package/dist/components/horoscope-card.js +1 -1
  83. package/dist/components/horoscope-card.js.map +2 -2
  84. package/dist/components/kp-chart.js +1 -1
  85. package/dist/components/kp-chart.js.map +2 -2
  86. package/dist/components/kp-ruling-planets.js +1 -1
  87. package/dist/components/kp-ruling-planets.js.map +2 -2
  88. package/dist/components/location-search.js +1 -1
  89. package/dist/components/location-search.js.map +2 -2
  90. package/dist/components/nakshatra-card.js +1 -1
  91. package/dist/components/nakshatra-card.js.map +2 -2
  92. package/dist/components/natal-chart.js +2 -2
  93. package/dist/components/natal-chart.js.map +2 -2
  94. package/dist/components/numerology-card.js +1 -1
  95. package/dist/components/numerology-card.js.map +2 -2
  96. package/dist/components/panchang-table.d.ts +22 -1
  97. package/dist/components/panchang-table.d.ts.map +1 -1
  98. package/dist/components/panchang-table.js +1 -1
  99. package/dist/components/panchang-table.js.map +3 -3
  100. package/dist/components/shadbala-table.js +1 -1
  101. package/dist/components/shadbala-table.js.map +2 -2
  102. package/dist/components/synastry-chart.js +4 -4
  103. package/dist/components/synastry-chart.js.map +2 -2
  104. package/dist/components/transits-table.js +1 -1
  105. package/dist/components/transits-table.js.map +2 -2
  106. package/dist/components/vedic-kundli.js +2 -2
  107. package/dist/components/vedic-kundli.js.map +2 -2
  108. package/dist/components/vedic-planets-table.d.ts +14 -0
  109. package/dist/components/vedic-planets-table.d.ts.map +1 -1
  110. package/dist/components/vedic-planets-table.js +1 -1
  111. package/dist/components/vedic-planets-table.js.map +4 -4
  112. package/dist/components/yoga-list.js +1 -1
  113. package/dist/components/yoga-list.js.map +2 -2
  114. package/dist/index.cjs +53 -44
  115. package/dist/index.cjs.map +4 -4
  116. package/dist/index.d.ts +2 -0
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +52 -43
  119. package/dist/index.js.map +4 -4
  120. package/dist/manifest.d.ts.map +1 -1
  121. package/dist/manifest.json +2 -0
  122. package/dist/styles/tokens.css +4 -4
  123. package/dist/types/index.d.ts +1 -1
  124. package/dist/types/index.d.ts.map +1 -1
  125. package/dist/types/types.gen.d.ts +6058 -1431
  126. package/dist/types/types.gen.d.ts.map +1 -1
  127. package/dist/utils/bodygraph-render.d.ts +105 -0
  128. package/dist/utils/bodygraph-render.d.ts.map +1 -0
  129. package/dist/version.d.ts +1 -1
  130. package/package.json +1 -1
  131. package/src/components/ashtakavarga-grid.ts +1 -1
  132. package/src/components/biorhythm-chart.ts +1 -1
  133. package/src/components/bodygraph.ts +390 -0
  134. package/src/components/choghadiya-grid.ts +1 -1
  135. package/src/components/compatibility-card.ts +2 -2
  136. package/src/components/dasha-timeline.ts +3 -3
  137. package/src/components/endpoint-form.ts +2 -2
  138. package/src/components/forecast-timeline.ts +336 -0
  139. package/src/components/guna-milan.ts +1 -1
  140. package/src/components/hexagram.ts +3 -3
  141. package/src/components/horoscope-card.ts +1 -1
  142. package/src/components/kp-chart.ts +1 -1
  143. package/src/components/kp-ruling-planets.ts +1 -1
  144. package/src/components/location-search.ts +1 -1
  145. package/src/components/nakshatra-card.ts +1 -1
  146. package/src/components/natal-chart.ts +5 -5
  147. package/src/components/numerology-card.ts +2 -2
  148. package/src/components/panchang-table.ts +112 -18
  149. package/src/components/shadbala-table.ts +1 -1
  150. package/src/components/synastry-chart.ts +4 -4
  151. package/src/components/transits-table.ts +1 -1
  152. package/src/components/vedic-planets-table.ts +221 -3
  153. package/src/components/yoga-list.ts +1 -1
  154. package/src/index.ts +4 -0
  155. package/src/manifest.ts +26 -0
  156. package/src/styles/tokens.css +4 -4
  157. package/src/types/index.ts +1 -1
  158. package/src/types/types.gen.ts +5999 -1287
  159. package/src/utils/bodygraph-render.ts +641 -0
  160. package/src/utils/kundli-styles.ts +2 -2
  161. package/src/utils/tablist.ts +1 -1
  162. package/src/version.ts +1 -1
@@ -0,0 +1,336 @@
1
+ import { css, html, LitElement, nothing } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { ASPECT_SYMBOL } from '../tokens/index.js';
4
+ import type { GenerateTimelineResponse } from '../types/index.js';
5
+ import { baseStyles } from '../utils/base-styles.js';
6
+ import { ASPECT_CLASS, formatDate, formatNumber } from '../utils/format.js';
7
+ import { MarkupDataController } from '../utils/markup-data.js';
8
+ import { capitalize, humanize } from '../utils/string.js';
9
+
10
+ type ForecastEvent = GenerateTimelineResponse['events'][number];
11
+ type ForecastDomain = ForecastEvent['domain'];
12
+
13
+ /**
14
+ * Display label per forecast domain. Keyed by the spec `domain` union so a new
15
+ * domain in the spec surfaces as a typecheck failure here rather than an
16
+ * unlabeled swatch.
17
+ */
18
+ const DOMAIN_LABEL: Record<ForecastDomain, string> = {
19
+ western: 'Western',
20
+ vedic: 'Vedic',
21
+ biorhythm: 'Biorhythm',
22
+ };
23
+
24
+ const DOMAIN_ORDER: readonly ForecastDomain[] = [
25
+ 'western',
26
+ 'vedic',
27
+ 'biorhythm',
28
+ ];
29
+
30
+ /**
31
+ * Cross-domain forecast timeline. Pass `data` from /forecast/timeline. Events
32
+ * are grouped by calendar date down a vertical axis, each colored by its domain
33
+ * and weighted by significance (0-100), showing the event type, body, target,
34
+ * aspect, and description. The visual language matches the dasha timeline: a
35
+ * left rail of dates, a significance bar per event, and a colored marker for
36
+ * the domain.
37
+ *
38
+ * Theming flows through `--roxy-*` custom properties on `:host`.
39
+ */
40
+ @customElement('roxy-forecast-timeline')
41
+ export class RoxyForecastTimeline extends LitElement {
42
+ static styles = [
43
+ baseStyles,
44
+ css`
45
+ .wrap {
46
+ display: grid;
47
+ gap: var(--roxy-space-md, 1rem);
48
+ }
49
+ .head {
50
+ display: flex;
51
+ justify-content: space-between;
52
+ align-items: baseline;
53
+ flex-wrap: wrap;
54
+ gap: var(--roxy-space-sm, 0.5rem);
55
+ }
56
+ .title {
57
+ margin: 0;
58
+ font-size: var(--roxy-text-lg, 1.125rem);
59
+ font-weight: var(--roxy-weight-bold, 600);
60
+ }
61
+ .range {
62
+ color: var(--roxy-muted, #71717a);
63
+ font-size: var(--roxy-text-sm, 0.875rem);
64
+ font-variant-numeric: tabular-nums;
65
+ }
66
+ .legend {
67
+ display: flex;
68
+ flex-wrap: wrap;
69
+ gap: var(--roxy-space-md, 1rem);
70
+ font-size: var(--roxy-text-xs, 0.75rem);
71
+ color: var(--roxy-muted, #71717a);
72
+ }
73
+ .legend .swatch {
74
+ display: inline-block;
75
+ width: 10px;
76
+ height: 10px;
77
+ border-radius: 50%;
78
+ margin-right: 4px;
79
+ vertical-align: middle;
80
+ }
81
+ .swatch-western {
82
+ background: var(--roxy-accent, #f59e0b);
83
+ }
84
+ .swatch-vedic {
85
+ background: var(--roxy-info, #2563eb);
86
+ }
87
+ .swatch-biorhythm {
88
+ background: var(--roxy-success, #16a34a);
89
+ }
90
+
91
+ .day {
92
+ display: grid;
93
+ grid-template-columns: 4.5rem 1fr;
94
+ gap: var(--roxy-space-md, 1rem);
95
+ padding-block: var(--roxy-space-sm, 0.5rem);
96
+ border-top: 1px solid var(--roxy-border, #e4e4e7);
97
+ }
98
+ .day:first-of-type {
99
+ border-top: none;
100
+ }
101
+ .day-date {
102
+ color: var(--roxy-muted, #71717a);
103
+ font-size: var(--roxy-text-xs, 0.75rem);
104
+ font-variant-numeric: tabular-nums;
105
+ padding-top: 2px;
106
+ }
107
+ .events {
108
+ display: grid;
109
+ gap: var(--roxy-space-sm, 0.5rem);
110
+ }
111
+ .event {
112
+ display: grid;
113
+ grid-template-columns: 0.6rem 1fr;
114
+ gap: var(--roxy-space-sm, 0.5rem);
115
+ align-items: start;
116
+ }
117
+ .dot {
118
+ width: 0.6rem;
119
+ height: 0.6rem;
120
+ border-radius: 50%;
121
+ margin-top: 4px;
122
+ }
123
+ .dot-western {
124
+ background: var(--roxy-accent, #f59e0b);
125
+ }
126
+ .dot-vedic {
127
+ background: var(--roxy-info, #2563eb);
128
+ }
129
+ .dot-biorhythm {
130
+ background: var(--roxy-success, #16a34a);
131
+ }
132
+ .event-body {
133
+ min-width: 0;
134
+ }
135
+ .event-line {
136
+ font-size: var(--roxy-text-sm, 0.875rem);
137
+ color: var(--roxy-fg, #0a0a0a);
138
+ }
139
+ .event-line .aspect-trine,
140
+ .event-line .aspect-sextile {
141
+ color: var(--roxy-success-fg, #166534);
142
+ }
143
+ .event-line .aspect-square,
144
+ .event-line .aspect-opposition {
145
+ color: var(--roxy-danger-fg, #991b1b);
146
+ }
147
+ .event-line .aspect-conjunction {
148
+ color: var(--roxy-accent-ink, #b45309);
149
+ }
150
+ .event-line .kind {
151
+ margin-left: 0.35em;
152
+ color: var(--roxy-muted, #71717a);
153
+ font-size: var(--roxy-text-xs, 0.75rem);
154
+ text-transform: uppercase;
155
+ letter-spacing: 0.04em;
156
+ }
157
+ .event-desc {
158
+ color: var(--roxy-muted, #71717a);
159
+ font-size: var(--roxy-text-xs, 0.75rem);
160
+ margin: 2px 0 0;
161
+ }
162
+ .sig {
163
+ display: flex;
164
+ align-items: center;
165
+ gap: 0.4rem;
166
+ margin-top: 4px;
167
+ }
168
+ .sig-track {
169
+ flex: 1;
170
+ max-width: 8rem;
171
+ height: 4px;
172
+ background: var(--roxy-border, #e4e4e7);
173
+ border-radius: var(--roxy-radius-full, 9999px);
174
+ overflow: hidden;
175
+ }
176
+ .sig-fill {
177
+ display: block;
178
+ height: 100%;
179
+ border-radius: var(--roxy-radius-full, 9999px);
180
+ }
181
+ .sig-fill-western {
182
+ background: var(--roxy-accent, #f59e0b);
183
+ }
184
+ .sig-fill-vedic {
185
+ background: var(--roxy-info, #2563eb);
186
+ }
187
+ .sig-fill-biorhythm {
188
+ background: var(--roxy-success, #16a34a);
189
+ }
190
+ .sig-val {
191
+ color: var(--roxy-muted, #71717a);
192
+ font-size: var(--roxy-text-xs, 0.75rem);
193
+ font-variant-numeric: tabular-nums;
194
+ }
195
+ `,
196
+ ];
197
+
198
+ constructor() {
199
+ super();
200
+ // Enables hydrating `data` from a direct-child
201
+ // <script type="application/json" class="roxy-data"> for server-rendered
202
+ // and cached consumers. The JavaScript `data` property still wins.
203
+ new MarkupDataController(this);
204
+ }
205
+
206
+ @property({ attribute: false })
207
+ data: GenerateTimelineResponse | null = null;
208
+
209
+ render() {
210
+ const d = this.data;
211
+ if (!d)
212
+ return html`<div class="roxy-empty" role="status">No forecast data</div>`;
213
+
214
+ const events = d.events ?? [];
215
+ const grouped = this.groupByDate(events);
216
+ const present = DOMAIN_ORDER.filter((dom) =>
217
+ events.some((e) => e.domain === dom),
218
+ );
219
+
220
+ return html`<div class="wrap" aria-label="Forecast timeline">
221
+ <header class="head">
222
+ <h2 class="title">Forecast timeline</h2>
223
+ ${
224
+ d.startDate && d.endDate
225
+ ? html`<div class="range">
226
+ ${formatDate(d.startDate)} - ${formatDate(d.endDate)} · ${d.count ?? events.length} events
227
+ </div>`
228
+ : nothing
229
+ }
230
+ </header>
231
+ ${
232
+ present.length
233
+ ? html`<div class="legend">
234
+ ${present.map(
235
+ (dom) =>
236
+ html`<span><span class="swatch swatch-${dom}"></span>${DOMAIN_LABEL[dom]}</span>`,
237
+ )}
238
+ </div>`
239
+ : nothing
240
+ }
241
+ ${
242
+ grouped.length
243
+ ? html`<div class="days" role="list">
244
+ ${grouped.map(([date, dayEvents]) => this.renderDay(date, dayEvents))}
245
+ </div>`
246
+ : html`<p class="roxy-empty" role="status">No events in this window</p>`
247
+ }
248
+ </div>`;
249
+ }
250
+
251
+ /** Group events by their calendar date, preserving response order. */
252
+ private groupByDate(
253
+ events: ForecastEvent[],
254
+ ): Array<[string, ForecastEvent[]]> {
255
+ const map = new Map<string, ForecastEvent[]>();
256
+ for (const e of events) {
257
+ const key = e.date ?? '';
258
+ const list = map.get(key) ?? [];
259
+ list.push(e);
260
+ map.set(key, list);
261
+ }
262
+ return [...map.entries()];
263
+ }
264
+
265
+ private renderDay(date: string, events: ForecastEvent[]) {
266
+ return html`<div class="day" role="listitem">
267
+ <div class="day-date">${formatDate(date)}</div>
268
+ <div class="events">
269
+ ${events.map((e) => this.renderEvent(e))}
270
+ </div>
271
+ </div>`;
272
+ }
273
+
274
+ private renderEvent(e: ForecastEvent) {
275
+ const sig = typeof e.significance === 'number' ? e.significance : 0;
276
+ const width = Math.max(0, Math.min(100, sig));
277
+ return html`<div class="event">
278
+ <span class="dot dot-${e.domain}" aria-hidden="true"></span>
279
+ <div class="event-body">
280
+ <div class="event-line">${this.renderHeadline(e)}</div>
281
+ ${e.description ? html`<p class="event-desc">${e.description}</p>` : nothing}
282
+ <div class="sig" title="Significance ${width} of 100">
283
+ <span class="sig-track">
284
+ <span class="sig-fill sig-fill-${e.domain}" style="width: ${width}%"></span>
285
+ </span>
286
+ <span class="sig-val">${width}</span>
287
+ </div>
288
+ </div>
289
+ </div>`;
290
+ }
291
+
292
+ /**
293
+ * Compact event headline built from the spec fields. Transit aspects read
294
+ * "body aspect target"; ingresses, stations, eclipses, dasha changes, and
295
+ * critical days fall back to "body" plus the qualifier the spec carries for
296
+ * that type (station direction, eclipse kind). The aspect symbol is colored
297
+ * by nature, matching the chart aspect encoding.
298
+ */
299
+ private renderHeadline(e: ForecastEvent) {
300
+ const body = e.body ? capitalize(e.body) : '';
301
+ const target = e.target ? capitalize(e.target) : '';
302
+ const aspect = e.aspect ? e.aspect.toLowerCase() : '';
303
+ const aspectClass = ASPECT_CLASS[aspect] ?? '';
304
+ const aspectSym = aspect ? (ASPECT_SYMBOL[aspect] ?? aspect) : '';
305
+ const orb = typeof e.orb === 'number' ? formatNumber(e.orb, 1) : '';
306
+ const qualifier = this.typeQualifier(e);
307
+
308
+ if (aspect && target) {
309
+ return html`<strong>${body}</strong>
310
+ <span class=${aspectClass}>${aspectSym}</span>
311
+ <strong>${target}</strong>${orb ? html` <span class="kind">orb ${orb}°</span>` : nothing}`;
312
+ }
313
+ return html`<strong>${body || humanize(e.type ?? '')}</strong>${
314
+ qualifier ? html` <span class="kind">${qualifier}</span>` : nothing
315
+ }`;
316
+ }
317
+
318
+ /** Type-specific qualifier text from the optional spec fields. */
319
+ private typeQualifier(e: ForecastEvent): string {
320
+ if (e.type === 'sign-ingress' && e.target)
321
+ return `enters ${capitalize(e.target)}`;
322
+ if (e.type === 'retrograde-station' && e.station) return e.station;
323
+ if (e.type === 'eclipse')
324
+ return [e.kind, 'eclipse'].filter(Boolean).join(' ');
325
+ if (e.type === 'critical-day') return 'critical day';
326
+ if (e.type === 'dasha-change' && e.target)
327
+ return `dasha ${capitalize(e.target)}`;
328
+ return humanize(e.type ?? '');
329
+ }
330
+ }
331
+
332
+ declare global {
333
+ interface HTMLElementTagNameMap {
334
+ 'roxy-forecast-timeline': RoxyForecastTimeline;
335
+ }
336
+ }
@@ -51,7 +51,7 @@ export class RoxyGunaMilan extends LitElement {
51
51
  .total {
52
52
  font-size: 2.25rem;
53
53
  font-weight: var(--roxy-weight-bold, 600);
54
- color: var(--roxy-accent-fg, #b45309);
54
+ color: var(--roxy-accent-ink, #b45309);
55
55
  font-variant-numeric: tabular-nums;
56
56
  line-height: 1;
57
57
  }
@@ -53,7 +53,7 @@ export class RoxyHexagram extends LitElement {
53
53
  .symbol {
54
54
  font-size: 3rem;
55
55
  line-height: 1;
56
- color: var(--roxy-accent-fg, #b45309);
56
+ color: var(--roxy-accent-ink, #b45309);
57
57
  }
58
58
  .lines {
59
59
  display: grid;
@@ -105,7 +105,7 @@ export class RoxyHexagram extends LitElement {
105
105
  }
106
106
  .tri-glyph {
107
107
  font-size: var(--roxy-text-xl, 1.5rem);
108
- color: var(--roxy-accent-fg, #b45309);
108
+ color: var(--roxy-accent-ink, #b45309);
109
109
  margin-right: 4px;
110
110
  vertical-align: middle;
111
111
  }
@@ -131,7 +131,7 @@ export class RoxyHexagram extends LitElement {
131
131
  margin-top: var(--roxy-space-md, 1rem);
132
132
  padding-top: var(--roxy-space-md, 1rem);
133
133
  border-top: 1px solid var(--roxy-border, #e4e4e7);
134
- color: var(--roxy-accent-fg, #b45309);
134
+ color: var(--roxy-accent-ink, #b45309);
135
135
  font-size: var(--roxy-text-sm, 0.875rem);
136
136
  }
137
137
  `,
@@ -42,7 +42,7 @@ export class RoxyHoroscopeCard extends LitElement {
42
42
 
43
43
  .glyph {
44
44
  font-size: 2.25rem;
45
- color: var(--roxy-accent-fg, #b45309);
45
+ color: var(--roxy-accent-ink, #b45309);
46
46
  line-height: 1;
47
47
  }
48
48
 
@@ -76,7 +76,7 @@ export class RoxyKpChart extends LitElement {
76
76
  font-family: inherit;
77
77
  }
78
78
  .tab[aria-selected='true'] {
79
- color: var(--roxy-accent-fg, #b45309);
79
+ color: var(--roxy-accent-ink, #b45309);
80
80
  border-bottom-color: var(--roxy-accent, #f59e0b);
81
81
  font-weight: var(--roxy-weight-bold, 600);
82
82
  }
@@ -91,7 +91,7 @@ export class RoxyKpRulingPlanets extends LitElement {
91
91
  }
92
92
  .rp .rank {
93
93
  font-size: var(--roxy-text-xs, 0.75rem);
94
- color: var(--roxy-accent-fg, #b45309);
94
+ color: var(--roxy-accent-ink, #b45309);
95
95
  }
96
96
  table {
97
97
  width: 100%;
@@ -50,7 +50,7 @@ export class RoxyLocationSearch extends LitElement {
50
50
  input:focus {
51
51
  outline: 2px solid var(--roxy-ring, rgba(245, 158, 11, 0.4));
52
52
  outline-offset: 2px;
53
- border-color: var(--roxy-accent-fg, #b45309);
53
+ border-color: var(--roxy-accent-ink, #b45309);
54
54
  }
55
55
  .spinner {
56
56
  position: absolute;
@@ -35,7 +35,7 @@ export class RoxyNakshatraCard extends LitElement {
35
35
  font-weight: var(--roxy-weight-bold, 600);
36
36
  }
37
37
  .number {
38
- color: var(--roxy-accent-fg, #b45309);
38
+ color: var(--roxy-accent-ink, #b45309);
39
39
  font-size: var(--roxy-text-sm, 0.875rem);
40
40
  font-weight: var(--roxy-weight-bold, 600);
41
41
  }
@@ -159,7 +159,7 @@ export class RoxyNatalChart extends LitElement {
159
159
  stroke: var(--roxy-danger, #dc2626);
160
160
  }
161
161
  .aspect-conjunction {
162
- stroke: var(--roxy-accent-fg, #b45309);
162
+ stroke: var(--roxy-accent-ink, #b45309);
163
163
  }
164
164
  .aspect-other {
165
165
  stroke: var(--roxy-muted, #71717a);
@@ -167,14 +167,14 @@ export class RoxyNatalChart extends LitElement {
167
167
  }
168
168
 
169
169
  .angle-marker {
170
- fill: var(--roxy-accent-fg, #b45309);
170
+ fill: var(--roxy-accent-ink, #b45309);
171
171
  font-size: 10px;
172
172
  font-weight: 700;
173
173
  font-family: var(--roxy-font-sans);
174
174
  letter-spacing: 0.04em;
175
175
  }
176
176
  .angle-tick {
177
- stroke: var(--roxy-accent-fg, #b45309);
177
+ stroke: var(--roxy-accent-ink, #b45309);
178
178
  stroke-width: 1.5;
179
179
  }
180
180
 
@@ -234,7 +234,7 @@ export class RoxyNatalChart extends LitElement {
234
234
  color: var(--roxy-danger, #dc2626);
235
235
  }
236
236
  table.aspect-grid td.aspect-conjunction .asp {
237
- color: var(--roxy-accent-fg, #b45309);
237
+ color: var(--roxy-accent-ink, #b45309);
238
238
  }
239
239
  table.aspect-grid td.aspect-other .asp {
240
240
  color: var(--roxy-muted, #71717a);
@@ -363,7 +363,7 @@ export class RoxyNatalChart extends LitElement {
363
363
  padding: 1px 8px;
364
364
  border-radius: 9999px;
365
365
  background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 14%, transparent);
366
- color: var(--roxy-accent-fg, #b45309);
366
+ color: var(--roxy-accent-ink, #b45309);
367
367
  font-size: var(--roxy-text-xs, 0.75rem);
368
368
  }
369
369
  `,
@@ -44,7 +44,7 @@ export class RoxyNumerologyCard extends LitElement {
44
44
  font-size: 4rem;
45
45
  line-height: 1;
46
46
  font-weight: var(--roxy-weight-bold, 600);
47
- color: var(--roxy-accent-fg, #b45309);
47
+ color: var(--roxy-accent-ink, #b45309);
48
48
  font-variant-numeric: tabular-nums;
49
49
  }
50
50
  .label {
@@ -106,7 +106,7 @@ export class RoxyNumerologyCard extends LitElement {
106
106
  text-transform: capitalize;
107
107
  }
108
108
  .cores .item strong {
109
- color: var(--roxy-accent-fg, #b45309);
109
+ color: var(--roxy-accent-ink, #b45309);
110
110
  font-variant-numeric: tabular-nums;
111
111
  font-size: var(--roxy-text-base, 1rem);
112
112
  font-weight: var(--roxy-weight-bold, 600);
@@ -11,7 +11,18 @@ import { MarkupDataController } from '../utils/markup-data.js';
11
11
  type PanchangData = GetBasicPanchangResponse | GetDetailedPanchangResponse;
12
12
  type PanchangTime = GetDetailedPanchangResponse['rahuKaal'];
13
13
 
14
- /** Panchang table for /vedic-astrology/panchang/{basic,detailed}. */
14
+ /**
15
+ * Panchang table for /vedic-astrology/panchang/{basic,detailed}.
16
+ *
17
+ * @remarks
18
+ * The main grid lists the five limbs (tithi, nakshatra, yoga, karana, vara),
19
+ * sun and moon timings, and, in detailed mode, the sunrise placements a reader
20
+ * scans first: Moon rashi, Sun rashi, Sun nakshatra, and the current hora. The
21
+ * detailed mode then groups every timed window into two sections, auspicious
22
+ * (the fixed muhurtas plus each Amrit Kalam) and inauspicious (Rahu Kaal,
23
+ * Yamaganda, Gulika, plus each Dur Muhurta and Varjyam), so a consumer can act
24
+ * on timing without parsing the raw response.
25
+ */
15
26
  @customElement('roxy-panchang-table')
16
27
  export class RoxyPanchangTable extends LitElement {
17
28
  static styles = [
@@ -105,6 +116,15 @@ export class RoxyPanchangTable extends LitElement {
105
116
  ];
106
117
  if (detailed) fivefold.push(['Vara', this.formatPart(detailed.vara)]);
107
118
 
119
+ const placements: Array<[string, string]> = detailed
120
+ ? [
121
+ ['Moon sign', this.formatRashi(detailed.moonSign)],
122
+ ['Sun sign', this.formatRashi(detailed.sunSign)],
123
+ ['Sun nakshatra', this.formatSunNakshatra(detailed.sunNakshatra)],
124
+ ['Hora', this.formatHora(detailed.hora)],
125
+ ].filter((row): row is [string, string] => Boolean(row[1]))
126
+ : [];
127
+
108
128
  const muhurtas: Array<[string, PanchangTime | undefined]> = detailed
109
129
  ? [
110
130
  ['Brahma Muhurta', detailed.brahmaMuhurta],
@@ -117,6 +137,10 @@ export class RoxyPanchangTable extends LitElement {
117
137
  ]
118
138
  : [];
119
139
 
140
+ const auspiciousWindows: Array<[string, PanchangTime]> = detailed
141
+ ? this.expandWindows('Amrit Kalam', detailed.amritKalam)
142
+ : [];
143
+
120
144
  const inauspicious: Array<[string, PanchangTime | undefined]> = detailed
121
145
  ? [
122
146
  ['Rahu Kaal', detailed.rahuKaal],
@@ -125,6 +149,13 @@ export class RoxyPanchangTable extends LitElement {
125
149
  ]
126
150
  : [];
127
151
 
152
+ const inauspiciousWindows: Array<[string, PanchangTime]> = detailed
153
+ ? [
154
+ ...this.expandWindows('Dur Muhurta', detailed.durMuhurta),
155
+ ...this.expandWindows('Varjyam', detailed.varjyam),
156
+ ]
157
+ : [];
158
+
128
159
  const transitions =
129
160
  detailed && 'transitions' in detailed ? detailed.transitions : undefined;
130
161
 
@@ -173,6 +204,12 @@ export class RoxyPanchangTable extends LitElement {
173
204
  </tr>`
174
205
  : nothing
175
206
  }
207
+ ${placements.map(
208
+ ([k, v]) => html`<tr>
209
+ <th>${k}</th>
210
+ <td>${v}</td>
211
+ </tr>`,
212
+ )}
176
213
  </tbody>
177
214
  </table>
178
215
  ${
@@ -192,32 +229,33 @@ export class RoxyPanchangTable extends LitElement {
192
229
  }
193
230
  ${
194
231
  this.detail === 'detailed' &&
195
- (muhurtas.some((m) => !!m[1]) || inauspicious.some((m) => !!m[1]))
232
+ (
233
+ muhurtas.some((m) => !!m[1]) ||
234
+ auspiciousWindows.length > 0 ||
235
+ inauspicious.some((m) => !!m[1]) ||
236
+ inauspiciousWindows.length > 0
237
+ )
196
238
  ? html`
197
239
  <div class="section">Auspicious muhurtas</div>
198
240
  <table>
199
241
  <tbody>
200
- ${muhurtas
201
- .filter(([, v]) => !!v)
202
- .map(
203
- ([k, v]) => html`<tr>
204
- <th>${k}</th>
205
- <td>${formatTimeRange(v)}</td>
206
- </tr>`,
207
- )}
242
+ ${this.renderPeriodRows([
243
+ ...muhurtas.filter(
244
+ (m): m is [string, PanchangTime] => !!m[1],
245
+ ),
246
+ ...auspiciousWindows,
247
+ ])}
208
248
  </tbody>
209
249
  </table>
210
250
  <div class="section">Inauspicious periods</div>
211
251
  <table>
212
252
  <tbody>
213
- ${inauspicious
214
- .filter(([, v]) => !!v)
215
- .map(
216
- ([k, v]) => html`<tr>
217
- <th>${k}</th>
218
- <td>${formatTimeRange(v)}</td>
219
- </tr>`,
220
- )}
253
+ ${this.renderPeriodRows([
254
+ ...inauspicious.filter(
255
+ (m): m is [string, PanchangTime] => !!m[1],
256
+ ),
257
+ ...inauspiciousWindows,
258
+ ])}
221
259
  </tbody>
222
260
  </table>
223
261
  `
@@ -226,6 +264,31 @@ export class RoxyPanchangTable extends LitElement {
226
264
  </div>`;
227
265
  }
228
266
 
267
+ /** Renders one row per [label, period] pair, dropping any with no range. */
268
+ private renderPeriodRows(rows: Array<[string, PanchangTime]>) {
269
+ return rows.map(([k, v]) => {
270
+ const range = formatTimeRange(v);
271
+ return range
272
+ ? html`<tr>
273
+ <th>${k}</th>
274
+ <td>${range}</td>
275
+ </tr>`
276
+ : nothing;
277
+ });
278
+ }
279
+
280
+ /** Expands an array of periods into labeled rows, numbering when more than one. */
281
+ private expandWindows(
282
+ label: string,
283
+ windows: PanchangTime[] | undefined,
284
+ ): Array<[string, PanchangTime]> {
285
+ if (!windows || windows.length === 0) return [];
286
+ return windows.map((w, i) => [
287
+ windows.length > 1 ? `${label} ${i + 1}` : label,
288
+ w,
289
+ ]);
290
+ }
291
+
229
292
  private renderTransitionRow(
230
293
  label: string,
231
294
  t: { endsAt?: string; next?: string } | undefined,
@@ -260,8 +323,39 @@ export class RoxyPanchangTable extends LitElement {
260
323
  }
261
324
  return String(v);
262
325
  }
326
+
327
+ /** "English (Sanskrit)" label for the Moon or Sun rashi at sunrise. */
328
+ private formatRashi(r: RashiPlacement | undefined): string {
329
+ if (!r?.name) return '';
330
+ return r.sanskritName && r.sanskritName !== r.name
331
+ ? `${r.name} (${r.sanskritName})`
332
+ : r.name;
333
+ }
334
+
335
+ /** Sun nakshatra with pada and lord, the form a panchang reader expects. */
336
+ private formatSunNakshatra(n: SunNakshatra | undefined): string {
337
+ if (!n?.name) return '';
338
+ const parts = [
339
+ n.name,
340
+ typeof n.pada === 'number' ? `pada ${n.pada}` : '',
341
+ n.lord ? `· ${n.lord}` : '',
342
+ ].filter(Boolean);
343
+ return parts.join(' ');
344
+ }
345
+
346
+ /** Current planetary hora with its active window. */
347
+ private formatHora(h: Hora | undefined): string {
348
+ if (!h?.current) return '';
349
+ const range = formatTimeRange(h);
350
+ return range ? `${h.current} (${range})` : h.current;
351
+ }
263
352
  }
264
353
 
354
+ type PanchangPlacements = GetDetailedPanchangResponse;
355
+ type RashiPlacement = PanchangPlacements['moonSign'];
356
+ type SunNakshatra = PanchangPlacements['sunNakshatra'];
357
+ type Hora = PanchangPlacements['hora'];
358
+
265
359
  declare global {
266
360
  interface HTMLElementTagNameMap {
267
361
  'roxy-panchang-table': RoxyPanchangTable;
@@ -149,7 +149,7 @@ export class RoxyShadbalaTable extends LitElement {
149
149
 
150
150
  .rank-badge {
151
151
  font-size: var(--roxy-text-xs, 0.75rem);
152
- color: var(--roxy-accent-fg, #b45309);
152
+ color: var(--roxy-accent-ink, #b45309);
153
153
  font-weight: var(--roxy-weight-bold, 600);
154
154
  }
155
155