@roxyapi/ui 0.1.2 → 0.2.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 (220) hide show
  1. package/AGENTS.md +6 -0
  2. package/README.md +327 -14
  3. package/THEMING.md +24 -7
  4. package/dist/cdn/components/ashtakavarga-grid.js +349 -0
  5. package/dist/cdn/components/ashtakavarga-grid.js.map +7 -0
  6. package/dist/cdn/components/biorhythm-chart.js +15 -22
  7. package/dist/cdn/components/biorhythm-chart.js.map +3 -3
  8. package/dist/cdn/components/choghadiya-grid.js +239 -0
  9. package/dist/cdn/components/choghadiya-grid.js.map +7 -0
  10. package/dist/cdn/components/compatibility-card.js +36 -34
  11. package/dist/cdn/components/compatibility-card.js.map +4 -4
  12. package/dist/cdn/components/dasha-timeline.js +35 -39
  13. package/dist/cdn/components/dasha-timeline.js.map +4 -4
  14. package/dist/cdn/components/data.js +9 -9
  15. package/dist/cdn/components/data.js.map +4 -4
  16. package/dist/cdn/components/divisional-chart.js +279 -0
  17. package/dist/cdn/components/divisional-chart.js.map +7 -0
  18. package/dist/cdn/components/dosha-card.js +49 -49
  19. package/dist/cdn/components/dosha-card.js.map +3 -3
  20. package/dist/cdn/components/endpoint-form.js +47 -28
  21. package/dist/cdn/components/endpoint-form.js.map +4 -4
  22. package/dist/cdn/components/guna-milan.js +66 -24
  23. package/dist/cdn/components/guna-milan.js.map +4 -4
  24. package/dist/cdn/components/hexagram.js +26 -26
  25. package/dist/cdn/components/hexagram.js.map +3 -3
  26. package/dist/cdn/components/horoscope-card.js +47 -40
  27. package/dist/cdn/components/horoscope-card.js.map +4 -4
  28. package/dist/cdn/components/kp-planets-table.js +10 -10
  29. package/dist/cdn/components/kp-planets-table.js.map +4 -4
  30. package/dist/cdn/components/location-search.js +6 -6
  31. package/dist/cdn/components/location-search.js.map +3 -3
  32. package/dist/cdn/components/moon-phase.js +18 -18
  33. package/dist/cdn/components/moon-phase.js.map +4 -4
  34. package/dist/cdn/components/natal-chart.js +240 -24
  35. package/dist/cdn/components/natal-chart.js.map +4 -4
  36. package/dist/cdn/components/numerology-card.js +40 -31
  37. package/dist/cdn/components/numerology-card.js.map +4 -4
  38. package/dist/cdn/components/panchang-table.js +30 -30
  39. package/dist/cdn/components/panchang-table.js.map +4 -4
  40. package/dist/cdn/components/shadbala-table.js +312 -0
  41. package/dist/cdn/components/shadbala-table.js.map +7 -0
  42. package/dist/cdn/components/synastry-chart.js +129 -39
  43. package/dist/cdn/components/synastry-chart.js.map +4 -4
  44. package/dist/cdn/components/tarot-card.js +49 -20
  45. package/dist/cdn/components/tarot-card.js.map +3 -3
  46. package/dist/cdn/components/tarot-spread.js +43 -27
  47. package/dist/cdn/components/tarot-spread.js.map +3 -3
  48. package/dist/cdn/components/transits-table.js +391 -0
  49. package/dist/cdn/components/transits-table.js.map +7 -0
  50. package/dist/cdn/components/vedic-kundli.js +63 -27
  51. package/dist/cdn/components/vedic-kundli.js.map +4 -4
  52. package/dist/cdn/components/yoga-list.js +334 -0
  53. package/dist/cdn/components/yoga-list.js.map +7 -0
  54. package/dist/cdn/roxy-ui.js +2104 -544
  55. package/dist/cdn/roxy-ui.js.map +4 -4
  56. package/dist/components/ashtakavarga-grid.d.ts +26 -0
  57. package/dist/components/ashtakavarga-grid.d.ts.map +1 -0
  58. package/dist/components/ashtakavarga-grid.js +457 -0
  59. package/dist/components/ashtakavarga-grid.js.map +7 -0
  60. package/dist/components/biorhythm-chart.d.ts +2 -46
  61. package/dist/components/biorhythm-chart.d.ts.map +1 -1
  62. package/dist/components/biorhythm-chart.js +24 -23
  63. package/dist/components/biorhythm-chart.js.map +2 -2
  64. package/dist/components/choghadiya-grid.d.ts +19 -0
  65. package/dist/components/choghadiya-grid.d.ts.map +1 -0
  66. package/dist/components/choghadiya-grid.js +304 -0
  67. package/dist/components/choghadiya-grid.js.map +7 -0
  68. package/dist/components/compatibility-card.d.ts +2 -27
  69. package/dist/components/compatibility-card.d.ts.map +1 -1
  70. package/dist/components/compatibility-card.js +50 -29
  71. package/dist/components/compatibility-card.js.map +3 -3
  72. package/dist/components/dasha-timeline.d.ts +2 -31
  73. package/dist/components/dasha-timeline.d.ts.map +1 -1
  74. package/dist/components/dasha-timeline.js +32 -30
  75. package/dist/components/dasha-timeline.js.map +3 -3
  76. package/dist/components/data.d.ts +11 -7
  77. package/dist/components/data.d.ts.map +1 -1
  78. package/dist/components/data.js +16 -6
  79. package/dist/components/data.js.map +3 -3
  80. package/dist/components/divisional-chart.d.ts +20 -0
  81. package/dist/components/divisional-chart.d.ts.map +1 -0
  82. package/dist/components/divisional-chart.js +471 -0
  83. package/dist/components/divisional-chart.js.map +7 -0
  84. package/dist/components/dosha-card.d.ts +2 -16
  85. package/dist/components/dosha-card.d.ts.map +1 -1
  86. package/dist/components/dosha-card.js +45 -43
  87. package/dist/components/dosha-card.js.map +2 -2
  88. package/dist/components/endpoint-form.d.ts +2 -0
  89. package/dist/components/endpoint-form.d.ts.map +1 -1
  90. package/dist/components/endpoint-form.js +71 -11
  91. package/dist/components/endpoint-form.js.map +3 -3
  92. package/dist/components/guna-milan.d.ts +2 -20
  93. package/dist/components/guna-milan.d.ts.map +1 -1
  94. package/dist/components/guna-milan.js +79 -20
  95. package/dist/components/guna-milan.js.map +4 -4
  96. package/dist/components/hexagram.d.ts +3 -27
  97. package/dist/components/hexagram.d.ts.map +1 -1
  98. package/dist/components/hexagram.js +48 -15
  99. package/dist/components/hexagram.js.map +2 -2
  100. package/dist/components/horoscope-card.d.ts +2 -20
  101. package/dist/components/horoscope-card.d.ts.map +1 -1
  102. package/dist/components/horoscope-card.js +54 -18
  103. package/dist/components/horoscope-card.js.map +3 -3
  104. package/dist/components/kp-planets-table.d.ts +2 -21
  105. package/dist/components/kp-planets-table.d.ts.map +1 -1
  106. package/dist/components/kp-planets-table.js +10 -4
  107. package/dist/components/kp-planets-table.js.map +3 -3
  108. package/dist/components/location-search.d.ts +5 -14
  109. package/dist/components/location-search.d.ts.map +1 -1
  110. package/dist/components/location-search.js +45 -5
  111. package/dist/components/location-search.js.map +2 -2
  112. package/dist/components/moon-phase.d.ts +4 -21
  113. package/dist/components/moon-phase.d.ts.map +1 -1
  114. package/dist/components/moon-phase.js +34 -4
  115. package/dist/components/moon-phase.js.map +3 -3
  116. package/dist/components/natal-chart.d.ts +9 -43
  117. package/dist/components/natal-chart.d.ts.map +1 -1
  118. package/dist/components/natal-chart.js +346 -79
  119. package/dist/components/natal-chart.js.map +3 -3
  120. package/dist/components/numerology-card.d.ts +5 -37
  121. package/dist/components/numerology-card.d.ts.map +1 -1
  122. package/dist/components/numerology-card.js +58 -30
  123. package/dist/components/numerology-card.js.map +3 -3
  124. package/dist/components/panchang-table.d.ts +3 -62
  125. package/dist/components/panchang-table.d.ts.map +1 -1
  126. package/dist/components/panchang-table.js +62 -32
  127. package/dist/components/panchang-table.js.map +3 -3
  128. package/dist/components/shadbala-table.d.ts +18 -0
  129. package/dist/components/shadbala-table.d.ts.map +1 -0
  130. package/dist/components/shadbala-table.js +400 -0
  131. package/dist/components/shadbala-table.js.map +7 -0
  132. package/dist/components/synastry-chart.d.ts +9 -28
  133. package/dist/components/synastry-chart.d.ts.map +1 -1
  134. package/dist/components/synastry-chart.js +201 -56
  135. package/dist/components/synastry-chart.js.map +3 -3
  136. package/dist/components/tarot-card.d.ts +5 -29
  137. package/dist/components/tarot-card.d.ts.map +1 -1
  138. package/dist/components/tarot-card.js +59 -20
  139. package/dist/components/tarot-card.js.map +2 -2
  140. package/dist/components/tarot-spread.d.ts +2 -24
  141. package/dist/components/tarot-spread.d.ts.map +1 -1
  142. package/dist/components/tarot-spread.js +39 -13
  143. package/dist/components/tarot-spread.js.map +2 -2
  144. package/dist/components/transits-table.d.ts +21 -0
  145. package/dist/components/transits-table.d.ts.map +1 -0
  146. package/dist/components/transits-table.js +515 -0
  147. package/dist/components/transits-table.js.map +7 -0
  148. package/dist/components/vedic-kundli.d.ts +5 -28
  149. package/dist/components/vedic-kundli.d.ts.map +1 -1
  150. package/dist/components/vedic-kundli.js +147 -83
  151. package/dist/components/vedic-kundli.js.map +3 -3
  152. package/dist/components/yoga-list.d.ts +29 -0
  153. package/dist/components/yoga-list.d.ts.map +1 -0
  154. package/dist/components/yoga-list.js +389 -0
  155. package/dist/components/yoga-list.js.map +7 -0
  156. package/dist/index.cjs +3693 -1180
  157. package/dist/index.cjs.map +4 -4
  158. package/dist/index.d.ts +11 -4
  159. package/dist/index.d.ts.map +1 -1
  160. package/dist/index.js +3709 -1196
  161. package/dist/index.js.map +4 -4
  162. package/dist/manifest.d.ts +43 -0
  163. package/dist/manifest.d.ts.map +1 -0
  164. package/dist/manifest.json +7 -2
  165. package/dist/styles/tokens.css +73 -1
  166. package/dist/tokens/index.d.ts +6 -0
  167. package/dist/tokens/index.d.ts.map +1 -1
  168. package/dist/types/index.d.ts +2 -0
  169. package/dist/types/index.d.ts.map +1 -0
  170. package/dist/types/types.gen.d.ts +27811 -0
  171. package/dist/types/types.gen.d.ts.map +1 -0
  172. package/dist/utils/debounce.d.ts +9 -1
  173. package/dist/utils/debounce.d.ts.map +1 -1
  174. package/dist/utils/format.d.ts +29 -0
  175. package/dist/utils/format.d.ts.map +1 -0
  176. package/dist/utils/kundli-render.d.ts +63 -0
  177. package/dist/utils/kundli-render.d.ts.map +1 -0
  178. package/dist/utils/string.d.ts +14 -0
  179. package/dist/utils/string.d.ts.map +1 -0
  180. package/dist/version.d.ts +2 -0
  181. package/dist/version.d.ts.map +1 -0
  182. package/package.json +7 -1
  183. package/src/components/ashtakavarga-grid.ts +354 -0
  184. package/src/components/biorhythm-chart.ts +39 -84
  185. package/src/components/choghadiya-grid.ts +185 -0
  186. package/src/components/compatibility-card.ts +85 -52
  187. package/src/components/dasha-timeline.ts +55 -73
  188. package/src/components/data.ts +28 -16
  189. package/src/components/divisional-chart.ts +214 -0
  190. package/src/components/dosha-card.ts +72 -68
  191. package/src/components/endpoint-form.ts +80 -18
  192. package/src/components/guna-milan.ts +87 -47
  193. package/src/components/hexagram.ts +53 -43
  194. package/src/components/horoscope-card.ts +59 -43
  195. package/src/components/kp-planets-table.ts +8 -27
  196. package/src/components/location-search.ts +47 -23
  197. package/src/components/moon-phase.ts +28 -25
  198. package/src/components/natal-chart.ts +364 -110
  199. package/src/components/numerology-card.ts +86 -84
  200. package/src/components/panchang-table.ts +40 -78
  201. package/src/components/shadbala-table.ts +286 -0
  202. package/src/components/synastry-chart.ts +213 -97
  203. package/src/components/tarot-card.ts +76 -62
  204. package/src/components/tarot-spread.ts +72 -45
  205. package/src/components/transits-table.ts +350 -0
  206. package/src/components/vedic-kundli.ts +59 -173
  207. package/src/components/yoga-list.ts +328 -0
  208. package/src/index.ts +18 -26
  209. package/src/manifest.ts +340 -0
  210. package/src/styles/tokens.css +73 -1
  211. package/src/tokens/index.ts +14 -0
  212. package/src/types/types.gen.ts +3 -3
  213. package/src/utils/debounce.ts +23 -4
  214. package/src/utils/format.ts +75 -0
  215. package/src/utils/kundli-render.ts +197 -0
  216. package/src/utils/string.ts +23 -0
  217. package/src/version.ts +2 -0
  218. package/dist/utils/motion.d.ts +0 -13
  219. package/dist/utils/motion.d.ts.map +0 -1
  220. package/src/utils/motion.ts +0 -18
@@ -1,58 +1,16 @@
1
1
  import { css, html, LitElement, nothing, svg } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
+ import type {
4
+ GetCriticalDaysResponse,
5
+ GetDailyBiorhythmResponse,
6
+ GetForecastResponse,
7
+ } from '../types/index.js';
3
8
  import { baseStyles } from '../utils/base-styles.js';
4
9
 
5
- interface DailyBiorhythm {
6
- birthDate?: string;
7
- targetDate?: string;
8
- daysSinceBirth?: number;
9
- cycles?: Record<string, number>;
10
- energyRating?: number;
11
- overallPhase?: string;
12
- interpretation?: string;
13
- advice?: string;
14
- criticalAlerts?: string[];
15
- }
16
-
17
- interface BiorhythmDay {
18
- date?: string;
19
- cycles?: Record<string, number>;
20
- energyRating?: number;
21
- }
22
-
23
- interface BiorhythmForecast {
24
- birthDate?: string;
25
- startDate?: string;
26
- endDate?: string;
27
- totalDays?: number;
28
- summary?: {
29
- bestDay?: string;
30
- worstDay?: string;
31
- criticalDayCount?: number;
32
- averageEnergy?: number;
33
- periodAdvice?: string;
34
- };
35
- days?: BiorhythmDay[];
36
- }
37
-
38
- interface CriticalDay {
39
- date?: string;
40
- cycle?: string;
41
- period?: string;
42
- direction?: string;
43
- severity?: string;
44
- advisory?: string;
45
- }
46
-
47
- interface CriticalDays {
48
- birthDate?: string;
49
- startDate?: string;
50
- endDate?: string;
51
- totalCriticalDays?: number;
52
- criticalDays?: CriticalDay[];
53
- }
54
-
55
- type BiorhythmData = DailyBiorhythm & BiorhythmForecast & CriticalDays;
10
+ type BiorhythmData =
11
+ | GetDailyBiorhythmResponse
12
+ | GetForecastResponse
13
+ | GetCriticalDaysResponse;
56
14
 
57
15
  const CYCLE_COLOR: Record<string, string> = {
58
16
  physical: '#dc2626',
@@ -164,18 +122,22 @@ export class RoxyBiorhythmChart extends LitElement {
164
122
  if (!d)
165
123
  return html`<div class="roxy-empty" role="status">No biorhythm data</div>`;
166
124
 
167
- if (this.mode === 'critical-days' && d.criticalDays?.length) {
168
- return this.renderCritical(d);
125
+ if (this.mode === 'critical-days' && 'criticalDays' in d) {
126
+ return this.renderCritical(d as GetCriticalDaysResponse);
169
127
  }
170
- if (this.mode === 'forecast' && d.days?.length) {
171
- return this.renderForecast(d);
128
+ if (this.mode === 'forecast' && 'days' in d) {
129
+ return this.renderForecast(d as GetForecastResponse);
172
130
  }
173
- return this.renderDaily(d);
131
+ return this.renderDaily(d as GetDailyBiorhythmResponse);
174
132
  }
175
133
 
176
- private renderDaily(d: DailyBiorhythm) {
177
- const cycles = d.cycles ?? {};
178
- const entries = Object.entries(cycles);
134
+ private renderDaily(d: GetDailyBiorhythmResponse) {
135
+ const raw = d.quickRead ?? {};
136
+ const entries = Object.entries(raw).map(([cycle, value]) => {
137
+ const v = typeof value === 'number' ? value : 0;
138
+ const normalized = Math.abs(v) > 1 ? v / 100 : v;
139
+ return [cycle, normalized] as const;
140
+ });
179
141
  return html`<section class="wrap" aria-label="Daily biorhythm">
180
142
  <header class="head">
181
143
  <h2 class="title">Biorhythm</h2>
@@ -186,8 +148,7 @@ export class RoxyBiorhythmChart extends LitElement {
186
148
  }
187
149
  </header>
188
150
  <div class="bars" role="list">
189
- ${entries.map(([cycle, value]) => {
190
- const v = typeof value === 'number' ? value : 0;
151
+ ${entries.map(([cycle, v]) => {
191
152
  const pct = ((v + 1) / 2) * 100; // -1..1 -> 0..100
192
153
  const color = CYCLE_COLOR[cycle] ?? 'var(--roxy-accent, #f59e0b)';
193
154
  return html`<div class="bar" role="listitem">
@@ -198,36 +159,32 @@ export class RoxyBiorhythmChart extends LitElement {
198
159
  style="width: ${pct}%; background: ${color}"
199
160
  ></span>
200
161
  </span>
201
- <span class="value">${(v * 100).toFixed(0)}%</span>
162
+ <span class="value">${Math.round(v * 100)}%</span>
202
163
  </div>`;
203
164
  })}
204
165
  </div>
205
- ${d.interpretation ? html`<p class="advice">${d.interpretation}</p>` : nothing}
166
+ ${d.dailyMessage ? html`<p class="advice">${d.dailyMessage}</p>` : nothing}
206
167
  ${d.advice ? html`<p class="advice">${d.advice}</p>` : nothing}
207
- ${
208
- d.criticalAlerts?.length
209
- ? html`<div>
210
- ${d.criticalAlerts.map((a) => html`<p class="alert">${a}</p>`)}
211
- </div>`
212
- : nothing
213
- }
214
168
  </section>`;
215
169
  }
216
170
 
217
- private renderForecast(d: BiorhythmForecast) {
171
+ private renderForecast(d: GetForecastResponse) {
218
172
  const days = d.days ?? [];
219
173
  if (days.length === 0)
220
174
  return html`<div class="roxy-empty" role="status">No forecast</div>`;
221
175
  const w = 600;
222
176
  const h = 160;
223
177
  const xStep = w / Math.max(days.length - 1, 1);
224
- const cycles = Object.keys(days[0]?.cycles ?? {});
178
+ const cycleKeys = [
179
+ 'physical',
180
+ 'emotional',
181
+ 'intellectual',
182
+ 'intuitive',
183
+ ] as const;
225
184
  return html`<section class="wrap" aria-label="Biorhythm forecast">
226
185
  <header class="head">
227
186
  <h2 class="title">Forecast</h2>
228
- <span class="energy"
229
- >${d.startDate ?? ''} - ${d.endDate ?? ''}</span
230
- >
187
+ <span class="energy">${d.startDate} - ${d.endDate}</span>
231
188
  </header>
232
189
  <svg
233
190
  viewBox="0 0 ${w} ${h}"
@@ -243,12 +200,12 @@ export class RoxyBiorhythmChart extends LitElement {
243
200
  stroke="var(--roxy-border, #e4e4e7)"
244
201
  stroke-width="1"
245
202
  />
246
- ${cycles.map((cycle) => {
203
+ ${cycleKeys.map((cycle) => {
247
204
  const points = days
248
205
  .map((day, i) => {
249
- const v = day.cycles?.[cycle] ?? 0;
206
+ const v = day[cycle] ?? 0;
250
207
  const x = i * xStep;
251
- const y = h / 2 - v * (h / 2 - 8);
208
+ const y = h / 2 - (v / 100) * (h / 2 - 8);
252
209
  return `${x.toFixed(2)},${y.toFixed(2)}`;
253
210
  })
254
211
  .join(' ');
@@ -264,18 +221,16 @@ export class RoxyBiorhythmChart extends LitElement {
264
221
  </section>`;
265
222
  }
266
223
 
267
- private renderCritical(d: CriticalDays) {
224
+ private renderCritical(d: GetCriticalDaysResponse) {
268
225
  return html`<section class="wrap" aria-label="Critical days">
269
226
  <header class="head">
270
227
  <h2 class="title">Critical days</h2>
271
- <span class="energy"
272
- >${d.totalCriticalDays ?? d.criticalDays?.length ?? 0} total</span
273
- >
228
+ <span class="energy">${d.totalCriticalDays} total</span>
274
229
  </header>
275
230
  <div>
276
- ${(d.criticalDays ?? []).map(
231
+ ${d.criticalDays.map(
277
232
  (day) => html`<span class="crit"
278
- >${day.date} · ${day.cycle ?? ''} ${day.severity ?? ''}</span
233
+ >${day.date} · ${day.cycle} ${day.severity}</span
279
234
  >`,
280
235
  )}
281
236
  </div>
@@ -0,0 +1,185 @@
1
+ import { css, html, LitElement, nothing } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { PLANET_GLYPH } from '../tokens/index.js';
4
+ import type { GetChoghadiyaResponse } from '../types/index.js';
5
+ import { baseStyles } from '../utils/base-styles.js';
6
+ import { capitalize } from '../utils/string.js';
7
+
8
+ type ChoghadiyaPeriod = GetChoghadiyaResponse['dayChoghadiya'][number];
9
+
10
+ /**
11
+ * Format an ISO 8601 datetime string to a short local time (HH:MM).
12
+ * Falls back to the raw string when parsing fails.
13
+ */
14
+ function fmtTime(iso: string): string {
15
+ try {
16
+ const d = new Date(iso);
17
+ if (Number.isNaN(d.getTime())) return iso;
18
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
19
+ } catch {
20
+ return iso;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Choghadiya muhurta grid. Accepts a GetChoghadiyaResponse and renders
26
+ * 8 daytime and 8 nighttime muhurta tiles in a two-column responsive layout.
27
+ * Good periods are highlighted in green, Bad periods in red.
28
+ */
29
+ @customElement('roxy-choghadiya-grid')
30
+ export class RoxyChoghadiyaGrid extends LitElement {
31
+ static styles = [
32
+ baseStyles,
33
+ css`
34
+ .wrap {
35
+ display: grid;
36
+ gap: var(--roxy-space-md, 1rem);
37
+ }
38
+ .header {
39
+ display: grid;
40
+ gap: var(--roxy-space-xs, 0.25rem);
41
+ }
42
+ .title {
43
+ font-size: var(--roxy-text-lg, 1.125rem);
44
+ font-weight: var(--roxy-weight-bold, 600);
45
+ margin: 0;
46
+ }
47
+ .subtitle {
48
+ font-size: var(--roxy-text-sm, 0.875rem);
49
+ color: var(--roxy-muted, #71717a);
50
+ margin: 0;
51
+ }
52
+ .cho-grid {
53
+ display: grid;
54
+ grid-template-columns: 1fr;
55
+ gap: var(--roxy-space-md, 1rem);
56
+ }
57
+ @media (min-width: 720px) {
58
+ .cho-grid {
59
+ grid-template-columns: 1fr 1fr;
60
+ }
61
+ }
62
+ .period-col {
63
+ display: grid;
64
+ gap: var(--roxy-space-xs, 0.25rem);
65
+ }
66
+ .period-heading {
67
+ font-size: var(--roxy-text-base, 1rem);
68
+ font-weight: var(--roxy-weight-bold, 600);
69
+ margin: 0 0 var(--roxy-space-xs, 0.25rem);
70
+ color: var(--roxy-fg, #0a0a0a);
71
+ }
72
+ .cho-tile {
73
+ display: grid;
74
+ grid-template-columns: 1fr auto;
75
+ align-items: center;
76
+ gap: 0.25em 0.75em;
77
+ padding: 0.55em 0.85em;
78
+ border: 1px solid var(--roxy-border, #e4e4e7);
79
+ border-radius: var(--roxy-radius-md, 8px);
80
+ }
81
+ .cho-tile.good {
82
+ background: color-mix(in srgb, var(--roxy-success, #22c55e) 18%, transparent);
83
+ border-color: color-mix(in srgb, var(--roxy-success, #22c55e) 45%, transparent);
84
+ color: var(--roxy-fg, #0a0a0a);
85
+ }
86
+ .cho-tile.bad {
87
+ background: color-mix(in srgb, var(--roxy-danger, #ef4444) 18%, transparent);
88
+ border-color: color-mix(in srgb, var(--roxy-danger, #ef4444) 45%, transparent);
89
+ color: var(--roxy-fg, #0a0a0a);
90
+ }
91
+ .cho-tile.neutral {
92
+ background: transparent;
93
+ color: var(--roxy-fg, #0a0a0a);
94
+ }
95
+ .tile-name {
96
+ font-size: var(--roxy-text-base, 1rem);
97
+ font-weight: var(--roxy-weight-bold, 600);
98
+ grid-column: 1;
99
+ }
100
+ .tile-time {
101
+ font-size: var(--roxy-text-xs, 0.75rem);
102
+ opacity: 0.8;
103
+ white-space: nowrap;
104
+ grid-column: 2;
105
+ grid-row: 1 / 3;
106
+ text-align: right;
107
+ align-self: center;
108
+ }
109
+ .tile-lord {
110
+ font-size: var(--roxy-text-sm, 0.875rem);
111
+ opacity: 0.85;
112
+ grid-column: 1;
113
+ display: flex;
114
+ align-items: center;
115
+ gap: 0.25em;
116
+ }
117
+ `,
118
+ ];
119
+
120
+ @property({ attribute: false })
121
+ data: GetChoghadiyaResponse | null = null;
122
+
123
+ private renderTile(period: ChoghadiyaPeriod) {
124
+ const effectClass =
125
+ period.effect === 'Good'
126
+ ? 'good'
127
+ : period.effect === 'Bad'
128
+ ? 'bad'
129
+ : 'neutral';
130
+ const lordGlyph = PLANET_GLYPH[capitalize(period.lord)] ?? '';
131
+ const timeRange = `${fmtTime(period.start)} - ${fmtTime(period.end)}`;
132
+ return html`<div class="cho-tile ${effectClass}" role="listitem">
133
+ <span class="tile-name">${period.name}</span>
134
+ <span class="tile-time" aria-label="Time range">${timeRange}</span>
135
+ <span class="tile-lord">
136
+ ${lordGlyph ? html`<span aria-hidden="true">${lordGlyph}</span>` : nothing}
137
+ ${period.lord}
138
+ </span>
139
+ </div>`;
140
+ }
141
+
142
+ render() {
143
+ if (!this.data)
144
+ return html`<div class="roxy-empty" role="status">No choghadiya data</div>`;
145
+
146
+ const { date, dayChoghadiya, nightChoghadiya } = this.data;
147
+
148
+ return html`<div class="wrap">
149
+ <div class="header">
150
+ <h2 class="title">Choghadiya</h2>
151
+ ${date ? html`<p class="subtitle">${date}</p>` : nothing}
152
+ </div>
153
+
154
+ <div class="cho-grid">
155
+ <section class="period-col" aria-label="Day muhurta periods">
156
+ <h3 class="period-heading">Day</h3>
157
+ <div role="list" aria-label="Daytime choghadiya">
158
+ ${
159
+ dayChoghadiya && dayChoghadiya.length > 0
160
+ ? dayChoghadiya.map((p) => this.renderTile(p))
161
+ : html`<p class="roxy-empty" role="status">No daytime periods</p>`
162
+ }
163
+ </div>
164
+ </section>
165
+
166
+ <section class="period-col" aria-label="Night muhurta periods">
167
+ <h3 class="period-heading">Night</h3>
168
+ <div role="list" aria-label="Nighttime choghadiya">
169
+ ${
170
+ nightChoghadiya && nightChoghadiya.length > 0
171
+ ? nightChoghadiya.map((p) => this.renderTile(p))
172
+ : html`<p class="roxy-empty" role="status">No nighttime periods</p>`
173
+ }
174
+ </div>
175
+ </section>
176
+ </div>
177
+ </div>`;
178
+ }
179
+ }
180
+
181
+ declare global {
182
+ interface HTMLElementTagNameMap {
183
+ 'roxy-choghadiya-grid': RoxyChoghadiyaGrid;
184
+ }
185
+ }
@@ -1,26 +1,17 @@
1
1
  import { css, html, LitElement, nothing } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
+ import type {
4
+ CalculateBioCompatibilityResponse,
5
+ CalculateCompatibilityResponse,
6
+ CalculateNumCompatibilityResponse,
7
+ } from '../types/index.js';
3
8
  import { baseStyles } from '../utils/base-styles.js';
9
+ import { formatNumber } from '../utils/format.js';
4
10
 
5
- interface CompatibilityData {
6
- overallScore?: number;
7
- score?: number;
8
- rating?: string;
9
- relationshipArchetype?: string;
10
- advice?: string;
11
- summary?: string;
12
- categoryScores?: Record<string, number>;
13
- categoryBreakdown?: Record<string, number>;
14
- emotional?: number;
15
- communication?: number;
16
- romance?: number;
17
- strengths?: string[];
18
- challenges?: string[];
19
- keyAspects?: string[];
20
- elementBalance?: Record<string, number>;
21
- person1?: { name?: string; sign?: string; lifePath?: number };
22
- person2?: { name?: string; sign?: string; lifePath?: number };
23
- }
11
+ type CompatibilityData =
12
+ | CalculateCompatibilityResponse
13
+ | CalculateNumCompatibilityResponse
14
+ | CalculateBioCompatibilityResponse;
24
15
 
25
16
  /**
26
17
  * Cross-domain compatibility card. Renders /astrology/compatibility-score,
@@ -94,7 +85,7 @@ export class RoxyCompatibilityCard extends LitElement {
94
85
  }
95
86
 
96
87
  .archetype {
97
- color: var(--roxy-info, #0284c7);
88
+ color: var(--roxy-accent-fg, #b45309);
98
89
  font-weight: var(--roxy-weight-bold, 600);
99
90
  }
100
91
 
@@ -126,23 +117,49 @@ export class RoxyCompatibilityCard extends LitElement {
126
117
  private getBreakdown(): Record<string, number> {
127
118
  const d = this.data;
128
119
  if (!d) return {};
129
- if (d.categoryScores) return d.categoryScores;
130
- if (d.categoryBreakdown) return d.categoryBreakdown;
131
- const inferred: Record<string, number> = {};
132
- if (typeof d.emotional === 'number') inferred.emotional = d.emotional;
133
- if (typeof d.communication === 'number')
134
- inferred.communication = d.communication;
135
- if (typeof d.romance === 'number') inferred.romance = d.romance;
136
- if (d.elementBalance) Object.assign(inferred, d.elementBalance);
137
- return inferred;
120
+ if ('categories' in d && d.categories) {
121
+ const out: Record<string, number> = {};
122
+ for (const [k, v] of Object.entries(d.categories)) {
123
+ if (typeof v === 'number' && Number.isFinite(v)) out[k] = v;
124
+ }
125
+ return out;
126
+ }
127
+ return {};
138
128
  }
139
129
 
140
130
  render() {
141
131
  const d = this.data;
142
132
  if (!d)
143
133
  return html`<div class="roxy-empty" role="status">No compatibility data</div>`;
144
- const score = d.overallScore ?? d.score;
134
+
135
+ const score = d.overallScore;
145
136
  const breakdown = this.getBreakdown();
137
+ const rating =
138
+ 'rating' in d
139
+ ? (d as CalculateNumCompatibilityResponse).rating
140
+ : undefined;
141
+ const archetype =
142
+ 'archetype' in d
143
+ ? (d as CalculateCompatibilityResponse).archetype
144
+ : undefined;
145
+ const advice =
146
+ 'advice' in d
147
+ ? (d as CalculateNumCompatibilityResponse).advice
148
+ : undefined;
149
+ const summary =
150
+ 'summary' in d
151
+ ? (d as CalculateCompatibilityResponse).summary
152
+ : undefined;
153
+ const interpretation =
154
+ 'interpretation' in d
155
+ ? (d as CalculateCompatibilityResponse).interpretation
156
+ : undefined;
157
+ const strengths = 'strengths' in d ? d.strengths : undefined;
158
+ const challenges = 'challenges' in d ? d.challenges : undefined;
159
+ const keyAspects =
160
+ 'keyAspects' in d
161
+ ? (d as CalculateCompatibilityResponse).keyAspects
162
+ : undefined;
146
163
 
147
164
  return html`<article
148
165
  class="card"
@@ -153,10 +170,10 @@ export class RoxyCompatibilityCard extends LitElement {
153
170
  <div>
154
171
  ${
155
172
  typeof score === 'number'
156
- ? html`<div class="score">${score}</div>`
173
+ ? html`<div class="score">${formatNumber(score, 0)}</div>`
157
174
  : nothing
158
175
  }
159
- ${d.rating ? html`<div class="rating">${d.rating}</div>` : nothing}
176
+ ${rating ? html`<div class="rating">${rating}</div>` : nothing}
160
177
  </div>
161
178
  </div>
162
179
 
@@ -169,50 +186,42 @@ export class RoxyCompatibilityCard extends LitElement {
169
186
  <span class="bar"
170
187
  ><span style="width: ${Math.max(0, Math.min(100, v))}%"></span
171
188
  ></span>
172
- <span>${v}</span>
189
+ <span>${formatNumber(v, 0)}</span>
173
190
  </div>`,
174
191
  )}
175
192
  </div>`
176
193
  : nothing
177
194
  }
178
195
  ${
179
- d.relationshipArchetype
196
+ archetype
180
197
  ? html`<p>
181
- <span class="archetype">${d.relationshipArchetype}</span>
198
+ <span class="archetype">${archetype.label}</span>
199
+ ${archetype.description ? html` · ${archetype.description}` : nothing}
182
200
  </p>`
183
201
  : nothing
184
202
  }
185
- ${d.summary ? html`<p>${d.summary}</p>` : nothing}
186
- ${d.advice ? html`<p>${d.advice}</p>` : nothing}
203
+ ${summary ? html`<p>${summary}</p>` : nothing}
204
+ ${interpretation && !summary ? html`<p>${interpretation}</p>` : nothing}
205
+ ${advice ? html`<p>${advice}</p>` : nothing}
187
206
  ${
188
- (d.strengths?.length ?? 0) > 0 || (d.challenges?.length ?? 0) > 0
207
+ (strengths?.length ?? 0) > 0 || (challenges?.length ?? 0) > 0
189
208
  ? html`<div class="lists">
190
209
  ${
191
- d.strengths?.length
210
+ strengths?.length
192
211
  ? html`<div>
193
212
  <h3>Strengths</h3>
194
213
  <ul>
195
- ${d.strengths.map((s) => html`<li>${s}</li>`)}
214
+ ${strengths.map((s) => html`<li>${s}</li>`)}
196
215
  </ul>
197
216
  </div>`
198
217
  : nothing
199
218
  }
200
219
  ${
201
- d.challenges?.length
220
+ challenges?.length
202
221
  ? html`<div>
203
222
  <h3>Challenges</h3>
204
223
  <ul>
205
- ${d.challenges.map((s) => html`<li>${s}</li>`)}
206
- </ul>
207
- </div>`
208
- : nothing
209
- }
210
- ${
211
- d.keyAspects?.length
212
- ? html`<div>
213
- <h3>Key aspects</h3>
214
- <ul>
215
- ${d.keyAspects.map((s) => html`<li>${s}</li>`)}
224
+ ${challenges.map((s) => html`<li>${s}</li>`)}
216
225
  </ul>
217
226
  </div>`
218
227
  : nothing
@@ -220,10 +229,34 @@ export class RoxyCompatibilityCard extends LitElement {
220
229
  </div>`
221
230
  : nothing
222
231
  }
232
+ ${
233
+ keyAspects?.length
234
+ ? html`<div>
235
+ <h3 style="margin: 0 0 0.25rem; font-size: var(--roxy-text-xs); color: var(--roxy-muted); text-transform: uppercase; letter-spacing: 0.06em;">Key aspects</h3>
236
+ <ul style="margin: 0; padding-left: 1rem; font-size: var(--roxy-text-sm);">
237
+ ${keyAspects.slice(0, 6).map((a) => html`<li>${formatAspect(a)}</li>`)}
238
+ </ul>
239
+ </div>`
240
+ : nothing
241
+ }
223
242
  </article>`;
224
243
  }
225
244
  }
226
245
 
246
+ type KeyAspect = CalculateCompatibilityResponse extends {
247
+ keyAspects: Array<infer T>;
248
+ }
249
+ ? T
250
+ : never;
251
+
252
+ function formatAspect(a: KeyAspect): string {
253
+ const aspect = a.type.toLowerCase().replace(/_/g, '-');
254
+ const orb =
255
+ typeof a.orb === 'number' ? ` (orb ${formatNumber(a.orb, 1)}°)` : '';
256
+ const head = [a.planet1, aspect, a.planet2].filter(Boolean).join(' ');
257
+ return a.description ? `${head}${orb} · ${a.description}` : `${head}${orb}`;
258
+ }
259
+
227
260
  declare global {
228
261
  interface HTMLElementTagNameMap {
229
262
  'roxy-compatibility-card': RoxyCompatibilityCard;