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 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
- - [ ] Charts
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
- - [ ] Cross Page Store.
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
- - [ ] Distributable Bundle (Static Sites)
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
- // Copy template file from lib/presets/index.juxt
187
- const templatePath = path.join(PATHS.packageRoot, 'lib', 'presets', 'index.juxt');
188
- const targetPath = path.join(juxDir, 'index.jux');
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
- jux.style('/lib/layouts/default.css');
200
- jux.theme('light');
190
+ // Apply a layout preset (optional)
191
+ // import 'juxscript/presets/notion.js';
201
192
 
202
- const header = jux.header('header').render("#app");
203
- const main = jux.main('main').render("#app");
204
- const footer = jux.footer('footer').render("#app");
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('hero1', {
200
+ jux.hero('welcome-hero', {
207
201
  title: 'Welcome to JUX',
208
202
  subtitle: 'A JavaScript UX authorship platform'
209
- }).render('#main');
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
- fs.writeFileSync(targetPath, fallbackContent);
212
- console.log('✅ Created jux/index.jux');
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. Edit jux/index.jux');
230
- console.log(' 2. Run: npx jux build');
231
- console.log(' 3. Serve jux-dist/ from your backend\n');
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 (Convention):
279
+ Project Structure:
253
280
  my-project/
254
- ├── jux/ # Your .jux source files (REQUIRED)
255
- │ ├── index.jux
256
- ├── samples/
257
- │ │ └── mypage.ts # TypeScript files transpiled to .js
258
- │ └── pages/
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 jux/ directory
268
- 2. npx jux build # Build to jux-dist/
269
- 3. Serve jux-dist/ from your backend
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-21T05:03:38.497Z"
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
+ }
@@ -0,0 +1,4 @@
1
+ import { hero1 } from './hero1.js'
2
+
3
+
4
+ export { hero1 }
@@ -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
- // Clear and rebuild
257
- element.innerHTML = '';
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: ${styleMode === 'gradient' ? 'rgba(255, 255, 255, 0.95)' : '#6b7280'};
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: ${styleMode === 'gradient' ? '#ffffff' : '#1f2937'};
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
- const deltaColor = styleMode === 'gradient'
398
- ? (delta > 0 ? 'rgba(255, 255, 255, 0.95)' : 'rgba(255, 200, 200, 0.95)')
399
- : (delta > 0 ? '#10b981' : '#ef4444');
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 ?? '300px',
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
- sidebar.style.width = '0';
120
+ if (width) {
121
+ sidebar.style.width = '0';
122
+ }
121
123
  } else {
122
124
  sidebar.classList.remove('jux-sidebar-collapsed');
123
- sidebar.style.width = width;
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
- sidebar.style.width = collapsed ? '0' : width;
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 } from './components/areachartsmooth.js';
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
  */
@@ -133,7 +133,6 @@ p {
133
133
 
134
134
  /* Sidebar Component */
135
135
  .jux-sidebar {
136
- padding: var(--space-md);
137
136
  height: 100%;
138
137
  overflow-y: auto;
139
138
  }
@@ -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 -->
@@ -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') { // Changed default
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)) return res.sendFile(heyPath);
73
- if (fs.existsSync(indexPath)) return res.sendFile(indexPath);
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'); // Changed default
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.8",
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
- "./lib/*": "./lib/*",
15
- "./lib/components/*": "./lib/components/*",
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
- "publishConfig": {
51
- "access": "public"
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
- "authoring",
41
+ "ux",
42
+ "components",
43
+ "framework",
44
+ "reactive",
61
45
  "javascript"
62
46
  ],
63
- "author": "Tim Kerr",
64
- "license": "MIT",
65
- "dependencies": {
66
- "acorn": "^8.15.0",
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
- "optionalDependencies": {
78
- "mysql2": "^3.6.5",
79
- "pg": "^8.11.3"
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.9.3"
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
- }