juxscript 1.0.3 → 1.0.5

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 (73) 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/barchart.ts +1248 -0
  7. package/lib/components/button.ts +188 -53
  8. package/lib/components/card.ts +75 -61
  9. package/lib/components/chart.ts +17 -15
  10. package/lib/components/checkbox.ts +199 -0
  11. package/lib/components/code.ts +66 -152
  12. package/lib/components/container.ts +104 -208
  13. package/lib/components/data.ts +1 -3
  14. package/lib/components/datepicker.ts +226 -0
  15. package/lib/components/dialog.ts +258 -0
  16. package/lib/components/docs-data.json +1969 -423
  17. package/lib/components/dropdown.ts +244 -0
  18. package/lib/components/element.ts +271 -0
  19. package/lib/components/fileupload.ts +319 -0
  20. package/lib/components/footer.ts +37 -18
  21. package/lib/components/header.ts +53 -33
  22. package/lib/components/heading.ts +119 -0
  23. package/lib/components/helpers.ts +34 -0
  24. package/lib/components/hero.ts +57 -31
  25. package/lib/components/include.ts +292 -0
  26. package/lib/components/input.ts +508 -77
  27. package/lib/components/layout.ts +144 -18
  28. package/lib/components/list.ts +83 -74
  29. package/lib/components/loading.ts +263 -0
  30. package/lib/components/main.ts +43 -17
  31. package/lib/components/menu.ts +108 -24
  32. package/lib/components/modal.ts +50 -21
  33. package/lib/components/nav.ts +60 -18
  34. package/lib/components/paragraph.ts +111 -0
  35. package/lib/components/progress.ts +276 -0
  36. package/lib/components/radio.ts +236 -0
  37. package/lib/components/req.ts +300 -0
  38. package/lib/components/script.ts +33 -74
  39. package/lib/components/select.ts +280 -0
  40. package/lib/components/sidebar.ts +87 -37
  41. package/lib/components/style.ts +47 -70
  42. package/lib/components/switch.ts +261 -0
  43. package/lib/components/table.ts +47 -24
  44. package/lib/components/tabs.ts +105 -63
  45. package/lib/components/theme-toggle.ts +361 -0
  46. package/lib/components/token-calculator.ts +380 -0
  47. package/lib/components/tooltip.ts +244 -0
  48. package/lib/components/view.ts +36 -20
  49. package/lib/components/write.ts +284 -0
  50. package/lib/globals.d.ts +21 -0
  51. package/lib/jux.ts +178 -68
  52. package/lib/presets/notion.css +521 -0
  53. package/lib/presets/notion.jux +27 -0
  54. package/lib/reactivity/state.ts +364 -0
  55. package/lib/themes/charts.js +126 -0
  56. package/machinery/compiler.js +126 -38
  57. package/machinery/generators/html.js +2 -3
  58. package/machinery/server.js +2 -2
  59. package/package.json +29 -3
  60. package/lib/components/import.ts +0 -430
  61. package/lib/components/node.ts +0 -200
  62. package/lib/components/reactivity.js +0 -104
  63. package/lib/components/theme.ts +0 -97
  64. package/lib/layouts/notion.css +0 -258
  65. package/lib/styles/base-theme.css +0 -186
  66. package/lib/styles/dark-theme.css +0 -144
  67. package/lib/styles/light-theme.css +0 -144
  68. package/lib/styles/tokens/dark.css +0 -86
  69. package/lib/styles/tokens/light.css +0 -86
  70. package/lib/templates/index.juxt +0 -33
  71. package/lib/themes/dark.css +0 -86
  72. package/lib/themes/light.css +0 -86
  73. /package/lib/{styles → presets}/global.css +0 -0
@@ -0,0 +1,319 @@
1
+ import { getOrCreateContainer } from './helpers.js';
2
+
3
+ /**
4
+ * FileUpload component options
5
+ */
6
+ export interface FileUploadOptions {
7
+ accept?: string;
8
+ multiple?: boolean;
9
+ maxSize?: number;
10
+ disabled?: boolean;
11
+ text?: string;
12
+ dragText?: string;
13
+ onChange?: (files: FileList) => void;
14
+ onError?: (error: string) => void;
15
+ style?: string;
16
+ class?: string;
17
+ }
18
+
19
+ /**
20
+ * FileUpload component state
21
+ */
22
+ type FileUploadState = {
23
+ accept: string;
24
+ multiple: boolean;
25
+ maxSize: number;
26
+ disabled: boolean;
27
+ text: string;
28
+ dragText: string;
29
+ style: string;
30
+ class: string;
31
+ files: File[];
32
+ };
33
+
34
+ /**
35
+ * FileUpload component - Drag & drop file input
36
+ *
37
+ * Usage:
38
+ * jux.fileupload('docs', {
39
+ * accept: '.pdf,.doc,.docx',
40
+ * multiple: true,
41
+ * maxSize: 5242880, // 5MB
42
+ * text: 'Click to upload or drag and drop',
43
+ * onChange: (files) => console.log(files),
44
+ * onError: (err) => console.error(err)
45
+ * }).render('#form');
46
+ */
47
+ export class FileUpload {
48
+ state: FileUploadState;
49
+ container: HTMLElement | null = null;
50
+ _id: string;
51
+ id: string;
52
+ private _onChange?: (files: FileList) => void;
53
+ private _onError?: (error: string) => void;
54
+
55
+ constructor(id: string, options: FileUploadOptions = {}) {
56
+ this._id = id;
57
+ this.id = id;
58
+ this._onChange = options.onChange;
59
+ this._onError = options.onError;
60
+
61
+ this.state = {
62
+ accept: options.accept ?? '*',
63
+ multiple: options.multiple ?? false,
64
+ maxSize: options.maxSize ?? 10485760, // 10MB default
65
+ disabled: options.disabled ?? false,
66
+ text: options.text ?? 'Click to upload or drag and drop',
67
+ dragText: options.dragText ?? 'Drop files here',
68
+ style: options.style ?? '',
69
+ class: options.class ?? '',
70
+ files: []
71
+ };
72
+ }
73
+
74
+ /* -------------------------
75
+ * Fluent API
76
+ * ------------------------- */
77
+
78
+ accept(value: string): this {
79
+ this.state.accept = value;
80
+ return this;
81
+ }
82
+
83
+ multiple(value: boolean): this {
84
+ this.state.multiple = value;
85
+ return this;
86
+ }
87
+
88
+ maxSize(value: number): this {
89
+ this.state.maxSize = value;
90
+ return this;
91
+ }
92
+
93
+ disabled(value: boolean): this {
94
+ this.state.disabled = value;
95
+ return this;
96
+ }
97
+
98
+ text(value: string): this {
99
+ this.state.text = value;
100
+ return this;
101
+ }
102
+
103
+ dragText(value: string): this {
104
+ this.state.dragText = value;
105
+ return this;
106
+ }
107
+
108
+ style(value: string): this {
109
+ this.state.style = value;
110
+ return this;
111
+ }
112
+
113
+ class(value: string): this {
114
+ this.state.class = value;
115
+ return this;
116
+ }
117
+
118
+ onChange(handler: (files: FileList) => void): this {
119
+ this._onChange = handler;
120
+ return this;
121
+ }
122
+
123
+ onError(handler: (error: string) => void): this {
124
+ this._onError = handler;
125
+ return this;
126
+ }
127
+
128
+ /* -------------------------
129
+ * Helpers
130
+ * ------------------------- */
131
+
132
+ private _validateFiles(files: FileList): boolean {
133
+ for (let i = 0; i < files.length; i++) {
134
+ const file = files[i];
135
+ if (file.size > this.state.maxSize) {
136
+ const maxMB = Math.round(this.state.maxSize / 1048576);
137
+ const error = `File "${file.name}" exceeds maximum size of ${maxMB}MB`;
138
+ if (this._onError) {
139
+ this._onError(error);
140
+ }
141
+ return false;
142
+ }
143
+ }
144
+ return true;
145
+ }
146
+
147
+ private _handleFiles(files: FileList): void {
148
+ if (this._validateFiles(files)) {
149
+ this.state.files = Array.from(files);
150
+ this._updateFileList();
151
+ if (this._onChange) {
152
+ this._onChange(files);
153
+ }
154
+ }
155
+ }
156
+
157
+ private _updateFileList(): void {
158
+ const fileList = document.getElementById(`${this._id}-list`);
159
+ if (fileList) {
160
+ fileList.innerHTML = '';
161
+ this.state.files.forEach((file, index) => {
162
+ const item = document.createElement('div');
163
+ item.className = 'jux-fileupload-file';
164
+
165
+ const name = document.createElement('span');
166
+ name.className = 'jux-fileupload-file-name';
167
+ name.textContent = file.name;
168
+
169
+ const size = document.createElement('span');
170
+ size.className = 'jux-fileupload-file-size';
171
+ const sizeKB = Math.round(file.size / 1024);
172
+ size.textContent = `${sizeKB} KB`;
173
+
174
+ const removeBtn = document.createElement('button');
175
+ removeBtn.className = 'jux-fileupload-file-remove';
176
+ removeBtn.textContent = '×';
177
+ removeBtn.addEventListener('click', () => {
178
+ this.state.files.splice(index, 1);
179
+ this._updateFileList();
180
+ });
181
+
182
+ item.appendChild(name);
183
+ item.appendChild(size);
184
+ item.appendChild(removeBtn);
185
+ fileList.appendChild(item);
186
+ });
187
+ }
188
+ }
189
+
190
+ getFiles(): File[] {
191
+ return this.state.files;
192
+ }
193
+
194
+ clear(): void {
195
+ this.state.files = [];
196
+ this._updateFileList();
197
+ const input = document.getElementById(`${this._id}-input`) as HTMLInputElement;
198
+ if (input) {
199
+ input.value = '';
200
+ }
201
+ }
202
+
203
+ /* -------------------------
204
+ * Render
205
+ * ------------------------- */
206
+
207
+ render(targetId?: string): this {
208
+ let container: HTMLElement;
209
+
210
+ if (targetId) {
211
+ const target = document.querySelector(targetId);
212
+ if (!target || !(target instanceof HTMLElement)) {
213
+ throw new Error(`FileUpload: Target element "${targetId}" not found`);
214
+ }
215
+ container = target;
216
+ } else {
217
+ container = getOrCreateContainer(this._id);
218
+ }
219
+
220
+ this.container = container;
221
+ const { accept, multiple, disabled, text, dragText, style, class: className } = this.state;
222
+
223
+ const wrapper = document.createElement('div');
224
+ wrapper.className = 'jux-fileupload';
225
+ wrapper.id = this._id;
226
+
227
+ if (className) {
228
+ wrapper.className += ` ${className}`;
229
+ }
230
+
231
+ if (style) {
232
+ wrapper.setAttribute('style', style);
233
+ }
234
+
235
+ // Hidden file input
236
+ const input = document.createElement('input');
237
+ input.type = 'file';
238
+ input.className = 'jux-fileupload-input';
239
+ input.id = `${this._id}-input`;
240
+ input.accept = accept;
241
+ input.multiple = multiple;
242
+ input.disabled = disabled;
243
+
244
+ input.addEventListener('change', (e) => {
245
+ const target = e.target as HTMLInputElement;
246
+ if (target.files && target.files.length > 0) {
247
+ this._handleFiles(target.files);
248
+ }
249
+ });
250
+
251
+ // Drop zone
252
+ const dropzone = document.createElement('div');
253
+ dropzone.className = 'jux-fileupload-dropzone';
254
+
255
+ const icon = document.createElement('div');
256
+ icon.className = 'jux-fileupload-icon';
257
+ icon.textContent = '📁';
258
+
259
+ const textEl = document.createElement('div');
260
+ textEl.className = 'jux-fileupload-text';
261
+ textEl.textContent = text;
262
+
263
+ dropzone.appendChild(icon);
264
+ dropzone.appendChild(textEl);
265
+
266
+ // Click to open file dialog
267
+ dropzone.addEventListener('click', () => {
268
+ if (!disabled) {
269
+ input.click();
270
+ }
271
+ });
272
+
273
+ // Drag & drop
274
+ dropzone.addEventListener('dragover', (e) => {
275
+ e.preventDefault();
276
+ if (!disabled) {
277
+ dropzone.classList.add('jux-fileupload-dropzone-active');
278
+ textEl.textContent = dragText;
279
+ }
280
+ });
281
+
282
+ dropzone.addEventListener('dragleave', () => {
283
+ dropzone.classList.remove('jux-fileupload-dropzone-active');
284
+ textEl.textContent = text;
285
+ });
286
+
287
+ dropzone.addEventListener('drop', (e) => {
288
+ e.preventDefault();
289
+ dropzone.classList.remove('jux-fileupload-dropzone-active');
290
+ textEl.textContent = text;
291
+
292
+ if (!disabled && e.dataTransfer?.files) {
293
+ this._handleFiles(e.dataTransfer.files);
294
+ }
295
+ });
296
+
297
+ // File list
298
+ const fileList = document.createElement('div');
299
+ fileList.className = 'jux-fileupload-list';
300
+ fileList.id = `${this._id}-list`;
301
+
302
+ wrapper.appendChild(input);
303
+ wrapper.appendChild(dropzone);
304
+ wrapper.appendChild(fileList);
305
+ container.appendChild(wrapper);
306
+ return this;
307
+ }
308
+
309
+ renderTo(juxComponent: any): this {
310
+ if (!juxComponent?._id) {
311
+ throw new Error('FileUpload.renderTo: Invalid component');
312
+ }
313
+ return this.render(`#${juxComponent._id}`);
314
+ }
315
+ }
316
+
317
+ export function fileupload(id: string, options: FileUploadOptions = {}): FileUpload {
318
+ return new FileUpload(id, options);
319
+ }
@@ -1,4 +1,4 @@
1
- import { Reactive, getOrCreateContainer } from './reactivity.js';
1
+ import { getOrCreateContainer } from './helpers.js';
2
2
 
3
3
  /**
4
4
  * Footer component options
@@ -7,6 +7,8 @@ export interface FooterOptions {
7
7
  content?: string;
8
8
  copyright?: string;
9
9
  links?: Array<{ label: string; href: string }>;
10
+ class?: string;
11
+ style?: string;
10
12
  }
11
13
 
12
14
  /**
@@ -16,6 +18,8 @@ type FooterState = {
16
18
  content: string;
17
19
  copyright: string;
18
20
  links: Array<{ label: string; href: string }>;
21
+ class: string;
22
+ style: string;
19
23
  };
20
24
 
21
25
  /**
@@ -31,19 +35,23 @@ type FooterState = {
31
35
  * });
32
36
  * footer.render('#appfooter');
33
37
  */
34
- export class Footer extends Reactive {
35
- state!: FooterState;
38
+ export class Footer {
39
+ state: FooterState;
36
40
  container: HTMLElement | null = null;
41
+ _id: string;
42
+ id: string;
37
43
 
38
- constructor(componentId: string, options: FooterOptions = {}) {
39
- super();
40
- this._setComponentId(componentId);
44
+ constructor(id: string, options: FooterOptions = {}) {
45
+ this._id = id;
46
+ this.id = id;
41
47
 
42
- this.state = this._createReactiveState({
48
+ this.state = {
43
49
  content: options.content ?? '',
44
50
  copyright: options.copyright ?? '',
45
- links: options.links ?? []
46
- }) as FooterState;
51
+ links: options.links ?? [],
52
+ class: options.class ?? '',
53
+ style: options.style ?? ''
54
+ };
47
55
  }
48
56
 
49
57
  /* -------------------------
@@ -65,6 +73,16 @@ export class Footer extends Reactive {
65
73
  return this;
66
74
  }
67
75
 
76
+ class(value: string): this {
77
+ this.state.class = value;
78
+ return this;
79
+ }
80
+
81
+ style(value: string): this {
82
+ this.state.style = value;
83
+ return this;
84
+ }
85
+
68
86
  /* -------------------------
69
87
  * Render
70
88
  * ------------------------- */
@@ -79,15 +97,16 @@ export class Footer extends Reactive {
79
97
  }
80
98
  container = target;
81
99
  } else {
82
- container = getOrCreateContainer(this._componentId) as HTMLElement;
100
+ container = getOrCreateContainer(this._id);
83
101
  }
84
102
 
85
103
  this.container = container;
86
- const { content, copyright, links } = this.state;
104
+ const { content, copyright, links, class: className, style } = this.state;
87
105
 
88
106
  const footer = document.createElement('footer');
89
- footer.className = 'jux-footer';
90
- footer.id = this._componentId;
107
+ footer.className = `jux-footer ${className}`.trim();
108
+ footer.style.cssText = style;
109
+ footer.id = this._id;
91
110
 
92
111
  if (content) {
93
112
  const contentEl = document.createElement('div');
@@ -130,17 +149,17 @@ export class Footer extends Reactive {
130
149
  throw new Error('Footer.renderTo: Invalid component - not an object');
131
150
  }
132
151
 
133
- if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
134
- throw new Error('Footer.renderTo: Invalid component - missing _componentId (not a Jux component)');
152
+ if (!juxComponent._id || typeof juxComponent._id !== 'string') {
153
+ throw new Error('Footer.renderTo: Invalid component - missing _id (not a Jux component)');
135
154
  }
136
155
 
137
- return this.render(`#${juxComponent._componentId}`);
156
+ return this.render(`#${juxComponent._id}`);
138
157
  }
139
158
  }
140
159
 
141
160
  /**
142
161
  * Factory helper
143
162
  */
144
- export function footer(componentId: string, options: FooterOptions = {}): Footer {
145
- return new Footer(componentId, options);
163
+ export function footer(id: string, options: FooterOptions = {}): Footer {
164
+ return new Footer(id, options);
146
165
  }
@@ -1,4 +1,4 @@
1
- import { Reactive, getOrCreateContainer } from './reactivity.js';
1
+ import { getOrCreateContainer } from './helpers.js';
2
2
 
3
3
  /**
4
4
  * Header component options
@@ -8,6 +8,8 @@ export interface HeaderOptions {
8
8
  logo?: string;
9
9
  navigation?: Array<{ label: string; href: string }>;
10
10
  sticky?: boolean;
11
+ style?: string;
12
+ class?: string;
11
13
  }
12
14
 
13
15
  /**
@@ -18,6 +20,8 @@ type HeaderState = {
18
20
  logo: string;
19
21
  navigation: Array<{ label: string; href: string }>;
20
22
  sticky: boolean;
23
+ style: string;
24
+ class: string;
21
25
  };
22
26
 
23
27
  /**
@@ -33,20 +37,24 @@ type HeaderState = {
33
37
  * });
34
38
  * header.render('#appheader');
35
39
  */
36
- export class Header extends Reactive {
37
- state!: HeaderState;
40
+ export class Header {
41
+ state: HeaderState;
38
42
  container: HTMLElement | null = null;
39
-
40
- constructor(componentId: string, options: HeaderOptions = {}) {
41
- super();
42
- this._setComponentId(componentId);
43
-
44
- this.state = this._createReactiveState({
43
+ _id: string;
44
+ id: string;
45
+
46
+ constructor(id: string, options: HeaderOptions = {}) {
47
+ this._id = id;
48
+ this.id = id;
49
+
50
+ this.state = {
45
51
  title: options.title ?? '',
46
52
  logo: options.logo ?? '',
47
53
  navigation: options.navigation ?? [],
48
- sticky: options.sticky ?? true
49
- }) as HeaderState;
54
+ sticky: options.sticky ?? true,
55
+ style: options.style ?? '',
56
+ class: options.class ?? ''
57
+ };
50
58
  }
51
59
 
52
60
  /* -------------------------
@@ -73,13 +81,22 @@ export class Header extends Reactive {
73
81
  return this;
74
82
  }
75
83
 
84
+ class(value: string): this {
85
+ this.state.class = value;
86
+ return this;
87
+ }
88
+
89
+ style(value: string): this {
90
+ this.state.style = value;
91
+ return this;
92
+ }
76
93
  /* -------------------------
77
94
  * Render
78
95
  * ------------------------- */
79
96
 
80
97
  render(targetId?: string): this {
81
98
  let container: HTMLElement;
82
-
99
+
83
100
  if (targetId) {
84
101
  const target = document.querySelector(targetId);
85
102
  if (!target || !(target instanceof HTMLElement)) {
@@ -87,47 +104,50 @@ export class Header extends Reactive {
87
104
  }
88
105
  container = target;
89
106
  } else {
90
- container = getOrCreateContainer(this._componentId) as HTMLElement;
107
+ container = getOrCreateContainer(this._id);
91
108
  }
92
-
109
+
93
110
  this.container = container;
94
- const { title, logo, navigation, sticky } = this.state;
95
-
111
+ const { title, logo, navigation, sticky, style, class: className } = this.state;
112
+
96
113
  const header = document.createElement('header');
97
114
  header.className = 'jux-header';
98
- header.id = this._componentId;
99
-
115
+
116
+ header.id = this._id;
117
+ header.style.cssText = style;
118
+ header.className = `jux-header ${className}`.trim();
119
+
100
120
  if (sticky) {
101
121
  header.classList.add('jux-header-sticky');
102
122
  }
103
-
123
+
104
124
  // Logo section
105
125
  if (logo || title) {
106
126
  const logoSection = document.createElement('div');
107
127
  logoSection.className = 'jux-header-logo';
108
-
128
+
109
129
  if (logo) {
110
130
  const logoImg = document.createElement('img');
111
131
  logoImg.src = logo;
112
132
  logoImg.alt = title || 'Logo';
113
133
  logoSection.appendChild(logoImg);
114
134
  }
115
-
135
+
116
136
  if (title) {
117
137
  const titleEl = document.createElement('span');
118
138
  titleEl.className = 'jux-header-title';
119
139
  titleEl.textContent = title;
120
140
  logoSection.appendChild(titleEl);
121
141
  }
122
-
142
+
123
143
  header.appendChild(logoSection);
124
144
  }
125
-
145
+
126
146
  // Navigation
127
147
  if (navigation.length > 0) {
128
148
  const nav = document.createElement('nav');
129
149
  nav.className = 'jux-header-nav';
130
-
150
+
131
151
  navigation.forEach(item => {
132
152
  const link = document.createElement('a');
133
153
  link.className = 'jux-header-nav-item';
@@ -135,10 +155,10 @@ export class Header extends Reactive {
135
155
  link.textContent = item.label;
136
156
  nav.appendChild(link);
137
157
  });
138
-
158
+
139
159
  header.appendChild(nav);
140
160
  }
141
-
161
+
142
162
  container.appendChild(header);
143
163
  return this;
144
164
  }
@@ -150,18 +170,18 @@ export class Header extends Reactive {
150
170
  if (!juxComponent || typeof juxComponent !== 'object') {
151
171
  throw new Error('Header.renderTo: Invalid component - not an object');
152
172
  }
153
-
154
- if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
155
- throw new Error('Header.renderTo: Invalid component - missing _componentId (not a Jux component)');
173
+
174
+ if (!juxComponent._id || typeof juxComponent._id !== 'string') {
175
+ throw new Error('Header.renderTo: Invalid component - missing _id (not a Jux component)');
156
176
  }
157
-
158
- return this.render(`#${juxComponent._componentId}`);
177
+
178
+ return this.render(`#${juxComponent._id}`);
159
179
  }
160
180
  }
161
181
 
162
182
  /**
163
183
  * Factory helper
164
184
  */
165
- export function header(componentId: string, options: HeaderOptions = {}): Header {
166
- return new Header(componentId, options);
185
+ export function header(id: string, options: HeaderOptions = {}): Header {
186
+ return new Header(id, options);
167
187
  }