@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.js CHANGED
@@ -122,25 +122,28 @@ var RoxyBiorhythmChart = class extends LitElement {
122
122
  const d = this.data;
123
123
  if (!d)
124
124
  return html`<div class="roxy-empty" role="status">No biorhythm data</div>`;
125
- if (this.mode === "critical-days" && d.criticalDays?.length) {
125
+ if (this.mode === "critical-days" && "criticalDays" in d) {
126
126
  return this.renderCritical(d);
127
127
  }
128
- if (this.mode === "forecast" && d.days?.length) {
128
+ if (this.mode === "forecast" && "days" in d) {
129
129
  return this.renderForecast(d);
130
130
  }
131
131
  return this.renderDaily(d);
132
132
  }
133
133
  renderDaily(d) {
134
- const cycles = d.cycles ?? {};
135
- const entries = Object.entries(cycles);
134
+ const raw = d.quickRead ?? {};
135
+ const entries = Object.entries(raw).map(([cycle, value]) => {
136
+ const v = typeof value === "number" ? value : 0;
137
+ const normalized = Math.abs(v) > 1 ? v / 100 : v;
138
+ return [cycle, normalized];
139
+ });
136
140
  return html`<section class="wrap" aria-label="Daily biorhythm">
137
141
  <header class="head">
138
142
  <h2 class="title">Biorhythm</h2>
139
143
  ${typeof d.energyRating === "number" ? html`<span class="energy">Energy ${d.energyRating}/10</span>` : nothing}
140
144
  </header>
141
145
  <div class="bars" role="list">
142
- ${entries.map(([cycle, value]) => {
143
- const v = typeof value === "number" ? value : 0;
146
+ ${entries.map(([cycle, v]) => {
144
147
  const pct = (v + 1) / 2 * 100;
145
148
  const color = CYCLE_COLOR[cycle] ?? "var(--roxy-accent, #f59e0b)";
146
149
  return html`<div class="bar" role="listitem">
@@ -151,15 +154,12 @@ var RoxyBiorhythmChart = class extends LitElement {
151
154
  style="width: ${pct}%; background: ${color}"
152
155
  ></span>
153
156
  </span>
154
- <span class="value">${(v * 100).toFixed(0)}%</span>
157
+ <span class="value">${Math.round(v * 100)}%</span>
155
158
  </div>`;
156
159
  })}
157
160
  </div>
158
- ${d.interpretation ? html`<p class="advice">${d.interpretation}</p>` : nothing}
161
+ ${d.dailyMessage ? html`<p class="advice">${d.dailyMessage}</p>` : nothing}
159
162
  ${d.advice ? html`<p class="advice">${d.advice}</p>` : nothing}
160
- ${d.criticalAlerts?.length ? html`<div>
161
- ${d.criticalAlerts.map((a) => html`<p class="alert">${a}</p>`)}
162
- </div>` : nothing}
163
163
  </section>`;
164
164
  }
165
165
  renderForecast(d) {
@@ -169,13 +169,16 @@ var RoxyBiorhythmChart = class extends LitElement {
169
169
  const w = 600;
170
170
  const h = 160;
171
171
  const xStep = w / Math.max(days.length - 1, 1);
172
- const cycles = Object.keys(days[0]?.cycles ?? {});
172
+ const cycleKeys = [
173
+ "physical",
174
+ "emotional",
175
+ "intellectual",
176
+ "intuitive"
177
+ ];
173
178
  return html`<section class="wrap" aria-label="Biorhythm forecast">
174
179
  <header class="head">
175
180
  <h2 class="title">Forecast</h2>
176
- <span class="energy"
177
- >${d.startDate ?? ""} - ${d.endDate ?? ""}</span
178
- >
181
+ <span class="energy">${d.startDate} - ${d.endDate}</span>
179
182
  </header>
180
183
  <svg
181
184
  viewBox="0 0 ${w} ${h}"
@@ -191,11 +194,11 @@ var RoxyBiorhythmChart = class extends LitElement {
191
194
  stroke="var(--roxy-border, #e4e4e7)"
192
195
  stroke-width="1"
193
196
  />
194
- ${cycles.map((cycle) => {
197
+ ${cycleKeys.map((cycle) => {
195
198
  const points = days.map((day, i) => {
196
- const v = day.cycles?.[cycle] ?? 0;
199
+ const v = day[cycle] ?? 0;
197
200
  const x = i * xStep;
198
- const y = h / 2 - v * (h / 2 - 8);
201
+ const y = h / 2 - v / 100 * (h / 2 - 8);
199
202
  return `${x.toFixed(2)},${y.toFixed(2)}`;
200
203
  }).join(" ");
201
204
  const color = CYCLE_COLOR[cycle] ?? "#475569";
@@ -209,14 +212,12 @@ var RoxyBiorhythmChart = class extends LitElement {
209
212
  return html`<section class="wrap" aria-label="Critical days">
210
213
  <header class="head">
211
214
  <h2 class="title">Critical days</h2>
212
- <span class="energy"
213
- >${d.totalCriticalDays ?? d.criticalDays?.length ?? 0} total</span
214
- >
215
+ <span class="energy">${d.totalCriticalDays} total</span>
215
216
  </header>
216
217
  <div>
217
- ${(d.criticalDays ?? []).map(
218
+ ${d.criticalDays.map(
218
219
  (day) => html`<span class="crit"
219
- >${day.date} · ${day.cycle ?? ""} ${day.severity ?? ""}</span
220
+ >${day.date} · ${day.cycle} ${day.severity}</span
220
221
  >`
221
222
  )}
222
223
  </div>
@@ -316,6 +317,50 @@ RoxyBiorhythmChart = __decorateClass([
316
317
  // packages/ui/src/components/compatibility-card.ts
317
318
  import { css as css3, html as html2, LitElement as LitElement2, nothing as nothing2 } from "lit";
318
319
  import { customElement as customElement2, property as property2 } from "lit/decorators.js";
320
+
321
+ // packages/ui/src/utils/format.ts
322
+ function formatTime(input) {
323
+ if (typeof input !== "string" || input.length === 0) return "";
324
+ if (/^\d{4}-\d{2}-\d{2}$/.test(input)) return "";
325
+ const bareTime = /^\d{2}:\d{2}(:\d{2})?$/.test(input);
326
+ const iso = bareTime ? `1970-01-01T${input}` : input;
327
+ const d = new Date(iso);
328
+ if (Number.isNaN(d.getTime())) return input;
329
+ return d.toLocaleTimeString(void 0, {
330
+ hour: "numeric",
331
+ minute: "2-digit",
332
+ hour12: true
333
+ });
334
+ }
335
+ function formatDate(input) {
336
+ if (typeof input !== "string" || input.length === 0) return "";
337
+ const d = new Date(
338
+ /^\d{4}-\d{2}-\d{2}$/.test(input) ? `${input}T00:00:00` : input
339
+ );
340
+ if (Number.isNaN(d.getTime())) return input;
341
+ return d.toLocaleDateString(void 0, {
342
+ month: "short",
343
+ day: "numeric",
344
+ year: "numeric"
345
+ });
346
+ }
347
+ function formatTimeRange(t) {
348
+ if (!t) return "";
349
+ const start = formatTime(t.start);
350
+ const end = formatTime(t.end);
351
+ if (start && end) return `${start} - ${end}`;
352
+ return start || end || "";
353
+ }
354
+ function formatNumber(value, dp = 1) {
355
+ if (typeof value !== "number" || !Number.isFinite(value)) return "";
356
+ return value.toFixed(dp).replace(/\.?0+$/, "");
357
+ }
358
+ function formatPercent(value, dp = 1) {
359
+ const n = formatNumber(value, dp);
360
+ return n ? `${n}%` : "";
361
+ }
362
+
363
+ // packages/ui/src/components/compatibility-card.ts
319
364
  var RoxyCompatibilityCard = class extends LitElement2 {
320
365
  constructor() {
321
366
  super(...arguments);
@@ -325,22 +370,29 @@ var RoxyCompatibilityCard = class extends LitElement2 {
325
370
  getBreakdown() {
326
371
  const d = this.data;
327
372
  if (!d) return {};
328
- if (d.categoryScores) return d.categoryScores;
329
- if (d.categoryBreakdown) return d.categoryBreakdown;
330
- const inferred = {};
331
- if (typeof d.emotional === "number") inferred.emotional = d.emotional;
332
- if (typeof d.communication === "number")
333
- inferred.communication = d.communication;
334
- if (typeof d.romance === "number") inferred.romance = d.romance;
335
- if (d.elementBalance) Object.assign(inferred, d.elementBalance);
336
- return inferred;
373
+ if ("categories" in d && d.categories) {
374
+ const out = {};
375
+ for (const [k, v] of Object.entries(d.categories)) {
376
+ if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
377
+ }
378
+ return out;
379
+ }
380
+ return {};
337
381
  }
338
382
  render() {
339
383
  const d = this.data;
340
384
  if (!d)
341
385
  return html2`<div class="roxy-empty" role="status">No compatibility data</div>`;
342
- const score = d.overallScore ?? d.score;
386
+ const score = d.overallScore;
343
387
  const breakdown = this.getBreakdown();
388
+ const rating = "rating" in d ? d.rating : void 0;
389
+ const archetype = "archetype" in d ? d.archetype : void 0;
390
+ const advice = "advice" in d ? d.advice : void 0;
391
+ const summary = "summary" in d ? d.summary : void 0;
392
+ const interpretation = "interpretation" in d ? d.interpretation : void 0;
393
+ const strengths = "strengths" in d ? d.strengths : void 0;
394
+ const challenges = "challenges" in d ? d.challenges : void 0;
395
+ const keyAspects = "keyAspects" in d ? d.keyAspects : void 0;
344
396
  return html2`<article
345
397
  class="card"
346
398
  aria-label=${`Compatibility (${this.mode})`}
@@ -348,8 +400,8 @@ var RoxyCompatibilityCard = class extends LitElement2 {
348
400
  <div class="head">
349
401
  <h2>${this.mode} compatibility</h2>
350
402
  <div>
351
- ${typeof score === "number" ? html2`<div class="score">${score}</div>` : nothing2}
352
- ${d.rating ? html2`<div class="rating">${d.rating}</div>` : nothing2}
403
+ ${typeof score === "number" ? html2`<div class="score">${formatNumber(score, 0)}</div>` : nothing2}
404
+ ${rating ? html2`<div class="rating">${rating}</div>` : nothing2}
353
405
  </div>
354
406
  </div>
355
407
 
@@ -360,35 +412,37 @@ var RoxyCompatibilityCard = class extends LitElement2 {
360
412
  <span class="bar"
361
413
  ><span style="width: ${Math.max(0, Math.min(100, v))}%"></span
362
414
  ></span>
363
- <span>${v}</span>
415
+ <span>${formatNumber(v, 0)}</span>
364
416
  </div>`
365
417
  )}
366
418
  </div>` : nothing2}
367
- ${d.relationshipArchetype ? html2`<p>
368
- <span class="archetype">${d.relationshipArchetype}</span>
419
+ ${archetype ? html2`<p>
420
+ <span class="archetype">${archetype.label}</span>
421
+ ${archetype.description ? html2` · ${archetype.description}` : nothing2}
369
422
  </p>` : nothing2}
370
- ${d.summary ? html2`<p>${d.summary}</p>` : nothing2}
371
- ${d.advice ? html2`<p>${d.advice}</p>` : nothing2}
372
- ${(d.strengths?.length ?? 0) > 0 || (d.challenges?.length ?? 0) > 0 ? html2`<div class="lists">
373
- ${d.strengths?.length ? html2`<div>
423
+ ${summary ? html2`<p>${summary}</p>` : nothing2}
424
+ ${interpretation && !summary ? html2`<p>${interpretation}</p>` : nothing2}
425
+ ${advice ? html2`<p>${advice}</p>` : nothing2}
426
+ ${(strengths?.length ?? 0) > 0 || (challenges?.length ?? 0) > 0 ? html2`<div class="lists">
427
+ ${strengths?.length ? html2`<div>
374
428
  <h3>Strengths</h3>
375
429
  <ul>
376
- ${d.strengths.map((s) => html2`<li>${s}</li>`)}
430
+ ${strengths.map((s) => html2`<li>${s}</li>`)}
377
431
  </ul>
378
432
  </div>` : nothing2}
379
- ${d.challenges?.length ? html2`<div>
433
+ ${challenges?.length ? html2`<div>
380
434
  <h3>Challenges</h3>
381
435
  <ul>
382
- ${d.challenges.map((s) => html2`<li>${s}</li>`)}
383
- </ul>
384
- </div>` : nothing2}
385
- ${d.keyAspects?.length ? html2`<div>
386
- <h3>Key aspects</h3>
387
- <ul>
388
- ${d.keyAspects.map((s) => html2`<li>${s}</li>`)}
436
+ ${challenges.map((s) => html2`<li>${s}</li>`)}
389
437
  </ul>
390
438
  </div>` : nothing2}
391
439
  </div>` : nothing2}
440
+ ${keyAspects?.length ? html2`<div>
441
+ <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>
442
+ <ul style="margin: 0; padding-left: 1rem; font-size: var(--roxy-text-sm);">
443
+ ${keyAspects.slice(0, 6).map((a) => html2`<li>${formatAspect(a)}</li>`)}
444
+ </ul>
445
+ </div>` : nothing2}
392
446
  </article>`;
393
447
  }
394
448
  };
@@ -458,7 +512,7 @@ RoxyCompatibilityCard.styles = [
458
512
  }
459
513
 
460
514
  .archetype {
461
- color: var(--roxy-info, #0284c7);
515
+ color: var(--roxy-accent-fg, #b45309);
462
516
  font-weight: var(--roxy-weight-bold, 600);
463
517
  }
464
518
 
@@ -489,6 +543,12 @@ __decorateClass([
489
543
  RoxyCompatibilityCard = __decorateClass([
490
544
  customElement2("roxy-compatibility-card")
491
545
  ], RoxyCompatibilityCard);
546
+ function formatAspect(a) {
547
+ const aspect = a.type.toLowerCase().replace(/_/g, "-");
548
+ const orb = typeof a.orb === "number" ? ` (orb ${formatNumber(a.orb, 1)}\xB0)` : "";
549
+ const head = [a.planet1, aspect, a.planet2].filter(Boolean).join(" ");
550
+ return a.description ? `${head}${orb} \xB7 ${a.description}` : `${head}${orb}`;
551
+ }
492
552
 
493
553
  // packages/ui/src/components/dasha-timeline.ts
494
554
  import { css as css4, html as html3, LitElement as LitElement3, nothing as nothing3 } from "lit";
@@ -504,16 +564,16 @@ var RoxyDashaTimeline = class extends LitElement3 {
504
564
  if (!d)
505
565
  return html3`<div class="roxy-empty" role="status">No dasha data</div>`;
506
566
  const periods = this.collectPeriods(d);
507
- const maxYears = periods.length ? Math.max(...periods.map((p) => p.durationYears ?? p.years ?? 1)) : 0;
567
+ const maxYears = periods.length ? Math.max(...periods.map((p) => p.durationYears)) : 0;
508
568
  return html3`<div class="wrap" aria-label="Dasha timeline">
509
569
  <header class="head">
510
570
  <h2 class="title">
511
571
  ${this.period === "major" ? "Vimshottari Mahadasha" : this.period === "sub" ? "Antardasha" : "Active dashas"}
512
572
  </h2>
513
- ${d.nakshatraName || d.moonNakshatra ? html3`<div class="nakshatra">
514
- Moon nakshatra: ${d.nakshatraName ?? d.moonNakshatra}
515
- ${d.nakshatraLord ? html3`(lord ${d.nakshatraLord})` : nothing3}
516
- </div>` : nothing3}
573
+ ${"nakshatraName" in d && d.nakshatraName ? html3`<div class="nakshatra">
574
+ Moon nakshatra: ${d.nakshatraName}
575
+ ${"nakshatraLord" in d && d.nakshatraLord ? html3`(lord ${d.nakshatraLord})` : nothing3}
576
+ </div>` : nothing3}
517
577
  </header>
518
578
 
519
579
  ${this.period === "current" ? this.renderCurrent(d) : nothing3}
@@ -523,39 +583,35 @@ var RoxyDashaTimeline = class extends LitElement3 {
523
583
  </div>`;
524
584
  }
525
585
  renderCurrent(d) {
586
+ if (!("mahadasha" in d)) return nothing3;
526
587
  return html3`<div class="current">
527
- ${d.mahadasha ? html3`<div>
528
- <span>Mahadasha</span>
529
- <strong>${d.mahadasha.lord ?? d.mahadasha.mahadashaLord}</strong>
530
- ${typeof d.remainingInMahadasha === "number" ? html3`<small>${d.remainingInMahadasha.toFixed(1)} years left</small>` : nothing3}
531
- </div>` : nothing3}
532
- ${d.antardasha ? html3`<div>
533
- <span>Antardasha</span>
534
- <strong>${d.antardasha.lord ?? d.antardasha.antardashaLord}</strong>
535
- ${typeof d.remainingInAntardasha === "number" ? html3`<small>${d.remainingInAntardasha.toFixed(1)} years left</small>` : nothing3}
536
- </div>` : nothing3}
537
- ${d.pratyantardasha ? html3`<div>
538
- <span>Pratyantardasha</span>
539
- <strong
540
- >${d.pratyantardasha.lord ?? d.pratyantardasha.pratyantardashaLord}</strong
541
- >
542
- ${typeof d.remainingInPratyantardasha === "number" ? html3`<small
543
- >${d.remainingInPratyantardasha.toFixed(2)} years left</small
544
- >` : nothing3}
545
- </div>` : nothing3}
588
+ ${"mahadasha" in d && d.mahadasha ? html3`<div>
589
+ <span>Mahadasha</span>
590
+ <strong>${d.mahadasha.planet}</strong>
591
+ ${"remainingInMahadasha" in d && d.remainingInMahadasha ? html3`<small>${formatNumber(d.remainingInMahadasha.years + d.remainingInMahadasha.months / 12, 1)} years left</small>` : nothing3}
592
+ </div>` : nothing3}
593
+ ${"antardasha" in d && d.antardasha ? html3`<div>
594
+ <span>Antardasha</span>
595
+ <strong>${d.antardasha.planet}</strong>
596
+ ${"remainingInAntardasha" in d && d.remainingInAntardasha ? html3`<small>${formatNumber(d.remainingInAntardasha.years + d.remainingInAntardasha.months / 12, 1)} years left</small>` : nothing3}
597
+ </div>` : nothing3}
598
+ ${"pratyantardasha" in d && d.pratyantardasha ? html3`<div>
599
+ <span>Pratyantardasha</span>
600
+ <strong>${d.pratyantardasha.planet}</strong>
601
+ ${"remainingInPratyantardasha" in d && d.remainingInPratyantardasha ? html3`<small>${formatNumber(d.remainingInPratyantardasha.years + d.remainingInPratyantardasha.months / 12, 1)} years left</small>` : nothing3}
602
+ </div>` : nothing3}
546
603
  </div>`;
547
604
  }
548
605
  collectPeriods(d) {
549
- if (this.period === "major" && d.mahadashas?.length) return d.mahadashas;
550
- if (this.period === "sub" && d.antardashas?.length) return d.antardashas;
551
- return d.mahadashas ?? d.antardashas ?? [];
606
+ if ("mahadashas" in d && d.mahadashas?.length) return d.mahadashas;
607
+ if ("antardashas" in d && d.antardashas?.length) return d.antardashas;
608
+ return [];
552
609
  }
553
610
  renderBar(p, max) {
554
- const lord = p.lord ?? p.mahadashaLord ?? p.antardashaLord ?? p.planet ?? "";
555
- const years = p.durationYears ?? p.years ?? 0;
611
+ const years = p.durationYears;
556
612
  const width = max > 0 ? years / max * 100 : 0;
557
613
  return html3`<div class="bar" role="listitem">
558
- <span>${lord}</span>
614
+ <span>${p.planet}</span>
559
615
  <span class="bar-track"><span style="width: ${width}%"></span></span>
560
616
  <span class="dates">
561
617
  ${p.startDate ? formatYear(p.startDate) : ""}
@@ -663,15 +719,20 @@ import { customElement as customElement4, property as property4 } from "lit/deco
663
719
  var TITLE_KEYS = ["title", "name", "label", "heading", "overview", "summary"];
664
720
  var IMAGE_KEYS = ["imageUrl", "image", "icon", "symbol"];
665
721
  var SKIP_KEYS = ["imageUrl", "image"];
722
+ var MAX_DEPTH = 6;
666
723
  var RoxyData = class extends LitElement4 {
667
724
  constructor() {
668
725
  super(...arguments);
669
726
  this.data = null;
727
+ this.depth = 0;
670
728
  }
671
729
  render() {
672
730
  if (this.data == null) {
673
731
  return html4`<div class="roxy-empty" role="status">No data</div>`;
674
732
  }
733
+ if (this.depth >= MAX_DEPTH) {
734
+ return html4`<div class="roxy-empty" role="status">…</div>`;
735
+ }
675
736
  return html4`<div
676
737
  class="roxy-card"
677
738
  aria-label="Generic data display"
@@ -768,7 +829,7 @@ var RoxyData = class extends LitElement4 {
768
829
  </ul>`;
769
830
  }
770
831
  }
771
- return html4`<roxy-data .data=${value}></roxy-data>`;
832
+ return html4`<roxy-data .data=${value} .depth=${this.depth + 1}></roxy-data>`;
772
833
  }
773
834
  formatPrimitive(value) {
774
835
  if (value === null || value === void 0) return "";
@@ -890,6 +951,9 @@ RoxyData.styles = [
890
951
  __decorateClass([
891
952
  property4({ attribute: false })
892
953
  ], RoxyData.prototype, "data", 2);
954
+ __decorateClass([
955
+ property4({ attribute: false })
956
+ ], RoxyData.prototype, "depth", 2);
893
957
  RoxyData = __decorateClass([
894
958
  customElement4("roxy-data")
895
959
  ], RoxyData);
@@ -935,25 +999,24 @@ var RoxyDoshaCard = class extends LitElement5 {
935
999
  </div>
936
1000
  </header>
937
1001
  ${d.description ? html5`<p class="description">${d.description}</p>` : nothing5}
938
- ${this.renderEffects(d.effects)}
1002
+ ${this.renderEffects(d)}
939
1003
  ${d.remedies && d.remedies.length > 0 ? html5`<div>
940
1004
  <h3>Remedies</h3>
941
1005
  <ul>
942
1006
  ${d.remedies.map((r) => html5`<li>${r}</li>`)}
943
1007
  </ul>
944
1008
  </div>` : nothing5}
945
- ${d.exceptions && d.exceptions.length > 0 ? html5`<div>
946
- <h3>Exceptions</h3>
947
- <ul>
948
- ${d.exceptions.map((r) => html5`<li>${r}</li>`)}
949
- </ul>
950
- </div>` : nothing5}
1009
+ ${"exceptions" in d && d.exceptions && d.exceptions.length > 0 ? html5`<div>
1010
+ <h3>Exceptions</h3>
1011
+ <ul>
1012
+ ${d.exceptions.map((r) => html5`<li>${r}</li>`)}
1013
+ </ul>
1014
+ </div>` : nothing5}
951
1015
  </article>`;
952
1016
  }
953
- renderEffects(e) {
954
- if (!e) return nothing5;
955
- if (typeof e === "string") return html5`<p>${e}</p>`;
956
- const entries = Object.entries(e).filter(
1017
+ renderEffects(d) {
1018
+ if (!d.effects) return nothing5;
1019
+ const entries = Object.entries(d.effects).filter(
957
1020
  ([, v]) => typeof v === "string" && v.length > 0
958
1021
  );
959
1022
  if (entries.length === 0) return nothing5;
@@ -1005,11 +1068,11 @@ RoxyDoshaCard.styles = [
1005
1068
  }
1006
1069
  .badge.absent {
1007
1070
  background: color-mix(in srgb, var(--roxy-success, #16a34a) 16%, transparent);
1008
- color: var(--roxy-success, #16a34a);
1071
+ color: var(--roxy-success-fg, #166534);
1009
1072
  }
1010
1073
  .badge.present {
1011
1074
  background: color-mix(in srgb, var(--roxy-danger, #dc2626) 16%, transparent);
1012
- color: var(--roxy-danger, #dc2626);
1075
+ color: var(--roxy-danger-fg, #991b1b);
1013
1076
  }
1014
1077
  .severity {
1015
1078
  display: flex;
@@ -1073,6 +1136,21 @@ RoxyDoshaCard = __decorateClass([
1073
1136
  // packages/ui/src/components/endpoint-form.ts
1074
1137
  import { css as css7, html as html6, LitElement as LitElement6, nothing as nothing6 } from "lit";
1075
1138
  import { customElement as customElement6, property as property6, state } from "lit/decorators.js";
1139
+ var specCache = /* @__PURE__ */ new Map();
1140
+ async function loadSpec(url) {
1141
+ let pending = specCache.get(url);
1142
+ if (!pending) {
1143
+ pending = fetch(url).then(async (res) => {
1144
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1145
+ return await res.json();
1146
+ }).catch((err) => {
1147
+ specCache.delete(url);
1148
+ throw err;
1149
+ });
1150
+ specCache.set(url, pending);
1151
+ }
1152
+ return pending;
1153
+ }
1076
1154
  var RoxyEndpointForm = class extends LitElement6 {
1077
1155
  constructor() {
1078
1156
  super(...arguments);
@@ -1084,6 +1162,12 @@ var RoxyEndpointForm = class extends LitElement6 {
1084
1162
  this.values = {};
1085
1163
  this.hasLocation = false;
1086
1164
  this.loaded = false;
1165
+ this.specError = null;
1166
+ this.retryLoadSchema = () => {
1167
+ this.loaded = false;
1168
+ this.specError = null;
1169
+ void this.loadSchema();
1170
+ };
1087
1171
  this.onLocation = (e) => {
1088
1172
  const detail = e.detail;
1089
1173
  if (detail) {
@@ -1124,13 +1208,16 @@ var RoxyEndpointForm = class extends LitElement6 {
1124
1208
  void this.loadSchema();
1125
1209
  }
1126
1210
  async loadSchema() {
1211
+ this.specError = null;
1127
1212
  try {
1128
- const res = await fetch(this.specUrl);
1129
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
1130
- const spec = await res.json();
1213
+ const spec = await loadSpec(this.specUrl);
1131
1214
  const path = `/${this.endpoint.replace(/^\//, "")}`;
1132
1215
  const op = spec.paths?.[path]?.[this.method.toLowerCase()];
1133
- if (!op) return;
1216
+ if (!op) {
1217
+ throw new Error(
1218
+ `Endpoint ${this.method} ${path} not found in OpenAPI spec`
1219
+ );
1220
+ }
1134
1221
  const schemas = spec.components?.schemas ?? {};
1135
1222
  const fields = [];
1136
1223
  let bodySchema;
@@ -1175,8 +1262,17 @@ var RoxyEndpointForm = class extends LitElement6 {
1175
1262
  }
1176
1263
  this.values = init;
1177
1264
  this.loaded = true;
1178
- } catch (_err) {
1265
+ } catch (err) {
1266
+ const message = err instanceof Error ? err.message : String(err);
1267
+ this.specError = message;
1179
1268
  this.loaded = true;
1269
+ this.dispatchEvent(
1270
+ new CustomEvent("roxy-spec-error", {
1271
+ detail: { url: this.specUrl, message },
1272
+ bubbles: true,
1273
+ composed: true
1274
+ })
1275
+ );
1180
1276
  }
1181
1277
  }
1182
1278
  resolve(schema, all) {
@@ -1202,6 +1298,12 @@ var RoxyEndpointForm = class extends LitElement6 {
1202
1298
  if (!this.loaded) {
1203
1299
  return html6`<form><div class="roxy-skeleton" style="height: 8rem"></div></form>`;
1204
1300
  }
1301
+ if (this.specError) {
1302
+ return html6`<div class="spec-error" role="alert">
1303
+ Schema load failed: ${this.specError}
1304
+ <button type="button" class="submit" @click=${this.retryLoadSchema}>Retry</button>
1305
+ </div>`;
1306
+ }
1205
1307
  const renderField = (f) => {
1206
1308
  if (this.hasLocation && (f.name === "latitude" || f.name === "longitude" || f.name === "timezone")) {
1207
1309
  return nothing6;
@@ -1299,22 +1401,27 @@ RoxyEndpointForm.styles = [
1299
1401
  .fields {
1300
1402
  display: grid;
1301
1403
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
1404
+ align-items: start;
1302
1405
  gap: var(--roxy-space-md, 1rem);
1303
1406
  }
1304
1407
  .field {
1305
- display: grid;
1408
+ display: flex;
1409
+ flex-direction: column;
1306
1410
  gap: var(--roxy-space-xs, 0.25rem);
1411
+ min-width: 0;
1307
1412
  }
1308
1413
  label {
1309
1414
  font-size: var(--roxy-text-sm, 0.875rem);
1310
1415
  color: var(--roxy-secondary, #475569);
1311
1416
  }
1312
1417
  label .req {
1313
- color: var(--roxy-danger, #dc2626);
1418
+ color: var(--roxy-danger-fg, #991b1b);
1314
1419
  margin-left: 4px;
1315
1420
  }
1316
1421
  input,
1317
1422
  select {
1423
+ width: 100%;
1424
+ box-sizing: border-box;
1318
1425
  padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem);
1319
1426
  font-size: var(--roxy-text-base, 1rem);
1320
1427
  font-family: inherit;
@@ -1349,7 +1456,7 @@ RoxyEndpointForm.styles = [
1349
1456
  button.submit {
1350
1457
  justify-self: start;
1351
1458
  background: var(--roxy-accent-fg, #b45309);
1352
- color: #fff;
1459
+ color: var(--roxy-bg, #fff);
1353
1460
  border: 0;
1354
1461
  border-radius: var(--roxy-radius-md, 8px);
1355
1462
  padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-lg, 1.5rem);
@@ -1367,6 +1474,17 @@ RoxyEndpointForm.styles = [
1367
1474
  outline: 2px solid var(--roxy-ring, rgba(245, 158, 11, 0.4));
1368
1475
  outline-offset: 2px;
1369
1476
  }
1477
+ .spec-error {
1478
+ display: grid;
1479
+ gap: var(--roxy-space-md, 1rem);
1480
+ justify-items: start;
1481
+ background: var(--roxy-bg, #fff);
1482
+ border: 1px solid var(--roxy-danger, #dc2626);
1483
+ border-radius: var(--roxy-radius-md, 8px);
1484
+ padding: var(--roxy-space-lg, 1.5rem);
1485
+ color: var(--roxy-danger-fg, #991b1b);
1486
+ font-size: var(--roxy-text-sm, 0.875rem);
1487
+ }
1370
1488
  `
1371
1489
  ];
1372
1490
  __decorateClass([
@@ -1393,6 +1511,9 @@ __decorateClass([
1393
1511
  __decorateClass([
1394
1512
  state()
1395
1513
  ], RoxyEndpointForm.prototype, "loaded", 2);
1514
+ __decorateClass([
1515
+ state()
1516
+ ], RoxyEndpointForm.prototype, "specError", 2);
1396
1517
  RoxyEndpointForm = __decorateClass([
1397
1518
  customElement6("roxy-endpoint-form")
1398
1519
  ], RoxyEndpointForm);
@@ -1412,18 +1533,16 @@ var RoxyGunaMilan = class extends LitElement7 {
1412
1533
  const d = this.data;
1413
1534
  if (!d)
1414
1535
  return html7`<div class="roxy-empty" role="status">No Guna Milan data</div>`;
1415
- const total = d.total ?? d.totalScore ?? 0;
1416
- const max = d.maxScore ?? 36;
1417
1536
  const breakdown = (d.breakdown ?? []).filter(
1418
- (b) => b && (b.name || b.score !== void 0)
1537
+ (b) => b?.category !== void 0
1419
1538
  );
1420
1539
  return html7`<article class="card" aria-label="Guna Milan score">
1421
1540
  <div class="score-bar">
1422
1541
  <div>
1423
- <span class="total">${total}</span>
1424
- <span class="over"> / ${max}</span>
1542
+ <span class="total">${formatNumber(d.total, 1)}</span>
1543
+ <span class="over"> / ${d.maxScore}</span>
1425
1544
  ${typeof d.percentage === "number" ? html7`<small style="margin-left: 0.5rem; color: var(--roxy-muted)">
1426
- ${d.percentage}%
1545
+ ${formatPercent(d.percentage, 1)}
1427
1546
  </small>` : nothing7}
1428
1547
  </div>
1429
1548
  ${d.recommendation ? html7`<span class="recommendation">${d.recommendation}</span>` : nothing7}
@@ -1440,23 +1559,25 @@ var RoxyGunaMilan = class extends LitElement7 {
1440
1559
  <tbody>
1441
1560
  ${breakdown.map((b) => {
1442
1561
  const score = b.score ?? 0;
1443
- const maxScore = b.max ?? b.maxScore ?? defaultMax(b.name);
1562
+ const maxScore = b.maxScore ?? defaultMax(b.category);
1444
1563
  const pct = maxScore ? score / maxScore * 100 : 0;
1445
1564
  return html7`<tr>
1446
- <td>${b.name ?? ""}</td>
1565
+ <td>${b.category}</td>
1447
1566
  <td class="bar-cell">
1448
1567
  <div class="mini-bar">
1449
1568
  <span style="width: ${pct}%"></span>
1450
1569
  </div>
1451
1570
  </td>
1452
- <td class="score">${score} / ${maxScore}</td>
1571
+ <td class="score">${formatNumber(score, 1)} / ${maxScore}</td>
1453
1572
  </tr>`;
1454
1573
  })}
1455
1574
  </tbody>
1456
1575
  </table>` : nothing7}
1457
1576
  ${(d.doshas?.length ?? 0) > 0 || (d.doshaCancellations?.length ?? 0) > 0 ? html7`<div class="tags">
1458
1577
  ${d.doshas?.map((x) => html7`<span class="dosha">${x}</span>`)}
1459
- ${d.doshaCancellations?.map((x) => html7`<span class="cancel">${x}</span>`)}
1578
+ ${d.doshaCancellations?.map(
1579
+ (x) => html7`<span class="cancel" title=${x.reason}>${x.dosha} cancelled</span>`
1580
+ )}
1460
1581
  </div>` : nothing7}
1461
1582
  </article>`;
1462
1583
  }
@@ -1550,11 +1671,11 @@ RoxyGunaMilan.styles = [
1550
1671
  }
1551
1672
  .tags .dosha {
1552
1673
  background: color-mix(in srgb, var(--roxy-danger, #dc2626) 16%, transparent);
1553
- color: var(--roxy-danger, #dc2626);
1674
+ color: var(--roxy-danger-fg, #991b1b);
1554
1675
  }
1555
1676
  .tags .cancel {
1556
1677
  background: color-mix(in srgb, var(--roxy-success, #16a34a) 18%, transparent);
1557
- color: var(--roxy-success, #16a34a);
1678
+ color: var(--roxy-success-fg, #166534);
1558
1679
  }
1559
1680
  `
1560
1681
  ];
@@ -1610,7 +1731,12 @@ var PLANET_GLYPH = {
1610
1731
  Ascendant: "Asc",
1611
1732
  Lagna: "La",
1612
1733
  NorthNode: "\u260A",
1613
- SouthNode: "\u260B"
1734
+ SouthNode: "\u260B",
1735
+ "North node": "\u260A",
1736
+ "South node": "\u260B",
1737
+ Chiron: "\u26B7",
1738
+ Lilith: "\u26B8",
1739
+ "Black moon lilith": "\u26B8"
1614
1740
  };
1615
1741
  var PLANET_ABBR = {
1616
1742
  Sun: "Su",
@@ -1692,23 +1818,40 @@ var RoxyHexagram = class extends LitElement8 {
1692
1818
  this.data = null;
1693
1819
  this.mode = "lookup";
1694
1820
  }
1695
- getHexagram() {
1696
- if (!this.data) return null;
1697
- if ("hexagram" in this.data && this.data.hexagram) {
1821
+ resolveHexagram() {
1822
+ const d = this.data;
1823
+ if (!d) return null;
1824
+ if ("hexagram" in d && d.hexagram) {
1825
+ if ("lines" in d) {
1826
+ const cast = d;
1827
+ return {
1828
+ hex: cast.hexagram,
1829
+ lines: cast.lines,
1830
+ changingLinePositions: cast.changingLinePositions,
1831
+ resultingHexagram: cast.resultingHexagram
1832
+ };
1833
+ }
1834
+ const daily = d;
1698
1835
  return {
1699
- ...this.data.hexagram,
1700
- lines: this.data.lines,
1701
- changingLinePositions: this.data.changingLinePositions
1836
+ hex: daily.hexagram,
1837
+ dailyMessage: daily.dailyMessage
1702
1838
  };
1703
1839
  }
1704
- return this.data;
1840
+ return { hex: d };
1705
1841
  }
1706
1842
  render() {
1707
- const h = this.getHexagram();
1708
- if (!h)
1843
+ const resolved = this.resolveHexagram();
1844
+ if (!resolved)
1709
1845
  return html8`<div class="roxy-empty" role="status">No hexagram data</div>`;
1710
- const lines = h.lines ?? this.derivedLines(h);
1711
- const changing = new Set(h.changingLinePositions ?? []);
1846
+ const {
1847
+ hex: h,
1848
+ lines: castLines,
1849
+ changingLinePositions,
1850
+ dailyMessage,
1851
+ resultingHexagram
1852
+ } = resolved;
1853
+ const lines = castLines ?? this.derivedLines(h);
1854
+ const changing = new Set(changingLinePositions ?? []);
1712
1855
  return html8`<article class="card" aria-label="I Ching hexagram">
1713
1856
  <div class="glyphs">
1714
1857
  ${h.symbol ? html8`<div class="symbol">${h.symbol}</div>` : nothing8}
@@ -1748,19 +1891,18 @@ var RoxyHexagram = class extends LitElement8 {
1748
1891
  </div>
1749
1892
  ${h.judgment ? html8`<p class="judgment">${h.judgment}</p>` : nothing8}
1750
1893
  ${h.image ? html8`<p class="image">${h.image}</p>` : nothing8}
1751
- ${h.dailyMessage ? html8`<p class="message">${h.dailyMessage}</p>` : nothing8}
1894
+ ${dailyMessage ? html8`<p class="message">${dailyMessage}</p>` : nothing8}
1752
1895
  ${h.interpretation?.general ? html8`<p>${h.interpretation.general}</p>` : nothing8}
1753
1896
  ${changing.size > 0 ? html8`<div class="changing">
1754
1897
  Changing lines: ${Array.from(changing).sort((a, b) => a - b).join(", ")}.
1755
- ${h.resultingHexagram?.english ? html8` Becomes hexagram ${h.resultingHexagram.number}
1756
- ${h.resultingHexagram.english}.` : nothing8}
1898
+ ${resultingHexagram?.english ? html8` Becomes hexagram ${resultingHexagram.number}
1899
+ ${resultingHexagram.english}.` : nothing8}
1757
1900
  </div>` : nothing8}
1758
1901
  </div>
1759
1902
  </article>`;
1760
1903
  }
1761
1904
  /** When the API only ships symbol+number with no line array, render six solid yang. */
1762
1905
  derivedLines(h) {
1763
- if (!h.symbol) return Array.from({ length: 6 }, () => 7);
1764
1906
  const cp = h.symbol.codePointAt(0) ?? 0;
1765
1907
  if (cp >= 19904 && cp <= 19967) {
1766
1908
  const offset = cp - 19904;
@@ -1910,8 +2052,8 @@ var RoxyHoroscopeCard = class extends LitElement9 {
1910
2052
  return html9`<div class="roxy-empty" role="status">No horoscope data</div>`;
1911
2053
  const sign = d.sign ?? "";
1912
2054
  const glyph = sign ? SIGN_GLYPH[capitalize(sign)] ?? "" : "";
1913
- const energy = typeof d.energyRating === "number" ? d.energyRating : null;
1914
- const dateLabel = d.date ?? d.week ?? d.month ?? "";
2055
+ const energy = "energyRating" in d && typeof d.energyRating === "number" ? d.energyRating : null;
2056
+ const dateLabel = "date" in d && d.date || "week" in d && d.week || "month" in d && d.month || "";
1915
2057
  return html9`<article
1916
2058
  class="card"
1917
2059
  aria-label=${`${this.period} horoscope for ${sign}`}
@@ -1949,31 +2091,40 @@ var RoxyHoroscopeCard = class extends LitElement9 {
1949
2091
  <h3>Finance</h3>
1950
2092
  <p>${d.finance}</p>
1951
2093
  </div>` : nothing9}
1952
- ${d.advice ? html9`<div class="section">
2094
+ ${"advice" in d && d.advice ? html9`<div class="section">
1953
2095
  <h3>Advice</h3>
1954
2096
  <p>${d.advice}</p>
1955
2097
  </div>` : nothing9}
1956
2098
  </div>
1957
2099
 
1958
- ${d.luckyNumber || d.luckyColor || (d.compatibleSigns?.length ?? 0) > 0 ? html9`<div class="lucky">
1959
- ${d.luckyNumber !== void 0 ? html9`<span>Lucky number <strong>${d.luckyNumber}</strong></span>` : nothing9}
1960
- ${d.luckyColor ? html9`<span>Lucky color <strong>${d.luckyColor}</strong></span>` : nothing9}
1961
- ${d.luckyNumbers?.length ? html9`<span
2100
+ ${(() => {
2101
+ const luckyNumber = "luckyNumber" in d && d.luckyNumber !== void 0 ? d.luckyNumber : void 0;
2102
+ const luckyColor = "luckyColor" in d && d.luckyColor ? d.luckyColor : "";
2103
+ const luckyNumbers = "luckyNumbers" in d && d.luckyNumbers ? d.luckyNumbers : [];
2104
+ const luckyDays = "luckyDays" in d && d.luckyDays ? d.luckyDays : [];
2105
+ const compatibleSigns = d.compatibleSigns ?? [];
2106
+ if (luckyNumber === void 0 && !luckyColor && luckyNumbers.length === 0 && luckyDays.length === 0 && compatibleSigns.length === 0)
2107
+ return nothing9;
2108
+ return html9`<div class="lucky">
2109
+ ${luckyNumber !== void 0 ? html9`<span>Lucky number <strong>${luckyNumber}</strong></span>` : nothing9}
2110
+ ${luckyColor ? html9`<span>Lucky color <strong>${luckyColor}</strong></span>` : nothing9}
2111
+ ${luckyNumbers.length ? html9`<span
1962
2112
  >Lucky numbers
1963
- <strong>${d.luckyNumbers.join(", ")}</strong></span
2113
+ <strong>${luckyNumbers.join(", ")}</strong></span
1964
2114
  >` : nothing9}
1965
- ${d.luckyDays?.length ? html9`<span
1966
- >Lucky days <strong>${d.luckyDays.join(", ")}</strong></span
2115
+ ${luckyDays.length ? html9`<span
2116
+ >Lucky days <strong>${luckyDays.join(", ")}</strong></span
1967
2117
  >` : nothing9}
1968
- ${d.compatibleSigns?.length ? html9`<span class="compat-wrap">
2118
+ ${compatibleSigns.length ? html9`<span class="compat-wrap">
1969
2119
  Best with
1970
2120
  <span class="compat"
1971
- >${d.compatibleSigns.map(
1972
- (s) => html9`<span>${s}</span>`
1973
- )}</span
2121
+ >${compatibleSigns.map(
2122
+ (s) => html9`<span>${s}</span>`
2123
+ )}</span
1974
2124
  >
1975
2125
  </span>` : nothing9}
1976
- </div>` : nothing9}
2126
+ </div>`;
2127
+ })()}
1977
2128
  </article>`;
1978
2129
  }
1979
2130
  };
@@ -2128,7 +2279,7 @@ var RoxyKpPlanetsTable = class extends LitElement10 {
2128
2279
  >
2129
2280
  <header class="head">
2130
2281
  <h2 class="title">KP planets</h2>
2131
- ${this.data.ayanamsa ? html10`<span class="ayanamsa">Ayanamsa: ${this.data.ayanamsa}</span>` : nothing10}
2282
+ ${typeof this.data.ayanamsa === "number" ? html10`<span class="ayanamsa">Ayanamsa: ${formatNumber(this.data.ayanamsa, 2)}°</span>` : nothing10}
2132
2283
  </header>
2133
2284
  <table role="table">
2134
2285
  <thead>
@@ -2147,13 +2298,13 @@ var RoxyKpPlanetsTable = class extends LitElement10 {
2147
2298
  ${planets.map(
2148
2299
  (p) => html10`<tr>
2149
2300
  <td class="planet">
2150
- ${p.planet ?? p.name ?? ""}
2301
+ ${p.planet}
2151
2302
  ${p.retrograde ? html10`<span class="retro">R</span>` : nothing10}
2152
2303
  </td>
2153
2304
  <td>${p.sign ?? ""}</td>
2154
2305
  <td>${p.signLord ?? ""}</td>
2155
2306
  <td>${p.nakshatra ?? ""}</td>
2156
- <td>${p.starLord ?? p.nakshatraLord ?? ""}</td>
2307
+ <td>${p.nakshatraLord ?? ""}</td>
2157
2308
  <td>${p.subLord ?? ""}</td>
2158
2309
  <td>${p.subSubLord ?? ""}</td>
2159
2310
  <td>${p.kpNumber ?? ""}</td>
@@ -2221,7 +2372,7 @@ RoxyKpPlanetsTable.styles = [
2221
2372
  color: var(--roxy-fg, #0a0a0a);
2222
2373
  }
2223
2374
  .retro {
2224
- color: var(--roxy-warning, #ea580c);
2375
+ color: var(--roxy-warning-fg, #9a3412);
2225
2376
  font-size: var(--roxy-text-xs, 0.75rem);
2226
2377
  margin-left: 4px;
2227
2378
  }
@@ -2241,10 +2392,20 @@ import { customElement as customElement11, property as property11, state as stat
2241
2392
  // packages/ui/src/utils/debounce.ts
2242
2393
  function debounce(fn, wait) {
2243
2394
  let timer;
2244
- return ((...args) => {
2395
+ const debounced = ((...args) => {
2245
2396
  if (timer) clearTimeout(timer);
2246
- timer = setTimeout(() => fn(...args), wait);
2397
+ timer = setTimeout(() => {
2398
+ timer = void 0;
2399
+ fn(...args);
2400
+ }, wait);
2247
2401
  });
2402
+ debounced.cancel = () => {
2403
+ if (timer) {
2404
+ clearTimeout(timer);
2405
+ timer = void 0;
2406
+ }
2407
+ };
2408
+ return debounced;
2248
2409
  }
2249
2410
 
2250
2411
  // packages/ui/src/components/location-search.ts
@@ -2259,6 +2420,7 @@ var RoxyLocationSearch = class extends LitElement11 {
2259
2420
  this.isOpen = false;
2260
2421
  this.isLoading = false;
2261
2422
  this.highlight = -1;
2423
+ this.secretKeyWarned = false;
2262
2424
  this.debouncedFetch = debounce((q) => {
2263
2425
  void this.fetchResults(q);
2264
2426
  }, 300);
@@ -2310,8 +2472,32 @@ var RoxyLocationSearch = class extends LitElement11 {
2310
2472
  if (this.clickOutsideHandler) {
2311
2473
  document.removeEventListener("mousedown", this.clickOutsideHandler);
2312
2474
  }
2475
+ this.debouncedFetch.cancel();
2476
+ if (this.abortController) {
2477
+ this.abortController.abort();
2478
+ this.abortController = void 0;
2479
+ }
2480
+ }
2481
+ warnIfSecretKey() {
2482
+ if (this.secretKeyWarned) return;
2483
+ if (!this.apiKey) return;
2484
+ if (this.apiKey.startsWith("pk_")) return;
2485
+ this.secretKeyWarned = true;
2486
+ const message = "Possible secret key in client-side <roxy-location-search>; use a `pk_` publishable key with origin allowlist instead.";
2487
+ console.warn(message);
2488
+ this.dispatchEvent(
2489
+ new CustomEvent("roxy-validation-error", {
2490
+ detail: { reason: "possible-secret-key", message },
2491
+ bubbles: true,
2492
+ composed: true
2493
+ })
2494
+ );
2313
2495
  }
2314
2496
  async fetchResults(q) {
2497
+ this.warnIfSecretKey();
2498
+ if (this.abortController) this.abortController.abort();
2499
+ const controller = new AbortController();
2500
+ this.abortController = controller;
2315
2501
  this.isLoading = true;
2316
2502
  try {
2317
2503
  const url = new URL(this.endpoint);
@@ -2322,17 +2508,22 @@ var RoxyLocationSearch = class extends LitElement11 {
2322
2508
  };
2323
2509
  if (this.apiKey) headers["X-API-Key"] = this.apiKey;
2324
2510
  if (this.publishableKey) headers["X-API-Key"] = this.publishableKey;
2325
- const res = await fetch(url, { headers });
2511
+ const res = await fetch(url, { headers, signal: controller.signal });
2326
2512
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
2327
2513
  const json = await res.json();
2514
+ if (controller.signal.aborted) return;
2328
2515
  this.results = json.cities ?? [];
2329
2516
  this.isOpen = this.results.length > 0;
2330
2517
  this.highlight = this.results.length > 0 ? 0 : -1;
2331
- } catch (_err) {
2518
+ } catch (err) {
2519
+ if (err?.name === "AbortError") return;
2332
2520
  this.results = [];
2333
2521
  this.isOpen = false;
2334
2522
  } finally {
2335
- this.isLoading = false;
2523
+ if (this.abortController === controller) {
2524
+ this.abortController = void 0;
2525
+ }
2526
+ if (!controller.signal.aborted) this.isLoading = false;
2336
2527
  }
2337
2528
  }
2338
2529
  select(city) {
@@ -2548,18 +2739,21 @@ var RoxyMoonPhase = class extends LitElement12 {
2548
2739
  const d = this.data;
2549
2740
  if (!d)
2550
2741
  return html12`<div class="roxy-empty" role="status">No moon phase data</div>`;
2551
- const list = d.phases ?? d.upcoming ?? [];
2742
+ const list = "phases" in d ? d.phases : "calendar" in d ? d.calendar : [];
2552
2743
  if (this.mode !== "current" && list.length > 0) {
2744
+ const month = "month" in d ? d.month : void 0;
2745
+ const year = "year" in d ? d.year : void 0;
2553
2746
  return html12`<article
2554
2747
  class="card"
2555
2748
  aria-label="Moon phase calendar"
2556
2749
  >
2557
- <h2 class="label">${d.month ?? "Moon phases"} ${d.year ?? ""}</h2>
2750
+ <h2 class="label">${month ?? "Moon phases"} ${year ?? ""}</h2>
2558
2751
  <div class="list" role="list">
2559
2752
  ${list.map((phase) => this.renderListItem(phase))}
2560
2753
  </div>
2561
2754
  </article>`;
2562
2755
  }
2756
+ if (!("phase" in d)) return nothing12;
2563
2757
  return this.renderSingle(d);
2564
2758
  }
2565
2759
  renderSingle(d) {
@@ -2575,11 +2769,11 @@ var RoxyMoonPhase = class extends LitElement12 {
2575
2769
  <div class="stats">
2576
2770
  ${typeof d.illumination === "number" ? html12`<div>
2577
2771
  <span>Illumination</span>
2578
- <strong>${(d.illumination * 100).toFixed(0)}%</strong>
2772
+ <strong>${formatIllumination(d.illumination)}</strong>
2579
2773
  </div>` : nothing12}
2580
2774
  ${typeof d.age === "number" ? html12`<div>
2581
2775
  <span>Age</span>
2582
- <strong>${d.age.toFixed(1)} days</strong>
2776
+ <strong>${formatNumber(d.age, 1)} days</strong>
2583
2777
  </div>` : nothing12}
2584
2778
  ${d.sign ? html12`<div>
2585
2779
  <span>Sign</span>
@@ -2704,6 +2898,10 @@ function phaseEmoji(phase) {
2704
2898
  if (!phase) return "\u{1F319}";
2705
2899
  return MOON_PHASE_EMOJI[phase.toLowerCase()] ?? "\u{1F319}";
2706
2900
  }
2901
+ function formatIllumination(v) {
2902
+ const pct = v <= 1 ? v * 100 : v;
2903
+ return `${Math.round(pct)}%`;
2904
+ }
2707
2905
 
2708
2906
  // packages/ui/src/components/natal-chart.ts
2709
2907
  import { css as css14, html as html13, LitElement as LitElement13, nothing as nothing13, svg as svg3 } from "lit";
@@ -2719,12 +2917,14 @@ function polarToCartesian(cx, cy, radius, angleDeg) {
2719
2917
  }
2720
2918
 
2721
2919
  // packages/ui/src/components/natal-chart.ts
2722
- var SIZE = 320;
2920
+ var SIZE = 384;
2723
2921
  var CENTER = SIZE / 2;
2724
2922
  var OUTER_R = 150;
2725
2923
  var SIGN_R = 134;
2726
2924
  var HOUSE_R = 110;
2727
2925
  var PLANET_R = 88;
2926
+ var ANGLE_TICK_R = 162;
2927
+ var ANGLE_LABEL_R = 176;
2728
2928
  var RoxyNatalChart = class extends LitElement13 {
2729
2929
  constructor() {
2730
2930
  super(...arguments);
@@ -2732,10 +2932,17 @@ var RoxyNatalChart = class extends LitElement13 {
2732
2932
  this.houseSystem = "placidus";
2733
2933
  }
2734
2934
  getPlanets() {
2735
- const p = this.data?.planets;
2736
- if (!p) return [];
2737
- if (Array.isArray(p)) return p;
2738
- return Object.entries(p).map(([name, entry]) => ({ ...entry, name }));
2935
+ return this.data?.planets ?? [];
2936
+ }
2937
+ getAscendant() {
2938
+ return this.data?.ascendant?.longitude ?? 0;
2939
+ }
2940
+ getMidheaven() {
2941
+ const m = this.data?.midheaven?.longitude;
2942
+ return typeof m === "number" ? m : null;
2943
+ }
2944
+ toAngle(lon) {
2945
+ return 180 + this.getAscendant() - lon;
2739
2946
  }
2740
2947
  render() {
2741
2948
  if (!this.data)
@@ -2746,11 +2953,7 @@ var RoxyNatalChart = class extends LitElement13 {
2746
2953
  <header>
2747
2954
  <h2 class="title">Natal chart</h2>
2748
2955
  ${this.data.birthDetails ? html13`<div class="meta">
2749
- ${[
2750
- this.data.birthDetails.date,
2751
- this.data.birthDetails.time,
2752
- this.data.birthDetails.location
2753
- ].filter(Boolean).join(" \xB7 ")}
2956
+ ${[this.data.birthDetails.date, this.data.birthDetails.time].filter(Boolean).join(" \xB7 ")}
2754
2957
  </div>` : nothing13}
2755
2958
  </header>
2756
2959
  <svg
@@ -2786,17 +2989,38 @@ var RoxyNatalChart = class extends LitElement13 {
2786
2989
  />
2787
2990
  ${this.renderSpokes()} ${this.renderSigns()} ${this.renderHouseNumbers()}
2788
2991
  ${this.renderAspects(planets, aspects)} ${this.renderPlanets(planets)}
2992
+ ${this.renderAngles()}
2789
2993
  </svg>
2790
2994
  <div class="legend">
2791
2995
  <span>${planets.length} planets</span>
2792
2996
  <span>${aspects.length} aspects</span>
2793
- <span>House system: ${this.houseSystem}</span>
2997
+ <span><span class="legend-swatch" style="background: var(--roxy-success)"></span>harmonious</span>
2998
+ <span><span class="legend-swatch" style="background: var(--roxy-danger)"></span>challenging</span>
2794
2999
  </div>
2795
3000
  </div>`;
2796
3001
  }
3002
+ renderAngles() {
3003
+ const asc = this.getAscendant();
3004
+ const mc = this.getMidheaven();
3005
+ const items = [this.renderAngleMark(asc, "ASC")];
3006
+ if (mc !== null) items.push(this.renderAngleMark(mc, "MC"));
3007
+ return items;
3008
+ }
3009
+ renderAngleMark(longitude, label) {
3010
+ const angle = this.toAngle(longitude);
3011
+ const tickInner = polarToCartesian(CENTER, CENTER, OUTER_R, angle);
3012
+ const tickOuter = polarToCartesian(CENTER, CENTER, ANGLE_TICK_R, angle);
3013
+ const labelPos = polarToCartesian(CENTER, CENTER, ANGLE_LABEL_R, angle);
3014
+ return svg3`
3015
+ <g>
3016
+ <line class="angle-tick" x1=${tickInner.x} y1=${tickInner.y} x2=${tickOuter.x} y2=${tickOuter.y} />
3017
+ <text class="angle-marker" x=${labelPos.x} y=${labelPos.y} text-anchor="middle" dominant-baseline="central">${label}</text>
3018
+ </g>
3019
+ `;
3020
+ }
2797
3021
  renderSpokes() {
2798
3022
  return Array.from({ length: 12 }, (_, i) => {
2799
- const angle = i * 30 - 90;
3023
+ const angle = this.toAngle(i * 30);
2800
3024
  const start = polarToCartesian(CENTER, CENTER, HOUSE_R, angle);
2801
3025
  const end = polarToCartesian(CENTER, CENTER, OUTER_R, angle);
2802
3026
  return svg3`<line class="wheel-line" x1=${start.x} y1=${start.y} x2=${end.x} y2=${end.y} stroke-width="0.8" />`;
@@ -2818,45 +3042,58 @@ var RoxyNatalChart = class extends LitElement13 {
2818
3042
  "Pisces"
2819
3043
  ];
2820
3044
  return order.map((sign, i) => {
2821
- const angle = i * 30 + 15 - 90;
3045
+ const angle = this.toAngle(i * 30 + 15);
2822
3046
  const pos = polarToCartesian(CENTER, CENTER, SIGN_R, angle);
2823
3047
  return svg3`<text class="sign-glyph" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central">${SIGN_GLYPH[sign]}</text>`;
2824
3048
  });
2825
3049
  }
2826
3050
  renderHouseNumbers() {
3051
+ const ascSignIndex = Math.floor(this.getAscendant() / 30);
2827
3052
  return Array.from({ length: 12 }, (_, i) => {
2828
- const angle = i * 30 + 15 - 90;
3053
+ const angle = this.toAngle(i * 30 + 15);
2829
3054
  const pos = polarToCartesian(CENTER, CENTER, HOUSE_R - 12, angle);
2830
- return svg3`<text class="house-num" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central">${i + 1}</text>`;
3055
+ const houseNum = (i - ascSignIndex + 12) % 12 + 1;
3056
+ return svg3`<text class="house-num" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central">${houseNum}</text>`;
2831
3057
  });
2832
3058
  }
2833
3059
  renderPlanets(planets) {
2834
3060
  return planets.map((p) => {
2835
- const lon = typeof p.longitude === "number" ? p.longitude : typeof p.degree === "number" ? p.degree : NaN;
2836
- if (!Number.isFinite(lon)) return nothing13;
2837
- const angle = lon - 90;
3061
+ if (!Number.isFinite(p.longitude)) return nothing13;
3062
+ const angle = this.toAngle(p.longitude);
2838
3063
  const pos = polarToCartesian(CENTER, CENTER, PLANET_R, angle);
2839
- const name = p.name ?? p.planet ?? "";
2840
- const glyph = PLANET_GLYPH[capitalize2(name)] ?? name.slice(0, 2);
2841
- const retro = p.retrograde || p.isRetrograde ? " R" : "";
2842
- return svg3`<text class="planet-glyph" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central"><title>${name}${retro}</title>${glyph}</text>`;
3064
+ const glyph = PLANET_GLYPH[capitalize2(p.name)] ?? p.name.slice(0, 2);
3065
+ const retro = p.isRetrograde ? " R" : "";
3066
+ const display = retro ? `${glyph}\u1D3F` : glyph;
3067
+ return svg3`<text class="planet-glyph" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central"><title>${p.name}${retro}</title>${display}</text>`;
2843
3068
  });
2844
3069
  }
2845
3070
  renderAspects(planets, aspects) {
2846
3071
  const planetMap = /* @__PURE__ */ new Map();
2847
3072
  for (const p of planets) {
2848
- const lon = typeof p.longitude === "number" ? p.longitude : typeof p.degree === "number" ? p.degree : null;
2849
- if (lon === null) continue;
2850
- const name = capitalize2(p.name ?? p.planet ?? "");
2851
- if (name) planetMap.set(name, lon);
3073
+ if (typeof p.longitude !== "number") continue;
3074
+ const name = capitalize2(p.name);
3075
+ if (name) planetMap.set(name, p.longitude);
2852
3076
  }
2853
3077
  return aspects.map((a) => {
2854
- const l1 = planetMap.get(capitalize2(a.planet1 ?? ""));
2855
- const l2 = planetMap.get(capitalize2(a.planet2 ?? ""));
3078
+ const l1 = planetMap.get(capitalize2(a.planet1));
3079
+ const l2 = planetMap.get(capitalize2(a.planet2));
2856
3080
  if (l1 === void 0 || l2 === void 0) return nothing13;
2857
- const p1 = polarToCartesian(CENTER, CENTER, PLANET_R - 18, l1 - 90);
2858
- const p2 = polarToCartesian(CENTER, CENTER, PLANET_R - 18, l2 - 90);
2859
- return svg3`<line class="aspect" x1=${p1.x} y1=${p1.y} x2=${p2.x} y2=${p2.y} />`;
3081
+ const p1 = polarToCartesian(
3082
+ CENTER,
3083
+ CENTER,
3084
+ PLANET_R - 18,
3085
+ this.toAngle(l1)
3086
+ );
3087
+ const p2 = polarToCartesian(
3088
+ CENTER,
3089
+ CENTER,
3090
+ PLANET_R - 18,
3091
+ this.toAngle(l2)
3092
+ );
3093
+ const aspectName = normalizeAspect(a);
3094
+ const aspectClass = ASPECT_CLASS[aspectName] ?? "aspect-other";
3095
+ const orbLabel = formatNumber(a.orb, 1);
3096
+ return svg3`<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>`;
2860
3097
  });
2861
3098
  }
2862
3099
  };
@@ -2914,9 +3151,36 @@ RoxyNatalChart.styles = [
2914
3151
  }
2915
3152
 
2916
3153
  .aspect {
2917
- stroke: color-mix(in srgb, var(--roxy-accent, #f59e0b) 32%, transparent);
2918
- stroke-width: 0.6;
3154
+ stroke-width: 0.8;
2919
3155
  fill: none;
3156
+ opacity: 0.55;
3157
+ }
3158
+ .aspect-trine,
3159
+ .aspect-sextile {
3160
+ stroke: var(--roxy-success, #16a34a);
3161
+ }
3162
+ .aspect-square,
3163
+ .aspect-opposition {
3164
+ stroke: var(--roxy-danger, #dc2626);
3165
+ }
3166
+ .aspect-conjunction {
3167
+ stroke: var(--roxy-accent-fg, #b45309);
3168
+ }
3169
+ .aspect-other {
3170
+ stroke: var(--roxy-muted, #71717a);
3171
+ opacity: 0.4;
3172
+ }
3173
+
3174
+ .angle-marker {
3175
+ fill: var(--roxy-accent-fg, #b45309);
3176
+ font-size: 10px;
3177
+ font-weight: 700;
3178
+ font-family: var(--roxy-font-sans);
3179
+ letter-spacing: 0.04em;
3180
+ }
3181
+ .angle-tick {
3182
+ stroke: var(--roxy-accent-fg, #b45309);
3183
+ stroke-width: 1.5;
2920
3184
  }
2921
3185
 
2922
3186
  .legend {
@@ -2926,6 +3190,14 @@ RoxyNatalChart.styles = [
2926
3190
  flex-wrap: wrap;
2927
3191
  gap: var(--roxy-space-md, 1rem);
2928
3192
  }
3193
+ .legend-swatch {
3194
+ display: inline-block;
3195
+ width: 8px;
3196
+ height: 8px;
3197
+ border-radius: 50%;
3198
+ margin-right: 4px;
3199
+ vertical-align: middle;
3200
+ }
2929
3201
  `
2930
3202
  ];
2931
3203
  __decorateClass([
@@ -2941,6 +3213,16 @@ function capitalize2(s) {
2941
3213
  if (!s) return "";
2942
3214
  return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
2943
3215
  }
3216
+ var ASPECT_CLASS = {
3217
+ conjunction: "aspect-conjunction",
3218
+ sextile: "aspect-sextile",
3219
+ square: "aspect-square",
3220
+ trine: "aspect-trine",
3221
+ opposition: "aspect-opposition"
3222
+ };
3223
+ function normalizeAspect(a) {
3224
+ return (a.type ?? "").toLowerCase().replace(/_/g, "-");
3225
+ }
2944
3226
 
2945
3227
  // packages/ui/src/components/numerology-card.ts
2946
3228
  import { css as css15, html as html14, LitElement as LitElement14, nothing as nothing14 } from "lit";
@@ -2956,42 +3238,63 @@ var RoxyNumerologyCard = class extends LitElement14 {
2956
3238
  if (!d)
2957
3239
  return html14`<div class="roxy-empty" role="status">No numerology data</div>`;
2958
3240
  const headerLabel = LABELS[this.type] ?? this.type;
2959
- const number = d.personalYear ?? d.number;
2960
- const cores = d.coreNumbers ? Object.entries(d.coreNumbers).filter(
2961
- ([, v]) => v !== null && v !== void 0
2962
- ) : [];
2963
- return html14`<article
2964
- class="card"
2965
- aria-label=${headerLabel}
2966
- >
3241
+ if ("coreNumbers" in d) return this.renderChart(d, headerLabel);
3242
+ if ("personalYear" in d) return this.renderPersonalYear(d, headerLabel);
3243
+ return this.renderNumberCard(
3244
+ d,
3245
+ headerLabel
3246
+ );
3247
+ }
3248
+ renderNumberCard(d, headerLabel) {
3249
+ const keywords = d.meaning?.keywords ?? [];
3250
+ return html14`<article class="card" aria-label=${headerLabel}>
2967
3251
  <div class="hero">
2968
- ${typeof number === "number" ? html14`<div class="numeral">${number}</div>` : nothing14}
3252
+ ${typeof d.number === "number" ? html14`<div class="numeral">${d.number}</div>` : nothing14}
2969
3253
  <div>
2970
3254
  <p class="label">${headerLabel}</p>
2971
- ${d.title ? html14`<h2 class="title">${d.title}</h2>` : d.type ? html14`<h2 class="title">
2972
- ${d.type === "master" ? "Master number" : "Single digit"}
2973
- </h2>` : nothing14}
3255
+ ${d.meaning?.title ? html14`<h2 class="title">${d.meaning.title}</h2>` : nothing14}
2974
3256
  </div>
2975
3257
  </div>
2976
- ${d.theme ? html14`<p><strong>Theme:</strong> ${d.theme}</p>` : nothing14}
2977
- ${d.meaning ? html14`<p class="meaning">${d.meaning}</p>` : nothing14}
2978
- ${d.advice ? html14`<p>${d.advice}</p>` : nothing14}
3258
+ ${d.meaning?.description ? html14`<p class="meaning">${d.meaning.description}</p>` : nothing14}
2979
3259
  ${d.calculation ? html14`<pre class="calc">${d.calculation}</pre>` : nothing14}
2980
- ${d.keywords?.length ? html14`<div class="chips">
2981
- ${d.keywords.map((k) => html14`<span>${k}</span>`)}
2982
- </div>` : nothing14}
2983
- ${cores.length > 0 ? html14`<div class="cores">
2984
- ${cores.map(([k, v]) => {
2985
- const value = typeof v === "number" ? v : v.number;
2986
- return html14`<div class="item">
2987
- <span>${humanize2(k)}</span>
2988
- <strong>${value ?? ""}</strong>
2989
- </div>`;
2990
- })}
3260
+ ${keywords.length > 0 ? html14`<div class="chips">
3261
+ ${keywords.map((k) => html14`<span>${k}</span>`)}
2991
3262
  </div>` : nothing14}
2992
3263
  ${d.hasKarmicDebt && d.karmicDebtNumber ? html14`<div class="karmic">
2993
3264
  Karmic debt ${d.karmicDebtNumber}.
2994
- ${d.karmicDebtMeaning ? d.karmicDebtMeaning : ""}
3265
+ ${karmicDebtText(d.karmicDebtMeaning)}
3266
+ </div>` : nothing14}
3267
+ </article>`;
3268
+ }
3269
+ renderPersonalYear(d, headerLabel) {
3270
+ return html14`<article class="card" aria-label=${headerLabel}>
3271
+ <div class="hero">
3272
+ ${typeof d.personalYear === "number" ? html14`<div class="numeral">${d.personalYear}</div>` : nothing14}
3273
+ <div>
3274
+ <p class="label">${headerLabel}</p>
3275
+ ${d.theme ? html14`<h2 class="title">${d.theme}</h2>` : nothing14}
3276
+ </div>
3277
+ </div>
3278
+ ${d.forecast ? html14`<p class="meaning">${d.forecast}</p>` : nothing14}
3279
+ ${d.advice ? html14`<p>${d.advice}</p>` : nothing14}
3280
+ </article>`;
3281
+ }
3282
+ renderChart(d, headerLabel) {
3283
+ const cores = Object.entries(d.coreNumbers).filter(
3284
+ ([, v]) => v !== null && v !== void 0
3285
+ );
3286
+ return html14`<article class="card" aria-label=${headerLabel}>
3287
+ <div>
3288
+ <p class="label">${headerLabel}</p>
3289
+ ${d.profile?.name ? html14`<h2 class="title">${d.profile.name}</h2>` : nothing14}
3290
+ </div>
3291
+ ${cores.length > 0 ? html14`<div class="cores">
3292
+ ${cores.map(
3293
+ ([k, v]) => html14`<div class="item">
3294
+ <span>${humanize2(k)}</span>
3295
+ <strong>${v.number ?? ""}</strong>
3296
+ </div>`
3297
+ )}
2995
3298
  </div>` : nothing14}
2996
3299
  </article>`;
2997
3300
  }
@@ -3046,7 +3349,8 @@ RoxyNumerologyCard.styles = [
3046
3349
  background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 30%, transparent);
3047
3350
  padding: var(--roxy-space-sm, 0.5rem);
3048
3351
  border-radius: var(--roxy-radius-sm, 4px);
3049
- word-break: break-all;
3352
+ white-space: pre-wrap;
3353
+ overflow-wrap: anywhere;
3050
3354
  }
3051
3355
 
3052
3356
  .chips {
@@ -3110,6 +3414,10 @@ var LABELS = {
3110
3414
  "personal-year": "Personal Year",
3111
3415
  chart: "Numerology chart"
3112
3416
  };
3417
+ function karmicDebtText(value) {
3418
+ if (!value) return "";
3419
+ return [value.description, value.challenge, value.resolution].filter(Boolean).join(" ");
3420
+ }
3113
3421
  function humanize2(s) {
3114
3422
  return s.replace(/[_-]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^\w/, (c) => c.toUpperCase());
3115
3423
  }
@@ -3127,31 +3435,32 @@ var RoxyPanchangTable = class extends LitElement15 {
3127
3435
  const d = this.data;
3128
3436
  if (!d)
3129
3437
  return html15`<div class="roxy-empty" role="status">No panchang data</div>`;
3438
+ const detailed = "sunrise" in d ? d : null;
3130
3439
  const fivefold = [
3131
3440
  ["Tithi", this.formatPart(d.tithi)],
3132
3441
  ["Nakshatra", this.formatPart(d.nakshatra)],
3133
3442
  ["Yoga", this.formatPart(d.yoga)],
3134
- ["Karana", this.formatPart(d.karana)],
3135
- ["Vara", d.vara ?? ""]
3136
- ];
3137
- const muhurtas = [
3138
- ["Brahma Muhurta", d.brahmaMuhurta],
3139
- ["Abhijit Muhurta", d.abhijitMuhurta],
3140
- ["Vijaya Muhurta", d.vijayaMuhurta],
3141
- ["Godhuli Muhurta", d.godhuliMuhurta],
3142
- ["Nishita Muhurta", d.nishitaMuhurta],
3143
- ["Pratah Sandhya", d.pratahSandhya],
3144
- ["Sayahna Sandhya", d.sayahnaSandhya]
3145
- ];
3146
- const inauspicious = [
3147
- ["Rahu Kaal", d.rahuKaal],
3148
- ["Yamaganda", d.yamaganda],
3149
- ["Gulika", d.gulika]
3443
+ ["Karana", this.formatPart(d.karana)]
3150
3444
  ];
3445
+ if (detailed) fivefold.push(["Vara", this.formatPart(detailed.vara)]);
3446
+ const muhurtas = detailed ? [
3447
+ ["Brahma Muhurta", detailed.brahmaMuhurta],
3448
+ ["Abhijit Muhurta", detailed.abhijitMuhurta],
3449
+ ["Vijaya Muhurta", detailed.vijayaMuhurta],
3450
+ ["Godhuli Muhurta", detailed.godhuliMuhurta],
3451
+ ["Nishita Muhurta", detailed.nishitaMuhurta],
3452
+ ["Pratah Sandhya", detailed.pratahSandhya],
3453
+ ["Sayahna Sandhya", detailed.sayahnaSandhya]
3454
+ ] : [];
3455
+ const inauspicious = detailed ? [
3456
+ ["Rahu Kaal", detailed.rahuKaal],
3457
+ ["Yamaganda", detailed.yamaganda],
3458
+ ["Gulika", detailed.gulika]
3459
+ ] : [];
3151
3460
  return html15`<div class="wrap" aria-label="Panchang">
3152
3461
  <header class="head">
3153
3462
  <h2 class="title">Panchang</h2>
3154
- <span class="date">${d.date ?? ""}</span>
3463
+ <span class="date">${detailed ? formatDate(detailed.date) : ""}</span>
3155
3464
  </header>
3156
3465
  <table>
3157
3466
  <tbody>
@@ -3161,21 +3470,21 @@ var RoxyPanchangTable = class extends LitElement15 {
3161
3470
  <td>${v}</td>
3162
3471
  </tr>`
3163
3472
  )}
3164
- ${d.sunrise ? html15`<tr>
3473
+ ${detailed?.sunrise ? html15`<tr>
3165
3474
  <th>Sunrise</th>
3166
- <td>${d.sunrise}</td>
3475
+ <td>${formatTime(detailed.sunrise)}</td>
3167
3476
  </tr>` : nothing15}
3168
- ${d.sunset ? html15`<tr>
3477
+ ${detailed?.sunset ? html15`<tr>
3169
3478
  <th>Sunset</th>
3170
- <td>${d.sunset}</td>
3479
+ <td>${formatTime(detailed.sunset)}</td>
3171
3480
  </tr>` : nothing15}
3172
- ${d.moonrise ? html15`<tr>
3481
+ ${detailed?.moonrise ? html15`<tr>
3173
3482
  <th>Moonrise</th>
3174
- <td>${d.moonrise}</td>
3483
+ <td>${formatTime(detailed.moonrise)}</td>
3175
3484
  </tr>` : nothing15}
3176
- ${d.moonset ? html15`<tr>
3485
+ ${detailed?.moonset ? html15`<tr>
3177
3486
  <th>Moonset</th>
3178
- <td>${d.moonset}</td>
3487
+ <td>${formatTime(detailed.moonset)}</td>
3179
3488
  </tr>` : nothing15}
3180
3489
  </tbody>
3181
3490
  </table>
@@ -3186,7 +3495,7 @@ var RoxyPanchangTable = class extends LitElement15 {
3186
3495
  ${muhurtas.filter(([, v]) => !!v).map(
3187
3496
  ([k, v]) => html15`<tr>
3188
3497
  <th>${k}</th>
3189
- <td>${formatRange(v)}</td>
3498
+ <td>${formatTimeRange(v)}</td>
3190
3499
  </tr>`
3191
3500
  )}
3192
3501
  </tbody>
@@ -3197,7 +3506,7 @@ var RoxyPanchangTable = class extends LitElement15 {
3197
3506
  ${inauspicious.filter(([, v]) => !!v).map(
3198
3507
  ([k, v]) => html15`<tr>
3199
3508
  <th>${k}</th>
3200
- <td>${formatRange(v)}</td>
3509
+ <td>${formatTimeRange(v)}</td>
3201
3510
  </tr>`
3202
3511
  )}
3203
3512
  </tbody>
@@ -3291,11 +3600,6 @@ __decorateClass([
3291
3600
  RoxyPanchangTable = __decorateClass([
3292
3601
  customElement15("roxy-panchang-table")
3293
3602
  ], RoxyPanchangTable);
3294
- function formatRange(t) {
3295
- if (!t) return "";
3296
- if (t.start && t.end) return `${t.start} - ${t.end}`;
3297
- return t.start ?? t.end ?? "";
3298
- }
3299
3603
 
3300
3604
  // packages/ui/src/components/synastry-chart.ts
3301
3605
  import { css as css17, html as html16, LitElement as LitElement16, nothing as nothing16, svg as svg4 } from "lit";
@@ -3314,23 +3618,58 @@ var RoxySynastryChart = class extends LitElement16 {
3314
3618
  render() {
3315
3619
  if (!this.data)
3316
3620
  return html16`<div class="roxy-empty" role="status">No synastry data</div>`;
3317
- const {
3318
- person1,
3319
- person2,
3320
- compatibilityScore,
3321
- summary,
3322
- interAspects = []
3323
- } = this.data;
3324
- const p1Planets = this.normalizePlanets(person1?.planets);
3325
- const p2Planets = this.normalizePlanets(person2?.planets);
3621
+ const { person1, person2, compatibilityScore, analysis } = this.data;
3622
+ const interAspects = this.data.interAspects ?? [];
3623
+ const p1Planets = person1?.planets ?? [];
3624
+ const p2Planets = person2?.planets ?? [];
3625
+ const score = typeof compatibilityScore === "number" ? Math.round(compatibilityScore) : void 0;
3626
+ const summaryText = analysis?.overall;
3627
+ const strengths = analysis?.strengths ?? [];
3628
+ const challenges = analysis?.challenges ?? [];
3629
+ const hasPlanets = p1Planets.length > 0 && p2Planets.length > 0;
3630
+ if (!hasPlanets) {
3631
+ return html16`<div
3632
+ class="wrap"
3633
+ aria-label="Synastry compatibility chart"
3634
+ >
3635
+ <div class="head">
3636
+ <h2 class="title">Synastry</h2>
3637
+ ${typeof score === "number" ? html16`<span class="score" aria-label=${`Score ${score} of 100`}
3638
+ >${score} / 100</span
3639
+ >` : nothing16}
3640
+ </div>
3641
+ <div class="missing-planets" role="status">
3642
+ Synastry response missing planet positions. Pass
3643
+ <code>data</code> with <code>person1.planets</code> and
3644
+ <code>person2.planets</code> arrays from the natal-chart endpoint, or
3645
+ use the <code>&lt;roxy-data&gt;</code> fallback.
3646
+ </div>
3647
+ ${summaryText ? html16`<p class="summary">${summaryText}</p>` : nothing16}
3648
+ ${interAspects.length > 0 ? this.renderAspects(interAspects) : nothing16}
3649
+ ${strengths.length > 0 || challenges.length > 0 ? html16`<div class="lists">
3650
+ ${strengths.length ? html16`<div>
3651
+ <h3>Strengths</h3>
3652
+ <ul>
3653
+ ${strengths.map((s) => html16`<li>${s}</li>`)}
3654
+ </ul>
3655
+ </div>` : nothing16}
3656
+ ${challenges.length ? html16`<div>
3657
+ <h3>Challenges</h3>
3658
+ <ul>
3659
+ ${challenges.map((s) => html16`<li>${s}</li>`)}
3660
+ </ul>
3661
+ </div>` : nothing16}
3662
+ </div>` : nothing16}
3663
+ </div>`;
3664
+ }
3326
3665
  return html16`<div
3327
3666
  class="wrap"
3328
3667
  aria-label="Synastry compatibility chart"
3329
3668
  >
3330
3669
  <div class="head">
3331
3670
  <h2 class="title">Synastry</h2>
3332
- ${typeof compatibilityScore === "number" ? html16`<span class="score" aria-label=${`Score ${compatibilityScore} of 100`}
3333
- >${compatibilityScore} / 100</span
3671
+ ${typeof score === "number" ? html16`<span class="score" aria-label=${`Score ${score} of 100`}
3672
+ >${score} / 100</span
3334
3673
  >` : nothing16}
3335
3674
  </div>
3336
3675
  <svg
@@ -3361,34 +3700,39 @@ var RoxySynastryChart = class extends LitElement16 {
3361
3700
  stroke-width="0.6"
3362
3701
  />
3363
3702
  ${this.renderSpokes()} ${this.renderSigns()}
3703
+ ${this.renderInterAspectLines(p1Planets, p2Planets, interAspects)}
3364
3704
  ${this.renderRing(p1Planets, P1_R, "p1")} ${this.renderRing(p2Planets, P2_R, "p2")}
3365
3705
  </svg>
3366
- ${summary ? html16`<p class="summary">${summary}</p>` : nothing16}
3706
+ <div class="legend-row">
3707
+ <span><span class="swatch" style="background: var(--roxy-accent)"></span>Person 1</span>
3708
+ <span><span class="swatch" style="background: var(--roxy-info)"></span>Person 2</span>
3709
+ <span><span class="swatch" style="background: var(--roxy-success)"></span>harmonious</span>
3710
+ <span><span class="swatch" style="background: var(--roxy-danger)"></span>challenging</span>
3711
+ </div>
3712
+ ${summaryText ? html16`<p class="summary">${summaryText}</p>` : nothing16}
3367
3713
  ${interAspects.length > 0 ? this.renderAspects(interAspects) : nothing16}
3368
- ${(this.data.strengths?.length ?? 0) > 0 || (this.data.challenges?.length ?? 0) > 0 ? html16`<div class="lists">
3369
- ${this.data.strengths?.length ? html16`<div>
3714
+ ${strengths.length > 0 || challenges.length > 0 ? html16`<div class="lists">
3715
+ ${strengths.length ? html16`<div>
3370
3716
  <h3>Strengths</h3>
3371
3717
  <ul>
3372
- ${this.data.strengths.map((s) => html16`<li>${s}</li>`)}
3718
+ ${strengths.map((s) => html16`<li>${s}</li>`)}
3373
3719
  </ul>
3374
3720
  </div>` : nothing16}
3375
- ${this.data.challenges?.length ? html16`<div>
3721
+ ${challenges.length ? html16`<div>
3376
3722
  <h3>Challenges</h3>
3377
3723
  <ul>
3378
- ${this.data.challenges.map((s) => html16`<li>${s}</li>`)}
3724
+ ${challenges.map((s) => html16`<li>${s}</li>`)}
3379
3725
  </ul>
3380
3726
  </div>` : nothing16}
3381
3727
  </div>` : nothing16}
3382
3728
  </div>`;
3383
3729
  }
3384
- normalizePlanets(p) {
3385
- if (!p) return [];
3386
- if (Array.isArray(p)) return p;
3387
- return Object.entries(p).map(([name, e]) => ({ ...e, name }));
3730
+ toAngle(longitude) {
3731
+ return 180 - longitude;
3388
3732
  }
3389
3733
  renderSpokes() {
3390
3734
  return Array.from({ length: 12 }, (_, i) => {
3391
- const angle = i * 30 - 90;
3735
+ const angle = this.toAngle(i * 30);
3392
3736
  const start = polarToCartesian(CENTER2, CENTER2, P2_R - 14, angle);
3393
3737
  const end = polarToCartesian(CENTER2, CENTER2, OUTER_R2, angle);
3394
3738
  return svg4`<line class="wheel-line" x1=${start.x} y1=${start.y} x2=${end.x} y2=${end.y} stroke-width="0.6" />`;
@@ -3410,19 +3754,43 @@ var RoxySynastryChart = class extends LitElement16 {
3410
3754
  "Pisces"
3411
3755
  ];
3412
3756
  return order.map((s, i) => {
3413
- const angle = i * 30 + 15 - 90;
3757
+ const angle = this.toAngle(i * 30 + 15);
3414
3758
  const pos = polarToCartesian(CENTER2, CENTER2, SIGN_R2, angle);
3415
3759
  return svg4`<text class="sign" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central">${SIGN_GLYPH[s]}</text>`;
3416
3760
  });
3417
3761
  }
3418
3762
  renderRing(planets, radius, cls) {
3419
3763
  return planets.map((p) => {
3420
- const lon = typeof p.longitude === "number" ? p.longitude : typeof p.degree === "number" ? p.degree : NaN;
3421
- if (!Number.isFinite(lon)) return nothing16;
3422
- const pos = polarToCartesian(CENTER2, CENTER2, radius, lon - 90);
3423
- const name = p.name ?? p.planet ?? "";
3424
- const glyph = PLANET_GLYPH[capitalize3(name)] ?? name.slice(0, 2);
3425
- return svg4`<text class=${cls} x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central"><title>${name}</title>${glyph}</text>`;
3764
+ if (!Number.isFinite(p.longitude)) return nothing16;
3765
+ const pos = polarToCartesian(
3766
+ CENTER2,
3767
+ CENTER2,
3768
+ radius,
3769
+ this.toAngle(p.longitude)
3770
+ );
3771
+ const glyph = PLANET_GLYPH[capitalize3(p.name)] ?? p.name.slice(0, 2);
3772
+ return svg4`<text class=${cls} x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central"><title>${p.name}</title>${glyph}</text>`;
3773
+ });
3774
+ }
3775
+ renderInterAspectLines(p1, p2, aspects) {
3776
+ const longitudeOf = (list, name) => {
3777
+ const target = capitalize3(name);
3778
+ for (const p of list) {
3779
+ if (capitalize3(p.name) !== target) continue;
3780
+ if (typeof p.longitude === "number") return p.longitude;
3781
+ }
3782
+ return void 0;
3783
+ };
3784
+ return aspects.map((a) => {
3785
+ const l1 = longitudeOf(p1, a.planet1);
3786
+ const l2 = longitudeOf(p2, a.planet2);
3787
+ if (l1 === void 0 || l2 === void 0) return nothing16;
3788
+ const out = polarToCartesian(CENTER2, CENTER2, P1_R - 12, this.toAngle(l1));
3789
+ const inn = polarToCartesian(CENTER2, CENTER2, P2_R + 8, this.toAngle(l2));
3790
+ const aspectName = normalizeAspect2(a);
3791
+ const cls = ASPECT_CLASS2[aspectName] ?? "aspect-other";
3792
+ const orbLabel = formatNumber(a.orb, 1);
3793
+ return svg4`<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>`;
3426
3794
  });
3427
3795
  }
3428
3796
  renderAspects(aspects) {
@@ -3437,15 +3805,13 @@ var RoxySynastryChart = class extends LitElement16 {
3437
3805
  </tr>
3438
3806
  </thead>
3439
3807
  <tbody>
3440
- ${aspects.slice(0, 16).map(
3808
+ ${aspects.slice(0, 12).map(
3441
3809
  (a) => html16`<tr>
3442
- <td>${a.planet1 ?? ""}</td>
3443
- <td>${a.planet2 ?? ""}</td>
3444
- <td>${a.aspect ?? ""}</td>
3445
- <td class="orb">
3446
- ${typeof a.orb === "number" ? a.orb.toFixed(1) : ""}
3447
- </td>
3448
- <td>${a.strength ?? ""}</td>
3810
+ <td>${a.planet1}</td>
3811
+ <td>${a.planet2}</td>
3812
+ <td>${normalizeAspect2(a) || ""}</td>
3813
+ <td class="orb">${formatNumber(a.orb, 1)}</td>
3814
+ <td>${formatStrength(a.strength)}</td>
3449
3815
  </tr>`
3450
3816
  )}
3451
3817
  </tbody>
@@ -3506,6 +3872,42 @@ RoxySynastryChart.styles = [
3506
3872
  font-weight: 600;
3507
3873
  font-size: 13px;
3508
3874
  }
3875
+ .aspect {
3876
+ stroke-width: 0.8;
3877
+ fill: none;
3878
+ opacity: 0.5;
3879
+ }
3880
+ .aspect-trine,
3881
+ .aspect-sextile {
3882
+ stroke: var(--roxy-success, #16a34a);
3883
+ }
3884
+ .aspect-square,
3885
+ .aspect-opposition {
3886
+ stroke: var(--roxy-danger, #dc2626);
3887
+ }
3888
+ .aspect-conjunction {
3889
+ stroke: var(--roxy-accent-fg, #b45309);
3890
+ }
3891
+ .aspect-other {
3892
+ stroke: var(--roxy-muted, #71717a);
3893
+ opacity: 0.35;
3894
+ }
3895
+ .legend-row {
3896
+ display: flex;
3897
+ flex-wrap: wrap;
3898
+ gap: var(--roxy-space-md, 1rem);
3899
+ font-size: var(--roxy-text-xs, 0.75rem);
3900
+ color: var(--roxy-muted, #71717a);
3901
+ margin-top: calc(var(--roxy-space-xs, 0.25rem) * -1);
3902
+ }
3903
+ .legend-row .swatch {
3904
+ display: inline-block;
3905
+ width: 8px;
3906
+ height: 8px;
3907
+ border-radius: 50%;
3908
+ margin-right: 4px;
3909
+ vertical-align: middle;
3910
+ }
3509
3911
 
3510
3912
  .summary {
3511
3913
  margin: 0;
@@ -3553,6 +3955,23 @@ RoxySynastryChart.styles = [
3553
3955
  padding-left: var(--roxy-space-md, 1rem);
3554
3956
  font-size: var(--roxy-text-sm, 0.875rem);
3555
3957
  }
3958
+
3959
+ .missing-planets {
3960
+ background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 8%, transparent);
3961
+ border: 1px solid var(--roxy-border, #e4e4e7);
3962
+ border-radius: var(--roxy-radius-md, 8px);
3963
+ padding: var(--roxy-space-md, 1rem);
3964
+ color: var(--roxy-fg, #0a0a0a);
3965
+ font-size: var(--roxy-text-sm, 0.875rem);
3966
+ line-height: 1.5;
3967
+ }
3968
+ .missing-planets code {
3969
+ font-family: var(--roxy-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
3970
+ font-size: 0.95em;
3971
+ background: color-mix(in srgb, var(--roxy-fg, #0a0a0a) 6%, transparent);
3972
+ padding: 0 4px;
3973
+ border-radius: 4px;
3974
+ }
3556
3975
  `
3557
3976
  ];
3558
3977
  __decorateClass([
@@ -3565,6 +3984,20 @@ function capitalize3(s) {
3565
3984
  if (!s) return "";
3566
3985
  return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
3567
3986
  }
3987
+ var ASPECT_CLASS2 = {
3988
+ conjunction: "aspect-conjunction",
3989
+ sextile: "aspect-sextile",
3990
+ square: "aspect-square",
3991
+ trine: "aspect-trine",
3992
+ opposition: "aspect-opposition"
3993
+ };
3994
+ function normalizeAspect2(a) {
3995
+ return (a.type ?? "").toLowerCase().replace(/_/g, "-");
3996
+ }
3997
+ function formatStrength(s) {
3998
+ if (typeof s === "number") return Math.round(s).toString();
3999
+ return "";
4000
+ }
3568
4001
 
3569
4002
  // packages/ui/src/components/tarot-card.ts
3570
4003
  import { css as css18, html as html17, LitElement as LitElement17, nothing as nothing17 } from "lit";
@@ -3578,18 +4011,17 @@ var RoxyTarotCard = class extends LitElement17 {
3578
4011
  this.flipped = !this.flipped;
3579
4012
  };
3580
4013
  }
3581
- getCard() {
3582
- if (!this.data) return null;
3583
- if ("card" in this.data && this.data.card) return this.data.card;
3584
- return this.data;
3585
- }
3586
4014
  render() {
3587
- const card = this.getCard();
3588
- if (!card)
4015
+ const d = this.data;
4016
+ if (!d)
3589
4017
  return html17`<div class="roxy-empty" role="status">No tarot data</div>`;
4018
+ if ("card" in d) return this.renderDailyCard(d);
4019
+ return this.renderFullCard(d);
4020
+ }
4021
+ renderDailyCard(d) {
4022
+ const card = d.card;
3590
4023
  const isReversed = this.flipped !== Boolean(card.reversed);
3591
- const meaning = typeof card.meaning === "string" ? card.meaning : (isReversed ? card.meaning?.reversed : card.meaning?.upright) ?? card.meaning?.spiritual ?? card.upright?.meaning;
3592
- const dailyMessage = this.data && "dailyMessage" in this.data ? this.data.dailyMessage : void 0;
4024
+ const keywords = card.keywords ?? [];
3593
4025
  return html17`<article class="card" aria-label=${card.name ?? "Tarot card"}>
3594
4026
  <div class="image-wrap">
3595
4027
  ${card.imageUrl ? html17`<img
@@ -3614,15 +4046,60 @@ var RoxyTarotCard = class extends LitElement17 {
3614
4046
  <div>
3615
4047
  <div class="meta">
3616
4048
  ${card.arcana ? html17`${card.arcana} arcana` : nothing17}
3617
- ${card.number !== void 0 && card.number !== null ? html17` · ${card.number}` : nothing17}
3618
4049
  ${isReversed ? html17` · reversed` : nothing17}
3619
- ${card.position ? html17`<span class="position">${card.position}</span>` : nothing17}
3620
4050
  </div>
3621
4051
  <h2 class="title">${card.name ?? "Tarot card"}</h2>
3622
- ${dailyMessage ? html17`<p class="message">${dailyMessage}</p>` : nothing17}
3623
- ${meaning ? html17`<p>${meaning}</p>` : nothing17}
3624
- ${card.keywords?.length ? html17`<div class="chips">
3625
- ${card.keywords.map((k) => html17`<span>${k}</span>`)}
4052
+ ${d.dailyMessage ? html17`<p class="message">${d.dailyMessage}</p>` : nothing17}
4053
+ ${card.meaning ? html17`<p>${card.meaning}</p>` : nothing17}
4054
+ ${keywords.length > 0 ? html17`<div class="chips">
4055
+ ${keywords.map((k) => html17`<span>${k}</span>`)}
4056
+ </div>` : nothing17}
4057
+ <button
4058
+ class="flip"
4059
+ type="button"
4060
+ @click=${this.toggleFlip}
4061
+ aria-pressed=${this.flipped ? "true" : "false"}
4062
+ >
4063
+ Flip card
4064
+ </button>
4065
+ </div>
4066
+ </article>`;
4067
+ }
4068
+ renderFullCard(d) {
4069
+ const isReversed = this.flipped;
4070
+ const orientedMeaning = isReversed ? d.reversed : d.upright;
4071
+ const keywords = isReversed ? d.keywords?.reversed ?? [] : d.keywords?.upright ?? [];
4072
+ return html17`<article class="card" aria-label=${d.name ?? "Tarot card"}>
4073
+ <div class="image-wrap">
4074
+ ${d.imageUrl ? html17`<img
4075
+ class=${`image ${isReversed ? "reversed" : ""}`}
4076
+ src=${d.imageUrl}
4077
+ alt=${d.name ?? "Tarot card"}
4078
+ tabindex="0"
4079
+ @click=${this.toggleFlip}
4080
+ @keydown=${(e) => {
4081
+ if (e.key === "Enter" || e.key === " ") {
4082
+ e.preventDefault();
4083
+ this.toggleFlip();
4084
+ }
4085
+ }}
4086
+ />` : html17`<div
4087
+ class=${`image ${isReversed ? "reversed" : ""}`}
4088
+ style="aspect-ratio: 0.6; display: flex; align-items: center; justify-content: center; color: var(--roxy-muted)"
4089
+ >
4090
+ ${d.name ?? "?"}
4091
+ </div>`}
4092
+ </div>
4093
+ <div>
4094
+ <div class="meta">
4095
+ ${d.arcana ? html17`${d.arcana} arcana` : nothing17}
4096
+ ${d.number !== void 0 && d.number !== null ? html17` · ${d.number}` : nothing17}
4097
+ ${isReversed ? html17` · reversed` : nothing17}
4098
+ </div>
4099
+ <h2 class="title">${d.name ?? "Tarot card"}</h2>
4100
+ ${orientedMeaning?.description ? html17`<p>${orientedMeaning.description}</p>` : nothing17}
4101
+ ${keywords.length > 0 ? html17`<div class="chips">
4102
+ ${keywords.map((k) => html17`<span>${k}</span>`)}
3626
4103
  </div>` : nothing17}
3627
4104
  <button
3628
4105
  class="flip"
@@ -3692,11 +4169,6 @@ RoxyTarotCard.styles = [
3692
4169
  letter-spacing: 0.06em;
3693
4170
  margin-bottom: var(--roxy-space-sm, 0.5rem);
3694
4171
  }
3695
- .position {
3696
- color: var(--roxy-info, #0284c7);
3697
- margin-left: var(--roxy-space-xs, 0.25rem);
3698
- text-transform: capitalize;
3699
- }
3700
4172
 
3701
4173
  .message {
3702
4174
  color: var(--roxy-fg, #0a0a0a);
@@ -3758,22 +4230,30 @@ var RoxyTarotSpread = class extends LitElement18 {
3758
4230
  const d = this.data;
3759
4231
  if (!d)
3760
4232
  return html18`<div class="roxy-empty" role="status">No tarot spread</div>`;
3761
- const positions = d.positions ?? d.cards ?? [];
3762
- const isYesNo = !!d.answer;
3763
- const answerClass = isYesNo ? (d.answer ?? "").toLowerCase().replace(/[^a-z]/g, "") : "";
4233
+ const isYesNo = "answer" in d;
4234
+ const isDrawn = "cards" in d && !("spread" in d);
4235
+ const positions = isDrawn ? [] : "positions" in d ? d.positions ?? [] : [];
4236
+ const cards = isDrawn && "cards" in d ? d.cards : [];
4237
+ const answer = isYesNo ? d.answer : void 0;
4238
+ const strength = isYesNo ? d.strength : void 0;
4239
+ const spreadLabel = "spread" in d ? d.spread : this.spread.replace(/-/g, " ");
4240
+ const question = "question" in d ? d.question : void 0;
4241
+ const summary = "summary" in d ? d.summary : void 0;
4242
+ const yesNoInterp = isYesNo ? d.interpretation : void 0;
4243
+ const answerClass = answer ? answer.toLowerCase().replace(/[^a-z]/g, "") : "";
3764
4244
  return html18`<article class="wrap" aria-label="Tarot spread">
3765
4245
  <header class="head">
3766
- <h2 class="title">${d.spread ?? this.spread.replace(/-/g, " ")}</h2>
3767
- ${d.question ? html18`<span class="question">"${d.question}"</span>` : nothing18}
4246
+ <h2 class="title">${spreadLabel}</h2>
4247
+ ${question ? html18`<span class="question">"${question}"</span>` : nothing18}
3768
4248
  </header>
3769
4249
  ${isYesNo ? html18`<div>
3770
- <span class=${`answer ${answerClass}`}>${d.answer}</span>
3771
- ${d.strength ? html18`<small> · ${d.strength}</small>` : nothing18}
4250
+ <span class=${`answer ${answerClass}`}>${answer}</span>
4251
+ ${strength ? html18`<small> · ${strength}</small>` : nothing18}
3772
4252
  </div>` : nothing18}
3773
4253
  ${positions.length > 0 ? html18`<div class="grid">
3774
4254
  ${positions.map(
3775
4255
  (p) => html18`<div class="card">
3776
- <p class="label">${p.label ?? p.name ?? p.position ?? ""}</p>
4256
+ <p class="label">${p.name ?? ""}</p>
3777
4257
  <div class="image">
3778
4258
  ${p.card?.imageUrl ? html18`<img
3779
4259
  src=${p.card.imageUrl}
@@ -3789,8 +4269,26 @@ var RoxyTarotSpread = class extends LitElement18 {
3789
4269
  </div>`
3790
4270
  )}
3791
4271
  </div>` : nothing18}
3792
- ${d.reading ? html18`<p class="reading">${d.reading}</p>` : nothing18}
3793
- ${d.interpretation && !d.reading ? html18`<p class="reading">${d.interpretation}</p>` : nothing18}
4272
+ ${cards.length > 0 ? html18`<div class="grid">
4273
+ ${cards.map(
4274
+ (c) => html18`<div class="card">
4275
+ <div class="image">
4276
+ ${c.imageUrl ? html18`<img
4277
+ src=${c.imageUrl}
4278
+ alt=${c.name ?? "tarot card"}
4279
+ class=${c.reversed ? "reversed" : ""}
4280
+ />` : html18`${c.name ?? "?"}`}
4281
+ </div>
4282
+ <p class="name">
4283
+ ${c.name ?? ""}
4284
+ ${c.reversed ? html18`<small>(reversed)</small>` : nothing18}
4285
+ </p>
4286
+ ${c.meaning ? html18`<p class="interp">${c.meaning}</p>` : nothing18}
4287
+ </div>`
4288
+ )}
4289
+ </div>` : nothing18}
4290
+ ${summary ? html18`<p class="reading">${summary}</p>` : nothing18}
4291
+ ${yesNoInterp ? html18`<p class="reading">${yesNoInterp}</p>` : nothing18}
3794
4292
  </article>`;
3795
4293
  }
3796
4294
  };
@@ -3832,15 +4330,15 @@ RoxyTarotSpread.styles = [
3832
4330
  }
3833
4331
  .answer.yes {
3834
4332
  background: color-mix(in srgb, var(--roxy-success, #16a34a) 16%, transparent);
3835
- color: var(--roxy-success, #16a34a);
4333
+ color: var(--roxy-success-fg, #166534);
3836
4334
  }
3837
4335
  .answer.no {
3838
4336
  background: color-mix(in srgb, var(--roxy-danger, #dc2626) 16%, transparent);
3839
- color: var(--roxy-danger, #dc2626);
4337
+ color: var(--roxy-danger-fg, #991b1b);
3840
4338
  }
3841
4339
  .answer.maybe {
3842
4340
  background: color-mix(in srgb, var(--roxy-warning, #ea580c) 16%, transparent);
3843
- color: var(--roxy-warning, #ea580c);
4341
+ color: var(--roxy-warning-fg, #9a3412);
3844
4342
  }
3845
4343
 
3846
4344
  .grid {
@@ -3981,21 +4479,12 @@ var RoxyVedicKundli = class extends LitElement19 {
3981
4479
  }
3982
4480
  buildHouses() {
3983
4481
  if (!this.data) return [];
4482
+ const data = this.data;
3984
4483
  const houses = [];
3985
- if (Array.isArray(this.data.houses)) {
3986
- for (const h of this.data.houses) {
3987
- houses.push({
3988
- house: h.house ?? h.number ?? houses.length + 1,
3989
- sign: h.sign ?? "",
3990
- planets: h.planets ?? []
3991
- });
3992
- }
3993
- if (houses.length > 0) return houses;
3994
- }
3995
4484
  for (let i = 0; i < 12; i++) {
3996
4485
  const key = RASHI_KEYS[i];
3997
- const bucket = this.data[key];
3998
- const planets = (bucket?.signs ?? []).map((p) => p.planet ?? "").filter(Boolean);
4486
+ const bucket = data[key];
4487
+ const planets = (bucket?.signs ?? []).map((p) => p.graha).filter(Boolean);
3999
4488
  houses.push({
4000
4489
  house: i + 1,
4001
4490
  sign: RASHI_TO_SIGN[key] ?? "",
@@ -4035,19 +4524,28 @@ var RoxyVedicKundli = class extends LitElement19 {
4035
4524
  </svg>
4036
4525
  </div>`;
4037
4526
  }
4527
+ isLagna(h) {
4528
+ const ascSign = this.data?.meta?.Lagna?.rashi;
4529
+ if (!ascSign) return false;
4530
+ return ascSign.toLowerCase() === h.sign.toLowerCase();
4531
+ }
4038
4532
  renderHouseGroup(h) {
4039
4533
  const center = SOUTH_HOUSE_CENTERS[h.house];
4040
4534
  const signPos = SOUTH_SIGN_POSITIONS[h.house];
4041
4535
  if (!center || !signPos) return nothing19;
4042
4536
  const signAbbr = SIGN_ABBR[h.sign] ?? "";
4043
4537
  const planets = h.planets ?? [];
4538
+ const isLagna = this.isLagna(h);
4044
4539
  return svg5`
4045
4540
  <g>
4541
+ ${isLagna ? svg5`<rect class="lagna-bg" x=${center.x - 30} y=${center.y - 28} width="60" height="56" rx="6" />` : nothing19}
4046
4542
  ${signAbbr ? svg5`<text class="sign-text" x=${signPos.x} y=${signPos.y} text-anchor="middle" dominant-baseline="central">${signAbbr}</text>` : nothing19}
4543
+ ${isLagna ? svg5`<text class="lagna-marker" x=${center.x} y=${center.y - 18} text-anchor="middle" dominant-baseline="central">LAGNA</text>` : nothing19}
4047
4544
  ${planets.map((planet, j) => {
4048
4545
  const abbr = PLANET_ABBR[capitalize4(planet)] ?? planet.slice(0, 2);
4049
4546
  const lineHeight = 13;
4050
- const startY = center.y - (planets.length - 1) * lineHeight / 2;
4547
+ const baseY = isLagna ? center.y + 8 : center.y;
4548
+ const startY = baseY - (planets.length - 1) * lineHeight / 2;
4051
4549
  const yPos = startY + j * lineHeight;
4052
4550
  return svg5`<text class="planet-text" x=${center.x} y=${yPos} text-anchor="middle" dominant-baseline="central">${abbr}</text>`;
4053
4551
  })}
@@ -4089,6 +4587,18 @@ RoxyVedicKundli.styles = [
4089
4587
  font-weight: 600;
4090
4588
  font-family: var(--roxy-font-sans);
4091
4589
  }
4590
+ .lagna-marker {
4591
+ fill: var(--roxy-accent-fg, #b45309);
4592
+ font-size: 8px;
4593
+ font-weight: 700;
4594
+ font-family: var(--roxy-font-sans);
4595
+ letter-spacing: 0.05em;
4596
+ }
4597
+ .lagna-bg {
4598
+ fill: color-mix(in srgb, var(--roxy-accent, #f59e0b) 12%, transparent);
4599
+ stroke: color-mix(in srgb, var(--roxy-accent, #f59e0b) 45%, transparent);
4600
+ stroke-width: 0.8;
4601
+ }
4092
4602
  `
4093
4603
  ];
4094
4604
  __decorateClass([
@@ -4105,30 +4615,311 @@ function capitalize4(s) {
4105
4615
  return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
4106
4616
  }
4107
4617
 
4108
- // packages/ui/src/index.ts
4109
- var ROXY_UI_VERSION = "0.1.0";
4110
- var ROXY_UI_COMPONENTS = [
4111
- "natal-chart",
4112
- "horoscope-card",
4113
- "synastry-chart",
4114
- "compatibility-card",
4115
- "moon-phase",
4116
- "vedic-kundli",
4117
- "panchang-table",
4118
- "dasha-timeline",
4119
- "dosha-card",
4120
- "guna-milan",
4121
- "kp-planets-table",
4122
- "numerology-card",
4123
- "tarot-card",
4124
- "tarot-spread",
4125
- "biorhythm-chart",
4126
- "hexagram",
4127
- "endpoint-form",
4128
- "location-search",
4129
- "data"
4618
+ // packages/ui/src/manifest.ts
4619
+ var ROXY_COMPONENTS = [
4620
+ {
4621
+ pascal: "RoxyNatalChart",
4622
+ tag: "roxy-natal-chart",
4623
+ slug: "natal-chart",
4624
+ domain: "astrology",
4625
+ heading: "Natal chart",
4626
+ endpoints: ["astrology.generateNatalChart"],
4627
+ description: "Western natal chart wheel for /astrology/natal-chart responses",
4628
+ docsLabel: "Western",
4629
+ endpointLabel: "POST /astrology/natal-chart",
4630
+ docsSummary: "Natal chart wheel with planet glyphs and aspect lines",
4631
+ topic: "Astrology"
4632
+ },
4633
+ {
4634
+ pascal: "RoxyHoroscopeCard",
4635
+ tag: "roxy-horoscope-card",
4636
+ slug: "horoscope-card",
4637
+ domain: "astrology",
4638
+ heading: "Daily horoscope",
4639
+ endpoints: [
4640
+ "astrology.getDailyHoroscope",
4641
+ "astrology.getWeeklyHoroscope",
4642
+ "astrology.getMonthlyHoroscope"
4643
+ ],
4644
+ description: "Daily, weekly, or monthly horoscope card for /astrology/horoscope/...",
4645
+ docsLabel: "Western",
4646
+ endpointLabel: "GET /astrology/horoscope/{sign}/{daily,weekly,monthly}",
4647
+ docsSummary: "Daily, weekly, or monthly horoscope card",
4648
+ topic: "Astrology"
4649
+ },
4650
+ {
4651
+ pascal: "RoxySynastryChart",
4652
+ tag: "roxy-synastry-chart",
4653
+ slug: "synastry-chart",
4654
+ domain: "astrology",
4655
+ heading: "Synastry",
4656
+ endpoints: ["astrology.calculateSynastry"],
4657
+ description: "Dual-wheel synastry chart with inter-aspects table",
4658
+ docsLabel: "Western",
4659
+ endpointLabel: "POST /astrology/synastry",
4660
+ docsSummary: "Dual-wheel synastry with inter-aspects table",
4661
+ topic: "Astrology"
4662
+ },
4663
+ {
4664
+ pascal: "RoxyCompatibilityCard",
4665
+ tag: "roxy-compatibility-card",
4666
+ slug: "compatibility-card",
4667
+ domain: "astrology",
4668
+ heading: "Compatibility score",
4669
+ endpoints: [
4670
+ "astrology.calculateCompatibility",
4671
+ "numerology.calculateNumCompatibility",
4672
+ "biorhythm.calculateBioCompatibility"
4673
+ ],
4674
+ description: "Cross-domain compatibility score card",
4675
+ docsLabel: "Cross",
4676
+ endpointLabel: "POST /astrology/compatibility-score, /numerology/compatibility, /biorhythm/compatibility",
4677
+ docsSummary: "Score card with category breakdown",
4678
+ topic: "Astrology"
4679
+ },
4680
+ {
4681
+ pascal: "RoxyMoonPhase",
4682
+ tag: "roxy-moon-phase",
4683
+ slug: "moon-phase",
4684
+ domain: "astrology",
4685
+ heading: "Moon phase",
4686
+ endpoints: [
4687
+ "astrology.getCurrentMoonPhase",
4688
+ "astrology.getUpcomingMoonPhases",
4689
+ "astrology.getMoonCalendar"
4690
+ ],
4691
+ description: "Moon phase card and calendar",
4692
+ docsLabel: "Western",
4693
+ endpointLabel: "GET /astrology/moon-phase/{current,upcoming,calendar/...}",
4694
+ docsSummary: "Moon phase card and calendar",
4695
+ topic: "Astrology"
4696
+ },
4697
+ {
4698
+ pascal: "RoxyVedicKundli",
4699
+ tag: "roxy-vedic-kundli",
4700
+ slug: "vedic-kundli",
4701
+ domain: "vedic",
4702
+ heading: "Vedic kundli",
4703
+ endpoints: ["vedicAstrology.generateBirthChart"],
4704
+ description: "South or North Indian Vedic kundli for /vedic-astrology/birth-chart",
4705
+ docsLabel: "Vedic",
4706
+ endpointLabel: "POST /vedic-astrology/birth-chart",
4707
+ docsSummary: "South or North Indian kundli",
4708
+ topic: "Vedic"
4709
+ },
4710
+ {
4711
+ pascal: "RoxyPanchangTable",
4712
+ tag: "roxy-panchang-table",
4713
+ slug: "panchang-table",
4714
+ domain: "vedic",
4715
+ heading: "Panchang",
4716
+ endpoints: [
4717
+ "vedicAstrology.getBasicPanchang",
4718
+ "vedicAstrology.getDetailedPanchang"
4719
+ ],
4720
+ description: "Panchang muhurta table with auspicious and inauspicious periods",
4721
+ docsLabel: "Vedic",
4722
+ endpointLabel: "POST /vedic-astrology/panchang/{basic,detailed}",
4723
+ docsSummary: "15+ muhurtas in detailed mode",
4724
+ topic: "Vedic"
4725
+ },
4726
+ {
4727
+ pascal: "RoxyDashaTimeline",
4728
+ tag: "roxy-dasha-timeline",
4729
+ slug: "dasha-timeline",
4730
+ domain: "vedic",
4731
+ heading: "Vimshottari dasha",
4732
+ endpoints: [
4733
+ "vedicAstrology.getCurrentDasha",
4734
+ "vedicAstrology.getMajorDashas",
4735
+ "vedicAstrology.getSubDashas"
4736
+ ],
4737
+ description: "Vimshottari dasha timeline with active mahadasha highlighted",
4738
+ docsLabel: "Vedic",
4739
+ endpointLabel: "POST /vedic-astrology/dasha/{current,major,sub/...}",
4740
+ docsSummary: "Vimshottari mahadasha + antardasha + pratyantardasha",
4741
+ topic: "Vedic"
4742
+ },
4743
+ {
4744
+ pascal: "RoxyDoshaCard",
4745
+ tag: "roxy-dosha-card",
4746
+ slug: "dosha-card",
4747
+ domain: "vedic",
4748
+ heading: "Manglik dosha",
4749
+ endpoints: [
4750
+ "vedicAstrology.checkManglikDosha",
4751
+ "vedicAstrology.checkKalsarpaDosha",
4752
+ "vedicAstrology.checkSadhesati"
4753
+ ],
4754
+ description: "Manglik, Kaal Sarp, or Sade Sati presence card",
4755
+ docsLabel: "Vedic",
4756
+ endpointLabel: "POST /vedic-astrology/dosha/{manglik,kalsarpa,sadhesati}",
4757
+ docsSummary: "Presence, severity, remedies, scoped effects",
4758
+ topic: "Vedic"
4759
+ },
4760
+ {
4761
+ pascal: "RoxyGunaMilan",
4762
+ tag: "roxy-guna-milan",
4763
+ slug: "guna-milan",
4764
+ domain: "vedic",
4765
+ heading: "Guna milan",
4766
+ endpoints: ["vedicAstrology.calculateGunMilan"],
4767
+ description: "36-point Ashtakoota matrimonial compatibility breakdown",
4768
+ docsLabel: "Vedic",
4769
+ endpointLabel: "POST /vedic-astrology/compatibility",
4770
+ docsSummary: "36-point Ashtakoota with eight sub-scores",
4771
+ topic: "Vedic"
4772
+ },
4773
+ {
4774
+ pascal: "RoxyKpPlanetsTable",
4775
+ tag: "roxy-kp-planets-table",
4776
+ slug: "kp-planets-table",
4777
+ domain: "vedic",
4778
+ heading: "KP planets",
4779
+ endpoints: ["vedicAstrology.getKpPlanets"],
4780
+ description: "KP planets table with sub-lord and sub-sub-lord columns",
4781
+ docsLabel: "Vedic (KP)",
4782
+ endpointLabel: "POST /vedic-astrology/kp/planets",
4783
+ docsSummary: "Sub-lord and sub-sub-lord columns",
4784
+ topic: "Vedic"
4785
+ },
4786
+ {
4787
+ pascal: "RoxyNumerologyCard",
4788
+ tag: "roxy-numerology-card",
4789
+ slug: "numerology-card",
4790
+ domain: "numerology",
4791
+ heading: "Life path number",
4792
+ endpoints: [
4793
+ "numerology.calculateLifePath",
4794
+ "numerology.calculateExpression",
4795
+ "numerology.calculatePersonalYear",
4796
+ "numerology.generateNumerologyChart"
4797
+ ],
4798
+ description: "Numerology card for life path, expression, personal year, or full chart",
4799
+ docsLabel: "Numerology",
4800
+ endpointLabel: "POST /numerology/{life-path,expression,personal-year,chart}",
4801
+ docsSummary: "Life path, expression, personal year, full chart",
4802
+ topic: "Numerology"
4803
+ },
4804
+ {
4805
+ pascal: "RoxyTarotCard",
4806
+ tag: "roxy-tarot-card",
4807
+ slug: "tarot-card",
4808
+ domain: "tarot",
4809
+ heading: "Daily tarot card",
4810
+ endpoints: ["tarot.getCard", "tarot.getDailyCard"],
4811
+ description: "Single tarot card with upright/reversed flip animation",
4812
+ docsLabel: "Tarot",
4813
+ endpointLabel: "GET /tarot/cards/{id}, POST /tarot/daily",
4814
+ docsSummary: "Single card with upright and reversed flip",
4815
+ topic: "Tarot"
4816
+ },
4817
+ {
4818
+ pascal: "RoxyTarotSpread",
4819
+ tag: "roxy-tarot-spread",
4820
+ slug: "tarot-spread",
4821
+ domain: "tarot",
4822
+ heading: "Three-card spread",
4823
+ endpoints: [
4824
+ "tarot.castThreeCard",
4825
+ "tarot.castCelticCross",
4826
+ "tarot.castLoveSpread",
4827
+ "tarot.castYesNo",
4828
+ "tarot.drawCards"
4829
+ ],
4830
+ description: "Tarot spread renderer for three-card, Celtic Cross, love, or yes/no",
4831
+ docsLabel: "Tarot",
4832
+ endpointLabel: "POST /tarot/spreads/{three-card,celtic-cross,love}, /tarot/yes-no, /tarot/draw",
4833
+ docsSummary: "Spreads with positions and reading",
4834
+ topic: "Tarot"
4835
+ },
4836
+ {
4837
+ pascal: "RoxyBiorhythmChart",
4838
+ tag: "roxy-biorhythm-chart",
4839
+ slug: "biorhythm-chart",
4840
+ domain: "biorhythm",
4841
+ heading: "Daily biorhythm",
4842
+ endpoints: [
4843
+ "biorhythm.getDailyBiorhythm",
4844
+ "biorhythm.getForecast",
4845
+ "biorhythm.getCriticalDays"
4846
+ ],
4847
+ description: "Daily biorhythm bars or multi-day forecast cycle lines",
4848
+ docsLabel: "Biorhythm",
4849
+ endpointLabel: "POST /biorhythm/{daily,forecast,critical-days}",
4850
+ docsSummary: "Daily bars, forecast cycle lines, critical days",
4851
+ topic: "Biorhythm"
4852
+ },
4853
+ {
4854
+ pascal: "RoxyHexagram",
4855
+ tag: "roxy-hexagram",
4856
+ slug: "hexagram",
4857
+ domain: "iching",
4858
+ heading: "I Ching hexagram",
4859
+ endpoints: [
4860
+ "iching.getHexagram",
4861
+ "iching.castReading",
4862
+ "iching.getDailyHexagram",
4863
+ "iching.castDailyReading",
4864
+ "iching.getRandomHexagram"
4865
+ ],
4866
+ description: "I Ching hexagram with trigram glyphs, judgment, image, and changing lines",
4867
+ docsLabel: "I Ching",
4868
+ endpointLabel: "GET /iching/hexagrams/{number}, /iching/cast, POST /iching/daily, /iching/daily/cast",
4869
+ docsSummary: "Hexagram with trigrams, judgment, image, changing lines",
4870
+ topic: "I Ching"
4871
+ },
4872
+ {
4873
+ pascal: "RoxyEndpointForm",
4874
+ tag: "roxy-endpoint-form",
4875
+ slug: "endpoint-form",
4876
+ domain: "utility",
4877
+ heading: "Schema-driven form",
4878
+ endpoints: [],
4879
+ description: "Schema-driven form that emits roxy-submit with a validated payload",
4880
+ docsLabel: "Helper",
4881
+ endpointLabel: "Any endpoint via x-roxy-ui hints",
4882
+ docsSummary: "Schema-driven form, emits roxy-submit",
4883
+ topic: "Helpers",
4884
+ selfFetching: true
4885
+ },
4886
+ {
4887
+ pascal: "RoxyLocationSearch",
4888
+ tag: "roxy-location-search",
4889
+ slug: "location-search",
4890
+ domain: "utility",
4891
+ heading: "City search",
4892
+ endpoints: ["location.searchCities"],
4893
+ description: "City search input with debounced /location/search calls",
4894
+ docsLabel: "Helper",
4895
+ endpointLabel: "GET /location/search",
4896
+ docsSummary: "Debounced city search input, emits roxy-location-select",
4897
+ topic: "Helpers",
4898
+ selfFetching: true
4899
+ },
4900
+ {
4901
+ pascal: "RoxyData",
4902
+ tag: "roxy-data",
4903
+ slug: "data",
4904
+ domain: "utility",
4905
+ heading: "Generic renderer",
4906
+ endpoints: [],
4907
+ description: "Generic fallback renderer for any OpenAPI response shape",
4908
+ docsLabel: "Helper",
4909
+ endpointLabel: "Any response shape",
4910
+ docsSummary: "Generic fallback renderer for unknown shapes",
4911
+ topic: "Helpers",
4912
+ selfFetching: true
4913
+ }
4130
4914
  ];
4915
+
4916
+ // packages/ui/src/version.ts
4917
+ var ROXY_UI_VERSION = "0.1.3";
4918
+
4919
+ // packages/ui/src/index.ts
4920
+ var ROXY_UI_COMPONENTS = ROXY_COMPONENTS.map((c) => c.slug);
4131
4921
  export {
4922
+ ROXY_COMPONENTS,
4132
4923
  ROXY_UI_COMPONENTS,
4133
4924
  ROXY_UI_VERSION,
4134
4925
  RoxyBiorhythmChart,