pns-component-library 1.5.8 → 1.5.10

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 CHANGED
@@ -1,79 +1,643 @@
1
- # PNS Component Library Vue 3 + Vite
1
+ # PNS Component Library
2
2
 
3
- This library provides some ready-to-use responsive components with Vue 3. Developers can directly use the components without setting the non-web UI and UX (all included in the library).
3
+ A comprehensive Vue 3 component library built with modern development practices, providing reusable UI components, composables, and directives for building robust web applications.
4
4
 
5
- ## How to Set Up
6
- You have already created a vue project.
7
- ### Step 1:
8
- ```
5
+ ## 🚀 Features
6
+
7
+ - **9 Production-Ready Components** - Form controls, layout components, and utility components
8
+ - **2 Powerful Composables** - Reusable logic for common patterns
9
+ - **2 Custom Directives** - Loading states and UI enhancements
10
+ - **Vue 3 Composition API** - Modern, performant, and type-safe
11
+ - **Responsive Design** - Mobile-first approach with breakpoint handling
12
+ - **Accessibility** - ARIA attributes and keyboard navigation support
13
+ - **Customizable Themes** - Multiple built-in color schemes
14
+
15
+ ## 📦 Installation
16
+
17
+ ```bash
9
18
  npm install pns-component-library
10
19
  ```
11
- ### Step 2:
12
- In your main.js, import the library and use it for your app instance.
20
+ or
21
+
22
+ ```bash
23
+ npm install pns-component-library@latest
24
+ ```
25
+
26
+ ## 🎯 Quick Start
27
+
28
+ ### 1. Install and Setup
29
+
30
+ First, import and register the library in your `main.js`:
31
+
32
+ ```javascript
33
+ // main.js
34
+ import { createApp } from 'vue'
35
+ import App from './App.vue'
36
+ import PnsComponentLibrary from 'pns-component-library'
37
+ import 'pns-component-library/style.css'
38
+
39
+ const app = createApp(App)
40
+ app.use(PnsComponentLibrary)
41
+ app.mount('#app')
42
+ ```
43
+
44
+ ### 2. Use Components
45
+
46
+ ```vue
47
+ <template>
48
+ <div>
49
+ <!-- Basic checkbox -->
50
+ <Checkbox v-model="isChecked" label="Accept terms" />
51
+
52
+ <!-- Phone input with country selection -->
53
+ <AreaCodePhoneInput v-model="phoneData" />
54
+
55
+ <!-- Responsive button -->
56
+ <ResponsiveButton @click="handleClick">
57
+ Click me
58
+ </ResponsiveButton>
59
+ </div>
60
+ </template>
61
+
62
+ <script setup>
63
+ import { ref } from 'vue'
64
+
65
+ const isChecked = ref(false)
66
+ const phoneData = ref({ area_code: '', phone_number: '' })
67
+
68
+ const handleClick = () => {
69
+ console.log('Button clicked!')
70
+ }
71
+ </script>
72
+ ```
73
+
74
+ ## 📚 Components
75
+
76
+ ### Form Components
77
+
78
+ #### Checkbox
79
+ Customizable checkbox with multiple themes and validation support.
80
+
81
+ ##### Attributes
82
+
83
+ | Attribute | Description | Type | Default |
84
+ |-----------|-------------|------|---------|
85
+ | model-value / v-model | binding value - `true` when checked, `false` when unchecked | `boolean` | `false` |
86
+ | label | checkbox label | `string` | — |
87
+ | size | checkbox size | `'small' \| 'medium'` | `'medium'` |
88
+ | built-in-theme | color theme | `'green' \| 'primary-blue' \| 'secondary-blue' \| 'red'` | `'green'` |
89
+ | disabled | whether checkbox is disabled | `boolean` | `false` |
90
+ | indeterminate | whether checkbox is indeterminate | `boolean` | `false` |
91
+
92
+ ##### Events
93
+
94
+ | Event | Description | Parameters |
95
+ |-------|-------------|------------|
96
+ | change | triggers when the binding value changes | `(value: boolean)` |
97
+
98
+ ##### Exposes
99
+
100
+ | Method | Description | Type |
101
+ |--------|-------------|------|
102
+ | focus | focus the checkbox | `() => void` |
103
+ | blur | blur the checkbox | `() => void` |
104
+ | alert | show error message | `(message: string) => void` |
105
+ | removeAlertOrErrorEffect | clear error state | `() => void` |
106
+
107
+ ```vue
108
+ <template>
109
+ <Checkbox
110
+ v-model="agreed"
111
+ label="I agree to terms"
112
+ built_in_theme="primary-blue"
113
+ @change="handleChange"
114
+ />
115
+ </template>
116
+ ```
117
+
118
+ #### GroupCheckbox
119
+ Container for managing multiple related checkboxes as a group.
120
+
121
+ ##### Attributes
122
+
123
+ | Attribute | Description | Type | Default |
124
+ |-----------|-------------|------|---------|
125
+ | model-value / v-model | binding value - array of selected checkbox labels from child Checkbox components | `array` | `[]` |
126
+
127
+ ##### Events
128
+
129
+ | Event | Description | Parameters |
130
+ |-------|-------------|------------|
131
+ | change | triggers when the binding value changes | `(value: array)` |
132
+
133
+ ```vue
134
+ <template>
135
+ <GroupCheckbox v-model="selectedItems">
136
+ <Checkbox v-for="item in options" :key="item" :label="item" />
137
+ </GroupCheckbox>
138
+ </template>
139
+
140
+ <script setup>
141
+ const selectedItems = ref(['option1'])
142
+ const options = ['option1', 'option2', 'option3']
143
+ </script>
144
+ ```
145
+
146
+ #### AreaCodePhoneInput
147
+ International phone number input with country selection and area code handling.
148
+
149
+ ##### Attributes
150
+
151
+ | Attribute | Description | Type | Default |
152
+ |-----------|-------------|------|---------|
153
+ | model-value / v-model | binding value - object containing `area_code` (country code like "+1") and `phone_number` (local number) | `{area_code: string, phone_number: string}` | `{area_code: '', phone_number: ''}` |
154
+ | country-options | custom country list | `array` | — |
155
+ | enable-backup-country-options | use built-in country list | `boolean` | `true` |
156
+ | country-filterable | enable country search | `boolean` | `false` |
157
+ | disabled | whether input is disabled | `boolean` | `false` |
158
+
159
+ ##### Events
160
+
161
+ | Event | Description | Parameters |
162
+ |-------|-------------|------------|
163
+ | change | triggers when the binding value changes | `(value: object)` |
164
+ | focus | triggers when input is focused | `(event: FocusEvent)` |
165
+ | blur | triggers when input is blurred | `(event: FocusEvent)` |
166
+ | clear | triggers when clear button is clicked | — |
167
+
168
+ ```vue
169
+ <template>
170
+ <AreaCodePhoneInput
171
+ v-model="phoneData"
172
+ :country_filterable="true"
173
+ @change="handlePhoneChange"
174
+ />
175
+ </template>
176
+ ```
177
+
178
+ #### InputBox
179
+ Versatile input field component with validation and styling options.
180
+
181
+ ##### Attributes
182
+
183
+ | Attribute | Description | Type | Default |
184
+ |-----------|-------------|------|---------|
185
+ | model-value / v-model | binding value - the current input text value | `string` | — |
186
+ | type | input type | `string` | `'text'` |
187
+ | placeholder | placeholder text | `string` | — |
188
+ | clearable | whether to show clear button | `boolean` | `false` |
189
+ | disabled | whether input is disabled | `boolean` | `false` |
190
+
191
+ ##### Events
192
+
193
+ | Event | Description | Parameters |
194
+ |-------|-------------|------------|
195
+ | input | triggers when input value changes | `(value: string)` |
196
+ | change | triggers when input value changes and loses focus | `(value: string)` |
197
+ | focus | triggers when input is focused | `(event: FocusEvent)` |
198
+ | blur | triggers when input is blurred | `(event: FocusEvent)` |
199
+ | clear | triggers when clear button is clicked | — |
200
+
201
+ ##### Exposes
202
+
203
+ | Method | Description | Type |
204
+ |--------|-------------|------|
205
+ | focus | focus the input | `() => void` |
206
+ | blur | blur the input | `() => void` |
207
+ | alert | show alert message | `(message: string) => void` |
208
+ | error | show error message | `(message: string) => void` |
209
+ | removeAlertOrErrorEffect | clear alert/error state | `() => void` |
210
+
211
+ ##### Slots
212
+
213
+ | Name | Description |
214
+ |------|-------------|
215
+ | prefix | content before input |
216
+ | suffix | content after input |
217
+
13
218
  ```vue
14
- import { createApp } from 'vue';
15
- import PNSComponentLibray from "pns-component-library";
16
-
17
- const app = createApp(App);
18
- app.use(PNSComponentLibray);
19
- ```
20
- ### Step 3
21
- Now you should be able to access the components, directives and icons in the library.
22
-
23
- ## Components:
24
- - SimplifiedNotification
25
- - InputBox
26
- - SingleSelector
27
- - ResponsiveButton
28
- - Checkbox
29
- - Radio (support group radio)
30
- - WholePageErrorPopup
31
-
32
- ## Directives:
33
- - component-loading
34
-
35
- ## Composables:
36
- - useWindowSize
37
- - FloatingNotification
38
-
39
- ## Icons:
40
- If `npm install pns-component-library`, and want to use any following icons: <br>
41
- <pre>import { iconsMap } from "pns-component-library</pre>
42
- iconsMap.[iconName] is the URL or Data URI (Uniform Resource Identifier) of the corresponding svg icon.
43
- Eg.
219
+ <InputBox
220
+ v-model="value"
221
+ type="text"
222
+ placeholder="Enter text"
223
+ clearable
224
+ >
225
+ <template #prefix>
226
+ <Icon name="search" />
227
+ </template>
228
+ </InputBox>
229
+ ```
230
+
231
+ #### Radio
232
+ Radio button component for single selection from multiple options.
233
+
234
+ ##### Attributes
235
+
236
+ | Attribute | Description | Type | Default |
237
+ |-----------|-------------|------|---------|
238
+ | model-value / v-model | binding value - the `value` of the currently selected radio button in the group | `string` | — |
239
+ | value | radio value | `string` | — |
240
+ | label | radio label | `string` | — |
241
+ | name | group name for radio group | `string` | — |
242
+ | size | radio size | `'small' \| 'medium'` | `'medium'` |
243
+ | built-in-theme | color theme | `'primary-blue' \| 'secondary-blue' \| 'red'` | `'primary-blue'` |
244
+ | customized-class | custom CSS class | `string` | — |
245
+ | disabled | whether radio is disabled | `boolean` | `false` |
246
+ | autofocus | whether to auto focus | `boolean` | `false` |
247
+
248
+ ##### Events
249
+
250
+ | Event | Description | Parameters |
251
+ |-------|-------------|------------|
252
+ | change | triggers when the binding value changes | `(value: string)` |
253
+
254
+ ##### Exposes
255
+
256
+ | Method | Description | Type |
257
+ |--------|-------------|------|
258
+ | focus | focus the radio | `() => void` |
259
+ | blur | blur the radio | `() => void` |
260
+ | alert | show alert message | `(message: string) => void` |
261
+ | removeAlertOrErrorEffect | clear alert/error state | `() => void` |
262
+
263
+ ```vue
264
+ <!-- Radio Group -->
265
+ <Radio v-model="selected" value="option1" name="group1" label="Option 1" />
266
+ <Radio v-model="selected" value="option2" name="group1" label="Option 2" />
267
+
268
+ <!-- Single Radio -->
269
+ <Radio v-model="single" value="yes" label="Agree to terms" />
270
+ ```
271
+
272
+ #### SingleSelector
273
+ Dropdown selector component with filtering and search capabilities.
274
+
275
+ ##### Attributes
276
+
277
+ | Attribute | Description | Type | Default |
278
+ |-----------|-------------|------|---------|
279
+ | model-value / v-model | binding value - the `value` property of the selected option from the options array | `string \| number \| boolean \| object \| array` | — |
280
+ | options | options array | `Array<{value: any, label: string}>` | `[]` |
281
+ | filterable | whether to enable filtering | `boolean` | `false` |
282
+ | remote-search | whether to enable remote search | `boolean` | `false` |
283
+ | disabled | whether selector is disabled | `boolean` | `false` |
284
+ | placeholder | placeholder text | `string` | `'Select'` |
285
+ | clearable | whether to show clear button | `boolean` | `false` |
286
+ | loading | loading state | `boolean` | `false` |
287
+ | loading-text | loading text | `string` | `'Loading'` |
288
+ | no-data-text | no data text | `string` | `'No data'` |
289
+
290
+ ##### Events
291
+
292
+ | Event | Description | Parameters |
293
+ |-------|-------------|------------|
294
+ | change | triggers when the binding value changes | `(value: any)` |
295
+ | remote-search | triggers when remote search is performed | `(query: string)` |
296
+ | filter-change | triggers when filter text changes | `(query: string)` |
297
+ | visible-change | triggers when dropdown visibility changes | `(visible: boolean)` |
298
+ | focus | triggers when selector is focused | `(event: FocusEvent)` |
299
+ | blur | triggers when selector is blurred | `(event: FocusEvent)` |
300
+ | clear | triggers when clear button is clicked | — |
301
+
302
+ ##### Exposes
303
+
304
+ | Method | Description | Type |
305
+ |--------|-------------|------|
306
+ | focus | focus the selector | `() => void` |
307
+ | blur | blur the selector | `() => void` |
308
+ | alert | show alert message | `(message: string) => void` |
309
+ | error | show error message | `(message: string) => void` |
310
+ | removeAlertOrErrorEffect | clear alert/error state | `() => void` |
311
+ | setDropdownContentLoading | set loading state | `(loading: boolean) => void` |
312
+
313
+ ##### Slots
314
+
315
+ | Name | Description |
316
+ |------|-------------|
317
+ | prefix | content before selector |
318
+ | suffix | content after selector |
319
+
320
+ ```vue
321
+ <SingleSelector
322
+ v-model="selected"
323
+ :options="options"
324
+ filterable
325
+ clearable
326
+ placeholder="Choose option"
327
+ >
328
+ <template #prefix>
329
+ <Icon name="search" />
330
+ </template>
331
+ </SingleSelector>
332
+ ```
333
+
334
+ ### Layout Components
335
+
336
+ #### ResponsiveButton
337
+ Feature-rich button component with responsive behavior and multiple states.
338
+
339
+ ##### Attributes
340
+
341
+ | Attribute | Description | Type | Default |
342
+ |-----------|-------------|------|---------|
343
+ | display-name | button text | `string` | `'button'` |
344
+ | size | button size | `'xsmall' \| 'small' \| 'medium' \| 'large'` | `'small'` |
345
+ | width-type | button width type | `'fill-whole' \| 'fit-content'` | `'fit-content'` |
346
+ | build-in-theme | color theme | `'outlined-primary' \| 'filled-primary' \| 'text-primary' \| 'outlined-secondary' \| 'filled-secondary' \| 'text-secondary'` | `'outlined-primary'` |
347
+ | customized-class | custom CSS class | `string` | — |
348
+ | hold | whether button is in hold state | `boolean` | `false` |
349
+ | disabled | whether button is disabled | `boolean` | `false` |
350
+ | dropdown-options | dropdown menu options | `array` | `[]` |
351
+ | is-dropdown-option | whether this is a dropdown item | `boolean` | `false` |
352
+
353
+ ##### Events
354
+
355
+ | Event | Description | Parameters |
356
+ |-------|-------------|------------|
357
+ | click | triggers when button is clicked | `(event: MouseEvent)` |
358
+ | update-currently-hovered-dropdown-option | triggers when dropdown option is hovered | `(option: object)` |
359
+ | add-nested-dropdown-history-stack | triggers when nested dropdown is navigated | `(option: object)` |
360
+ | remove-nested-dropdown-history-stack | triggers when navigating back in nested dropdown | — |
361
+
362
+ ##### Slots
363
+
364
+ | Name | Description |
365
+ |------|-------------|
366
+ | prefix | content before button text |
367
+ | suffix | content after button text |
368
+
44
369
  ```vue
45
- <template>
46
- <img :src = "iconsMap['eye_icon']" />
370
+ <ResponsiveButton
371
+ display_name="Save Changes"
372
+ size="medium"
373
+ build_in_theme="filled-primary"
374
+ @click="handleSave"
375
+ >
376
+ <template #prefix>
377
+ <Icon name="save" />
47
378
  </template>
48
- <script>
49
- import { iconsMap } from "pns-component-library
50
- export default{
51
- data(){
52
- return{
53
- iconsMap,
54
- }
55
- }
379
+ </ResponsiveButton>
380
+ ```
381
+
382
+ ### Utility Components
383
+
384
+ #### SimplifiedNotification
385
+ Toast notification component for user feedback.
386
+
387
+ ##### Attributes
388
+
389
+ | Attribute | Description | Type | Default |
390
+ |-----------|-------------|------|---------|
391
+ | content | notification content | `string` | — |
392
+ | build-in-theme | color theme | `'blue' \| 'red' \| 'yellow' \| 'green'` | `'blue'` |
393
+ | title | notification title | `string` | — |
394
+ | customized-alert-icon-src | custom icon URL | `string` | — |
395
+ | customized-class | custom CSS class | `string` | — |
396
+ | with-close-btn | whether to show close button | `boolean` | `true` |
397
+ | mounted-programmatically | whether mounted programmatically | `boolean` | `false` |
398
+ | hide-after | auto-hide after milliseconds (0 = never) | `number` | `0` |
399
+
400
+ ##### Events
401
+
402
+ | Event | Description | Parameters |
403
+ |-------|-------------|------------|
404
+ | close | triggers when notification is closed | — |
405
+
406
+ ##### Slots
407
+
408
+ | Name | Description |
409
+ |------|-------------|
410
+ | content | custom notification content |
411
+
412
+ ```vue
413
+ <SimplifiedNotification
414
+ title="Success!"
415
+ content="Operation completed successfully"
416
+ build_in_theme="green"
417
+ :hide_after="3000"
418
+ @close="handleClose"
419
+ />
420
+ ```
421
+
422
+ #### WholePageErrorPopup
423
+ Full-page error overlay for critical error states.
424
+
425
+ ##### Attributes
426
+
427
+ | Attribute | Description | Type | Default |
428
+ |-----------|-------------|------|---------|
429
+ | title | error title | `string` | `'Error'` |
430
+ | content | error message | `string` | `'An error has occured'` |
431
+
432
+ ##### Events
433
+
434
+ | Event | Description | Parameters |
435
+ |-------|-------------|------------|
436
+ | retry | triggers when retry button is clicked | — |
437
+
438
+ ##### Slots
439
+
440
+ | Name | Description |
441
+ |------|-------------|
442
+ | content | custom error content |
443
+
444
+ ```vue
445
+ <WholePageErrorPopup
446
+ title="Connection Error"
447
+ content="Unable to connect to server. Please check your internet connection."
448
+ @retry="handleRetry"
449
+ />
450
+ ```
451
+
452
+ ## 🔧 Composables
453
+
454
+ ### FloatingNotification
455
+ Programmatically create floating notifications.
456
+
457
+ ##### Parameters
458
+
459
+ | Parameter | Description | Type | Default |
460
+ |-----------|-------------|------|---------|
461
+ | type | notification type | `'success' \| 'error' \| 'warning' \| 'info'` | `'info'` |
462
+ | message | notification message | `string` | — |
463
+ | duration | auto-hide duration in milliseconds | `number` | `3000` |
464
+ | title | notification title | `string` | — |
465
+
466
+ ##### Returns
467
+
468
+ | Type | Description |
469
+ |------|-------------|
470
+ | `void` | Creates and displays the notification |
471
+
472
+ ```javascript
473
+ import { FloatingNotification } from 'pns-component-library/composables'
474
+
475
+ // Show notification
476
+ FloatingNotification({
477
+ type: 'success',
478
+ message: 'Operation completed!',
479
+ duration: 3000
480
+ })
481
+ ```
482
+
483
+ ### useWindowSize
484
+ Reactive window size tracking composable.
485
+
486
+ ##### Returns
487
+
488
+ | Property | Description | Type |
489
+ |----------|-------------|------|
490
+ | width | current window width | `Ref<number>` |
491
+ | height | current window height | `Ref<number>` |
492
+
493
+ ```javascript
494
+ import { useWindowSize } from 'pns-component-library/composables'
495
+
496
+ const { width, height } = useWindowSize()
497
+
498
+ // Use in template or computed
499
+ watchEffect(() => {
500
+ console.log(`Window size: ${width.value}x${height.value}`)
501
+ })
502
+ ```
503
+
504
+ ## 🎨 Directives
505
+
506
+ ### v-component-loading
507
+ Add loading state to any component with overlay spinner.
508
+
509
+ ##### Usage
510
+
511
+ | Binding | Description | Type |
512
+ |---------|-------------|------|
513
+ | value | whether to show loading state | `boolean` |
514
+
515
+ ```vue
516
+ <template>
517
+ <div v-component-loading="isLoading">
518
+ Content here
519
+ </div>
520
+ </template>
521
+
522
+ <script setup>
523
+ import { ref } from 'vue'
524
+ const isLoading = ref(false)
525
+
526
+ // Toggle loading
527
+ const startLoading = () => {
528
+ isLoading.value = true
529
+ setTimeout(() => {
530
+ isLoading.value = false
531
+ }, 2000)
532
+ }
533
+ </script>
534
+ ```
535
+
536
+ ### v-whole-page-loading
537
+ Full-page loading overlay that covers the entire viewport.
538
+
539
+ ##### Usage
540
+
541
+ | Binding | Description | Type |
542
+ |---------|-------------|------|
543
+ | value | whether to show full-page loading | `boolean` |
544
+
545
+ ```vue
546
+ <template>
547
+ <div v-whole-page-loading="isPageLoading">
548
+ App content
549
+ </div>
550
+ </template>
551
+
552
+ <script setup>
553
+ import { ref } from 'vue'
554
+ const isPageLoading = ref(false)
555
+
556
+ // Show page loading
557
+ const loadPage = async () => {
558
+ isPageLoading.value = true
559
+ try {
560
+ await fetchData()
561
+ } finally {
562
+ isPageLoading.value = false
56
563
  }
57
- </script>
58
- ```
59
- ### All icons' names
60
- - black_arrow_down
61
- - black_arrow_to_right
62
- - black_calendar_icon
63
- - black_magnifying_glass_icon
64
- - cyan_arrow_down
65
- - dark_blue_arrow_down
66
- - dark_gray_arrow_down
67
- - eye_icon
68
- - eye_with_cross_icon
69
- - green_notification_icon
70
- - grey_arrow_down
71
- - grey_arrow_to_right
72
- - info_blue_icon
73
- - input_clear_icon
74
- - notification_cross_icon
75
- - red_notification_icon
76
- - red_sad_face
77
- - white_arrow_down
78
- - yellow_notification_icon
564
+ }
565
+ </script>
566
+ ```
567
+
568
+ ## 🎨 Theming
569
+
570
+ The library provides multiple built-in themes:
571
+
572
+ - **Green** (default) - Primary green theme
573
+ - **Primary Blue** - Professional blue theme
574
+ - **Secondary Blue** - Light blue accent theme
575
+ - **Red** - Error/warning red theme
576
+
577
+ ```vue
578
+ <Checkbox built_in_theme="primary-blue" />
579
+ <ResponsiveButton variant="secondary-blue" />
580
+ ```
581
+
582
+ ## 📱 Responsive Design
583
+
584
+ Components automatically adapt to different screen sizes:
585
+
586
+ - **Desktop**: Full feature set with optimal spacing
587
+ - **Mobile**: Compact layouts with touch-friendly interactions
588
+ - **Breakpoint**: 767px for mobile/desktop switching
589
+
590
+ ## ♿ Accessibility
591
+
592
+ All components include:
593
+
594
+ - ARIA attributes for screen readers
595
+ - Keyboard navigation support
596
+ - Focus management
597
+ - High contrast support
598
+ - Semantic HTML structure
599
+
600
+ ## 🛠 Development
601
+
602
+ ### Project Setup
603
+
604
+ ```bash
605
+ npm install
606
+ ```
607
+
608
+ ### Development Server
609
+
610
+ ```bash
611
+ npm run dev
612
+ ```
613
+
614
+ ### Build for Production
615
+
616
+ ```bash
617
+ npm run build
618
+ ```
619
+
620
+ ### Component Documentation
621
+
622
+ Visit the demo pages to see all components in action with interactive examples.
623
+
624
+ ## 📄 License
625
+
626
+ MIT License - see LICENSE file for details.
627
+
628
+ ## 🤝 Contributing
629
+
630
+ 1. Fork the repository
631
+ 2. Create a feature branch
632
+ 3. Make your changes
633
+ 4. Add tests and documentation
634
+ 5. Submit a pull request
635
+
636
+ ## 📞 Support
637
+
638
+ For questions and support, please open an issue on the GitHub repository.
639
+
640
+ ---
641
+
642
+ Built with ❤️ using Vue 3 and modern web technologies.
79
643