juxscript 1.0.2 → 1.0.4

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 (71) hide show
  1. package/README.md +37 -92
  2. package/bin/cli.js +57 -56
  3. package/lib/components/alert.ts +240 -0
  4. package/lib/components/app.ts +216 -82
  5. package/lib/components/badge.ts +164 -0
  6. package/lib/components/button.ts +188 -53
  7. package/lib/components/card.ts +75 -61
  8. package/lib/components/chart.ts +17 -15
  9. package/lib/components/checkbox.ts +228 -0
  10. package/lib/components/code.ts +66 -152
  11. package/lib/components/container.ts +104 -208
  12. package/lib/components/data.ts +1 -3
  13. package/lib/components/datepicker.ts +226 -0
  14. package/lib/components/dialog.ts +258 -0
  15. package/lib/components/docs-data.json +1697 -388
  16. package/lib/components/dropdown.ts +244 -0
  17. package/lib/components/element.ts +271 -0
  18. package/lib/components/fileupload.ts +319 -0
  19. package/lib/components/footer.ts +37 -18
  20. package/lib/components/header.ts +53 -33
  21. package/lib/components/heading.ts +119 -0
  22. package/lib/components/helpers.ts +34 -0
  23. package/lib/components/hero.ts +57 -31
  24. package/lib/components/include.ts +292 -0
  25. package/lib/components/input.ts +166 -78
  26. package/lib/components/layout.ts +144 -18
  27. package/lib/components/list.ts +83 -74
  28. package/lib/components/loading.ts +263 -0
  29. package/lib/components/main.ts +43 -17
  30. package/lib/components/menu.ts +108 -24
  31. package/lib/components/modal.ts +50 -21
  32. package/lib/components/nav.ts +60 -18
  33. package/lib/components/paragraph.ts +111 -0
  34. package/lib/components/progress.ts +276 -0
  35. package/lib/components/radio.ts +236 -0
  36. package/lib/components/req.ts +300 -0
  37. package/lib/components/script.ts +33 -74
  38. package/lib/components/select.ts +247 -0
  39. package/lib/components/sidebar.ts +86 -36
  40. package/lib/components/style.ts +47 -70
  41. package/lib/components/switch.ts +261 -0
  42. package/lib/components/table.ts +47 -24
  43. package/lib/components/tabs.ts +105 -63
  44. package/lib/components/theme-toggle.ts +361 -0
  45. package/lib/components/token-calculator.ts +380 -0
  46. package/lib/components/tooltip.ts +244 -0
  47. package/lib/components/view.ts +36 -20
  48. package/lib/components/write.ts +284 -0
  49. package/lib/globals.d.ts +21 -0
  50. package/lib/jux.ts +172 -68
  51. package/lib/presets/notion.css +521 -0
  52. package/lib/presets/notion.jux +27 -0
  53. package/lib/reactivity/state.ts +364 -0
  54. package/machinery/compiler.js +126 -38
  55. package/machinery/generators/html.js +2 -3
  56. package/machinery/server.js +2 -2
  57. package/package.json +29 -3
  58. package/lib/components/import.ts +0 -430
  59. package/lib/components/node.ts +0 -200
  60. package/lib/components/reactivity.js +0 -104
  61. package/lib/components/theme.ts +0 -97
  62. package/lib/layouts/notion.css +0 -258
  63. package/lib/styles/base-theme.css +0 -186
  64. package/lib/styles/dark-theme.css +0 -144
  65. package/lib/styles/light-theme.css +0 -144
  66. package/lib/styles/tokens/dark.css +0 -86
  67. package/lib/styles/tokens/light.css +0 -86
  68. package/lib/templates/index.juxt +0 -33
  69. package/lib/themes/dark.css +0 -86
  70. package/lib/themes/light.css +0 -86
  71. /package/lib/{styles → presets}/global.css +0 -0
@@ -0,0 +1,380 @@
1
+ import { getOrCreateContainer } from './helpers.js';
2
+ import { ErrorHandler } from './error-handler.js';
3
+
4
+ /**
5
+ * Token Calculator component - Compare framework token costs
6
+ * Estimates AI token usage between JUX vs traditional frameworks (React, Vue, etc.)
7
+ */
8
+
9
+ export interface TokenCalculatorOptions {
10
+ juxLines?: number;
11
+ multipliers?: {
12
+ react: number;
13
+ vue: number;
14
+ angular: number;
15
+ svelte: number;
16
+ };
17
+ tokensPerLine?: number;
18
+ showComparison?: boolean;
19
+ animated?: boolean;
20
+ style?: string;
21
+ class?: string;
22
+ }
23
+
24
+ export interface TokenEstimate {
25
+ framework: string;
26
+ lines: number;
27
+ tokens: number;
28
+ percentage: number;
29
+ savings: number;
30
+ }
31
+
32
+ export class TokenCalculator {
33
+ state: {
34
+ juxLines: number;
35
+ multipliers: Record<string, number>;
36
+ tokensPerLine: number;
37
+ showComparison: boolean;
38
+ animated: boolean;
39
+ style: string;
40
+ class: string;
41
+ };
42
+ container: HTMLElement | null = null;
43
+ _id: string;
44
+ id: string;
45
+ private animationFrame?: number;
46
+
47
+ constructor(id: string, options: TokenCalculatorOptions = {}) {
48
+ this._id = id;
49
+ this.id = id;
50
+
51
+ // Default multipliers based on typical framework overhead
52
+ const defaultMultipliers = {
53
+ react: 3.5, // JSX + component boilerplate + imports
54
+ vue: 3.2, // SFC (template + script + style) + composition API
55
+ angular: 4.0, // TypeScript decorators + templates + modules
56
+ svelte: 2.8 // Less overhead but still has markup
57
+ };
58
+
59
+ this.state = {
60
+ juxLines: options.juxLines ?? 0,
61
+ multipliers: { ...defaultMultipliers, ...options.multipliers },
62
+ tokensPerLine: options.tokensPerLine ?? 4, // Average tokens per line of code
63
+ showComparison: options.showComparison ?? true,
64
+ animated: options.animated ?? true,
65
+ style: options.style ?? '',
66
+ class: options.class ?? ''
67
+ };
68
+ }
69
+
70
+ /* -------------------------
71
+ * Calculation Methods
72
+ * ------------------------- */
73
+
74
+ /**
75
+ * Calculate token estimates for all frameworks
76
+ */
77
+ calculate(): TokenEstimate[] {
78
+ const { juxLines, multipliers, tokensPerLine } = this.state;
79
+ const juxTokens = juxLines * tokensPerLine;
80
+
81
+ const estimates: TokenEstimate[] = [
82
+ {
83
+ framework: 'JUX',
84
+ lines: juxLines,
85
+ tokens: juxTokens,
86
+ percentage: 100,
87
+ savings: 0
88
+ }
89
+ ];
90
+
91
+ // Calculate for each framework
92
+ Object.entries(multipliers).forEach(([framework, multiplier]) => {
93
+ const lines = Math.round(juxLines * multiplier);
94
+ const tokens = lines * tokensPerLine;
95
+ const percentage = Math.round((juxTokens / tokens) * 100);
96
+ const savings = tokens - juxTokens;
97
+
98
+ estimates.push({
99
+ framework: framework.charAt(0).toUpperCase() + framework.slice(1),
100
+ lines,
101
+ tokens,
102
+ percentage,
103
+ savings
104
+ });
105
+ });
106
+
107
+ return estimates;
108
+ }
109
+
110
+ /**
111
+ * Get summary statistics
112
+ */
113
+ getSummary() {
114
+ const estimates = this.calculate();
115
+ const jux = estimates[0];
116
+ const others = estimates.slice(1);
117
+
118
+ const avgSavings = others.reduce((sum, e) => sum + e.savings, 0) / others.length;
119
+ const maxSavings = Math.max(...others.map(e => e.savings));
120
+ const avgReduction = others.reduce((sum, e) => sum + (100 - e.percentage), 0) / others.length;
121
+
122
+ return {
123
+ juxTokens: jux.tokens,
124
+ averageTokenSavings: Math.round(avgSavings),
125
+ maxTokenSavings: Math.round(maxSavings),
126
+ averageReduction: Math.round(avgReduction),
127
+ estimates
128
+ };
129
+ }
130
+
131
+ /* -------------------------
132
+ * Fluent API
133
+ * ------------------------- */
134
+
135
+ lines(value: number): this {
136
+ this.state.juxLines = value;
137
+ this._updateDOM();
138
+ return this;
139
+ }
140
+
141
+ multiplier(framework: string, value: number): this {
142
+ this.state.multipliers[framework] = value;
143
+ this._updateDOM();
144
+ return this;
145
+ }
146
+
147
+ tokensPerLine(value: number): this {
148
+ this.state.tokensPerLine = value;
149
+ this._updateDOM();
150
+ return this;
151
+ }
152
+
153
+ showComparison(value: boolean): this {
154
+ this.state.showComparison = value;
155
+ this._updateDOM();
156
+ return this;
157
+ }
158
+
159
+ animated(value: boolean): this {
160
+ this.state.animated = value;
161
+ return this;
162
+ }
163
+
164
+ style(value: string): this {
165
+ this.state.style = value;
166
+ return this;
167
+ }
168
+
169
+ class(value: string): this {
170
+ this.state.class = value;
171
+ return this;
172
+ }
173
+
174
+ /* -------------------------
175
+ * DOM Methods
176
+ * ------------------------- */
177
+
178
+ private _updateDOM(): void {
179
+ if (!this.container) return;
180
+
181
+ const wrapper = this.container.querySelector(`#${this._id}`);
182
+ if (!wrapper) return;
183
+
184
+ const content = wrapper.querySelector('.jux-token-calculator-content');
185
+ if (content) {
186
+ content.innerHTML = this._buildContent();
187
+ if (this.state.animated) {
188
+ this._animateNumbers();
189
+ }
190
+ }
191
+ }
192
+
193
+ private _buildContent(): string {
194
+ const summary = this.getSummary();
195
+ const { estimates } = summary;
196
+
197
+ return `
198
+ <div class="jux-token-summary">
199
+ <div class="jux-token-stat">
200
+ <div class="jux-token-stat-value" data-value="${summary.juxTokens}">0</div>
201
+ <div class="jux-token-stat-label">JUX Tokens</div>
202
+ </div>
203
+ <div class="jux-token-stat jux-token-stat-highlight">
204
+ <div class="jux-token-stat-value" data-value="${summary.averageTokenSavings}">0</div>
205
+ <div class="jux-token-stat-label">Avg. Tokens Saved</div>
206
+ </div>
207
+ <div class="jux-token-stat">
208
+ <div class="jux-token-stat-value" data-value="${summary.averageReduction}">0</div>
209
+ <div class="jux-token-stat-label">% Reduction</div>
210
+ </div>
211
+ </div>
212
+
213
+ ${this.state.showComparison ? `
214
+ <div class="jux-token-comparison">
215
+ ${estimates.map(est => this._buildComparisonBar(est)).join('')}
216
+ </div>
217
+
218
+ <div class="jux-token-details">
219
+ <table class="jux-token-table">
220
+ <thead>
221
+ <tr>
222
+ <th>Framework</th>
223
+ <th>Lines</th>
224
+ <th>Tokens</th>
225
+ <th>vs JUX</th>
226
+ <th>Savings</th>
227
+ </tr>
228
+ </thead>
229
+ <tbody>
230
+ ${estimates.map(est => `
231
+ <tr class="${est.framework === 'JUX' ? 'jux-token-row-highlight' : ''}">
232
+ <td><strong>${est.framework}</strong></td>
233
+ <td>${est.lines.toLocaleString()}</td>
234
+ <td>${est.tokens.toLocaleString()}</td>
235
+ <td>${est.percentage}%</td>
236
+ <td>${est.savings > 0 ? '+' + est.savings.toLocaleString() : '—'}</td>
237
+ </tr>
238
+ `).join('')}
239
+ </tbody>
240
+ </table>
241
+ </div>
242
+ ` : ''}
243
+ `;
244
+ }
245
+
246
+ private _buildComparisonBar(estimate: TokenEstimate): string {
247
+ const isJux = estimate.framework === 'JUX';
248
+ const barClass = isJux ? 'jux-token-bar-jux' : 'jux-token-bar-other';
249
+
250
+ // Calculate bar width relative to largest value
251
+ const maxTokens = Math.max(...this.calculate().map(e => e.tokens));
252
+ const widthPercent = (estimate.tokens / maxTokens) * 100;
253
+
254
+ return `
255
+ <div class="jux-token-bar-row">
256
+ <div class="jux-token-bar-label">${estimate.framework}</div>
257
+ <div class="jux-token-bar-container">
258
+ <div
259
+ class="jux-token-bar ${barClass}"
260
+ style="width: 0%"
261
+ data-width="${widthPercent}"
262
+ >
263
+ <span class="jux-token-bar-value">${estimate.tokens.toLocaleString()} tokens</span>
264
+ </div>
265
+ </div>
266
+ </div>
267
+ `;
268
+ }
269
+
270
+ private _animateNumbers(): void {
271
+ if (!this.container) return;
272
+
273
+ const values = this.container.querySelectorAll('.jux-token-stat-value[data-value]');
274
+ const bars = this.container.querySelectorAll('.jux-token-bar[data-width]');
275
+
276
+ // Animate stat numbers
277
+ values.forEach((el) => {
278
+ const target = parseInt((el as HTMLElement).dataset.value || '0');
279
+ this._animateValue(el as HTMLElement, 0, target, 1000);
280
+ });
281
+
282
+ // Animate bars
283
+ setTimeout(() => {
284
+ bars.forEach((bar) => {
285
+ const width = (bar as HTMLElement).dataset.width;
286
+ (bar as HTMLElement).style.width = `${width}%`;
287
+ });
288
+ }, 100);
289
+ }
290
+
291
+ private _animateValue(element: HTMLElement, start: number, end: number, duration: number): void {
292
+ const startTime = performance.now();
293
+
294
+ const animate = (currentTime: number) => {
295
+ const elapsed = currentTime - startTime;
296
+ const progress = Math.min(elapsed / duration, 1);
297
+
298
+ // Ease out cubic
299
+ const easeProgress = 1 - Math.pow(1 - progress, 3);
300
+ const current = Math.round(start + (end - start) * easeProgress);
301
+
302
+ element.textContent = current.toLocaleString();
303
+
304
+ if (progress < 1) {
305
+ this.animationFrame = requestAnimationFrame(animate);
306
+ }
307
+ };
308
+
309
+ this.animationFrame = requestAnimationFrame(animate);
310
+ }
311
+
312
+ /* -------------------------
313
+ * Render
314
+ * ------------------------- */
315
+
316
+ render(targetId?: string): this {
317
+ let container: HTMLElement;
318
+
319
+ if (targetId) {
320
+ const target = document.querySelector(targetId);
321
+ if (!target || !(target instanceof HTMLElement)) {
322
+ throw new Error(`TokenCalculator: Target element "${targetId}" not found`);
323
+ }
324
+ container = target;
325
+ } else {
326
+ container = getOrCreateContainer(this._id);
327
+ }
328
+
329
+ this.container = container;
330
+
331
+ const wrapper = document.createElement('div');
332
+ wrapper.id = this._id;
333
+ wrapper.className = `jux-token-calculator ${this.state.class}`.trim();
334
+
335
+ if (this.state.style) {
336
+ wrapper.setAttribute('style', this.state.style);
337
+ }
338
+
339
+ wrapper.innerHTML = `
340
+ <div class="jux-token-calculator-header">
341
+ <h3>Token Usage Calculator</h3>
342
+ </div>
343
+ <div class="jux-token-calculator-content">
344
+ ${this._buildContent()}
345
+ </div>
346
+ `;
347
+
348
+ container.appendChild(wrapper);
349
+
350
+ // Trigger animations
351
+ if (this.state.animated) {
352
+ requestAnimationFrame(() => this._animateNumbers());
353
+ }
354
+
355
+ return this;
356
+ }
357
+
358
+ /**
359
+ * Destroy component and cleanup animations
360
+ */
361
+ destroy(): void {
362
+ if (this.animationFrame) {
363
+ cancelAnimationFrame(this.animationFrame);
364
+ }
365
+
366
+ if (this.container) {
367
+ const wrapper = this.container.querySelector(`#${this._id}`);
368
+ if (wrapper) {
369
+ wrapper.remove();
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Factory function
377
+ */
378
+ export function tokenCalculator(id: string, options: TokenCalculatorOptions = {}): TokenCalculator {
379
+ return new TokenCalculator(id, options);
380
+ }
@@ -0,0 +1,244 @@
1
+ import { getOrCreateContainer } from './helpers.js';
2
+
3
+ /**
4
+ * Tooltip component options
5
+ */
6
+ export interface TooltipOptions {
7
+ text?: string;
8
+ position?: 'top' | 'bottom' | 'left' | 'right';
9
+ trigger?: 'hover' | 'click' | 'focus';
10
+ style?: string;
11
+ class?: string;
12
+ }
13
+
14
+ /**
15
+ * Tooltip component state
16
+ */
17
+ type TooltipState = {
18
+ text: string;
19
+ position: string;
20
+ trigger: string;
21
+ style: string;
22
+ class: string;
23
+ };
24
+
25
+ /**
26
+ * Tooltip component - Contextual help on hover
27
+ *
28
+ * Usage:
29
+ * // Attach to existing element
30
+ * jux.tooltip('help-tip', {
31
+ * text: 'This is helpful information',
32
+ * position: 'top'
33
+ * }).attachTo('#help-icon');
34
+ *
35
+ * // Or render with content
36
+ * jux.button('info').label('ℹ️').render('#app');
37
+ * jux.tooltip('info-tip').text('More info').attachTo('#info');
38
+ */
39
+ export class Tooltip {
40
+ state: TooltipState;
41
+ container: HTMLElement | null = null;
42
+ _id: string;
43
+ id: string;
44
+ private _targetElement: HTMLElement | null = null;
45
+ private _tooltipElement: HTMLElement | null = null;
46
+
47
+ constructor(id: string, options: TooltipOptions = {}) {
48
+ this._id = id;
49
+ this.id = id;
50
+
51
+ this.state = {
52
+ text: options.text ?? '',
53
+ position: options.position ?? 'top',
54
+ trigger: options.trigger ?? 'hover',
55
+ style: options.style ?? '',
56
+ class: options.class ?? ''
57
+ };
58
+ }
59
+
60
+ /* -------------------------
61
+ * Fluent API
62
+ * ------------------------- */
63
+
64
+ text(value: string): this {
65
+ this.state.text = value;
66
+ return this;
67
+ }
68
+
69
+ position(value: 'top' | 'bottom' | 'left' | 'right'): this {
70
+ this.state.position = value;
71
+ return this;
72
+ }
73
+
74
+ trigger(value: 'hover' | 'click' | 'focus'): this {
75
+ this.state.trigger = value;
76
+ return this;
77
+ }
78
+
79
+ style(value: string): this {
80
+ this.state.style = value;
81
+ return this;
82
+ }
83
+
84
+ class(value: string): this {
85
+ this.state.class = value;
86
+ return this;
87
+ }
88
+
89
+ /* -------------------------
90
+ * Methods
91
+ * ------------------------- */
92
+
93
+ /**
94
+ * Attach tooltip to an element
95
+ */
96
+ attachTo(target: string | HTMLElement | any): this {
97
+ let targetElement: HTMLElement | null = null;
98
+
99
+ if (typeof target === 'string') {
100
+ // String selector
101
+ const el = document.querySelector(target);
102
+ if (!el || !(el instanceof HTMLElement)) {
103
+ throw new Error(`Tooltip: Target element "${target}" not found`);
104
+ }
105
+ targetElement = el;
106
+ } else if (target instanceof HTMLElement) {
107
+ // Direct HTMLElement
108
+ targetElement = target;
109
+ } else if (target && target.container) {
110
+ // Jux component with container
111
+ targetElement = target.container;
112
+ } else if (target && target._id) {
113
+ // Jux component with _id
114
+ const el = document.getElementById(target._id);
115
+ if (!el) {
116
+ throw new Error(`Tooltip: Target element with id "${target._id}" not found`);
117
+ }
118
+ targetElement = el;
119
+ } else {
120
+ throw new Error('Tooltip: Invalid target element');
121
+ }
122
+
123
+ this._targetElement = targetElement;
124
+ this._createTooltip();
125
+ this._attachEventListeners();
126
+
127
+ return this;
128
+ }
129
+
130
+ show(): void {
131
+ if (this._tooltipElement) {
132
+ this._tooltipElement.classList.add('jux-tooltip-visible');
133
+ this._positionTooltip();
134
+ }
135
+ }
136
+
137
+ hide(): void {
138
+ if (this._tooltipElement) {
139
+ this._tooltipElement.classList.remove('jux-tooltip-visible');
140
+ }
141
+ }
142
+
143
+ /* -------------------------
144
+ * Helpers
145
+ * ------------------------- */
146
+
147
+ private _createTooltip(): void {
148
+ const { text, position, style, class: className } = this.state;
149
+
150
+ const tooltip = document.createElement('div');
151
+ tooltip.className = `jux-tooltip jux-tooltip-${position}`;
152
+ tooltip.id = this._id;
153
+ tooltip.setAttribute('role', 'tooltip');
154
+ tooltip.textContent = text;
155
+
156
+ if (className) {
157
+ tooltip.className += ` ${className}`;
158
+ }
159
+
160
+ if (style) {
161
+ tooltip.setAttribute('style', style);
162
+ }
163
+
164
+ document.body.appendChild(tooltip);
165
+ this._tooltipElement = tooltip;
166
+ }
167
+
168
+ private _attachEventListeners(): void {
169
+ if (!this._targetElement) return;
170
+
171
+ const { trigger } = this.state;
172
+
173
+ if (trigger === 'hover') {
174
+ this._targetElement.addEventListener('mouseenter', () => this.show());
175
+ this._targetElement.addEventListener('mouseleave', () => this.hide());
176
+ } else if (trigger === 'click') {
177
+ this._targetElement.addEventListener('click', () => {
178
+ if (this._tooltipElement?.classList.contains('jux-tooltip-visible')) {
179
+ this.hide();
180
+ } else {
181
+ this.show();
182
+ }
183
+ });
184
+ } else if (trigger === 'focus') {
185
+ this._targetElement.addEventListener('focus', () => this.show());
186
+ this._targetElement.addEventListener('blur', () => this.hide());
187
+ }
188
+ }
189
+
190
+ private _positionTooltip(): void {
191
+ if (!this._targetElement || !this._tooltipElement) return;
192
+
193
+ const targetRect = this._targetElement.getBoundingClientRect();
194
+ const tooltipRect = this._tooltipElement.getBoundingClientRect();
195
+ const { position } = this.state;
196
+
197
+ let top = 0;
198
+ let left = 0;
199
+
200
+ switch (position) {
201
+ case 'top':
202
+ top = targetRect.top - tooltipRect.height - 8;
203
+ left = targetRect.left + (targetRect.width - tooltipRect.width) / 2;
204
+ break;
205
+ case 'bottom':
206
+ top = targetRect.bottom + 8;
207
+ left = targetRect.left + (targetRect.width - tooltipRect.width) / 2;
208
+ break;
209
+ case 'left':
210
+ top = targetRect.top + (targetRect.height - tooltipRect.height) / 2;
211
+ left = targetRect.left - tooltipRect.width - 8;
212
+ break;
213
+ case 'right':
214
+ top = targetRect.top + (targetRect.height - tooltipRect.height) / 2;
215
+ left = targetRect.right + 8;
216
+ break;
217
+ }
218
+
219
+ this._tooltipElement.style.top = `${top + window.scrollY}px`;
220
+ this._tooltipElement.style.left = `${left + window.scrollX}px`;
221
+ }
222
+
223
+ /* -------------------------
224
+ * Render (Alternative usage)
225
+ * ------------------------- */
226
+
227
+ render(targetId?: string): this {
228
+ if (targetId) {
229
+ return this.attachTo(targetId);
230
+ }
231
+ throw new Error('Tooltip requires a target element. Use attachTo(selector) instead.');
232
+ }
233
+
234
+ renderTo(juxComponent: any): this {
235
+ if (!juxComponent?._id) {
236
+ throw new Error('Tooltip.renderTo: Invalid component');
237
+ }
238
+ return this.attachTo(`#${juxComponent._id}`);
239
+ }
240
+ }
241
+
242
+ export function tooltip(id: string, options: TooltipOptions = {}): Tooltip {
243
+ return new Tooltip(id, options);
244
+ }