@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
@@ -1,53 +1,16 @@
1
1
  import { css, html, LitElement, nothing } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
+ import type {
4
+ GetBasicPanchangResponse,
5
+ GetDetailedPanchangResponse,
6
+ } from '../types/index.js';
3
7
  import { baseStyles } from '../utils/base-styles.js';
8
+ import { formatDate, formatTime, formatTimeRange } from '../utils/format.js';
4
9
 
5
- interface PanchangTime {
6
- start?: string;
7
- end?: string;
8
- }
9
-
10
- interface PanchangData {
11
- date?: string;
12
- location?: { name?: string; latitude?: number; longitude?: number };
13
- vara?: string;
14
- sunrise?: string;
15
- sunset?: string;
16
- moonrise?: string;
17
- moonset?: string;
18
- sunSign?: string;
19
- moonSign?: string;
20
- sunNakshatra?: string;
21
- tithi?: string | { name?: string; phase?: string; end?: string };
22
- nakshatra?: string | { name?: string; lord?: string; end?: string };
23
- yoga?: string | { name?: string; end?: string };
24
- karana?: string | { name?: string; end?: string };
25
- hora?: string;
26
- rahuKaal?: PanchangTime;
27
- yamaganda?: PanchangTime;
28
- gulika?: PanchangTime;
29
- abhijitMuhurta?: PanchangTime;
30
- brahmaMuhurta?: PanchangTime;
31
- vijayaMuhurta?: PanchangTime;
32
- nishitaMuhurta?: PanchangTime;
33
- godhuliMuhurta?: PanchangTime;
34
- pratahSandhya?: PanchangTime;
35
- sayahnaSandhya?: PanchangTime;
36
- durMuhurta?: PanchangTime[];
37
- varjyam?: PanchangTime[];
38
- amritKalam?: PanchangTime[];
39
- chandrabalam?: string | string[];
40
- tarabalam?: string;
41
- panchaka?: string;
42
- bhadra?: string;
43
- sunLongitude?: number;
44
- moonLongitude?: number;
45
- }
10
+ type PanchangData = GetBasicPanchangResponse | GetDetailedPanchangResponse;
11
+ type PanchangTime = GetDetailedPanchangResponse['rahuKaal'];
46
12
 
47
- /**
48
- * Panchang table for /vedic-astrology/panchang/{basic,detailed}. Detailed mode
49
- * renders 15+ muhurtas. Basic mode renders the five elements only.
50
- */
13
+ /** Panchang table for /vedic-astrology/panchang/{basic,detailed}. */
51
14
  @customElement('roxy-panchang-table')
52
15
  export class RoxyPanchangTable extends LitElement {
53
16
  static styles = [
@@ -123,35 +86,40 @@ export class RoxyPanchangTable extends LitElement {
123
86
  const d = this.data;
124
87
  if (!d)
125
88
  return html`<div class="roxy-empty" role="status">No panchang data</div>`;
89
+ const detailed = 'sunrise' in d ? d : null;
126
90
 
127
- const fivefold = [
91
+ const fivefold: Array<[string, string]> = [
128
92
  ['Tithi', this.formatPart(d.tithi)],
129
93
  ['Nakshatra', this.formatPart(d.nakshatra)],
130
94
  ['Yoga', this.formatPart(d.yoga)],
131
95
  ['Karana', this.formatPart(d.karana)],
132
- ['Vara', d.vara ?? ''],
133
96
  ];
97
+ if (detailed) fivefold.push(['Vara', this.formatPart(detailed.vara)]);
134
98
 
135
- const muhurtas: Array<[string, PanchangTime | undefined]> = [
136
- ['Brahma Muhurta', d.brahmaMuhurta],
137
- ['Abhijit Muhurta', d.abhijitMuhurta],
138
- ['Vijaya Muhurta', d.vijayaMuhurta],
139
- ['Godhuli Muhurta', d.godhuliMuhurta],
140
- ['Nishita Muhurta', d.nishitaMuhurta],
141
- ['Pratah Sandhya', d.pratahSandhya],
142
- ['Sayahna Sandhya', d.sayahnaSandhya],
143
- ];
99
+ const muhurtas: Array<[string, PanchangTime | undefined]> = detailed
100
+ ? [
101
+ ['Brahma Muhurta', detailed.brahmaMuhurta],
102
+ ['Abhijit Muhurta', detailed.abhijitMuhurta],
103
+ ['Vijaya Muhurta', detailed.vijayaMuhurta],
104
+ ['Godhuli Muhurta', detailed.godhuliMuhurta],
105
+ ['Nishita Muhurta', detailed.nishitaMuhurta],
106
+ ['Pratah Sandhya', detailed.pratahSandhya],
107
+ ['Sayahna Sandhya', detailed.sayahnaSandhya],
108
+ ]
109
+ : [];
144
110
 
145
- const inauspicious: Array<[string, PanchangTime | undefined]> = [
146
- ['Rahu Kaal', d.rahuKaal],
147
- ['Yamaganda', d.yamaganda],
148
- ['Gulika', d.gulika],
149
- ];
111
+ const inauspicious: Array<[string, PanchangTime | undefined]> = detailed
112
+ ? [
113
+ ['Rahu Kaal', detailed.rahuKaal],
114
+ ['Yamaganda', detailed.yamaganda],
115
+ ['Gulika', detailed.gulika],
116
+ ]
117
+ : [];
150
118
 
151
119
  return html`<div class="wrap" aria-label="Panchang">
152
120
  <header class="head">
153
121
  <h2 class="title">Panchang</h2>
154
- <span class="date">${d.date ?? ''}</span>
122
+ <span class="date">${detailed ? formatDate(detailed.date) : ''}</span>
155
123
  </header>
156
124
  <table>
157
125
  <tbody>
@@ -162,34 +130,34 @@ export class RoxyPanchangTable extends LitElement {
162
130
  </tr>`,
163
131
  )}
164
132
  ${
165
- d.sunrise
133
+ detailed?.sunrise
166
134
  ? html`<tr>
167
135
  <th>Sunrise</th>
168
- <td>${d.sunrise}</td>
136
+ <td>${formatTime(detailed.sunrise)}</td>
169
137
  </tr>`
170
138
  : nothing
171
139
  }
172
140
  ${
173
- d.sunset
141
+ detailed?.sunset
174
142
  ? html`<tr>
175
143
  <th>Sunset</th>
176
- <td>${d.sunset}</td>
144
+ <td>${formatTime(detailed.sunset)}</td>
177
145
  </tr>`
178
146
  : nothing
179
147
  }
180
148
  ${
181
- d.moonrise
149
+ detailed?.moonrise
182
150
  ? html`<tr>
183
151
  <th>Moonrise</th>
184
- <td>${d.moonrise}</td>
152
+ <td>${formatTime(detailed.moonrise)}</td>
185
153
  </tr>`
186
154
  : nothing
187
155
  }
188
156
  ${
189
- d.moonset
157
+ detailed?.moonset
190
158
  ? html`<tr>
191
159
  <th>Moonset</th>
192
- <td>${d.moonset}</td>
160
+ <td>${formatTime(detailed.moonset)}</td>
193
161
  </tr>`
194
162
  : nothing
195
163
  }
@@ -207,7 +175,7 @@ export class RoxyPanchangTable extends LitElement {
207
175
  .map(
208
176
  ([k, v]) => html`<tr>
209
177
  <th>${k}</th>
210
- <td>${formatRange(v)}</td>
178
+ <td>${formatTimeRange(v)}</td>
211
179
  </tr>`,
212
180
  )}
213
181
  </tbody>
@@ -220,7 +188,7 @@ export class RoxyPanchangTable extends LitElement {
220
188
  .map(
221
189
  ([k, v]) => html`<tr>
222
190
  <th>${k}</th>
223
- <td>${formatRange(v)}</td>
191
+ <td>${formatTimeRange(v)}</td>
224
192
  </tr>`,
225
193
  )}
226
194
  </tbody>
@@ -252,12 +220,6 @@ export class RoxyPanchangTable extends LitElement {
252
220
  }
253
221
  }
254
222
 
255
- function formatRange(t: PanchangTime | undefined): string {
256
- if (!t) return '';
257
- if (t.start && t.end) return `${t.start} - ${t.end}`;
258
- return t.start ?? t.end ?? '';
259
- }
260
-
261
223
  declare global {
262
224
  interface HTMLElementTagNameMap {
263
225
  'roxy-panchang-table': RoxyPanchangTable;
@@ -1,41 +1,26 @@
1
1
  import { css, html, LitElement, nothing, svg } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
3
  import { PLANET_GLYPH, SIGN_GLYPH } from '../tokens/index.js';
4
+ import type {
5
+ CalculateSynastryResponse,
6
+ NatalChartResponse,
7
+ } from '../types/index.js';
4
8
  import { baseStyles } from '../utils/base-styles.js';
5
9
  import { polarToCartesian } from '../utils/degree.js';
10
+ import { formatNumber } from '../utils/format.js';
6
11
 
7
- interface PlanetEntry {
8
- name?: string;
9
- planet?: string;
10
- longitude?: number;
11
- degree?: number;
12
- sign?: string;
13
- }
12
+ type PlanetEntry = NatalChartResponse['planets'][number];
13
+ type InterAspect = CalculateSynastryResponse['interAspects'][number];
14
14
 
15
- interface InterAspect {
16
- planet1?: string;
17
- planet2?: string;
18
- aspect?: string;
19
- orb?: number;
20
- strength?: string;
21
- interpretation?: string;
22
- }
23
-
24
- interface SynastryData {
25
- person1?: {
26
- planets?: PlanetEntry[] | Record<string, PlanetEntry>;
27
- name?: string;
28
- };
29
- person2?: {
30
- planets?: PlanetEntry[] | Record<string, PlanetEntry>;
31
- name?: string;
32
- };
33
- compatibilityScore?: number;
34
- summary?: string;
35
- interAspects?: InterAspect[];
36
- strengths?: string[];
37
- challenges?: string[];
38
- }
15
+ // TODO(spec): /astrology/synastry does not expose person1/person2 planet
16
+ // positions, but the wheel needs them to plot the dual chart. The preview
17
+ // injects them via scripts/refresh-samples.ts; production callers see an
18
+ // empty wheel. Either add `planets` to the CalculateSynastry response or
19
+ // document that callers must merge in their own natal-chart payloads.
20
+ type SynastryWithPlanets = CalculateSynastryResponse & {
21
+ person1?: { planets?: PlanetEntry[] };
22
+ person2?: { planets?: PlanetEntry[] };
23
+ };
39
24
 
40
25
  const SIZE = 360;
41
26
  const CENTER = SIZE / 2;
@@ -104,6 +89,42 @@ export class RoxySynastryChart extends LitElement {
104
89
  font-weight: 600;
105
90
  font-size: 13px;
106
91
  }
92
+ .aspect {
93
+ stroke-width: 0.8;
94
+ fill: none;
95
+ opacity: 0.5;
96
+ }
97
+ .aspect-trine,
98
+ .aspect-sextile {
99
+ stroke: var(--roxy-success, #16a34a);
100
+ }
101
+ .aspect-square,
102
+ .aspect-opposition {
103
+ stroke: var(--roxy-danger, #dc2626);
104
+ }
105
+ .aspect-conjunction {
106
+ stroke: var(--roxy-accent-fg, #b45309);
107
+ }
108
+ .aspect-other {
109
+ stroke: var(--roxy-muted, #71717a);
110
+ opacity: 0.35;
111
+ }
112
+ .legend-row {
113
+ display: flex;
114
+ flex-wrap: wrap;
115
+ gap: var(--roxy-space-md, 1rem);
116
+ font-size: var(--roxy-text-xs, 0.75rem);
117
+ color: var(--roxy-muted, #71717a);
118
+ margin-top: calc(var(--roxy-space-xs, 0.25rem) * -1);
119
+ }
120
+ .legend-row .swatch {
121
+ display: inline-block;
122
+ width: 8px;
123
+ height: 8px;
124
+ border-radius: 50%;
125
+ margin-right: 4px;
126
+ vertical-align: middle;
127
+ }
107
128
 
108
129
  .summary {
109
130
  margin: 0;
@@ -151,24 +172,101 @@ export class RoxySynastryChart extends LitElement {
151
172
  padding-left: var(--roxy-space-md, 1rem);
152
173
  font-size: var(--roxy-text-sm, 0.875rem);
153
174
  }
175
+
176
+ .missing-planets {
177
+ background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 8%, transparent);
178
+ border: 1px solid var(--roxy-border, #e4e4e7);
179
+ border-radius: var(--roxy-radius-md, 8px);
180
+ padding: var(--roxy-space-md, 1rem);
181
+ color: var(--roxy-fg, #0a0a0a);
182
+ font-size: var(--roxy-text-sm, 0.875rem);
183
+ line-height: 1.5;
184
+ }
185
+ .missing-planets code {
186
+ font-family: var(--roxy-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
187
+ font-size: 0.95em;
188
+ background: color-mix(in srgb, var(--roxy-fg, #0a0a0a) 6%, transparent);
189
+ padding: 0 4px;
190
+ border-radius: 4px;
191
+ }
154
192
  `,
155
193
  ];
156
194
 
157
195
  @property({ attribute: false })
158
- data: SynastryData | null = null;
196
+ data: SynastryWithPlanets | null = null;
159
197
 
160
198
  render() {
161
199
  if (!this.data)
162
200
  return html`<div class="roxy-empty" role="status">No synastry data</div>`;
163
- const {
164
- person1,
165
- person2,
166
- compatibilityScore,
167
- summary,
168
- interAspects = [],
169
- } = this.data;
170
- const p1Planets = this.normalizePlanets(person1?.planets);
171
- const p2Planets = this.normalizePlanets(person2?.planets);
201
+ const { person1, person2, compatibilityScore, analysis } = this.data;
202
+ const interAspects = this.data.interAspects ?? [];
203
+ const p1Planets = person1?.planets ?? [];
204
+ const p2Planets = person2?.planets ?? [];
205
+
206
+ const score =
207
+ typeof compatibilityScore === 'number'
208
+ ? Math.round(compatibilityScore)
209
+ : undefined;
210
+ const summaryText = analysis?.overall;
211
+ const strengths = analysis?.strengths ?? [];
212
+ const challenges = analysis?.challenges ?? [];
213
+
214
+ // /astrology/synastry does not return per-person planet positions, so the
215
+ // dual-wheel cannot be drawn from a bare synastry response. Surface this
216
+ // explicitly instead of rendering a blank wheel; keep the inter-aspects
217
+ // table when it is present so callers still get useful output.
218
+ const hasPlanets = p1Planets.length > 0 && p2Planets.length > 0;
219
+ if (!hasPlanets) {
220
+ return html`<div
221
+ class="wrap"
222
+ aria-label="Synastry compatibility chart"
223
+ >
224
+ <div class="head">
225
+ <h2 class="title">Synastry</h2>
226
+ ${
227
+ typeof score === 'number'
228
+ ? html`<span class="score" aria-label=${`Score ${score} of 100`}
229
+ >${score} / 100</span
230
+ >`
231
+ : nothing
232
+ }
233
+ </div>
234
+ <div class="missing-planets" role="status">
235
+ Synastry response missing planet positions. Pass
236
+ <code>data</code> with <code>person1.planets</code> and
237
+ <code>person2.planets</code> arrays from the natal-chart endpoint, or
238
+ use the <code>&lt;roxy-data&gt;</code> fallback.
239
+ </div>
240
+ ${summaryText ? html`<p class="summary">${summaryText}</p>` : nothing}
241
+ ${interAspects.length > 0 ? this.renderAspects(interAspects) : nothing}
242
+ ${
243
+ strengths.length > 0 || challenges.length > 0
244
+ ? html`<div class="lists">
245
+ ${
246
+ strengths.length
247
+ ? html`<div>
248
+ <h3>Strengths</h3>
249
+ <ul>
250
+ ${strengths.map((s) => html`<li>${s}</li>`)}
251
+ </ul>
252
+ </div>`
253
+ : nothing
254
+ }
255
+ ${
256
+ challenges.length
257
+ ? html`<div>
258
+ <h3>Challenges</h3>
259
+ <ul>
260
+ ${challenges.map((s) => html`<li>${s}</li>`)}
261
+ </ul>
262
+ </div>`
263
+ : nothing
264
+ }
265
+ </div>`
266
+ : nothing
267
+ }
268
+ </div>`;
269
+ }
172
270
 
173
271
  return html`<div
174
272
  class="wrap"
@@ -177,9 +275,9 @@ export class RoxySynastryChart extends LitElement {
177
275
  <div class="head">
178
276
  <h2 class="title">Synastry</h2>
179
277
  ${
180
- typeof compatibilityScore === 'number'
181
- ? html`<span class="score" aria-label=${`Score ${compatibilityScore} of 100`}
182
- >${compatibilityScore} / 100</span
278
+ typeof score === 'number'
279
+ ? html`<span class="score" aria-label=${`Score ${score} of 100`}
280
+ >${score} / 100</span
183
281
  >`
184
282
  : nothing
185
283
  }
@@ -212,30 +310,36 @@ export class RoxySynastryChart extends LitElement {
212
310
  stroke-width="0.6"
213
311
  />
214
312
  ${this.renderSpokes()} ${this.renderSigns()}
313
+ ${this.renderInterAspectLines(p1Planets, p2Planets, interAspects)}
215
314
  ${this.renderRing(p1Planets, P1_R, 'p1')} ${this.renderRing(p2Planets, P2_R, 'p2')}
216
315
  </svg>
217
- ${summary ? html`<p class="summary">${summary}</p>` : nothing}
316
+ <div class="legend-row">
317
+ <span><span class="swatch" style="background: var(--roxy-accent)"></span>Person 1</span>
318
+ <span><span class="swatch" style="background: var(--roxy-info)"></span>Person 2</span>
319
+ <span><span class="swatch" style="background: var(--roxy-success)"></span>harmonious</span>
320
+ <span><span class="swatch" style="background: var(--roxy-danger)"></span>challenging</span>
321
+ </div>
322
+ ${summaryText ? html`<p class="summary">${summaryText}</p>` : nothing}
218
323
  ${interAspects.length > 0 ? this.renderAspects(interAspects) : nothing}
219
324
  ${
220
- (this.data.strengths?.length ?? 0) > 0 ||
221
- (this.data.challenges?.length ?? 0) > 0
325
+ strengths.length > 0 || challenges.length > 0
222
326
  ? html`<div class="lists">
223
327
  ${
224
- this.data.strengths?.length
328
+ strengths.length
225
329
  ? html`<div>
226
330
  <h3>Strengths</h3>
227
331
  <ul>
228
- ${this.data.strengths.map((s) => html`<li>${s}</li>`)}
332
+ ${strengths.map((s) => html`<li>${s}</li>`)}
229
333
  </ul>
230
334
  </div>`
231
335
  : nothing
232
336
  }
233
337
  ${
234
- this.data.challenges?.length
338
+ challenges.length
235
339
  ? html`<div>
236
340
  <h3>Challenges</h3>
237
341
  <ul>
238
- ${this.data.challenges.map((s) => html`<li>${s}</li>`)}
342
+ ${challenges.map((s) => html`<li>${s}</li>`)}
239
343
  </ul>
240
344
  </div>`
241
345
  : nothing
@@ -246,17 +350,13 @@ export class RoxySynastryChart extends LitElement {
246
350
  </div>`;
247
351
  }
248
352
 
249
- private normalizePlanets(
250
- p: PlanetEntry[] | Record<string, PlanetEntry> | undefined,
251
- ) {
252
- if (!p) return [];
253
- if (Array.isArray(p)) return p;
254
- return Object.entries(p).map(([name, e]) => ({ ...e, name }));
353
+ private toAngle(longitude: number): number {
354
+ return 180 - longitude;
255
355
  }
256
356
 
257
357
  private renderSpokes() {
258
358
  return Array.from({ length: 12 }, (_, i) => {
259
- const angle = i * 30 - 90;
359
+ const angle = this.toAngle(i * 30);
260
360
  const start = polarToCartesian(CENTER, CENTER, P2_R - 14, angle);
261
361
  const end = polarToCartesian(CENTER, CENTER, OUTER_R, angle);
262
362
  return svg`<line class="wheel-line" x1=${start.x} y1=${start.y} x2=${end.x} y2=${end.y} stroke-width="0.6" />`;
@@ -279,7 +379,7 @@ export class RoxySynastryChart extends LitElement {
279
379
  'Pisces',
280
380
  ];
281
381
  return order.map((s, i) => {
282
- const angle = i * 30 + 15 - 90;
382
+ const angle = this.toAngle(i * 30 + 15);
283
383
  const pos = polarToCartesian(CENTER, CENTER, SIGN_R, angle);
284
384
  return svg`<text class="sign" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central">${SIGN_GLYPH[s]}</text>`;
285
385
  });
@@ -287,17 +387,44 @@ export class RoxySynastryChart extends LitElement {
287
387
 
288
388
  private renderRing(planets: PlanetEntry[], radius: number, cls: string) {
289
389
  return planets.map((p) => {
290
- const lon =
291
- typeof p.longitude === 'number'
292
- ? p.longitude
293
- : typeof p.degree === 'number'
294
- ? p.degree
295
- : NaN;
296
- if (!Number.isFinite(lon)) return nothing;
297
- const pos = polarToCartesian(CENTER, CENTER, radius, lon - 90);
298
- const name = p.name ?? p.planet ?? '';
299
- const glyph = PLANET_GLYPH[capitalize(name)] ?? name.slice(0, 2);
300
- return svg`<text class=${cls} x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central"><title>${name}</title>${glyph}</text>`;
390
+ if (!Number.isFinite(p.longitude)) return nothing;
391
+ const pos = polarToCartesian(
392
+ CENTER,
393
+ CENTER,
394
+ radius,
395
+ this.toAngle(p.longitude),
396
+ );
397
+ const glyph = PLANET_GLYPH[capitalize(p.name)] ?? p.name.slice(0, 2);
398
+ return svg`<text class=${cls} x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central"><title>${p.name}</title>${glyph}</text>`;
399
+ });
400
+ }
401
+
402
+ private renderInterAspectLines(
403
+ p1: PlanetEntry[],
404
+ p2: PlanetEntry[],
405
+ aspects: InterAspect[],
406
+ ) {
407
+ const longitudeOf = (
408
+ list: PlanetEntry[],
409
+ name: string,
410
+ ): number | undefined => {
411
+ const target = capitalize(name);
412
+ for (const p of list) {
413
+ if (capitalize(p.name) !== target) continue;
414
+ if (typeof p.longitude === 'number') return p.longitude;
415
+ }
416
+ return undefined;
417
+ };
418
+ return aspects.map((a) => {
419
+ const l1 = longitudeOf(p1, a.planet1);
420
+ const l2 = longitudeOf(p2, a.planet2);
421
+ if (l1 === undefined || l2 === undefined) return nothing;
422
+ const out = polarToCartesian(CENTER, CENTER, P1_R - 12, this.toAngle(l1));
423
+ const inn = polarToCartesian(CENTER, CENTER, P2_R + 8, this.toAngle(l2));
424
+ const aspectName = normalizeAspect(a);
425
+ const cls = ASPECT_CLASS[aspectName] ?? 'aspect-other';
426
+ const orbLabel = formatNumber(a.orb, 1);
427
+ return svg`<line class=${`aspect ${cls}`} x1=${out.x} y1=${out.y} x2=${inn.x} y2=${inn.y}><title>${a.planet1} ${aspectName} ${a.planet2}${orbLabel ? ` (orb ${orbLabel}°)` : ''}</title></line>`;
301
428
  });
302
429
  }
303
430
 
@@ -313,15 +440,13 @@ export class RoxySynastryChart extends LitElement {
313
440
  </tr>
314
441
  </thead>
315
442
  <tbody>
316
- ${aspects.slice(0, 16).map(
443
+ ${aspects.slice(0, 12).map(
317
444
  (a) => html`<tr>
318
- <td>${a.planet1 ?? ''}</td>
319
- <td>${a.planet2 ?? ''}</td>
320
- <td>${a.aspect ?? ''}</td>
321
- <td class="orb">
322
- ${typeof a.orb === 'number' ? a.orb.toFixed(1) : ''}
323
- </td>
324
- <td>${a.strength ?? ''}</td>
445
+ <td>${a.planet1}</td>
446
+ <td>${a.planet2}</td>
447
+ <td>${normalizeAspect(a) || ''}</td>
448
+ <td class="orb">${formatNumber(a.orb, 1)}</td>
449
+ <td>${formatStrength(a.strength)}</td>
325
450
  </tr>`,
326
451
  )}
327
452
  </tbody>
@@ -334,6 +459,23 @@ function capitalize(s: string): string {
334
459
  return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
335
460
  }
336
461
 
462
+ const ASPECT_CLASS: Record<string, string> = {
463
+ conjunction: 'aspect-conjunction',
464
+ sextile: 'aspect-sextile',
465
+ square: 'aspect-square',
466
+ trine: 'aspect-trine',
467
+ opposition: 'aspect-opposition',
468
+ };
469
+
470
+ function normalizeAspect(a: InterAspect): string {
471
+ return (a.type ?? '').toLowerCase().replace(/_/g, '-');
472
+ }
473
+
474
+ function formatStrength(s: number | undefined): string {
475
+ if (typeof s === 'number') return Math.round(s).toString();
476
+ return '';
477
+ }
478
+
337
479
  declare global {
338
480
  interface HTMLElementTagNameMap {
339
481
  'roxy-synastry-chart': RoxySynastryChart;