@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,235 @@
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 TarotCard {
6
+ id?: string;
7
+ name?: string;
8
+ arcana?: 'major' | 'minor' | string;
9
+ number?: number | string;
10
+ position?: string;
11
+ reversed?: boolean;
12
+ keywords?: string[];
13
+ meaning?:
14
+ | string
15
+ | {
16
+ upright?: string;
17
+ reversed?: string;
18
+ spiritual?: string;
19
+ emotional?: string;
20
+ physical?: string;
21
+ };
22
+ imageUrl?: string;
23
+ upright?: { meaning?: string; keywords?: string[] };
24
+ }
25
+
26
+ interface TarotData {
27
+ date?: string;
28
+ seed?: string;
29
+ card?: TarotCard;
30
+ dailyMessage?: string;
31
+ }
32
+
33
+ /**
34
+ * Tarot card. Renders /tarot/cards/{id} or /tarot/daily. Click to flip
35
+ * between upright and reversed where the data shape supports it.
36
+ */
37
+ @customElement('roxy-tarot-card')
38
+ export class RoxyTarotCard extends LitElement {
39
+ static styles = [
40
+ baseStyles,
41
+ css`
42
+ .card {
43
+ background: var(--roxy-bg, #fff);
44
+ border: 1px solid var(--roxy-border, #e4e4e7);
45
+ border-radius: var(--roxy-radius-md, 8px);
46
+ padding: var(--roxy-space-lg, 1.5rem);
47
+ box-shadow: var(--roxy-shadow-sm);
48
+ display: grid;
49
+ grid-template-columns: minmax(0, 9rem) 1fr;
50
+ gap: var(--roxy-space-lg, 1.5rem);
51
+ align-items: start;
52
+ }
53
+
54
+ @container (max-width: 480px) {
55
+ .card {
56
+ grid-template-columns: 1fr;
57
+ }
58
+ }
59
+
60
+ .image-wrap {
61
+ perspective: 800px;
62
+ }
63
+ .image {
64
+ display: block;
65
+ width: 100%;
66
+ height: auto;
67
+ border-radius: var(--roxy-radius-md, 8px);
68
+ background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 60%, transparent);
69
+ transition:
70
+ transform var(--roxy-motion-duration, 200ms)
71
+ var(--roxy-motion-easing, cubic-bezier(0.4, 0, 0.2, 1));
72
+ cursor: pointer;
73
+ }
74
+ .image.reversed {
75
+ transform: rotate(180deg);
76
+ }
77
+ .image:focus-visible {
78
+ outline: 2px solid var(--roxy-ring, rgba(245, 158, 11, 0.4));
79
+ outline-offset: 2px;
80
+ }
81
+
82
+ .title {
83
+ margin: 0;
84
+ font-size: var(--roxy-text-xl, 1.5rem);
85
+ font-weight: var(--roxy-weight-bold, 600);
86
+ letter-spacing: var(--roxy-tracking-tight);
87
+ }
88
+ .meta {
89
+ color: var(--roxy-muted, #71717a);
90
+ font-size: var(--roxy-text-sm, 0.875rem);
91
+ text-transform: uppercase;
92
+ letter-spacing: 0.06em;
93
+ margin-bottom: var(--roxy-space-sm, 0.5rem);
94
+ }
95
+ .position {
96
+ color: var(--roxy-info, #0284c7);
97
+ margin-left: var(--roxy-space-xs, 0.25rem);
98
+ text-transform: capitalize;
99
+ }
100
+
101
+ .message {
102
+ color: var(--roxy-fg, #0a0a0a);
103
+ margin: var(--roxy-space-sm, 0.5rem) 0 var(--roxy-space-md, 1rem);
104
+ }
105
+
106
+ .chips {
107
+ display: flex;
108
+ flex-wrap: wrap;
109
+ gap: var(--roxy-space-xs, 0.25rem);
110
+ margin-top: var(--roxy-space-sm, 0.5rem);
111
+ }
112
+ .chips span {
113
+ background: color-mix(in srgb, var(--roxy-accent, #f59e0b) 14%, transparent);
114
+ padding: 2px 8px;
115
+ border-radius: var(--roxy-radius-full, 9999px);
116
+ font-size: var(--roxy-text-xs, 0.75rem);
117
+ }
118
+
119
+ .flip {
120
+ margin-top: var(--roxy-space-sm, 0.5rem);
121
+ background: transparent;
122
+ border: 1px solid var(--roxy-border, #e4e4e7);
123
+ border-radius: var(--roxy-radius-md, 8px);
124
+ padding: 4px 12px;
125
+ font-family: inherit;
126
+ font-size: var(--roxy-text-xs, 0.75rem);
127
+ color: var(--roxy-secondary, #475569);
128
+ cursor: pointer;
129
+ transition:
130
+ transform var(--roxy-motion-duration, 200ms)
131
+ var(--roxy-motion-easing, cubic-bezier(0.4, 0, 0.2, 1));
132
+ }
133
+ .flip:hover {
134
+ transform: scale(1.02);
135
+ }
136
+ `,
137
+ ];
138
+
139
+ @property({ attribute: false })
140
+ data: TarotData | TarotCard | null = null;
141
+
142
+ @state()
143
+ private flipped = false;
144
+
145
+ private getCard(): TarotCard | null {
146
+ if (!this.data) return null;
147
+ if ('card' in this.data && this.data.card) return this.data.card;
148
+ return this.data as TarotCard;
149
+ }
150
+
151
+ private toggleFlip = () => {
152
+ this.flipped = !this.flipped;
153
+ };
154
+
155
+ render() {
156
+ const card = this.getCard();
157
+ if (!card)
158
+ return html`<div class="roxy-empty" role="status">No tarot data</div>`;
159
+
160
+ const isReversed = this.flipped !== Boolean(card.reversed); // start at server-provided orientation, toggle on click
161
+ const meaning =
162
+ typeof card.meaning === 'string'
163
+ ? card.meaning
164
+ : ((isReversed ? card.meaning?.reversed : card.meaning?.upright) ??
165
+ card.meaning?.spiritual ??
166
+ card.upright?.meaning);
167
+ const dailyMessage =
168
+ this.data && 'dailyMessage' in this.data
169
+ ? this.data.dailyMessage
170
+ : undefined;
171
+
172
+ return html`<article class="card" aria-label=${card.name ?? 'Tarot card'}>
173
+ <div class="image-wrap">
174
+ ${
175
+ card.imageUrl
176
+ ? html`<img
177
+ class=${`image ${isReversed ? 'reversed' : ''}`}
178
+ src=${card.imageUrl}
179
+ alt=${card.name ?? 'Tarot card'}
180
+ tabindex="0"
181
+ @click=${this.toggleFlip}
182
+ @keydown=${(e: KeyboardEvent) => {
183
+ if (e.key === 'Enter' || e.key === ' ') {
184
+ e.preventDefault();
185
+ this.toggleFlip();
186
+ }
187
+ }}
188
+ />`
189
+ : html`<div
190
+ class=${`image ${isReversed ? 'reversed' : ''}`}
191
+ style="aspect-ratio: 0.6; display: flex; align-items: center; justify-content: center; color: var(--roxy-muted)"
192
+ >
193
+ ${card.name ?? '?'}
194
+ </div>`
195
+ }
196
+ </div>
197
+ <div>
198
+ <div class="meta">
199
+ ${card.arcana ? html`${card.arcana} arcana` : nothing}
200
+ ${card.number !== undefined && card.number !== null ? html` · ${card.number}` : nothing}
201
+ ${isReversed ? html` · reversed` : nothing}
202
+ ${
203
+ card.position
204
+ ? html`<span class="position">${card.position}</span>`
205
+ : nothing
206
+ }
207
+ </div>
208
+ <h2 class="title">${card.name ?? 'Tarot card'}</h2>
209
+ ${dailyMessage ? html`<p class="message">${dailyMessage}</p>` : nothing}
210
+ ${meaning ? html`<p>${meaning}</p>` : nothing}
211
+ ${
212
+ card.keywords?.length
213
+ ? html`<div class="chips">
214
+ ${card.keywords.map((k) => html`<span>${k}</span>`)}
215
+ </div>`
216
+ : nothing
217
+ }
218
+ <button
219
+ class="flip"
220
+ type="button"
221
+ @click=${this.toggleFlip}
222
+ aria-pressed=${this.flipped ? 'true' : 'false'}
223
+ >
224
+ Flip card
225
+ </button>
226
+ </div>
227
+ </article>`;
228
+ }
229
+ }
230
+
231
+ declare global {
232
+ interface HTMLElementTagNameMap {
233
+ 'roxy-tarot-card': RoxyTarotCard;
234
+ }
235
+ }
@@ -0,0 +1,224 @@
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 TarotPosition {
6
+ number?: number;
7
+ label?: string;
8
+ name?: string;
9
+ position?: string;
10
+ card?: {
11
+ name?: string;
12
+ imageUrl?: string;
13
+ reversed?: boolean;
14
+ keywords?: string[];
15
+ arcana?: string;
16
+ };
17
+ interpretation?: string;
18
+ }
19
+
20
+ interface TarotSpreadData {
21
+ spread?: string;
22
+ positions?: TarotPosition[];
23
+ cards?: TarotPosition[];
24
+ reading?: string;
25
+ question?: string;
26
+ answer?: 'Yes' | 'No' | 'Maybe' | string;
27
+ strength?: string;
28
+ interpretation?: string;
29
+ }
30
+
31
+ /**
32
+ * Tarot spread card. Renders /tarot/spreads/{three-card,celtic-cross,love},
33
+ * /tarot/yes-no, /tarot/draw responses.
34
+ */
35
+ @customElement('roxy-tarot-spread')
36
+ export class RoxyTarotSpread extends LitElement {
37
+ static styles = [
38
+ baseStyles,
39
+ css`
40
+ .wrap {
41
+ display: grid;
42
+ gap: var(--roxy-space-md, 1rem);
43
+ }
44
+
45
+ .head {
46
+ display: flex;
47
+ justify-content: space-between;
48
+ gap: var(--roxy-space-md, 1rem);
49
+ flex-wrap: wrap;
50
+ align-items: baseline;
51
+ }
52
+ .title {
53
+ margin: 0;
54
+ font-size: var(--roxy-text-lg, 1.125rem);
55
+ font-weight: var(--roxy-weight-bold, 600);
56
+ text-transform: capitalize;
57
+ }
58
+ .question {
59
+ color: var(--roxy-muted, #71717a);
60
+ font-size: var(--roxy-text-sm, 0.875rem);
61
+ font-style: italic;
62
+ }
63
+
64
+ .answer {
65
+ display: inline-block;
66
+ padding: 4px 14px;
67
+ border-radius: var(--roxy-radius-full, 9999px);
68
+ font-weight: var(--roxy-weight-bold, 600);
69
+ font-size: var(--roxy-text-base, 1rem);
70
+ text-transform: uppercase;
71
+ letter-spacing: 0.06em;
72
+ }
73
+ .answer.yes {
74
+ background: color-mix(in srgb, var(--roxy-success, #16a34a) 16%, transparent);
75
+ color: var(--roxy-success, #16a34a);
76
+ }
77
+ .answer.no {
78
+ background: color-mix(in srgb, var(--roxy-danger, #dc2626) 16%, transparent);
79
+ color: var(--roxy-danger, #dc2626);
80
+ }
81
+ .answer.maybe {
82
+ background: color-mix(in srgb, var(--roxy-warning, #ea580c) 16%, transparent);
83
+ color: var(--roxy-warning, #ea580c);
84
+ }
85
+
86
+ .grid {
87
+ display: grid;
88
+ grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
89
+ gap: var(--roxy-space-md, 1rem);
90
+ }
91
+
92
+ .card {
93
+ border: 1px solid var(--roxy-border, #e4e4e7);
94
+ border-radius: var(--roxy-radius-md, 8px);
95
+ padding: var(--roxy-space-sm, 0.5rem);
96
+ background: var(--roxy-bg, #fff);
97
+ display: grid;
98
+ gap: var(--roxy-space-xs, 0.25rem);
99
+ }
100
+ .label {
101
+ font-size: var(--roxy-text-xs, 0.75rem);
102
+ color: var(--roxy-muted, #71717a);
103
+ text-transform: uppercase;
104
+ letter-spacing: 0.06em;
105
+ margin: 0;
106
+ }
107
+ .image {
108
+ width: 100%;
109
+ aspect-ratio: 0.6;
110
+ background: color-mix(in srgb, var(--roxy-border, #e4e4e7) 60%, transparent);
111
+ border-radius: var(--roxy-radius-sm, 4px);
112
+ display: flex;
113
+ align-items: center;
114
+ justify-content: center;
115
+ font-size: var(--roxy-text-xs, 0.75rem);
116
+ color: var(--roxy-muted, #71717a);
117
+ overflow: hidden;
118
+ }
119
+ .image img {
120
+ width: 100%;
121
+ height: 100%;
122
+ object-fit: cover;
123
+ transition:
124
+ transform var(--roxy-motion-duration, 200ms)
125
+ var(--roxy-motion-easing, cubic-bezier(0.4, 0, 0.2, 1));
126
+ }
127
+ .image img.reversed {
128
+ transform: rotate(180deg);
129
+ }
130
+ .name {
131
+ margin: 0;
132
+ font-size: var(--roxy-text-sm, 0.875rem);
133
+ font-weight: var(--roxy-weight-bold, 600);
134
+ }
135
+ .interp {
136
+ margin: 0;
137
+ font-size: var(--roxy-text-xs, 0.75rem);
138
+ color: var(--roxy-secondary, #475569);
139
+ }
140
+
141
+ .reading {
142
+ margin: 0;
143
+ color: var(--roxy-fg, #0a0a0a);
144
+ }
145
+ `,
146
+ ];
147
+
148
+ @property({ attribute: false })
149
+ data: TarotSpreadData | null = null;
150
+
151
+ @property({ type: String, reflect: true })
152
+ spread: 'three-card' | 'celtic-cross' | 'love' | 'yes-no' | 'draw' =
153
+ 'three-card';
154
+
155
+ render() {
156
+ const d = this.data;
157
+ if (!d)
158
+ return html`<div class="roxy-empty" role="status">No tarot spread</div>`;
159
+
160
+ const positions = d.positions ?? d.cards ?? [];
161
+ const isYesNo = !!d.answer;
162
+ const answerClass = isYesNo
163
+ ? (d.answer ?? '').toLowerCase().replace(/[^a-z]/g, '')
164
+ : '';
165
+
166
+ return html`<article class="wrap" aria-label="Tarot spread">
167
+ <header class="head">
168
+ <h2 class="title">${d.spread ?? this.spread.replace(/-/g, ' ')}</h2>
169
+ ${d.question ? html`<span class="question">"${d.question}"</span>` : nothing}
170
+ </header>
171
+ ${
172
+ isYesNo
173
+ ? html`<div>
174
+ <span class=${`answer ${answerClass}`}>${d.answer}</span>
175
+ ${d.strength ? html`<small> · ${d.strength}</small>` : nothing}
176
+ </div>`
177
+ : nothing
178
+ }
179
+ ${
180
+ positions.length > 0
181
+ ? html`<div class="grid">
182
+ ${positions.map(
183
+ (p) => html`<div class="card">
184
+ <p class="label">${p.label ?? p.name ?? p.position ?? ''}</p>
185
+ <div class="image">
186
+ ${
187
+ p.card?.imageUrl
188
+ ? html`<img
189
+ src=${p.card.imageUrl}
190
+ alt=${p.card.name ?? 'tarot card'}
191
+ class=${p.card.reversed ? 'reversed' : ''}
192
+ />`
193
+ : html`${p.card?.name ?? '?'}`
194
+ }
195
+ </div>
196
+ <p class="name">
197
+ ${p.card?.name ?? ''}
198
+ ${p.card?.reversed ? html`<small>(reversed)</small>` : nothing}
199
+ </p>
200
+ ${
201
+ p.interpretation
202
+ ? html`<p class="interp">${p.interpretation}</p>`
203
+ : nothing
204
+ }
205
+ </div>`,
206
+ )}
207
+ </div>`
208
+ : nothing
209
+ }
210
+ ${d.reading ? html`<p class="reading">${d.reading}</p>` : nothing}
211
+ ${
212
+ d.interpretation && !d.reading
213
+ ? html`<p class="reading">${d.interpretation}</p>`
214
+ : nothing
215
+ }
216
+ </article>`;
217
+ }
218
+ }
219
+
220
+ declare global {
221
+ interface HTMLElementTagNameMap {
222
+ 'roxy-tarot-spread': RoxyTarotSpread;
223
+ }
224
+ }
@@ -0,0 +1,257 @@
1
+ import { css, html, LitElement, nothing, svg } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { PLANET_ABBR, SIGN_ABBR } from '../tokens/index.js';
4
+ import { baseStyles } from '../utils/base-styles.js';
5
+
6
+ interface KundliMeta {
7
+ [planet: string]: {
8
+ graha?: string;
9
+ rashi?: string;
10
+ longitude?: number;
11
+ nakshatra?: string;
12
+ isRetrograde?: boolean;
13
+ };
14
+ }
15
+
16
+ interface RashiBucket {
17
+ rashi?: string;
18
+ signs?: Array<{
19
+ planet?: string;
20
+ longitude?: number;
21
+ isRetrograde?: boolean;
22
+ }>;
23
+ }
24
+
25
+ interface KundliHouse {
26
+ house: number;
27
+ sign: string;
28
+ planets: string[];
29
+ }
30
+
31
+ interface KundliData {
32
+ meta?: KundliMeta;
33
+ houses?: Array<{
34
+ house?: number;
35
+ number?: number;
36
+ sign?: string;
37
+ planets?: string[];
38
+ }>;
39
+ combustion?: unknown[];
40
+ planetaryWar?: unknown[];
41
+ [rashi: string]: unknown;
42
+ }
43
+
44
+ const SOUTH_HOUSE_CENTERS: Record<number, { x: number; y: number }> = {
45
+ 1: { x: 150, y: 58 },
46
+ 2: { x: 205, y: 52 },
47
+ 3: { x: 253, y: 112 },
48
+ 4: { x: 243, y: 150 },
49
+ 5: { x: 253, y: 188 },
50
+ 6: { x: 205, y: 248 },
51
+ 7: { x: 150, y: 242 },
52
+ 8: { x: 95, y: 248 },
53
+ 9: { x: 47, y: 188 },
54
+ 10: { x: 57, y: 150 },
55
+ 11: { x: 47, y: 112 },
56
+ 12: { x: 95, y: 52 },
57
+ };
58
+
59
+ const SOUTH_SIGN_POSITIONS: Record<number, { x: number; y: number }> = {
60
+ 1: { x: 150, y: 35 },
61
+ 2: { x: 222, y: 40 },
62
+ 3: { x: 265, y: 100 },
63
+ 4: { x: 265, y: 150 },
64
+ 5: { x: 265, y: 200 },
65
+ 6: { x: 222, y: 260 },
66
+ 7: { x: 150, y: 265 },
67
+ 8: { x: 78, y: 260 },
68
+ 9: { x: 35, y: 200 },
69
+ 10: { x: 35, y: 150 },
70
+ 11: { x: 35, y: 100 },
71
+ 12: { x: 78, y: 40 },
72
+ };
73
+
74
+ const RASHI_KEYS = [
75
+ 'aries',
76
+ 'taurus',
77
+ 'gemini',
78
+ 'cancer',
79
+ 'leo',
80
+ 'virgo',
81
+ 'libra',
82
+ 'scorpio',
83
+ 'sagittarius',
84
+ 'capricorn',
85
+ 'aquarius',
86
+ 'pisces',
87
+ ] as const;
88
+
89
+ const RASHI_TO_SIGN: Record<string, string> = {
90
+ aries: 'Aries',
91
+ taurus: 'Taurus',
92
+ gemini: 'Gemini',
93
+ cancer: 'Cancer',
94
+ leo: 'Leo',
95
+ virgo: 'Virgo',
96
+ libra: 'Libra',
97
+ scorpio: 'Scorpio',
98
+ sagittarius: 'Sagittarius',
99
+ capricorn: 'Capricorn',
100
+ aquarius: 'Aquarius',
101
+ pisces: 'Pisces',
102
+ };
103
+
104
+ /**
105
+ * Vedic kundli (D1 Rashi chart). South Indian style by default. Pass `data`
106
+ * from /vedic-astrology/birth-chart. North Indian style via style="north".
107
+ *
108
+ * Lifted from jyotish-vedic-astrology-app/src/components/birth-chart.tsx,
109
+ * keeping HOUSE_CENTERS + SIGN_POSITIONS + abbreviations, dropping the React
110
+ * DOM color-probing hook in favor of CSS custom properties on :host.
111
+ */
112
+ @customElement('roxy-vedic-kundli')
113
+ export class RoxyVedicKundli extends LitElement {
114
+ static styles = [
115
+ baseStyles,
116
+ css`
117
+ .wrap {
118
+ display: grid;
119
+ gap: var(--roxy-space-md, 1rem);
120
+ }
121
+ .title {
122
+ font-size: var(--roxy-text-lg, 1.125rem);
123
+ font-weight: var(--roxy-weight-bold, 600);
124
+ margin: 0;
125
+ }
126
+ svg {
127
+ display: block;
128
+ width: 100%;
129
+ max-width: 360px;
130
+ margin: 0 auto;
131
+ }
132
+ .line {
133
+ fill: transparent;
134
+ stroke: var(--roxy-border, #e4e4e7);
135
+ }
136
+ .sign-text {
137
+ fill: var(--roxy-muted, #71717a);
138
+ font-size: 9px;
139
+ font-weight: 500;
140
+ font-family: var(--roxy-font-sans);
141
+ }
142
+ .planet-text {
143
+ fill: var(--roxy-fg, #0a0a0a);
144
+ font-size: 11px;
145
+ font-weight: 600;
146
+ font-family: var(--roxy-font-sans);
147
+ }
148
+ `,
149
+ ];
150
+
151
+ @property({ attribute: false })
152
+ data: KundliData | null = null;
153
+
154
+ @property({ type: String, reflect: true, attribute: 'chart-style' })
155
+ chartStyle: 'south' | 'north' = 'south';
156
+
157
+ private buildHouses(): KundliHouse[] {
158
+ if (!this.data) return [];
159
+ const houses: KundliHouse[] = [];
160
+ // Prefer normalized .houses array if present
161
+ if (Array.isArray(this.data.houses)) {
162
+ for (const h of this.data.houses) {
163
+ houses.push({
164
+ house: (h.house ?? h.number ?? houses.length + 1) as number,
165
+ sign: h.sign ?? '',
166
+ planets: h.planets ?? [],
167
+ });
168
+ }
169
+ if (houses.length > 0) return houses;
170
+ }
171
+ // Otherwise read the rashi buckets and project them as houses 1..12
172
+ // keyed by sign order. Lagna-anchored ordering would require knowing
173
+ // the ascendant rashi; we render rashi buckets directly which is the
174
+ // canonical South Indian layout.
175
+ for (let i = 0; i < 12; i++) {
176
+ const key = RASHI_KEYS[i];
177
+ const bucket = this.data[key] as RashiBucket | undefined;
178
+ const planets = (bucket?.signs ?? [])
179
+ .map((p) => p.planet ?? '')
180
+ .filter(Boolean);
181
+ houses.push({
182
+ house: i + 1,
183
+ sign: RASHI_TO_SIGN[key] ?? '',
184
+ planets,
185
+ });
186
+ }
187
+ return houses;
188
+ }
189
+
190
+ render() {
191
+ if (!this.data)
192
+ return html`<div class="roxy-empty" role="status">No kundli data</div>`;
193
+ const houses = this.buildHouses();
194
+
195
+ return html`<div class="wrap">
196
+ <h2 class="title">Vedic kundli</h2>
197
+ <svg
198
+ viewBox="0 0 300 300"
199
+ role="img"
200
+ aria-label="Vedic birth chart with twelve sign houses"
201
+ >
202
+ <title>Vedic kundli</title>
203
+ <polygon class="line" points="150,10 290,150 150,290 10,150" stroke-width="1.5" />
204
+ <polygon
205
+ class="line"
206
+ points="220,80 220,220 80,220 80,80"
207
+ stroke-width="1"
208
+ fill="none"
209
+ />
210
+ <line class="line" x1="150" y1="10" x2="80" y2="80" stroke-width="1" />
211
+ <line class="line" x1="150" y1="10" x2="220" y2="80" stroke-width="1" />
212
+ <line class="line" x1="290" y1="150" x2="220" y2="80" stroke-width="1" />
213
+ <line class="line" x1="290" y1="150" x2="220" y2="220" stroke-width="1" />
214
+ <line class="line" x1="150" y1="290" x2="220" y2="220" stroke-width="1" />
215
+ <line class="line" x1="150" y1="290" x2="80" y2="220" stroke-width="1" />
216
+ <line class="line" x1="10" y1="150" x2="80" y2="220" stroke-width="1" />
217
+ <line class="line" x1="10" y1="150" x2="80" y2="80" stroke-width="1" />
218
+ ${houses.map((h) => this.renderHouseGroup(h))}
219
+ </svg>
220
+ </div>`;
221
+ }
222
+
223
+ private renderHouseGroup(h: KundliHouse) {
224
+ const center = SOUTH_HOUSE_CENTERS[h.house];
225
+ const signPos = SOUTH_SIGN_POSITIONS[h.house];
226
+ if (!center || !signPos) return nothing;
227
+ const signAbbr = SIGN_ABBR[h.sign] ?? '';
228
+ const planets = h.planets ?? [];
229
+ return svg`
230
+ <g>
231
+ ${
232
+ signAbbr
233
+ ? svg`<text class="sign-text" x=${signPos.x} y=${signPos.y} text-anchor="middle" dominant-baseline="central">${signAbbr}</text>`
234
+ : nothing
235
+ }
236
+ ${planets.map((planet, j) => {
237
+ const abbr = PLANET_ABBR[capitalize(planet)] ?? planet.slice(0, 2);
238
+ const lineHeight = 13;
239
+ const startY = center.y - ((planets.length - 1) * lineHeight) / 2;
240
+ const yPos = startY + j * lineHeight;
241
+ return svg`<text class="planet-text" x=${center.x} y=${yPos} text-anchor="middle" dominant-baseline="central">${abbr}</text>`;
242
+ })}
243
+ </g>
244
+ `;
245
+ }
246
+ }
247
+
248
+ function capitalize(s: string): string {
249
+ if (!s) return '';
250
+ return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
251
+ }
252
+
253
+ declare global {
254
+ interface HTMLElementTagNameMap {
255
+ 'roxy-vedic-kundli': RoxyVedicKundli;
256
+ }
257
+ }