juxscript 1.0.24 → 1.0.26
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/bin/cli.js +22 -15
- package/lib/components/docs-data.json +1 -1
- package/machinery/compiler.js +17 -6
- package/machinery/server.js +59 -25
- package/package.json +1 -1
- package/presets/grid.jux +55 -0
- package/presets/index.jux +90 -0
- package/presets/styles/layout.css +280 -0
- package/presets/hey.jux +0 -60
package/bin/cli.js
CHANGED
|
@@ -247,18 +247,17 @@ async function buildProject(isServe = false) {
|
|
|
247
247
|
console.log('+ Created jux/index.jux');
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
// Copy preset styles to jux/styles/
|
|
251
|
-
const
|
|
250
|
+
// Copy preset styles from presets/styles/ to jux/styles/
|
|
251
|
+
const presetsStylesSrc = path.join(PATHS.packageRoot, 'presets', 'styles');
|
|
252
252
|
const stylesDest = path.join(juxDir, 'styles');
|
|
253
253
|
|
|
254
|
-
if (fs.existsSync(
|
|
254
|
+
if (fs.existsSync(presetsStylesSrc)) {
|
|
255
255
|
fs.mkdirSync(stylesDest, { recursive: true });
|
|
256
256
|
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
// Only copy CSS files, skip hey.jux
|
|
257
|
+
const styleFiles = fs.readdirSync(presetsStylesSrc);
|
|
258
|
+
styleFiles.forEach(file => {
|
|
260
259
|
if (file.endsWith('.css')) {
|
|
261
|
-
const srcFile = path.join(
|
|
260
|
+
const srcFile = path.join(presetsStylesSrc, file);
|
|
262
261
|
const destFile = path.join(stylesDest, file);
|
|
263
262
|
fs.copyFileSync(srcFile, destFile);
|
|
264
263
|
console.log(`+ Copied preset style: styles/${file}`);
|
|
@@ -312,17 +311,24 @@ node_modules/
|
|
|
312
311
|
// ✅ Always serves router bundle
|
|
313
312
|
await buildProject(true);
|
|
314
313
|
|
|
315
|
-
|
|
316
|
-
|
|
314
|
+
// Parse port arguments: npx jux serve [httpPort] [wsPort]
|
|
315
|
+
const httpPort = parseInt(process.argv[3]) || 3000;
|
|
316
|
+
const wsPort = parseInt(process.argv[4]) || 3001;
|
|
317
|
+
|
|
318
|
+
await start(httpPort, wsPort);
|
|
317
319
|
|
|
318
320
|
} else {
|
|
319
321
|
console.log(`
|
|
320
322
|
JUX CLI - A JavaScript UX authorship platform
|
|
321
323
|
|
|
322
324
|
Usage:
|
|
323
|
-
npx jux init
|
|
324
|
-
npx jux build
|
|
325
|
-
npx jux serve [
|
|
325
|
+
npx jux init Initialize a new JUX project
|
|
326
|
+
npx jux build Build router bundle to ./jux-dist/
|
|
327
|
+
npx jux serve [http] [ws] Start dev server with hot reload
|
|
328
|
+
|
|
329
|
+
Arguments:
|
|
330
|
+
[http] HTTP server port (default: 3000)
|
|
331
|
+
[ws] WebSocket port (default: 3001)
|
|
326
332
|
|
|
327
333
|
Project Structure:
|
|
328
334
|
my-project/
|
|
@@ -345,9 +351,10 @@ Getting Started:
|
|
|
345
351
|
4. Serve jux-dist/ from your backend
|
|
346
352
|
|
|
347
353
|
Examples:
|
|
348
|
-
npx jux build Build production bundle
|
|
349
|
-
npx jux serve
|
|
350
|
-
npx jux serve 8080
|
|
354
|
+
npx jux build # Build production bundle
|
|
355
|
+
npx jux serve # Dev server (ports 3000/3001)
|
|
356
|
+
npx jux serve 8080 # HTTP on 8080, WS on 3001
|
|
357
|
+
npx jux serve 8080 8081 # HTTP on 8080, WS on 8081
|
|
351
358
|
`);
|
|
352
359
|
}
|
|
353
360
|
})();
|
package/machinery/compiler.js
CHANGED
|
@@ -580,8 +580,23 @@ ${routeTable}
|
|
|
580
580
|
const app = document.getElementById('app');
|
|
581
581
|
|
|
582
582
|
function render() {
|
|
583
|
-
|
|
584
|
-
|
|
583
|
+
let path = location.pathname;
|
|
584
|
+
|
|
585
|
+
// Try exact match first
|
|
586
|
+
let view = routes[path];
|
|
587
|
+
|
|
588
|
+
// If no match and path ends with /, try appending 'index'
|
|
589
|
+
if (!view && path.endsWith('/')) {
|
|
590
|
+
view = routes[path + 'index'] || routes[path.slice(0, -1) + '/index'];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// If still no match and path doesn't end with /, try appending '/index'
|
|
594
|
+
if (!view && !path.endsWith('/')) {
|
|
595
|
+
view = routes[path + '/index'];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Fall back to 404
|
|
599
|
+
view = view || JuxNotFound;
|
|
585
600
|
|
|
586
601
|
app.innerHTML = '';
|
|
587
602
|
app.removeAttribute('data-jux-page');
|
|
@@ -638,10 +653,6 @@ export function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js') {
|
|
|
638
653
|
<title>Jux Application</title>
|
|
639
654
|
</head>
|
|
640
655
|
<body data-theme="">
|
|
641
|
-
<!-- Navigation -->
|
|
642
|
-
<nav style="padding: 20px; background: #f5f5f5; border-bottom: 2px solid #ddd;">
|
|
643
|
-
${navLinks}
|
|
644
|
-
</nav>
|
|
645
656
|
<!-- App container - router renders here -->
|
|
646
657
|
<div id="app"></div>
|
|
647
658
|
${importMapScript}
|
package/machinery/server.js
CHANGED
|
@@ -9,7 +9,45 @@ import { WebSocketServer } from 'ws';
|
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
10
|
const __dirname = path.dirname(__filename);
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Try to start a server on a port, with fallback to next port if busy
|
|
14
|
+
* Returns the actual port that was successfully allocated
|
|
15
|
+
*/
|
|
16
|
+
async function tryPort(startPort, maxAttempts = 5, reservedPorts = []) {
|
|
17
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
18
|
+
const port = startPort + attempt;
|
|
19
|
+
|
|
20
|
+
// Skip if this port is already reserved
|
|
21
|
+
if (reservedPorts.includes(port)) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Test if port is available
|
|
27
|
+
await new Promise((resolve, reject) => {
|
|
28
|
+
const testServer = http.createServer();
|
|
29
|
+
testServer.once('error', reject);
|
|
30
|
+
testServer.once('listening', () => {
|
|
31
|
+
testServer.close();
|
|
32
|
+
resolve(port);
|
|
33
|
+
});
|
|
34
|
+
testServer.listen(port);
|
|
35
|
+
});
|
|
36
|
+
return port;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (err.code === 'EADDRINUSE') {
|
|
39
|
+
if (attempt < maxAttempts - 1) {
|
|
40
|
+
console.log(` Port ${port} in use, trying ${port + 1}...`);
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Could not find available port after ${maxAttempts} attempts starting from ${startPort}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function serve(httpPort = 3000, wsPort = 3001, distDir = './jux-dist') {
|
|
13
51
|
const app = express();
|
|
14
52
|
const absoluteDistDir = path.resolve(distDir);
|
|
15
53
|
const projectRoot = path.resolve('.');
|
|
@@ -22,50 +60,46 @@ async function serve(port = 3000, distDir = './jux-dist') {
|
|
|
22
60
|
process.exit(1);
|
|
23
61
|
}
|
|
24
62
|
|
|
25
|
-
//
|
|
63
|
+
// Strong cache prevention
|
|
26
64
|
app.use((req, res, next) => {
|
|
27
65
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
28
66
|
res.setHeader('Pragma', 'no-cache');
|
|
29
67
|
res.setHeader('Expires', '0');
|
|
30
|
-
|
|
31
|
-
// ✅ Add ETag prevention
|
|
32
68
|
res.setHeader('ETag', 'W/"' + Date.now() + '"');
|
|
33
|
-
|
|
34
69
|
next();
|
|
35
70
|
});
|
|
36
71
|
|
|
37
|
-
//
|
|
38
|
-
app.use((req, res, next) => {
|
|
39
|
-
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
40
|
-
res.setHeader('Pragma', 'no-cache');
|
|
41
|
-
res.setHeader('Expires', '0');
|
|
42
|
-
next();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// Serve static files (CSS, JS, images, etc.)
|
|
72
|
+
// Serve static files
|
|
46
73
|
app.use(express.static(absoluteDistDir));
|
|
47
74
|
|
|
48
|
-
// SPA fallback
|
|
75
|
+
// SPA fallback
|
|
49
76
|
app.get('*', (req, res) => {
|
|
50
|
-
// If request accepts HTML, serve index.html
|
|
51
77
|
if (req.accepts('html')) {
|
|
52
78
|
const indexPath = path.join(absoluteDistDir, 'index.html');
|
|
53
79
|
if (fs.existsSync(indexPath)) {
|
|
54
80
|
res.sendFile(indexPath);
|
|
55
81
|
} else {
|
|
56
|
-
res.status(404).send('index.html not found. Run `npx jux
|
|
82
|
+
res.status(404).send('index.html not found. Run `npx jux build` first.');
|
|
57
83
|
}
|
|
58
84
|
} else {
|
|
59
|
-
// For non-HTML requests (like .js, .css), return 404
|
|
60
85
|
res.status(404).send('Not found');
|
|
61
86
|
}
|
|
62
87
|
});
|
|
63
88
|
|
|
89
|
+
// Find available ports sequentially to avoid collision
|
|
90
|
+
console.log('🔍 Finding available ports...');
|
|
91
|
+
|
|
92
|
+
// First, find HTTP port
|
|
93
|
+
const availableHttpPort = await tryPort(httpPort);
|
|
94
|
+
|
|
95
|
+
// Then find WS port, excluding the HTTP port we just allocated
|
|
96
|
+
const availableWsPort = await tryPort(wsPort, 5, [availableHttpPort]);
|
|
97
|
+
|
|
64
98
|
// Create HTTP server
|
|
65
99
|
const server = http.createServer(app);
|
|
66
100
|
|
|
67
101
|
// WebSocket server for hot reload
|
|
68
|
-
const wss = new WebSocketServer({ port:
|
|
102
|
+
const wss = new WebSocketServer({ port: availableWsPort });
|
|
69
103
|
const clients = [];
|
|
70
104
|
|
|
71
105
|
wss.on('connection', (ws) => {
|
|
@@ -83,11 +117,11 @@ async function serve(port = 3000, distDir = './jux-dist') {
|
|
|
83
117
|
});
|
|
84
118
|
});
|
|
85
119
|
|
|
86
|
-
console.log(
|
|
120
|
+
console.log(`🔌 WebSocket server running at ws://localhost:${availableWsPort}`);
|
|
87
121
|
|
|
88
122
|
// Start HTTP server
|
|
89
|
-
server.listen(
|
|
90
|
-
console.log(`🚀 JUX dev server running at http://localhost:${
|
|
123
|
+
server.listen(availableHttpPort, () => {
|
|
124
|
+
console.log(`🚀 JUX dev server running at http://localhost:${availableHttpPort}`);
|
|
91
125
|
console.log(` Serving: ${absoluteDistDir}`);
|
|
92
126
|
console.log(` Press Ctrl+C to stop\n`);
|
|
93
127
|
});
|
|
@@ -110,9 +144,9 @@ async function serve(port = 3000, distDir = './jux-dist') {
|
|
|
110
144
|
process.on('SIGINT', shutdown);
|
|
111
145
|
process.on('SIGTERM', shutdown);
|
|
112
146
|
|
|
113
|
-
return { server };
|
|
147
|
+
return { server, httpPort: availableHttpPort, wsPort: availableWsPort };
|
|
114
148
|
}
|
|
115
149
|
|
|
116
|
-
export async function start(
|
|
117
|
-
return serve(
|
|
150
|
+
export async function start(httpPort = 3000, wsPort = 3001) {
|
|
151
|
+
return serve(httpPort, wsPort, './jux-dist');
|
|
118
152
|
}
|
package/package.json
CHANGED
package/presets/grid.jux
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jux } from 'juxscript';
|
|
2
|
+
|
|
3
|
+
// Import the layout styles
|
|
4
|
+
jux.include('styles/layout.css');
|
|
5
|
+
|
|
6
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
7
|
+
// GRID LAYOUT - INITIALIZATION FUNCTION
|
|
8
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
9
|
+
// Note: #app is automatically created by the Jux compiler
|
|
10
|
+
|
|
11
|
+
export function initializeGrid() {
|
|
12
|
+
// Header area
|
|
13
|
+
const appHeader = jux.container('appheader').render('#app');
|
|
14
|
+
const appHeaderContent = jux.container('appheader-content').render('#appheader');
|
|
15
|
+
const appHeaderLogo = jux.container('appheader-logo').render('#appheader-content');
|
|
16
|
+
const appHeaderNav = jux.container('appheader-nav').render('#appheader-content');
|
|
17
|
+
const appHeaderActions = jux.container('appheader-actions').render('#appheader-content');
|
|
18
|
+
|
|
19
|
+
// Left sidebar
|
|
20
|
+
const appAside = jux.container('appaside').render('#app');
|
|
21
|
+
|
|
22
|
+
// Main content area
|
|
23
|
+
const appMain = jux.container('appmain').render('#app');
|
|
24
|
+
const appMainContent = jux.container('appmain-content').render('#appmain');
|
|
25
|
+
|
|
26
|
+
// Right sidebar (optional - starts hidden)
|
|
27
|
+
const appSidebar = jux.container('appsidebar').render('#app');
|
|
28
|
+
const appSidebarHeader = jux.container('appsidebar-header').render('#appsidebar');
|
|
29
|
+
const appSidebarContent = jux.container('appsidebar-content').render('#appsidebar');
|
|
30
|
+
const appSidebarFooter = jux.container('appsidebar-footer').render('#appsidebar');
|
|
31
|
+
|
|
32
|
+
// Footer area
|
|
33
|
+
const appFooter = jux.container('appfooter').render('#app');
|
|
34
|
+
const appFooterContent = jux.container('appfooter-content').render('#appfooter');
|
|
35
|
+
const appFooterLegal = jux.container('appfooter-legal').render('#appfooter-content');
|
|
36
|
+
|
|
37
|
+
// Return references to all containers
|
|
38
|
+
return {
|
|
39
|
+
appHeader,
|
|
40
|
+
appHeaderContent,
|
|
41
|
+
appHeaderLogo,
|
|
42
|
+
appHeaderNav,
|
|
43
|
+
appHeaderActions,
|
|
44
|
+
appAside,
|
|
45
|
+
appMain,
|
|
46
|
+
appMainContent,
|
|
47
|
+
appSidebar,
|
|
48
|
+
appSidebarHeader,
|
|
49
|
+
appSidebarContent,
|
|
50
|
+
appSidebarFooter,
|
|
51
|
+
appFooter,
|
|
52
|
+
appFooterContent,
|
|
53
|
+
appFooterLegal
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jux, state } from 'juxscript';
|
|
2
|
+
import { initializeGrid } from './grid.jux';
|
|
3
|
+
|
|
4
|
+
// Initialize the grid layout - this executes the rendering
|
|
5
|
+
const grid = initializeGrid();
|
|
6
|
+
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
8
|
+
// HEADER CONTENT
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
10
|
+
|
|
11
|
+
jux.heading('logo-text')
|
|
12
|
+
.level(1)
|
|
13
|
+
.text('JUX')
|
|
14
|
+
.render('#appheader-logo');
|
|
15
|
+
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
17
|
+
// MAIN CONTENT
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
jux.hero('welcome', {
|
|
21
|
+
title: 'Welcome to JUX',
|
|
22
|
+
subtitle: 'A JavaScript UX authorship platform'
|
|
23
|
+
}).render('#appmain-content');
|
|
24
|
+
|
|
25
|
+
jux.divider().render('#appmain-content');
|
|
26
|
+
|
|
27
|
+
jux.heading('start-heading')
|
|
28
|
+
.level(2)
|
|
29
|
+
.text('Getting Started')
|
|
30
|
+
.render('#appmain-content');
|
|
31
|
+
|
|
32
|
+
jux.paragraph('start-intro')
|
|
33
|
+
.text('Edit this file to build your app. Here are some quick tips:')
|
|
34
|
+
.style('margin: 1rem 0;')
|
|
35
|
+
.render('#appmain-content');
|
|
36
|
+
|
|
37
|
+
jux.list('quick-tips', {
|
|
38
|
+
items: [
|
|
39
|
+
'Run npx jux serve for dev mode with hot reload',
|
|
40
|
+
'Run npx jux build to compile for production',
|
|
41
|
+
'Serve jux-dist/ from your backend',
|
|
42
|
+
'Check out the docs at juxscript.com/docs'
|
|
43
|
+
]
|
|
44
|
+
}).render('#appmain-content');
|
|
45
|
+
|
|
46
|
+
jux.divider().render('#appmain-content');
|
|
47
|
+
|
|
48
|
+
jux.heading('example-heading')
|
|
49
|
+
.level(3)
|
|
50
|
+
.text('Quick Example')
|
|
51
|
+
.render('#appmain-content');
|
|
52
|
+
|
|
53
|
+
jux.code('example-code')
|
|
54
|
+
.language('javascript')
|
|
55
|
+
.code(`import { jux, state } from 'juxscript';
|
|
56
|
+
|
|
57
|
+
const count = state(0);
|
|
58
|
+
|
|
59
|
+
jux.button('increment')
|
|
60
|
+
.label('Click me!')
|
|
61
|
+
.bind('click', () => count.value++)
|
|
62
|
+
.render('#app');
|
|
63
|
+
|
|
64
|
+
jux.paragraph('counter')
|
|
65
|
+
.sync('text', count, val => \`Count: \${val}\`)
|
|
66
|
+
.render('#app');`)
|
|
67
|
+
.render('#appmain-content');
|
|
68
|
+
|
|
69
|
+
// Create a reactive counter demo
|
|
70
|
+
const count = state(0);
|
|
71
|
+
|
|
72
|
+
jux.button('increment')
|
|
73
|
+
.label('Click me!')
|
|
74
|
+
.bind('click', () => count.value++)
|
|
75
|
+
.style('margin: 1rem 0;')
|
|
76
|
+
.render('#appmain-content');
|
|
77
|
+
|
|
78
|
+
jux.paragraph('counter')
|
|
79
|
+
.sync('text', count, val => `Count: ${val}`)
|
|
80
|
+
.style('font-size: 1.25rem; font-weight: 600; color: var(--color-brand);')
|
|
81
|
+
.render('#appmain-content');
|
|
82
|
+
|
|
83
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
84
|
+
// FOOTER CONTENT
|
|
85
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
86
|
+
|
|
87
|
+
jux.paragraph('footer-text')
|
|
88
|
+
.text('© 2026 JUX Platform')
|
|
89
|
+
.style('color: var(--color-text-tertiary);')
|
|
90
|
+
.render('#appfooter-content');
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
APP CONTAINER - Main Layout Grid
|
|
3
|
+
============================================ */
|
|
4
|
+
#app {
|
|
5
|
+
display: grid;
|
|
6
|
+
grid-template-columns: auto 1fr auto;
|
|
7
|
+
grid-template-rows: auto 1fr auto;
|
|
8
|
+
grid-template-areas:
|
|
9
|
+
"header header header"
|
|
10
|
+
"sidebar main aside"
|
|
11
|
+
"footer footer footer";
|
|
12
|
+
width: 100vw;
|
|
13
|
+
height: 100vh;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* ============================================
|
|
18
|
+
HEADER - Top Navigation Bar
|
|
19
|
+
============================================ */
|
|
20
|
+
#appheader {
|
|
21
|
+
grid-area: header;
|
|
22
|
+
background: var(--color-surface-elevated);
|
|
23
|
+
border-bottom: var(--border-width) solid var(--color-border);
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
padding: 0 var(--space-lg);
|
|
27
|
+
height: 48px;
|
|
28
|
+
z-index: 100;
|
|
29
|
+
transition: background-color var(--transition-base), border-color var(--transition-base);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#appheader-content {
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
justify-content: space-between;
|
|
36
|
+
width: 100%;
|
|
37
|
+
gap: var(--space-xl);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#appheader-logo {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: var(--space-md);
|
|
44
|
+
font-size: var(--font-size-lg);
|
|
45
|
+
font-weight: var(--font-weight-semibold);
|
|
46
|
+
color: var(--color-text-primary);
|
|
47
|
+
text-decoration: none;
|
|
48
|
+
padding: var(--space-sm) var(--space-md);
|
|
49
|
+
border-radius: var(--radius-md);
|
|
50
|
+
transition: background-color var(--transition-fast);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#appheader-logo:hover {
|
|
54
|
+
background: var(--color-surface-hover);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#appheader-nav {
|
|
58
|
+
flex: 1;
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: var(--space-sm);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#appheader-actions {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: var(--space-md);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* ============================================
|
|
71
|
+
LEFT SIDEBAR - Navigation
|
|
72
|
+
============================================ */
|
|
73
|
+
.jux-sidebar,
|
|
74
|
+
#appaside {
|
|
75
|
+
grid-area: sidebar;
|
|
76
|
+
width: 260px;
|
|
77
|
+
background: var(--color-surface-base);
|
|
78
|
+
border-right: var(--border-width) solid var(--color-border);
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
transition: width var(--transition-base);
|
|
83
|
+
position: relative;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* ============================================
|
|
87
|
+
MAIN CONTENT - Center Area
|
|
88
|
+
============================================ */
|
|
89
|
+
#appmain {
|
|
90
|
+
grid-area: main;
|
|
91
|
+
background: var(--color-background);
|
|
92
|
+
overflow-y: auto;
|
|
93
|
+
overflow-x: hidden;
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
position: relative;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#appmain-content {
|
|
100
|
+
flex: 1;
|
|
101
|
+
padding: var(--space-3xl) var(--space-2xl);
|
|
102
|
+
max-width: 1600px;
|
|
103
|
+
width: 100%;
|
|
104
|
+
margin: 0 auto;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Custom scrollbar for main content */
|
|
108
|
+
#appmain::-webkit-scrollbar {
|
|
109
|
+
width: 8px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#appmain::-webkit-scrollbar-track {
|
|
113
|
+
background: var(--color-surface-base);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#appmain::-webkit-scrollbar-thumb {
|
|
117
|
+
background: var(--color-border);
|
|
118
|
+
border-radius: var(--radius-full);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#appmain::-webkit-scrollbar-thumb:hover {
|
|
122
|
+
background: var(--color-border-hover);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ============================================
|
|
126
|
+
RIGHT SIDEBAR - Optional Panel
|
|
127
|
+
============================================ */
|
|
128
|
+
#appsidebar {
|
|
129
|
+
grid-area: aside;
|
|
130
|
+
width: 280px;
|
|
131
|
+
background: var(--color-surface-base);
|
|
132
|
+
border-left: var(--border-width) solid var(--color-border);
|
|
133
|
+
display: none;
|
|
134
|
+
flex-direction: column;
|
|
135
|
+
overflow: hidden;
|
|
136
|
+
transition: width var(--transition-base), background-color var(--transition-base);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#appsidebar.show {
|
|
140
|
+
display: flex;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#appsidebar.collapsed {
|
|
144
|
+
width: 0;
|
|
145
|
+
border: none;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#appsidebar-header {
|
|
149
|
+
padding: var(--space-lg);
|
|
150
|
+
border-bottom: var(--border-width) solid var(--color-border);
|
|
151
|
+
font-weight: var(--font-weight-semibold);
|
|
152
|
+
font-size: var(--font-size-sm);
|
|
153
|
+
color: var(--color-text-secondary);
|
|
154
|
+
text-transform: uppercase;
|
|
155
|
+
letter-spacing: 0.05em;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#appsidebar-content {
|
|
159
|
+
flex: 1;
|
|
160
|
+
overflow-y: auto;
|
|
161
|
+
overflow-x: hidden;
|
|
162
|
+
padding: var(--space-lg);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#appsidebar-content::-webkit-scrollbar {
|
|
166
|
+
width: 6px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#appsidebar-content::-webkit-scrollbar-track {
|
|
170
|
+
background: transparent;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#appsidebar-content::-webkit-scrollbar-thumb {
|
|
174
|
+
background: var(--color-border);
|
|
175
|
+
border-radius: var(--radius-full);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#appsidebar-content::-webkit-scrollbar-thumb:hover {
|
|
179
|
+
background: var(--color-border-hover);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#appsidebar-footer {
|
|
183
|
+
padding: var(--space-lg);
|
|
184
|
+
border-top: var(--border-width) solid var(--color-border);
|
|
185
|
+
font-size: var(--font-size-xs);
|
|
186
|
+
color: var(--color-text-tertiary);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* ============================================
|
|
190
|
+
FOOTER - Bottom Bar
|
|
191
|
+
============================================ */
|
|
192
|
+
#appfooter {
|
|
193
|
+
grid-area: footer;
|
|
194
|
+
background: var(--color-surface-elevated);
|
|
195
|
+
border-top: var(--border-width) solid var(--color-border);
|
|
196
|
+
padding: var(--space-lg) var(--space-2xl);
|
|
197
|
+
display: flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
justify-content: space-between;
|
|
200
|
+
font-size: var(--font-size-sm);
|
|
201
|
+
color: var(--color-text-tertiary);
|
|
202
|
+
transition: background-color var(--transition-base), border-color var(--transition-base);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#appfooter-content {
|
|
206
|
+
display: flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
gap: var(--space-xl);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#appfooter-legal {
|
|
212
|
+
display: flex;
|
|
213
|
+
align-items: center;
|
|
214
|
+
gap: var(--space-lg);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#appfooter a {
|
|
218
|
+
color: var(--color-text-secondary);
|
|
219
|
+
text-decoration: none;
|
|
220
|
+
transition: color var(--transition-fast);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#appfooter a:hover {
|
|
224
|
+
color: var(--color-text-primary);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* ============================================
|
|
228
|
+
RESPONSIVE GRID LAYOUTS
|
|
229
|
+
============================================ */
|
|
230
|
+
@media (max-width: 1024px) {
|
|
231
|
+
#appsidebar {
|
|
232
|
+
display: none;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#app {
|
|
236
|
+
grid-template-columns: auto 1fr;
|
|
237
|
+
grid-template-areas:
|
|
238
|
+
"header header"
|
|
239
|
+
"sidebar main"
|
|
240
|
+
"footer footer";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@media (max-width: 768px) {
|
|
245
|
+
#app {
|
|
246
|
+
grid-template-columns: 1fr;
|
|
247
|
+
grid-template-areas:
|
|
248
|
+
"header"
|
|
249
|
+
"main"
|
|
250
|
+
"footer";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.jux-sidebar,
|
|
254
|
+
#appaside {
|
|
255
|
+
display: none;
|
|
256
|
+
position: fixed;
|
|
257
|
+
left: 0;
|
|
258
|
+
top: 0;
|
|
259
|
+
bottom: 0;
|
|
260
|
+
z-index: 1000;
|
|
261
|
+
width: 260px;
|
|
262
|
+
box-shadow: var(--shadow-xl);
|
|
263
|
+
transform: translateX(-100%);
|
|
264
|
+
transition: transform var(--transition-base);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.jux-sidebar.open,
|
|
268
|
+
#appaside.open {
|
|
269
|
+
display: flex;
|
|
270
|
+
transform: translateX(0);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#appmain-content {
|
|
274
|
+
padding: var(--space-2xl) var(--space-lg);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#appheader {
|
|
278
|
+
padding: 0 var(--space-md) 0 calc(var(--space-md) + 48px);
|
|
279
|
+
}
|
|
280
|
+
}
|
package/presets/hey.jux
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { jux } from 'juxscript';
|
|
2
|
-
|
|
3
|
-
// Welcome page with examples
|
|
4
|
-
jux.hero('welcome', {
|
|
5
|
-
title: 'Welcome to JUX',
|
|
6
|
-
subtitle: 'A JavaScript UX authorship platform'
|
|
7
|
-
}).render('#app');
|
|
8
|
-
|
|
9
|
-
jux.divider().render('#app');
|
|
10
|
-
|
|
11
|
-
jux.container('getting-started')
|
|
12
|
-
.style('max-width: 800px; margin: 2rem auto; padding: 2rem;')
|
|
13
|
-
.render('#app');
|
|
14
|
-
|
|
15
|
-
jux.heading('start-heading')
|
|
16
|
-
.level(2)
|
|
17
|
-
.text('Getting Started')
|
|
18
|
-
.render('#getting-started');
|
|
19
|
-
|
|
20
|
-
jux.paragraph('start-intro')
|
|
21
|
-
.text('Edit this file to build your app. Here are some quick tips:')
|
|
22
|
-
.style('margin: 1rem 0;')
|
|
23
|
-
.render('#getting-started');
|
|
24
|
-
|
|
25
|
-
jux.list('quick-tips', {
|
|
26
|
-
items: [
|
|
27
|
-
'Run npx jux serve for dev mode with hot reload',
|
|
28
|
-
'Run npx jux build to compile for production',
|
|
29
|
-
'Serve jux-dist/ from your backend',
|
|
30
|
-
'Check out the docs at juxscript.com/docs'
|
|
31
|
-
]
|
|
32
|
-
}).render('#getting-started');
|
|
33
|
-
|
|
34
|
-
jux.divider().render('#app');
|
|
35
|
-
|
|
36
|
-
jux.container('example-container')
|
|
37
|
-
.style('max-width: 800px; margin: 2rem auto; padding: 2rem; background: #f9fafb; border-radius: 8px;')
|
|
38
|
-
.render('#app');
|
|
39
|
-
|
|
40
|
-
jux.heading('example-heading')
|
|
41
|
-
.level(3)
|
|
42
|
-
.text('Quick Example')
|
|
43
|
-
.render('#example-container');
|
|
44
|
-
|
|
45
|
-
jux.code('example-code')
|
|
46
|
-
.language('javascript')
|
|
47
|
-
.code(`import { jux, state } from 'juxscript';
|
|
48
|
-
|
|
49
|
-
// Create a reactive counter
|
|
50
|
-
const count = state(0);
|
|
51
|
-
|
|
52
|
-
jux.button('increment')
|
|
53
|
-
.label('Click me!')
|
|
54
|
-
.bind('click', () => count.value++)
|
|
55
|
-
.render('#app');
|
|
56
|
-
|
|
57
|
-
jux.paragraph('counter')
|
|
58
|
-
.sync('text', count, val => \`Count: \${val}\`)
|
|
59
|
-
.render('#app');`)
|
|
60
|
-
.render('#example-container');
|