@roxyapi/ui 0.11.0 → 0.12.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 (139) hide show
  1. package/AGENTS.md +6 -0
  2. package/README.md +6 -0
  3. package/components-catalog.json +111 -1
  4. package/dist/cdn/components/astrocartography-map.js +58 -0
  5. package/dist/cdn/components/astrocartography-map.js.map +7 -0
  6. package/dist/cdn/components/divisional-chart.js +7 -7
  7. package/dist/cdn/components/divisional-chart.js.map +1 -1
  8. package/dist/cdn/components/dosha-card.js +2 -2
  9. package/dist/cdn/components/dosha-card.js.map +3 -3
  10. package/dist/cdn/components/fixed-stars.js +52 -0
  11. package/dist/cdn/components/fixed-stars.js.map +7 -0
  12. package/dist/cdn/components/hd-variables.js +2 -2
  13. package/dist/cdn/components/hd-variables.js.map +3 -3
  14. package/dist/cdn/components/hexagram.js +3 -3
  15. package/dist/cdn/components/hexagram.js.map +3 -3
  16. package/dist/cdn/components/local-space-compass.js +58 -0
  17. package/dist/cdn/components/local-space-compass.js.map +7 -0
  18. package/dist/cdn/components/moon-phase.js +3 -3
  19. package/dist/cdn/components/moon-phase.js.map +3 -3
  20. package/dist/cdn/components/natal-chart.js +8 -8
  21. package/dist/cdn/components/natal-chart.js.map +2 -2
  22. package/dist/cdn/components/positions-table.js +52 -0
  23. package/dist/cdn/components/positions-table.js.map +7 -0
  24. package/dist/cdn/components/profection-card.js +52 -0
  25. package/dist/cdn/components/profection-card.js.map +7 -0
  26. package/dist/cdn/components/reference-card.js +3 -3
  27. package/dist/cdn/components/reference-card.js.map +3 -3
  28. package/dist/cdn/components/relocation-wheel.js +61 -0
  29. package/dist/cdn/components/relocation-wheel.js.map +7 -0
  30. package/dist/cdn/components/synastry-chart.js +4 -4
  31. package/dist/cdn/components/synastry-chart.js.map +2 -2
  32. package/dist/cdn/components/vedic-kundli.js +5 -5
  33. package/dist/cdn/components/vedic-kundli.js.map +1 -1
  34. package/dist/cdn/components/vedic-planets-table.js +2 -2
  35. package/dist/cdn/components/vedic-planets-table.js.map +1 -1
  36. package/dist/cdn/components/western-planets-table.js +2 -2
  37. package/dist/cdn/components/western-planets-table.js.map +1 -1
  38. package/dist/cdn/components/yoga-list.js +3 -3
  39. package/dist/cdn/components/yoga-list.js.map +3 -3
  40. package/dist/cdn/roxy-ui.js +83 -71
  41. package/dist/cdn/roxy-ui.js.map +4 -4
  42. package/dist/components/astrocartography-map.d.ts +27 -0
  43. package/dist/components/astrocartography-map.d.ts.map +1 -0
  44. package/dist/components/astrocartography-map.js +8 -0
  45. package/dist/components/astrocartography-map.js.map +7 -0
  46. package/dist/components/divisional-chart.js +22 -22
  47. package/dist/components/divisional-chart.js.map +1 -1
  48. package/dist/components/dosha-card.d.ts.map +1 -1
  49. package/dist/components/dosha-card.js +1 -1
  50. package/dist/components/dosha-card.js.map +3 -3
  51. package/dist/components/fixed-stars.d.ts +21 -0
  52. package/dist/components/fixed-stars.d.ts.map +1 -0
  53. package/dist/components/fixed-stars.js +2 -0
  54. package/dist/components/fixed-stars.js.map +7 -0
  55. package/dist/components/hd-variables.d.ts.map +1 -1
  56. package/dist/components/hd-variables.js +1 -1
  57. package/dist/components/hd-variables.js.map +3 -3
  58. package/dist/components/hexagram.d.ts +3 -1
  59. package/dist/components/hexagram.d.ts.map +1 -1
  60. package/dist/components/hexagram.js +1 -1
  61. package/dist/components/hexagram.js.map +3 -3
  62. package/dist/components/local-space-compass.d.ts +23 -0
  63. package/dist/components/local-space-compass.d.ts.map +1 -0
  64. package/dist/components/local-space-compass.js +8 -0
  65. package/dist/components/local-space-compass.js.map +7 -0
  66. package/dist/components/moon-phase.d.ts.map +1 -1
  67. package/dist/components/moon-phase.js +1 -1
  68. package/dist/components/moon-phase.js.map +3 -3
  69. package/dist/components/natal-chart.d.ts +2 -0
  70. package/dist/components/natal-chart.d.ts.map +1 -1
  71. package/dist/components/natal-chart.js +6 -6
  72. package/dist/components/natal-chart.js.map +2 -2
  73. package/dist/components/positions-table.d.ts +34 -0
  74. package/dist/components/positions-table.d.ts.map +1 -0
  75. package/dist/components/positions-table.js +2 -0
  76. package/dist/components/positions-table.js.map +7 -0
  77. package/dist/components/profection-card.d.ts +18 -0
  78. package/dist/components/profection-card.d.ts.map +1 -0
  79. package/dist/components/profection-card.js +2 -0
  80. package/dist/components/profection-card.js.map +7 -0
  81. package/dist/components/reference-card.d.ts.map +1 -1
  82. package/dist/components/reference-card.js +1 -1
  83. package/dist/components/reference-card.js.map +3 -3
  84. package/dist/components/relocation-wheel.d.ts +21 -0
  85. package/dist/components/relocation-wheel.d.ts.map +1 -0
  86. package/dist/components/relocation-wheel.js +11 -0
  87. package/dist/components/relocation-wheel.js.map +7 -0
  88. package/dist/components/synastry-chart.js +3 -3
  89. package/dist/components/synastry-chart.js.map +2 -2
  90. package/dist/components/vedic-kundli.js +14 -14
  91. package/dist/components/vedic-kundli.js.map +1 -1
  92. package/dist/components/vedic-planets-table.js +1 -1
  93. package/dist/components/vedic-planets-table.js.map +1 -1
  94. package/dist/components/western-planets-table.js +1 -1
  95. package/dist/components/western-planets-table.js.map +1 -1
  96. package/dist/components/yoga-list.d.ts +5 -2
  97. package/dist/components/yoga-list.d.ts.map +1 -1
  98. package/dist/components/yoga-list.js +1 -1
  99. package/dist/components/yoga-list.js.map +3 -3
  100. package/dist/generated/endpoint-bindings.d.ts.map +1 -1
  101. package/dist/index.cjs +62 -50
  102. package/dist/index.cjs.map +4 -4
  103. package/dist/index.d.ts +6 -0
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +62 -50
  106. package/dist/index.js.map +4 -4
  107. package/dist/manifest.d.ts.map +1 -1
  108. package/dist/manifest.json +6 -0
  109. package/dist/types/index.d.ts +1 -1
  110. package/dist/types/index.d.ts.map +1 -1
  111. package/dist/types/types.gen.d.ts +7864 -5381
  112. package/dist/types/types.gen.d.ts.map +1 -1
  113. package/dist/utils/degree.d.ts +2 -0
  114. package/dist/utils/degree.d.ts.map +1 -1
  115. package/dist/utils/planet-color.d.ts +3 -0
  116. package/dist/utils/planet-color.d.ts.map +1 -0
  117. package/dist/version.d.ts +1 -1
  118. package/package.json +2 -1
  119. package/src/components/astrocartography-map.ts +436 -0
  120. package/src/components/dosha-card.ts +48 -16
  121. package/src/components/fixed-stars.ts +254 -0
  122. package/src/components/hd-variables.ts +30 -2
  123. package/src/components/hexagram.ts +11 -11
  124. package/src/components/local-space-compass.ts +299 -0
  125. package/src/components/moon-phase.ts +21 -2
  126. package/src/components/natal-chart.ts +36 -24
  127. package/src/components/positions-table.ts +442 -0
  128. package/src/components/profection-card.ts +173 -0
  129. package/src/components/reference-card.ts +40 -8
  130. package/src/components/relocation-wheel.ts +170 -0
  131. package/src/components/yoga-list.ts +95 -2
  132. package/src/generated/endpoint-bindings.ts +62 -0
  133. package/src/index.ts +6 -0
  134. package/src/manifest.ts +79 -0
  135. package/src/types/index.ts +1 -1
  136. package/src/types/types.gen.ts +7814 -5263
  137. package/src/utils/degree.ts +11 -0
  138. package/src/utils/planet-color.ts +45 -0
  139. package/src/version.ts +1 -1
@@ -21,6 +21,8 @@ export declare function normalizeLongitude(lon: number): number;
21
21
  export declare function longitudeToSignPosition(longitude: number): SignPosition;
22
22
  /** Compact display string like "12° Leo 34'". Used in chart labels. */
23
23
  export declare function formatSignPosition(longitude: number): string;
24
+ /** Format a within-sign decimal degree (0-30) as degree-and-minute, e.g. 17.99 to "17°59'". The reference-grade form astrologers read when the sign is already known (asteroids, lots, directed points, fixed stars). */
25
+ export declare function formatDegreeInSign(deg: number): string;
24
26
  /**
25
27
  * The point diametrically opposite a longitude (e.g. Descendant from
26
28
  * Ascendant, IC from MC). Exact derivation, always 180 degrees away.
@@ -1 +1 @@
1
- {"version":3,"file":"degree.d.ts","sourceRoot":"","sources":["../../src/utils/degree.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGtD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,CAevE;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAG5D;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAK9D;AAED,sFAAsF;AACtF,wBAAgB,gBAAgB,CAC/B,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACd;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAM1B"}
1
+ {"version":3,"file":"degree.d.ts","sourceRoot":"","sources":["../../src/utils/degree.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGtD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,CAevE;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAG5D;AAED,yNAAyN;AACzN,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQtD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAK9D;AAED,sFAAsF;AACtF,wBAAgB,gBAAgB,CAC/B,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACd;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAM1B"}
@@ -0,0 +1,3 @@
1
+ /** Theme-token color for a body, by name with an index fallback. {@link capitalize} matches the canonical TitleCase keys; unmapped or localized names cycle by `index`. */
2
+ export declare function planetColor(name: string, index?: number): string;
3
+ //# sourceMappingURL=planet-color.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planet-color.d.ts","sourceRoot":"","sources":["../../src/utils/planet-color.ts"],"names":[],"mappings":"AAqCA,2KAA2K;AAC3K,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,MAAM,CAM3D"}
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const ROXY_UI_VERSION = "0.11.0";
1
+ export declare const ROXY_UI_VERSION = "0.12.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roxyapi/ui",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Web components for the RoxyAPI catalog. Drop-in charts, tables, cards, forms for astrology, tarot, numerology, biorhythm, I Ching, crystals, dreams, angel numbers, and more. One key, beautiful in 30 minutes.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -66,6 +66,7 @@
66
66
  "url": "https://github.com/RoxyAPI/ui/issues"
67
67
  },
68
68
  "sideEffects": [
69
+ "./src/components/*.ts",
69
70
  "./dist/components/*.js",
70
71
  "./dist/cdn/*.js"
71
72
  ],
@@ -0,0 +1,436 @@
1
+ import { css, html, nothing, svg } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+ import type { AstrocartographyResponse } from '../types/index.js';
4
+ import { RoxyDataElement } from '../utils/base-element.js';
5
+ import { baseStyles } from '../utils/base-styles.js';
6
+ import { chevron, disclosureStyles } from '../utils/disclosure.js';
7
+ import { planetColor } from '../utils/planet-color.js';
8
+
9
+ type LineSet = AstrocartographyResponse['lines'][number];
10
+ type GeoPoint = LineSet['ascendant']['points'][number];
11
+
12
+ // Equirectangular (plate carree) projection in a 360x180 unit canvas: one unit
13
+ // per degree. The SVG scales to the container width; the 2:1 aspect ratio holds.
14
+ const W = 360;
15
+ const H = 180;
16
+ const lonToX = (lon: number): number => lon + 180;
17
+ const latToY = (lat: number): number => 90 - lat;
18
+
19
+ // Reference parallels (degrees). Tropics and polar circles use the current mean
20
+ // obliquity; drawn as dashed guides so the curved rising/setting lines read
21
+ // against real climate bands, not just a bare grid.
22
+ const TROPIC = 23.44;
23
+ const POLAR = 66.56;
24
+
25
+ const formatLon = (lon: number): string =>
26
+ lon === 0 ? '0' : `${Math.abs(lon)}°${lon > 0 ? 'E' : 'W'}`;
27
+ const formatLat = (lat: number): string =>
28
+ lat === 0 ? '0' : `${Math.abs(lat)}°${lat > 0 ? 'N' : 'S'}`;
29
+
30
+ /**
31
+ * Split a rising/setting line into screen polylines, breaking the path wherever
32
+ * consecutive samples jump more than 180 degrees of longitude. Without the
33
+ * split, a line that crosses the antimeridian draws a stray horizontal streak
34
+ * straight across the whole map.
35
+ */
36
+ function toSegments(points: GeoPoint[]): string[] {
37
+ const segments: string[][] = [];
38
+ let current: string[] = [];
39
+ let prevLon: number | null = null;
40
+ for (const p of points) {
41
+ if (prevLon !== null && Math.abs(p.longitude - prevLon) > 180) {
42
+ if (current.length) segments.push(current);
43
+ current = [];
44
+ }
45
+ current.push(`${lonToX(p.longitude)},${latToY(p.latitude)}`);
46
+ prevLon = p.longitude;
47
+ }
48
+ if (current.length) segments.push(current);
49
+ return segments.filter((s) => s.length > 1).map((s) => s.join(' '));
50
+ }
51
+
52
+ const ANGLE_LABEL: Record<string, string> = {
53
+ mc: 'MC',
54
+ ic: 'IC',
55
+ ascendant: 'AC',
56
+ descendant: 'DC',
57
+ };
58
+
59
+ /**
60
+ * Astrocartography (relocation) world map. Plots the four planetary lines for
61
+ * every body from a /astrology/astrocartography response over a labeled
62
+ * graticule: MC and IC as straight meridians, the Ascendant and Descendant as
63
+ * latitude-sampled curves. Color is per body and theme-token driven; solid
64
+ * lines are the Ascendant and Midheaven, dashed are the Descendant and IC.
65
+ */
66
+ @customElement('roxy-astrocartography-map')
67
+ export class RoxyAstrocartographyMap extends RoxyDataElement<AstrocartographyResponse> {
68
+ static styles = [
69
+ baseStyles,
70
+ disclosureStyles,
71
+ css`
72
+ .wrap {
73
+ width: 100%;
74
+ background: var(--roxy-surface, #fff);
75
+ color: var(--roxy-fg, #0a0a0a);
76
+ border: 1px solid var(--roxy-border, #e4e4e7);
77
+ border-radius: var(--roxy-radius-md, 8px);
78
+ padding: var(--roxy-space-lg, 1.5rem);
79
+ box-shadow: var(--roxy-shadow-sm);
80
+ display: grid;
81
+ gap: var(--roxy-space-md, 1rem);
82
+ }
83
+ .title {
84
+ font-size: var(--roxy-text-lg, 1.125rem);
85
+ font-weight: var(--roxy-weight-bold, 600);
86
+ margin: 0;
87
+ color: var(--roxy-primary, #0f172a);
88
+ }
89
+ .meta {
90
+ color: var(--roxy-muted, #71717a);
91
+ font-size: var(--roxy-text-sm, 0.875rem);
92
+ }
93
+ svg {
94
+ display: block;
95
+ width: 100%;
96
+ height: auto;
97
+ border-radius: var(--roxy-radius-sm, 4px);
98
+ }
99
+ .map-frame {
100
+ fill: color-mix(in srgb, var(--roxy-border, #e4e4e7) 12%, transparent);
101
+ stroke: var(--roxy-border, #e4e4e7);
102
+ stroke-width: 0.8;
103
+ }
104
+ .grat {
105
+ stroke: var(--roxy-border, #e4e4e7);
106
+ stroke-width: 0.4;
107
+ fill: none;
108
+ }
109
+ .grat-axis {
110
+ stroke: var(--roxy-muted, #71717a);
111
+ stroke-width: 0.6;
112
+ opacity: 0.6;
113
+ }
114
+ .grat-ref {
115
+ stroke: var(--roxy-secondary, #475569);
116
+ stroke-width: 0.4;
117
+ stroke-dasharray: 2 2;
118
+ opacity: 0.5;
119
+ fill: none;
120
+ }
121
+ .axis-label {
122
+ fill: var(--roxy-muted, #71717a);
123
+ font-size: 5px;
124
+ font-family: var(--roxy-font-sans);
125
+ }
126
+ .acg-line {
127
+ fill: none;
128
+ stroke-width: 1;
129
+ opacity: 0.95;
130
+ }
131
+ .acg-line.dashed {
132
+ stroke-dasharray: 4 2.5;
133
+ }
134
+ .acg-glyph {
135
+ font-size: 8px;
136
+ font-family: var(--roxy-font-sans);
137
+ font-weight: 600;
138
+ }
139
+ .birthplace {
140
+ fill: var(--roxy-fg, #0a0a0a);
141
+ font-size: 9px;
142
+ }
143
+ .legend {
144
+ display: flex;
145
+ flex-wrap: wrap;
146
+ gap: var(--roxy-space-xs, 0.25rem) var(--roxy-space-md, 1rem);
147
+ font-size: var(--roxy-text-xs, 0.75rem);
148
+ color: var(--roxy-muted, #71717a);
149
+ }
150
+ .legend-item {
151
+ display: inline-flex;
152
+ align-items: center;
153
+ gap: 0.3rem;
154
+ }
155
+ .legend-swatch {
156
+ width: 14px;
157
+ height: 0;
158
+ border-top-width: 2px;
159
+ border-top-style: solid;
160
+ }
161
+ .legend-note {
162
+ width: 100%;
163
+ color: var(--roxy-muted, #71717a);
164
+ }
165
+ .summary {
166
+ color: var(--roxy-fg, #0a0a0a);
167
+ font-size: var(--roxy-text-sm, 0.875rem);
168
+ margin: 0;
169
+ }
170
+ .interpretations h3 {
171
+ font-size: var(--roxy-text-sm, 0.875rem);
172
+ font-weight: 600;
173
+ color: var(--roxy-muted, #71717a);
174
+ text-transform: uppercase;
175
+ letter-spacing: 0.06em;
176
+ margin: 0 0 var(--roxy-space-sm, 0.5rem);
177
+ }
178
+ .interp-card {
179
+ border: 1px solid var(--roxy-border, #e4e4e7);
180
+ border-radius: var(--roxy-radius-md, 8px);
181
+ padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem);
182
+ margin-bottom: var(--roxy-space-xs, 0.25rem);
183
+ }
184
+ .interp-card summary {
185
+ cursor: pointer;
186
+ font-weight: 500;
187
+ color: var(--roxy-fg, #0a0a0a);
188
+ display: flex;
189
+ align-items: center;
190
+ justify-content: space-between;
191
+ gap: var(--roxy-space-md, 1rem);
192
+ }
193
+ .interp-head {
194
+ display: inline-flex;
195
+ align-items: center;
196
+ gap: 0.5rem;
197
+ }
198
+ .interp-dot {
199
+ width: 10px;
200
+ height: 10px;
201
+ border-radius: 50%;
202
+ }
203
+ .interp-body {
204
+ margin-top: var(--roxy-space-sm, 0.5rem);
205
+ display: grid;
206
+ gap: var(--roxy-space-xs, 0.25rem);
207
+ }
208
+ .interp-line {
209
+ font-size: var(--roxy-text-sm, 0.875rem);
210
+ color: var(--roxy-fg, #0a0a0a);
211
+ }
212
+ .interp-line .code {
213
+ font-weight: 600;
214
+ color: var(--roxy-accent-ink, #b45309);
215
+ margin-right: 0.4rem;
216
+ }
217
+ `,
218
+ ];
219
+
220
+ protected renderEmpty() {
221
+ return html`<div class="roxy-empty" role="status">No astrocartography data</div>`;
222
+ }
223
+
224
+ protected renderData(data: AstrocartographyResponse) {
225
+ const lines = data.lines ?? [];
226
+ const bd = data.birthDetails;
227
+ return html`<div class="wrap">
228
+ <header>
229
+ <h2 class="title">Astrocartography</h2>
230
+ ${
231
+ bd
232
+ ? html`<div class="meta">
233
+ ${[bd.date, bd.time].filter(Boolean).join(' · ')} ·
234
+ ${formatLat(Math.round(bd.latitude))} ${formatLon(Math.round(bd.longitude))}
235
+ </div>`
236
+ : nothing
237
+ }
238
+ </header>
239
+ ${this.renderMap(lines, bd)}
240
+ ${this.renderLegend(lines)}
241
+ ${data.summary ? html`<p class="summary">${data.summary}</p>` : nothing}
242
+ ${this.renderInterpretations(lines)}
243
+ </div>`;
244
+ }
245
+
246
+ private renderMap(
247
+ lines: LineSet[],
248
+ bd: AstrocartographyResponse['birthDetails'],
249
+ ) {
250
+ return html`<svg
251
+ viewBox="0 0 ${W} ${H}"
252
+ role="img"
253
+ aria-label="World map of planetary astrocartography lines"
254
+ >
255
+ <title>Astrocartography world map</title>
256
+ <desc>
257
+ Equirectangular world map. Each body has a Midheaven and Imum Coeli
258
+ meridian and a curved Ascendant and Descendant line, colored per body.
259
+ </desc>
260
+ <rect class="map-frame" x="0" y="0" width=${W} height=${H} />
261
+ ${this.renderGraticule()}
262
+ ${lines.map((l, i) => this.renderBodyLines(l, i))}
263
+ ${
264
+ bd
265
+ ? svg`<text class="birthplace" x=${lonToX(bd.longitude)} y=${latToY(bd.latitude)} text-anchor="middle" dominant-baseline="central"><title>Birthplace</title>★</text>`
266
+ : nothing
267
+ }
268
+ </svg>`;
269
+ }
270
+
271
+ private renderGraticule() {
272
+ const meridians = [];
273
+ for (let lon = -150; lon <= 150; lon += 30) {
274
+ const x = lonToX(lon);
275
+ const axis = lon === 0;
276
+ meridians.push(
277
+ svg`<line class=${axis ? 'grat-axis' : 'grat'} x1=${x} y1="0" x2=${x} y2=${H} />`,
278
+ );
279
+ meridians.push(
280
+ svg`<text class="axis-label" x=${x} y=${H - 2} text-anchor="middle">${formatLon(lon)}</text>`,
281
+ );
282
+ }
283
+ const parallels = [];
284
+ for (let lat = -60; lat <= 60; lat += 30) {
285
+ const y = latToY(lat);
286
+ const axis = lat === 0;
287
+ parallels.push(
288
+ svg`<line class=${axis ? 'grat-axis' : 'grat'} x1="0" y1=${y} x2=${W} y2=${y} />`,
289
+ );
290
+ parallels.push(
291
+ svg`<text class="axis-label" x="2" y=${y - 1}>${formatLat(lat)}</text>`,
292
+ );
293
+ }
294
+ // Tropics and polar circles as dashed climate-band references.
295
+ const refs = [TROPIC, -TROPIC, POLAR, -POLAR].map(
296
+ (lat) =>
297
+ svg`<line class="grat-ref" x1="0" y1=${latToY(lat)} x2=${W} y2=${latToY(lat)} />`,
298
+ );
299
+ return svg`${meridians}${parallels}${refs}`;
300
+ }
301
+
302
+ private renderBodyLines(line: LineSet, index: number) {
303
+ const color = planetColor(line.planet, index);
304
+ const glyph = line.symbol || line.planet.slice(0, 2);
305
+ const items = [
306
+ this.renderMeridian(
307
+ line.mc.longitude,
308
+ color,
309
+ glyph,
310
+ line.planet,
311
+ 'mc',
312
+ false,
313
+ ),
314
+ this.renderMeridian(
315
+ line.ic.longitude,
316
+ color,
317
+ glyph,
318
+ line.planet,
319
+ 'ic',
320
+ true,
321
+ ),
322
+ this.renderCurve(
323
+ line.ascendant.points,
324
+ color,
325
+ glyph,
326
+ line.planet,
327
+ 'ascendant',
328
+ false,
329
+ ),
330
+ this.renderCurve(
331
+ line.descendant.points,
332
+ color,
333
+ glyph,
334
+ line.planet,
335
+ 'descendant',
336
+ true,
337
+ ),
338
+ ];
339
+ return svg`${items}`;
340
+ }
341
+
342
+ private renderMeridian(
343
+ lon: number,
344
+ color: string,
345
+ glyph: string,
346
+ planet: string,
347
+ angle: string,
348
+ dashed: boolean,
349
+ ) {
350
+ const x = lonToX(lon);
351
+ // MC label rides the top edge, IC the bottom, so the two meridians of one
352
+ // body never stack their glyphs at the same point.
353
+ const labelY = angle === 'ic' ? H - 7 : 9;
354
+ return svg`<g>
355
+ <line class=${`acg-line${dashed ? ' dashed' : ''}`} stroke=${color} x1=${x} y1="0" x2=${x} y2=${H}><title>${planet} ${ANGLE_LABEL[angle]} line</title></line>
356
+ <text class="acg-glyph" fill=${color} x=${x} y=${labelY} text-anchor="middle" dominant-baseline="central">${glyph}</text>
357
+ </g>`;
358
+ }
359
+
360
+ private renderCurve(
361
+ points: GeoPoint[],
362
+ color: string,
363
+ glyph: string,
364
+ planet: string,
365
+ angle: string,
366
+ dashed: boolean,
367
+ ) {
368
+ const segments = toSegments(points ?? []);
369
+ if (segments.length === 0) return nothing;
370
+ // Label at the sample nearest the equator, the most visible band.
371
+ const anchor = (points ?? []).reduce(
372
+ (best, p) => (Math.abs(p.latitude) < Math.abs(best.latitude) ? p : best),
373
+ points[0] ?? { latitude: 0, longitude: 0 },
374
+ );
375
+ return svg`<g>
376
+ ${segments.map(
377
+ (pts) =>
378
+ svg`<polyline class=${`acg-line${dashed ? ' dashed' : ''}`} stroke=${color} points=${pts}><title>${planet} ${ANGLE_LABEL[angle]} line</title></polyline>`,
379
+ )}
380
+ <text class="acg-glyph" fill=${color} x=${lonToX(anchor.longitude)} y=${latToY(anchor.latitude)} text-anchor="middle" dominant-baseline="central">${glyph}</text>
381
+ </g>`;
382
+ }
383
+
384
+ private renderLegend(lines: LineSet[]) {
385
+ if (lines.length === 0) return nothing;
386
+ return html`<div class="legend">
387
+ ${lines.map((l, i) => {
388
+ const color = planetColor(l.planet, i);
389
+ return html`<span class="legend-item">
390
+ <span class="legend-swatch" style=${`border-top-color: ${color}`}></span>
391
+ ${l.symbol ? html`${l.symbol} ` : nothing}${l.planet}
392
+ </span>`;
393
+ })}
394
+ <span class="legend-note">Solid lines are the Ascendant and Midheaven, dashed are the Descendant and IC.</span>
395
+ </div>`;
396
+ }
397
+
398
+ private renderInterpretations(lines: LineSet[]) {
399
+ if (lines.length === 0) return nothing;
400
+ return html`<section class="interpretations">
401
+ <h3>Planetary lines</h3>
402
+ ${lines.map((l, i) => {
403
+ const color = planetColor(l.planet, i);
404
+ const rows: Array<[string, string]> = [
405
+ ['MC', l.mc.interpretation],
406
+ ['IC', l.ic.interpretation],
407
+ ['AC', l.ascendant.interpretation],
408
+ ['DC', l.descendant.interpretation],
409
+ ];
410
+ return html`<details class="interp-card" name="acg-lines" ?open=${i === 0}>
411
+ <summary>
412
+ <span class="interp-head">
413
+ <span class="interp-dot" style=${`background: ${color}`}></span>
414
+ ${l.symbol ? html`${l.symbol} ` : nothing}${l.planet}
415
+ </span>
416
+ ${chevron()}
417
+ </summary>
418
+ <div class="interp-body">
419
+ ${rows
420
+ .filter(([, text]) => text)
421
+ .map(
422
+ ([code, text]) =>
423
+ html`<p class="interp-line"><span class="code">${code}</span>${text}</p>`,
424
+ )}
425
+ </div>
426
+ </details>`;
427
+ })}
428
+ </section>`;
429
+ }
430
+ }
431
+
432
+ declare global {
433
+ interface HTMLElementTagNameMap {
434
+ 'roxy-astrocartography-map': RoxyAstrocartographyMap;
435
+ }
436
+ }
@@ -47,6 +47,18 @@ export class RoxyDoshaCard extends RoxyDataElement<DoshaData> {
47
47
  font-weight: var(--roxy-weight-bold, 600);
48
48
  text-transform: capitalize;
49
49
  }
50
+ .subtype {
51
+ margin: 0;
52
+ font-size: var(--roxy-text-sm, 0.875rem);
53
+ color: var(--roxy-fg, #0a0a0a);
54
+ }
55
+ .subtype .subtype-label {
56
+ color: var(--roxy-muted, #71717a);
57
+ text-transform: uppercase;
58
+ letter-spacing: 0.06em;
59
+ font-size: var(--roxy-text-xs, 0.75rem);
60
+ margin-right: 0.4rem;
61
+ }
50
62
  .badge {
51
63
  display: inline-flex;
52
64
  align-items: center;
@@ -135,14 +147,15 @@ export class RoxyDoshaCard extends RoxyDataElement<DoshaData> {
135
147
  ? 1
136
148
  : 0;
137
149
  const pct = tier * 33;
150
+ // A present dosha is never "good news": keep the ramp in the warning/danger
151
+ // family so a Mild severity does not paint green and read as benign. The bar
152
+ // width already conveys the tier.
138
153
  const barColor =
139
154
  tier === 3
140
155
  ? 'var(--roxy-danger)'
141
- : tier === 2
156
+ : tier >= 1
142
157
  ? 'var(--roxy-warning)'
143
- : tier === 1
144
- ? 'var(--roxy-success)'
145
- : 'transparent';
158
+ : 'transparent';
146
159
 
147
160
  return html`<article
148
161
  class="card"
@@ -154,6 +167,13 @@ export class RoxyDoshaCard extends RoxyDataElement<DoshaData> {
154
167
  ${present ? 'Present' : 'Absent'}
155
168
  </span>
156
169
  </header>
170
+ ${
171
+ 'type' in d && d.type
172
+ ? html`<p class="subtype">
173
+ <span class="subtype-label">${this.type === 'sadhesati' ? 'Current phase' : 'Type'}</span>${d.type}
174
+ </p>`
175
+ : nothing
176
+ }
157
177
  ${
158
178
  d.severity
159
179
  ? html`<div
@@ -195,18 +215,30 @@ export class RoxyDoshaCard extends RoxyDataElement<DoshaData> {
195
215
 
196
216
  private renderEffects(d: DoshaData) {
197
217
  if (!d.effects) return nothing;
198
- const entries = Object.entries(d.effects).filter(
199
- ([, v]) => typeof v === 'string' && v.length > 0,
200
- );
201
- if (entries.length === 0) return nothing;
202
- return html`<div class="effects">
203
- ${entries.map(
204
- ([k, v]) => html`<div>
205
- <h3>${k}</h3>
206
- <p>${v}</p>
207
- </div>`,
208
- )}
209
- </div>`;
218
+ // Effects mix flat string fields (marriage, career...) with a nested map
219
+ // (Sadhesati effects.phases: { Rising, Peak, Setting }). Render both; the
220
+ // old string-only filter silently dropped the phase-specific effects, which
221
+ // are the substance of a Sade Sati reading.
222
+ const sections: unknown[] = [];
223
+ for (const [key, value] of Object.entries(
224
+ d.effects as Record<string, unknown>,
225
+ )) {
226
+ if (typeof value === 'string' && value.length > 0) {
227
+ sections.push(html`<div><h3>${key}</h3><p>${value}</p></div>`);
228
+ } else if (value && typeof value === 'object') {
229
+ for (const [nestedKey, nestedValue] of Object.entries(
230
+ value as Record<string, unknown>,
231
+ )) {
232
+ if (typeof nestedValue === 'string' && nestedValue.length > 0) {
233
+ sections.push(
234
+ html`<div><h3>${nestedKey}</h3><p>${nestedValue}</p></div>`,
235
+ );
236
+ }
237
+ }
238
+ }
239
+ }
240
+ if (sections.length === 0) return nothing;
241
+ return html`<div class="effects">${sections}</div>`;
210
242
  }
211
243
  }
212
244