@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,6 +1,7 @@
1
1
  import { css, html, LitElement, nothing } from 'lit';
2
2
  import { customElement, property, state } from 'lit/decorators.js';
3
3
  import { baseStyles } from '../utils/base-styles.js';
4
+ import { humanize } from '../utils/string.js';
4
5
 
5
6
  interface OpenApiSchemaRef {
6
7
  $ref?: string;
@@ -31,6 +32,33 @@ interface FieldDef {
31
32
  default?: unknown;
32
33
  }
33
34
 
35
+ interface OpenApiDoc {
36
+ paths?: Record<string, Record<string, unknown>>;
37
+ components?: { schemas?: Record<string, OpenApiSchema> };
38
+ }
39
+
40
+ const specCache = new Map<string, Promise<OpenApiDoc>>();
41
+
42
+ async function loadSpec(url: string): Promise<OpenApiDoc> {
43
+ let pending = specCache.get(url);
44
+ if (!pending) {
45
+ pending = fetch(url)
46
+ .then(async (res) => {
47
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
48
+ return (await res.json()) as OpenApiDoc;
49
+ })
50
+ .catch((err) => {
51
+ // Evict the rejected promise BEFORE rethrowing so subsequent
52
+ // callers (the user clicking Retry, a remount) hit the network
53
+ // again instead of replaying the cached failure forever.
54
+ specCache.delete(url);
55
+ throw err;
56
+ });
57
+ specCache.set(url, pending);
58
+ }
59
+ return pending;
60
+ }
61
+
34
62
  /**
35
63
  * Schema-driven form. Pass `endpoint` (e.g. "vedic-astrology/birth-chart").
36
64
  * The form introspects the cached OpenAPI spec, slots a roxy-location-search
@@ -64,22 +92,27 @@ export class RoxyEndpointForm extends LitElement {
64
92
  .fields {
65
93
  display: grid;
66
94
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
95
+ align-items: start;
67
96
  gap: var(--roxy-space-md, 1rem);
68
97
  }
69
98
  .field {
70
- display: grid;
99
+ display: flex;
100
+ flex-direction: column;
71
101
  gap: var(--roxy-space-xs, 0.25rem);
102
+ min-width: 0;
72
103
  }
73
104
  label {
74
105
  font-size: var(--roxy-text-sm, 0.875rem);
75
106
  color: var(--roxy-secondary, #475569);
76
107
  }
77
108
  label .req {
78
- color: var(--roxy-danger, #dc2626);
109
+ color: var(--roxy-danger-fg, #991b1b);
79
110
  margin-left: 4px;
80
111
  }
81
112
  input,
82
113
  select {
114
+ width: 100%;
115
+ box-sizing: border-box;
83
116
  padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem);
84
117
  font-size: var(--roxy-text-base, 1rem);
85
118
  font-family: inherit;
@@ -114,7 +147,7 @@ export class RoxyEndpointForm extends LitElement {
114
147
  button.submit {
115
148
  justify-self: start;
116
149
  background: var(--roxy-accent-fg, #b45309);
117
- color: #fff;
150
+ color: var(--roxy-bg, #fff);
118
151
  border: 0;
119
152
  border-radius: var(--roxy-radius-md, 8px);
120
153
  padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-lg, 1.5rem);
@@ -132,6 +165,17 @@ export class RoxyEndpointForm extends LitElement {
132
165
  outline: 2px solid var(--roxy-ring, rgba(245, 158, 11, 0.4));
133
166
  outline-offset: 2px;
134
167
  }
168
+ .spec-error {
169
+ display: grid;
170
+ gap: var(--roxy-space-md, 1rem);
171
+ justify-items: start;
172
+ background: var(--roxy-bg, #fff);
173
+ border: 1px solid var(--roxy-danger, #dc2626);
174
+ border-radius: var(--roxy-radius-md, 8px);
175
+ padding: var(--roxy-space-lg, 1.5rem);
176
+ color: var(--roxy-danger-fg, #991b1b);
177
+ font-size: var(--roxy-text-sm, 0.875rem);
178
+ }
135
179
  `,
136
180
  ];
137
181
 
@@ -159,19 +203,18 @@ export class RoxyEndpointForm extends LitElement {
159
203
  @state()
160
204
  private loaded = false;
161
205
 
206
+ @state()
207
+ private specError: string | null = null;
208
+
162
209
  connectedCallback(): void {
163
210
  super.connectedCallback();
164
211
  void this.loadSchema();
165
212
  }
166
213
 
167
214
  private async loadSchema() {
215
+ this.specError = null;
168
216
  try {
169
- const res = await fetch(this.specUrl);
170
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
171
- const spec = (await res.json()) as {
172
- paths?: Record<string, Record<string, unknown>>;
173
- components?: { schemas?: Record<string, OpenApiSchema> };
174
- };
217
+ const spec = await loadSpec(this.specUrl);
175
218
  const path = `/${this.endpoint.replace(/^\//, '')}`;
176
219
  const op = spec.paths?.[path]?.[this.method.toLowerCase()] as
177
220
  | {
@@ -189,7 +232,11 @@ export class RoxyEndpointForm extends LitElement {
189
232
  }>;
190
233
  }
191
234
  | undefined;
192
- if (!op) return;
235
+ if (!op) {
236
+ throw new Error(
237
+ `Endpoint ${this.method} ${path} not found in OpenAPI spec`,
238
+ );
239
+ }
193
240
 
194
241
  const schemas = spec.components?.schemas ?? {};
195
242
  const fields: FieldDef[] = [];
@@ -244,11 +291,26 @@ export class RoxyEndpointForm extends LitElement {
244
291
  }
245
292
  this.values = init;
246
293
  this.loaded = true;
247
- } catch (_err) {
294
+ } catch (err) {
295
+ const message = err instanceof Error ? err.message : String(err);
296
+ this.specError = message;
248
297
  this.loaded = true;
298
+ this.dispatchEvent(
299
+ new CustomEvent('roxy-spec-error', {
300
+ detail: { url: this.specUrl, message },
301
+ bubbles: true,
302
+ composed: true,
303
+ }),
304
+ );
249
305
  }
250
306
  }
251
307
 
308
+ private retryLoadSchema = () => {
309
+ this.loaded = false;
310
+ this.specError = null;
311
+ void this.loadSchema();
312
+ };
313
+
252
314
  private resolve(
253
315
  schema: OpenApiSchema | OpenApiSchemaRef | undefined,
254
316
  all: Record<string, OpenApiSchema>,
@@ -322,6 +384,13 @@ export class RoxyEndpointForm extends LitElement {
322
384
  return html`<form><div class="roxy-skeleton" style="height: 8rem"></div></form>`;
323
385
  }
324
386
 
387
+ if (this.specError) {
388
+ return html`<div class="spec-error" role="alert">
389
+ Schema load failed: ${this.specError}
390
+ <button type="button" class="submit" @click=${this.retryLoadSchema}>Retry</button>
391
+ </div>`;
392
+ }
393
+
325
394
  const renderField = (f: FieldDef) => {
326
395
  if (
327
396
  this.hasLocation &&
@@ -419,13 +488,6 @@ export class RoxyEndpointForm extends LitElement {
419
488
  }
420
489
  }
421
490
 
422
- function humanize(s: string): string {
423
- return s
424
- .replace(/[_-]+/g, ' ')
425
- .replace(/([a-z])([A-Z])/g, '$1 $2')
426
- .replace(/^\w/, (c) => c.toUpperCase());
427
- }
428
-
429
491
  declare global {
430
492
  interface HTMLElementTagNameMap {
431
493
  'roxy-endpoint-form': RoxyEndpointForm;
@@ -1,26 +1,8 @@
1
1
  import { css, html, LitElement, nothing } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
+ import type { CompatibilityResponse } from '../types/index.js';
3
4
  import { baseStyles } from '../utils/base-styles.js';
4
-
5
- interface GunaCategory {
6
- name?: string;
7
- score?: number;
8
- max?: number;
9
- maxScore?: number;
10
- description?: string;
11
- }
12
-
13
- interface GunaData {
14
- total?: number;
15
- totalScore?: number;
16
- maxScore?: number;
17
- percentage?: number;
18
- isCompatible?: boolean;
19
- recommendation?: string;
20
- doshas?: string[];
21
- doshaCancellations?: string[];
22
- breakdown?: GunaCategory[];
23
- }
5
+ import { formatNumber, formatPercent } from '../utils/format.js';
24
6
 
25
7
  const STANDARD_CATEGORIES = [
26
8
  'Varna',
@@ -51,6 +33,14 @@ export class RoxyGunaMilan extends LitElement {
51
33
  gap: var(--roxy-space-md, 1rem);
52
34
  }
53
35
 
36
+ .score-header {
37
+ display: flex;
38
+ align-items: center;
39
+ gap: 1rem;
40
+ }
41
+ .score-info {
42
+ flex: 1;
43
+ }
54
44
  .score-bar {
55
45
  display: grid;
56
46
  grid-template-columns: 1fr auto;
@@ -72,6 +62,26 @@ export class RoxyGunaMilan extends LitElement {
72
62
  font-size: var(--roxy-text-sm, 0.875rem);
73
63
  color: var(--roxy-secondary, #475569);
74
64
  }
65
+ .score-ring {
66
+ width: 120px;
67
+ height: 120px;
68
+ flex-shrink: 0;
69
+ }
70
+ .score-ring svg {
71
+ width: 100%;
72
+ height: 100%;
73
+ }
74
+ .score-ring .ring-text {
75
+ font-size: 22px;
76
+ font-weight: 700;
77
+ fill: var(--roxy-fg, #0a0a0a);
78
+ font-family: var(--roxy-font-sans);
79
+ }
80
+ .score-ring .ring-max {
81
+ font-size: 10px;
82
+ fill: var(--roxy-muted, #71717a);
83
+ font-family: var(--roxy-font-sans);
84
+ }
75
85
 
76
86
  table {
77
87
  width: 100%;
@@ -127,47 +137,75 @@ export class RoxyGunaMilan extends LitElement {
127
137
  }
128
138
  .tags .dosha {
129
139
  background: color-mix(in srgb, var(--roxy-danger, #dc2626) 16%, transparent);
130
- color: var(--roxy-danger, #dc2626);
140
+ color: var(--roxy-danger-fg, #991b1b);
131
141
  }
132
142
  .tags .cancel {
133
143
  background: color-mix(in srgb, var(--roxy-success, #16a34a) 18%, transparent);
134
- color: var(--roxy-success, #16a34a);
144
+ color: var(--roxy-success-fg, #166534);
135
145
  }
136
146
  `,
137
147
  ];
138
148
 
139
149
  @property({ attribute: false })
140
- data: GunaData | null = null;
150
+ data: CompatibilityResponse | null = null;
141
151
 
142
152
  render() {
143
153
  const d = this.data;
144
154
  if (!d)
145
155
  return html`<div class="roxy-empty" role="status">No Guna Milan data</div>`;
146
156
 
147
- const total = d.total ?? d.totalScore ?? 0;
148
- const max = d.maxScore ?? 36;
149
157
  const breakdown = (d.breakdown ?? []).filter(
150
- (b) => b && (b.name || b.score !== undefined),
158
+ (b) => b?.category !== undefined,
151
159
  );
152
160
 
161
+ const score = d.total ?? 0;
162
+ const max = d.maxScore ?? 36;
163
+ const pct = (score / max) * 100;
164
+ const trackColor =
165
+ 'color-mix(in srgb, var(--roxy-border) 50%, transparent)';
166
+ const fillColor =
167
+ pct >= 70
168
+ ? 'var(--roxy-success)'
169
+ : pct >= 50
170
+ ? 'var(--roxy-warning)'
171
+ : 'var(--roxy-danger)';
172
+ // SVG circle with r=45: circumference = 2 * pi * 45 = 282.74
173
+ // dasharray segments = pct * 2.827, (100 - pct) * 2.827
174
+ const dashFill = pct * 2.827;
175
+ const dashGap = (100 - pct) * 2.827;
176
+
153
177
  return html`<article class="card" aria-label="Guna Milan score">
154
- <div class="score-bar">
155
- <div>
156
- <span class="total">${total}</span>
157
- <span class="over"> / ${max}</span>
158
- ${
159
- typeof d.percentage === 'number'
160
- ? html`<small style="margin-left: 0.5rem; color: var(--roxy-muted)">
161
- ${d.percentage}%
162
- </small>`
163
- : nothing
164
- }
178
+ <div class="score-header">
179
+ <div class="score-info">
180
+ <div class="score-bar">
181
+ <div>
182
+ <span class="total">${formatNumber(d.total, 1)}</span>
183
+ <span class="over"> / ${d.maxScore}</span>
184
+ ${
185
+ typeof d.percentage === 'number'
186
+ ? html`<small style="margin-left: 0.5rem; color: var(--roxy-muted)">
187
+ ${formatPercent(d.percentage, 1)}
188
+ </small>`
189
+ : nothing
190
+ }
191
+ </div>
192
+ ${
193
+ d.recommendation
194
+ ? html`<span class="recommendation">${d.recommendation}</span>`
195
+ : nothing
196
+ }
197
+ </div>
198
+ </div>
199
+ <div class="score-ring" role="meter" aria-label="Guna milan score" aria-valuemin="0" aria-valuemax="36" aria-valuenow="${score}">
200
+ <svg viewBox="0 0 100 100" aria-hidden="true">
201
+ <circle class="ring-track" cx="50" cy="50" r="45" fill="none" stroke="${trackColor}" stroke-width="8"/>
202
+ <circle class="ring-fill" cx="50" cy="50" r="45" fill="none" stroke="${fillColor}" stroke-width="8"
203
+ stroke-dasharray="${dashFill},${dashGap}" stroke-linecap="round"
204
+ transform="rotate(-90 50 50)"/>
205
+ <text x="50" y="50" text-anchor="middle" dominant-baseline="central" class="ring-text">${score}</text>
206
+ <text x="50" y="64" text-anchor="middle" dominant-baseline="central" class="ring-max">/${max}</text>
207
+ </svg>
165
208
  </div>
166
- ${
167
- d.recommendation
168
- ? html`<span class="recommendation">${d.recommendation}</span>`
169
- : nothing
170
- }
171
209
  </div>
172
210
 
173
211
  ${
@@ -183,16 +221,16 @@ export class RoxyGunaMilan extends LitElement {
183
221
  <tbody>
184
222
  ${breakdown.map((b) => {
185
223
  const score = b.score ?? 0;
186
- const maxScore = b.max ?? b.maxScore ?? defaultMax(b.name);
224
+ const maxScore = b.maxScore ?? defaultMax(b.category);
187
225
  const pct = maxScore ? (score / maxScore) * 100 : 0;
188
226
  return html`<tr>
189
- <td>${b.name ?? ''}</td>
227
+ <td>${b.category}</td>
190
228
  <td class="bar-cell">
191
229
  <div class="mini-bar">
192
230
  <span style="width: ${pct}%"></span>
193
231
  </div>
194
232
  </td>
195
- <td class="score">${score} / ${maxScore}</td>
233
+ <td class="score">${formatNumber(score, 1)} / ${maxScore}</td>
196
234
  </tr>`;
197
235
  })}
198
236
  </tbody>
@@ -203,7 +241,10 @@ export class RoxyGunaMilan extends LitElement {
203
241
  (d.doshas?.length ?? 0) > 0 || (d.doshaCancellations?.length ?? 0) > 0
204
242
  ? html`<div class="tags">
205
243
  ${d.doshas?.map((x) => html`<span class="dosha">${x}</span>`)}
206
- ${d.doshaCancellations?.map((x) => html`<span class="cancel">${x}</span>`)}
244
+ ${d.doshaCancellations?.map(
245
+ (x) =>
246
+ html`<span class="cancel" title=${x.reason}>${x.dosha} cancelled</span>`,
247
+ )}
207
248
  </div>`
208
249
  : nothing
209
250
  }
@@ -235,7 +276,6 @@ function defaultMax(name?: string): number {
235
276
  }
236
277
  }
237
278
 
238
- // Reference list (kept for documentation, used at codegen time)
239
279
  export const GUNA_CATEGORIES = STANDARD_CATEGORIES;
240
280
 
241
281
  declare global {
@@ -1,34 +1,22 @@
1
1
  import { css, html, LitElement, nothing, svg } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
3
  import { TRIGRAM_GLYPH } from '../tokens/index.js';
4
+ import type {
5
+ CastReadingResponse,
6
+ GetDailyHexagramResponse,
7
+ GetHexagramResponse,
8
+ GetRandomHexagramResponse,
9
+ Hexagram,
10
+ LookupHexagramResponse,
11
+ } from '../types/index.js';
4
12
  import { baseStyles } from '../utils/base-styles.js';
5
13
 
6
- interface HexagramData {
7
- number?: number;
8
- symbol?: string;
9
- chinese?: string;
10
- english?: string;
11
- pinyin?: string;
12
- upperTrigram?: string;
13
- lowerTrigram?: string;
14
- judgment?: string;
15
- image?: string;
16
- interpretation?: {
17
- general?: string;
18
- love?: string;
19
- career?: string;
20
- decision?: string;
21
- advice?: string;
22
- };
23
- changingLines?: number[];
24
- resultingHexagram?: HexagramData;
25
- dailyMessage?: string;
26
- hexagram?: HexagramData;
27
- lines?: number[]; // 6, 7, 8, 9 cast values
28
- changingLinePositions?: number[];
29
- seed?: string;
30
- date?: string;
31
- }
14
+ type HexagramData =
15
+ | GetHexagramResponse
16
+ | GetRandomHexagramResponse
17
+ | LookupHexagramResponse
18
+ | GetDailyHexagramResponse
19
+ | CastReadingResponse;
32
20
 
33
21
  /**
34
22
  * I Ching hexagram card. Renders /iching/hexagrams/{number}, /iching/cast,
@@ -154,25 +142,48 @@ export class RoxyHexagram extends LitElement {
154
142
  @property({ type: String, reflect: true })
155
143
  mode: 'lookup' | 'cast' | 'daily' = 'lookup';
156
144
 
157
- private getHexagram(): HexagramData | null {
158
- if (!this.data) return null;
159
- if ('hexagram' in this.data && this.data.hexagram) {
145
+ private resolveHexagram(): {
146
+ hex: Hexagram;
147
+ lines?: number[];
148
+ changingLinePositions?: number[];
149
+ dailyMessage?: string;
150
+ resultingHexagram?: Hexagram;
151
+ } | null {
152
+ const d = this.data;
153
+ if (!d) return null;
154
+ if ('hexagram' in d && d.hexagram) {
155
+ if ('lines' in d) {
156
+ const cast = d as CastReadingResponse;
157
+ return {
158
+ hex: cast.hexagram as Hexagram,
159
+ lines: cast.lines,
160
+ changingLinePositions: cast.changingLinePositions,
161
+ resultingHexagram: cast.resultingHexagram as Hexagram | undefined,
162
+ };
163
+ }
164
+ const daily = d as GetDailyHexagramResponse;
160
165
  return {
161
- ...this.data.hexagram,
162
- lines: this.data.lines,
163
- changingLinePositions: this.data.changingLinePositions,
166
+ hex: daily.hexagram as Hexagram,
167
+ dailyMessage: daily.dailyMessage,
164
168
  };
165
169
  }
166
- return this.data;
170
+ return { hex: d as Hexagram };
167
171
  }
168
172
 
169
173
  render() {
170
- const h = this.getHexagram();
171
- if (!h)
174
+ const resolved = this.resolveHexagram();
175
+ if (!resolved)
172
176
  return html`<div class="roxy-empty" role="status">No hexagram data</div>`;
173
177
 
174
- const lines = h.lines ?? this.derivedLines(h);
175
- const changing = new Set(h.changingLinePositions ?? []);
178
+ const {
179
+ hex: h,
180
+ lines: castLines,
181
+ changingLinePositions,
182
+ dailyMessage,
183
+ resultingHexagram,
184
+ } = resolved;
185
+ const lines = castLines ?? this.derivedLines(h);
186
+ const changing = new Set(changingLinePositions ?? []);
176
187
 
177
188
  return html`<article class="card" aria-label="I Ching hexagram">
178
189
  <div class="glyphs">
@@ -229,7 +240,7 @@ export class RoxyHexagram extends LitElement {
229
240
  </div>
230
241
  ${h.judgment ? html`<p class="judgment">${h.judgment}</p>` : nothing}
231
242
  ${h.image ? html`<p class="image">${h.image}</p>` : nothing}
232
- ${h.dailyMessage ? html`<p class="message">${h.dailyMessage}</p>` : nothing}
243
+ ${dailyMessage ? html`<p class="message">${dailyMessage}</p>` : nothing}
233
244
  ${
234
245
  h.interpretation?.general
235
246
  ? html`<p>${h.interpretation.general}</p>`
@@ -242,9 +253,9 @@ export class RoxyHexagram extends LitElement {
242
253
  .sort((a, b) => a - b)
243
254
  .join(', ')}.
244
255
  ${
245
- h.resultingHexagram?.english
246
- ? html` Becomes hexagram ${h.resultingHexagram.number}
247
- ${h.resultingHexagram.english}.`
256
+ resultingHexagram?.english
257
+ ? html` Becomes hexagram ${resultingHexagram.number}
258
+ ${resultingHexagram.english}.`
248
259
  : nothing
249
260
  }
250
261
  </div>`
@@ -255,8 +266,7 @@ export class RoxyHexagram extends LitElement {
255
266
  }
256
267
 
257
268
  /** When the API only ships symbol+number with no line array, render six solid yang. */
258
- private derivedLines(h: HexagramData): number[] {
259
- if (!h.symbol) return Array.from({ length: 6 }, () => 7);
269
+ private derivedLines(h: Hexagram): number[] {
260
270
  // Map each character of the unicode hexagram block (U+4DC0..) to broken/solid
261
271
  const cp = h.symbol.codePointAt(0) ?? 0;
262
272
  if (cp >= 0x4dc0 && cp <= 0x4dff) {