@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
@@ -0,0 +1,286 @@
1
+ import { css, html, LitElement, nothing } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { PLANET_GLYPH } from '../tokens/index.js';
4
+ import type { ShadbalaResponse } from '../types/index.js';
5
+ import { baseStyles } from '../utils/base-styles.js';
6
+ import { formatNumber } from '../utils/format.js';
7
+ import { capitalize } from '../utils/string.js';
8
+
9
+ type Planet = ShadbalaResponse['planets'][number];
10
+
11
+ /** CSS variable and display name for each of the 6 bala components. */
12
+ const BALA_COMPONENTS: Array<{
13
+ key: keyof Pick<
14
+ Planet,
15
+ | 'sthanaBala'
16
+ | 'digBala'
17
+ | 'kalaBala'
18
+ | 'chestaBala'
19
+ | 'naisargikaBala'
20
+ | 'drikBala'
21
+ >;
22
+ label: string;
23
+ color: string;
24
+ }> = [
25
+ { key: 'sthanaBala', label: 'Sthana', color: 'var(--roxy-info, #0284c7)' },
26
+ { key: 'digBala', label: 'Dig', color: 'var(--roxy-success, #16a34a)' },
27
+ { key: 'kalaBala', label: 'Kala', color: 'var(--roxy-warning, #ea580c)' },
28
+ { key: 'chestaBala', label: 'Chesta', color: 'var(--roxy-accent, #f59e0b)' },
29
+ {
30
+ key: 'naisargikaBala',
31
+ label: 'Naisargika',
32
+ color: 'var(--roxy-secondary, #475569)',
33
+ },
34
+ { key: 'drikBala', label: 'Drik', color: 'var(--roxy-danger, #dc2626)' },
35
+ ];
36
+
37
+ /**
38
+ * Shadbala six-fold planetary strength table with stacked bar visualization.
39
+ * Pass `data` from /vedic-astrology/shadbala.
40
+ */
41
+ @customElement('roxy-shadbala-table')
42
+ export class RoxyShadbalaTable extends LitElement {
43
+ static styles = [
44
+ baseStyles,
45
+ css`
46
+ .wrap {
47
+ display: grid;
48
+ gap: var(--roxy-space-md, 1rem);
49
+ }
50
+
51
+ .head {
52
+ display: flex;
53
+ justify-content: space-between;
54
+ align-items: baseline;
55
+ gap: var(--roxy-space-md, 1rem);
56
+ flex-wrap: wrap;
57
+ }
58
+
59
+ .title {
60
+ font-size: var(--roxy-text-lg, 1.125rem);
61
+ font-weight: var(--roxy-weight-bold, 600);
62
+ margin: 0;
63
+ }
64
+
65
+ .subtitle {
66
+ color: var(--roxy-muted, #71717a);
67
+ font-size: var(--roxy-text-sm, 0.875rem);
68
+ margin: 0;
69
+ }
70
+
71
+ .planet-row {
72
+ display: grid;
73
+ grid-template-columns: 8rem 1fr auto;
74
+ align-items: center;
75
+ gap: var(--roxy-space-sm, 0.5rem);
76
+ padding: var(--roxy-space-sm, 0.5rem) 0;
77
+ border-bottom: 1px solid var(--roxy-border, #e4e4e7);
78
+ }
79
+
80
+ .planet-row:last-of-type {
81
+ border-bottom: none;
82
+ }
83
+
84
+ .planet-label {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 6px;
88
+ font-size: var(--roxy-text-sm, 0.875rem);
89
+ font-weight: var(--roxy-weight-bold, 600);
90
+ }
91
+
92
+ .glyph {
93
+ font-size: 1.2em;
94
+ line-height: 1;
95
+ }
96
+
97
+ .bar-wrap {
98
+ display: flex;
99
+ flex-direction: column;
100
+ gap: 4px;
101
+ }
102
+
103
+ .bar {
104
+ display: flex;
105
+ height: 12px;
106
+ border-radius: var(--roxy-radius-sm, 4px);
107
+ overflow: hidden;
108
+ background: var(--roxy-border, #e4e4e7);
109
+ }
110
+
111
+ .bar-segment {
112
+ height: 100%;
113
+ transition: flex-grow var(--roxy-motion-duration, 200ms)
114
+ var(--roxy-motion-easing, cubic-bezier(0.4, 0, 0.2, 1));
115
+ }
116
+
117
+ .pills {
118
+ display: flex;
119
+ flex-direction: column;
120
+ align-items: flex-end;
121
+ gap: 4px;
122
+ }
123
+
124
+ .rupas-label {
125
+ font-variant-numeric: tabular-nums;
126
+ font-size: var(--roxy-text-xs, 0.75rem);
127
+ color: var(--roxy-muted, #71717a);
128
+ white-space: nowrap;
129
+ }
130
+
131
+ .adequacy-badge {
132
+ display: inline-block;
133
+ padding: 1px 6px;
134
+ border-radius: var(--roxy-radius-full, 9999px);
135
+ font-size: var(--roxy-text-xs, 0.75rem);
136
+ font-weight: var(--roxy-weight-bold, 600);
137
+ }
138
+
139
+ .adequacy-badge--adequate {
140
+ background: color-mix(in srgb, var(--roxy-success, #16a34a) 12%, transparent);
141
+ color: var(--roxy-success-fg, #166534);
142
+ }
143
+
144
+ .adequacy-badge--weak {
145
+ background: color-mix(in srgb, var(--roxy-danger, #dc2626) 12%, transparent);
146
+ color: var(--roxy-danger-fg, #991b1b);
147
+ }
148
+
149
+ .rank-badge {
150
+ font-size: var(--roxy-text-xs, 0.75rem);
151
+ color: var(--roxy-accent-fg, #b45309);
152
+ font-weight: var(--roxy-weight-bold, 600);
153
+ }
154
+
155
+ .legend {
156
+ display: flex;
157
+ flex-wrap: wrap;
158
+ gap: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem);
159
+ border-top: 1px solid var(--roxy-border, #e4e4e7);
160
+ padding-top: var(--roxy-space-sm, 0.5rem);
161
+ }
162
+
163
+ .legend-row {
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 6px;
167
+ font-size: var(--roxy-text-xs, 0.75rem);
168
+ color: var(--roxy-muted, #71717a);
169
+ }
170
+
171
+ .legend-swatch {
172
+ display: inline-block;
173
+ width: 10px;
174
+ height: 10px;
175
+ border-radius: var(--roxy-radius-sm, 4px);
176
+ flex-shrink: 0;
177
+ }
178
+
179
+ @container (max-width: 480px) {
180
+ .planet-row {
181
+ grid-template-columns: 6rem 1fr;
182
+ grid-template-rows: auto auto;
183
+ }
184
+ .pills {
185
+ grid-column: 1 / -1;
186
+ flex-direction: row;
187
+ align-items: center;
188
+ justify-content: flex-start;
189
+ }
190
+ }
191
+ `,
192
+ ];
193
+
194
+ @property({ attribute: false })
195
+ data: ShadbalaResponse | null = null;
196
+
197
+ render() {
198
+ if (!this.data?.planets?.length) {
199
+ return html`<div class="roxy-empty" role="status">No shadbala data</div>`;
200
+ }
201
+
202
+ const sorted = [...this.data.planets].sort(
203
+ (a, b) => a.relativeRank - b.relativeRank,
204
+ );
205
+
206
+ return html`<div class="wrap" aria-label="Shadbala planetary strength">
207
+ <div class="head">
208
+ <h2 class="title">Shadbala</h2>
209
+ <p class="subtitle">${sorted.length} planets ranked by strength</p>
210
+ </div>
211
+
212
+ <div role="list" aria-label="Planet strength bars">
213
+ ${sorted.map((p) => this.renderPlanetRow(p))}
214
+ </div>
215
+
216
+ <div class="legend" aria-label="Strength component legend">
217
+ ${BALA_COMPONENTS.map(
218
+ (b) => html`<div class="legend-row">
219
+ <span
220
+ class="legend-swatch"
221
+ style="background: ${b.color}"
222
+ aria-hidden="true"
223
+ ></span>
224
+ ${b.label}
225
+ </div>`,
226
+ )}
227
+ </div>
228
+ </div>`;
229
+ }
230
+
231
+ private renderPlanetRow(p: Planet) {
232
+ const glyph = PLANET_GLYPH[capitalize(p.planet)] ?? '';
233
+
234
+ // Compute positive component values (drikBala can be negative)
235
+ const values = BALA_COMPONENTS.map((b) => Math.max(0, p[b.key] as number));
236
+ const total = values.reduce((s, v) => s + v, 0);
237
+
238
+ const isAdequate =
239
+ typeof p.strengthRatio === 'number' && p.strengthRatio >= 1;
240
+ const badgeClass = isAdequate
241
+ ? 'adequacy-badge--adequate'
242
+ : 'adequacy-badge--weak';
243
+ const badgeLabel = isAdequate ? 'adequate' : 'weak';
244
+
245
+ const rupasStr =
246
+ formatNumber(p.totalRupas, 2) && formatNumber(p.minRequired, 2)
247
+ ? `${formatNumber(p.totalRupas, 2)} / ${formatNumber(p.minRequired, 2)} R`
248
+ : '';
249
+
250
+ return html`<div class="planet-row" role="listitem" aria-label="${p.planet} shadbala">
251
+ <div class="planet-label">
252
+ <span class="glyph" aria-hidden="true">${glyph}</span>
253
+ ${p.planet}
254
+ <span class="rank-badge" aria-label="rank ${p.relativeRank}">#${p.relativeRank}</span>
255
+ </div>
256
+ <div class="bar-wrap">
257
+ <div class="bar" role="img" aria-label="Strength components for ${p.planet}">
258
+ ${
259
+ total > 0
260
+ ? BALA_COMPONENTS.map((b, i) => {
261
+ const v = values[i];
262
+ if (v <= 0) return nothing;
263
+ const grow = (v / total) * 100;
264
+ return html`<div
265
+ class="bar-segment"
266
+ style="flex-grow: ${grow}; background: ${b.color};"
267
+ title="${b.label}: ${formatNumber(v, 1)}"
268
+ ></div>`;
269
+ })
270
+ : nothing
271
+ }
272
+ </div>
273
+ </div>
274
+ <div class="pills">
275
+ ${rupasStr ? html`<span class="rupas-label">${rupasStr}</span>` : nothing}
276
+ <span class="${`adequacy-badge ${badgeClass}`}">${badgeLabel}</span>
277
+ </div>
278
+ </div>`;
279
+ }
280
+ }
281
+
282
+ declare global {
283
+ interface HTMLElementTagNameMap {
284
+ 'roxy-shadbala-table': RoxyShadbalaTable;
285
+ }
286
+ }
@@ -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 {
5
5
  CalculateSynastryResponse,
6
6
  NatalChartResponse,
7
7
  } from '../types/index.js';
8
8
  import { baseStyles } from '../utils/base-styles.js';
9
9
  import { polarToCartesian } from '../utils/degree.js';
10
- import { formatNumber } from '../utils/format.js';
10
+ import {
11
+ ASPECT_CLASS,
12
+ formatNumber,
13
+ normalizeAspect,
14
+ } from '../utils/format.js';
15
+ import { capitalize } from '../utils/string.js';
11
16
 
12
17
  type PlanetEntry = NatalChartResponse['planets'][number];
13
18
  type InterAspect = CalculateSynastryResponse['interAspects'][number];
14
19
 
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
+ // 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.
20
25
  type SynastryWithPlanets = CalculateSynastryResponse & {
21
26
  person1?: { planets?: PlanetEntry[] };
22
27
  person2?: { planets?: PlanetEntry[] };
@@ -364,21 +369,7 @@ export class RoxySynastryChart extends LitElement {
364
369
  }
365
370
 
366
371
  private renderSigns() {
367
- const order = [
368
- 'Aries',
369
- 'Taurus',
370
- 'Gemini',
371
- 'Cancer',
372
- 'Leo',
373
- 'Virgo',
374
- 'Libra',
375
- 'Scorpio',
376
- 'Sagittarius',
377
- 'Capricorn',
378
- 'Aquarius',
379
- 'Pisces',
380
- ];
381
- return order.map((s, i) => {
372
+ return SIGNS_ORDER.map((s, i) => {
382
373
  const angle = this.toAngle(i * 30 + 15);
383
374
  const pos = polarToCartesian(CENTER, CENTER, SIGN_R, angle);
384
375
  return svg`<text class="sign" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central">${SIGN_GLYPH[s]}</text>`;
@@ -454,23 +445,6 @@ export class RoxySynastryChart extends LitElement {
454
445
  }
455
446
  }
456
447
 
457
- function capitalize(s: string): string {
458
- if (!s) return '';
459
- return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
460
- }
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
448
  function formatStrength(s: number | undefined): string {
475
449
  if (typeof s === 'number') return Math.round(s).toString();
476
450
  return '';
@@ -0,0 +1,350 @@
1
+ import { css, html, LitElement, nothing } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { PLANET_GLYPH, SIGN_GLYPH } from '../tokens/index.js';
4
+ import type { TransitsResponse } from '../types/index.js';
5
+ import { baseStyles } from '../utils/base-styles.js';
6
+ import { formatDate, formatNumber, formatTime } from '../utils/format.js';
7
+ import { capitalize } from '../utils/string.js';
8
+
9
+ /**
10
+ * Transit positions and aspect table. Pass `data` from /astrology/transits.
11
+ * When natalChart is included in the request, `data.transitAspects` and
12
+ * `data.summary` are present and rendered automatically.
13
+ */
14
+ @customElement('roxy-transits-table')
15
+ export class RoxyTransitsTable extends LitElement {
16
+ static styles = [
17
+ baseStyles,
18
+ css`
19
+ .wrap {
20
+ display: grid;
21
+ gap: var(--roxy-space-md, 1rem);
22
+ }
23
+
24
+ .head {
25
+ display: flex;
26
+ justify-content: space-between;
27
+ align-items: baseline;
28
+ gap: var(--roxy-space-md, 1rem);
29
+ flex-wrap: wrap;
30
+ }
31
+
32
+ .title {
33
+ font-size: var(--roxy-text-lg, 1.125rem);
34
+ font-weight: var(--roxy-weight-bold, 600);
35
+ margin: 0;
36
+ }
37
+
38
+ .subtitle {
39
+ color: var(--roxy-muted, #71717a);
40
+ font-size: var(--roxy-text-sm, 0.875rem);
41
+ margin: 0;
42
+ }
43
+
44
+ .summary-pills {
45
+ display: flex;
46
+ flex-wrap: wrap;
47
+ gap: var(--roxy-space-sm, 0.5rem);
48
+ }
49
+
50
+ .pill {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ gap: 4px;
54
+ padding: 2px var(--roxy-space-sm, 0.5rem);
55
+ border-radius: var(--roxy-radius-full, 9999px);
56
+ font-size: var(--roxy-text-xs, 0.75rem);
57
+ font-weight: var(--roxy-weight-bold, 600);
58
+ border: 1px solid currentColor;
59
+ }
60
+
61
+ .pill--muted {
62
+ color: var(--roxy-fg, #0a0a0a);
63
+ background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 60%, transparent);
64
+ }
65
+
66
+ .pill--success {
67
+ color: var(--roxy-success-fg, #166534);
68
+ background: color-mix(in srgb, var(--roxy-success, #16a34a) 10%, transparent);
69
+ }
70
+
71
+ .pill--danger {
72
+ color: var(--roxy-danger-fg, #991b1b);
73
+ background: color-mix(in srgb, var(--roxy-danger, #dc2626) 10%, transparent);
74
+ }
75
+
76
+ table {
77
+ width: 100%;
78
+ border-collapse: collapse;
79
+ font-size: var(--roxy-text-sm, 0.875rem);
80
+ }
81
+
82
+ th,
83
+ td {
84
+ padding: var(--roxy-space-sm, 0.5rem);
85
+ border-bottom: 1px solid var(--roxy-border, #e4e4e7);
86
+ text-align: left;
87
+ }
88
+
89
+ th {
90
+ color: var(--roxy-muted, #71717a);
91
+ font-weight: var(--roxy-weight-bold, 600);
92
+ text-transform: uppercase;
93
+ font-size: var(--roxy-text-xs, 0.75rem);
94
+ letter-spacing: 0.06em;
95
+ }
96
+
97
+ .section-label {
98
+ font-size: var(--roxy-text-xs, 0.75rem);
99
+ color: var(--roxy-muted, #71717a);
100
+ text-transform: uppercase;
101
+ letter-spacing: 0.06em;
102
+ font-weight: var(--roxy-weight-bold, 600);
103
+ margin: 0 0 var(--roxy-space-xs, 0.25rem) 0;
104
+ }
105
+
106
+ .glyph {
107
+ font-size: 1.1em;
108
+ margin-right: 2px;
109
+ line-height: 1;
110
+ }
111
+
112
+ .planet-cell {
113
+ display: flex;
114
+ align-items: center;
115
+ gap: 4px;
116
+ white-space: nowrap;
117
+ }
118
+
119
+ .retro-badge {
120
+ display: inline-block;
121
+ font-size: 0.7em;
122
+ padding: 1px 4px;
123
+ border-radius: var(--roxy-radius-sm, 4px);
124
+ background: color-mix(in srgb, var(--roxy-warning, #ea580c) 12%, transparent);
125
+ color: var(--roxy-warning-fg, #9a3412);
126
+ font-weight: var(--roxy-weight-bold, 600);
127
+ margin-left: 2px;
128
+ vertical-align: middle;
129
+ }
130
+
131
+ .speed {
132
+ font-variant-numeric: tabular-nums;
133
+ color: var(--roxy-muted, #71717a);
134
+ white-space: nowrap;
135
+ }
136
+
137
+ .speed-arrow {
138
+ font-size: 0.85em;
139
+ }
140
+
141
+ td.num {
142
+ font-variant-numeric: tabular-nums;
143
+ color: var(--roxy-muted, #71717a);
144
+ }
145
+
146
+ .nature-harmonious {
147
+ color: var(--roxy-success-fg, #166534);
148
+ }
149
+
150
+ .nature-challenging {
151
+ color: var(--roxy-danger-fg, #991b1b);
152
+ }
153
+
154
+ .nature-neutral {
155
+ color: var(--roxy-muted, #71717a);
156
+ }
157
+
158
+ .arrow-cell {
159
+ display: inline-flex;
160
+ align-items: center;
161
+ gap: 4px;
162
+ white-space: nowrap;
163
+ }
164
+
165
+ .interp {
166
+ color: var(--roxy-secondary, #475569);
167
+ font-size: var(--roxy-text-xs, 0.75rem);
168
+ max-width: 22rem;
169
+ white-space: nowrap;
170
+ overflow: hidden;
171
+ text-overflow: ellipsis;
172
+ }
173
+
174
+ @container (max-width: 600px) {
175
+ .interp {
176
+ display: none;
177
+ }
178
+ }
179
+
180
+ .overflow-scroll {
181
+ overflow-x: auto;
182
+ -webkit-overflow-scrolling: touch;
183
+ }
184
+ `,
185
+ ];
186
+
187
+ @property({ attribute: false })
188
+ data: TransitsResponse | null = null;
189
+
190
+ render() {
191
+ if (!this.data?.transitPlanets?.length) {
192
+ return html`<div class="roxy-empty" role="status">No transits data</div>`;
193
+ }
194
+
195
+ const {
196
+ transitDate,
197
+ transitTime,
198
+ transitPlanets,
199
+ transitAspects,
200
+ summary,
201
+ } = this.data;
202
+
203
+ const dateStr = [formatDate(transitDate), formatTime(transitTime)]
204
+ .filter(Boolean)
205
+ .join(' ');
206
+
207
+ return html`<div class="wrap" aria-label="Transit positions table">
208
+ <div class="head">
209
+ <h2 class="title">Transits</h2>
210
+ ${dateStr ? html`<p class="subtitle">${dateStr}</p>` : nothing}
211
+ </div>
212
+
213
+ ${summary ? this.renderSummaryPills(summary) : nothing}
214
+
215
+ <div>
216
+ <p class="section-label">Planet positions</p>
217
+ <div class="overflow-scroll">
218
+ ${this.renderPlanetsTable(transitPlanets)}
219
+ </div>
220
+ </div>
221
+
222
+ ${
223
+ transitAspects?.length
224
+ ? html`<div>
225
+ <p class="section-label">Transit aspects</p>
226
+ <div class="overflow-scroll">
227
+ ${this.renderAspectsTable(transitAspects)}
228
+ </div>
229
+ </div>`
230
+ : nothing
231
+ }
232
+ </div>`;
233
+ }
234
+
235
+ private renderSummaryPills(
236
+ summary: NonNullable<TransitsResponse['summary']>,
237
+ ) {
238
+ return html`<div class="summary-pills" role="region" aria-label="Aspect summary">
239
+ <span class="pill pill--muted">
240
+ Total: ${summary.totalAspects}
241
+ </span>
242
+ <span class="pill pill--success">
243
+ Harmonious: ${summary.harmonious}
244
+ </span>
245
+ <span class="pill pill--danger">
246
+ Challenging: ${summary.challenging}
247
+ </span>
248
+ <span class="pill pill--muted">
249
+ Neutral: ${summary.neutral}
250
+ </span>
251
+ </div>`;
252
+ }
253
+
254
+ private renderPlanetsTable(planets: TransitsResponse['transitPlanets']) {
255
+ return html`<table class="planets-table">
256
+ <thead>
257
+ <tr>
258
+ <th scope="col">Planet</th>
259
+ <th scope="col">Sign</th>
260
+ <th scope="col">Degree</th>
261
+ <th scope="col">Speed</th>
262
+ </tr>
263
+ </thead>
264
+ <tbody>
265
+ ${planets.map((p) => {
266
+ const pGlyph = PLANET_GLYPH[capitalize(p.name)] ?? '';
267
+ const sGlyph = SIGN_GLYPH[capitalize(p.sign)] ?? '';
268
+ const speedArrow = p.speed >= 0 ? '↑' : '↓';
269
+ return html`<tr>
270
+ <td>
271
+ <div class="planet-cell">
272
+ <span class="glyph" aria-hidden="true">${pGlyph}</span>
273
+ ${p.name}
274
+ ${
275
+ p.isRetrograde
276
+ ? html`<span class="retro-badge" aria-label="retrograde">R</span>`
277
+ : nothing
278
+ }
279
+ </div>
280
+ </td>
281
+ <td>
282
+ <div class="planet-cell">
283
+ <span class="glyph" aria-hidden="true">${sGlyph}</span>
284
+ ${p.sign}
285
+ </div>
286
+ </td>
287
+ <td class="num">${formatNumber(p.degree, 2)}</td>
288
+ <td class="speed">
289
+ <span class="speed-arrow" aria-hidden="true">${speedArrow}</span>
290
+ ${formatNumber(Math.abs(p.speed), 4)}
291
+ </td>
292
+ </tr>`;
293
+ })}
294
+ </tbody>
295
+ </table>`;
296
+ }
297
+
298
+ private renderAspectsTable(
299
+ aspects: NonNullable<TransitsResponse['transitAspects']>,
300
+ ) {
301
+ return html`<table class="aspects-table">
302
+ <thead>
303
+ <tr>
304
+ <th scope="col">Transit Planet</th>
305
+ <th scope="col">Natal Planet</th>
306
+ <th scope="col">Type</th>
307
+ <th scope="col">Orb</th>
308
+ <th scope="col">Status</th>
309
+ <th scope="col">Strength</th>
310
+ <th scope="col" class="interp">Interpretation</th>
311
+ </tr>
312
+ </thead>
313
+ <tbody>
314
+ ${aspects.map((a) => {
315
+ const tGlyph = PLANET_GLYPH[capitalize(a.transitPlanet)] ?? '';
316
+ const nGlyph = PLANET_GLYPH[capitalize(a.natalPlanet)] ?? '';
317
+ const natureClass = `nature-${(a.nature ?? '').toLowerCase()}`;
318
+ const summary = a.interpretation?.summary ?? '';
319
+ const truncated =
320
+ summary.length > 120 ? `${summary.slice(0, 120)}...` : summary;
321
+ return html`<tr>
322
+ <td>
323
+ <div class="arrow-cell">
324
+ <span class="glyph" aria-hidden="true">${tGlyph}</span>
325
+ ${a.transitPlanet}
326
+ </div>
327
+ </td>
328
+ <td>
329
+ <div class="arrow-cell">
330
+ <span class="glyph" aria-hidden="true">${nGlyph}</span>
331
+ ${a.natalPlanet}
332
+ </div>
333
+ </td>
334
+ <td class=${natureClass}>${(a.type ?? '').toLowerCase()}</td>
335
+ <td class="num">${formatNumber(a.orb, 2)}</td>
336
+ <td>${a.isApplying ? 'Applying' : 'Separating'}</td>
337
+ <td class="num">${formatNumber(a.strength, 1)}</td>
338
+ <td class="interp" title=${summary}>${truncated}</td>
339
+ </tr>`;
340
+ })}
341
+ </tbody>
342
+ </table>`;
343
+ }
344
+ }
345
+
346
+ declare global {
347
+ interface HTMLElementTagNameMap {
348
+ 'roxy-transits-table': RoxyTransitsTable;
349
+ }
350
+ }