juxscript 1.0.20 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +121 -72
- package/lib/components/alert.ts +143 -92
- package/lib/components/badge.ts +93 -94
- package/lib/components/base/BaseComponent.ts +397 -0
- package/lib/components/base/FormInput.ts +322 -0
- package/lib/components/button.ts +40 -131
- package/lib/components/card.ts +57 -79
- package/lib/components/charts/areachart.ts +315 -0
- package/lib/components/charts/barchart.ts +421 -0
- package/lib/components/charts/doughnutchart.ts +263 -0
- package/lib/components/charts/lib/BaseChart.ts +402 -0
- package/lib/components/{chart-types.ts → charts/lib/chart-types.ts} +1 -1
- package/lib/components/{chart-utils.ts → charts/lib/chart-utils.ts} +1 -1
- package/lib/components/{chart.ts → charts/lib/chart.ts} +3 -3
- package/lib/components/checkbox.ts +255 -204
- package/lib/components/code.ts +31 -78
- package/lib/components/container.ts +113 -130
- package/lib/components/data.ts +37 -5
- package/lib/components/datepicker.ts +180 -147
- package/lib/components/dialog.ts +218 -221
- package/lib/components/divider.ts +63 -87
- package/lib/components/docs-data.json +498 -2404
- package/lib/components/dropdown.ts +191 -236
- package/lib/components/element.ts +196 -145
- package/lib/components/fileupload.ts +253 -167
- package/lib/components/guard.ts +92 -0
- package/lib/components/heading.ts +31 -97
- package/lib/components/helpers.ts +13 -6
- package/lib/components/hero.ts +51 -114
- package/lib/components/icon.ts +33 -120
- package/lib/components/icons.ts +2 -1
- package/lib/components/include.ts +76 -3
- package/lib/components/input.ts +155 -407
- package/lib/components/kpicard.ts +16 -16
- package/lib/components/list.ts +358 -261
- package/lib/components/loading.ts +142 -211
- package/lib/components/menu.ts +63 -152
- package/lib/components/modal.ts +42 -129
- package/lib/components/nav.ts +79 -101
- package/lib/components/paragraph.ts +38 -102
- package/lib/components/progress.ts +108 -166
- package/lib/components/radio.ts +283 -234
- package/lib/components/script.ts +19 -87
- package/lib/components/select.ts +189 -199
- package/lib/components/sidebar.ts +110 -141
- package/lib/components/style.ts +19 -82
- package/lib/components/switch.ts +254 -183
- package/lib/components/table.ts +1078 -208
- package/lib/components/tabs.ts +42 -106
- package/lib/components/theme-toggle.ts +73 -165
- package/lib/components/tooltip.ts +85 -316
- package/lib/components/write.ts +108 -127
- package/lib/jux.ts +67 -41
- package/machinery/build.js +466 -0
- package/machinery/compiler.js +354 -105
- package/machinery/server.js +23 -100
- package/machinery/watcher.js +153 -130
- package/package.json +1 -1
- package/presets/base.css +1166 -0
- package/presets/notion.css +2 -1975
- package/lib/adapters/base-adapter.js +0 -35
- package/lib/adapters/index.js +0 -33
- package/lib/adapters/mysql-adapter.js +0 -65
- package/lib/adapters/postgres-adapter.js +0 -70
- package/lib/adapters/sqlite-adapter.js +0 -56
- package/lib/components/areachart.ts +0 -1128
- package/lib/components/areachartsmooth.ts +0 -1380
- package/lib/components/barchart.ts +0 -1322
- package/lib/components/doughnutchart.ts +0 -1259
- package/lib/components/footer.ts +0 -165
- package/lib/components/header.ts +0 -187
- package/lib/components/layout.ts +0 -239
- package/lib/components/main.ts +0 -137
- package/lib/layouts/default.jux +0 -8
- package/lib/layouts/figma.jux +0 -0
- /package/lib/{themes → components/charts/lib}/charts.js +0 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { FormInput, FormInputState } from './base/FormInput.js';
|
|
2
|
+
import { renderIcon } from './icons.js';
|
|
3
|
+
|
|
4
|
+
// Event definitions
|
|
5
|
+
const TRIGGER_EVENTS = [] as const;
|
|
6
|
+
const CALLBACK_EVENTS = ['change', 'filesSelected', 'clear'] as const;
|
|
3
7
|
|
|
4
8
|
export interface FileUploadOptions {
|
|
5
9
|
label?: string;
|
|
@@ -7,61 +11,62 @@ export interface FileUploadOptions {
|
|
|
7
11
|
multiple?: boolean;
|
|
8
12
|
disabled?: boolean;
|
|
9
13
|
name?: string;
|
|
14
|
+
icon?: string;
|
|
15
|
+
required?: boolean;
|
|
10
16
|
style?: string;
|
|
11
17
|
class?: string;
|
|
18
|
+
onValidate?: (files: File[]) => boolean | string;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
|
-
|
|
21
|
+
interface FileUploadState extends FormInputState {
|
|
15
22
|
files: File[];
|
|
16
|
-
label: string;
|
|
17
23
|
accept: string;
|
|
18
24
|
multiple: boolean;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
style: string;
|
|
22
|
-
class: string;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export class FileUpload {
|
|
26
|
-
state: FileUploadState;
|
|
27
|
-
container: HTMLElement | null = null;
|
|
28
|
-
_id: string;
|
|
29
|
-
id: string;
|
|
30
|
-
|
|
31
|
-
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
32
|
-
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
33
|
-
private _syncBindings: Array<{
|
|
34
|
-
property: string,
|
|
35
|
-
stateObj: State<any>,
|
|
36
|
-
toState?: Function,
|
|
37
|
-
toComponent?: Function
|
|
38
|
-
}> = [];
|
|
25
|
+
icon: string;
|
|
26
|
+
}
|
|
39
27
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
this.id = id;
|
|
28
|
+
export class FileUpload extends FormInput<FileUploadState> {
|
|
29
|
+
private _fileListElement: HTMLElement | null = null;
|
|
43
30
|
|
|
44
|
-
|
|
31
|
+
constructor(id: string, options: FileUploadOptions = {}) {
|
|
32
|
+
super(id, {
|
|
45
33
|
files: [],
|
|
46
|
-
label: options.label ?? '',
|
|
47
34
|
accept: options.accept ?? '',
|
|
48
35
|
multiple: options.multiple ?? false,
|
|
36
|
+
icon: options.icon ?? 'upload',
|
|
37
|
+
label: options.label ?? '',
|
|
38
|
+
required: options.required ?? false,
|
|
49
39
|
disabled: options.disabled ?? false,
|
|
50
40
|
name: options.name ?? id,
|
|
51
41
|
style: options.style ?? '',
|
|
52
|
-
class: options.class ?? ''
|
|
53
|
-
|
|
42
|
+
class: options.class ?? '',
|
|
43
|
+
errorMessage: undefined
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (options.onValidate) {
|
|
47
|
+
this._onValidate = options.onValidate;
|
|
48
|
+
}
|
|
54
49
|
}
|
|
55
50
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
protected getTriggerEvents(): readonly string[] {
|
|
52
|
+
return TRIGGER_EVENTS;
|
|
53
|
+
}
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return this;
|
|
55
|
+
protected getCallbackEvents(): readonly string[] {
|
|
56
|
+
return CALLBACK_EVENTS;
|
|
63
57
|
}
|
|
64
58
|
|
|
59
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
60
|
+
* FLUENT API
|
|
61
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
62
|
+
|
|
63
|
+
// ✅ Inherited from FormInput/BaseComponent:
|
|
64
|
+
// - label(), required(), name(), onValidate()
|
|
65
|
+
// - validate(), isValid()
|
|
66
|
+
// - style(), class()
|
|
67
|
+
// - bind(), sync(), renderTo()
|
|
68
|
+
// - disabled(), enable(), disable()
|
|
69
|
+
|
|
65
70
|
accept(value: string): this {
|
|
66
71
|
this.state.accept = value;
|
|
67
72
|
return this;
|
|
@@ -72,194 +77,224 @@ export class FileUpload {
|
|
|
72
77
|
return this;
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
this.state.
|
|
77
|
-
this._updateElement();
|
|
80
|
+
icon(value: string): this {
|
|
81
|
+
this.state.icon = value;
|
|
78
82
|
return this;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
this.state.
|
|
85
|
+
clear(): this {
|
|
86
|
+
this.state.files = [];
|
|
87
|
+
if (this._inputElement) {
|
|
88
|
+
(this._inputElement as HTMLInputElement).value = '';
|
|
89
|
+
}
|
|
90
|
+
if (this._fileListElement) {
|
|
91
|
+
this._updateFileList([]);
|
|
92
|
+
}
|
|
93
|
+
// 🎯 Fire the clear callback event
|
|
94
|
+
this._triggerCallback('clear');
|
|
83
95
|
return this;
|
|
84
96
|
}
|
|
85
97
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
98
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
99
|
+
* FORM INPUT IMPLEMENTATION
|
|
100
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
90
101
|
|
|
91
|
-
|
|
92
|
-
this.state.
|
|
93
|
-
return this;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
bind(event: string, handler: Function): this {
|
|
97
|
-
this._bindings.push({ event, handler });
|
|
98
|
-
return this;
|
|
102
|
+
getValue(): File[] {
|
|
103
|
+
return this.state.files;
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
setValue(files: File[]): this {
|
|
107
|
+
this.state.files = files;
|
|
108
|
+
if (this._fileListElement) {
|
|
109
|
+
this._updateFileList(files);
|
|
104
110
|
}
|
|
105
|
-
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
106
111
|
return this;
|
|
107
112
|
}
|
|
108
113
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
getFiles(): File[] {
|
|
115
|
+
return this.getValue();
|
|
116
|
+
}
|
|
112
117
|
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
const button = document.getElementById(`${this._id}-button`) as HTMLButtonElement;
|
|
118
|
+
protected _validateValue(files: File[]): boolean | string {
|
|
119
|
+
const { required } = this.state;
|
|
116
120
|
|
|
117
|
-
if (
|
|
118
|
-
|
|
121
|
+
if (required && files.length === 0) {
|
|
122
|
+
return 'Please select at least one file';
|
|
119
123
|
}
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
|
|
125
|
+
if (this._onValidate) {
|
|
126
|
+
const result = this._onValidate(files);
|
|
127
|
+
if (result !== true) {
|
|
128
|
+
return result || 'Invalid files';
|
|
129
|
+
}
|
|
122
130
|
}
|
|
123
|
-
}
|
|
124
131
|
|
|
125
|
-
|
|
126
|
-
return this.state.files;
|
|
132
|
+
return true;
|
|
127
133
|
}
|
|
128
134
|
|
|
129
|
-
|
|
130
|
-
this.state
|
|
131
|
-
const input = document.getElementById(`${this._id}-input`) as HTMLInputElement;
|
|
132
|
-
const fileList = document.getElementById(`${this._id}-list`);
|
|
135
|
+
protected _buildInputElement(): HTMLElement {
|
|
136
|
+
const { accept, multiple, required, disabled, name } = this.state;
|
|
133
137
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
const input = document.createElement('input');
|
|
139
|
+
input.type = 'file';
|
|
140
|
+
input.className = 'jux-fileupload-input';
|
|
141
|
+
input.id = `${this._id}-input`;
|
|
142
|
+
input.name = name;
|
|
143
|
+
input.required = required;
|
|
144
|
+
input.disabled = disabled;
|
|
145
|
+
|
|
146
|
+
if (accept) input.accept = accept;
|
|
147
|
+
if (multiple) input.multiple = multiple;
|
|
148
|
+
|
|
149
|
+
return input;
|
|
140
150
|
}
|
|
141
151
|
|
|
142
|
-
/*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
152
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
153
|
+
* RENDER
|
|
154
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
145
155
|
|
|
146
156
|
render(targetId?: string): this {
|
|
147
|
-
|
|
148
|
-
let container: HTMLElement;
|
|
149
|
-
if (targetId) {
|
|
150
|
-
const target = document.querySelector(targetId);
|
|
151
|
-
if (!target || !(target instanceof HTMLElement)) {
|
|
152
|
-
throw new Error(`FileUpload: Target "${targetId}" not found`);
|
|
153
|
-
}
|
|
154
|
-
container = target;
|
|
155
|
-
} else {
|
|
156
|
-
container = getOrCreateContainer(this._id);
|
|
157
|
-
}
|
|
158
|
-
this.container = container;
|
|
157
|
+
const container = this._setupContainer(targetId);
|
|
159
158
|
|
|
160
|
-
|
|
161
|
-
const { label, accept, multiple, disabled, name, style, class: className } = this.state;
|
|
162
|
-
const hasFilesSync = this._syncBindings.some(b => b.property === 'files');
|
|
159
|
+
const { icon, style, class: className } = this.state;
|
|
163
160
|
|
|
164
|
-
//
|
|
161
|
+
// Build wrapper
|
|
165
162
|
const wrapper = document.createElement('div');
|
|
166
|
-
wrapper.className = 'jux-fileupload';
|
|
163
|
+
wrapper.className = 'jux-input jux-fileupload';
|
|
167
164
|
wrapper.id = this._id;
|
|
168
165
|
if (className) wrapper.className += ` ${className}`;
|
|
169
166
|
if (style) wrapper.setAttribute('style', style);
|
|
170
167
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
labelEl.textContent = label;
|
|
175
|
-
wrapper.appendChild(labelEl);
|
|
168
|
+
// Label
|
|
169
|
+
if (this.state.label) {
|
|
170
|
+
wrapper.appendChild(this._renderLabel());
|
|
176
171
|
}
|
|
177
172
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
173
|
+
// Hidden file input
|
|
174
|
+
const inputEl = this._buildInputElement() as HTMLInputElement;
|
|
175
|
+
this._inputElement = inputEl;
|
|
176
|
+
wrapper.appendChild(inputEl);
|
|
177
|
+
|
|
178
|
+
// Button container
|
|
179
|
+
const buttonContainer = document.createElement('div');
|
|
180
|
+
buttonContainer.className = 'jux-fileupload-button-container';
|
|
181
|
+
|
|
182
|
+
if (icon) {
|
|
183
|
+
const iconEl = document.createElement('span');
|
|
184
|
+
iconEl.className = 'jux-fileupload-icon';
|
|
185
|
+
iconEl.appendChild(renderIcon(icon));
|
|
186
|
+
buttonContainer.appendChild(iconEl);
|
|
187
|
+
}
|
|
186
188
|
|
|
187
189
|
const button = document.createElement('button');
|
|
188
190
|
button.type = 'button';
|
|
189
191
|
button.className = 'jux-fileupload-button';
|
|
190
|
-
button.id = `${this._id}-button`;
|
|
191
192
|
button.textContent = 'Choose File(s)';
|
|
192
|
-
button.disabled = disabled;
|
|
193
|
+
button.disabled = this.state.disabled;
|
|
193
194
|
|
|
195
|
+
buttonContainer.appendChild(button);
|
|
196
|
+
wrapper.appendChild(buttonContainer);
|
|
197
|
+
|
|
198
|
+
// File list
|
|
194
199
|
const fileList = document.createElement('div');
|
|
195
200
|
fileList.className = 'jux-fileupload-list';
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
wrapper.appendChild(button);
|
|
199
|
-
wrapper.appendChild(input);
|
|
201
|
+
this._fileListElement = fileList;
|
|
200
202
|
wrapper.appendChild(fileList);
|
|
201
203
|
|
|
202
|
-
//
|
|
204
|
+
// Error element
|
|
205
|
+
wrapper.appendChild(this._renderError());
|
|
203
206
|
|
|
204
207
|
// Button click triggers file input
|
|
205
|
-
button.addEventListener('click', () =>
|
|
208
|
+
button.addEventListener('click', () => inputEl.click());
|
|
206
209
|
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
// Wire events
|
|
211
|
+
this._wireStandardEvents(wrapper);
|
|
212
|
+
|
|
213
|
+
// Wire file-specific sync
|
|
214
|
+
const filesSync = this._syncBindings.find(b => b.property === 'files' || b.property === 'value');
|
|
215
|
+
|
|
216
|
+
if (filesSync) {
|
|
217
|
+
const { stateObj, toState, toComponent } = filesSync;
|
|
218
|
+
|
|
219
|
+
const transformToState = toState || ((v: File[]) => v);
|
|
220
|
+
const transformToComponent = toComponent || ((v: any) => v);
|
|
221
|
+
|
|
222
|
+
let isUpdating = false;
|
|
223
|
+
|
|
224
|
+
// State → Component
|
|
225
|
+
stateObj.subscribe((val: any) => {
|
|
226
|
+
if (isUpdating) return;
|
|
227
|
+
const transformed = transformToComponent(val);
|
|
228
|
+
this.setValue(transformed);
|
|
212
229
|
});
|
|
213
|
-
}
|
|
214
230
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
231
|
+
// Component → State
|
|
232
|
+
inputEl.addEventListener('change', () => {
|
|
233
|
+
if (isUpdating) return;
|
|
234
|
+
isUpdating = true;
|
|
219
235
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const transformToComponent = toComponent || ((v: any) => v);
|
|
236
|
+
const files = Array.from(inputEl.files || []);
|
|
237
|
+
this.state.files = files;
|
|
238
|
+
this._updateFileList(files);
|
|
239
|
+
this._clearError();
|
|
225
240
|
|
|
226
|
-
|
|
241
|
+
const transformed = transformToState(files);
|
|
242
|
+
stateObj.set(transformed);
|
|
227
243
|
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const transformed = transformToComponent(val);
|
|
232
|
-
this.state.files = transformed;
|
|
233
|
-
this._updateFileList(fileList, transformed);
|
|
234
|
-
});
|
|
244
|
+
// 🎯 Fire the callback events
|
|
245
|
+
this._triggerCallback('change', files);
|
|
246
|
+
this._triggerCallback('filesSelected', files);
|
|
235
247
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
248
|
+
setTimeout(() => { isUpdating = false; }, 0);
|
|
249
|
+
});
|
|
250
|
+
} else {
|
|
251
|
+
// Default behavior without sync
|
|
252
|
+
inputEl.addEventListener('change', () => {
|
|
253
|
+
const files = Array.from(inputEl.files || []);
|
|
254
|
+
this.state.files = files;
|
|
255
|
+
this._updateFileList(files);
|
|
256
|
+
this._clearError();
|
|
257
|
+
|
|
258
|
+
// 🎯 Fire the callback events
|
|
259
|
+
this._triggerCallback('change', files);
|
|
260
|
+
this._triggerCallback('filesSelected', files);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
240
263
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
264
|
+
// Always add blur validation
|
|
265
|
+
inputEl.addEventListener('blur', () => {
|
|
266
|
+
this.validate();
|
|
267
|
+
});
|
|
244
268
|
|
|
245
|
-
|
|
246
|
-
|
|
269
|
+
// Sync label changes
|
|
270
|
+
const labelSync = this._syncBindings.find(b => b.property === 'label');
|
|
271
|
+
if (labelSync) {
|
|
272
|
+
const transform = labelSync.toComponent || ((v: any) => String(v));
|
|
273
|
+
labelSync.stateObj.subscribe((val: any) => {
|
|
274
|
+
this.label(transform(val));
|
|
275
|
+
});
|
|
276
|
+
}
|
|
247
277
|
|
|
248
|
-
|
|
249
|
-
|
|
278
|
+
container.appendChild(wrapper);
|
|
279
|
+
this._injectFileUploadStyles();
|
|
280
|
+
this._injectFormStyles();
|
|
281
|
+
|
|
282
|
+
requestAnimationFrame(() => {
|
|
283
|
+
if ((window as any).lucide) {
|
|
284
|
+
(window as any).lucide.createIcons();
|
|
250
285
|
}
|
|
251
286
|
});
|
|
252
287
|
|
|
253
|
-
// === 5. RENDER: Append to DOM and finalize ===
|
|
254
|
-
container.appendChild(wrapper);
|
|
255
288
|
return this;
|
|
256
289
|
}
|
|
257
290
|
|
|
258
|
-
private _updateFileList(
|
|
259
|
-
|
|
291
|
+
private _updateFileList(files: File[]): void {
|
|
292
|
+
if (!this._fileListElement) return;
|
|
293
|
+
|
|
294
|
+
this._fileListElement.innerHTML = '';
|
|
260
295
|
|
|
261
296
|
if (files.length === 0) {
|
|
262
|
-
|
|
297
|
+
this._fileListElement.textContent = 'No files selected';
|
|
263
298
|
return;
|
|
264
299
|
}
|
|
265
300
|
|
|
@@ -267,7 +302,7 @@ export class FileUpload {
|
|
|
267
302
|
const fileItem = document.createElement('div');
|
|
268
303
|
fileItem.className = 'jux-fileupload-item';
|
|
269
304
|
fileItem.textContent = `${file.name} (${this._formatFileSize(file.size)})`;
|
|
270
|
-
|
|
305
|
+
this._fileListElement!.appendChild(fileItem);
|
|
271
306
|
});
|
|
272
307
|
}
|
|
273
308
|
|
|
@@ -277,11 +312,62 @@ export class FileUpload {
|
|
|
277
312
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
278
313
|
}
|
|
279
314
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
315
|
+
private _injectFileUploadStyles(): void {
|
|
316
|
+
const styleId = 'jux-fileupload-styles';
|
|
317
|
+
if (document.getElementById(styleId)) return;
|
|
318
|
+
|
|
319
|
+
const style = document.createElement('style');
|
|
320
|
+
style.id = styleId;
|
|
321
|
+
style.textContent = `
|
|
322
|
+
.jux-fileupload-input {
|
|
323
|
+
display: none;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.jux-fileupload-button-container {
|
|
327
|
+
display: inline-flex;
|
|
328
|
+
align-items: center;
|
|
329
|
+
gap: 8px;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.jux-fileupload-icon svg {
|
|
333
|
+
width: 18px;
|
|
334
|
+
height: 18px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.jux-fileupload-button {
|
|
338
|
+
padding: 8px 16px;
|
|
339
|
+
background: #3b82f6;
|
|
340
|
+
color: white;
|
|
341
|
+
border: none;
|
|
342
|
+
border-radius: 6px;
|
|
343
|
+
font-size: 14px;
|
|
344
|
+
cursor: pointer;
|
|
345
|
+
transition: background 0.2s;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.jux-fileupload-button:hover:not(:disabled) {
|
|
349
|
+
background: #2563eb;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.jux-fileupload-button:disabled {
|
|
353
|
+
background: #9ca3af;
|
|
354
|
+
cursor: not-allowed;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.jux-fileupload-list {
|
|
358
|
+
margin-top: 12px;
|
|
359
|
+
font-size: 14px;
|
|
360
|
+
color: #6b7280;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.jux-fileupload-item {
|
|
364
|
+
padding: 8px;
|
|
365
|
+
background: #f3f4f6;
|
|
366
|
+
border-radius: 4px;
|
|
367
|
+
margin-bottom: 4px;
|
|
368
|
+
}
|
|
369
|
+
`;
|
|
370
|
+
document.head.appendChild(style);
|
|
285
371
|
}
|
|
286
372
|
}
|
|
287
373
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
3
|
+
|
|
4
|
+
// Extend Window interface to include Jux navigation hooks
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
juxBeforeNavigate?: (from: string, to: string) => boolean | string;
|
|
8
|
+
juxAfterNavigate?: (path: string) => void;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Event definitions
|
|
13
|
+
const TRIGGER_EVENTS = [] as const;
|
|
14
|
+
const CALLBACK_EVENTS = ['blocked', 'allowed'] as const;
|
|
15
|
+
|
|
16
|
+
export interface GuardOptions {
|
|
17
|
+
authState?: State<boolean>; // Check if user is authenticated
|
|
18
|
+
loginPath?: string; // Where to redirect if blocked
|
|
19
|
+
protectedPaths?: string[]; // Paths that require auth
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type GuardState = {
|
|
23
|
+
authState: State<boolean> | null;
|
|
24
|
+
loginPath: string;
|
|
25
|
+
protectedPaths: string[];
|
|
26
|
+
isActive: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* ⚠️ DEPRECATED: Guard component is no longer supported after removing global middleware.
|
|
31
|
+
*
|
|
32
|
+
* Recommended alternatives:
|
|
33
|
+
* 1. Server-side authentication (Express, FastAPI, Laravel)
|
|
34
|
+
* 2. Manual route checks in each view
|
|
35
|
+
* 3. Custom wrapper components
|
|
36
|
+
*
|
|
37
|
+
* This component will be removed in a future version.
|
|
38
|
+
*/
|
|
39
|
+
export class Guard extends BaseComponent<GuardState> {
|
|
40
|
+
constructor(id: string, options: GuardOptions = {}) {
|
|
41
|
+
super(id, {
|
|
42
|
+
authState: options.authState ?? null,
|
|
43
|
+
loginPath: options.loginPath ?? '/login',
|
|
44
|
+
protectedPaths: options.protectedPaths ?? [],
|
|
45
|
+
isActive: false
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.warn(
|
|
49
|
+
'[Jux Guard] DEPRECATED: Guard component no longer supported after middleware removal.\n' +
|
|
50
|
+
'Use server-side auth or manual checks instead.'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protected getTriggerEvents(): readonly string[] {
|
|
55
|
+
return TRIGGER_EVENTS;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protected getCallbackEvents(): readonly string[] {
|
|
59
|
+
return CALLBACK_EVENTS;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
63
|
+
* FLUENT API (No-ops now)
|
|
64
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
65
|
+
|
|
66
|
+
requireAuth(authState: State<boolean>, loginPath?: string): this {
|
|
67
|
+
console.warn('[Jux Guard] DEPRECATED: requireAuth() has no effect');
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protect(...paths: string[]): this {
|
|
72
|
+
console.warn('[Jux Guard] DEPRECATED: protect() has no effect');
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
77
|
+
* RENDER (No-op)
|
|
78
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
79
|
+
|
|
80
|
+
render(targetId?: string): this {
|
|
81
|
+
console.warn('[Jux Guard] DEPRECATED: Guard rendering has no effect');
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
deactivate(): this {
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function guard(id: string, options?: GuardOptions): Guard {
|
|
91
|
+
return new Guard(id, options);
|
|
92
|
+
}
|