juxscript 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +37 -92
  2. package/bin/cli.js +57 -56
  3. package/lib/components/alert.ts +240 -0
  4. package/lib/components/app.ts +216 -82
  5. package/lib/components/badge.ts +164 -0
  6. package/lib/components/button.ts +188 -53
  7. package/lib/components/card.ts +75 -61
  8. package/lib/components/chart.ts +17 -15
  9. package/lib/components/checkbox.ts +228 -0
  10. package/lib/components/code.ts +66 -152
  11. package/lib/components/container.ts +104 -208
  12. package/lib/components/data.ts +1 -3
  13. package/lib/components/datepicker.ts +226 -0
  14. package/lib/components/dialog.ts +258 -0
  15. package/lib/components/docs-data.json +1697 -388
  16. package/lib/components/dropdown.ts +244 -0
  17. package/lib/components/element.ts +271 -0
  18. package/lib/components/fileupload.ts +319 -0
  19. package/lib/components/footer.ts +37 -18
  20. package/lib/components/header.ts +53 -33
  21. package/lib/components/heading.ts +119 -0
  22. package/lib/components/helpers.ts +34 -0
  23. package/lib/components/hero.ts +57 -31
  24. package/lib/components/include.ts +292 -0
  25. package/lib/components/input.ts +166 -78
  26. package/lib/components/layout.ts +144 -18
  27. package/lib/components/list.ts +83 -74
  28. package/lib/components/loading.ts +263 -0
  29. package/lib/components/main.ts +43 -17
  30. package/lib/components/menu.ts +108 -24
  31. package/lib/components/modal.ts +50 -21
  32. package/lib/components/nav.ts +60 -18
  33. package/lib/components/paragraph.ts +111 -0
  34. package/lib/components/progress.ts +276 -0
  35. package/lib/components/radio.ts +236 -0
  36. package/lib/components/req.ts +300 -0
  37. package/lib/components/script.ts +33 -74
  38. package/lib/components/select.ts +247 -0
  39. package/lib/components/sidebar.ts +86 -36
  40. package/lib/components/style.ts +47 -70
  41. package/lib/components/switch.ts +261 -0
  42. package/lib/components/table.ts +47 -24
  43. package/lib/components/tabs.ts +105 -63
  44. package/lib/components/theme-toggle.ts +361 -0
  45. package/lib/components/token-calculator.ts +380 -0
  46. package/lib/components/tooltip.ts +244 -0
  47. package/lib/components/view.ts +36 -20
  48. package/lib/components/write.ts +284 -0
  49. package/lib/globals.d.ts +21 -0
  50. package/lib/jux.ts +172 -68
  51. package/lib/presets/notion.css +521 -0
  52. package/lib/presets/notion.jux +27 -0
  53. package/lib/reactivity/state.ts +364 -0
  54. package/machinery/compiler.js +126 -38
  55. package/machinery/generators/html.js +2 -3
  56. package/machinery/server.js +2 -2
  57. package/package.json +29 -3
  58. package/lib/components/import.ts +0 -430
  59. package/lib/components/node.ts +0 -200
  60. package/lib/components/reactivity.js +0 -104
  61. package/lib/components/theme.ts +0 -97
  62. package/lib/layouts/notion.css +0 -258
  63. package/lib/styles/base-theme.css +0 -186
  64. package/lib/styles/dark-theme.css +0 -144
  65. package/lib/styles/light-theme.css +0 -144
  66. package/lib/styles/tokens/dark.css +0 -86
  67. package/lib/styles/tokens/light.css +0 -86
  68. package/lib/templates/index.juxt +0 -33
  69. package/lib/themes/dark.css +0 -86
  70. package/lib/themes/light.css +0 -86
  71. /package/lib/{styles → presets}/global.css +0 -0
@@ -1,16 +1,21 @@
1
- import { Reactive, getOrCreateContainer } from './reactivity.js';
1
+ import { getOrCreateContainer } from './helpers.js';
2
+ import { State } from '../reactivity/state.js';
2
3
 
3
4
  /**
4
5
  * Input component options
5
6
  */
6
7
  export interface InputOptions {
7
- type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search';
8
- label?: string;
9
- placeholder?: string;
8
+ type?: string;
10
9
  value?: string;
10
+ placeholder?: string;
11
+ label?: string;
11
12
  required?: boolean;
12
13
  disabled?: boolean;
14
+ name?: string;
15
+ rows?: number;
13
16
  onChange?: (value: string) => void;
17
+ style?: string;
18
+ class?: string;
14
19
  }
15
20
 
16
21
  /**
@@ -18,43 +23,57 @@ export interface InputOptions {
18
23
  */
19
24
  type InputState = {
20
25
  type: string;
21
- label: string;
22
- placeholder: string;
23
26
  value: string;
27
+ placeholder: string;
28
+ label: string;
24
29
  required: boolean;
25
30
  disabled: boolean;
26
- onChange: ((value: string) => void) | null;
31
+ name: string;
32
+ rows: number;
33
+ style: string;
34
+ class: string;
27
35
  };
28
36
 
29
37
  /**
30
- * Input component
38
+ * Input component - text input and textarea
31
39
  *
32
40
  * Usage:
33
- * const input = jux.input('myInput', {
34
- * label: 'Email',
35
- * type: 'email',
36
- * placeholder: 'Enter your email',
37
- * onChange: (value) => console.log(value)
38
- * });
39
- * input.render();
41
+ * jux.input('username')
42
+ * .label('Username')
43
+ * .placeholder('Enter username')
44
+ * .required(true)
45
+ * .render('#form');
46
+ *
47
+ * jux.input('bio')
48
+ * .type('textarea')
49
+ * .rows(5)
50
+ * .render('#form');
40
51
  */
41
- export class Input extends Reactive {
42
- state!: InputState;
52
+ export class Input {
53
+ state: InputState;
43
54
  container: HTMLElement | null = null;
44
-
45
- constructor(componentId: string, options: InputOptions = {}) {
46
- super();
47
- this._setComponentId(componentId);
48
-
49
- this.state = this._createReactiveState({
55
+ _id: string;
56
+ id: string;
57
+ private _onChange?: (value: string) => void;
58
+ private _boundState?: State<string>;
59
+
60
+ constructor(id: string, options: InputOptions = {}) {
61
+ this._id = id;
62
+ this.id = id;
63
+ this._onChange = options.onChange;
64
+
65
+ this.state = {
50
66
  type: options.type ?? 'text',
51
- label: options.label ?? '',
52
- placeholder: options.placeholder ?? '',
53
67
  value: options.value ?? '',
68
+ placeholder: options.placeholder ?? '',
69
+ label: options.label ?? '',
54
70
  required: options.required ?? false,
55
71
  disabled: options.disabled ?? false,
56
- onChange: options.onChange ?? null
57
- }) as InputState;
72
+ name: options.name ?? id,
73
+ rows: options.rows ?? 3,
74
+ style: options.style ?? '',
75
+ class: options.class ?? ''
76
+ };
58
77
  }
59
78
 
60
79
  /* -------------------------
@@ -66,8 +85,9 @@ export class Input extends Reactive {
66
85
  return this;
67
86
  }
68
87
 
69
- label(value: string): this {
70
- this.state.label = value;
88
+ value(value: string): this {
89
+ this.state.value = value;
90
+ this._updateElement();
71
91
  return this;
72
92
  }
73
93
 
@@ -76,8 +96,8 @@ export class Input extends Reactive {
76
96
  return this;
77
97
  }
78
98
 
79
- value(value: string): this {
80
- this.state.value = value;
99
+ label(value: string): this {
100
+ this.state.label = value;
81
101
  return this;
82
102
  }
83
103
 
@@ -88,21 +108,76 @@ export class Input extends Reactive {
88
108
 
89
109
  disabled(value: boolean): this {
90
110
  this.state.disabled = value;
111
+ this._updateElement();
112
+ return this;
113
+ }
114
+
115
+ name(value: string): this {
116
+ this.state.name = value;
117
+ return this;
118
+ }
119
+
120
+ rows(value: number): this {
121
+ this.state.rows = value;
122
+ return this;
123
+ }
124
+
125
+ style(value: string): this {
126
+ this.state.style = value;
127
+ return this;
128
+ }
129
+
130
+ class(value: string): this {
131
+ this.state.class = value;
132
+ return this;
133
+ }
134
+
135
+ onChange(handler: (value: string) => void): this {
136
+ this._onChange = handler;
91
137
  return this;
92
138
  }
93
139
 
94
- onChange(callback: (value: string) => void): this {
95
- this.state.onChange = callback;
140
+ /**
141
+ * Two-way binding to state
142
+ */
143
+ bind(stateObj: State<string>): this {
144
+ this._boundState = stateObj;
145
+
146
+ // Subscribe to state changes
147
+ stateObj.subscribe((val) => {
148
+ this.state.value = val;
149
+ this._updateElement();
150
+ });
151
+
152
+ // Update state on input change
153
+ this.onChange((value) => stateObj.set(value));
154
+
96
155
  return this;
97
156
  }
98
157
 
158
+ /* -------------------------
159
+ * Helpers
160
+ * ------------------------- */
161
+
162
+ private _updateElement(): void {
163
+ const input = document.getElementById(`${this._id}-input`) as HTMLInputElement | HTMLTextAreaElement;
164
+ if (input) {
165
+ input.value = this.state.value;
166
+ input.disabled = this.state.disabled;
167
+ }
168
+ }
169
+
170
+ getValue(): string {
171
+ return this.state.value;
172
+ }
173
+
99
174
  /* -------------------------
100
175
  * Render
101
176
  * ------------------------- */
102
177
 
103
178
  render(targetId?: string): this {
104
179
  let container: HTMLElement;
105
-
180
+
106
181
  if (targetId) {
107
182
  const target = document.querySelector(targetId);
108
183
  if (!target || !(target instanceof HTMLElement)) {
@@ -110,66 +185,79 @@ export class Input extends Reactive {
110
185
  }
111
186
  container = target;
112
187
  } else {
113
- container = getOrCreateContainer(this._componentId) as HTMLElement;
188
+ container = getOrCreateContainer(this._id);
114
189
  }
115
-
190
+
116
191
  this.container = container;
117
- const { type, label, placeholder, value, required, disabled, onChange } = this.state;
118
-
192
+ const { type, value, placeholder, label, required, disabled, name, rows, style, class: className } = this.state;
193
+
119
194
  const wrapper = document.createElement('div');
120
- wrapper.className = 'jux-input-wrapper';
121
- wrapper.id = this._componentId;
122
-
195
+ wrapper.className = 'jux-input';
196
+ wrapper.id = this._id;
197
+
198
+ if (className) {
199
+ wrapper.className += ` ${className}`;
200
+ }
201
+
202
+ if (style) {
203
+ wrapper.setAttribute('style', style);
204
+ }
205
+
206
+ // Label
123
207
  if (label) {
124
208
  const labelEl = document.createElement('label');
125
209
  labelEl.className = 'jux-input-label';
210
+ labelEl.htmlFor = `${this._id}-input`;
126
211
  labelEl.textContent = label;
127
- labelEl.setAttribute('for', `${this._componentId}-input`);
212
+ if (required) {
213
+ const requiredSpan = document.createElement('span');
214
+ requiredSpan.className = 'jux-input-required';
215
+ requiredSpan.textContent = ' *';
216
+ labelEl.appendChild(requiredSpan);
217
+ }
128
218
  wrapper.appendChild(labelEl);
129
219
  }
130
-
131
- const input = document.createElement('input');
132
- input.id = `${this._componentId}-input`;
133
- input.className = 'jux-input';
134
- input.type = type;
135
- input.placeholder = placeholder;
136
- input.value = value;
137
- input.required = required;
138
- input.disabled = disabled;
139
-
140
- wrapper.appendChild(input);
141
- container.appendChild(wrapper);
142
-
143
- // Event binding - onChange
144
- if (onChange) {
145
- input.addEventListener('input', (e) => {
146
- const target = e.target as HTMLInputElement;
147
- onChange(target.value);
148
- });
220
+
221
+ // Input/Textarea
222
+ let inputEl: HTMLInputElement | HTMLTextAreaElement;
223
+
224
+ if (type === 'textarea') {
225
+ inputEl = document.createElement('textarea');
226
+ inputEl.rows = rows;
227
+ } else {
228
+ inputEl = document.createElement('input');
229
+ inputEl.type = type;
149
230
  }
150
-
231
+
232
+ inputEl.className = 'jux-input-element';
233
+ inputEl.id = `${this._id}-input`;
234
+ inputEl.name = name;
235
+ inputEl.value = value;
236
+ inputEl.placeholder = placeholder;
237
+ inputEl.required = required;
238
+ inputEl.disabled = disabled;
239
+
240
+ inputEl.addEventListener('input', (e) => {
241
+ const target = e.target as HTMLInputElement | HTMLTextAreaElement;
242
+ this.state.value = target.value;
243
+ if (this._onChange) {
244
+ this._onChange(target.value);
245
+ }
246
+ });
247
+
248
+ wrapper.appendChild(inputEl);
249
+ container.appendChild(wrapper);
151
250
  return this;
152
251
  }
153
252
 
154
- /**
155
- * Render to another Jux component's container
156
- */
157
253
  renderTo(juxComponent: any): this {
158
- if (!juxComponent || typeof juxComponent !== 'object') {
159
- throw new Error('Input.renderTo: Invalid component - not an object');
160
- }
161
-
162
- if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
163
- throw new Error('Input.renderTo: Invalid component - missing _componentId (not a Jux component)');
254
+ if (!juxComponent?._id) {
255
+ throw new Error('Input.renderTo: Invalid component');
164
256
  }
165
-
166
- return this.render(`#${juxComponent._componentId}`);
257
+ return this.render(`#${juxComponent._id}`);
167
258
  }
168
259
  }
169
260
 
170
- /**
171
- * Factory helper
172
- */
173
- export function input(componentId: string, options: InputOptions = {}): Input {
174
- return new Input(componentId, options);
261
+ export function input(id: string, options: InputOptions = {}): Input {
262
+ return new Input(id, options);
175
263
  }
@@ -1,19 +1,45 @@
1
1
  import { ErrorHandler } from './error-handler.js';
2
2
 
3
3
  /**
4
- * Layout - Load a JUX layout file
5
- * Auto-loads when file is set
4
+ * Layout component interface
5
+ * Represents any Jux component that can be rendered
6
+ */
7
+ interface LayoutComponent {
8
+ render: (target?: string) => any;
9
+ _componentId?: string;
10
+ _id?: string;
11
+ }
12
+
13
+ /**
14
+ * Layout - Compose and render multiple components
15
+ *
16
+ * Usage:
17
+ * // As a composition function
18
+ * jux.layout(
19
+ * jux.include('/lib/presets/notion/notion.css'),
20
+ * jux.header('appheader').render('#app'),
21
+ * jux.sidebar('appsidebar').render('#app'),
22
+ * jux.main('appmain').render('#app')
23
+ * );
24
+ *
25
+ * // Or load from a .jux file
26
+ * jux.layout('/lib/presets/notion/main.jux');
6
27
  */
7
28
  export class Layout {
8
29
  private _juxFile: string;
9
30
  private _loaded: boolean = false;
31
+ private _components: any[] = [];
10
32
 
11
- constructor(juxFile: string = '') {
12
- this._juxFile = juxFile;
13
-
14
- // Auto-load if file provided
15
- if (juxFile) {
33
+ constructor(...args: any[]) {
34
+ // If first arg is string, it's a file path
35
+ if (args.length === 1 && typeof args[0] === 'string' && args[0].endsWith('.jux')) {
36
+ this._juxFile = args[0];
16
37
  this.load();
38
+ } else {
39
+ // Otherwise, it's a list of components to compose
40
+ this._juxFile = '';
41
+ this._components = args;
42
+ this._compose();
17
43
  }
18
44
  }
19
45
 
@@ -34,6 +60,41 @@ export class Layout {
34
60
  return this._juxFile;
35
61
  }
36
62
 
63
+ /**
64
+ * Compose components immediately
65
+ * This handles the case where components are passed directly
66
+ */
67
+ private _compose(): this {
68
+ try {
69
+ // Components are already rendered via chaining
70
+ // This just ensures they're tracked
71
+ this._components.forEach((component, index) => {
72
+ // Components that return themselves from .render() are already in DOM
73
+ // Just log for debugging
74
+ if (component && typeof component === 'object') {
75
+ const id = component._componentId || component._id || component.id || `component-${index}`;
76
+ console.log(`✓ Layout component composed: ${id}`);
77
+ }
78
+ });
79
+
80
+ this._loaded = true;
81
+ } catch (error: any) {
82
+ ErrorHandler.captureError({
83
+ component: 'Layout',
84
+ method: '_compose',
85
+ message: `Failed to compose layout: ${error.message}`,
86
+ stack: error.stack,
87
+ timestamp: new Date(),
88
+ context: {
89
+ componentCount: this._components.length,
90
+ error: 'composition_failed'
91
+ }
92
+ });
93
+ }
94
+
95
+ return this;
96
+ }
97
+
37
98
  /**
38
99
  * Normalize path to absolute URL from site root
39
100
  */
@@ -42,22 +103,22 @@ export class Layout {
42
103
  if (path.startsWith('http://') || path.startsWith('https://')) {
43
104
  return path;
44
105
  }
45
-
106
+
46
107
  // Convert relative path to absolute
47
108
  // Remove leading './' if present
48
109
  let cleanPath = path.replace(/^\.\//, '');
49
-
110
+
50
111
  // Ensure it starts with /
51
112
  if (!cleanPath.startsWith('/')) {
52
113
  cleanPath = '/' + cleanPath;
53
114
  }
54
-
115
+
55
116
  // Return absolute URL with origin
56
117
  return new URL(cleanPath, window.location.origin).href;
57
118
  }
58
119
 
59
120
  /**
60
- * Load the layout
121
+ * Load the layout from a .jux file
61
122
  * This will dynamically import the compiled JS file
62
123
  */
63
124
  async load(): Promise<this> {
@@ -65,25 +126,33 @@ export class Layout {
65
126
  return this;
66
127
  }
67
128
 
129
+ if (!this._juxFile) {
130
+ console.warn('Layout: No file specified to load');
131
+ return this;
132
+ }
133
+
68
134
  try {
69
135
  // Convert .jux to .js for the compiled output
70
136
  let jsFile = this._juxFile.replace(/\.jux$/, '.js');
71
-
137
+
72
138
  // Normalize to absolute URL for browser import
73
139
  jsFile = this.normalizePath(jsFile);
74
-
140
+
75
141
  console.log(`Loading layout: ${jsFile}`);
76
-
142
+
77
143
  // Dynamic import of the layout module
78
144
  const layoutModule = await import(jsFile);
79
-
145
+
80
146
  // If the module has an init or default export, call it
81
147
  if (typeof layoutModule.default === 'function') {
82
148
  await layoutModule.default();
83
149
  } else if (typeof layoutModule.init === 'function') {
84
150
  await layoutModule.init();
151
+ } else if (layoutModule.default && typeof layoutModule.default === 'object') {
152
+ // If default export is the layout object itself
153
+ console.log('✓ Layout module loaded (object export)');
85
154
  }
86
-
155
+
87
156
  this._loaded = true;
88
157
  console.log(`✓ Layout loaded: ${this._juxFile}`);
89
158
  } catch (error: any) {
@@ -93,7 +162,7 @@ export class Layout {
93
162
  message: `Failed to load layout: ${error.message}`,
94
163
  stack: error.stack,
95
164
  timestamp: new Date(),
96
- context: {
165
+ context: {
97
166
  juxFile: this._juxFile,
98
167
  jsFile: this._juxFile.replace(/\.jux$/, '.js'),
99
168
  errorCode: error.code
@@ -110,4 +179,61 @@ export class Layout {
110
179
  isLoaded(): boolean {
111
180
  return this._loaded;
112
181
  }
113
- }
182
+
183
+ /**
184
+ * Get all composed components
185
+ */
186
+ getComponents(): any[] {
187
+ return this._components;
188
+ }
189
+
190
+ /**
191
+ * Add a component to the layout
192
+ */
193
+ add(component: any): this {
194
+ this._components.push(component);
195
+ return this;
196
+ }
197
+
198
+ /**
199
+ * Render all components
200
+ * Useful if components weren't already rendered
201
+ */
202
+ render(target?: string): this {
203
+ this._components.forEach((component) => {
204
+ if (component && typeof component.render === 'function') {
205
+ try {
206
+ component.render(target);
207
+ } catch (error: any) {
208
+ ErrorHandler.captureError({
209
+ component: 'Layout',
210
+ method: 'render',
211
+ message: `Failed to render component: ${error.message}`,
212
+ stack: error.stack,
213
+ timestamp: new Date(),
214
+ context: {
215
+ componentId: component._componentId || component._id || 'unknown',
216
+ target
217
+ }
218
+ });
219
+ }
220
+ }
221
+ });
222
+
223
+ return this;
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Factory helper
229
+ * Accepts either:
230
+ * - A file path: layout('/lib/presets/notion/main.jux')
231
+ * - A list of components: layout(header, sidebar, main)
232
+ */
233
+ export function layout(...args: any[]): Layout {
234
+ return new Layout(...args);
235
+ }
236
+
237
+ /* -------------------------
238
+ * Module Exports
239
+ * ------------------------- */