@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.
- package/AGENTS.md +6 -0
- package/README.md +7 -1
- package/components-catalog.json +111 -1
- package/dist/cdn/components/astrocartography-map.js +58 -0
- package/dist/cdn/components/astrocartography-map.js.map +7 -0
- package/dist/cdn/components/divisional-chart.js +7 -7
- package/dist/cdn/components/divisional-chart.js.map +1 -1
- package/dist/cdn/components/dosha-card.js +2 -2
- package/dist/cdn/components/dosha-card.js.map +3 -3
- package/dist/cdn/components/fixed-stars.js +52 -0
- package/dist/cdn/components/fixed-stars.js.map +7 -0
- package/dist/cdn/components/hd-variables.js +2 -2
- package/dist/cdn/components/hd-variables.js.map +3 -3
- package/dist/cdn/components/hexagram.js +3 -3
- package/dist/cdn/components/hexagram.js.map +3 -3
- package/dist/cdn/components/local-space-compass.js +58 -0
- package/dist/cdn/components/local-space-compass.js.map +7 -0
- package/dist/cdn/components/moon-phase.js +3 -3
- package/dist/cdn/components/moon-phase.js.map +3 -3
- package/dist/cdn/components/natal-chart.js +8 -8
- package/dist/cdn/components/natal-chart.js.map +2 -2
- package/dist/cdn/components/positions-table.js +52 -0
- package/dist/cdn/components/positions-table.js.map +7 -0
- package/dist/cdn/components/profection-card.js +52 -0
- package/dist/cdn/components/profection-card.js.map +7 -0
- package/dist/cdn/components/reference-card.js +3 -3
- package/dist/cdn/components/reference-card.js.map +3 -3
- package/dist/cdn/components/relocation-wheel.js +61 -0
- package/dist/cdn/components/relocation-wheel.js.map +7 -0
- package/dist/cdn/components/synastry-chart.js +4 -4
- package/dist/cdn/components/synastry-chart.js.map +2 -2
- package/dist/cdn/components/vedic-kundli.js +5 -5
- package/dist/cdn/components/vedic-kundli.js.map +1 -1
- package/dist/cdn/components/vedic-planets-table.js +2 -2
- package/dist/cdn/components/vedic-planets-table.js.map +1 -1
- package/dist/cdn/components/western-planets-table.js +2 -2
- package/dist/cdn/components/western-planets-table.js.map +1 -1
- package/dist/cdn/components/yoga-list.js +3 -3
- package/dist/cdn/components/yoga-list.js.map +3 -3
- package/dist/cdn/roxy-ui.js +84 -72
- package/dist/cdn/roxy-ui.js.map +4 -4
- package/dist/components/astrocartography-map.d.ts +27 -0
- package/dist/components/astrocartography-map.d.ts.map +1 -0
- package/dist/components/astrocartography-map.js +8 -0
- package/dist/components/astrocartography-map.js.map +7 -0
- package/dist/components/divisional-chart.js +22 -22
- package/dist/components/divisional-chart.js.map +1 -1
- package/dist/components/dosha-card.d.ts.map +1 -1
- package/dist/components/dosha-card.js +1 -1
- package/dist/components/dosha-card.js.map +3 -3
- package/dist/components/fixed-stars.d.ts +21 -0
- package/dist/components/fixed-stars.d.ts.map +1 -0
- package/dist/components/fixed-stars.js +2 -0
- package/dist/components/fixed-stars.js.map +7 -0
- package/dist/components/hd-variables.d.ts.map +1 -1
- package/dist/components/hd-variables.js +1 -1
- package/dist/components/hd-variables.js.map +3 -3
- package/dist/components/hexagram.d.ts +3 -1
- package/dist/components/hexagram.d.ts.map +1 -1
- package/dist/components/hexagram.js +1 -1
- package/dist/components/hexagram.js.map +3 -3
- package/dist/components/local-space-compass.d.ts +23 -0
- package/dist/components/local-space-compass.d.ts.map +1 -0
- package/dist/components/local-space-compass.js +8 -0
- package/dist/components/local-space-compass.js.map +7 -0
- package/dist/components/moon-phase.d.ts.map +1 -1
- package/dist/components/moon-phase.js +1 -1
- package/dist/components/moon-phase.js.map +3 -3
- package/dist/components/natal-chart.d.ts +2 -0
- package/dist/components/natal-chart.d.ts.map +1 -1
- package/dist/components/natal-chart.js +6 -6
- package/dist/components/natal-chart.js.map +2 -2
- package/dist/components/positions-table.d.ts +34 -0
- package/dist/components/positions-table.d.ts.map +1 -0
- package/dist/components/positions-table.js +2 -0
- package/dist/components/positions-table.js.map +7 -0
- package/dist/components/profection-card.d.ts +18 -0
- package/dist/components/profection-card.d.ts.map +1 -0
- package/dist/components/profection-card.js +2 -0
- package/dist/components/profection-card.js.map +7 -0
- package/dist/components/reference-card.d.ts.map +1 -1
- package/dist/components/reference-card.js +1 -1
- package/dist/components/reference-card.js.map +3 -3
- package/dist/components/relocation-wheel.d.ts +21 -0
- package/dist/components/relocation-wheel.d.ts.map +1 -0
- package/dist/components/relocation-wheel.js +11 -0
- package/dist/components/relocation-wheel.js.map +7 -0
- package/dist/components/synastry-chart.js +3 -3
- package/dist/components/synastry-chart.js.map +2 -2
- package/dist/components/vedic-kundli.js +14 -14
- package/dist/components/vedic-kundli.js.map +1 -1
- package/dist/components/vedic-planets-table.js +1 -1
- package/dist/components/vedic-planets-table.js.map +1 -1
- package/dist/components/western-planets-table.js +1 -1
- package/dist/components/western-planets-table.js.map +1 -1
- package/dist/components/yoga-list.d.ts +5 -2
- package/dist/components/yoga-list.d.ts.map +1 -1
- package/dist/components/yoga-list.js +1 -1
- package/dist/components/yoga-list.js.map +3 -3
- package/dist/generated/endpoint-bindings.d.ts.map +1 -1
- package/dist/index.cjs +55 -43
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +63 -51
- package/dist/index.js.map +4 -4
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.json +6 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/types.gen.d.ts +7864 -5381
- package/dist/types/types.gen.d.ts.map +1 -1
- package/dist/utils/degree.d.ts +2 -0
- package/dist/utils/degree.d.ts.map +1 -1
- package/dist/utils/planet-color.d.ts +3 -0
- package/dist/utils/planet-color.d.ts.map +1 -0
- package/dist/utils/world-map.d.ts +8 -0
- package/dist/utils/world-map.d.ts.map +1 -0
- package/dist/version.d.ts +1 -1
- package/package.json +2 -1
- package/src/components/astrocartography-map.ts +442 -0
- package/src/components/dosha-card.ts +48 -16
- package/src/components/fixed-stars.ts +254 -0
- package/src/components/hd-variables.ts +30 -2
- package/src/components/hexagram.ts +11 -11
- package/src/components/local-space-compass.ts +299 -0
- package/src/components/moon-phase.ts +21 -2
- package/src/components/natal-chart.ts +36 -24
- package/src/components/positions-table.ts +442 -0
- package/src/components/profection-card.ts +173 -0
- package/src/components/reference-card.ts +40 -8
- package/src/components/relocation-wheel.ts +170 -0
- package/src/components/yoga-list.ts +95 -2
- package/src/generated/endpoint-bindings.ts +62 -0
- package/src/index.ts +6 -0
- package/src/manifest.ts +79 -0
- package/src/types/index.ts +1 -1
- package/src/types/types.gen.ts +7814 -5263
- package/src/utils/degree.ts +11 -0
- package/src/utils/planet-color.ts +45 -0
- package/src/utils/world-map.ts +8 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { css, html, nothing } from 'lit';
|
|
2
|
+
import { customElement } from 'lit/decorators.js';
|
|
3
|
+
import { SIGN_GLYPH } from '../tokens/index.js';
|
|
4
|
+
import type { FixedStarsResponse } from '../types/index.js';
|
|
5
|
+
import { RoxyDataElement } from '../utils/base-element.js';
|
|
6
|
+
import { baseStyles } from '../utils/base-styles.js';
|
|
7
|
+
import { formatDegreeInSign } from '../utils/degree.js';
|
|
8
|
+
import { chevron, disclosureStyles } from '../utils/disclosure.js';
|
|
9
|
+
import { formatNumber } from '../utils/format.js';
|
|
10
|
+
import { capitalize } from '../utils/string.js';
|
|
11
|
+
|
|
12
|
+
type Star = FixedStarsResponse['stars'][number];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fixed stars table. Leads with the high-value view from a
|
|
16
|
+
* /astrology/fixed-stars response: every star-to-natal-point conjunction sorted
|
|
17
|
+
* tightest first, each with its reading. The full precessed star catalog
|
|
18
|
+
* (position, magnitude, traditional nature, keywords) sits in a secondary
|
|
19
|
+
* disclosure so the contacts stay front and center.
|
|
20
|
+
*/
|
|
21
|
+
@customElement('roxy-fixed-stars')
|
|
22
|
+
export class RoxyFixedStars extends RoxyDataElement<FixedStarsResponse> {
|
|
23
|
+
static styles = [
|
|
24
|
+
baseStyles,
|
|
25
|
+
disclosureStyles,
|
|
26
|
+
css`
|
|
27
|
+
.wrap {
|
|
28
|
+
width: 100%;
|
|
29
|
+
background: var(--roxy-surface, #fff);
|
|
30
|
+
color: var(--roxy-fg, #0a0a0a);
|
|
31
|
+
border: 1px solid var(--roxy-border, #e4e4e7);
|
|
32
|
+
border-radius: var(--roxy-radius-md, 8px);
|
|
33
|
+
padding: var(--roxy-space-lg, 1.5rem);
|
|
34
|
+
box-shadow: var(--roxy-shadow-sm);
|
|
35
|
+
display: grid;
|
|
36
|
+
gap: var(--roxy-space-md, 1rem);
|
|
37
|
+
}
|
|
38
|
+
header {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-wrap: wrap;
|
|
41
|
+
align-items: baseline;
|
|
42
|
+
gap: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem);
|
|
43
|
+
}
|
|
44
|
+
.title {
|
|
45
|
+
font-size: var(--roxy-text-lg, 1.125rem);
|
|
46
|
+
font-weight: var(--roxy-weight-bold, 600);
|
|
47
|
+
margin: 0;
|
|
48
|
+
color: var(--roxy-primary, #0f172a);
|
|
49
|
+
}
|
|
50
|
+
.badge {
|
|
51
|
+
padding: 2px 8px;
|
|
52
|
+
border-radius: var(--roxy-radius-full, 9999px);
|
|
53
|
+
font-size: var(--roxy-text-xs, 0.75rem);
|
|
54
|
+
background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 14%, transparent);
|
|
55
|
+
color: var(--roxy-fg, #0a0a0a);
|
|
56
|
+
}
|
|
57
|
+
.badge b {
|
|
58
|
+
color: var(--roxy-accent-ink, #b45309);
|
|
59
|
+
font-weight: 600;
|
|
60
|
+
}
|
|
61
|
+
.summary {
|
|
62
|
+
color: var(--roxy-fg, #0a0a0a);
|
|
63
|
+
font-size: var(--roxy-text-sm, 0.875rem);
|
|
64
|
+
margin: 0;
|
|
65
|
+
}
|
|
66
|
+
.empty-note {
|
|
67
|
+
color: var(--roxy-muted, #71717a);
|
|
68
|
+
font-size: var(--roxy-text-sm, 0.875rem);
|
|
69
|
+
margin: 0;
|
|
70
|
+
}
|
|
71
|
+
.subhead {
|
|
72
|
+
font-size: var(--roxy-text-sm, 0.875rem);
|
|
73
|
+
font-weight: 600;
|
|
74
|
+
color: var(--roxy-muted, #71717a);
|
|
75
|
+
text-transform: uppercase;
|
|
76
|
+
letter-spacing: 0.06em;
|
|
77
|
+
margin: 0 0 var(--roxy-space-sm, 0.5rem);
|
|
78
|
+
}
|
|
79
|
+
.interp-card {
|
|
80
|
+
border: 1px solid var(--roxy-border, #e4e4e7);
|
|
81
|
+
border-radius: var(--roxy-radius-md, 8px);
|
|
82
|
+
padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem);
|
|
83
|
+
margin-bottom: var(--roxy-space-xs, 0.25rem);
|
|
84
|
+
}
|
|
85
|
+
.interp-card summary {
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
font-weight: 500;
|
|
88
|
+
color: var(--roxy-fg, #0a0a0a);
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
justify-content: space-between;
|
|
92
|
+
gap: var(--roxy-space-md, 1rem);
|
|
93
|
+
}
|
|
94
|
+
.contact {
|
|
95
|
+
display: inline-flex;
|
|
96
|
+
align-items: baseline;
|
|
97
|
+
gap: 0.4rem;
|
|
98
|
+
}
|
|
99
|
+
.contact .point {
|
|
100
|
+
color: var(--roxy-accent-ink, #b45309);
|
|
101
|
+
font-weight: 600;
|
|
102
|
+
}
|
|
103
|
+
.interp-aside {
|
|
104
|
+
display: inline-flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
gap: 0.5rem;
|
|
107
|
+
}
|
|
108
|
+
.interp-aside small {
|
|
109
|
+
color: var(--roxy-muted, #71717a);
|
|
110
|
+
font-weight: 400;
|
|
111
|
+
font-variant-numeric: tabular-nums;
|
|
112
|
+
}
|
|
113
|
+
.interp-body {
|
|
114
|
+
margin-top: var(--roxy-space-xs, 0.25rem);
|
|
115
|
+
color: var(--roxy-fg, #0a0a0a);
|
|
116
|
+
font-size: var(--roxy-text-sm, 0.875rem);
|
|
117
|
+
}
|
|
118
|
+
.catalog summary {
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
color: var(--roxy-fg, #0a0a0a);
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 0.5rem;
|
|
125
|
+
}
|
|
126
|
+
.scroll {
|
|
127
|
+
overflow-x: auto;
|
|
128
|
+
margin-top: var(--roxy-space-sm, 0.5rem);
|
|
129
|
+
}
|
|
130
|
+
table {
|
|
131
|
+
width: 100%;
|
|
132
|
+
border-collapse: collapse;
|
|
133
|
+
font-size: var(--roxy-text-sm, 0.875rem);
|
|
134
|
+
}
|
|
135
|
+
th,
|
|
136
|
+
td {
|
|
137
|
+
text-align: left;
|
|
138
|
+
padding: 6px 10px;
|
|
139
|
+
border-bottom: 1px solid var(--roxy-border, #e4e4e7);
|
|
140
|
+
white-space: nowrap;
|
|
141
|
+
vertical-align: top;
|
|
142
|
+
}
|
|
143
|
+
th {
|
|
144
|
+
color: var(--roxy-muted, #71717a);
|
|
145
|
+
font-weight: 600;
|
|
146
|
+
text-transform: uppercase;
|
|
147
|
+
letter-spacing: 0.04em;
|
|
148
|
+
font-size: var(--roxy-text-xs, 0.75rem);
|
|
149
|
+
}
|
|
150
|
+
td.num {
|
|
151
|
+
text-align: right;
|
|
152
|
+
font-variant-numeric: tabular-nums;
|
|
153
|
+
}
|
|
154
|
+
.sg {
|
|
155
|
+
color: var(--roxy-secondary, #475569);
|
|
156
|
+
margin-right: 0.3rem;
|
|
157
|
+
}
|
|
158
|
+
.kw {
|
|
159
|
+
display: flex;
|
|
160
|
+
flex-wrap: wrap;
|
|
161
|
+
gap: 0.2rem;
|
|
162
|
+
white-space: normal;
|
|
163
|
+
max-width: 18rem;
|
|
164
|
+
}
|
|
165
|
+
.kw span {
|
|
166
|
+
padding: 0 6px;
|
|
167
|
+
border-radius: var(--roxy-radius-full, 9999px);
|
|
168
|
+
font-size: var(--roxy-text-xs, 0.75rem);
|
|
169
|
+
background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 45%, transparent);
|
|
170
|
+
color: var(--roxy-fg, #0a0a0a);
|
|
171
|
+
}
|
|
172
|
+
`,
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
protected renderEmpty() {
|
|
176
|
+
return html`<div class="roxy-empty" role="status">No fixed star data</div>`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
protected renderData(data: FixedStarsResponse) {
|
|
180
|
+
const conjunctions = data.conjunctions ?? [];
|
|
181
|
+
const stars = data.stars ?? [];
|
|
182
|
+
return html`<div class="wrap">
|
|
183
|
+
<header>
|
|
184
|
+
<h2 class="title">Fixed stars</h2>
|
|
185
|
+
${
|
|
186
|
+
typeof data.orb === 'number'
|
|
187
|
+
? html`<span class="badge"><b>Orb</b> ${formatNumber(data.orb, 1)}°</span>`
|
|
188
|
+
: nothing
|
|
189
|
+
}
|
|
190
|
+
</header>
|
|
191
|
+
${data.summary ? html`<p class="summary">${data.summary}</p>` : nothing}
|
|
192
|
+
${
|
|
193
|
+
conjunctions.length
|
|
194
|
+
? html`<section>
|
|
195
|
+
<p class="subhead">Conjunctions to the chart</p>
|
|
196
|
+
${conjunctions.map((c, i) => {
|
|
197
|
+
return html`<details class="interp-card" name="fixed-star-contacts" ?open=${i === 0}>
|
|
198
|
+
<summary>
|
|
199
|
+
<span class="contact"><span class="point">${c.point}</span> conjunct ${c.star}</span>
|
|
200
|
+
<span class="interp-aside">
|
|
201
|
+
<small>orb ${formatNumber(c.orb, 2)}°</small>
|
|
202
|
+
${chevron()}
|
|
203
|
+
</span>
|
|
204
|
+
</summary>
|
|
205
|
+
${c.interpretation ? html`<div class="interp-body">${c.interpretation}</div>` : nothing}
|
|
206
|
+
</details>`;
|
|
207
|
+
})}
|
|
208
|
+
</section>`
|
|
209
|
+
: html`<p class="empty-note">No star sits within the orb of a natal point.</p>`
|
|
210
|
+
}
|
|
211
|
+
${stars.length ? this.renderCatalog(stars) : nothing}
|
|
212
|
+
</div>`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private renderCatalog(stars: Star[]) {
|
|
216
|
+
return html`<details class="catalog">
|
|
217
|
+
<summary>${chevron()} Star catalog (${stars.length})</summary>
|
|
218
|
+
<div class="scroll">
|
|
219
|
+
<table>
|
|
220
|
+
<caption class="subhead">Precessed positions for the chart date</caption>
|
|
221
|
+
<thead>
|
|
222
|
+
<tr>
|
|
223
|
+
<th scope="col">Star</th>
|
|
224
|
+
<th scope="col">Position</th>
|
|
225
|
+
<th scope="col" class="num">Mag</th>
|
|
226
|
+
<th scope="col">Nature</th>
|
|
227
|
+
<th scope="col">Keywords</th>
|
|
228
|
+
</tr>
|
|
229
|
+
</thead>
|
|
230
|
+
<tbody>
|
|
231
|
+
${stars.map((s) => {
|
|
232
|
+
const g = SIGN_GLYPH[capitalize(s.sign)];
|
|
233
|
+
return html`<tr>
|
|
234
|
+
<td>${s.name}</td>
|
|
235
|
+
<td>${g ? html`<span class="sg">${g}</span>` : nothing}${formatDegreeInSign(s.degree)} ${s.sign}</td>
|
|
236
|
+
<td class="num">${formatNumber(s.magnitude, 1)}</td>
|
|
237
|
+
<td>${s.nature}</td>
|
|
238
|
+
<td>
|
|
239
|
+
<div class="kw">${(s.keywords ?? []).map((k) => html`<span>${k}</span>`)}</div>
|
|
240
|
+
</td>
|
|
241
|
+
</tr>`;
|
|
242
|
+
})}
|
|
243
|
+
</tbody>
|
|
244
|
+
</table>
|
|
245
|
+
</div>
|
|
246
|
+
</details>`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
declare global {
|
|
251
|
+
interface HTMLElementTagNameMap {
|
|
252
|
+
'roxy-fixed-stars': RoxyFixedStars;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -80,7 +80,14 @@ export class RoxyHdVariables extends RoxyDataElement<CalculateVariablesResponse>
|
|
|
80
80
|
];
|
|
81
81
|
|
|
82
82
|
protected renderData(d: CalculateVariablesResponse) {
|
|
83
|
-
|
|
83
|
+
// Place the arrows by their bodygraph `position`, not response order: the
|
|
84
|
+
// 2-col grid fills row-major, so sorting to Top left, Top right, Bottom left,
|
|
85
|
+
// Bottom right keeps the design column (Determination + Environment) on the
|
|
86
|
+
// left and the personality column (Motivation + Perspective) on the right,
|
|
87
|
+
// which is the whole point of the four-arrow layout.
|
|
88
|
+
const arrows = [...(d.arrows ?? [])].sort(
|
|
89
|
+
(a, b) => quadrantOrder(a.position) - quadrantOrder(b.position),
|
|
90
|
+
);
|
|
84
91
|
return html`<div class="wrap" aria-label="Human Design variables">
|
|
85
92
|
<h2 class="title">Variables</h2>
|
|
86
93
|
<div class="grid">${arrows.map((a) => this.renderArrow(a))}</div>
|
|
@@ -110,7 +117,12 @@ export class RoxyHdVariables extends RoxyDataElement<CalculateVariablesResponse>
|
|
|
110
117
|
</span>
|
|
111
118
|
${
|
|
112
119
|
typeof a.color === 'number'
|
|
113
|
-
? html`<span class="ctb">Color ${a.color} · Tone ${a.tone} · Base ${a.base}</span>`
|
|
120
|
+
? html`<span class="ctb">Color ${a.color} · Tone ${a.tone} · Base ${a.base}${a.activation?.planet ? ` · ${a.activation.planet}${a.activation.side ? ` (${a.activation.side})` : ''}` : ''}</span>`
|
|
121
|
+
: nothing
|
|
122
|
+
}
|
|
123
|
+
${
|
|
124
|
+
a.confident === false
|
|
125
|
+
? html`<span class="note" role="note">Knife-edge: could flip with a more precise birth time.</span>`
|
|
114
126
|
: nothing
|
|
115
127
|
}
|
|
116
128
|
</div>`;
|
|
@@ -121,6 +133,22 @@ export class RoxyHdVariables extends RoxyDataElement<CalculateVariablesResponse>
|
|
|
121
133
|
}
|
|
122
134
|
}
|
|
123
135
|
|
|
136
|
+
/** Canonical bodygraph reading order for the four arrows, so the 2-col grid lays them out by quadrant. Unknown positions sort last. */
|
|
137
|
+
function quadrantOrder(position: string | undefined): number {
|
|
138
|
+
switch (position) {
|
|
139
|
+
case 'Top left':
|
|
140
|
+
return 0;
|
|
141
|
+
case 'Top right':
|
|
142
|
+
return 1;
|
|
143
|
+
case 'Bottom left':
|
|
144
|
+
return 2;
|
|
145
|
+
case 'Bottom right':
|
|
146
|
+
return 3;
|
|
147
|
+
default:
|
|
148
|
+
return 99;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
124
152
|
declare global {
|
|
125
153
|
interface HTMLElementTagNameMap {
|
|
126
154
|
'roxy-hd-variables': RoxyHdVariables;
|
|
@@ -266,18 +266,18 @@ export class RoxyHexagram extends RoxyDataElement<HexagramData> {
|
|
|
266
266
|
</article>`;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
/**
|
|
269
|
+
/**
|
|
270
|
+
* Lines for a static hexagram (lookup/random/daily, which carry no cast `lines` array): read the `binary` pattern. Per the spec it is 6 digits bottom to top, 1 = yang (solid), 0 = yin (broken), so index 0 is line 1 (bottom). Mapped to the same 7 = solid / 8 = broken code the renderer uses for cast lines. Falls back to all-yang only if `binary` is malformed. The Unicode `symbol` block (U+4DC0) is in King Wen order, NOT line order, so it must never be used to derive the lines.
|
|
271
|
+
*/
|
|
270
272
|
private derivedLines(h: Hexagram): number[] {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
return lines;
|
|
273
|
+
const binary = h.binary ?? '';
|
|
274
|
+
if (/^[01]{6}$/.test(binary)) {
|
|
275
|
+
// `binary` is top to bottom (index 0 = line 6), verified against the
|
|
276
|
+
// canonical encoding: hexagram 46 (Earth over Wind) is "000110", which is
|
|
277
|
+
// Earth/Wind only when read top down. The renderer expects bottom to top
|
|
278
|
+
// (line 1 first, like the cast `lines` array), so reverse. 1 = yang
|
|
279
|
+
// (solid, 7), 0 = yin (broken, 8).
|
|
280
|
+
return Array.from(binary, (c) => (c === '1' ? 7 : 8)).reverse();
|
|
281
281
|
}
|
|
282
282
|
return Array.from({ length: 6 }, () => 7);
|
|
283
283
|
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { css, html, nothing, svg } from 'lit';
|
|
2
|
+
import { customElement } from 'lit/decorators.js';
|
|
3
|
+
import type { LocalSpaceResponse } from '../types/index.js';
|
|
4
|
+
import { RoxyDataElement } from '../utils/base-element.js';
|
|
5
|
+
import { baseStyles } from '../utils/base-styles.js';
|
|
6
|
+
import { planetColor } from '../utils/planet-color.js';
|
|
7
|
+
|
|
8
|
+
type Body = LocalSpaceResponse['bodies'][number];
|
|
9
|
+
|
|
10
|
+
const SIZE = 320;
|
|
11
|
+
const CENTER = SIZE / 2;
|
|
12
|
+
const RIM = 128;
|
|
13
|
+
const SPOKE = 118;
|
|
14
|
+
const GLYPH_R = 140;
|
|
15
|
+
const TICK_LABEL_R = 150;
|
|
16
|
+
|
|
17
|
+
// Compass azimuth (0 = north, clockwise) to a screen point. North is up, east
|
|
18
|
+
// is right, matching how the local space line is read off a real compass.
|
|
19
|
+
function azimuthPoint(az: number, r: number): { x: number; y: number } {
|
|
20
|
+
const rad = (az * Math.PI) / 180;
|
|
21
|
+
return { x: CENTER + r * Math.sin(rad), y: CENTER - r * Math.cos(rad) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PRINCIPAL = [
|
|
25
|
+
{ az: 0, label: 'N' },
|
|
26
|
+
{ az: 45, label: 'NE' },
|
|
27
|
+
{ az: 90, label: 'E' },
|
|
28
|
+
{ az: 135, label: 'SE' },
|
|
29
|
+
{ az: 180, label: 'S' },
|
|
30
|
+
{ az: 225, label: 'SW' },
|
|
31
|
+
{ az: 270, label: 'W' },
|
|
32
|
+
{ az: 315, label: 'NW' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Local space compass. Plots each body from a /astrology/local-space response as
|
|
37
|
+
* a directional line radiating from the birthplace at its azimuth (0 = north,
|
|
38
|
+
* clockwise), with a 16-point ring. Bodies below the horizon are dimmed. Color
|
|
39
|
+
* is per body and theme-token driven.
|
|
40
|
+
*/
|
|
41
|
+
@customElement('roxy-local-space-compass')
|
|
42
|
+
export class RoxyLocalSpaceCompass extends RoxyDataElement<LocalSpaceResponse> {
|
|
43
|
+
static styles = [
|
|
44
|
+
baseStyles,
|
|
45
|
+
css`
|
|
46
|
+
.wrap {
|
|
47
|
+
width: 100%;
|
|
48
|
+
background: var(--roxy-surface, #fff);
|
|
49
|
+
color: var(--roxy-fg, #0a0a0a);
|
|
50
|
+
border: 1px solid var(--roxy-border, #e4e4e7);
|
|
51
|
+
border-radius: var(--roxy-radius-md, 8px);
|
|
52
|
+
padding: var(--roxy-space-lg, 1.5rem);
|
|
53
|
+
box-shadow: var(--roxy-shadow-sm);
|
|
54
|
+
display: grid;
|
|
55
|
+
gap: var(--roxy-space-md, 1rem);
|
|
56
|
+
}
|
|
57
|
+
.title {
|
|
58
|
+
font-size: var(--roxy-text-lg, 1.125rem);
|
|
59
|
+
font-weight: var(--roxy-weight-bold, 600);
|
|
60
|
+
margin: 0;
|
|
61
|
+
color: var(--roxy-primary, #0f172a);
|
|
62
|
+
}
|
|
63
|
+
.meta {
|
|
64
|
+
color: var(--roxy-muted, #71717a);
|
|
65
|
+
font-size: var(--roxy-text-sm, 0.875rem);
|
|
66
|
+
}
|
|
67
|
+
svg {
|
|
68
|
+
display: block;
|
|
69
|
+
width: 100%;
|
|
70
|
+
max-width: var(--roxy-chart-max-width, 480px);
|
|
71
|
+
aspect-ratio: 1 / 1;
|
|
72
|
+
height: auto;
|
|
73
|
+
margin: 0 auto;
|
|
74
|
+
}
|
|
75
|
+
.dial {
|
|
76
|
+
fill: none;
|
|
77
|
+
stroke: var(--roxy-border, #e4e4e7);
|
|
78
|
+
}
|
|
79
|
+
.dial-fill {
|
|
80
|
+
fill: color-mix(in srgb, var(--roxy-border, #e4e4e7) 10%, transparent);
|
|
81
|
+
stroke: var(--roxy-border, #e4e4e7);
|
|
82
|
+
stroke-width: 1;
|
|
83
|
+
}
|
|
84
|
+
.cardinal-axis {
|
|
85
|
+
stroke: var(--roxy-border, #e4e4e7);
|
|
86
|
+
stroke-width: 0.5;
|
|
87
|
+
}
|
|
88
|
+
.tick {
|
|
89
|
+
stroke: var(--roxy-secondary, #475569);
|
|
90
|
+
stroke-width: 0.6;
|
|
91
|
+
opacity: 0.5;
|
|
92
|
+
}
|
|
93
|
+
.compass-label {
|
|
94
|
+
fill: var(--roxy-secondary, #475569);
|
|
95
|
+
font-size: 9px;
|
|
96
|
+
font-weight: 600;
|
|
97
|
+
font-family: var(--roxy-font-sans);
|
|
98
|
+
}
|
|
99
|
+
.compass-label.cardinal {
|
|
100
|
+
fill: var(--roxy-fg, #0a0a0a);
|
|
101
|
+
}
|
|
102
|
+
.center-dot {
|
|
103
|
+
fill: var(--roxy-fg, #0a0a0a);
|
|
104
|
+
}
|
|
105
|
+
.spoke {
|
|
106
|
+
stroke-width: 1.4;
|
|
107
|
+
}
|
|
108
|
+
.spoke.below {
|
|
109
|
+
stroke-dasharray: 3 3;
|
|
110
|
+
opacity: 0.4;
|
|
111
|
+
}
|
|
112
|
+
.body-glyph {
|
|
113
|
+
font-size: 11px;
|
|
114
|
+
font-weight: 600;
|
|
115
|
+
font-family: var(--roxy-font-sans);
|
|
116
|
+
}
|
|
117
|
+
.body-glyph.below {
|
|
118
|
+
opacity: 0.45;
|
|
119
|
+
}
|
|
120
|
+
.list {
|
|
121
|
+
width: 100%;
|
|
122
|
+
border-collapse: collapse;
|
|
123
|
+
font-size: var(--roxy-text-sm, 0.875rem);
|
|
124
|
+
}
|
|
125
|
+
.list th,
|
|
126
|
+
.list td {
|
|
127
|
+
text-align: left;
|
|
128
|
+
padding: 4px 8px;
|
|
129
|
+
border-bottom: 1px solid var(--roxy-border, #e4e4e7);
|
|
130
|
+
}
|
|
131
|
+
.list th {
|
|
132
|
+
color: var(--roxy-muted, #71717a);
|
|
133
|
+
font-weight: 600;
|
|
134
|
+
text-transform: uppercase;
|
|
135
|
+
letter-spacing: 0.04em;
|
|
136
|
+
font-size: var(--roxy-text-xs, 0.75rem);
|
|
137
|
+
}
|
|
138
|
+
.list td.num {
|
|
139
|
+
text-align: right;
|
|
140
|
+
font-variant-numeric: tabular-nums;
|
|
141
|
+
}
|
|
142
|
+
.body-cell {
|
|
143
|
+
display: inline-flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
gap: 0.4rem;
|
|
146
|
+
}
|
|
147
|
+
.body-dot {
|
|
148
|
+
width: 10px;
|
|
149
|
+
height: 10px;
|
|
150
|
+
border-radius: 50%;
|
|
151
|
+
flex-shrink: 0;
|
|
152
|
+
}
|
|
153
|
+
.horizon-pill {
|
|
154
|
+
padding: 1px 7px;
|
|
155
|
+
border-radius: var(--roxy-radius-full, 9999px);
|
|
156
|
+
font-size: var(--roxy-text-xs, 0.75rem);
|
|
157
|
+
}
|
|
158
|
+
.horizon-pill.up {
|
|
159
|
+
background: color-mix(in srgb, var(--roxy-success, #16a34a) 16%, transparent);
|
|
160
|
+
color: var(--roxy-success-fg, #166534);
|
|
161
|
+
}
|
|
162
|
+
.horizon-pill.down {
|
|
163
|
+
background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 55%, transparent);
|
|
164
|
+
color: var(--roxy-fg, #0a0a0a);
|
|
165
|
+
}
|
|
166
|
+
.summary {
|
|
167
|
+
color: var(--roxy-fg, #0a0a0a);
|
|
168
|
+
font-size: var(--roxy-text-sm, 0.875rem);
|
|
169
|
+
margin: 0;
|
|
170
|
+
}
|
|
171
|
+
`,
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
protected renderEmpty() {
|
|
175
|
+
return html`<div class="roxy-empty" role="status">No local space data</div>`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
protected renderData(data: LocalSpaceResponse) {
|
|
179
|
+
const bodies = data.bodies ?? [];
|
|
180
|
+
const bd = data.birthDetails;
|
|
181
|
+
return html`<div class="wrap">
|
|
182
|
+
<header>
|
|
183
|
+
<h2 class="title">Local space</h2>
|
|
184
|
+
${
|
|
185
|
+
bd
|
|
186
|
+
? html`<div class="meta">${[bd.date, bd.time].filter(Boolean).join(' · ')}</div>`
|
|
187
|
+
: nothing
|
|
188
|
+
}
|
|
189
|
+
</header>
|
|
190
|
+
${this.renderDial(bodies)}
|
|
191
|
+
${data.summary ? html`<p class="summary">${data.summary}</p>` : nothing}
|
|
192
|
+
${this.renderList(bodies)}
|
|
193
|
+
</div>`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private renderDial(bodies: Body[]) {
|
|
197
|
+
return html`<svg
|
|
198
|
+
viewBox="0 0 ${SIZE} ${SIZE}"
|
|
199
|
+
role="img"
|
|
200
|
+
aria-label="Local space compass of planetary directions from the birthplace"
|
|
201
|
+
>
|
|
202
|
+
<title>Local space compass</title>
|
|
203
|
+
<desc>
|
|
204
|
+
A compass centered on the birthplace. Each body is a line pointing to
|
|
205
|
+
its azimuth, clockwise from north. Bodies below the horizon are dimmed.
|
|
206
|
+
</desc>
|
|
207
|
+
<circle class="dial-fill" cx=${CENTER} cy=${CENTER} r=${RIM} />
|
|
208
|
+
<circle class="dial" cx=${CENTER} cy=${CENTER} r=${RIM * 0.66} stroke-width="0.5" />
|
|
209
|
+
<circle class="dial" cx=${CENTER} cy=${CENTER} r=${RIM * 0.33} stroke-width="0.5" />
|
|
210
|
+
${this.renderCompassRing()}
|
|
211
|
+
${this.renderSpokes(bodies)}
|
|
212
|
+
<circle class="center-dot" cx=${CENTER} cy=${CENTER} r="2.5" />
|
|
213
|
+
</svg>`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private renderCompassRing() {
|
|
217
|
+
const ticks = [];
|
|
218
|
+
// 16-point ring: a tick every 22.5 degrees.
|
|
219
|
+
for (let az = 0; az < 360; az += 22.5) {
|
|
220
|
+
const outer = azimuthPoint(az, RIM);
|
|
221
|
+
const inner = azimuthPoint(az, RIM - (az % 45 === 0 ? 8 : 4));
|
|
222
|
+
ticks.push(
|
|
223
|
+
svg`<line class="tick" x1=${inner.x} y1=${inner.y} x2=${outer.x} y2=${outer.y} />`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
// Cardinal cross.
|
|
227
|
+
const ns1 = azimuthPoint(0, RIM);
|
|
228
|
+
const ns2 = azimuthPoint(180, RIM);
|
|
229
|
+
const ew1 = azimuthPoint(90, RIM);
|
|
230
|
+
const ew2 = azimuthPoint(270, RIM);
|
|
231
|
+
const labels = PRINCIPAL.map(({ az, label }) => {
|
|
232
|
+
const pos = azimuthPoint(az, TICK_LABEL_R);
|
|
233
|
+
const cardinal = az % 90 === 0;
|
|
234
|
+
return svg`<text class=${`compass-label${cardinal ? ' cardinal' : ''}`} x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central">${label}</text>`;
|
|
235
|
+
});
|
|
236
|
+
return svg`
|
|
237
|
+
<line class="cardinal-axis" x1=${ns1.x} y1=${ns1.y} x2=${ns2.x} y2=${ns2.y} />
|
|
238
|
+
<line class="cardinal-axis" x1=${ew1.x} y1=${ew1.y} x2=${ew2.x} y2=${ew2.y} />
|
|
239
|
+
${ticks}${labels}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private renderSpokes(bodies: Body[]) {
|
|
243
|
+
return bodies.map((b, i) => {
|
|
244
|
+
const color = planetColor(b.planet, i);
|
|
245
|
+
const below = b.aboveHorizon === false;
|
|
246
|
+
const end = azimuthPoint(b.azimuth, SPOKE);
|
|
247
|
+
const glyphPos = azimuthPoint(b.azimuth, GLYPH_R);
|
|
248
|
+
const glyph = b.symbol || b.planet.slice(0, 2);
|
|
249
|
+
const altLabel = `${b.altitude > 0 ? '+' : ''}${Math.round(b.altitude)}°`;
|
|
250
|
+
return svg`<g>
|
|
251
|
+
<line class=${`spoke${below ? ' below' : ''}`} stroke=${color} x1=${CENTER} y1=${CENTER} x2=${end.x} y2=${end.y}><title>${b.planet} ${b.compassDirection} ${Math.round(b.azimuth)}° altitude ${altLabel}</title></line>
|
|
252
|
+
<text class=${`body-glyph${below ? ' below' : ''}`} fill=${color} x=${glyphPos.x} y=${glyphPos.y} text-anchor="middle" dominant-baseline="central">${glyph}</text>
|
|
253
|
+
</g>`;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private renderList(bodies: Body[]) {
|
|
258
|
+
if (bodies.length === 0) return nothing;
|
|
259
|
+
return html`<table class="list">
|
|
260
|
+
<thead>
|
|
261
|
+
<tr>
|
|
262
|
+
<th>Body</th>
|
|
263
|
+
<th>Direction</th>
|
|
264
|
+
<th class="num">Azimuth</th>
|
|
265
|
+
<th class="num">Altitude</th>
|
|
266
|
+
<th>Horizon</th>
|
|
267
|
+
</tr>
|
|
268
|
+
</thead>
|
|
269
|
+
<tbody>
|
|
270
|
+
${bodies.map((b, i) => {
|
|
271
|
+
const color = planetColor(b.planet, i);
|
|
272
|
+
const below = b.aboveHorizon === false;
|
|
273
|
+
return html`<tr>
|
|
274
|
+
<td>
|
|
275
|
+
<span class="body-cell">
|
|
276
|
+
<span class="body-dot" style=${`background: ${color}`}></span>
|
|
277
|
+
${b.symbol ? html`${b.symbol} ` : nothing}${b.planet}
|
|
278
|
+
</span>
|
|
279
|
+
</td>
|
|
280
|
+
<td>${b.compassDirection}</td>
|
|
281
|
+
<td class="num">${Math.round(b.azimuth)}°</td>
|
|
282
|
+
<td class="num">${b.altitude > 0 ? '+' : ''}${Math.round(b.altitude)}°</td>
|
|
283
|
+
<td>
|
|
284
|
+
<span class=${`horizon-pill ${below ? 'down' : 'up'}`}>
|
|
285
|
+
${below ? 'Below' : 'Above'}
|
|
286
|
+
</span>
|
|
287
|
+
</td>
|
|
288
|
+
</tr>`;
|
|
289
|
+
})}
|
|
290
|
+
</tbody>
|
|
291
|
+
</table>`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
declare global {
|
|
296
|
+
interface HTMLElementTagNameMap {
|
|
297
|
+
'roxy-local-space-compass': RoxyLocalSpaceCompass;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -139,7 +139,9 @@ export class RoxyMoonPhase extends RoxyDataElement<MoonPhaseData> {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
private renderSingle(d: GetCurrentMoonPhaseResponse) {
|
|
142
|
-
|
|
142
|
+
// The API ships the exact phase emoji in meaning.symbol; prefer it and fall
|
|
143
|
+
// back to the name-derived glyph for the list endpoints that omit meaning.
|
|
144
|
+
const emoji = d.meaning?.symbol || phaseEmoji(d.phase);
|
|
143
145
|
return html`<article class="card" aria-label="Current moon phase">
|
|
144
146
|
<div class="hero">
|
|
145
147
|
<span class="emoji" aria-hidden="true">${emoji}</span>
|
|
@@ -207,9 +209,26 @@ export class RoxyMoonPhase extends RoxyDataElement<MoonPhaseData> {
|
|
|
207
209
|
}
|
|
208
210
|
}
|
|
209
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Map a phase name to its emoji, tolerant of the live API naming. The API sends
|
|
214
|
+
* suffixed names ("Waxing Gibbous Moon") and "Third Quarter Moon" where the map
|
|
215
|
+
* keys are unsuffixed and use "last quarter"; only "new moon"/"full moon" keep
|
|
216
|
+
* the suffix. Try the raw lowercase, then the suffix stripped, then the
|
|
217
|
+
* third->last quarter alias, then the alias re-suffixed, so every one of the
|
|
218
|
+
* eight phases resolves in both the suffixed and unsuffixed forms.
|
|
219
|
+
*/
|
|
210
220
|
function phaseEmoji(phase: string | undefined): string {
|
|
211
221
|
if (!phase) return '🌙';
|
|
212
|
-
|
|
222
|
+
const lc = phase.toLowerCase().trim();
|
|
223
|
+
const noMoon = lc.replace(/\s*moon$/, '').trim();
|
|
224
|
+
const alias = noMoon === 'third quarter' ? 'last quarter' : noMoon;
|
|
225
|
+
return (
|
|
226
|
+
MOON_PHASE_EMOJI[lc] ??
|
|
227
|
+
MOON_PHASE_EMOJI[noMoon] ??
|
|
228
|
+
MOON_PHASE_EMOJI[alias] ??
|
|
229
|
+
MOON_PHASE_EMOJI[`${alias} moon`] ??
|
|
230
|
+
'🌙'
|
|
231
|
+
);
|
|
213
232
|
}
|
|
214
233
|
|
|
215
234
|
function formatIllumination(v: number): string {
|