juxscript 1.0.5 → 1.0.7

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.
@@ -0,0 +1,501 @@
1
+ import { State } from '../reactivity/state.js';
2
+ import {
3
+ googleTheme,
4
+ seriesaTheme,
5
+ hrTheme,
6
+ figmaTheme,
7
+ notionTheme,
8
+ chalkTheme,
9
+ mintTheme
10
+ } from '../themes/charts.js';
11
+
12
+ /**
13
+ * KPI card options
14
+ */
15
+ export interface KPICardOptions {
16
+ title?: string;
17
+ value?: string | number;
18
+ delta?: number; // Percentage change (e.g., 10 for +10%, -5 for -5%)
19
+ prefix?: string; // e.g., "$", "€"
20
+ suffix?: string; // e.g., "k", "M", "%"
21
+ width?: number;
22
+ height?: number;
23
+ theme?: 'google' | 'seriesa' | 'hr' | 'figma' | 'notion' | 'chalk' | 'mint';
24
+ styleMode?: 'default' | 'gradient' | 'glass' | 'outline';
25
+ animate?: boolean;
26
+ animationDuration?: number;
27
+ class?: string;
28
+ style?: string;
29
+ }
30
+
31
+ /**
32
+ * KPI card state
33
+ */
34
+ type KPICardState = {
35
+ title: string;
36
+ value: string | number;
37
+ delta: number;
38
+ prefix: string;
39
+ suffix: string;
40
+ width: number;
41
+ height: number;
42
+ theme: 'google' | 'seriesa' | 'hr' | 'figma' | 'notion' | 'chalk' | 'mint';
43
+ styleMode: 'default' | 'gradient' | 'glass' | 'outline';
44
+ animate: boolean;
45
+ animationDuration: number;
46
+ class: string;
47
+ style: string;
48
+ };
49
+
50
+ /**
51
+ * KPI card component - Displays key performance indicators
52
+ *
53
+ * Usage:
54
+ * jux.kpicard('users-kpi')
55
+ * .title('New users')
56
+ * .value('78k')
57
+ * .delta(10)
58
+ * .theme('mint')
59
+ * .render('#app');
60
+ */
61
+ export class KPICard {
62
+ private _id: string;
63
+ private _element?: HTMLElement;
64
+ private _container?: string;
65
+
66
+ public state: KPICardState = {
67
+ title: 'KPI',
68
+ value: 0,
69
+ delta: 0,
70
+ prefix: '',
71
+ suffix: '',
72
+ width: 280,
73
+ height: 200,
74
+ theme: 'google',
75
+ styleMode: 'default',
76
+ animate: true,
77
+ animationDuration: 600,
78
+ class: '',
79
+ style: ''
80
+ };
81
+
82
+ private _boundTheme?: State<string>;
83
+ private _boundStyleMode?: State<string>;
84
+ private _boundTitle?: State<string>;
85
+ private _boundValue?: State<string | number>;
86
+ private _boundDelta?: State<number>;
87
+
88
+ constructor(id: string, options: KPICardOptions = {}) {
89
+ this._id = id;
90
+
91
+ // Apply options
92
+ if (options.title !== undefined) this.state.title = options.title;
93
+ if (options.value !== undefined) this.state.value = options.value;
94
+ if (options.delta !== undefined) this.state.delta = options.delta;
95
+ if (options.prefix !== undefined) this.state.prefix = options.prefix;
96
+ if (options.suffix !== undefined) this.state.suffix = options.suffix;
97
+ if (options.width !== undefined) this.state.width = options.width;
98
+ if (options.height !== undefined) this.state.height = options.height;
99
+ if (options.theme !== undefined) this.state.theme = options.theme;
100
+ if (options.styleMode !== undefined) this.state.styleMode = options.styleMode;
101
+ if (options.animate !== undefined) this.state.animate = options.animate;
102
+ if (options.animationDuration !== undefined) this.state.animationDuration = options.animationDuration;
103
+ if (options.class !== undefined) this.state.class = options.class;
104
+ if (options.style !== undefined) this.state.style = options.style;
105
+ }
106
+
107
+ // Chainable setters
108
+ title(value: string | State<string>): this {
109
+ if (value instanceof State) {
110
+ this._boundTitle = value;
111
+ this.state.title = value.value;
112
+ value.subscribe((newValue) => {
113
+ this.state.title = newValue;
114
+ this._updateCard();
115
+ });
116
+ } else {
117
+ this.state.title = value;
118
+ }
119
+ return this;
120
+ }
121
+
122
+ value(value: string | number | State<string | number>): this {
123
+ if (value instanceof State) {
124
+ this._boundValue = value;
125
+ this.state.value = value.value;
126
+ value.subscribe((newValue) => {
127
+ this.state.value = newValue;
128
+ this._updateCard();
129
+ });
130
+ } else {
131
+ this.state.value = value;
132
+ }
133
+ return this;
134
+ }
135
+
136
+ delta(value: number | State<number>): this {
137
+ if (value instanceof State) {
138
+ this._boundDelta = value;
139
+ this.state.delta = value.value;
140
+ value.subscribe((newValue) => {
141
+ this.state.delta = newValue;
142
+ this._updateCard();
143
+ });
144
+ } else {
145
+ this.state.delta = value;
146
+ }
147
+ return this;
148
+ }
149
+
150
+ prefix(value: string): this {
151
+ this.state.prefix = value;
152
+ return this;
153
+ }
154
+
155
+ suffix(value: string): this {
156
+ this.state.suffix = value;
157
+ return this;
158
+ }
159
+
160
+ width(value: number): this {
161
+ this.state.width = value;
162
+ return this;
163
+ }
164
+
165
+ height(value: number): this {
166
+ this.state.height = value;
167
+ return this;
168
+ }
169
+
170
+ theme(value: string | State<string>): this {
171
+ if (value instanceof State) {
172
+ this._boundTheme = value;
173
+ this.state.theme = value.value as any;
174
+ value.subscribe((newValue) => {
175
+ this.state.theme = newValue as any;
176
+ this._updateCard();
177
+ });
178
+ } else {
179
+ this.state.theme = value as any;
180
+ }
181
+ return this;
182
+ }
183
+
184
+ styleMode(value: string | State<string>): this {
185
+ if (value instanceof State) {
186
+ this._boundStyleMode = value;
187
+ this.state.styleMode = value.value as any;
188
+ value.subscribe((newValue) => {
189
+ this.state.styleMode = newValue as any;
190
+ this._updateCard();
191
+ });
192
+ } else {
193
+ this.state.styleMode = value as any;
194
+ }
195
+ return this;
196
+ }
197
+
198
+ animate(value: boolean): this {
199
+ this.state.animate = value;
200
+ return this;
201
+ }
202
+
203
+ animationDuration(value: number): this {
204
+ this.state.animationDuration = value;
205
+ return this;
206
+ }
207
+
208
+ class(value: string): this {
209
+ this.state.class = value;
210
+ return this;
211
+ }
212
+
213
+ style(value: string): this {
214
+ this.state.style = value;
215
+ return this;
216
+ }
217
+
218
+ render(container: string): this {
219
+ this._container = container;
220
+ const element = document.querySelector(container);
221
+ if (!element) {
222
+ console.warn(`Container ${container} not found`);
223
+ return this;
224
+ }
225
+
226
+ this._buildCard(element as HTMLElement);
227
+ return this;
228
+ }
229
+
230
+ private _updateCard(): void {
231
+ if (!this._container) return;
232
+
233
+ const element = document.querySelector(this._container);
234
+ if (!element) return;
235
+
236
+ // Clear and rebuild
237
+ element.innerHTML = '';
238
+ this._buildCard(element as HTMLElement);
239
+ }
240
+
241
+ private _buildCard(container: HTMLElement): void {
242
+ const { width, height, class: className, style, animate, animationDuration } = this.state;
243
+
244
+ // Create wrapper
245
+ const wrapper = document.createElement('div');
246
+ wrapper.className = `jux-kpicard ${className}`;
247
+ wrapper.id = this._id;
248
+ wrapper.style.cssText = `
249
+ width: ${width}px;
250
+ height: ${height}px;
251
+ ${style}
252
+ `;
253
+
254
+ // Apply theme colors and style mode
255
+ this._applyThemeAndStyle(wrapper);
256
+
257
+ // Build card content
258
+ const content = this._createContent();
259
+ wrapper.appendChild(content);
260
+
261
+ // Add animation
262
+ if (animate) {
263
+ wrapper.style.opacity = '0';
264
+ wrapper.style.transform = 'translateY(20px)';
265
+ wrapper.style.transition = `opacity ${animationDuration}ms ease-out, transform ${animationDuration}ms ease-out`;
266
+
267
+ requestAnimationFrame(() => {
268
+ setTimeout(() => {
269
+ wrapper.style.opacity = '1';
270
+ wrapper.style.transform = 'translateY(0)';
271
+ }, 50);
272
+ });
273
+ }
274
+
275
+ container.appendChild(wrapper);
276
+ this._element = wrapper;
277
+ }
278
+
279
+ private _createContent(): HTMLElement {
280
+ const { title, value, delta, prefix, suffix, animate, animationDuration } = this.state;
281
+
282
+ const content = document.createElement('div');
283
+ content.className = 'jux-kpicard-content';
284
+ content.style.cssText = `
285
+ padding: 24px;
286
+ display: flex;
287
+ flex-direction: column;
288
+ height: 100%;
289
+ position: relative;
290
+ `;
291
+
292
+ // Title
293
+ const titleEl = document.createElement('div');
294
+ titleEl.className = 'jux-kpicard-title';
295
+ titleEl.textContent = title;
296
+ titleEl.style.cssText = `
297
+ font-size: 16px;
298
+ font-weight: 500;
299
+ color: #6b7280;
300
+ margin-bottom: 16px;
301
+ font-family: inherit;
302
+ `;
303
+ content.appendChild(titleEl);
304
+
305
+ // Value container
306
+ const valueContainer = document.createElement('div');
307
+ valueContainer.style.cssText = `
308
+ display: flex;
309
+ align-items: baseline;
310
+ margin-bottom: 12px;
311
+ `;
312
+
313
+ // Value
314
+ const valueEl = document.createElement('div');
315
+ valueEl.className = 'jux-kpicard-value';
316
+ valueEl.textContent = `${prefix}${value}${suffix}`;
317
+ valueEl.style.cssText = `
318
+ font-size: 56px;
319
+ font-weight: 800;
320
+ color: #1f2937;
321
+ line-height: 1;
322
+ font-family: inherit;
323
+ `;
324
+
325
+ if (animate) {
326
+ valueEl.style.opacity = '0';
327
+ valueEl.style.transform = 'scale(0.8)';
328
+ valueEl.style.transition = `opacity ${animationDuration}ms ease-out ${animationDuration / 2}ms, transform ${animationDuration}ms cubic-bezier(0.34, 1.56, 0.64, 1) ${animationDuration / 2}ms`;
329
+
330
+ requestAnimationFrame(() => {
331
+ setTimeout(() => {
332
+ valueEl.style.opacity = '1';
333
+ valueEl.style.transform = 'scale(1)';
334
+ }, 100);
335
+ });
336
+ }
337
+
338
+ valueContainer.appendChild(valueEl);
339
+ content.appendChild(valueContainer);
340
+
341
+ // Delta with arrow
342
+ if (delta !== 0) {
343
+ const deltaContainer = document.createElement('div');
344
+ deltaContainer.className = 'jux-kpicard-delta';
345
+ deltaContainer.style.cssText = `
346
+ display: flex;
347
+ align-items: center;
348
+ gap: 8px;
349
+ `;
350
+
351
+ // Arrow SVG
352
+ const arrow = this._createArrowSVG(delta > 0);
353
+ deltaContainer.appendChild(arrow);
354
+
355
+ // Delta text
356
+ const deltaText = document.createElement('span');
357
+ deltaText.textContent = `${delta > 0 ? '+' : ''}${delta}%`;
358
+ deltaText.style.cssText = `
359
+ font-size: 18px;
360
+ font-weight: 700;
361
+ color: ${delta > 0 ? '#10b981' : '#ef4444'};
362
+ font-family: inherit;
363
+ `;
364
+
365
+ if (animate) {
366
+ deltaContainer.style.opacity = '0';
367
+ deltaContainer.style.transform = 'translateX(-10px)';
368
+ deltaContainer.style.transition = `opacity ${animationDuration}ms ease-out ${animationDuration}ms, transform ${animationDuration}ms ease-out ${animationDuration}ms`;
369
+
370
+ requestAnimationFrame(() => {
371
+ setTimeout(() => {
372
+ deltaContainer.style.opacity = '1';
373
+ deltaContainer.style.transform = 'translateX(0)';
374
+ }, 150);
375
+ });
376
+ }
377
+
378
+ deltaContainer.appendChild(deltaText);
379
+ content.appendChild(deltaContainer);
380
+ }
381
+
382
+ return content;
383
+ }
384
+
385
+ private _createArrowSVG(isUp: boolean): SVGSVGElement {
386
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
387
+ svg.setAttribute('width', '24');
388
+ svg.setAttribute('height', '24');
389
+ svg.setAttribute('viewBox', '0 0 24 24');
390
+ svg.setAttribute('fill', 'none');
391
+ svg.style.flexShrink = '0';
392
+
393
+ const color = isUp ? '#10b981' : '#ef4444';
394
+
395
+ // Create squiggly arrow path
396
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
397
+
398
+ if (isUp) {
399
+ // Up arrow with squiggly line
400
+ path.setAttribute('d', 'M12 4L12 20M12 4L8 8M12 4L16 8M10 10Q11 11 12 10T14 10M10 14Q11 15 12 14T14 14');
401
+ } else {
402
+ // Down arrow with squiggly line
403
+ path.setAttribute('d', 'M12 20L12 4M12 20L8 16M12 20L16 16M10 10Q11 9 12 10T14 10M10 14Q11 13 12 14T14 14');
404
+ }
405
+
406
+ path.setAttribute('stroke', color);
407
+ path.setAttribute('stroke-width', '2');
408
+ path.setAttribute('stroke-linecap', 'round');
409
+ path.setAttribute('stroke-linejoin', 'round');
410
+
411
+ svg.appendChild(path);
412
+ return svg;
413
+ }
414
+
415
+ private _getThemeColors(): string[] {
416
+ const { theme } = this.state;
417
+
418
+ switch (theme) {
419
+ case 'google':
420
+ return googleTheme;
421
+ case 'seriesa':
422
+ return seriesaTheme;
423
+ case 'hr':
424
+ return hrTheme;
425
+ case 'figma':
426
+ return figmaTheme;
427
+ case 'notion':
428
+ return notionTheme;
429
+ case 'chalk':
430
+ return chalkTheme;
431
+ case 'mint':
432
+ return mintTheme;
433
+ default:
434
+ return googleTheme;
435
+ }
436
+ }
437
+
438
+ private _applyThemeAndStyle(wrapper: HTMLElement): void {
439
+ const { styleMode } = this.state;
440
+ const colors = this._getThemeColors();
441
+
442
+ const baseStyles = `
443
+ border-radius: 16px;
444
+ overflow: hidden;
445
+ position: relative;
446
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
447
+ `;
448
+
449
+ if (styleMode === 'gradient') {
450
+ wrapper.style.cssText += `
451
+ ${baseStyles}
452
+ background: linear-gradient(135deg, ${colors[0]} 0%, ${colors[1]} 100%);
453
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
454
+ `;
455
+
456
+ // Adjust text colors for gradient
457
+ const titleElements = wrapper.querySelectorAll('.jux-kpicard-title');
458
+ const valueElements = wrapper.querySelectorAll('.jux-kpicard-value');
459
+ titleElements.forEach(el => (el as HTMLElement).style.color = 'rgba(255, 255, 255, 0.9)');
460
+ valueElements.forEach(el => (el as HTMLElement).style.color = '#ffffff');
461
+ } else if (styleMode === 'glass') {
462
+ wrapper.style.cssText += `
463
+ ${baseStyles}
464
+ background: rgba(255, 255, 255, 0.7);
465
+ backdrop-filter: blur(10px);
466
+ border: 1px solid rgba(255, 255, 255, 0.8);
467
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
468
+ `;
469
+ } else if (styleMode === 'outline') {
470
+ wrapper.style.cssText += `
471
+ ${baseStyles}
472
+ background: transparent;
473
+ border: 3px solid ${colors[0]};
474
+ `;
475
+ } else {
476
+ // default
477
+ wrapper.style.cssText += `
478
+ ${baseStyles}
479
+ background: #ffffff;
480
+ border: 1px solid #e5e7eb;
481
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
482
+ `;
483
+ }
484
+
485
+ // Apply theme color accent
486
+ const accentBar = document.createElement('div');
487
+ accentBar.style.cssText = `
488
+ position: absolute;
489
+ top: 0;
490
+ left: 0;
491
+ right: 0;
492
+ height: 4px;
493
+ background: linear-gradient(90deg, ${colors[0]}, ${colors[1]});
494
+ `;
495
+ wrapper.appendChild(accentBar);
496
+ }
497
+ }
498
+
499
+ export function kpicard(id: string, options: KPICardOptions = {}): KPICard {
500
+ return new KPICard(id, options);
501
+ }
package/lib/jux.ts CHANGED
@@ -50,7 +50,10 @@ import { req, Req, type RequestInfo } from './components/req.js';
50
50
  import { heading, Heading, type HeadingOptions } from './components/heading.js';
51
51
  import { paragraph, Paragraph, type ParagraphOptions } from './components/paragraph.js';
52
52
  import { barchart, BarChart, type BarChartOptions, type BarChartDataPoint } from './components/barchart.js';
53
-
53
+ import { areachart, AreaChart, type AreaChartOptions, type AreaChartDataPoint } from './components/areachart.js';
54
+ import { areachartsmooth, AreaChartSmooth, type AreaChartSmoothOptions, AreaChartSmoothDataPoint } from './components/areachartsmooth.js';
55
+ import { doughnutchart, DoughnutChart, type DoughnutChartOptions, type DoughnutChartDataPoint } from './components/doughnutchart.js';
56
+ import { kpicard, KPICard, type KPICardOptions } from './components/kpicard.js';
54
57
 
55
58
  /* -------------------------
56
59
  * Type Exports
@@ -106,7 +109,14 @@ export type {
106
109
  HeadingOptions,
107
110
  ParagraphOptions,
108
111
  BarChartOptions,
109
- BarChartDataPoint
112
+ BarChartDataPoint,
113
+ AreaChartOptions,
114
+ AreaChartDataPoint,
115
+ AreaChartSmoothOptions,
116
+ AreaChartSmoothDataPoint,
117
+ DoughnutChartOptions,
118
+ DoughnutChartDataPoint,
119
+ KPICardOptions
110
120
  };
111
121
 
112
122
  /* -------------------------
@@ -158,7 +168,11 @@ export {
158
168
  Req,
159
169
  Heading,
160
170
  Paragraph,
161
- BarChart
171
+ BarChart,
172
+ AreaChart,
173
+ AreaChartSmooth,
174
+ DoughnutChart,
175
+ KPICard
162
176
  };
163
177
 
164
178
  /* -------------------------
@@ -216,6 +230,10 @@ export interface JuxAPI {
216
230
  heading: typeof heading;
217
231
  paragraph: typeof paragraph;
218
232
  barchart: typeof barchart;
233
+ areachart: typeof areachart;
234
+ areachartsmooth: typeof areachartsmooth;
235
+ doughnutchart: typeof doughnutchart;
236
+ kpicard: typeof kpicard;
219
237
  }
220
238
 
221
239
  /* -------------------------
@@ -289,6 +307,10 @@ class Jux implements JuxAPI {
289
307
  heading = heading;
290
308
  paragraph = paragraph;
291
309
  barchart = barchart;
310
+ areachart = areachart;
311
+ areachartsmooth = areachartsmooth;
312
+ doughnutchart = doughnutchart;
313
+ kpicard = kpicard;
292
314
  }
293
315
 
294
316
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "lib/jux.js",