@a-vision-software/vue-input-components 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.
package/README.md ADDED
@@ -0,0 +1,419 @@
1
+ # Vue Input Components
2
+
3
+ A collection of reusable Vue 3 input components with TypeScript support.
4
+
5
+ ## Features
6
+
7
+ ### TextInput Component
8
+
9
+ - Customizable label position (top, left, right, bottom)
10
+ - Label alignment options (left, right, center)
11
+ - Icon support with click-to-focus functionality
12
+ - Error and success states with messages
13
+ - Required field indicator
14
+ - Autosave functionality with debounce
15
+ - Change indicators (saved, changed)
16
+ - Fully responsive
17
+ - Accessible
18
+ - TypeScript support
19
+ - Multiline text input support (textarea)
20
+ - Adjustable number of visible rows
21
+ - Optional maximum length limit
22
+ - Vertical resizing support
23
+
24
+ ### FileUpload Component
25
+
26
+ - Drag and drop file upload
27
+ - File size validation (20MB limit)
28
+ - Multiple file selection
29
+ - Progress bar for upload status
30
+ - Customizable icons
31
+ - Automatic or manual upload modes
32
+ - File list display with sizes
33
+ - Error handling and status messages
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ npm install
39
+ ```
40
+
41
+ ## Development
42
+
43
+ ```bash
44
+ npm run dev
45
+ ```
46
+
47
+ ## Build
48
+
49
+ ```bash
50
+ npm run build
51
+ ```
52
+
53
+ ## Installation in Other Projects
54
+
55
+ ### Option 1: Copy Components Directly
56
+
57
+ 1. Copy the following files to your project:
58
+
59
+ - `src/components/TextInput.vue`
60
+ - `src/components/FileUpload.vue`
61
+ - `src/assets/colors.css`
62
+
63
+ 2. Install required dependencies in your project:
64
+
65
+ ```bash
66
+ npm install @fortawesome/fontawesome-svg-core @fortawesome/free-regular-svg-icons @fortawesome/free-solid-svg-icons @fortawesome/vue-fontawesome
67
+ ```
68
+
69
+ 3. Import the color variables in your main CSS file:
70
+
71
+ ```css
72
+ @import './assets/colors.css';
73
+ ```
74
+
75
+ 4. Import and register Font Awesome in your main.js or main.ts:
76
+
77
+ ```typescript
78
+ import { library } from '@fortawesome/fontawesome-svg-core'
79
+ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
80
+ import { faUser, faUpload, faImage, faFile, faHome } from '@fortawesome/free-solid-svg-icons'
81
+
82
+ library.add(faUser, faUpload, faImage, faFile, faHome)
83
+ app.component('font-awesome-icon', FontAwesomeIcon)
84
+ ```
85
+
86
+ ### Option 2: Create a Package
87
+
88
+ 1. Create a new package.json with the following structure:
89
+
90
+ ```json
91
+ {
92
+ "name": "vue-input-components",
93
+ "version": "1.0.0",
94
+ "main": "dist/index.js",
95
+ "types": "dist/index.d.ts",
96
+ "files": ["dist"],
97
+ "dependencies": {
98
+ "@fortawesome/fontawesome-svg-core": "^6.7.2",
99
+ "@fortawesome/free-regular-svg-icons": "^6.7.2",
100
+ "@fortawesome/free-solid-svg-icons": "^6.7.2",
101
+ "@fortawesome/vue-fontawesome": "^3.0.8",
102
+ "vue": "^3.5.13"
103
+ },
104
+ "peerDependencies": {
105
+ "vue": "^3.5.13"
106
+ }
107
+ }
108
+ ```
109
+
110
+ 2. Build the components for distribution:
111
+
112
+ ```bash
113
+ npm run build
114
+ ```
115
+
116
+ 3. Publish to npm:
117
+
118
+ ```bash
119
+ npm publish
120
+ ```
121
+
122
+ 4. Install in other projects:
123
+ ```bash
124
+ npm install vue-input-components
125
+ ```
126
+
127
+ ### Usage in Other Projects
128
+
129
+ 1. Import the components:
130
+
131
+ ```vue
132
+ <script setup>
133
+ import { TextInput, FileUpload } from 'vue-input-components'
134
+ </script>
135
+ ```
136
+
137
+ 2. Use the components in your template:
138
+
139
+ ```vue
140
+ <template>
141
+ <TextInput v-model="username" label="Username" icon="user" :required="true" />
142
+
143
+ <FileUpload
144
+ icon="upload"
145
+ upload-url="https://api.example.com/upload"
146
+ @upload-complete="handleUploadComplete"
147
+ />
148
+ </template>
149
+ ```
150
+
151
+ 3. Import the color variables in your project's main CSS file:
152
+ ```css
153
+ @import 'vue-input-components/dist/colors.css';
154
+ ```
155
+
156
+ ### Customization
157
+
158
+ 1. Override color variables in your project:
159
+
160
+ ```css
161
+ :root {
162
+ --primary-color: #your-color;
163
+ --text-color: #your-color;
164
+ /* Override other variables as needed */
165
+ }
166
+ ```
167
+
168
+ 2. Customize component styles:
169
+
170
+ ```css
171
+ .text-input {
172
+ /* Your custom styles */
173
+ }
174
+
175
+ .file-upload {
176
+ /* Your custom styles */
177
+ }
178
+ ```
179
+
180
+ ### Requirements
181
+
182
+ - Vue 3.x
183
+ - TypeScript (optional but recommended)
184
+ - Font Awesome (for icons)
185
+
186
+ ### Browser Support
187
+
188
+ All components use modern browser features and should work in all modern browsers that support:
189
+
190
+ - CSS Variables
191
+ - Flexbox
192
+ - File API (for FileUpload)
193
+ - Drag and Drop API (for FileUpload)
194
+ - FormData (for FileUpload)
195
+ - XMLHttpRequest (for FileUpload)
196
+
197
+ ## Components
198
+
199
+ ### TextInput
200
+
201
+ Basic text input component with advanced features, including textarea support.
202
+
203
+ #### Props
204
+
205
+ | Prop | Type | Default | Description |
206
+ | ------------- | -------------------------------------- | --------- | ------------------------------------------ |
207
+ | modelValue | string | required | The input value (v-model) |
208
+ | label | string | undefined | Input label |
209
+ | type | string | 'text' | Input type (text, password, email, etc.) |
210
+ | icon | string | undefined | Font Awesome icon name |
211
+ | placeholder | string | undefined | Input placeholder |
212
+ | required | boolean | false | Whether the field is required |
213
+ | disabled | boolean | false | Whether the field is disabled |
214
+ | error | string | undefined | Error message |
215
+ | success | string | undefined | Success message |
216
+ | labelPosition | 'top' \| 'left' \| 'right' \| 'bottom' | 'top' | Label position |
217
+ | labelAlign | 'left' \| 'right' \| 'center' | 'left' | Label text alignment |
218
+ | totalWidth | string | '100%' | Total width of the component |
219
+ | inputWidth | string | undefined | Width of the input field |
220
+ | labelWidth | string | undefined | Width of the label (when position is left) |
221
+ | autosave | (value: string) => Promise<void> | undefined | Autosave callback function |
222
+ | isTextarea | boolean | false | Whether to render as a textarea |
223
+ | maxHeight | string | '14rem' | Maximum height for textarea |
224
+ | height | string | '5.5rem' | Initial height for textarea |
225
+
226
+ #### Events
227
+
228
+ | Event | Payload | Description |
229
+ | ----------------- | ------- | --------------------------------------------------- |
230
+ | update:modelValue | string | Emitted when the input value changes |
231
+ | changed | void | Emitted when the value has changed (500ms debounce) |
232
+ | saved | void | Emitted when autosave completes successfully |
233
+
234
+ #### Examples
235
+
236
+ ##### Basic Text Input
237
+
238
+ ```vue
239
+ <template>
240
+ <TextInput
241
+ v-model="username"
242
+ label="Username"
243
+ type="text"
244
+ icon="user"
245
+ placeholder="Enter your username"
246
+ :error="usernameError"
247
+ :autosave="handleUsernameAutosave"
248
+ label-position="top"
249
+ label-align="left"
250
+ required
251
+ />
252
+ </template>
253
+
254
+ <script setup lang="ts">
255
+ import { ref } from 'vue'
256
+ import TextInput from '@/components/TextInput.vue'
257
+
258
+ const username = ref('')
259
+ const usernameError = ref('')
260
+
261
+ const handleUsernameAutosave = async (value: string) => {
262
+ // Your autosave logic here
263
+ }
264
+ </script>
265
+ ```
266
+
267
+ ##### Textarea Input
268
+
269
+ ```vue
270
+ <template>
271
+ <TextInput
272
+ v-model="description"
273
+ label="Description"
274
+ icon="pen"
275
+ placeholder="Enter a detailed description"
276
+ :isTextarea="true"
277
+ :error="descriptionError"
278
+ :autosave="handleDescriptionAutosave"
279
+ label-position="top"
280
+ label-align="left"
281
+ required
282
+ />
283
+ </template>
284
+
285
+ <script setup lang="ts">
286
+ import { ref } from 'vue'
287
+ import TextInput from '@/components/TextInput.vue'
288
+
289
+ const description = ref('')
290
+ const descriptionError = ref('')
291
+
292
+ const handleDescriptionAutosave = async (value: string) => {
293
+ // Your autosave logic here
294
+ }
295
+ </script>
296
+ ```
297
+
298
+ ### FileUpload
299
+
300
+ Flexible file upload component with drag and drop support.
301
+
302
+ #### Props
303
+
304
+ | Prop | Type | Required | Default | Description |
305
+ | ----------- | ------ | -------- | -------- | ---------------------------------------------------------------------- |
306
+ | `icon` | string | No | 'upload' | Font Awesome icon name to display |
307
+ | `uploadUrl` | string | No | - | URL to upload files to. If not provided, manual upload mode is enabled |
308
+
309
+ #### Events
310
+
311
+ | Event | Parameters | Description |
312
+ | ----------------- | --------------- | --------------------------------------------------- |
313
+ | `upload-complete` | `files: File[]` | Emitted when files are successfully uploaded |
314
+ | `upload-error` | `error: string` | Emitted when an upload error occurs |
315
+ | `files-selected` | `files: File[]` | Emitted when files are selected (manual mode) |
316
+ | `start-upload` | `files: File[]` | Emitted when upload button is clicked (manual mode) |
317
+
318
+ #### Examples
319
+
320
+ ##### Automatic Upload Mode
321
+
322
+ ```vue
323
+ <template>
324
+ <FileUpload
325
+ icon="upload"
326
+ upload-url="https://api.example.com/upload"
327
+ @upload-complete="handleUploadComplete"
328
+ @upload-error="handleUploadError"
329
+ />
330
+ </template>
331
+
332
+ <script setup>
333
+ import FileUpload from '@/components/FileUpload.vue'
334
+
335
+ const handleUploadComplete = (files) => {
336
+ console.log('Uploaded files:', files)
337
+ }
338
+
339
+ const handleUploadError = (error) => {
340
+ console.error('Upload error:', error)
341
+ }
342
+ </script>
343
+ ```
344
+
345
+ ##### Manual Upload Mode
346
+
347
+ ```vue
348
+ <template>
349
+ <FileUpload
350
+ icon="image"
351
+ @files-selected="handleFilesSelected"
352
+ @start-upload="handleStartUpload"
353
+ />
354
+ </template>
355
+
356
+ <script setup>
357
+ import FileUpload from '@/components/FileUpload.vue'
358
+
359
+ const handleFilesSelected = (files) => {
360
+ console.log('Selected files:', files)
361
+ }
362
+
363
+ const handleStartUpload = (files) => {
364
+ // Implement your custom upload logic here
365
+ console.log('Starting upload for:', files)
366
+ }
367
+ </script>
368
+ ```
369
+
370
+ ## Styling
371
+
372
+ All components use CSS variables for theming. You can customize the colors by overriding these variables in your application's CSS:
373
+
374
+ ```css
375
+ :root {
376
+ /* Base colors */
377
+ --primary-color: #3498db;
378
+ --primary-color-light: rgba(52, 152, 219, 0.2);
379
+ --secondary-color: #2ecc71;
380
+
381
+ /* Text colors */
382
+ --text-color: #2c3e50;
383
+ --text-color-light: #7f8c8d;
384
+
385
+ /* UI colors */
386
+ --border-color: #dcdfe6;
387
+ --icon-color: #95a5a6;
388
+
389
+ /* State colors */
390
+ --error-color: #e74c3c;
391
+ --error-color-light: rgba(231, 76, 60, 0.2);
392
+ --success-color: #2ecc71;
393
+ --success-color-light: rgba(46, 204, 113, 0.2);
394
+ --warning-color: #f1c40f;
395
+ --warning-color-light: rgba(241, 196, 15, 0.2);
396
+
397
+ /* Background colors */
398
+ --disabled-color: #bdc3c7;
399
+ --disabled-background: #f5f7fa;
400
+ --card-bg: #ffffff;
401
+ --background-color: #f8f9fa;
402
+
403
+ /* File Upload specific colors */
404
+ --upload-border-color: var(--border-color);
405
+ --upload-bg-color: var(--background-color);
406
+ --upload-dragging-border-color: var(--primary-color);
407
+ --upload-dragging-bg-color: var(--primary-color-light);
408
+ --upload-has-files-border-color: var(--success-color);
409
+ --upload-has-files-bg-color: var(--success-color-light);
410
+ --upload-icon-color: var(--icon-color);
411
+ --upload-text-color: var(--text-color-light);
412
+ --progress-bg-color: var(--disabled-background);
413
+ --progress-color: var(--primary-color);
414
+ }
415
+ ```
416
+
417
+ ## License
418
+
419
+ MIT
Binary file
package/dist/index.css ADDED
@@ -0,0 +1 @@
1
+ .text-input[data-v-cc2497d0]{display:grid;gap:.5rem;width:100%;margin-top:.7rem}.text-input.label-top[data-v-cc2497d0]{grid-template-rows:auto 1fr}.text-input.label-left[data-v-cc2497d0]{grid-template-columns:30% 1fr;align-items:start;gap:1rem}.text-input.label-left .label[data-v-cc2497d0]{padding-top:.75rem;width:100%}.label[data-v-cc2497d0]{font-weight:500;color:var(--text-color);text-align:left}.label-align-left .label[data-v-cc2497d0]{text-align:left}.label-align-right .label[data-v-cc2497d0]{text-align:right}.label-align-center .label[data-v-cc2497d0]{text-align:center}.required[data-v-cc2497d0]{color:var(--danger-color);margin-left:.25rem}.input-wrapper[data-v-cc2497d0]{position:relative;display:grid;grid-template-columns:1fr;border:1px solid var(--border-color);border-radius:.5rem;transition:all .2s ease;width:100%;min-height:2rem;background:var(--input-bg-color)}.input-wrapper.has-icon[data-v-cc2497d0]{grid-template-columns:auto 1fr}.input-wrapper[data-v-cc2497d0]:focus-within{border-color:var(--primary-color);box-shadow:0 0 0 3px var(--shadow-color)}.input-wrapper.has-error[data-v-cc2497d0]{border-color:var(--danger-color);border-bottom-left-radius:0;border-bottom-right-radius:0}.icon-wrapper[data-v-cc2497d0]{display:grid;place-items:start;padding:1rem;border-right:1px solid var(--border-color);cursor:pointer;overflow:hidden}.icon-wrapper[data-v-cc2497d0]:hover{background-color:var(--input-bg-hover)}.icon[data-v-cc2497d0]{color:var(--text-muted);font-size:1rem}.input[data-v-cc2497d0]{padding:.75rem 1rem;border:none;outline:none;font-size:1rem;color:var(--text-color);background:transparent;width:100%;line-height:var(--line-height)}.input[data-v-cc2497d0]::placeholder{color:var(--text-muted)}.input[data-v-cc2497d0]:disabled{background-color:var(--input-bg-disabled);cursor:not-allowed}.message[data-v-cc2497d0]{position:absolute;bottom:-1.5rem;left:0;font-size:.75rem;white-space:nowrap}.error-message[data-v-cc2497d0]{position:absolute;bottom:0;left:0;right:0;padding:.25rem .75rem;background-color:var(--danger-color);color:#fff;font-size:.75rem;border-radius:0 0 .5rem .5rem;transform:translateY(100%);transition:transform .2s ease;line-height:var(--line-height);z-index:1}.success-message[data-v-cc2497d0]{position:absolute;bottom:-1.5rem;left:0;color:var(--success-color);font-size:.75rem;line-height:var(--line-height)}.status-indicator[data-v-cc2497d0]{position:absolute;top:-.1rem;right:.5rem;font-size:.75rem;color:var(--text-muted);line-height:0px;background-color:var(--input-bg-color);padding:0 .25rem}.saved-indicator[data-v-cc2497d0]{color:var(--success-color)}.changed-indicator[data-v-cc2497d0]{color:var(--warning-color)}.fade-enter-active[data-v-cc2497d0],.fade-leave-active[data-v-cc2497d0]{transition:opacity .2s ease}.fade-enter-from[data-v-cc2497d0],.fade-leave-to[data-v-cc2497d0]{opacity:0}textarea[data-v-cc2497d0]{min-height:var(--textarea-height, 5.5rem);max-height:var(--max-textarea-height, 14rem);overflow-y:auto;resize:none}.file-upload[data-v-75b2aeea]{width:100%;max-width:600px;margin:0 auto}.upload-area[data-v-75b2aeea]{border:2px dashed var(--upload-border-color);border-radius:.75rem;padding:2rem;text-align:center;cursor:pointer;transition:all .3s ease;background-color:var(--upload-bg-color)}.upload-area.is-dragging[data-v-75b2aeea]{border-color:var(--upload-dragging-border-color);background-color:var(--upload-dragging-bg-color)}.upload-area.has-files[data-v-75b2aeea]{border-color:var(--upload-has-files-border-color);background-color:var(--upload-has-files-bg-color)}.file-input[data-v-75b2aeea]{display:none}.upload-content[data-v-75b2aeea]{display:flex;flex-direction:column;align-items:center;gap:1rem}.upload-content[data-v-75b2aeea] svg{font-size:2rem;color:var(--upload-icon-color)}.selected-files[data-v-75b2aeea]{width:100%;text-align:left;max-height:200px;overflow-y:auto;font-size:.75rem;color:var(--upload-text-color)}.file-info[data-v-75b2aeea]{display:flex;justify-content:space-between;align-items:center;padding:.125rem 0;border-radius:.25rem;gap:.5rem;font-size:.75rem}.file-name[data-v-75b2aeea]{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:.5rem}.file-size[data-v-75b2aeea]{font-size:.7rem;flex-shrink:0}.error-message[data-v-75b2aeea]{color:var(--error-text-color);margin-top:1rem;font-size:.875rem}.progress-bar[data-v-75b2aeea]{height:.5rem;background-color:var(--progress-bg-color);border-radius:.25rem;margin-top:1rem;overflow:hidden}.progress[data-v-75b2aeea]{height:100%;background-color:var(--progress-color);transition:width .3s ease}.status-message[data-v-75b2aeea]{margin-top:1rem;padding:.5rem;border-radius:.25rem;font-size:.875rem}.status-message.success[data-v-75b2aeea]{background-color:var(--success-bg-color);color:var(--success-text-color)}.status-message.error[data-v-75b2aeea]{background-color:var(--error-bg-color);color:var(--error-text-color)}.upload-button[data-v-75b2aeea]{margin-top:1rem;padding:.5rem 1rem;background-color:var(--primary-color);color:#fff;border:none;border-radius:.25rem;cursor:pointer;font-size:.875rem;transition:background-color .3s ease}.upload-button[data-v-75b2aeea]:hover{background-color:var(--primary-color-light)}
package/dist/index.mjs ADDED
@@ -0,0 +1,300 @@
1
+ import { defineComponent as z, computed as q, ref as u, onUnmounted as H, resolveComponent as A, createElementBlock as s, openBlock as o, normalizeStyle as L, normalizeClass as I, createCommentVNode as c, createElementVNode as m, toDisplayString as g, createVNode as U, Transition as B, withCtx as M, watch as N, withModifiers as $, Fragment as P, renderList as R } from "vue";
2
+ const O = ["for"], X = ["id", "type", "value", "placeholder", "required", "disabled"], j = ["id", "value", "placeholder", "required", "disabled"], G = {
3
+ key: 3,
4
+ class: "status-indicator required-indicator"
5
+ }, K = {
6
+ key: 0,
7
+ class: "status-indicator saved-indicator"
8
+ }, Z = {
9
+ key: 0,
10
+ class: "status-indicator changed-indicator"
11
+ }, J = {
12
+ key: 4,
13
+ class: "error-message"
14
+ }, Q = {
15
+ key: 5,
16
+ class: "message success-message"
17
+ }, Y = /* @__PURE__ */ z({
18
+ __name: "TextInput",
19
+ props: {
20
+ modelValue: {},
21
+ label: {},
22
+ type: {},
23
+ icon: {},
24
+ placeholder: {},
25
+ required: { type: Boolean },
26
+ disabled: { type: Boolean },
27
+ error: {},
28
+ success: {},
29
+ labelPosition: {},
30
+ labelAlign: {},
31
+ totalWidth: {},
32
+ inputWidth: {},
33
+ labelWidth: {},
34
+ autosave: { type: Function },
35
+ isTextarea: { type: Boolean },
36
+ maxHeight: {},
37
+ height: {}
38
+ },
39
+ emits: ["update:modelValue", "changed", "saved"],
40
+ setup(v, { emit: b }) {
41
+ const r = v, p = b, h = q(() => `input-${Math.random().toString(36).substr(2, 9)}`), t = u(!1), f = u(!1), y = u(!1), i = u(null), d = u(null), k = u(null), T = q(() => r.label ? r.labelPosition === "left" && r.labelWidth ? {
42
+ "grid-template-columns": `${r.labelWidth} 1fr`
43
+ } : {} : {}), C = async (e) => {
44
+ if (r.autosave)
45
+ try {
46
+ await r.autosave(e), r.error || (p("saved"), t.value = !0, f.value = !1, setTimeout(() => {
47
+ t.value = !1;
48
+ }, 3e3));
49
+ } catch (_) {
50
+ console.error("Autosave failed:", _);
51
+ }
52
+ }, D = (e) => {
53
+ i.value && clearTimeout(i.value), d.value && clearTimeout(d.value), r.error || (f.value = !0), d.value = window.setTimeout(() => {
54
+ p("changed"), y.value = !0;
55
+ }, 500), i.value = window.setTimeout(() => {
56
+ C(e);
57
+ }, 1500);
58
+ }, E = () => {
59
+ var e;
60
+ (e = k.value) == null || e.focus();
61
+ }, S = (e) => {
62
+ e.style.height = "auto", e.style.height = `${e.scrollHeight}px`;
63
+ }, F = (e) => {
64
+ const _ = e.target.value;
65
+ p("update:modelValue", _), D(_), S(e.target);
66
+ };
67
+ return H(() => {
68
+ i.value && clearTimeout(i.value), d.value && clearTimeout(d.value);
69
+ }), (e, _) => {
70
+ const l = A("font-awesome-icon");
71
+ return o(), s("div", {
72
+ class: I(["text-input", {
73
+ [`label-${e.labelPosition}`]: e.label,
74
+ [`label-align-${e.labelAlign}`]: e.label
75
+ }]),
76
+ style: L([
77
+ { width: e.totalWidth || "100%" },
78
+ T.value,
79
+ {
80
+ "--max-textarea-height": r.maxHeight || r.height || "14rem",
81
+ "--textarea-height": r.height || "5.5rem"
82
+ }
83
+ ])
84
+ }, [
85
+ e.label ? (o(), s("label", {
86
+ key: 0,
87
+ for: h.value,
88
+ class: "label"
89
+ }, g(e.label), 9, O)) : c("", !0),
90
+ m("div", {
91
+ class: I(["input-wrapper", {
92
+ "has-error": e.error,
93
+ "has-icon": e.icon
94
+ }])
95
+ }, [
96
+ e.icon ? (o(), s("div", {
97
+ key: 0,
98
+ class: "icon-wrapper",
99
+ onClick: E
100
+ }, [
101
+ U(l, {
102
+ icon: e.icon,
103
+ class: "icon"
104
+ }, null, 8, ["icon"])
105
+ ])) : c("", !0),
106
+ e.isTextarea ? (o(), s("textarea", {
107
+ key: 2,
108
+ id: h.value,
109
+ value: e.modelValue,
110
+ placeholder: e.placeholder,
111
+ required: e.required,
112
+ disabled: e.disabled,
113
+ class: "input",
114
+ onInput: F,
115
+ ref_key: "inputRef",
116
+ ref: k
117
+ }, null, 40, j)) : (o(), s("input", {
118
+ key: 1,
119
+ id: h.value,
120
+ type: e.type,
121
+ value: e.modelValue,
122
+ placeholder: e.placeholder,
123
+ required: e.required,
124
+ disabled: e.disabled,
125
+ class: "input",
126
+ onInput: F,
127
+ ref_key: "inputRef",
128
+ ref: k
129
+ }, null, 40, X)),
130
+ e.required && !t.value && !f.value ? (o(), s("span", G, "required")) : c("", !0),
131
+ U(B, { name: "fade" }, {
132
+ default: M(() => [
133
+ t.value && !e.error ? (o(), s("span", K, "saved")) : c("", !0)
134
+ ]),
135
+ _: 1
136
+ }),
137
+ U(B, { name: "fade" }, {
138
+ default: M(() => [
139
+ f.value && !e.error ? (o(), s("span", Z, "changed")) : c("", !0)
140
+ ]),
141
+ _: 1
142
+ }),
143
+ e.error ? (o(), s("div", J, g(e.error), 1)) : c("", !0),
144
+ e.success ? (o(), s("span", Q, g(e.success), 1)) : c("", !0)
145
+ ], 2)
146
+ ], 6);
147
+ };
148
+ }
149
+ }), V = (v, b) => {
150
+ const r = v.__vccOpts || v;
151
+ for (const [p, h] of b)
152
+ r[p] = h;
153
+ return r;
154
+ }, x = /* @__PURE__ */ V(Y, [["__scopeId", "data-v-cc2497d0"]]), ee = { class: "file-upload" }, ae = { class: "upload-content" }, le = { key: 0 }, te = {
155
+ key: 1,
156
+ class: "selected-files"
157
+ }, se = { class: "file-name" }, oe = { class: "file-size" }, re = {
158
+ key: 0,
159
+ class: "error-message"
160
+ }, ne = {
161
+ key: 1,
162
+ class: "progress-bar"
163
+ }, ie = 20 * 1024 * 1024, ue = /* @__PURE__ */ z({
164
+ __name: "FileUpload",
165
+ props: {
166
+ icon: {},
167
+ uploadUrl: {}
168
+ },
169
+ emits: ["upload-complete", "upload-error", "files-selected", "start-upload"],
170
+ setup(v, { emit: b }) {
171
+ const r = v, p = b, h = u(null), t = u([]), f = u(!1), y = u(0), i = u(""), d = u(null), k = (l) => {
172
+ if (l === 0) return "0 Bytes";
173
+ const a = 1024, n = ["Bytes", "KB", "MB", "GB"], w = Math.floor(Math.log(l) / Math.log(a));
174
+ return parseFloat((l / Math.pow(a, w)).toFixed(2)) + " " + n[w];
175
+ }, T = (l) => l.size > ie ? (i.value = `File "${l.name}" exceeds the maximum size of 20MB`, !1) : !0, C = () => {
176
+ f.value = !0;
177
+ }, D = () => {
178
+ f.value = !1;
179
+ }, E = (l) => {
180
+ var a;
181
+ if (f.value = !1, i.value = "", (a = l.dataTransfer) != null && a.files) {
182
+ const n = Array.from(l.dataTransfer.files);
183
+ n.every(T) && (t.value = [...t.value, ...n]);
184
+ }
185
+ }, S = () => {
186
+ var l;
187
+ (l = h.value) == null || l.click();
188
+ }, F = (l) => {
189
+ i.value = "";
190
+ const a = l.target;
191
+ if (a.files) {
192
+ const n = Array.from(a.files);
193
+ n.every(T) && (t.value = [...t.value, ...n]);
194
+ }
195
+ a.value = "";
196
+ }, e = async () => {
197
+ if (!r.uploadUrl) {
198
+ i.value = "No upload URL provided";
199
+ return;
200
+ }
201
+ if (t.value.length === 0) {
202
+ i.value = "No files selected";
203
+ return;
204
+ }
205
+ const l = new FormData();
206
+ t.value.forEach((a) => {
207
+ l.append("files", a);
208
+ });
209
+ try {
210
+ const a = new XMLHttpRequest();
211
+ a.upload.addEventListener("progress", (n) => {
212
+ n.lengthComputable && (y.value = n.loaded / n.total * 100);
213
+ }), a.addEventListener("load", () => {
214
+ if (a.status >= 200 && a.status < 300)
215
+ d.value = {
216
+ type: "success",
217
+ message: "Upload completed successfully"
218
+ }, p("upload-complete", t.value), t.value = [], y.value = 0;
219
+ else
220
+ throw new Error(`Upload failed with status ${a.status}`);
221
+ }), a.addEventListener("error", () => {
222
+ throw new Error("Upload failed");
223
+ }), a.open("POST", r.uploadUrl), a.send(l);
224
+ } catch (a) {
225
+ const n = a instanceof Error ? a.message : "Upload failed";
226
+ i.value = n, d.value = {
227
+ type: "error",
228
+ message: n
229
+ }, p("upload-error", n);
230
+ }
231
+ }, _ = () => {
232
+ p("start-upload", t.value);
233
+ };
234
+ return N(t, (l) => {
235
+ l.length > 0 && (r.uploadUrl ? e() : p("files-selected", l));
236
+ }), (l, a) => {
237
+ const n = A("font-awesome-icon");
238
+ return o(), s("div", ee, [
239
+ m("div", {
240
+ class: I(["upload-area", { "is-dragging": f.value, "has-files": t.value.length > 0 }]),
241
+ onDragenter: $(C, ["prevent"]),
242
+ onDragleave: $(D, ["prevent"]),
243
+ onDragover: a[0] || (a[0] = $(() => {
244
+ }, ["prevent"])),
245
+ onDrop: $(E, ["prevent"]),
246
+ onClick: S
247
+ }, [
248
+ m("input", {
249
+ ref_key: "fileInput",
250
+ ref: h,
251
+ type: "file",
252
+ multiple: "",
253
+ class: "file-input",
254
+ onChange: F
255
+ }, null, 544),
256
+ m("div", ae, [
257
+ U(n, {
258
+ icon: ["fas", l.icon || "upload"]
259
+ }, null, 8, ["icon"]),
260
+ t.value.length === 0 ? (o(), s("p", le, "Drag & drop files here or click to select")) : (o(), s("div", te, [
261
+ m("p", null, g(t.value.length) + " file(s) selected", 1),
262
+ (o(!0), s(P, null, R(t.value, (w, W) => (o(), s("div", {
263
+ key: W,
264
+ class: "file-info"
265
+ }, [
266
+ m("span", se, g(w.name), 1),
267
+ m("span", oe, g(k(w.size)), 1)
268
+ ]))), 128))
269
+ ]))
270
+ ])
271
+ ], 34),
272
+ i.value ? (o(), s("div", re, g(i.value), 1)) : c("", !0),
273
+ y.value > 0 && y.value < 100 ? (o(), s("div", ne, [
274
+ m("div", {
275
+ class: "progress",
276
+ style: L({ width: `${y.value}%` })
277
+ }, null, 4)
278
+ ])) : c("", !0),
279
+ d.value ? (o(), s("div", {
280
+ key: 2,
281
+ class: I(["status-message", d.value.type])
282
+ }, g(d.value.message), 3)) : c("", !0),
283
+ t.value.length > 0 && !l.uploadUrl ? (o(), s("button", {
284
+ key: 3,
285
+ class: "upload-button",
286
+ onClick: _
287
+ }, " Upload Files ")) : c("", !0)
288
+ ]);
289
+ };
290
+ }
291
+ }), de = /* @__PURE__ */ V(ue, [["__scopeId", "data-v-75b2aeea"]]), pe = {
292
+ install: (v) => {
293
+ v.component("TextInput", x), v.component("FileUpload", de);
294
+ }
295
+ };
296
+ export {
297
+ de as FileUpload,
298
+ x as TextInput,
299
+ pe as default
300
+ };
@@ -0,0 +1 @@
1
+ (function(d,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(d=typeof globalThis<"u"?globalThis:d||self,e(d.VueInputComponents={},d.Vue))})(this,function(d,e){"use strict";const S=["for"],D=["id","type","value","placeholder","required","disabled"],F=["id","value","placeholder","required","disabled"],U={key:3,class:"status-indicator required-indicator"},$={key:0,class:"status-indicator saved-indicator"},I={key:0,class:"status-indicator changed-indicator"},M={key:4,class:"error-message"},z={key:5,class:"message success-message"},q=e.defineComponent({__name:"TextInput",props:{modelValue:{},label:{},type:{},icon:{},placeholder:{},required:{type:Boolean},disabled:{type:Boolean},error:{},success:{},labelPosition:{},labelAlign:{},totalWidth:{},inputWidth:{},labelWidth:{},autosave:{type:Function},isTextarea:{type:Boolean},maxHeight:{},height:{}},emits:["update:modelValue","changed","saved"],setup(p,{emit:g}){const n=p,c=g,m=e.computed(()=>`input-${Math.random().toString(36).substr(2,9)}`),a=e.ref(!1),u=e.ref(!1),f=e.ref(!1),r=e.ref(null),i=e.ref(null),k=e.ref(null),B=e.computed(()=>n.label?n.labelPosition==="left"&&n.labelWidth?{"grid-template-columns":`${n.labelWidth} 1fr`}:{}:{}),E=async t=>{if(n.autosave)try{await n.autosave(t),n.error||(c("saved"),a.value=!0,u.value=!1,setTimeout(()=>{a.value=!1},3e3))}catch(h){console.error("Autosave failed:",h)}},b=t=>{r.value&&clearTimeout(r.value),i.value&&clearTimeout(i.value),n.error||(u.value=!0),i.value=window.setTimeout(()=>{c("changed"),f.value=!0},500),r.value=window.setTimeout(()=>{E(t)},1500)},w=()=>{var t;(t=k.value)==null||t.focus()},V=t=>{t.style.height="auto",t.style.height=`${t.scrollHeight}px`},_=t=>{const h=t.target.value;c("update:modelValue",h),b(h),V(t.target)};return e.onUnmounted(()=>{r.value&&clearTimeout(r.value),i.value&&clearTimeout(i.value)}),(t,h)=>{const l=e.resolveComponent("font-awesome-icon");return e.openBlock(),e.createElementBlock("div",{class:e.normalizeClass(["text-input",{[`label-${t.labelPosition}`]:t.label,[`label-align-${t.labelAlign}`]:t.label}]),style:e.normalizeStyle([{width:t.totalWidth||"100%"},B.value,{"--max-textarea-height":n.maxHeight||n.height||"14rem","--textarea-height":n.height||"5.5rem"}])},[t.label?(e.openBlock(),e.createElementBlock("label",{key:0,for:m.value,class:"label"},e.toDisplayString(t.label),9,S)):e.createCommentVNode("",!0),e.createElementVNode("div",{class:e.normalizeClass(["input-wrapper",{"has-error":t.error,"has-icon":t.icon}])},[t.icon?(e.openBlock(),e.createElementBlock("div",{key:0,class:"icon-wrapper",onClick:w},[e.createVNode(l,{icon:t.icon,class:"icon"},null,8,["icon"])])):e.createCommentVNode("",!0),t.isTextarea?(e.openBlock(),e.createElementBlock("textarea",{key:2,id:m.value,value:t.modelValue,placeholder:t.placeholder,required:t.required,disabled:t.disabled,class:"input",onInput:_,ref_key:"inputRef",ref:k},null,40,F)):(e.openBlock(),e.createElementBlock("input",{key:1,id:m.value,type:t.type,value:t.modelValue,placeholder:t.placeholder,required:t.required,disabled:t.disabled,class:"input",onInput:_,ref_key:"inputRef",ref:k},null,40,D)),t.required&&!a.value&&!u.value?(e.openBlock(),e.createElementBlock("span",U,"required")):e.createCommentVNode("",!0),e.createVNode(e.Transition,{name:"fade"},{default:e.withCtx(()=>[a.value&&!t.error?(e.openBlock(),e.createElementBlock("span",$,"saved")):e.createCommentVNode("",!0)]),_:1}),e.createVNode(e.Transition,{name:"fade"},{default:e.withCtx(()=>[u.value&&!t.error?(e.openBlock(),e.createElementBlock("span",I,"changed")):e.createCommentVNode("",!0)]),_:1}),t.error?(e.openBlock(),e.createElementBlock("div",M,e.toDisplayString(t.error),1)):e.createCommentVNode("",!0),t.success?(e.openBlock(),e.createElementBlock("span",z,e.toDisplayString(t.success),1)):e.createCommentVNode("",!0)],2)],6)}}}),C=(p,g)=>{const n=p.__vccOpts||p;for(const[c,m]of g)n[c]=m;return n},N=C(q,[["__scopeId","data-v-cc2497d0"]]),v={class:"file-upload"},A={class:"upload-content"},L={key:0},P={key:1,class:"selected-files"},W={class:"file-name"},H={class:"file-size"},R={key:0,class:"error-message"},j={key:1,class:"progress-bar"},O=20*1024*1024,T=C(e.defineComponent({__name:"FileUpload",props:{icon:{},uploadUrl:{}},emits:["upload-complete","upload-error","files-selected","start-upload"],setup(p,{emit:g}){const n=p,c=g,m=e.ref(null),a=e.ref([]),u=e.ref(!1),f=e.ref(0),r=e.ref(""),i=e.ref(null),k=l=>{if(l===0)return"0 Bytes";const o=1024,s=["Bytes","KB","MB","GB"],y=Math.floor(Math.log(l)/Math.log(o));return parseFloat((l/Math.pow(o,y)).toFixed(2))+" "+s[y]},B=l=>l.size>O?(r.value=`File "${l.name}" exceeds the maximum size of 20MB`,!1):!0,E=()=>{u.value=!0},b=()=>{u.value=!1},w=l=>{var o;if(u.value=!1,r.value="",(o=l.dataTransfer)!=null&&o.files){const s=Array.from(l.dataTransfer.files);s.every(B)&&(a.value=[...a.value,...s])}},V=()=>{var l;(l=m.value)==null||l.click()},_=l=>{r.value="";const o=l.target;if(o.files){const s=Array.from(o.files);s.every(B)&&(a.value=[...a.value,...s])}o.value=""},t=async()=>{if(!n.uploadUrl){r.value="No upload URL provided";return}if(a.value.length===0){r.value="No files selected";return}const l=new FormData;a.value.forEach(o=>{l.append("files",o)});try{const o=new XMLHttpRequest;o.upload.addEventListener("progress",s=>{s.lengthComputable&&(f.value=s.loaded/s.total*100)}),o.addEventListener("load",()=>{if(o.status>=200&&o.status<300)i.value={type:"success",message:"Upload completed successfully"},c("upload-complete",a.value),a.value=[],f.value=0;else throw new Error(`Upload failed with status ${o.status}`)}),o.addEventListener("error",()=>{throw new Error("Upload failed")}),o.open("POST",n.uploadUrl),o.send(l)}catch(o){const s=o instanceof Error?o.message:"Upload failed";r.value=s,i.value={type:"error",message:s},c("upload-error",s)}},h=()=>{c("start-upload",a.value)};return e.watch(a,l=>{l.length>0&&(n.uploadUrl?t():c("files-selected",l))}),(l,o)=>{const s=e.resolveComponent("font-awesome-icon");return e.openBlock(),e.createElementBlock("div",v,[e.createElementVNode("div",{class:e.normalizeClass(["upload-area",{"is-dragging":u.value,"has-files":a.value.length>0}]),onDragenter:e.withModifiers(E,["prevent"]),onDragleave:e.withModifiers(b,["prevent"]),onDragover:o[0]||(o[0]=e.withModifiers(()=>{},["prevent"])),onDrop:e.withModifiers(w,["prevent"]),onClick:V},[e.createElementVNode("input",{ref_key:"fileInput",ref:m,type:"file",multiple:"",class:"file-input",onChange:_},null,544),e.createElementVNode("div",A,[e.createVNode(s,{icon:["fas",l.icon||"upload"]},null,8,["icon"]),a.value.length===0?(e.openBlock(),e.createElementBlock("p",L,"Drag & drop files here or click to select")):(e.openBlock(),e.createElementBlock("div",P,[e.createElementVNode("p",null,e.toDisplayString(a.value.length)+" file(s) selected",1),(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(a.value,(y,G)=>(e.openBlock(),e.createElementBlock("div",{key:G,class:"file-info"},[e.createElementVNode("span",W,e.toDisplayString(y.name),1),e.createElementVNode("span",H,e.toDisplayString(k(y.size)),1)]))),128))]))])],34),r.value?(e.openBlock(),e.createElementBlock("div",R,e.toDisplayString(r.value),1)):e.createCommentVNode("",!0),f.value>0&&f.value<100?(e.openBlock(),e.createElementBlock("div",j,[e.createElementVNode("div",{class:"progress",style:e.normalizeStyle({width:`${f.value}%`})},null,4)])):e.createCommentVNode("",!0),i.value?(e.openBlock(),e.createElementBlock("div",{key:2,class:e.normalizeClass(["status-message",i.value.type])},e.toDisplayString(i.value.message),3)):e.createCommentVNode("",!0),a.value.length>0&&!l.uploadUrl?(e.openBlock(),e.createElementBlock("button",{key:3,class:"upload-button",onClick:h}," Upload Files ")):e.createCommentVNode("",!0)])}}}),[["__scopeId","data-v-75b2aeea"]]),X={install:p=>{p.component("TextInput",N),p.component("FileUpload",T)}};d.FileUpload=T,d.TextInput=N,d.default=X,Object.defineProperties(d,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@a-vision-software/vue-input-components",
3
+ "version": "1.0.0",
4
+ "description": "A collection of reusable Vue 3 input components with TypeScript support",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "src/assets/colors.css"
10
+ ],
11
+ "scripts": {
12
+ "dev": "vite",
13
+ "build": "run-p type-check \"build-only {@}\" --",
14
+ "preview": "vite preview",
15
+ "build-only": "vite build && vue-tsc --declaration --emitDeclarationOnly",
16
+ "type-check": "vue-tsc --noEmit",
17
+ "lint": "eslint . --fix",
18
+ "format": "prettier --write src/"
19
+ },
20
+ "keywords": [
21
+ "vue",
22
+ "vue3",
23
+ "components",
24
+ "input",
25
+ "form",
26
+ "typescript"
27
+ ],
28
+ "author": "",
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "@fortawesome/fontawesome-svg-core": "^6.7.2",
32
+ "@fortawesome/free-regular-svg-icons": "^6.7.2",
33
+ "@fortawesome/free-solid-svg-icons": "^6.7.2",
34
+ "@fortawesome/vue-fontawesome": "^3.0.8",
35
+ "vue": "^3.5.13"
36
+ },
37
+ "peerDependencies": {
38
+ "vue": "^3.5.13"
39
+ },
40
+ "devDependencies": {
41
+ "@tsconfig/node22": "^22.0.1",
42
+ "@types/node": "^22.14.0",
43
+ "@vitejs/plugin-vue": "^5.2.3",
44
+ "@vue/eslint-config-prettier": "^10.2.0",
45
+ "@vue/eslint-config-typescript": "^14.5.0",
46
+ "@vue/tsconfig": "^0.7.0",
47
+ "eslint": "^9.22.0",
48
+ "eslint-plugin-vue": "~10.0.0",
49
+ "jiti": "^2.4.2",
50
+ "npm-run-all2": "^7.0.2",
51
+ "prettier": "3.5.3",
52
+ "typescript": "~5.8.0",
53
+ "vite": "^6.2.4",
54
+ "vite-plugin-vue-devtools": "^7.7.2",
55
+ "vue-tsc": "^2.2.8"
56
+ }
57
+ }
@@ -0,0 +1,67 @@
1
+ :root {
2
+ /* Base colors */
3
+ --primary-color: #3498db;
4
+ --primary-color-light: rgba(52, 152, 219, 0.2);
5
+ --secondary-color: #2ecc71;
6
+
7
+ /* Text colors */
8
+ --text-color: #2c3e50;
9
+ --text-color-light: #7f8c8d;
10
+
11
+ /* UI colors */
12
+ --border-color: #dcdfe6;
13
+ --icon-color: #95a5a6;
14
+
15
+ /* State colors */
16
+ --error-color: #e74c3c;
17
+ --error-color-light: rgba(231, 76, 60, 0.2);
18
+ --success-color: #2ecc71;
19
+ --success-color-light: rgba(46, 204, 113, 0.2);
20
+ --warning-color: #f1c40f;
21
+ --warning-color-light: rgba(241, 196, 15, 0.2);
22
+
23
+ /* Background colors */
24
+ --disabled-color: #bdc3c7;
25
+ --disabled-background: #f5f7fa;
26
+ --card-bg: #ffffff;
27
+ --background-color: #f8f9fa;
28
+
29
+ /* Input colors */
30
+ --input-bg-color: rgba(255, 255, 255, 0.8);
31
+ --input-bg-hover: rgba(0, 0, 0, 0.05);
32
+ --input-bg-disabled: rgba(0, 0, 0, 0.05);
33
+
34
+ /* Text Colors */
35
+ --text-muted: #6c757d;
36
+
37
+ /* Border Colors */
38
+ --input-bg: #ffffff;
39
+ --input-bg-disabled: #e9ecef;
40
+
41
+ /* Status Colors */
42
+ --danger-color: #dc3545;
43
+ --success-color: #28a745;
44
+ --warning-color: #ffc107;
45
+
46
+ /* Shadow Colors */
47
+ --shadow-color: rgba(74, 108, 247, 0.25);
48
+
49
+ /* Typography */
50
+ --line-height: 20px;
51
+
52
+ /* File Upload Colors */
53
+ --upload-border-color: var(--border-color);
54
+ --upload-bg-color: var(--background-color);
55
+ --upload-dragging-border-color: var(--primary-color);
56
+ --upload-dragging-bg-color: var(--primary-color-light);
57
+ --upload-has-files-border-color: var(--success-color);
58
+ --upload-has-files-bg-color: var(--success-color-light);
59
+ --upload-icon-color: var(--icon-color);
60
+ --upload-text-color: var(--text-color-light);
61
+ --progress-bg-color: var(--disabled-background);
62
+ --progress-color: var(--primary-color);
63
+ --success-bg-color: var(--success-color-light);
64
+ --success-text-color: var(--success-color);
65
+ --error-bg-color: var(--error-color-light);
66
+ --error-text-color: var(--error-color);
67
+ }