lightview 1.8.1-b → 2.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.
- package/.agent/workflows/daisyui-component-migration.md +155 -0
- package/.codacy/cli.sh +149 -0
- package/.codacy/codacy.yaml +15 -0
- package/.github/instructions/codacy.instructions.md +72 -0
- package/.wranglerignore +21 -0
- package/README.md +1331 -21
- package/_headers +4 -0
- package/build.js +70 -0
- package/components/actions/button.js +151 -0
- package/components/actions/dropdown.js +120 -0
- package/components/actions/modal.js +146 -0
- package/components/actions/swap.js +118 -0
- package/components/daisyui.js +288 -0
- package/components/data-display/accordion.js +128 -0
- package/components/data-display/alert.js +112 -0
- package/components/data-display/avatar.js +170 -0
- package/components/data-display/badge.js +82 -0
- package/components/data-display/card.js +151 -0
- package/components/data-display/carousel.js +94 -0
- package/components/data-display/chart.js +220 -0
- package/components/data-display/chat.js +128 -0
- package/components/data-display/collapse.js +103 -0
- package/components/data-display/countdown.js +69 -0
- package/components/data-display/diff.js +111 -0
- package/components/data-display/kbd.js +65 -0
- package/components/data-display/loading.js +75 -0
- package/components/data-display/progress.js +79 -0
- package/components/data-display/radial-progress.js +88 -0
- package/components/data-display/skeleton.js +66 -0
- package/components/data-display/stats.js +159 -0
- package/components/data-display/table.js +146 -0
- package/components/data-display/timeline.js +146 -0
- package/components/data-display/toast.js +72 -0
- package/components/data-display/tooltip.js +74 -0
- package/components/data-input/checkbox.js +253 -0
- package/components/data-input/file-input.js +224 -0
- package/components/data-input/input.js +264 -0
- package/components/data-input/radio.js +338 -0
- package/components/data-input/range.js +204 -0
- package/components/data-input/rating.js +219 -0
- package/components/data-input/select.js +287 -0
- package/components/data-input/textarea.js +287 -0
- package/components/data-input/toggle.js +201 -0
- package/components/index.js +137 -0
- package/components/layout/divider.js +72 -0
- package/components/layout/drawer.js +142 -0
- package/components/layout/footer.js +100 -0
- package/components/layout/hero.js +109 -0
- package/components/layout/indicator.js +90 -0
- package/components/layout/join.js +78 -0
- package/components/layout/navbar.js +110 -0
- package/components/navigation/breadcrumbs.js +91 -0
- package/components/navigation/dock.js +103 -0
- package/components/navigation/menu.js +126 -0
- package/components/navigation/pagination.js +105 -0
- package/components/navigation/steps.js +89 -0
- package/components/navigation/tabs.css +177 -0
- package/components/navigation/tabs.js +123 -0
- package/components/theme/theme-switch.css +65 -0
- package/components/theme/theme-switch.js +177 -0
- package/docs/about.html +164 -0
- package/docs/api/computed.html +184 -0
- package/docs/api/effects.html +173 -0
- package/docs/api/elements.html +180 -0
- package/docs/api/enhance.html +225 -0
- package/docs/api/hypermedia.html +165 -0
- package/docs/api/index.html +178 -0
- package/docs/api/nav.html +18 -0
- package/docs/api/signals.html +136 -0
- package/docs/api/state.html +217 -0
- package/docs/assets/images/logo-favicon.svg +42 -0
- package/docs/assets/images/logo-static.svg +40 -0
- package/docs/assets/images/logo.svg +66 -0
- package/docs/assets/js/examplify.js +395 -0
- package/docs/assets/styles/site.css +1102 -0
- package/docs/assets/styles/themes.css +236 -0
- package/docs/components/accordion.html +439 -0
- package/docs/components/alert.html +528 -0
- package/docs/components/avatar.html +586 -0
- package/docs/components/badge.html +531 -0
- package/docs/components/breadcrumbs.html +278 -0
- package/docs/components/button.html +579 -0
- package/docs/components/card.html +561 -0
- package/docs/components/carousel.html +286 -0
- package/docs/components/chart-area.html +702 -0
- package/docs/components/chart-bar.html +782 -0
- package/docs/components/chart-column.html +735 -0
- package/docs/components/chart-line.html +794 -0
- package/docs/components/chart-pie.html +823 -0
- package/docs/components/chart.html +612 -0
- package/docs/components/chat.html +547 -0
- package/docs/components/checkbox.html +641 -0
- package/docs/components/collapse.html +536 -0
- package/docs/components/component-nav.html +53 -0
- package/docs/components/countdown.html +470 -0
- package/docs/components/diff.html +245 -0
- package/docs/components/divider.html +240 -0
- package/docs/components/dock.html +277 -0
- package/docs/components/drawer.html +515 -0
- package/docs/components/dropdown.html +479 -0
- package/docs/components/file-input.html +591 -0
- package/docs/components/footer.html +301 -0
- package/docs/components/gallery.html +504 -0
- package/docs/components/hero.html +264 -0
- package/docs/components/index.css +840 -0
- package/docs/components/index.html +735 -0
- package/docs/components/indicator.html +342 -0
- package/docs/components/input.html +644 -0
- package/docs/components/join.html +285 -0
- package/docs/components/kbd.html +322 -0
- package/docs/components/loading.html +521 -0
- package/docs/components/menu.html +461 -0
- package/docs/components/modal.html +639 -0
- package/docs/components/navbar.html +321 -0
- package/docs/components/pagination.html +279 -0
- package/docs/components/progress.html +514 -0
- package/docs/components/radial-progress.html +434 -0
- package/docs/components/radio.html +655 -0
- package/docs/components/range.html +611 -0
- package/docs/components/rating.html +642 -0
- package/docs/components/select.html +696 -0
- package/docs/components/sidebar-setup.js +93 -0
- package/docs/components/skeleton.html +447 -0
- package/docs/components/spinner.html +68 -0
- package/docs/components/stats.html +486 -0
- package/docs/components/steps.html +356 -0
- package/docs/components/swap.html +517 -0
- package/docs/components/switch.html +68 -0
- package/docs/components/table.html +668 -0
- package/docs/components/tabs.html +506 -0
- package/docs/components/text-input.html +68 -0
- package/docs/components/textarea.html +603 -0
- package/docs/components/timeline.html +487 -0
- package/docs/components/toast.html +474 -0
- package/docs/components/toggle.html +564 -0
- package/docs/components/tooltip.html +423 -0
- package/docs/examples/getting-started-example.html +40 -0
- package/docs/examples/index.html +93 -0
- package/docs/getting-started/index.html +739 -0
- package/docs/getting-started/reviews.html +23 -0
- package/docs/getting-started/reviews.odom +108 -0
- package/docs/getting-started/reviews.vdom +84 -0
- package/docs/index.html +134 -0
- package/docs/playground.html +416 -0
- package/docs/router.html +285 -0
- package/docs/styles/index.html +190 -0
- package/functions/_middleware.js +32 -0
- package/index.html +309 -0
- package/lightview-router.js +364 -0
- package/lightview-x.js +1577 -0
- package/lightview.js +658 -1109
- package/lightview.js.backup +793 -0
- package/middleware/locale.js +25 -0
- package/middleware/markdown.js +44 -0
- package/middleware/notFound.js +37 -0
- package/package.json +27 -41
- package/watch.js +92 -0
- package/wrangler.toml +12 -0
- package/.idea/lightview.iml +0 -12
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -6
- package/LICENSE +0 -21
- package/codepen-no-tabs-embed.css +0 -2
- package/components/chart/chart.html +0 -17
- package/components/chart/example.html +0 -32
- package/components/chart.html +0 -83
- package/components/components.js +0 -113
- package/components/gantt/example.html +0 -22
- package/components/gantt/gantt.html +0 -42
- package/components/gauge/example.html +0 -28
- package/components/gauge/gauge.html +0 -20
- package/components/gauge.html +0 -60
- package/components/orgchart/example.html +0 -25
- package/components/orgchart/orgchart.html +0 -41
- package/components/repl/code-editor.html +0 -64
- package/components/repl/editor.html +0 -37
- package/components/repl/editorjs-inline-tool/index.js +0 -3
- package/components/repl/editorjs-inline-tool/inline-tools.js +0 -28
- package/components/repl/editorjs-inline-tool/tool.js +0 -175
- package/components/repl/repl-with-wysiwyg.html +0 -355
- package/components/repl/repl.html +0 -345
- package/components/repl/sup.js +0 -44
- package/components/repl/wysiwyg-repl.html +0 -258
- package/components/timeline/example.html +0 -33
- package/components/timeline/timeline.html +0 -44
- package/components/timeline.html +0 -81
- package/examples/anchor.html +0 -11
- package/examples/chart.html +0 -34
- package/examples/counter.html +0 -26
- package/examples/counter.test.mjs +0 -47
- package/examples/counter2.html +0 -26
- package/examples/directives.html +0 -79
- package/examples/foreign.html +0 -50
- package/examples/forgeinform.html +0 -98
- package/examples/form.html +0 -61
- package/examples/gauge.html +0 -18
- package/examples/invalid-template-literals.html +0 -44
- package/examples/medium/remote.html +0 -60
- package/examples/message.html +0 -18
- package/examples/nested.html +0 -11
- package/examples/object-bound-form.html +0 -34
- package/examples/remote-server.js +0 -51
- package/examples/remote.html +0 -34
- package/examples/remote.json +0 -1
- package/examples/scratch.html +0 -69
- package/examples/sensors/index.html +0 -30
- package/examples/sensors/sensor-server.js +0 -30
- package/examples/shared.html +0 -41
- package/examples/template.html +0 -33
- package/examples/timeline.html +0 -21
- package/examples/todo.html +0 -38
- package/examples/top.html +0 -10
- package/examples/types.html +0 -94
- package/examples/xor.html +0 -62
- package/jest-puppeteer.config.js +0 -5
- package/jest.config.json +0 -12
- package/sites/client.html +0 -48
- package/sites/index.html +0 -247
- package/test/basic.html +0 -93
- package/test/basic.test.mjs +0 -315
- package/test/extended.html +0 -29
- package/test/extended.test.mjs +0 -448
- package/types.js +0 -534
- package/unsplash.key +0 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightview Components - Checkbox
|
|
3
|
+
* A checkbox component using DaisyUI 5 styling with validation support
|
|
4
|
+
* @see https://daisyui.com/components/checkbox/
|
|
5
|
+
*
|
|
6
|
+
* Uses DaisyUI's form-control pattern:
|
|
7
|
+
* <div class="form-control">
|
|
8
|
+
* <label class="label cursor-pointer">
|
|
9
|
+
* <span class="label-text">Label</span>
|
|
10
|
+
* <input type="checkbox" class="checkbox" />
|
|
11
|
+
* </label>
|
|
12
|
+
* </div>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import '../daisyui.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Checkbox Component
|
|
19
|
+
* @param {Object} props - Checkbox properties
|
|
20
|
+
* @param {boolean|Signal} props.checked - Controlled checked state
|
|
21
|
+
* @param {boolean} props.defaultChecked - Initial checked state (uncontrolled)
|
|
22
|
+
* @param {boolean} props.indeterminate - Indeterminate state
|
|
23
|
+
* @param {string} props.size - 'xs' | 'sm' | 'md' | 'lg' (default: 'md')
|
|
24
|
+
* @param {string} props.color - 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error'
|
|
25
|
+
* @param {boolean} props.disabled - Disable checkbox
|
|
26
|
+
* @param {boolean} props.required - Required field
|
|
27
|
+
* @param {string} props.label - Label text
|
|
28
|
+
* @param {string} props.description - Description text below label
|
|
29
|
+
* @param {string|Function} props.error - Error message
|
|
30
|
+
* @param {Function} props.validate - Validation function (checked) => errorMessage | null
|
|
31
|
+
* @param {Function} props.onChange - Change handler
|
|
32
|
+
* @param {boolean} props.useShadow - Render in Shadow DOM with isolated DaisyUI styles
|
|
33
|
+
*/
|
|
34
|
+
const Checkbox = (props = {}) => {
|
|
35
|
+
const { tags, signal } = window.Lightview || {};
|
|
36
|
+
const LVX = window.LightviewX || {};
|
|
37
|
+
|
|
38
|
+
if (!tags) {
|
|
39
|
+
console.error('Lightview not found');
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { div, input, label, span, p, shadowDOM } = tags;
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
checked,
|
|
47
|
+
defaultChecked = false,
|
|
48
|
+
indeterminate = false,
|
|
49
|
+
size = 'md',
|
|
50
|
+
color,
|
|
51
|
+
disabled = false,
|
|
52
|
+
required = false,
|
|
53
|
+
label: labelText,
|
|
54
|
+
description,
|
|
55
|
+
error,
|
|
56
|
+
validate,
|
|
57
|
+
onChange,
|
|
58
|
+
name,
|
|
59
|
+
id,
|
|
60
|
+
value,
|
|
61
|
+
class: className = '',
|
|
62
|
+
useShadow,
|
|
63
|
+
...rest
|
|
64
|
+
} = props;
|
|
65
|
+
|
|
66
|
+
// Generate unique ID if not provided
|
|
67
|
+
const checkboxId = id || `checkbox-${Math.random().toString(36).slice(2, 9)}`;
|
|
68
|
+
|
|
69
|
+
// Internal state
|
|
70
|
+
const internalChecked = signal ? signal(defaultChecked) : { value: defaultChecked };
|
|
71
|
+
const internalError = signal ? signal(null) : { value: null };
|
|
72
|
+
|
|
73
|
+
const isControlled = checked !== undefined;
|
|
74
|
+
|
|
75
|
+
const getChecked = () => {
|
|
76
|
+
if (isControlled) {
|
|
77
|
+
return typeof checked === 'function' ? checked() :
|
|
78
|
+
(checked && typeof checked.value !== 'undefined') ? checked.value : checked;
|
|
79
|
+
}
|
|
80
|
+
return internalChecked.value;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const getError = () => {
|
|
84
|
+
if (error) {
|
|
85
|
+
const err = typeof error === 'function' ? error() : error;
|
|
86
|
+
if (err) return err;
|
|
87
|
+
}
|
|
88
|
+
return internalError.value;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const runValidation = (val) => {
|
|
92
|
+
if (validate) {
|
|
93
|
+
const result = validate(val);
|
|
94
|
+
internalError.value = result;
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
if (required && !val) {
|
|
98
|
+
internalError.value = 'This field is required';
|
|
99
|
+
return 'This field is required';
|
|
100
|
+
}
|
|
101
|
+
internalError.value = null;
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const handleChange = (e) => {
|
|
106
|
+
const newValue = e.target.checked;
|
|
107
|
+
|
|
108
|
+
if (!isControlled) {
|
|
109
|
+
internalChecked.value = newValue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (isControlled && checked && typeof checked.value !== 'undefined') {
|
|
113
|
+
checked.value = newValue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
runValidation(newValue);
|
|
117
|
+
|
|
118
|
+
if (onChange) onChange(newValue, e);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Build DaisyUI checkbox classes
|
|
122
|
+
const getCheckboxClass = () => {
|
|
123
|
+
const classes = ['checkbox'];
|
|
124
|
+
|
|
125
|
+
// Size
|
|
126
|
+
if (size && size !== 'md') {
|
|
127
|
+
classes.push(`checkbox-${size}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Color
|
|
131
|
+
if (color) {
|
|
132
|
+
classes.push(`checkbox-${color}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return classes.join(' ');
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Build checkbox input
|
|
139
|
+
const checkboxInput = input({
|
|
140
|
+
type: 'checkbox',
|
|
141
|
+
class: getCheckboxClass(),
|
|
142
|
+
checked: isControlled
|
|
143
|
+
? (typeof checked === 'function' ? checked : () => getChecked())
|
|
144
|
+
: () => internalChecked.value,
|
|
145
|
+
disabled: typeof disabled === 'function' ? disabled : disabled,
|
|
146
|
+
required,
|
|
147
|
+
name,
|
|
148
|
+
id: checkboxId,
|
|
149
|
+
value,
|
|
150
|
+
onchange: handleChange,
|
|
151
|
+
'aria-invalid': () => !!getError(),
|
|
152
|
+
onmount: (el) => {
|
|
153
|
+
// Handle indeterminate state (can only be set via JS)
|
|
154
|
+
if (indeterminate) {
|
|
155
|
+
el.indeterminate = typeof indeterminate === 'function' ? indeterminate() : indeterminate;
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
...rest
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// If no label, return just the checkbox
|
|
162
|
+
if (!labelText && !description) {
|
|
163
|
+
// Check if we should use shadow DOM
|
|
164
|
+
let usesShadow = false;
|
|
165
|
+
if (LVX.shouldUseShadow) {
|
|
166
|
+
usesShadow = LVX.shouldUseShadow(useShadow);
|
|
167
|
+
} else {
|
|
168
|
+
usesShadow = useShadow === true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (usesShadow) {
|
|
172
|
+
const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
|
|
173
|
+
const themeValue = LVX.themeSignal ? () => LVX.themeSignal.value : 'light';
|
|
174
|
+
|
|
175
|
+
return div({ class: 'content', style: 'display: inline-block' },
|
|
176
|
+
shadowDOM({ mode: 'open', adoptedStyleSheets },
|
|
177
|
+
div({ 'data-theme': themeValue }, checkboxInput)
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return checkboxInput;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Build label content
|
|
186
|
+
const labelContent = [];
|
|
187
|
+
if (labelText) {
|
|
188
|
+
labelContent.push(
|
|
189
|
+
span({ class: 'label-text' },
|
|
190
|
+
labelText,
|
|
191
|
+
required ? span({ class: 'text-error' }, ' *') : null
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Build the form control wrapper
|
|
197
|
+
const formControl = div({
|
|
198
|
+
class: `form-control ${className}`.trim()
|
|
199
|
+
},
|
|
200
|
+
label({ class: 'label cursor-pointer', style: 'justify-content: flex-start; gap: 0;' },
|
|
201
|
+
checkboxInput,
|
|
202
|
+
labelContent.length > 0 ? div({ style: 'display: flex; flex-direction: column; margin-left: 0.5rem;' },
|
|
203
|
+
...labelContent,
|
|
204
|
+
description ? span({ class: 'label-text-alt', style: 'opacity: 0.7;' }, description) : null
|
|
205
|
+
) : null
|
|
206
|
+
),
|
|
207
|
+
// Error message
|
|
208
|
+
(validate || error || required) ? () => {
|
|
209
|
+
const currentError = getError();
|
|
210
|
+
return currentError
|
|
211
|
+
? p({ class: 'label', style: 'color: oklch(var(--er)); font-size: 0.875rem;', role: 'alert' }, currentError)
|
|
212
|
+
: null;
|
|
213
|
+
} : null
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Check if we should use shadow DOM
|
|
217
|
+
let usesShadow = false;
|
|
218
|
+
if (LVX.shouldUseShadow) {
|
|
219
|
+
usesShadow = LVX.shouldUseShadow(useShadow);
|
|
220
|
+
} else {
|
|
221
|
+
usesShadow = useShadow === true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (usesShadow) {
|
|
225
|
+
const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
|
|
226
|
+
|
|
227
|
+
const themeValue = LVX.themeSignal ? () => LVX.themeSignal.value : 'light';
|
|
228
|
+
|
|
229
|
+
return span({ style: 'margin-right: 0.5rem' },
|
|
230
|
+
shadowDOM({ mode: 'open', adoptedStyleSheets },
|
|
231
|
+
div({ 'data-theme': themeValue, style: 'display: inline-block' },
|
|
232
|
+
formControl
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return formControl;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Auto-register
|
|
242
|
+
window.Lightview.tags.Checkbox = Checkbox;
|
|
243
|
+
|
|
244
|
+
// Register as Custom Element
|
|
245
|
+
if (window.LightviewX?.createCustomElement) {
|
|
246
|
+
const CheckboxElement = window.LightviewX.createCustomElement(Checkbox);
|
|
247
|
+
if (!customElements.get('lv-checkbox')) {
|
|
248
|
+
customElements.define('lv-checkbox', CheckboxElement);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export default Checkbox;
|
|
253
|
+
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightview Components - FileInput
|
|
3
|
+
* A file input component using DaisyUI 5 styling
|
|
4
|
+
* @see https://daisyui.com/components/file-input/
|
|
5
|
+
*
|
|
6
|
+
* Uses DaisyUI's fieldset pattern:
|
|
7
|
+
* <fieldset class="fieldset">
|
|
8
|
+
* <legend class="fieldset-legend">Label</legend>
|
|
9
|
+
* <input type="file" class="file-input" />
|
|
10
|
+
* <p class="label">Helper text</p>
|
|
11
|
+
* </fieldset>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import '../daisyui.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* FileInput Component
|
|
18
|
+
* @param {Object} props - FileInput properties
|
|
19
|
+
* @param {string} props.size - 'xs' | 'sm' | 'md' | 'lg' (default: 'md')
|
|
20
|
+
* @param {string} props.color - 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error'
|
|
21
|
+
* @param {boolean} props.ghost - Ghost style (no background)
|
|
22
|
+
* @param {boolean} props.disabled - Disable file input
|
|
23
|
+
* @param {boolean} props.required - Required field
|
|
24
|
+
* @param {string} props.accept - Accepted file types (e.g., '.pdf,.doc', 'image/*')
|
|
25
|
+
* @param {boolean} props.multiple - Allow multiple file selection
|
|
26
|
+
* @param {string} props.label - Label text
|
|
27
|
+
* @param {string} props.helper - Helper text
|
|
28
|
+
* @param {string|Function} props.error - Error message
|
|
29
|
+
* @param {Function} props.validate - Validation function (files) => errorMessage | null
|
|
30
|
+
* @param {Function} props.onChange - Change handler (files, event) => void
|
|
31
|
+
* @param {boolean} props.useShadow - Render in Shadow DOM
|
|
32
|
+
*/
|
|
33
|
+
const FileInput = (props = {}) => {
|
|
34
|
+
const { tags, signal } = window.Lightview || {};
|
|
35
|
+
const LVX = window.LightviewX || {};
|
|
36
|
+
|
|
37
|
+
if (!tags) {
|
|
38
|
+
console.error('Lightview not found');
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { div, input, fieldset, legend, p, span, shadowDOM } = tags;
|
|
43
|
+
|
|
44
|
+
const {
|
|
45
|
+
size = 'md',
|
|
46
|
+
color,
|
|
47
|
+
ghost = false,
|
|
48
|
+
disabled = false,
|
|
49
|
+
required = false,
|
|
50
|
+
accept,
|
|
51
|
+
multiple = false,
|
|
52
|
+
label: labelText,
|
|
53
|
+
helper,
|
|
54
|
+
error,
|
|
55
|
+
validate,
|
|
56
|
+
onChange,
|
|
57
|
+
name,
|
|
58
|
+
id,
|
|
59
|
+
class: className = '',
|
|
60
|
+
useShadow,
|
|
61
|
+
...rest
|
|
62
|
+
} = props;
|
|
63
|
+
|
|
64
|
+
const fileInputId = id || `file-input-${Math.random().toString(36).slice(2, 9)}`;
|
|
65
|
+
|
|
66
|
+
// Internal state
|
|
67
|
+
const internalError = signal ? signal(null) : { value: null };
|
|
68
|
+
|
|
69
|
+
const getError = () => {
|
|
70
|
+
if (error) {
|
|
71
|
+
const err = typeof error === 'function' ? error() : error;
|
|
72
|
+
if (err) return err;
|
|
73
|
+
}
|
|
74
|
+
return internalError.value;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const handleChange = (e) => {
|
|
78
|
+
const files = e.target.files;
|
|
79
|
+
|
|
80
|
+
// Validation
|
|
81
|
+
if (validate) {
|
|
82
|
+
const validationError = validate(files);
|
|
83
|
+
internalError.value = validationError;
|
|
84
|
+
} else if (required && (!files || files.length === 0)) {
|
|
85
|
+
internalError.value = 'Please select a file';
|
|
86
|
+
} else {
|
|
87
|
+
internalError.value = null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (onChange) onChange(files, e);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Build DaisyUI file-input classes
|
|
94
|
+
const getFileInputClass = () => {
|
|
95
|
+
const classes = ['file-input', 'w-full'];
|
|
96
|
+
|
|
97
|
+
// Ghost style
|
|
98
|
+
if (ghost) {
|
|
99
|
+
classes.push('file-input-ghost');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Size
|
|
103
|
+
if (size && size !== 'md') {
|
|
104
|
+
classes.push(`file-input-${size}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Color
|
|
108
|
+
if (color) {
|
|
109
|
+
classes.push(`file-input-${color}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Error state
|
|
113
|
+
const currentError = getError();
|
|
114
|
+
if (currentError) {
|
|
115
|
+
classes.push('file-input-error');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return classes.join(' ');
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const fileInputEl = input({
|
|
122
|
+
type: 'file',
|
|
123
|
+
class: validate || error ? () => getFileInputClass() : getFileInputClass(),
|
|
124
|
+
accept,
|
|
125
|
+
multiple,
|
|
126
|
+
disabled,
|
|
127
|
+
required,
|
|
128
|
+
name,
|
|
129
|
+
id: fileInputId,
|
|
130
|
+
onchange: handleChange,
|
|
131
|
+
'aria-invalid': () => !!getError(),
|
|
132
|
+
...rest
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// If no label and no helper, return just the file input
|
|
136
|
+
if (!labelText && !helper && !validate && !error) {
|
|
137
|
+
let usesShadow = false;
|
|
138
|
+
if (LVX.shouldUseShadow) {
|
|
139
|
+
usesShadow = LVX.shouldUseShadow(useShadow);
|
|
140
|
+
} else {
|
|
141
|
+
usesShadow = useShadow === true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (usesShadow) {
|
|
145
|
+
const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
|
|
146
|
+
const themeValue = LVX.themeSignal ? () => LVX.themeSignal.value : 'light';
|
|
147
|
+
|
|
148
|
+
return div({ class: 'content', style: 'display: inline-block' },
|
|
149
|
+
shadowDOM({ mode: 'open', adoptedStyleSheets },
|
|
150
|
+
div({ 'data-theme': themeValue }, fileInputEl)
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return fileInputEl;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Build the component using DaisyUI fieldset pattern
|
|
159
|
+
const fieldsetContent = [];
|
|
160
|
+
|
|
161
|
+
// Legend/Label
|
|
162
|
+
if (labelText) {
|
|
163
|
+
fieldsetContent.push(
|
|
164
|
+
legend({ class: 'fieldset-legend' },
|
|
165
|
+
labelText,
|
|
166
|
+
required ? span({ class: 'text-error' }, ' *') : null
|
|
167
|
+
)
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// File input element
|
|
172
|
+
fieldsetContent.push(fileInputEl);
|
|
173
|
+
|
|
174
|
+
// Helper or error text
|
|
175
|
+
if (helper || validate || error) {
|
|
176
|
+
fieldsetContent.push(
|
|
177
|
+
() => {
|
|
178
|
+
const currentError = getError();
|
|
179
|
+
if (currentError) {
|
|
180
|
+
return p({
|
|
181
|
+
class: 'label text-error',
|
|
182
|
+
role: 'alert'
|
|
183
|
+
}, currentError);
|
|
184
|
+
}
|
|
185
|
+
if (helper) {
|
|
186
|
+
return p({ class: 'label' }, helper);
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Wrapper
|
|
194
|
+
const wrapperEl = fieldset({
|
|
195
|
+
class: `fieldset ${className}`.trim()
|
|
196
|
+
}, ...fieldsetContent);
|
|
197
|
+
|
|
198
|
+
// Check if we should use shadow DOM
|
|
199
|
+
let usesShadow = false;
|
|
200
|
+
if (LVX.shouldUseShadow) {
|
|
201
|
+
usesShadow = LVX.shouldUseShadow(useShadow);
|
|
202
|
+
} else {
|
|
203
|
+
usesShadow = useShadow === true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (usesShadow) {
|
|
207
|
+
const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
|
|
208
|
+
|
|
209
|
+
const themeValue = LVX.themeSignal ? () => LVX.themeSignal.value : 'light';
|
|
210
|
+
|
|
211
|
+
return div({ class: 'contents' },
|
|
212
|
+
shadowDOM({ mode: 'open', adoptedStyleSheets },
|
|
213
|
+
div({ 'data-theme': themeValue }, wrapperEl)
|
|
214
|
+
)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return wrapperEl;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Auto-register
|
|
222
|
+
window.Lightview.tags.FileInput = FileInput;
|
|
223
|
+
|
|
224
|
+
export default FileInput;
|