mirror of
https://github.com/CorentinTh/it-tools.git
synced 2024-08-16 16:50:41 +03:00
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",
|
||||||
|
@ -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