@roxyapi/ui 0.11.0 → 0.12.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 (142) hide show
  1. package/AGENTS.md +6 -0
  2. package/README.md +7 -1
  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 +84 -72
  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 +55 -43
  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 +63 -51
  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/utils/world-map.d.ts +8 -0
  118. package/dist/utils/world-map.d.ts.map +1 -0
  119. package/dist/version.d.ts +1 -1
  120. package/package.json +2 -1
  121. package/src/components/astrocartography-map.ts +442 -0
  122. package/src/components/dosha-card.ts +48 -16
  123. package/src/components/fixed-stars.ts +254 -0
  124. package/src/components/hd-variables.ts +30 -2
  125. package/src/components/hexagram.ts +11 -11
  126. package/src/components/local-space-compass.ts +299 -0
  127. package/src/components/moon-phase.ts +21 -2
  128. package/src/components/natal-chart.ts +36 -24
  129. package/src/components/positions-table.ts +442 -0
  130. package/src/components/profection-card.ts +173 -0
  131. package/src/components/reference-card.ts +40 -8
  132. package/src/components/relocation-wheel.ts +170 -0
  133. package/src/components/yoga-list.ts +95 -2
  134. package/src/generated/endpoint-bindings.ts +62 -0
  135. package/src/index.ts +6 -0
  136. package/src/manifest.ts +79 -0
  137. package/src/types/index.ts +1 -1
  138. package/src/types/types.gen.ts +7814 -5263
  139. package/src/utils/degree.ts +11 -0
  140. package/src/utils/planet-color.ts +45 -0
  141. package/src/utils/world-map.ts +8 -0
  142. package/src/version.ts +1 -1
@@ -378,6 +378,10 @@ export class RoxyNatalChart extends RoxyDataElement<NatalChartResponse> {
378
378
  @property({ type: String, attribute: 'house-system', reflect: true })
379
379
  houseSystem: 'placidus' | 'whole-sign' | 'equal' | 'koch' = 'placidus';
380
380
 
381
+ /** Heading above the wheel. Defaults to "Natal chart"; reuse (e.g. the relocation wheel) sets its own. */
382
+ @property({ type: String })
383
+ heading = 'Natal chart';
384
+
381
385
  /** Which view is showing: the wheel or the planet-by-planet aspect grid. */
382
386
  @state()
383
387
  private view: 'wheel' | 'grid' = 'wheel';
@@ -410,7 +414,7 @@ export class RoxyNatalChart extends RoxyDataElement<NatalChartResponse> {
410
414
 
411
415
  return html`<div class="wrap">
412
416
  <header>
413
- <h2 class="title">Natal chart</h2>
417
+ <h2 class="title">${this.heading}</h2>
414
418
  ${
415
419
  data.birthDetails
416
420
  ? html`<div class="meta">
@@ -421,36 +425,44 @@ export class RoxyNatalChart extends RoxyDataElement<NatalChartResponse> {
421
425
  : nothing
422
426
  }
423
427
  </header>
424
- ${renderTablist({
425
- items: [
426
- { id: 'wheel', label: 'Wheel' },
427
- { id: 'grid', label: 'Aspect grid' },
428
- ],
429
- active: view,
430
- onSelect: (v) => {
431
- this.view = v;
432
- },
433
- label: 'Natal chart views',
434
- idPrefix: 'natal',
435
- controls: true,
436
- })}
437
- <div
438
- id="natal-panel-${view}"
439
- role="tabpanel"
440
- aria-labelledby="natal-tab-${view}"
441
- >
442
- ${view === 'wheel' ? this.renderWheel(planets, aspects) : this.renderAspectGrid(planets, aspects)}
443
- </div>
428
+ ${
429
+ aspects.length > 0
430
+ ? html`${renderTablist({
431
+ items: [
432
+ { id: 'wheel', label: 'Wheel' },
433
+ { id: 'grid', label: 'Aspect grid' },
434
+ ],
435
+ active: view,
436
+ onSelect: (v) => {
437
+ this.view = v;
438
+ },
439
+ label: 'Natal chart views',
440
+ idPrefix: 'natal',
441
+ controls: true,
442
+ })}
443
+ <div
444
+ id="natal-panel-${view}"
445
+ role="tabpanel"
446
+ aria-labelledby="natal-tab-${view}"
447
+ >
448
+ ${view === 'wheel' ? this.renderWheel(planets, aspects) : this.renderAspectGrid(planets, aspects)}
449
+ </div>`
450
+ : this.renderWheel(planets, aspects)
451
+ }
444
452
  <div class="legend">
445
453
  <span>${planets.length} planets</span>
446
- <span>${aspects.length} aspects</span>
454
+ ${aspects.length > 0 ? html`<span>${aspects.length} aspects</span>` : nothing}
447
455
  ${
448
456
  data.houseSystem
449
457
  ? html`<span>${data.houseSystem} houses</span>`
450
458
  : nothing
451
459
  }
452
- <span><span class="legend-swatch" style="background: var(--roxy-success)"></span>harmonious</span>
453
- <span><span class="legend-swatch" style="background: var(--roxy-danger)"></span>challenging</span>
460
+ ${
461
+ aspects.length > 0
462
+ ? html`<span><span class="legend-swatch" style="background: var(--roxy-success)"></span>harmonious</span>
463
+ <span><span class="legend-swatch" style="background: var(--roxy-danger)"></span>challenging</span>`
464
+ : nothing
465
+ }
454
466
  </div>
455
467
  ${this.renderDetails()}
456
468
  ${this.renderInterpretations()}
@@ -0,0 +1,442 @@
1
+ import { css, html, nothing } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { PLANET_GLYPH, SIGN_GLYPH } from '../tokens/index.js';
4
+ import type {
5
+ ArabicLotsResponse,
6
+ AsteroidsResponse,
7
+ LilithResponse,
8
+ ProgressionsResponse,
9
+ SolarArcResponse,
10
+ } from '../types/index.js';
11
+ import { RoxyDataElement } from '../utils/base-element.js';
12
+ import { baseStyles } from '../utils/base-styles.js';
13
+ import {
14
+ formatDegreeInSign,
15
+ longitudeToSignPosition,
16
+ } from '../utils/degree.js';
17
+ import { chevron, disclosureStyles } from '../utils/disclosure.js';
18
+ import { formatNumber } from '../utils/format.js';
19
+ import { capitalize } from '../utils/string.js';
20
+
21
+ /**
22
+ * Union of the position-list Western responses this one editorial table renders.
23
+ * Each carries an array of bodies in zodiac signs plus a per-body
24
+ * interpretation; the table discriminates on which array key is present.
25
+ */
26
+ type PositionsResponse =
27
+ | AsteroidsResponse
28
+ | LilithResponse
29
+ | ProgressionsResponse
30
+ | SolarArcResponse
31
+ | ArabicLotsResponse;
32
+
33
+ interface Row {
34
+ label: string;
35
+ sign: string;
36
+ degree: number;
37
+ house?: number;
38
+ speed?: number;
39
+ isRetrograde?: boolean;
40
+ formula?: string;
41
+ natalLongitude?: number;
42
+ interpretation?: string;
43
+ isAngle?: boolean;
44
+ }
45
+
46
+ interface ViewModel {
47
+ title: string;
48
+ badges: Array<{ label: string; value: string }>;
49
+ summary?: string;
50
+ rows: Row[];
51
+ cols: { house: boolean; motion: boolean; formula: boolean; natal: boolean };
52
+ }
53
+
54
+ /**
55
+ * Editorial positions table for the Western point-list endpoints: asteroids,
56
+ * Black Moon Lilith, secondary progressions, solar arc directions, and the
57
+ * Arabic lots. One component, five shapes: it detects the response by its array
58
+ * key and shows only the columns that response carries (house, motion, formula,
59
+ * or a natal-to-directed comparison), then lists each body reading below.
60
+ */
61
+ @customElement('roxy-positions-table')
62
+ export class RoxyPositionsTable extends RoxyDataElement<PositionsResponse> {
63
+ static styles = [
64
+ baseStyles,
65
+ disclosureStyles,
66
+ css`
67
+ .wrap {
68
+ width: 100%;
69
+ background: var(--roxy-surface, #fff);
70
+ color: var(--roxy-fg, #0a0a0a);
71
+ border: 1px solid var(--roxy-border, #e4e4e7);
72
+ border-radius: var(--roxy-radius-md, 8px);
73
+ padding: var(--roxy-space-lg, 1.5rem);
74
+ box-shadow: var(--roxy-shadow-sm);
75
+ display: grid;
76
+ gap: var(--roxy-space-md, 1rem);
77
+ }
78
+ header {
79
+ display: flex;
80
+ flex-wrap: wrap;
81
+ align-items: baseline;
82
+ gap: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem);
83
+ }
84
+ .title {
85
+ font-size: var(--roxy-text-lg, 1.125rem);
86
+ font-weight: var(--roxy-weight-bold, 600);
87
+ margin: 0;
88
+ color: var(--roxy-primary, #0f172a);
89
+ }
90
+ .badges {
91
+ display: flex;
92
+ flex-wrap: wrap;
93
+ gap: var(--roxy-space-xs, 0.25rem);
94
+ }
95
+ .badge {
96
+ padding: 2px 8px;
97
+ border-radius: var(--roxy-radius-full, 9999px);
98
+ font-size: var(--roxy-text-xs, 0.75rem);
99
+ background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 14%, transparent);
100
+ color: var(--roxy-fg, #0a0a0a);
101
+ }
102
+ .badge b {
103
+ color: var(--roxy-accent-ink, #b45309);
104
+ font-weight: 600;
105
+ }
106
+ .summary {
107
+ color: var(--roxy-fg, #0a0a0a);
108
+ font-size: var(--roxy-text-sm, 0.875rem);
109
+ margin: 0;
110
+ }
111
+ .scroll {
112
+ overflow-x: auto;
113
+ -webkit-overflow-scrolling: touch;
114
+ }
115
+ table {
116
+ width: 100%;
117
+ border-collapse: collapse;
118
+ font-size: var(--roxy-text-sm, 0.875rem);
119
+ }
120
+ caption {
121
+ text-align: left;
122
+ color: var(--roxy-muted, #71717a);
123
+ font-size: var(--roxy-text-xs, 0.75rem);
124
+ padding-bottom: var(--roxy-space-xs, 0.25rem);
125
+ }
126
+ th,
127
+ td {
128
+ text-align: left;
129
+ padding: 6px 10px;
130
+ border-bottom: 1px solid var(--roxy-border, #e4e4e7);
131
+ white-space: nowrap;
132
+ }
133
+ th {
134
+ color: var(--roxy-muted, #71717a);
135
+ font-weight: 600;
136
+ text-transform: uppercase;
137
+ letter-spacing: 0.04em;
138
+ font-size: var(--roxy-text-xs, 0.75rem);
139
+ }
140
+ td.num {
141
+ text-align: right;
142
+ font-variant-numeric: tabular-nums;
143
+ }
144
+ .body-cell {
145
+ font-weight: 500;
146
+ color: var(--roxy-fg, #0a0a0a);
147
+ }
148
+ .body-cell .glyph {
149
+ color: var(--roxy-accent-ink, #b45309);
150
+ margin-right: 0.35rem;
151
+ }
152
+ tr.angle td {
153
+ color: var(--roxy-secondary, #475569);
154
+ }
155
+ .sign {
156
+ display: inline-flex;
157
+ align-items: baseline;
158
+ gap: 0.3rem;
159
+ }
160
+ .sign .sg {
161
+ color: var(--roxy-secondary, #475569);
162
+ }
163
+ .retro {
164
+ color: var(--roxy-danger, #dc2626);
165
+ font-weight: 600;
166
+ }
167
+ .formula {
168
+ color: var(--roxy-muted, #71717a);
169
+ font-variant-numeric: tabular-nums;
170
+ white-space: normal;
171
+ }
172
+ .readings h3 {
173
+ font-size: var(--roxy-text-sm, 0.875rem);
174
+ font-weight: 600;
175
+ color: var(--roxy-muted, #71717a);
176
+ text-transform: uppercase;
177
+ letter-spacing: 0.06em;
178
+ margin: 0 0 var(--roxy-space-sm, 0.5rem);
179
+ }
180
+ .interp-card {
181
+ border: 1px solid var(--roxy-border, #e4e4e7);
182
+ border-radius: var(--roxy-radius-md, 8px);
183
+ padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem);
184
+ margin-bottom: var(--roxy-space-xs, 0.25rem);
185
+ }
186
+ .interp-card summary {
187
+ cursor: pointer;
188
+ font-weight: 500;
189
+ color: var(--roxy-fg, #0a0a0a);
190
+ display: flex;
191
+ align-items: center;
192
+ justify-content: space-between;
193
+ gap: var(--roxy-space-md, 1rem);
194
+ }
195
+ .interp-aside {
196
+ display: inline-flex;
197
+ align-items: center;
198
+ gap: 0.5rem;
199
+ }
200
+ .interp-aside small {
201
+ color: var(--roxy-muted, #71717a);
202
+ font-weight: 400;
203
+ }
204
+ .interp-body {
205
+ margin-top: var(--roxy-space-xs, 0.25rem);
206
+ color: var(--roxy-fg, #0a0a0a);
207
+ font-size: var(--roxy-text-sm, 0.875rem);
208
+ }
209
+ `,
210
+ ];
211
+
212
+ /** Override the auto-derived heading. Empty keeps the per-shape default (e.g. "Asteroids"). */
213
+ @property({ type: String })
214
+ heading = '';
215
+
216
+ protected renderEmpty() {
217
+ return html`<div class="roxy-empty" role="status">No positions data</div>`;
218
+ }
219
+
220
+ protected renderData(data: PositionsResponse) {
221
+ const vm = this.toViewModel(data);
222
+ const cols = vm.cols;
223
+ const readings = vm.rows.filter((r) => r.interpretation);
224
+ return html`<div class="wrap">
225
+ <header>
226
+ <h2 class="title">${this.heading || vm.title}</h2>
227
+ ${
228
+ vm.badges.length
229
+ ? html`<div class="badges">
230
+ ${vm.badges.map((b) => html`<span class="badge"><b>${b.label}</b> ${b.value}</span>`)}
231
+ </div>`
232
+ : nothing
233
+ }
234
+ </header>
235
+ ${vm.summary ? html`<p class="summary">${vm.summary}</p>` : nothing}
236
+ <div class="scroll">
237
+ <table>
238
+ <caption>
239
+ ${vm.title}
240
+ </caption>
241
+ <thead>
242
+ <tr>
243
+ <th scope="col">Body</th>
244
+ <th scope="col">Position</th>
245
+ ${cols.natal ? html`<th scope="col">Natal</th>` : nothing}
246
+ ${cols.house ? html`<th scope="col" class="num">House</th>` : nothing}
247
+ ${cols.motion ? html`<th scope="col">Motion</th>` : nothing}
248
+ ${cols.formula ? html`<th scope="col">Formula</th>` : nothing}
249
+ </tr>
250
+ </thead>
251
+ <tbody>
252
+ ${vm.rows.map((r) => this.renderRow(r, cols))}
253
+ </tbody>
254
+ </table>
255
+ </div>
256
+ ${
257
+ readings.length
258
+ ? html`<section class="readings">
259
+ <h3>Readings</h3>
260
+ ${readings.map((r, i) => this.renderReading(r, i === 0))}
261
+ </section>`
262
+ : nothing
263
+ }
264
+ </div>`;
265
+ }
266
+
267
+ private renderRow(r: Row, cols: ViewModel['cols']) {
268
+ const glyph = PLANET_GLYPH[capitalize(r.label)];
269
+ return html`<tr class=${r.isAngle ? 'angle' : ''}>
270
+ <td class="body-cell">${glyph ? html`<span class="glyph">${glyph}</span>` : nothing}${r.label}</td>
271
+ <td>${this.signCell(r.sign, r.degree)}</td>
272
+ ${
273
+ cols.natal
274
+ ? html`<td>${r.natalLongitude != null ? this.signFromLongitude(r.natalLongitude) : html`&mdash;`}</td>`
275
+ : nothing
276
+ }
277
+ ${
278
+ cols.house
279
+ ? html`<td class="num">${r.house != null ? r.house : html`&mdash;`}</td>`
280
+ : nothing
281
+ }
282
+ ${
283
+ cols.motion
284
+ ? html`<td>${
285
+ r.speed != null
286
+ ? html`${formatNumber(r.speed, 3)}°/day${r.isRetrograde ? html` <span class="retro">℞</span>` : nothing}`
287
+ : html`&mdash;`
288
+ }</td>`
289
+ : nothing
290
+ }
291
+ ${cols.formula ? html`<td class="formula">${r.formula ?? html`&mdash;`}</td>` : nothing}
292
+ </tr>`;
293
+ }
294
+
295
+ private signCell(sign: string, degree: number) {
296
+ const g = SIGN_GLYPH[capitalize(sign)];
297
+ return html`<span class="sign">${g ? html`<span class="sg">${g}</span>` : nothing}${formatDegreeInSign(degree)} ${sign}</span>`;
298
+ }
299
+
300
+ private signFromLongitude(longitude: number) {
301
+ const p = longitudeToSignPosition(longitude);
302
+ return this.signCell(p.sign, p.degree + p.minute / 60);
303
+ }
304
+
305
+ private renderReading(r: Row, open: boolean) {
306
+ const glyph = PLANET_GLYPH[capitalize(r.label)] ?? '';
307
+ return html`<details class="interp-card" name="positions-readings" ?open=${open}>
308
+ <summary>
309
+ <span>${glyph ? html`${glyph} ` : nothing}${r.label}</span>
310
+ <span class="interp-aside">
311
+ <small>${r.sign} ${formatDegreeInSign(r.degree)}</small>
312
+ ${chevron()}
313
+ </span>
314
+ </summary>
315
+ <div class="interp-body">${r.interpretation}</div>
316
+ </details>`;
317
+ }
318
+
319
+ private toViewModel(data: PositionsResponse): ViewModel {
320
+ if ('asteroids' in data) {
321
+ return {
322
+ title: 'Asteroids',
323
+ badges: data.houseSystem
324
+ ? [{ label: 'Houses', value: data.houseSystem }]
325
+ : [],
326
+ summary: data.summary,
327
+ cols: { house: true, motion: true, formula: false, natal: false },
328
+ rows: data.asteroids.map((a) => ({
329
+ label: a.name,
330
+ sign: a.sign,
331
+ degree: a.degree,
332
+ house: a.house,
333
+ speed: a.speed,
334
+ isRetrograde: a.isRetrograde,
335
+ interpretation: a.interpretation,
336
+ })),
337
+ };
338
+ }
339
+ if ('lilith' in data) {
340
+ return {
341
+ title: 'Black Moon Lilith',
342
+ badges: data.houseSystem
343
+ ? [{ label: 'Houses', value: data.houseSystem }]
344
+ : [],
345
+ summary: data.summary,
346
+ cols: { house: true, motion: true, formula: false, natal: false },
347
+ rows: data.lilith.map((l) => ({
348
+ label: `${capitalize(l.variant)} apogee`,
349
+ sign: l.sign,
350
+ degree: l.degree,
351
+ house: l.house,
352
+ speed: l.speed,
353
+ isRetrograde: l.isRetrograde,
354
+ interpretation: l.interpretation,
355
+ })),
356
+ };
357
+ }
358
+ if ('directed' in data) {
359
+ return {
360
+ title: 'Solar arc directions',
361
+ badges: [
362
+ { label: 'Arc', value: `${formatNumber(data.solarArc, 2)}°` },
363
+ { label: 'Directed to', value: data.targetDate },
364
+ ],
365
+ summary: data.summary,
366
+ cols: { house: false, motion: false, formula: false, natal: true },
367
+ rows: data.directed.map((d) => ({
368
+ label: d.name,
369
+ sign: d.sign,
370
+ degree: d.degree,
371
+ natalLongitude: d.natalLongitude,
372
+ interpretation: d.interpretation,
373
+ })),
374
+ };
375
+ }
376
+ if ('lots' in data) {
377
+ return {
378
+ title: 'Arabic lots',
379
+ badges: data.sect
380
+ ? [{ label: 'Sect', value: capitalize(data.sect) }]
381
+ : [],
382
+ summary: data.summary,
383
+ cols: { house: false, motion: false, formula: true, natal: false },
384
+ rows: data.lots.map((l) => ({
385
+ label: l.name,
386
+ sign: l.sign,
387
+ degree: l.degree,
388
+ formula: l.formula,
389
+ interpretation: l.interpretation,
390
+ })),
391
+ };
392
+ }
393
+ // Secondary progressions: planets plus the progressed angles.
394
+ const angleRows: Row[] = [];
395
+ if (data.ascendant) {
396
+ angleRows.push({
397
+ label: 'Ascendant',
398
+ sign: data.ascendant.sign,
399
+ degree: data.ascendant.degree,
400
+ isAngle: true,
401
+ });
402
+ }
403
+ if (data.midheaven) {
404
+ angleRows.push({
405
+ label: 'Midheaven',
406
+ sign: data.midheaven.sign,
407
+ degree: data.midheaven.degree,
408
+ isAngle: true,
409
+ });
410
+ }
411
+ return {
412
+ title: 'Secondary progressions',
413
+ badges: [
414
+ { label: 'Progressed to', value: data.targetDate },
415
+ {
416
+ label: 'Elapsed',
417
+ value: `${formatNumber(data.elapsedYears, 1)} yrs`,
418
+ },
419
+ ],
420
+ summary: data.summary,
421
+ cols: { house: true, motion: true, formula: false, natal: false },
422
+ rows: [
423
+ ...angleRows,
424
+ ...data.planets.map((p) => ({
425
+ label: p.name,
426
+ sign: p.sign,
427
+ degree: p.degree,
428
+ house: p.house,
429
+ speed: p.speed,
430
+ isRetrograde: p.isRetrograde,
431
+ interpretation: p.interpretation,
432
+ })),
433
+ ],
434
+ };
435
+ }
436
+ }
437
+
438
+ declare global {
439
+ interface HTMLElementTagNameMap {
440
+ 'roxy-positions-table': RoxyPositionsTable;
441
+ }
442
+ }
@@ -0,0 +1,173 @@
1
+ import { css, html, nothing } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+ import { PLANET_GLYPH, SIGN_GLYPH } from '../tokens/index.js';
4
+ import type { ProfectionsResponse } from '../types/index.js';
5
+ import { RoxyDataElement } from '../utils/base-element.js';
6
+ import { baseStyles } from '../utils/base-styles.js';
7
+ import { capitalize } from '../utils/string.js';
8
+
9
+ /**
10
+ * Annual profection card. Renders a /astrology/profections response: the year's
11
+ * profected whole-sign house and sign, the lord of the year and where it sits in
12
+ * the natal chart, and the reading. A single focal card, not a table.
13
+ */
14
+ @customElement('roxy-profection-card')
15
+ export class RoxyProfectionCard extends RoxyDataElement<ProfectionsResponse> {
16
+ static styles = [
17
+ baseStyles,
18
+ css`
19
+ .wrap {
20
+ width: 100%;
21
+ background: var(--roxy-surface, #fff);
22
+ color: var(--roxy-fg, #0a0a0a);
23
+ border: 1px solid var(--roxy-border, #e4e4e7);
24
+ border-radius: var(--roxy-radius-md, 8px);
25
+ padding: var(--roxy-space-lg, 1.5rem);
26
+ box-shadow: var(--roxy-shadow-sm);
27
+ display: grid;
28
+ gap: var(--roxy-space-md, 1rem);
29
+ }
30
+ header {
31
+ display: flex;
32
+ flex-wrap: wrap;
33
+ align-items: baseline;
34
+ justify-content: space-between;
35
+ gap: var(--roxy-space-sm, 0.5rem);
36
+ }
37
+ .title {
38
+ font-size: var(--roxy-text-lg, 1.125rem);
39
+ font-weight: var(--roxy-weight-bold, 600);
40
+ margin: 0;
41
+ color: var(--roxy-primary, #0f172a);
42
+ }
43
+ .badge {
44
+ padding: 2px 8px;
45
+ border-radius: var(--roxy-radius-full, 9999px);
46
+ font-size: var(--roxy-text-xs, 0.75rem);
47
+ background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 14%, transparent);
48
+ color: var(--roxy-fg, #0a0a0a);
49
+ }
50
+ .badge b {
51
+ color: var(--roxy-accent-ink, #b45309);
52
+ font-weight: 600;
53
+ }
54
+ .focus {
55
+ display: flex;
56
+ flex-wrap: wrap;
57
+ align-items: center;
58
+ gap: var(--roxy-space-md, 1rem);
59
+ padding: var(--roxy-space-md, 1rem);
60
+ border-radius: var(--roxy-radius-md, 8px);
61
+ background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 9%, transparent);
62
+ }
63
+ .age {
64
+ display: grid;
65
+ justify-items: center;
66
+ min-width: 4.5rem;
67
+ }
68
+ .age .n {
69
+ font-size: 2rem;
70
+ font-weight: 700;
71
+ line-height: 1;
72
+ color: var(--roxy-accent-ink, #b45309);
73
+ font-variant-numeric: tabular-nums;
74
+ }
75
+ .age .l {
76
+ font-size: var(--roxy-text-xs, 0.75rem);
77
+ color: var(--roxy-muted, #71717a);
78
+ text-transform: uppercase;
79
+ letter-spacing: 0.06em;
80
+ }
81
+ .arrow {
82
+ color: var(--roxy-muted, #71717a);
83
+ font-size: var(--roxy-text-sm, 0.875rem);
84
+ }
85
+ .house {
86
+ display: grid;
87
+ gap: 2px;
88
+ }
89
+ .house .h {
90
+ font-size: var(--roxy-text-lg, 1.125rem);
91
+ font-weight: 600;
92
+ color: var(--roxy-fg, #0a0a0a);
93
+ }
94
+ .house .sign {
95
+ font-size: var(--roxy-text-sm, 0.875rem);
96
+ color: var(--roxy-secondary, #475569);
97
+ }
98
+ .house .sign .sg {
99
+ margin-right: 0.3rem;
100
+ }
101
+ .lord {
102
+ display: flex;
103
+ flex-wrap: wrap;
104
+ align-items: baseline;
105
+ gap: 0.4rem 0.75rem;
106
+ font-size: var(--roxy-text-sm, 0.875rem);
107
+ }
108
+ .lord .label {
109
+ color: var(--roxy-muted, #71717a);
110
+ text-transform: uppercase;
111
+ letter-spacing: 0.06em;
112
+ font-size: var(--roxy-text-xs, 0.75rem);
113
+ }
114
+ .lord .value {
115
+ font-weight: 600;
116
+ color: var(--roxy-fg, #0a0a0a);
117
+ }
118
+ .lord .sub {
119
+ color: var(--roxy-muted, #71717a);
120
+ }
121
+ .interp {
122
+ font-size: var(--roxy-text-sm, 0.875rem);
123
+ color: var(--roxy-fg, #0a0a0a);
124
+ margin: 0;
125
+ line-height: var(--roxy-leading-normal, 1.5);
126
+ }
127
+ `,
128
+ ];
129
+
130
+ protected renderEmpty() {
131
+ return html`<div class="roxy-empty" role="status">No profection data</div>`;
132
+ }
133
+
134
+ protected renderData(data: ProfectionsResponse) {
135
+ const signGlyph = SIGN_GLYPH[capitalize(data.profectedSign ?? '')];
136
+ const lordGlyph = PLANET_GLYPH[capitalize(data.lordOfYear ?? '')];
137
+ const lordNatalGlyph =
138
+ SIGN_GLYPH[capitalize(data.lordNatalPosition?.sign ?? '')];
139
+ return html`<div class="wrap">
140
+ <header>
141
+ <h2 class="title">Annual profection</h2>
142
+ ${data.targetDate ? html`<span class="badge"><b>For</b> ${data.targetDate}</span>` : nothing}
143
+ </header>
144
+ <div class="focus">
145
+ <div class="age">
146
+ <span class="n">${data.age}</span>
147
+ <span class="l">Age</span>
148
+ </div>
149
+ <span class="arrow">activates</span>
150
+ <div class="house">
151
+ <span class="h">House ${data.profectedHouse}</span>
152
+ <span class="sign">${signGlyph ? html`<span class="sg">${signGlyph}</span>` : nothing}${data.profectedSign}</span>
153
+ </div>
154
+ </div>
155
+ <div class="lord">
156
+ <span class="label">Lord of the year</span>
157
+ <span class="value">${lordGlyph ? html`${lordGlyph} ` : nothing}${data.lordOfYear}</span>
158
+ ${
159
+ data.lordNatalPosition
160
+ ? html`<span class="sub">natal ${lordNatalGlyph ? html`<span>${lordNatalGlyph}</span> ` : nothing}${data.lordNatalPosition.sign} · house ${data.lordNatalPosition.house}</span>`
161
+ : nothing
162
+ }
163
+ </div>
164
+ ${data.interpretation ? html`<p class="interp">${data.interpretation}</p>` : nothing}
165
+ </div>`;
166
+ }
167
+ }
168
+
169
+ declare global {
170
+ interface HTMLElementTagNameMap {
171
+ 'roxy-profection-card': RoxyProfectionCard;
172
+ }
173
+ }