juxscript 1.0.3 → 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.
- package/README.md +37 -92
- package/bin/cli.js +57 -56
- package/lib/components/alert.ts +240 -0
- package/lib/components/app.ts +216 -82
- package/lib/components/badge.ts +164 -0
- package/lib/components/button.ts +188 -53
- package/lib/components/card.ts +75 -61
- package/lib/components/chart.ts +17 -15
- package/lib/components/checkbox.ts +228 -0
- package/lib/components/code.ts +66 -152
- package/lib/components/container.ts +104 -208
- package/lib/components/data.ts +1 -3
- package/lib/components/datepicker.ts +226 -0
- package/lib/components/dialog.ts +258 -0
- package/lib/components/docs-data.json +1697 -388
- package/lib/components/dropdown.ts +244 -0
- package/lib/components/element.ts +271 -0
- package/lib/components/fileupload.ts +319 -0
- package/lib/components/footer.ts +37 -18
- package/lib/components/header.ts +53 -33
- package/lib/components/heading.ts +119 -0
- package/lib/components/helpers.ts +34 -0
- package/lib/components/hero.ts +57 -31
- package/lib/components/include.ts +292 -0
- package/lib/components/input.ts +166 -78
- package/lib/components/layout.ts +144 -18
- package/lib/components/list.ts +83 -74
- package/lib/components/loading.ts +263 -0
- package/lib/components/main.ts +43 -17
- package/lib/components/menu.ts +108 -24
- package/lib/components/modal.ts +50 -21
- package/lib/components/nav.ts +60 -18
- package/lib/components/paragraph.ts +111 -0
- package/lib/components/progress.ts +276 -0
- package/lib/components/radio.ts +236 -0
- package/lib/components/req.ts +300 -0
- package/lib/components/script.ts +33 -74
- package/lib/components/select.ts +247 -0
- package/lib/components/sidebar.ts +86 -36
- package/lib/components/style.ts +47 -70
- package/lib/components/switch.ts +261 -0
- package/lib/components/table.ts +47 -24
- package/lib/components/tabs.ts +105 -63
- package/lib/components/theme-toggle.ts +361 -0
- package/lib/components/token-calculator.ts +380 -0
- package/lib/components/tooltip.ts +244 -0
- package/lib/components/view.ts +36 -20
- package/lib/components/write.ts +284 -0
- package/lib/globals.d.ts +21 -0
- package/lib/jux.ts +172 -68
- package/lib/presets/notion.css +521 -0
- package/lib/presets/notion.jux +27 -0
- package/lib/reactivity/state.ts +364 -0
- package/machinery/compiler.js +126 -38
- package/machinery/generators/html.js +2 -3
- package/machinery/server.js +2 -2
- package/package.json +29 -3
- package/lib/components/import.ts +0 -430
- package/lib/components/node.ts +0 -200
- package/lib/components/reactivity.js +0 -104
- package/lib/components/theme.ts +0 -97
- package/lib/layouts/notion.css +0 -258
- package/lib/styles/base-theme.css +0 -186
- package/lib/styles/dark-theme.css +0 -144
- package/lib/styles/light-theme.css +0 -144
- package/lib/styles/tokens/dark.css +0 -86
- package/lib/styles/tokens/light.css +0 -86
- package/lib/templates/index.juxt +0 -33
- package/lib/themes/dark.css +0 -86
- package/lib/themes/light.css +0 -86
- /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
|
+
}
|