@roxyapi/ui 0.1.1 → 0.1.3

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 (169) hide show
  1. package/AGENTS.md +2 -2
  2. package/LICENSE +21 -0
  3. package/README.md +505 -0
  4. package/THEMING.md +24 -7
  5. package/dist/cdn/components/biorhythm-chart.js +15 -22
  6. package/dist/cdn/components/biorhythm-chart.js.map +3 -3
  7. package/dist/cdn/components/compatibility-card.js +36 -34
  8. package/dist/cdn/components/compatibility-card.js.map +4 -4
  9. package/dist/cdn/components/dasha-timeline.js +35 -39
  10. package/dist/cdn/components/dasha-timeline.js.map +4 -4
  11. package/dist/cdn/components/data.js +6 -6
  12. package/dist/cdn/components/data.js.map +3 -3
  13. package/dist/cdn/components/dosha-card.js +13 -13
  14. package/dist/cdn/components/dosha-card.js.map +2 -2
  15. package/dist/cdn/components/endpoint-form.js +47 -28
  16. package/dist/cdn/components/endpoint-form.js.map +3 -3
  17. package/dist/cdn/components/guna-milan.js +18 -18
  18. package/dist/cdn/components/guna-milan.js.map +4 -4
  19. package/dist/cdn/components/hexagram.js +26 -26
  20. package/dist/cdn/components/hexagram.js.map +3 -3
  21. package/dist/cdn/components/horoscope-card.js +38 -38
  22. package/dist/cdn/components/horoscope-card.js.map +3 -3
  23. package/dist/cdn/components/kp-planets-table.js +10 -10
  24. package/dist/cdn/components/kp-planets-table.js.map +4 -4
  25. package/dist/cdn/components/location-search.js +6 -6
  26. package/dist/cdn/components/location-search.js.map +3 -3
  27. package/dist/cdn/components/moon-phase.js +21 -21
  28. package/dist/cdn/components/moon-phase.js.map +4 -4
  29. package/dist/cdn/components/natal-chart.js +61 -19
  30. package/dist/cdn/components/natal-chart.js.map +4 -4
  31. package/dist/cdn/components/numerology-card.js +40 -31
  32. package/dist/cdn/components/numerology-card.js.map +3 -3
  33. package/dist/cdn/components/panchang-table.js +25 -25
  34. package/dist/cdn/components/panchang-table.js.map +4 -4
  35. package/dist/cdn/components/synastry-chart.js +129 -39
  36. package/dist/cdn/components/synastry-chart.js.map +4 -4
  37. package/dist/cdn/components/tarot-card.js +49 -20
  38. package/dist/cdn/components/tarot-card.js.map +3 -3
  39. package/dist/cdn/components/tarot-spread.js +43 -27
  40. package/dist/cdn/components/tarot-spread.js.map +3 -3
  41. package/dist/cdn/components/vedic-kundli.js +23 -9
  42. package/dist/cdn/components/vedic-kundli.js.map +3 -3
  43. package/dist/cdn/roxy-ui.js +560 -350
  44. package/dist/cdn/roxy-ui.js.map +4 -4
  45. package/dist/components/biorhythm-chart.d.ts +2 -46
  46. package/dist/components/biorhythm-chart.d.ts.map +1 -1
  47. package/dist/components/biorhythm-chart.js +24 -23
  48. package/dist/components/biorhythm-chart.js.map +2 -2
  49. package/dist/components/compatibility-card.d.ts +2 -27
  50. package/dist/components/compatibility-card.d.ts.map +1 -1
  51. package/dist/components/compatibility-card.js +50 -29
  52. package/dist/components/compatibility-card.js.map +3 -3
  53. package/dist/components/dasha-timeline.d.ts +2 -31
  54. package/dist/components/dasha-timeline.d.ts.map +1 -1
  55. package/dist/components/dasha-timeline.js +32 -30
  56. package/dist/components/dasha-timeline.js.map +3 -3
  57. package/dist/components/data.d.ts +6 -0
  58. package/dist/components/data.d.ts.map +1 -1
  59. package/dist/components/data.js +9 -1
  60. package/dist/components/data.js.map +2 -2
  61. package/dist/components/dosha-card.d.ts +2 -16
  62. package/dist/components/dosha-card.d.ts.map +1 -1
  63. package/dist/components/dosha-card.js +12 -13
  64. package/dist/components/dosha-card.js.map +2 -2
  65. package/dist/components/endpoint-form.d.ts +2 -0
  66. package/dist/components/endpoint-form.d.ts.map +1 -1
  67. package/dist/components/endpoint-form.js +66 -8
  68. package/dist/components/endpoint-form.js.map +2 -2
  69. package/dist/components/guna-milan.d.ts +2 -20
  70. package/dist/components/guna-milan.d.ts.map +1 -1
  71. package/dist/components/guna-milan.js +22 -12
  72. package/dist/components/guna-milan.js.map +3 -3
  73. package/dist/components/hexagram.d.ts +3 -27
  74. package/dist/components/hexagram.d.ts.map +1 -1
  75. package/dist/components/hexagram.js +31 -15
  76. package/dist/components/hexagram.js.map +2 -2
  77. package/dist/components/horoscope-card.d.ts +2 -20
  78. package/dist/components/horoscope-card.d.ts.map +1 -1
  79. package/dist/components/horoscope-card.js +24 -15
  80. package/dist/components/horoscope-card.js.map +2 -2
  81. package/dist/components/kp-planets-table.d.ts +2 -21
  82. package/dist/components/kp-planets-table.d.ts.map +1 -1
  83. package/dist/components/kp-planets-table.js +10 -4
  84. package/dist/components/kp-planets-table.js.map +3 -3
  85. package/dist/components/location-search.d.ts +3 -11
  86. package/dist/components/location-search.d.ts.map +1 -1
  87. package/dist/components/location-search.js +45 -5
  88. package/dist/components/location-search.js.map +2 -2
  89. package/dist/components/moon-phase.d.ts +4 -21
  90. package/dist/components/moon-phase.d.ts.map +1 -1
  91. package/dist/components/moon-phase.js +17 -4
  92. package/dist/components/moon-phase.js.map +3 -3
  93. package/dist/components/natal-chart.d.ts +7 -43
  94. package/dist/components/natal-chart.d.ts.map +1 -1
  95. package/dist/components/natal-chart.js +130 -70
  96. package/dist/components/natal-chart.js.map +3 -3
  97. package/dist/components/numerology-card.d.ts +5 -37
  98. package/dist/components/numerology-card.d.ts.map +1 -1
  99. package/dist/components/numerology-card.js +54 -28
  100. package/dist/components/numerology-card.js.map +2 -2
  101. package/dist/components/panchang-table.d.ts +3 -62
  102. package/dist/components/panchang-table.d.ts.map +1 -1
  103. package/dist/components/panchang-table.js +62 -32
  104. package/dist/components/panchang-table.js.map +3 -3
  105. package/dist/components/synastry-chart.d.ts +9 -28
  106. package/dist/components/synastry-chart.d.ts.map +1 -1
  107. package/dist/components/synastry-chart.js +178 -38
  108. package/dist/components/synastry-chart.js.map +3 -3
  109. package/dist/components/tarot-card.d.ts +5 -29
  110. package/dist/components/tarot-card.d.ts.map +1 -1
  111. package/dist/components/tarot-card.js +59 -20
  112. package/dist/components/tarot-card.js.map +2 -2
  113. package/dist/components/tarot-spread.d.ts +2 -24
  114. package/dist/components/tarot-spread.d.ts.map +1 -1
  115. package/dist/components/tarot-spread.js +39 -13
  116. package/dist/components/tarot-spread.js.map +2 -2
  117. package/dist/components/vedic-kundli.d.ts +3 -23
  118. package/dist/components/vedic-kundli.d.ts.map +1 -1
  119. package/dist/components/vedic-kundli.js +25 -13
  120. package/dist/components/vedic-kundli.js.map +2 -2
  121. package/dist/index.cjs +1149 -358
  122. package/dist/index.cjs.map +4 -4
  123. package/dist/index.d.ts +6 -4
  124. package/dist/index.d.ts.map +1 -1
  125. package/dist/index.js +1149 -358
  126. package/dist/index.js.map +4 -4
  127. package/dist/manifest.d.ts +49 -0
  128. package/dist/manifest.d.ts.map +1 -0
  129. package/dist/manifest.json +1 -1
  130. package/dist/styles/tokens.css +47 -1
  131. package/dist/tokens/index.d.ts.map +1 -1
  132. package/dist/types/index.d.ts +2 -0
  133. package/dist/types/index.d.ts.map +1 -0
  134. package/dist/types/types.gen.d.ts +27811 -0
  135. package/dist/types/types.gen.d.ts.map +1 -0
  136. package/dist/utils/debounce.d.ts +9 -1
  137. package/dist/utils/debounce.d.ts.map +1 -1
  138. package/dist/utils/format.d.ts +15 -0
  139. package/dist/utils/format.d.ts.map +1 -0
  140. package/dist/version.d.ts +2 -0
  141. package/dist/version.d.ts.map +1 -0
  142. package/package.json +9 -1
  143. package/src/components/biorhythm-chart.ts +39 -84
  144. package/src/components/compatibility-card.ts +85 -52
  145. package/src/components/dasha-timeline.ts +55 -73
  146. package/src/components/data.ts +20 -1
  147. package/src/components/dosha-card.ts +18 -31
  148. package/src/components/endpoint-form.ts +79 -11
  149. package/src/components/guna-milan.ts +16 -34
  150. package/src/components/hexagram.ts +53 -43
  151. package/src/components/horoscope-card.ts +51 -39
  152. package/src/components/kp-planets-table.ts +8 -27
  153. package/src/components/location-search.ts +45 -20
  154. package/src/components/moon-phase.ts +28 -25
  155. package/src/components/natal-chart.ts +129 -84
  156. package/src/components/numerology-card.ts +87 -79
  157. package/src/components/panchang-table.ts +40 -78
  158. package/src/components/synastry-chart.ts +220 -78
  159. package/src/components/tarot-card.ts +76 -62
  160. package/src/components/tarot-spread.ts +72 -45
  161. package/src/components/vedic-kundli.ts +42 -51
  162. package/src/index.ts +14 -24
  163. package/src/manifest.ts +366 -0
  164. package/src/styles/tokens.css +47 -1
  165. package/src/tokens/index.ts +5 -0
  166. package/src/types/types.gen.ts +1 -1
  167. package/src/utils/debounce.ts +23 -4
  168. package/src/utils/format.ts +57 -0
  169. package/src/version.ts +2 -0
package/dist/index.cjs CHANGED
@@ -28,6 +28,7 @@ var __decorateClass = (decorators, target, key, kind) => {
28
28
  // packages/ui/src/index.ts
29
29
  var src_exports = {};
30
30
  __export(src_exports, {
31
+ ROXY_COMPONENTS: () => ROXY_COMPONENTS,
31
32
  ROXY_UI_COMPONENTS: () => ROXY_UI_COMPONENTS,
32
33
  ROXY_UI_VERSION: () => ROXY_UI_VERSION,
33
34
  RoxyBiorhythmChart: () => RoxyBiorhythmChart,
@@ -165,25 +166,28 @@ var RoxyBiorhythmChart = class extends import_lit2.LitElement {
165
166
  const d = this.data;
166
167
  if (!d)
167
168
  return import_lit2.html`<div class="roxy-empty" role="status">No biorhythm data</div>`;
168
- if (this.mode === "critical-days" && d.criticalDays?.length) {
169
+ if (this.mode === "critical-days" && "criticalDays" in d) {
169
170
  return this.renderCritical(d);
170
171
  }
171
- if (this.mode === "forecast" && d.days?.length) {
172
+ if (this.mode === "forecast" && "days" in d) {
172
173
  return this.renderForecast(d);
173
174
  }
174
175
  return this.renderDaily(d);
175
176
  }
176
177
  renderDaily(d) {
177
- const cycles = d.cycles ?? {};
178
- const entries = Object.entries(cycles);
178
+ const raw = d.quickRead ?? {};
179
+ const entries = Object.entries(raw).map(([cycle, value]) => {
180
+ const v = typeof value === "number" ? value : 0;
181
+ const normalized = Math.abs(v) > 1 ? v / 100 : v;
182
+ return [cycle, normalized];
183
+ });
179
184
  return import_lit2.html`<section class="wrap" aria-label="Daily biorhythm">
180
185
  <header class="head">
181
186
  <h2 class="title">Biorhythm</h2>
182
187
  ${typeof d.energyRating === "number" ? import_lit2.html`<span class="energy">Energy ${d.energyRating}/10</span>` : import_lit2.nothing}
183
188
  </header>
184
189
  <div class="bars" role="list">
185
- ${entries.map(([cycle, value]) => {
186
- const v = typeof value === "number" ? value : 0;
190
+ ${entries.map(([cycle, v]) => {
187
191
  const pct = (v + 1) / 2 * 100;
188
192
  const color = CYCLE_COLOR[cycle] ?? "var(--roxy-accent, #f59e0b)";
189
193
  return import_lit2.html`<div class="bar" role="listitem">
@@ -194,15 +198,12 @@ var RoxyBiorhythmChart = class extends import_lit2.LitElement {
194
198
  style="width: ${pct}%; background: ${color}"
195
199
  ></span>
196
200
  </span>
197
- <span class="value">${(v * 100).toFixed(0)}%</span>
201
+ <span class="value">${Math.round(v * 100)}%</span>
198
202
  </div>`;
199
203
  })}
200
204
  </div>
201
- ${d.interpretation ? import_lit2.html`<p class="advice">${d.interpretation}</p>` : import_lit2.nothing}
205
+ ${d.dailyMessage ? import_lit2.html`<p class="advice">${d.dailyMessage}</p>` : import_lit2.nothing}
202
206
  ${d.advice ? import_lit2.html`<p class="advice">${d.advice}</p>` : import_lit2.nothing}
203
- ${d.criticalAlerts?.length ? import_lit2.html`<div>
204
- ${d.criticalAlerts.map((a) => import_lit2.html`<p class="alert">${a}</p>`)}
205
- </div>` : import_lit2.nothing}
206
207
  </section>`;
207
208
  }
208
209
  renderForecast(d) {
@@ -212,13 +213,16 @@ var RoxyBiorhythmChart = class extends import_lit2.LitElement {
212
213
  const w = 600;
213
214
  const h = 160;
214
215
  const xStep = w / Math.max(days.length - 1, 1);
215
- const cycles = Object.keys(days[0]?.cycles ?? {});
216
+ const cycleKeys = [
217
+ "physical",
218
+ "emotional",
219
+ "intellectual",
220
+ "intuitive"
221
+ ];
216
222
  return import_lit2.html`<section class="wrap" aria-label="Biorhythm forecast">
217
223
  <header class="head">
218
224
  <h2 class="title">Forecast</h2>
219
- <span class="energy"
220
- >${d.startDate ?? ""} - ${d.endDate ?? ""}</span
221
- >
225
+ <span class="energy">${d.startDate} - ${d.endDate}</span>
222
226
  </header>
223
227
  <svg
224
228
  viewBox="0 0 ${w} ${h}"
@@ -234,11 +238,11 @@ var RoxyBiorhythmChart = class extends import_lit2.LitElement {
234
238
  stroke="var(--roxy-border, #e4e4e7)"
235
239
  stroke-width="1"
236
240
  />
237
- ${cycles.map((cycle) => {
241
+ ${cycleKeys.map((cycle) => {
238
242
  const points = days.map((day, i) => {
239
- const v = day.cycles?.[cycle] ?? 0;
243
+ const v = day[cycle] ?? 0;
240
244
  const x = i * xStep;
241
- const y = h / 2 - v * (h / 2 - 8);
245
+ const y = h / 2 - v / 100 * (h / 2 - 8);
242
246
  return `${x.toFixed(2)},${y.toFixed(2)}`;
243
247
  }).join(" ");
244
248
  const color = CYCLE_COLOR[cycle] ?? "#475569";
@@ -252,14 +256,12 @@ var RoxyBiorhythmChart = class extends import_lit2.LitElement {
252
256
  return import_lit2.html`<section class="wrap" aria-label="Critical days">
253
257
  <header class="head">
254
258
  <h2 class="title">Critical days</h2>
255
- <span class="energy"
256
- >${d.totalCriticalDays ?? d.criticalDays?.length ?? 0} total</span
257
- >
259
+ <span class="energy">${d.totalCriticalDays} total</span>
258
260
  </header>
259
261
  <div>
260
- ${(d.criticalDays ?? []).map(
262
+ ${d.criticalDays.map(
261
263
  (day) => import_lit2.html`<span class="crit"
262
- >${day.date} · ${day.cycle ?? ""} ${day.severity ?? ""}</span
264
+ >${day.date} · ${day.cycle} ${day.severity}</span
263
265
  >`
264
266
  )}
265
267
  </div>
@@ -359,6 +361,50 @@ RoxyBiorhythmChart = __decorateClass([
359
361
  // packages/ui/src/components/compatibility-card.ts
360
362
  var import_lit3 = require("lit");
361
363
  var import_decorators2 = require("lit/decorators.js");
364
+
365
+ // packages/ui/src/utils/format.ts
366
+ function formatTime(input) {
367
+ if (typeof input !== "string" || input.length === 0) return "";
368
+ if (/^\d{4}-\d{2}-\d{2}$/.test(input)) return "";
369
+ const bareTime = /^\d{2}:\d{2}(:\d{2})?$/.test(input);
370
+ const iso = bareTime ? `1970-01-01T${input}` : input;
371
+ const d = new Date(iso);
372
+ if (Number.isNaN(d.getTime())) return input;
373
+ return d.toLocaleTimeString(void 0, {
374
+ hour: "numeric",
375
+ minute: "2-digit",
376
+ hour12: true
377
+ });
378
+ }
379
+ function formatDate(input) {
380
+ if (typeof input !== "string" || input.length === 0) return "";
381
+ const d = new Date(
382
+ /^\d{4}-\d{2}-\d{2}$/.test(input) ? `${input}T00:00:00` : input
383
+ );
384
+ if (Number.isNaN(d.getTime())) return input;
385
+ return d.toLocaleDateString(void 0, {
386
+ month: "short",
387
+ day: "numeric",
388
+ year: "numeric"
389
+ });
390
+ }
391
+ function formatTimeRange(t) {
392
+ if (!t) return "";
393
+ const start = formatTime(t.start);
394
+ const end = formatTime(t.end);
395
+ if (start && end) return `${start} - ${end}`;
396
+ return start || end || "";
397
+ }
398
+ function formatNumber(value, dp = 1) {
399
+ if (typeof value !== "number" || !Number.isFinite(value)) return "";
400
+ return value.toFixed(dp).replace(/\.?0+$/, "");
401
+ }
402
+ function formatPercent(value, dp = 1) {
403
+ const n = formatNumber(value, dp);
404
+ return n ? `${n}%` : "";
405
+ }
406
+
407
+ // packages/ui/src/components/compatibility-card.ts
362
408
  var RoxyCompatibilityCard = class extends import_lit3.LitElement {
363
409
  constructor() {
364
410
  super(...arguments);
@@ -368,22 +414,29 @@ var RoxyCompatibilityCard = class extends import_lit3.LitElement {
368
414
  getBreakdown() {
369
415
  const d = this.data;
370
416
  if (!d) return {};
371
- if (d.categoryScores) return d.categoryScores;
372
- if (d.categoryBreakdown) return d.categoryBreakdown;
373
- const inferred = {};
374
- if (typeof d.emotional === "number") inferred.emotional = d.emotional;
375
- if (typeof d.communication === "number")
376
- inferred.communication = d.communication;
377
- if (typeof d.romance === "number") inferred.romance = d.romance;
378
- if (d.elementBalance) Object.assign(inferred, d.elementBalance);
379
- return inferred;
417
+ if ("categories" in d && d.categories) {
418
+ const out = {};
419
+ for (const [k, v] of Object.entries(d.categories)) {
420
+ if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
421
+ }
422
+ return out;
423
+ }
424
+ return {};
380
425
  }
381
426
  render() {
382
427
  const d = this.data;
383
428
  if (!d)
384
429
  return import_lit3.html`<div class="roxy-empty" role="status">No compatibility data</div>`;
385
- const score = d.overallScore ?? d.score;
430
+ const score = d.overallScore;
386
431
  const breakdown = this.getBreakdown();
432
+ const rating = "rating" in d ? d.rating : void 0;
433
+ const archetype = "archetype" in d ? d.archetype : void 0;
434
+ const advice = "advice" in d ? d.advice : void 0;
435
+ const summary = "summary" in d ? d.summary : void 0;
436
+ const interpretation = "interpretation" in d ? d.interpretation : void 0;
437
+ const strengths = "strengths" in d ? d.strengths : void 0;
438
+ const challenges = "challenges" in d ? d.challenges : void 0;
439
+ const keyAspects = "keyAspects" in d ? d.keyAspects : void 0;
387
440
  return import_lit3.html`<article
388
441
  class="card"
389
442
  aria-label=${`Compatibility (${this.mode})`}
@@ -391,8 +444,8 @@ var RoxyCompatibilityCard = class extends import_lit3.LitElement {
391
444
  <div class="head">
392
445
  <h2>${this.mode} compatibility</h2>
393
446
  <div>
394
- ${typeof score === "number" ? import_lit3.html`<div class="score">${score}</div>` : import_lit3.nothing}
395
- ${d.rating ? import_lit3.html`<div class="rating">${d.rating}</div>` : import_lit3.nothing}
447
+ ${typeof score === "number" ? import_lit3.html`<div class="score">${formatNumber(score, 0)}</div>` : import_lit3.nothing}
448
+ ${rating ? import_lit3.html`<div class="rating">${rating}</div>` : import_lit3.nothing}
396
449
  </div>
397
450
  </div>
398
451
 
@@ -403,35 +456,37 @@ var RoxyCompatibilityCard = class extends import_lit3.LitElement {
403
456
  <span class="bar"
404
457
  ><span style="width: ${Math.max(0, Math.min(100, v))}%"></span
405
458
  ></span>
406
- <span>${v}</span>
459
+ <span>${formatNumber(v, 0)}</span>
407
460
  </div>`
408
461
  )}
409
462
  </div>` : import_lit3.nothing}
410
- ${d.relationshipArchetype ? import_lit3.html`<p>
411
- <span class="archetype">${d.relationshipArchetype}</span>
463
+ ${archetype ? import_lit3.html`<p>
464
+ <span class="archetype">${archetype.label}</span>
465
+ ${archetype.description ? import_lit3.html` · ${archetype.description}` : import_lit3.nothing}
412
466
  </p>` : import_lit3.nothing}
413
- ${d.summary ? import_lit3.html`<p>${d.summary}</p>` : import_lit3.nothing}
414
- ${d.advice ? import_lit3.html`<p>${d.advice}</p>` : import_lit3.nothing}
415
- ${(d.strengths?.length ?? 0) > 0 || (d.challenges?.length ?? 0) > 0 ? import_lit3.html`<div class="lists">
416
- ${d.strengths?.length ? import_lit3.html`<div>
467
+ ${summary ? import_lit3.html`<p>${summary}</p>` : import_lit3.nothing}
468
+ ${interpretation && !summary ? import_lit3.html`<p>${interpretation}</p>` : import_lit3.nothing}
469
+ ${advice ? import_lit3.html`<p>${advice}</p>` : import_lit3.nothing}
470
+ ${(strengths?.length ?? 0) > 0 || (challenges?.length ?? 0) > 0 ? import_lit3.html`<div class="lists">
471
+ ${strengths?.length ? import_lit3.html`<div>
417
472
  <h3>Strengths</h3>
418
473
  <ul>
419
- ${d.strengths.map((s) => import_lit3.html`<li>${s}</li>`)}
474
+ ${strengths.map((s) => import_lit3.html`<li>${s}</li>`)}
420
475
  </ul>
421
476
  </div>` : import_lit3.nothing}
422
- ${d.challenges?.length ? import_lit3.html`<div>
477
+ ${challenges?.length ? import_lit3.html`<div>
423
478
  <h3>Challenges</h3>
424
479
  <ul>
425
- ${d.challenges.map((s) => import_lit3.html`<li>${s}</li>`)}
426
- </ul>
427
- </div>` : import_lit3.nothing}
428
- ${d.keyAspects?.length ? import_lit3.html`<div>
429
- <h3>Key aspects</h3>
430
- <ul>
431
- ${d.keyAspects.map((s) => import_lit3.html`<li>${s}</li>`)}
480
+ ${challenges.map((s) => import_lit3.html`<li>${s}</li>`)}
432
481
  </ul>
433
482
  </div>` : import_lit3.nothing}
434
483
  </div>` : import_lit3.nothing}
484
+ ${keyAspects?.length ? import_lit3.html`<div>
485
+ <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>
486
+ <ul style="margin: 0; padding-left: 1rem; font-size: var(--roxy-text-sm);">
487
+ ${keyAspects.slice(0, 6).map((a) => import_lit3.html`<li>${formatAspect(a)}</li>`)}
488
+ </ul>
489
+ </div>` : import_lit3.nothing}
435
490
  </article>`;
436
491
  }
437
492
  };
@@ -501,7 +556,7 @@ RoxyCompatibilityCard.styles = [
501
556
  }
502
557
 
503
558
  .archetype {
504
- color: var(--roxy-info, #0284c7);
559
+ color: var(--roxy-accent-fg, #b45309);
505
560
  font-weight: var(--roxy-weight-bold, 600);
506
561
  }
507
562
 
@@ -532,6 +587,12 @@ __decorateClass([
532
587
  RoxyCompatibilityCard = __decorateClass([
533
588
  (0, import_decorators2.customElement)("roxy-compatibility-card")
534
589
  ], RoxyCompatibilityCard);
590
+ function formatAspect(a) {
591
+ const aspect = a.type.toLowerCase().replace(/_/g, "-");
592
+ const orb = typeof a.orb === "number" ? ` (orb ${formatNumber(a.orb, 1)}\xB0)` : "";
593
+ const head = [a.planet1, aspect, a.planet2].filter(Boolean).join(" ");
594
+ return a.description ? `${head}${orb} \xB7 ${a.description}` : `${head}${orb}`;
595
+ }
535
596
 
536
597
  // packages/ui/src/components/dasha-timeline.ts
537
598
  var import_lit4 = require("lit");
@@ -547,16 +608,16 @@ var RoxyDashaTimeline = class extends import_lit4.LitElement {
547
608
  if (!d)
548
609
  return import_lit4.html`<div class="roxy-empty" role="status">No dasha data</div>`;
549
610
  const periods = this.collectPeriods(d);
550
- const maxYears = periods.length ? Math.max(...periods.map((p) => p.durationYears ?? p.years ?? 1)) : 0;
611
+ const maxYears = periods.length ? Math.max(...periods.map((p) => p.durationYears)) : 0;
551
612
  return import_lit4.html`<div class="wrap" aria-label="Dasha timeline">
552
613
  <header class="head">
553
614
  <h2 class="title">
554
615
  ${this.period === "major" ? "Vimshottari Mahadasha" : this.period === "sub" ? "Antardasha" : "Active dashas"}
555
616
  </h2>
556
- ${d.nakshatraName || d.moonNakshatra ? import_lit4.html`<div class="nakshatra">
557
- Moon nakshatra: ${d.nakshatraName ?? d.moonNakshatra}
558
- ${d.nakshatraLord ? import_lit4.html`(lord ${d.nakshatraLord})` : import_lit4.nothing}
559
- </div>` : import_lit4.nothing}
617
+ ${"nakshatraName" in d && d.nakshatraName ? import_lit4.html`<div class="nakshatra">
618
+ Moon nakshatra: ${d.nakshatraName}
619
+ ${"nakshatraLord" in d && d.nakshatraLord ? import_lit4.html`(lord ${d.nakshatraLord})` : import_lit4.nothing}
620
+ </div>` : import_lit4.nothing}
560
621
  </header>
561
622
 
562
623
  ${this.period === "current" ? this.renderCurrent(d) : import_lit4.nothing}
@@ -566,39 +627,35 @@ var RoxyDashaTimeline = class extends import_lit4.LitElement {
566
627
  </div>`;
567
628
  }
568
629
  renderCurrent(d) {
630
+ if (!("mahadasha" in d)) return import_lit4.nothing;
569
631
  return import_lit4.html`<div class="current">
570
- ${d.mahadasha ? import_lit4.html`<div>
571
- <span>Mahadasha</span>
572
- <strong>${d.mahadasha.lord ?? d.mahadasha.mahadashaLord}</strong>
573
- ${typeof d.remainingInMahadasha === "number" ? import_lit4.html`<small>${d.remainingInMahadasha.toFixed(1)} years left</small>` : import_lit4.nothing}
574
- </div>` : import_lit4.nothing}
575
- ${d.antardasha ? import_lit4.html`<div>
576
- <span>Antardasha</span>
577
- <strong>${d.antardasha.lord ?? d.antardasha.antardashaLord}</strong>
578
- ${typeof d.remainingInAntardasha === "number" ? import_lit4.html`<small>${d.remainingInAntardasha.toFixed(1)} years left</small>` : import_lit4.nothing}
579
- </div>` : import_lit4.nothing}
580
- ${d.pratyantardasha ? import_lit4.html`<div>
581
- <span>Pratyantardasha</span>
582
- <strong
583
- >${d.pratyantardasha.lord ?? d.pratyantardasha.pratyantardashaLord}</strong
584
- >
585
- ${typeof d.remainingInPratyantardasha === "number" ? import_lit4.html`<small
586
- >${d.remainingInPratyantardasha.toFixed(2)} years left</small
587
- >` : import_lit4.nothing}
588
- </div>` : import_lit4.nothing}
632
+ ${"mahadasha" in d && d.mahadasha ? import_lit4.html`<div>
633
+ <span>Mahadasha</span>
634
+ <strong>${d.mahadasha.planet}</strong>
635
+ ${"remainingInMahadasha" in d && d.remainingInMahadasha ? import_lit4.html`<small>${formatNumber(d.remainingInMahadasha.years + d.remainingInMahadasha.months / 12, 1)} years left</small>` : import_lit4.nothing}
636
+ </div>` : import_lit4.nothing}
637
+ ${"antardasha" in d && d.antardasha ? import_lit4.html`<div>
638
+ <span>Antardasha</span>
639
+ <strong>${d.antardasha.planet}</strong>
640
+ ${"remainingInAntardasha" in d && d.remainingInAntardasha ? import_lit4.html`<small>${formatNumber(d.remainingInAntardasha.years + d.remainingInAntardasha.months / 12, 1)} years left</small>` : import_lit4.nothing}
641
+ </div>` : import_lit4.nothing}
642
+ ${"pratyantardasha" in d && d.pratyantardasha ? import_lit4.html`<div>
643
+ <span>Pratyantardasha</span>
644
+ <strong>${d.pratyantardasha.planet}</strong>
645
+ ${"remainingInPratyantardasha" in d && d.remainingInPratyantardasha ? import_lit4.html`<small>${formatNumber(d.remainingInPratyantardasha.years + d.remainingInPratyantardasha.months / 12, 1)} years left</small>` : import_lit4.nothing}
646
+ </div>` : import_lit4.nothing}
589
647
  </div>`;
590
648
  }
591
649
  collectPeriods(d) {
592
- if (this.period === "major" && d.mahadashas?.length) return d.mahadashas;
593
- if (this.period === "sub" && d.antardashas?.length) return d.antardashas;
594
- return d.mahadashas ?? d.antardashas ?? [];
650
+ if ("mahadashas" in d && d.mahadashas?.length) return d.mahadashas;
651
+ if ("antardashas" in d && d.antardashas?.length) return d.antardashas;
652
+ return [];
595
653
  }
596
654
  renderBar(p, max) {
597
- const lord = p.lord ?? p.mahadashaLord ?? p.antardashaLord ?? p.planet ?? "";
598
- const years = p.durationYears ?? p.years ?? 0;
655
+ const years = p.durationYears;
599
656
  const width = max > 0 ? years / max * 100 : 0;
600
657
  return import_lit4.html`<div class="bar" role="listitem">
601
- <span>${lord}</span>
658
+ <span>${p.planet}</span>
602
659
  <span class="bar-track"><span style="width: ${width}%"></span></span>
603
660
  <span class="dates">
604
661
  ${p.startDate ? formatYear(p.startDate) : ""}
@@ -706,15 +763,20 @@ var import_decorators4 = require("lit/decorators.js");
706
763
  var TITLE_KEYS = ["title", "name", "label", "heading", "overview", "summary"];
707
764
  var IMAGE_KEYS = ["imageUrl", "image", "icon", "symbol"];
708
765
  var SKIP_KEYS = ["imageUrl", "image"];
766
+ var MAX_DEPTH = 6;
709
767
  var RoxyData = class extends import_lit5.LitElement {
710
768
  constructor() {
711
769
  super(...arguments);
712
770
  this.data = null;
771
+ this.depth = 0;
713
772
  }
714
773
  render() {
715
774
  if (this.data == null) {
716
775
  return import_lit5.html`<div class="roxy-empty" role="status">No data</div>`;
717
776
  }
777
+ if (this.depth >= MAX_DEPTH) {
778
+ return import_lit5.html`<div class="roxy-empty" role="status">…</div>`;
779
+ }
718
780
  return import_lit5.html`<div
719
781
  class="roxy-card"
720
782
  aria-label="Generic data display"
@@ -811,7 +873,7 @@ var RoxyData = class extends import_lit5.LitElement {
811
873
  </ul>`;
812
874
  }
813
875
  }
814
- return import_lit5.html`<roxy-data .data=${value}></roxy-data>`;
876
+ return import_lit5.html`<roxy-data .data=${value} .depth=${this.depth + 1}></roxy-data>`;
815
877
  }
816
878
  formatPrimitive(value) {
817
879
  if (value === null || value === void 0) return "";
@@ -933,6 +995,9 @@ RoxyData.styles = [
933
995
  __decorateClass([
934
996
  (0, import_decorators4.property)({ attribute: false })
935
997
  ], RoxyData.prototype, "data", 2);
998
+ __decorateClass([
999
+ (0, import_decorators4.property)({ attribute: false })
1000
+ ], RoxyData.prototype, "depth", 2);
936
1001
  RoxyData = __decorateClass([
937
1002
  (0, import_decorators4.customElement)("roxy-data")
938
1003
  ], RoxyData);
@@ -978,25 +1043,24 @@ var RoxyDoshaCard = class extends import_lit6.LitElement {
978
1043
  </div>
979
1044
  </header>
980
1045
  ${d.description ? import_lit6.html`<p class="description">${d.description}</p>` : import_lit6.nothing}
981
- ${this.renderEffects(d.effects)}
1046
+ ${this.renderEffects(d)}
982
1047
  ${d.remedies && d.remedies.length > 0 ? import_lit6.html`<div>
983
1048
  <h3>Remedies</h3>
984
1049
  <ul>
985
1050
  ${d.remedies.map((r) => import_lit6.html`<li>${r}</li>`)}
986
1051
  </ul>
987
1052
  </div>` : import_lit6.nothing}
988
- ${d.exceptions && d.exceptions.length > 0 ? import_lit6.html`<div>
989
- <h3>Exceptions</h3>
990
- <ul>
991
- ${d.exceptions.map((r) => import_lit6.html`<li>${r}</li>`)}
992
- </ul>
993
- </div>` : import_lit6.nothing}
1053
+ ${"exceptions" in d && d.exceptions && d.exceptions.length > 0 ? import_lit6.html`<div>
1054
+ <h3>Exceptions</h3>
1055
+ <ul>
1056
+ ${d.exceptions.map((r) => import_lit6.html`<li>${r}</li>`)}
1057
+ </ul>
1058
+ </div>` : import_lit6.nothing}
994
1059
  </article>`;
995
1060
  }
996
- renderEffects(e) {
997
- if (!e) return import_lit6.nothing;
998
- if (typeof e === "string") return import_lit6.html`<p>${e}</p>`;
999
- const entries = Object.entries(e).filter(
1061
+ renderEffects(d) {
1062
+ if (!d.effects) return import_lit6.nothing;
1063
+ const entries = Object.entries(d.effects).filter(
1000
1064
  ([, v]) => typeof v === "string" && v.length > 0
1001
1065
  );
1002
1066
  if (entries.length === 0) return import_lit6.nothing;
@@ -1048,11 +1112,11 @@ RoxyDoshaCard.styles = [
1048
1112
  }
1049
1113
  .badge.absent {
1050
1114
  background: color-mix(in srgb, var(--roxy-success, #16a34a) 16%, transparent);
1051
- color: var(--roxy-success, #16a34a);
1115
+ color: var(--roxy-success-fg, #166534);
1052
1116
  }
1053
1117
  .badge.present {
1054
1118
  background: color-mix(in srgb, var(--roxy-danger, #dc2626) 16%, transparent);
1055
- color: var(--roxy-danger, #dc2626);
1119
+ color: var(--roxy-danger-fg, #991b1b);
1056
1120
  }
1057
1121
  .severity {
1058
1122
  display: flex;
@@ -1116,6 +1180,21 @@ RoxyDoshaCard = __decorateClass([
1116
1180
  // packages/ui/src/components/endpoint-form.ts
1117
1181
  var import_lit7 = require("lit");
1118
1182
  var import_decorators6 = require("lit/decorators.js");
1183
+ var specCache = /* @__PURE__ */ new Map();
1184
+ async function loadSpec(url) {
1185
+ let pending = specCache.get(url);
1186
+ if (!pending) {
1187
+ pending = fetch(url).then(async (res) => {
1188
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1189
+ return await res.json();
1190
+ }).catch((err) => {
1191
+ specCache.delete(url);
1192
+ throw err;
1193
+ });
1194
+ specCache.set(url, pending);
1195
+ }
1196
+ return pending;
1197
+ }
1119
1198
  var RoxyEndpointForm = class extends import_lit7.LitElement {
1120
1199
  constructor() {
1121
1200
  super(...arguments);
@@ -1127,6 +1206,12 @@ var RoxyEndpointForm = class extends import_lit7.LitElement {
1127
1206
  this.values = {};
1128
1207
  this.hasLocation = false;
1129
1208
  this.loaded = false;
1209
+ this.specError = null;
1210
+ this.retryLoadSchema = () => {
1211
+ this.loaded = false;
1212
+ this.specError = null;
1213
+ void this.loadSchema();
1214
+ };
1130
1215
  this.onLocation = (e) => {
1131
1216
  const detail = e.detail;
1132
1217
  if (detail) {
@@ -1167,13 +1252,16 @@ var RoxyEndpointForm = class extends import_lit7.LitElement {
1167
1252
  void this.loadSchema();
1168
1253
  }
1169
1254
  async loadSchema() {
1255
+ this.specError = null;
1170
1256
  try {
1171
- const res = await fetch(this.specUrl);
1172
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
1173
- const spec = await res.json();
1257
+ const spec = await loadSpec(this.specUrl);
1174
1258
  const path = `/${this.endpoint.replace(/^\//, "")}`;
1175
1259
  const op = spec.paths?.[path]?.[this.method.toLowerCase()];
1176
- if (!op) return;
1260
+ if (!op) {
1261
+ throw new Error(
1262
+ `Endpoint ${this.method} ${path} not found in OpenAPI spec`
1263
+ );
1264
+ }
1177
1265
  const schemas = spec.components?.schemas ?? {};
1178
1266
  const fields = [];
1179
1267
  let bodySchema;
@@ -1218,8 +1306,17 @@ var RoxyEndpointForm = class extends import_lit7.LitElement {
1218
1306
  }
1219
1307
  this.values = init;
1220
1308
  this.loaded = true;
1221
- } catch (_err) {
1309
+ } catch (err) {
1310
+ const message = err instanceof Error ? err.message : String(err);
1311
+ this.specError = message;
1222
1312
  this.loaded = true;
1313
+ this.dispatchEvent(
1314
+ new CustomEvent("roxy-spec-error", {
1315
+ detail: { url: this.specUrl, message },
1316
+ bubbles: true,
1317
+ composed: true
1318
+ })
1319
+ );
1223
1320
  }
1224
1321
  }
1225
1322
  resolve(schema, all) {
@@ -1245,6 +1342,12 @@ var RoxyEndpointForm = class extends import_lit7.LitElement {
1245
1342
  if (!this.loaded) {
1246
1343
  return import_lit7.html`<form><div class="roxy-skeleton" style="height: 8rem"></div></form>`;
1247
1344
  }
1345
+ if (this.specError) {
1346
+ return import_lit7.html`<div class="spec-error" role="alert">
1347
+ Schema load failed: ${this.specError}
1348
+ <button type="button" class="submit" @click=${this.retryLoadSchema}>Retry</button>
1349
+ </div>`;
1350
+ }
1248
1351
  const renderField = (f) => {
1249
1352
  if (this.hasLocation && (f.name === "latitude" || f.name === "longitude" || f.name === "timezone")) {
1250
1353
  return import_lit7.nothing;
@@ -1342,22 +1445,27 @@ RoxyEndpointForm.styles = [
1342
1445
  .fields {
1343
1446
  display: grid;
1344
1447
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
1448
+ align-items: start;
1345
1449
  gap: var(--roxy-space-md, 1rem);
1346
1450
  }
1347
1451
  .field {
1348
- display: grid;
1452
+ display: flex;
1453
+ flex-direction: column;
1349
1454
  gap: var(--roxy-space-xs, 0.25rem);
1455
+ min-width: 0;
1350
1456
  }
1351
1457
  label {
1352
1458
  font-size: var(--roxy-text-sm, 0.875rem);
1353
1459
  color: var(--roxy-secondary, #475569);
1354
1460
  }
1355
1461
  label .req {
1356
- color: var(--roxy-danger, #dc2626);
1462
+ color: var(--roxy-danger-fg, #991b1b);
1357
1463
  margin-left: 4px;
1358
1464
  }
1359
1465
  input,
1360
1466
  select {
1467
+ width: 100%;
1468
+ box-sizing: border-box;
1361
1469
  padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem);
1362
1470
  font-size: var(--roxy-text-base, 1rem);
1363
1471
  font-family: inherit;
@@ -1392,7 +1500,7 @@ RoxyEndpointForm.styles = [
1392
1500
  button.submit {
1393
1501
  justify-self: start;
1394
1502
  background: var(--roxy-accent-fg, #b45309);
1395
- color: #fff;
1503
+ color: var(--roxy-bg, #fff);
1396
1504
  border: 0;
1397
1505
  border-radius: var(--roxy-radius-md, 8px);
1398
1506
  padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-lg, 1.5rem);
@@ -1410,6 +1518,17 @@ RoxyEndpointForm.styles = [
1410
1518
  outline: 2px solid var(--roxy-ring, rgba(245, 158, 11, 0.4));
1411
1519
  outline-offset: 2px;
1412
1520
  }
1521
+ .spec-error {
1522
+ display: grid;
1523
+ gap: var(--roxy-space-md, 1rem);
1524
+ justify-items: start;
1525
+ background: var(--roxy-bg, #fff);
1526
+ border: 1px solid var(--roxy-danger, #dc2626);
1527
+ border-radius: var(--roxy-radius-md, 8px);
1528
+ padding: var(--roxy-space-lg, 1.5rem);
1529
+ color: var(--roxy-danger-fg, #991b1b);
1530
+ font-size: var(--roxy-text-sm, 0.875rem);
1531
+ }
1413
1532
  `
1414
1533
  ];
1415
1534
  __decorateClass([
@@ -1436,6 +1555,9 @@ __decorateClass([
1436
1555
  __decorateClass([
1437
1556
  (0, import_decorators6.state)()
1438
1557
  ], RoxyEndpointForm.prototype, "loaded", 2);
1558
+ __decorateClass([
1559
+ (0, import_decorators6.state)()
1560
+ ], RoxyEndpointForm.prototype, "specError", 2);
1439
1561
  RoxyEndpointForm = __decorateClass([
1440
1562
  (0, import_decorators6.customElement)("roxy-endpoint-form")
1441
1563
  ], RoxyEndpointForm);
@@ -1455,18 +1577,16 @@ var RoxyGunaMilan = class extends import_lit8.LitElement {
1455
1577
  const d = this.data;
1456
1578
  if (!d)
1457
1579
  return import_lit8.html`<div class="roxy-empty" role="status">No Guna Milan data</div>`;
1458
- const total = d.total ?? d.totalScore ?? 0;
1459
- const max = d.maxScore ?? 36;
1460
1580
  const breakdown = (d.breakdown ?? []).filter(
1461
- (b) => b && (b.name || b.score !== void 0)
1581
+ (b) => b?.category !== void 0
1462
1582
  );
1463
1583
  return import_lit8.html`<article class="card" aria-label="Guna Milan score">
1464
1584
  <div class="score-bar">
1465
1585
  <div>
1466
- <span class="total">${total}</span>
1467
- <span class="over"> / ${max}</span>
1586
+ <span class="total">${formatNumber(d.total, 1)}</span>
1587
+ <span class="over"> / ${d.maxScore}</span>
1468
1588
  ${typeof d.percentage === "number" ? import_lit8.html`<small style="margin-left: 0.5rem; color: var(--roxy-muted)">
1469
- ${d.percentage}%
1589
+ ${formatPercent(d.percentage, 1)}
1470
1590
  </small>` : import_lit8.nothing}
1471
1591
  </div>
1472
1592
  ${d.recommendation ? import_lit8.html`<span class="recommendation">${d.recommendation}</span>` : import_lit8.nothing}
@@ -1483,23 +1603,25 @@ var RoxyGunaMilan = class extends import_lit8.LitElement {
1483
1603
  <tbody>
1484
1604
  ${breakdown.map((b) => {
1485
1605
  const score = b.score ?? 0;
1486
- const maxScore = b.max ?? b.maxScore ?? defaultMax(b.name);
1606
+ const maxScore = b.maxScore ?? defaultMax(b.category);
1487
1607
  const pct = maxScore ? score / maxScore * 100 : 0;
1488
1608
  return import_lit8.html`<tr>
1489
- <td>${b.name ?? ""}</td>
1609
+ <td>${b.category}</td>
1490
1610
  <td class="bar-cell">
1491
1611
  <div class="mini-bar">
1492
1612
  <span style="width: ${pct}%"></span>
1493
1613
  </div>
1494
1614
  </td>
1495
- <td class="score">${score} / ${maxScore}</td>
1615
+ <td class="score">${formatNumber(score, 1)} / ${maxScore}</td>
1496
1616
  </tr>`;
1497
1617
  })}
1498
1618
  </tbody>
1499
1619
  </table>` : import_lit8.nothing}
1500
1620
  ${(d.doshas?.length ?? 0) > 0 || (d.doshaCancellations?.length ?? 0) > 0 ? import_lit8.html`<div class="tags">
1501
1621
  ${d.doshas?.map((x) => import_lit8.html`<span class="dosha">${x}</span>`)}
1502
- ${d.doshaCancellations?.map((x) => import_lit8.html`<span class="cancel">${x}</span>`)}
1622
+ ${d.doshaCancellations?.map(
1623
+ (x) => import_lit8.html`<span class="cancel" title=${x.reason}>${x.dosha} cancelled</span>`
1624
+ )}
1503
1625
  </div>` : import_lit8.nothing}
1504
1626
  </article>`;
1505
1627
  }
@@ -1593,11 +1715,11 @@ RoxyGunaMilan.styles = [
1593
1715
  }
1594
1716
  .tags .dosha {
1595
1717
  background: color-mix(in srgb, var(--roxy-danger, #dc2626) 16%, transparent);
1596
- color: var(--roxy-danger, #dc2626);
1718
+ color: var(--roxy-danger-fg, #991b1b);
1597
1719
  }
1598
1720
  .tags .cancel {
1599
1721
  background: color-mix(in srgb, var(--roxy-success, #16a34a) 18%, transparent);
1600
- color: var(--roxy-success, #16a34a);
1722
+ color: var(--roxy-success-fg, #166534);
1601
1723
  }
1602
1724
  `
1603
1725
  ];
@@ -1653,7 +1775,12 @@ var PLANET_GLYPH = {
1653
1775
  Ascendant: "Asc",
1654
1776
  Lagna: "La",
1655
1777
  NorthNode: "\u260A",
1656
- SouthNode: "\u260B"
1778
+ SouthNode: "\u260B",
1779
+ "North node": "\u260A",
1780
+ "South node": "\u260B",
1781
+ Chiron: "\u26B7",
1782
+ Lilith: "\u26B8",
1783
+ "Black moon lilith": "\u26B8"
1657
1784
  };
1658
1785
  var PLANET_ABBR = {
1659
1786
  Sun: "Su",
@@ -1735,23 +1862,40 @@ var RoxyHexagram = class extends import_lit9.LitElement {
1735
1862
  this.data = null;
1736
1863
  this.mode = "lookup";
1737
1864
  }
1738
- getHexagram() {
1739
- if (!this.data) return null;
1740
- if ("hexagram" in this.data && this.data.hexagram) {
1865
+ resolveHexagram() {
1866
+ const d = this.data;
1867
+ if (!d) return null;
1868
+ if ("hexagram" in d && d.hexagram) {
1869
+ if ("lines" in d) {
1870
+ const cast = d;
1871
+ return {
1872
+ hex: cast.hexagram,
1873
+ lines: cast.lines,
1874
+ changingLinePositions: cast.changingLinePositions,
1875
+ resultingHexagram: cast.resultingHexagram
1876
+ };
1877
+ }
1878
+ const daily = d;
1741
1879
  return {
1742
- ...this.data.hexagram,
1743
- lines: this.data.lines,
1744
- changingLinePositions: this.data.changingLinePositions
1880
+ hex: daily.hexagram,
1881
+ dailyMessage: daily.dailyMessage
1745
1882
  };
1746
1883
  }
1747
- return this.data;
1884
+ return { hex: d };
1748
1885
  }
1749
1886
  render() {
1750
- const h = this.getHexagram();
1751
- if (!h)
1887
+ const resolved = this.resolveHexagram();
1888
+ if (!resolved)
1752
1889
  return import_lit9.html`<div class="roxy-empty" role="status">No hexagram data</div>`;
1753
- const lines = h.lines ?? this.derivedLines(h);
1754
- const changing = new Set(h.changingLinePositions ?? []);
1890
+ const {
1891
+ hex: h,
1892
+ lines: castLines,
1893
+ changingLinePositions,
1894
+ dailyMessage,
1895
+ resultingHexagram
1896
+ } = resolved;
1897
+ const lines = castLines ?? this.derivedLines(h);
1898
+ const changing = new Set(changingLinePositions ?? []);
1755
1899
  return import_lit9.html`<article class="card" aria-label="I Ching hexagram">
1756
1900
  <div class="glyphs">
1757
1901
  ${h.symbol ? import_lit9.html`<div class="symbol">${h.symbol}</div>` : import_lit9.nothing}
@@ -1791,19 +1935,18 @@ var RoxyHexagram = class extends import_lit9.LitElement {
1791
1935
  </div>
1792
1936
  ${h.judgment ? import_lit9.html`<p class="judgment">${h.judgment}</p>` : import_lit9.nothing}
1793
1937
  ${h.image ? import_lit9.html`<p class="image">${h.image}</p>` : import_lit9.nothing}
1794
- ${h.dailyMessage ? import_lit9.html`<p class="message">${h.dailyMessage}</p>` : import_lit9.nothing}
1938
+ ${dailyMessage ? import_lit9.html`<p class="message">${dailyMessage}</p>` : import_lit9.nothing}
1795
1939
  ${h.interpretation?.general ? import_lit9.html`<p>${h.interpretation.general}</p>` : import_lit9.nothing}
1796
1940
  ${changing.size > 0 ? import_lit9.html`<div class="changing">
1797
1941
  Changing lines: ${Array.from(changing).sort((a, b) => a - b).join(", ")}.
1798
- ${h.resultingHexagram?.english ? import_lit9.html` Becomes hexagram ${h.resultingHexagram.number}
1799
- ${h.resultingHexagram.english}.` : import_lit9.nothing}
1942
+ ${resultingHexagram?.english ? import_lit9.html` Becomes hexagram ${resultingHexagram.number}
1943
+ ${resultingHexagram.english}.` : import_lit9.nothing}
1800
1944
  </div>` : import_lit9.nothing}
1801
1945
  </div>
1802
1946
  </article>`;
1803
1947
  }
1804
1948
  /** When the API only ships symbol+number with no line array, render six solid yang. */
1805
1949
  derivedLines(h) {
1806
- if (!h.symbol) return Array.from({ length: 6 }, () => 7);
1807
1950
  const cp = h.symbol.codePointAt(0) ?? 0;
1808
1951
  if (cp >= 19904 && cp <= 19967) {
1809
1952
  const offset = cp - 19904;
@@ -1953,8 +2096,8 @@ var RoxyHoroscopeCard = class extends import_lit10.LitElement {
1953
2096
  return import_lit10.html`<div class="roxy-empty" role="status">No horoscope data</div>`;
1954
2097
  const sign = d.sign ?? "";
1955
2098
  const glyph = sign ? SIGN_GLYPH[capitalize(sign)] ?? "" : "";
1956
- const energy = typeof d.energyRating === "number" ? d.energyRating : null;
1957
- const dateLabel = d.date ?? d.week ?? d.month ?? "";
2099
+ const energy = "energyRating" in d && typeof d.energyRating === "number" ? d.energyRating : null;
2100
+ const dateLabel = "date" in d && d.date || "week" in d && d.week || "month" in d && d.month || "";
1958
2101
  return import_lit10.html`<article
1959
2102
  class="card"
1960
2103
  aria-label=${`${this.period} horoscope for ${sign}`}
@@ -1992,31 +2135,40 @@ var RoxyHoroscopeCard = class extends import_lit10.LitElement {
1992
2135
  <h3>Finance</h3>
1993
2136
  <p>${d.finance}</p>
1994
2137
  </div>` : import_lit10.nothing}
1995
- ${d.advice ? import_lit10.html`<div class="section">
2138
+ ${"advice" in d && d.advice ? import_lit10.html`<div class="section">
1996
2139
  <h3>Advice</h3>
1997
2140
  <p>${d.advice}</p>
1998
2141
  </div>` : import_lit10.nothing}
1999
2142
  </div>
2000
2143
 
2001
- ${d.luckyNumber || d.luckyColor || (d.compatibleSigns?.length ?? 0) > 0 ? import_lit10.html`<div class="lucky">
2002
- ${d.luckyNumber !== void 0 ? import_lit10.html`<span>Lucky number <strong>${d.luckyNumber}</strong></span>` : import_lit10.nothing}
2003
- ${d.luckyColor ? import_lit10.html`<span>Lucky color <strong>${d.luckyColor}</strong></span>` : import_lit10.nothing}
2004
- ${d.luckyNumbers?.length ? import_lit10.html`<span
2144
+ ${(() => {
2145
+ const luckyNumber = "luckyNumber" in d && d.luckyNumber !== void 0 ? d.luckyNumber : void 0;
2146
+ const luckyColor = "luckyColor" in d && d.luckyColor ? d.luckyColor : "";
2147
+ const luckyNumbers = "luckyNumbers" in d && d.luckyNumbers ? d.luckyNumbers : [];
2148
+ const luckyDays = "luckyDays" in d && d.luckyDays ? d.luckyDays : [];
2149
+ const compatibleSigns = d.compatibleSigns ?? [];
2150
+ if (luckyNumber === void 0 && !luckyColor && luckyNumbers.length === 0 && luckyDays.length === 0 && compatibleSigns.length === 0)
2151
+ return import_lit10.nothing;
2152
+ return import_lit10.html`<div class="lucky">
2153
+ ${luckyNumber !== void 0 ? import_lit10.html`<span>Lucky number <strong>${luckyNumber}</strong></span>` : import_lit10.nothing}
2154
+ ${luckyColor ? import_lit10.html`<span>Lucky color <strong>${luckyColor}</strong></span>` : import_lit10.nothing}
2155
+ ${luckyNumbers.length ? import_lit10.html`<span
2005
2156
  >Lucky numbers
2006
- <strong>${d.luckyNumbers.join(", ")}</strong></span
2157
+ <strong>${luckyNumbers.join(", ")}</strong></span
2007
2158
  >` : import_lit10.nothing}
2008
- ${d.luckyDays?.length ? import_lit10.html`<span
2009
- >Lucky days <strong>${d.luckyDays.join(", ")}</strong></span
2159
+ ${luckyDays.length ? import_lit10.html`<span
2160
+ >Lucky days <strong>${luckyDays.join(", ")}</strong></span
2010
2161
  >` : import_lit10.nothing}
2011
- ${d.compatibleSigns?.length ? import_lit10.html`<span class="compat-wrap">
2162
+ ${compatibleSigns.length ? import_lit10.html`<span class="compat-wrap">
2012
2163
  Best with
2013
2164
  <span class="compat"
2014
- >${d.compatibleSigns.map(
2015
- (s) => import_lit10.html`<span>${s}</span>`
2016
- )}</span
2165
+ >${compatibleSigns.map(
2166
+ (s) => import_lit10.html`<span>${s}</span>`
2167
+ )}</span
2017
2168
  >
2018
2169
  </span>` : import_lit10.nothing}
2019
- </div>` : import_lit10.nothing}
2170
+ </div>`;
2171
+ })()}
2020
2172
  </article>`;
2021
2173
  }
2022
2174
  };
@@ -2171,7 +2323,7 @@ var RoxyKpPlanetsTable = class extends import_lit11.LitElement {
2171
2323
  >
2172
2324
  <header class="head">
2173
2325
  <h2 class="title">KP planets</h2>
2174
- ${this.data.ayanamsa ? import_lit11.html`<span class="ayanamsa">Ayanamsa: ${this.data.ayanamsa}</span>` : import_lit11.nothing}
2326
+ ${typeof this.data.ayanamsa === "number" ? import_lit11.html`<span class="ayanamsa">Ayanamsa: ${formatNumber(this.data.ayanamsa, 2)}°</span>` : import_lit11.nothing}
2175
2327
  </header>
2176
2328
  <table role="table">
2177
2329
  <thead>
@@ -2190,13 +2342,13 @@ var RoxyKpPlanetsTable = class extends import_lit11.LitElement {
2190
2342
  ${planets.map(
2191
2343
  (p) => import_lit11.html`<tr>
2192
2344
  <td class="planet">
2193
- ${p.planet ?? p.name ?? ""}
2345
+ ${p.planet}
2194
2346
  ${p.retrograde ? import_lit11.html`<span class="retro">R</span>` : import_lit11.nothing}
2195
2347
  </td>
2196
2348
  <td>${p.sign ?? ""}</td>
2197
2349
  <td>${p.signLord ?? ""}</td>
2198
2350
  <td>${p.nakshatra ?? ""}</td>
2199
- <td>${p.starLord ?? p.nakshatraLord ?? ""}</td>
2351
+ <td>${p.nakshatraLord ?? ""}</td>
2200
2352
  <td>${p.subLord ?? ""}</td>
2201
2353
  <td>${p.subSubLord ?? ""}</td>
2202
2354
  <td>${p.kpNumber ?? ""}</td>
@@ -2264,7 +2416,7 @@ RoxyKpPlanetsTable.styles = [
2264
2416
  color: var(--roxy-fg, #0a0a0a);
2265
2417
  }
2266
2418
  .retro {
2267
- color: var(--roxy-warning, #ea580c);
2419
+ color: var(--roxy-warning-fg, #9a3412);
2268
2420
  font-size: var(--roxy-text-xs, 0.75rem);
2269
2421
  margin-left: 4px;
2270
2422
  }
@@ -2284,10 +2436,20 @@ var import_decorators11 = require("lit/decorators.js");
2284
2436
  // packages/ui/src/utils/debounce.ts
2285
2437
  function debounce(fn, wait) {
2286
2438
  let timer;
2287
- return ((...args) => {
2439
+ const debounced = ((...args) => {
2288
2440
  if (timer) clearTimeout(timer);
2289
- timer = setTimeout(() => fn(...args), wait);
2441
+ timer = setTimeout(() => {
2442
+ timer = void 0;
2443
+ fn(...args);
2444
+ }, wait);
2290
2445
  });
2446
+ debounced.cancel = () => {
2447
+ if (timer) {
2448
+ clearTimeout(timer);
2449
+ timer = void 0;
2450
+ }
2451
+ };
2452
+ return debounced;
2291
2453
  }
2292
2454
 
2293
2455
  // packages/ui/src/components/location-search.ts
@@ -2302,6 +2464,7 @@ var RoxyLocationSearch = class extends import_lit12.LitElement {
2302
2464
  this.isOpen = false;
2303
2465
  this.isLoading = false;
2304
2466
  this.highlight = -1;
2467
+ this.secretKeyWarned = false;
2305
2468
  this.debouncedFetch = debounce((q) => {
2306
2469
  void this.fetchResults(q);
2307
2470
  }, 300);
@@ -2353,8 +2516,32 @@ var RoxyLocationSearch = class extends import_lit12.LitElement {
2353
2516
  if (this.clickOutsideHandler) {
2354
2517
  document.removeEventListener("mousedown", this.clickOutsideHandler);
2355
2518
  }
2519
+ this.debouncedFetch.cancel();
2520
+ if (this.abortController) {
2521
+ this.abortController.abort();
2522
+ this.abortController = void 0;
2523
+ }
2524
+ }
2525
+ warnIfSecretKey() {
2526
+ if (this.secretKeyWarned) return;
2527
+ if (!this.apiKey) return;
2528
+ if (this.apiKey.startsWith("pk_")) return;
2529
+ this.secretKeyWarned = true;
2530
+ const message = "Possible secret key in client-side <roxy-location-search>; use a `pk_` publishable key with origin allowlist instead.";
2531
+ console.warn(message);
2532
+ this.dispatchEvent(
2533
+ new CustomEvent("roxy-validation-error", {
2534
+ detail: { reason: "possible-secret-key", message },
2535
+ bubbles: true,
2536
+ composed: true
2537
+ })
2538
+ );
2356
2539
  }
2357
2540
  async fetchResults(q) {
2541
+ this.warnIfSecretKey();
2542
+ if (this.abortController) this.abortController.abort();
2543
+ const controller = new AbortController();
2544
+ this.abortController = controller;
2358
2545
  this.isLoading = true;
2359
2546
  try {
2360
2547
  const url = new URL(this.endpoint);
@@ -2365,17 +2552,22 @@ var RoxyLocationSearch = class extends import_lit12.LitElement {
2365
2552
  };
2366
2553
  if (this.apiKey) headers["X-API-Key"] = this.apiKey;
2367
2554
  if (this.publishableKey) headers["X-API-Key"] = this.publishableKey;
2368
- const res = await fetch(url, { headers });
2555
+ const res = await fetch(url, { headers, signal: controller.signal });
2369
2556
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
2370
2557
  const json = await res.json();
2558
+ if (controller.signal.aborted) return;
2371
2559
  this.results = json.cities ?? [];
2372
2560
  this.isOpen = this.results.length > 0;
2373
2561
  this.highlight = this.results.length > 0 ? 0 : -1;
2374
- } catch (_err) {
2562
+ } catch (err) {
2563
+ if (err?.name === "AbortError") return;
2375
2564
  this.results = [];
2376
2565
  this.isOpen = false;
2377
2566
  } finally {
2378
- this.isLoading = false;
2567
+ if (this.abortController === controller) {
2568
+ this.abortController = void 0;
2569
+ }
2570
+ if (!controller.signal.aborted) this.isLoading = false;
2379
2571
  }
2380
2572
  }
2381
2573
  select(city) {
@@ -2591,18 +2783,21 @@ var RoxyMoonPhase = class extends import_lit13.LitElement {
2591
2783
  const d = this.data;
2592
2784
  if (!d)
2593
2785
  return import_lit13.html`<div class="roxy-empty" role="status">No moon phase data</div>`;
2594
- const list = d.phases ?? d.upcoming ?? [];
2786
+ const list = "phases" in d ? d.phases : "calendar" in d ? d.calendar : [];
2595
2787
  if (this.mode !== "current" && list.length > 0) {
2788
+ const month = "month" in d ? d.month : void 0;
2789
+ const year = "year" in d ? d.year : void 0;
2596
2790
  return import_lit13.html`<article
2597
2791
  class="card"
2598
2792
  aria-label="Moon phase calendar"
2599
2793
  >
2600
- <h2 class="label">${d.month ?? "Moon phases"} ${d.year ?? ""}</h2>
2794
+ <h2 class="label">${month ?? "Moon phases"} ${year ?? ""}</h2>
2601
2795
  <div class="list" role="list">
2602
2796
  ${list.map((phase) => this.renderListItem(phase))}
2603
2797
  </div>
2604
2798
  </article>`;
2605
2799
  }
2800
+ if (!("phase" in d)) return import_lit13.nothing;
2606
2801
  return this.renderSingle(d);
2607
2802
  }
2608
2803
  renderSingle(d) {
@@ -2618,11 +2813,11 @@ var RoxyMoonPhase = class extends import_lit13.LitElement {
2618
2813
  <div class="stats">
2619
2814
  ${typeof d.illumination === "number" ? import_lit13.html`<div>
2620
2815
  <span>Illumination</span>
2621
- <strong>${(d.illumination * 100).toFixed(0)}%</strong>
2816
+ <strong>${formatIllumination(d.illumination)}</strong>
2622
2817
  </div>` : import_lit13.nothing}
2623
2818
  ${typeof d.age === "number" ? import_lit13.html`<div>
2624
2819
  <span>Age</span>
2625
- <strong>${d.age.toFixed(1)} days</strong>
2820
+ <strong>${formatNumber(d.age, 1)} days</strong>
2626
2821
  </div>` : import_lit13.nothing}
2627
2822
  ${d.sign ? import_lit13.html`<div>
2628
2823
  <span>Sign</span>
@@ -2747,6 +2942,10 @@ function phaseEmoji(phase) {
2747
2942
  if (!phase) return "\u{1F319}";
2748
2943
  return MOON_PHASE_EMOJI[phase.toLowerCase()] ?? "\u{1F319}";
2749
2944
  }
2945
+ function formatIllumination(v) {
2946
+ const pct = v <= 1 ? v * 100 : v;
2947
+ return `${Math.round(pct)}%`;
2948
+ }
2750
2949
 
2751
2950
  // packages/ui/src/components/natal-chart.ts
2752
2951
  var import_lit14 = require("lit");
@@ -2762,12 +2961,14 @@ function polarToCartesian(cx, cy, radius, angleDeg) {
2762
2961
  }
2763
2962
 
2764
2963
  // packages/ui/src/components/natal-chart.ts
2765
- var SIZE = 320;
2964
+ var SIZE = 384;
2766
2965
  var CENTER = SIZE / 2;
2767
2966
  var OUTER_R = 150;
2768
2967
  var SIGN_R = 134;
2769
2968
  var HOUSE_R = 110;
2770
2969
  var PLANET_R = 88;
2970
+ var ANGLE_TICK_R = 162;
2971
+ var ANGLE_LABEL_R = 176;
2771
2972
  var RoxyNatalChart = class extends import_lit14.LitElement {
2772
2973
  constructor() {
2773
2974
  super(...arguments);
@@ -2775,10 +2976,17 @@ var RoxyNatalChart = class extends import_lit14.LitElement {
2775
2976
  this.houseSystem = "placidus";
2776
2977
  }
2777
2978
  getPlanets() {
2778
- const p = this.data?.planets;
2779
- if (!p) return [];
2780
- if (Array.isArray(p)) return p;
2781
- return Object.entries(p).map(([name, entry]) => ({ ...entry, name }));
2979
+ return this.data?.planets ?? [];
2980
+ }
2981
+ getAscendant() {
2982
+ return this.data?.ascendant?.longitude ?? 0;
2983
+ }
2984
+ getMidheaven() {
2985
+ const m = this.data?.midheaven?.longitude;
2986
+ return typeof m === "number" ? m : null;
2987
+ }
2988
+ toAngle(lon) {
2989
+ return 180 + this.getAscendant() - lon;
2782
2990
  }
2783
2991
  render() {
2784
2992
  if (!this.data)
@@ -2789,11 +2997,7 @@ var RoxyNatalChart = class extends import_lit14.LitElement {
2789
2997
  <header>
2790
2998
  <h2 class="title">Natal chart</h2>
2791
2999
  ${this.data.birthDetails ? import_lit14.html`<div class="meta">
2792
- ${[
2793
- this.data.birthDetails.date,
2794
- this.data.birthDetails.time,
2795
- this.data.birthDetails.location
2796
- ].filter(Boolean).join(" \xB7 ")}
3000
+ ${[this.data.birthDetails.date, this.data.birthDetails.time].filter(Boolean).join(" \xB7 ")}
2797
3001
  </div>` : import_lit14.nothing}
2798
3002
  </header>
2799
3003
  <svg
@@ -2829,17 +3033,38 @@ var RoxyNatalChart = class extends import_lit14.LitElement {
2829
3033
  />
2830
3034
  ${this.renderSpokes()} ${this.renderSigns()} ${this.renderHouseNumbers()}
2831
3035
  ${this.renderAspects(planets, aspects)} ${this.renderPlanets(planets)}
3036
+ ${this.renderAngles()}
2832
3037
  </svg>
2833
3038
  <div class="legend">
2834
3039
  <span>${planets.length} planets</span>
2835
3040
  <span>${aspects.length} aspects</span>
2836
- <span>House system: ${this.houseSystem}</span>
3041
+ <span><span class="legend-swatch" style="background: var(--roxy-success)"></span>harmonious</span>
3042
+ <span><span class="legend-swatch" style="background: var(--roxy-danger)"></span>challenging</span>
2837
3043
  </div>
2838
3044
  </div>`;
2839
3045
  }
3046
+ renderAngles() {
3047
+ const asc = this.getAscendant();
3048
+ const mc = this.getMidheaven();
3049
+ const items = [this.renderAngleMark(asc, "ASC")];
3050
+ if (mc !== null) items.push(this.renderAngleMark(mc, "MC"));
3051
+ return items;
3052
+ }
3053
+ renderAngleMark(longitude, label) {
3054
+ const angle = this.toAngle(longitude);
3055
+ const tickInner = polarToCartesian(CENTER, CENTER, OUTER_R, angle);
3056
+ const tickOuter = polarToCartesian(CENTER, CENTER, ANGLE_TICK_R, angle);
3057
+ const labelPos = polarToCartesian(CENTER, CENTER, ANGLE_LABEL_R, angle);
3058
+ return import_lit14.svg`
3059
+ <g>
3060
+ <line class="angle-tick" x1=${tickInner.x} y1=${tickInner.y} x2=${tickOuter.x} y2=${tickOuter.y} />
3061
+ <text class="angle-marker" x=${labelPos.x} y=${labelPos.y} text-anchor="middle" dominant-baseline="central">${label}</text>
3062
+ </g>
3063
+ `;
3064
+ }
2840
3065
  renderSpokes() {
2841
3066
  return Array.from({ length: 12 }, (_, i) => {
2842
- const angle = i * 30 - 90;
3067
+ const angle = this.toAngle(i * 30);
2843
3068
  const start = polarToCartesian(CENTER, CENTER, HOUSE_R, angle);
2844
3069
  const end = polarToCartesian(CENTER, CENTER, OUTER_R, angle);
2845
3070
  return import_lit14.svg`<line class="wheel-line" x1=${start.x} y1=${start.y} x2=${end.x} y2=${end.y} stroke-width="0.8" />`;
@@ -2861,45 +3086,58 @@ var RoxyNatalChart = class extends import_lit14.LitElement {
2861
3086
  "Pisces"
2862
3087
  ];
2863
3088
  return order.map((sign, i) => {
2864
- const angle = i * 30 + 15 - 90;
3089
+ const angle = this.toAngle(i * 30 + 15);
2865
3090
  const pos = polarToCartesian(CENTER, CENTER, SIGN_R, angle);
2866
3091
  return import_lit14.svg`<text class="sign-glyph" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central">${SIGN_GLYPH[sign]}</text>`;
2867
3092
  });
2868
3093
  }
2869
3094
  renderHouseNumbers() {
3095
+ const ascSignIndex = Math.floor(this.getAscendant() / 30);
2870
3096
  return Array.from({ length: 12 }, (_, i) => {
2871
- const angle = i * 30 + 15 - 90;
3097
+ const angle = this.toAngle(i * 30 + 15);
2872
3098
  const pos = polarToCartesian(CENTER, CENTER, HOUSE_R - 12, angle);
2873
- return import_lit14.svg`<text class="house-num" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central">${i + 1}</text>`;
3099
+ const houseNum = (i - ascSignIndex + 12) % 12 + 1;
3100
+ return import_lit14.svg`<text class="house-num" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central">${houseNum}</text>`;
2874
3101
  });
2875
3102
  }
2876
3103
  renderPlanets(planets) {
2877
3104
  return planets.map((p) => {
2878
- const lon = typeof p.longitude === "number" ? p.longitude : typeof p.degree === "number" ? p.degree : NaN;
2879
- if (!Number.isFinite(lon)) return import_lit14.nothing;
2880
- const angle = lon - 90;
3105
+ if (!Number.isFinite(p.longitude)) return import_lit14.nothing;
3106
+ const angle = this.toAngle(p.longitude);
2881
3107
  const pos = polarToCartesian(CENTER, CENTER, PLANET_R, angle);
2882
- const name = p.name ?? p.planet ?? "";
2883
- const glyph = PLANET_GLYPH[capitalize2(name)] ?? name.slice(0, 2);
2884
- const retro = p.retrograde || p.isRetrograde ? " R" : "";
2885
- return import_lit14.svg`<text class="planet-glyph" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central"><title>${name}${retro}</title>${glyph}</text>`;
3108
+ const glyph = PLANET_GLYPH[capitalize2(p.name)] ?? p.name.slice(0, 2);
3109
+ const retro = p.isRetrograde ? " R" : "";
3110
+ const display = retro ? `${glyph}\u1D3F` : glyph;
3111
+ return import_lit14.svg`<text class="planet-glyph" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central"><title>${p.name}${retro}</title>${display}</text>`;
2886
3112
  });
2887
3113
  }
2888
3114
  renderAspects(planets, aspects) {
2889
3115
  const planetMap = /* @__PURE__ */ new Map();
2890
3116
  for (const p of planets) {
2891
- const lon = typeof p.longitude === "number" ? p.longitude : typeof p.degree === "number" ? p.degree : null;
2892
- if (lon === null) continue;
2893
- const name = capitalize2(p.name ?? p.planet ?? "");
2894
- if (name) planetMap.set(name, lon);
3117
+ if (typeof p.longitude !== "number") continue;
3118
+ const name = capitalize2(p.name);
3119
+ if (name) planetMap.set(name, p.longitude);
2895
3120
  }
2896
3121
  return aspects.map((a) => {
2897
- const l1 = planetMap.get(capitalize2(a.planet1 ?? ""));
2898
- const l2 = planetMap.get(capitalize2(a.planet2 ?? ""));
3122
+ const l1 = planetMap.get(capitalize2(a.planet1));
3123
+ const l2 = planetMap.get(capitalize2(a.planet2));
2899
3124
  if (l1 === void 0 || l2 === void 0) return import_lit14.nothing;
2900
- const p1 = polarToCartesian(CENTER, CENTER, PLANET_R - 18, l1 - 90);
2901
- const p2 = polarToCartesian(CENTER, CENTER, PLANET_R - 18, l2 - 90);
2902
- return import_lit14.svg`<line class="aspect" x1=${p1.x} y1=${p1.y} x2=${p2.x} y2=${p2.y} />`;
3125
+ const p1 = polarToCartesian(
3126
+ CENTER,
3127
+ CENTER,
3128
+ PLANET_R - 18,
3129
+ this.toAngle(l1)
3130
+ );
3131
+ const p2 = polarToCartesian(
3132
+ CENTER,
3133
+ CENTER,
3134
+ PLANET_R - 18,
3135
+ this.toAngle(l2)
3136
+ );
3137
+ const aspectName = normalizeAspect(a);
3138
+ const aspectClass = ASPECT_CLASS[aspectName] ?? "aspect-other";
3139
+ const orbLabel = formatNumber(a.orb, 1);
3140
+ return import_lit14.svg`<line class=${`aspect ${aspectClass}`} x1=${p1.x} y1=${p1.y} x2=${p2.x} y2=${p2.y}><title>${a.planet1} ${aspectName || ""} ${a.planet2}${orbLabel ? ` (orb ${orbLabel}\xB0)` : ""}</title></line>`;
2903
3141
  });
2904
3142
  }
2905
3143
  };
@@ -2957,9 +3195,36 @@ RoxyNatalChart.styles = [
2957
3195
  }
2958
3196
 
2959
3197
  .aspect {
2960
- stroke: color-mix(in srgb, var(--roxy-accent, #f59e0b) 32%, transparent);
2961
- stroke-width: 0.6;
3198
+ stroke-width: 0.8;
2962
3199
  fill: none;
3200
+ opacity: 0.55;
3201
+ }
3202
+ .aspect-trine,
3203
+ .aspect-sextile {
3204
+ stroke: var(--roxy-success, #16a34a);
3205
+ }
3206
+ .aspect-square,
3207
+ .aspect-opposition {
3208
+ stroke: var(--roxy-danger, #dc2626);
3209
+ }
3210
+ .aspect-conjunction {
3211
+ stroke: var(--roxy-accent-fg, #b45309);
3212
+ }
3213
+ .aspect-other {
3214
+ stroke: var(--roxy-muted, #71717a);
3215
+ opacity: 0.4;
3216
+ }
3217
+
3218
+ .angle-marker {
3219
+ fill: var(--roxy-accent-fg, #b45309);
3220
+ font-size: 10px;
3221
+ font-weight: 700;
3222
+ font-family: var(--roxy-font-sans);
3223
+ letter-spacing: 0.04em;
3224
+ }
3225
+ .angle-tick {
3226
+ stroke: var(--roxy-accent-fg, #b45309);
3227
+ stroke-width: 1.5;
2963
3228
  }
2964
3229
 
2965
3230
  .legend {
@@ -2969,6 +3234,14 @@ RoxyNatalChart.styles = [
2969
3234
  flex-wrap: wrap;
2970
3235
  gap: var(--roxy-space-md, 1rem);
2971
3236
  }
3237
+ .legend-swatch {
3238
+ display: inline-block;
3239
+ width: 8px;
3240
+ height: 8px;
3241
+ border-radius: 50%;
3242
+ margin-right: 4px;
3243
+ vertical-align: middle;
3244
+ }
2972
3245
  `
2973
3246
  ];
2974
3247
  __decorateClass([
@@ -2984,6 +3257,16 @@ function capitalize2(s) {
2984
3257
  if (!s) return "";
2985
3258
  return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
2986
3259
  }
3260
+ var ASPECT_CLASS = {
3261
+ conjunction: "aspect-conjunction",
3262
+ sextile: "aspect-sextile",
3263
+ square: "aspect-square",
3264
+ trine: "aspect-trine",
3265
+ opposition: "aspect-opposition"
3266
+ };
3267
+ function normalizeAspect(a) {
3268
+ return (a.type ?? "").toLowerCase().replace(/_/g, "-");
3269
+ }
2987
3270
 
2988
3271
  // packages/ui/src/components/numerology-card.ts
2989
3272
  var import_lit15 = require("lit");
@@ -2999,42 +3282,63 @@ var RoxyNumerologyCard = class extends import_lit15.LitElement {
2999
3282
  if (!d)
3000
3283
  return import_lit15.html`<div class="roxy-empty" role="status">No numerology data</div>`;
3001
3284
  const headerLabel = LABELS[this.type] ?? this.type;
3002
- const number = d.personalYear ?? d.number;
3003
- const cores = d.coreNumbers ? Object.entries(d.coreNumbers).filter(
3004
- ([, v]) => v !== null && v !== void 0
3005
- ) : [];
3006
- return import_lit15.html`<article
3007
- class="card"
3008
- aria-label=${headerLabel}
3009
- >
3285
+ if ("coreNumbers" in d) return this.renderChart(d, headerLabel);
3286
+ if ("personalYear" in d) return this.renderPersonalYear(d, headerLabel);
3287
+ return this.renderNumberCard(
3288
+ d,
3289
+ headerLabel
3290
+ );
3291
+ }
3292
+ renderNumberCard(d, headerLabel) {
3293
+ const keywords = d.meaning?.keywords ?? [];
3294
+ return import_lit15.html`<article class="card" aria-label=${headerLabel}>
3010
3295
  <div class="hero">
3011
- ${typeof number === "number" ? import_lit15.html`<div class="numeral">${number}</div>` : import_lit15.nothing}
3296
+ ${typeof d.number === "number" ? import_lit15.html`<div class="numeral">${d.number}</div>` : import_lit15.nothing}
3012
3297
  <div>
3013
3298
  <p class="label">${headerLabel}</p>
3014
- ${d.title ? import_lit15.html`<h2 class="title">${d.title}</h2>` : d.type ? import_lit15.html`<h2 class="title">
3015
- ${d.type === "master" ? "Master number" : "Single digit"}
3016
- </h2>` : import_lit15.nothing}
3299
+ ${d.meaning?.title ? import_lit15.html`<h2 class="title">${d.meaning.title}</h2>` : import_lit15.nothing}
3017
3300
  </div>
3018
3301
  </div>
3019
- ${d.theme ? import_lit15.html`<p><strong>Theme:</strong> ${d.theme}</p>` : import_lit15.nothing}
3020
- ${d.meaning ? import_lit15.html`<p class="meaning">${d.meaning}</p>` : import_lit15.nothing}
3021
- ${d.advice ? import_lit15.html`<p>${d.advice}</p>` : import_lit15.nothing}
3302
+ ${d.meaning?.description ? import_lit15.html`<p class="meaning">${d.meaning.description}</p>` : import_lit15.nothing}
3022
3303
  ${d.calculation ? import_lit15.html`<pre class="calc">${d.calculation}</pre>` : import_lit15.nothing}
3023
- ${d.keywords?.length ? import_lit15.html`<div class="chips">
3024
- ${d.keywords.map((k) => import_lit15.html`<span>${k}</span>`)}
3025
- </div>` : import_lit15.nothing}
3026
- ${cores.length > 0 ? import_lit15.html`<div class="cores">
3027
- ${cores.map(([k, v]) => {
3028
- const value = typeof v === "number" ? v : v.number;
3029
- return import_lit15.html`<div class="item">
3030
- <span>${humanize2(k)}</span>
3031
- <strong>${value ?? ""}</strong>
3032
- </div>`;
3033
- })}
3304
+ ${keywords.length > 0 ? import_lit15.html`<div class="chips">
3305
+ ${keywords.map((k) => import_lit15.html`<span>${k}</span>`)}
3034
3306
  </div>` : import_lit15.nothing}
3035
3307
  ${d.hasKarmicDebt && d.karmicDebtNumber ? import_lit15.html`<div class="karmic">
3036
3308
  Karmic debt ${d.karmicDebtNumber}.
3037
- ${d.karmicDebtMeaning ? d.karmicDebtMeaning : ""}
3309
+ ${karmicDebtText(d.karmicDebtMeaning)}
3310
+ </div>` : import_lit15.nothing}
3311
+ </article>`;
3312
+ }
3313
+ renderPersonalYear(d, headerLabel) {
3314
+ return import_lit15.html`<article class="card" aria-label=${headerLabel}>
3315
+ <div class="hero">
3316
+ ${typeof d.personalYear === "number" ? import_lit15.html`<div class="numeral">${d.personalYear}</div>` : import_lit15.nothing}
3317
+ <div>
3318
+ <p class="label">${headerLabel}</p>
3319
+ ${d.theme ? import_lit15.html`<h2 class="title">${d.theme}</h2>` : import_lit15.nothing}
3320
+ </div>
3321
+ </div>
3322
+ ${d.forecast ? import_lit15.html`<p class="meaning">${d.forecast}</p>` : import_lit15.nothing}
3323
+ ${d.advice ? import_lit15.html`<p>${d.advice}</p>` : import_lit15.nothing}
3324
+ </article>`;
3325
+ }
3326
+ renderChart(d, headerLabel) {
3327
+ const cores = Object.entries(d.coreNumbers).filter(
3328
+ ([, v]) => v !== null && v !== void 0
3329
+ );
3330
+ return import_lit15.html`<article class="card" aria-label=${headerLabel}>
3331
+ <div>
3332
+ <p class="label">${headerLabel}</p>
3333
+ ${d.profile?.name ? import_lit15.html`<h2 class="title">${d.profile.name}</h2>` : import_lit15.nothing}
3334
+ </div>
3335
+ ${cores.length > 0 ? import_lit15.html`<div class="cores">
3336
+ ${cores.map(
3337
+ ([k, v]) => import_lit15.html`<div class="item">
3338
+ <span>${humanize2(k)}</span>
3339
+ <strong>${v.number ?? ""}</strong>
3340
+ </div>`
3341
+ )}
3038
3342
  </div>` : import_lit15.nothing}
3039
3343
  </article>`;
3040
3344
  }
@@ -3089,7 +3393,8 @@ RoxyNumerologyCard.styles = [
3089
3393
  background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 30%, transparent);
3090
3394
  padding: var(--roxy-space-sm, 0.5rem);
3091
3395
  border-radius: var(--roxy-radius-sm, 4px);
3092
- word-break: break-all;
3396
+ white-space: pre-wrap;
3397
+ overflow-wrap: anywhere;
3093
3398
  }
3094
3399
 
3095
3400
  .chips {
@@ -3153,6 +3458,10 @@ var LABELS = {
3153
3458
  "personal-year": "Personal Year",
3154
3459
  chart: "Numerology chart"
3155
3460
  };
3461
+ function karmicDebtText(value) {
3462
+ if (!value) return "";
3463
+ return [value.description, value.challenge, value.resolution].filter(Boolean).join(" ");
3464
+ }
3156
3465
  function humanize2(s) {
3157
3466
  return s.replace(/[_-]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^\w/, (c) => c.toUpperCase());
3158
3467
  }
@@ -3170,31 +3479,32 @@ var RoxyPanchangTable = class extends import_lit16.LitElement {
3170
3479
  const d = this.data;
3171
3480
  if (!d)
3172
3481
  return import_lit16.html`<div class="roxy-empty" role="status">No panchang data</div>`;
3482
+ const detailed = "sunrise" in d ? d : null;
3173
3483
  const fivefold = [
3174
3484
  ["Tithi", this.formatPart(d.tithi)],
3175
3485
  ["Nakshatra", this.formatPart(d.nakshatra)],
3176
3486
  ["Yoga", this.formatPart(d.yoga)],
3177
- ["Karana", this.formatPart(d.karana)],
3178
- ["Vara", d.vara ?? ""]
3179
- ];
3180
- const muhurtas = [
3181
- ["Brahma Muhurta", d.brahmaMuhurta],
3182
- ["Abhijit Muhurta", d.abhijitMuhurta],
3183
- ["Vijaya Muhurta", d.vijayaMuhurta],
3184
- ["Godhuli Muhurta", d.godhuliMuhurta],
3185
- ["Nishita Muhurta", d.nishitaMuhurta],
3186
- ["Pratah Sandhya", d.pratahSandhya],
3187
- ["Sayahna Sandhya", d.sayahnaSandhya]
3188
- ];
3189
- const inauspicious = [
3190
- ["Rahu Kaal", d.rahuKaal],
3191
- ["Yamaganda", d.yamaganda],
3192
- ["Gulika", d.gulika]
3487
+ ["Karana", this.formatPart(d.karana)]
3193
3488
  ];
3489
+ if (detailed) fivefold.push(["Vara", this.formatPart(detailed.vara)]);
3490
+ const muhurtas = detailed ? [
3491
+ ["Brahma Muhurta", detailed.brahmaMuhurta],
3492
+ ["Abhijit Muhurta", detailed.abhijitMuhurta],
3493
+ ["Vijaya Muhurta", detailed.vijayaMuhurta],
3494
+ ["Godhuli Muhurta", detailed.godhuliMuhurta],
3495
+ ["Nishita Muhurta", detailed.nishitaMuhurta],
3496
+ ["Pratah Sandhya", detailed.pratahSandhya],
3497
+ ["Sayahna Sandhya", detailed.sayahnaSandhya]
3498
+ ] : [];
3499
+ const inauspicious = detailed ? [
3500
+ ["Rahu Kaal", detailed.rahuKaal],
3501
+ ["Yamaganda", detailed.yamaganda],
3502
+ ["Gulika", detailed.gulika]
3503
+ ] : [];
3194
3504
  return import_lit16.html`<div class="wrap" aria-label="Panchang">
3195
3505
  <header class="head">
3196
3506
  <h2 class="title">Panchang</h2>
3197
- <span class="date">${d.date ?? ""}</span>
3507
+ <span class="date">${detailed ? formatDate(detailed.date) : ""}</span>
3198
3508
  </header>
3199
3509
  <table>
3200
3510
  <tbody>
@@ -3204,21 +3514,21 @@ var RoxyPanchangTable = class extends import_lit16.LitElement {
3204
3514
  <td>${v}</td>
3205
3515
  </tr>`
3206
3516
  )}
3207
- ${d.sunrise ? import_lit16.html`<tr>
3517
+ ${detailed?.sunrise ? import_lit16.html`<tr>
3208
3518
  <th>Sunrise</th>
3209
- <td>${d.sunrise}</td>
3519
+ <td>${formatTime(detailed.sunrise)}</td>
3210
3520
  </tr>` : import_lit16.nothing}
3211
- ${d.sunset ? import_lit16.html`<tr>
3521
+ ${detailed?.sunset ? import_lit16.html`<tr>
3212
3522
  <th>Sunset</th>
3213
- <td>${d.sunset}</td>
3523
+ <td>${formatTime(detailed.sunset)}</td>
3214
3524
  </tr>` : import_lit16.nothing}
3215
- ${d.moonrise ? import_lit16.html`<tr>
3525
+ ${detailed?.moonrise ? import_lit16.html`<tr>
3216
3526
  <th>Moonrise</th>
3217
- <td>${d.moonrise}</td>
3527
+ <td>${formatTime(detailed.moonrise)}</td>
3218
3528
  </tr>` : import_lit16.nothing}
3219
- ${d.moonset ? import_lit16.html`<tr>
3529
+ ${detailed?.moonset ? import_lit16.html`<tr>
3220
3530
  <th>Moonset</th>
3221
- <td>${d.moonset}</td>
3531
+ <td>${formatTime(detailed.moonset)}</td>
3222
3532
  </tr>` : import_lit16.nothing}
3223
3533
  </tbody>
3224
3534
  </table>
@@ -3229,7 +3539,7 @@ var RoxyPanchangTable = class extends import_lit16.LitElement {
3229
3539
  ${muhurtas.filter(([, v]) => !!v).map(
3230
3540
  ([k, v]) => import_lit16.html`<tr>
3231
3541
  <th>${k}</th>
3232
- <td>${formatRange(v)}</td>
3542
+ <td>${formatTimeRange(v)}</td>
3233
3543
  </tr>`
3234
3544
  )}
3235
3545
  </tbody>
@@ -3240,7 +3550,7 @@ var RoxyPanchangTable = class extends import_lit16.LitElement {
3240
3550
  ${inauspicious.filter(([, v]) => !!v).map(
3241
3551
  ([k, v]) => import_lit16.html`<tr>
3242
3552
  <th>${k}</th>
3243
- <td>${formatRange(v)}</td>
3553
+ <td>${formatTimeRange(v)}</td>
3244
3554
  </tr>`
3245
3555
  )}
3246
3556
  </tbody>
@@ -3334,11 +3644,6 @@ __decorateClass([
3334
3644
  RoxyPanchangTable = __decorateClass([
3335
3645
  (0, import_decorators15.customElement)("roxy-panchang-table")
3336
3646
  ], RoxyPanchangTable);
3337
- function formatRange(t) {
3338
- if (!t) return "";
3339
- if (t.start && t.end) return `${t.start} - ${t.end}`;
3340
- return t.start ?? t.end ?? "";
3341
- }
3342
3647
 
3343
3648
  // packages/ui/src/components/synastry-chart.ts
3344
3649
  var import_lit17 = require("lit");
@@ -3357,23 +3662,58 @@ var RoxySynastryChart = class extends import_lit17.LitElement {
3357
3662
  render() {
3358
3663
  if (!this.data)
3359
3664
  return import_lit17.html`<div class="roxy-empty" role="status">No synastry data</div>`;
3360
- const {
3361
- person1,
3362
- person2,
3363
- compatibilityScore,
3364
- summary,
3365
- interAspects = []
3366
- } = this.data;
3367
- const p1Planets = this.normalizePlanets(person1?.planets);
3368
- const p2Planets = this.normalizePlanets(person2?.planets);
3665
+ const { person1, person2, compatibilityScore, analysis } = this.data;
3666
+ const interAspects = this.data.interAspects ?? [];
3667
+ const p1Planets = person1?.planets ?? [];
3668
+ const p2Planets = person2?.planets ?? [];
3669
+ const score = typeof compatibilityScore === "number" ? Math.round(compatibilityScore) : void 0;
3670
+ const summaryText = analysis?.overall;
3671
+ const strengths = analysis?.strengths ?? [];
3672
+ const challenges = analysis?.challenges ?? [];
3673
+ const hasPlanets = p1Planets.length > 0 && p2Planets.length > 0;
3674
+ if (!hasPlanets) {
3675
+ return import_lit17.html`<div
3676
+ class="wrap"
3677
+ aria-label="Synastry compatibility chart"
3678
+ >
3679
+ <div class="head">
3680
+ <h2 class="title">Synastry</h2>
3681
+ ${typeof score === "number" ? import_lit17.html`<span class="score" aria-label=${`Score ${score} of 100`}
3682
+ >${score} / 100</span
3683
+ >` : import_lit17.nothing}
3684
+ </div>
3685
+ <div class="missing-planets" role="status">
3686
+ Synastry response missing planet positions. Pass
3687
+ <code>data</code> with <code>person1.planets</code> and
3688
+ <code>person2.planets</code> arrays from the natal-chart endpoint, or
3689
+ use the <code>&lt;roxy-data&gt;</code> fallback.
3690
+ </div>
3691
+ ${summaryText ? import_lit17.html`<p class="summary">${summaryText}</p>` : import_lit17.nothing}
3692
+ ${interAspects.length > 0 ? this.renderAspects(interAspects) : import_lit17.nothing}
3693
+ ${strengths.length > 0 || challenges.length > 0 ? import_lit17.html`<div class="lists">
3694
+ ${strengths.length ? import_lit17.html`<div>
3695
+ <h3>Strengths</h3>
3696
+ <ul>
3697
+ ${strengths.map((s) => import_lit17.html`<li>${s}</li>`)}
3698
+ </ul>
3699
+ </div>` : import_lit17.nothing}
3700
+ ${challenges.length ? import_lit17.html`<div>
3701
+ <h3>Challenges</h3>
3702
+ <ul>
3703
+ ${challenges.map((s) => import_lit17.html`<li>${s}</li>`)}
3704
+ </ul>
3705
+ </div>` : import_lit17.nothing}
3706
+ </div>` : import_lit17.nothing}
3707
+ </div>`;
3708
+ }
3369
3709
  return import_lit17.html`<div
3370
3710
  class="wrap"
3371
3711
  aria-label="Synastry compatibility chart"
3372
3712
  >
3373
3713
  <div class="head">
3374
3714
  <h2 class="title">Synastry</h2>
3375
- ${typeof compatibilityScore === "number" ? import_lit17.html`<span class="score" aria-label=${`Score ${compatibilityScore} of 100`}
3376
- >${compatibilityScore} / 100</span
3715
+ ${typeof score === "number" ? import_lit17.html`<span class="score" aria-label=${`Score ${score} of 100`}
3716
+ >${score} / 100</span
3377
3717
  >` : import_lit17.nothing}
3378
3718
  </div>
3379
3719
  <svg
@@ -3404,34 +3744,39 @@ var RoxySynastryChart = class extends import_lit17.LitElement {
3404
3744
  stroke-width="0.6"
3405
3745
  />
3406
3746
  ${this.renderSpokes()} ${this.renderSigns()}
3747
+ ${this.renderInterAspectLines(p1Planets, p2Planets, interAspects)}
3407
3748
  ${this.renderRing(p1Planets, P1_R, "p1")} ${this.renderRing(p2Planets, P2_R, "p2")}
3408
3749
  </svg>
3409
- ${summary ? import_lit17.html`<p class="summary">${summary}</p>` : import_lit17.nothing}
3750
+ <div class="legend-row">
3751
+ <span><span class="swatch" style="background: var(--roxy-accent)"></span>Person 1</span>
3752
+ <span><span class="swatch" style="background: var(--roxy-info)"></span>Person 2</span>
3753
+ <span><span class="swatch" style="background: var(--roxy-success)"></span>harmonious</span>
3754
+ <span><span class="swatch" style="background: var(--roxy-danger)"></span>challenging</span>
3755
+ </div>
3756
+ ${summaryText ? import_lit17.html`<p class="summary">${summaryText}</p>` : import_lit17.nothing}
3410
3757
  ${interAspects.length > 0 ? this.renderAspects(interAspects) : import_lit17.nothing}
3411
- ${(this.data.strengths?.length ?? 0) > 0 || (this.data.challenges?.length ?? 0) > 0 ? import_lit17.html`<div class="lists">
3412
- ${this.data.strengths?.length ? import_lit17.html`<div>
3758
+ ${strengths.length > 0 || challenges.length > 0 ? import_lit17.html`<div class="lists">
3759
+ ${strengths.length ? import_lit17.html`<div>
3413
3760
  <h3>Strengths</h3>
3414
3761
  <ul>
3415
- ${this.data.strengths.map((s) => import_lit17.html`<li>${s}</li>`)}
3762
+ ${strengths.map((s) => import_lit17.html`<li>${s}</li>`)}
3416
3763
  </ul>
3417
3764
  </div>` : import_lit17.nothing}
3418
- ${this.data.challenges?.length ? import_lit17.html`<div>
3765
+ ${challenges.length ? import_lit17.html`<div>
3419
3766
  <h3>Challenges</h3>
3420
3767
  <ul>
3421
- ${this.data.challenges.map((s) => import_lit17.html`<li>${s}</li>`)}
3768
+ ${challenges.map((s) => import_lit17.html`<li>${s}</li>`)}
3422
3769
  </ul>
3423
3770
  </div>` : import_lit17.nothing}
3424
3771
  </div>` : import_lit17.nothing}
3425
3772
  </div>`;
3426
3773
  }
3427
- normalizePlanets(p) {
3428
- if (!p) return [];
3429
- if (Array.isArray(p)) return p;
3430
- return Object.entries(p).map(([name, e]) => ({ ...e, name }));
3774
+ toAngle(longitude) {
3775
+ return 180 - longitude;
3431
3776
  }
3432
3777
  renderSpokes() {
3433
3778
  return Array.from({ length: 12 }, (_, i) => {
3434
- const angle = i * 30 - 90;
3779
+ const angle = this.toAngle(i * 30);
3435
3780
  const start = polarToCartesian(CENTER2, CENTER2, P2_R - 14, angle);
3436
3781
  const end = polarToCartesian(CENTER2, CENTER2, OUTER_R2, angle);
3437
3782
  return import_lit17.svg`<line class="wheel-line" x1=${start.x} y1=${start.y} x2=${end.x} y2=${end.y} stroke-width="0.6" />`;
@@ -3453,19 +3798,43 @@ var RoxySynastryChart = class extends import_lit17.LitElement {
3453
3798
  "Pisces"
3454
3799
  ];
3455
3800
  return order.map((s, i) => {
3456
- const angle = i * 30 + 15 - 90;
3801
+ const angle = this.toAngle(i * 30 + 15);
3457
3802
  const pos = polarToCartesian(CENTER2, CENTER2, SIGN_R2, angle);
3458
3803
  return import_lit17.svg`<text class="sign" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central">${SIGN_GLYPH[s]}</text>`;
3459
3804
  });
3460
3805
  }
3461
3806
  renderRing(planets, radius, cls) {
3462
3807
  return planets.map((p) => {
3463
- const lon = typeof p.longitude === "number" ? p.longitude : typeof p.degree === "number" ? p.degree : NaN;
3464
- if (!Number.isFinite(lon)) return import_lit17.nothing;
3465
- const pos = polarToCartesian(CENTER2, CENTER2, radius, lon - 90);
3466
- const name = p.name ?? p.planet ?? "";
3467
- const glyph = PLANET_GLYPH[capitalize3(name)] ?? name.slice(0, 2);
3468
- return import_lit17.svg`<text class=${cls} x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central"><title>${name}</title>${glyph}</text>`;
3808
+ if (!Number.isFinite(p.longitude)) return import_lit17.nothing;
3809
+ const pos = polarToCartesian(
3810
+ CENTER2,
3811
+ CENTER2,
3812
+ radius,
3813
+ this.toAngle(p.longitude)
3814
+ );
3815
+ const glyph = PLANET_GLYPH[capitalize3(p.name)] ?? p.name.slice(0, 2);
3816
+ return import_lit17.svg`<text class=${cls} x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central"><title>${p.name}</title>${glyph}</text>`;
3817
+ });
3818
+ }
3819
+ renderInterAspectLines(p1, p2, aspects) {
3820
+ const longitudeOf = (list, name) => {
3821
+ const target = capitalize3(name);
3822
+ for (const p of list) {
3823
+ if (capitalize3(p.name) !== target) continue;
3824
+ if (typeof p.longitude === "number") return p.longitude;
3825
+ }
3826
+ return void 0;
3827
+ };
3828
+ return aspects.map((a) => {
3829
+ const l1 = longitudeOf(p1, a.planet1);
3830
+ const l2 = longitudeOf(p2, a.planet2);
3831
+ if (l1 === void 0 || l2 === void 0) return import_lit17.nothing;
3832
+ const out = polarToCartesian(CENTER2, CENTER2, P1_R - 12, this.toAngle(l1));
3833
+ const inn = polarToCartesian(CENTER2, CENTER2, P2_R + 8, this.toAngle(l2));
3834
+ const aspectName = normalizeAspect2(a);
3835
+ const cls = ASPECT_CLASS2[aspectName] ?? "aspect-other";
3836
+ const orbLabel = formatNumber(a.orb, 1);
3837
+ return import_lit17.svg`<line class=${`aspect ${cls}`} x1=${out.x} y1=${out.y} x2=${inn.x} y2=${inn.y}><title>${a.planet1} ${aspectName} ${a.planet2}${orbLabel ? ` (orb ${orbLabel}\xB0)` : ""}</title></line>`;
3469
3838
  });
3470
3839
  }
3471
3840
  renderAspects(aspects) {
@@ -3480,15 +3849,13 @@ var RoxySynastryChart = class extends import_lit17.LitElement {
3480
3849
  </tr>
3481
3850
  </thead>
3482
3851
  <tbody>
3483
- ${aspects.slice(0, 16).map(
3852
+ ${aspects.slice(0, 12).map(
3484
3853
  (a) => import_lit17.html`<tr>
3485
- <td>${a.planet1 ?? ""}</td>
3486
- <td>${a.planet2 ?? ""}</td>
3487
- <td>${a.aspect ?? ""}</td>
3488
- <td class="orb">
3489
- ${typeof a.orb === "number" ? a.orb.toFixed(1) : ""}
3490
- </td>
3491
- <td>${a.strength ?? ""}</td>
3854
+ <td>${a.planet1}</td>
3855
+ <td>${a.planet2}</td>
3856
+ <td>${normalizeAspect2(a) || ""}</td>
3857
+ <td class="orb">${formatNumber(a.orb, 1)}</td>
3858
+ <td>${formatStrength(a.strength)}</td>
3492
3859
  </tr>`
3493
3860
  )}
3494
3861
  </tbody>
@@ -3549,6 +3916,42 @@ RoxySynastryChart.styles = [
3549
3916
  font-weight: 600;
3550
3917
  font-size: 13px;
3551
3918
  }
3919
+ .aspect {
3920
+ stroke-width: 0.8;
3921
+ fill: none;
3922
+ opacity: 0.5;
3923
+ }
3924
+ .aspect-trine,
3925
+ .aspect-sextile {
3926
+ stroke: var(--roxy-success, #16a34a);
3927
+ }
3928
+ .aspect-square,
3929
+ .aspect-opposition {
3930
+ stroke: var(--roxy-danger, #dc2626);
3931
+ }
3932
+ .aspect-conjunction {
3933
+ stroke: var(--roxy-accent-fg, #b45309);
3934
+ }
3935
+ .aspect-other {
3936
+ stroke: var(--roxy-muted, #71717a);
3937
+ opacity: 0.35;
3938
+ }
3939
+ .legend-row {
3940
+ display: flex;
3941
+ flex-wrap: wrap;
3942
+ gap: var(--roxy-space-md, 1rem);
3943
+ font-size: var(--roxy-text-xs, 0.75rem);
3944
+ color: var(--roxy-muted, #71717a);
3945
+ margin-top: calc(var(--roxy-space-xs, 0.25rem) * -1);
3946
+ }
3947
+ .legend-row .swatch {
3948
+ display: inline-block;
3949
+ width: 8px;
3950
+ height: 8px;
3951
+ border-radius: 50%;
3952
+ margin-right: 4px;
3953
+ vertical-align: middle;
3954
+ }
3552
3955
 
3553
3956
  .summary {
3554
3957
  margin: 0;
@@ -3596,6 +3999,23 @@ RoxySynastryChart.styles = [
3596
3999
  padding-left: var(--roxy-space-md, 1rem);
3597
4000
  font-size: var(--roxy-text-sm, 0.875rem);
3598
4001
  }
4002
+
4003
+ .missing-planets {
4004
+ background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 8%, transparent);
4005
+ border: 1px solid var(--roxy-border, #e4e4e7);
4006
+ border-radius: var(--roxy-radius-md, 8px);
4007
+ padding: var(--roxy-space-md, 1rem);
4008
+ color: var(--roxy-fg, #0a0a0a);
4009
+ font-size: var(--roxy-text-sm, 0.875rem);
4010
+ line-height: 1.5;
4011
+ }
4012
+ .missing-planets code {
4013
+ font-family: var(--roxy-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
4014
+ font-size: 0.95em;
4015
+ background: color-mix(in srgb, var(--roxy-fg, #0a0a0a) 6%, transparent);
4016
+ padding: 0 4px;
4017
+ border-radius: 4px;
4018
+ }
3599
4019
  `
3600
4020
  ];
3601
4021
  __decorateClass([
@@ -3608,6 +4028,20 @@ function capitalize3(s) {
3608
4028
  if (!s) return "";
3609
4029
  return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
3610
4030
  }
4031
+ var ASPECT_CLASS2 = {
4032
+ conjunction: "aspect-conjunction",
4033
+ sextile: "aspect-sextile",
4034
+ square: "aspect-square",
4035
+ trine: "aspect-trine",
4036
+ opposition: "aspect-opposition"
4037
+ };
4038
+ function normalizeAspect2(a) {
4039
+ return (a.type ?? "").toLowerCase().replace(/_/g, "-");
4040
+ }
4041
+ function formatStrength(s) {
4042
+ if (typeof s === "number") return Math.round(s).toString();
4043
+ return "";
4044
+ }
3611
4045
 
3612
4046
  // packages/ui/src/components/tarot-card.ts
3613
4047
  var import_lit18 = require("lit");
@@ -3621,18 +4055,17 @@ var RoxyTarotCard = class extends import_lit18.LitElement {
3621
4055
  this.flipped = !this.flipped;
3622
4056
  };
3623
4057
  }
3624
- getCard() {
3625
- if (!this.data) return null;
3626
- if ("card" in this.data && this.data.card) return this.data.card;
3627
- return this.data;
3628
- }
3629
4058
  render() {
3630
- const card = this.getCard();
3631
- if (!card)
4059
+ const d = this.data;
4060
+ if (!d)
3632
4061
  return import_lit18.html`<div class="roxy-empty" role="status">No tarot data</div>`;
4062
+ if ("card" in d) return this.renderDailyCard(d);
4063
+ return this.renderFullCard(d);
4064
+ }
4065
+ renderDailyCard(d) {
4066
+ const card = d.card;
3633
4067
  const isReversed = this.flipped !== Boolean(card.reversed);
3634
- const meaning = typeof card.meaning === "string" ? card.meaning : (isReversed ? card.meaning?.reversed : card.meaning?.upright) ?? card.meaning?.spiritual ?? card.upright?.meaning;
3635
- const dailyMessage = this.data && "dailyMessage" in this.data ? this.data.dailyMessage : void 0;
4068
+ const keywords = card.keywords ?? [];
3636
4069
  return import_lit18.html`<article class="card" aria-label=${card.name ?? "Tarot card"}>
3637
4070
  <div class="image-wrap">
3638
4071
  ${card.imageUrl ? import_lit18.html`<img
@@ -3657,15 +4090,60 @@ var RoxyTarotCard = class extends import_lit18.LitElement {
3657
4090
  <div>
3658
4091
  <div class="meta">
3659
4092
  ${card.arcana ? import_lit18.html`${card.arcana} arcana` : import_lit18.nothing}
3660
- ${card.number !== void 0 && card.number !== null ? import_lit18.html` · ${card.number}` : import_lit18.nothing}
3661
4093
  ${isReversed ? import_lit18.html` · reversed` : import_lit18.nothing}
3662
- ${card.position ? import_lit18.html`<span class="position">${card.position}</span>` : import_lit18.nothing}
3663
4094
  </div>
3664
4095
  <h2 class="title">${card.name ?? "Tarot card"}</h2>
3665
- ${dailyMessage ? import_lit18.html`<p class="message">${dailyMessage}</p>` : import_lit18.nothing}
3666
- ${meaning ? import_lit18.html`<p>${meaning}</p>` : import_lit18.nothing}
3667
- ${card.keywords?.length ? import_lit18.html`<div class="chips">
3668
- ${card.keywords.map((k) => import_lit18.html`<span>${k}</span>`)}
4096
+ ${d.dailyMessage ? import_lit18.html`<p class="message">${d.dailyMessage}</p>` : import_lit18.nothing}
4097
+ ${card.meaning ? import_lit18.html`<p>${card.meaning}</p>` : import_lit18.nothing}
4098
+ ${keywords.length > 0 ? import_lit18.html`<div class="chips">
4099
+ ${keywords.map((k) => import_lit18.html`<span>${k}</span>`)}
4100
+ </div>` : import_lit18.nothing}
4101
+ <button
4102
+ class="flip"
4103
+ type="button"
4104
+ @click=${this.toggleFlip}
4105
+ aria-pressed=${this.flipped ? "true" : "false"}
4106
+ >
4107
+ Flip card
4108
+ </button>
4109
+ </div>
4110
+ </article>`;
4111
+ }
4112
+ renderFullCard(d) {
4113
+ const isReversed = this.flipped;
4114
+ const orientedMeaning = isReversed ? d.reversed : d.upright;
4115
+ const keywords = isReversed ? d.keywords?.reversed ?? [] : d.keywords?.upright ?? [];
4116
+ return import_lit18.html`<article class="card" aria-label=${d.name ?? "Tarot card"}>
4117
+ <div class="image-wrap">
4118
+ ${d.imageUrl ? import_lit18.html`<img
4119
+ class=${`image ${isReversed ? "reversed" : ""}`}
4120
+ src=${d.imageUrl}
4121
+ alt=${d.name ?? "Tarot card"}
4122
+ tabindex="0"
4123
+ @click=${this.toggleFlip}
4124
+ @keydown=${(e) => {
4125
+ if (e.key === "Enter" || e.key === " ") {
4126
+ e.preventDefault();
4127
+ this.toggleFlip();
4128
+ }
4129
+ }}
4130
+ />` : import_lit18.html`<div
4131
+ class=${`image ${isReversed ? "reversed" : ""}`}
4132
+ style="aspect-ratio: 0.6; display: flex; align-items: center; justify-content: center; color: var(--roxy-muted)"
4133
+ >
4134
+ ${d.name ?? "?"}
4135
+ </div>`}
4136
+ </div>
4137
+ <div>
4138
+ <div class="meta">
4139
+ ${d.arcana ? import_lit18.html`${d.arcana} arcana` : import_lit18.nothing}
4140
+ ${d.number !== void 0 && d.number !== null ? import_lit18.html` · ${d.number}` : import_lit18.nothing}
4141
+ ${isReversed ? import_lit18.html` · reversed` : import_lit18.nothing}
4142
+ </div>
4143
+ <h2 class="title">${d.name ?? "Tarot card"}</h2>
4144
+ ${orientedMeaning?.description ? import_lit18.html`<p>${orientedMeaning.description}</p>` : import_lit18.nothing}
4145
+ ${keywords.length > 0 ? import_lit18.html`<div class="chips">
4146
+ ${keywords.map((k) => import_lit18.html`<span>${k}</span>`)}
3669
4147
  </div>` : import_lit18.nothing}
3670
4148
  <button
3671
4149
  class="flip"
@@ -3735,11 +4213,6 @@ RoxyTarotCard.styles = [
3735
4213
  letter-spacing: 0.06em;
3736
4214
  margin-bottom: var(--roxy-space-sm, 0.5rem);
3737
4215
  }
3738
- .position {
3739
- color: var(--roxy-info, #0284c7);
3740
- margin-left: var(--roxy-space-xs, 0.25rem);
3741
- text-transform: capitalize;
3742
- }
3743
4216
 
3744
4217
  .message {
3745
4218
  color: var(--roxy-fg, #0a0a0a);
@@ -3801,22 +4274,30 @@ var RoxyTarotSpread = class extends import_lit19.LitElement {
3801
4274
  const d = this.data;
3802
4275
  if (!d)
3803
4276
  return import_lit19.html`<div class="roxy-empty" role="status">No tarot spread</div>`;
3804
- const positions = d.positions ?? d.cards ?? [];
3805
- const isYesNo = !!d.answer;
3806
- const answerClass = isYesNo ? (d.answer ?? "").toLowerCase().replace(/[^a-z]/g, "") : "";
4277
+ const isYesNo = "answer" in d;
4278
+ const isDrawn = "cards" in d && !("spread" in d);
4279
+ const positions = isDrawn ? [] : "positions" in d ? d.positions ?? [] : [];
4280
+ const cards = isDrawn && "cards" in d ? d.cards : [];
4281
+ const answer = isYesNo ? d.answer : void 0;
4282
+ const strength = isYesNo ? d.strength : void 0;
4283
+ const spreadLabel = "spread" in d ? d.spread : this.spread.replace(/-/g, " ");
4284
+ const question = "question" in d ? d.question : void 0;
4285
+ const summary = "summary" in d ? d.summary : void 0;
4286
+ const yesNoInterp = isYesNo ? d.interpretation : void 0;
4287
+ const answerClass = answer ? answer.toLowerCase().replace(/[^a-z]/g, "") : "";
3807
4288
  return import_lit19.html`<article class="wrap" aria-label="Tarot spread">
3808
4289
  <header class="head">
3809
- <h2 class="title">${d.spread ?? this.spread.replace(/-/g, " ")}</h2>
3810
- ${d.question ? import_lit19.html`<span class="question">"${d.question}"</span>` : import_lit19.nothing}
4290
+ <h2 class="title">${spreadLabel}</h2>
4291
+ ${question ? import_lit19.html`<span class="question">"${question}"</span>` : import_lit19.nothing}
3811
4292
  </header>
3812
4293
  ${isYesNo ? import_lit19.html`<div>
3813
- <span class=${`answer ${answerClass}`}>${d.answer}</span>
3814
- ${d.strength ? import_lit19.html`<small> · ${d.strength}</small>` : import_lit19.nothing}
4294
+ <span class=${`answer ${answerClass}`}>${answer}</span>
4295
+ ${strength ? import_lit19.html`<small> · ${strength}</small>` : import_lit19.nothing}
3815
4296
  </div>` : import_lit19.nothing}
3816
4297
  ${positions.length > 0 ? import_lit19.html`<div class="grid">
3817
4298
  ${positions.map(
3818
4299
  (p) => import_lit19.html`<div class="card">
3819
- <p class="label">${p.label ?? p.name ?? p.position ?? ""}</p>
4300
+ <p class="label">${p.name ?? ""}</p>
3820
4301
  <div class="image">
3821
4302
  ${p.card?.imageUrl ? import_lit19.html`<img
3822
4303
  src=${p.card.imageUrl}
@@ -3832,8 +4313,26 @@ var RoxyTarotSpread = class extends import_lit19.LitElement {
3832
4313
  </div>`
3833
4314
  )}
3834
4315
  </div>` : import_lit19.nothing}
3835
- ${d.reading ? import_lit19.html`<p class="reading">${d.reading}</p>` : import_lit19.nothing}
3836
- ${d.interpretation && !d.reading ? import_lit19.html`<p class="reading">${d.interpretation}</p>` : import_lit19.nothing}
4316
+ ${cards.length > 0 ? import_lit19.html`<div class="grid">
4317
+ ${cards.map(
4318
+ (c) => import_lit19.html`<div class="card">
4319
+ <div class="image">
4320
+ ${c.imageUrl ? import_lit19.html`<img
4321
+ src=${c.imageUrl}
4322
+ alt=${c.name ?? "tarot card"}
4323
+ class=${c.reversed ? "reversed" : ""}
4324
+ />` : import_lit19.html`${c.name ?? "?"}`}
4325
+ </div>
4326
+ <p class="name">
4327
+ ${c.name ?? ""}
4328
+ ${c.reversed ? import_lit19.html`<small>(reversed)</small>` : import_lit19.nothing}
4329
+ </p>
4330
+ ${c.meaning ? import_lit19.html`<p class="interp">${c.meaning}</p>` : import_lit19.nothing}
4331
+ </div>`
4332
+ )}
4333
+ </div>` : import_lit19.nothing}
4334
+ ${summary ? import_lit19.html`<p class="reading">${summary}</p>` : import_lit19.nothing}
4335
+ ${yesNoInterp ? import_lit19.html`<p class="reading">${yesNoInterp}</p>` : import_lit19.nothing}
3837
4336
  </article>`;
3838
4337
  }
3839
4338
  };
@@ -3875,15 +4374,15 @@ RoxyTarotSpread.styles = [
3875
4374
  }
3876
4375
  .answer.yes {
3877
4376
  background: color-mix(in srgb, var(--roxy-success, #16a34a) 16%, transparent);
3878
- color: var(--roxy-success, #16a34a);
4377
+ color: var(--roxy-success-fg, #166534);
3879
4378
  }
3880
4379
  .answer.no {
3881
4380
  background: color-mix(in srgb, var(--roxy-danger, #dc2626) 16%, transparent);
3882
- color: var(--roxy-danger, #dc2626);
4381
+ color: var(--roxy-danger-fg, #991b1b);
3883
4382
  }
3884
4383
  .answer.maybe {
3885
4384
  background: color-mix(in srgb, var(--roxy-warning, #ea580c) 16%, transparent);
3886
- color: var(--roxy-warning, #ea580c);
4385
+ color: var(--roxy-warning-fg, #9a3412);
3887
4386
  }
3888
4387
 
3889
4388
  .grid {
@@ -4024,21 +4523,12 @@ var RoxyVedicKundli = class extends import_lit20.LitElement {
4024
4523
  }
4025
4524
  buildHouses() {
4026
4525
  if (!this.data) return [];
4526
+ const data = this.data;
4027
4527
  const houses = [];
4028
- if (Array.isArray(this.data.houses)) {
4029
- for (const h of this.data.houses) {
4030
- houses.push({
4031
- house: h.house ?? h.number ?? houses.length + 1,
4032
- sign: h.sign ?? "",
4033
- planets: h.planets ?? []
4034
- });
4035
- }
4036
- if (houses.length > 0) return houses;
4037
- }
4038
4528
  for (let i = 0; i < 12; i++) {
4039
4529
  const key = RASHI_KEYS[i];
4040
- const bucket = this.data[key];
4041
- const planets = (bucket?.signs ?? []).map((p) => p.planet ?? "").filter(Boolean);
4530
+ const bucket = data[key];
4531
+ const planets = (bucket?.signs ?? []).map((p) => p.graha).filter(Boolean);
4042
4532
  houses.push({
4043
4533
  house: i + 1,
4044
4534
  sign: RASHI_TO_SIGN[key] ?? "",
@@ -4078,19 +4568,28 @@ var RoxyVedicKundli = class extends import_lit20.LitElement {
4078
4568
  </svg>
4079
4569
  </div>`;
4080
4570
  }
4571
+ isLagna(h) {
4572
+ const ascSign = this.data?.meta?.Lagna?.rashi;
4573
+ if (!ascSign) return false;
4574
+ return ascSign.toLowerCase() === h.sign.toLowerCase();
4575
+ }
4081
4576
  renderHouseGroup(h) {
4082
4577
  const center = SOUTH_HOUSE_CENTERS[h.house];
4083
4578
  const signPos = SOUTH_SIGN_POSITIONS[h.house];
4084
4579
  if (!center || !signPos) return import_lit20.nothing;
4085
4580
  const signAbbr = SIGN_ABBR[h.sign] ?? "";
4086
4581
  const planets = h.planets ?? [];
4582
+ const isLagna = this.isLagna(h);
4087
4583
  return import_lit20.svg`
4088
4584
  <g>
4585
+ ${isLagna ? import_lit20.svg`<rect class="lagna-bg" x=${center.x - 30} y=${center.y - 28} width="60" height="56" rx="6" />` : import_lit20.nothing}
4089
4586
  ${signAbbr ? import_lit20.svg`<text class="sign-text" x=${signPos.x} y=${signPos.y} text-anchor="middle" dominant-baseline="central">${signAbbr}</text>` : import_lit20.nothing}
4587
+ ${isLagna ? import_lit20.svg`<text class="lagna-marker" x=${center.x} y=${center.y - 18} text-anchor="middle" dominant-baseline="central">LAGNA</text>` : import_lit20.nothing}
4090
4588
  ${planets.map((planet, j) => {
4091
4589
  const abbr = PLANET_ABBR[capitalize4(planet)] ?? planet.slice(0, 2);
4092
4590
  const lineHeight = 13;
4093
- const startY = center.y - (planets.length - 1) * lineHeight / 2;
4591
+ const baseY = isLagna ? center.y + 8 : center.y;
4592
+ const startY = baseY - (planets.length - 1) * lineHeight / 2;
4094
4593
  const yPos = startY + j * lineHeight;
4095
4594
  return import_lit20.svg`<text class="planet-text" x=${center.x} y=${yPos} text-anchor="middle" dominant-baseline="central">${abbr}</text>`;
4096
4595
  })}
@@ -4132,6 +4631,18 @@ RoxyVedicKundli.styles = [
4132
4631
  font-weight: 600;
4133
4632
  font-family: var(--roxy-font-sans);
4134
4633
  }
4634
+ .lagna-marker {
4635
+ fill: var(--roxy-accent-fg, #b45309);
4636
+ font-size: 8px;
4637
+ font-weight: 700;
4638
+ font-family: var(--roxy-font-sans);
4639
+ letter-spacing: 0.05em;
4640
+ }
4641
+ .lagna-bg {
4642
+ fill: color-mix(in srgb, var(--roxy-accent, #f59e0b) 12%, transparent);
4643
+ stroke: color-mix(in srgb, var(--roxy-accent, #f59e0b) 45%, transparent);
4644
+ stroke-width: 0.8;
4645
+ }
4135
4646
  `
4136
4647
  ];
4137
4648
  __decorateClass([
@@ -4148,27 +4659,307 @@ function capitalize4(s) {
4148
4659
  return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
4149
4660
  }
4150
4661
 
4151
- // packages/ui/src/index.ts
4152
- var ROXY_UI_VERSION = "0.1.0";
4153
- var ROXY_UI_COMPONENTS = [
4154
- "natal-chart",
4155
- "horoscope-card",
4156
- "synastry-chart",
4157
- "compatibility-card",
4158
- "moon-phase",
4159
- "vedic-kundli",
4160
- "panchang-table",
4161
- "dasha-timeline",
4162
- "dosha-card",
4163
- "guna-milan",
4164
- "kp-planets-table",
4165
- "numerology-card",
4166
- "tarot-card",
4167
- "tarot-spread",
4168
- "biorhythm-chart",
4169
- "hexagram",
4170
- "endpoint-form",
4171
- "location-search",
4172
- "data"
4662
+ // packages/ui/src/manifest.ts
4663
+ var ROXY_COMPONENTS = [
4664
+ {
4665
+ pascal: "RoxyNatalChart",
4666
+ tag: "roxy-natal-chart",
4667
+ slug: "natal-chart",
4668
+ domain: "astrology",
4669
+ heading: "Natal chart",
4670
+ endpoints: ["astrology.generateNatalChart"],
4671
+ description: "Western natal chart wheel for /astrology/natal-chart responses",
4672
+ docsLabel: "Western",
4673
+ endpointLabel: "POST /astrology/natal-chart",
4674
+ docsSummary: "Natal chart wheel with planet glyphs and aspect lines",
4675
+ topic: "Astrology"
4676
+ },
4677
+ {
4678
+ pascal: "RoxyHoroscopeCard",
4679
+ tag: "roxy-horoscope-card",
4680
+ slug: "horoscope-card",
4681
+ domain: "astrology",
4682
+ heading: "Daily horoscope",
4683
+ endpoints: [
4684
+ "astrology.getDailyHoroscope",
4685
+ "astrology.getWeeklyHoroscope",
4686
+ "astrology.getMonthlyHoroscope"
4687
+ ],
4688
+ description: "Daily, weekly, or monthly horoscope card for /astrology/horoscope/...",
4689
+ docsLabel: "Western",
4690
+ endpointLabel: "GET /astrology/horoscope/{sign}/{daily,weekly,monthly}",
4691
+ docsSummary: "Daily, weekly, or monthly horoscope card",
4692
+ topic: "Astrology"
4693
+ },
4694
+ {
4695
+ pascal: "RoxySynastryChart",
4696
+ tag: "roxy-synastry-chart",
4697
+ slug: "synastry-chart",
4698
+ domain: "astrology",
4699
+ heading: "Synastry",
4700
+ endpoints: ["astrology.calculateSynastry"],
4701
+ description: "Dual-wheel synastry chart with inter-aspects table",
4702
+ docsLabel: "Western",
4703
+ endpointLabel: "POST /astrology/synastry",
4704
+ docsSummary: "Dual-wheel synastry with inter-aspects table",
4705
+ topic: "Astrology"
4706
+ },
4707
+ {
4708
+ pascal: "RoxyCompatibilityCard",
4709
+ tag: "roxy-compatibility-card",
4710
+ slug: "compatibility-card",
4711
+ domain: "astrology",
4712
+ heading: "Compatibility score",
4713
+ endpoints: [
4714
+ "astrology.calculateCompatibility",
4715
+ "numerology.calculateNumCompatibility",
4716
+ "biorhythm.calculateBioCompatibility"
4717
+ ],
4718
+ description: "Cross-domain compatibility score card",
4719
+ docsLabel: "Cross",
4720
+ endpointLabel: "POST /astrology/compatibility-score, /numerology/compatibility, /biorhythm/compatibility",
4721
+ docsSummary: "Score card with category breakdown",
4722
+ topic: "Astrology"
4723
+ },
4724
+ {
4725
+ pascal: "RoxyMoonPhase",
4726
+ tag: "roxy-moon-phase",
4727
+ slug: "moon-phase",
4728
+ domain: "astrology",
4729
+ heading: "Moon phase",
4730
+ endpoints: [
4731
+ "astrology.getCurrentMoonPhase",
4732
+ "astrology.getUpcomingMoonPhases",
4733
+ "astrology.getMoonCalendar"
4734
+ ],
4735
+ description: "Moon phase card and calendar",
4736
+ docsLabel: "Western",
4737
+ endpointLabel: "GET /astrology/moon-phase/{current,upcoming,calendar/...}",
4738
+ docsSummary: "Moon phase card and calendar",
4739
+ topic: "Astrology"
4740
+ },
4741
+ {
4742
+ pascal: "RoxyVedicKundli",
4743
+ tag: "roxy-vedic-kundli",
4744
+ slug: "vedic-kundli",
4745
+ domain: "vedic",
4746
+ heading: "Vedic kundli",
4747
+ endpoints: ["vedicAstrology.generateBirthChart"],
4748
+ description: "South or North Indian Vedic kundli for /vedic-astrology/birth-chart",
4749
+ docsLabel: "Vedic",
4750
+ endpointLabel: "POST /vedic-astrology/birth-chart",
4751
+ docsSummary: "South or North Indian kundli",
4752
+ topic: "Vedic"
4753
+ },
4754
+ {
4755
+ pascal: "RoxyPanchangTable",
4756
+ tag: "roxy-panchang-table",
4757
+ slug: "panchang-table",
4758
+ domain: "vedic",
4759
+ heading: "Panchang",
4760
+ endpoints: [
4761
+ "vedicAstrology.getBasicPanchang",
4762
+ "vedicAstrology.getDetailedPanchang"
4763
+ ],
4764
+ description: "Panchang muhurta table with auspicious and inauspicious periods",
4765
+ docsLabel: "Vedic",
4766
+ endpointLabel: "POST /vedic-astrology/panchang/{basic,detailed}",
4767
+ docsSummary: "15+ muhurtas in detailed mode",
4768
+ topic: "Vedic"
4769
+ },
4770
+ {
4771
+ pascal: "RoxyDashaTimeline",
4772
+ tag: "roxy-dasha-timeline",
4773
+ slug: "dasha-timeline",
4774
+ domain: "vedic",
4775
+ heading: "Vimshottari dasha",
4776
+ endpoints: [
4777
+ "vedicAstrology.getCurrentDasha",
4778
+ "vedicAstrology.getMajorDashas",
4779
+ "vedicAstrology.getSubDashas"
4780
+ ],
4781
+ description: "Vimshottari dasha timeline with active mahadasha highlighted",
4782
+ docsLabel: "Vedic",
4783
+ endpointLabel: "POST /vedic-astrology/dasha/{current,major,sub/...}",
4784
+ docsSummary: "Vimshottari mahadasha + antardasha + pratyantardasha",
4785
+ topic: "Vedic"
4786
+ },
4787
+ {
4788
+ pascal: "RoxyDoshaCard",
4789
+ tag: "roxy-dosha-card",
4790
+ slug: "dosha-card",
4791
+ domain: "vedic",
4792
+ heading: "Manglik dosha",
4793
+ endpoints: [
4794
+ "vedicAstrology.checkManglikDosha",
4795
+ "vedicAstrology.checkKalsarpaDosha",
4796
+ "vedicAstrology.checkSadhesati"
4797
+ ],
4798
+ description: "Manglik, Kaal Sarp, or Sade Sati presence card",
4799
+ docsLabel: "Vedic",
4800
+ endpointLabel: "POST /vedic-astrology/dosha/{manglik,kalsarpa,sadhesati}",
4801
+ docsSummary: "Presence, severity, remedies, scoped effects",
4802
+ topic: "Vedic"
4803
+ },
4804
+ {
4805
+ pascal: "RoxyGunaMilan",
4806
+ tag: "roxy-guna-milan",
4807
+ slug: "guna-milan",
4808
+ domain: "vedic",
4809
+ heading: "Guna milan",
4810
+ endpoints: ["vedicAstrology.calculateGunMilan"],
4811
+ description: "36-point Ashtakoota matrimonial compatibility breakdown",
4812
+ docsLabel: "Vedic",
4813
+ endpointLabel: "POST /vedic-astrology/compatibility",
4814
+ docsSummary: "36-point Ashtakoota with eight sub-scores",
4815
+ topic: "Vedic"
4816
+ },
4817
+ {
4818
+ pascal: "RoxyKpPlanetsTable",
4819
+ tag: "roxy-kp-planets-table",
4820
+ slug: "kp-planets-table",
4821
+ domain: "vedic",
4822
+ heading: "KP planets",
4823
+ endpoints: ["vedicAstrology.getKpPlanets"],
4824
+ description: "KP planets table with sub-lord and sub-sub-lord columns",
4825
+ docsLabel: "Vedic (KP)",
4826
+ endpointLabel: "POST /vedic-astrology/kp/planets",
4827
+ docsSummary: "Sub-lord and sub-sub-lord columns",
4828
+ topic: "Vedic"
4829
+ },
4830
+ {
4831
+ pascal: "RoxyNumerologyCard",
4832
+ tag: "roxy-numerology-card",
4833
+ slug: "numerology-card",
4834
+ domain: "numerology",
4835
+ heading: "Life path number",
4836
+ endpoints: [
4837
+ "numerology.calculateLifePath",
4838
+ "numerology.calculateExpression",
4839
+ "numerology.calculatePersonalYear",
4840
+ "numerology.generateNumerologyChart"
4841
+ ],
4842
+ description: "Numerology card for life path, expression, personal year, or full chart",
4843
+ docsLabel: "Numerology",
4844
+ endpointLabel: "POST /numerology/{life-path,expression,personal-year,chart}",
4845
+ docsSummary: "Life path, expression, personal year, full chart",
4846
+ topic: "Numerology"
4847
+ },
4848
+ {
4849
+ pascal: "RoxyTarotCard",
4850
+ tag: "roxy-tarot-card",
4851
+ slug: "tarot-card",
4852
+ domain: "tarot",
4853
+ heading: "Daily tarot card",
4854
+ endpoints: ["tarot.getCard", "tarot.getDailyCard"],
4855
+ description: "Single tarot card with upright/reversed flip animation",
4856
+ docsLabel: "Tarot",
4857
+ endpointLabel: "GET /tarot/cards/{id}, POST /tarot/daily",
4858
+ docsSummary: "Single card with upright and reversed flip",
4859
+ topic: "Tarot"
4860
+ },
4861
+ {
4862
+ pascal: "RoxyTarotSpread",
4863
+ tag: "roxy-tarot-spread",
4864
+ slug: "tarot-spread",
4865
+ domain: "tarot",
4866
+ heading: "Three-card spread",
4867
+ endpoints: [
4868
+ "tarot.castThreeCard",
4869
+ "tarot.castCelticCross",
4870
+ "tarot.castLoveSpread",
4871
+ "tarot.castYesNo",
4872
+ "tarot.drawCards"
4873
+ ],
4874
+ description: "Tarot spread renderer for three-card, Celtic Cross, love, or yes/no",
4875
+ docsLabel: "Tarot",
4876
+ endpointLabel: "POST /tarot/spreads/{three-card,celtic-cross,love}, /tarot/yes-no, /tarot/draw",
4877
+ docsSummary: "Spreads with positions and reading",
4878
+ topic: "Tarot"
4879
+ },
4880
+ {
4881
+ pascal: "RoxyBiorhythmChart",
4882
+ tag: "roxy-biorhythm-chart",
4883
+ slug: "biorhythm-chart",
4884
+ domain: "biorhythm",
4885
+ heading: "Daily biorhythm",
4886
+ endpoints: [
4887
+ "biorhythm.getDailyBiorhythm",
4888
+ "biorhythm.getForecast",
4889
+ "biorhythm.getCriticalDays"
4890
+ ],
4891
+ description: "Daily biorhythm bars or multi-day forecast cycle lines",
4892
+ docsLabel: "Biorhythm",
4893
+ endpointLabel: "POST /biorhythm/{daily,forecast,critical-days}",
4894
+ docsSummary: "Daily bars, forecast cycle lines, critical days",
4895
+ topic: "Biorhythm"
4896
+ },
4897
+ {
4898
+ pascal: "RoxyHexagram",
4899
+ tag: "roxy-hexagram",
4900
+ slug: "hexagram",
4901
+ domain: "iching",
4902
+ heading: "I Ching hexagram",
4903
+ endpoints: [
4904
+ "iching.getHexagram",
4905
+ "iching.castReading",
4906
+ "iching.getDailyHexagram",
4907
+ "iching.castDailyReading",
4908
+ "iching.getRandomHexagram"
4909
+ ],
4910
+ description: "I Ching hexagram with trigram glyphs, judgment, image, and changing lines",
4911
+ docsLabel: "I Ching",
4912
+ endpointLabel: "GET /iching/hexagrams/{number}, /iching/cast, POST /iching/daily, /iching/daily/cast",
4913
+ docsSummary: "Hexagram with trigrams, judgment, image, changing lines",
4914
+ topic: "I Ching"
4915
+ },
4916
+ {
4917
+ pascal: "RoxyEndpointForm",
4918
+ tag: "roxy-endpoint-form",
4919
+ slug: "endpoint-form",
4920
+ domain: "utility",
4921
+ heading: "Schema-driven form",
4922
+ endpoints: [],
4923
+ description: "Schema-driven form that emits roxy-submit with a validated payload",
4924
+ docsLabel: "Helper",
4925
+ endpointLabel: "Any endpoint via x-roxy-ui hints",
4926
+ docsSummary: "Schema-driven form, emits roxy-submit",
4927
+ topic: "Helpers",
4928
+ selfFetching: true
4929
+ },
4930
+ {
4931
+ pascal: "RoxyLocationSearch",
4932
+ tag: "roxy-location-search",
4933
+ slug: "location-search",
4934
+ domain: "utility",
4935
+ heading: "City search",
4936
+ endpoints: ["location.searchCities"],
4937
+ description: "City search input with debounced /location/search calls",
4938
+ docsLabel: "Helper",
4939
+ endpointLabel: "GET /location/search",
4940
+ docsSummary: "Debounced city search input, emits roxy-location-select",
4941
+ topic: "Helpers",
4942
+ selfFetching: true
4943
+ },
4944
+ {
4945
+ pascal: "RoxyData",
4946
+ tag: "roxy-data",
4947
+ slug: "data",
4948
+ domain: "utility",
4949
+ heading: "Generic renderer",
4950
+ endpoints: [],
4951
+ description: "Generic fallback renderer for any OpenAPI response shape",
4952
+ docsLabel: "Helper",
4953
+ endpointLabel: "Any response shape",
4954
+ docsSummary: "Generic fallback renderer for unknown shapes",
4955
+ topic: "Helpers",
4956
+ selfFetching: true
4957
+ }
4173
4958
  ];
4959
+
4960
+ // packages/ui/src/version.ts
4961
+ var ROXY_UI_VERSION = "0.1.3";
4962
+
4963
+ // packages/ui/src/index.ts
4964
+ var ROXY_UI_COMPONENTS = ROXY_COMPONENTS.map((c) => c.slug);
4174
4965
  //# sourceMappingURL=index.cjs.map