@roxyapi/ui 0.1.2 → 0.2.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 (220) hide show
  1. package/AGENTS.md +6 -0
  2. package/README.md +327 -14
  3. package/THEMING.md +24 -7
  4. package/dist/cdn/components/ashtakavarga-grid.js +349 -0
  5. package/dist/cdn/components/ashtakavarga-grid.js.map +7 -0
  6. package/dist/cdn/components/biorhythm-chart.js +15 -22
  7. package/dist/cdn/components/biorhythm-chart.js.map +3 -3
  8. package/dist/cdn/components/choghadiya-grid.js +239 -0
  9. package/dist/cdn/components/choghadiya-grid.js.map +7 -0
  10. package/dist/cdn/components/compatibility-card.js +36 -34
  11. package/dist/cdn/components/compatibility-card.js.map +4 -4
  12. package/dist/cdn/components/dasha-timeline.js +35 -39
  13. package/dist/cdn/components/dasha-timeline.js.map +4 -4
  14. package/dist/cdn/components/data.js +9 -9
  15. package/dist/cdn/components/data.js.map +4 -4
  16. package/dist/cdn/components/divisional-chart.js +279 -0
  17. package/dist/cdn/components/divisional-chart.js.map +7 -0
  18. package/dist/cdn/components/dosha-card.js +49 -49
  19. package/dist/cdn/components/dosha-card.js.map +3 -3
  20. package/dist/cdn/components/endpoint-form.js +47 -28
  21. package/dist/cdn/components/endpoint-form.js.map +4 -4
  22. package/dist/cdn/components/guna-milan.js +66 -24
  23. package/dist/cdn/components/guna-milan.js.map +4 -4
  24. package/dist/cdn/components/hexagram.js +26 -26
  25. package/dist/cdn/components/hexagram.js.map +3 -3
  26. package/dist/cdn/components/horoscope-card.js +47 -40
  27. package/dist/cdn/components/horoscope-card.js.map +4 -4
  28. package/dist/cdn/components/kp-planets-table.js +10 -10
  29. package/dist/cdn/components/kp-planets-table.js.map +4 -4
  30. package/dist/cdn/components/location-search.js +6 -6
  31. package/dist/cdn/components/location-search.js.map +3 -3
  32. package/dist/cdn/components/moon-phase.js +18 -18
  33. package/dist/cdn/components/moon-phase.js.map +4 -4
  34. package/dist/cdn/components/natal-chart.js +240 -24
  35. package/dist/cdn/components/natal-chart.js.map +4 -4
  36. package/dist/cdn/components/numerology-card.js +40 -31
  37. package/dist/cdn/components/numerology-card.js.map +4 -4
  38. package/dist/cdn/components/panchang-table.js +30 -30
  39. package/dist/cdn/components/panchang-table.js.map +4 -4
  40. package/dist/cdn/components/shadbala-table.js +312 -0
  41. package/dist/cdn/components/shadbala-table.js.map +7 -0
  42. package/dist/cdn/components/synastry-chart.js +129 -39
  43. package/dist/cdn/components/synastry-chart.js.map +4 -4
  44. package/dist/cdn/components/tarot-card.js +49 -20
  45. package/dist/cdn/components/tarot-card.js.map +3 -3
  46. package/dist/cdn/components/tarot-spread.js +43 -27
  47. package/dist/cdn/components/tarot-spread.js.map +3 -3
  48. package/dist/cdn/components/transits-table.js +391 -0
  49. package/dist/cdn/components/transits-table.js.map +7 -0
  50. package/dist/cdn/components/vedic-kundli.js +63 -27
  51. package/dist/cdn/components/vedic-kundli.js.map +4 -4
  52. package/dist/cdn/components/yoga-list.js +334 -0
  53. package/dist/cdn/components/yoga-list.js.map +7 -0
  54. package/dist/cdn/roxy-ui.js +2104 -544
  55. package/dist/cdn/roxy-ui.js.map +4 -4
  56. package/dist/components/ashtakavarga-grid.d.ts +26 -0
  57. package/dist/components/ashtakavarga-grid.d.ts.map +1 -0
  58. package/dist/components/ashtakavarga-grid.js +457 -0
  59. package/dist/components/ashtakavarga-grid.js.map +7 -0
  60. package/dist/components/biorhythm-chart.d.ts +2 -46
  61. package/dist/components/biorhythm-chart.d.ts.map +1 -1
  62. package/dist/components/biorhythm-chart.js +24 -23
  63. package/dist/components/biorhythm-chart.js.map +2 -2
  64. package/dist/components/choghadiya-grid.d.ts +19 -0
  65. package/dist/components/choghadiya-grid.d.ts.map +1 -0
  66. package/dist/components/choghadiya-grid.js +304 -0
  67. package/dist/components/choghadiya-grid.js.map +7 -0
  68. package/dist/components/compatibility-card.d.ts +2 -27
  69. package/dist/components/compatibility-card.d.ts.map +1 -1
  70. package/dist/components/compatibility-card.js +50 -29
  71. package/dist/components/compatibility-card.js.map +3 -3
  72. package/dist/components/dasha-timeline.d.ts +2 -31
  73. package/dist/components/dasha-timeline.d.ts.map +1 -1
  74. package/dist/components/dasha-timeline.js +32 -30
  75. package/dist/components/dasha-timeline.js.map +3 -3
  76. package/dist/components/data.d.ts +11 -7
  77. package/dist/components/data.d.ts.map +1 -1
  78. package/dist/components/data.js +16 -6
  79. package/dist/components/data.js.map +3 -3
  80. package/dist/components/divisional-chart.d.ts +20 -0
  81. package/dist/components/divisional-chart.d.ts.map +1 -0
  82. package/dist/components/divisional-chart.js +471 -0
  83. package/dist/components/divisional-chart.js.map +7 -0
  84. package/dist/components/dosha-card.d.ts +2 -16
  85. package/dist/components/dosha-card.d.ts.map +1 -1
  86. package/dist/components/dosha-card.js +45 -43
  87. package/dist/components/dosha-card.js.map +2 -2
  88. package/dist/components/endpoint-form.d.ts +2 -0
  89. package/dist/components/endpoint-form.d.ts.map +1 -1
  90. package/dist/components/endpoint-form.js +71 -11
  91. package/dist/components/endpoint-form.js.map +3 -3
  92. package/dist/components/guna-milan.d.ts +2 -20
  93. package/dist/components/guna-milan.d.ts.map +1 -1
  94. package/dist/components/guna-milan.js +79 -20
  95. package/dist/components/guna-milan.js.map +4 -4
  96. package/dist/components/hexagram.d.ts +3 -27
  97. package/dist/components/hexagram.d.ts.map +1 -1
  98. package/dist/components/hexagram.js +48 -15
  99. package/dist/components/hexagram.js.map +2 -2
  100. package/dist/components/horoscope-card.d.ts +2 -20
  101. package/dist/components/horoscope-card.d.ts.map +1 -1
  102. package/dist/components/horoscope-card.js +54 -18
  103. package/dist/components/horoscope-card.js.map +3 -3
  104. package/dist/components/kp-planets-table.d.ts +2 -21
  105. package/dist/components/kp-planets-table.d.ts.map +1 -1
  106. package/dist/components/kp-planets-table.js +10 -4
  107. package/dist/components/kp-planets-table.js.map +3 -3
  108. package/dist/components/location-search.d.ts +5 -14
  109. package/dist/components/location-search.d.ts.map +1 -1
  110. package/dist/components/location-search.js +45 -5
  111. package/dist/components/location-search.js.map +2 -2
  112. package/dist/components/moon-phase.d.ts +4 -21
  113. package/dist/components/moon-phase.d.ts.map +1 -1
  114. package/dist/components/moon-phase.js +34 -4
  115. package/dist/components/moon-phase.js.map +3 -3
  116. package/dist/components/natal-chart.d.ts +9 -43
  117. package/dist/components/natal-chart.d.ts.map +1 -1
  118. package/dist/components/natal-chart.js +346 -79
  119. package/dist/components/natal-chart.js.map +3 -3
  120. package/dist/components/numerology-card.d.ts +5 -37
  121. package/dist/components/numerology-card.d.ts.map +1 -1
  122. package/dist/components/numerology-card.js +58 -30
  123. package/dist/components/numerology-card.js.map +3 -3
  124. package/dist/components/panchang-table.d.ts +3 -62
  125. package/dist/components/panchang-table.d.ts.map +1 -1
  126. package/dist/components/panchang-table.js +62 -32
  127. package/dist/components/panchang-table.js.map +3 -3
  128. package/dist/components/shadbala-table.d.ts +18 -0
  129. package/dist/components/shadbala-table.d.ts.map +1 -0
  130. package/dist/components/shadbala-table.js +400 -0
  131. package/dist/components/shadbala-table.js.map +7 -0
  132. package/dist/components/synastry-chart.d.ts +9 -28
  133. package/dist/components/synastry-chart.d.ts.map +1 -1
  134. package/dist/components/synastry-chart.js +201 -56
  135. package/dist/components/synastry-chart.js.map +3 -3
  136. package/dist/components/tarot-card.d.ts +5 -29
  137. package/dist/components/tarot-card.d.ts.map +1 -1
  138. package/dist/components/tarot-card.js +59 -20
  139. package/dist/components/tarot-card.js.map +2 -2
  140. package/dist/components/tarot-spread.d.ts +2 -24
  141. package/dist/components/tarot-spread.d.ts.map +1 -1
  142. package/dist/components/tarot-spread.js +39 -13
  143. package/dist/components/tarot-spread.js.map +2 -2
  144. package/dist/components/transits-table.d.ts +21 -0
  145. package/dist/components/transits-table.d.ts.map +1 -0
  146. package/dist/components/transits-table.js +515 -0
  147. package/dist/components/transits-table.js.map +7 -0
  148. package/dist/components/vedic-kundli.d.ts +5 -28
  149. package/dist/components/vedic-kundli.d.ts.map +1 -1
  150. package/dist/components/vedic-kundli.js +147 -83
  151. package/dist/components/vedic-kundli.js.map +3 -3
  152. package/dist/components/yoga-list.d.ts +29 -0
  153. package/dist/components/yoga-list.d.ts.map +1 -0
  154. package/dist/components/yoga-list.js +389 -0
  155. package/dist/components/yoga-list.js.map +7 -0
  156. package/dist/index.cjs +3693 -1180
  157. package/dist/index.cjs.map +4 -4
  158. package/dist/index.d.ts +11 -4
  159. package/dist/index.d.ts.map +1 -1
  160. package/dist/index.js +3709 -1196
  161. package/dist/index.js.map +4 -4
  162. package/dist/manifest.d.ts +43 -0
  163. package/dist/manifest.d.ts.map +1 -0
  164. package/dist/manifest.json +7 -2
  165. package/dist/styles/tokens.css +73 -1
  166. package/dist/tokens/index.d.ts +6 -0
  167. package/dist/tokens/index.d.ts.map +1 -1
  168. package/dist/types/index.d.ts +2 -0
  169. package/dist/types/index.d.ts.map +1 -0
  170. package/dist/types/types.gen.d.ts +27811 -0
  171. package/dist/types/types.gen.d.ts.map +1 -0
  172. package/dist/utils/debounce.d.ts +9 -1
  173. package/dist/utils/debounce.d.ts.map +1 -1
  174. package/dist/utils/format.d.ts +29 -0
  175. package/dist/utils/format.d.ts.map +1 -0
  176. package/dist/utils/kundli-render.d.ts +63 -0
  177. package/dist/utils/kundli-render.d.ts.map +1 -0
  178. package/dist/utils/string.d.ts +14 -0
  179. package/dist/utils/string.d.ts.map +1 -0
  180. package/dist/version.d.ts +2 -0
  181. package/dist/version.d.ts.map +1 -0
  182. package/package.json +7 -1
  183. package/src/components/ashtakavarga-grid.ts +354 -0
  184. package/src/components/biorhythm-chart.ts +39 -84
  185. package/src/components/choghadiya-grid.ts +185 -0
  186. package/src/components/compatibility-card.ts +85 -52
  187. package/src/components/dasha-timeline.ts +55 -73
  188. package/src/components/data.ts +28 -16
  189. package/src/components/divisional-chart.ts +214 -0
  190. package/src/components/dosha-card.ts +72 -68
  191. package/src/components/endpoint-form.ts +80 -18
  192. package/src/components/guna-milan.ts +87 -47
  193. package/src/components/hexagram.ts +53 -43
  194. package/src/components/horoscope-card.ts +59 -43
  195. package/src/components/kp-planets-table.ts +8 -27
  196. package/src/components/location-search.ts +47 -23
  197. package/src/components/moon-phase.ts +28 -25
  198. package/src/components/natal-chart.ts +364 -110
  199. package/src/components/numerology-card.ts +86 -84
  200. package/src/components/panchang-table.ts +40 -78
  201. package/src/components/shadbala-table.ts +286 -0
  202. package/src/components/synastry-chart.ts +213 -97
  203. package/src/components/tarot-card.ts +76 -62
  204. package/src/components/tarot-spread.ts +72 -45
  205. package/src/components/transits-table.ts +350 -0
  206. package/src/components/vedic-kundli.ts +59 -173
  207. package/src/components/yoga-list.ts +328 -0
  208. package/src/index.ts +18 -26
  209. package/src/manifest.ts +340 -0
  210. package/src/styles/tokens.css +73 -1
  211. package/src/tokens/index.ts +14 -0
  212. package/src/types/types.gen.ts +3 -3
  213. package/src/utils/debounce.ts +23 -4
  214. package/src/utils/format.ts +75 -0
  215. package/src/utils/kundli-render.ts +197 -0
  216. package/src/utils/string.ts +23 -0
  217. package/src/version.ts +2 -0
  218. package/dist/utils/motion.d.ts +0 -13
  219. package/dist/utils/motion.d.ts.map +0 -1
  220. package/src/utils/motion.ts +0 -18
@@ -1,28 +1,18 @@
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';
10
+ import { capitalize } from '../utils/string.js';
5
11
 
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
- }
12
+ type HoroscopeData =
13
+ | GetDailyHoroscopeResponse
14
+ | GetWeeklyHoroscopeResponse
15
+ | GetMonthlyHoroscopeResponse;
26
16
 
27
17
  /**
28
18
  * Daily, weekly, or monthly horoscope card. Pass `data` from
@@ -134,6 +124,13 @@ export class RoxyHoroscopeCard extends LitElement {
134
124
  font-weight: var(--roxy-weight-bold, 600);
135
125
  }
136
126
 
127
+ .compat-wrap {
128
+ width: 100%;
129
+ display: flex;
130
+ align-items: center;
131
+ flex-wrap: wrap;
132
+ gap: var(--roxy-space-xs, 0.25rem);
133
+ }
137
134
  .compat {
138
135
  display: flex;
139
136
  flex-wrap: wrap;
@@ -163,8 +160,15 @@ export class RoxyHoroscopeCard extends LitElement {
163
160
 
164
161
  const sign = d.sign ?? '';
165
162
  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 ?? '';
163
+ const energy =
164
+ 'energyRating' in d && typeof d.energyRating === 'number'
165
+ ? d.energyRating
166
+ : null;
167
+ const dateLabel =
168
+ ('date' in d && d.date) ||
169
+ ('week' in d && d.week) ||
170
+ ('month' in d && d.month) ||
171
+ '';
168
172
 
169
173
  return html`<article
170
174
  class="card"
@@ -224,7 +228,7 @@ export class RoxyHoroscopeCard extends LitElement {
224
228
  : nothing
225
229
  }
226
230
  ${
227
- d.advice
231
+ 'advice' in d && d.advice
228
232
  ? html`<div class="section">
229
233
  <h3>Advice</h3>
230
234
  <p>${d.advice}</p>
@@ -233,57 +237,69 @@ export class RoxyHoroscopeCard extends LitElement {
233
237
  }
234
238
  </div>
235
239
 
236
- ${
237
- d.luckyNumber || d.luckyColor || (d.compatibleSigns?.length ?? 0) > 0
238
- ? html`<div class="lucky">
240
+ ${(() => {
241
+ const luckyNumber =
242
+ 'luckyNumber' in d && d.luckyNumber !== undefined
243
+ ? d.luckyNumber
244
+ : undefined;
245
+ const luckyColor =
246
+ 'luckyColor' in d && d.luckyColor ? d.luckyColor : '';
247
+ const luckyNumbers =
248
+ 'luckyNumbers' in d && d.luckyNumbers ? d.luckyNumbers : [];
249
+ const luckyDays = 'luckyDays' in d && d.luckyDays ? d.luckyDays : [];
250
+ const compatibleSigns = d.compatibleSigns ?? [];
251
+ if (
252
+ luckyNumber === undefined &&
253
+ !luckyColor &&
254
+ luckyNumbers.length === 0 &&
255
+ luckyDays.length === 0 &&
256
+ compatibleSigns.length === 0
257
+ )
258
+ return nothing;
259
+ return html`<div class="lucky">
239
260
  ${
240
- d.luckyNumber !== undefined
241
- ? html`<span>Lucky number <strong>${d.luckyNumber}</strong></span>`
261
+ luckyNumber !== undefined
262
+ ? html`<span>Lucky number <strong>${luckyNumber}</strong></span>`
242
263
  : nothing
243
264
  }
244
265
  ${
245
- d.luckyColor
246
- ? html`<span>Lucky color <strong>${d.luckyColor}</strong></span>`
266
+ luckyColor
267
+ ? html`<span>Lucky color <strong>${luckyColor}</strong></span>`
247
268
  : nothing
248
269
  }
249
270
  ${
250
- d.luckyNumbers?.length
271
+ luckyNumbers.length
251
272
  ? html`<span
252
273
  >Lucky numbers
253
- <strong>${d.luckyNumbers.join(', ')}</strong></span
274
+ <strong>${luckyNumbers.join(', ')}</strong></span
254
275
  >`
255
276
  : nothing
256
277
  }
257
278
  ${
258
- d.luckyDays?.length
279
+ luckyDays.length
259
280
  ? html`<span
260
- >Lucky days <strong>${d.luckyDays.join(', ')}</strong></span
281
+ >Lucky days <strong>${luckyDays.join(', ')}</strong></span
261
282
  >`
262
283
  : nothing
263
284
  }
264
285
  ${
265
- d.compatibleSigns?.length
286
+ compatibleSigns.length
266
287
  ? html`<span class="compat-wrap">
267
288
  Best with
268
289
  <span class="compat"
269
- >${d.compatibleSigns.map(
290
+ >${compatibleSigns.map(
270
291
  (s) => html`<span>${s}</span>`,
271
292
  )}</span
272
293
  >
273
294
  </span>`
274
295
  : nothing
275
296
  }
276
- </div>`
277
- : nothing
278
- }
297
+ </div>`;
298
+ })()}
279
299
  </article>`;
280
300
  }
281
301
  }
282
302
 
283
- function capitalize(s: string): string {
284
- return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
285
- }
286
-
287
303
  declare global {
288
304
  interface HTMLElementTagNameMap {
289
305
  'roxy-horoscope-card': RoxyHoroscopeCard;
@@ -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,33 +1,18 @@
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
25
11
  * `roxy-location-select` CustomEvent with the chosen city. Required for any
26
12
  * chart endpoint.
27
13
  *
28
- * Lifted from jyotish-vedic-astrology-app/src/components/city-search.tsx,
29
- * keeping the 300ms debounce and click-outside behavior, replacing React
30
- * state with Lit reactive properties and using direct fetch to RoxyAPI.
14
+ * Behavior: 300ms input debounce, click-outside dismiss, keyboard navigation
15
+ * with arrow keys / Enter / Escape, AbortController on stale requests.
31
16
  *
32
17
  * Attributes:
33
18
  * api-key optional. Direct call to roxyapi.com when set.
@@ -175,6 +160,8 @@ export class RoxyLocationSearch extends LitElement {
175
160
  private highlight = -1;
176
161
 
177
162
  private clickOutsideHandler?: (e: MouseEvent) => void;
163
+ private abortController?: AbortController;
164
+ private secretKeyWarned = false;
178
165
  private debouncedFetch = debounce((q: string) => {
179
166
  void this.fetchResults(q);
180
167
  }, 300);
@@ -194,9 +181,41 @@ export class RoxyLocationSearch extends LitElement {
194
181
  if (this.clickOutsideHandler) {
195
182
  document.removeEventListener('mousedown', this.clickOutsideHandler);
196
183
  }
184
+ this.debouncedFetch.cancel();
185
+ if (this.abortController) {
186
+ this.abortController.abort();
187
+ this.abortController = undefined;
188
+ }
189
+ }
190
+
191
+ private warnIfSecretKey() {
192
+ if (this.secretKeyWarned) return;
193
+ if (!this.apiKey) return;
194
+ // Browser-safe publishable keys carry the `pk_` prefix and a server-side
195
+ // origin allowlist. Anything else (a raw secret key, UUID-style token)
196
+ // must not ship to the browser.
197
+ if (this.apiKey.startsWith('pk_')) return;
198
+ this.secretKeyWarned = true;
199
+ const message =
200
+ 'Possible secret key in client-side <roxy-location-search>; use a `pk_` publishable key with origin allowlist instead.';
201
+ // eslint-disable-next-line no-console
202
+ console.warn(message);
203
+ this.dispatchEvent(
204
+ new CustomEvent('roxy-validation-error', {
205
+ detail: { reason: 'possible-secret-key', message },
206
+ bubbles: true,
207
+ composed: true,
208
+ }),
209
+ );
197
210
  }
198
211
 
199
212
  private async fetchResults(q: string) {
213
+ this.warnIfSecretKey();
214
+ // Abort any in-flight request so a stale response cannot overwrite a
215
+ // fresher one (debounced typing) or land after disconnect.
216
+ if (this.abortController) this.abortController.abort();
217
+ const controller = new AbortController();
218
+ this.abortController = controller;
200
219
  this.isLoading = true;
201
220
  try {
202
221
  const url = new URL(this.endpoint);
@@ -207,17 +226,22 @@ export class RoxyLocationSearch extends LitElement {
207
226
  };
208
227
  if (this.apiKey) headers['X-API-Key'] = this.apiKey;
209
228
  if (this.publishableKey) headers['X-API-Key'] = this.publishableKey;
210
- const res = await fetch(url, { headers });
229
+ const res = await fetch(url, { headers, signal: controller.signal });
211
230
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
212
- const json = (await res.json()) as CitySearchResponse;
231
+ const json = (await res.json()) as SearchCitiesResponse;
232
+ if (controller.signal.aborted) return;
213
233
  this.results = json.cities ?? [];
214
234
  this.isOpen = this.results.length > 0;
215
235
  this.highlight = this.results.length > 0 ? 0 : -1;
216
- } catch (_err) {
236
+ } catch (err) {
237
+ if ((err as { name?: string })?.name === 'AbortError') return;
217
238
  this.results = [];
218
239
  this.isOpen = false;
219
240
  } finally {
220
- this.isLoading = false;
241
+ if (this.abortController === controller) {
242
+ this.abortController = undefined;
243
+ }
244
+ if (!controller.signal.aborted) this.isLoading = false;
221
245
  }
222
246
  }
223
247
 
@@ -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;