juxscript 1.0.0
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 +292 -0
- package/bin/cli.js +149 -0
- package/lib/adapters/base-adapter.js +35 -0
- package/lib/adapters/index.js +33 -0
- package/lib/adapters/mysql-adapter.js +65 -0
- package/lib/adapters/postgres-adapter.js +70 -0
- package/lib/adapters/sqlite-adapter.js +56 -0
- package/lib/components/app.ts +124 -0
- package/lib/components/button.ts +136 -0
- package/lib/components/card.ts +205 -0
- package/lib/components/chart.ts +125 -0
- package/lib/components/code.ts +242 -0
- package/lib/components/container.ts +282 -0
- package/lib/components/data.ts +105 -0
- package/lib/components/docs-data.json +1211 -0
- package/lib/components/error-handler.ts +285 -0
- package/lib/components/footer.ts +146 -0
- package/lib/components/header.ts +167 -0
- package/lib/components/hero.ts +170 -0
- package/lib/components/import.ts +430 -0
- package/lib/components/input.ts +175 -0
- package/lib/components/layout.ts +113 -0
- package/lib/components/list.ts +392 -0
- package/lib/components/main.ts +111 -0
- package/lib/components/menu.ts +170 -0
- package/lib/components/modal.ts +216 -0
- package/lib/components/nav.ts +136 -0
- package/lib/components/node.ts +200 -0
- package/lib/components/reactivity.js +104 -0
- package/lib/components/script.ts +152 -0
- package/lib/components/sidebar.ts +168 -0
- package/lib/components/style.ts +129 -0
- package/lib/components/table.ts +279 -0
- package/lib/components/tabs.ts +191 -0
- package/lib/components/theme.ts +97 -0
- package/lib/components/view.ts +174 -0
- package/lib/jux.ts +203 -0
- package/lib/layouts/default.css +260 -0
- package/lib/layouts/default.jux +8 -0
- package/lib/layouts/figma.css +334 -0
- package/lib/layouts/figma.jux +0 -0
- package/lib/layouts/notion.css +258 -0
- package/lib/styles/base-theme.css +186 -0
- package/lib/styles/dark-theme.css +144 -0
- package/lib/styles/global.css +1131 -0
- package/lib/styles/light-theme.css +144 -0
- package/lib/styles/tokens/dark.css +86 -0
- package/lib/styles/tokens/light.css +86 -0
- package/lib/themes/dark.css +86 -0
- package/lib/themes/light.css +86 -0
- package/lib/utils/path-resolver.js +23 -0
- package/machinery/compiler.js +262 -0
- package/machinery/doc-generator.js +160 -0
- package/machinery/generators/css.js +128 -0
- package/machinery/generators/html.js +108 -0
- package/machinery/imports.js +155 -0
- package/machinery/server.js +185 -0
- package/machinery/validators/file-validator.js +123 -0
- package/machinery/watcher.js +148 -0
- package/package.json +58 -0
- package/types/globals.d.ts +16 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Reactive, getOrCreateContainer } from './reactivity.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Input component options
|
|
5
|
+
*/
|
|
6
|
+
export interface InputOptions {
|
|
7
|
+
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search';
|
|
8
|
+
label?: string;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
value?: string;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
onChange?: (value: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Input component state
|
|
18
|
+
*/
|
|
19
|
+
type InputState = {
|
|
20
|
+
type: string;
|
|
21
|
+
label: string;
|
|
22
|
+
placeholder: string;
|
|
23
|
+
value: string;
|
|
24
|
+
required: boolean;
|
|
25
|
+
disabled: boolean;
|
|
26
|
+
onChange: ((value: string) => void) | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Input component
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* const input = jux.input('myInput', {
|
|
34
|
+
* label: 'Email',
|
|
35
|
+
* type: 'email',
|
|
36
|
+
* placeholder: 'Enter your email',
|
|
37
|
+
* onChange: (value) => console.log(value)
|
|
38
|
+
* });
|
|
39
|
+
* input.render();
|
|
40
|
+
*/
|
|
41
|
+
export class Input extends Reactive {
|
|
42
|
+
state!: InputState;
|
|
43
|
+
container: HTMLElement | null = null;
|
|
44
|
+
|
|
45
|
+
constructor(componentId: string, options: InputOptions = {}) {
|
|
46
|
+
super();
|
|
47
|
+
this._setComponentId(componentId);
|
|
48
|
+
|
|
49
|
+
this.state = this._createReactiveState({
|
|
50
|
+
type: options.type ?? 'text',
|
|
51
|
+
label: options.label ?? '',
|
|
52
|
+
placeholder: options.placeholder ?? '',
|
|
53
|
+
value: options.value ?? '',
|
|
54
|
+
required: options.required ?? false,
|
|
55
|
+
disabled: options.disabled ?? false,
|
|
56
|
+
onChange: options.onChange ?? null
|
|
57
|
+
}) as InputState;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* -------------------------
|
|
61
|
+
* Fluent API
|
|
62
|
+
* ------------------------- */
|
|
63
|
+
|
|
64
|
+
type(value: string): this {
|
|
65
|
+
this.state.type = value;
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
label(value: string): this {
|
|
70
|
+
this.state.label = value;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
placeholder(value: string): this {
|
|
75
|
+
this.state.placeholder = value;
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
value(value: string): this {
|
|
80
|
+
this.state.value = value;
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
required(value: boolean): this {
|
|
85
|
+
this.state.required = value;
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
disabled(value: boolean): this {
|
|
90
|
+
this.state.disabled = value;
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
onChange(callback: (value: string) => void): this {
|
|
95
|
+
this.state.onChange = callback;
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* -------------------------
|
|
100
|
+
* Render
|
|
101
|
+
* ------------------------- */
|
|
102
|
+
|
|
103
|
+
render(targetId?: string): this {
|
|
104
|
+
let container: HTMLElement;
|
|
105
|
+
|
|
106
|
+
if (targetId) {
|
|
107
|
+
const target = document.querySelector(targetId);
|
|
108
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
109
|
+
throw new Error(`Input: Target element "${targetId}" not found`);
|
|
110
|
+
}
|
|
111
|
+
container = target;
|
|
112
|
+
} else {
|
|
113
|
+
container = getOrCreateContainer(this._componentId) as HTMLElement;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.container = container;
|
|
117
|
+
const { type, label, placeholder, value, required, disabled, onChange } = this.state;
|
|
118
|
+
|
|
119
|
+
const wrapper = document.createElement('div');
|
|
120
|
+
wrapper.className = 'jux-input-wrapper';
|
|
121
|
+
wrapper.id = this._componentId;
|
|
122
|
+
|
|
123
|
+
if (label) {
|
|
124
|
+
const labelEl = document.createElement('label');
|
|
125
|
+
labelEl.className = 'jux-input-label';
|
|
126
|
+
labelEl.textContent = label;
|
|
127
|
+
labelEl.setAttribute('for', `${this._componentId}-input`);
|
|
128
|
+
wrapper.appendChild(labelEl);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const input = document.createElement('input');
|
|
132
|
+
input.id = `${this._componentId}-input`;
|
|
133
|
+
input.className = 'jux-input';
|
|
134
|
+
input.type = type;
|
|
135
|
+
input.placeholder = placeholder;
|
|
136
|
+
input.value = value;
|
|
137
|
+
input.required = required;
|
|
138
|
+
input.disabled = disabled;
|
|
139
|
+
|
|
140
|
+
wrapper.appendChild(input);
|
|
141
|
+
container.appendChild(wrapper);
|
|
142
|
+
|
|
143
|
+
// Event binding - onChange
|
|
144
|
+
if (onChange) {
|
|
145
|
+
input.addEventListener('input', (e) => {
|
|
146
|
+
const target = e.target as HTMLInputElement;
|
|
147
|
+
onChange(target.value);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return this;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Render to another Jux component's container
|
|
156
|
+
*/
|
|
157
|
+
renderTo(juxComponent: any): this {
|
|
158
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
159
|
+
throw new Error('Input.renderTo: Invalid component - not an object');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
|
|
163
|
+
throw new Error('Input.renderTo: Invalid component - missing _componentId (not a Jux component)');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return this.render(`#${juxComponent._componentId}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Factory helper
|
|
172
|
+
*/
|
|
173
|
+
export function input(componentId: string, options: InputOptions = {}): Input {
|
|
174
|
+
return new Input(componentId, options);
|
|
175
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { ErrorHandler } from './error-handler.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Layout - Load a JUX layout file
|
|
5
|
+
* Auto-loads when file is set
|
|
6
|
+
*/
|
|
7
|
+
export class Layout {
|
|
8
|
+
private _juxFile: string;
|
|
9
|
+
private _loaded: boolean = false;
|
|
10
|
+
|
|
11
|
+
constructor(juxFile: string = '') {
|
|
12
|
+
this._juxFile = juxFile;
|
|
13
|
+
|
|
14
|
+
// Auto-load if file provided
|
|
15
|
+
if (juxFile) {
|
|
16
|
+
this.load();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Set the JUX file to load
|
|
22
|
+
*/
|
|
23
|
+
file(juxFile: string): this {
|
|
24
|
+
this._juxFile = juxFile;
|
|
25
|
+
this._loaded = false;
|
|
26
|
+
this.load();
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get the current JUX file path
|
|
32
|
+
*/
|
|
33
|
+
getFile(): string {
|
|
34
|
+
return this._juxFile;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Normalize path to absolute URL from site root
|
|
39
|
+
*/
|
|
40
|
+
private normalizePath(path: string): string {
|
|
41
|
+
// If already a full URL, return as-is
|
|
42
|
+
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
43
|
+
return path;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Convert relative path to absolute
|
|
47
|
+
// Remove leading './' if present
|
|
48
|
+
let cleanPath = path.replace(/^\.\//, '');
|
|
49
|
+
|
|
50
|
+
// Ensure it starts with /
|
|
51
|
+
if (!cleanPath.startsWith('/')) {
|
|
52
|
+
cleanPath = '/' + cleanPath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Return absolute URL with origin
|
|
56
|
+
return new URL(cleanPath, window.location.origin).href;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Load the layout
|
|
61
|
+
* This will dynamically import the compiled JS file
|
|
62
|
+
*/
|
|
63
|
+
async load(): Promise<this> {
|
|
64
|
+
if (typeof document === 'undefined') {
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// Convert .jux to .js for the compiled output
|
|
70
|
+
let jsFile = this._juxFile.replace(/\.jux$/, '.js');
|
|
71
|
+
|
|
72
|
+
// Normalize to absolute URL for browser import
|
|
73
|
+
jsFile = this.normalizePath(jsFile);
|
|
74
|
+
|
|
75
|
+
console.log(`Loading layout: ${jsFile}`);
|
|
76
|
+
|
|
77
|
+
// Dynamic import of the layout module
|
|
78
|
+
const layoutModule = await import(jsFile);
|
|
79
|
+
|
|
80
|
+
// If the module has an init or default export, call it
|
|
81
|
+
if (typeof layoutModule.default === 'function') {
|
|
82
|
+
await layoutModule.default();
|
|
83
|
+
} else if (typeof layoutModule.init === 'function') {
|
|
84
|
+
await layoutModule.init();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this._loaded = true;
|
|
88
|
+
console.log(`✓ Layout loaded: ${this._juxFile}`);
|
|
89
|
+
} catch (error: any) {
|
|
90
|
+
ErrorHandler.captureError({
|
|
91
|
+
component: 'Layout',
|
|
92
|
+
method: 'load',
|
|
93
|
+
message: `Failed to load layout: ${error.message}`,
|
|
94
|
+
stack: error.stack,
|
|
95
|
+
timestamp: new Date(),
|
|
96
|
+
context: {
|
|
97
|
+
juxFile: this._juxFile,
|
|
98
|
+
jsFile: this._juxFile.replace(/\.jux$/, '.js'),
|
|
99
|
+
errorCode: error.code
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if layout is loaded
|
|
109
|
+
*/
|
|
110
|
+
isLoaded(): boolean {
|
|
111
|
+
return this._loaded;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { Reactive, getOrCreateContainer } from './reactivity.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* List item interface
|
|
5
|
+
*/
|
|
6
|
+
export interface ListItem {
|
|
7
|
+
icon?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
body?: string;
|
|
10
|
+
type?: 'success' | 'warning' | 'error' | 'info' | 'default' | string;
|
|
11
|
+
metadata?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* List component options
|
|
16
|
+
*/
|
|
17
|
+
export interface ListOptions {
|
|
18
|
+
items?: ListItem[];
|
|
19
|
+
header?: string;
|
|
20
|
+
gap?: string;
|
|
21
|
+
direction?: 'vertical' | 'horizontal';
|
|
22
|
+
selectable?: boolean;
|
|
23
|
+
selectedIndex?: number | null;
|
|
24
|
+
onItemClick?: (item: ListItem, index: number, e: Event) => void;
|
|
25
|
+
onItemDoubleClick?: (item: ListItem, index: number, e: Event) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* List component state
|
|
30
|
+
*/
|
|
31
|
+
type ListState = {
|
|
32
|
+
items: ListItem[];
|
|
33
|
+
header: string;
|
|
34
|
+
gap: string;
|
|
35
|
+
direction: string;
|
|
36
|
+
selectable: boolean;
|
|
37
|
+
selectedIndex: number | null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* List component - renders a list of items with optional header
|
|
42
|
+
*
|
|
43
|
+
* Usage:
|
|
44
|
+
* const myList = jux.list('myList', {
|
|
45
|
+
* header: '✓ Accomplishments',
|
|
46
|
+
* items: [
|
|
47
|
+
* { icon: '✓', title: 'Task 1', body: 'Description', type: 'success' },
|
|
48
|
+
* { icon: '⚠️', title: 'Task 2', body: 'Description', type: 'warning' }
|
|
49
|
+
* ],
|
|
50
|
+
* gap: '0.75rem',
|
|
51
|
+
* selectable: true,
|
|
52
|
+
* onItemClick: (item, index) => console.log('Clicked:', item, index)
|
|
53
|
+
* });
|
|
54
|
+
* myList.render();
|
|
55
|
+
*
|
|
56
|
+
* // Add item
|
|
57
|
+
* myList.add({ icon: '🎉', title: 'New Task', body: 'Done!', type: 'success' });
|
|
58
|
+
*
|
|
59
|
+
* // Remove item by index
|
|
60
|
+
* myList.remove(1);
|
|
61
|
+
*
|
|
62
|
+
* // Move item from index 0 to index 2
|
|
63
|
+
* myList.move(0, 2);
|
|
64
|
+
*/
|
|
65
|
+
export class List extends Reactive {
|
|
66
|
+
state!: ListState;
|
|
67
|
+
container: HTMLElement | null = null;
|
|
68
|
+
private _onItemClick: ((item: ListItem, index: number, e: Event) => void) | null;
|
|
69
|
+
private _onItemDoubleClick: ((item: ListItem, index: number, e: Event) => void) | null;
|
|
70
|
+
|
|
71
|
+
constructor(componentId: string, options: ListOptions = {}) {
|
|
72
|
+
super();
|
|
73
|
+
this._setComponentId(componentId);
|
|
74
|
+
|
|
75
|
+
this.state = this._createReactiveState({
|
|
76
|
+
items: options.items ?? [],
|
|
77
|
+
header: options.header ?? '',
|
|
78
|
+
gap: options.gap ?? '0.5rem',
|
|
79
|
+
direction: options.direction ?? 'vertical',
|
|
80
|
+
selectable: options.selectable ?? false,
|
|
81
|
+
selectedIndex: options.selectedIndex ?? null
|
|
82
|
+
}) as ListState;
|
|
83
|
+
|
|
84
|
+
this._onItemClick = options.onItemClick ?? null;
|
|
85
|
+
this._onItemDoubleClick = options.onItemDoubleClick ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* -------------------------
|
|
89
|
+
* Fluent API
|
|
90
|
+
* ------------------------- */
|
|
91
|
+
|
|
92
|
+
items(value: ListItem[]): this {
|
|
93
|
+
this.state.items = value;
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
header(value: string): this {
|
|
98
|
+
this.state.header = value;
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
gap(value: string): this {
|
|
103
|
+
this.state.gap = value;
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
direction(value: 'vertical' | 'horizontal'): this {
|
|
108
|
+
this.state.direction = value;
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
selectable(value: boolean): this {
|
|
113
|
+
this.state.selectable = value;
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* -------------------------
|
|
118
|
+
* List operations
|
|
119
|
+
* ------------------------- */
|
|
120
|
+
|
|
121
|
+
add(item: ListItem, index?: number): this {
|
|
122
|
+
const items = [...this.state.items];
|
|
123
|
+
|
|
124
|
+
if (typeof index === 'number' && index >= 0 && index <= items.length) {
|
|
125
|
+
items.splice(index, 0, item);
|
|
126
|
+
} else {
|
|
127
|
+
index = items.length;
|
|
128
|
+
items.push(item);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.state.items = items;
|
|
132
|
+
this.emit('itemAdded', { item, index });
|
|
133
|
+
this._updateDOM();
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
remove(index: number): this {
|
|
138
|
+
if (typeof index !== 'number' || index < 0 || index >= this.state.items.length) {
|
|
139
|
+
console.error(`List: Invalid index ${index} for remove`);
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const items = [...this.state.items];
|
|
144
|
+
const removed = items.splice(index, 1)[0];
|
|
145
|
+
|
|
146
|
+
// Adjust selected index
|
|
147
|
+
if (this.state.selectedIndex !== null) {
|
|
148
|
+
if (this.state.selectedIndex === index) {
|
|
149
|
+
this.state.selectedIndex = null;
|
|
150
|
+
} else if (this.state.selectedIndex > index) {
|
|
151
|
+
this.state.selectedIndex--;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.state.items = items;
|
|
156
|
+
this.emit('itemRemoved', { item: removed, index });
|
|
157
|
+
this._updateDOM();
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
move(fromIndex: number, toIndex: number): this {
|
|
162
|
+
const items = [...this.state.items];
|
|
163
|
+
|
|
164
|
+
if (fromIndex < 0 || fromIndex >= items.length) {
|
|
165
|
+
console.error(`List: Invalid fromIndex ${fromIndex}`);
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (toIndex < 0 || toIndex >= items.length) {
|
|
170
|
+
console.error(`List: Invalid toIndex ${toIndex}`);
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (fromIndex === toIndex) {
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const [movedItem] = items.splice(fromIndex, 1);
|
|
179
|
+
items.splice(toIndex, 0, movedItem);
|
|
180
|
+
|
|
181
|
+
// Adjust selected index
|
|
182
|
+
if (this.state.selectedIndex !== null) {
|
|
183
|
+
if (this.state.selectedIndex === fromIndex) {
|
|
184
|
+
this.state.selectedIndex = toIndex;
|
|
185
|
+
} else if (fromIndex < this.state.selectedIndex && toIndex >= this.state.selectedIndex) {
|
|
186
|
+
this.state.selectedIndex--;
|
|
187
|
+
} else if (fromIndex > this.state.selectedIndex && toIndex <= this.state.selectedIndex) {
|
|
188
|
+
this.state.selectedIndex++;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.state.items = items;
|
|
193
|
+
this.emit('itemMoved', { item: movedItem, fromIndex, toIndex });
|
|
194
|
+
this._updateDOM();
|
|
195
|
+
return this;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
select(index: number): this {
|
|
199
|
+
if (index < 0 || index >= this.state.items.length) {
|
|
200
|
+
console.error(`List: Invalid index ${index} for select`);
|
|
201
|
+
return this;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const previousIndex = this.state.selectedIndex;
|
|
205
|
+
this.state.selectedIndex = index;
|
|
206
|
+
|
|
207
|
+
this.emit('itemSelect', {
|
|
208
|
+
item: this.state.items[index],
|
|
209
|
+
index,
|
|
210
|
+
previousIndex
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
this._updateDOM();
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
deselect(): this {
|
|
218
|
+
if (this.state.selectedIndex === null) {
|
|
219
|
+
return this;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const previousIndex = this.state.selectedIndex;
|
|
223
|
+
this.state.selectedIndex = null;
|
|
224
|
+
|
|
225
|
+
this.emit('itemDeselect', { previousIndex });
|
|
226
|
+
this._updateDOM();
|
|
227
|
+
return this;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
getSelected(): { item: ListItem; index: number } | null {
|
|
231
|
+
if (this.state.selectedIndex === null) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
item: this.state.items[this.state.selectedIndex],
|
|
236
|
+
index: this.state.selectedIndex
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* -------------------------
|
|
241
|
+
* Helpers
|
|
242
|
+
* ------------------------- */
|
|
243
|
+
|
|
244
|
+
private _updateDOM(): void {
|
|
245
|
+
if (!this.container) return;
|
|
246
|
+
|
|
247
|
+
// Clear and re-render
|
|
248
|
+
this.container.innerHTML = '';
|
|
249
|
+
this._renderContent();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private _renderContent(): void {
|
|
253
|
+
if (!this.container) return;
|
|
254
|
+
|
|
255
|
+
const { items, header, gap, direction, selectable, selectedIndex } = this.state;
|
|
256
|
+
|
|
257
|
+
const wrapper = document.createElement('div');
|
|
258
|
+
wrapper.className = 'jux-list-wrapper';
|
|
259
|
+
wrapper.id = this._componentId;
|
|
260
|
+
|
|
261
|
+
// Header
|
|
262
|
+
if (header) {
|
|
263
|
+
const headerEl = document.createElement('div');
|
|
264
|
+
headerEl.className = 'jux-list-header';
|
|
265
|
+
headerEl.textContent = header;
|
|
266
|
+
wrapper.appendChild(headerEl);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// List container
|
|
270
|
+
const listContainer = document.createElement('div');
|
|
271
|
+
listContainer.className = `jux-list jux-list-${direction}`;
|
|
272
|
+
listContainer.style.gap = gap;
|
|
273
|
+
|
|
274
|
+
// Render items
|
|
275
|
+
items.forEach((item, index) => {
|
|
276
|
+
const itemEl = document.createElement('div');
|
|
277
|
+
itemEl.className = `jux-list-item jux-list-item-${item.type || 'default'}`;
|
|
278
|
+
|
|
279
|
+
if (selectable && selectedIndex === index) {
|
|
280
|
+
itemEl.classList.add('jux-list-item-selected');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Icon
|
|
284
|
+
if (item.icon) {
|
|
285
|
+
const iconEl = document.createElement('span');
|
|
286
|
+
iconEl.className = 'jux-list-item-icon';
|
|
287
|
+
iconEl.textContent = item.icon;
|
|
288
|
+
itemEl.appendChild(iconEl);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Content
|
|
292
|
+
const contentEl = document.createElement('div');
|
|
293
|
+
contentEl.className = 'jux-list-item-content';
|
|
294
|
+
|
|
295
|
+
if (item.title) {
|
|
296
|
+
const titleEl = document.createElement('div');
|
|
297
|
+
titleEl.className = 'jux-list-item-title';
|
|
298
|
+
titleEl.textContent = item.title;
|
|
299
|
+
contentEl.appendChild(titleEl);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (item.body) {
|
|
303
|
+
const bodyEl = document.createElement('div');
|
|
304
|
+
bodyEl.className = 'jux-list-item-body';
|
|
305
|
+
bodyEl.textContent = item.body;
|
|
306
|
+
contentEl.appendChild(bodyEl);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
itemEl.appendChild(contentEl);
|
|
310
|
+
|
|
311
|
+
// Metadata
|
|
312
|
+
if (item.metadata) {
|
|
313
|
+
const metadataEl = document.createElement('span');
|
|
314
|
+
metadataEl.className = 'jux-list-item-metadata';
|
|
315
|
+
metadataEl.textContent = item.metadata;
|
|
316
|
+
itemEl.appendChild(metadataEl);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
listContainer.appendChild(itemEl);
|
|
320
|
+
|
|
321
|
+
// Event binding - click handlers
|
|
322
|
+
itemEl.addEventListener('click', (e) => {
|
|
323
|
+
if (selectable) {
|
|
324
|
+
this.select(index);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.emit('itemClick', { item, index, event: e });
|
|
328
|
+
|
|
329
|
+
if (this._onItemClick) {
|
|
330
|
+
this._onItemClick(item, index, e);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (this._onItemDoubleClick) {
|
|
335
|
+
itemEl.addEventListener('dblclick', (e) => {
|
|
336
|
+
this.emit('itemDoubleClick', { item, index, event: e });
|
|
337
|
+
this._onItemDoubleClick!(item, index, e);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
wrapper.appendChild(listContainer);
|
|
343
|
+
this.container.appendChild(wrapper);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* -------------------------
|
|
347
|
+
* Render
|
|
348
|
+
* ------------------------- */
|
|
349
|
+
|
|
350
|
+
render(targetId?: string): this {
|
|
351
|
+
let container: HTMLElement;
|
|
352
|
+
|
|
353
|
+
if (targetId) {
|
|
354
|
+
const target = document.querySelector(targetId);
|
|
355
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
356
|
+
throw new Error(`List: Target element "${targetId}" not found`);
|
|
357
|
+
}
|
|
358
|
+
container = target;
|
|
359
|
+
} else {
|
|
360
|
+
container = getOrCreateContainer(this._componentId) as HTMLElement;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this.container = container;
|
|
364
|
+
this.container.innerHTML = '';
|
|
365
|
+
|
|
366
|
+
this._renderContent();
|
|
367
|
+
|
|
368
|
+
return this;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Render to another Jux component's container
|
|
373
|
+
*/
|
|
374
|
+
renderTo(juxComponent: any): this {
|
|
375
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
376
|
+
throw new Error('List.renderTo: Invalid component - not an object');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
|
|
380
|
+
throw new Error('List.renderTo: Invalid component - missing _componentId (not a Jux component)');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return this.render(`#${juxComponent._componentId}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Factory helper
|
|
389
|
+
*/
|
|
390
|
+
export function list(componentId: string, options: ListOptions = {}): List {
|
|
391
|
+
return new List(componentId, options);
|
|
392
|
+
}
|