@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
@@ -25,6 +25,14 @@ const TITLE_KEYS = ['title', 'name', 'label', 'heading', 'overview', 'summary'];
25
25
  const IMAGE_KEYS = ['imageUrl', 'image', 'icon', 'symbol'];
26
26
  const SKIP_KEYS = ['imageUrl', 'image']; // rendered separately, not in body rows
27
27
 
28
+ // Hard cap on recursion. Real RoxyAPI responses nest at most 5-6 deep; anything
29
+ // deeper is either a circular reference (which would otherwise infinite-loop)
30
+ // or a payload too rich for the generic fallback to render usefully. The
31
+ // recursion is otherwise safe: <roxy-data> is registered globally by its
32
+ // `@customElement` decorator on import, so the nested template resolves to
33
+ // this same class without a separate import.
34
+ const MAX_DEPTH = 6;
35
+
28
36
  @customElement('roxy-data')
29
37
  export class RoxyData extends LitElement {
30
38
  static styles = [
@@ -129,10 +137,21 @@ export class RoxyData extends LitElement {
129
137
  @property({ attribute: false })
130
138
  data: Json = null;
131
139
 
140
+ /**
141
+ * Internal recursion depth. Nested <roxy-data> instances inherit this from
142
+ * the parent and increment to guard against circular references in the
143
+ * input. Not part of the public API; do not set from consumer code.
144
+ */
145
+ @property({ attribute: false })
146
+ depth = 0;
147
+
132
148
  render() {
133
149
  if (this.data == null) {
134
150
  return html`<div class="roxy-empty" role="status">No data</div>`;
135
151
  }
152
+ if (this.depth >= MAX_DEPTH) {
153
+ return html`<div class="roxy-empty" role="status">…</div>`;
154
+ }
136
155
  return html`<div
137
156
  class="roxy-card"
138
157
  aria-label="Generic data display"
@@ -252,7 +271,7 @@ export class RoxyData extends LitElement {
252
271
  </ul>`;
253
272
  }
254
273
  }
255
- return html`<roxy-data .data=${value}></roxy-data>`;
274
+ return html`<roxy-data .data=${value} .depth=${this.depth + 1}></roxy-data>`;
256
275
  }
257
276
 
258
277
  private formatPrimitive(value: Json | undefined): string {
@@ -1,25 +1,13 @@
1
1
  import { css, html, LitElement, nothing } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
+ import type {
4
+ KalsarpaResponse,
5
+ ManglikResponse,
6
+ SadhesatiResponse,
7
+ } from '../types/index.js';
3
8
  import { baseStyles } from '../utils/base-styles.js';
4
9
 
5
- interface DoshaData {
6
- present?: boolean;
7
- severity?: 'Mild' | 'Moderate' | 'Severe' | string;
8
- type?: string;
9
- description?: string;
10
- remedies?: string[];
11
- exceptions?: string[];
12
- effects?:
13
- | string
14
- | {
15
- marriage?: string;
16
- personality?: string;
17
- timing?: string;
18
- relationships?: string;
19
- general?: string;
20
- phases?: Record<string, string>;
21
- };
22
- }
10
+ type DoshaData = ManglikResponse | KalsarpaResponse | SadhesatiResponse;
23
11
 
24
12
  const DOSHA_LABELS: Record<string, string> = {
25
13
  manglik: 'Mangal Dosha',
@@ -71,11 +59,11 @@ export class RoxyDoshaCard extends LitElement {
71
59
  }
72
60
  .badge.absent {
73
61
  background: color-mix(in srgb, var(--roxy-success, #16a34a) 16%, transparent);
74
- color: var(--roxy-success, #16a34a);
62
+ color: var(--roxy-success-fg, #166534);
75
63
  }
76
64
  .badge.present {
77
65
  background: color-mix(in srgb, var(--roxy-danger, #dc2626) 16%, transparent);
78
- color: var(--roxy-danger, #dc2626);
66
+ color: var(--roxy-danger-fg, #991b1b);
79
67
  }
80
68
  .severity {
81
69
  display: flex;
@@ -166,7 +154,7 @@ export class RoxyDoshaCard extends LitElement {
166
154
  </div>
167
155
  </header>
168
156
  ${d.description ? html`<p class="description">${d.description}</p>` : nothing}
169
- ${this.renderEffects(d.effects)}
157
+ ${this.renderEffects(d)}
170
158
  ${
171
159
  d.remedies && d.remedies.length > 0
172
160
  ? html`<div>
@@ -178,22 +166,21 @@ export class RoxyDoshaCard extends LitElement {
178
166
  : nothing
179
167
  }
180
168
  ${
181
- d.exceptions && d.exceptions.length > 0
169
+ 'exceptions' in d && d.exceptions && d.exceptions.length > 0
182
170
  ? html`<div>
183
- <h3>Exceptions</h3>
184
- <ul>
185
- ${d.exceptions.map((r) => html`<li>${r}</li>`)}
186
- </ul>
187
- </div>`
171
+ <h3>Exceptions</h3>
172
+ <ul>
173
+ ${d.exceptions.map((r) => html`<li>${r}</li>`)}
174
+ </ul>
175
+ </div>`
188
176
  : nothing
189
177
  }
190
178
  </article>`;
191
179
  }
192
180
 
193
- private renderEffects(e: DoshaData['effects']) {
194
- if (!e) return nothing;
195
- if (typeof e === 'string') return html`<p>${e}</p>`;
196
- const entries = Object.entries(e).filter(
181
+ private renderEffects(d: DoshaData) {
182
+ if (!d.effects) return nothing;
183
+ const entries = Object.entries(d.effects).filter(
197
184
  ([, v]) => typeof v === 'string' && v.length > 0,
198
185
  );
199
186
  if (entries.length === 0) return nothing;
@@ -31,6 +31,33 @@ interface FieldDef {
31
31
  default?: unknown;
32
32
  }
33
33
 
34
+ interface OpenApiDoc {
35
+ paths?: Record<string, Record<string, unknown>>;
36
+ components?: { schemas?: Record<string, OpenApiSchema> };
37
+ }
38
+
39
+ const specCache = new Map<string, Promise<OpenApiDoc>>();
40
+
41
+ async function loadSpec(url: string): Promise<OpenApiDoc> {
42
+ let pending = specCache.get(url);
43
+ if (!pending) {
44
+ pending = fetch(url)
45
+ .then(async (res) => {
46
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
47
+ return (await res.json()) as OpenApiDoc;
48
+ })
49
+ .catch((err) => {
50
+ // Evict the rejected promise BEFORE rethrowing so subsequent
51
+ // callers (the user clicking Retry, a remount) hit the network
52
+ // again instead of replaying the cached failure forever.
53
+ specCache.delete(url);
54
+ throw err;
55
+ });
56
+ specCache.set(url, pending);
57
+ }
58
+ return pending;
59
+ }
60
+
34
61
  /**
35
62
  * Schema-driven form. Pass `endpoint` (e.g. "vedic-astrology/birth-chart").
36
63
  * The form introspects the cached OpenAPI spec, slots a roxy-location-search
@@ -64,22 +91,27 @@ export class RoxyEndpointForm extends LitElement {
64
91
  .fields {
65
92
  display: grid;
66
93
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
94
+ align-items: start;
67
95
  gap: var(--roxy-space-md, 1rem);
68
96
  }
69
97
  .field {
70
- display: grid;
98
+ display: flex;
99
+ flex-direction: column;
71
100
  gap: var(--roxy-space-xs, 0.25rem);
101
+ min-width: 0;
72
102
  }
73
103
  label {
74
104
  font-size: var(--roxy-text-sm, 0.875rem);
75
105
  color: var(--roxy-secondary, #475569);
76
106
  }
77
107
  label .req {
78
- color: var(--roxy-danger, #dc2626);
108
+ color: var(--roxy-danger-fg, #991b1b);
79
109
  margin-left: 4px;
80
110
  }
81
111
  input,
82
112
  select {
113
+ width: 100%;
114
+ box-sizing: border-box;
83
115
  padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem);
84
116
  font-size: var(--roxy-text-base, 1rem);
85
117
  font-family: inherit;
@@ -114,7 +146,7 @@ export class RoxyEndpointForm extends LitElement {
114
146
  button.submit {
115
147
  justify-self: start;
116
148
  background: var(--roxy-accent-fg, #b45309);
117
- color: #fff;
149
+ color: var(--roxy-bg, #fff);
118
150
  border: 0;
119
151
  border-radius: var(--roxy-radius-md, 8px);
120
152
  padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-lg, 1.5rem);
@@ -132,6 +164,17 @@ export class RoxyEndpointForm extends LitElement {
132
164
  outline: 2px solid var(--roxy-ring, rgba(245, 158, 11, 0.4));
133
165
  outline-offset: 2px;
134
166
  }
167
+ .spec-error {
168
+ display: grid;
169
+ gap: var(--roxy-space-md, 1rem);
170
+ justify-items: start;
171
+ background: var(--roxy-bg, #fff);
172
+ border: 1px solid var(--roxy-danger, #dc2626);
173
+ border-radius: var(--roxy-radius-md, 8px);
174
+ padding: var(--roxy-space-lg, 1.5rem);
175
+ color: var(--roxy-danger-fg, #991b1b);
176
+ font-size: var(--roxy-text-sm, 0.875rem);
177
+ }
135
178
  `,
136
179
  ];
137
180
 
@@ -159,19 +202,18 @@ export class RoxyEndpointForm extends LitElement {
159
202
  @state()
160
203
  private loaded = false;
161
204
 
205
+ @state()
206
+ private specError: string | null = null;
207
+
162
208
  connectedCallback(): void {
163
209
  super.connectedCallback();
164
210
  void this.loadSchema();
165
211
  }
166
212
 
167
213
  private async loadSchema() {
214
+ this.specError = null;
168
215
  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
- };
216
+ const spec = await loadSpec(this.specUrl);
175
217
  const path = `/${this.endpoint.replace(/^\//, '')}`;
176
218
  const op = spec.paths?.[path]?.[this.method.toLowerCase()] as
177
219
  | {
@@ -189,7 +231,11 @@ export class RoxyEndpointForm extends LitElement {
189
231
  }>;
190
232
  }
191
233
  | undefined;
192
- if (!op) return;
234
+ if (!op) {
235
+ throw new Error(
236
+ `Endpoint ${this.method} ${path} not found in OpenAPI spec`,
237
+ );
238
+ }
193
239
 
194
240
  const schemas = spec.components?.schemas ?? {};
195
241
  const fields: FieldDef[] = [];
@@ -244,11 +290,26 @@ export class RoxyEndpointForm extends LitElement {
244
290
  }
245
291
  this.values = init;
246
292
  this.loaded = true;
247
- } catch (_err) {
293
+ } catch (err) {
294
+ const message = err instanceof Error ? err.message : String(err);
295
+ this.specError = message;
248
296
  this.loaded = true;
297
+ this.dispatchEvent(
298
+ new CustomEvent('roxy-spec-error', {
299
+ detail: { url: this.specUrl, message },
300
+ bubbles: true,
301
+ composed: true,
302
+ }),
303
+ );
249
304
  }
250
305
  }
251
306
 
307
+ private retryLoadSchema = () => {
308
+ this.loaded = false;
309
+ this.specError = null;
310
+ void this.loadSchema();
311
+ };
312
+
252
313
  private resolve(
253
314
  schema: OpenApiSchema | OpenApiSchemaRef | undefined,
254
315
  all: Record<string, OpenApiSchema>,
@@ -322,6 +383,13 @@ export class RoxyEndpointForm extends LitElement {
322
383
  return html`<form><div class="roxy-skeleton" style="height: 8rem"></div></form>`;
323
384
  }
324
385
 
386
+ if (this.specError) {
387
+ return html`<div class="spec-error" role="alert">
388
+ Schema load failed: ${this.specError}
389
+ <button type="button" class="submit" @click=${this.retryLoadSchema}>Retry</button>
390
+ </div>`;
391
+ }
392
+
325
393
  const renderField = (f: FieldDef) => {
326
394
  if (
327
395
  this.hasLocation &&
@@ -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',
@@ -127,38 +109,36 @@ export class RoxyGunaMilan extends LitElement {
127
109
  }
128
110
  .tags .dosha {
129
111
  background: color-mix(in srgb, var(--roxy-danger, #dc2626) 16%, transparent);
130
- color: var(--roxy-danger, #dc2626);
112
+ color: var(--roxy-danger-fg, #991b1b);
131
113
  }
132
114
  .tags .cancel {
133
115
  background: color-mix(in srgb, var(--roxy-success, #16a34a) 18%, transparent);
134
- color: var(--roxy-success, #16a34a);
116
+ color: var(--roxy-success-fg, #166534);
135
117
  }
136
118
  `,
137
119
  ];
138
120
 
139
121
  @property({ attribute: false })
140
- data: GunaData | null = null;
122
+ data: CompatibilityResponse | null = null;
141
123
 
142
124
  render() {
143
125
  const d = this.data;
144
126
  if (!d)
145
127
  return html`<div class="roxy-empty" role="status">No Guna Milan data</div>`;
146
128
 
147
- const total = d.total ?? d.totalScore ?? 0;
148
- const max = d.maxScore ?? 36;
149
129
  const breakdown = (d.breakdown ?? []).filter(
150
- (b) => b && (b.name || b.score !== undefined),
130
+ (b) => b?.category !== undefined,
151
131
  );
152
132
 
153
133
  return html`<article class="card" aria-label="Guna Milan score">
154
134
  <div class="score-bar">
155
135
  <div>
156
- <span class="total">${total}</span>
157
- <span class="over"> / ${max}</span>
136
+ <span class="total">${formatNumber(d.total, 1)}</span>
137
+ <span class="over"> / ${d.maxScore}</span>
158
138
  ${
159
139
  typeof d.percentage === 'number'
160
140
  ? html`<small style="margin-left: 0.5rem; color: var(--roxy-muted)">
161
- ${d.percentage}%
141
+ ${formatPercent(d.percentage, 1)}
162
142
  </small>`
163
143
  : nothing
164
144
  }
@@ -183,16 +163,16 @@ export class RoxyGunaMilan extends LitElement {
183
163
  <tbody>
184
164
  ${breakdown.map((b) => {
185
165
  const score = b.score ?? 0;
186
- const maxScore = b.max ?? b.maxScore ?? defaultMax(b.name);
166
+ const maxScore = b.maxScore ?? defaultMax(b.category);
187
167
  const pct = maxScore ? (score / maxScore) * 100 : 0;
188
168
  return html`<tr>
189
- <td>${b.name ?? ''}</td>
169
+ <td>${b.category}</td>
190
170
  <td class="bar-cell">
191
171
  <div class="mini-bar">
192
172
  <span style="width: ${pct}%"></span>
193
173
  </div>
194
174
  </td>
195
- <td class="score">${score} / ${maxScore}</td>
175
+ <td class="score">${formatNumber(score, 1)} / ${maxScore}</td>
196
176
  </tr>`;
197
177
  })}
198
178
  </tbody>
@@ -203,7 +183,10 @@ export class RoxyGunaMilan extends LitElement {
203
183
  (d.doshas?.length ?? 0) > 0 || (d.doshaCancellations?.length ?? 0) > 0
204
184
  ? html`<div class="tags">
205
185
  ${d.doshas?.map((x) => html`<span class="dosha">${x}</span>`)}
206
- ${d.doshaCancellations?.map((x) => html`<span class="cancel">${x}</span>`)}
186
+ ${d.doshaCancellations?.map(
187
+ (x) =>
188
+ html`<span class="cancel" title=${x.reason}>${x.dosha} cancelled</span>`,
189
+ )}
207
190
  </div>`
208
191
  : nothing
209
192
  }
@@ -235,7 +218,6 @@ function defaultMax(name?: string): number {
235
218
  }
236
219
  }
237
220
 
238
- // Reference list (kept for documentation, used at codegen time)
239
221
  export const GUNA_CATEGORIES = STANDARD_CATEGORIES;
240
222
 
241
223
  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) {