@roxyapi/ui 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/AGENTS.md +6 -0
  2. package/README.md +327 -14
  3. package/THEMING.md +24 -7
  4. package/dist/cdn/components/ashtakavarga-grid.js +349 -0
  5. package/dist/cdn/components/ashtakavarga-grid.js.map +7 -0
  6. package/dist/cdn/components/biorhythm-chart.js +15 -22
  7. package/dist/cdn/components/biorhythm-chart.js.map +3 -3
  8. package/dist/cdn/components/choghadiya-grid.js +239 -0
  9. package/dist/cdn/components/choghadiya-grid.js.map +7 -0
  10. package/dist/cdn/components/compatibility-card.js +36 -34
  11. package/dist/cdn/components/compatibility-card.js.map +4 -4
  12. package/dist/cdn/components/dasha-timeline.js +35 -39
  13. package/dist/cdn/components/dasha-timeline.js.map +4 -4
  14. package/dist/cdn/components/data.js +9 -9
  15. package/dist/cdn/components/data.js.map +4 -4
  16. package/dist/cdn/components/divisional-chart.js +279 -0
  17. package/dist/cdn/components/divisional-chart.js.map +7 -0
  18. package/dist/cdn/components/dosha-card.js +49 -49
  19. package/dist/cdn/components/dosha-card.js.map +3 -3
  20. package/dist/cdn/components/endpoint-form.js +47 -28
  21. package/dist/cdn/components/endpoint-form.js.map +4 -4
  22. package/dist/cdn/components/guna-milan.js +66 -24
  23. package/dist/cdn/components/guna-milan.js.map +4 -4
  24. package/dist/cdn/components/hexagram.js +26 -26
  25. package/dist/cdn/components/hexagram.js.map +3 -3
  26. package/dist/cdn/components/horoscope-card.js +47 -40
  27. package/dist/cdn/components/horoscope-card.js.map +4 -4
  28. package/dist/cdn/components/kp-planets-table.js +10 -10
  29. package/dist/cdn/components/kp-planets-table.js.map +4 -4
  30. package/dist/cdn/components/location-search.js +6 -6
  31. package/dist/cdn/components/location-search.js.map +3 -3
  32. package/dist/cdn/components/moon-phase.js +18 -18
  33. package/dist/cdn/components/moon-phase.js.map +4 -4
  34. package/dist/cdn/components/natal-chart.js +240 -24
  35. package/dist/cdn/components/natal-chart.js.map +4 -4
  36. package/dist/cdn/components/numerology-card.js +40 -31
  37. package/dist/cdn/components/numerology-card.js.map +4 -4
  38. package/dist/cdn/components/panchang-table.js +30 -30
  39. package/dist/cdn/components/panchang-table.js.map +4 -4
  40. package/dist/cdn/components/shadbala-table.js +312 -0
  41. package/dist/cdn/components/shadbala-table.js.map +7 -0
  42. package/dist/cdn/components/synastry-chart.js +129 -39
  43. package/dist/cdn/components/synastry-chart.js.map +4 -4
  44. package/dist/cdn/components/tarot-card.js +49 -20
  45. package/dist/cdn/components/tarot-card.js.map +3 -3
  46. package/dist/cdn/components/tarot-spread.js +43 -27
  47. package/dist/cdn/components/tarot-spread.js.map +3 -3
  48. package/dist/cdn/components/transits-table.js +391 -0
  49. package/dist/cdn/components/transits-table.js.map +7 -0
  50. package/dist/cdn/components/vedic-kundli.js +63 -27
  51. package/dist/cdn/components/vedic-kundli.js.map +4 -4
  52. package/dist/cdn/components/yoga-list.js +334 -0
  53. package/dist/cdn/components/yoga-list.js.map +7 -0
  54. package/dist/cdn/roxy-ui.js +2104 -544
  55. package/dist/cdn/roxy-ui.js.map +4 -4
  56. package/dist/components/ashtakavarga-grid.d.ts +26 -0
  57. package/dist/components/ashtakavarga-grid.d.ts.map +1 -0
  58. package/dist/components/ashtakavarga-grid.js +457 -0
  59. package/dist/components/ashtakavarga-grid.js.map +7 -0
  60. package/dist/components/biorhythm-chart.d.ts +2 -46
  61. package/dist/components/biorhythm-chart.d.ts.map +1 -1
  62. package/dist/components/biorhythm-chart.js +24 -23
  63. package/dist/components/biorhythm-chart.js.map +2 -2
  64. package/dist/components/choghadiya-grid.d.ts +19 -0
  65. package/dist/components/choghadiya-grid.d.ts.map +1 -0
  66. package/dist/components/choghadiya-grid.js +304 -0
  67. package/dist/components/choghadiya-grid.js.map +7 -0
  68. package/dist/components/compatibility-card.d.ts +2 -27
  69. package/dist/components/compatibility-card.d.ts.map +1 -1
  70. package/dist/components/compatibility-card.js +50 -29
  71. package/dist/components/compatibility-card.js.map +3 -3
  72. package/dist/components/dasha-timeline.d.ts +2 -31
  73. package/dist/components/dasha-timeline.d.ts.map +1 -1
  74. package/dist/components/dasha-timeline.js +32 -30
  75. package/dist/components/dasha-timeline.js.map +3 -3
  76. package/dist/components/data.d.ts +11 -7
  77. package/dist/components/data.d.ts.map +1 -1
  78. package/dist/components/data.js +16 -6
  79. package/dist/components/data.js.map +3 -3
  80. package/dist/components/divisional-chart.d.ts +20 -0
  81. package/dist/components/divisional-chart.d.ts.map +1 -0
  82. package/dist/components/divisional-chart.js +471 -0
  83. package/dist/components/divisional-chart.js.map +7 -0
  84. package/dist/components/dosha-card.d.ts +2 -16
  85. package/dist/components/dosha-card.d.ts.map +1 -1
  86. package/dist/components/dosha-card.js +45 -43
  87. package/dist/components/dosha-card.js.map +2 -2
  88. package/dist/components/endpoint-form.d.ts +2 -0
  89. package/dist/components/endpoint-form.d.ts.map +1 -1
  90. package/dist/components/endpoint-form.js +71 -11
  91. package/dist/components/endpoint-form.js.map +3 -3
  92. package/dist/components/guna-milan.d.ts +2 -20
  93. package/dist/components/guna-milan.d.ts.map +1 -1
  94. package/dist/components/guna-milan.js +79 -20
  95. package/dist/components/guna-milan.js.map +4 -4
  96. package/dist/components/hexagram.d.ts +3 -27
  97. package/dist/components/hexagram.d.ts.map +1 -1
  98. package/dist/components/hexagram.js +48 -15
  99. package/dist/components/hexagram.js.map +2 -2
  100. package/dist/components/horoscope-card.d.ts +2 -20
  101. package/dist/components/horoscope-card.d.ts.map +1 -1
  102. package/dist/components/horoscope-card.js +54 -18
  103. package/dist/components/horoscope-card.js.map +3 -3
  104. package/dist/components/kp-planets-table.d.ts +2 -21
  105. package/dist/components/kp-planets-table.d.ts.map +1 -1
  106. package/dist/components/kp-planets-table.js +10 -4
  107. package/dist/components/kp-planets-table.js.map +3 -3
  108. package/dist/components/location-search.d.ts +5 -14
  109. package/dist/components/location-search.d.ts.map +1 -1
  110. package/dist/components/location-search.js +45 -5
  111. package/dist/components/location-search.js.map +2 -2
  112. package/dist/components/moon-phase.d.ts +4 -21
  113. package/dist/components/moon-phase.d.ts.map +1 -1
  114. package/dist/components/moon-phase.js +34 -4
  115. package/dist/components/moon-phase.js.map +3 -3
  116. package/dist/components/natal-chart.d.ts +9 -43
  117. package/dist/components/natal-chart.d.ts.map +1 -1
  118. package/dist/components/natal-chart.js +346 -79
  119. package/dist/components/natal-chart.js.map +3 -3
  120. package/dist/components/numerology-card.d.ts +5 -37
  121. package/dist/components/numerology-card.d.ts.map +1 -1
  122. package/dist/components/numerology-card.js +58 -30
  123. package/dist/components/numerology-card.js.map +3 -3
  124. package/dist/components/panchang-table.d.ts +3 -62
  125. package/dist/components/panchang-table.d.ts.map +1 -1
  126. package/dist/components/panchang-table.js +62 -32
  127. package/dist/components/panchang-table.js.map +3 -3
  128. package/dist/components/shadbala-table.d.ts +18 -0
  129. package/dist/components/shadbala-table.d.ts.map +1 -0
  130. package/dist/components/shadbala-table.js +400 -0
  131. package/dist/components/shadbala-table.js.map +7 -0
  132. package/dist/components/synastry-chart.d.ts +9 -28
  133. package/dist/components/synastry-chart.d.ts.map +1 -1
  134. package/dist/components/synastry-chart.js +201 -56
  135. package/dist/components/synastry-chart.js.map +3 -3
  136. package/dist/components/tarot-card.d.ts +5 -29
  137. package/dist/components/tarot-card.d.ts.map +1 -1
  138. package/dist/components/tarot-card.js +59 -20
  139. package/dist/components/tarot-card.js.map +2 -2
  140. package/dist/components/tarot-spread.d.ts +2 -24
  141. package/dist/components/tarot-spread.d.ts.map +1 -1
  142. package/dist/components/tarot-spread.js +39 -13
  143. package/dist/components/tarot-spread.js.map +2 -2
  144. package/dist/components/transits-table.d.ts +21 -0
  145. package/dist/components/transits-table.d.ts.map +1 -0
  146. package/dist/components/transits-table.js +515 -0
  147. package/dist/components/transits-table.js.map +7 -0
  148. package/dist/components/vedic-kundli.d.ts +5 -28
  149. package/dist/components/vedic-kundli.d.ts.map +1 -1
  150. package/dist/components/vedic-kundli.js +147 -83
  151. package/dist/components/vedic-kundli.js.map +3 -3
  152. package/dist/components/yoga-list.d.ts +29 -0
  153. package/dist/components/yoga-list.d.ts.map +1 -0
  154. package/dist/components/yoga-list.js +389 -0
  155. package/dist/components/yoga-list.js.map +7 -0
  156. package/dist/index.cjs +3693 -1180
  157. package/dist/index.cjs.map +4 -4
  158. package/dist/index.d.ts +11 -4
  159. package/dist/index.d.ts.map +1 -1
  160. package/dist/index.js +3709 -1196
  161. package/dist/index.js.map +4 -4
  162. package/dist/manifest.d.ts +43 -0
  163. package/dist/manifest.d.ts.map +1 -0
  164. package/dist/manifest.json +7 -2
  165. package/dist/styles/tokens.css +73 -1
  166. package/dist/tokens/index.d.ts +6 -0
  167. package/dist/tokens/index.d.ts.map +1 -1
  168. package/dist/types/index.d.ts +2 -0
  169. package/dist/types/index.d.ts.map +1 -0
  170. package/dist/types/types.gen.d.ts +27811 -0
  171. package/dist/types/types.gen.d.ts.map +1 -0
  172. package/dist/utils/debounce.d.ts +9 -1
  173. package/dist/utils/debounce.d.ts.map +1 -1
  174. package/dist/utils/format.d.ts +29 -0
  175. package/dist/utils/format.d.ts.map +1 -0
  176. package/dist/utils/kundli-render.d.ts +63 -0
  177. package/dist/utils/kundli-render.d.ts.map +1 -0
  178. package/dist/utils/string.d.ts +14 -0
  179. package/dist/utils/string.d.ts.map +1 -0
  180. package/dist/version.d.ts +2 -0
  181. package/dist/version.d.ts.map +1 -0
  182. package/package.json +7 -1
  183. package/src/components/ashtakavarga-grid.ts +354 -0
  184. package/src/components/biorhythm-chart.ts +39 -84
  185. package/src/components/choghadiya-grid.ts +185 -0
  186. package/src/components/compatibility-card.ts +85 -52
  187. package/src/components/dasha-timeline.ts +55 -73
  188. package/src/components/data.ts +28 -16
  189. package/src/components/divisional-chart.ts +214 -0
  190. package/src/components/dosha-card.ts +72 -68
  191. package/src/components/endpoint-form.ts +80 -18
  192. package/src/components/guna-milan.ts +87 -47
  193. package/src/components/hexagram.ts +53 -43
  194. package/src/components/horoscope-card.ts +59 -43
  195. package/src/components/kp-planets-table.ts +8 -27
  196. package/src/components/location-search.ts +47 -23
  197. package/src/components/moon-phase.ts +28 -25
  198. package/src/components/natal-chart.ts +364 -110
  199. package/src/components/numerology-card.ts +86 -84
  200. package/src/components/panchang-table.ts +40 -78
  201. package/src/components/shadbala-table.ts +286 -0
  202. package/src/components/synastry-chart.ts +213 -97
  203. package/src/components/tarot-card.ts +76 -62
  204. package/src/components/tarot-spread.ts +72 -45
  205. package/src/components/transits-table.ts +350 -0
  206. package/src/components/vedic-kundli.ts +59 -173
  207. package/src/components/yoga-list.ts +328 -0
  208. package/src/index.ts +18 -26
  209. package/src/manifest.ts +340 -0
  210. package/src/styles/tokens.css +73 -1
  211. package/src/tokens/index.ts +14 -0
  212. package/src/types/types.gen.ts +3 -3
  213. package/src/utils/debounce.ts +23 -4
  214. package/src/utils/format.ts +75 -0
  215. package/src/utils/kundli-render.ts +197 -0
  216. package/src/utils/string.ts +23 -0
  217. package/src/version.ts +2 -0
  218. package/dist/utils/motion.d.ts +0 -13
  219. package/dist/utils/motion.d.ts.map +0 -1
  220. package/src/utils/motion.ts +0 -18
@@ -1,41 +1,31 @@
1
1
  import { css, html, LitElement, nothing, svg } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
- import { PLANET_GLYPH, SIGN_GLYPH } from '../tokens/index.js';
3
+ import { PLANET_GLYPH, SIGN_GLYPH, SIGNS_ORDER } 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 {
11
+ ASPECT_CLASS,
12
+ formatNumber,
13
+ normalizeAspect,
14
+ } from '../utils/format.js';
15
+ import { capitalize } from '../utils/string.js';
6
16
 
7
- interface PlanetEntry {
8
- name?: string;
9
- planet?: string;
10
- longitude?: number;
11
- degree?: number;
12
- sign?: string;
13
- }
17
+ type PlanetEntry = NatalChartResponse['planets'][number];
18
+ type InterAspect = CalculateSynastryResponse['interAspects'][number];
14
19
 
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
- }
20
+ // Drawing the dual wheel requires per-person planet longitudes alongside
21
+ // the synastry response. Callers can merge planet arrays from
22
+ // /astrology/natal-chart into `person1.planets` and `person2.planets`
23
+ // before passing the payload in; without them, the component falls back
24
+ // to the inter-aspects table and a status note instead of an empty wheel.
25
+ type SynastryWithPlanets = CalculateSynastryResponse & {
26
+ person1?: { planets?: PlanetEntry[] };
27
+ person2?: { planets?: PlanetEntry[] };
28
+ };
39
29
 
40
30
  const SIZE = 360;
41
31
  const CENTER = SIZE / 2;
@@ -104,6 +94,42 @@ export class RoxySynastryChart extends LitElement {
104
94
  font-weight: 600;
105
95
  font-size: 13px;
106
96
  }
97
+ .aspect {
98
+ stroke-width: 0.8;
99
+ fill: none;
100
+ opacity: 0.5;
101
+ }
102
+ .aspect-trine,
103
+ .aspect-sextile {
104
+ stroke: var(--roxy-success, #16a34a);
105
+ }
106
+ .aspect-square,
107
+ .aspect-opposition {
108
+ stroke: var(--roxy-danger, #dc2626);
109
+ }
110
+ .aspect-conjunction {
111
+ stroke: var(--roxy-accent-fg, #b45309);
112
+ }
113
+ .aspect-other {
114
+ stroke: var(--roxy-muted, #71717a);
115
+ opacity: 0.35;
116
+ }
117
+ .legend-row {
118
+ display: flex;
119
+ flex-wrap: wrap;
120
+ gap: var(--roxy-space-md, 1rem);
121
+ font-size: var(--roxy-text-xs, 0.75rem);
122
+ color: var(--roxy-muted, #71717a);
123
+ margin-top: calc(var(--roxy-space-xs, 0.25rem) * -1);
124
+ }
125
+ .legend-row .swatch {
126
+ display: inline-block;
127
+ width: 8px;
128
+ height: 8px;
129
+ border-radius: 50%;
130
+ margin-right: 4px;
131
+ vertical-align: middle;
132
+ }
107
133
 
108
134
  .summary {
109
135
  margin: 0;
@@ -151,24 +177,101 @@ export class RoxySynastryChart extends LitElement {
151
177
  padding-left: var(--roxy-space-md, 1rem);
152
178
  font-size: var(--roxy-text-sm, 0.875rem);
153
179
  }
180
+
181
+ .missing-planets {
182
+ background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 8%, transparent);
183
+ border: 1px solid var(--roxy-border, #e4e4e7);
184
+ border-radius: var(--roxy-radius-md, 8px);
185
+ padding: var(--roxy-space-md, 1rem);
186
+ color: var(--roxy-fg, #0a0a0a);
187
+ font-size: var(--roxy-text-sm, 0.875rem);
188
+ line-height: 1.5;
189
+ }
190
+ .missing-planets code {
191
+ font-family: var(--roxy-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
192
+ font-size: 0.95em;
193
+ background: color-mix(in srgb, var(--roxy-fg, #0a0a0a) 6%, transparent);
194
+ padding: 0 4px;
195
+ border-radius: 4px;
196
+ }
154
197
  `,
155
198
  ];
156
199
 
157
200
  @property({ attribute: false })
158
- data: SynastryData | null = null;
201
+ data: SynastryWithPlanets | null = null;
159
202
 
160
203
  render() {
161
204
  if (!this.data)
162
205
  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);
206
+ const { person1, person2, compatibilityScore, analysis } = this.data;
207
+ const interAspects = this.data.interAspects ?? [];
208
+ const p1Planets = person1?.planets ?? [];
209
+ const p2Planets = person2?.planets ?? [];
210
+
211
+ const score =
212
+ typeof compatibilityScore === 'number'
213
+ ? Math.round(compatibilityScore)
214
+ : undefined;
215
+ const summaryText = analysis?.overall;
216
+ const strengths = analysis?.strengths ?? [];
217
+ const challenges = analysis?.challenges ?? [];
218
+
219
+ // /astrology/synastry does not return per-person planet positions, so the
220
+ // dual-wheel cannot be drawn from a bare synastry response. Surface this
221
+ // explicitly instead of rendering a blank wheel; keep the inter-aspects
222
+ // table when it is present so callers still get useful output.
223
+ const hasPlanets = p1Planets.length > 0 && p2Planets.length > 0;
224
+ if (!hasPlanets) {
225
+ return html`<div
226
+ class="wrap"
227
+ aria-label="Synastry compatibility chart"
228
+ >
229
+ <div class="head">
230
+ <h2 class="title">Synastry</h2>
231
+ ${
232
+ typeof score === 'number'
233
+ ? html`<span class="score" aria-label=${`Score ${score} of 100`}
234
+ >${score} / 100</span
235
+ >`
236
+ : nothing
237
+ }
238
+ </div>
239
+ <div class="missing-planets" role="status">
240
+ Synastry response missing planet positions. Pass
241
+ <code>data</code> with <code>person1.planets</code> and
242
+ <code>person2.planets</code> arrays from the natal-chart endpoint, or
243
+ use the <code>&lt;roxy-data&gt;</code> fallback.
244
+ </div>
245
+ ${summaryText ? html`<p class="summary">${summaryText}</p>` : nothing}
246
+ ${interAspects.length > 0 ? this.renderAspects(interAspects) : nothing}
247
+ ${
248
+ strengths.length > 0 || challenges.length > 0
249
+ ? html`<div class="lists">
250
+ ${
251
+ strengths.length
252
+ ? html`<div>
253
+ <h3>Strengths</h3>
254
+ <ul>
255
+ ${strengths.map((s) => html`<li>${s}</li>`)}
256
+ </ul>
257
+ </div>`
258
+ : nothing
259
+ }
260
+ ${
261
+ challenges.length
262
+ ? html`<div>
263
+ <h3>Challenges</h3>
264
+ <ul>
265
+ ${challenges.map((s) => html`<li>${s}</li>`)}
266
+ </ul>
267
+ </div>`
268
+ : nothing
269
+ }
270
+ </div>`
271
+ : nothing
272
+ }
273
+ </div>`;
274
+ }
172
275
 
173
276
  return html`<div
174
277
  class="wrap"
@@ -177,9 +280,9 @@ export class RoxySynastryChart extends LitElement {
177
280
  <div class="head">
178
281
  <h2 class="title">Synastry</h2>
179
282
  ${
180
- typeof compatibilityScore === 'number'
181
- ? html`<span class="score" aria-label=${`Score ${compatibilityScore} of 100`}
182
- >${compatibilityScore} / 100</span
283
+ typeof score === 'number'
284
+ ? html`<span class="score" aria-label=${`Score ${score} of 100`}
285
+ >${score} / 100</span
183
286
  >`
184
287
  : nothing
185
288
  }
@@ -212,30 +315,36 @@ export class RoxySynastryChart extends LitElement {
212
315
  stroke-width="0.6"
213
316
  />
214
317
  ${this.renderSpokes()} ${this.renderSigns()}
318
+ ${this.renderInterAspectLines(p1Planets, p2Planets, interAspects)}
215
319
  ${this.renderRing(p1Planets, P1_R, 'p1')} ${this.renderRing(p2Planets, P2_R, 'p2')}
216
320
  </svg>
217
- ${summary ? html`<p class="summary">${summary}</p>` : nothing}
321
+ <div class="legend-row">
322
+ <span><span class="swatch" style="background: var(--roxy-accent)"></span>Person 1</span>
323
+ <span><span class="swatch" style="background: var(--roxy-info)"></span>Person 2</span>
324
+ <span><span class="swatch" style="background: var(--roxy-success)"></span>harmonious</span>
325
+ <span><span class="swatch" style="background: var(--roxy-danger)"></span>challenging</span>
326
+ </div>
327
+ ${summaryText ? html`<p class="summary">${summaryText}</p>` : nothing}
218
328
  ${interAspects.length > 0 ? this.renderAspects(interAspects) : nothing}
219
329
  ${
220
- (this.data.strengths?.length ?? 0) > 0 ||
221
- (this.data.challenges?.length ?? 0) > 0
330
+ strengths.length > 0 || challenges.length > 0
222
331
  ? html`<div class="lists">
223
332
  ${
224
- this.data.strengths?.length
333
+ strengths.length
225
334
  ? html`<div>
226
335
  <h3>Strengths</h3>
227
336
  <ul>
228
- ${this.data.strengths.map((s) => html`<li>${s}</li>`)}
337
+ ${strengths.map((s) => html`<li>${s}</li>`)}
229
338
  </ul>
230
339
  </div>`
231
340
  : nothing
232
341
  }
233
342
  ${
234
- this.data.challenges?.length
343
+ challenges.length
235
344
  ? html`<div>
236
345
  <h3>Challenges</h3>
237
346
  <ul>
238
- ${this.data.challenges.map((s) => html`<li>${s}</li>`)}
347
+ ${challenges.map((s) => html`<li>${s}</li>`)}
239
348
  </ul>
240
349
  </div>`
241
350
  : nothing
@@ -246,17 +355,13 @@ export class RoxySynastryChart extends LitElement {
246
355
  </div>`;
247
356
  }
248
357
 
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 }));
358
+ private toAngle(longitude: number): number {
359
+ return 180 - longitude;
255
360
  }
256
361
 
257
362
  private renderSpokes() {
258
363
  return Array.from({ length: 12 }, (_, i) => {
259
- const angle = i * 30 - 90;
364
+ const angle = this.toAngle(i * 30);
260
365
  const start = polarToCartesian(CENTER, CENTER, P2_R - 14, angle);
261
366
  const end = polarToCartesian(CENTER, CENTER, OUTER_R, angle);
262
367
  return svg`<line class="wheel-line" x1=${start.x} y1=${start.y} x2=${end.x} y2=${end.y} stroke-width="0.6" />`;
@@ -264,22 +369,8 @@ export class RoxySynastryChart extends LitElement {
264
369
  }
265
370
 
266
371
  private renderSigns() {
267
- const order = [
268
- 'Aries',
269
- 'Taurus',
270
- 'Gemini',
271
- 'Cancer',
272
- 'Leo',
273
- 'Virgo',
274
- 'Libra',
275
- 'Scorpio',
276
- 'Sagittarius',
277
- 'Capricorn',
278
- 'Aquarius',
279
- 'Pisces',
280
- ];
281
- return order.map((s, i) => {
282
- const angle = i * 30 + 15 - 90;
372
+ return SIGNS_ORDER.map((s, i) => {
373
+ const angle = this.toAngle(i * 30 + 15);
283
374
  const pos = polarToCartesian(CENTER, CENTER, SIGN_R, angle);
284
375
  return svg`<text class="sign" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central">${SIGN_GLYPH[s]}</text>`;
285
376
  });
@@ -287,17 +378,44 @@ export class RoxySynastryChart extends LitElement {
287
378
 
288
379
  private renderRing(planets: PlanetEntry[], radius: number, cls: string) {
289
380
  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>`;
381
+ if (!Number.isFinite(p.longitude)) return nothing;
382
+ const pos = polarToCartesian(
383
+ CENTER,
384
+ CENTER,
385
+ radius,
386
+ this.toAngle(p.longitude),
387
+ );
388
+ const glyph = PLANET_GLYPH[capitalize(p.name)] ?? p.name.slice(0, 2);
389
+ return svg`<text class=${cls} x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central"><title>${p.name}</title>${glyph}</text>`;
390
+ });
391
+ }
392
+
393
+ private renderInterAspectLines(
394
+ p1: PlanetEntry[],
395
+ p2: PlanetEntry[],
396
+ aspects: InterAspect[],
397
+ ) {
398
+ const longitudeOf = (
399
+ list: PlanetEntry[],
400
+ name: string,
401
+ ): number | undefined => {
402
+ const target = capitalize(name);
403
+ for (const p of list) {
404
+ if (capitalize(p.name) !== target) continue;
405
+ if (typeof p.longitude === 'number') return p.longitude;
406
+ }
407
+ return undefined;
408
+ };
409
+ return aspects.map((a) => {
410
+ const l1 = longitudeOf(p1, a.planet1);
411
+ const l2 = longitudeOf(p2, a.planet2);
412
+ if (l1 === undefined || l2 === undefined) return nothing;
413
+ const out = polarToCartesian(CENTER, CENTER, P1_R - 12, this.toAngle(l1));
414
+ const inn = polarToCartesian(CENTER, CENTER, P2_R + 8, this.toAngle(l2));
415
+ const aspectName = normalizeAspect(a);
416
+ const cls = ASPECT_CLASS[aspectName] ?? 'aspect-other';
417
+ const orbLabel = formatNumber(a.orb, 1);
418
+ 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
419
  });
302
420
  }
303
421
 
@@ -313,15 +431,13 @@ export class RoxySynastryChart extends LitElement {
313
431
  </tr>
314
432
  </thead>
315
433
  <tbody>
316
- ${aspects.slice(0, 16).map(
434
+ ${aspects.slice(0, 12).map(
317
435
  (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>
436
+ <td>${a.planet1}</td>
437
+ <td>${a.planet2}</td>
438
+ <td>${normalizeAspect(a) || ''}</td>
439
+ <td class="orb">${formatNumber(a.orb, 1)}</td>
440
+ <td>${formatStrength(a.strength)}</td>
325
441
  </tr>`,
326
442
  )}
327
443
  </tbody>
@@ -329,9 +445,9 @@ export class RoxySynastryChart extends LitElement {
329
445
  }
330
446
  }
331
447
 
332
- function capitalize(s: string): string {
333
- if (!s) return '';
334
- return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
448
+ function formatStrength(s: number | undefined): string {
449
+ if (typeof s === 'number') return Math.round(s).toString();
450
+ return '';
335
451
  }
336
452
 
337
453
  declare global {
@@ -1,34 +1,9 @@
1
1
  import { css, html, LitElement, nothing } from 'lit';
2
2
  import { customElement, property, state } from 'lit/decorators.js';
3
+ import type { GetCardResponse, GetDailyCardResponse } from '../types/index.js';
3
4
  import { baseStyles } from '../utils/base-styles.js';
4
5
 
5
- interface TarotCard {
6
- id?: string;
7
- name?: string;
8
- arcana?: 'major' | 'minor' | string;
9
- number?: number | string;
10
- position?: string;
11
- reversed?: boolean;
12
- keywords?: string[];
13
- meaning?:
14
- | string
15
- | {
16
- upright?: string;
17
- reversed?: string;
18
- spiritual?: string;
19
- emotional?: string;
20
- physical?: string;
21
- };
22
- imageUrl?: string;
23
- upright?: { meaning?: string; keywords?: string[] };
24
- }
25
-
26
- interface TarotData {
27
- date?: string;
28
- seed?: string;
29
- card?: TarotCard;
30
- dailyMessage?: string;
31
- }
6
+ type TarotData = GetCardResponse | GetDailyCardResponse;
32
7
 
33
8
  /**
34
9
  * Tarot card. Renders /tarot/cards/{id} or /tarot/daily. Click to flip
@@ -92,11 +67,6 @@ export class RoxyTarotCard extends LitElement {
92
67
  letter-spacing: 0.06em;
93
68
  margin-bottom: var(--roxy-space-sm, 0.5rem);
94
69
  }
95
- .position {
96
- color: var(--roxy-info, #0284c7);
97
- margin-left: var(--roxy-space-xs, 0.25rem);
98
- text-transform: capitalize;
99
- }
100
70
 
101
71
  .message {
102
72
  color: var(--roxy-fg, #0a0a0a);
@@ -137,37 +107,28 @@ export class RoxyTarotCard extends LitElement {
137
107
  ];
138
108
 
139
109
  @property({ attribute: false })
140
- data: TarotData | TarotCard | null = null;
110
+ data: TarotData | null = null;
141
111
 
142
112
  @state()
143
113
  private flipped = false;
144
114
 
145
- private getCard(): TarotCard | null {
146
- if (!this.data) return null;
147
- if ('card' in this.data && this.data.card) return this.data.card;
148
- return this.data as TarotCard;
149
- }
150
-
151
115
  private toggleFlip = () => {
152
116
  this.flipped = !this.flipped;
153
117
  };
154
118
 
155
119
  render() {
156
- const card = this.getCard();
157
- if (!card)
120
+ const d = this.data;
121
+ if (!d)
158
122
  return html`<div class="roxy-empty" role="status">No tarot data</div>`;
159
123
 
160
- const isReversed = this.flipped !== Boolean(card.reversed); // start at server-provided orientation, toggle on click
161
- const meaning =
162
- typeof card.meaning === 'string'
163
- ? card.meaning
164
- : ((isReversed ? card.meaning?.reversed : card.meaning?.upright) ??
165
- card.meaning?.spiritual ??
166
- card.upright?.meaning);
167
- const dailyMessage =
168
- this.data && 'dailyMessage' in this.data
169
- ? this.data.dailyMessage
170
- : undefined;
124
+ if ('card' in d) return this.renderDailyCard(d);
125
+ return this.renderFullCard(d);
126
+ }
127
+
128
+ private renderDailyCard(d: GetDailyCardResponse) {
129
+ const card = d.card;
130
+ const isReversed = this.flipped !== Boolean(card.reversed);
131
+ const keywords = card.keywords ?? [];
171
132
 
172
133
  return html`<article class="card" aria-label=${card.name ?? 'Tarot card'}>
173
134
  <div class="image-wrap">
@@ -197,21 +158,74 @@ export class RoxyTarotCard extends LitElement {
197
158
  <div>
198
159
  <div class="meta">
199
160
  ${card.arcana ? html`${card.arcana} arcana` : nothing}
200
- ${card.number !== undefined && card.number !== null ? html` · ${card.number}` : nothing}
201
161
  ${isReversed ? html` · reversed` : nothing}
202
- ${
203
- card.position
204
- ? html`<span class="position">${card.position}</span>`
205
- : nothing
206
- }
207
162
  </div>
208
163
  <h2 class="title">${card.name ?? 'Tarot card'}</h2>
209
- ${dailyMessage ? html`<p class="message">${dailyMessage}</p>` : nothing}
210
- ${meaning ? html`<p>${meaning}</p>` : nothing}
164
+ ${d.dailyMessage ? html`<p class="message">${d.dailyMessage}</p>` : nothing}
165
+ ${card.meaning ? html`<p>${card.meaning}</p>` : nothing}
166
+ ${
167
+ keywords.length > 0
168
+ ? html`<div class="chips">
169
+ ${keywords.map((k) => html`<span>${k}</span>`)}
170
+ </div>`
171
+ : nothing
172
+ }
173
+ <button
174
+ class="flip"
175
+ type="button"
176
+ @click=${this.toggleFlip}
177
+ aria-pressed=${this.flipped ? 'true' : 'false'}
178
+ >
179
+ Flip card
180
+ </button>
181
+ </div>
182
+ </article>`;
183
+ }
184
+
185
+ private renderFullCard(d: GetCardResponse) {
186
+ const isReversed = this.flipped;
187
+ const orientedMeaning = isReversed ? d.reversed : d.upright;
188
+ const keywords = isReversed
189
+ ? (d.keywords?.reversed ?? [])
190
+ : (d.keywords?.upright ?? []);
191
+
192
+ return html`<article class="card" aria-label=${d.name ?? 'Tarot card'}>
193
+ <div class="image-wrap">
194
+ ${
195
+ d.imageUrl
196
+ ? html`<img
197
+ class=${`image ${isReversed ? 'reversed' : ''}`}
198
+ src=${d.imageUrl}
199
+ alt=${d.name ?? 'Tarot card'}
200
+ tabindex="0"
201
+ @click=${this.toggleFlip}
202
+ @keydown=${(e: KeyboardEvent) => {
203
+ if (e.key === 'Enter' || e.key === ' ') {
204
+ e.preventDefault();
205
+ this.toggleFlip();
206
+ }
207
+ }}
208
+ />`
209
+ : html`<div
210
+ class=${`image ${isReversed ? 'reversed' : ''}`}
211
+ style="aspect-ratio: 0.6; display: flex; align-items: center; justify-content: center; color: var(--roxy-muted)"
212
+ >
213
+ ${d.name ?? '?'}
214
+ </div>`
215
+ }
216
+ </div>
217
+ <div>
218
+ <div class="meta">
219
+ ${d.arcana ? html`${d.arcana} arcana` : nothing}
220
+ ${d.number !== undefined && d.number !== null ? html` · ${d.number}` : nothing}
221
+ ${isReversed ? html` · reversed` : nothing}
222
+ </div>
223
+ <h2 class="title">${d.name ?? 'Tarot card'}</h2>
224
+ ${orientedMeaning?.description ? html`<p>${orientedMeaning.description}</p>` : nothing}
211
225
  ${
212
- card.keywords?.length
226
+ keywords.length > 0
213
227
  ? html`<div class="chips">
214
- ${card.keywords.map((k) => html`<span>${k}</span>`)}
228
+ ${keywords.map((k) => html`<span>${k}</span>`)}
215
229
  </div>`
216
230
  : nothing
217
231
  }