@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,298 @@
1
+ import { css, html, LitElement, nothing, svg } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { PLANET_GLYPH, SIGN_GLYPH } from '../tokens/index.js';
4
+ import { baseStyles } from '../utils/base-styles.js';
5
+ import { longitudeToSignPosition, polarToCartesian } from '../utils/degree.js';
6
+
7
+ interface PlanetEntry {
8
+ name?: string;
9
+ planet?: string;
10
+ longitude?: number;
11
+ degree?: number;
12
+ sign?: string;
13
+ house?: number;
14
+ retrograde?: boolean;
15
+ isRetrograde?: boolean;
16
+ }
17
+
18
+ interface AspectEntry {
19
+ planet1?: string;
20
+ planet2?: string;
21
+ aspect?: string;
22
+ orb?: number;
23
+ }
24
+
25
+ interface HouseEntry {
26
+ house?: number;
27
+ number?: number;
28
+ cusp?: number;
29
+ sign?: string;
30
+ }
31
+
32
+ interface NatalChartData {
33
+ planets?: PlanetEntry[] | Record<string, PlanetEntry>;
34
+ houses?: HouseEntry[];
35
+ aspects?: AspectEntry[];
36
+ ascendant?: number | { longitude?: number; sign?: string };
37
+ midheaven?: number | { longitude?: number; sign?: string };
38
+ birthDetails?: {
39
+ date?: string;
40
+ time?: string;
41
+ location?: string;
42
+ };
43
+ }
44
+
45
+ const SIZE = 320;
46
+ const CENTER = SIZE / 2;
47
+ const OUTER_R = 150;
48
+ const SIGN_R = 134;
49
+ const HOUSE_R = 110;
50
+ const PLANET_R = 88;
51
+
52
+ /**
53
+ * Western natal chart wheel. Renders the 12 zodiac signs, 12 houses, planet
54
+ * markers, and aspect lines from a /astrology/natal-chart response.
55
+ */
56
+ @customElement('roxy-natal-chart')
57
+ export class RoxyNatalChart extends LitElement {
58
+ static styles = [
59
+ baseStyles,
60
+ css`
61
+ .wrap {
62
+ width: 100%;
63
+ display: grid;
64
+ gap: var(--roxy-space-md, 1rem);
65
+ }
66
+
67
+ .title {
68
+ font-size: var(--roxy-text-lg, 1.125rem);
69
+ font-weight: var(--roxy-weight-bold, 600);
70
+ margin: 0;
71
+ color: var(--roxy-primary, #0f172a);
72
+ }
73
+
74
+ .meta {
75
+ color: var(--roxy-muted, #71717a);
76
+ font-size: var(--roxy-text-sm, 0.875rem);
77
+ }
78
+
79
+ svg {
80
+ display: block;
81
+ width: 100%;
82
+ max-width: 360px;
83
+ height: auto;
84
+ margin: 0 auto;
85
+ }
86
+
87
+ .wheel-line {
88
+ fill: none;
89
+ stroke: var(--roxy-border, #e4e4e7);
90
+ }
91
+
92
+ .sign-glyph {
93
+ fill: var(--roxy-secondary, #475569);
94
+ font-size: 14px;
95
+ font-family: var(--roxy-font-sans);
96
+ }
97
+
98
+ .planet-glyph {
99
+ fill: var(--roxy-accent, #f59e0b);
100
+ font-size: 14px;
101
+ font-weight: 600;
102
+ font-family: var(--roxy-font-sans);
103
+ }
104
+
105
+ .house-num {
106
+ fill: var(--roxy-muted, #71717a);
107
+ font-size: 9px;
108
+ font-family: var(--roxy-font-sans);
109
+ }
110
+
111
+ .aspect {
112
+ stroke: color-mix(in srgb, var(--roxy-accent, #f59e0b) 32%, transparent);
113
+ stroke-width: 0.6;
114
+ fill: none;
115
+ }
116
+
117
+ .legend {
118
+ font-size: var(--roxy-text-xs, 0.75rem);
119
+ color: var(--roxy-muted, #71717a);
120
+ display: flex;
121
+ flex-wrap: wrap;
122
+ gap: var(--roxy-space-md, 1rem);
123
+ }
124
+ `,
125
+ ];
126
+
127
+ @property({ attribute: false })
128
+ data: NatalChartData | null = null;
129
+
130
+ @property({ type: String, attribute: 'house-system', reflect: true })
131
+ houseSystem: 'placidus' | 'whole-sign' | 'equal' | 'koch' = 'placidus';
132
+
133
+ private getPlanets(): PlanetEntry[] {
134
+ const p = this.data?.planets;
135
+ if (!p) return [];
136
+ if (Array.isArray(p)) return p;
137
+ return Object.entries(p).map(([name, entry]) => ({ ...entry, name }));
138
+ }
139
+
140
+ render() {
141
+ if (!this.data)
142
+ return html`<div class="roxy-empty" role="status">No chart data</div>`;
143
+ const planets = this.getPlanets();
144
+ const aspects = this.data.aspects ?? [];
145
+
146
+ return html`<div class="wrap">
147
+ <header>
148
+ <h2 class="title">Natal chart</h2>
149
+ ${
150
+ this.data.birthDetails
151
+ ? html`<div class="meta">
152
+ ${[
153
+ this.data.birthDetails.date,
154
+ this.data.birthDetails.time,
155
+ this.data.birthDetails.location,
156
+ ]
157
+ .filter(Boolean)
158
+ .join(' · ')}
159
+ </div>`
160
+ : nothing
161
+ }
162
+ </header>
163
+ <svg
164
+ viewBox="0 0 ${SIZE} ${SIZE}"
165
+ role="img"
166
+ aria-label="Natal chart wheel with twelve houses, planets, and aspects"
167
+ >
168
+ <title>Natal chart wheel</title>
169
+ <desc>
170
+ Twelve zodiac sign segments around a circular wheel. Planet glyphs are
171
+ placed at their ecliptic longitudes. Aspect lines connect related planets.
172
+ </desc>
173
+ <circle
174
+ class="wheel-line"
175
+ cx=${CENTER}
176
+ cy=${CENTER}
177
+ r=${OUTER_R}
178
+ stroke-width="1.5"
179
+ />
180
+ <circle
181
+ class="wheel-line"
182
+ cx=${CENTER}
183
+ cy=${CENTER}
184
+ r=${HOUSE_R}
185
+ stroke-width="1"
186
+ />
187
+ <circle
188
+ class="wheel-line"
189
+ cx=${CENTER}
190
+ cy=${CENTER}
191
+ r=${PLANET_R - 16}
192
+ stroke-width="0.5"
193
+ />
194
+ ${this.renderSpokes()} ${this.renderSigns()} ${this.renderHouseNumbers()}
195
+ ${this.renderAspects(planets, aspects)} ${this.renderPlanets(planets)}
196
+ </svg>
197
+ <div class="legend">
198
+ <span>${planets.length} planets</span>
199
+ <span>${aspects.length} aspects</span>
200
+ <span>House system: ${this.houseSystem}</span>
201
+ </div>
202
+ </div>`;
203
+ }
204
+
205
+ private renderSpokes() {
206
+ return Array.from({ length: 12 }, (_, i) => {
207
+ const angle = i * 30 - 90;
208
+ const start = polarToCartesian(CENTER, CENTER, HOUSE_R, angle);
209
+ const end = polarToCartesian(CENTER, CENTER, OUTER_R, angle);
210
+ return svg`<line class="wheel-line" x1=${start.x} y1=${start.y} x2=${end.x} y2=${end.y} stroke-width="0.8" />`;
211
+ });
212
+ }
213
+
214
+ private renderSigns() {
215
+ const order = [
216
+ 'Aries',
217
+ 'Taurus',
218
+ 'Gemini',
219
+ 'Cancer',
220
+ 'Leo',
221
+ 'Virgo',
222
+ 'Libra',
223
+ 'Scorpio',
224
+ 'Sagittarius',
225
+ 'Capricorn',
226
+ 'Aquarius',
227
+ 'Pisces',
228
+ ];
229
+ return order.map((sign, i) => {
230
+ const angle = i * 30 + 15 - 90;
231
+ const pos = polarToCartesian(CENTER, CENTER, SIGN_R, angle);
232
+ return svg`<text class="sign-glyph" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central">${SIGN_GLYPH[sign]}</text>`;
233
+ });
234
+ }
235
+
236
+ private renderHouseNumbers() {
237
+ return Array.from({ length: 12 }, (_, i) => {
238
+ const angle = i * 30 + 15 - 90;
239
+ const pos = polarToCartesian(CENTER, CENTER, HOUSE_R - 12, angle);
240
+ return svg`<text class="house-num" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central">${i + 1}</text>`;
241
+ });
242
+ }
243
+
244
+ private renderPlanets(planets: PlanetEntry[]) {
245
+ return planets.map((p) => {
246
+ const lon =
247
+ typeof p.longitude === 'number'
248
+ ? p.longitude
249
+ : typeof p.degree === 'number'
250
+ ? p.degree
251
+ : NaN;
252
+ if (!Number.isFinite(lon)) return nothing;
253
+ const angle = lon - 90;
254
+ const pos = polarToCartesian(CENTER, CENTER, PLANET_R, angle);
255
+ const name = p.name ?? p.planet ?? '';
256
+ const glyph = PLANET_GLYPH[capitalize(name)] ?? name.slice(0, 2);
257
+ const retro = p.retrograde || p.isRetrograde ? ' R' : '';
258
+ return svg`<text class="planet-glyph" x=${pos.x} y=${pos.y} text-anchor="middle" dominant-baseline="central"><title>${name}${retro}</title>${glyph}</text>`;
259
+ });
260
+ }
261
+
262
+ private renderAspects(planets: PlanetEntry[], aspects: AspectEntry[]) {
263
+ const planetMap = new Map<string, number>();
264
+ for (const p of planets) {
265
+ const lon =
266
+ typeof p.longitude === 'number'
267
+ ? p.longitude
268
+ : typeof p.degree === 'number'
269
+ ? p.degree
270
+ : null;
271
+ if (lon === null) continue;
272
+ const name = capitalize(p.name ?? p.planet ?? '');
273
+ if (name) planetMap.set(name, lon);
274
+ }
275
+ return aspects.map((a) => {
276
+ const l1 = planetMap.get(capitalize(a.planet1 ?? ''));
277
+ const l2 = planetMap.get(capitalize(a.planet2 ?? ''));
278
+ if (l1 === undefined || l2 === undefined) return nothing;
279
+ const p1 = polarToCartesian(CENTER, CENTER, PLANET_R - 18, l1 - 90);
280
+ const p2 = polarToCartesian(CENTER, CENTER, PLANET_R - 18, l2 - 90);
281
+ return svg`<line class="aspect" x1=${p1.x} y1=${p1.y} x2=${p2.x} y2=${p2.y} />`;
282
+ });
283
+ }
284
+ }
285
+
286
+ function capitalize(s: string): string {
287
+ if (!s) return '';
288
+ return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
289
+ }
290
+
291
+ declare global {
292
+ interface HTMLElementTagNameMap {
293
+ 'roxy-natal-chart': RoxyNatalChart;
294
+ }
295
+ }
296
+
297
+ // Export for external use
298
+ export { longitudeToSignPosition };
@@ -0,0 +1,243 @@
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 NumerologyCommon {
6
+ number?: number;
7
+ calculation?: string;
8
+ type?: 'single' | 'master' | string;
9
+ hasKarmicDebt?: boolean;
10
+ karmicDebtNumber?: number;
11
+ karmicDebtMeaning?: string;
12
+ meaning?: string;
13
+ }
14
+
15
+ interface CoreNumber {
16
+ number?: number;
17
+ type?: string;
18
+ meaning?: string;
19
+ calculation?: string;
20
+ }
21
+
22
+ interface FullChart {
23
+ profile?: { fullName?: string; birthDate?: string };
24
+ coreNumbers?: Record<string, CoreNumber | number>;
25
+ additionalInsights?: Record<string, unknown>;
26
+ birthDayProfile?: Record<string, unknown>;
27
+ maturityStatus?: string;
28
+ luckyAssociations?: Record<string, unknown>;
29
+ summary?: string;
30
+ }
31
+
32
+ interface PersonalYear {
33
+ year?: number;
34
+ personalYear?: number;
35
+ title?: string;
36
+ theme?: string;
37
+ keywords?: string[];
38
+ meaning?: string;
39
+ advice?: string;
40
+ }
41
+
42
+ type NumerologyData = NumerologyCommon & FullChart & PersonalYear;
43
+
44
+ /**
45
+ * Numerology card. Renders /numerology/{life-path,expression,personal-year,chart}.
46
+ * Use the `type` attribute to switch the layout.
47
+ */
48
+ @customElement('roxy-numerology-card')
49
+ export class RoxyNumerologyCard extends LitElement {
50
+ static styles = [
51
+ baseStyles,
52
+ css`
53
+ .card {
54
+ background: var(--roxy-bg, #fff);
55
+ border: 1px solid var(--roxy-border, #e4e4e7);
56
+ border-radius: var(--roxy-radius-md, 8px);
57
+ padding: var(--roxy-space-lg, 1.5rem);
58
+ box-shadow: var(--roxy-shadow-sm);
59
+ display: grid;
60
+ gap: var(--roxy-space-md, 1rem);
61
+ }
62
+
63
+ .hero {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: var(--roxy-space-md, 1rem);
67
+ }
68
+ .numeral {
69
+ font-size: 4rem;
70
+ line-height: 1;
71
+ font-weight: var(--roxy-weight-bold, 600);
72
+ color: var(--roxy-accent-fg, #b45309);
73
+ font-variant-numeric: tabular-nums;
74
+ }
75
+ .label {
76
+ margin: 0;
77
+ font-size: var(--roxy-text-xs, 0.75rem);
78
+ color: var(--roxy-muted, #71717a);
79
+ text-transform: uppercase;
80
+ letter-spacing: 0.06em;
81
+ }
82
+ .title {
83
+ margin: 0;
84
+ font-size: var(--roxy-text-lg, 1.125rem);
85
+ font-weight: var(--roxy-weight-bold, 600);
86
+ }
87
+ .meaning {
88
+ margin: 0;
89
+ color: var(--roxy-fg, #0a0a0a);
90
+ }
91
+
92
+ .calc {
93
+ margin: 0;
94
+ font-family: var(--roxy-font-mono);
95
+ font-size: var(--roxy-text-xs, 0.75rem);
96
+ color: var(--roxy-muted, #71717a);
97
+ background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 30%, transparent);
98
+ padding: var(--roxy-space-sm, 0.5rem);
99
+ border-radius: var(--roxy-radius-sm, 4px);
100
+ word-break: break-all;
101
+ }
102
+
103
+ .chips {
104
+ display: flex;
105
+ flex-wrap: wrap;
106
+ gap: var(--roxy-space-xs, 0.25rem);
107
+ }
108
+ .chips span {
109
+ background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 14%, transparent);
110
+ padding: 2px 8px;
111
+ border-radius: var(--roxy-radius-full, 9999px);
112
+ font-size: var(--roxy-text-xs, 0.75rem);
113
+ }
114
+
115
+ .cores {
116
+ display: grid;
117
+ grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
118
+ gap: var(--roxy-space-sm, 0.5rem);
119
+ border-top: 1px solid var(--roxy-border, #e4e4e7);
120
+ padding-top: var(--roxy-space-md, 1rem);
121
+ }
122
+ .cores .item {
123
+ display: flex;
124
+ align-items: baseline;
125
+ gap: var(--roxy-space-xs, 0.25rem);
126
+ font-size: var(--roxy-text-sm, 0.875rem);
127
+ }
128
+ .cores .item span:first-child {
129
+ color: var(--roxy-muted, #71717a);
130
+ text-transform: capitalize;
131
+ }
132
+ .cores .item strong {
133
+ color: var(--roxy-accent-fg, #b45309);
134
+ font-variant-numeric: tabular-nums;
135
+ font-size: var(--roxy-text-base, 1rem);
136
+ font-weight: var(--roxy-weight-bold, 600);
137
+ }
138
+
139
+ .karmic {
140
+ background: color-mix(in srgb, var(--roxy-warning, #ea580c) 12%, transparent);
141
+ border: 1px solid color-mix(in srgb, var(--roxy-warning, #ea580c) 32%, transparent);
142
+ padding: var(--roxy-space-sm, 0.5rem) var(--roxy-space-md, 1rem);
143
+ border-radius: var(--roxy-radius-md, 8px);
144
+ font-size: var(--roxy-text-sm, 0.875rem);
145
+ color: var(--roxy-fg, #0a0a0a);
146
+ }
147
+ `,
148
+ ];
149
+
150
+ @property({ attribute: false })
151
+ data: NumerologyData | null = null;
152
+
153
+ @property({ type: String, reflect: true })
154
+ type: 'life-path' | 'expression' | 'personal-year' | 'chart' = 'life-path';
155
+
156
+ render() {
157
+ const d = this.data;
158
+ if (!d)
159
+ return html`<div class="roxy-empty" role="status">No numerology data</div>`;
160
+
161
+ const headerLabel = LABELS[this.type] ?? this.type;
162
+ const number = d.personalYear ?? d.number;
163
+ const cores = d.coreNumbers
164
+ ? Object.entries(d.coreNumbers).filter(
165
+ ([, v]) => v !== null && v !== undefined,
166
+ )
167
+ : [];
168
+
169
+ return html`<article
170
+ class="card"
171
+ aria-label=${headerLabel}
172
+ >
173
+ <div class="hero">
174
+ ${typeof number === 'number' ? html`<div class="numeral">${number}</div>` : nothing}
175
+ <div>
176
+ <p class="label">${headerLabel}</p>
177
+ ${
178
+ d.title
179
+ ? html`<h2 class="title">${d.title}</h2>`
180
+ : d.type
181
+ ? html`<h2 class="title">
182
+ ${d.type === 'master' ? 'Master number' : 'Single digit'}
183
+ </h2>`
184
+ : nothing
185
+ }
186
+ </div>
187
+ </div>
188
+ ${d.theme ? html`<p><strong>Theme:</strong> ${d.theme}</p>` : nothing}
189
+ ${d.meaning ? html`<p class="meaning">${d.meaning}</p>` : nothing}
190
+ ${d.advice ? html`<p>${d.advice}</p>` : nothing}
191
+ ${d.calculation ? html`<pre class="calc">${d.calculation}</pre>` : nothing}
192
+ ${
193
+ d.keywords?.length
194
+ ? html`<div class="chips">
195
+ ${d.keywords.map((k) => html`<span>${k}</span>`)}
196
+ </div>`
197
+ : nothing
198
+ }
199
+ ${
200
+ cores.length > 0
201
+ ? html`<div class="cores">
202
+ ${cores.map(([k, v]) => {
203
+ const value =
204
+ typeof v === 'number' ? v : (v as CoreNumber).number;
205
+ return html`<div class="item">
206
+ <span>${humanize(k)}</span>
207
+ <strong>${value ?? ''}</strong>
208
+ </div>`;
209
+ })}
210
+ </div>`
211
+ : nothing
212
+ }
213
+ ${
214
+ d.hasKarmicDebt && d.karmicDebtNumber
215
+ ? html`<div class="karmic">
216
+ Karmic debt ${d.karmicDebtNumber}.
217
+ ${d.karmicDebtMeaning ? d.karmicDebtMeaning : ''}
218
+ </div>`
219
+ : nothing
220
+ }
221
+ </article>`;
222
+ }
223
+ }
224
+
225
+ const LABELS: Record<string, string> = {
226
+ 'life-path': 'Life Path',
227
+ expression: 'Expression',
228
+ 'personal-year': 'Personal Year',
229
+ chart: 'Numerology chart',
230
+ };
231
+
232
+ function humanize(s: string): string {
233
+ return s
234
+ .replace(/[_-]+/g, ' ')
235
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
236
+ .replace(/^\w/, (c) => c.toUpperCase());
237
+ }
238
+
239
+ declare global {
240
+ interface HTMLElementTagNameMap {
241
+ 'roxy-numerology-card': RoxyNumerologyCard;
242
+ }
243
+ }