@roxyapi/ui 0.3.0 → 0.4.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 (164) hide show
  1. package/AGENTS.md +228 -29
  2. package/README.md +291 -19
  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/styles/tokens.css +8 -23
  145. package/dist/utils/base-styles.d.ts.map +1 -1
  146. package/dist/utils/kundli-render.d.ts +43 -104
  147. package/dist/utils/kundli-render.d.ts.map +1 -1
  148. package/dist/utils/kundli-styles.d.ts +13 -0
  149. package/dist/utils/kundli-styles.d.ts.map +1 -0
  150. package/dist/version.d.ts +1 -1
  151. package/package.json +1 -1
  152. package/src/components/ashtakavarga-grid.ts +73 -11
  153. package/src/components/choghadiya-grid.ts +37 -2
  154. package/src/components/dasha-timeline.ts +135 -4
  155. package/src/components/divisional-chart.ts +40 -97
  156. package/src/components/natal-chart.ts +89 -6
  157. package/src/components/panchang-table.ts +34 -1
  158. package/src/components/synastry-chart.ts +84 -8
  159. package/src/components/vedic-kundli.ts +35 -95
  160. package/src/styles/tokens.css +8 -23
  161. package/src/utils/base-styles.ts +14 -0
  162. package/src/utils/kundli-render.ts +609 -270
  163. package/src/utils/kundli-styles.ts +124 -0
  164. package/src/version.ts +1 -1
@@ -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(' ');
@@ -6,7 +6,7 @@ import type {
6
6
  NatalChartResponse,
7
7
  } from '../types/index.js';
8
8
  import { baseStyles } from '../utils/base-styles.js';
9
- import { polarToCartesian } from '../utils/degree.js';
9
+ import { longitudeToSignPosition, polarToCartesian } from '../utils/degree.js';
10
10
  import {
11
11
  ASPECT_CLASS,
12
12
  formatNumber,
@@ -72,7 +72,9 @@ export class RoxySynastryChart extends LitElement {
72
72
  svg {
73
73
  display: block;
74
74
  width: 100%;
75
- max-width: 400px;
75
+ max-width: 560px;
76
+ aspect-ratio: 1 / 1;
77
+ height: auto;
76
78
  margin: 0 auto;
77
79
  }
78
80
 
@@ -94,6 +96,31 @@ export class RoxySynastryChart extends LitElement {
94
96
  font-weight: 600;
95
97
  font-size: 13px;
96
98
  }
99
+ .person-tag {
100
+ font-size: 7px;
101
+ font-weight: 700;
102
+ opacity: 0.85;
103
+ }
104
+ .planet-deg {
105
+ fill: var(--roxy-muted, #71717a);
106
+ font-size: 7px;
107
+ font-family: var(--roxy-font-sans);
108
+ }
109
+ .planet-deg .retro {
110
+ fill: var(--roxy-danger, #dc2626);
111
+ }
112
+ .asc-tick {
113
+ stroke: var(--roxy-accent-fg, #b45309);
114
+ stroke-width: 1;
115
+ opacity: 0.75;
116
+ }
117
+ .asc-label {
118
+ fill: var(--roxy-accent-fg, #b45309);
119
+ font-size: 9px;
120
+ font-weight: 700;
121
+ font-family: var(--roxy-font-sans);
122
+ letter-spacing: 0.04em;
123
+ }
97
124
  .aspect {
98
125
  stroke-width: 0.8;
99
126
  fill: none;
@@ -316,7 +343,8 @@ export class RoxySynastryChart extends LitElement {
316
343
  />
317
344
  ${this.renderSpokes()} ${this.renderSigns()}
318
345
  ${this.renderInterAspectLines(p1Planets, p2Planets, interAspects)}
319
- ${this.renderRing(p1Planets, P1_R, 'p1')} ${this.renderRing(p2Planets, P2_R, 'p2')}
346
+ ${this.renderRing(p1Planets, P1_R, 'p1', 1)} ${this.renderRing(p2Planets, P2_R, 'p2', 2)}
347
+ ${this.renderAscendants(this.data)}
320
348
  </svg>
321
349
  <div class="legend-row">
322
350
  <span><span class="swatch" style="background: var(--roxy-accent)"></span>Person 1</span>
@@ -376,20 +404,68 @@ export class RoxySynastryChart extends LitElement {
376
404
  });
377
405
  }
378
406
 
379
- private renderRing(planets: PlanetEntry[], radius: number, cls: string) {
407
+ private renderRing(
408
+ planets: PlanetEntry[],
409
+ radius: number,
410
+ cls: string,
411
+ personIndex: 1 | 2,
412
+ ) {
380
413
  return planets.map((p) => {
381
414
  if (!Number.isFinite(p.longitude)) return nothing;
382
- const pos = polarToCartesian(
415
+ const angle = this.toAngle(p.longitude);
416
+ const pos = polarToCartesian(CENTER, CENTER, radius, angle);
417
+ // Degree label sits one tier inward from the glyph so the two
418
+ // concentric rings never blur their numbers into the aspect lines.
419
+ const degOffset = personIndex === 1 ? -12 : -10;
420
+ const degPos = polarToCartesian(
383
421
  CENTER,
384
422
  CENTER,
385
- radius,
386
- this.toAngle(p.longitude),
423
+ radius + degOffset,
424
+ angle,
387
425
  );
388
426
  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>`;
427
+ const sp = longitudeToSignPosition(p.longitude);
428
+ const retro = p.isRetrograde === true;
429
+ const degLabel = `${sp.degree}°${String(sp.minute).padStart(2, '0')}'`;
430
+ const tooltip = `${p.name}${retro ? ' retrograde' : ''} - ${degLabel} ${sp.sign}`;
431
+ return svg`<g>
432
+ <text class=${cls} x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central"><title>${tooltip}</title>${glyph}<tspan class="person-tag" dy="-0.55em" dx="0.15em">${personIndex}</tspan></text>
433
+ <text class="planet-deg" x=${degPos.x} y=${degPos.y} text-anchor="middle" dominant-baseline="central">${sp.degree}°${retro ? svg`<tspan class="retro"> ℞</tspan>` : nothing}</text>
434
+ </g>`;
390
435
  });
391
436
  }
392
437
 
438
+ /**
439
+ * Ascendant markers for both people. Drawn as small spokes at the inner
440
+ * rim with the label outside, so the two rising signs are immediately
441
+ * scannable on the wheel without depending on tooltips.
442
+ */
443
+ private renderAscendants(data: SynastryWithPlanets) {
444
+ const items: ReturnType<typeof svg>[] = [];
445
+ const make = (
446
+ asc: { sign: string; degree: number } | undefined,
447
+ personIndex: 1 | 2,
448
+ ) => {
449
+ if (!asc) return;
450
+ const signIdx = SIGNS_ORDER.findIndex(
451
+ (s) => s.toLowerCase() === asc.sign.toLowerCase(),
452
+ );
453
+ if (signIdx === -1) return;
454
+ const longitude = signIdx * 30 + asc.degree;
455
+ const angle = this.toAngle(longitude);
456
+ const innerR = personIndex === 1 ? P1_R + 14 : P2_R + 14;
457
+ const tickPos = polarToCartesian(CENTER, CENTER, innerR, angle);
458
+ const labelPos = polarToCartesian(CENTER, CENTER, OUTER_R + 14, angle);
459
+ items.push(svg`<g>
460
+ <line class="asc-tick" x1=${tickPos.x} y1=${tickPos.y} x2=${labelPos.x} y2=${labelPos.y} />
461
+ <text class="asc-label" x=${labelPos.x} y=${labelPos.y} text-anchor="middle" dominant-baseline="central">Asc${personIndex}</text>
462
+ </g>`);
463
+ };
464
+ make(data.person1?.ascendant, 1);
465
+ make(data.person2?.ascendant, 2);
466
+ return items;
467
+ }
468
+
393
469
  private renderInterAspectLines(
394
470
  p1: PlanetEntry[],
395
471
  p2: PlanetEntry[],
@@ -1,126 +1,66 @@
1
- import { css, html, LitElement } from 'lit';
1
+ import { html, LitElement } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
3
  import type { BirthChartResponse } from '../types/index.js';
4
4
  import { baseStyles } from '../utils/base-styles.js';
5
5
  import {
6
- buildHousesFromMeta,
7
- type HouseDef,
8
- renderEastFrame,
9
- renderEastHouseGroup,
10
- renderNorthFrame,
11
- renderNorthHouseGroup,
12
- renderSouthFrame,
13
- renderSouthHouseGroup,
6
+ type ChartStyle,
7
+ type KundliViewModel,
8
+ renderKundliStyleTablist,
9
+ renderKundliSvg,
10
+ toKundliViewModel,
14
11
  } from '../utils/kundli-render.js';
12
+ import { kundliStyles } from '../utils/kundli-styles.js';
15
13
 
16
14
  /**
17
15
  * Vedic kundli (D1 Rashi chart). Pass `data` from /vedic-astrology/birth-chart.
18
- * Three render styles via the `chart-style` attribute: south (default),
19
- * north, and east. All three draw the identical planet-in-sign data, so the
20
- * style is purely a layout choice. Each planet shows its abbreviation and
21
- * whole-degree, with an SVG tooltip carrying exact position, nakshatra, pada,
22
- * and avastha.
16
+ * Three regional render styles are available; the visible tablist lets the
17
+ * end user switch between South / North / East at any time. The same planet-
18
+ * in-sign data feeds every style, so the toggle is purely a layout choice.
23
19
  *
24
- * Theming flows through CSS custom properties on :host, so the chart adopts
25
- * the host page palette without runtime color probing.
20
+ * Each planet shows its abbreviation and whole-degree-within-sign, with an
21
+ * SVG tooltip carrying exact position, nakshatra, pada, and avastha. The host
22
+ * page sets the initial style via `chart-style` attribute; from there the
23
+ * user takes over.
24
+ *
25
+ * Theming flows through CSS custom properties on `:host`, so the chart
26
+ * adopts the host page palette without runtime color probing.
26
27
  */
27
28
  @customElement('roxy-vedic-kundli')
28
29
  export class RoxyVedicKundli extends LitElement {
29
- static styles = [
30
- baseStyles,
31
- css`
32
- .wrap {
33
- display: grid;
34
- gap: var(--roxy-space-md, 1rem);
35
- }
36
- .title {
37
- font-size: var(--roxy-text-lg, 1.125rem);
38
- font-weight: var(--roxy-weight-bold, 600);
39
- margin: 0;
40
- }
41
- svg {
42
- display: block;
43
- width: 100%;
44
- max-width: 360px;
45
- margin: 0 auto;
46
- }
47
- .line {
48
- fill: transparent;
49
- stroke: var(--roxy-border, #e4e4e7);
50
- }
51
- .sign-text {
52
- fill: var(--roxy-muted, #71717a);
53
- font-size: 9px;
54
- font-weight: 500;
55
- font-family: var(--roxy-font-sans);
56
- }
57
- .planet-text {
58
- fill: var(--roxy-fg, #0a0a0a);
59
- font-size: 10px;
60
- font-weight: 600;
61
- font-family: var(--roxy-font-sans);
62
- }
63
- .house-num {
64
- fill: var(--roxy-muted, #71717a);
65
- font-size: 9px;
66
- font-weight: 400;
67
- font-family: var(--roxy-font-sans);
68
- }
69
- .lagna-marker {
70
- fill: var(--roxy-accent-fg, #b45309);
71
- font-size: 8px;
72
- font-weight: 700;
73
- font-family: var(--roxy-font-sans);
74
- letter-spacing: 0.05em;
75
- }
76
- .lagna-bg {
77
- fill: color-mix(in srgb, var(--roxy-accent, #f59e0b) 12%, transparent);
78
- stroke: color-mix(in srgb, var(--roxy-accent, #f59e0b) 45%, transparent);
79
- stroke-width: 0.8;
80
- }
81
- `,
82
- ];
30
+ static styles = [baseStyles, kundliStyles];
83
31
 
84
32
  @property({ attribute: false })
85
33
  data: BirthChartResponse | null = null;
86
34
 
87
35
  @property({ type: String, reflect: true, attribute: 'chart-style' })
88
- chartStyle: 'south' | 'north' | 'east' = 'south';
36
+ chartStyle: ChartStyle = 'north';
89
37
 
90
- private buildHouses(): HouseDef[] {
91
- if (!this.data?.meta) return [];
92
- return buildHousesFromMeta(this.data.meta);
38
+ private viewModel(): KundliViewModel | null {
39
+ if (!this.data?.meta) return null;
40
+ return toKundliViewModel(this.data.meta, 'D1 Rashi');
93
41
  }
94
42
 
43
+ private setStyle = (next: ChartStyle) => {
44
+ this.chartStyle = next;
45
+ };
46
+
95
47
  render() {
96
- if (!this.data)
48
+ const vm = this.viewModel();
49
+ if (!vm)
97
50
  return html`<div class="roxy-empty" role="status">No kundli data</div>`;
98
- const houses = this.buildHouses();
99
- const style = this.chartStyle;
100
-
101
- const frame =
102
- style === 'north'
103
- ? renderNorthFrame()
104
- : style === 'east'
105
- ? renderEastFrame()
106
- : renderSouthFrame();
107
- const houseGroup =
108
- style === 'north'
109
- ? renderNorthHouseGroup
110
- : style === 'east'
111
- ? renderEastHouseGroup
112
- : renderSouthHouseGroup;
113
-
114
51
  return html`<div class="wrap">
115
- <h2 class="title">Vedic kundli</h2>
52
+ <div class="header">
53
+ <h2 class="title">Vedic kundli</h2>
54
+ ${renderKundliStyleTablist(this.chartStyle, this.setStyle)}
55
+ </div>
116
56
  <svg
117
- viewBox="0 0 300 300"
57
+ viewBox="0 0 400 400"
58
+ preserveAspectRatio="xMidYMid meet"
118
59
  role="img"
119
60
  aria-label="Vedic birth chart with twelve sign houses"
120
61
  >
121
62
  <title>Vedic kundli</title>
122
- ${frame}
123
- ${houses.map((h) => houseGroup(h))}
63
+ ${renderKundliSvg(vm, this.chartStyle)}
124
64
  </svg>
125
65
  </div>`;
126
66
  }