juxscript 1.0.19 → 1.0.20
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/components/alert.ts +124 -128
- package/lib/components/areachart.ts +169 -287
- package/lib/components/areachartsmooth.ts +2 -2
- package/lib/components/badge.ts +63 -72
- package/lib/components/barchart.ts +120 -48
- package/lib/components/button.ts +92 -60
- package/lib/components/card.ts +97 -121
- package/lib/components/chart-types.ts +159 -0
- package/lib/components/chart-utils.ts +160 -0
- package/lib/components/chart.ts +628 -48
- package/lib/components/checkbox.ts +137 -51
- package/lib/components/code.ts +89 -75
- package/lib/components/container.ts +1 -1
- package/lib/components/datepicker.ts +93 -78
- package/lib/components/dialog.ts +163 -130
- package/lib/components/divider.ts +111 -193
- package/lib/components/docs-data.json +697 -274
- package/lib/components/doughnutchart.ts +125 -57
- package/lib/components/dropdown.ts +172 -85
- package/lib/components/element.ts +66 -61
- package/lib/components/fileupload.ts +142 -171
- package/lib/components/heading.ts +64 -21
- package/lib/components/hero.ts +109 -34
- package/lib/components/icon.ts +247 -0
- package/lib/components/icons.ts +174 -0
- package/lib/components/include.ts +77 -2
- package/lib/components/input.ts +105 -53
- package/lib/components/list.ts +120 -79
- package/lib/components/menu.ts +97 -2
- package/lib/components/modal.ts +144 -63
- package/lib/components/nav.ts +153 -52
- package/lib/components/paragraph.ts +54 -91
- package/lib/components/progress.ts +83 -107
- package/lib/components/radio.ts +151 -52
- package/lib/components/select.ts +110 -102
- package/lib/components/sidebar.ts +148 -105
- package/lib/components/switch.ts +124 -125
- package/lib/components/table.ts +214 -137
- package/lib/components/tabs.ts +194 -113
- package/lib/components/theme-toggle.ts +38 -7
- package/lib/components/tooltip.ts +207 -47
- package/lib/jux.ts +24 -5
- package/package.json +1 -2
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon utilities for components
|
|
3
|
+
* Handles emoji-to-Lucide mapping, direct icon names, and image paths
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const EMOJI_TO_LUCIDE: Record<string, string> = {
|
|
7
|
+
"✔️": "check-circle",
|
|
8
|
+
"✓": "check",
|
|
9
|
+
"❌": "x-circle",
|
|
10
|
+
"✗": "x",
|
|
11
|
+
"🔥": "flame",
|
|
12
|
+
"🚀": "rocket",
|
|
13
|
+
"⚙️": "settings",
|
|
14
|
+
"🏠": "home",
|
|
15
|
+
"👤": "user",
|
|
16
|
+
"💡": "lightbulb",
|
|
17
|
+
"🌈": "rainbow",
|
|
18
|
+
"🧪": "flask-conical",
|
|
19
|
+
"✉️": "mail",
|
|
20
|
+
"📞": "phone",
|
|
21
|
+
"🔍": "search",
|
|
22
|
+
"❤️": "heart",
|
|
23
|
+
"⭐": "star",
|
|
24
|
+
"⚠️": "alert-triangle",
|
|
25
|
+
"ℹ️": "info",
|
|
26
|
+
"❓": "help-circle",
|
|
27
|
+
"👁️": "eye",
|
|
28
|
+
"👁️🗨️": "eye-off",
|
|
29
|
+
"☰": "menu",
|
|
30
|
+
"🕐": "clock",
|
|
31
|
+
"📅": "calendar",
|
|
32
|
+
"⬇️": "chevron-down",
|
|
33
|
+
"⬆️": "chevron-up",
|
|
34
|
+
"⬅️": "chevron-left",
|
|
35
|
+
"➡️": "chevron-right",
|
|
36
|
+
"📈": "arrow-up",
|
|
37
|
+
"📉": "arrow-down",
|
|
38
|
+
"⬇": "download",
|
|
39
|
+
"📤": "upload",
|
|
40
|
+
"📄": "file-text",
|
|
41
|
+
"🗑️": "trash-2",
|
|
42
|
+
"✏️": "edit",
|
|
43
|
+
"📋": "clipboard",
|
|
44
|
+
"🔗": "link",
|
|
45
|
+
"↗️": "external-link",
|
|
46
|
+
"☀️": "sun",
|
|
47
|
+
"🌙": "moon",
|
|
48
|
+
"📊": "bar-chart-3",
|
|
49
|
+
"📁": "folder",
|
|
50
|
+
"💰": "coins",
|
|
51
|
+
"📧": "mail",
|
|
52
|
+
"✅": "square-check",
|
|
53
|
+
"🗓️": "calendar-days",
|
|
54
|
+
"💬": "message-circle",
|
|
55
|
+
"🌐": "globe",
|
|
56
|
+
"🔬": "microscope",
|
|
57
|
+
"💊": "pill",
|
|
58
|
+
"🔒": "lock",
|
|
59
|
+
"⚖️": "scale",
|
|
60
|
+
"🔌": "plug",
|
|
61
|
+
"🔐": "lock-keyhole",
|
|
62
|
+
"🏥": "cross",
|
|
63
|
+
"👥": "users",
|
|
64
|
+
"💚": "heart",
|
|
65
|
+
"💸": "banknote",
|
|
66
|
+
"🧾": "receipt",
|
|
67
|
+
"➕": "plus",
|
|
68
|
+
"➖": "minus",
|
|
69
|
+
"💾": "save"
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const LUCIDE_CDN_URL = "https://unpkg.com/lucide@latest";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Render an icon from emoji, icon name, or image path
|
|
76
|
+
* @param value - Emoji (🚀), icon name (rocket), or image path (/icon.png)
|
|
77
|
+
* @returns HTMLElement containing the icon
|
|
78
|
+
*
|
|
79
|
+
* Usage:
|
|
80
|
+
* const icon = renderIcon('🚀'); // Lucide rocket icon
|
|
81
|
+
* const icon = renderIcon('rocket'); // Lucide rocket icon
|
|
82
|
+
* const icon = renderIcon('/icon.png'); // Image element
|
|
83
|
+
*/
|
|
84
|
+
export function renderIcon(value: string): HTMLElement {
|
|
85
|
+
// Check if it's an image path (contains / or . or starts with http)
|
|
86
|
+
if (value.includes('/') || value.includes('.') || value.startsWith('http')) {
|
|
87
|
+
return createImageIcon(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check if it's an emoji that maps to Lucide
|
|
91
|
+
const lucideName = EMOJI_TO_LUCIDE[value];
|
|
92
|
+
if (lucideName) {
|
|
93
|
+
const element = createVectorIcon(lucideName);
|
|
94
|
+
ensureLucideLoaded();
|
|
95
|
+
return element;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check if it's a direct Lucide icon name (lowercase with hyphens)
|
|
99
|
+
if (/^[a-z][a-z0-9-]*$/.test(value)) {
|
|
100
|
+
const element = createVectorIcon(value);
|
|
101
|
+
ensureLucideLoaded();
|
|
102
|
+
return element;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Fallback: render as emoji
|
|
106
|
+
return createEmojiFallback(value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Render raw emoji without conversion
|
|
111
|
+
* @param emoji - The emoji character
|
|
112
|
+
* @returns HTMLElement containing just the emoji
|
|
113
|
+
*/
|
|
114
|
+
export function renderEmoji(emoji: string): HTMLElement {
|
|
115
|
+
return createEmojiFallback(emoji);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Ensures Lucide is loaded and icons are rendered
|
|
120
|
+
*/
|
|
121
|
+
function ensureLucideLoaded(): void {
|
|
122
|
+
if ((window as any).lucide) {
|
|
123
|
+
// Already loaded, render immediately
|
|
124
|
+
(window as any).lucide.createIcons();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Not loaded yet, inject script
|
|
129
|
+
if (!document.querySelector(`script[src="${LUCIDE_CDN_URL}"]`)) {
|
|
130
|
+
const script = document.createElement('script');
|
|
131
|
+
script.src = LUCIDE_CDN_URL;
|
|
132
|
+
script.onload = () => {
|
|
133
|
+
if ((window as any).lucide) {
|
|
134
|
+
(window as any).lucide.createIcons();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
document.head.appendChild(script);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create Lucide icon element
|
|
143
|
+
*/
|
|
144
|
+
function createVectorIcon(name: string): HTMLElement {
|
|
145
|
+
const iconEl = document.createElement('i');
|
|
146
|
+
iconEl.setAttribute('data-lucide', name);
|
|
147
|
+
iconEl.style.width = '24px';
|
|
148
|
+
iconEl.style.height = '24px';
|
|
149
|
+
iconEl.style.display = 'inline-block';
|
|
150
|
+
return iconEl;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create image icon element
|
|
155
|
+
*/
|
|
156
|
+
function createImageIcon(src: string): HTMLImageElement {
|
|
157
|
+
const img = document.createElement('img');
|
|
158
|
+
img.src = src;
|
|
159
|
+
img.style.width = '24px';
|
|
160
|
+
img.style.height = '24px';
|
|
161
|
+
img.style.display = 'inline-block';
|
|
162
|
+
img.style.objectFit = 'contain';
|
|
163
|
+
return img;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Render native emoji or text
|
|
168
|
+
*/
|
|
169
|
+
function createEmojiFallback(emoji: string): HTMLSpanElement {
|
|
170
|
+
const span = document.createElement('span');
|
|
171
|
+
span.textContent = emoji;
|
|
172
|
+
span.style.display = 'inline-block';
|
|
173
|
+
return span;
|
|
174
|
+
}
|
|
@@ -5,7 +5,7 @@ import { ErrorHandler } from './error-handler.js';
|
|
|
5
5
|
* Auto-detects resource type from URL and provides simple, fluent API
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
type IncludeType = 'stylesheet' | 'script' | 'image' | 'font' | 'preload' | 'prefetch' | 'module';
|
|
8
|
+
type IncludeType = 'stylesheet' | 'script' | 'image' | 'font' | 'preload' | 'prefetch' | 'module' | 'json';
|
|
9
9
|
type IncludeLocation = 'head' | 'body-start' | 'body-end';
|
|
10
10
|
|
|
11
11
|
interface IncludeOptions {
|
|
@@ -35,6 +35,7 @@ export class Include {
|
|
|
35
35
|
private detectType(url: string): IncludeType {
|
|
36
36
|
if (url.endsWith('.css')) return 'stylesheet';
|
|
37
37
|
if (url.endsWith('.js') || url.endsWith('.mjs')) return 'script';
|
|
38
|
+
if (url.endsWith('.json')) return 'json';
|
|
38
39
|
if (url.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i)) return 'image';
|
|
39
40
|
if (url.match(/\.(woff|woff2|ttf|otf|eot)$/i)) return 'font';
|
|
40
41
|
return 'preload';
|
|
@@ -82,6 +83,65 @@ export class Include {
|
|
|
82
83
|
return this;
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
withJson(): this {
|
|
87
|
+
this.type = 'json';
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* -------------------------
|
|
92
|
+
* JSON Fetching
|
|
93
|
+
* ------------------------- */
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Fetch and parse JSON file
|
|
97
|
+
* Returns a Promise that resolves to the parsed JSON data
|
|
98
|
+
*
|
|
99
|
+
* Usage:
|
|
100
|
+
* const config = await jux.include('config.json').asJson();
|
|
101
|
+
* const data = await jux.include('/api/data').asJson();
|
|
102
|
+
*/
|
|
103
|
+
async asJson<T = any>(): Promise<T> {
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetch(this.url);
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const data = await response.json();
|
|
112
|
+
console.log(`✓ JSON loaded: ${this.url}`);
|
|
113
|
+
return data;
|
|
114
|
+
} catch (error: any) {
|
|
115
|
+
ErrorHandler.captureError({
|
|
116
|
+
component: 'Include',
|
|
117
|
+
method: 'asJson',
|
|
118
|
+
message: error.message,
|
|
119
|
+
stack: error.stack,
|
|
120
|
+
timestamp: new Date(),
|
|
121
|
+
context: {
|
|
122
|
+
url: this.url,
|
|
123
|
+
error: 'json_fetch_failed'
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Fetch JSON and execute callback with data
|
|
132
|
+
*
|
|
133
|
+
* Usage:
|
|
134
|
+
* jux.include('config.json').onJson(data => {
|
|
135
|
+
* console.log('Config:', data);
|
|
136
|
+
* });
|
|
137
|
+
*/
|
|
138
|
+
onJson<T = any>(callback: (data: T) => void): this {
|
|
139
|
+
this.asJson<T>().then(callback).catch(error => {
|
|
140
|
+
console.error('Failed to load JSON:', error);
|
|
141
|
+
});
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
|
|
85
145
|
/* -------------------------
|
|
86
146
|
* Options
|
|
87
147
|
* ------------------------- */
|
|
@@ -128,6 +188,12 @@ export class Include {
|
|
|
128
188
|
render(): this {
|
|
129
189
|
if (typeof document === 'undefined') return this;
|
|
130
190
|
|
|
191
|
+
// Don't render JSON type (it's fetched via asJson() instead)
|
|
192
|
+
if (this.type === 'json') {
|
|
193
|
+
console.warn('Include: JSON files should be loaded with .asJson() instead of .render()');
|
|
194
|
+
return this;
|
|
195
|
+
}
|
|
196
|
+
|
|
131
197
|
try {
|
|
132
198
|
this.remove();
|
|
133
199
|
|
|
@@ -284,9 +350,18 @@ export class Include {
|
|
|
284
350
|
* jux.include('app.mjs').withModule();
|
|
285
351
|
* jux.include('custom.js').withJs({ async: true, defer: true });
|
|
286
352
|
* jux.include('https://cdn.com/lib.js').inHead().defer();
|
|
353
|
+
*
|
|
354
|
+
* // For JSON:
|
|
355
|
+
* const data = await jux.include('config.json').asJson();
|
|
356
|
+
* jux.include('data.json').onJson(data => console.log(data));
|
|
287
357
|
*/
|
|
288
358
|
export function include(urlOrFile: string): Include {
|
|
289
359
|
const imp = new Include(urlOrFile);
|
|
290
|
-
|
|
360
|
+
|
|
361
|
+
// Don't auto-render JSON files
|
|
362
|
+
if (imp['type'] !== 'json') {
|
|
363
|
+
imp.render();
|
|
364
|
+
}
|
|
365
|
+
|
|
291
366
|
return imp;
|
|
292
367
|
}
|
package/lib/components/input.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getOrCreateContainer } from './helpers.js';
|
|
2
2
|
import { State } from '../reactivity/state.js';
|
|
3
|
+
import { renderIcon } from './icons.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Input component options
|
|
@@ -9,6 +10,7 @@ export interface InputOptions {
|
|
|
9
10
|
value?: string;
|
|
10
11
|
placeholder?: string;
|
|
11
12
|
label?: string;
|
|
13
|
+
icon?: string;
|
|
12
14
|
required?: boolean;
|
|
13
15
|
disabled?: boolean;
|
|
14
16
|
name?: string;
|
|
@@ -19,7 +21,6 @@ export interface InputOptions {
|
|
|
19
21
|
minLength?: number;
|
|
20
22
|
maxLength?: number;
|
|
21
23
|
pattern?: string;
|
|
22
|
-
onChange?: (value: string) => void;
|
|
23
24
|
onValidate?: (value: string) => boolean | string;
|
|
24
25
|
style?: string;
|
|
25
26
|
class?: string;
|
|
@@ -33,6 +34,7 @@ type InputState = {
|
|
|
33
34
|
value: string;
|
|
34
35
|
placeholder: string;
|
|
35
36
|
label: string;
|
|
37
|
+
icon: string;
|
|
36
38
|
required: boolean;
|
|
37
39
|
disabled: boolean;
|
|
38
40
|
name: string;
|
|
@@ -48,27 +50,22 @@ type InputState = {
|
|
|
48
50
|
errorMessage?: string;
|
|
49
51
|
};
|
|
50
52
|
|
|
51
|
-
/**
|
|
52
|
-
* Input component
|
|
53
|
-
*/
|
|
54
53
|
export class Input {
|
|
55
54
|
state: InputState;
|
|
56
55
|
container: HTMLElement | null = null;
|
|
57
56
|
_id: string;
|
|
58
57
|
id: string;
|
|
59
|
-
private _onChange?: (value: string) => void;
|
|
60
58
|
private _onValidate?: (value: string) => boolean | string;
|
|
61
59
|
|
|
62
|
-
// Store bind() instructions
|
|
60
|
+
// Store bind() instructions (DOM events only)
|
|
63
61
|
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
64
62
|
|
|
65
|
-
// Store sync() instructions
|
|
63
|
+
// Store sync() instructions (state synchronization)
|
|
66
64
|
private _syncBindings: Array<{ property: string, stateObj: State<any>, toState?: Function, toComponent?: Function }> = [];
|
|
67
65
|
|
|
68
66
|
constructor(id: string, options: InputOptions = {}) {
|
|
69
67
|
this._id = id;
|
|
70
68
|
this.id = id;
|
|
71
|
-
this._onChange = options.onChange;
|
|
72
69
|
this._onValidate = options.onValidate;
|
|
73
70
|
|
|
74
71
|
this.state = {
|
|
@@ -76,6 +73,7 @@ export class Input {
|
|
|
76
73
|
value: options.value ?? '',
|
|
77
74
|
placeholder: options.placeholder ?? '',
|
|
78
75
|
label: options.label ?? '',
|
|
76
|
+
icon: options.icon ?? '',
|
|
79
77
|
required: options.required ?? false,
|
|
80
78
|
disabled: options.disabled ?? false,
|
|
81
79
|
name: options.name ?? id,
|
|
@@ -116,6 +114,11 @@ export class Input {
|
|
|
116
114
|
return this;
|
|
117
115
|
}
|
|
118
116
|
|
|
117
|
+
icon(value: string): this {
|
|
118
|
+
this.state.icon = value;
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
|
|
119
122
|
required(value: boolean): this {
|
|
120
123
|
this.state.required = value;
|
|
121
124
|
return this;
|
|
@@ -183,11 +186,6 @@ export class Input {
|
|
|
183
186
|
return this;
|
|
184
187
|
}
|
|
185
188
|
|
|
186
|
-
onChange(handler: (value: string) => void): this {
|
|
187
|
-
this._onChange = handler;
|
|
188
|
-
return this;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
189
|
onValidate(handler: (value: string) => boolean | string): this {
|
|
192
190
|
this._onValidate = handler;
|
|
193
191
|
return this;
|
|
@@ -195,6 +193,7 @@ export class Input {
|
|
|
195
193
|
|
|
196
194
|
/**
|
|
197
195
|
* Bind event handler (stores for wiring in render)
|
|
196
|
+
* DOM events only: input, change, blur, focus, etc.
|
|
198
197
|
*/
|
|
199
198
|
bind(event: string, handler: Function): this {
|
|
200
199
|
this._bindings.push({ event, handler });
|
|
@@ -210,6 +209,9 @@ export class Input {
|
|
|
210
209
|
* @param toComponent - Optional transform function when going from state to component
|
|
211
210
|
*/
|
|
212
211
|
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
212
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
213
|
+
throw new Error(`Input.sync: Expected a State object for property "${property}"`);
|
|
214
|
+
}
|
|
213
215
|
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
214
216
|
return this;
|
|
215
217
|
}
|
|
@@ -356,8 +358,8 @@ export class Input {
|
|
|
356
358
|
* ------------------------- */
|
|
357
359
|
|
|
358
360
|
render(targetId?: string): this {
|
|
361
|
+
// === 1. SETUP: Get container ===
|
|
359
362
|
let container: HTMLElement;
|
|
360
|
-
|
|
361
363
|
if (targetId) {
|
|
362
364
|
const target = document.querySelector(targetId);
|
|
363
365
|
if (!target || !(target instanceof HTMLElement)) {
|
|
@@ -367,21 +369,22 @@ export class Input {
|
|
|
367
369
|
} else {
|
|
368
370
|
container = getOrCreateContainer(this._id);
|
|
369
371
|
}
|
|
370
|
-
|
|
371
372
|
this.container = container;
|
|
372
|
-
const { type, value, placeholder, label, required, disabled, name, rows, min, max, step, minLength, maxLength, pattern, style, class: className } = this.state;
|
|
373
373
|
|
|
374
|
+
// === 2. PREPARE: Destructure state and check bindings ===
|
|
375
|
+
const {
|
|
376
|
+
type, value, placeholder, label, icon, required, disabled, name, rows,
|
|
377
|
+
min, max, step, minLength, maxLength, pattern, style, class: className
|
|
378
|
+
} = this.state;
|
|
379
|
+
|
|
380
|
+
const hasValueSync = this._syncBindings.some(binding => binding.property === 'value');
|
|
381
|
+
|
|
382
|
+
// === 3. BUILD: Create all DOM elements ===
|
|
374
383
|
const wrapper = document.createElement('div');
|
|
375
384
|
wrapper.className = 'jux-input';
|
|
376
385
|
wrapper.id = this._id;
|
|
377
|
-
|
|
378
|
-
if (
|
|
379
|
-
wrapper.className += ` ${className}`;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (style) {
|
|
383
|
-
wrapper.setAttribute('style', style);
|
|
384
|
-
}
|
|
386
|
+
if (className) wrapper.className += ` ${className}`;
|
|
387
|
+
if (style) wrapper.setAttribute('style', style);
|
|
385
388
|
|
|
386
389
|
// Label
|
|
387
390
|
const labelEl = document.createElement('label');
|
|
@@ -394,13 +397,24 @@ export class Input {
|
|
|
394
397
|
requiredSpan.textContent = ' *';
|
|
395
398
|
labelEl.appendChild(requiredSpan);
|
|
396
399
|
}
|
|
397
|
-
if (label)
|
|
398
|
-
|
|
400
|
+
if (label) wrapper.appendChild(labelEl);
|
|
401
|
+
|
|
402
|
+
// Input container
|
|
403
|
+
const inputContainer = document.createElement('div');
|
|
404
|
+
inputContainer.className = 'jux-input-container';
|
|
405
|
+
if (icon) inputContainer.classList.add('jux-input-with-icon');
|
|
406
|
+
|
|
407
|
+
// Icon
|
|
408
|
+
if (icon) {
|
|
409
|
+
const iconEl = document.createElement('span');
|
|
410
|
+
iconEl.className = 'jux-input-icon';
|
|
411
|
+
const iconElement = renderIcon(icon);
|
|
412
|
+
iconEl.appendChild(iconElement);
|
|
413
|
+
inputContainer.appendChild(iconEl);
|
|
399
414
|
}
|
|
400
415
|
|
|
401
|
-
// Input/Textarea
|
|
416
|
+
// Input/Textarea element
|
|
402
417
|
let inputEl: HTMLInputElement | HTMLTextAreaElement;
|
|
403
|
-
|
|
404
418
|
if (type === 'textarea') {
|
|
405
419
|
inputEl = document.createElement('textarea');
|
|
406
420
|
inputEl.rows = rows;
|
|
@@ -431,54 +445,51 @@ export class Input {
|
|
|
431
445
|
inputEl.required = required;
|
|
432
446
|
inputEl.disabled = disabled;
|
|
433
447
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const target = e.target as HTMLInputElement | HTMLTextAreaElement;
|
|
437
|
-
this.state.value = target.value;
|
|
438
|
-
|
|
439
|
-
this._clearError();
|
|
440
|
-
|
|
441
|
-
if (this._onChange) {
|
|
442
|
-
this._onChange(target.value);
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
inputEl.addEventListener('blur', () => {
|
|
447
|
-
this.validate();
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
wrapper.appendChild(inputEl);
|
|
448
|
+
inputContainer.appendChild(inputEl);
|
|
449
|
+
wrapper.appendChild(inputContainer);
|
|
451
450
|
|
|
452
|
-
// Error
|
|
451
|
+
// Error element
|
|
453
452
|
const errorEl = document.createElement('div');
|
|
454
453
|
errorEl.className = 'jux-input-error';
|
|
455
454
|
errorEl.id = `${this._id}-error`;
|
|
456
455
|
errorEl.style.display = 'none';
|
|
457
456
|
wrapper.appendChild(errorEl);
|
|
458
457
|
|
|
459
|
-
// Character counter
|
|
458
|
+
// Character counter
|
|
460
459
|
if (maxLength && (type === 'text' || type === 'textarea')) {
|
|
461
460
|
const counterEl = document.createElement('div');
|
|
462
461
|
counterEl.className = 'jux-input-counter';
|
|
463
462
|
counterEl.id = `${this._id}-counter`;
|
|
464
463
|
counterEl.textContent = `${value.length}/${maxLength}`;
|
|
464
|
+
wrapper.appendChild(counterEl);
|
|
465
465
|
|
|
466
|
+
// Wire counter immediately
|
|
466
467
|
inputEl.addEventListener('input', () => {
|
|
467
468
|
counterEl.textContent = `${inputEl.value.length}/${maxLength}`;
|
|
468
469
|
});
|
|
470
|
+
}
|
|
469
471
|
|
|
470
|
-
|
|
472
|
+
// === 4. WIRE: Add event listeners ===
|
|
473
|
+
|
|
474
|
+
// Default input handler (only if NOT using sync)
|
|
475
|
+
if (!hasValueSync) {
|
|
476
|
+
inputEl.addEventListener('input', () => {
|
|
477
|
+
this.state.value = inputEl.value;
|
|
478
|
+
this._clearError();
|
|
479
|
+
});
|
|
471
480
|
}
|
|
472
481
|
|
|
473
|
-
|
|
474
|
-
|
|
482
|
+
// Always add blur validation
|
|
483
|
+
inputEl.addEventListener('blur', () => {
|
|
484
|
+
this.validate();
|
|
485
|
+
});
|
|
475
486
|
|
|
476
|
-
//
|
|
487
|
+
// Wire up custom event bindings (from .bind() calls)
|
|
477
488
|
this._bindings.forEach(({ event, handler }) => {
|
|
478
489
|
wrapper.addEventListener(event, handler as EventListener);
|
|
479
490
|
});
|
|
480
491
|
|
|
481
|
-
//
|
|
492
|
+
// Wire up sync bindings (from .sync() calls)
|
|
482
493
|
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
483
494
|
if (property === 'value') {
|
|
484
495
|
// Default transforms
|
|
@@ -503,13 +514,18 @@ export class Input {
|
|
|
503
514
|
inputEl.addEventListener('input', () => {
|
|
504
515
|
if (isUpdating) return;
|
|
505
516
|
isUpdating = true;
|
|
517
|
+
|
|
506
518
|
const transformed = transformToState(inputEl.value);
|
|
519
|
+
this.state.value = inputEl.value;
|
|
520
|
+
this._clearError();
|
|
521
|
+
|
|
507
522
|
stateObj.set(transformed);
|
|
523
|
+
|
|
508
524
|
setTimeout(() => { isUpdating = false; }, 0);
|
|
509
525
|
});
|
|
510
526
|
}
|
|
511
527
|
else if (property === 'label') {
|
|
512
|
-
// Sync label
|
|
528
|
+
// Sync label (one-way: state → component)
|
|
513
529
|
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
514
530
|
|
|
515
531
|
stateObj.subscribe((val: any) => {
|
|
@@ -520,6 +536,17 @@ export class Input {
|
|
|
520
536
|
}
|
|
521
537
|
});
|
|
522
538
|
|
|
539
|
+
// === 5. RENDER: Append to DOM and finalize ===
|
|
540
|
+
container.appendChild(wrapper);
|
|
541
|
+
this._injectDefaultStyles();
|
|
542
|
+
|
|
543
|
+
// Trigger Lucide icon rendering
|
|
544
|
+
requestAnimationFrame(() => {
|
|
545
|
+
if ((window as any).lucide) {
|
|
546
|
+
(window as any).lucide.createIcons();
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
523
550
|
return this;
|
|
524
551
|
}
|
|
525
552
|
|
|
@@ -534,6 +561,31 @@ export class Input {
|
|
|
534
561
|
margin-bottom: 16px;
|
|
535
562
|
}
|
|
536
563
|
|
|
564
|
+
.jux-input-container {
|
|
565
|
+
position: relative;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.jux-input-with-icon .jux-input-element {
|
|
569
|
+
padding-left: 40px;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.jux-input-icon {
|
|
573
|
+
position: absolute;
|
|
574
|
+
left: 12px;
|
|
575
|
+
top: 50%;
|
|
576
|
+
transform: translateY(-50%);
|
|
577
|
+
display: flex;
|
|
578
|
+
align-items: center;
|
|
579
|
+
justify-content: center;
|
|
580
|
+
color: #6b7280;
|
|
581
|
+
pointer-events: none;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.jux-input-icon svg {
|
|
585
|
+
width: 18px;
|
|
586
|
+
height: 18px;
|
|
587
|
+
}
|
|
588
|
+
|
|
537
589
|
.jux-input-label {
|
|
538
590
|
display: block;
|
|
539
591
|
margin-bottom: 6px;
|