@roxyapi/ui 0.1.1 → 0.1.3

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 (169) hide show
  1. package/AGENTS.md +2 -2
  2. package/LICENSE +21 -0
  3. package/README.md +505 -0
  4. package/THEMING.md +24 -7
  5. package/dist/cdn/components/biorhythm-chart.js +15 -22
  6. package/dist/cdn/components/biorhythm-chart.js.map +3 -3
  7. package/dist/cdn/components/compatibility-card.js +36 -34
  8. package/dist/cdn/components/compatibility-card.js.map +4 -4
  9. package/dist/cdn/components/dasha-timeline.js +35 -39
  10. package/dist/cdn/components/dasha-timeline.js.map +4 -4
  11. package/dist/cdn/components/data.js +6 -6
  12. package/dist/cdn/components/data.js.map +3 -3
  13. package/dist/cdn/components/dosha-card.js +13 -13
  14. package/dist/cdn/components/dosha-card.js.map +2 -2
  15. package/dist/cdn/components/endpoint-form.js +47 -28
  16. package/dist/cdn/components/endpoint-form.js.map +3 -3
  17. package/dist/cdn/components/guna-milan.js +18 -18
  18. package/dist/cdn/components/guna-milan.js.map +4 -4
  19. package/dist/cdn/components/hexagram.js +26 -26
  20. package/dist/cdn/components/hexagram.js.map +3 -3
  21. package/dist/cdn/components/horoscope-card.js +38 -38
  22. package/dist/cdn/components/horoscope-card.js.map +3 -3
  23. package/dist/cdn/components/kp-planets-table.js +10 -10
  24. package/dist/cdn/components/kp-planets-table.js.map +4 -4
  25. package/dist/cdn/components/location-search.js +6 -6
  26. package/dist/cdn/components/location-search.js.map +3 -3
  27. package/dist/cdn/components/moon-phase.js +21 -21
  28. package/dist/cdn/components/moon-phase.js.map +4 -4
  29. package/dist/cdn/components/natal-chart.js +61 -19
  30. package/dist/cdn/components/natal-chart.js.map +4 -4
  31. package/dist/cdn/components/numerology-card.js +40 -31
  32. package/dist/cdn/components/numerology-card.js.map +3 -3
  33. package/dist/cdn/components/panchang-table.js +25 -25
  34. package/dist/cdn/components/panchang-table.js.map +4 -4
  35. package/dist/cdn/components/synastry-chart.js +129 -39
  36. package/dist/cdn/components/synastry-chart.js.map +4 -4
  37. package/dist/cdn/components/tarot-card.js +49 -20
  38. package/dist/cdn/components/tarot-card.js.map +3 -3
  39. package/dist/cdn/components/tarot-spread.js +43 -27
  40. package/dist/cdn/components/tarot-spread.js.map +3 -3
  41. package/dist/cdn/components/vedic-kundli.js +23 -9
  42. package/dist/cdn/components/vedic-kundli.js.map +3 -3
  43. package/dist/cdn/roxy-ui.js +560 -350
  44. package/dist/cdn/roxy-ui.js.map +4 -4
  45. package/dist/components/biorhythm-chart.d.ts +2 -46
  46. package/dist/components/biorhythm-chart.d.ts.map +1 -1
  47. package/dist/components/biorhythm-chart.js +24 -23
  48. package/dist/components/biorhythm-chart.js.map +2 -2
  49. package/dist/components/compatibility-card.d.ts +2 -27
  50. package/dist/components/compatibility-card.d.ts.map +1 -1
  51. package/dist/components/compatibility-card.js +50 -29
  52. package/dist/components/compatibility-card.js.map +3 -3
  53. package/dist/components/dasha-timeline.d.ts +2 -31
  54. package/dist/components/dasha-timeline.d.ts.map +1 -1
  55. package/dist/components/dasha-timeline.js +32 -30
  56. package/dist/components/dasha-timeline.js.map +3 -3
  57. package/dist/components/data.d.ts +6 -0
  58. package/dist/components/data.d.ts.map +1 -1
  59. package/dist/components/data.js +9 -1
  60. package/dist/components/data.js.map +2 -2
  61. package/dist/components/dosha-card.d.ts +2 -16
  62. package/dist/components/dosha-card.d.ts.map +1 -1
  63. package/dist/components/dosha-card.js +12 -13
  64. package/dist/components/dosha-card.js.map +2 -2
  65. package/dist/components/endpoint-form.d.ts +2 -0
  66. package/dist/components/endpoint-form.d.ts.map +1 -1
  67. package/dist/components/endpoint-form.js +66 -8
  68. package/dist/components/endpoint-form.js.map +2 -2
  69. package/dist/components/guna-milan.d.ts +2 -20
  70. package/dist/components/guna-milan.d.ts.map +1 -1
  71. package/dist/components/guna-milan.js +22 -12
  72. package/dist/components/guna-milan.js.map +3 -3
  73. package/dist/components/hexagram.d.ts +3 -27
  74. package/dist/components/hexagram.d.ts.map +1 -1
  75. package/dist/components/hexagram.js +31 -15
  76. package/dist/components/hexagram.js.map +2 -2
  77. package/dist/components/horoscope-card.d.ts +2 -20
  78. package/dist/components/horoscope-card.d.ts.map +1 -1
  79. package/dist/components/horoscope-card.js +24 -15
  80. package/dist/components/horoscope-card.js.map +2 -2
  81. package/dist/components/kp-planets-table.d.ts +2 -21
  82. package/dist/components/kp-planets-table.d.ts.map +1 -1
  83. package/dist/components/kp-planets-table.js +10 -4
  84. package/dist/components/kp-planets-table.js.map +3 -3
  85. package/dist/components/location-search.d.ts +3 -11
  86. package/dist/components/location-search.d.ts.map +1 -1
  87. package/dist/components/location-search.js +45 -5
  88. package/dist/components/location-search.js.map +2 -2
  89. package/dist/components/moon-phase.d.ts +4 -21
  90. package/dist/components/moon-phase.d.ts.map +1 -1
  91. package/dist/components/moon-phase.js +17 -4
  92. package/dist/components/moon-phase.js.map +3 -3
  93. package/dist/components/natal-chart.d.ts +7 -43
  94. package/dist/components/natal-chart.d.ts.map +1 -1
  95. package/dist/components/natal-chart.js +130 -70
  96. package/dist/components/natal-chart.js.map +3 -3
  97. package/dist/components/numerology-card.d.ts +5 -37
  98. package/dist/components/numerology-card.d.ts.map +1 -1
  99. package/dist/components/numerology-card.js +54 -28
  100. package/dist/components/numerology-card.js.map +2 -2
  101. package/dist/components/panchang-table.d.ts +3 -62
  102. package/dist/components/panchang-table.d.ts.map +1 -1
  103. package/dist/components/panchang-table.js +62 -32
  104. package/dist/components/panchang-table.js.map +3 -3
  105. package/dist/components/synastry-chart.d.ts +9 -28
  106. package/dist/components/synastry-chart.d.ts.map +1 -1
  107. package/dist/components/synastry-chart.js +178 -38
  108. package/dist/components/synastry-chart.js.map +3 -3
  109. package/dist/components/tarot-card.d.ts +5 -29
  110. package/dist/components/tarot-card.d.ts.map +1 -1
  111. package/dist/components/tarot-card.js +59 -20
  112. package/dist/components/tarot-card.js.map +2 -2
  113. package/dist/components/tarot-spread.d.ts +2 -24
  114. package/dist/components/tarot-spread.d.ts.map +1 -1
  115. package/dist/components/tarot-spread.js +39 -13
  116. package/dist/components/tarot-spread.js.map +2 -2
  117. package/dist/components/vedic-kundli.d.ts +3 -23
  118. package/dist/components/vedic-kundli.d.ts.map +1 -1
  119. package/dist/components/vedic-kundli.js +25 -13
  120. package/dist/components/vedic-kundli.js.map +2 -2
  121. package/dist/index.cjs +1149 -358
  122. package/dist/index.cjs.map +4 -4
  123. package/dist/index.d.ts +6 -4
  124. package/dist/index.d.ts.map +1 -1
  125. package/dist/index.js +1149 -358
  126. package/dist/index.js.map +4 -4
  127. package/dist/manifest.d.ts +49 -0
  128. package/dist/manifest.d.ts.map +1 -0
  129. package/dist/manifest.json +1 -1
  130. package/dist/styles/tokens.css +47 -1
  131. package/dist/tokens/index.d.ts.map +1 -1
  132. package/dist/types/index.d.ts +2 -0
  133. package/dist/types/index.d.ts.map +1 -0
  134. package/dist/types/types.gen.d.ts +27811 -0
  135. package/dist/types/types.gen.d.ts.map +1 -0
  136. package/dist/utils/debounce.d.ts +9 -1
  137. package/dist/utils/debounce.d.ts.map +1 -1
  138. package/dist/utils/format.d.ts +15 -0
  139. package/dist/utils/format.d.ts.map +1 -0
  140. package/dist/version.d.ts +2 -0
  141. package/dist/version.d.ts.map +1 -0
  142. package/package.json +9 -1
  143. package/src/components/biorhythm-chart.ts +39 -84
  144. package/src/components/compatibility-card.ts +85 -52
  145. package/src/components/dasha-timeline.ts +55 -73
  146. package/src/components/data.ts +20 -1
  147. package/src/components/dosha-card.ts +18 -31
  148. package/src/components/endpoint-form.ts +79 -11
  149. package/src/components/guna-milan.ts +16 -34
  150. package/src/components/hexagram.ts +53 -43
  151. package/src/components/horoscope-card.ts +51 -39
  152. package/src/components/kp-planets-table.ts +8 -27
  153. package/src/components/location-search.ts +45 -20
  154. package/src/components/moon-phase.ts +28 -25
  155. package/src/components/natal-chart.ts +129 -84
  156. package/src/components/numerology-card.ts +87 -79
  157. package/src/components/panchang-table.ts +40 -78
  158. package/src/components/synastry-chart.ts +220 -78
  159. package/src/components/tarot-card.ts +76 -62
  160. package/src/components/tarot-spread.ts +72 -45
  161. package/src/components/vedic-kundli.ts +42 -51
  162. package/src/index.ts +14 -24
  163. package/src/manifest.ts +366 -0
  164. package/src/styles/tokens.css +47 -1
  165. package/src/tokens/index.ts +5 -0
  166. package/src/types/types.gen.ts +1 -1
  167. package/src/utils/debounce.ts +23 -4
  168. package/src/utils/format.ts +57 -0
  169. package/src/version.ts +2 -0
@@ -1,28 +1,17 @@
1
1
  import { css, html, LitElement, nothing } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
3
  import { SIGN_GLYPH } from '../tokens/index.js';
4
+ import type {
5
+ GetDailyHoroscopeResponse,
6
+ GetMonthlyHoroscopeResponse,
7
+ GetWeeklyHoroscopeResponse,
8
+ } from '../types/index.js';
4
9
  import { baseStyles } from '../utils/base-styles.js';
5
10
 
6
- interface HoroscopeData {
7
- sign?: string;
8
- date?: string;
9
- overview?: string;
10
- love?: string;
11
- career?: string;
12
- health?: string;
13
- finance?: string;
14
- advice?: string;
15
- luckyNumber?: number | string;
16
- luckyColor?: string;
17
- compatibleSigns?: string[];
18
- moonSign?: string;
19
- moonPhase?: string;
20
- energyRating?: number;
21
- week?: string;
22
- month?: string;
23
- luckyDays?: string[];
24
- luckyNumbers?: number[];
25
- }
11
+ type HoroscopeData =
12
+ | GetDailyHoroscopeResponse
13
+ | GetWeeklyHoroscopeResponse
14
+ | GetMonthlyHoroscopeResponse;
26
15
 
27
16
  /**
28
17
  * Daily, weekly, or monthly horoscope card. Pass `data` from
@@ -163,8 +152,15 @@ export class RoxyHoroscopeCard extends LitElement {
163
152
 
164
153
  const sign = d.sign ?? '';
165
154
  const glyph = sign ? (SIGN_GLYPH[capitalize(sign)] ?? '') : '';
166
- const energy = typeof d.energyRating === 'number' ? d.energyRating : null;
167
- const dateLabel = d.date ?? d.week ?? d.month ?? '';
155
+ const energy =
156
+ 'energyRating' in d && typeof d.energyRating === 'number'
157
+ ? d.energyRating
158
+ : null;
159
+ const dateLabel =
160
+ ('date' in d && d.date) ||
161
+ ('week' in d && d.week) ||
162
+ ('month' in d && d.month) ||
163
+ '';
168
164
 
169
165
  return html`<article
170
166
  class="card"
@@ -224,7 +220,7 @@ export class RoxyHoroscopeCard extends LitElement {
224
220
  : nothing
225
221
  }
226
222
  ${
227
- d.advice
223
+ 'advice' in d && d.advice
228
224
  ? html`<div class="section">
229
225
  <h3>Advice</h3>
230
226
  <p>${d.advice}</p>
@@ -233,49 +229,65 @@ export class RoxyHoroscopeCard extends LitElement {
233
229
  }
234
230
  </div>
235
231
 
236
- ${
237
- d.luckyNumber || d.luckyColor || (d.compatibleSigns?.length ?? 0) > 0
238
- ? html`<div class="lucky">
232
+ ${(() => {
233
+ const luckyNumber =
234
+ 'luckyNumber' in d && d.luckyNumber !== undefined
235
+ ? d.luckyNumber
236
+ : undefined;
237
+ const luckyColor =
238
+ 'luckyColor' in d && d.luckyColor ? d.luckyColor : '';
239
+ const luckyNumbers =
240
+ 'luckyNumbers' in d && d.luckyNumbers ? d.luckyNumbers : [];
241
+ const luckyDays = 'luckyDays' in d && d.luckyDays ? d.luckyDays : [];
242
+ const compatibleSigns = d.compatibleSigns ?? [];
243
+ if (
244
+ luckyNumber === undefined &&
245
+ !luckyColor &&
246
+ luckyNumbers.length === 0 &&
247
+ luckyDays.length === 0 &&
248
+ compatibleSigns.length === 0
249
+ )
250
+ return nothing;
251
+ return html`<div class="lucky">
239
252
  ${
240
- d.luckyNumber !== undefined
241
- ? html`<span>Lucky number <strong>${d.luckyNumber}</strong></span>`
253
+ luckyNumber !== undefined
254
+ ? html`<span>Lucky number <strong>${luckyNumber}</strong></span>`
242
255
  : nothing
243
256
  }
244
257
  ${
245
- d.luckyColor
246
- ? html`<span>Lucky color <strong>${d.luckyColor}</strong></span>`
258
+ luckyColor
259
+ ? html`<span>Lucky color <strong>${luckyColor}</strong></span>`
247
260
  : nothing
248
261
  }
249
262
  ${
250
- d.luckyNumbers?.length
263
+ luckyNumbers.length
251
264
  ? html`<span
252
265
  >Lucky numbers
253
- <strong>${d.luckyNumbers.join(', ')}</strong></span
266
+ <strong>${luckyNumbers.join(', ')}</strong></span
254
267
  >`
255
268
  : nothing
256
269
  }
257
270
  ${
258
- d.luckyDays?.length
271
+ luckyDays.length
259
272
  ? html`<span
260
- >Lucky days <strong>${d.luckyDays.join(', ')}</strong></span
273
+ >Lucky days <strong>${luckyDays.join(', ')}</strong></span
261
274
  >`
262
275
  : nothing
263
276
  }
264
277
  ${
265
- d.compatibleSigns?.length
278
+ compatibleSigns.length
266
279
  ? html`<span class="compat-wrap">
267
280
  Best with
268
281
  <span class="compat"
269
- >${d.compatibleSigns.map(
282
+ >${compatibleSigns.map(
270
283
  (s) => html`<span>${s}</span>`,
271
284
  )}</span
272
285
  >
273
286
  </span>`
274
287
  : nothing
275
288
  }
276
- </div>`
277
- : nothing
278
- }
289
+ </div>`;
290
+ })()}
279
291
  </article>`;
280
292
  }
281
293
  }
@@ -1,27 +1,8 @@
1
1
  import { css, html, LitElement, nothing } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
+ import type { KpPlanetsResponse } from '../types/index.js';
3
4
  import { baseStyles } from '../utils/base-styles.js';
4
-
5
- interface KpPlanet {
6
- planet?: string;
7
- name?: string;
8
- sign?: string;
9
- signLord?: string;
10
- nakshatra?: string;
11
- nakshatraLord?: string;
12
- pada?: number;
13
- starLord?: string;
14
- subLord?: string;
15
- subSubLord?: string;
16
- kpNumber?: number;
17
- retrograde?: boolean;
18
- longitude?: number;
19
- }
20
-
21
- interface KpData {
22
- ayanamsa?: number | string;
23
- planets?: KpPlanet[];
24
- }
5
+ import { formatNumber } from '../utils/format.js';
25
6
 
26
7
  /**
27
8
  * KP planets table with sub-lord and sub-sub-lord columns. Renders
@@ -86,7 +67,7 @@ export class RoxyKpPlanetsTable extends LitElement {
86
67
  color: var(--roxy-fg, #0a0a0a);
87
68
  }
88
69
  .retro {
89
- color: var(--roxy-warning, #ea580c);
70
+ color: var(--roxy-warning-fg, #9a3412);
90
71
  font-size: var(--roxy-text-xs, 0.75rem);
91
72
  margin-left: 4px;
92
73
  }
@@ -94,7 +75,7 @@ export class RoxyKpPlanetsTable extends LitElement {
94
75
  ];
95
76
 
96
77
  @property({ attribute: false })
97
- data: KpData | null = null;
78
+ data: KpPlanetsResponse | null = null;
98
79
 
99
80
  render() {
100
81
  if (!this.data)
@@ -109,8 +90,8 @@ export class RoxyKpPlanetsTable extends LitElement {
109
90
  <header class="head">
110
91
  <h2 class="title">KP planets</h2>
111
92
  ${
112
- this.data.ayanamsa
113
- ? html`<span class="ayanamsa">Ayanamsa: ${this.data.ayanamsa}</span>`
93
+ typeof this.data.ayanamsa === 'number'
94
+ ? html`<span class="ayanamsa">Ayanamsa: ${formatNumber(this.data.ayanamsa, 2)}°</span>`
114
95
  : nothing
115
96
  }
116
97
  </header>
@@ -131,13 +112,13 @@ export class RoxyKpPlanetsTable extends LitElement {
131
112
  ${planets.map(
132
113
  (p) => html`<tr>
133
114
  <td class="planet">
134
- ${p.planet ?? p.name ?? ''}
115
+ ${p.planet}
135
116
  ${p.retrograde ? html`<span class="retro">R</span>` : nothing}
136
117
  </td>
137
118
  <td>${p.sign ?? ''}</td>
138
119
  <td>${p.signLord ?? ''}</td>
139
120
  <td>${p.nakshatra ?? ''}</td>
140
- <td>${p.starLord ?? p.nakshatraLord ?? ''}</td>
121
+ <td>${p.nakshatraLord ?? ''}</td>
141
122
  <td>${p.subLord ?? ''}</td>
142
123
  <td>${p.subSubLord ?? ''}</td>
143
124
  <td>${p.kpNumber ?? ''}</td>
@@ -1,24 +1,10 @@
1
1
  import { css, html, LitElement, nothing } from 'lit';
2
2
  import { customElement, property, state } from 'lit/decorators.js';
3
+ import type { SearchCitiesResponse } from '../types/index.js';
3
4
  import { baseStyles } from '../utils/base-styles.js';
4
5
  import { debounce } from '../utils/debounce.js';
5
6
 
6
- export interface CityResult {
7
- city: string;
8
- province?: string;
9
- country: string;
10
- iso2?: string;
11
- latitude: number;
12
- longitude: number;
13
- timezone: string;
14
- utcOffset: number;
15
- population?: number;
16
- }
17
-
18
- interface CitySearchResponse {
19
- total?: number;
20
- cities?: CityResult[];
21
- }
7
+ type CityResult = SearchCitiesResponse['cities'][number];
22
8
 
23
9
  /**
24
10
  * Stateful location search input. Calls /location/search and emits
@@ -175,6 +161,8 @@ export class RoxyLocationSearch extends LitElement {
175
161
  private highlight = -1;
176
162
 
177
163
  private clickOutsideHandler?: (e: MouseEvent) => void;
164
+ private abortController?: AbortController;
165
+ private secretKeyWarned = false;
178
166
  private debouncedFetch = debounce((q: string) => {
179
167
  void this.fetchResults(q);
180
168
  }, 300);
@@ -194,9 +182,41 @@ export class RoxyLocationSearch extends LitElement {
194
182
  if (this.clickOutsideHandler) {
195
183
  document.removeEventListener('mousedown', this.clickOutsideHandler);
196
184
  }
185
+ this.debouncedFetch.cancel();
186
+ if (this.abortController) {
187
+ this.abortController.abort();
188
+ this.abortController = undefined;
189
+ }
190
+ }
191
+
192
+ private warnIfSecretKey() {
193
+ if (this.secretKeyWarned) return;
194
+ if (!this.apiKey) return;
195
+ // Browser-safe publishable keys carry the `pk_` prefix and a server-side
196
+ // origin allowlist. Anything else (a raw secret key, UUID-style token)
197
+ // must not ship to the browser.
198
+ if (this.apiKey.startsWith('pk_')) return;
199
+ this.secretKeyWarned = true;
200
+ const message =
201
+ 'Possible secret key in client-side <roxy-location-search>; use a `pk_` publishable key with origin allowlist instead.';
202
+ // eslint-disable-next-line no-console
203
+ console.warn(message);
204
+ this.dispatchEvent(
205
+ new CustomEvent('roxy-validation-error', {
206
+ detail: { reason: 'possible-secret-key', message },
207
+ bubbles: true,
208
+ composed: true,
209
+ }),
210
+ );
197
211
  }
198
212
 
199
213
  private async fetchResults(q: string) {
214
+ this.warnIfSecretKey();
215
+ // Abort any in-flight request so a stale response cannot overwrite a
216
+ // fresher one (debounced typing) or land after disconnect.
217
+ if (this.abortController) this.abortController.abort();
218
+ const controller = new AbortController();
219
+ this.abortController = controller;
200
220
  this.isLoading = true;
201
221
  try {
202
222
  const url = new URL(this.endpoint);
@@ -207,17 +227,22 @@ export class RoxyLocationSearch extends LitElement {
207
227
  };
208
228
  if (this.apiKey) headers['X-API-Key'] = this.apiKey;
209
229
  if (this.publishableKey) headers['X-API-Key'] = this.publishableKey;
210
- const res = await fetch(url, { headers });
230
+ const res = await fetch(url, { headers, signal: controller.signal });
211
231
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
212
- const json = (await res.json()) as CitySearchResponse;
232
+ const json = (await res.json()) as SearchCitiesResponse;
233
+ if (controller.signal.aborted) return;
213
234
  this.results = json.cities ?? [];
214
235
  this.isOpen = this.results.length > 0;
215
236
  this.highlight = this.results.length > 0 ? 0 : -1;
216
- } catch (_err) {
237
+ } catch (err) {
238
+ if ((err as { name?: string })?.name === 'AbortError') return;
217
239
  this.results = [];
218
240
  this.isOpen = false;
219
241
  } finally {
220
- this.isLoading = false;
242
+ if (this.abortController === controller) {
243
+ this.abortController = undefined;
244
+ }
245
+ if (!controller.signal.aborted) this.isLoading = false;
221
246
  }
222
247
  }
223
248
 
@@ -1,27 +1,21 @@
1
1
  import { css, html, LitElement, nothing } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
3
  import { MOON_PHASE_EMOJI } from '../tokens/index.js';
4
+ import type {
5
+ GetCurrentMoonPhaseResponse,
6
+ GetMoonCalendarResponse,
7
+ GetUpcomingMoonPhasesResponse,
8
+ } from '../types/index.js';
4
9
  import { baseStyles } from '../utils/base-styles.js';
10
+ import { formatNumber } from '../utils/format.js';
5
11
 
6
- interface MoonPhaseData {
7
- date?: string;
8
- phase?: string;
9
- illumination?: number;
10
- age?: number;
11
- sign?: string;
12
- degree?: number;
13
- distance?: number;
14
- meaning?: {
15
- name?: string;
16
- symbol?: string;
17
- description?: string;
18
- keywords?: string[];
19
- };
20
- month?: string;
21
- year?: number;
22
- phases?: Array<MoonPhaseData>;
23
- upcoming?: Array<MoonPhaseData>;
24
- }
12
+ type MoonPhaseData =
13
+ | GetCurrentMoonPhaseResponse
14
+ | GetUpcomingMoonPhasesResponse
15
+ | GetMoonCalendarResponse;
16
+ type MoonListEntry =
17
+ | GetUpcomingMoonPhasesResponse['phases'][number]
18
+ | GetMoonCalendarResponse['calendar'][number];
25
19
 
26
20
  /**
27
21
  * Moon phase card. Renders /astrology/moon-phase/{current,upcoming,calendar/...}.
@@ -125,22 +119,26 @@ export class RoxyMoonPhase extends LitElement {
125
119
  const d = this.data;
126
120
  if (!d)
127
121
  return html`<div class="roxy-empty" role="status">No moon phase data</div>`;
128
- const list = d.phases ?? d.upcoming ?? [];
122
+ const list: MoonListEntry[] =
123
+ 'phases' in d ? d.phases : 'calendar' in d ? d.calendar : [];
129
124
  if (this.mode !== 'current' && list.length > 0) {
125
+ const month = 'month' in d ? d.month : undefined;
126
+ const year = 'year' in d ? d.year : undefined;
130
127
  return html`<article
131
128
  class="card"
132
129
  aria-label="Moon phase calendar"
133
130
  >
134
- <h2 class="label">${d.month ?? 'Moon phases'} ${d.year ?? ''}</h2>
131
+ <h2 class="label">${month ?? 'Moon phases'} ${year ?? ''}</h2>
135
132
  <div class="list" role="list">
136
133
  ${list.map((phase) => this.renderListItem(phase))}
137
134
  </div>
138
135
  </article>`;
139
136
  }
137
+ if (!('phase' in d)) return nothing;
140
138
  return this.renderSingle(d);
141
139
  }
142
140
 
143
- private renderSingle(d: MoonPhaseData) {
141
+ private renderSingle(d: GetCurrentMoonPhaseResponse) {
144
142
  const emoji = phaseEmoji(d.phase);
145
143
  return html`<article class="card" aria-label="Current moon phase">
146
144
  <div class="hero">
@@ -155,7 +153,7 @@ export class RoxyMoonPhase extends LitElement {
155
153
  typeof d.illumination === 'number'
156
154
  ? html`<div>
157
155
  <span>Illumination</span>
158
- <strong>${(d.illumination * 100).toFixed(0)}%</strong>
156
+ <strong>${formatIllumination(d.illumination)}</strong>
159
157
  </div>`
160
158
  : nothing
161
159
  }
@@ -163,7 +161,7 @@ export class RoxyMoonPhase extends LitElement {
163
161
  typeof d.age === 'number'
164
162
  ? html`<div>
165
163
  <span>Age</span>
166
- <strong>${d.age.toFixed(1)} days</strong>
164
+ <strong>${formatNumber(d.age, 1)} days</strong>
167
165
  </div>`
168
166
  : nothing
169
167
  }
@@ -199,7 +197,7 @@ export class RoxyMoonPhase extends LitElement {
199
197
  </article>`;
200
198
  }
201
199
 
202
- private renderListItem(p: MoonPhaseData) {
200
+ private renderListItem(p: MoonListEntry) {
203
201
  const emoji = phaseEmoji(p.phase);
204
202
  return html`<div class="list-item" role="listitem">
205
203
  <span aria-hidden="true">${emoji}</span>
@@ -214,6 +212,11 @@ function phaseEmoji(phase: string | undefined): string {
214
212
  return MOON_PHASE_EMOJI[phase.toLowerCase()] ?? '🌙';
215
213
  }
216
214
 
215
+ function formatIllumination(v: number): string {
216
+ const pct = v <= 1 ? v * 100 : v;
217
+ return `${Math.round(pct)}%`;
218
+ }
219
+
217
220
  declare global {
218
221
  interface HTMLElementTagNameMap {
219
222
  'roxy-moon-phase': RoxyMoonPhase;