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,242 @@
|
|
|
1
|
+
import { Reactive, getOrCreateContainer } from './reactivity.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Code component options
|
|
5
|
+
*/
|
|
6
|
+
export interface CodeOptions {
|
|
7
|
+
code?: string;
|
|
8
|
+
language?: string;
|
|
9
|
+
title?: string | null;
|
|
10
|
+
showLineNumbers?: boolean;
|
|
11
|
+
highlightLines?: number[];
|
|
12
|
+
maxHeight?: string | null;
|
|
13
|
+
wrap?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Code component state
|
|
18
|
+
*/
|
|
19
|
+
type CodeState = {
|
|
20
|
+
code: string;
|
|
21
|
+
language: string;
|
|
22
|
+
title: string | null;
|
|
23
|
+
showLineNumbers: boolean;
|
|
24
|
+
highlightLines: number[];
|
|
25
|
+
maxHeight: string | null;
|
|
26
|
+
wrap: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Code component - displays formatted code blocks with syntax highlighting
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* const codeBlock = jux.code('myCode', {
|
|
34
|
+
* code: 'const x = 42;',
|
|
35
|
+
* language: 'javascript',
|
|
36
|
+
* title: 'Example Code',
|
|
37
|
+
* showLineNumbers: true
|
|
38
|
+
* });
|
|
39
|
+
* codeBlock.render();
|
|
40
|
+
*/
|
|
41
|
+
export class Code extends Reactive {
|
|
42
|
+
state!: CodeState;
|
|
43
|
+
container: HTMLElement | null = null;
|
|
44
|
+
|
|
45
|
+
constructor(componentId: string, options: CodeOptions = {}) {
|
|
46
|
+
super();
|
|
47
|
+
this._setComponentId(componentId);
|
|
48
|
+
|
|
49
|
+
// Map 'jux' language to 'javascript' for syntax highlighting
|
|
50
|
+
let language = options.language || 'javascript';
|
|
51
|
+
if (language === 'jux') {
|
|
52
|
+
language = 'javascript';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.state = this._createReactiveState({
|
|
56
|
+
code: options.code ?? '',
|
|
57
|
+
language: language,
|
|
58
|
+
title: options.title ?? null,
|
|
59
|
+
showLineNumbers: options.showLineNumbers ?? false,
|
|
60
|
+
highlightLines: options.highlightLines ?? [],
|
|
61
|
+
maxHeight: options.maxHeight ?? null,
|
|
62
|
+
wrap: options.wrap ?? false
|
|
63
|
+
}) as CodeState;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* -------------------------
|
|
67
|
+
* Fluent API
|
|
68
|
+
* ------------------------- */
|
|
69
|
+
|
|
70
|
+
code(value: string): this {
|
|
71
|
+
this.state.code = value;
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
language(value: string): this {
|
|
76
|
+
this.state.language = value === 'jux' ? 'javascript' : value;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
title(value: string): this {
|
|
81
|
+
this.state.title = value;
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
showLineNumbers(value: boolean): this {
|
|
86
|
+
this.state.showLineNumbers = value;
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
highlightLines(value: number[]): this {
|
|
91
|
+
this.state.highlightLines = value;
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
maxHeight(value: string): this {
|
|
96
|
+
this.state.maxHeight = value;
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
wrap(value: boolean): this {
|
|
101
|
+
this.state.wrap = value;
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* -------------------------
|
|
106
|
+
* Helpers
|
|
107
|
+
* ------------------------- */
|
|
108
|
+
|
|
109
|
+
private _escapeHtml(text: string): string {
|
|
110
|
+
const div = document.createElement('div');
|
|
111
|
+
div.textContent = text;
|
|
112
|
+
return div.innerHTML;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private _formatCode(): string {
|
|
116
|
+
const { code, showLineNumbers, highlightLines } = this.state;
|
|
117
|
+
const lines = code.split('\n');
|
|
118
|
+
|
|
119
|
+
if (!showLineNumbers) {
|
|
120
|
+
return `<code class="jux-code-block">${this._escapeHtml(code)}</code>`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const formattedLines = lines
|
|
124
|
+
.map((line, index) => {
|
|
125
|
+
const lineNum = index + 1;
|
|
126
|
+
const isHighlighted = highlightLines.includes(lineNum);
|
|
127
|
+
const highlightClass = isHighlighted ? ' jux-code-line-highlighted' : '';
|
|
128
|
+
|
|
129
|
+
return `<div class="jux-code-line${highlightClass}">
|
|
130
|
+
<span class="jux-code-line-number">${lineNum}</span>
|
|
131
|
+
<span class="jux-code-line-content">${this._escapeHtml(line) || ' '}</span>
|
|
132
|
+
</div>`;
|
|
133
|
+
})
|
|
134
|
+
.join('');
|
|
135
|
+
|
|
136
|
+
return `<code class="jux-code-block jux-code-numbered">${formattedLines}</code>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* -------------------------
|
|
140
|
+
* Render
|
|
141
|
+
* ------------------------- */
|
|
142
|
+
|
|
143
|
+
render(targetId?: string): this {
|
|
144
|
+
let container: HTMLElement;
|
|
145
|
+
|
|
146
|
+
if (targetId) {
|
|
147
|
+
const target = document.querySelector(targetId);
|
|
148
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
149
|
+
throw new Error(`Code: Target element "${targetId}" not found`);
|
|
150
|
+
}
|
|
151
|
+
container = target;
|
|
152
|
+
} else {
|
|
153
|
+
container = getOrCreateContainer(this._componentId) as HTMLElement;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.container = container;
|
|
157
|
+
const { language, title, maxHeight, wrap } = this.state;
|
|
158
|
+
|
|
159
|
+
container.innerHTML = '';
|
|
160
|
+
container.className = 'jux-code-container';
|
|
161
|
+
container.id = this._componentId;
|
|
162
|
+
|
|
163
|
+
// Title bar
|
|
164
|
+
if (title) {
|
|
165
|
+
const titleBar = document.createElement('div');
|
|
166
|
+
titleBar.className = 'jux-code-title-bar';
|
|
167
|
+
|
|
168
|
+
const titleEl = document.createElement('div');
|
|
169
|
+
titleEl.className = 'jux-code-title';
|
|
170
|
+
titleEl.textContent = title;
|
|
171
|
+
|
|
172
|
+
const langBadge = document.createElement('div');
|
|
173
|
+
langBadge.className = 'jux-code-language-badge';
|
|
174
|
+
langBadge.textContent = language;
|
|
175
|
+
|
|
176
|
+
titleBar.appendChild(titleEl);
|
|
177
|
+
titleBar.appendChild(langBadge);
|
|
178
|
+
container.appendChild(titleBar);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Code block wrapper
|
|
182
|
+
const codeWrapper = document.createElement('div');
|
|
183
|
+
codeWrapper.className = 'jux-code-wrapper';
|
|
184
|
+
if (maxHeight) {
|
|
185
|
+
codeWrapper.style.maxHeight = maxHeight;
|
|
186
|
+
codeWrapper.style.overflowY = 'auto';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Pre element
|
|
190
|
+
const preEl = document.createElement('pre');
|
|
191
|
+
preEl.className = `jux-code-pre language-${language}`;
|
|
192
|
+
if (wrap) {
|
|
193
|
+
preEl.classList.add('jux-code-wrap');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
preEl.innerHTML = this._formatCode();
|
|
197
|
+
|
|
198
|
+
codeWrapper.appendChild(preEl);
|
|
199
|
+
container.appendChild(codeWrapper);
|
|
200
|
+
|
|
201
|
+
// Copy button
|
|
202
|
+
const copyBtn = document.createElement('button');
|
|
203
|
+
copyBtn.className = 'jux-code-copy-btn';
|
|
204
|
+
copyBtn.textContent = 'Copy';
|
|
205
|
+
container.appendChild(copyBtn);
|
|
206
|
+
|
|
207
|
+
// Event binding - copy functionality
|
|
208
|
+
copyBtn.addEventListener('click', () => {
|
|
209
|
+
navigator.clipboard.writeText(this.state.code).then(() => {
|
|
210
|
+
copyBtn.textContent = 'Copied!';
|
|
211
|
+
setTimeout(() => {
|
|
212
|
+
copyBtn.textContent = 'Copy';
|
|
213
|
+
}, 2000);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
this.emit('rendered');
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Render to another Jux component's container
|
|
223
|
+
*/
|
|
224
|
+
renderTo(juxComponent: any): this {
|
|
225
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
226
|
+
throw new Error('Code.renderTo: Invalid component - not an object');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
|
|
230
|
+
throw new Error('Code.renderTo: Invalid component - missing _componentId (not a Jux component)');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return this.render(`#${juxComponent._componentId}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Factory helper
|
|
239
|
+
*/
|
|
240
|
+
export function code(componentId: string, options: CodeOptions = {}): Code {
|
|
241
|
+
return new Code(componentId, options);
|
|
242
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { Reactive, getOrCreateContainer } from './reactivity.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Container options
|
|
5
|
+
*/
|
|
6
|
+
export interface ContainerOptions {
|
|
7
|
+
direction?: 'horizontal' | 'vertical';
|
|
8
|
+
layout?: 'flex' | 'grid' | 'responsive';
|
|
9
|
+
minWidth?: string;
|
|
10
|
+
gap?: string;
|
|
11
|
+
justify?: 'start' | 'end' | 'center' | 'space-between' | 'space-around' | 'space-evenly';
|
|
12
|
+
align?: 'start' | 'end' | 'center' | 'stretch' | 'baseline';
|
|
13
|
+
wrap?: boolean;
|
|
14
|
+
children?: any[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Container state
|
|
19
|
+
*/
|
|
20
|
+
type ContainerState = {
|
|
21
|
+
direction: string;
|
|
22
|
+
layout: string;
|
|
23
|
+
minWidth: string;
|
|
24
|
+
gap: string;
|
|
25
|
+
justify: string;
|
|
26
|
+
align: string;
|
|
27
|
+
wrap: boolean;
|
|
28
|
+
children: any[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Container component - creates a flex or grid container for organizing child components
|
|
33
|
+
*
|
|
34
|
+
* Usage:
|
|
35
|
+
* const container = jux.container('myContainer', {
|
|
36
|
+
* layout: 'flex',
|
|
37
|
+
* direction: 'vertical',
|
|
38
|
+
* justify: 'space-between',
|
|
39
|
+
* align: 'center'
|
|
40
|
+
* });
|
|
41
|
+
* container.add(component1);
|
|
42
|
+
* container.add([component2, component3]);
|
|
43
|
+
* await container.render();
|
|
44
|
+
*
|
|
45
|
+
* FLEX Layout Options:
|
|
46
|
+
* - layout: 'flex' (default)
|
|
47
|
+
* - direction: 'vertical' | 'horizontal' (default: 'vertical')
|
|
48
|
+
* - justify: 'start' | 'end' | 'center' | 'space-between' | 'space-around' | 'space-evenly' (default: 'start')
|
|
49
|
+
* - align: 'start' | 'end' | 'center' | 'stretch' | 'baseline' (default: 'stretch')
|
|
50
|
+
* - wrap: boolean (default: false)
|
|
51
|
+
* - gap: string (default: '16px')
|
|
52
|
+
*
|
|
53
|
+
* GRID Layout Options:
|
|
54
|
+
* - layout: 'grid'
|
|
55
|
+
* - minWidth: string - for auto-fit (default: '250px')
|
|
56
|
+
* - gap: string (default: '16px')
|
|
57
|
+
* - align: 'start' | 'end' | 'center' | 'stretch' (default: 'stretch')
|
|
58
|
+
* - justify: 'start' | 'end' | 'center' | 'stretch' (default: 'stretch')
|
|
59
|
+
*/
|
|
60
|
+
export class Container extends Reactive {
|
|
61
|
+
state!: ContainerState;
|
|
62
|
+
container: HTMLElement | null = null;
|
|
63
|
+
|
|
64
|
+
constructor(componentId: string, options: ContainerOptions = {}) {
|
|
65
|
+
super();
|
|
66
|
+
this._setComponentId(componentId);
|
|
67
|
+
|
|
68
|
+
this.state = this._createReactiveState({
|
|
69
|
+
direction: options.direction ?? 'vertical',
|
|
70
|
+
layout: options.layout ?? 'flex',
|
|
71
|
+
minWidth: options.minWidth ?? '250px',
|
|
72
|
+
gap: options.gap ?? '16px',
|
|
73
|
+
justify: options.justify ?? 'start',
|
|
74
|
+
align: options.align ?? 'stretch',
|
|
75
|
+
wrap: options.wrap ?? false,
|
|
76
|
+
children: Array.isArray(options.children) ? [...options.children] : []
|
|
77
|
+
}) as ContainerState;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* -------------------------
|
|
81
|
+
* Fluent API
|
|
82
|
+
* ------------------------- */
|
|
83
|
+
|
|
84
|
+
direction(value: 'horizontal' | 'vertical'): this {
|
|
85
|
+
this.state.direction = value;
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
layout(value: 'flex' | 'grid' | 'responsive'): this {
|
|
90
|
+
this.state.layout = value;
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
minWidth(value: string): this {
|
|
95
|
+
this.state.minWidth = value;
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
gap(value: string): this {
|
|
100
|
+
this.state.gap = value;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
justify(value: 'start' | 'end' | 'center' | 'space-between' | 'space-around' | 'space-evenly'): this {
|
|
105
|
+
this.state.justify = value;
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
align(value: 'start' | 'end' | 'center' | 'stretch' | 'baseline'): this {
|
|
110
|
+
this.state.align = value;
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
wrap(value: boolean): this {
|
|
115
|
+
this.state.wrap = value;
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* -------------------------
|
|
120
|
+
* Add children
|
|
121
|
+
* ------------------------- */
|
|
122
|
+
|
|
123
|
+
add(component: any | any[]): this {
|
|
124
|
+
if (Array.isArray(component)) {
|
|
125
|
+
this.state.children.push(...component);
|
|
126
|
+
} else {
|
|
127
|
+
this.state.children.push(component);
|
|
128
|
+
}
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* -------------------------
|
|
133
|
+
* Layout styles
|
|
134
|
+
* ------------------------- */
|
|
135
|
+
|
|
136
|
+
private _applyFlexStyles(containerEl: HTMLElement): void {
|
|
137
|
+
const { direction, gap, justify, align, wrap } = this.state;
|
|
138
|
+
containerEl.style.cssText = `
|
|
139
|
+
display: flex;
|
|
140
|
+
flex-direction: ${direction === 'horizontal' ? 'row' : 'column'};
|
|
141
|
+
justify-content: ${justify};
|
|
142
|
+
align-items: ${align};
|
|
143
|
+
flex-wrap: ${wrap ? 'wrap' : 'nowrap'};
|
|
144
|
+
gap: ${gap};
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private _applyGridStyles(containerEl: HTMLElement): void {
|
|
149
|
+
const { minWidth, gap, direction } = this.state;
|
|
150
|
+
|
|
151
|
+
if (direction === 'horizontal') {
|
|
152
|
+
// For horizontal grid, use auto-fit columns
|
|
153
|
+
containerEl.style.cssText = `
|
|
154
|
+
display: grid;
|
|
155
|
+
grid-template-columns: repeat(auto-fit, minmax(${minWidth}, 1fr));
|
|
156
|
+
gap: ${gap};
|
|
157
|
+
`;
|
|
158
|
+
} else {
|
|
159
|
+
// For vertical grid, stack rows
|
|
160
|
+
containerEl.style.cssText = `
|
|
161
|
+
display: grid;
|
|
162
|
+
grid-template-columns: 1fr;
|
|
163
|
+
grid-auto-rows: auto;
|
|
164
|
+
gap: ${gap};
|
|
165
|
+
`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* -------------------------
|
|
170
|
+
* Render
|
|
171
|
+
* ------------------------- */
|
|
172
|
+
|
|
173
|
+
async render(targetId?: string | HTMLElement): Promise<this> {
|
|
174
|
+
let container: HTMLElement;
|
|
175
|
+
|
|
176
|
+
if (targetId) {
|
|
177
|
+
// If targetId is an HTMLElement (passed from parent container), use it directly
|
|
178
|
+
if (targetId instanceof HTMLElement) {
|
|
179
|
+
container = targetId;
|
|
180
|
+
} else {
|
|
181
|
+
// Otherwise query for it
|
|
182
|
+
const target = document.querySelector(targetId);
|
|
183
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
184
|
+
throw new Error(`Container: Target element "${targetId}" not found`);
|
|
185
|
+
}
|
|
186
|
+
container = target;
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
container = getOrCreateContainer(this._componentId) as HTMLElement;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.container = container;
|
|
193
|
+
const { layout, children } = this.state;
|
|
194
|
+
|
|
195
|
+
container.innerHTML = '';
|
|
196
|
+
container.className = 'jux-container';
|
|
197
|
+
container.id = this._componentId;
|
|
198
|
+
|
|
199
|
+
// Apply layout-specific styles
|
|
200
|
+
if (layout === 'grid') {
|
|
201
|
+
this._applyGridStyles(container);
|
|
202
|
+
} else {
|
|
203
|
+
this._applyFlexStyles(container);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Render children
|
|
207
|
+
for (let i = 0; i < children.length; i++) {
|
|
208
|
+
const child = children[i];
|
|
209
|
+
if (!child) continue;
|
|
210
|
+
|
|
211
|
+
// Handle string children
|
|
212
|
+
if (typeof child === 'string') {
|
|
213
|
+
const textWrapper = document.createElement('div');
|
|
214
|
+
textWrapper.className = 'jux-container-item';
|
|
215
|
+
textWrapper.style.cssText = 'padding: 1rem; display: flex; align-items: center; justify-content: center; font-weight: bold;';
|
|
216
|
+
textWrapper.textContent = child;
|
|
217
|
+
container.appendChild(textWrapper);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Get child ID
|
|
222
|
+
let childId: string | null = null;
|
|
223
|
+
if ((child as any).id) {
|
|
224
|
+
childId = (child as any).id;
|
|
225
|
+
} else if ((child as any)._componentId) {
|
|
226
|
+
childId = (child as any)._componentId;
|
|
227
|
+
} else {
|
|
228
|
+
childId = `${this._componentId}-child-${i}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Create wrapper for child
|
|
232
|
+
const childWrapper = document.createElement('div');
|
|
233
|
+
childWrapper.className = 'jux-container-item';
|
|
234
|
+
childWrapper.id = `${childId}-wrapper`;
|
|
235
|
+
container.appendChild(childWrapper);
|
|
236
|
+
|
|
237
|
+
// Render child INTO the wrapper (not to its own container)
|
|
238
|
+
if (typeof (child as any).render === 'function') {
|
|
239
|
+
try {
|
|
240
|
+
// Pass the wrapper element directly, not a selector
|
|
241
|
+
const result = (child as any).render(childWrapper);
|
|
242
|
+
if (result && typeof (result as any).then === 'function') {
|
|
243
|
+
await result;
|
|
244
|
+
}
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.error(`Container: Error rendering child ${i}:`, err);
|
|
247
|
+
childWrapper.innerHTML = `<div style="color: #ff6b6b; padding: 1rem;">Error: ${(err as Error).message}</div>`;
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
// If no render method, try to append directly
|
|
251
|
+
if (child instanceof HTMLElement) {
|
|
252
|
+
childWrapper.appendChild(child);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.emit('rendered');
|
|
258
|
+
return this;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Render to another Jux component's container
|
|
263
|
+
*/
|
|
264
|
+
async renderTo(juxComponent: any): Promise<this> {
|
|
265
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
266
|
+
throw new Error('Container.renderTo: Invalid component - not an object');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
|
|
270
|
+
throw new Error('Container.renderTo: Invalid component - missing _componentId (not a Jux component)');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return this.render(`#${juxComponent._componentId}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Factory helper
|
|
279
|
+
*/
|
|
280
|
+
export function container(componentId: string, options: ContainerOptions = {}): Container {
|
|
281
|
+
return new Container(componentId, options);
|
|
282
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Reactive, getOrCreateContainer } from './reactivity.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Data component - SQL query execution
|
|
5
|
+
* Note: No componentId needed - this is a data-only component
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const posts = jux.data('SELECT * FROM posts WHERE id = ?', [1]);
|
|
9
|
+
* await posts.execute();
|
|
10
|
+
* console.log(posts.data);
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface DataOptions {
|
|
14
|
+
sql: string;
|
|
15
|
+
params?: any[];
|
|
16
|
+
apiUrl?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class Data {
|
|
20
|
+
private _data: any[] = [];
|
|
21
|
+
private _executed: boolean = false;
|
|
22
|
+
private _loading: boolean = false;
|
|
23
|
+
private _sql: string;
|
|
24
|
+
private _params: any[];
|
|
25
|
+
private _apiUrl: string;
|
|
26
|
+
|
|
27
|
+
constructor(sql: string, params: any[] = [], apiUrl: string = '/api/query') {
|
|
28
|
+
this._sql = sql;
|
|
29
|
+
this._params = params;
|
|
30
|
+
this._apiUrl = apiUrl;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* -------------------------
|
|
34
|
+
* Getters
|
|
35
|
+
* ------------------------- */
|
|
36
|
+
|
|
37
|
+
get data(): any[] {
|
|
38
|
+
return this._data;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get executed(): boolean {
|
|
42
|
+
return this._executed;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get loading(): boolean {
|
|
46
|
+
return this._loading;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get sql(): string {
|
|
50
|
+
return this._sql;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
set sql(value: string) {
|
|
54
|
+
this._sql = value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* -------------------------
|
|
58
|
+
* Methods
|
|
59
|
+
* ------------------------- */
|
|
60
|
+
|
|
61
|
+
async execute(): Promise<void> {
|
|
62
|
+
if (this._executed) return;
|
|
63
|
+
this._loading = true;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch(this._apiUrl, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json'
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
sql: this._sql,
|
|
73
|
+
params: this._params
|
|
74
|
+
})
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new Error(`API error: ${response.statusText}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const result = await response.json();
|
|
82
|
+
this._data = result.data || [];
|
|
83
|
+
this._executed = true;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('Data execution error:', error);
|
|
86
|
+
this._data = [];
|
|
87
|
+
throw error;
|
|
88
|
+
} finally {
|
|
89
|
+
this._loading = false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
reset(): void {
|
|
94
|
+
this._executed = false;
|
|
95
|
+
this._data = [];
|
|
96
|
+
this._loading = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Factory helper
|
|
102
|
+
*/
|
|
103
|
+
export function data(sql: string, params: any[] = [], apiUrl: string = '/api/query'): Data {
|
|
104
|
+
return new Data(sql, params, apiUrl);
|
|
105
|
+
}
|