juxscript 1.0.0

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 (61) hide show
  1. package/README.md +292 -0
  2. package/bin/cli.js +149 -0
  3. package/lib/adapters/base-adapter.js +35 -0
  4. package/lib/adapters/index.js +33 -0
  5. package/lib/adapters/mysql-adapter.js +65 -0
  6. package/lib/adapters/postgres-adapter.js +70 -0
  7. package/lib/adapters/sqlite-adapter.js +56 -0
  8. package/lib/components/app.ts +124 -0
  9. package/lib/components/button.ts +136 -0
  10. package/lib/components/card.ts +205 -0
  11. package/lib/components/chart.ts +125 -0
  12. package/lib/components/code.ts +242 -0
  13. package/lib/components/container.ts +282 -0
  14. package/lib/components/data.ts +105 -0
  15. package/lib/components/docs-data.json +1211 -0
  16. package/lib/components/error-handler.ts +285 -0
  17. package/lib/components/footer.ts +146 -0
  18. package/lib/components/header.ts +167 -0
  19. package/lib/components/hero.ts +170 -0
  20. package/lib/components/import.ts +430 -0
  21. package/lib/components/input.ts +175 -0
  22. package/lib/components/layout.ts +113 -0
  23. package/lib/components/list.ts +392 -0
  24. package/lib/components/main.ts +111 -0
  25. package/lib/components/menu.ts +170 -0
  26. package/lib/components/modal.ts +216 -0
  27. package/lib/components/nav.ts +136 -0
  28. package/lib/components/node.ts +200 -0
  29. package/lib/components/reactivity.js +104 -0
  30. package/lib/components/script.ts +152 -0
  31. package/lib/components/sidebar.ts +168 -0
  32. package/lib/components/style.ts +129 -0
  33. package/lib/components/table.ts +279 -0
  34. package/lib/components/tabs.ts +191 -0
  35. package/lib/components/theme.ts +97 -0
  36. package/lib/components/view.ts +174 -0
  37. package/lib/jux.ts +203 -0
  38. package/lib/layouts/default.css +260 -0
  39. package/lib/layouts/default.jux +8 -0
  40. package/lib/layouts/figma.css +334 -0
  41. package/lib/layouts/figma.jux +0 -0
  42. package/lib/layouts/notion.css +258 -0
  43. package/lib/styles/base-theme.css +186 -0
  44. package/lib/styles/dark-theme.css +144 -0
  45. package/lib/styles/global.css +1131 -0
  46. package/lib/styles/light-theme.css +144 -0
  47. package/lib/styles/tokens/dark.css +86 -0
  48. package/lib/styles/tokens/light.css +86 -0
  49. package/lib/themes/dark.css +86 -0
  50. package/lib/themes/light.css +86 -0
  51. package/lib/utils/path-resolver.js +23 -0
  52. package/machinery/compiler.js +262 -0
  53. package/machinery/doc-generator.js +160 -0
  54. package/machinery/generators/css.js +128 -0
  55. package/machinery/generators/html.js +108 -0
  56. package/machinery/imports.js +155 -0
  57. package/machinery/server.js +185 -0
  58. package/machinery/validators/file-validator.js +123 -0
  59. package/machinery/watcher.js +148 -0
  60. package/package.json +58 -0
  61. package/types/globals.d.ts +16 -0
@@ -0,0 +1,175 @@
1
+ import { Reactive, getOrCreateContainer } from './reactivity.js';
2
+
3
+ /**
4
+ * Input component options
5
+ */
6
+ export interface InputOptions {
7
+ type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search';
8
+ label?: string;
9
+ placeholder?: string;
10
+ value?: string;
11
+ required?: boolean;
12
+ disabled?: boolean;
13
+ onChange?: (value: string) => void;
14
+ }
15
+
16
+ /**
17
+ * Input component state
18
+ */
19
+ type InputState = {
20
+ type: string;
21
+ label: string;
22
+ placeholder: string;
23
+ value: string;
24
+ required: boolean;
25
+ disabled: boolean;
26
+ onChange: ((value: string) => void) | null;
27
+ };
28
+
29
+ /**
30
+ * Input component
31
+ *
32
+ * 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();
40
+ */
41
+ export class Input extends Reactive {
42
+ state!: InputState;
43
+ container: HTMLElement | null = null;
44
+
45
+ constructor(componentId: string, options: InputOptions = {}) {
46
+ super();
47
+ this._setComponentId(componentId);
48
+
49
+ this.state = this._createReactiveState({
50
+ type: options.type ?? 'text',
51
+ label: options.label ?? '',
52
+ placeholder: options.placeholder ?? '',
53
+ value: options.value ?? '',
54
+ required: options.required ?? false,
55
+ disabled: options.disabled ?? false,
56
+ onChange: options.onChange ?? null
57
+ }) as InputState;
58
+ }
59
+
60
+ /* -------------------------
61
+ * Fluent API
62
+ * ------------------------- */
63
+
64
+ type(value: string): this {
65
+ this.state.type = value;
66
+ return this;
67
+ }
68
+
69
+ label(value: string): this {
70
+ this.state.label = value;
71
+ return this;
72
+ }
73
+
74
+ placeholder(value: string): this {
75
+ this.state.placeholder = value;
76
+ return this;
77
+ }
78
+
79
+ value(value: string): this {
80
+ this.state.value = value;
81
+ return this;
82
+ }
83
+
84
+ required(value: boolean): this {
85
+ this.state.required = value;
86
+ return this;
87
+ }
88
+
89
+ disabled(value: boolean): this {
90
+ this.state.disabled = value;
91
+ return this;
92
+ }
93
+
94
+ onChange(callback: (value: string) => void): this {
95
+ this.state.onChange = callback;
96
+ return this;
97
+ }
98
+
99
+ /* -------------------------
100
+ * Render
101
+ * ------------------------- */
102
+
103
+ render(targetId?: string): this {
104
+ let container: HTMLElement;
105
+
106
+ if (targetId) {
107
+ const target = document.querySelector(targetId);
108
+ if (!target || !(target instanceof HTMLElement)) {
109
+ throw new Error(`Input: Target element "${targetId}" not found`);
110
+ }
111
+ container = target;
112
+ } else {
113
+ container = getOrCreateContainer(this._componentId) as HTMLElement;
114
+ }
115
+
116
+ this.container = container;
117
+ const { type, label, placeholder, value, required, disabled, onChange } = this.state;
118
+
119
+ const wrapper = document.createElement('div');
120
+ wrapper.className = 'jux-input-wrapper';
121
+ wrapper.id = this._componentId;
122
+
123
+ if (label) {
124
+ const labelEl = document.createElement('label');
125
+ labelEl.className = 'jux-input-label';
126
+ labelEl.textContent = label;
127
+ labelEl.setAttribute('for', `${this._componentId}-input`);
128
+ wrapper.appendChild(labelEl);
129
+ }
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
+ });
149
+ }
150
+
151
+ return this;
152
+ }
153
+
154
+ /**
155
+ * Render to another Jux component's container
156
+ */
157
+ 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)');
164
+ }
165
+
166
+ return this.render(`#${juxComponent._componentId}`);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Factory helper
172
+ */
173
+ export function input(componentId: string, options: InputOptions = {}): Input {
174
+ return new Input(componentId, options);
175
+ }
@@ -0,0 +1,113 @@
1
+ import { ErrorHandler } from './error-handler.js';
2
+
3
+ /**
4
+ * Layout - Load a JUX layout file
5
+ * Auto-loads when file is set
6
+ */
7
+ export class Layout {
8
+ private _juxFile: string;
9
+ private _loaded: boolean = false;
10
+
11
+ constructor(juxFile: string = '') {
12
+ this._juxFile = juxFile;
13
+
14
+ // Auto-load if file provided
15
+ if (juxFile) {
16
+ this.load();
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Set the JUX file to load
22
+ */
23
+ file(juxFile: string): this {
24
+ this._juxFile = juxFile;
25
+ this._loaded = false;
26
+ this.load();
27
+ return this;
28
+ }
29
+
30
+ /**
31
+ * Get the current JUX file path
32
+ */
33
+ getFile(): string {
34
+ return this._juxFile;
35
+ }
36
+
37
+ /**
38
+ * Normalize path to absolute URL from site root
39
+ */
40
+ private normalizePath(path: string): string {
41
+ // If already a full URL, return as-is
42
+ if (path.startsWith('http://') || path.startsWith('https://')) {
43
+ return path;
44
+ }
45
+
46
+ // Convert relative path to absolute
47
+ // Remove leading './' if present
48
+ let cleanPath = path.replace(/^\.\//, '');
49
+
50
+ // Ensure it starts with /
51
+ if (!cleanPath.startsWith('/')) {
52
+ cleanPath = '/' + cleanPath;
53
+ }
54
+
55
+ // Return absolute URL with origin
56
+ return new URL(cleanPath, window.location.origin).href;
57
+ }
58
+
59
+ /**
60
+ * Load the layout
61
+ * This will dynamically import the compiled JS file
62
+ */
63
+ async load(): Promise<this> {
64
+ if (typeof document === 'undefined') {
65
+ return this;
66
+ }
67
+
68
+ try {
69
+ // Convert .jux to .js for the compiled output
70
+ let jsFile = this._juxFile.replace(/\.jux$/, '.js');
71
+
72
+ // Normalize to absolute URL for browser import
73
+ jsFile = this.normalizePath(jsFile);
74
+
75
+ console.log(`Loading layout: ${jsFile}`);
76
+
77
+ // Dynamic import of the layout module
78
+ const layoutModule = await import(jsFile);
79
+
80
+ // If the module has an init or default export, call it
81
+ if (typeof layoutModule.default === 'function') {
82
+ await layoutModule.default();
83
+ } else if (typeof layoutModule.init === 'function') {
84
+ await layoutModule.init();
85
+ }
86
+
87
+ this._loaded = true;
88
+ console.log(`✓ Layout loaded: ${this._juxFile}`);
89
+ } catch (error: any) {
90
+ ErrorHandler.captureError({
91
+ component: 'Layout',
92
+ method: 'load',
93
+ message: `Failed to load layout: ${error.message}`,
94
+ stack: error.stack,
95
+ timestamp: new Date(),
96
+ context: {
97
+ juxFile: this._juxFile,
98
+ jsFile: this._juxFile.replace(/\.jux$/, '.js'),
99
+ errorCode: error.code
100
+ }
101
+ });
102
+ }
103
+
104
+ return this;
105
+ }
106
+
107
+ /**
108
+ * Check if layout is loaded
109
+ */
110
+ isLoaded(): boolean {
111
+ return this._loaded;
112
+ }
113
+ }
@@ -0,0 +1,392 @@
1
+ import { Reactive, getOrCreateContainer } from './reactivity.js';
2
+
3
+ /**
4
+ * List item interface
5
+ */
6
+ export interface ListItem {
7
+ icon?: string;
8
+ title?: string;
9
+ body?: string;
10
+ type?: 'success' | 'warning' | 'error' | 'info' | 'default' | string;
11
+ metadata?: string;
12
+ }
13
+
14
+ /**
15
+ * List component options
16
+ */
17
+ export interface ListOptions {
18
+ items?: ListItem[];
19
+ header?: string;
20
+ gap?: string;
21
+ direction?: 'vertical' | 'horizontal';
22
+ selectable?: boolean;
23
+ selectedIndex?: number | null;
24
+ onItemClick?: (item: ListItem, index: number, e: Event) => void;
25
+ onItemDoubleClick?: (item: ListItem, index: number, e: Event) => void;
26
+ }
27
+
28
+ /**
29
+ * List component state
30
+ */
31
+ type ListState = {
32
+ items: ListItem[];
33
+ header: string;
34
+ gap: string;
35
+ direction: string;
36
+ selectable: boolean;
37
+ selectedIndex: number | null;
38
+ };
39
+
40
+ /**
41
+ * List component - renders a list of items with optional header
42
+ *
43
+ * Usage:
44
+ * const myList = jux.list('myList', {
45
+ * header: '✓ Accomplishments',
46
+ * items: [
47
+ * { icon: '✓', title: 'Task 1', body: 'Description', type: 'success' },
48
+ * { icon: '⚠️', title: 'Task 2', body: 'Description', type: 'warning' }
49
+ * ],
50
+ * gap: '0.75rem',
51
+ * selectable: true,
52
+ * onItemClick: (item, index) => console.log('Clicked:', item, index)
53
+ * });
54
+ * myList.render();
55
+ *
56
+ * // Add item
57
+ * myList.add({ icon: '🎉', title: 'New Task', body: 'Done!', type: 'success' });
58
+ *
59
+ * // Remove item by index
60
+ * myList.remove(1);
61
+ *
62
+ * // Move item from index 0 to index 2
63
+ * myList.move(0, 2);
64
+ */
65
+ export class List extends Reactive {
66
+ state!: ListState;
67
+ container: HTMLElement | null = null;
68
+ private _onItemClick: ((item: ListItem, index: number, e: Event) => void) | null;
69
+ private _onItemDoubleClick: ((item: ListItem, index: number, e: Event) => void) | null;
70
+
71
+ constructor(componentId: string, options: ListOptions = {}) {
72
+ super();
73
+ this._setComponentId(componentId);
74
+
75
+ this.state = this._createReactiveState({
76
+ items: options.items ?? [],
77
+ header: options.header ?? '',
78
+ gap: options.gap ?? '0.5rem',
79
+ direction: options.direction ?? 'vertical',
80
+ selectable: options.selectable ?? false,
81
+ selectedIndex: options.selectedIndex ?? null
82
+ }) as ListState;
83
+
84
+ this._onItemClick = options.onItemClick ?? null;
85
+ this._onItemDoubleClick = options.onItemDoubleClick ?? null;
86
+ }
87
+
88
+ /* -------------------------
89
+ * Fluent API
90
+ * ------------------------- */
91
+
92
+ items(value: ListItem[]): this {
93
+ this.state.items = value;
94
+ return this;
95
+ }
96
+
97
+ header(value: string): this {
98
+ this.state.header = value;
99
+ return this;
100
+ }
101
+
102
+ gap(value: string): this {
103
+ this.state.gap = value;
104
+ return this;
105
+ }
106
+
107
+ direction(value: 'vertical' | 'horizontal'): this {
108
+ this.state.direction = value;
109
+ return this;
110
+ }
111
+
112
+ selectable(value: boolean): this {
113
+ this.state.selectable = value;
114
+ return this;
115
+ }
116
+
117
+ /* -------------------------
118
+ * List operations
119
+ * ------------------------- */
120
+
121
+ add(item: ListItem, index?: number): this {
122
+ const items = [...this.state.items];
123
+
124
+ if (typeof index === 'number' && index >= 0 && index <= items.length) {
125
+ items.splice(index, 0, item);
126
+ } else {
127
+ index = items.length;
128
+ items.push(item);
129
+ }
130
+
131
+ this.state.items = items;
132
+ this.emit('itemAdded', { item, index });
133
+ this._updateDOM();
134
+ return this;
135
+ }
136
+
137
+ remove(index: number): this {
138
+ if (typeof index !== 'number' || index < 0 || index >= this.state.items.length) {
139
+ console.error(`List: Invalid index ${index} for remove`);
140
+ return this;
141
+ }
142
+
143
+ const items = [...this.state.items];
144
+ const removed = items.splice(index, 1)[0];
145
+
146
+ // Adjust selected index
147
+ if (this.state.selectedIndex !== null) {
148
+ if (this.state.selectedIndex === index) {
149
+ this.state.selectedIndex = null;
150
+ } else if (this.state.selectedIndex > index) {
151
+ this.state.selectedIndex--;
152
+ }
153
+ }
154
+
155
+ this.state.items = items;
156
+ this.emit('itemRemoved', { item: removed, index });
157
+ this._updateDOM();
158
+ return this;
159
+ }
160
+
161
+ move(fromIndex: number, toIndex: number): this {
162
+ const items = [...this.state.items];
163
+
164
+ if (fromIndex < 0 || fromIndex >= items.length) {
165
+ console.error(`List: Invalid fromIndex ${fromIndex}`);
166
+ return this;
167
+ }
168
+
169
+ if (toIndex < 0 || toIndex >= items.length) {
170
+ console.error(`List: Invalid toIndex ${toIndex}`);
171
+ return this;
172
+ }
173
+
174
+ if (fromIndex === toIndex) {
175
+ return this;
176
+ }
177
+
178
+ const [movedItem] = items.splice(fromIndex, 1);
179
+ items.splice(toIndex, 0, movedItem);
180
+
181
+ // Adjust selected index
182
+ if (this.state.selectedIndex !== null) {
183
+ if (this.state.selectedIndex === fromIndex) {
184
+ this.state.selectedIndex = toIndex;
185
+ } else if (fromIndex < this.state.selectedIndex && toIndex >= this.state.selectedIndex) {
186
+ this.state.selectedIndex--;
187
+ } else if (fromIndex > this.state.selectedIndex && toIndex <= this.state.selectedIndex) {
188
+ this.state.selectedIndex++;
189
+ }
190
+ }
191
+
192
+ this.state.items = items;
193
+ this.emit('itemMoved', { item: movedItem, fromIndex, toIndex });
194
+ this._updateDOM();
195
+ return this;
196
+ }
197
+
198
+ select(index: number): this {
199
+ if (index < 0 || index >= this.state.items.length) {
200
+ console.error(`List: Invalid index ${index} for select`);
201
+ return this;
202
+ }
203
+
204
+ const previousIndex = this.state.selectedIndex;
205
+ this.state.selectedIndex = index;
206
+
207
+ this.emit('itemSelect', {
208
+ item: this.state.items[index],
209
+ index,
210
+ previousIndex
211
+ });
212
+
213
+ this._updateDOM();
214
+ return this;
215
+ }
216
+
217
+ deselect(): this {
218
+ if (this.state.selectedIndex === null) {
219
+ return this;
220
+ }
221
+
222
+ const previousIndex = this.state.selectedIndex;
223
+ this.state.selectedIndex = null;
224
+
225
+ this.emit('itemDeselect', { previousIndex });
226
+ this._updateDOM();
227
+ return this;
228
+ }
229
+
230
+ getSelected(): { item: ListItem; index: number } | null {
231
+ if (this.state.selectedIndex === null) {
232
+ return null;
233
+ }
234
+ return {
235
+ item: this.state.items[this.state.selectedIndex],
236
+ index: this.state.selectedIndex
237
+ };
238
+ }
239
+
240
+ /* -------------------------
241
+ * Helpers
242
+ * ------------------------- */
243
+
244
+ private _updateDOM(): void {
245
+ if (!this.container) return;
246
+
247
+ // Clear and re-render
248
+ this.container.innerHTML = '';
249
+ this._renderContent();
250
+ }
251
+
252
+ private _renderContent(): void {
253
+ if (!this.container) return;
254
+
255
+ const { items, header, gap, direction, selectable, selectedIndex } = this.state;
256
+
257
+ const wrapper = document.createElement('div');
258
+ wrapper.className = 'jux-list-wrapper';
259
+ wrapper.id = this._componentId;
260
+
261
+ // Header
262
+ if (header) {
263
+ const headerEl = document.createElement('div');
264
+ headerEl.className = 'jux-list-header';
265
+ headerEl.textContent = header;
266
+ wrapper.appendChild(headerEl);
267
+ }
268
+
269
+ // List container
270
+ const listContainer = document.createElement('div');
271
+ listContainer.className = `jux-list jux-list-${direction}`;
272
+ listContainer.style.gap = gap;
273
+
274
+ // Render items
275
+ items.forEach((item, index) => {
276
+ const itemEl = document.createElement('div');
277
+ itemEl.className = `jux-list-item jux-list-item-${item.type || 'default'}`;
278
+
279
+ if (selectable && selectedIndex === index) {
280
+ itemEl.classList.add('jux-list-item-selected');
281
+ }
282
+
283
+ // Icon
284
+ if (item.icon) {
285
+ const iconEl = document.createElement('span');
286
+ iconEl.className = 'jux-list-item-icon';
287
+ iconEl.textContent = item.icon;
288
+ itemEl.appendChild(iconEl);
289
+ }
290
+
291
+ // Content
292
+ const contentEl = document.createElement('div');
293
+ contentEl.className = 'jux-list-item-content';
294
+
295
+ if (item.title) {
296
+ const titleEl = document.createElement('div');
297
+ titleEl.className = 'jux-list-item-title';
298
+ titleEl.textContent = item.title;
299
+ contentEl.appendChild(titleEl);
300
+ }
301
+
302
+ if (item.body) {
303
+ const bodyEl = document.createElement('div');
304
+ bodyEl.className = 'jux-list-item-body';
305
+ bodyEl.textContent = item.body;
306
+ contentEl.appendChild(bodyEl);
307
+ }
308
+
309
+ itemEl.appendChild(contentEl);
310
+
311
+ // Metadata
312
+ if (item.metadata) {
313
+ const metadataEl = document.createElement('span');
314
+ metadataEl.className = 'jux-list-item-metadata';
315
+ metadataEl.textContent = item.metadata;
316
+ itemEl.appendChild(metadataEl);
317
+ }
318
+
319
+ listContainer.appendChild(itemEl);
320
+
321
+ // Event binding - click handlers
322
+ itemEl.addEventListener('click', (e) => {
323
+ if (selectable) {
324
+ this.select(index);
325
+ }
326
+
327
+ this.emit('itemClick', { item, index, event: e });
328
+
329
+ if (this._onItemClick) {
330
+ this._onItemClick(item, index, e);
331
+ }
332
+ });
333
+
334
+ if (this._onItemDoubleClick) {
335
+ itemEl.addEventListener('dblclick', (e) => {
336
+ this.emit('itemDoubleClick', { item, index, event: e });
337
+ this._onItemDoubleClick!(item, index, e);
338
+ });
339
+ }
340
+ });
341
+
342
+ wrapper.appendChild(listContainer);
343
+ this.container.appendChild(wrapper);
344
+ }
345
+
346
+ /* -------------------------
347
+ * Render
348
+ * ------------------------- */
349
+
350
+ render(targetId?: string): this {
351
+ let container: HTMLElement;
352
+
353
+ if (targetId) {
354
+ const target = document.querySelector(targetId);
355
+ if (!target || !(target instanceof HTMLElement)) {
356
+ throw new Error(`List: Target element "${targetId}" not found`);
357
+ }
358
+ container = target;
359
+ } else {
360
+ container = getOrCreateContainer(this._componentId) as HTMLElement;
361
+ }
362
+
363
+ this.container = container;
364
+ this.container.innerHTML = '';
365
+
366
+ this._renderContent();
367
+
368
+ return this;
369
+ }
370
+
371
+ /**
372
+ * Render to another Jux component's container
373
+ */
374
+ renderTo(juxComponent: any): this {
375
+ if (!juxComponent || typeof juxComponent !== 'object') {
376
+ throw new Error('List.renderTo: Invalid component - not an object');
377
+ }
378
+
379
+ if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
380
+ throw new Error('List.renderTo: Invalid component - missing _componentId (not a Jux component)');
381
+ }
382
+
383
+ return this.render(`#${juxComponent._componentId}`);
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Factory helper
389
+ */
390
+ export function list(componentId: string, options: ListOptions = {}): List {
391
+ return new List(componentId, options);
392
+ }