@roxyapi/ui 0.1.3 → 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 (161) hide show
  1. package/AGENTS.md +6 -0
  2. package/README.md +9 -3
  3. package/dist/cdn/components/ashtakavarga-grid.js +349 -0
  4. package/dist/cdn/components/ashtakavarga-grid.js.map +7 -0
  5. package/dist/cdn/components/choghadiya-grid.js +239 -0
  6. package/dist/cdn/components/choghadiya-grid.js.map +7 -0
  7. package/dist/cdn/components/compatibility-card.js +6 -6
  8. package/dist/cdn/components/compatibility-card.js.map +1 -1
  9. package/dist/cdn/components/dasha-timeline.js +4 -4
  10. package/dist/cdn/components/dasha-timeline.js.map +1 -1
  11. package/dist/cdn/components/data.js +9 -9
  12. package/dist/cdn/components/data.js.map +4 -4
  13. package/dist/cdn/components/divisional-chart.js +279 -0
  14. package/dist/cdn/components/divisional-chart.js.map +7 -0
  15. package/dist/cdn/components/dosha-card.js +39 -39
  16. package/dist/cdn/components/dosha-card.js.map +3 -3
  17. package/dist/cdn/components/endpoint-form.js +8 -8
  18. package/dist/cdn/components/endpoint-form.js.map +4 -4
  19. package/dist/cdn/components/guna-milan.js +64 -22
  20. package/dist/cdn/components/guna-milan.js.map +3 -3
  21. package/dist/cdn/components/hexagram.js +9 -9
  22. package/dist/cdn/components/hexagram.js.map +3 -3
  23. package/dist/cdn/components/horoscope-card.js +28 -21
  24. package/dist/cdn/components/horoscope-card.js.map +4 -4
  25. package/dist/cdn/components/kp-planets-table.js +4 -4
  26. package/dist/cdn/components/kp-planets-table.js.map +1 -1
  27. package/dist/cdn/components/location-search.js.map +2 -2
  28. package/dist/cdn/components/moon-phase.js +13 -13
  29. package/dist/cdn/components/moon-phase.js.map +3 -3
  30. package/dist/cdn/components/natal-chart.js +196 -22
  31. package/dist/cdn/components/natal-chart.js.map +4 -4
  32. package/dist/cdn/components/numerology-card.js +6 -6
  33. package/dist/cdn/components/numerology-card.js.map +4 -4
  34. package/dist/cdn/components/panchang-table.js +9 -9
  35. package/dist/cdn/components/panchang-table.js.map +1 -1
  36. package/dist/cdn/components/shadbala-table.js +312 -0
  37. package/dist/cdn/components/shadbala-table.js.map +7 -0
  38. package/dist/cdn/components/synastry-chart.js +21 -21
  39. package/dist/cdn/components/synastry-chart.js.map +4 -4
  40. package/dist/cdn/components/transits-table.js +391 -0
  41. package/dist/cdn/components/transits-table.js.map +7 -0
  42. package/dist/cdn/components/vedic-kundli.js +51 -29
  43. package/dist/cdn/components/vedic-kundli.js.map +4 -4
  44. package/dist/cdn/components/yoga-list.js +334 -0
  45. package/dist/cdn/components/yoga-list.js.map +7 -0
  46. package/dist/cdn/roxy-ui.js +1872 -522
  47. package/dist/cdn/roxy-ui.js.map +4 -4
  48. package/dist/components/ashtakavarga-grid.d.ts +26 -0
  49. package/dist/components/ashtakavarga-grid.d.ts.map +1 -0
  50. package/dist/components/ashtakavarga-grid.js +457 -0
  51. package/dist/components/ashtakavarga-grid.js.map +7 -0
  52. package/dist/components/choghadiya-grid.d.ts +19 -0
  53. package/dist/components/choghadiya-grid.d.ts.map +1 -0
  54. package/dist/components/choghadiya-grid.js +304 -0
  55. package/dist/components/choghadiya-grid.js.map +7 -0
  56. package/dist/components/compatibility-card.js.map +1 -1
  57. package/dist/components/dasha-timeline.js.map +1 -1
  58. package/dist/components/data.d.ts +5 -7
  59. package/dist/components/data.d.ts.map +1 -1
  60. package/dist/components/data.js +7 -5
  61. package/dist/components/data.js.map +3 -3
  62. package/dist/components/divisional-chart.d.ts +20 -0
  63. package/dist/components/divisional-chart.d.ts.map +1 -0
  64. package/dist/components/divisional-chart.js +471 -0
  65. package/dist/components/divisional-chart.js.map +7 -0
  66. package/dist/components/dosha-card.d.ts.map +1 -1
  67. package/dist/components/dosha-card.js +33 -30
  68. package/dist/components/dosha-card.js.map +2 -2
  69. package/dist/components/endpoint-form.d.ts.map +1 -1
  70. package/dist/components/endpoint-form.js +5 -3
  71. package/dist/components/endpoint-form.js.map +3 -3
  72. package/dist/components/guna-milan.d.ts.map +1 -1
  73. package/dist/components/guna-milan.js +61 -12
  74. package/dist/components/guna-milan.js.map +3 -3
  75. package/dist/components/hexagram.js +17 -0
  76. package/dist/components/hexagram.js.map +2 -2
  77. package/dist/components/horoscope-card.d.ts.map +1 -1
  78. package/dist/components/horoscope-card.js +30 -3
  79. package/dist/components/horoscope-card.js.map +3 -3
  80. package/dist/components/kp-planets-table.js.map +1 -1
  81. package/dist/components/location-search.d.ts +2 -3
  82. package/dist/components/location-search.d.ts.map +1 -1
  83. package/dist/components/location-search.js.map +2 -2
  84. package/dist/components/moon-phase.js +17 -0
  85. package/dist/components/moon-phase.js.map +2 -2
  86. package/dist/components/natal-chart.d.ts +2 -0
  87. package/dist/components/natal-chart.d.ts.map +1 -1
  88. package/dist/components/natal-chart.js +243 -36
  89. package/dist/components/natal-chart.js.map +3 -3
  90. package/dist/components/numerology-card.d.ts.map +1 -1
  91. package/dist/components/numerology-card.js +5 -3
  92. package/dist/components/numerology-card.js.map +3 -3
  93. package/dist/components/panchang-table.js.map +1 -1
  94. package/dist/components/shadbala-table.d.ts +18 -0
  95. package/dist/components/shadbala-table.d.ts.map +1 -0
  96. package/dist/components/shadbala-table.js +400 -0
  97. package/dist/components/shadbala-table.js.map +7 -0
  98. package/dist/components/synastry-chart.d.ts.map +1 -1
  99. package/dist/components/synastry-chart.js +34 -29
  100. package/dist/components/synastry-chart.js.map +3 -3
  101. package/dist/components/transits-table.d.ts +21 -0
  102. package/dist/components/transits-table.d.ts.map +1 -0
  103. package/dist/components/transits-table.js +515 -0
  104. package/dist/components/transits-table.js.map +7 -0
  105. package/dist/components/vedic-kundli.d.ts +3 -6
  106. package/dist/components/vedic-kundli.d.ts.map +1 -1
  107. package/dist/components/vedic-kundli.js +132 -80
  108. package/dist/components/vedic-kundli.js.map +3 -3
  109. package/dist/components/yoga-list.d.ts +29 -0
  110. package/dist/components/yoga-list.d.ts.map +1 -0
  111. package/dist/components/yoga-list.js +389 -0
  112. package/dist/components/yoga-list.js.map +7 -0
  113. package/dist/index.cjs +2693 -971
  114. package/dist/index.cjs.map +4 -4
  115. package/dist/index.d.ts +7 -2
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +2712 -990
  118. package/dist/index.js.map +4 -4
  119. package/dist/manifest.d.ts +4 -10
  120. package/dist/manifest.d.ts.map +1 -1
  121. package/dist/manifest.json +7 -2
  122. package/dist/styles/tokens.css +26 -0
  123. package/dist/tokens/index.d.ts +6 -0
  124. package/dist/tokens/index.d.ts.map +1 -1
  125. package/dist/types/types.gen.d.ts +2 -2
  126. package/dist/utils/format.d.ts +15 -1
  127. package/dist/utils/format.d.ts.map +1 -1
  128. package/dist/utils/kundli-render.d.ts +63 -0
  129. package/dist/utils/kundli-render.d.ts.map +1 -0
  130. package/dist/utils/string.d.ts +14 -0
  131. package/dist/utils/string.d.ts.map +1 -0
  132. package/dist/version.d.ts +1 -1
  133. package/package.json +1 -1
  134. package/src/components/ashtakavarga-grid.ts +354 -0
  135. package/src/components/choghadiya-grid.ts +185 -0
  136. package/src/components/data.ts +8 -15
  137. package/src/components/divisional-chart.ts +214 -0
  138. package/src/components/dosha-card.ts +53 -36
  139. package/src/components/endpoint-form.ts +1 -7
  140. package/src/components/guna-milan.ts +74 -16
  141. package/src/components/horoscope-card.ts +8 -4
  142. package/src/components/location-search.ts +2 -3
  143. package/src/components/natal-chart.ts +251 -42
  144. package/src/components/numerology-card.ts +1 -7
  145. package/src/components/shadbala-table.ts +286 -0
  146. package/src/components/synastry-chart.ts +13 -39
  147. package/src/components/transits-table.ts +350 -0
  148. package/src/components/vedic-kundli.ts +38 -143
  149. package/src/components/yoga-list.ts +328 -0
  150. package/src/index.ts +8 -6
  151. package/src/manifest.ts +74 -100
  152. package/src/styles/tokens.css +26 -0
  153. package/src/tokens/index.ts +9 -0
  154. package/src/types/types.gen.ts +2 -2
  155. package/src/utils/format.ts +21 -3
  156. package/src/utils/kundli-render.ts +197 -0
  157. package/src/utils/string.ts +23 -0
  158. package/src/version.ts +1 -1
  159. package/dist/utils/motion.d.ts +0 -13
  160. package/dist/utils/motion.d.ts.map +0 -1
  161. package/src/utils/motion.ts +0 -18
@@ -33,6 +33,14 @@ export class RoxyGunaMilan extends LitElement {
33
33
  gap: var(--roxy-space-md, 1rem);
34
34
  }
35
35
 
36
+ .score-header {
37
+ display: flex;
38
+ align-items: center;
39
+ gap: 1rem;
40
+ }
41
+ .score-info {
42
+ flex: 1;
43
+ }
36
44
  .score-bar {
37
45
  display: grid;
38
46
  grid-template-columns: 1fr auto;
@@ -54,6 +62,26 @@ export class RoxyGunaMilan extends LitElement {
54
62
  font-size: var(--roxy-text-sm, 0.875rem);
55
63
  color: var(--roxy-secondary, #475569);
56
64
  }
65
+ .score-ring {
66
+ width: 120px;
67
+ height: 120px;
68
+ flex-shrink: 0;
69
+ }
70
+ .score-ring svg {
71
+ width: 100%;
72
+ height: 100%;
73
+ }
74
+ .score-ring .ring-text {
75
+ font-size: 22px;
76
+ font-weight: 700;
77
+ fill: var(--roxy-fg, #0a0a0a);
78
+ font-family: var(--roxy-font-sans);
79
+ }
80
+ .score-ring .ring-max {
81
+ font-size: 10px;
82
+ fill: var(--roxy-muted, #71717a);
83
+ font-family: var(--roxy-font-sans);
84
+ }
57
85
 
58
86
  table {
59
87
  width: 100%;
@@ -130,24 +158,54 @@ export class RoxyGunaMilan extends LitElement {
130
158
  (b) => b?.category !== undefined,
131
159
  );
132
160
 
161
+ const score = d.total ?? 0;
162
+ const max = d.maxScore ?? 36;
163
+ const pct = (score / max) * 100;
164
+ const trackColor =
165
+ 'color-mix(in srgb, var(--roxy-border) 50%, transparent)';
166
+ const fillColor =
167
+ pct >= 70
168
+ ? 'var(--roxy-success)'
169
+ : pct >= 50
170
+ ? 'var(--roxy-warning)'
171
+ : 'var(--roxy-danger)';
172
+ // SVG circle with r=45: circumference = 2 * pi * 45 = 282.74
173
+ // dasharray segments = pct * 2.827, (100 - pct) * 2.827
174
+ const dashFill = pct * 2.827;
175
+ const dashGap = (100 - pct) * 2.827;
176
+
133
177
  return html`<article class="card" aria-label="Guna Milan score">
134
- <div class="score-bar">
135
- <div>
136
- <span class="total">${formatNumber(d.total, 1)}</span>
137
- <span class="over"> / ${d.maxScore}</span>
138
- ${
139
- typeof d.percentage === 'number'
140
- ? html`<small style="margin-left: 0.5rem; color: var(--roxy-muted)">
141
- ${formatPercent(d.percentage, 1)}
142
- </small>`
143
- : nothing
144
- }
178
+ <div class="score-header">
179
+ <div class="score-info">
180
+ <div class="score-bar">
181
+ <div>
182
+ <span class="total">${formatNumber(d.total, 1)}</span>
183
+ <span class="over"> / ${d.maxScore}</span>
184
+ ${
185
+ typeof d.percentage === 'number'
186
+ ? html`<small style="margin-left: 0.5rem; color: var(--roxy-muted)">
187
+ ${formatPercent(d.percentage, 1)}
188
+ </small>`
189
+ : nothing
190
+ }
191
+ </div>
192
+ ${
193
+ d.recommendation
194
+ ? html`<span class="recommendation">${d.recommendation}</span>`
195
+ : nothing
196
+ }
197
+ </div>
198
+ </div>
199
+ <div class="score-ring" role="meter" aria-label="Guna milan score" aria-valuemin="0" aria-valuemax="36" aria-valuenow="${score}">
200
+ <svg viewBox="0 0 100 100" aria-hidden="true">
201
+ <circle class="ring-track" cx="50" cy="50" r="45" fill="none" stroke="${trackColor}" stroke-width="8"/>
202
+ <circle class="ring-fill" cx="50" cy="50" r="45" fill="none" stroke="${fillColor}" stroke-width="8"
203
+ stroke-dasharray="${dashFill},${dashGap}" stroke-linecap="round"
204
+ transform="rotate(-90 50 50)"/>
205
+ <text x="50" y="50" text-anchor="middle" dominant-baseline="central" class="ring-text">${score}</text>
206
+ <text x="50" y="64" text-anchor="middle" dominant-baseline="central" class="ring-max">/${max}</text>
207
+ </svg>
145
208
  </div>
146
- ${
147
- d.recommendation
148
- ? html`<span class="recommendation">${d.recommendation}</span>`
149
- : nothing
150
- }
151
209
  </div>
152
210
 
153
211
  ${
@@ -7,6 +7,7 @@ import type {
7
7
  GetWeeklyHoroscopeResponse,
8
8
  } from '../types/index.js';
9
9
  import { baseStyles } from '../utils/base-styles.js';
10
+ import { capitalize } from '../utils/string.js';
10
11
 
11
12
  type HoroscopeData =
12
13
  | GetDailyHoroscopeResponse
@@ -123,6 +124,13 @@ export class RoxyHoroscopeCard extends LitElement {
123
124
  font-weight: var(--roxy-weight-bold, 600);
124
125
  }
125
126
 
127
+ .compat-wrap {
128
+ width: 100%;
129
+ display: flex;
130
+ align-items: center;
131
+ flex-wrap: wrap;
132
+ gap: var(--roxy-space-xs, 0.25rem);
133
+ }
126
134
  .compat {
127
135
  display: flex;
128
136
  flex-wrap: wrap;
@@ -292,10 +300,6 @@ export class RoxyHoroscopeCard extends LitElement {
292
300
  }
293
301
  }
294
302
 
295
- function capitalize(s: string): string {
296
- return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
297
- }
298
-
299
303
  declare global {
300
304
  interface HTMLElementTagNameMap {
301
305
  'roxy-horoscope-card': RoxyHoroscopeCard;
@@ -11,9 +11,8 @@ type CityResult = SearchCitiesResponse['cities'][number];
11
11
  * `roxy-location-select` CustomEvent with the chosen city. Required for any
12
12
  * chart endpoint.
13
13
  *
14
- * Lifted from jyotish-vedic-astrology-app/src/components/city-search.tsx,
15
- * keeping the 300ms debounce and click-outside behavior, replacing React
16
- * state with Lit reactive properties and using direct fetch to RoxyAPI.
14
+ * Behavior: 300ms input debounce, click-outside dismiss, keyboard navigation
15
+ * with arrow keys / Enter / Escape, AbortController on stale requests.
17
16
  *
18
17
  * Attributes:
19
18
  * api-key optional. Direct call to roxyapi.com when set.
@@ -1,22 +1,27 @@
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
4
  import type { NatalChartResponse } from '../types/index.js';
5
5
  import { baseStyles } from '../utils/base-styles.js';
6
- import { longitudeToSignPosition, polarToCartesian } from '../utils/degree.js';
7
- import { formatNumber } from '../utils/format.js';
6
+ import { polarToCartesian } from '../utils/degree.js';
7
+ import {
8
+ ASPECT_CLASS,
9
+ formatNumber,
10
+ normalizeAspect,
11
+ } from '../utils/format.js';
12
+ import { capitalize } from '../utils/string.js';
8
13
 
9
14
  type PlanetEntry = NatalChartResponse['planets'][number];
10
15
  type AspectEntry = NatalChartResponse['aspects'][number];
11
16
 
12
- const SIZE = 384;
17
+ const SIZE = 420;
13
18
  const CENTER = SIZE / 2;
14
- const OUTER_R = 150;
15
- const SIGN_R = 134;
16
- const HOUSE_R = 110;
17
- const PLANET_R = 88;
18
- const ANGLE_TICK_R = 162;
19
- const ANGLE_LABEL_R = 176;
19
+ const OUTER_R = 164;
20
+ const SIGN_R = 146;
21
+ const HOUSE_R = 120;
22
+ const PLANET_R = 96;
23
+ const ANGLE_TICK_R = 178;
24
+ const ANGLE_LABEL_R = 196;
20
25
 
21
26
  /**
22
27
  * Western natal chart wheel. Renders the 12 zodiac signs, 12 houses, planet
@@ -125,6 +130,136 @@ export class RoxyNatalChart extends LitElement {
125
130
  margin-right: 4px;
126
131
  vertical-align: middle;
127
132
  }
133
+
134
+ .details {
135
+ margin-top: var(--roxy-space-md, 1rem);
136
+ }
137
+
138
+ .pill-row {
139
+ display: flex;
140
+ flex-wrap: wrap;
141
+ gap: var(--roxy-space-xs, 0.25rem);
142
+ margin-bottom: var(--roxy-space-xs, 0.25rem);
143
+ }
144
+
145
+ .pill {
146
+ padding: 2px 8px;
147
+ border-radius: var(--roxy-radius-sm, 4px);
148
+ font-size: var(--roxy-text-xs, 0.75rem);
149
+ background: color-mix(in srgb, var(--roxy-fg, #0f172a) 8%, transparent);
150
+ color: var(--roxy-fg, #0f172a);
151
+ }
152
+
153
+ .pill--success {
154
+ background: color-mix(in srgb, var(--roxy-success, #16a34a) 15%, transparent);
155
+ color: var(--roxy-success, #16a34a);
156
+ }
157
+
158
+ .pill--danger {
159
+ background: color-mix(in srgb, var(--roxy-danger, #dc2626) 15%, transparent);
160
+ color: var(--roxy-danger, #dc2626);
161
+ }
162
+
163
+ .pill--muted {
164
+ background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 60%, transparent);
165
+ color: var(--roxy-fg, #0a0a0a);
166
+ }
167
+
168
+ .summary {
169
+ color: var(--roxy-fg, #0f172a);
170
+ font-size: var(--roxy-text-sm, 0.875rem);
171
+ margin: var(--roxy-space-md, 1rem) 0;
172
+ }
173
+
174
+ .dist-grid {
175
+ display: grid;
176
+ grid-template-columns: 1fr 1fr;
177
+ gap: var(--roxy-space-md, 1rem);
178
+ }
179
+
180
+ @container (max-width: 639px) {
181
+ .dist-grid {
182
+ grid-template-columns: 1fr;
183
+ }
184
+ }
185
+
186
+ .dist-section h3 {
187
+ font-size: var(--roxy-text-xs, 0.75rem);
188
+ font-weight: var(--roxy-weight-bold, 600);
189
+ color: var(--roxy-muted, #71717a);
190
+ margin: 0 0 var(--roxy-space-xs, 0.25rem);
191
+ text-transform: uppercase;
192
+ letter-spacing: 0.05em;
193
+ }
194
+
195
+ .dist-row {
196
+ display: grid;
197
+ grid-template-columns: 4rem 1fr 1.5rem;
198
+ align-items: center;
199
+ gap: var(--roxy-space-xs, 0.25rem);
200
+ font-size: var(--roxy-text-xs, 0.75rem);
201
+ color: var(--roxy-fg, #0f172a);
202
+ margin-bottom: 4px;
203
+ }
204
+
205
+ .dist-bar {
206
+ background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 20%, transparent);
207
+ height: 6px;
208
+ border-radius: 3px;
209
+ }
210
+
211
+ .dist-bar > span {
212
+ display: block;
213
+ height: 100%;
214
+ background: var(--roxy-accent, #f59e0b);
215
+ border-radius: 3px;
216
+ }
217
+
218
+ .interpretations {
219
+ margin-top: var(--roxy-space-md, 1rem);
220
+ }
221
+ .interpretations h3 {
222
+ font-size: var(--roxy-text-sm, 0.875rem);
223
+ font-weight: 600;
224
+ color: var(--roxy-muted, #71717a);
225
+ text-transform: uppercase;
226
+ letter-spacing: 0.06em;
227
+ margin: 0 0 var(--roxy-space-sm, 0.5rem);
228
+ }
229
+ .interp-card {
230
+ border: 1px solid var(--roxy-border, #e4e4e7);
231
+ border-radius: var(--roxy-radius-md, 8px);
232
+ padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem);
233
+ margin-bottom: var(--roxy-space-xs, 0.25rem);
234
+ }
235
+ .interp-card summary {
236
+ cursor: pointer;
237
+ font-weight: 500;
238
+ color: var(--roxy-fg, #0f172a);
239
+ }
240
+ .interp-card summary small {
241
+ color: var(--roxy-muted, #71717a);
242
+ margin-left: 0.5em;
243
+ font-weight: 400;
244
+ }
245
+ .interp-body {
246
+ margin-top: var(--roxy-space-xs, 0.25rem);
247
+ color: var(--roxy-fg, #0f172a);
248
+ font-size: var(--roxy-text-sm, 0.875rem);
249
+ }
250
+ .interp-keywords {
251
+ display: flex;
252
+ flex-wrap: wrap;
253
+ gap: 0.25rem;
254
+ margin-top: 0.5rem;
255
+ }
256
+ .interp-keywords .kw {
257
+ padding: 1px 8px;
258
+ border-radius: 9999px;
259
+ background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 14%, transparent);
260
+ color: var(--roxy-accent-fg, #b45309);
261
+ font-size: var(--roxy-text-xs, 0.75rem);
262
+ }
128
263
  `,
129
264
  ];
130
265
 
@@ -211,6 +346,8 @@ export class RoxyNatalChart extends LitElement {
211
346
  <span><span class="legend-swatch" style="background: var(--roxy-success)"></span>harmonious</span>
212
347
  <span><span class="legend-swatch" style="background: var(--roxy-danger)"></span>challenging</span>
213
348
  </div>
349
+ ${this.renderDetails()}
350
+ ${this.renderInterpretations()}
214
351
  </div>`;
215
352
  }
216
353
 
@@ -245,21 +382,7 @@ export class RoxyNatalChart extends LitElement {
245
382
  }
246
383
 
247
384
  private renderSigns() {
248
- const order = [
249
- 'Aries',
250
- 'Taurus',
251
- 'Gemini',
252
- 'Cancer',
253
- 'Leo',
254
- 'Virgo',
255
- 'Libra',
256
- 'Scorpio',
257
- 'Sagittarius',
258
- 'Capricorn',
259
- 'Aquarius',
260
- 'Pisces',
261
- ];
262
- return order.map((sign, i) => {
385
+ return SIGNS_ORDER.map((sign, i) => {
263
386
  const angle = this.toAngle(i * 30 + 15);
264
387
  const pos = polarToCartesian(CENTER, CENTER, SIGN_R, angle);
265
388
  return svg`<text class="sign-glyph" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central">${SIGN_GLYPH[sign]}</text>`;
@@ -288,6 +411,109 @@ export class RoxyNatalChart extends LitElement {
288
411
  });
289
412
  }
290
413
 
414
+ private renderDetails() {
415
+ const summary = this.data?.summary;
416
+ const ai = this.data?.aspectsInterpretation;
417
+ if (!summary && !ai) return nothing;
418
+
419
+ const retrogrades = summary?.retrogradePlanets ?? [];
420
+ const elementDist = summary?.elementDistribution ?? {};
421
+ const modalityDist = summary?.modalityDistribution ?? {};
422
+ const elementMax = Math.max(1, ...Object.values(elementDist));
423
+ const modalityMax = Math.max(1, ...Object.values(modalityDist));
424
+
425
+ return html`<div class="details">
426
+ ${
427
+ summary?.dominantElement || summary?.dominantModality
428
+ ? html`<div class="pill-row">
429
+ ${summary.dominantElement ? html`<span class="pill">Dominant element: ${summary.dominantElement}</span>` : nothing}
430
+ ${summary.dominantModality ? html`<span class="pill">Dominant modality: ${summary.dominantModality}</span>` : nothing}
431
+ </div>`
432
+ : nothing
433
+ }
434
+ ${
435
+ ai
436
+ ? html`<div class="pill-row">
437
+ <span class="pill pill--success">Harmonious ${ai.harmonious}</span>
438
+ <span class="pill pill--danger">Challenging ${ai.challenging}</span>
439
+ <span class="pill pill--muted">Neutral ${ai.neutral}</span>
440
+ </div>`
441
+ : nothing
442
+ }
443
+ ${
444
+ retrogrades.length > 0
445
+ ? html`<div class="pill-row">
446
+ ${retrogrades.map((p) => {
447
+ const glyph = PLANET_GLYPH[p] ?? p.slice(0, 2);
448
+ return html`<span class="pill pill--muted">${glyph} ${p} R</span>`;
449
+ })}
450
+ </div>`
451
+ : nothing
452
+ }
453
+ ${ai?.summary ? html`<p class="summary">${ai.summary}</p>` : nothing}
454
+ ${
455
+ Object.keys(elementDist).length > 0 ||
456
+ Object.keys(modalityDist).length > 0
457
+ ? html`<div class="dist-grid">
458
+ ${
459
+ Object.keys(elementDist).length > 0
460
+ ? html`<div class="dist-section">
461
+ <h3>Elements</h3>
462
+ ${Object.entries(elementDist).map(
463
+ ([label, count]) => html`<div class="dist-row">
464
+ <span>${label}</span>
465
+ <div class="dist-bar"><span style="width: ${Math.round((count / elementMax) * 100)}%"></span></div>
466
+ <span>${count}</span>
467
+ </div>`,
468
+ )}
469
+ </div>`
470
+ : nothing
471
+ }
472
+ ${
473
+ Object.keys(modalityDist).length > 0
474
+ ? html`<div class="dist-section">
475
+ <h3>Modalities</h3>
476
+ ${Object.entries(modalityDist).map(
477
+ ([label, count]) => html`<div class="dist-row">
478
+ <span>${label}</span>
479
+ <div class="dist-bar"><span style="width: ${Math.round((count / modalityMax) * 100)}%"></span></div>
480
+ <span>${count}</span>
481
+ </div>`,
482
+ )}
483
+ </div>`
484
+ : nothing
485
+ }
486
+ </div>`
487
+ : nothing
488
+ }
489
+ </div>`;
490
+ }
491
+
492
+ private renderInterpretations() {
493
+ const planets = this.getPlanets().filter((p) => p.interpretation);
494
+ if (planets.length === 0) return nothing;
495
+ return html`<section class="interpretations">
496
+ <h3>Planet readings</h3>
497
+ ${planets.map((p) => {
498
+ const interp = p.interpretation!;
499
+ const glyph = PLANET_GLYPH[capitalize(p.name)] ?? '';
500
+ const deg = formatNumber(p.degree ?? 0, 1);
501
+ return html`<details class="interp-card">
502
+ <summary>${glyph} ${p.name} <small>${p.sign ?? ''} ${deg}</small></summary>
503
+ <div class="interp-body">
504
+ ${interp.summary ? html`<p class="interp-summary">${interp.summary}</p>` : nothing}
505
+ ${interp.detailed ? html`<p class="interp-detail">${interp.detailed}</p>` : nothing}
506
+ ${
507
+ interp.keywords?.length
508
+ ? html`<div class="interp-keywords">${interp.keywords.map((k) => html`<span class="kw">${k}</span>`)}</div>`
509
+ : nothing
510
+ }
511
+ </div>
512
+ </details>`;
513
+ })}
514
+ </section>`;
515
+ }
516
+
291
517
  private renderAspects(planets: PlanetEntry[], aspects: AspectEntry[]) {
292
518
  const planetMap = new Map<string, number>();
293
519
  for (const p of planets) {
@@ -319,23 +545,6 @@ export class RoxyNatalChart extends LitElement {
319
545
  }
320
546
  }
321
547
 
322
- function capitalize(s: string): string {
323
- if (!s) return '';
324
- return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
325
- }
326
-
327
- const ASPECT_CLASS: Record<string, string> = {
328
- conjunction: 'aspect-conjunction',
329
- sextile: 'aspect-sextile',
330
- square: 'aspect-square',
331
- trine: 'aspect-trine',
332
- opposition: 'aspect-opposition',
333
- };
334
-
335
- function normalizeAspect(a: AspectEntry): string {
336
- return (a.type ?? '').toLowerCase().replace(/_/g, '-');
337
- }
338
-
339
548
  declare global {
340
549
  interface HTMLElementTagNameMap {
341
550
  'roxy-natal-chart': RoxyNatalChart;
@@ -7,6 +7,7 @@ import type {
7
7
  GenerateNumerologyChartResponse,
8
8
  } from '../types/index.js';
9
9
  import { baseStyles } from '../utils/base-styles.js';
10
+ import { humanize } from '../utils/string.js';
10
11
 
11
12
  type NumerologyData =
12
13
  | CalculateLifePathResponse
@@ -237,13 +238,6 @@ function karmicDebtText(value: KarmicDebtMeaning | undefined): string {
237
238
  .join(' ');
238
239
  }
239
240
 
240
- function humanize(s: string): string {
241
- return s
242
- .replace(/[_-]+/g, ' ')
243
- .replace(/([a-z])([A-Z])/g, '$1 $2')
244
- .replace(/^\w/, (c) => c.toUpperCase());
245
- }
246
-
247
241
  declare global {
248
242
  interface HTMLElementTagNameMap {
249
243
  'roxy-numerology-card': RoxyNumerologyCard;