@roxyapi/ui 0.0.1 → 0.1.2

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 (168) hide show
  1. package/AGENTS.md +169 -0
  2. package/LICENSE +21 -0
  3. package/README.md +198 -0
  4. package/THEMING.md +129 -0
  5. package/dist/cdn/components/biorhythm-chart.js +261 -0
  6. package/dist/cdn/components/biorhythm-chart.js.map +7 -0
  7. package/dist/cdn/components/compatibility-card.js +257 -0
  8. package/dist/cdn/components/compatibility-card.js.map +7 -0
  9. package/dist/cdn/components/dasha-timeline.js +244 -0
  10. package/dist/cdn/components/dasha-timeline.js.map +7 -0
  11. package/dist/cdn/components/data.js +258 -0
  12. package/dist/cdn/components/data.js.map +7 -0
  13. package/dist/cdn/components/dosha-card.js +254 -0
  14. package/dist/cdn/components/dosha-card.js.map +7 -0
  15. package/dist/cdn/components/endpoint-form.js +253 -0
  16. package/dist/cdn/components/endpoint-form.js.map +7 -0
  17. package/dist/cdn/components/guna-milan.js +256 -0
  18. package/dist/cdn/components/guna-milan.js.map +7 -0
  19. package/dist/cdn/components/hexagram.js +275 -0
  20. package/dist/cdn/components/hexagram.js.map +7 -0
  21. package/dist/cdn/components/horoscope-card.js +302 -0
  22. package/dist/cdn/components/horoscope-card.js.map +7 -0
  23. package/dist/cdn/components/kp-planets-table.js +224 -0
  24. package/dist/cdn/components/kp-planets-table.js.map +7 -0
  25. package/dist/cdn/components/location-search.js +267 -0
  26. package/dist/cdn/components/location-search.js.map +7 -0
  27. package/dist/cdn/components/moon-phase.js +251 -0
  28. package/dist/cdn/components/moon-phase.js.map +7 -0
  29. package/dist/cdn/components/natal-chart.js +237 -0
  30. package/dist/cdn/components/natal-chart.js.map +7 -0
  31. package/dist/cdn/components/numerology-card.js +252 -0
  32. package/dist/cdn/components/numerology-card.js.map +7 -0
  33. package/dist/cdn/components/panchang-table.js +234 -0
  34. package/dist/cdn/components/panchang-table.js.map +7 -0
  35. package/dist/cdn/components/synastry-chart.js +303 -0
  36. package/dist/cdn/components/synastry-chart.js.map +7 -0
  37. package/dist/cdn/components/tarot-card.js +260 -0
  38. package/dist/cdn/components/tarot-card.js.map +7 -0
  39. package/dist/cdn/components/tarot-spread.js +261 -0
  40. package/dist/cdn/components/tarot-spread.js.map +7 -0
  41. package/dist/cdn/components/vedic-kundli.js +189 -0
  42. package/dist/cdn/components/vedic-kundli.js.map +7 -0
  43. package/dist/cdn/roxy-ui.js +2552 -0
  44. package/dist/cdn/roxy-ui.js.map +7 -0
  45. package/dist/cdn/widgets.js +114 -0
  46. package/dist/components/biorhythm-chart.d.ts +66 -0
  47. package/dist/components/biorhythm-chart.d.ts.map +1 -0
  48. package/dist/components/biorhythm-chart.js +318 -0
  49. package/dist/components/biorhythm-chart.js.map +7 -0
  50. package/dist/components/compatibility-card.d.ts +46 -0
  51. package/dist/components/compatibility-card.d.ts.map +1 -0
  52. package/dist/components/compatibility-card.js +279 -0
  53. package/dist/components/compatibility-card.js.map +7 -0
  54. package/dist/components/dasha-timeline.d.ts +53 -0
  55. package/dist/components/dasha-timeline.d.ts.map +1 -0
  56. package/dist/components/dasha-timeline.js +269 -0
  57. package/dist/components/dasha-timeline.js.map +7 -0
  58. package/dist/components/data.d.ts +40 -0
  59. package/dist/components/data.d.ts.map +1 -0
  60. package/dist/components/data.js +339 -0
  61. package/dist/components/data.js.map +7 -0
  62. package/dist/components/dosha-card.d.ts +35 -0
  63. package/dist/components/dosha-card.d.ts.map +1 -0
  64. package/dist/components/dosha-card.js +278 -0
  65. package/dist/components/dosha-card.js.map +7 -0
  66. package/dist/components/endpoint-form.d.ts +39 -0
  67. package/dist/components/endpoint-form.d.ts.map +1 -0
  68. package/dist/components/endpoint-form.js +432 -0
  69. package/dist/components/endpoint-form.js.map +7 -0
  70. package/dist/components/guna-milan.d.ts +35 -0
  71. package/dist/components/guna-milan.d.ts.map +1 -0
  72. package/dist/components/guna-milan.js +302 -0
  73. package/dist/components/guna-milan.js.map +7 -0
  74. package/dist/components/hexagram.d.ts +47 -0
  75. package/dist/components/hexagram.d.ts.map +1 -0
  76. package/dist/components/hexagram.js +334 -0
  77. package/dist/components/hexagram.js.map +7 -0
  78. package/dist/components/horoscope-card.d.ts +38 -0
  79. package/dist/components/horoscope-card.d.ts.map +1 -0
  80. package/dist/components/horoscope-card.js +332 -0
  81. package/dist/components/horoscope-card.js.map +7 -0
  82. package/dist/components/kp-planets-table.d.ts +36 -0
  83. package/dist/components/kp-planets-table.d.ts.map +1 -0
  84. package/dist/components/kp-planets-table.js +227 -0
  85. package/dist/components/kp-planets-table.js.map +7 -0
  86. package/dist/components/location-search.d.ts +56 -0
  87. package/dist/components/location-search.d.ts.map +1 -0
  88. package/dist/components/location-search.js +401 -0
  89. package/dist/components/location-search.js.map +7 -0
  90. package/dist/components/moon-phase.d.ts +38 -0
  91. package/dist/components/moon-phase.d.ts.map +1 -0
  92. package/dist/components/moon-phase.js +284 -0
  93. package/dist/components/moon-phase.js.map +7 -0
  94. package/dist/components/natal-chart.d.ts +65 -0
  95. package/dist/components/natal-chart.d.ts.map +1 -0
  96. package/dist/components/natal-chart.js +407 -0
  97. package/dist/components/natal-chart.js.map +7 -0
  98. package/dist/components/numerology-card.d.ts +55 -0
  99. package/dist/components/numerology-card.d.ts.map +1 -0
  100. package/dist/components/numerology-card.js +274 -0
  101. package/dist/components/numerology-card.js.map +7 -0
  102. package/dist/components/panchang-table.d.ts +77 -0
  103. package/dist/components/panchang-table.d.ts.map +1 -0
  104. package/dist/components/panchang-table.js +285 -0
  105. package/dist/components/panchang-table.js.map +7 -0
  106. package/dist/components/synastry-chart.d.ts +52 -0
  107. package/dist/components/synastry-chart.d.ts.map +1 -0
  108. package/dist/components/synastry-chart.js +415 -0
  109. package/dist/components/synastry-chart.js.map +7 -0
  110. package/dist/components/tarot-card.d.ts +47 -0
  111. package/dist/components/tarot-card.d.ts.map +1 -0
  112. package/dist/components/tarot-card.js +281 -0
  113. package/dist/components/tarot-card.js.map +7 -0
  114. package/dist/components/tarot-spread.d.ts +42 -0
  115. package/dist/components/tarot-spread.d.ts.map +1 -0
  116. package/dist/components/tarot-spread.js +271 -0
  117. package/dist/components/tarot-spread.js.map +7 -0
  118. package/dist/components/vedic-kundli.d.ts +45 -0
  119. package/dist/components/vedic-kundli.d.ts.map +1 -0
  120. package/dist/components/vedic-kundli.js +325 -0
  121. package/dist/components/vedic-kundli.js.map +7 -0
  122. package/dist/index.cjs +4174 -0
  123. package/dist/index.cjs.map +7 -0
  124. package/dist/index.d.ts +30 -0
  125. package/dist/index.d.ts.map +1 -0
  126. package/dist/index.js +4154 -0
  127. package/dist/index.js.map +7 -0
  128. package/dist/manifest.json +24 -0
  129. package/dist/styles/tokens.css +147 -0
  130. package/dist/tokens/index.d.ts +17 -0
  131. package/dist/tokens/index.d.ts.map +1 -0
  132. package/dist/utils/base-styles.d.ts +6 -0
  133. package/dist/utils/base-styles.d.ts.map +1 -0
  134. package/dist/utils/debounce.d.ts +5 -0
  135. package/dist/utils/debounce.d.ts.map +1 -0
  136. package/dist/utils/degree.d.ts +29 -0
  137. package/dist/utils/degree.d.ts.map +1 -0
  138. package/dist/utils/motion.d.ts +13 -0
  139. package/dist/utils/motion.d.ts.map +1 -0
  140. package/package.json +71 -3
  141. package/src/components/biorhythm-chart.ts +290 -0
  142. package/src/components/compatibility-card.ts +231 -0
  143. package/src/components/dasha-timeline.ts +251 -0
  144. package/src/components/data.ts +287 -0
  145. package/src/components/dosha-card.ts +215 -0
  146. package/src/components/endpoint-form.ts +433 -0
  147. package/src/components/guna-milan.ts +245 -0
  148. package/src/components/hexagram.ts +279 -0
  149. package/src/components/horoscope-card.ts +291 -0
  150. package/src/components/kp-planets-table.ts +156 -0
  151. package/src/components/location-search.ts +335 -0
  152. package/src/components/moon-phase.ts +221 -0
  153. package/src/components/natal-chart.ts +298 -0
  154. package/src/components/numerology-card.ts +243 -0
  155. package/src/components/panchang-table.ts +265 -0
  156. package/src/components/synastry-chart.ts +341 -0
  157. package/src/components/tarot-card.ts +235 -0
  158. package/src/components/tarot-spread.ts +224 -0
  159. package/src/components/vedic-kundli.ts +257 -0
  160. package/src/index.ts +61 -0
  161. package/src/styles/tokens.css +147 -0
  162. package/src/tokens/index.ts +130 -0
  163. package/src/types/index.ts +3 -0
  164. package/src/types/types.gen.ts +28526 -0
  165. package/src/utils/base-styles.ts +89 -0
  166. package/src/utils/debounce.ts +13 -0
  167. package/src/utils/degree.ts +64 -0
  168. package/src/utils/motion.ts +18 -0
@@ -0,0 +1,287 @@
1
+ import { css, html, LitElement, nothing, type TemplateResult } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { baseStyles } from '../utils/base-styles.js';
4
+
5
+ /**
6
+ * Generic fallback renderer. Accepts ANY OpenAPI response shape and renders it
7
+ * via field-name heuristics so future spec additions render reasonably without
8
+ * hand-wired components. Mirrors the WordPress GenericRenderer pattern.
9
+ *
10
+ * Heuristic order:
11
+ * 1. Primitive (string, number, boolean) -> single line.
12
+ * 2. Array of primitives -> chip list.
13
+ * 3. Array of objects with shared keys -> table.
14
+ * 4. Object with title-like field -> card with key/value rows.
15
+ * 5. Otherwise -> definition list of all keys.
16
+ *
17
+ * Future spec hint: when the server emits `x-roxy-ui` on a schema, the
18
+ * dispatcher in tool-call (Phase 2.5) will select a hand-tuned component
19
+ * instead of this fallback.
20
+ */
21
+
22
+ type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
23
+
24
+ const TITLE_KEYS = ['title', 'name', 'label', 'heading', 'overview', 'summary'];
25
+ const IMAGE_KEYS = ['imageUrl', 'image', 'icon', 'symbol'];
26
+ const SKIP_KEYS = ['imageUrl', 'image']; // rendered separately, not in body rows
27
+
28
+ @customElement('roxy-data')
29
+ export class RoxyData extends LitElement {
30
+ static styles = [
31
+ baseStyles,
32
+ css`
33
+ .roxy-card {
34
+ background: var(--roxy-bg, #fff);
35
+ border: 1px solid var(--roxy-border, #e4e4e7);
36
+ border-radius: var(--roxy-radius-md, 8px);
37
+ padding: var(--roxy-space-md, 1rem);
38
+ box-shadow: var(--roxy-shadow-sm);
39
+ }
40
+
41
+ .roxy-title {
42
+ font-size: var(--roxy-text-lg, 1.125rem);
43
+ font-weight: var(--roxy-weight-bold, 600);
44
+ margin: 0 0 var(--roxy-space-sm, 0.5rem) 0;
45
+ color: var(--roxy-primary, #0f172a);
46
+ letter-spacing: var(--roxy-tracking-tight);
47
+ }
48
+
49
+ .roxy-summary {
50
+ color: var(--roxy-secondary, #475569);
51
+ margin: 0 0 var(--roxy-space-md, 1rem) 0;
52
+ font-size: var(--roxy-text-sm, 0.875rem);
53
+ }
54
+
55
+ dl.roxy-rows {
56
+ margin: 0;
57
+ display: grid;
58
+ grid-template-columns: minmax(8ch, max-content) 1fr;
59
+ gap: var(--roxy-space-xs, 0.25rem) var(--roxy-space-md, 1rem);
60
+ }
61
+ dl.roxy-rows dt {
62
+ color: var(--roxy-muted, #71717a);
63
+ font-size: var(--roxy-text-sm, 0.875rem);
64
+ text-transform: capitalize;
65
+ }
66
+ dl.roxy-rows dd {
67
+ margin: 0;
68
+ color: var(--roxy-fg, #0a0a0a);
69
+ font-size: var(--roxy-text-sm, 0.875rem);
70
+ word-break: break-word;
71
+ }
72
+
73
+ ul.roxy-chips {
74
+ display: flex;
75
+ flex-wrap: wrap;
76
+ gap: var(--roxy-space-xs, 0.25rem);
77
+ padding: 0;
78
+ margin: 0;
79
+ list-style: none;
80
+ }
81
+ ul.roxy-chips li {
82
+ background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 14%, transparent);
83
+ color: var(--roxy-fg, #0a0a0a);
84
+ padding: 2px 8px;
85
+ border-radius: var(--roxy-radius-full, 9999px);
86
+ font-size: var(--roxy-text-xs, 0.75rem);
87
+ }
88
+
89
+ table.roxy-table {
90
+ width: 100%;
91
+ border-collapse: collapse;
92
+ font-size: var(--roxy-text-sm, 0.875rem);
93
+ }
94
+ table.roxy-table th,
95
+ table.roxy-table td {
96
+ border-bottom: 1px solid var(--roxy-border, #e4e4e7);
97
+ padding: var(--roxy-space-sm, 0.5rem);
98
+ text-align: left;
99
+ text-transform: none;
100
+ }
101
+ table.roxy-table th {
102
+ color: var(--roxy-muted, #71717a);
103
+ font-weight: var(--roxy-weight-bold, 600);
104
+ text-transform: capitalize;
105
+ font-size: var(--roxy-text-xs, 0.75rem);
106
+ letter-spacing: 0.04em;
107
+ }
108
+
109
+ .roxy-image {
110
+ max-width: 100%;
111
+ height: auto;
112
+ border-radius: var(--roxy-radius-md, 8px);
113
+ margin-bottom: var(--roxy-space-md, 1rem);
114
+ }
115
+
116
+ .roxy-section {
117
+ margin-bottom: var(--roxy-space-md, 1rem);
118
+ }
119
+ .roxy-section h4 {
120
+ font-size: var(--roxy-text-sm, 0.875rem);
121
+ font-weight: var(--roxy-weight-bold, 600);
122
+ color: var(--roxy-secondary, #475569);
123
+ margin: 0 0 var(--roxy-space-xs, 0.25rem) 0;
124
+ text-transform: capitalize;
125
+ }
126
+ `,
127
+ ];
128
+
129
+ @property({ attribute: false })
130
+ data: Json = null;
131
+
132
+ render() {
133
+ if (this.data == null) {
134
+ return html`<div class="roxy-empty" role="status">No data</div>`;
135
+ }
136
+ return html`<div
137
+ class="roxy-card"
138
+ aria-label="Generic data display"
139
+ >
140
+ ${this.renderValue(this.data)}
141
+ </div>`;
142
+ }
143
+
144
+ private renderValue(value: Json): TemplateResult | typeof nothing {
145
+ if (value === null || value === undefined) return nothing;
146
+ if (typeof value === 'string') return html`<p>${value}</p>`;
147
+ if (typeof value === 'number' || typeof value === 'boolean') {
148
+ return html`<p>${String(value)}</p>`;
149
+ }
150
+ if (Array.isArray(value)) return this.renderArray(value);
151
+ return this.renderObject(value as Record<string, Json>);
152
+ }
153
+
154
+ private renderArray(arr: Json[]): TemplateResult {
155
+ if (arr.length === 0) {
156
+ return html`<div class="roxy-empty" role="status">Empty list</div>`;
157
+ }
158
+ const allPrimitive = arr.every(
159
+ (v) => v === null || ['string', 'number', 'boolean'].includes(typeof v),
160
+ );
161
+ if (allPrimitive) {
162
+ return html`<ul class="roxy-chips">
163
+ ${arr.map((v) => html`<li>${String(v)}</li>`)}
164
+ </ul>`;
165
+ }
166
+ const allObjects = arr.every(
167
+ (v) => v !== null && typeof v === 'object' && !Array.isArray(v),
168
+ );
169
+ if (allObjects) return this.renderTable(arr as Record<string, Json>[]);
170
+ return html`<ol>
171
+ ${arr.map((v) => html`<li>${this.renderValue(v)}</li>`)}
172
+ </ol>`;
173
+ }
174
+
175
+ private renderTable(rows: Record<string, Json>[]): TemplateResult {
176
+ const keys = this.collectKeys(rows);
177
+ return html`<table class="roxy-table" role="table">
178
+ <thead>
179
+ <tr>
180
+ ${keys.map((k) => html`<th>${this.humanize(k)}</th>`)}
181
+ </tr>
182
+ </thead>
183
+ <tbody>
184
+ ${rows.map(
185
+ (row) => html`<tr>
186
+ ${keys.map((k) => html`<td>${this.formatPrimitive(row[k])}</td>`)}
187
+ </tr>`,
188
+ )}
189
+ </tbody>
190
+ </table>`;
191
+ }
192
+
193
+ private renderObject(obj: Record<string, Json>): TemplateResult {
194
+ const titleKey = TITLE_KEYS.find((k) => typeof obj[k] === 'string');
195
+ const imageKey = IMAGE_KEYS.find(
196
+ (k) =>
197
+ typeof obj[k] === 'string' && (obj[k] as string).startsWith('http'),
198
+ );
199
+ const summaryKey =
200
+ titleKey !== 'summary' && typeof obj.summary === 'string'
201
+ ? 'summary'
202
+ : null;
203
+ const rows = Object.entries(obj).filter(
204
+ ([k, v]) =>
205
+ k !== titleKey &&
206
+ k !== summaryKey &&
207
+ !SKIP_KEYS.includes(k) &&
208
+ v !== null &&
209
+ v !== undefined,
210
+ );
211
+
212
+ return html`
213
+ ${
214
+ imageKey
215
+ ? html`<img
216
+ class="roxy-image"
217
+ src=${String(obj[imageKey])}
218
+ alt=${titleKey ? String(obj[titleKey]) : 'illustration'}
219
+ loading="lazy"
220
+ />`
221
+ : nothing
222
+ }
223
+ ${titleKey ? html`<h3 class="roxy-title">${obj[titleKey]}</h3>` : nothing}
224
+ ${summaryKey ? html`<p class="roxy-summary">${obj[summaryKey]}</p>` : nothing}
225
+ ${
226
+ rows.length > 0
227
+ ? html`<dl class="roxy-rows">
228
+ ${rows.map(
229
+ ([k, v]) => html`
230
+ <dt>${this.humanize(k)}</dt>
231
+ <dd>${this.renderField(v)}</dd>
232
+ `,
233
+ )}
234
+ </dl>`
235
+ : nothing
236
+ }
237
+ `;
238
+ }
239
+
240
+ private renderField(value: Json): TemplateResult | string {
241
+ if (value === null || value === undefined) return '';
242
+ if (typeof value === 'string') return value;
243
+ if (typeof value === 'number' || typeof value === 'boolean')
244
+ return String(value);
245
+ if (Array.isArray(value)) {
246
+ const allPrimitive = value.every((v) =>
247
+ ['string', 'number', 'boolean'].includes(typeof v),
248
+ );
249
+ if (allPrimitive) {
250
+ return html`<ul class="roxy-chips">
251
+ ${value.map((v) => html`<li>${String(v)}</li>`)}
252
+ </ul>`;
253
+ }
254
+ }
255
+ return html`<roxy-data .data=${value}></roxy-data>`;
256
+ }
257
+
258
+ private formatPrimitive(value: Json | undefined): string {
259
+ if (value === null || value === undefined) return '';
260
+ if (typeof value === 'string') return value;
261
+ if (typeof value === 'number' || typeof value === 'boolean')
262
+ return String(value);
263
+ if (Array.isArray(value)) return value.map(String).join(', ');
264
+ return JSON.stringify(value);
265
+ }
266
+
267
+ private collectKeys(rows: Record<string, Json>[]): string[] {
268
+ const seen = new Set<string>();
269
+ for (const row of rows) {
270
+ for (const k of Object.keys(row)) seen.add(k);
271
+ }
272
+ return Array.from(seen);
273
+ }
274
+
275
+ private humanize(key: string): string {
276
+ return key
277
+ .replace(/[_-]+/g, ' ')
278
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
279
+ .replace(/^\w/, (c) => c.toUpperCase());
280
+ }
281
+ }
282
+
283
+ declare global {
284
+ interface HTMLElementTagNameMap {
285
+ 'roxy-data': RoxyData;
286
+ }
287
+ }
@@ -0,0 +1,215 @@
1
+ import { css, html, LitElement, nothing } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { baseStyles } from '../utils/base-styles.js';
4
+
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
+ }
23
+
24
+ const DOSHA_LABELS: Record<string, string> = {
25
+ manglik: 'Mangal Dosha',
26
+ kalsarpa: 'Kaal Sarp Dosha',
27
+ sadhesati: 'Sade Sati',
28
+ };
29
+
30
+ /**
31
+ * Dosha presence card. Renders /vedic-astrology/dosha/{manglik,kalsarpa,sadhesati}.
32
+ * Visual severity indicator + remedies + scoped effects.
33
+ */
34
+ @customElement('roxy-dosha-card')
35
+ export class RoxyDoshaCard extends LitElement {
36
+ static styles = [
37
+ baseStyles,
38
+ css`
39
+ .card {
40
+ background: var(--roxy-bg, #fff);
41
+ border: 1px solid var(--roxy-border, #e4e4e7);
42
+ border-radius: var(--roxy-radius-md, 8px);
43
+ padding: var(--roxy-space-lg, 1.5rem);
44
+ box-shadow: var(--roxy-shadow-sm);
45
+ display: grid;
46
+ gap: var(--roxy-space-md, 1rem);
47
+ }
48
+ .head {
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: space-between;
52
+ gap: var(--roxy-space-md, 1rem);
53
+ flex-wrap: wrap;
54
+ }
55
+ .title {
56
+ margin: 0;
57
+ font-size: var(--roxy-text-lg, 1.125rem);
58
+ font-weight: var(--roxy-weight-bold, 600);
59
+ text-transform: capitalize;
60
+ }
61
+ .badge {
62
+ display: inline-flex;
63
+ align-items: center;
64
+ gap: var(--roxy-space-xs, 0.25rem);
65
+ padding: 4px 10px;
66
+ border-radius: var(--roxy-radius-full, 9999px);
67
+ font-size: var(--roxy-text-xs, 0.75rem);
68
+ font-weight: var(--roxy-weight-bold, 600);
69
+ text-transform: uppercase;
70
+ letter-spacing: 0.06em;
71
+ }
72
+ .badge.absent {
73
+ background: color-mix(in srgb, var(--roxy-success, #16a34a) 16%, transparent);
74
+ color: var(--roxy-success, #16a34a);
75
+ }
76
+ .badge.present {
77
+ background: color-mix(in srgb, var(--roxy-danger, #dc2626) 16%, transparent);
78
+ color: var(--roxy-danger, #dc2626);
79
+ }
80
+ .severity {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 4px;
84
+ }
85
+ .severity span {
86
+ width: 14px;
87
+ height: 4px;
88
+ border-radius: 2px;
89
+ background: var(--roxy-border, #e4e4e7);
90
+ }
91
+ .severity.mild span:nth-child(1) {
92
+ background: var(--roxy-warning, #ea580c);
93
+ }
94
+ .severity.moderate span:nth-child(-n + 2) {
95
+ background: var(--roxy-warning, #ea580c);
96
+ }
97
+ .severity.severe span {
98
+ background: var(--roxy-danger, #dc2626);
99
+ }
100
+
101
+ .description {
102
+ margin: 0;
103
+ color: var(--roxy-fg, #0a0a0a);
104
+ }
105
+
106
+ h3 {
107
+ margin: 0 0 var(--roxy-space-xs, 0.25rem);
108
+ font-size: var(--roxy-text-xs, 0.75rem);
109
+ color: var(--roxy-muted, #71717a);
110
+ text-transform: uppercase;
111
+ letter-spacing: 0.06em;
112
+ }
113
+ ul {
114
+ margin: 0;
115
+ padding-left: var(--roxy-space-md, 1rem);
116
+ font-size: var(--roxy-text-sm, 0.875rem);
117
+ }
118
+ .effects {
119
+ display: grid;
120
+ grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
121
+ gap: var(--roxy-space-md, 1rem);
122
+ }
123
+ .effects p {
124
+ margin: 0;
125
+ font-size: var(--roxy-text-sm, 0.875rem);
126
+ }
127
+ `,
128
+ ];
129
+
130
+ @property({ attribute: false })
131
+ data: DoshaData | null = null;
132
+
133
+ @property({ type: String, reflect: true })
134
+ type: 'manglik' | 'kalsarpa' | 'sadhesati' | string = 'manglik';
135
+
136
+ render() {
137
+ const d = this.data;
138
+ if (!d)
139
+ return html`<div class="roxy-empty" role="status">No dosha data</div>`;
140
+
141
+ const present = !!d.present;
142
+ const label = DOSHA_LABELS[this.type] ?? this.type;
143
+ const sevClass = (d.severity ?? '').toLowerCase();
144
+
145
+ return html`<article
146
+ class="card"
147
+ aria-label=${label}
148
+ >
149
+ <header class="head">
150
+ <h2 class="title">${label}</h2>
151
+ <div style="display:flex; gap:0.5rem; align-items:center;">
152
+ <span class=${`badge ${present ? 'present' : 'absent'}`}>
153
+ ${present ? 'Present' : 'Absent'}
154
+ </span>
155
+ ${
156
+ d.severity
157
+ ? html`<span
158
+ class=${`severity ${sevClass}`}
159
+ role="img"
160
+ aria-label=${`Severity ${d.severity}`}
161
+ >
162
+ <span></span><span></span><span></span>
163
+ </span>`
164
+ : nothing
165
+ }
166
+ </div>
167
+ </header>
168
+ ${d.description ? html`<p class="description">${d.description}</p>` : nothing}
169
+ ${this.renderEffects(d.effects)}
170
+ ${
171
+ d.remedies && d.remedies.length > 0
172
+ ? html`<div>
173
+ <h3>Remedies</h3>
174
+ <ul>
175
+ ${d.remedies.map((r) => html`<li>${r}</li>`)}
176
+ </ul>
177
+ </div>`
178
+ : nothing
179
+ }
180
+ ${
181
+ d.exceptions && d.exceptions.length > 0
182
+ ? html`<div>
183
+ <h3>Exceptions</h3>
184
+ <ul>
185
+ ${d.exceptions.map((r) => html`<li>${r}</li>`)}
186
+ </ul>
187
+ </div>`
188
+ : nothing
189
+ }
190
+ </article>`;
191
+ }
192
+
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(
197
+ ([, v]) => typeof v === 'string' && v.length > 0,
198
+ );
199
+ if (entries.length === 0) return nothing;
200
+ return html`<div class="effects">
201
+ ${entries.map(
202
+ ([k, v]) => html`<div>
203
+ <h3>${k}</h3>
204
+ <p>${v}</p>
205
+ </div>`,
206
+ )}
207
+ </div>`;
208
+ }
209
+ }
210
+
211
+ declare global {
212
+ interface HTMLElementTagNameMap {
213
+ 'roxy-dosha-card': RoxyDoshaCard;
214
+ }
215
+ }