@roxyapi/ui 0.3.1 → 0.4.1

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 (165) hide show
  1. package/AGENTS.md +34 -7
  2. package/README.md +145 -26
  3. package/dist/cdn/components/ashtakavarga-grid.js +74 -19
  4. package/dist/cdn/components/ashtakavarga-grid.js.map +2 -2
  5. package/dist/cdn/components/biorhythm-chart.js +18 -4
  6. package/dist/cdn/components/biorhythm-chart.js.map +2 -2
  7. package/dist/cdn/components/choghadiya-grid.js +47 -12
  8. package/dist/cdn/components/choghadiya-grid.js.map +3 -3
  9. package/dist/cdn/components/compatibility-card.js +21 -7
  10. package/dist/cdn/components/compatibility-card.js.map +2 -2
  11. package/dist/cdn/components/dasha-timeline.js +113 -28
  12. package/dist/cdn/components/dasha-timeline.js.map +3 -3
  13. package/dist/cdn/components/data.js +27 -13
  14. package/dist/cdn/components/data.js.map +2 -2
  15. package/dist/cdn/components/divisional-chart.js +225 -118
  16. package/dist/cdn/components/divisional-chart.js.map +4 -4
  17. package/dist/cdn/components/dosha-card.js +18 -4
  18. package/dist/cdn/components/dosha-card.js.map +2 -2
  19. package/dist/cdn/components/endpoint-form.js +25 -11
  20. package/dist/cdn/components/endpoint-form.js.map +2 -2
  21. package/dist/cdn/components/guna-milan.js +20 -6
  22. package/dist/cdn/components/guna-milan.js.map +2 -2
  23. package/dist/cdn/components/hexagram.js +22 -8
  24. package/dist/cdn/components/hexagram.js.map +2 -2
  25. package/dist/cdn/components/horoscope-card.js +20 -6
  26. package/dist/cdn/components/horoscope-card.js.map +2 -2
  27. package/dist/cdn/components/kp-chart.js +19 -5
  28. package/dist/cdn/components/kp-chart.js.map +2 -2
  29. package/dist/cdn/components/kp-planets-table.js +17 -3
  30. package/dist/cdn/components/kp-planets-table.js.map +2 -2
  31. package/dist/cdn/components/kp-ruling-planets.js +17 -3
  32. package/dist/cdn/components/kp-ruling-planets.js.map +2 -2
  33. package/dist/cdn/components/location-search.js +18 -4
  34. package/dist/cdn/components/location-search.js.map +2 -2
  35. package/dist/cdn/components/moon-phase.js +27 -13
  36. package/dist/cdn/components/moon-phase.js.map +2 -2
  37. package/dist/cdn/components/nakshatra-card.js +16 -2
  38. package/dist/cdn/components/nakshatra-card.js.map +2 -2
  39. package/dist/cdn/components/natal-chart.js +79 -40
  40. package/dist/cdn/components/natal-chart.js.map +3 -3
  41. package/dist/cdn/components/numerology-card.js +18 -4
  42. package/dist/cdn/components/numerology-card.js.map +2 -2
  43. package/dist/cdn/components/panchang-table.js +53 -25
  44. package/dist/cdn/components/panchang-table.js.map +3 -3
  45. package/dist/cdn/components/shadbala-table.js +24 -10
  46. package/dist/cdn/components/shadbala-table.js.map +2 -2
  47. package/dist/cdn/components/synastry-chart.js +96 -48
  48. package/dist/cdn/components/synastry-chart.js.map +3 -3
  49. package/dist/cdn/components/tarot-card.js +17 -3
  50. package/dist/cdn/components/tarot-card.js.map +2 -2
  51. package/dist/cdn/components/tarot-spread.js +39 -25
  52. package/dist/cdn/components/tarot-spread.js.map +2 -2
  53. package/dist/cdn/components/transits-table.js +18 -4
  54. package/dist/cdn/components/transits-table.js.map +2 -2
  55. package/dist/cdn/components/vedic-kundli.js +215 -105
  56. package/dist/cdn/components/vedic-kundli.js.map +4 -4
  57. package/dist/cdn/components/vedic-planets-table.js +22 -8
  58. package/dist/cdn/components/vedic-planets-table.js.map +2 -2
  59. package/dist/cdn/components/western-planets-table.js +18 -4
  60. package/dist/cdn/components/western-planets-table.js.map +2 -2
  61. package/dist/cdn/components/yoga-list.js +17 -3
  62. package/dist/cdn/components/yoga-list.js.map +2 -2
  63. package/dist/cdn/roxy-ui.js +1082 -816
  64. package/dist/cdn/roxy-ui.js.map +4 -4
  65. package/dist/components/ashtakavarga-grid.d.ts +13 -1
  66. package/dist/components/ashtakavarga-grid.d.ts.map +1 -1
  67. package/dist/components/ashtakavarga-grid.js +86 -11
  68. package/dist/components/ashtakavarga-grid.js.map +2 -2
  69. package/dist/components/biorhythm-chart.js +14 -0
  70. package/dist/components/biorhythm-chart.js.map +2 -2
  71. package/dist/components/choghadiya-grid.d.ts +6 -0
  72. package/dist/components/choghadiya-grid.d.ts.map +1 -1
  73. package/dist/components/choghadiya-grid.js +50 -2
  74. package/dist/components/choghadiya-grid.js.map +2 -2
  75. package/dist/components/compatibility-card.js +14 -0
  76. package/dist/components/compatibility-card.js.map +2 -2
  77. package/dist/components/dasha-timeline.d.ts +10 -0
  78. package/dist/components/dasha-timeline.d.ts.map +1 -1
  79. package/dist/components/dasha-timeline.js +135 -4
  80. package/dist/components/dasha-timeline.js.map +2 -2
  81. package/dist/components/data.js +14 -0
  82. package/dist/components/data.js.map +2 -2
  83. package/dist/components/divisional-chart.d.ts +9 -6
  84. package/dist/components/divisional-chart.d.ts.map +1 -1
  85. package/dist/components/divisional-chart.js +546 -251
  86. package/dist/components/divisional-chart.js.map +4 -4
  87. package/dist/components/dosha-card.js +14 -0
  88. package/dist/components/dosha-card.js.map +2 -2
  89. package/dist/components/endpoint-form.js +14 -0
  90. package/dist/components/endpoint-form.js.map +2 -2
  91. package/dist/components/guna-milan.js +14 -0
  92. package/dist/components/guna-milan.js.map +2 -2
  93. package/dist/components/hexagram.js +14 -0
  94. package/dist/components/hexagram.js.map +2 -2
  95. package/dist/components/horoscope-card.js +14 -0
  96. package/dist/components/horoscope-card.js.map +2 -2
  97. package/dist/components/kp-chart.js +14 -0
  98. package/dist/components/kp-chart.js.map +2 -2
  99. package/dist/components/kp-planets-table.js +14 -0
  100. package/dist/components/kp-planets-table.js.map +2 -2
  101. package/dist/components/kp-ruling-planets.js +14 -0
  102. package/dist/components/kp-ruling-planets.js.map +2 -2
  103. package/dist/components/location-search.js +14 -0
  104. package/dist/components/location-search.js.map +2 -2
  105. package/dist/components/moon-phase.js +14 -0
  106. package/dist/components/moon-phase.js.map +2 -2
  107. package/dist/components/nakshatra-card.js +14 -0
  108. package/dist/components/nakshatra-card.js.map +2 -2
  109. package/dist/components/natal-chart.d.ts.map +1 -1
  110. package/dist/components/natal-chart.js +76 -6
  111. package/dist/components/natal-chart.js.map +2 -2
  112. package/dist/components/numerology-card.js +14 -0
  113. package/dist/components/numerology-card.js.map +2 -2
  114. package/dist/components/panchang-table.d.ts +1 -0
  115. package/dist/components/panchang-table.d.ts.map +1 -1
  116. package/dist/components/panchang-table.js +37 -1
  117. package/dist/components/panchang-table.js.map +2 -2
  118. package/dist/components/shadbala-table.js +14 -0
  119. package/dist/components/shadbala-table.js.map +2 -2
  120. package/dist/components/synastry-chart.d.ts +6 -0
  121. package/dist/components/synastry-chart.d.ts.map +1 -1
  122. package/dist/components/synastry-chart.js +106 -7
  123. package/dist/components/synastry-chart.js.map +2 -2
  124. package/dist/components/tarot-card.js +14 -0
  125. package/dist/components/tarot-card.js.map +2 -2
  126. package/dist/components/tarot-spread.js +14 -0
  127. package/dist/components/tarot-spread.js.map +2 -2
  128. package/dist/components/transits-table.js +14 -0
  129. package/dist/components/transits-table.js.map +2 -2
  130. package/dist/components/vedic-kundli.d.ts +14 -9
  131. package/dist/components/vedic-kundli.d.ts.map +1 -1
  132. package/dist/components/vedic-kundli.js +537 -245
  133. package/dist/components/vedic-kundli.js.map +4 -4
  134. package/dist/components/vedic-planets-table.js +14 -0
  135. package/dist/components/vedic-planets-table.js.map +2 -2
  136. package/dist/components/western-planets-table.js +14 -0
  137. package/dist/components/western-planets-table.js.map +2 -2
  138. package/dist/components/yoga-list.js +14 -0
  139. package/dist/components/yoga-list.js.map +2 -2
  140. package/dist/index.cjs +1397 -797
  141. package/dist/index.cjs.map +4 -4
  142. package/dist/index.js +1278 -678
  143. package/dist/index.js.map +4 -4
  144. package/dist/manifest.json +23 -23
  145. package/dist/styles/tokens.css +8 -23
  146. package/dist/utils/base-styles.d.ts.map +1 -1
  147. package/dist/utils/kundli-render.d.ts +43 -104
  148. package/dist/utils/kundli-render.d.ts.map +1 -1
  149. package/dist/utils/kundli-styles.d.ts +13 -0
  150. package/dist/utils/kundli-styles.d.ts.map +1 -0
  151. package/dist/version.d.ts +1 -1
  152. package/package.json +1 -1
  153. package/src/components/ashtakavarga-grid.ts +73 -11
  154. package/src/components/choghadiya-grid.ts +37 -2
  155. package/src/components/dasha-timeline.ts +135 -4
  156. package/src/components/divisional-chart.ts +40 -97
  157. package/src/components/natal-chart.ts +89 -6
  158. package/src/components/panchang-table.ts +34 -1
  159. package/src/components/synastry-chart.ts +84 -8
  160. package/src/components/vedic-kundli.ts +35 -95
  161. package/src/styles/tokens.css +8 -23
  162. package/src/utils/base-styles.ts +14 -0
  163. package/src/utils/kundli-render.ts +609 -270
  164. package/src/utils/kundli-styles.ts +124 -0
  165. package/src/version.ts +1 -1
@@ -68,6 +68,13 @@ export class RoxyDashaTimeline extends LitElement {
68
68
  color: var(--roxy-fg, #0a0a0a);
69
69
  }
70
70
 
71
+ .balance {
72
+ font-size: var(--roxy-text-sm, 0.875rem);
73
+ color: var(--roxy-muted, #71717a);
74
+ border-left: 2px solid var(--roxy-border, #e4e4e7);
75
+ padding-left: var(--roxy-space-sm, 0.5rem);
76
+ margin: 0;
77
+ }
71
78
  .timeline {
72
79
  display: grid;
73
80
  gap: var(--roxy-space-xs, 0.25rem);
@@ -79,26 +86,69 @@ export class RoxyDashaTimeline extends LitElement {
79
86
  align-items: center;
80
87
  font-size: var(--roxy-text-sm, 0.875rem);
81
88
  }
89
+ .bar.now strong {
90
+ color: var(--roxy-accent-fg, #b45309);
91
+ }
92
+ .now-badge {
93
+ display: inline-block;
94
+ margin-left: 0.4em;
95
+ font-size: var(--roxy-text-xs, 0.75rem);
96
+ font-weight: var(--roxy-weight-bold, 600);
97
+ color: var(--roxy-accent-fg, #b45309);
98
+ text-transform: uppercase;
99
+ letter-spacing: 0.06em;
100
+ }
82
101
  .bar-track {
102
+ position: relative;
83
103
  height: 14px;
84
104
  background: var(--roxy-border, #e4e4e7);
85
105
  border-radius: var(--roxy-radius-full, 9999px);
86
106
  overflow: hidden;
87
107
  }
88
- .bar-track > span {
108
+ .bar-fill {
89
109
  display: block;
90
110
  height: 100%;
91
111
  background: var(--roxy-accent, #f59e0b);
112
+ opacity: 0.45;
92
113
  transition:
93
114
  width var(--roxy-motion-duration, 200ms)
94
115
  var(--roxy-motion-easing, cubic-bezier(0.4, 0, 0.2, 1));
95
116
  }
117
+ .bar-now .bar-fill {
118
+ opacity: 1;
119
+ }
120
+ .bar-progress {
121
+ position: absolute;
122
+ top: -2px;
123
+ bottom: -2px;
124
+ width: 2px;
125
+ background: var(--roxy-accent-fg, #b45309);
126
+ border-radius: 2px;
127
+ box-shadow: 0 0 0 2px
128
+ color-mix(in srgb, var(--roxy-accent, #f59e0b) 35%, transparent);
129
+ }
96
130
  .dates {
97
131
  color: var(--roxy-muted, #71717a);
98
132
  font-size: var(--roxy-text-xs, 0.75rem);
99
133
  font-variant-numeric: tabular-nums;
100
134
  text-align: right;
101
135
  }
136
+ details.interp {
137
+ border: 1px solid var(--roxy-border, #e4e4e7);
138
+ border-radius: var(--roxy-radius-md, 8px);
139
+ padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem);
140
+ background: var(--roxy-bg, #fff);
141
+ }
142
+ details.interp summary {
143
+ cursor: pointer;
144
+ font-size: var(--roxy-text-sm, 0.875rem);
145
+ font-weight: var(--roxy-weight-bold, 600);
146
+ }
147
+ details.interp p {
148
+ margin: var(--roxy-space-sm, 0.5rem) 0 0;
149
+ font-size: var(--roxy-text-sm, 0.875rem);
150
+ color: var(--roxy-muted, #71717a);
151
+ }
102
152
  `,
103
153
  ];
104
154
 
@@ -139,6 +189,7 @@ export class RoxyDashaTimeline extends LitElement {
139
189
  }
140
190
  </header>
141
191
 
192
+ ${this.renderBirthBalance(d)}
142
193
  ${this.period === 'current' ? this.renderCurrent(d) : nothing}
143
194
  ${
144
195
  periods.length > 0
@@ -147,9 +198,37 @@ export class RoxyDashaTimeline extends LitElement {
147
198
  </div>`
148
199
  : nothing
149
200
  }
201
+ ${this.renderActiveInterpretation(periods)}
150
202
  </div>`;
151
203
  }
152
204
 
205
+ private renderBirthBalance(d: DashaData) {
206
+ if (!('birthDashaBalance' in d) || !d.birthDashaBalance) return nothing;
207
+ const b = d.birthDashaBalance;
208
+ const lord = 'nakshatraLord' in d && d.nakshatraLord ? d.nakshatraLord : '';
209
+ const yrs = b.years ?? 0;
210
+ const mo = b.months ?? 0;
211
+ const da = b.days ?? 0;
212
+ const parts: string[] = [];
213
+ if (yrs) parts.push(`${yrs}y`);
214
+ if (mo) parts.push(`${mo}m`);
215
+ if (da) parts.push(`${da}d`);
216
+ const remaining = parts.length ? parts.join(' ') : '0d';
217
+ return html`<p class="balance">
218
+ Birth dasha balance: ${remaining} of
219
+ ${lord ? html`<strong>${lord}</strong>` : 'the opening mahadasha'} remained at birth.
220
+ </p>`;
221
+ }
222
+
223
+ private renderActiveInterpretation(periods: DashaPeriod[]) {
224
+ const active = periods.find((p) => this.isCurrent(p));
225
+ if (!active?.interpretation) return nothing;
226
+ return html`<details class="interp">
227
+ <summary>${active.planet} mahadasha interpretation</summary>
228
+ <p>${active.interpretation}</p>
229
+ </details>`;
230
+ }
231
+
153
232
  private renderCurrent(d: DashaData) {
154
233
  if (!('mahadasha' in d)) return nothing;
155
234
  return html`<div class="current">
@@ -201,12 +280,64 @@ export class RoxyDashaTimeline extends LitElement {
201
280
  return [];
202
281
  }
203
282
 
283
+ /** True when the current wall-clock time falls between the period's start and end. */
284
+ private isCurrent(p: DashaPeriod): boolean {
285
+ if (!p.startDate || !p.endDate) return false;
286
+ const now = Date.now();
287
+ const start = Date.parse(p.startDate);
288
+ const end = Date.parse(p.endDate);
289
+ if (Number.isNaN(start) || Number.isNaN(end)) return false;
290
+ return now >= start && now < end;
291
+ }
292
+
293
+ /**
294
+ * Fractional progress (0..1) through a period at the current time. Used to
295
+ * draw a vertical "now" marker inside the active bar. Returns -1 outside the
296
+ * period so the caller can skip the marker.
297
+ */
298
+ private progressIn(p: DashaPeriod): number {
299
+ if (!p.startDate || !p.endDate) return -1;
300
+ const start = Date.parse(p.startDate);
301
+ const end = Date.parse(p.endDate);
302
+ const now = Date.now();
303
+ if (
304
+ Number.isNaN(start) ||
305
+ Number.isNaN(end) ||
306
+ now < start ||
307
+ now >= end ||
308
+ end <= start
309
+ ) {
310
+ return -1;
311
+ }
312
+ return (now - start) / (end - start);
313
+ }
314
+
204
315
  private renderBar(p: DashaPeriod, max: number) {
205
316
  const years = p.durationYears;
206
317
  const width = max > 0 ? (years / max) * 100 : 0;
207
- return html`<div class="bar" role="listitem">
208
- <span>${p.planet}</span>
209
- <span class="bar-track"><span style="width: ${width}%"></span></span>
318
+ const current = this.isCurrent(p);
319
+ const progress = current ? this.progressIn(p) : -1;
320
+ const trackClass = current ? 'bar-track bar-now' : 'bar-track';
321
+ return html`<div
322
+ class=${current ? 'bar now' : 'bar'}
323
+ role="listitem"
324
+ aria-current=${current ? 'time' : 'false'}
325
+ >
326
+ <span>
327
+ <strong>${p.planet}</strong>${current ? html`<span class="now-badge">Now</span>` : nothing}
328
+ </span>
329
+ <span class=${trackClass}>
330
+ <span class="bar-fill" style="width: ${width}%"></span>
331
+ ${
332
+ progress >= 0
333
+ ? html`<span
334
+ class="bar-progress"
335
+ style="left: ${progress * width}%"
336
+ aria-hidden="true"
337
+ ></span>`
338
+ : nothing
339
+ }
340
+ </span>
210
341
  <span class="dates">
211
342
  ${p.startDate ? formatYear(p.startDate) : ''}
212
343
  ${p.endDate ? html`- ${formatYear(p.endDate)}` : ''}
@@ -4,41 +4,28 @@ import { PLANET_GLYPH } from '../tokens/index.js';
4
4
  import type { DivisionalChartResponse } from '../types/index.js';
5
5
  import { baseStyles } from '../utils/base-styles.js';
6
6
  import {
7
- buildHousesFromMeta,
8
- type HouseDef,
9
- renderEastFrame,
10
- renderEastHouseGroup,
11
- renderNorthFrame,
12
- renderNorthHouseGroup,
13
- renderSouthFrame,
14
- renderSouthHouseGroup,
7
+ type ChartStyle,
8
+ type KundliViewModel,
9
+ renderKundliStyleTablist,
10
+ renderKundliSvg,
11
+ toKundliViewModel,
15
12
  } from '../utils/kundli-render.js';
13
+ import { kundliStyles } from '../utils/kundli-styles.js';
16
14
 
17
15
  /**
18
16
  * Divisional chart renderer (D2-D60). Accepts a DivisionalChartResponse and
19
- * renders the same south/north/east kundli wheel as the birth chart, plus
20
- * division metadata and Vargottama planet pills. The varga response carries a
21
- * graha-keyed `chart.meta` map (no per-rashi buckets), so houses are bucketed
22
- * from that map.
17
+ * renders the same South / North / East kundli grid as the birth chart, plus
18
+ * division metadata and Vargottama planet pills. A visible tablist lets the
19
+ * end user switch styles at runtime. The varga response carries a graha-keyed
20
+ * `chart.meta` map (no per-rashi buckets), so houses are bucketed from that
21
+ * map.
23
22
  */
24
23
  @customElement('roxy-divisional-chart')
25
24
  export class RoxyDivisionalChart extends LitElement {
26
25
  static styles = [
27
26
  baseStyles,
27
+ kundliStyles,
28
28
  css`
29
- .wrap {
30
- display: grid;
31
- gap: var(--roxy-space-md, 1rem);
32
- }
33
- .header {
34
- display: grid;
35
- gap: var(--roxy-space-xs, 0.25rem);
36
- }
37
- .title {
38
- font-size: var(--roxy-text-lg, 1.125rem);
39
- font-weight: var(--roxy-weight-bold, 600);
40
- margin: 0;
41
- }
42
29
  .division-meta {
43
30
  font-size: var(--roxy-text-sm, 0.875rem);
44
31
  color: var(--roxy-muted, #71717a);
@@ -51,46 +38,6 @@ export class RoxyDivisionalChart extends LitElement {
51
38
  padding-left: var(--roxy-space-sm, 0.5rem);
52
39
  margin: 0;
53
40
  }
54
- svg {
55
- display: block;
56
- width: 100%;
57
- max-width: 360px;
58
- margin: 0 auto;
59
- }
60
- .line {
61
- fill: transparent;
62
- stroke: var(--roxy-border, #e4e4e7);
63
- }
64
- .sign-text {
65
- fill: var(--roxy-muted, #71717a);
66
- font-size: 9px;
67
- font-weight: 500;
68
- font-family: var(--roxy-font-sans);
69
- }
70
- .planet-text {
71
- fill: var(--roxy-fg, #0a0a0a);
72
- font-size: 11px;
73
- font-weight: 600;
74
- font-family: var(--roxy-font-sans);
75
- }
76
- .house-num {
77
- fill: var(--roxy-muted, #71717a);
78
- font-size: 9px;
79
- font-weight: 400;
80
- font-family: var(--roxy-font-sans);
81
- }
82
- .lagna-marker {
83
- fill: var(--roxy-accent-fg, #b45309);
84
- font-size: 8px;
85
- font-weight: 700;
86
- font-family: var(--roxy-font-sans);
87
- letter-spacing: 0.05em;
88
- }
89
- .lagna-bg {
90
- fill: color-mix(in srgb, var(--roxy-accent, #f59e0b) 12%, transparent);
91
- stroke: color-mix(in srgb, var(--roxy-accent, #f59e0b) 45%, transparent);
92
- stroke-width: 0.8;
93
- }
94
41
  .vargottama-row {
95
42
  display: flex;
96
43
  flex-wrap: wrap;
@@ -122,58 +69,54 @@ export class RoxyDivisionalChart extends LitElement {
122
69
  data: DivisionalChartResponse | null = null;
123
70
 
124
71
  @property({ type: String, reflect: true, attribute: 'chart-style' })
125
- chartStyle: 'south' | 'north' | 'east' = 'south';
72
+ chartStyle: ChartStyle = 'north';
73
+
74
+ private setStyle = (next: ChartStyle) => {
75
+ this.chartStyle = next;
76
+ };
126
77
 
127
- private buildHouses(): HouseDef[] {
128
- if (!this.data?.chart?.meta) return [];
129
- return buildHousesFromMeta(this.data.chart.meta);
78
+ private viewModel(): KundliViewModel | null {
79
+ if (!this.data?.chart?.meta) return null;
80
+ const { division } = this.data;
81
+ const label = `D${division.number} ${division.name}`;
82
+ return toKundliViewModel(this.data.chart.meta, label);
130
83
  }
131
84
 
132
85
  render() {
133
- if (!this.data)
86
+ const vm = this.viewModel();
87
+ if (!this.data || !vm)
134
88
  return html`<div class="roxy-empty" role="status">No divisional chart data</div>`;
135
89
 
136
90
  const { division, vargottama } = this.data;
137
- const houses = this.buildHouses();
138
- const style = this.chartStyle;
139
- const frame =
140
- style === 'north'
141
- ? renderNorthFrame()
142
- : style === 'east'
143
- ? renderEastFrame()
144
- : renderSouthFrame();
145
- const houseGroup =
146
- style === 'north'
147
- ? renderNorthHouseGroup
148
- : style === 'east'
149
- ? renderEastHouseGroup
150
- : renderSouthHouseGroup;
151
91
 
152
92
  return html`<div class="wrap">
153
93
  <div class="header">
154
- <h2 class="title">
155
- D${division.number} ${division.name}
94
+ <div>
95
+ <h2 class="title">
96
+ D${division.number} ${division.name}
97
+ ${
98
+ division.sanskritName && division.sanskritName !== division.name
99
+ ? html`<span class="division-meta"> · ${division.sanskritName}</span>`
100
+ : nothing
101
+ }
102
+ </h2>
156
103
  ${
157
- division.sanskritName && division.sanskritName !== division.name
158
- ? html`<span class="division-meta"> · ${division.sanskritName}</span>`
104
+ division.significance
105
+ ? html`<p class="significance">${division.significance}</p>`
159
106
  : nothing
160
107
  }
161
- </h2>
162
- ${
163
- division.significance
164
- ? html`<p class="significance">${division.significance}</p>`
165
- : nothing
166
- }
108
+ </div>
109
+ ${renderKundliStyleTablist(this.chartStyle, this.setStyle)}
167
110
  </div>
168
111
 
169
112
  <svg
170
- viewBox="0 0 300 300"
113
+ viewBox="0 0 400 400"
114
+ preserveAspectRatio="xMidYMid meet"
171
115
  role="img"
172
116
  aria-label="D${division.number} ${division.name} divisional chart with twelve sign houses"
173
117
  >
174
118
  <title>D${division.number} ${division.name}</title>
175
- ${frame}
176
- ${houses.map((h) => houseGroup(h))}
119
+ ${renderKundliSvg(vm, this.chartStyle)}
177
120
  </svg>
178
121
 
179
122
  ${
@@ -64,7 +64,8 @@ export class RoxyNatalChart extends LitElement {
64
64
  svg {
65
65
  display: block;
66
66
  width: 100%;
67
- max-width: 360px;
67
+ max-width: 560px;
68
+ aspect-ratio: 1 / 1;
68
69
  height: auto;
69
70
  margin: 0 auto;
70
71
  }
@@ -93,10 +94,33 @@ export class RoxyNatalChart extends LitElement {
93
94
  font-family: var(--roxy-font-sans);
94
95
  }
95
96
 
97
+ /* Below 480px the chart container shrinks to ~320px on phones.
98
+ * Bump in-SVG text up proportionally so the 7px degree band
99
+ * does not collapse below ~6px on screen.
100
+ */
101
+ @container (max-width: 480px) {
102
+ .sign-glyph,
103
+ .planet-glyph {
104
+ font-size: 18px;
105
+ }
106
+ .planet-deg {
107
+ font-size: 10px;
108
+ }
109
+ .house-num {
110
+ font-size: 12px;
111
+ }
112
+ }
113
+
96
114
  .planet-deg .retro {
97
115
  fill: var(--roxy-danger, #dc2626);
98
116
  }
99
117
 
118
+ .planet-leader {
119
+ stroke: var(--roxy-accent, #f59e0b);
120
+ stroke-width: 0.5;
121
+ opacity: 0.55;
122
+ }
123
+
100
124
  .house-num {
101
125
  fill: var(--roxy-muted, #71717a);
102
126
  font-size: 9px;
@@ -559,6 +583,10 @@ export class RoxyNatalChart extends LitElement {
559
583
  }
560
584
 
561
585
  private renderAngleMark(longitude: number, label: string) {
586
+ // Tick AND label share the same angle so the label sits right at the
587
+ // tip of the arrow, where a practitioner expects to find it. The label
588
+ // halo at radius ANGLE_LABEL_R is clear of the wheel rim, so there is
589
+ // no overlap with house dividers despite the shared angle.
562
590
  const angle = this.toAngle(longitude);
563
591
  const tickInner = polarToCartesian(CENTER, CENTER, OUTER_R, angle);
564
592
  const tickOuter = polarToCartesian(CENTER, CENTER, ANGLE_TICK_R, angle);
@@ -662,16 +690,71 @@ export class RoxyNatalChart extends LitElement {
662
690
  }
663
691
 
664
692
  private renderPlanets(planets: PlanetEntry[]) {
665
- return planets.map((p) => {
666
- if (!Number.isFinite(p.longitude)) return nothing;
667
- const angle = this.toAngle(p.longitude);
668
- const glyphPos = polarToCartesian(CENTER, CENTER, PLANET_R, angle);
669
- const degPos = polarToCartesian(CENTER, CENTER, PLANET_R - 13, angle);
693
+ // Stellium-aware angular fan-out. Conjunctions within 8° are the norm
694
+ // in professional natal charts (Sun-Mercury-Venus cluster, outer-planet
695
+ // stacks). To keep every glyph legible without losing precision, sort
696
+ // by longitude and push later members forward in angle until they
697
+ // clear a minimum separation, then draw a thin leader line from each
698
+ // displaced glyph back to the planet's true position on the outer
699
+ // rim. Conventional approach used by professional Western natal
700
+ // software; preserves both readability and astronomical accuracy.
701
+ const MIN_SEPARATION = 7;
702
+ type Placed = {
703
+ p: PlanetEntry;
704
+ trueLon: number;
705
+ displayLon: number;
706
+ };
707
+ const sorted: Placed[] = planets
708
+ .filter((p) => Number.isFinite(p.longitude))
709
+ .map((p) => ({
710
+ p,
711
+ trueLon: normalizeLongitude(p.longitude),
712
+ displayLon: normalizeLongitude(p.longitude),
713
+ }))
714
+ .sort((a, b) => a.trueLon - b.trueLon);
715
+ // Forward sweep: clamp each to at least prev + MIN_SEPARATION.
716
+ for (let i = 1; i < sorted.length; i++) {
717
+ const prev = sorted[i - 1];
718
+ const cur = sorted[i];
719
+ if (!prev || !cur) continue;
720
+ const wanted = prev.displayLon + MIN_SEPARATION;
721
+ if (cur.displayLon < wanted) cur.displayLon = wanted;
722
+ }
723
+ // If the cluster overshot 360°, slide everything back equally so the
724
+ // stack stays anchored near the original longitudes.
725
+ const last = sorted[sorted.length - 1];
726
+ if (last && last.displayLon > 360) {
727
+ const shift = last.displayLon - 360;
728
+ for (const s of sorted) s.displayLon -= shift;
729
+ }
730
+ return sorted.map(({ p, trueLon, displayLon }) => {
731
+ const trueAngle = this.toAngle(trueLon);
732
+ const displayAngle = this.toAngle(displayLon);
733
+ const glyphPos = polarToCartesian(CENTER, CENTER, PLANET_R, displayAngle);
734
+ const degPos = polarToCartesian(
735
+ CENTER,
736
+ CENTER,
737
+ PLANET_R - 13,
738
+ displayAngle,
739
+ );
740
+ const rimPos = polarToCartesian(CENTER, CENTER, OUTER_R - 4, trueAngle);
741
+ const leaderInner = polarToCartesian(
742
+ CENTER,
743
+ CENTER,
744
+ PLANET_R + 8,
745
+ displayAngle,
746
+ );
670
747
  const glyph = PLANET_GLYPH[capitalize(p.name)] ?? p.name.slice(0, 2);
671
748
  const sp = longitudeToSignPosition(p.longitude);
672
749
  const retro = p.isRetrograde === true;
673
750
  const degLabel = `${sp.degree}°${String(sp.minute).padStart(2, '0')}'`;
751
+ const offset = Math.abs(displayLon - trueLon) > 0.5;
674
752
  return svg`<g>
753
+ ${
754
+ offset
755
+ ? svg`<line class="planet-leader" x1=${rimPos.x} y1=${rimPos.y} x2=${leaderInner.x} y2=${leaderInner.y} />`
756
+ : nothing
757
+ }
675
758
  <text class="planet-glyph" x=${glyphPos.x} y=${glyphPos.y} text-anchor="middle" dominant-baseline="central"><title>${p.name}${retro ? ' retrograde' : ''} - ${degLabel} ${p.sign ?? ''}</title>${glyph}</text>
676
759
  <text class="planet-deg" x=${degPos.x} y=${degPos.y} text-anchor="middle" dominant-baseline="central">${degLabel}${retro ? svg`<tspan class="retro"> ℞</tspan>` : nothing}</text>
677
760
  </g>`;
@@ -116,6 +116,9 @@ export class RoxyPanchangTable extends LitElement {
116
116
  ]
117
117
  : [];
118
118
 
119
+ const transitions =
120
+ detailed && 'transitions' in detailed ? detailed.transitions : undefined;
121
+
119
122
  return html`<div class="wrap" aria-label="Panchang">
120
123
  <header class="head">
121
124
  <h2 class="title">Panchang</h2>
@@ -163,6 +166,21 @@ export class RoxyPanchangTable extends LitElement {
163
166
  }
164
167
  </tbody>
165
168
  </table>
169
+ ${
170
+ transitions
171
+ ? html`
172
+ <div class="section">Next transitions</div>
173
+ <table>
174
+ <tbody>
175
+ ${this.renderTransitionRow('Tithi', transitions.tithi)}
176
+ ${this.renderTransitionRow('Nakshatra', transitions.nakshatra)}
177
+ ${this.renderTransitionRow('Yoga', transitions.yoga)}
178
+ ${this.renderTransitionRow('Karana', transitions.karana)}
179
+ </tbody>
180
+ </table>
181
+ `
182
+ : nothing
183
+ }
166
184
  ${
167
185
  this.detail === 'detailed' &&
168
186
  (muhurtas.some((m) => !!m[1]) || inauspicious.some((m) => !!m[1]))
@@ -199,6 +217,19 @@ export class RoxyPanchangTable extends LitElement {
199
217
  </div>`;
200
218
  }
201
219
 
220
+ private renderTransitionRow(
221
+ label: string,
222
+ t: { endsAt?: string; next?: string } | undefined,
223
+ ) {
224
+ if (!t?.endsAt) return nothing;
225
+ const when = formatTime(t.endsAt);
226
+ const next = t.next ? ` → ${t.next}` : '';
227
+ return html`<tr>
228
+ <th>${label}</th>
229
+ <td>ends ${when}${next}</td>
230
+ </tr>`;
231
+ }
232
+
202
233
  private formatPart(v: unknown): string {
203
234
  if (!v) return '';
204
235
  if (typeof v === 'string') return v;
@@ -207,11 +238,13 @@ export class RoxyPanchangTable extends LitElement {
207
238
  name?: string;
208
239
  lord?: string;
209
240
  phase?: string;
241
+ paksha?: string;
210
242
  end?: string;
211
243
  };
212
244
  const parts = [
213
245
  obj.name,
214
- obj.lord ? `(${obj.lord})` : '',
246
+ obj.paksha ? `(${obj.paksha} paksha)` : '',
247
+ obj.lord ? `· ${obj.lord}` : '',
215
248
  obj.phase,
216
249
  ].filter(Boolean);
217
250
  return parts.join(' ');