@roxyapi/ui 0.12.1 → 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -866,11 +866,11 @@ export type TransitsRequest = {
866
866
  */
867
867
  natalChart?: {
868
868
  /**
869
- * Date in YYYY-MM-DD format.
869
+ * Date in YYYY-MM-DD format. A single-digit month or day is accepted and zero-padded (2026-3-5 becomes 2026-03-05). Impossible calendar dates are rejected.
870
870
  */
871
871
  date: string;
872
872
  /**
873
- * Time in 24-hour HH:MM:SS format.
873
+ * Time in 24-hour format. Seconds are optional and default to 00 (14:30 becomes 14:30:00); a single-digit hour is zero-padded. Out-of-range values are rejected.
874
874
  */
875
875
  time: string;
876
876
  latitude: number;
@@ -2749,7 +2749,7 @@ export type BirthChartResponse = {
2749
2749
  };
2750
2750
  };
2751
2751
  /**
2752
- * Twelve classical yogas detected against this chart: Gajakesari (three-rule parashara definition), Sunapha, Anapha, Dhurdhura, Kemadruma, Chandra Mangala, Budha-Aditya, and the five Pancha Mahapurusha (Ruchaka, Bhadra, Hamsa, Malavya, Sasa). Each entry carries an `id` (matches `GET /yoga/{id}` for full glossary lookup), a `present` boolean, and classical-text `evidence` for the rule that triggered or failed.
2752
+ * Twelve classical yogas detected against this chart: Gajakesari (three-rule parashara definition), Sunapha, Anapha, Dhurdhura, Kemadruma, Chandra Mangala, Budha-Aditya, and the five Pancha Mahapurusha (Ruchaka, Bhadra, Hamsa, Malavya, Sasa). Each entry carries an `id` (matches `GET /yoga/{id}` for full glossary lookup), a `present` boolean, a `quality` (Positive, Negative, or Both = auspicious, inauspicious, or context-dependent), and classical-text `evidence` for the rule that triggered or failed.
2753
2753
  */
2754
2754
  yogas?: Array<{
2755
2755
  /**
@@ -3586,7 +3586,7 @@ export type SadhesatiRequest = {
3586
3586
  };
3587
3587
  export type YogaDetectResponse = {
3588
3588
  /**
3589
- * Array of 12 detected yogas. Every entry carries a present boolean; filter on present === true for active yogas. Evidence text names the rule that triggered or failed.
3589
+ * Array of 12 detected yogas. Every entry carries a `present` boolean and a `quality` (Positive, Negative, or Both = auspicious, inauspicious, or context-dependent); filter on present === true for active yogas. Evidence text names the rule that triggered or failed.
3590
3590
  */
3591
3591
  yogas: Array<{
3592
3592
  /**
@@ -4703,11 +4703,11 @@ export type KpPlanetsIntervalResponse = {
4703
4703
  };
4704
4704
  export type KpPlanetsIntervalRequest = {
4705
4705
  /**
4706
- * Start datetime in ISO 8601 format. Always interpreted as local time when a non-zero timezone is provided (Z suffix is ignored). With timezone 0, Z suffix is treated as UTC.
4706
+ * Start datetime in ISO 8601 (YYYY-MM-DDTHH:MM:SS). Interpreted as local time when a non-zero timezone is provided (a trailing Z is accepted but ignored); with timezone 0 it is UTC.
4707
4707
  */
4708
4708
  startDatetime: string;
4709
4709
  /**
4710
- * End datetime in ISO 8601 format. Maximum 7 days from start. Always interpreted as local time when a non-zero timezone is provided (Z suffix is ignored).
4710
+ * End datetime in ISO 8601 (YYYY-MM-DDTHH:MM:SS). Maximum 7 days from start. Interpreted as local time when a non-zero timezone is provided (a trailing Z is accepted but ignored); with timezone 0 it is UTC.
4711
4711
  */
4712
4712
  endDatetime: string;
4713
4713
  /**
@@ -14042,7 +14042,7 @@ export type GetBasicPanchangResponse = GetBasicPanchangResponses[keyof GetBasicP
14042
14042
  export type GetDetailedPanchangData = {
14043
14043
  body?: {
14044
14044
  /**
14045
- * Date in YYYY-MM-DD format.
14045
+ * Date in YYYY-MM-DD format. A single-digit month or day is accepted and zero-padded (2026-3-5 becomes 2026-03-05). Impossible calendar dates are rejected.
14046
14046
  */
14047
14047
  date: string;
14048
14048
  /**
@@ -14596,7 +14596,7 @@ export type GetDetailedPanchangResponses = {
14596
14596
  */
14597
14597
  panchaka: {
14598
14598
  /**
14599
- * Whether Panchaka is in force at sunrise, the panchang day reference. Can be false while startsAt and endsAt are populated when Panchaka begins later in the day.
14599
+ * True when Panchaka is in effect on this date, whether it is already running at sunrise or begins later in the day, in which case startsAt and endsAt give the window. False only when no Panchaka touches this date.
14600
14600
  */
14601
14601
  active: boolean;
14602
14602
  /**
@@ -14613,11 +14613,11 @@ export type GetDetailedPanchangResponses = {
14613
14613
  endsAt: string;
14614
14614
  };
14615
14615
  /**
14616
- * Bhadra (Vishti Karana), the 7th movable karana, avoided for all auspicious activities. Bhadra recurs roughly every 3 to 5 days and lasts about half a tithi, so most occurrences begin after sunrise rather than at it. startsAt and endsAt report the Bhadra beginning on this date regardless of whether it is active at sunrise.
14616
+ * Bhadra (Vishti Karana), the 7th movable karana, avoided for all auspicious activities. Bhadra recurs roughly every 3 to 5 days and lasts about half a tithi. active is true whenever a Bhadra is attributed to this date; startsAt and endsAt give the window, which may end on the next calendar day.
14617
14617
  */
14618
14618
  bhadra: {
14619
14619
  /**
14620
- * Whether Bhadra (Vishti Karana) is in force at sunrise, the panchang day reference moment. Can be false while startsAt and endsAt are populated when Bhadra begins later in the day.
14620
+ * True when a Bhadra (Vishti Karana) occurs on this date, in which case startsAt and endsAt give its window. False only when no Bhadra begins on this date.
14621
14621
  */
14622
14622
  active: boolean;
14623
14623
  /**
@@ -14713,7 +14713,7 @@ export type GetDetailedPanchangResponse = GetDetailedPanchangResponses[keyof Get
14713
14713
  export type GetChoghadiyaData = {
14714
14714
  body?: {
14715
14715
  /**
14716
- * Date in YYYY-MM-DD format.
14716
+ * Date in YYYY-MM-DD format. A single-digit month or day is accepted and zero-padded (2026-3-5 becomes 2026-03-05). Impossible calendar dates are rejected.
14717
14717
  */
14718
14718
  date: string;
14719
14719
  /**
@@ -14897,7 +14897,7 @@ export type GetChoghadiyaResponse = GetChoghadiyaResponses[keyof GetChoghadiyaRe
14897
14897
  export type GetHoraData = {
14898
14898
  body?: {
14899
14899
  /**
14900
- * Date in YYYY-MM-DD format.
14900
+ * Date in YYYY-MM-DD format. A single-digit month or day is accepted and zero-padded (2026-3-5 becomes 2026-03-05). Impossible calendar dates are rejected.
14901
14901
  */
14902
14902
  date: string;
14903
14903
  /**
@@ -16320,7 +16320,7 @@ export type GetKpRulingPlanetsData = {
16320
16320
  */
16321
16321
  timezone?: number | string;
16322
16322
  /**
16323
- * ISO 8601 datetime for ruling planets. Defaults to current time. Always interpreted as local time when a non-zero timezone is provided (Z suffix is ignored).
16323
+ * ISO 8601 datetime (YYYY-MM-DDTHH:MM:SS) for ruling planets. Defaults to current time. Interpreted as local time when a non-zero timezone is provided (a trailing Z is accepted but ignored); with timezone 0 it is UTC.
16324
16324
  */
16325
16325
  datetime?: string;
16326
16326
  /**
@@ -16452,11 +16452,11 @@ export type GetKpRulingPlanetsResponse = GetKpRulingPlanetsResponses[keyof GetKp
16452
16452
  export type GetKpRulingIntervalData = {
16453
16453
  body?: {
16454
16454
  /**
16455
- * Start of the interval range in ISO 8601 format. Always interpreted as local time when a non-zero timezone is provided (Z suffix is ignored).
16455
+ * Start of the interval range in ISO 8601 (YYYY-MM-DDTHH:MM:SS). Interpreted as local time when a non-zero timezone is provided (a trailing Z is accepted but ignored); with timezone 0 it is UTC.
16456
16456
  */
16457
16457
  startDatetime: string;
16458
16458
  /**
16459
- * End of the interval range in ISO 8601 format. Always interpreted as local time when a non-zero timezone is provided (Z suffix is ignored).
16459
+ * End of the interval range in ISO 8601 (YYYY-MM-DDTHH:MM:SS). Interpreted as local time when a non-zero timezone is provided (a trailing Z is accepted but ignored); with timezone 0 it is UTC.
16460
16460
  */
16461
16461
  endDatetime: string;
16462
16462
  /**
@@ -21156,7 +21156,7 @@ export type CalculatePersonalDayData = {
21156
21156
  */
21157
21157
  day: number;
21158
21158
  /**
21159
- * Target date in YYYY-MM-DD format (defaults to today)
21159
+ * Target date in YYYY-MM-DD format. Defaults to today (UTC).
21160
21160
  */
21161
21161
  targetDate?: string;
21162
21162
  };
@@ -33097,7 +33097,7 @@ export type GetCrystalPairingsData = {
33097
33097
  body?: never;
33098
33098
  path: {
33099
33099
  /**
33100
- * URL-safe crystal identifier to find pairings for (e.g., "amethyst", "rose-quartz").
33100
+ * URL-safe crystal identifier to find pairings for, case-insensitive (e.g., "amethyst", "Amethyst", "rose-quartz" all resolve).
33101
33101
  */
33102
33102
  id: string;
33103
33103
  };
@@ -34026,7 +34026,7 @@ export type GetCrystalData = {
34026
34026
  body?: never;
34027
34027
  path: {
34028
34028
  /**
34029
- * URL-safe crystal identifier (e.g., "amethyst", "rose-quartz", "black-tourmaline"). Must match an entry in the database.
34029
+ * URL-safe crystal identifier, case-insensitive (e.g., "amethyst", "Amethyst", "rose-quartz" all resolve). Must match an entry in the database.
34030
34030
  */
34031
34031
  id: string;
34032
34032
  };
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const ROXY_UI_VERSION = "0.12.1";
1
+ export declare const ROXY_UI_VERSION = "0.12.3";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roxyapi/ui",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
4
4
  "description": "Web components for the RoxyAPI catalog. Drop-in charts, tables, cards, forms for astrology, tarot, numerology, biorhythm, I Ching, crystals, dreams, angel numbers, and more. One key, beautiful in 30 minutes.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -2,6 +2,7 @@ import { css, html, nothing, type TemplateResult } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
3
  import { RoxyDataElement } from '../utils/base-element.js';
4
4
  import { baseStyles } from '../utils/base-styles.js';
5
+ import { formatDate, formatNumber, formatTime } from '../utils/format.js';
5
6
  import { humanize } from '../utils/string.js';
6
7
 
7
8
  /**
@@ -13,19 +14,31 @@ import { humanize } from '../utils/string.js';
13
14
  * 1. Primitive (string, number, boolean) -> single line.
14
15
  * 2. Array of primitives -> chip list.
15
16
  * 3. Array of objects with shared keys -> table.
16
- * 4. Object with title-like field -> card with key/value rows.
17
+ * 4. Object with title-like field -> card with key/value rows; object and
18
+ * object-array values promote to full-width sections below the rows so
19
+ * nested tables keep the whole container width.
17
20
  * 5. Otherwise -> definition list of all keys.
18
21
  *
22
+ * Scalar display rules (shared by rows, chips, and table cells): numbers
23
+ * round to 2 decimals, booleans read Yes/No, ISO dates and datetimes format
24
+ * for the locale, SCREAMING_SNAKE enums humanize, http(s) strings link out.
25
+ *
19
26
  * When a schema declares an `x-roxy-ui` hint, a future dispatcher can opt
20
27
  * into a hand-tuned component instead of this fallback.
21
28
  */
22
29
 
23
30
  type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
31
+ type Scalar = string | number | boolean;
24
32
 
25
33
  const TITLE_KEYS = ['title', 'name', 'label', 'heading', 'overview', 'summary'];
26
34
  const IMAGE_KEYS = ['imageUrl', 'image', 'icon', 'symbol'];
27
35
  const SKIP_KEYS = ['imageUrl', 'image']; // rendered separately, not in body rows
28
36
 
37
+ const ISO_DATE =
38
+ /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?)?$/;
39
+ const ENUM_STRING = /^[A-Z0-9]+(_[A-Z0-9]+)+$/;
40
+ const LINK_STRING = /^https?:\/\//;
41
+
29
42
  // Hard cap on recursion. Real RoxyAPI responses nest at most 5-6 deep; anything
30
43
  // deeper is either a circular reference (which would otherwise infinite-loop)
31
44
  // or a payload too rich for the generic fallback to render usefully. The
@@ -34,6 +47,21 @@ const SKIP_KEYS = ['imageUrl', 'image']; // rendered separately, not in body row
34
47
  // this same class without a separate import.
35
48
  const MAX_DEPTH = 6;
36
49
 
50
+ function isPrimitive(value: Json | undefined): value is Scalar | null {
51
+ return (
52
+ value === null || ['string', 'number', 'boolean'].includes(typeof value)
53
+ );
54
+ }
55
+
56
+ /** Object or object-bearing array: needs full width, never fits a dl value cell. */
57
+ function isComplex(value: Json): boolean {
58
+ return (
59
+ value !== null &&
60
+ typeof value === 'object' &&
61
+ !(Array.isArray(value) && value.every(isPrimitive))
62
+ );
63
+ }
64
+
37
65
  @customElement('roxy-data')
38
66
  export class RoxyData extends RoxyDataElement<Json> {
39
67
  static styles = [
@@ -47,6 +75,10 @@ export class RoxyData extends RoxyDataElement<Json> {
47
75
  box-shadow: var(--roxy-shadow-sm);
48
76
  }
49
77
 
78
+ .roxy-card a {
79
+ color: var(--roxy-accent-ink, #b45309);
80
+ }
81
+
50
82
  .roxy-title {
51
83
  font-size: var(--roxy-text-lg, 1.125rem);
52
84
  font-weight: var(--roxy-weight-bold, 600);
@@ -95,6 +127,9 @@ export class RoxyData extends RoxyDataElement<Json> {
95
127
  font-size: var(--roxy-text-xs, 0.75rem);
96
128
  }
97
129
 
130
+ .roxy-table-wrap {
131
+ overflow-x: auto;
132
+ }
98
133
  table.roxy-table {
99
134
  width: 100%;
100
135
  border-collapse: collapse;
@@ -106,6 +141,8 @@ export class RoxyData extends RoxyDataElement<Json> {
106
141
  padding: var(--roxy-space-sm, 0.5rem);
107
142
  text-align: left;
108
143
  text-transform: none;
144
+ word-break: normal;
145
+ overflow-wrap: normal;
109
146
  }
110
147
  table.roxy-table th {
111
148
  color: var(--roxy-muted, #71717a);
@@ -113,6 +150,7 @@ export class RoxyData extends RoxyDataElement<Json> {
113
150
  text-transform: capitalize;
114
151
  font-size: var(--roxy-text-xs, 0.75rem);
115
152
  letter-spacing: 0.04em;
153
+ white-space: nowrap;
116
154
  }
117
155
 
118
156
  .roxy-image {
@@ -123,7 +161,7 @@ export class RoxyData extends RoxyDataElement<Json> {
123
161
  }
124
162
 
125
163
  .roxy-section {
126
- margin-bottom: var(--roxy-space-md, 1rem);
164
+ margin-top: var(--roxy-space-md, 1rem);
127
165
  }
128
166
  .roxy-section h4 {
129
167
  font-size: var(--roxy-text-sm, 0.875rem);
@@ -143,7 +181,9 @@ export class RoxyData extends RoxyDataElement<Json> {
143
181
 
144
182
  protected renderData(data: Json) {
145
183
  if (this.depth >= MAX_DEPTH) {
146
- return html`<div class="roxy-empty" role="status">…</div>`;
184
+ return html`<div class="roxy-empty" role="status">
185
+ Nested data omitted
186
+ </div>`;
147
187
  }
148
188
  return html`<div class="roxy-card" aria-label="Generic data display">
149
189
  ${this.renderValue(data)}
@@ -152,10 +192,7 @@ export class RoxyData extends RoxyDataElement<Json> {
152
192
 
153
193
  private renderValue(value: Json): TemplateResult | typeof nothing {
154
194
  if (value === null || value === undefined) return nothing;
155
- if (typeof value === 'string') return html`<p>${value}</p>`;
156
- if (typeof value === 'number' || typeof value === 'boolean') {
157
- return html`<p>${String(value)}</p>`;
158
- }
195
+ if (isPrimitive(value)) return html`<p>${this.scalarTemplate(value)}</p>`;
159
196
  if (Array.isArray(value)) return this.renderArray(value);
160
197
  return this.renderObject(value as Record<string, Json>);
161
198
  }
@@ -164,13 +201,8 @@ export class RoxyData extends RoxyDataElement<Json> {
164
201
  if (arr.length === 0) {
165
202
  return html`<div class="roxy-empty" role="status">Empty list</div>`;
166
203
  }
167
- const allPrimitive = arr.every(
168
- (v) => v === null || ['string', 'number', 'boolean'].includes(typeof v),
169
- );
170
- if (allPrimitive) {
171
- return html`<ul class="roxy-chips">
172
- ${arr.map((v) => html`<li>${String(v)}</li>`)}
173
- </ul>`;
204
+ if (arr.every(isPrimitive)) {
205
+ return this.renderChips(arr as (Scalar | null)[]);
174
206
  }
175
207
  const allObjects = arr.every(
176
208
  (v) => v !== null && typeof v === 'object' && !Array.isArray(v),
@@ -181,22 +213,37 @@ export class RoxyData extends RoxyDataElement<Json> {
181
213
  </ol>`;
182
214
  }
183
215
 
216
+ private renderChips(arr: (Scalar | null)[]): TemplateResult {
217
+ return html`<ul class="roxy-chips">
218
+ ${arr
219
+ .filter((v): v is Scalar => v !== null)
220
+ .map((v) => html`<li>${this.formatScalar(v)}</li>`)}
221
+ </ul>`;
222
+ }
223
+
184
224
  private renderTable(rows: Record<string, Json>[]): TemplateResult {
185
225
  const keys = this.collectKeys(rows);
186
- return html`<table class="roxy-table" role="table">
187
- <thead>
188
- <tr>
189
- ${keys.map((k) => html`<th>${humanize(k)}</th>`)}
190
- </tr>
191
- </thead>
192
- <tbody>
193
- ${rows.map(
194
- (row) => html`<tr>
195
- ${keys.map((k) => html`<td>${this.formatPrimitive(row[k])}</td>`)}
196
- </tr>`,
197
- )}
198
- </tbody>
199
- </table>`;
226
+ return html`<div
227
+ class="roxy-table-wrap"
228
+ role="group"
229
+ aria-label="Data table"
230
+ tabindex="0"
231
+ >
232
+ <table class="roxy-table" role="table">
233
+ <thead>
234
+ <tr>
235
+ ${keys.map((k) => html`<th>${humanize(k)}</th>`)}
236
+ </tr>
237
+ </thead>
238
+ <tbody>
239
+ ${rows.map(
240
+ (row) => html`<tr>
241
+ ${keys.map((k) => html`<td>${this.renderCell(row[k])}</td>`)}
242
+ </tr>`,
243
+ )}
244
+ </tbody>
245
+ </table>
246
+ </div>`;
200
247
  }
201
248
 
202
249
  private renderObject(obj: Record<string, Json>): TemplateResult {
@@ -209,7 +256,7 @@ export class RoxyData extends RoxyDataElement<Json> {
209
256
  titleKey !== 'summary' && typeof obj.summary === 'string'
210
257
  ? 'summary'
211
258
  : null;
212
- const rows = Object.entries(obj).filter(
259
+ const entries = Object.entries(obj).filter(
213
260
  ([k, v]) =>
214
261
  k !== titleKey &&
215
262
  k !== summaryKey &&
@@ -217,6 +264,11 @@ export class RoxyData extends RoxyDataElement<Json> {
217
264
  v !== null &&
218
265
  v !== undefined,
219
266
  );
267
+ // Scalars and primitive arrays fit the two-column rows; objects and
268
+ // object arrays promote to full-width sections so nested tables are
269
+ // not squeezed into the value column.
270
+ const rows = entries.filter(([, v]) => !isComplex(v));
271
+ const sections = entries.filter(([, v]) => isComplex(v));
220
272
 
221
273
  return html`
222
274
  ${
@@ -243,34 +295,64 @@ export class RoxyData extends RoxyDataElement<Json> {
243
295
  </dl>`
244
296
  : nothing
245
297
  }
298
+ ${sections.map(
299
+ ([k, v]) => html`<div class="roxy-section">
300
+ <h4>${humanize(k)}</h4>
301
+ ${this.renderField(v)}
302
+ </div>`,
303
+ )}
246
304
  `;
247
305
  }
248
306
 
249
307
  private renderField(value: Json): TemplateResult | string {
250
308
  if (value === null || value === undefined) return '';
251
- if (typeof value === 'string') return value;
252
- if (typeof value === 'number' || typeof value === 'boolean')
253
- return String(value);
254
- if (Array.isArray(value)) {
255
- const allPrimitive = value.every((v) =>
256
- ['string', 'number', 'boolean'].includes(typeof v),
257
- );
258
- if (allPrimitive) {
259
- return html`<ul class="roxy-chips">
260
- ${value.map((v) => html`<li>${String(v)}</li>`)}
261
- </ul>`;
262
- }
309
+ if (isPrimitive(value)) return html`${this.scalarTemplate(value)}`;
310
+ if (Array.isArray(value) && value.every(isPrimitive)) {
311
+ return this.renderChips(value as (Scalar | null)[]);
263
312
  }
264
313
  return html`<roxy-data .data=${value} .depth=${this.depth + 1}></roxy-data>`;
265
314
  }
266
315
 
267
- private formatPrimitive(value: Json | undefined): string {
316
+ private renderCell(value: Json | undefined): TemplateResult | string {
268
317
  if (value === null || value === undefined) return '';
269
- if (typeof value === 'string') return value;
270
- if (typeof value === 'number' || typeof value === 'boolean')
271
- return String(value);
272
- if (Array.isArray(value)) return value.map(String).join(', ');
273
- return JSON.stringify(value);
318
+ if (isPrimitive(value)) return this.scalarTemplate(value);
319
+ if (Array.isArray(value) && value.every(isPrimitive)) {
320
+ return (value as (Scalar | null)[])
321
+ .filter((v): v is Scalar => v !== null)
322
+ .map((v) => this.formatScalar(v))
323
+ .join(', ');
324
+ }
325
+ // Nested object or object array inside a row: recurse instead of
326
+ // stringifying to [object Object]. `bare` drops the card surface so
327
+ // the cell does not paint a card inside a cell.
328
+ return html`<roxy-data
329
+ bare
330
+ .data=${value}
331
+ .depth=${this.depth + 1}
332
+ ></roxy-data>`;
333
+ }
334
+
335
+ private scalarTemplate(value: Scalar): TemplateResult | string {
336
+ if (typeof value === 'string' && LINK_STRING.test(value)) {
337
+ return html`<a href=${value} target="_blank" rel="noopener noreferrer"
338
+ >${value}</a
339
+ >`;
340
+ }
341
+ return this.formatScalar(value);
342
+ }
343
+
344
+ private formatScalar(value: Scalar): string {
345
+ if (typeof value === 'number') {
346
+ return formatNumber(value, 2) || String(value);
347
+ }
348
+ if (typeof value === 'boolean') return value ? 'Yes' : 'No';
349
+ if (ISO_DATE.test(value)) {
350
+ const time = formatTime(value);
351
+ const date = formatDate(value);
352
+ return time ? `${date}, ${time}` : date;
353
+ }
354
+ if (ENUM_STRING.test(value)) return humanize(value.toLowerCase());
355
+ return value;
274
356
  }
275
357
 
276
358
  private collectKeys(rows: Record<string, Json>[]): string[] {
@@ -877,11 +877,11 @@ export type TransitsRequest = {
877
877
  */
878
878
  natalChart?: {
879
879
  /**
880
- * Date in YYYY-MM-DD format.
880
+ * Date in YYYY-MM-DD format. A single-digit month or day is accepted and zero-padded (2026-3-5 becomes 2026-03-05). Impossible calendar dates are rejected.
881
881
  */
882
882
  date: string;
883
883
  /**
884
- * Time in 24-hour HH:MM:SS format.
884
+ * Time in 24-hour format. Seconds are optional and default to 00 (14:30 becomes 14:30:00); a single-digit hour is zero-padded. Out-of-range values are rejected.
885
885
  */
886
886
  time: string;
887
887
  latitude: number;
@@ -2779,7 +2779,7 @@ export type BirthChartResponse = {
2779
2779
  };
2780
2780
  };
2781
2781
  /**
2782
- * Twelve classical yogas detected against this chart: Gajakesari (three-rule parashara definition), Sunapha, Anapha, Dhurdhura, Kemadruma, Chandra Mangala, Budha-Aditya, and the five Pancha Mahapurusha (Ruchaka, Bhadra, Hamsa, Malavya, Sasa). Each entry carries an `id` (matches `GET /yoga/{id}` for full glossary lookup), a `present` boolean, and classical-text `evidence` for the rule that triggered or failed.
2782
+ * Twelve classical yogas detected against this chart: Gajakesari (three-rule parashara definition), Sunapha, Anapha, Dhurdhura, Kemadruma, Chandra Mangala, Budha-Aditya, and the five Pancha Mahapurusha (Ruchaka, Bhadra, Hamsa, Malavya, Sasa). Each entry carries an `id` (matches `GET /yoga/{id}` for full glossary lookup), a `present` boolean, a `quality` (Positive, Negative, or Both = auspicious, inauspicious, or context-dependent), and classical-text `evidence` for the rule that triggered or failed.
2783
2783
  */
2784
2784
  yogas?: Array<{
2785
2785
  /**
@@ -3632,7 +3632,7 @@ export type SadhesatiRequest = {
3632
3632
 
3633
3633
  export type YogaDetectResponse = {
3634
3634
  /**
3635
- * Array of 12 detected yogas. Every entry carries a present boolean; filter on present === true for active yogas. Evidence text names the rule that triggered or failed.
3635
+ * Array of 12 detected yogas. Every entry carries a `present` boolean and a `quality` (Positive, Negative, or Both = auspicious, inauspicious, or context-dependent); filter on present === true for active yogas. Evidence text names the rule that triggered or failed.
3636
3636
  */
3637
3637
  yogas: Array<{
3638
3638
  /**
@@ -4765,11 +4765,11 @@ export type KpPlanetsIntervalResponse = {
4765
4765
 
4766
4766
  export type KpPlanetsIntervalRequest = {
4767
4767
  /**
4768
- * Start datetime in ISO 8601 format. Always interpreted as local time when a non-zero timezone is provided (Z suffix is ignored). With timezone 0, Z suffix is treated as UTC.
4768
+ * Start datetime in ISO 8601 (YYYY-MM-DDTHH:MM:SS). Interpreted as local time when a non-zero timezone is provided (a trailing Z is accepted but ignored); with timezone 0 it is UTC.
4769
4769
  */
4770
4770
  startDatetime: string;
4771
4771
  /**
4772
- * End datetime in ISO 8601 format. Maximum 7 days from start. Always interpreted as local time when a non-zero timezone is provided (Z suffix is ignored).
4772
+ * End datetime in ISO 8601 (YYYY-MM-DDTHH:MM:SS). Maximum 7 days from start. Interpreted as local time when a non-zero timezone is provided (a trailing Z is accepted but ignored); with timezone 0 it is UTC.
4773
4773
  */
4774
4774
  endDatetime: string;
4775
4775
  /**
@@ -14341,7 +14341,7 @@ export type GetBasicPanchangResponse = GetBasicPanchangResponses[keyof GetBasicP
14341
14341
  export type GetDetailedPanchangData = {
14342
14342
  body?: {
14343
14343
  /**
14344
- * Date in YYYY-MM-DD format.
14344
+ * Date in YYYY-MM-DD format. A single-digit month or day is accepted and zero-padded (2026-3-5 becomes 2026-03-05). Impossible calendar dates are rejected.
14345
14345
  */
14346
14346
  date: string;
14347
14347
  /**
@@ -14898,7 +14898,7 @@ export type GetDetailedPanchangResponses = {
14898
14898
  */
14899
14899
  panchaka: {
14900
14900
  /**
14901
- * Whether Panchaka is in force at sunrise, the panchang day reference. Can be false while startsAt and endsAt are populated when Panchaka begins later in the day.
14901
+ * True when Panchaka is in effect on this date, whether it is already running at sunrise or begins later in the day, in which case startsAt and endsAt give the window. False only when no Panchaka touches this date.
14902
14902
  */
14903
14903
  active: boolean;
14904
14904
  /**
@@ -14915,11 +14915,11 @@ export type GetDetailedPanchangResponses = {
14915
14915
  endsAt: string;
14916
14916
  };
14917
14917
  /**
14918
- * Bhadra (Vishti Karana), the 7th movable karana, avoided for all auspicious activities. Bhadra recurs roughly every 3 to 5 days and lasts about half a tithi, so most occurrences begin after sunrise rather than at it. startsAt and endsAt report the Bhadra beginning on this date regardless of whether it is active at sunrise.
14918
+ * Bhadra (Vishti Karana), the 7th movable karana, avoided for all auspicious activities. Bhadra recurs roughly every 3 to 5 days and lasts about half a tithi. active is true whenever a Bhadra is attributed to this date; startsAt and endsAt give the window, which may end on the next calendar day.
14919
14919
  */
14920
14920
  bhadra: {
14921
14921
  /**
14922
- * Whether Bhadra (Vishti Karana) is in force at sunrise, the panchang day reference moment. Can be false while startsAt and endsAt are populated when Bhadra begins later in the day.
14922
+ * True when a Bhadra (Vishti Karana) occurs on this date, in which case startsAt and endsAt give its window. False only when no Bhadra begins on this date.
14923
14923
  */
14924
14924
  active: boolean;
14925
14925
  /**
@@ -15017,7 +15017,7 @@ export type GetDetailedPanchangResponse = GetDetailedPanchangResponses[keyof Get
15017
15017
  export type GetChoghadiyaData = {
15018
15018
  body?: {
15019
15019
  /**
15020
- * Date in YYYY-MM-DD format.
15020
+ * Date in YYYY-MM-DD format. A single-digit month or day is accepted and zero-padded (2026-3-5 becomes 2026-03-05). Impossible calendar dates are rejected.
15021
15021
  */
15022
15022
  date: string;
15023
15023
  /**
@@ -15206,7 +15206,7 @@ export type GetChoghadiyaResponse = GetChoghadiyaResponses[keyof GetChoghadiyaRe
15206
15206
  export type GetHoraData = {
15207
15207
  body?: {
15208
15208
  /**
15209
- * Date in YYYY-MM-DD format.
15209
+ * Date in YYYY-MM-DD format. A single-digit month or day is accepted and zero-padded (2026-3-5 becomes 2026-03-05). Impossible calendar dates are rejected.
15210
15210
  */
15211
15211
  date: string;
15212
15212
  /**
@@ -16684,7 +16684,7 @@ export type GetKpRulingPlanetsData = {
16684
16684
  */
16685
16685
  timezone?: number | string;
16686
16686
  /**
16687
- * ISO 8601 datetime for ruling planets. Defaults to current time. Always interpreted as local time when a non-zero timezone is provided (Z suffix is ignored).
16687
+ * ISO 8601 datetime (YYYY-MM-DDTHH:MM:SS) for ruling planets. Defaults to current time. Interpreted as local time when a non-zero timezone is provided (a trailing Z is accepted but ignored); with timezone 0 it is UTC.
16688
16688
  */
16689
16689
  datetime?: string;
16690
16690
  /**
@@ -16821,11 +16821,11 @@ export type GetKpRulingPlanetsResponse = GetKpRulingPlanetsResponses[keyof GetKp
16821
16821
  export type GetKpRulingIntervalData = {
16822
16822
  body?: {
16823
16823
  /**
16824
- * Start of the interval range in ISO 8601 format. Always interpreted as local time when a non-zero timezone is provided (Z suffix is ignored).
16824
+ * Start of the interval range in ISO 8601 (YYYY-MM-DDTHH:MM:SS). Interpreted as local time when a non-zero timezone is provided (a trailing Z is accepted but ignored); with timezone 0 it is UTC.
16825
16825
  */
16826
16826
  startDatetime: string;
16827
16827
  /**
16828
- * End of the interval range in ISO 8601 format. Always interpreted as local time when a non-zero timezone is provided (Z suffix is ignored).
16828
+ * End of the interval range in ISO 8601 (YYYY-MM-DDTHH:MM:SS). Interpreted as local time when a non-zero timezone is provided (a trailing Z is accepted but ignored); with timezone 0 it is UTC.
16829
16829
  */
16830
16830
  endDatetime: string;
16831
16831
  /**
@@ -21665,7 +21665,7 @@ export type CalculatePersonalDayData = {
21665
21665
  */
21666
21666
  day: number;
21667
21667
  /**
21668
- * Target date in YYYY-MM-DD format (defaults to today)
21668
+ * Target date in YYYY-MM-DD format. Defaults to today (UTC).
21669
21669
  */
21670
21670
  targetDate?: string;
21671
21671
  };
@@ -33896,7 +33896,7 @@ export type GetCrystalPairingsData = {
33896
33896
  body?: never;
33897
33897
  path: {
33898
33898
  /**
33899
- * URL-safe crystal identifier to find pairings for (e.g., "amethyst", "rose-quartz").
33899
+ * URL-safe crystal identifier to find pairings for, case-insensitive (e.g., "amethyst", "Amethyst", "rose-quartz" all resolve).
33900
33900
  */
33901
33901
  id: string;
33902
33902
  };
@@ -34855,7 +34855,7 @@ export type GetCrystalData = {
34855
34855
  body?: never;
34856
34856
  path: {
34857
34857
  /**
34858
- * URL-safe crystal identifier (e.g., "amethyst", "rose-quartz", "black-tourmaline"). Must match an entry in the database.
34858
+ * URL-safe crystal identifier, case-insensitive (e.g., "amethyst", "Amethyst", "rose-quartz" all resolve). Must match an entry in the database.
34859
34859
  */
34860
34860
  id: string;
34861
34861
  };
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/sync-version.ts. Do not edit.
2
- export const ROXY_UI_VERSION = '0.12.1';
2
+ export const ROXY_UI_VERSION = '0.12.3';