@roxyapi/ui 0.0.1 → 0.1.1

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 (166) hide show
  1. package/AGENTS.md +169 -0
  2. package/THEMING.md +129 -0
  3. package/dist/cdn/components/biorhythm-chart.js +261 -0
  4. package/dist/cdn/components/biorhythm-chart.js.map +7 -0
  5. package/dist/cdn/components/compatibility-card.js +257 -0
  6. package/dist/cdn/components/compatibility-card.js.map +7 -0
  7. package/dist/cdn/components/dasha-timeline.js +244 -0
  8. package/dist/cdn/components/dasha-timeline.js.map +7 -0
  9. package/dist/cdn/components/data.js +258 -0
  10. package/dist/cdn/components/data.js.map +7 -0
  11. package/dist/cdn/components/dosha-card.js +254 -0
  12. package/dist/cdn/components/dosha-card.js.map +7 -0
  13. package/dist/cdn/components/endpoint-form.js +253 -0
  14. package/dist/cdn/components/endpoint-form.js.map +7 -0
  15. package/dist/cdn/components/guna-milan.js +256 -0
  16. package/dist/cdn/components/guna-milan.js.map +7 -0
  17. package/dist/cdn/components/hexagram.js +275 -0
  18. package/dist/cdn/components/hexagram.js.map +7 -0
  19. package/dist/cdn/components/horoscope-card.js +302 -0
  20. package/dist/cdn/components/horoscope-card.js.map +7 -0
  21. package/dist/cdn/components/kp-planets-table.js +224 -0
  22. package/dist/cdn/components/kp-planets-table.js.map +7 -0
  23. package/dist/cdn/components/location-search.js +267 -0
  24. package/dist/cdn/components/location-search.js.map +7 -0
  25. package/dist/cdn/components/moon-phase.js +251 -0
  26. package/dist/cdn/components/moon-phase.js.map +7 -0
  27. package/dist/cdn/components/natal-chart.js +237 -0
  28. package/dist/cdn/components/natal-chart.js.map +7 -0
  29. package/dist/cdn/components/numerology-card.js +252 -0
  30. package/dist/cdn/components/numerology-card.js.map +7 -0
  31. package/dist/cdn/components/panchang-table.js +234 -0
  32. package/dist/cdn/components/panchang-table.js.map +7 -0
  33. package/dist/cdn/components/synastry-chart.js +303 -0
  34. package/dist/cdn/components/synastry-chart.js.map +7 -0
  35. package/dist/cdn/components/tarot-card.js +260 -0
  36. package/dist/cdn/components/tarot-card.js.map +7 -0
  37. package/dist/cdn/components/tarot-spread.js +261 -0
  38. package/dist/cdn/components/tarot-spread.js.map +7 -0
  39. package/dist/cdn/components/vedic-kundli.js +189 -0
  40. package/dist/cdn/components/vedic-kundli.js.map +7 -0
  41. package/dist/cdn/roxy-ui.js +2552 -0
  42. package/dist/cdn/roxy-ui.js.map +7 -0
  43. package/dist/cdn/widgets.js +114 -0
  44. package/dist/components/biorhythm-chart.d.ts +66 -0
  45. package/dist/components/biorhythm-chart.d.ts.map +1 -0
  46. package/dist/components/biorhythm-chart.js +318 -0
  47. package/dist/components/biorhythm-chart.js.map +7 -0
  48. package/dist/components/compatibility-card.d.ts +46 -0
  49. package/dist/components/compatibility-card.d.ts.map +1 -0
  50. package/dist/components/compatibility-card.js +279 -0
  51. package/dist/components/compatibility-card.js.map +7 -0
  52. package/dist/components/dasha-timeline.d.ts +53 -0
  53. package/dist/components/dasha-timeline.d.ts.map +1 -0
  54. package/dist/components/dasha-timeline.js +269 -0
  55. package/dist/components/dasha-timeline.js.map +7 -0
  56. package/dist/components/data.d.ts +40 -0
  57. package/dist/components/data.d.ts.map +1 -0
  58. package/dist/components/data.js +339 -0
  59. package/dist/components/data.js.map +7 -0
  60. package/dist/components/dosha-card.d.ts +35 -0
  61. package/dist/components/dosha-card.d.ts.map +1 -0
  62. package/dist/components/dosha-card.js +278 -0
  63. package/dist/components/dosha-card.js.map +7 -0
  64. package/dist/components/endpoint-form.d.ts +39 -0
  65. package/dist/components/endpoint-form.d.ts.map +1 -0
  66. package/dist/components/endpoint-form.js +432 -0
  67. package/dist/components/endpoint-form.js.map +7 -0
  68. package/dist/components/guna-milan.d.ts +35 -0
  69. package/dist/components/guna-milan.d.ts.map +1 -0
  70. package/dist/components/guna-milan.js +302 -0
  71. package/dist/components/guna-milan.js.map +7 -0
  72. package/dist/components/hexagram.d.ts +47 -0
  73. package/dist/components/hexagram.d.ts.map +1 -0
  74. package/dist/components/hexagram.js +334 -0
  75. package/dist/components/hexagram.js.map +7 -0
  76. package/dist/components/horoscope-card.d.ts +38 -0
  77. package/dist/components/horoscope-card.d.ts.map +1 -0
  78. package/dist/components/horoscope-card.js +332 -0
  79. package/dist/components/horoscope-card.js.map +7 -0
  80. package/dist/components/kp-planets-table.d.ts +36 -0
  81. package/dist/components/kp-planets-table.d.ts.map +1 -0
  82. package/dist/components/kp-planets-table.js +227 -0
  83. package/dist/components/kp-planets-table.js.map +7 -0
  84. package/dist/components/location-search.d.ts +56 -0
  85. package/dist/components/location-search.d.ts.map +1 -0
  86. package/dist/components/location-search.js +401 -0
  87. package/dist/components/location-search.js.map +7 -0
  88. package/dist/components/moon-phase.d.ts +38 -0
  89. package/dist/components/moon-phase.d.ts.map +1 -0
  90. package/dist/components/moon-phase.js +284 -0
  91. package/dist/components/moon-phase.js.map +7 -0
  92. package/dist/components/natal-chart.d.ts +65 -0
  93. package/dist/components/natal-chart.d.ts.map +1 -0
  94. package/dist/components/natal-chart.js +407 -0
  95. package/dist/components/natal-chart.js.map +7 -0
  96. package/dist/components/numerology-card.d.ts +55 -0
  97. package/dist/components/numerology-card.d.ts.map +1 -0
  98. package/dist/components/numerology-card.js +274 -0
  99. package/dist/components/numerology-card.js.map +7 -0
  100. package/dist/components/panchang-table.d.ts +77 -0
  101. package/dist/components/panchang-table.d.ts.map +1 -0
  102. package/dist/components/panchang-table.js +285 -0
  103. package/dist/components/panchang-table.js.map +7 -0
  104. package/dist/components/synastry-chart.d.ts +52 -0
  105. package/dist/components/synastry-chart.d.ts.map +1 -0
  106. package/dist/components/synastry-chart.js +415 -0
  107. package/dist/components/synastry-chart.js.map +7 -0
  108. package/dist/components/tarot-card.d.ts +47 -0
  109. package/dist/components/tarot-card.d.ts.map +1 -0
  110. package/dist/components/tarot-card.js +281 -0
  111. package/dist/components/tarot-card.js.map +7 -0
  112. package/dist/components/tarot-spread.d.ts +42 -0
  113. package/dist/components/tarot-spread.d.ts.map +1 -0
  114. package/dist/components/tarot-spread.js +271 -0
  115. package/dist/components/tarot-spread.js.map +7 -0
  116. package/dist/components/vedic-kundli.d.ts +45 -0
  117. package/dist/components/vedic-kundli.d.ts.map +1 -0
  118. package/dist/components/vedic-kundli.js +325 -0
  119. package/dist/components/vedic-kundli.js.map +7 -0
  120. package/dist/index.cjs +4174 -0
  121. package/dist/index.cjs.map +7 -0
  122. package/dist/index.d.ts +30 -0
  123. package/dist/index.d.ts.map +1 -0
  124. package/dist/index.js +4154 -0
  125. package/dist/index.js.map +7 -0
  126. package/dist/manifest.json +24 -0
  127. package/dist/styles/tokens.css +147 -0
  128. package/dist/tokens/index.d.ts +17 -0
  129. package/dist/tokens/index.d.ts.map +1 -0
  130. package/dist/utils/base-styles.d.ts +6 -0
  131. package/dist/utils/base-styles.d.ts.map +1 -0
  132. package/dist/utils/debounce.d.ts +5 -0
  133. package/dist/utils/debounce.d.ts.map +1 -0
  134. package/dist/utils/degree.d.ts +29 -0
  135. package/dist/utils/degree.d.ts.map +1 -0
  136. package/dist/utils/motion.d.ts +13 -0
  137. package/dist/utils/motion.d.ts.map +1 -0
  138. package/package.json +69 -3
  139. package/src/components/biorhythm-chart.ts +290 -0
  140. package/src/components/compatibility-card.ts +231 -0
  141. package/src/components/dasha-timeline.ts +251 -0
  142. package/src/components/data.ts +287 -0
  143. package/src/components/dosha-card.ts +215 -0
  144. package/src/components/endpoint-form.ts +433 -0
  145. package/src/components/guna-milan.ts +245 -0
  146. package/src/components/hexagram.ts +279 -0
  147. package/src/components/horoscope-card.ts +291 -0
  148. package/src/components/kp-planets-table.ts +156 -0
  149. package/src/components/location-search.ts +335 -0
  150. package/src/components/moon-phase.ts +221 -0
  151. package/src/components/natal-chart.ts +298 -0
  152. package/src/components/numerology-card.ts +243 -0
  153. package/src/components/panchang-table.ts +265 -0
  154. package/src/components/synastry-chart.ts +341 -0
  155. package/src/components/tarot-card.ts +235 -0
  156. package/src/components/tarot-spread.ts +224 -0
  157. package/src/components/vedic-kundli.ts +257 -0
  158. package/src/index.ts +61 -0
  159. package/src/styles/tokens.css +147 -0
  160. package/src/tokens/index.ts +130 -0
  161. package/src/types/index.ts +3 -0
  162. package/src/types/types.gen.ts +28526 -0
  163. package/src/utils/base-styles.ts +89 -0
  164. package/src/utils/debounce.ts +13 -0
  165. package/src/utils/degree.ts +64 -0
  166. package/src/utils/motion.ts +18 -0
@@ -0,0 +1,433 @@
1
+ import { css, html, LitElement, nothing } from 'lit';
2
+ import { customElement, property, state } from 'lit/decorators.js';
3
+ import { baseStyles } from '../utils/base-styles.js';
4
+
5
+ interface OpenApiSchemaRef {
6
+ $ref?: string;
7
+ }
8
+
9
+ interface OpenApiSchema extends OpenApiSchemaRef {
10
+ type?: string;
11
+ format?: string;
12
+ description?: string;
13
+ enum?: string[];
14
+ default?: unknown;
15
+ minimum?: number;
16
+ maximum?: number;
17
+ properties?: Record<string, OpenApiSchema>;
18
+ required?: string[];
19
+ items?: OpenApiSchema;
20
+ example?: unknown;
21
+ }
22
+
23
+ interface FieldDef {
24
+ name: string;
25
+ type: string;
26
+ required: boolean;
27
+ description?: string;
28
+ enum?: string[];
29
+ min?: number;
30
+ max?: number;
31
+ default?: unknown;
32
+ }
33
+
34
+ /**
35
+ * Schema-driven form. Pass `endpoint` (e.g. "vedic-astrology/birth-chart").
36
+ * The form introspects the cached OpenAPI spec, slots a roxy-location-search
37
+ * when latitude+longitude+timezone fields are present, and emits a
38
+ * `roxy-submit` CustomEvent with the validated payload on submit. The caller
39
+ * decides what to do (call the SDK, render a chart, navigate).
40
+ *
41
+ * Build-time hints (x-roxy-ui formGroups) are read by scripts/build.ts and
42
+ * baked into a static map. At runtime the component falls back to runtime
43
+ * fetch of /api/v2/openapi.json when no map is provided.
44
+ */
45
+ @customElement('roxy-endpoint-form')
46
+ export class RoxyEndpointForm extends LitElement {
47
+ static styles = [
48
+ baseStyles,
49
+ css`
50
+ form {
51
+ display: grid;
52
+ gap: var(--roxy-space-md, 1rem);
53
+ background: var(--roxy-bg, #fff);
54
+ border: 1px solid var(--roxy-border, #e4e4e7);
55
+ border-radius: var(--roxy-radius-md, 8px);
56
+ padding: var(--roxy-space-lg, 1.5rem);
57
+ box-shadow: var(--roxy-shadow-sm);
58
+ }
59
+ .title {
60
+ margin: 0;
61
+ font-size: var(--roxy-text-lg, 1.125rem);
62
+ font-weight: var(--roxy-weight-bold, 600);
63
+ }
64
+ .fields {
65
+ display: grid;
66
+ grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
67
+ gap: var(--roxy-space-md, 1rem);
68
+ }
69
+ .field {
70
+ display: grid;
71
+ gap: var(--roxy-space-xs, 0.25rem);
72
+ }
73
+ label {
74
+ font-size: var(--roxy-text-sm, 0.875rem);
75
+ color: var(--roxy-secondary, #475569);
76
+ }
77
+ label .req {
78
+ color: var(--roxy-danger, #dc2626);
79
+ margin-left: 4px;
80
+ }
81
+ input,
82
+ select {
83
+ padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem);
84
+ font-size: var(--roxy-text-base, 1rem);
85
+ font-family: inherit;
86
+ color: var(--roxy-fg, #0a0a0a);
87
+ background: var(--roxy-bg, #fff);
88
+ border: 1px solid var(--roxy-border, #e4e4e7);
89
+ border-radius: var(--roxy-radius-md, 8px);
90
+ }
91
+ input:focus,
92
+ select:focus {
93
+ outline: 2px solid var(--roxy-ring, rgba(245, 158, 11, 0.4));
94
+ outline-offset: 2px;
95
+ border-color: var(--roxy-accent-fg, #b45309);
96
+ }
97
+ .help {
98
+ color: var(--roxy-muted, #71717a);
99
+ font-size: var(--roxy-text-xs, 0.75rem);
100
+ }
101
+ .location-block {
102
+ display: grid;
103
+ gap: var(--roxy-space-xs, 0.25rem);
104
+ grid-column: 1 / -1;
105
+ }
106
+ .coords {
107
+ display: grid;
108
+ grid-template-columns: repeat(3, 1fr);
109
+ gap: var(--roxy-space-sm, 0.5rem);
110
+ }
111
+ .coords input {
112
+ font-size: var(--roxy-text-sm, 0.875rem);
113
+ }
114
+ button.submit {
115
+ justify-self: start;
116
+ background: var(--roxy-accent-fg, #b45309);
117
+ color: #fff;
118
+ border: 0;
119
+ border-radius: var(--roxy-radius-md, 8px);
120
+ padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-lg, 1.5rem);
121
+ font-size: var(--roxy-text-base, 1rem);
122
+ font-weight: var(--roxy-weight-bold, 600);
123
+ cursor: pointer;
124
+ transition:
125
+ transform var(--roxy-motion-duration, 200ms)
126
+ var(--roxy-motion-easing, cubic-bezier(0.4, 0, 0.2, 1));
127
+ }
128
+ button.submit:hover {
129
+ transform: scale(1.02);
130
+ }
131
+ button.submit:focus-visible {
132
+ outline: 2px solid var(--roxy-ring, rgba(245, 158, 11, 0.4));
133
+ outline-offset: 2px;
134
+ }
135
+ `,
136
+ ];
137
+
138
+ @property({ type: String, attribute: 'data-endpoint' })
139
+ endpoint = 'vedic-astrology/birth-chart';
140
+
141
+ @property({ type: String })
142
+ method: 'GET' | 'POST' = 'POST';
143
+
144
+ @property({ type: String, attribute: 'spec-url' })
145
+ specUrl = 'https://roxyapi.com/api/v2/openapi.json';
146
+
147
+ @property({ type: String, attribute: 'submit-label' })
148
+ submitLabel = 'Submit';
149
+
150
+ @state()
151
+ private fields: FieldDef[] = [];
152
+
153
+ @state()
154
+ private values: Record<string, unknown> = {};
155
+
156
+ @state()
157
+ private hasLocation = false;
158
+
159
+ @state()
160
+ private loaded = false;
161
+
162
+ connectedCallback(): void {
163
+ super.connectedCallback();
164
+ void this.loadSchema();
165
+ }
166
+
167
+ private async loadSchema() {
168
+ 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
+ };
175
+ const path = `/${this.endpoint.replace(/^\//, '')}`;
176
+ const op = spec.paths?.[path]?.[this.method.toLowerCase()] as
177
+ | {
178
+ requestBody?: {
179
+ content?: Record<
180
+ string,
181
+ { schema?: OpenApiSchema | OpenApiSchemaRef }
182
+ >;
183
+ };
184
+ parameters?: Array<{
185
+ name: string;
186
+ in: string;
187
+ required?: boolean;
188
+ schema?: OpenApiSchema;
189
+ }>;
190
+ }
191
+ | undefined;
192
+ if (!op) return;
193
+
194
+ const schemas = spec.components?.schemas ?? {};
195
+ const fields: FieldDef[] = [];
196
+ let bodySchema: OpenApiSchema | undefined;
197
+
198
+ if (op.requestBody) {
199
+ const ref = op.requestBody.content?.['application/json']?.schema;
200
+ bodySchema = this.resolve(ref, schemas);
201
+ }
202
+
203
+ if (bodySchema?.properties) {
204
+ const required = new Set(bodySchema.required ?? []);
205
+ for (const [name, sub] of Object.entries(bodySchema.properties)) {
206
+ const resolved = this.resolve(sub, schemas) ?? {};
207
+ fields.push({
208
+ name,
209
+ type: this.fieldType(resolved),
210
+ required: required.has(name),
211
+ description: resolved.description,
212
+ enum: resolved.enum,
213
+ min: resolved.minimum,
214
+ max: resolved.maximum,
215
+ default: resolved.default,
216
+ });
217
+ }
218
+ }
219
+
220
+ for (const param of op.parameters ?? []) {
221
+ if (param.in === 'path' || param.in === 'query') {
222
+ const resolved = this.resolve(param.schema, schemas) ?? {};
223
+ fields.push({
224
+ name: param.name,
225
+ type: this.fieldType(resolved),
226
+ required: !!param.required,
227
+ description: resolved.description,
228
+ enum: resolved.enum,
229
+ default: resolved.default,
230
+ });
231
+ }
232
+ }
233
+
234
+ this.fields = fields;
235
+ this.hasLocation =
236
+ fields.some((f) => f.name === 'latitude') &&
237
+ fields.some((f) => f.name === 'longitude') &&
238
+ fields.some((f) => f.name === 'timezone');
239
+
240
+ // Pre-fill defaults
241
+ const init: Record<string, unknown> = {};
242
+ for (const f of fields) {
243
+ if (f.default !== undefined) init[f.name] = f.default;
244
+ }
245
+ this.values = init;
246
+ this.loaded = true;
247
+ } catch (_err) {
248
+ this.loaded = true;
249
+ }
250
+ }
251
+
252
+ private resolve(
253
+ schema: OpenApiSchema | OpenApiSchemaRef | undefined,
254
+ all: Record<string, OpenApiSchema>,
255
+ ): OpenApiSchema | undefined {
256
+ if (!schema) return undefined;
257
+ if ('$ref' in schema && schema.$ref) {
258
+ const name = schema.$ref.split('/').pop();
259
+ return name ? all[name] : undefined;
260
+ }
261
+ return schema as OpenApiSchema;
262
+ }
263
+
264
+ private fieldType(s: OpenApiSchema): string {
265
+ if (s.enum) return 'enum';
266
+ if (s.format === 'date') return 'date';
267
+ if (s.format === 'time') return 'time';
268
+ if (s.format === 'date-time') return 'datetime';
269
+ if (s.type === 'integer' || s.type === 'number') return 'number';
270
+ return 'text';
271
+ }
272
+
273
+ private setValue(name: string, value: unknown) {
274
+ this.values = { ...this.values, [name]: value };
275
+ }
276
+
277
+ private onLocation = (e: Event) => {
278
+ const detail = (e as CustomEvent).detail as {
279
+ latitude?: number;
280
+ longitude?: number;
281
+ timezone?: string;
282
+ utcOffset?: number;
283
+ };
284
+ if (detail) {
285
+ this.values = {
286
+ ...this.values,
287
+ latitude: detail.latitude,
288
+ longitude: detail.longitude,
289
+ timezone: detail.timezone ?? detail.utcOffset,
290
+ };
291
+ }
292
+ };
293
+
294
+ private onSubmit = (e: Event) => {
295
+ e.preventDefault();
296
+ const missing = this.fields
297
+ .filter((f) => f.required)
298
+ .filter(
299
+ (f) => this.values[f.name] === undefined || this.values[f.name] === '',
300
+ );
301
+ if (missing.length > 0) {
302
+ this.dispatchEvent(
303
+ new CustomEvent('roxy-validation-error', {
304
+ detail: { missing: missing.map((m) => m.name) },
305
+ bubbles: true,
306
+ composed: true,
307
+ }),
308
+ );
309
+ return;
310
+ }
311
+ this.dispatchEvent(
312
+ new CustomEvent('roxy-submit', {
313
+ detail: { endpoint: this.endpoint, values: this.values },
314
+ bubbles: true,
315
+ composed: true,
316
+ }),
317
+ );
318
+ };
319
+
320
+ render() {
321
+ if (!this.loaded) {
322
+ return html`<form><div class="roxy-skeleton" style="height: 8rem"></div></form>`;
323
+ }
324
+
325
+ const renderField = (f: FieldDef) => {
326
+ if (
327
+ this.hasLocation &&
328
+ (f.name === 'latitude' ||
329
+ f.name === 'longitude' ||
330
+ f.name === 'timezone')
331
+ ) {
332
+ return nothing;
333
+ }
334
+ const inputId = `roxy-form-${f.name}`;
335
+ return html`<div class="field">
336
+ <label for=${inputId}>
337
+ ${humanize(f.name)}${f.required ? html`<span class="req" aria-hidden="true">*</span>` : nothing}
338
+ </label>
339
+ ${
340
+ f.enum
341
+ ? html`<select
342
+ id=${inputId}
343
+ ?required=${f.required}
344
+ @change=${(e: Event) => this.setValue(f.name, (e.target as HTMLSelectElement).value)}
345
+ >
346
+ <option value="">Choose</option>
347
+ ${f.enum.map(
348
+ (
349
+ opt,
350
+ ) => html`<option value=${opt} ?selected=${this.values[f.name] === opt}>
351
+ ${opt}
352
+ </option>`,
353
+ )}
354
+ </select>`
355
+ : html`<input
356
+ id=${inputId}
357
+ type=${this.htmlType(f.type)}
358
+ ?required=${f.required}
359
+ min=${f.min ?? ''}
360
+ max=${f.max ?? ''}
361
+ step=${f.type === 'number' ? 'any' : ''}
362
+ .value=${(this.values[f.name] ?? '') as string}
363
+ @input=${(e: Event) =>
364
+ this.setValue(
365
+ f.name,
366
+ this.coerce(f.type, (e.target as HTMLInputElement).value),
367
+ )}
368
+ />`
369
+ }
370
+ ${f.description ? html`<small class="help">${f.description}</small>` : nothing}
371
+ </div>`;
372
+ };
373
+
374
+ return html`<form @submit=${this.onSubmit}>
375
+ <h2 class="title">${humanize(this.endpoint.split('/').pop() ?? '')}</h2>
376
+ ${
377
+ this.hasLocation
378
+ ? html`<div class="location-block">
379
+ <label>Birth location</label>
380
+ <roxy-location-search
381
+ @roxy-location-select=${this.onLocation}
382
+ placeholder="City of birth"
383
+ ></roxy-location-search>
384
+ <small class="help">
385
+ Required: latitude, longitude, timezone. Pick a city to autofill.
386
+ </small>
387
+ </div>`
388
+ : nothing
389
+ }
390
+ <div class="fields">
391
+ ${this.fields.map((f) => renderField(f))}
392
+ </div>
393
+ <button class="submit" type="submit">${this.submitLabel}</button>
394
+ </form>`;
395
+ }
396
+
397
+ private htmlType(t: string): string {
398
+ switch (t) {
399
+ case 'date':
400
+ return 'date';
401
+ case 'time':
402
+ return 'time';
403
+ case 'datetime':
404
+ return 'datetime-local';
405
+ case 'number':
406
+ return 'number';
407
+ default:
408
+ return 'text';
409
+ }
410
+ }
411
+
412
+ private coerce(t: string, v: string): unknown {
413
+ if (v === '') return undefined;
414
+ if (t === 'number') {
415
+ const n = Number(v);
416
+ return Number.isFinite(n) ? n : undefined;
417
+ }
418
+ return v;
419
+ }
420
+ }
421
+
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
+ declare global {
430
+ interface HTMLElementTagNameMap {
431
+ 'roxy-endpoint-form': RoxyEndpointForm;
432
+ }
433
+ }
@@ -0,0 +1,245 @@
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 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
+ }
24
+
25
+ const STANDARD_CATEGORIES = [
26
+ 'Varna',
27
+ 'Vasya',
28
+ 'Tara',
29
+ 'Yoni',
30
+ 'Maitri',
31
+ 'Gana',
32
+ 'Bhakoot',
33
+ 'Nadi',
34
+ ];
35
+
36
+ /**
37
+ * 36-point Ashtakoota score card. Renders /vedic-astrology/compatibility.
38
+ */
39
+ @customElement('roxy-guna-milan')
40
+ export class RoxyGunaMilan extends LitElement {
41
+ static styles = [
42
+ baseStyles,
43
+ css`
44
+ .card {
45
+ background: var(--roxy-bg, #fff);
46
+ border: 1px solid var(--roxy-border, #e4e4e7);
47
+ border-radius: var(--roxy-radius-md, 8px);
48
+ padding: var(--roxy-space-lg, 1.5rem);
49
+ box-shadow: var(--roxy-shadow-sm);
50
+ display: grid;
51
+ gap: var(--roxy-space-md, 1rem);
52
+ }
53
+
54
+ .score-bar {
55
+ display: grid;
56
+ grid-template-columns: 1fr auto;
57
+ align-items: center;
58
+ gap: var(--roxy-space-md, 1rem);
59
+ }
60
+ .total {
61
+ font-size: 2.25rem;
62
+ font-weight: var(--roxy-weight-bold, 600);
63
+ color: var(--roxy-accent-fg, #b45309);
64
+ font-variant-numeric: tabular-nums;
65
+ line-height: 1;
66
+ }
67
+ .over {
68
+ color: var(--roxy-muted, #71717a);
69
+ font-size: var(--roxy-text-base, 1rem);
70
+ }
71
+ .recommendation {
72
+ font-size: var(--roxy-text-sm, 0.875rem);
73
+ color: var(--roxy-secondary, #475569);
74
+ }
75
+
76
+ table {
77
+ width: 100%;
78
+ border-collapse: collapse;
79
+ font-size: var(--roxy-text-sm, 0.875rem);
80
+ }
81
+ th,
82
+ td {
83
+ padding: var(--roxy-space-sm, 0.5rem);
84
+ border-bottom: 1px solid var(--roxy-border, #e4e4e7);
85
+ text-align: left;
86
+ }
87
+ th {
88
+ color: var(--roxy-muted, #71717a);
89
+ font-weight: var(--roxy-weight-bold, 600);
90
+ text-transform: uppercase;
91
+ font-size: var(--roxy-text-xs, 0.75rem);
92
+ letter-spacing: 0.06em;
93
+ }
94
+ td.score {
95
+ text-align: right;
96
+ font-variant-numeric: tabular-nums;
97
+ color: var(--roxy-fg, #0a0a0a);
98
+ font-weight: var(--roxy-weight-bold, 600);
99
+ }
100
+ td.bar-cell {
101
+ width: 30%;
102
+ }
103
+ .mini-bar {
104
+ height: 8px;
105
+ background: var(--roxy-border, #e4e4e7);
106
+ border-radius: var(--roxy-radius-full, 9999px);
107
+ overflow: hidden;
108
+ }
109
+ .mini-bar > span {
110
+ display: block;
111
+ height: 100%;
112
+ background: var(--roxy-accent, #f59e0b);
113
+ transition:
114
+ width var(--roxy-motion-duration, 200ms)
115
+ var(--roxy-motion-easing, cubic-bezier(0.4, 0, 0.2, 1));
116
+ }
117
+
118
+ .tags {
119
+ display: flex;
120
+ flex-wrap: wrap;
121
+ gap: var(--roxy-space-xs, 0.25rem);
122
+ }
123
+ .tags span {
124
+ padding: 2px 8px;
125
+ border-radius: var(--roxy-radius-full, 9999px);
126
+ font-size: var(--roxy-text-xs, 0.75rem);
127
+ }
128
+ .tags .dosha {
129
+ background: color-mix(in srgb, var(--roxy-danger, #dc2626) 16%, transparent);
130
+ color: var(--roxy-danger, #dc2626);
131
+ }
132
+ .tags .cancel {
133
+ background: color-mix(in srgb, var(--roxy-success, #16a34a) 18%, transparent);
134
+ color: var(--roxy-success, #16a34a);
135
+ }
136
+ `,
137
+ ];
138
+
139
+ @property({ attribute: false })
140
+ data: GunaData | null = null;
141
+
142
+ render() {
143
+ const d = this.data;
144
+ if (!d)
145
+ return html`<div class="roxy-empty" role="status">No Guna Milan data</div>`;
146
+
147
+ const total = d.total ?? d.totalScore ?? 0;
148
+ const max = d.maxScore ?? 36;
149
+ const breakdown = (d.breakdown ?? []).filter(
150
+ (b) => b && (b.name || b.score !== undefined),
151
+ );
152
+
153
+ 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
+ }
165
+ </div>
166
+ ${
167
+ d.recommendation
168
+ ? html`<span class="recommendation">${d.recommendation}</span>`
169
+ : nothing
170
+ }
171
+ </div>
172
+
173
+ ${
174
+ breakdown.length > 0
175
+ ? html`<table>
176
+ <thead>
177
+ <tr>
178
+ <th>Category</th>
179
+ <th>Progress</th>
180
+ <th class="score">Score</th>
181
+ </tr>
182
+ </thead>
183
+ <tbody>
184
+ ${breakdown.map((b) => {
185
+ const score = b.score ?? 0;
186
+ const maxScore = b.max ?? b.maxScore ?? defaultMax(b.name);
187
+ const pct = maxScore ? (score / maxScore) * 100 : 0;
188
+ return html`<tr>
189
+ <td>${b.name ?? ''}</td>
190
+ <td class="bar-cell">
191
+ <div class="mini-bar">
192
+ <span style="width: ${pct}%"></span>
193
+ </div>
194
+ </td>
195
+ <td class="score">${score} / ${maxScore}</td>
196
+ </tr>`;
197
+ })}
198
+ </tbody>
199
+ </table>`
200
+ : nothing
201
+ }
202
+ ${
203
+ (d.doshas?.length ?? 0) > 0 || (d.doshaCancellations?.length ?? 0) > 0
204
+ ? html`<div class="tags">
205
+ ${d.doshas?.map((x) => html`<span class="dosha">${x}</span>`)}
206
+ ${d.doshaCancellations?.map((x) => html`<span class="cancel">${x}</span>`)}
207
+ </div>`
208
+ : nothing
209
+ }
210
+ </article>`;
211
+ }
212
+ }
213
+
214
+ function defaultMax(name?: string): number {
215
+ if (!name) return 1;
216
+ switch (name.toLowerCase()) {
217
+ case 'varna':
218
+ return 1;
219
+ case 'vasya':
220
+ return 2;
221
+ case 'tara':
222
+ return 3;
223
+ case 'yoni':
224
+ return 4;
225
+ case 'maitri':
226
+ return 5;
227
+ case 'gana':
228
+ return 6;
229
+ case 'bhakoot':
230
+ return 7;
231
+ case 'nadi':
232
+ return 8;
233
+ default:
234
+ return 1;
235
+ }
236
+ }
237
+
238
+ // Reference list (kept for documentation, used at codegen time)
239
+ export const GUNA_CATEGORIES = STANDARD_CATEGORIES;
240
+
241
+ declare global {
242
+ interface HTMLElementTagNameMap {
243
+ 'roxy-guna-milan': RoxyGunaMilan;
244
+ }
245
+ }