juxscript 1.0.8 → 1.0.9
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 +12 -4
- package/bin/cli.js +66 -38
- package/lib/components/divider.ts +233 -0
- package/lib/components/docs-data.json +70 -1
- package/lib/components/hero1/hero1.ts +196 -0
- package/lib/components/hero1/index.js +4 -0
- package/lib/components/kpicard.ts +43 -8
- package/lib/components/sidebar.ts +13 -5
- package/lib/jux.ts +8 -4
- package/lib/presets/global.css +0 -1
- package/machinery/compiler.js +21 -4
- package/machinery/server.js +19 -16
- package/package.json +32 -52
- package/machinery/generators/css.js +0 -128
package/README.md
CHANGED
|
@@ -7,25 +7,33 @@
|
|
|
7
7
|
+ Stay tuned! For now, you will see our roadmap here!
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
+
- [X] Router
|
|
11
|
+
- [ ] Cross Page Store.
|
|
12
|
+
- [ ] Distributable Bundle (Static Sites)
|
|
13
|
+
- [ ] Tree Shake/Efficiencies.
|
|
14
|
+
|
|
10
15
|
- [X] Layouts (100% done.)
|
|
16
|
+
- [ ] *Authoring Layout Pages* - `docs`
|
|
17
|
+
- [ ] *Authoring Application Pages* - `docs`
|
|
18
|
+
- [ ] *Authoring Custom Components* - `docs`
|
|
19
|
+
|
|
11
20
|
- [ ] Render Dependency Tree
|
|
12
21
|
> Idea here is, one element may be a predicate for another. Will need promises. **predicting problems with slow-loading components that depend on containers from other components. May to to separate concerns with container "building" vs. content addition OR use async processes (promises).
|
|
13
22
|
- [X] Reactivity (90% done.)
|
|
14
23
|
- [ ] Client Components (99% of what would be needed.)
|
|
15
|
-
- [
|
|
24
|
+
- [X] Charts
|
|
16
25
|
- [ ] Poor Intellisense support? Could be this issue.
|
|
17
26
|
- [ ] Api Wrapper
|
|
18
27
|
- [X] Params/Active State for Menu/Nav matching - built in.
|
|
19
28
|
- [ ] CDN Bundle (import CDN/'state', 'jux' from cdn.)
|
|
20
29
|
- [ ] Icon
|
|
21
|
-
|
|
30
|
+
|
|
22
31
|
- [ ] Quickstart Boilerplates (20% done,notion.jux)
|
|
23
32
|
- [ ] Mobile Nav
|
|
24
33
|
- [ ] `npx jux present notion|default` etc..
|
|
25
34
|
- [ ] Server side components (api and database)
|
|
26
35
|
- [ ] Quick deploy option
|
|
27
|
-
|
|
28
|
-
- [ ] Tree Shake/Efficiencies.
|
|
36
|
+
|
|
29
37
|
|
|
30
38
|
## *JUX* Authoring UI's in pure javascript.
|
|
31
39
|
|
package/bin/cli.js
CHANGED
|
@@ -183,33 +183,59 @@ async function buildProject(isServe = false) {
|
|
|
183
183
|
// Create structure
|
|
184
184
|
fs.mkdirSync(juxDir, { recursive: true });
|
|
185
185
|
|
|
186
|
-
//
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (fs.existsSync(templatePath)) {
|
|
191
|
-
fs.copyFileSync(templatePath, targetPath);
|
|
192
|
-
console.log('✅ Created jux/index.jux from template');
|
|
193
|
-
} else {
|
|
194
|
-
// Fallback if template doesn't exist
|
|
195
|
-
console.warn('⚠️ Template not found, creating basic index.jux');
|
|
196
|
-
const fallbackContent = `// Welcome to JUX!
|
|
197
|
-
import { jux } from '/lib/jux.js';
|
|
186
|
+
// Create index.jux with proper imports
|
|
187
|
+
const indexContent = `// Welcome to JUX!
|
|
188
|
+
import { jux } from 'juxscript';
|
|
198
189
|
|
|
199
|
-
|
|
200
|
-
|
|
190
|
+
// Apply a layout preset (optional)
|
|
191
|
+
// import 'juxscript/presets/notion.js';
|
|
201
192
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
193
|
+
// Create your app structure
|
|
194
|
+
jux.container('app-container')
|
|
195
|
+
.direction('column')
|
|
196
|
+
.gap(20)
|
|
197
|
+
.style('padding: 40px;')
|
|
198
|
+
.render('body');
|
|
205
199
|
|
|
206
|
-
jux.hero('
|
|
200
|
+
jux.hero('welcome-hero', {
|
|
207
201
|
title: 'Welcome to JUX',
|
|
208
202
|
subtitle: 'A JavaScript UX authorship platform'
|
|
209
|
-
}).render('#
|
|
203
|
+
}).render('#app-container');
|
|
204
|
+
|
|
205
|
+
jux.divider({}).render('#app-container');
|
|
206
|
+
|
|
207
|
+
jux.write(\`
|
|
208
|
+
<h2>Getting Started</h2>
|
|
209
|
+
<p>Edit <code>jux/index.jux</code> to build your app.</p>
|
|
210
|
+
<ul>
|
|
211
|
+
<li>Run <code>npx jux build</code> to compile</li>
|
|
212
|
+
<li>Run <code>npx jux serve</code> for dev mode</li>
|
|
213
|
+
<li>Serve <code>jux-dist/</code> from your backend</li>
|
|
214
|
+
</ul>
|
|
215
|
+
\`).render('#app-container');
|
|
210
216
|
`;
|
|
211
|
-
|
|
212
|
-
|
|
217
|
+
|
|
218
|
+
const targetPath = path.join(juxDir, 'index.jux');
|
|
219
|
+
fs.writeFileSync(targetPath, indexContent);
|
|
220
|
+
console.log('✅ Created jux/index.jux');
|
|
221
|
+
|
|
222
|
+
// Create package.json if it doesn't exist
|
|
223
|
+
const pkgPath = path.join(PATHS.projectRoot, 'package.json');
|
|
224
|
+
if (!fs.existsSync(pkgPath)) {
|
|
225
|
+
const pkgContent = {
|
|
226
|
+
"name": "my-jux-project",
|
|
227
|
+
"version": "1.0.0",
|
|
228
|
+
"type": "module",
|
|
229
|
+
"scripts": {
|
|
230
|
+
"build": "jux build",
|
|
231
|
+
"serve": "jux serve"
|
|
232
|
+
},
|
|
233
|
+
"dependencies": {
|
|
234
|
+
"juxscript": "^1.0.8"
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkgContent, null, 2));
|
|
238
|
+
console.log('✅ Created package.json');
|
|
213
239
|
}
|
|
214
240
|
|
|
215
241
|
// Create .gitignore
|
|
@@ -226,9 +252,10 @@ node_modules/
|
|
|
226
252
|
|
|
227
253
|
console.log('✅ Created jux/ directory\n');
|
|
228
254
|
console.log('Next steps:');
|
|
229
|
-
console.log(' 1.
|
|
230
|
-
console.log(' 2.
|
|
231
|
-
console.log(' 3.
|
|
255
|
+
console.log(' 1. npm install # Install juxscript');
|
|
256
|
+
console.log(' 2. Edit jux/index.jux # Build your app');
|
|
257
|
+
console.log(' 3. npx jux build # Compile to jux-dist/');
|
|
258
|
+
console.log(' 4. Serve jux-dist/ from your backend\n');
|
|
232
259
|
|
|
233
260
|
} else if (command === 'build') {
|
|
234
261
|
await buildProject(false);
|
|
@@ -249,24 +276,25 @@ Usage:
|
|
|
249
276
|
npx jux build Compile .jux files from ./jux/ to ./jux-dist/
|
|
250
277
|
npx jux serve [port] Start dev server with hot reload (default: 3000)
|
|
251
278
|
|
|
252
|
-
Project Structure
|
|
279
|
+
Project Structure:
|
|
253
280
|
my-project/
|
|
254
|
-
├── jux/ # Your .jux source files
|
|
255
|
-
│ ├── index.jux
|
|
256
|
-
│
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
├── jux-dist/ # Build output (generated, git-ignore this)
|
|
260
|
-
│ ├── samples/
|
|
261
|
-
│ │ └── mypage.js # Transpiled TypeScript
|
|
262
|
-
│ └── ...
|
|
263
|
-
├── server/ # Your backend (untouched by jux)
|
|
281
|
+
├── jux/ # Your .jux source files
|
|
282
|
+
│ ├── index.jux # Entry point (uses 'juxscript' imports)
|
|
283
|
+
│ └── pages/ # Additional pages
|
|
284
|
+
├── jux-dist/ # Build output (git-ignore this)
|
|
285
|
+
├── server/ # Your backend
|
|
264
286
|
└── package.json
|
|
265
287
|
|
|
288
|
+
Import Style:
|
|
289
|
+
// In your project's .jux files
|
|
290
|
+
import { jux, state } from 'juxscript';
|
|
291
|
+
import 'juxscript/presets/notion.js';
|
|
292
|
+
|
|
266
293
|
Getting Started:
|
|
267
|
-
1. npx jux init # Create
|
|
268
|
-
2.
|
|
269
|
-
3.
|
|
294
|
+
1. npx jux init # Create project structure
|
|
295
|
+
2. npm install # Install dependencies
|
|
296
|
+
3. npx jux build # Build to jux-dist/
|
|
297
|
+
4. Serve jux-dist/ from your backend
|
|
270
298
|
|
|
271
299
|
Examples:
|
|
272
300
|
npx jux build Build for production
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Divider - Simple horizontal or vertical divider line
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface DividerOptions {
|
|
6
|
+
orientation?: 'horizontal' | 'vertical';
|
|
7
|
+
thickness?: number;
|
|
8
|
+
color?: string;
|
|
9
|
+
margin?: string;
|
|
10
|
+
style?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Divider component for visual separation
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* // Simple horizontal divider
|
|
19
|
+
* jux.divider().render('#container');
|
|
20
|
+
*
|
|
21
|
+
* // Vertical divider
|
|
22
|
+
* jux.divider({ orientation: 'vertical' }).render('#container');
|
|
23
|
+
*
|
|
24
|
+
* // Custom styling
|
|
25
|
+
* jux.divider({
|
|
26
|
+
* thickness: 2,
|
|
27
|
+
* color: '#3b82f6',
|
|
28
|
+
* margin: '24px 0'
|
|
29
|
+
* }).render('#container');
|
|
30
|
+
*
|
|
31
|
+
* // With custom class
|
|
32
|
+
* jux.divider({ className: 'my-divider' }).render('#container');
|
|
33
|
+
*/
|
|
34
|
+
export class Divider {
|
|
35
|
+
private options: Required<DividerOptions>;
|
|
36
|
+
|
|
37
|
+
constructor(options: DividerOptions = {}) {
|
|
38
|
+
this.options = {
|
|
39
|
+
orientation: 'horizontal',
|
|
40
|
+
thickness: 1,
|
|
41
|
+
color: '#e5e7eb',
|
|
42
|
+
margin: '16px 0',
|
|
43
|
+
style: '',
|
|
44
|
+
className: '',
|
|
45
|
+
...options
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Set orientation
|
|
51
|
+
*/
|
|
52
|
+
orientation(value: 'horizontal' | 'vertical'): this {
|
|
53
|
+
this.options.orientation = value;
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Set thickness in pixels
|
|
59
|
+
*/
|
|
60
|
+
thickness(value: number): this {
|
|
61
|
+
this.options.thickness = value;
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Set color
|
|
67
|
+
*/
|
|
68
|
+
color(value: string): this {
|
|
69
|
+
this.options.color = value;
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set margin
|
|
75
|
+
*/
|
|
76
|
+
margin(value: string): this {
|
|
77
|
+
this.options.margin = value;
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Set custom styles
|
|
83
|
+
*/
|
|
84
|
+
style(value: string): this {
|
|
85
|
+
this.options.style = value;
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Set class name
|
|
91
|
+
*/
|
|
92
|
+
className(value: string): this {
|
|
93
|
+
this.options.className = value;
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Render divider to target element
|
|
99
|
+
*/
|
|
100
|
+
render(targetSelector: string): this {
|
|
101
|
+
const target = document.querySelector(targetSelector);
|
|
102
|
+
|
|
103
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
104
|
+
console.warn(`Divider: Target element "${targetSelector}" not found`);
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const divider = document.createElement('hr');
|
|
109
|
+
divider.className = `jux-divider ${this.options.className}`.trim();
|
|
110
|
+
|
|
111
|
+
const isVertical = this.options.orientation === 'vertical';
|
|
112
|
+
|
|
113
|
+
const baseStyles = `
|
|
114
|
+
border: none;
|
|
115
|
+
background-color: ${this.options.color};
|
|
116
|
+
margin: ${this.options.margin};
|
|
117
|
+
${isVertical ? `
|
|
118
|
+
width: ${this.options.thickness}px;
|
|
119
|
+
height: 100%;
|
|
120
|
+
display: inline-block;
|
|
121
|
+
vertical-align: middle;
|
|
122
|
+
` : `
|
|
123
|
+
width: 100%;
|
|
124
|
+
height: ${this.options.thickness}px;
|
|
125
|
+
`}
|
|
126
|
+
${this.options.style}
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
divider.setAttribute('style', baseStyles);
|
|
130
|
+
|
|
131
|
+
target.appendChild(divider);
|
|
132
|
+
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Replace target content with divider
|
|
138
|
+
*/
|
|
139
|
+
replace(targetSelector: string): this {
|
|
140
|
+
const target = document.querySelector(targetSelector);
|
|
141
|
+
|
|
142
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
143
|
+
console.warn(`Divider: Target element "${targetSelector}" not found`);
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
target.innerHTML = '';
|
|
148
|
+
return this.render(targetSelector);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Render before target element
|
|
153
|
+
*/
|
|
154
|
+
before(targetSelector: string): this {
|
|
155
|
+
const target = document.querySelector(targetSelector);
|
|
156
|
+
|
|
157
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
158
|
+
console.warn(`Divider: Target element "${targetSelector}" not found`);
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const divider = document.createElement('hr');
|
|
163
|
+
divider.className = `jux-divider ${this.options.className}`.trim();
|
|
164
|
+
|
|
165
|
+
const isVertical = this.options.orientation === 'vertical';
|
|
166
|
+
|
|
167
|
+
const baseStyles = `
|
|
168
|
+
border: none;
|
|
169
|
+
background-color: ${this.options.color};
|
|
170
|
+
margin: ${this.options.margin};
|
|
171
|
+
${isVertical ? `
|
|
172
|
+
width: ${this.options.thickness}px;
|
|
173
|
+
height: 100%;
|
|
174
|
+
display: inline-block;
|
|
175
|
+
vertical-align: middle;
|
|
176
|
+
` : `
|
|
177
|
+
width: 100%;
|
|
178
|
+
height: ${this.options.thickness}px;
|
|
179
|
+
`}
|
|
180
|
+
${this.options.style}
|
|
181
|
+
`;
|
|
182
|
+
|
|
183
|
+
divider.setAttribute('style', baseStyles);
|
|
184
|
+
target.parentNode?.insertBefore(divider, target);
|
|
185
|
+
|
|
186
|
+
return this;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Render after target element
|
|
191
|
+
*/
|
|
192
|
+
after(targetSelector: string): this {
|
|
193
|
+
const target = document.querySelector(targetSelector);
|
|
194
|
+
|
|
195
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
196
|
+
console.warn(`Divider: Target element "${targetSelector}" not found`);
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const divider = document.createElement('hr');
|
|
201
|
+
divider.className = `jux-divider ${this.options.className}`.trim();
|
|
202
|
+
|
|
203
|
+
const isVertical = this.options.orientation === 'vertical';
|
|
204
|
+
|
|
205
|
+
const baseStyles = `
|
|
206
|
+
border: none;
|
|
207
|
+
background-color: ${this.options.color};
|
|
208
|
+
margin: ${this.options.margin};
|
|
209
|
+
${isVertical ? `
|
|
210
|
+
width: ${this.options.thickness}px;
|
|
211
|
+
height: 100%;
|
|
212
|
+
display: inline-block;
|
|
213
|
+
vertical-align: middle;
|
|
214
|
+
` : `
|
|
215
|
+
width: 100%;
|
|
216
|
+
height: ${this.options.thickness}px;
|
|
217
|
+
`}
|
|
218
|
+
${this.options.style}
|
|
219
|
+
`;
|
|
220
|
+
|
|
221
|
+
divider.setAttribute('style', baseStyles);
|
|
222
|
+
target.parentNode?.insertBefore(divider, target.nextSibling);
|
|
223
|
+
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Factory function for quick divider creation
|
|
230
|
+
*/
|
|
231
|
+
export function divider(options: DividerOptions = {}): Divider {
|
|
232
|
+
return new Divider(options);
|
|
233
|
+
}
|
|
@@ -1553,6 +1553,75 @@
|
|
|
1553
1553
|
],
|
|
1554
1554
|
"example": "jux.dialog('confirm-delete', {"
|
|
1555
1555
|
},
|
|
1556
|
+
{
|
|
1557
|
+
"name": "Divider",
|
|
1558
|
+
"category": "UI Components",
|
|
1559
|
+
"description": "Divider - Simple horizontal or vertical divider line",
|
|
1560
|
+
"constructor": "jux.divider(options: DividerOptions = {})",
|
|
1561
|
+
"fluentMethods": [
|
|
1562
|
+
{
|
|
1563
|
+
"name": "orientation",
|
|
1564
|
+
"params": "(value)",
|
|
1565
|
+
"returns": "this",
|
|
1566
|
+
"description": "Set orientation"
|
|
1567
|
+
},
|
|
1568
|
+
{
|
|
1569
|
+
"name": "thickness",
|
|
1570
|
+
"params": "(value)",
|
|
1571
|
+
"returns": "this",
|
|
1572
|
+
"description": "Set thickness"
|
|
1573
|
+
},
|
|
1574
|
+
{
|
|
1575
|
+
"name": "color",
|
|
1576
|
+
"params": "(value)",
|
|
1577
|
+
"returns": "this",
|
|
1578
|
+
"description": "Set color"
|
|
1579
|
+
},
|
|
1580
|
+
{
|
|
1581
|
+
"name": "margin",
|
|
1582
|
+
"params": "(value)",
|
|
1583
|
+
"returns": "this",
|
|
1584
|
+
"description": "Set margin"
|
|
1585
|
+
},
|
|
1586
|
+
{
|
|
1587
|
+
"name": "style",
|
|
1588
|
+
"params": "(value)",
|
|
1589
|
+
"returns": "this",
|
|
1590
|
+
"description": "Set style"
|
|
1591
|
+
},
|
|
1592
|
+
{
|
|
1593
|
+
"name": "className",
|
|
1594
|
+
"params": "(value)",
|
|
1595
|
+
"returns": "this",
|
|
1596
|
+
"description": "Set className"
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
"name": "render",
|
|
1600
|
+
"params": "(targetSelector)",
|
|
1601
|
+
"returns": "this",
|
|
1602
|
+
"description": "Set render"
|
|
1603
|
+
},
|
|
1604
|
+
{
|
|
1605
|
+
"name": "replace",
|
|
1606
|
+
"params": "(targetSelector)",
|
|
1607
|
+
"returns": "this",
|
|
1608
|
+
"description": "Set replace"
|
|
1609
|
+
},
|
|
1610
|
+
{
|
|
1611
|
+
"name": "before",
|
|
1612
|
+
"params": "(targetSelector)",
|
|
1613
|
+
"returns": "this",
|
|
1614
|
+
"description": "Set before"
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
"name": "after",
|
|
1618
|
+
"params": "(targetSelector)",
|
|
1619
|
+
"returns": "this",
|
|
1620
|
+
"description": "Set after"
|
|
1621
|
+
}
|
|
1622
|
+
],
|
|
1623
|
+
"example": "// Simple horizontal divider"
|
|
1624
|
+
},
|
|
1556
1625
|
{
|
|
1557
1626
|
"name": "Doughnutchart",
|
|
1558
1627
|
"category": "UI Components",
|
|
@@ -3443,5 +3512,5 @@
|
|
|
3443
3512
|
}
|
|
3444
3513
|
],
|
|
3445
3514
|
"version": "1.0.0",
|
|
3446
|
-
"lastUpdated": "2026-01-
|
|
3515
|
+
"lastUpdated": "2026-01-21T20:25:28.145Z"
|
|
3447
3516
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { getOrCreateContainer } from '../helpers.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hero component options
|
|
5
|
+
*/
|
|
6
|
+
export interface HeroOptions {
|
|
7
|
+
title?: string;
|
|
8
|
+
subtitle?: string;
|
|
9
|
+
cta?: string;
|
|
10
|
+
ctaLink?: string;
|
|
11
|
+
backgroundImage?: string;
|
|
12
|
+
variant?: 'default' | 'centered' | 'split';
|
|
13
|
+
style?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Hero component state
|
|
19
|
+
*/
|
|
20
|
+
type HeroState = {
|
|
21
|
+
title: string;
|
|
22
|
+
subtitle: string;
|
|
23
|
+
cta: string;
|
|
24
|
+
ctaLink: string;
|
|
25
|
+
backgroundImage: string;
|
|
26
|
+
variant: string;
|
|
27
|
+
style: string;
|
|
28
|
+
class: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Hero component
|
|
33
|
+
*
|
|
34
|
+
* Usage:
|
|
35
|
+
* const hero = jux.hero('myHero', {
|
|
36
|
+
* title: 'Welcome',
|
|
37
|
+
* subtitle: 'Get started today',
|
|
38
|
+
* cta: 'Learn More'
|
|
39
|
+
* });
|
|
40
|
+
* hero.render();
|
|
41
|
+
*/
|
|
42
|
+
export class Hero1 {
|
|
43
|
+
state: HeroState;
|
|
44
|
+
container: HTMLElement | null = null;
|
|
45
|
+
_id: string;
|
|
46
|
+
id: string;
|
|
47
|
+
|
|
48
|
+
constructor(id: string, options: HeroOptions = {}) {
|
|
49
|
+
this._id = id;
|
|
50
|
+
this.id = id;
|
|
51
|
+
|
|
52
|
+
this.state = {
|
|
53
|
+
title: options.title ?? '',
|
|
54
|
+
subtitle: options.subtitle ?? '',
|
|
55
|
+
cta: options.cta ?? '',
|
|
56
|
+
ctaLink: options.ctaLink ?? '#',
|
|
57
|
+
backgroundImage: options.backgroundImage ?? '',
|
|
58
|
+
variant: options.variant ?? 'default',
|
|
59
|
+
style: options.style ?? '',
|
|
60
|
+
class: options.class ?? ''
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* -------------------------
|
|
65
|
+
* Fluent API
|
|
66
|
+
* ------------------------- */
|
|
67
|
+
|
|
68
|
+
title(value: string): this {
|
|
69
|
+
this.state.title = value;
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
subtitle(value: string): this {
|
|
74
|
+
this.state.subtitle = value;
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
cta(value: string): this {
|
|
79
|
+
this.state.cta = value;
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
ctaLink(value: string): this {
|
|
84
|
+
this.state.ctaLink = value;
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
backgroundImage(value: string): this {
|
|
89
|
+
this.state.backgroundImage = value;
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
variant(value: string): this {
|
|
94
|
+
this.state.variant = value;
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
style(value: string): this {
|
|
99
|
+
this.state.style = value;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
class(value: string): this {
|
|
104
|
+
this.state.class = value;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* -------------------------
|
|
109
|
+
* Render
|
|
110
|
+
* ------------------------- */
|
|
111
|
+
|
|
112
|
+
render(targetId?: string): this {
|
|
113
|
+
let container: HTMLElement;
|
|
114
|
+
|
|
115
|
+
if (targetId) {
|
|
116
|
+
const target = document.querySelector(targetId);
|
|
117
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
118
|
+
throw new Error(`Hero: Target element "${targetId}" not found`);
|
|
119
|
+
}
|
|
120
|
+
container = target;
|
|
121
|
+
} else {
|
|
122
|
+
container = getOrCreateContainer(this._id);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.container = container;
|
|
126
|
+
const { title, subtitle, cta, ctaLink, backgroundImage, variant, style, class: className } = this.state;
|
|
127
|
+
|
|
128
|
+
const hero = document.createElement('div');
|
|
129
|
+
hero.className = `jux-hero jux-hero-${variant}`;
|
|
130
|
+
hero.id = this._id;
|
|
131
|
+
|
|
132
|
+
if (className) {
|
|
133
|
+
hero.className += ` ${className}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (style) {
|
|
137
|
+
hero.setAttribute('style', style);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (backgroundImage) {
|
|
141
|
+
hero.style.backgroundImage = `url(${backgroundImage})`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const content = document.createElement('div');
|
|
145
|
+
content.className = 'jux-hero-content';
|
|
146
|
+
|
|
147
|
+
if (title) {
|
|
148
|
+
const titleEl = document.createElement('h1');
|
|
149
|
+
titleEl.className = 'jux-hero-title';
|
|
150
|
+
titleEl.textContent = title;
|
|
151
|
+
content.appendChild(titleEl);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (subtitle) {
|
|
155
|
+
const subtitleEl = document.createElement('p');
|
|
156
|
+
subtitleEl.className = 'jux-hero-subtitle';
|
|
157
|
+
subtitleEl.textContent = subtitle;
|
|
158
|
+
content.appendChild(subtitleEl);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (cta) {
|
|
162
|
+
const ctaEl = document.createElement('a');
|
|
163
|
+
ctaEl.className = 'jux-hero-cta jux-button jux-button-primary';
|
|
164
|
+
ctaEl.href = ctaLink;
|
|
165
|
+
ctaEl.textContent = cta;
|
|
166
|
+
content.appendChild(ctaEl);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
hero.appendChild(content);
|
|
170
|
+
container.appendChild(hero);
|
|
171
|
+
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Render to another Jux component's container
|
|
177
|
+
*/
|
|
178
|
+
renderTo(juxComponent: any): this {
|
|
179
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
180
|
+
throw new Error('Hero.renderTo: Invalid component - not an object');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
184
|
+
throw new Error('Hero.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return this.render(`#${juxComponent._id}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Factory helper
|
|
193
|
+
*/
|
|
194
|
+
export function hero1(id: string, options: HeroOptions = {}): Hero1 {
|
|
195
|
+
return new Hero1(id, options);
|
|
196
|
+
}
|
|
@@ -242,6 +242,11 @@ export class KPICard {
|
|
|
242
242
|
return this;
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
// Remove existing element if it exists
|
|
246
|
+
if (this._element && this._element.parentNode) {
|
|
247
|
+
this._element.parentNode.removeChild(this._element);
|
|
248
|
+
}
|
|
249
|
+
|
|
245
250
|
this._loadThemeFont();
|
|
246
251
|
this._buildCard(element as HTMLElement);
|
|
247
252
|
return this;
|
|
@@ -253,8 +258,11 @@ export class KPICard {
|
|
|
253
258
|
const element = document.querySelector(this._container);
|
|
254
259
|
if (!element) return;
|
|
255
260
|
|
|
256
|
-
//
|
|
257
|
-
|
|
261
|
+
// Remove existing element
|
|
262
|
+
if (this._element && this._element.parentNode) {
|
|
263
|
+
this._element.parentNode.removeChild(this._element);
|
|
264
|
+
}
|
|
265
|
+
|
|
258
266
|
this._loadThemeFont();
|
|
259
267
|
this._buildCard(element as HTMLElement);
|
|
260
268
|
}
|
|
@@ -317,6 +325,9 @@ export class KPICard {
|
|
|
317
325
|
const baseHeight = 200;
|
|
318
326
|
const scaleFactor = Math.min(width / baseWidth, height / baseHeight);
|
|
319
327
|
|
|
328
|
+
// Determine if we're in dark mode based on style mode
|
|
329
|
+
const isDarkMode = styleMode === 'gradient' || styleMode === 'glass';
|
|
330
|
+
|
|
320
331
|
const content = document.createElement('div');
|
|
321
332
|
content.className = 'jux-kpicard-content';
|
|
322
333
|
content.style.cssText = `
|
|
@@ -331,10 +342,19 @@ export class KPICard {
|
|
|
331
342
|
const titleEl = document.createElement('div');
|
|
332
343
|
titleEl.className = 'jux-kpicard-title';
|
|
333
344
|
titleEl.textContent = title;
|
|
345
|
+
|
|
346
|
+
// Smart color selection based on theme
|
|
347
|
+
let titleColor = '#6b7280'; // default light mode
|
|
348
|
+
if (styleMode === 'gradient') {
|
|
349
|
+
titleColor = 'rgba(255, 255, 255, 0.95)';
|
|
350
|
+
} else if (styleMode === 'glass') {
|
|
351
|
+
titleColor = 'rgba(31, 41, 55, 0.9)'; // dark text for glass effect
|
|
352
|
+
}
|
|
353
|
+
|
|
334
354
|
titleEl.style.cssText = `
|
|
335
355
|
font-size: ${16 * scaleFactor}px;
|
|
336
356
|
font-weight: 500;
|
|
337
|
-
color: ${
|
|
357
|
+
color: ${titleColor};
|
|
338
358
|
margin-bottom: ${16 * scaleFactor}px;
|
|
339
359
|
font-family: ${themeConfig.variables['--chart-font-family']};
|
|
340
360
|
`;
|
|
@@ -352,10 +372,19 @@ export class KPICard {
|
|
|
352
372
|
const valueEl = document.createElement('div');
|
|
353
373
|
valueEl.className = 'jux-kpicard-value';
|
|
354
374
|
valueEl.textContent = `${prefix}${value}${suffix}`;
|
|
375
|
+
|
|
376
|
+
// Smart color selection for value
|
|
377
|
+
let valueColor = '#1f2937'; // default dark
|
|
378
|
+
if (styleMode === 'gradient') {
|
|
379
|
+
valueColor = '#ffffff';
|
|
380
|
+
} else if (styleMode === 'glass') {
|
|
381
|
+
valueColor = '#111827'; // very dark for glass
|
|
382
|
+
}
|
|
383
|
+
|
|
355
384
|
valueEl.style.cssText = `
|
|
356
385
|
font-size: ${56 * scaleFactor}px;
|
|
357
386
|
font-weight: 800;
|
|
358
|
-
color: ${
|
|
387
|
+
color: ${valueColor};
|
|
359
388
|
line-height: 1;
|
|
360
389
|
font-family: ${themeConfig.variables['--chart-font-family']};
|
|
361
390
|
${styleMode === 'glow' ? `text-shadow: 0 0 ${20 * scaleFactor}px ${themeConfig.colors[0]}40;` : ''}
|
|
@@ -391,12 +420,18 @@ export class KPICard {
|
|
|
391
420
|
const arrow = this._createArrowSVG(delta > 0, styleMode === 'gradient', scaleFactor);
|
|
392
421
|
deltaContainer.appendChild(arrow);
|
|
393
422
|
|
|
394
|
-
// Delta text
|
|
423
|
+
// Delta text with smart coloring
|
|
395
424
|
const deltaText = document.createElement('span');
|
|
396
425
|
deltaText.textContent = `${delta > 0 ? '+' : ''}${delta}%`;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
426
|
+
|
|
427
|
+
let deltaColor;
|
|
428
|
+
if (styleMode === 'gradient') {
|
|
429
|
+
deltaColor = delta > 0 ? 'rgba(255, 255, 255, 0.95)' : 'rgba(255, 200, 200, 0.95)';
|
|
430
|
+
} else if (styleMode === 'glass') {
|
|
431
|
+
deltaColor = delta > 0 ? '#10b981' : '#ef4444';
|
|
432
|
+
} else {
|
|
433
|
+
deltaColor = delta > 0 ? '#10b981' : '#ef4444';
|
|
434
|
+
}
|
|
400
435
|
|
|
401
436
|
deltaText.style.cssText = `
|
|
402
437
|
font-size: ${18 * scaleFactor}px;
|
|
@@ -18,7 +18,7 @@ export interface SidebarOptions {
|
|
|
18
18
|
*/
|
|
19
19
|
type SidebarState = {
|
|
20
20
|
title: string;
|
|
21
|
-
width: string;
|
|
21
|
+
width: string | null;
|
|
22
22
|
position: string;
|
|
23
23
|
collapsible: boolean;
|
|
24
24
|
collapsed: boolean;
|
|
@@ -49,7 +49,7 @@ export class Sidebar {
|
|
|
49
49
|
|
|
50
50
|
this.state = {
|
|
51
51
|
title: options.title ?? '',
|
|
52
|
-
width: options.width ??
|
|
52
|
+
width: options.width ?? null, // No default width - let CSS handle it
|
|
53
53
|
position: options.position ?? 'left',
|
|
54
54
|
collapsible: options.collapsible ?? false,
|
|
55
55
|
collapsed: options.collapsed ?? false,
|
|
@@ -117,10 +117,14 @@ export class Sidebar {
|
|
|
117
117
|
|
|
118
118
|
if (collapsed) {
|
|
119
119
|
sidebar.classList.add('jux-sidebar-collapsed');
|
|
120
|
-
|
|
120
|
+
if (width) {
|
|
121
|
+
sidebar.style.width = '0';
|
|
122
|
+
}
|
|
121
123
|
} else {
|
|
122
124
|
sidebar.classList.remove('jux-sidebar-collapsed');
|
|
123
|
-
|
|
125
|
+
if (width) {
|
|
126
|
+
sidebar.style.width = width;
|
|
127
|
+
}
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
const toggleBtn = sidebar.querySelector('.jux-sidebar-toggle');
|
|
@@ -152,7 +156,11 @@ export class Sidebar {
|
|
|
152
156
|
const sidebar = document.createElement('aside');
|
|
153
157
|
sidebar.className = `jux-sidebar jux-sidebar-${position}`;
|
|
154
158
|
sidebar.id = this._id;
|
|
155
|
-
|
|
159
|
+
|
|
160
|
+
// Only set width if explicitly provided
|
|
161
|
+
if (width) {
|
|
162
|
+
sidebar.style.width = collapsed ? '0' : width;
|
|
163
|
+
}
|
|
156
164
|
|
|
157
165
|
if (className) {
|
|
158
166
|
sidebar.className += ` ${className}`;
|
package/lib/jux.ts
CHANGED
|
@@ -51,9 +51,10 @@ import { heading, Heading, type HeadingOptions } from './components/heading.js';
|
|
|
51
51
|
import { paragraph, Paragraph, type ParagraphOptions } from './components/paragraph.js';
|
|
52
52
|
import { barchart, BarChart, type BarChartOptions, type BarChartDataPoint } from './components/barchart.js';
|
|
53
53
|
import { areachart, AreaChart, type AreaChartOptions, type AreaChartDataPoint } from './components/areachart.js';
|
|
54
|
-
import { areachartsmooth, AreaChartSmooth, type AreaChartSmoothOptions, AreaChartSmoothDataPoint
|
|
54
|
+
import { areachartsmooth, AreaChartSmooth, type AreaChartSmoothOptions, AreaChartSmoothDataPoint } from './components/areachartsmooth.js';
|
|
55
55
|
import { doughnutchart, DoughnutChart, type DoughnutChartOptions, type DoughnutChartDataPoint } from './components/doughnutchart.js';
|
|
56
56
|
import { kpicard, KPICard, type KPICardOptions } from './components/kpicard.js';
|
|
57
|
+
import { divider, Divider, type DividerOptions } from './components/divider.js';
|
|
57
58
|
|
|
58
59
|
/* -------------------------
|
|
59
60
|
* Type Exports
|
|
@@ -116,7 +117,8 @@ export type {
|
|
|
116
117
|
AreaChartSmoothDataPoint,
|
|
117
118
|
DoughnutChartOptions,
|
|
118
119
|
DoughnutChartDataPoint,
|
|
119
|
-
KPICardOptions
|
|
120
|
+
KPICardOptions,
|
|
121
|
+
DividerOptions
|
|
120
122
|
};
|
|
121
123
|
|
|
122
124
|
/* -------------------------
|
|
@@ -172,7 +174,8 @@ export {
|
|
|
172
174
|
AreaChart,
|
|
173
175
|
AreaChartSmooth,
|
|
174
176
|
DoughnutChart,
|
|
175
|
-
KPICard
|
|
177
|
+
KPICard,
|
|
178
|
+
Divider
|
|
176
179
|
};
|
|
177
180
|
|
|
178
181
|
/* -------------------------
|
|
@@ -234,6 +237,7 @@ export interface JuxAPI {
|
|
|
234
237
|
areachartsmooth: typeof areachartsmooth;
|
|
235
238
|
doughnutchart: typeof doughnutchart;
|
|
236
239
|
kpicard: typeof kpicard;
|
|
240
|
+
divider: typeof divider;
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
/* -------------------------
|
|
@@ -311,8 +315,8 @@ class Jux implements JuxAPI {
|
|
|
311
315
|
areachartsmooth = areachartsmooth;
|
|
312
316
|
doughnutchart = doughnutchart;
|
|
313
317
|
kpicard = kpicard;
|
|
318
|
+
divider = divider;
|
|
314
319
|
}
|
|
315
|
-
|
|
316
320
|
/**
|
|
317
321
|
* Global jux singleton instance
|
|
318
322
|
*/
|
package/lib/presets/global.css
CHANGED
package/machinery/compiler.js
CHANGED
|
@@ -2,6 +2,23 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import esbuild from 'esbuild';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Generate import map script tag
|
|
7
|
+
*/
|
|
8
|
+
function generateImportMapScript() {
|
|
9
|
+
return `<script type="importmap">
|
|
10
|
+
{
|
|
11
|
+
"imports": {
|
|
12
|
+
"juxscript": "./lib/jux.js",
|
|
13
|
+
"juxscript/": "./lib/",
|
|
14
|
+
"juxscript/reactivity": "./lib/reactivity/state.js",
|
|
15
|
+
"juxscript/presets/": "./lib/presets/",
|
|
16
|
+
"juxscript/components/": "./lib/components/"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
</script>`;
|
|
20
|
+
}
|
|
21
|
+
|
|
5
22
|
/**
|
|
6
23
|
* Compile a .jux file to .js and .html
|
|
7
24
|
*
|
|
@@ -36,7 +53,6 @@ export async function compileJuxFile(juxFilePath, options = {}) {
|
|
|
36
53
|
// Calculate depth for relative paths
|
|
37
54
|
const depth = parsedPath.dir.split(path.sep).filter(p => p).length;
|
|
38
55
|
const libPath = depth === 0 ? './lib/jux.js' : '../'.repeat(depth) + 'lib/jux.js';
|
|
39
|
-
const styleBasePath = depth === 0 ? './lib/presets/' : '../'.repeat(depth) + 'lib/presets/';
|
|
40
56
|
|
|
41
57
|
// Transform imports
|
|
42
58
|
let transformedContent = juxContent;
|
|
@@ -68,8 +84,9 @@ export async function compileJuxFile(juxFilePath, options = {}) {
|
|
|
68
84
|
|
|
69
85
|
console.log(` ✓ JS: ${path.relative(projectRoot, jsOutputPath)}`);
|
|
70
86
|
|
|
71
|
-
// Generate HTML with correct script path
|
|
87
|
+
// Generate HTML with import map and correct script path
|
|
72
88
|
const scriptPath = `./${parsedPath.name}.js`;
|
|
89
|
+
const importMapScript = generateImportMapScript();
|
|
73
90
|
|
|
74
91
|
const html = `<!DOCTYPE html>
|
|
75
92
|
<html lang="en">
|
|
@@ -77,12 +94,12 @@ export async function compileJuxFile(juxFilePath, options = {}) {
|
|
|
77
94
|
<meta charset="UTF-8">
|
|
78
95
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
79
96
|
<title>${parsedPath.name}</title>
|
|
80
|
-
<!-- JUX Core Styles -->
|
|
81
|
-
<link rel="stylesheet" href="${styleBasePath}global.css">
|
|
82
97
|
</head>
|
|
83
98
|
<body data-theme="">
|
|
84
99
|
<!-- App container -->
|
|
85
100
|
<div id="app" data-jux-page="${parsedPath.name}"></div>
|
|
101
|
+
|
|
102
|
+
${importMapScript}
|
|
86
103
|
<script type="module" src="${scriptPath}"></script>
|
|
87
104
|
${isServe ? `
|
|
88
105
|
<!-- Hot reload -->
|
package/machinery/server.js
CHANGED
|
@@ -12,7 +12,7 @@ const __dirname = path.dirname(__filename);
|
|
|
12
12
|
|
|
13
13
|
let db = null;
|
|
14
14
|
|
|
15
|
-
async function serve(port = 3000, distDir = './jux-dist') {
|
|
15
|
+
async function serve(port = 3000, distDir = './jux-dist') {
|
|
16
16
|
const app = express();
|
|
17
17
|
const absoluteDistDir = path.resolve(distDir);
|
|
18
18
|
const projectRoot = path.resolve('.');
|
|
@@ -29,14 +29,14 @@ async function serve(port = 3000, distDir = './jux-dist') { // Changed default
|
|
|
29
29
|
app.post('/api/query', async (req, res) => {
|
|
30
30
|
try {
|
|
31
31
|
const { sql, params = [] } = req.body;
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
if (!db) {
|
|
34
34
|
return res.status(500).json({ error: 'Database not initialized' });
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const stmt = db.prepare(sql);
|
|
38
38
|
stmt.bind(params);
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
const rows = [];
|
|
41
41
|
while (stmt.step()) {
|
|
42
42
|
rows.push(stmt.getAsObject());
|
|
@@ -61,16 +61,20 @@ async function serve(port = 3000, distDir = './jux-dist') { // Changed default
|
|
|
61
61
|
// Serve HTML files with clean URLs
|
|
62
62
|
const heyPath = path.join(absoluteDistDir, 'hey.html');
|
|
63
63
|
const indexPath = path.join(absoluteDistDir, 'index.html');
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
app.use((req, res, next) => {
|
|
66
|
-
let requestPath = req.path.endsWith('/') && req.path.length > 1
|
|
67
|
-
? req.path.slice(0, -1)
|
|
66
|
+
let requestPath = req.path.endsWith('/') && req.path.length > 1
|
|
67
|
+
? req.path.slice(0, -1)
|
|
68
68
|
: req.path;
|
|
69
69
|
|
|
70
70
|
// Root path - serve hey.html or index.html
|
|
71
71
|
if (requestPath === '/') {
|
|
72
|
-
if (fs.existsSync(heyPath))
|
|
73
|
-
|
|
72
|
+
if (fs.existsSync(heyPath)) {
|
|
73
|
+
return res.sendFile(heyPath);
|
|
74
|
+
}
|
|
75
|
+
if (fs.existsSync(indexPath)) {
|
|
76
|
+
return res.sendFile(indexPath);
|
|
77
|
+
}
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
// Try to serve as HTML file
|
|
@@ -87,7 +91,7 @@ async function serve(port = 3000, distDir = './jux-dist') { // Changed default
|
|
|
87
91
|
|
|
88
92
|
next();
|
|
89
93
|
});
|
|
90
|
-
|
|
94
|
+
|
|
91
95
|
// Serve static files (CSS, JS, images, etc.)
|
|
92
96
|
app.use(express.static(absoluteDistDir));
|
|
93
97
|
|
|
@@ -96,25 +100,24 @@ async function serve(port = 3000, distDir = './jux-dist') { // Changed default
|
|
|
96
100
|
const notFoundPath = path.join(absoluteDistDir, '404.html');
|
|
97
101
|
const requestedPath = path.join(absoluteDistDir, req.path);
|
|
98
102
|
const fileType = path.extname(req.path) || 'directory';
|
|
99
|
-
|
|
103
|
+
|
|
100
104
|
// Log to console for debugging
|
|
101
105
|
console.log(`❌ 404: ${req.path}`);
|
|
102
106
|
console.log(` Looked for: ${requestedPath}`);
|
|
103
107
|
console.log(` Type: ${fileType}`);
|
|
104
108
|
console.log(` Referer: ${req.get('referer') || 'direct'}`);
|
|
105
|
-
|
|
109
|
+
|
|
106
110
|
// If custom 404.html exists and this isn't already /404
|
|
107
111
|
if (fs.existsSync(notFoundPath) && req.path !== '/404') {
|
|
108
|
-
// Add debug info as query params
|
|
109
112
|
const debugUrl = `/404?path=${encodeURIComponent(req.path)}&type=${fileType}&from=${encodeURIComponent(req.get('referer') || 'direct')}`;
|
|
110
113
|
return res.redirect(debugUrl);
|
|
111
114
|
}
|
|
112
|
-
|
|
115
|
+
|
|
113
116
|
// Serve custom 404 page
|
|
114
117
|
if (fs.existsSync(notFoundPath)) {
|
|
115
118
|
return res.status(404).sendFile(notFoundPath);
|
|
116
119
|
}
|
|
117
|
-
|
|
120
|
+
|
|
118
121
|
// Fallback: minimal 404 response
|
|
119
122
|
res.status(404).send('<h1>404 - Not Found</h1>');
|
|
120
123
|
});
|
|
@@ -175,7 +178,7 @@ async function serve(port = 3000, distDir = './jux-dist') { // Changed default
|
|
|
175
178
|
async function initDatabase() {
|
|
176
179
|
const SQL = await initSqlJs();
|
|
177
180
|
const dbPath = path.join(__dirname, '../db/jux.db');
|
|
178
|
-
|
|
181
|
+
|
|
179
182
|
if (fs.existsSync(dbPath)) {
|
|
180
183
|
const buffer = fs.readFileSync(dbPath);
|
|
181
184
|
db = new SQL.Database(buffer);
|
|
@@ -188,5 +191,5 @@ async function initDatabase() {
|
|
|
188
191
|
|
|
189
192
|
export async function start(port = 3000) {
|
|
190
193
|
await initDatabase();
|
|
191
|
-
return serve(port, './jux-dist');
|
|
194
|
+
return serve(port, './jux-dist');
|
|
192
195
|
}
|
package/package.json
CHANGED
|
@@ -1,84 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "juxscript",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A JavaScript UX authorship platform",
|
|
6
6
|
"main": "lib/jux.js",
|
|
7
7
|
"types": "lib/jux.d.ts",
|
|
8
|
+
"access": "public",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/jux/juxscript.git"
|
|
12
|
+
},
|
|
8
13
|
"exports": {
|
|
9
14
|
".": {
|
|
10
15
|
"types": "./lib/jux.d.ts",
|
|
11
16
|
"import": "./lib/jux.js",
|
|
12
17
|
"default": "./lib/jux.js"
|
|
13
18
|
},
|
|
14
|
-
"./
|
|
15
|
-
|
|
19
|
+
"./reactivity": {
|
|
20
|
+
"types": "./lib/reactivity/index.d.ts",
|
|
21
|
+
"import": "./lib/reactivity/index.js",
|
|
22
|
+
"default": "./lib/reactivity/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./components/*": "./lib/components/*/index.js",
|
|
25
|
+
"./presets/*": "./lib/presets/*.js",
|
|
16
26
|
"./package.json": "./package.json"
|
|
17
27
|
},
|
|
18
|
-
"typesVersions": {
|
|
19
|
-
"*": {
|
|
20
|
-
"*": [
|
|
21
|
-
"lib/*"
|
|
22
|
-
],
|
|
23
|
-
"lib/*": [
|
|
24
|
-
"lib/*"
|
|
25
|
-
],
|
|
26
|
-
"lib/components/*": [
|
|
27
|
-
"lib/components/*"
|
|
28
|
-
]
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
"bin": {
|
|
32
|
-
"jux": "./bin/cli.js"
|
|
33
|
-
},
|
|
34
|
-
"scripts": {
|
|
35
|
-
"dev": "cd examples && npx jux serve",
|
|
36
|
-
"build:examples": "cd examples && rm -rf dist && npx jux build",
|
|
37
|
-
"build": "tsc",
|
|
38
|
-
"test": "node test/run-tests.js",
|
|
39
|
-
"generate:icons": "node scripts/generate-icon-types.js"
|
|
40
|
-
},
|
|
41
28
|
"files": [
|
|
42
29
|
"lib",
|
|
43
30
|
"bin",
|
|
44
31
|
"machinery",
|
|
45
32
|
"types",
|
|
46
|
-
"lib/**/*.d.ts",
|
|
47
33
|
"README.md",
|
|
48
34
|
"LICENSE"
|
|
49
35
|
],
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
},
|
|
53
|
-
"repository": {
|
|
54
|
-
"type": "git",
|
|
55
|
-
"url": "https://github.com/juxscript/jux.git"
|
|
36
|
+
"bin": {
|
|
37
|
+
"jux": "./bin/cli.js"
|
|
56
38
|
},
|
|
57
39
|
"keywords": [
|
|
58
|
-
"jux",
|
|
59
40
|
"ui",
|
|
60
|
-
"
|
|
41
|
+
"ux",
|
|
42
|
+
"components",
|
|
43
|
+
"framework",
|
|
44
|
+
"reactive",
|
|
61
45
|
"javascript"
|
|
62
46
|
],
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
"chokidar": "^5.0.0",
|
|
68
|
-
"clean-css": "^5.3.3",
|
|
69
|
-
"esbuild": "^0.27.2",
|
|
70
|
-
"express": "^5.2.1",
|
|
71
|
-
"glob": "^13.0.0",
|
|
72
|
-
"node": "^24.12.0",
|
|
73
|
-
"sql.js": "^1.10.3",
|
|
74
|
-
"terser": "^5.44.1",
|
|
75
|
-
"ws": "^8.19.0"
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc",
|
|
49
|
+
"dev": "tsc --watch",
|
|
50
|
+
"prepublishOnly": "npm run build"
|
|
76
51
|
},
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"express": "^4.18.2",
|
|
54
|
+
"chokidar": "^3.5.3",
|
|
55
|
+
"ws": "^8.13.0",
|
|
56
|
+
"sql.js": "^1.8.0"
|
|
80
57
|
},
|
|
81
58
|
"devDependencies": {
|
|
82
|
-
"typescript": "^5.
|
|
59
|
+
"typescript": "^5.0.0",
|
|
60
|
+
"@types/express": "^4.17.17",
|
|
61
|
+
"@types/node": "^20.0.0",
|
|
62
|
+
"@types/ws": "^8.5.5"
|
|
83
63
|
}
|
|
84
64
|
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import CleanCSS from 'clean-css';
|
|
5
|
-
import { FileValidator } from '../validators/file-validator.js';
|
|
6
|
-
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
-
const __dirname = path.dirname(__filename);
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Generates CSS content from Jux configuration
|
|
12
|
-
* Handles: global.css, themes, imports, styleImports, and styleInline blocks
|
|
13
|
-
*/
|
|
14
|
-
export function generateCSS(juxConfig, layoutParsed = null) {
|
|
15
|
-
const fileValidator = new FileValidator();
|
|
16
|
-
let cssOutput = '';
|
|
17
|
-
|
|
18
|
-
// 1. Add global.css (always first)
|
|
19
|
-
const globalCssPath = path.join(__dirname, '../../lib/global.css');
|
|
20
|
-
if (fs.existsSync(globalCssPath)) {
|
|
21
|
-
cssOutput += `/* Global Styles */\n`;
|
|
22
|
-
cssOutput += fs.readFileSync(globalCssPath, 'utf-8') + '\n\n';
|
|
23
|
-
console.log(` ✓ Included global.css`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// 2. Add theme CSS (layout theme first, then page theme if different)
|
|
27
|
-
const themesToLoad = [];
|
|
28
|
-
|
|
29
|
-
if (layoutParsed?.config?.theme) {
|
|
30
|
-
themesToLoad.push({ theme: layoutParsed.config.theme, source: 'layout' });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (juxConfig.theme && juxConfig.theme !== layoutParsed?.config?.theme) {
|
|
34
|
-
themesToLoad.push({ theme: juxConfig.theme, source: 'page' });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
for (const { theme, source } of themesToLoad) {
|
|
38
|
-
const themePath = path.join(__dirname, '../../lib/themes', `${theme}.css`);
|
|
39
|
-
if (fs.existsSync(themePath)) {
|
|
40
|
-
cssOutput += `/* Theme: ${theme} (${source}) */\n`;
|
|
41
|
-
cssOutput += fs.readFileSync(themePath, 'utf-8') + '\n\n';
|
|
42
|
-
console.log(` ✓ Included theme: ${theme} (${source})`);
|
|
43
|
-
} else {
|
|
44
|
-
console.warn(` ⚠️ Theme not found: ${theme}`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// 3. Process @import directives (CSS files only from layout, then page)
|
|
49
|
-
const allImports = [
|
|
50
|
-
...(layoutParsed?.config?.import || []),
|
|
51
|
-
...(juxConfig.import || [])
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
if (allImports.length > 0) {
|
|
55
|
-
const { categorized } = fileValidator.categorizeImports(allImports);
|
|
56
|
-
|
|
57
|
-
// Only process CSS files
|
|
58
|
-
for (const cssImport of categorized.css) {
|
|
59
|
-
const resolvedPath = path.join(__dirname, '../../', cssImport);
|
|
60
|
-
if (fs.existsSync(resolvedPath)) {
|
|
61
|
-
cssOutput += `/* Import: ${cssImport} */\n`;
|
|
62
|
-
cssOutput += fs.readFileSync(resolvedPath, 'utf-8') + '\n\n';
|
|
63
|
-
console.log(` ✓ Included import: ${cssImport}`);
|
|
64
|
-
} else {
|
|
65
|
-
console.warn(` ⚠️ Import not found: ${cssImport} (resolved to ${resolvedPath})`);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Log skipped JS imports (will be handled in HTML)
|
|
70
|
-
if (categorized.js.length > 0) {
|
|
71
|
-
console.log(` ℹ️ Skipped JS imports (will be added to HTML): ${categorized.js.length} file(s)`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// 4. Process @style imports (CSS file references from layout, then page)
|
|
76
|
-
const allStyleImports = [
|
|
77
|
-
...(layoutParsed?.config?.styleImports || []),
|
|
78
|
-
...(juxConfig.styleImports || [])
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
for (const styleImport of allStyleImports) {
|
|
82
|
-
// Handle URLs (CDN)
|
|
83
|
-
if (styleImport.startsWith('http://') || styleImport.startsWith('https://')) {
|
|
84
|
-
cssOutput += `/* External CSS: ${styleImport} */\n`;
|
|
85
|
-
cssOutput += `@import url('${styleImport}');\n\n`;
|
|
86
|
-
console.log(` ✓ Added CDN import: ${styleImport}`);
|
|
87
|
-
} else {
|
|
88
|
-
// Handle local files
|
|
89
|
-
const resolvedPath = path.join(__dirname, '../../', styleImport);
|
|
90
|
-
if (fs.existsSync(resolvedPath)) {
|
|
91
|
-
cssOutput += `/* Style Import: ${styleImport} */\n`;
|
|
92
|
-
cssOutput += fs.readFileSync(resolvedPath, 'utf-8') + '\n\n';
|
|
93
|
-
console.log(` ✓ Included style import: ${styleImport}`);
|
|
94
|
-
} else {
|
|
95
|
-
console.warn(` ⚠️ Style import not found: ${styleImport} (resolved to ${resolvedPath})`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// 5. Process inline @style blocks (layout first, then page)
|
|
101
|
-
const allInlineStyles = [
|
|
102
|
-
...(layoutParsed?.config?.styleInline || []),
|
|
103
|
-
...(juxConfig.styleInline || [])
|
|
104
|
-
];
|
|
105
|
-
|
|
106
|
-
if (allInlineStyles.length > 0) {
|
|
107
|
-
cssOutput += `/* Inline Styles (${allInlineStyles.length} block(s)) */\n`;
|
|
108
|
-
|
|
109
|
-
allInlineStyles.forEach((styleBlock, index) => {
|
|
110
|
-
const source = index < (layoutParsed?.config?.styleInline?.length || 0) ? 'layout' : 'page';
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const validatedStyle = fileValidator.validateStyleContent(styleBlock, `inline block #${index + 1}`);
|
|
114
|
-
|
|
115
|
-
if (!fileValidator.isEmptyStyle(validatedStyle)) {
|
|
116
|
-
cssOutput += `/* Inline Block #${index + 1} (${source}) */\n`;
|
|
117
|
-
cssOutput += validatedStyle + '\n\n';
|
|
118
|
-
}
|
|
119
|
-
} catch (error) {
|
|
120
|
-
console.error(` ❌ ${error.message}`);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
console.log(` ✓ Included ${allInlineStyles.length} inline style block(s)`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return cssOutput.trim();
|
|
128
|
-
}
|