juxscript 1.0.59 → 1.0.61

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.
package/lib/jux.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { state } from './reactivity/state.js';
7
+ import { fetchAPI, type FetchConfig, type FetchResult } from './utils/fetch.js';
7
8
  import { Guard, type GuardOptions, guard } from './components/guard.js';
8
9
  import { Data, data } from './components/data.js';
9
10
  import { table, Table, type TableOptions } from './components/table.js';
@@ -54,14 +55,8 @@ import { divider, Divider, type DividerOptions } from './components/divider.js';
54
55
  import { icon as iconComponent, Icon, type IconOptions } from './components/icon.js';
55
56
  import { renderIcon, renderEmoji } from './components/icons.js';
56
57
  import { sidebar, Sidebar, type SidebarOptions } from './components/sidebar.js';
58
+ import { grid, Grid } from './components/grid.js';
57
59
 
58
- // ✅ NEW: Chart imports from /charts folder
59
- import { chart, Chart, type ChartOptions } from './components/charts/lib/chart.js';
60
- import { barchart, BarChart, type BarChartOptions } from './components/charts/barchart.js';
61
- import { areachart, AreaChart, type AreaChartOptions } from './components/charts/areachart.js';
62
- import { doughnutchart, DoughnutChart, type DoughnutChartOptions } from './components/charts/doughnutchart.js';
63
- import { kpicard, KPICard, type KPICardOptions } from './components/kpicard.js';
64
- import type { ChartDataPoint } from './components/charts/lib/BaseChart.js';
65
60
 
66
61
  /* -------------------------
67
62
  * Type Exports
@@ -83,7 +78,6 @@ export type {
83
78
  NavItem,
84
79
  ViewOptions,
85
80
  TableOptions,
86
- ChartOptions,
87
81
  CodeOptions,
88
82
  InputOptions,
89
83
  CardOptions,
@@ -112,11 +106,6 @@ export type {
112
106
  RequestInfo,
113
107
  HeadingOptions,
114
108
  ParagraphOptions,
115
- BarChartOptions,
116
- AreaChartOptions,
117
- DoughnutChartOptions,
118
- KPICardOptions,
119
- ChartDataPoint,
120
109
  DividerOptions,
121
110
  IconOptions,
122
111
  SidebarOptions
@@ -139,7 +128,6 @@ export {
139
128
  Tabs,
140
129
  Menu,
141
130
  Nav,
142
- Chart,
143
131
  Code,
144
132
  Input,
145
133
  View,
@@ -167,13 +155,10 @@ export {
167
155
  Req,
168
156
  Heading,
169
157
  Paragraph,
170
- BarChart,
171
- AreaChart,
172
- DoughnutChart,
173
- KPICard,
174
158
  Divider,
175
159
  Icon,
176
- Sidebar
160
+ Sidebar,
161
+ Grid
177
162
  };
178
163
 
179
164
  /* -------------------------
@@ -185,6 +170,7 @@ export interface JuxAPI {
185
170
  apiUrl: string;
186
171
  param(name: string): string | null;
187
172
  req: typeof req;
173
+ fetch: typeof fetchAPI;
188
174
 
189
175
  // Icon utilities
190
176
  icon: typeof renderIcon;
@@ -213,7 +199,6 @@ export interface JuxAPI {
213
199
  nav: typeof nav;
214
200
  div: typeof div;
215
201
  span: typeof span;
216
- chart: typeof chart;
217
202
  code: typeof code;
218
203
 
219
204
  view: typeof view;
@@ -240,10 +225,6 @@ export interface JuxAPI {
240
225
  fileupload: typeof fileupload;
241
226
  heading: typeof heading;
242
227
  paragraph: typeof paragraph;
243
- barchart: typeof barchart;
244
- areachart: typeof areachart;
245
- doughnutchart: typeof doughnutchart;
246
- kpicard: typeof kpicard;
247
228
  divider: typeof divider;
248
229
 
249
230
  // Input factories
@@ -259,6 +240,7 @@ export interface JuxAPI {
259
240
  date: typeof date;
260
241
  time: typeof time;
261
242
  color: typeof color;
243
+ grid: typeof grid;
262
244
  }
263
245
 
264
246
  /* -------------------------
@@ -287,6 +269,9 @@ class Jux implements JuxAPI {
287
269
  // Request utilities
288
270
  req = req;
289
271
 
272
+ // Fetch utility
273
+ fetch = fetchAPI;
274
+
290
275
  // Icon utilities
291
276
  icon = renderIcon;
292
277
  emoji = renderEmoji;
@@ -315,7 +300,6 @@ class Jux implements JuxAPI {
315
300
  nav = nav;
316
301
  div = div;
317
302
  span = span;
318
- chart = chart;
319
303
  view = view;
320
304
  app = app;
321
305
  include = include;
@@ -340,10 +324,6 @@ class Jux implements JuxAPI {
340
324
  fileupload = fileupload;
341
325
  heading = heading;
342
326
  paragraph = paragraph;
343
- barchart = barchart;
344
- areachart = areachart;
345
- doughnutchart = doughnutchart;
346
- kpicard = kpicard;
347
327
  divider = divider;
348
328
 
349
329
  // Input factories
@@ -359,6 +339,7 @@ class Jux implements JuxAPI {
359
339
  date = date;
360
340
  time = time;
361
341
  color = color;
342
+ grid = grid;
362
343
  }
363
344
  /**
364
345
  * Global jux singleton instance
@@ -0,0 +1,553 @@
1
+ /**
2
+ * Jux Fetch Utility
3
+ *
4
+ * A lightweight fetch wrapper with sensible defaults, error handling, and method chaining.
5
+ * Includes configuration helpers for juxconfig.js integration.
6
+ *
7
+ * Usage:
8
+ * // Configure once (optional)
9
+ * jux.fetch.config({
10
+ * baseUrl: 'https://api.example.com',
11
+ * credentials: 'include',
12
+ * timeout: 10000,
13
+ * log: 'errors',
14
+ * onUnauthorized: () => window.location.href = '/login'
15
+ * });
16
+ *
17
+ * // Simple GET
18
+ * const { data, error } = await jux.fetch('/users').send();
19
+ *
20
+ * // Method chaining
21
+ * const { data, error } = await jux.fetch('/users')
22
+ * .method('POST')
23
+ * .body({ name: 'John' })
24
+ * .params({ limit: 10 })
25
+ * .timeout(5000)
26
+ * .log(true)
27
+ * .send();
28
+ *
29
+ * // With juxconfig services
30
+ * const { data } = await jux.fetch('/users')
31
+ * .baseUrl(jux.fetch.getServiceUrl('database'))
32
+ * .send();
33
+ *
34
+ * // Service client helper
35
+ * const db = jux.fetch.serviceClient('database');
36
+ * const { data } = await db.fetch('/users').send();
37
+ */
38
+
39
+ export type LogLevel = boolean | 'errors';
40
+
41
+ export interface FetchConfig {
42
+ baseUrl?: string;
43
+ credentials?: RequestCredentials;
44
+ headers?: Record<string, string>;
45
+ timeout?: number;
46
+ log?: LogLevel;
47
+ onUnauthorized?: () => void;
48
+ onError?: (error: FetchError) => void;
49
+ }
50
+
51
+ export interface FetchOptions extends Omit<RequestInit, 'body'> {
52
+ params?: Record<string, any>;
53
+ body?: any;
54
+ timeout?: number;
55
+ log?: LogLevel;
56
+ onUnauthorized?: () => void;
57
+ onError?: (error: FetchError) => void;
58
+ parseResponse?: boolean;
59
+ }
60
+
61
+ export interface FetchError {
62
+ message: string;
63
+ status?: number;
64
+ statusText?: string;
65
+ data?: any;
66
+ }
67
+
68
+ export interface FetchResult<T = any> {
69
+ data: T | null;
70
+ error: FetchError | null;
71
+ status: number;
72
+ response: Response;
73
+ }
74
+
75
+ // Global configuration
76
+ let globalConfig: FetchConfig = {
77
+ credentials: 'same-origin',
78
+ headers: {
79
+ 'Content-Type': 'application/json'
80
+ },
81
+ timeout: 30000,
82
+ log: false
83
+ };
84
+
85
+ /**
86
+ * Configure global fetch defaults
87
+ */
88
+ export function configureFetch(config: FetchConfig): void {
89
+ globalConfig = {
90
+ ...globalConfig,
91
+ ...config,
92
+ headers: {
93
+ ...globalConfig.headers,
94
+ ...config.headers
95
+ }
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Build URL with query parameters
101
+ */
102
+ function buildUrl(url: string, params?: Record<string, any>): string {
103
+ if (!params || Object.keys(params).length === 0) {
104
+ return url;
105
+ }
106
+
107
+ const searchParams = new URLSearchParams();
108
+ Object.entries(params).forEach(([key, value]) => {
109
+ if (value !== undefined && value !== null) {
110
+ searchParams.append(key, String(value));
111
+ }
112
+ });
113
+
114
+ const queryString = searchParams.toString();
115
+ if (!queryString) return url;
116
+
117
+ return url.includes('?')
118
+ ? `${url}&${queryString}`
119
+ : `${url}?${queryString}`;
120
+ }
121
+
122
+ /**
123
+ * Main fetch function
124
+ */
125
+ async function executeFetch<T = any>(
126
+ url: string,
127
+ options: FetchOptions = {}
128
+ ): Promise<FetchResult<T>> {
129
+ const {
130
+ params,
131
+ body,
132
+ timeout = globalConfig.timeout,
133
+ log = globalConfig.log,
134
+ onUnauthorized = globalConfig.onUnauthorized,
135
+ onError = globalConfig.onError,
136
+ parseResponse = true,
137
+ headers = {},
138
+ ...fetchOptions
139
+ } = options;
140
+
141
+ // Build full URL
142
+ let fullUrl = url;
143
+
144
+ // Add base URL if configured and URL is relative
145
+ if (globalConfig.baseUrl && !url.startsWith('http://') && !url.startsWith('https://')) {
146
+ fullUrl = `${globalConfig.baseUrl}${url.startsWith('/') ? url : `/${url}`}`;
147
+ }
148
+
149
+ // Add query params
150
+ fullUrl = buildUrl(fullUrl, params);
151
+
152
+ // Merge headers
153
+ const mergedHeaders = {
154
+ ...globalConfig.headers,
155
+ ...headers
156
+ };
157
+
158
+ // Prepare request init
159
+ const requestInit: RequestInit = {
160
+ ...fetchOptions,
161
+ headers: mergedHeaders,
162
+ credentials: options.credentials ?? globalConfig.credentials
163
+ };
164
+
165
+ // Stringify body if it's an object
166
+ if (body !== undefined) {
167
+ if (body instanceof FormData) {
168
+ requestInit.body = body;
169
+ // Remove Content-Type header for FormData (browser sets it with boundary)
170
+ delete (requestInit.headers as Record<string, string>)['Content-Type'];
171
+ } else if (typeof body === 'object') {
172
+ requestInit.body = JSON.stringify(body);
173
+ } else {
174
+ requestInit.body = body;
175
+ }
176
+ }
177
+
178
+ // Log request
179
+ if (log === true) {
180
+ console.log(`[JUX Fetch] ${requestInit.method || 'GET'} ${fullUrl}`, {
181
+ headers: requestInit.headers,
182
+ body: requestInit.body,
183
+ params
184
+ });
185
+ }
186
+
187
+ // Setup timeout
188
+ const controller = new AbortController();
189
+ const timeoutId = timeout ? setTimeout(() => controller.abort(), timeout) : null;
190
+
191
+ if (!requestInit.signal) {
192
+ requestInit.signal = controller.signal;
193
+ }
194
+
195
+ try {
196
+ const response = await fetch(fullUrl, requestInit);
197
+
198
+ if (timeoutId) clearTimeout(timeoutId);
199
+
200
+ // Handle unauthorized
201
+ if (response.status === 401 || response.status === 403) {
202
+ if (onUnauthorized) {
203
+ onUnauthorized();
204
+ }
205
+ }
206
+
207
+ // Parse response
208
+ let data: T | null = null;
209
+ let errorData: any = null;
210
+
211
+ if (parseResponse) {
212
+ const contentType = response.headers.get('content-type');
213
+
214
+ if (contentType?.includes('application/json')) {
215
+ try {
216
+ const json = await response.json();
217
+ if (response.ok) {
218
+ data = json;
219
+ } else {
220
+ errorData = json;
221
+ }
222
+ } catch (e) {
223
+ // JSON parse error
224
+ }
225
+ } else if (response.ok) {
226
+ // Non-JSON response, try to get text
227
+ try {
228
+ data = (await response.text()) as any;
229
+ } catch (e) {
230
+ // Ignore text parse errors
231
+ }
232
+ }
233
+ }
234
+
235
+ // Check if request was successful
236
+ if (!response.ok) {
237
+ const error: FetchError = {
238
+ message: errorData?.message || errorData?.error || response.statusText || 'Request failed',
239
+ status: response.status,
240
+ statusText: response.statusText,
241
+ data: errorData
242
+ };
243
+
244
+ // Log error
245
+ if (log === true || log === 'errors') {
246
+ console.error(`[JUX Fetch Error] ${requestInit.method || 'GET'} ${fullUrl}`, error);
247
+ }
248
+
249
+ if (onError) {
250
+ onError(error);
251
+ }
252
+
253
+ return {
254
+ data: null,
255
+ error,
256
+ status: response.status,
257
+ response
258
+ };
259
+ }
260
+
261
+ // Log success
262
+ if (log === true) {
263
+ console.log(`[JUX Fetch Success] ${requestInit.method || 'GET'} ${fullUrl}`, {
264
+ status: response.status,
265
+ data
266
+ });
267
+ }
268
+
269
+ return {
270
+ data,
271
+ error: null,
272
+ status: response.status,
273
+ response
274
+ };
275
+
276
+ } catch (err: any) {
277
+ if (timeoutId) clearTimeout(timeoutId);
278
+
279
+ const error: FetchError = {
280
+ message: err.name === 'AbortError'
281
+ ? 'Request timeout'
282
+ : err.message || 'Network error',
283
+ status: 0
284
+ };
285
+
286
+ // Log error
287
+ if (log === true || log === 'errors') {
288
+ console.error(`[JUX Fetch Error] ${requestInit.method || 'GET'} ${fullUrl}`, error);
289
+ }
290
+
291
+ if (onError) {
292
+ onError(error);
293
+ }
294
+
295
+ return {
296
+ data: null,
297
+ error,
298
+ status: 0,
299
+ response: null as any
300
+ };
301
+ }
302
+ }
303
+
304
+ /**
305
+ * FetchBuilder class for method chaining
306
+ */
307
+ class FetchBuilder<T = any> {
308
+ public url: string;
309
+ public options: FetchOptions = {};
310
+
311
+ constructor(url: string, options: FetchOptions = {}) {
312
+ this.url = url;
313
+ this.options = options;
314
+ }
315
+
316
+ method(value: string): this {
317
+ this.options.method = value;
318
+ return this;
319
+ }
320
+
321
+ body(value: any): this {
322
+ this.options.body = value;
323
+ return this;
324
+ }
325
+
326
+ params(value: Record<string, any>): this {
327
+ this.options.params = value;
328
+ return this;
329
+ }
330
+
331
+ headers(value: Record<string, string>): this {
332
+ this.options.headers = { ...this.options.headers, ...value };
333
+ return this;
334
+ }
335
+
336
+ header(key: string, value: string): this {
337
+ this.options.headers = { ...this.options.headers, [key]: value };
338
+ return this;
339
+ }
340
+
341
+ timeout(value: number): this {
342
+ this.options.timeout = value;
343
+ return this;
344
+ }
345
+
346
+ credentials(value: RequestCredentials): this {
347
+ this.options.credentials = value;
348
+ return this;
349
+ }
350
+
351
+ log(value: LogLevel): this {
352
+ this.options.log = value;
353
+ return this;
354
+ }
355
+
356
+ parseResponse(value: boolean): this {
357
+ this.options.parseResponse = value;
358
+ return this;
359
+ }
360
+
361
+ onUnauthorized(callback: () => void): this {
362
+ this.options.onUnauthorized = callback;
363
+ return this;
364
+ }
365
+
366
+ onError(callback: (error: FetchError) => void): this {
367
+ this.options.onError = callback;
368
+ return this;
369
+ }
370
+
371
+ /**
372
+ * Execute the fetch request
373
+ */
374
+ send(): Promise<FetchResult<T>> {
375
+ return executeFetch<T>(this.url, this.options);
376
+ }
377
+
378
+ /**
379
+ * Alias for send()
380
+ */
381
+ fetch(): Promise<FetchResult<T>> {
382
+ return this.send();
383
+ }
384
+
385
+ /**
386
+ * Make the builder thenable (Promise-like) so it can be awaited directly
387
+ * This allows: await jux.fetch('/users') without needing .send()
388
+ */
389
+ then<TResult1 = FetchResult<T>, TResult2 = never>(
390
+ onfulfilled?: ((value: FetchResult<T>) => TResult1 | PromiseLike<TResult1>) | null,
391
+ onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
392
+ ): Promise<TResult1 | TResult2> {
393
+ return this.send().then(onfulfilled, onrejected);
394
+ }
395
+
396
+ /**
397
+ * Make the builder catchable for Promise.catch()
398
+ */
399
+ catch<TResult = never>(
400
+ onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
401
+ ): Promise<FetchResult<T> | TResult> {
402
+ return this.send().catch(onrejected);
403
+ }
404
+
405
+ /**
406
+ * Make the builder finally-able for Promise.finally()
407
+ */
408
+ finally(onfinally?: (() => void) | null): Promise<FetchResult<T>> {
409
+ return this.send().finally(onfinally);
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Create a fetch builder
415
+ */
416
+ export function juxFetch<T = any>(url: string, options: FetchOptions = {}): FetchBuilder<T> {
417
+ return new FetchBuilder<T>(url, options);
418
+ }
419
+
420
+ /**
421
+ * Convenience methods for common HTTP verbs
422
+ */
423
+ export const fetchHelpers = {
424
+ get: <T = any>(url: string, options?: Omit<FetchOptions, 'method'>) =>
425
+ new FetchBuilder<T>(url, { ...options, method: 'GET' }),
426
+
427
+ post: <T = any>(url: string, body?: any, options?: Omit<FetchOptions, 'method' | 'body'>) =>
428
+ new FetchBuilder<T>(url, { ...options, method: 'POST', body }),
429
+
430
+ put: <T = any>(url: string, body?: any, options?: Omit<FetchOptions, 'method' | 'body'>) =>
431
+ new FetchBuilder<T>(url, { ...options, method: 'PUT', body }),
432
+
433
+ patch: <T = any>(url: string, body?: any, options?: Omit<FetchOptions, 'method' | 'body'>) =>
434
+ new FetchBuilder<T>(url, { ...options, method: 'PATCH', body }),
435
+
436
+ delete: <T = any>(url: string, options?: Omit<FetchOptions, 'method'>) =>
437
+ new FetchBuilder<T>(url, { ...options, method: 'DELETE' })
438
+ };
439
+
440
+ /**
441
+ * Fetch multiple URLs in parallel
442
+ */
443
+ export async function fetchAll<T = any>(
444
+ urls: string[],
445
+ options?: FetchOptions
446
+ ): Promise<FetchResult<T>[]> {
447
+ return Promise.all(urls.map(url => executeFetch<T>(url, options)));
448
+ }
449
+
450
+ /**
451
+ * Get a service URL from window.juxConfig
452
+ */
453
+ export function getServiceUrl(serviceName: string): string | null {
454
+ if (typeof window === 'undefined') return null;
455
+
456
+ const juxConfig = (window as any).juxConfig;
457
+ if (!juxConfig?.services) return null;
458
+
459
+ return juxConfig.services[serviceName] || null;
460
+ }
461
+
462
+ /**
463
+ * Create a fetch instance preconfigured for a specific service
464
+ *
465
+ * Usage:
466
+ * const api = jux.fetch.serviceClient('database');
467
+ * const { data } = await api.fetch('/users').send();
468
+ */
469
+ export function serviceClient(serviceName: string) {
470
+ const baseUrl = getServiceUrl(serviceName);
471
+
472
+ if (!baseUrl) {
473
+ console.warn(`[JUX Fetch] Service '${serviceName}' not found in juxConfig.services`);
474
+ }
475
+
476
+ return {
477
+ fetch: (url: string, options?: any) => {
478
+ return juxFetch(url, {
479
+ ...options,
480
+ baseUrl: baseUrl || undefined
481
+ });
482
+ },
483
+ baseUrl
484
+ };
485
+ }
486
+
487
+ /**
488
+ * Setup fetch from juxconfig
489
+ * Call this in your bootstrap function
490
+ *
491
+ * Usage:
492
+ * export default {
493
+ * bootstrap: [
494
+ * async function initFetch() {
495
+ * await jux.fetch.setupConfig();
496
+ * }
497
+ * ]
498
+ * };
499
+ */
500
+ export async function setupConfig() {
501
+ if (typeof window === 'undefined') {
502
+ console.warn('[JUX Fetch] setupConfig() called outside browser environment');
503
+ return;
504
+ }
505
+
506
+ const juxConfig = (window as any).juxConfig;
507
+
508
+ if (!juxConfig) {
509
+ console.warn('[JUX Fetch] No juxConfig found on window. Make sure your juxconfig.js is loaded.');
510
+ return;
511
+ }
512
+
513
+ const services = juxConfig.services || {};
514
+
515
+ if (Object.keys(services).length === 0) {
516
+ console.log('[JUX Fetch] No services configured in juxConfig');
517
+ return;
518
+ }
519
+
520
+ // Configure fetch with common defaults
521
+ const fetchConfig: FetchConfig = {
522
+ timeout: juxConfig.fetchTimeout || 30000,
523
+ log: juxConfig.fetchLog || false,
524
+ credentials: 'include'
525
+ };
526
+
527
+ // Add service-specific handlers
528
+ if (services.auth) {
529
+ fetchConfig.onUnauthorized = () => {
530
+ console.warn('[JUX Fetch] Unauthorized - consider redirecting to login');
531
+ };
532
+ }
533
+
534
+ configureFetch(fetchConfig);
535
+
536
+ // Log configured services
537
+ console.log('[JUX Fetch] Configured with services:', Object.keys(services));
538
+
539
+ return {
540
+ services,
541
+ getServiceUrl
542
+ };
543
+ }
544
+
545
+ // Export combined API
546
+ export const fetchAPI = Object.assign(juxFetch, {
547
+ config: configureFetch,
548
+ setupConfig,
549
+ getServiceUrl,
550
+ serviceClient,
551
+ ...fetchHelpers,
552
+ all: fetchAll
553
+ });