juxscript 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive state container
|
|
3
|
+
* Notifies subscribers when value changes
|
|
4
|
+
*/
|
|
5
|
+
export class State<T> {
|
|
6
|
+
private _value: T;
|
|
7
|
+
private _subscribers: Set<(value: T) => void> = new Set();
|
|
8
|
+
private _effects: Set<Effect> = new Set();
|
|
9
|
+
|
|
10
|
+
constructor(initialValue: T) {
|
|
11
|
+
this._value = initialValue;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get current value
|
|
16
|
+
*/
|
|
17
|
+
get value(): T {
|
|
18
|
+
return this._value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Set new value and notify subscribers
|
|
23
|
+
*/
|
|
24
|
+
set(newValue: T): void {
|
|
25
|
+
if (this._value !== newValue) {
|
|
26
|
+
this._value = newValue;
|
|
27
|
+
this._notify();
|
|
28
|
+
this._runEffects();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Update value using a function
|
|
34
|
+
*/
|
|
35
|
+
update(fn: (current: T) => T): void {
|
|
36
|
+
this.set(fn(this._value));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Subscribe to value changes
|
|
41
|
+
* Returns unsubscribe function
|
|
42
|
+
*/
|
|
43
|
+
subscribe(callback: (value: T) => void): () => void {
|
|
44
|
+
this._subscribers.add(callback);
|
|
45
|
+
callback(this._value); // Call immediately with current value
|
|
46
|
+
return () => this._subscribers.delete(callback);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Notify all subscribers of value change
|
|
51
|
+
*/
|
|
52
|
+
private _notify(): void {
|
|
53
|
+
this._subscribers.forEach(callback => callback(this._value));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Run all registered effects
|
|
58
|
+
*/
|
|
59
|
+
private _runEffects(): void {
|
|
60
|
+
this._effects.forEach(effect => effect.run(this._value));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* -------------------------
|
|
64
|
+
* Higher-Order Effects
|
|
65
|
+
* ------------------------- */
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Bind visibility to element (v-show style)
|
|
69
|
+
* Toggles display: none on/off
|
|
70
|
+
* For animations, use bindClass() instead
|
|
71
|
+
*/
|
|
72
|
+
bindVisibility(elementId: string): () => void {
|
|
73
|
+
if (typeof this._value !== 'boolean') {
|
|
74
|
+
console.warn(`State.bindVisibility: State value must be boolean, got ${typeof this._value}`);
|
|
75
|
+
return () => { };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const effect = new VisibilityEffect(elementId);
|
|
79
|
+
this._effects.add(effect);
|
|
80
|
+
effect.run(this._value); // Initial run
|
|
81
|
+
|
|
82
|
+
return () => this._effects.delete(effect);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Bind value property to element (for progress, input, range, etc.)
|
|
87
|
+
* @param elementId - DOM element ID
|
|
88
|
+
* @param transform - Optional transform function
|
|
89
|
+
*/
|
|
90
|
+
bindValue(elementId: string, transform?: (val: T) => number | string): () => void {
|
|
91
|
+
const effect = new ValueEffect(elementId, transform);
|
|
92
|
+
this._effects.add(effect);
|
|
93
|
+
effect.run(this._value);
|
|
94
|
+
|
|
95
|
+
return () => this._effects.delete(effect);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Bind text content to element
|
|
100
|
+
* @param elementId - DOM element ID
|
|
101
|
+
* @param transform - Optional transform function
|
|
102
|
+
*/
|
|
103
|
+
bindText(elementId: string, transform?: (val: T) => string): () => void {
|
|
104
|
+
const effect = new TextEffect(elementId, transform);
|
|
105
|
+
this._effects.add(effect);
|
|
106
|
+
effect.run(this._value);
|
|
107
|
+
|
|
108
|
+
return () => this._effects.delete(effect);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Bind attribute to element
|
|
113
|
+
* @param elementId - DOM element ID
|
|
114
|
+
* @param attrName - Attribute name
|
|
115
|
+
* @param transform - Optional transform function
|
|
116
|
+
*/
|
|
117
|
+
bindAttr(elementId: string, attrName: string, transform?: (val: T) => string): () => void {
|
|
118
|
+
const effect = new AttrEffect(elementId, attrName, transform);
|
|
119
|
+
this._effects.add(effect);
|
|
120
|
+
effect.run(this._value);
|
|
121
|
+
|
|
122
|
+
return () => this._effects.delete(effect);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Bind CSS class to element
|
|
127
|
+
* @param elementId - DOM element ID
|
|
128
|
+
* @param className - Class to toggle
|
|
129
|
+
* @param condition - Optional condition function
|
|
130
|
+
*/
|
|
131
|
+
bindClass(elementId: string, className: string, condition?: (val: T) => boolean): () => void {
|
|
132
|
+
const effect = new ClassEffect(elementId, className, condition);
|
|
133
|
+
this._effects.add(effect);
|
|
134
|
+
effect.run(this._value);
|
|
135
|
+
|
|
136
|
+
return () => this._effects.delete(effect);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Bind innerHTML to element
|
|
141
|
+
* @param elementId - DOM element ID
|
|
142
|
+
* @param transform - Optional transform function
|
|
143
|
+
*/
|
|
144
|
+
bindHTML(elementId: string, transform?: (val: T) => string): () => void {
|
|
145
|
+
const effect = new HTMLEffect(elementId, transform);
|
|
146
|
+
this._effects.add(effect);
|
|
147
|
+
effect.run(this._value);
|
|
148
|
+
|
|
149
|
+
return () => this._effects.delete(effect);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* -------------------------
|
|
153
|
+
* Number Helpers
|
|
154
|
+
* ------------------------- */
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Increment (numbers only)
|
|
158
|
+
*/
|
|
159
|
+
increment = (): void => {
|
|
160
|
+
if (typeof this._value === 'number') {
|
|
161
|
+
this.set((this._value + 1) as T);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Decrement (numbers only)
|
|
167
|
+
*/
|
|
168
|
+
decrement = (): void => {
|
|
169
|
+
if (typeof this._value === 'number') {
|
|
170
|
+
this.set((this._value - 1) as T);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Add amount (returns function for event handlers)
|
|
176
|
+
*/
|
|
177
|
+
add = (amount: number) => (): void => {
|
|
178
|
+
if (typeof this._value === 'number') {
|
|
179
|
+
this.set((this._value + amount) as T);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Subtract amount
|
|
185
|
+
*/
|
|
186
|
+
subtract = (amount: number) => (): void => {
|
|
187
|
+
if (typeof this._value === 'number') {
|
|
188
|
+
this.set((this._value - amount) as T);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Multiply by factor
|
|
194
|
+
*/
|
|
195
|
+
multiply = (factor: number) => (): void => {
|
|
196
|
+
if (typeof this._value === 'number') {
|
|
197
|
+
this.set((this._value * factor) as T);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* -------------------------
|
|
202
|
+
* Boolean Helpers
|
|
203
|
+
* ------------------------- */
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Toggle (booleans only)
|
|
207
|
+
*/
|
|
208
|
+
toggle = (): void => {
|
|
209
|
+
if (typeof this._value === 'boolean') {
|
|
210
|
+
this.set((!this._value) as T);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* -------------------------
|
|
215
|
+
* General Helpers
|
|
216
|
+
* ------------------------- */
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Reset to initial value (returns function)
|
|
220
|
+
*/
|
|
221
|
+
reset = (initialValue: T) => (): void => {
|
|
222
|
+
this.set(initialValue);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* -------------------------
|
|
227
|
+
* Effect Classes
|
|
228
|
+
* ------------------------- */
|
|
229
|
+
|
|
230
|
+
interface Effect {
|
|
231
|
+
run(value: any): void;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
class VisibilityEffect implements Effect {
|
|
235
|
+
constructor(
|
|
236
|
+
private elementId: string
|
|
237
|
+
) { }
|
|
238
|
+
|
|
239
|
+
run(visible: boolean): void {
|
|
240
|
+
const el = document.getElementById(this.elementId);
|
|
241
|
+
if (!el) return;
|
|
242
|
+
|
|
243
|
+
// Simple display toggle (Vue v-show style)
|
|
244
|
+
el.style.display = visible ? '' : 'none';
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
interface VisibilityOptions {
|
|
249
|
+
mode?: 'display' | 'visibility' | 'opacity' | 'class';
|
|
250
|
+
visibleClass?: string;
|
|
251
|
+
hiddenClass?: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
class TextEffect implements Effect {
|
|
255
|
+
constructor(
|
|
256
|
+
private elementId: string,
|
|
257
|
+
private transform?: (val: any) => string
|
|
258
|
+
) { }
|
|
259
|
+
|
|
260
|
+
run(value: any): void {
|
|
261
|
+
const el = document.getElementById(this.elementId);
|
|
262
|
+
if (!el) return;
|
|
263
|
+
|
|
264
|
+
const text = this.transform ? this.transform(value) : String(value);
|
|
265
|
+
el.textContent = text;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
class HTMLEffect implements Effect {
|
|
270
|
+
constructor(
|
|
271
|
+
private elementId: string,
|
|
272
|
+
private transform?: (val: any) => string
|
|
273
|
+
) { }
|
|
274
|
+
|
|
275
|
+
run(value: any): void {
|
|
276
|
+
const el = document.getElementById(this.elementId);
|
|
277
|
+
if (!el) return;
|
|
278
|
+
|
|
279
|
+
const html = this.transform ? this.transform(value) : String(value);
|
|
280
|
+
el.innerHTML = html;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
class AttrEffect implements Effect {
|
|
285
|
+
constructor(
|
|
286
|
+
private elementId: string,
|
|
287
|
+
private attrName: string,
|
|
288
|
+
private transform?: (val: any) => string
|
|
289
|
+
) { }
|
|
290
|
+
|
|
291
|
+
run(value: any): void {
|
|
292
|
+
const el = document.getElementById(this.elementId);
|
|
293
|
+
if (!el) return;
|
|
294
|
+
|
|
295
|
+
const attrValue = this.transform ? this.transform(value) : String(value);
|
|
296
|
+
el.setAttribute(this.attrName, attrValue);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
class ClassEffect implements Effect {
|
|
301
|
+
constructor(
|
|
302
|
+
private elementId: string,
|
|
303
|
+
private className: string,
|
|
304
|
+
private condition?: (val: any) => boolean
|
|
305
|
+
) { }
|
|
306
|
+
|
|
307
|
+
run(value: any): void {
|
|
308
|
+
const el = document.getElementById(this.elementId);
|
|
309
|
+
if (!el) return;
|
|
310
|
+
|
|
311
|
+
const shouldAdd = this.condition ? this.condition(value) : Boolean(value);
|
|
312
|
+
if (shouldAdd) {
|
|
313
|
+
el.classList.add(this.className);
|
|
314
|
+
} else {
|
|
315
|
+
el.classList.remove(this.className);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
class ValueEffect implements Effect {
|
|
321
|
+
constructor(
|
|
322
|
+
private elementId: string,
|
|
323
|
+
private transform?: (val: any) => number | string
|
|
324
|
+
) { }
|
|
325
|
+
|
|
326
|
+
run(value: any): void {
|
|
327
|
+
const el = document.getElementById(this.elementId);
|
|
328
|
+
if (!el) return;
|
|
329
|
+
|
|
330
|
+
const newValue = this.transform ? this.transform(value) : value;
|
|
331
|
+
|
|
332
|
+
// For progress/jux components, set data-value and trigger update
|
|
333
|
+
if (el.classList.contains('jux-progress')) {
|
|
334
|
+
el.setAttribute('data-value', String(newValue));
|
|
335
|
+
|
|
336
|
+
// Find the progress instance and call value() to trigger update
|
|
337
|
+
const progressBar = el.querySelector('.jux-progress-bar');
|
|
338
|
+
if (progressBar) {
|
|
339
|
+
const percentage = typeof newValue === 'number' ? newValue : parseFloat(String(newValue));
|
|
340
|
+
const max = parseFloat(progressBar.getAttribute('aria-valuemax') || '100');
|
|
341
|
+
const percentageValue = (percentage / max) * 100;
|
|
342
|
+
(progressBar as HTMLElement).style.width = `${percentageValue}%`;
|
|
343
|
+
progressBar.setAttribute('aria-valuenow', String(percentage));
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// For native HTML elements (input, progress, meter)
|
|
349
|
+
if ('value' in el) {
|
|
350
|
+
(el as any).value = String(newValue);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (el instanceof HTMLProgressElement || el.tagName === 'METER') {
|
|
354
|
+
el.setAttribute('value', String(newValue));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Create reactive state
|
|
361
|
+
*/
|
|
362
|
+
export function state<T>(initialValue: T): State<T> {
|
|
363
|
+
return new State(initialValue);
|
|
364
|
+
}
|
package/machinery/compiler.js
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import esbuild from 'esbuild';
|
|
4
|
-
import { generateDocs } from './doc-generator.js';
|
|
5
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Compile a .jux file to .js and .html
|
|
7
|
+
*
|
|
8
|
+
* @param {string} juxFilePath - Path to the .jux file
|
|
9
|
+
* @param {Object} options - Compilation options
|
|
10
|
+
* @param {string} options.distDir - Output directory
|
|
11
|
+
* @param {string} options.projectRoot - Project root directory
|
|
12
|
+
* @param {boolean} options.isServe - Whether serving for development
|
|
13
|
+
* @returns {Promise<{jsOutputPath: string, htmlOutputPath: string}>}
|
|
14
|
+
*/
|
|
6
15
|
export async function compileJuxFile(juxFilePath, options = {}) {
|
|
7
16
|
const { distDir, projectRoot, isServe = false } = options;
|
|
8
|
-
|
|
17
|
+
|
|
9
18
|
const relativePath = path.relative(projectRoot, juxFilePath);
|
|
10
19
|
const parsedPath = path.parse(relativePath);
|
|
11
|
-
|
|
20
|
+
|
|
12
21
|
// Output paths
|
|
13
22
|
const outputDir = path.join(distDir, parsedPath.dir);
|
|
14
23
|
const jsOutputPath = path.join(outputDir, `${parsedPath.name}.js`);
|
|
@@ -27,17 +36,17 @@ export async function compileJuxFile(juxFilePath, options = {}) {
|
|
|
27
36
|
// Calculate depth for relative paths
|
|
28
37
|
const depth = parsedPath.dir.split(path.sep).filter(p => p).length;
|
|
29
38
|
const libPath = depth === 0 ? './lib/jux.js' : '../'.repeat(depth) + 'lib/jux.js';
|
|
30
|
-
const styleBasePath = depth === 0 ? './lib/
|
|
39
|
+
const styleBasePath = depth === 0 ? './lib/presets/' : '../'.repeat(depth) + 'lib/presets/';
|
|
31
40
|
|
|
32
41
|
// Transform imports
|
|
33
42
|
let transformedContent = juxContent;
|
|
34
|
-
|
|
43
|
+
|
|
35
44
|
// Replace common import patterns with calculated path
|
|
36
45
|
transformedContent = transformedContent.replace(
|
|
37
46
|
/from\s+['"]\.\.?\/lib\/jux\.js['"]/g,
|
|
38
47
|
`from '${libPath}'`
|
|
39
48
|
);
|
|
40
|
-
|
|
49
|
+
|
|
41
50
|
// Only inject import if:
|
|
42
51
|
// 1. File is not empty (ignoring whitespace and comments)
|
|
43
52
|
// 2. File uses 'jux.' but has no import statement
|
|
@@ -45,11 +54,11 @@ export async function compileJuxFile(juxFilePath, options = {}) {
|
|
|
45
54
|
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments
|
|
46
55
|
.replace(/\/\/.*/g, '') // Remove line comments
|
|
47
56
|
.trim();
|
|
48
|
-
|
|
57
|
+
|
|
49
58
|
const hasContent = contentWithoutComments.length > 0;
|
|
50
59
|
const usesJux = /\bjux\./g.test(contentWithoutComments);
|
|
51
60
|
const hasImport = /import\s+.*from/.test(transformedContent);
|
|
52
|
-
|
|
61
|
+
|
|
53
62
|
if (hasContent && usesJux && !hasImport) {
|
|
54
63
|
transformedContent = `import { jux } from '${libPath}';\n\n${transformedContent}`;
|
|
55
64
|
}
|
|
@@ -61,28 +70,19 @@ export async function compileJuxFile(juxFilePath, options = {}) {
|
|
|
61
70
|
|
|
62
71
|
// Generate HTML with correct script path
|
|
63
72
|
const scriptPath = `./${parsedPath.name}.js`;
|
|
64
|
-
|
|
73
|
+
|
|
65
74
|
const html = `<!DOCTYPE html>
|
|
66
75
|
<html lang="en">
|
|
67
76
|
<head>
|
|
68
77
|
<meta charset="UTF-8">
|
|
69
78
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
70
79
|
<title>${parsedPath.name}</title>
|
|
71
|
-
|
|
72
80
|
<!-- JUX Core Styles -->
|
|
73
|
-
<link rel="stylesheet" href="${styleBasePath}tokens/dark.css">
|
|
74
81
|
<link rel="stylesheet" href="${styleBasePath}global.css">
|
|
75
82
|
</head>
|
|
76
|
-
<body data-theme="
|
|
83
|
+
<body data-theme="">
|
|
77
84
|
<!-- App container -->
|
|
78
85
|
<div id="app" data-jux-page="${parsedPath.name}"></div>
|
|
79
|
-
|
|
80
|
-
<!-- Main content -->
|
|
81
|
-
<div id="appmain"></div>
|
|
82
|
-
|
|
83
|
-
<!-- Modal overlay -->
|
|
84
|
-
<div id="appmodal" aria-hidden="true" role="dialog"></div>
|
|
85
|
-
|
|
86
86
|
<script type="module" src="${scriptPath}"></script>
|
|
87
87
|
${isServe ? `
|
|
88
88
|
<!-- Hot reload -->
|
|
@@ -107,10 +107,16 @@ export async function compileJuxFile(juxFilePath, options = {}) {
|
|
|
107
107
|
return { jsOutputPath, htmlOutputPath };
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Copy and build the JUX library from TypeScript to JavaScript
|
|
112
|
+
*
|
|
113
|
+
* @param {string} projectRoot - Root directory containing lib/
|
|
114
|
+
* @param {string} distDir - Destination directory for built files
|
|
115
|
+
*/
|
|
110
116
|
export async function copyLibToOutput(projectRoot, distDir) {
|
|
111
117
|
// Simplified lib path resolution
|
|
112
118
|
const libSrc = path.resolve(projectRoot, '../lib');
|
|
113
|
-
|
|
119
|
+
|
|
114
120
|
if (!fs.existsSync(libSrc)) {
|
|
115
121
|
throw new Error(`lib/ directory not found at ${libSrc}`);
|
|
116
122
|
}
|
|
@@ -129,7 +135,7 @@ export async function copyLibToOutput(projectRoot, distDir) {
|
|
|
129
135
|
|
|
130
136
|
// Find all TypeScript entry points
|
|
131
137
|
const tsFiles = findFiles(libSrc, '.ts');
|
|
132
|
-
|
|
138
|
+
|
|
133
139
|
if (tsFiles.length === 0) {
|
|
134
140
|
console.warn('⚠️ No TypeScript files found in lib/');
|
|
135
141
|
return;
|
|
@@ -168,50 +174,123 @@ export async function copyLibToOutput(projectRoot, distDir) {
|
|
|
168
174
|
console.log('✅ Library ready\n');
|
|
169
175
|
}
|
|
170
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Copy project assets (CSS, JS, images) from jux/ to dist/
|
|
179
|
+
*
|
|
180
|
+
* @param {string} projectRoot - Source directory (jux/)
|
|
181
|
+
* @param {string} distDir - Destination directory (jux-dist/)
|
|
182
|
+
*/
|
|
171
183
|
export async function copyProjectAssets(projectRoot, distDir) {
|
|
172
184
|
console.log('📦 Copying project assets...');
|
|
173
|
-
|
|
185
|
+
|
|
174
186
|
// Find all CSS and JS files in project root (excluding node_modules, dist, .git)
|
|
175
187
|
const allFiles = [];
|
|
176
188
|
findProjectFiles(projectRoot, ['.css', '.js'], allFiles, projectRoot);
|
|
177
|
-
|
|
189
|
+
|
|
178
190
|
console.log(` Found ${allFiles.length} asset file(s)`);
|
|
179
|
-
|
|
191
|
+
|
|
180
192
|
for (const srcPath of allFiles) {
|
|
181
193
|
const relativePath = path.relative(projectRoot, srcPath);
|
|
182
194
|
const destPath = path.join(distDir, relativePath);
|
|
183
195
|
const destDir = path.dirname(destPath);
|
|
184
|
-
|
|
196
|
+
|
|
185
197
|
// Create destination directory if needed
|
|
186
198
|
if (!fs.existsSync(destDir)) {
|
|
187
199
|
fs.mkdirSync(destDir, { recursive: true });
|
|
188
200
|
}
|
|
189
|
-
|
|
201
|
+
|
|
190
202
|
// Copy file
|
|
191
203
|
fs.copyFileSync(srcPath, destPath);
|
|
192
204
|
console.log(` ✓ ${relativePath}`);
|
|
193
205
|
}
|
|
194
|
-
|
|
206
|
+
|
|
195
207
|
console.log('✅ Project assets copied\n');
|
|
196
208
|
}
|
|
197
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Transpile TypeScript files from jux/ to jux-dist/, preserving folder structure
|
|
212
|
+
*
|
|
213
|
+
* @param {string} srcDir - Source directory (jux/)
|
|
214
|
+
* @param {string} destDir - Destination directory (jux-dist/)
|
|
215
|
+
* @example
|
|
216
|
+
* // jux/samples/mypage.ts -> jux-dist/samples/mypage.js
|
|
217
|
+
* await transpileProjectTypeScript('jux/', 'jux-dist/');
|
|
218
|
+
*/
|
|
219
|
+
export async function transpileProjectTypeScript(srcDir, destDir) {
|
|
220
|
+
console.log('🔷 Transpiling TypeScript files...');
|
|
221
|
+
|
|
222
|
+
// Find all TypeScript files in the project
|
|
223
|
+
const tsFiles = findFiles(srcDir, '.ts');
|
|
224
|
+
|
|
225
|
+
if (tsFiles.length === 0) {
|
|
226
|
+
console.log(' No TypeScript files found in project');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log(` Found ${tsFiles.length} TypeScript file(s)`);
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
// Build all TypeScript files with esbuild
|
|
234
|
+
await esbuild.build({
|
|
235
|
+
entryPoints: tsFiles,
|
|
236
|
+
bundle: false,
|
|
237
|
+
format: 'esm',
|
|
238
|
+
outdir: destDir,
|
|
239
|
+
outbase: srcDir,
|
|
240
|
+
platform: 'browser',
|
|
241
|
+
target: 'es2020',
|
|
242
|
+
loader: {
|
|
243
|
+
'.ts': 'ts'
|
|
244
|
+
},
|
|
245
|
+
logLevel: 'warning'
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Log each transpiled file
|
|
249
|
+
tsFiles.forEach(tsFile => {
|
|
250
|
+
const relativePath = path.relative(srcDir, tsFile);
|
|
251
|
+
const jsPath = relativePath.replace(/\.ts$/, '.js');
|
|
252
|
+
console.log(` ✓ ${relativePath} → ${jsPath}`);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
console.log('✅ TypeScript transpiled\n');
|
|
256
|
+
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error('❌ Failed to transpile TypeScript:', err.message);
|
|
259
|
+
throw err;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Recursively find files with a specific extension
|
|
265
|
+
*
|
|
266
|
+
* @param {string} dir - Directory to search
|
|
267
|
+
* @param {string} extension - File extension (e.g., '.ts')
|
|
268
|
+
* @param {string[]} fileList - Accumulator for found files
|
|
269
|
+
* @returns {string[]} Array of file paths
|
|
270
|
+
*/
|
|
198
271
|
function findFiles(dir, extension, fileList = []) {
|
|
199
272
|
const files = fs.readdirSync(dir);
|
|
200
|
-
|
|
273
|
+
|
|
201
274
|
files.forEach(file => {
|
|
202
275
|
const filePath = path.join(dir, file);
|
|
203
276
|
const stat = fs.statSync(filePath);
|
|
204
|
-
|
|
277
|
+
|
|
205
278
|
if (stat.isDirectory()) {
|
|
206
279
|
findFiles(filePath, extension, fileList);
|
|
207
280
|
} else if (file.endsWith(extension)) {
|
|
208
281
|
fileList.push(filePath);
|
|
209
282
|
}
|
|
210
283
|
});
|
|
211
|
-
|
|
284
|
+
|
|
212
285
|
return fileList;
|
|
213
286
|
}
|
|
214
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Copy non-TypeScript files (CSS, JSON, JS, SVG, etc.)
|
|
290
|
+
*
|
|
291
|
+
* @param {string} src - Source directory
|
|
292
|
+
* @param {string} dest - Destination directory
|
|
293
|
+
*/
|
|
215
294
|
function copyNonTsFiles(src, dest) {
|
|
216
295
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
217
296
|
|
|
@@ -226,23 +305,32 @@ function copyNonTsFiles(src, dest) {
|
|
|
226
305
|
copyNonTsFiles(srcPath, destPath);
|
|
227
306
|
} else if (entry.isFile()) {
|
|
228
307
|
const ext = path.extname(entry.name);
|
|
229
|
-
// Copy CSS, JSON, and JS files (but not .ts files)
|
|
230
|
-
if (
|
|
308
|
+
// Copy CSS, JSON, SVG, and JS files (but not .ts files)
|
|
309
|
+
if (['.css', '.json', '.js', '.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext)) {
|
|
231
310
|
fs.copyFileSync(srcPath, destPath);
|
|
232
|
-
console.log(` → Copied: ${path.relative(src, srcPath)}`);
|
|
233
311
|
}
|
|
234
312
|
}
|
|
235
313
|
}
|
|
236
314
|
}
|
|
237
315
|
|
|
238
|
-
|
|
316
|
+
/**
|
|
317
|
+
* Find project files with specific extensions, excluding certain directories
|
|
318
|
+
*
|
|
319
|
+
* @param {string} dir - Directory to search
|
|
320
|
+
* @param {string[]} extensions - File extensions to find
|
|
321
|
+
* @param {string[]} fileList - Accumulator for found files
|
|
322
|
+
* @param {string} rootDir - Root directory for relative paths
|
|
323
|
+
* @param {string[]} excludeDirs - Directories to exclude
|
|
324
|
+
* @returns {string[]} Array of file paths
|
|
325
|
+
*/
|
|
326
|
+
function findProjectFiles(dir, extensions, fileList = [], rootDir = dir, excludeDirs = ['node_modules', 'jux-dist', '.git', 'lib']) {
|
|
239
327
|
if (!fs.existsSync(dir)) return fileList;
|
|
240
|
-
|
|
328
|
+
|
|
241
329
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
242
|
-
|
|
330
|
+
|
|
243
331
|
for (const entry of entries) {
|
|
244
332
|
const fullPath = path.join(dir, entry.name);
|
|
245
|
-
|
|
333
|
+
|
|
246
334
|
if (entry.isDirectory()) {
|
|
247
335
|
// Skip excluded directories
|
|
248
336
|
if (excludeDirs.includes(entry.name)) {
|
|
@@ -257,6 +345,6 @@ function findProjectFiles(dir, extensions, fileList = [], rootDir = dir, exclude
|
|
|
257
345
|
}
|
|
258
346
|
}
|
|
259
347
|
}
|
|
260
|
-
|
|
348
|
+
|
|
261
349
|
return fileList;
|
|
262
350
|
}
|
|
@@ -42,8 +42,7 @@ export function generateHTML(fileName, options = {}) {
|
|
|
42
42
|
<title>${fileName}</title>
|
|
43
43
|
|
|
44
44
|
<!-- JUX Core Styles -->
|
|
45
|
-
<link rel="stylesheet" href="${prefix}lib/
|
|
46
|
-
<link rel="stylesheet" href="${prefix}lib/styles/global.css">
|
|
45
|
+
<link rel="stylesheet" href="${prefix}lib/presets/global.css">
|
|
47
46
|
</head>
|
|
48
47
|
<body data-theme="dark">
|
|
49
48
|
${appStructure}
|
|
@@ -61,7 +60,7 @@ ${appStructure}
|
|
|
61
60
|
*/
|
|
62
61
|
function buildAppStructure(pageName) {
|
|
63
62
|
const page = pageName || 'index';
|
|
64
|
-
|
|
63
|
+
|
|
65
64
|
return ` <!-- App Container -->
|
|
66
65
|
<div id="app" data-jux-page="${page}">
|
|
67
66
|
<!-- Header -->
|
package/machinery/server.js
CHANGED
|
@@ -186,7 +186,7 @@ async function initDatabase() {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
export async function start(port = 3000
|
|
189
|
+
export async function start(port = 3000) {
|
|
190
190
|
await initDatabase();
|
|
191
|
-
return serve(port,
|
|
191
|
+
return serve(port, './jux-dist'); // Changed default
|
|
192
192
|
}
|