feat(ui): added c-select in the ui lib (#550)
* feat(ui): added c-select in the ui lib * refactor(ui): switched n-select to c-select
This commit is contained in:
		
							parent
							
								
									6498c9b0fa
								
							
						
					
					
						commit
						dfa1ba8554
					
				| @ -285,6 +285,7 @@ | |||||||
|     "watchThrottled": true, |     "watchThrottled": true, | ||||||
|     "watchTriggerable": true, |     "watchTriggerable": true, | ||||||
|     "watchWithFilter": true, |     "watchWithFilter": true, | ||||||
|     "whenever": true |     "whenever": true, | ||||||
|  |     "toValue": true | ||||||
|   } |   } | ||||||
| } | } | ||||||
							
								
								
									
										9
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -31,6 +31,7 @@ declare module '@vue/runtime-core' { | |||||||
|     Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default'] |     Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default'] | ||||||
|     CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default'] |     CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default'] | ||||||
|     'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default'] |     'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default'] | ||||||
|  |     CLabel: typeof import('./src/ui/c-label/c-label.vue')['default'] | ||||||
|     CLink: typeof import('./src/ui/c-link/c-link.vue')['default'] |     CLink: typeof import('./src/ui/c-link/c-link.vue')['default'] | ||||||
|     'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default'] |     'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default'] | ||||||
|     CModal: typeof import('./src/ui/c-modal/c-modal.vue')['default'] |     CModal: typeof import('./src/ui/c-modal/c-modal.vue')['default'] | ||||||
| @ -41,7 +42,11 @@ declare module '@vue/runtime-core' { | |||||||
|     CommandPalette: typeof import('./src/modules/command-palette/command-palette.vue')['default'] |     CommandPalette: typeof import('./src/modules/command-palette/command-palette.vue')['default'] | ||||||
|     CommandPaletteOption: typeof import('./src/modules/command-palette/components/command-palette-option.vue')['default'] |     CommandPaletteOption: typeof import('./src/modules/command-palette/components/command-palette-option.vue')['default'] | ||||||
|     CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default'] |     CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default'] | ||||||
|  |     CSelect: typeof import('./src/ui/c-select/c-select.vue')['default'] | ||||||
|  |     'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default'] | ||||||
|     DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default'] |     DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default'] | ||||||
|  |     'Demo.routes': typeof import('./src/ui/demo/demo.routes.vue')['default'] | ||||||
|  |     'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default'] | ||||||
|     DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default'] |     DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default'] | ||||||
|     DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default'] |     DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default'] | ||||||
|     DiffViewer: typeof import('./src/tools/json-diff/diff-viewer/diff-viewer.vue')['default'] |     DiffViewer: typeof import('./src/tools/json-diff/diff-viewer/diff-viewer.vue')['default'] | ||||||
| @ -60,9 +65,13 @@ declare module '@vue/runtime-core' { | |||||||
|     HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default'] |     HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default'] | ||||||
|     HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default'] |     HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default'] | ||||||
|     HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] |     HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] | ||||||
|  |     'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default'] | ||||||
|     'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] |     'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] | ||||||
|     IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default'] |     IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default'] | ||||||
|     IconMdiCamera: typeof import('~icons/mdi/camera')['default'] |     IconMdiCamera: typeof import('~icons/mdi/camera')['default'] | ||||||
|  |     IconMdiCameraOutline: typeof import('~icons/mdi/camera-outline')['default'] | ||||||
|  |     IconMdiCameraVideoOff: typeof import('~icons/mdi/camera-video-off')['default'] | ||||||
|  |     IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] | ||||||
|     IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] |     IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] | ||||||
|     IconMdiClose: typeof import('~icons/mdi/close')['default'] |     IconMdiClose: typeof import('~icons/mdi/close')['default'] | ||||||
|     IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default'] |     IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default'] | ||||||
|  | |||||||
| @ -118,6 +118,7 @@ | |||||||
|     "prettier": "^2.8.7", |     "prettier": "^2.8.7", | ||||||
|     "typescript": "~4.9.0", |     "typescript": "~4.9.0", | ||||||
|     "unocss": "^0.53.0", |     "unocss": "^0.53.0", | ||||||
|  |     "unocss-preset-scrollbar": "^0.2.1", | ||||||
|     "unplugin-icons": "^0.16.1", |     "unplugin-icons": "^0.16.1", | ||||||
|     "unplugin-vue-components": "^0.25.0", |     "unplugin-vue-components": "^0.25.0", | ||||||
|     "vite": "^4.0.0", |     "vite": "^4.0.0", | ||||||
|  | |||||||
							
								
								
									
										37
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										37
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @ -256,6 +256,9 @@ devDependencies: | |||||||
|   unocss: |   unocss: | ||||||
|     specifier: ^0.53.0 |     specifier: ^0.53.0 | ||||||
|     version: 0.53.0(postcss@8.4.24)(rollup@2.79.1)(vite@4.3.9) |     version: 0.53.0(postcss@8.4.24)(rollup@2.79.1)(vite@4.3.9) | ||||||
|  |   unocss-preset-scrollbar: | ||||||
|  |     specifier: ^0.2.1 | ||||||
|  |     version: 0.2.1(unocss@0.53.0) | ||||||
|   unplugin-icons: |   unplugin-icons: | ||||||
|     specifier: ^0.16.1 |     specifier: ^0.16.1 | ||||||
|     version: 0.16.1(@vue/compiler-sfc@3.2.47) |     version: 0.16.1(@vue/compiler-sfc@3.2.47) | ||||||
| @ -3366,7 +3369,7 @@ packages: | |||||||
|     dependencies: |     dependencies: | ||||||
|       '@unhead/dom': 0.5.1 |       '@unhead/dom': 0.5.1 | ||||||
|       '@unhead/schema': 0.5.1 |       '@unhead/schema': 0.5.1 | ||||||
|       '@vueuse/shared': 10.2.1(vue@3.3.4) |       '@vueuse/shared': 10.3.0(vue@3.3.4) | ||||||
|       unhead: 0.5.1 |       unhead: 0.5.1 | ||||||
|       vue: 3.3.4 |       vue: 3.3.4 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
| @ -3413,6 +3416,10 @@ packages: | |||||||
|       unconfig: 0.3.9 |       unconfig: 0.3.9 | ||||||
|     dev: true |     dev: true | ||||||
| 
 | 
 | ||||||
|  |   /@unocss/core@0.31.17: | ||||||
|  |     resolution: {integrity: sha512-DJ3Uk2ePVXvV1qQmgoLK44aqB6f0s+naOEvouI97nzVXDZgxDQPBxIPB/L4vvE4U+gQxEiHwwE3gJ75iPqVzXw==} | ||||||
|  |     dev: true | ||||||
|  | 
 | ||||||
|   /@unocss/core@0.53.0: |   /@unocss/core@0.53.0: | ||||||
|     resolution: {integrity: sha512-MB6hqSN2wjmm3NNYspNqzxvMv7LnyLqz0uCWr15elRqnjsuq01w7DZ1iPS9ckA2M3YjQIRTXR9YPtDbSqY0jcA==} |     resolution: {integrity: sha512-MB6hqSN2wjmm3NNYspNqzxvMv7LnyLqz0uCWr15elRqnjsuq01w7DZ1iPS9ckA2M3YjQIRTXR9YPtDbSqY0jcA==} | ||||||
|     dev: true |     dev: true | ||||||
| @ -3486,6 +3493,12 @@ packages: | |||||||
|       - supports-color |       - supports-color | ||||||
|     dev: true |     dev: true | ||||||
| 
 | 
 | ||||||
|  |   /@unocss/preset-mini@0.31.17: | ||||||
|  |     resolution: {integrity: sha512-gVgMTOKLt3O1ym348QIBmR5sS9W0Ozkk5xelhH6e0VXcpg0dXDPDrl4hFErMy4x6IB86yyJG6Dz5JhcwQB13Ig==} | ||||||
|  |     dependencies: | ||||||
|  |       '@unocss/core': 0.31.17 | ||||||
|  |     dev: true | ||||||
|  | 
 | ||||||
|   /@unocss/preset-mini@0.53.0: |   /@unocss/preset-mini@0.53.0: | ||||||
|     resolution: {integrity: sha512-hGj9ltZUJIuPT+9bO+R0OlsQOSlV7rjQRkSSMnUaDsuKfzhahsyc7QglNHZI4wuTI/9iSJKGUD4nvTe559+8Hg==} |     resolution: {integrity: sha512-hGj9ltZUJIuPT+9bO+R0OlsQOSlV7rjQRkSSMnUaDsuKfzhahsyc7QglNHZI4wuTI/9iSJKGUD4nvTe559+8Hg==} | ||||||
|     dependencies: |     dependencies: | ||||||
| @ -3928,8 +3941,8 @@ packages: | |||||||
|       - vue |       - vue | ||||||
|     dev: false |     dev: false | ||||||
| 
 | 
 | ||||||
|   /@vueuse/shared@10.2.1(vue@3.3.4): |   /@vueuse/shared@10.3.0(vue@3.3.4): | ||||||
|     resolution: {integrity: sha512-QWHq2bSuGptkcxx4f4M/fBYC3Y8d3M2UYyLsyzoPgEoVzJURQ0oJeWXu79OiLlBb8gTKkqe4mO85T/sf39mmiw==} |     resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==} | ||||||
|     dependencies: |     dependencies: | ||||||
|       vue-demi: 0.14.5(vue@3.3.4) |       vue-demi: 0.14.5(vue@3.3.4) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
| @ -5013,7 +5026,6 @@ packages: | |||||||
|   /esbuild@0.17.19: |   /esbuild@0.17.19: | ||||||
|     resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} |     resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} | ||||||
|     engines: {node: '>=12'} |     engines: {node: '>=12'} | ||||||
|     hasBin: true |  | ||||||
|     requiresBuild: true |     requiresBuild: true | ||||||
|     optionalDependencies: |     optionalDependencies: | ||||||
|       '@esbuild/android-arm': 0.17.19 |       '@esbuild/android-arm': 0.17.19 | ||||||
| @ -7772,7 +7784,6 @@ packages: | |||||||
|   /rollup@3.25.1: |   /rollup@3.25.1: | ||||||
|     resolution: {integrity: sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==} |     resolution: {integrity: sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==} | ||||||
|     engines: {node: '>=14.18.0', npm: '>=8.0.0'} |     engines: {node: '>=14.18.0', npm: '>=8.0.0'} | ||||||
|     hasBin: true |  | ||||||
|     optionalDependencies: |     optionalDependencies: | ||||||
|       fsevents: 2.3.2 |       fsevents: 2.3.2 | ||||||
|     dev: true |     dev: true | ||||||
| @ -8502,6 +8513,15 @@ packages: | |||||||
|     engines: {node: '>= 10.0.0'} |     engines: {node: '>= 10.0.0'} | ||||||
|     dev: true |     dev: true | ||||||
| 
 | 
 | ||||||
|  |   /unocss-preset-scrollbar@0.2.1(unocss@0.53.0): | ||||||
|  |     resolution: {integrity: sha512-7ubHdOaUwr7xBn1glPpICNNsM2SZGjvWK5uRPNiQYsrZ9YFjsCGHk9x5S2R8pTkuMDQeiaSa/UQbYhjC8Fra5g==} | ||||||
|  |     peerDependencies: | ||||||
|  |       unocss: '>= 0.31.13 < 1' | ||||||
|  |     dependencies: | ||||||
|  |       '@unocss/preset-mini': 0.31.17 | ||||||
|  |       unocss: 0.53.0(postcss@8.4.24)(rollup@2.79.1)(vite@4.3.9) | ||||||
|  |     dev: true | ||||||
|  | 
 | ||||||
|   /unocss@0.53.0(postcss@8.4.24)(rollup@2.79.1)(vite@4.3.9): |   /unocss@0.53.0(postcss@8.4.24)(rollup@2.79.1)(vite@4.3.9): | ||||||
|     resolution: {integrity: sha512-kY4h5ERiDYlSnL2X+hbDfh+uaF7QNouy7j51GOTUr3Q0aaWehaNd05b15SjHrab559dEC0mYfrSEdh/DnCK1cw==} |     resolution: {integrity: sha512-kY4h5ERiDYlSnL2X+hbDfh+uaF7QNouy7j51GOTUr3Q0aaWehaNd05b15SjHrab559dEC0mYfrSEdh/DnCK1cw==} | ||||||
|     engines: {node: '>=14'} |     engines: {node: '>=14'} | ||||||
| @ -8635,7 +8655,6 @@ packages: | |||||||
| 
 | 
 | ||||||
|   /update-browserslist-db@1.0.10(browserslist@4.21.5): |   /update-browserslist-db@1.0.10(browserslist@4.21.5): | ||||||
|     resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} |     resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} | ||||||
|     hasBin: true |  | ||||||
|     peerDependencies: |     peerDependencies: | ||||||
|       browserslist: '>= 4.21.0' |       browserslist: '>= 4.21.0' | ||||||
|     dependencies: |     dependencies: | ||||||
| @ -8646,7 +8665,6 @@ packages: | |||||||
| 
 | 
 | ||||||
|   /update-browserslist-db@1.0.11(browserslist@4.21.9): |   /update-browserslist-db@1.0.11(browserslist@4.21.9): | ||||||
|     resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} |     resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} | ||||||
|     hasBin: true |  | ||||||
|     peerDependencies: |     peerDependencies: | ||||||
|       browserslist: '>= 4.21.0' |       browserslist: '>= 4.21.0' | ||||||
|     dependencies: |     dependencies: | ||||||
| @ -8777,7 +8795,6 @@ packages: | |||||||
|   /vite@4.3.9(@types/node@18.15.11)(less@4.1.3): |   /vite@4.3.9(@types/node@18.15.11)(less@4.1.3): | ||||||
|     resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} |     resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} | ||||||
|     engines: {node: ^14.18.0 || >=16.0.0} |     engines: {node: ^14.18.0 || >=16.0.0} | ||||||
|     hasBin: true |  | ||||||
|     peerDependencies: |     peerDependencies: | ||||||
|       '@types/node': '>= 14' |       '@types/node': '>= 14' | ||||||
|       less: '*' |       less: '*' | ||||||
| @ -8811,7 +8828,6 @@ packages: | |||||||
|   /vitest@0.32.0(jsdom@19.0.0)(less@4.1.3): |   /vitest@0.32.0(jsdom@19.0.0)(less@4.1.3): | ||||||
|     resolution: {integrity: sha512-SW83o629gCqnV3BqBnTxhB10DAwzwEx3z+rqYZESehUB+eWsJxwcBQx7CKy0otuGMJTYh7qCVuUX23HkftGl/Q==} |     resolution: {integrity: sha512-SW83o629gCqnV3BqBnTxhB10DAwzwEx3z+rqYZESehUB+eWsJxwcBQx7CKy0otuGMJTYh7qCVuUX23HkftGl/Q==} | ||||||
|     engines: {node: '>=v14.18.0'} |     engines: {node: '>=v14.18.0'} | ||||||
|     hasBin: true |  | ||||||
|     peerDependencies: |     peerDependencies: | ||||||
|       '@edge-runtime/vm': '*' |       '@edge-runtime/vm': '*' | ||||||
|       '@vitest/browser': '*' |       '@vitest/browser': '*' | ||||||
| @ -8886,7 +8902,6 @@ packages: | |||||||
|   /vue-demi@0.13.11(vue@3.3.4): |   /vue-demi@0.13.11(vue@3.3.4): | ||||||
|     resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} |     resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} | ||||||
|     engines: {node: '>=12'} |     engines: {node: '>=12'} | ||||||
|     hasBin: true |  | ||||||
|     requiresBuild: true |     requiresBuild: true | ||||||
|     peerDependencies: |     peerDependencies: | ||||||
|       '@vue/composition-api': ^1.0.0-rc.1 |       '@vue/composition-api': ^1.0.0-rc.1 | ||||||
| @ -8901,7 +8916,6 @@ packages: | |||||||
|   /vue-demi@0.14.1(vue@3.3.4): |   /vue-demi@0.14.1(vue@3.3.4): | ||||||
|     resolution: {integrity: sha512-rt+yuCtXvscYot9SQQj3WKZJVSriPNqVkpVBNEHPzSgBv7QIYzsS410VqVgvx8f9AAPgjg+XPKvmV3vOqqkJQQ==} |     resolution: {integrity: sha512-rt+yuCtXvscYot9SQQj3WKZJVSriPNqVkpVBNEHPzSgBv7QIYzsS410VqVgvx8f9AAPgjg+XPKvmV3vOqqkJQQ==} | ||||||
|     engines: {node: '>=12'} |     engines: {node: '>=12'} | ||||||
|     hasBin: true |  | ||||||
|     requiresBuild: true |     requiresBuild: true | ||||||
|     peerDependencies: |     peerDependencies: | ||||||
|       '@vue/composition-api': ^1.0.0-rc.1 |       '@vue/composition-api': ^1.0.0-rc.1 | ||||||
| @ -8976,7 +8990,6 @@ packages: | |||||||
| 
 | 
 | ||||||
|   /vue-tsc@1.8.1(typescript@4.9.3): |   /vue-tsc@1.8.1(typescript@4.9.3): | ||||||
|     resolution: {integrity: sha512-GxBQrcb0Qvyrj1uZqnTXQyWbXdNDRY2MTa+r7ESgjhf+WzBSdxZfkS3KD/C3WhKYG+aN8hf44Hp5Gqzb6PehAA==} |     resolution: {integrity: sha512-GxBQrcb0Qvyrj1uZqnTXQyWbXdNDRY2MTa+r7ESgjhf+WzBSdxZfkS3KD/C3WhKYG+aN8hf44Hp5Gqzb6PehAA==} | ||||||
|     hasBin: true |  | ||||||
|     peerDependencies: |     peerDependencies: | ||||||
|       typescript: '*' |       typescript: '*' | ||||||
|     dependencies: |     dependencies: | ||||||
|  | |||||||
| @ -11,12 +11,19 @@ function useFuzzySearch<Data>({ | |||||||
| }: { | }: { | ||||||
|   search: MaybeRef<string> |   search: MaybeRef<string> | ||||||
|   data: Data[] |   data: Data[] | ||||||
|   options?: Fuse.IFuseOptions<Data> |   options?: Fuse.IFuseOptions<Data> & { filterEmpty?: boolean } | ||||||
| }) { | }) { | ||||||
|   const fuse = new Fuse(data, options); |   const fuse = new Fuse(data, options); | ||||||
|  |   const filterEmpty = options.filterEmpty ?? true; | ||||||
| 
 | 
 | ||||||
|   const searchResult = computed(() => { |   const searchResult = computed<Data[]>(() => { | ||||||
|     return fuse.search(get(search)).map(({ item }) => item); |     const query = get(search); | ||||||
|  | 
 | ||||||
|  |     if (!filterEmpty && query === '') { | ||||||
|  |       return data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return fuse.search(query).map(({ item }) => item); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   return { searchResult }; |   return { searchResult }; | ||||||
|  | |||||||
| @ -103,6 +103,10 @@ const tools = computed<ToolCategory[]>(() => [ | |||||||
|           Home |           Home | ||||||
|         </n-tooltip> |         </n-tooltip> | ||||||
| 
 | 
 | ||||||
|  |         <c-button to="/c-lib" circle variant="text" aria-label="UI Lib"> | ||||||
|  |           <icon-mdi:brush-variant text-20px /> | ||||||
|  |         </c-button> | ||||||
|  | 
 | ||||||
|         <command-palette /> |         <command-palette /> | ||||||
| 
 | 
 | ||||||
|         <div> |         <div> | ||||||
|  | |||||||
							
								
								
									
										5
									
								
								src/modules/shared/number.models.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/modules/shared/number.models.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | function clamp({ value, min = 0, max = 100 }: { value: number; min?: number; max?: number }) { | ||||||
|  |   return Math.min(Math.max(value, min), max); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export { clamp }; | ||||||
| @ -84,12 +84,12 @@ const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase | |||||||
|   <div> |   <div> | ||||||
|     <n-grid cols="3" x-gap="12"> |     <n-grid cols="3" x-gap="12"> | ||||||
|       <n-gi span="1"> |       <n-gi span="1"> | ||||||
|         <n-form-item label="Language:"> |         <c-select | ||||||
|           <n-select |  | ||||||
|           v-model:value="language" |           v-model:value="language" | ||||||
|             :options="Object.keys(languages).map((label) => ({ label, value: label }))" |           searchable | ||||||
|  |           label="Language:" | ||||||
|  |           :options="Object.keys(languages)" | ||||||
|         /> |         /> | ||||||
|         </n-form-item> |  | ||||||
|       </n-gi> |       </n-gi> | ||||||
|       <n-gi span="2"> |       <n-gi span="2"> | ||||||
|         <n-form-item |         <n-form-item | ||||||
|  | |||||||
| @ -122,24 +122,25 @@ function downloadMedia({ type, value, createdAt }: Media) { | |||||||
|     </c-card> |     </c-card> | ||||||
| 
 | 
 | ||||||
|     <c-card v-else> |     <c-card v-else> | ||||||
|       <div flex gap-2> |       <div flex flex-col gap-2> | ||||||
|         <div flex-1> |         <c-select | ||||||
|           <div>Video</div> |  | ||||||
|           <n-select |  | ||||||
|           v-model:value="currentCamera" |           v-model:value="currentCamera" | ||||||
|  |           label-position="left" | ||||||
|  |           label-width="60px" | ||||||
|  |           label="Video:" | ||||||
|           :options="cameras.map(({ deviceId, label }) => ({ value: deviceId, label }))" |           :options="cameras.map(({ deviceId, label }) => ({ value: deviceId, label }))" | ||||||
|           placeholder="Select camera" |           placeholder="Select camera" | ||||||
|         /> |         /> | ||||||
|         </div> |         <c-select | ||||||
|         <div v-if="currentMicrophone && microphones.length > 0" flex-1> |           v-if="currentMicrophone && microphones.length > 0" | ||||||
|           <div>Audio</div> |  | ||||||
|           <n-select |  | ||||||
|           v-model:value="currentMicrophone" |           v-model:value="currentMicrophone" | ||||||
|  |           label="Audio:" | ||||||
|  |           label-position="left" | ||||||
|  |           label-width="60px" | ||||||
|           :options="microphones.map(({ deviceId, label }) => ({ value: deviceId, label }))" |           :options="microphones.map(({ deviceId, label }) => ({ value: deviceId, label }))" | ||||||
|           placeholder="Select microphone" |           placeholder="Select microphone" | ||||||
|         /> |         /> | ||||||
|       </div> |       </div> | ||||||
|       </div> |  | ||||||
| 
 | 
 | ||||||
|       <div v-if="!isMediaStreamAvailable" mt-3 flex justify-center> |       <div v-if="!isMediaStreamAvailable" mt-3 flex justify-center> | ||||||
|         <c-button type="primary" @click="start"> |         <c-button type="primary" @click="start"> | ||||||
|  | |||||||
| @ -142,7 +142,7 @@ function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date | |||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <n-input-group> |     <div flex gap-2> | ||||||
|       <c-input-text |       <c-input-text | ||||||
|         v-model:value="inputDate" |         v-model:value="inputDate" | ||||||
|         autofocus |         autofocus | ||||||
| @ -153,13 +153,13 @@ function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date | |||||||
|         @update:value="onDateInputChanged" |         @update:value="onDateInputChanged" | ||||||
|       /> |       /> | ||||||
| 
 | 
 | ||||||
|       <n-select |       <c-select | ||||||
|         v-model:value="formatIndex" |         v-model:value="formatIndex" | ||||||
|         style="flex: 0 0 170px" |         style="flex: 0 0 170px" | ||||||
|         :options="formats.map(({ name }, i) => ({ label: name, value: i }))" |         :options="formats.map(({ name }, i) => ({ label: name, value: i }))" | ||||||
|         data-test-id="date-time-converter-format-select" |         data-test-id="date-time-converter-format-select" | ||||||
|       /> |       /> | ||||||
|     </n-input-group> |     </div> | ||||||
| 
 | 
 | ||||||
|     <n-divider /> |     <n-divider /> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -29,12 +29,11 @@ const decryptOutput = computed(() => | |||||||
|       <div flex flex-1 flex-col gap-2> |       <div flex flex-1 flex-col gap-2> | ||||||
|         <c-input-text v-model:value="cypherSecret" label="Your secret key:" clearable raw-text /> |         <c-input-text v-model:value="cypherSecret" label="Your secret key:" clearable raw-text /> | ||||||
| 
 | 
 | ||||||
|         <n-form-item label="Encryption algorithm:" :show-feedback="false"> |         <c-select | ||||||
|           <n-select |  | ||||||
|           v-model:value="cypherAlgo" |           v-model:value="cypherAlgo" | ||||||
|  |           label="Encryption algorithm:" | ||||||
|           :options="Object.keys(algos).map((label) => ({ label, value: label }))" |           :options="Object.keys(algos).map((label) => ({ label, value: label }))" | ||||||
|         /> |         /> | ||||||
|         </n-form-item> |  | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|     <c-input-text |     <c-input-text | ||||||
| @ -57,12 +56,11 @@ const decryptOutput = computed(() => | |||||||
|       <div flex flex-1 flex-col gap-2> |       <div flex flex-1 flex-col gap-2> | ||||||
|         <c-input-text v-model:value="decryptSecret" label="Your secret key:" clearable raw-text /> |         <c-input-text v-model:value="decryptSecret" label="Your secret key:" clearable raw-text /> | ||||||
| 
 | 
 | ||||||
|         <n-form-item label="Encryption algorithm:" :show-feedback="false"> |         <c-select | ||||||
|           <n-select |  | ||||||
|           v-model:value="decryptAlgo" |           v-model:value="decryptAlgo" | ||||||
|  |           label="Encryption algorithm:" | ||||||
|           :options="Object.keys(algos).map((label) => ({ label, value: label }))" |           :options="Object.keys(algos).map((label) => ({ label, value: label }))" | ||||||
|         /> |         /> | ||||||
|         </n-form-item> |  | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|     <c-input-text |     <c-input-text | ||||||
|  | |||||||
| @ -39,13 +39,15 @@ const endAt = computed(() => | |||||||
|       </n-form-item> |       </n-form-item> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <n-form-item label="Amount of unit consumed by time span" :show-feedback="false"> |     <p>Amount of unit consumed by time span</p> | ||||||
|  |     <div flex flex-col items-baseline gap-y-2 md:flex-row> | ||||||
|       <n-input-number v-model:value="unitPerTimeSpan" :min="1" /> |       <n-input-number v-model:value="unitPerTimeSpan" :min="1" /> | ||||||
|       <span mx-3>in</span> |       <div flex items-baseline gap-2> | ||||||
|       <n-input-group> |         <span ml-2>in</span> | ||||||
|         <n-input-number v-model:value="timeSpan" :min="1" /> |         <n-input-number v-model:value="timeSpan" min-w-130px :min="1" /> | ||||||
|         <n-select |         <c-select | ||||||
|           v-model:value="timeSpanUnitMultiplier" |           v-model:value="timeSpanUnitMultiplier" | ||||||
|  |           min-w-130px | ||||||
|           :options="[ |           :options="[ | ||||||
|             { label: 'milliseconds', value: 1 }, |             { label: 'milliseconds', value: 1 }, | ||||||
|             { label: 'seconds', value: 1000 }, |             { label: 'seconds', value: 1000 }, | ||||||
| @ -54,8 +56,8 @@ const endAt = computed(() => | |||||||
|             { label: 'days', value: 1000 * 60 * 60 * 24 }, |             { label: 'days', value: 1000 * 60 * 60 * 24 }, | ||||||
|           ]" |           ]" | ||||||
|         /> |         /> | ||||||
|       </n-input-group> |       </div> | ||||||
|     </n-form-item> |     </div> | ||||||
| 
 | 
 | ||||||
|     <n-divider /> |     <n-divider /> | ||||||
|     <c-card mb-2> |     <c-card mb-2> | ||||||
|  | |||||||
| @ -41,9 +41,10 @@ const hashText = (algo: AlgoNames, value: string) => formatWithEncoding(algos[al | |||||||
| 
 | 
 | ||||||
|       <n-divider /> |       <n-divider /> | ||||||
| 
 | 
 | ||||||
|       <n-form-item label="Digest encoding"> |       <c-select | ||||||
|         <n-select |  | ||||||
|         v-model:value="encoding" |         v-model:value="encoding" | ||||||
|  |         mb-4 | ||||||
|  |         label="Digest encoding" | ||||||
|         :options="[ |         :options="[ | ||||||
|           { |           { | ||||||
|             label: 'Binary (base 2)', |             label: 'Binary (base 2)', | ||||||
| @ -63,7 +64,6 @@ const hashText = (algo: AlgoNames, value: string) => formatWithEncoding(algos[al | |||||||
|           }, |           }, | ||||||
|         ]" |         ]" | ||||||
|       /> |       /> | ||||||
|       </n-form-item> |  | ||||||
| 
 | 
 | ||||||
|       <div v-for="algo in algoNames" :key="algo" style="margin: 5px 0"> |       <div v-for="algo in algoNames" :key="algo" style="margin: 5px 0"> | ||||||
|         <n-input-group> |         <n-input-group> | ||||||
|  | |||||||
| @ -51,16 +51,15 @@ const { copy } = useCopy({ source: hmac }); | |||||||
|     <c-input-text v-model:value="secret" raw-text placeholder="Enter the secret key..." label="Secret key" clearable /> |     <c-input-text v-model:value="secret" raw-text placeholder="Enter the secret key..." label="Secret key" clearable /> | ||||||
| 
 | 
 | ||||||
|     <div flex gap-2> |     <div flex gap-2> | ||||||
|       <n-form-item label="Hashing function" flex-1> |       <c-select | ||||||
|         <n-select |         v-model:value="hashFunction" label="Hashing function" | ||||||
|           v-model:value="hashFunction" |         flex-1 | ||||||
|         placeholder="Select an hashing function..." |         placeholder="Select an hashing function..." | ||||||
|         :options="Object.keys(algos).map((label) => ({ label, value: label }))" |         :options="Object.keys(algos).map((label) => ({ label, value: label }))" | ||||||
|       /> |       /> | ||||||
|       </n-form-item> |       <c-select | ||||||
|       <n-form-item label="Output encoding" flex-1> |         v-model:value="encoding" label="Output encoding" | ||||||
|         <n-select |         flex-1 | ||||||
|           v-model:value="encoding" |  | ||||||
|         placeholder="Select the result encoding..." |         placeholder="Select the result encoding..." | ||||||
|         :options="[ |         :options="[ | ||||||
|           { |           { | ||||||
| @ -81,7 +80,6 @@ const { copy } = useCopy({ source: hmac }); | |||||||
|           }, |           }, | ||||||
|         ]" |         ]" | ||||||
|       /> |       /> | ||||||
|       </n-form-item> |  | ||||||
|     </div> |     </div> | ||||||
|     <input-copyable v-model:value="hmac" type="textarea" placeholder="The result of the HMAC..." label="HMAC of your text" /> |     <input-copyable v-model:value="hmac" type="textarea" placeholder="The result of the HMAC..." label="HMAC of your text" /> | ||||||
|     <div flex justify-center> |     <div flex justify-center> | ||||||
|  | |||||||
| @ -61,17 +61,19 @@ function transformer(value: string) { | |||||||
|             </n-form-item> |             </n-form-item> | ||||||
|           </div> |           </div> | ||||||
|           <div flex-1> |           <div flex-1> | ||||||
|             <n-form-item label="Sort list" label-placement="left" label-width="120" :show-feedback="false" mb-2> |             <c-select | ||||||
|               <n-select |  | ||||||
|               v-model:value="conversionConfig.sortList" |               v-model:value="conversionConfig.sortList" | ||||||
|  |               label="Sort list" | ||||||
|  |               label-position="left" | ||||||
|  |               label-width="120px" | ||||||
|  |               label-align="right" | ||||||
|  |               mb-2 | ||||||
|               :options="sortOrderOptions" |               :options="sortOrderOptions" | ||||||
|                 clearable |  | ||||||
|               w-full |               w-full | ||||||
|               :disabled="conversionConfig.reverseList" |               :disabled="conversionConfig.reverseList" | ||||||
|               data-test-id="sortList" |               data-test-id="sortList" | ||||||
|               placeholder="Sort alphabetically" |               placeholder="Sort alphabetically" | ||||||
|             /> |             /> | ||||||
|             </n-form-item> |  | ||||||
| 
 | 
 | ||||||
|             <c-input-text |             <c-input-text | ||||||
|               v-model:value="conversionConfig.separator" |               v-model:value="conversionConfig.separator" | ||||||
|  | |||||||
| @ -53,12 +53,15 @@ const metaTags = computed(() => { | |||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <div v-for="{ name, elements } of sections" :key="name" style="margin-bottom: 15px"> |     <div v-for="{ name, elements } of sections" :key="name" style="margin-bottom: 15px"> | ||||||
|       <n-form-item :label="name" :show-feedback="false" /> |       <div mb-5px> | ||||||
|  |         {{ name }} | ||||||
|  |       </div> | ||||||
| 
 | 
 | ||||||
|       <n-input-group v-for="{ key, type, label, placeholder, ...element } of elements" :key="key"> |       <n-input-group v-for="{ key, type, label, placeholder, ...element } of elements" :key="key"> | ||||||
|         <n-input-group-label style="flex: 0 0 110px"> |         <n-input-group-label style="flex: 0 0 110px"> | ||||||
|           {{ label }} |           {{ label }} | ||||||
|         </n-input-group-label> |         </n-input-group-label> | ||||||
|  | 
 | ||||||
|         <c-input-text v-if="type === 'input'" v-model:value="metadata[key]" :placeholder="placeholder" clearable /> |         <c-input-text v-if="type === 'input'" v-model:value="metadata[key]" :placeholder="placeholder" clearable /> | ||||||
|         <n-dynamic-input |         <n-dynamic-input | ||||||
|           v-else-if="type === 'input-multiple'" |           v-else-if="type === 'input-multiple'" | ||||||
| @ -69,9 +72,10 @@ const metaTags = computed(() => { | |||||||
|           :show-sort-button="true" |           :show-sort-button="true" | ||||||
|         /> |         /> | ||||||
| 
 | 
 | ||||||
|         <n-select |         <c-select | ||||||
|           v-else-if="type === 'select'" |           v-else-if="type === 'select'" | ||||||
|           v-model:value="metadata[key]" |           v-model:value="metadata[key]" | ||||||
|  |           w-full | ||||||
|           :placeholder="placeholder" |           :placeholder="placeholder" | ||||||
|           :options="(element as OGSchemaTypeElementSelect).options" |           :options="(element as OGSchemaTypeElementSelect).options" | ||||||
|         /> |         /> | ||||||
|  | |||||||
| @ -26,15 +26,13 @@ const mimeTypeFound = computed(() => (selectedExtension.value ? extensionToMimeT | |||||||
|     <div style="opacity: 0.8"> |     <div style="opacity: 0.8"> | ||||||
|       Know which file extensions are associated to a mime-type |       Know which file extensions are associated to a mime-type | ||||||
|     </div> |     </div> | ||||||
|     <n-form-item> |     <c-select | ||||||
|       <n-select |  | ||||||
|       v-model:value="selectedMimeType" |       v-model:value="selectedMimeType" | ||||||
|         filterable |       searchable | ||||||
|  |       my-4 | ||||||
|       :options="mimeToExtensionsOptions" |       :options="mimeToExtensionsOptions" | ||||||
|         size="large" |  | ||||||
|       placeholder="Select your mimetype here... (ex: application/pdf)" |       placeholder="Select your mimetype here... (ex: application/pdf)" | ||||||
|     /> |     /> | ||||||
|     </n-form-item> |  | ||||||
| 
 | 
 | ||||||
|     <div v-if="extensionsFound.length > 0"> |     <div v-if="extensionsFound.length > 0"> | ||||||
|       Extensions of files with the <n-tag round :bordered="false"> |       Extensions of files with the <n-tag round :bordered="false"> | ||||||
| @ -62,15 +60,13 @@ const mimeTypeFound = computed(() => (selectedExtension.value ? extensionToMimeT | |||||||
|     <div style="opacity: 0.8"> |     <div style="opacity: 0.8"> | ||||||
|       Know which mime type is associated to a file extension |       Know which mime type is associated to a file extension | ||||||
|     </div> |     </div> | ||||||
|     <n-form-item> |     <c-select | ||||||
|       <n-select |  | ||||||
|       v-model:value="selectedExtension" |       v-model:value="selectedExtension" | ||||||
|         filterable |       searchable | ||||||
|  |       my-4 | ||||||
|       :options="extensionToMimeTypeOptions" |       :options="extensionToMimeTypeOptions" | ||||||
|         size="large" |  | ||||||
|       placeholder="Select your mimetype here... (ex: application/pdf)" |       placeholder="Select your mimetype here... (ex: application/pdf)" | ||||||
|     /> |     /> | ||||||
|     </n-form-item> |  | ||||||
| 
 | 
 | ||||||
|     <div v-if="selectedExtension"> |     <div v-if="selectedExtension"> | ||||||
|       Mime type associated to the extension <n-tag round :bordered="false"> |       Mime type associated to the extension <n-tag round :bordered="false"> | ||||||
|  | |||||||
| @ -85,9 +85,7 @@ const countriesOptions = getCountries().map(code => ({ | |||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <n-form-item label="Default country code:"> |     <c-select v-model:value="defaultCountryCode" label="Default country code:" :options="countriesOptions" searchable mb-5 /> | ||||||
|       <n-select v-model:value="defaultCountryCode" :options="countriesOptions" filterable /> |  | ||||||
|     </n-form-item> |  | ||||||
| 
 | 
 | ||||||
|     <c-input-text |     <c-input-text | ||||||
|       v-model:value="rawPhone" |       v-model:value="rawPhone" | ||||||
|  | |||||||
| @ -35,6 +35,7 @@ const { download } = useDownloadFileFromBase64({ source: qrcode, filename: 'qr-c | |||||||
|           label="Text:" |           label="Text:" | ||||||
|           multiline |           multiline | ||||||
|           rows="1" |           rows="1" | ||||||
|  |           autosize | ||||||
|           placeholder="Your link or text..." |           placeholder="Your link or text..." | ||||||
|           mb-6 |           mb-6 | ||||||
|         /> |         /> | ||||||
| @ -45,12 +46,14 @@ const { download } = useDownloadFileFromBase64({ source: qrcode, filename: 'qr-c | |||||||
|           <n-form-item label="Background color:"> |           <n-form-item label="Background color:"> | ||||||
|             <n-color-picker v-model:value="background" :modes="['hex']" /> |             <n-color-picker v-model:value="background" :modes="['hex']" /> | ||||||
|           </n-form-item> |           </n-form-item> | ||||||
|           <n-form-item label="Error resistance:"> |           <c-select | ||||||
|             <n-select |  | ||||||
|             v-model:value="errorCorrectionLevel" |             v-model:value="errorCorrectionLevel" | ||||||
|  |             label="Error resistance:" | ||||||
|  |             label-position="left" | ||||||
|  |             label-width="130px" | ||||||
|  |             label-align="right" | ||||||
|             :options="errorCorrectionLevels.map((value) => ({ label: value, value }))" |             :options="errorCorrectionLevels.map((value) => ({ label: value, value }))" | ||||||
|           /> |           /> | ||||||
|           </n-form-item> |  | ||||||
|         </n-form> |         </n-form> | ||||||
|       </n-gi> |       </n-gi> | ||||||
|       <n-gi> |       <n-gi> | ||||||
|  | |||||||
| @ -19,10 +19,11 @@ const prettySQL = computed(() => formatSQL(rawSQL.value, config)); | |||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <div style="flex: 0 0 100%"> |   <div style="flex: 0 0 100%"> | ||||||
|     <div mx-auto style="max-width: 600px" flex gap-2 :class="{ 'flex-col': styleStore.isSmallScreen }"> |     <div style="max-width: 600px" :class="{ 'flex-col': styleStore.isSmallScreen }" mx-auto mb-5 flex gap-2> | ||||||
|       <n-form-item label="Dialect" label-width="500" flex-1> |       <c-select | ||||||
|         <n-select |  | ||||||
|         v-model:value="config.language" |         v-model:value="config.language" | ||||||
|  |         flex-1 | ||||||
|  |         label="Dialect" | ||||||
|         :options="[ |         :options="[ | ||||||
|           { label: 'GCP BigQuery', value: 'bigquery' }, |           { label: 'GCP BigQuery', value: 'bigquery' }, | ||||||
|           { label: 'IBM DB2', value: 'db2' }, |           { label: 'IBM DB2', value: 'db2' }, | ||||||
| @ -39,27 +40,24 @@ const prettySQL = computed(() => formatSQL(rawSQL.value, config)); | |||||||
|           { label: 'SQL Server Transact-SQL', value: 'tsql' }, |           { label: 'SQL Server Transact-SQL', value: 'tsql' }, | ||||||
|         ]" |         ]" | ||||||
|       /> |       /> | ||||||
|       </n-form-item> |       <c-select | ||||||
|       <n-form-item label="Keyword case" flex-1> |         v-model:value="config.keywordCase" label="Keyword case" | ||||||
|         <n-select |         flex-1 | ||||||
|           v-model:value="config.keywordCase" |  | ||||||
|         :options="[ |         :options="[ | ||||||
|           { label: 'UPPERCASE', value: 'upper' }, |           { label: 'UPPERCASE', value: 'upper' }, | ||||||
|           { label: 'lowercase', value: 'lower' }, |           { label: 'lowercase', value: 'lower' }, | ||||||
|           { label: 'Preserve', value: 'preserve' }, |           { label: 'Preserve', value: 'preserve' }, | ||||||
|         ]" |         ]" | ||||||
|       /> |       /> | ||||||
|       </n-form-item> |       <c-select | ||||||
|       <n-form-item label="Indent style" flex-1> |         v-model:value="config.indentStyle" label="Indent style" | ||||||
|         <n-select |         flex-1 | ||||||
|           v-model:value="config.indentStyle" |  | ||||||
|         :options="[ |         :options="[ | ||||||
|           { label: 'Standard', value: 'standard' }, |           { label: 'Standard', value: 'standard' }, | ||||||
|           { label: 'Tabular left', value: 'tabularLeft' }, |           { label: 'Tabular left', value: 'tabularLeft' }, | ||||||
|           { label: 'Tabular right', value: 'tabularRight' }, |           { label: 'Tabular right', value: 'tabularRight' }, | ||||||
|         ]" |         ]" | ||||||
|       /> |       /> | ||||||
|       </n-form-item> |  | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										7
									
								
								src/ui/c-label/c-label.types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/ui/c-label/c-label.types.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | export interface CLabelProps { | ||||||
|  |   label?: string | ||||||
|  |   labelFor?: string | ||||||
|  |   labelPosition?: 'top' | 'left' | ||||||
|  |   labelWidth?: string | ||||||
|  |   labelAlign?: 'left' | 'right' | 'center' | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								src/ui/c-label/c-label.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/ui/c-label/c-label.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | |||||||
|  | <script lang="ts" setup> | ||||||
|  | import { toRefs } from 'vue'; | ||||||
|  | import type { CLabelProps } from './c-label.types'; | ||||||
|  | 
 | ||||||
|  | const props = withDefaults(defineProps<CLabelProps>(), { label: undefined, labelAlign: 'left', labelFor: undefined, labelPosition: 'top', labelWidth: 'auto' }); | ||||||
|  | const { label, labelAlign, labelFor, labelPosition, labelWidth } = toRefs(props); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <div | ||||||
|  |     :class="{ | ||||||
|  |       'flex-col': labelPosition === 'top', | ||||||
|  |       'flex-row': labelPosition === 'left', | ||||||
|  |     }" | ||||||
|  |     flex | ||||||
|  |     items-baseline | ||||||
|  |   > | ||||||
|  |     <label | ||||||
|  |       v-if="label" :for="labelFor" :style="{ flex: `0 0 ${labelWidth}` }" | ||||||
|  |       mb-5px | ||||||
|  |       pr-12px | ||||||
|  |       :class="{ | ||||||
|  |         'text-left': labelAlign === 'left', | ||||||
|  |         'text-center': labelAlign === 'center', | ||||||
|  |         'text-right': labelAlign === 'right', | ||||||
|  |       }" | ||||||
|  |     > | ||||||
|  |       {{ label }} | ||||||
|  |     </label> | ||||||
|  |     <slot /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
							
								
								
									
										36
									
								
								src/ui/c-select/c-select.demo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/ui/c-select/c-select.demo.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | |||||||
|  | <script lang="ts" setup> | ||||||
|  | const optionsA = [ | ||||||
|  |   { label: 'Option A', value: 'a' }, | ||||||
|  |   { label: 'Option B', value: 'b' }, | ||||||
|  |   { label: 'Option C', value: 'c' }, | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | const optionsBig = Array.from({ length: 1000 }, (_, i) => ({ label: `Option ${i}`, value: i })); | ||||||
|  | 
 | ||||||
|  | const sizes = ['small', 'medium', 'large'] as const; | ||||||
|  | const value = ref(''); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <h2>Sizes</h2> | ||||||
|  |   <c-select v-for="size in sizes" :key="size" v-model:value="value" :options="optionsA" :size="size" mb-2 /> | ||||||
|  | 
 | ||||||
|  |   <h2>Searchable</h2> | ||||||
|  |   <c-select v-for="size in sizes" :key="size" v-model:value="value" :options="optionsA" :size="size" searchable mb-2 /> | ||||||
|  | 
 | ||||||
|  |   <h2>Big list</h2> | ||||||
|  |   <c-select v-model:value="value" :options="optionsBig" searchable /> | ||||||
|  | 
 | ||||||
|  |   <h2>Empty</h2> | ||||||
|  |   <c-select :options="[]" /> | ||||||
|  | 
 | ||||||
|  |   <h2>String array as options</h2> | ||||||
|  |   <c-select v-model:value="value" :options="['a', 'Option B', 'Option C']" /> | ||||||
|  | 
 | ||||||
|  |   <h2>Labels</h2> | ||||||
|  |   <c-select label="Label" mb-2 /> | ||||||
|  |   <c-select label="Label" label-position="left" mb-2 /> | ||||||
|  |   <c-select label="Label" label-position="left" label-align="left" mb-2 label-width="200px" /> | ||||||
|  |   <c-select label="Label" label-position="left" label-align="center" mb-2 label-width="200px" /> | ||||||
|  |   <c-select label="Label" label-position="left" label-align="right" mb-2 label-width="200px" /> | ||||||
|  | </template> | ||||||
							
								
								
									
										60
									
								
								src/ui/c-select/c-select.theme.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/ui/c-select/c-select.theme.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | |||||||
|  | import { defineThemes } from '../theme/theme.models'; | ||||||
|  | import { appThemes } from '../theme/themes'; | ||||||
|  | 
 | ||||||
|  | const sizes = { | ||||||
|  |   small: { | ||||||
|  |     height: '28px', | ||||||
|  |     fontSize: '12px', | ||||||
|  |   }, | ||||||
|  |   medium: { | ||||||
|  |     height: '34px', | ||||||
|  |     fontSize: '14px', | ||||||
|  |   }, | ||||||
|  |   large: { | ||||||
|  |     height: '40px', | ||||||
|  |     fontSize: '16px', | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const { useTheme } = defineThemes({ | ||||||
|  |   dark: { | ||||||
|  |     sizes, | ||||||
|  | 
 | ||||||
|  |     backgroundColor: '#333333', | ||||||
|  |     borderColor: '#333333', | ||||||
|  |     dropdownShadow: 'rgba(0, 0, 0, 0.2) 0px 8px 24px', | ||||||
|  | 
 | ||||||
|  |     option: { | ||||||
|  |       hover: { | ||||||
|  |         backgroundColor: '#444444', | ||||||
|  |       }, | ||||||
|  |       active: { | ||||||
|  |         textColor: appThemes.dark.primary.color, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     focus: { | ||||||
|  |       backgroundColor: '#1ea54c1a', | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   light: { | ||||||
|  |     sizes, | ||||||
|  | 
 | ||||||
|  |     backgroundColor: '#ffffff', | ||||||
|  |     borderColor: '#e0e0e69e', | ||||||
|  |     dropdownShadow: 'rgba(149, 157, 165, 0.2) 0px 8px 24px', | ||||||
|  | 
 | ||||||
|  |     option: { | ||||||
|  |       hover: { | ||||||
|  |         backgroundColor: '#eee', | ||||||
|  |       }, | ||||||
|  |       active: { | ||||||
|  |         textColor: appThemes.light.primary.color, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     focus: { | ||||||
|  |       backgroundColor: '#ffffff', | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }); | ||||||
							
								
								
									
										4
									
								
								src/ui/c-select/c-select.types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/ui/c-select/c-select.types.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | export interface CSelectOption<Value = unknown> { | ||||||
|  |   label: string | ||||||
|  |   value: Value | ||||||
|  | } | ||||||
							
								
								
									
										262
									
								
								src/ui/c-select/c-select.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								src/ui/c-select/c-select.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,262 @@ | |||||||
|  | <script setup lang="ts" generic="T extends unknown"> | ||||||
|  | import { useAppTheme } from '../theme/themes'; | ||||||
|  | import type { CLabelProps } from '../c-label/c-label.types'; | ||||||
|  | import type { CSelectOption } from './c-select.types'; | ||||||
|  | import { useTheme } from './c-select.theme'; | ||||||
|  | import { clamp } from '@/modules/shared/number.models'; | ||||||
|  | import { useFuzzySearch } from '@/composable/fuzzySearch'; | ||||||
|  | 
 | ||||||
|  | const props = withDefaults( | ||||||
|  |   defineProps<{ | ||||||
|  |     options?: CSelectOption<T>[] | string[] | ||||||
|  |     value?: T | ||||||
|  |     placeholder?: string | ||||||
|  |     size?: 'small' | 'medium' | 'large' | ||||||
|  |     searchable?: boolean | ||||||
|  |   } & CLabelProps >(), | ||||||
|  |   { | ||||||
|  |     options: () => [], | ||||||
|  |     value: undefined, | ||||||
|  |     placeholder: undefined, | ||||||
|  |     size: 'medium', | ||||||
|  |     searchable: false, | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const emits = defineEmits(['update:value']); | ||||||
|  | 
 | ||||||
|  | const { options: rawOptions, placeholder, size: sizeName, searchable } = toRefs(props); | ||||||
|  | 
 | ||||||
|  | const options = computed(() => { | ||||||
|  |   return rawOptions.value.map((option: string | CSelectOption<T>) => { | ||||||
|  |     if (typeof option === 'string') { | ||||||
|  |       return { label: option, value: option }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return option; | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const keys = useMagicKeys(); | ||||||
|  | const value = useVModel(props, 'value', emits); | ||||||
|  | const theme = useTheme(); | ||||||
|  | const appTheme = useAppTheme(); | ||||||
|  | 
 | ||||||
|  | const isOpen = ref(false); | ||||||
|  | const selectedOption = shallowRef<CSelectOption<T> | undefined>(options.value.find((option: CSelectOption<T>) => option.value === value.value)); | ||||||
|  | const focusIndex = ref(0); | ||||||
|  | const elementRef = ref(null); | ||||||
|  | 
 | ||||||
|  | const size = computed(() => theme.value.sizes[sizeName.value as 'small' | 'medium' | 'large']); | ||||||
|  | 
 | ||||||
|  | const searchQuery = ref(''); | ||||||
|  | const searchInputRef = ref(); | ||||||
|  | 
 | ||||||
|  | whenever(() => !isOpen.value, () => { | ||||||
|  |   focusIndex.value = 0; | ||||||
|  |   searchQuery.value = ''; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | whenever(() => isOpen.value, () => { | ||||||
|  |   nextTick(() => searchInputRef.value?.focus()); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | onClickOutside(elementRef, close); | ||||||
|  | whenever(keys.escape, close); | ||||||
|  | 
 | ||||||
|  | watch( | ||||||
|  |   value, | ||||||
|  |   (newValue) => { | ||||||
|  |     const option = options.value.find((option: CSelectOption<T>) => option.value === newValue); | ||||||
|  |     if (option) { | ||||||
|  |       selectedOption.value = option; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const { searchResult: filteredOptions } = useFuzzySearch<CSelectOption<T>>({ | ||||||
|  |   search: searchQuery, | ||||||
|  |   data: options.value, | ||||||
|  |   options: { | ||||||
|  |     keys: ['label'], | ||||||
|  |     shouldSort: false, | ||||||
|  |     threshold: 0.3, | ||||||
|  |     filterEmpty: false, | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function close() { | ||||||
|  |   isOpen.value = false; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function toggleOpen() { | ||||||
|  |   isOpen.value = !isOpen.value; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function selectOption({ option }: { option: CSelectOption<T> }) { | ||||||
|  |   selectedOption.value = option; | ||||||
|  |   // @ts-expect-error vue template generic is a bit flacky thanks to withDefaults | ||||||
|  |   value.value = option.value; | ||||||
|  |   isOpen.value = false; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function handleKeydown(event: KeyboardEvent) { | ||||||
|  |   const { key } = event; | ||||||
|  |   const isEnter = ['Enter'].includes(key); | ||||||
|  |   const isArrowUpOrDown = ['ArrowUp', 'ArrowDown'].includes(key); | ||||||
|  |   const isArrowDown = key === 'ArrowDown'; | ||||||
|  | 
 | ||||||
|  |   if (isEnter) { | ||||||
|  |     const valueCanBeSelected = isOpen.value && focusIndex.value !== -1; | ||||||
|  | 
 | ||||||
|  |     if (valueCanBeSelected) { | ||||||
|  |       selectOption({ option: filteredOptions.value[focusIndex.value] }); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |       toggleOpen(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     event.preventDefault(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (isArrowUpOrDown) { | ||||||
|  |     const increment = isArrowDown ? 1 : -1; | ||||||
|  |     focusIndex.value = clamp({ | ||||||
|  |       value: focusIndex.value + increment, | ||||||
|  |       min: 0, | ||||||
|  |       max: options.value.length - 1, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     event.preventDefault(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function onSearchInput() { | ||||||
|  |   focusIndex.value = 0; | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <c-label v-bind="props"> | ||||||
|  |     <div ref="elementRef" relative class="c-select" w-full> | ||||||
|  |       <div | ||||||
|  |         flex flex-nowrap cursor-pointer items-center | ||||||
|  |         :class="{ 'is-open': isOpen, 'important:border-primary': isOpen }" | ||||||
|  |         class="c-select-input" | ||||||
|  |         tabindex="0" | ||||||
|  |         hover:important:border-primary | ||||||
|  |         @click="toggleOpen" | ||||||
|  |         @keydown="handleKeydown" | ||||||
|  |       > | ||||||
|  |         <div flex-1 truncate> | ||||||
|  |           <input v-if="searchable && isOpen" ref="searchInputRef" v-model="searchQuery" type="text" placeholder="Search..." class="search-input" w-full lh-normal color-current @input="onSearchInput"> | ||||||
|  |           <span v-else-if="selectedOption" lh-normal> | ||||||
|  |             {{ selectedOption.label }} | ||||||
|  |           </span> | ||||||
|  |           <span v-else class="placeholder" lh-normal> | ||||||
|  |             {{ placeholder ?? 'Select an option' }} | ||||||
|  |           </span> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <icon-mdi-chevron-down class="chevron" /> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <transition name="dropdown"> | ||||||
|  |         <div v-show="isOpen" class="c-select-dropdown" absolute z-10 mt-1 max-h-312px w-full overflow-y-auto pretty-scrollbar> | ||||||
|  |           <template v-if="!filteredOptions.length"> | ||||||
|  |             <slot name="empty"> | ||||||
|  |               <div px-4 py-1 opacity-70> | ||||||
|  |                 No results found | ||||||
|  |               </div> | ||||||
|  |             </slot> | ||||||
|  |           </template> | ||||||
|  |           <template v-else> | ||||||
|  |             <div | ||||||
|  |               v-for="(option, index) in filteredOptions" | ||||||
|  |               :key="option.label" | ||||||
|  |               cursor-pointer | ||||||
|  |               px-4 | ||||||
|  |               py-1 | ||||||
|  |               :class="{ active: selectedOption?.label === option.label, hover: focusIndex === index }" | ||||||
|  |               class="c-select-dropdown-option" | ||||||
|  |               @click="selectOption({ option })" | ||||||
|  |             > | ||||||
|  |               {{ option.label }} | ||||||
|  |             </div> | ||||||
|  |           </template> | ||||||
|  |         </div> | ||||||
|  |       </transition> | ||||||
|  |     </div> | ||||||
|  |   </c-label> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style lang="less" scoped> | ||||||
|  | .c-select { | ||||||
|  |   .search-input{ | ||||||
|  |     all: unset; | ||||||
|  | 
 | ||||||
|  |     &::placeholder { | ||||||
|  |       color: v-bind('appTheme.text.mutedColor'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .c-select-input { | ||||||
|  |     background-color: v-bind('theme.backgroundColor'); | ||||||
|  |     border: 1px solid v-bind('theme.borderColor'); | ||||||
|  |     border-radius: 4px; | ||||||
|  |     padding: 0 12px; | ||||||
|  |     font-family: inherit; | ||||||
|  |     font-size: v-bind('size.fontSize'); | ||||||
|  |     height: v-bind('size.height'); | ||||||
|  |     transition: border-color 0.2s ease-in-out; | ||||||
|  | 
 | ||||||
|  |     .placeholder, .chevron { | ||||||
|  |       color: v-bind('appTheme.text.mutedColor'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .c-select-dropdown { | ||||||
|  |     background-color: v-bind('theme.backgroundColor'); | ||||||
|  |     border-radius: 4px; | ||||||
|  |     // box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; | ||||||
|  |     box-shadow: v-bind('theme.dropdownShadow'); | ||||||
|  |     font-family: inherit; | ||||||
|  |     font-size: inherit; | ||||||
|  |     line-height: 1; | ||||||
|  |     padding: 6px; | ||||||
|  | 
 | ||||||
|  |     .c-select-dropdown-option{ | ||||||
|  |       border-radius: 4px; | ||||||
|  |       padding: 8px 12px; | ||||||
|  |       background-color: transparent; | ||||||
|  |       transition: background-color 0.2s ease-in-out; | ||||||
|  | 
 | ||||||
|  |       &.active { | ||||||
|  |         color: v-bind('theme.option.active.textColor'); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       &:hover, &.hover { | ||||||
|  |         background-color: v-bind('theme.option.hover.backgroundColor'); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .dropdown-enter-active, | ||||||
|  | .dropdown-leave-active { | ||||||
|  |   transition: opacity 0.2s, transform 0.2s; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .dropdown-enter-from, | ||||||
|  | .dropdown-leave-to { | ||||||
|  |   opacity: 0; | ||||||
|  |   transform: translateY(-10px); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .dropdown-enter-to, | ||||||
|  | .dropdown-leave-from { | ||||||
|  |   opacity: 1; | ||||||
|  |   transform: translateY(0); | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										13
									
								
								src/ui/demo/demo-home.page.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/ui/demo/demo-home.page.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | <script lang="ts" setup> | ||||||
|  | import { demoRoutes } from './demo.routes'; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <div grid grid-cols-5 gap-2> | ||||||
|  |     <c-card v-for="{ name } of demoRoutes" :key="name" :title="String(name)"> | ||||||
|  |       <c-button :to="{ name }"> | ||||||
|  |         {{ name }} | ||||||
|  |       </c-button> | ||||||
|  |     </c-card> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
| @ -1,4 +1,5 @@ | |||||||
| import type { RouteRecordRaw } from 'vue-router'; | import type { RouteRecordRaw } from 'vue-router'; | ||||||
|  | import DemoHome from './demo-home.page.vue'; | ||||||
| 
 | 
 | ||||||
| const demoPages = import.meta.glob('../*/*.demo.vue'); | const demoPages = import.meta.glob('../*/*.demo.vue'); | ||||||
| 
 | 
 | ||||||
| @ -17,7 +18,14 @@ export const routes = [ | |||||||
|   { |   { | ||||||
|     path: '/c-lib', |     path: '/c-lib', | ||||||
|     name: 'c-lib', |     name: 'c-lib', | ||||||
|     children: demoRoutes, |     children: [ | ||||||
|  |       { | ||||||
|  |         path: '', | ||||||
|  |         name: 'c-lib-index', | ||||||
|  |         component: DemoHome, | ||||||
|  |       }, | ||||||
|  |       ...demoRoutes, | ||||||
|  |     ], | ||||||
|     component: () => import('./demo-wrapper.vue'), |     component: () => import('./demo-wrapper.vue'), | ||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
|  | |||||||
| @ -7,12 +7,17 @@ import { | |||||||
|   transformerVariantGroup, |   transformerVariantGroup, | ||||||
| } from 'unocss'; | } from 'unocss'; | ||||||
| 
 | 
 | ||||||
|  | import { presetScrollbar } from 'unocss-preset-scrollbar'; | ||||||
|  | 
 | ||||||
| export default defineConfig({ | export default defineConfig({ | ||||||
|   presets: [presetUno(), presetAttributify(), presetTypography()], |   presets: [presetUno(), presetAttributify(), presetTypography(), presetScrollbar()], | ||||||
|   transformers: [transformerDirectives(), transformerVariantGroup()], |   transformers: [transformerDirectives(), transformerVariantGroup()], | ||||||
|   theme: { |   theme: { | ||||||
|     colors: { |     colors: { | ||||||
|       primary: '#1ea54c', |       primary: '#1ea54c', | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   shortcuts: { | ||||||
|  |     'pretty-scrollbar': 'scrollbar scrollbar-rounded scrollbar-thumb-color-gray-300 scrollbar-track-color-gray-100 dark:scrollbar-thumb-color-#424242 dark:scrollbar-track-color-#686868', | ||||||
|  |   }, | ||||||
| }); | }); | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user