diff options
author | 2023-05-14 21:26:18 +0200 | |
---|---|---|
committer | 2023-05-14 22:30:23 +0200 | |
commit | 77f2efc0b92847c3b1198446f4b520ecb2263164 (patch) | |
tree | 942ea5ffd922dbe7a9913e235a4fae17b117c925 | |
parent | aad8d84e13ce31c1b7c1cbb930fb8bd4c0abe13a (diff) | |
download | it-tools-77f2efc0b92847c3b1198446f4b520ecb2263164.tar.gz it-tools-77f2efc0b92847c3b1198446f4b520ecb2263164.tar.zst it-tools-77f2efc0b92847c3b1198446f4b520ecb2263164.zip |
refactor(ui): replaced some n-input with c-input-text
39 files changed, 742 insertions, 452 deletions
diff --git a/auto-imports.d.ts b/auto-imports.d.ts index 2850890..9dccb44 100644 --- a/auto-imports.d.ts +++ b/auto-imports.d.ts @@ -19,7 +19,9 @@ declare global { const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] + const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] + const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] const customRef: typeof import('vue')['customRef'] const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] @@ -39,9 +41,6 @@ declare global { const isReactive: typeof import('vue')['isReactive'] const isReadonly: typeof import('vue')['isReadonly'] const isRef: typeof import('vue')['isRef'] - const logicAnd: typeof import('@vueuse/core')['logicAnd'] - const logicNot: typeof import('@vueuse/core')['logicNot'] - const logicOr: typeof import('@vueuse/core')['logicOr'] const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] const markRaw: typeof import('vue')['markRaw'] const nextTick: typeof import('vue')['nextTick'] @@ -92,8 +91,9 @@ declare global { const throttledWatch: typeof import('@vueuse/core')['throttledWatch'] const toRaw: typeof import('vue')['toRaw'] const toReactive: typeof import('@vueuse/core')['toReactive'] - const toRef: typeof import('vue')['toRef'] + const toRef: typeof import('@vueuse/core')['toRef'] const toRefs: typeof import('vue')['toRefs'] + const toValue: typeof import('@vueuse/core')['toValue'] const triggerRef: typeof import('vue')['triggerRef'] const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount'] @@ -104,6 +104,19 @@ declare global { const unrefElement: typeof import('@vueuse/core')['unrefElement'] const until: typeof import('@vueuse/core')['until'] const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] + const useAnimate: typeof import('@vueuse/core')['useAnimate'] + const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference'] + const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] + const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] + const useArrayFind: typeof import('@vueuse/core')['useArrayFind'] + const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex'] + const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast'] + const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes'] + const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin'] + const useArrayMap: typeof import('@vueuse/core')['useArrayMap'] + const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce'] + const useArraySome: typeof import('@vueuse/core')['useArraySome'] + const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique'] const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue'] const useAsyncState: typeof import('@vueuse/core')['useAsyncState'] const useAttrs: typeof import('vue')['useAttrs'] @@ -114,8 +127,8 @@ declare global { const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel'] const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] const useCached: typeof import('@vueuse/core')['useCached'] - const useClamp: typeof import('@vueuse/core')['useClamp'] const useClipboard: typeof import('@vueuse/core')['useClipboard'] + const useCloned: typeof import('@vueuse/core')['useCloned'] const useColorMode: typeof import('@vueuse/core')['useColorMode'] const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] const useCounter: typeof import('@vueuse/core')['useCounter'] @@ -189,12 +202,18 @@ declare global { const useOnline: typeof import('@vueuse/core')['useOnline'] const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] const useParallax: typeof import('@vueuse/core')['useParallax'] + const useParentElement: typeof import('@vueuse/core')['useParentElement'] + const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver'] const usePermission: typeof import('@vueuse/core')['usePermission'] const usePointer: typeof import('@vueuse/core')['usePointer'] + const usePointerLock: typeof import('@vueuse/core')['usePointerLock'] const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe'] const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme'] + const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast'] const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] + const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] + const usePrevious: typeof import('@vueuse/core')['usePrevious'] const useRafFn: typeof import('@vueuse/core')['useRafFn'] const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] @@ -208,14 +227,17 @@ declare global { const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage'] const useShare: typeof import('@vueuse/core')['useShare'] const useSlots: typeof import('vue')['useSlots'] + const useSorted: typeof import('@vueuse/core')['useSorted'] const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] const useStepper: typeof import('@vueuse/core')['useStepper'] const useStorage: typeof import('@vueuse/core')['useStorage'] const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] + const useSupported: typeof import('@vueuse/core')['useSupported'] const useSwipe: typeof import('@vueuse/core')['useSwipe'] const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] + const useTextDirection: typeof import('@vueuse/core')['useTextDirection'] const useTextSelection: typeof import('@vueuse/core')['useTextSelection'] const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize'] const useThrottle: typeof import('@vueuse/core')['useThrottle'] @@ -227,6 +249,8 @@ declare global { const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] const useTimestamp: typeof import('@vueuse/core')['useTimestamp'] const useTitle: typeof import('@vueuse/core')['useTitle'] + const useToNumber: typeof import('@vueuse/core')['useToNumber'] + const useToString: typeof import('@vueuse/core')['useToString'] const useToggle: typeof import('@vueuse/core')['useToggle'] const useTransition: typeof import('@vueuse/core')['useTransition'] const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams'] @@ -247,8 +271,10 @@ declare global { const watchArray: typeof import('@vueuse/core')['watchArray'] const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] + const watchDeep: typeof import('@vueuse/core')['watchDeep'] const watchEffect: typeof import('vue')['watchEffect'] const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable'] + const watchImmediate: typeof import('@vueuse/core')['watchImmediate'] const watchOnce: typeof import('@vueuse/core')['watchOnce'] const watchPausable: typeof import('@vueuse/core')['watchPausable'] const watchPostEffect: typeof import('vue')['watchPostEffect'] @@ -282,7 +308,9 @@ declare module 'vue' { readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']> readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']> readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']> + readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']> readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']> + readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']> readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']> readonly customRef: UnwrapRef<typeof import('vue')['customRef']> readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']> @@ -302,9 +330,6 @@ declare module 'vue' { readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']> readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']> readonly isRef: UnwrapRef<typeof import('vue')['isRef']> - readonly logicAnd: UnwrapRef<typeof import('@vueuse/core')['logicAnd']> - readonly logicNot: UnwrapRef<typeof import('@vueuse/core')['logicNot']> - readonly logicOr: UnwrapRef<typeof import('@vueuse/core')['logicOr']> readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']> readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']> readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']> @@ -355,8 +380,9 @@ declare module 'vue' { readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']> readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']> readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']> - readonly toRef: UnwrapRef<typeof import('vue')['toRef']> + readonly toRef: UnwrapRef<typeof import('@vueuse/core')['toRef']> readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']> + readonly toValue: UnwrapRef<typeof import('@vueuse/core')['toValue']> readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']> readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']> readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']> @@ -367,6 +393,19 @@ declare module 'vue' { readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']> readonly until: UnwrapRef<typeof import('@vueuse/core')['until']> readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']> + readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']> + readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']> + readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']> + readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']> + readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']> + readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']> + readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']> + readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']> + readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']> + readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']> + readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']> + readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']> + readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']> readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']> readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']> readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']> @@ -377,8 +416,8 @@ declare module 'vue' { readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']> readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']> readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']> - readonly useClamp: UnwrapRef<typeof import('@vueuse/core')['useClamp']> readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']> + readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']> readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']> readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']> readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']> @@ -452,12 +491,18 @@ declare module 'vue' { readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']> readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']> readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']> + readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']> + readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']> readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']> readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']> + readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']> readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']> readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']> + readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']> readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']> readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']> + readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']> + readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']> readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']> readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']> readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']> @@ -471,14 +516,17 @@ declare module 'vue' { readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']> readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']> readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']> + readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']> readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']> readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']> readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']> readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']> readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']> readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']> + readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']> readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']> readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']> + readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']> readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']> readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']> readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']> @@ -490,6 +538,8 @@ declare module 'vue' { readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']> readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']> readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']> + readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']> + readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']> readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']> readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']> readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']> @@ -510,8 +560,10 @@ declare module 'vue' { readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']> readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']> readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']> + readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']> readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']> readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']> + readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']> readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']> readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']> readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']> diff --git a/components.d.ts b/components.d.ts index 65c82ba..40b805a 100644 --- a/components.d.ts +++ b/components.d.ts @@ -56,7 +56,12 @@ declare module '@vue/runtime-core' { HtmlEntities: typeof import('./src/tools/html-entities/html-entities.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'] + IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default'] IconMdiClose: typeof import('~icons/mdi/close')['default'] + IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default'] + IconMdiEye: typeof import('~icons/mdi/eye')['default'] + IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default'] + IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] InputCopyable: typeof import('./src/components/InputCopyable.vue')['default'] IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default'] Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default'] diff --git a/src/components/FormatTransformer.vue b/src/components/FormatTransformer.vue index dea5d56..d187632 100644 --- a/src/components/FormatTransformer.vue +++ b/src/components/FormatTransformer.vue @@ -1,5 +1,5 @@ <template> - <n-form-item :label="inputLabel" v-bind="validationAttrs"> + <n-form-item :label="inputLabel" v-bind="validationAttrs as any"> <n-input ref="inputElement" v-model:value="input" @@ -10,7 +10,7 @@ autocorrect="off" autocapitalize="off" spellcheck="false" - :input-props="{ 'data-test-id': 'input' }" + :input-props="{ 'data-test-id': 'input' } as any" /> </n-form-item> <n-form-item :label="outputLabel"> diff --git a/src/components/InputCopyable.vue b/src/components/InputCopyable.vue index eaf29c8..cf7a42a 100644 --- a/src/components/InputCopyable.vue +++ b/src/components/InputCopyable.vue @@ -1,21 +1,20 @@ <template> - <n-input v-model:value="value"> + <c-input-text v-model:value="value"> <template #suffix> <n-tooltip trigger="hover"> <template #trigger> - <c-button circle variant="text" @click="onCopyClicked"> - <n-icon :component="ContentCopyFilled" /> + <c-button circle variant="text" size="small" @click="onCopyClicked"> + <icon-mdi-content-copy /> </c-button> </template> {{ tooltipText }} </n-tooltip> </template> - </n-input> + </c-input-text> </template> <script setup lang="ts"> import { useVModel, useClipboard } from '@vueuse/core'; -import { ContentCopyFilled } from '@vicons/material'; import { ref } from 'vue'; const props = defineProps<{ value: string }>(); @@ -35,9 +34,3 @@ function onCopyClicked() { }, 2000); } </script> - -<style scoped> -::v-deep(.n-input-wrapper) { - padding-right: 5px; -} -</style> diff --git a/src/tools/base64-string-converter/base64-string-converter.vue b/src/tools/base64-string-converter/base64-string-converter.vue index be4a635..e97c884 100644 --- a/src/tools/base64-string-converter/base64-string-converter.vue +++ b/src/tools/base64-string-converter/base64-string-converter.vue @@ -20,7 +20,7 @@ </c-card> <c-card title="Base64 to string"> - <n-form-item label="Base64 string to decode" v-bind="b64Validation.attrs"> + <n-form-item label="Base64 string to decode" v-bind="b64Validation.attrs as any"> <n-input v-model:value="base64Input" type="textarea" placeholder="Your base64 string..." rows="5" /> </n-form-item> diff --git a/src/tools/basic-auth-generator/basic-auth-generator.vue b/src/tools/basic-auth-generator/basic-auth-generator.vue index 3cab019..6764748 100644 --- a/src/tools/basic-auth-generator/basic-auth-generator.vue +++ b/src/tools/basic-auth-generator/basic-auth-generator.vue @@ -1,17 +1,15 @@ <template> <div> - <n-form-item label="Username"> - <n-input v-model:value="username" placeholder="Your username..." clearable /> - </n-form-item> - <n-form-item label="Password"> - <n-input - v-model:value="password" - placeholder="Your password..." - type="password" - show-password-on="click" - clearable - /> - </n-form-item> + <c-input-text v-model:value="username" label="Username" placeholder="Your username..." clearable raw-text mb-5 /> + <c-input-text + v-model:value="password" + label="Password" + placeholder="Your password..." + clearable + raw-text + mb-2 + type="password" + /> <c-card> <n-statistic label="Authorization header:" class="header"> diff --git a/src/tools/bcrypt/bcrypt.vue b/src/tools/bcrypt/bcrypt.vue index a091fb7..c0c86f6 100644 --- a/src/tools/bcrypt/bcrypt.vue +++ b/src/tools/bcrypt/bcrypt.vue @@ -1,21 +1,20 @@ <template> <c-card title="Hash"> - <n-form label-width="120"> - <n-form-item label="Your string: " label-placement="left"> - <n-input - v-model:value="input" - placeholder="Your string to bcrypt..." - autocomplete="off" - autocorrect="off" - autocapitalize="off" - spellcheck="false" - /> - </n-form-item> - <n-form-item label="Salt count: " label-placement="left"> - <n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="10" :min="0" w-full /> - </n-form-item> - <n-input :value="hashed" readonly style="text-align: center" /> - </n-form> + <c-input-text + v-model:value="input" + placeholder="Your string to bcrypt..." + raw-text + label="Your string: " + label-position="left" + label-width="120px" + mb-2 + /> + <n-form-item label="Salt count: " label-placement="left" label-width="120"> + <n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="10" :min="0" w-full /> + </n-form-item> + + <c-input-text :value="hashed" readonly text-center /> + <n-space justify="center" mt-5> <c-button @click="copy"> Copy hash </c-button> </n-space> @@ -24,24 +23,10 @@ <c-card title="Compare string with hash"> <n-form label-width="120"> <n-form-item label="Your string: " label-placement="left"> - <n-input - v-model:value="compareString" - placeholder="Your string to compare..." - autocomplete="off" - autocorrect="off" - autocapitalize="off" - spellcheck="false" - /> + <c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text /> </n-form-item> <n-form-item label="Your hash: " label-placement="left"> - <n-input - v-model:value="compareHash" - placeholder="Your hahs to compare..." - autocomplete="off" - autocorrect="off" - autocapitalize="off" - spellcheck="false" - /> + <c-input-text v-model:value="compareHash" placeholder="Your hahs to compare..." raw-text /> </n-form-item> <n-form-item label="Do they match ? " label-placement="left" :show-feedback="false"> <div class="compare-result" :class="{ positive: compareMatch }"> diff --git a/src/tools/benchmark-builder/benchmark-builder.vue b/src/tools/benchmark-builder/benchmark-builder.vue index 9943e06..3a6182f 100644 --- a/src/tools/benchmark-builder/benchmark-builder.vue +++ b/src/tools/benchmark-builder/benchmark-builder.vue @@ -1,11 +1,15 @@ <template> <n-scrollbar style="flex: 1" x-scrollable> - <n-space :wrap="false" style="flex: 1" justify="center" :size="0" mb-5> + <n-space :wrap="false" style="flex: 1" justify="center" :size="12" mb-5> <div v-for="(suite, index) of suites" :key="index"> - <c-card style="width: 292px; margin: 0 8px 5px"> - <n-form-item label="Suite name:" :show-feedback="false" label-placement="left"> - <n-input v-model:value="suite.title" placeholder="Suite name..." /> - </n-form-item> + <c-card style="width: 294px"> + <c-input-text + v-model:value="suite.title" + label-position="left" + label="Suite name" + placeholder="Suite name..." + clearable + /> <n-divider></n-divider> <n-form-item label="Suite values" :show-feedback="false"> @@ -33,9 +37,7 @@ <div style="flex: 0 0 100%"> <div style="max-width: 600px; margin: 0 auto"> <n-space justify="center"> - <n-form-item label="Unit:" label-placement="left"> - <n-input v-model:value="unit" placeholder="Unit (eg: ms)" /> - </n-form-item> + <c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 /> <c-button @click=" diff --git a/src/tools/bip39-generator/bip39-generator.vue b/src/tools/bip39-generator/bip39-generator.vue index 418ae0e..1aaa6e9 100644 --- a/src/tools/bip39-generator/bip39-generator.vue +++ b/src/tools/bip39-generator/bip39-generator.vue @@ -16,7 +16,8 @@ :validation-status="entropyValidation.status" > <n-input-group> - <n-input v-model:value="entropy" placeholder="Your string..." /> + <c-input-text v-model:value="entropy" placeholder="Your string..." /> + <c-button @click="refreshEntropy"> <n-icon size="22"> <Refresh /> @@ -37,15 +38,7 @@ :validation-status="mnemonicValidation.status" > <n-input-group> - <n-input - v-model:value="passphrase" - style="text-align: center; flex: 1" - placeholder="Your mnemonic..." - autocomplete="off" - autocorrect="off" - autocapitalize="off" - spellcheck="false" - /> + <c-input-text v-model:value="passphrase" placeholder="Your mnemonic..." raw-text /> <c-button @click="copyPassphrase"> <n-icon size="22" :component="Copy" /> diff --git a/src/tools/case-converter/case-converter.vue b/src/tools/case-converter/case-converter.vue index 854eb91..2ad3656 100644 --- a/src/tools/case-converter/case-converter.vue +++ b/src/tools/case-converter/case-converter.vue @@ -1,9 +1,15 @@ <template> <c-card> <n-form label-width="120" label-placement="left" :show-feedback="false"> - <n-form-item label="Your string:"> - <n-input v-model:value="input" /> - </n-form-item> + <c-input-text + v-model:value="input" + label="Your string" + label-position="left" + label-width="120px" + label-align="right" + placeholder="Your string..." + raw-text + /> <n-divider /> diff --git a/src/tools/chronometer/chronometer.vue b/src/tools/chronometer/chronometer.vue index d3cd425..0c8eb09 100644 --- a/src/tools/chronometer/chronometer.vue +++ b/src/tools/chronometer/chronometer.vue @@ -4,8 +4,8 @@ <div class="duration">{{ formatMs(counter) }}</div> </c-card> <n-space justify="center" mt-5> - <c-button v-if="!isRunning" secondary type="primary" @click="resume">Start</c-button> - <c-button v-else secondary type="warning" @click="pause">Stop</c-button> + <c-button v-if="!isRunning" type="primary" @click="resume">Start</c-button> + <c-button v-else type="warning" @click="pause">Stop</c-button> <c-button @click="counter = 0">Reset</c-button> </n-space> diff --git a/src/tools/color-converter/color-converter.vue b/src/tools/color-converter/color-converter.vue index fff7607..bfd4912 100644 --- a/src/tools/color-converter/color-converter.vue +++ b/src/tools/color-converter/color-converter.vue @@ -9,25 +9,25 @@ /> </n-form-item> <n-form-item label="color name:"> - <input-copyable v-model:value="name" :on-input="(v: string) => onInputUpdated(v, 'name')" /> + <input-copyable v-model:value="name" @update:value="(v: string) => onInputUpdated(v, 'name')" /> </n-form-item> <n-form-item label="hex:"> - <input-copyable v-model:value="hex" :on-input="(v: string) => onInputUpdated(v, 'hex')" /> + <input-copyable v-model:value="hex" @update:value="(v: string) => onInputUpdated(v, 'hex')" /> </n-form-item> <n-form-item label="rgb:"> - <input-copyable v-model:value="rgb" :on-input="(v: string) => onInputUpdated(v, 'rgb')" /> + <input-copyable v-model:value="rgb" @update:value="(v: string) => onInputUpdated(v, 'rgb')" /> </n-form-item> <n-form-item label="hsl:"> - <input-copyable v-model:value="hsl" :on-input="(v: string) => onInputUpdated(v, 'hsl')" /> + <input-copyable v-model:value="hsl" @update:value="(v: string) => onInputUpdated(v, 'hsl')" /> </n-form-item> <n-form-item label="hwb:"> - <input-copyable v-model:value="hwb" :on-input="(v: string) => onInputUpdated(v, 'hwb')" /> + <input-copyable v-model:value="hwb" @update:value="(v: string) => onInputUpdated(v, 'hwb')" /> </n-form-item> <n-form-item label="lch:"> - <input-copyable v-model:value="lch" :on-input="(v: string) => onInputUpdated(v, 'lch')" /> + <input-copyable v-model:value="lch" @update:value="(v: string) => onInputUpdated(v, 'lch')" /> </n-form-item> <n-form-item label="cmyk:"> - <input-copyable v-model:value="cmyk" :on-input="(v: string) => onInputUpdated(v, 'cmyk')" /> + <input-copyable v-model:value="cmyk" @update:value="(v: string) => onInputUpdated(v, 'cmyk')" /> </n-form-item> </n-form> </c-card> @@ -54,15 +54,19 @@ const cmyk = ref(''); const lch = ref(''); function onInputUpdated(value: string, omit: string) { - const color = colord(value); + try { + const color = colord(value); - if (omit !== 'name') name.value = color.toName({ closest: true }) ?? ''; - if (omit !== 'hex') hex.value = color.toHex(); - if (omit !== 'rgb') rgb.value = color.toRgbString(); - if (omit !== 'hsl') hsl.value = color.toHslString(); - if (omit !== 'hwb') hwb.value = color.toHwbString(); - if (omit !== 'cmyk') cmyk.value = color.toCmykString(); - if (omit !== 'lch') lch.value = color.toLchString(); + if (omit !== 'name') name.value = color.toName({ closest: true }) ?? ''; + if (omit !== 'hex') hex.value = color.toHex(); + if (omit !== 'rgb') rgb.value = color.toRgbString(); + if (omit !== 'hsl') hsl.value = color.toHslString(); + if (omit !== 'hwb') hwb.value = color.toHwbString(); + if (omit !== 'cmyk') cmyk.value = color.toCmykString(); + if (omit !== 'lch') lch.value = color.toLchString(); + } catch { + // + } } onInputUpdated(hex.value, 'hex'); diff --git a/src/tools/crontab-generator/crontab-generator.vue b/src/tools/crontab-generator/crontab-generator.vue index 978943d..6f8572d 100644 --- a/src/tools/crontab-generator/crontab-generator.vue +++ b/src/tools/crontab-generator/crontab-generator.vue @@ -1,13 +1,15 @@ <template> <c-card> - <n-form-item - class="cron" - :show-label="false" - :feedback="cronValidation.message" - :validation-status="cronValidation.status" - > - <n-input v-model:value="cron" size="large" placeholder="* * * * *" /> - </n-form-item> + <div mx-auto max-w-sm> + <c-input-text + v-model:value="cron" + size="large" + placeholder="* * * * *" + :validation-rules="cronValidationRules" + mb-3 + /> + </div> + <div class="cron-string"> {{ cronString }} </div> @@ -86,7 +88,6 @@ import cronstrue from 'cronstrue'; import { isValidCron } from 'cron-validator'; import { computed, reactive, ref } from 'vue'; -import { useValidation } from '@/composable/validation'; import { useStyleStore } from '@/stores/style.store'; function isCronValid(v: string) { @@ -185,30 +186,20 @@ const cronString = computed(() => { return ' '; }); -const cronValidation = useValidation({ - source: cron, - rules: [ - { - validator: (value) => isCronValid(value), - message: 'This cron is invalid', - }, - ], -}); +const cronValidationRules = [ + { + validator: (value: string) => isCronValid(value), + message: 'This cron is invalid', + }, +]; </script> <style lang="less" scoped> -.cron { +::v-deep(input) { + font-size: 30px; + font-family: monospace; + padding: 5px; text-align: center; - - margin: auto; - max-width: 400px; - display: block; - - .n-input { - font-size: 30px; - font-family: monospace; - padding: 5px; - } } .cron-string { diff --git a/src/tools/date-time-converter/date-time-converter.vue b/src/tools/date-time-converter/date-time-converter.vue index 0d174fd..276c829 100644 --- a/src/tools/date-time-converter/date-time-converter.vue +++ b/src/tools/date-time-converter/date-time-converter.vue @@ -1,6 +1,6 @@ <template> <div> - <n-form-item :show-label="false" v-bind="validation.attrs"> + <n-form-item :show-label="false" v-bind="validation.attrs as any"> <n-input-group> <n-input v-model:value="inputDate" @@ -8,7 +8,7 @@ :on-input="onDateInputChanged" placeholder="Put you date string here..." clearable - :input-props="{ 'data-test-id': 'date-time-converter-input' }" + :input-props="{ 'data-test-id': 'date-time-converter-input' } as any" /> <n-select @@ -20,16 +20,19 @@ </n-input-group> </n-form-item> <n-divider style="margin-top: 0" /> - <div v-for="{ name, fromDate } in formats" :key="name" mt-1> - <n-input-group> - <n-input-group-label style="flex: 0 0 170px"> {{ name }}: </n-input-group-label> - <input-copyable - :value="formatDateUsingFormatter(fromDate, normalizedDate)" - placeholder="Invalid date..." - :input-props="{ 'data-test-id': name }" - /> - </n-input-group> - </div> + <input-copyable + v-for="{ name, fromDate } in formats" + :key="name" + :label="name" + label-width="150px" + label-position="left" + label-align="right" + :value="formatDateUsingFormatter(fromDate, normalizedDate)" + placeholder="Invalid date..." + :test-id="name" + readonly + mt-2 + /> </div> </template> diff --git a/src/tools/encryption/encryption.vue b/src/tools/encryption/encryption.vue index 881dfa2..d4f6e34 100644 --- a/src/tools/encryption/encryption.vue +++ b/src/tools/encryption/encryption.vue @@ -7,12 +7,15 @@ type="textarea" placeholder="The string to cypher" :autosize="{ minRows: 4 }" + autocomplete="off" + autocorrect="off" + autocapitalize="off" + spellcheck="false" /> </n-form-item> <n-space vertical> - <n-form-item label="Your secret key:" :show-feedback="false"> - <n-input v-model:value="cypherSecret" /> - </n-form-item> + <c-input-text v-model:value="cypherSecret" label="Your secret key:" clearable raw-text /> + <n-form-item label="Encryption algorithm:" :show-feedback="false"> <n-select v-model:value="cypherAlgo" @@ -43,12 +46,15 @@ type="textarea" placeholder="The string to cypher" :autosize="{ minRows: 4 }" + autocomplete="off" + autocorrect="off" + autocapitalize="off" + spellcheck="false" /> </n-form-item> <n-space vertical> - <n-form-item label="Your secret key:" :show-feedback="false"> - <n-input v-model:value="decryptSecret" /> - </n-form-item> + <c-input-text v-model:value="decryptSecret" label="Your secret key:" clearable raw-text /> + <n-form-item label="Encryption algorithm:" :show-feedback="false"> <n-select v-model:value="decryptAlgo" diff --git a/src/tools/integer-base-converter/integer-base-converter.vue b/src/tools/integer-base-converter/integer-base-converter.vue index c480f4a..058d831 100644 --- a/src/tools/integer-base-converter/integer-base-converter.vue +++ b/src/tools/integer-base-converter/integer-base-converter.vue @@ -22,59 +22,54 @@ <n-alert v-if="error" style="margin-top: 25px" type="error">{{ error }}</n-alert> <n-divider /> - <n-input-group> - <n-input-group-label style="flex: 0 0 170px"> Binary (2): </n-input-group-label> - <input-copyable - :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })" - readonly - placeholder="Binary version will be here..." - /> - </n-input-group> + <input-copyable + label="Binary (2)" + v-bind="inputProps" + :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })" + placeholder="Binary version will be here..." + /> - <n-input-group> - <n-input-group-label style="flex: 0 0 170px"> Octal (8): </n-input-group-label> - <input-copyable - :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })" - readonly - placeholder="Octal version will be here..." - /> - </n-input-group> + <input-copyable + label="Octal (8)" + v-bind="inputProps" + :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })" + placeholder="Octal version will be here..." + /> - <n-input-group> - <n-input-group-label style="flex: 0 0 170px"> Decimal (10): </n-input-group-label> - <input-copyable - :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })" - readonly - placeholder="Decimal version will be here..." - /> - </n-input-group> + <input-copyable + label="Decimal (10)" + v-bind="inputProps" + :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })" + placeholder="Decimal version will be here..." + /> - <n-input-group> - <n-input-group-label style="flex: 0 0 170px"> Hexadecimal (16): </n-input-group-label> - <input-copyable - :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })" - readonly - placeholder="Decimal version will be here..." - /> - </n-input-group> + <input-copyable + label="Hexadecimal (16)" + v-bind="inputProps" + :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })" + placeholder="Hexadecimal version will be here..." + /> + + <input-copyable + label="Base64 (64)" + v-bind="inputProps" + :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })" + placeholder="Base64 version will be here..." + /> + + <div flex items-baseline> + <n-input-group style="width: 160px; margin-right: 10px"> + <n-input-group-label> Custom: </n-input-group-label> + <n-input-number v-model:value="outputBase" max="64" min="2" /> + </n-input-group> - <n-input-group> - <n-input-group-label style="flex: 0 0 170px"> Base64 (64): </n-input-group-label> - <input-copyable - :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })" - readonly - placeholder="Base64 version will be here..." - /> - </n-input-group> - <n-input-group> - <n-input-group-label style="flex: 0 0 85px"> Custom: </n-input-group-label> - <n-input-number v-model:value="outputBase" style="flex: 0 0 86px" max="64" min="2" /> <input-copyable + flex-1 + v-bind="inputProps" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: outputBase })" - readonly :placeholder="`Base ${outputBase} will be here...`" /> - </n-input-group> + </div> </c-card> </div> </template> @@ -88,6 +83,14 @@ import InputCopyable from '../../components/InputCopyable.vue'; const styleStore = useStyleStore(); +const inputProps = { + labelPosition: 'left', + labelWidth: '170px', + labelAlign: 'right', + readonly: true, + 'mb-2': '', +} as const; + const input = ref('42'); const inputBase = ref(10); const outputBase = ref(42); diff --git a/src/tools/ipv4-address-converter/ipv4-address-converter.vue b/src/tools/ipv4-address-converter/ipv4-address-converter.vue index 7474608..419b6be 100644 --- a/src/tools/ipv4-address-converter/ipv4-address-converter.vue +++ b/src/tools/ipv4-address-converter/ipv4-address-converter.vue @@ -1,23 +1,20 @@ <template> <div> - <n-form-item label="An ipv4 address:" v-bind="validationAttrs"> - <n-input v-model:value="rawIpAddress" placeholder="An ipv4 address..." /> - </n-form-item> + <c-input-text v-model:value="rawIpAddress" label="The ipv4 address:" placeholder="The ipv4 address..." readonly /> - <n-divider style="margin-top: 0" mt-0 /> + <n-divider /> - <n-form-item + <input-copyable v-for="{ label, value } of convertedSections" :key="label" :label="label" - label-placement="left" - label-width="100" - > - <input-copyable - :value="validationAttrs.validationStatus === 'error' ? '' : value" - placeholder="Set a correct ipv4 address" - /> - </n-form-item> + label-position="left" + label-width="100px" + label-align="right" + mb-2 + :value="validationAttrs.validationStatus === 'error' ? '' : value" + placeholder="Set a correct ipv4 address" + /> </div> </template> @@ -33,7 +30,7 @@ const convertedSections = computed(() => { return [ { - label: 'Decimal : ', + label: 'Decimal: ', value: String(ipInDecimal), }, { diff --git a/src/tools/ipv4-range-expander/ipv4-range-expander.vue b/src/tools/ipv4-range-expander/ipv4-range-expander.vue index 05b15b2..0d9f067 100644 --- a/src/tools/ipv4-range-expander/ipv4-range-expander.vue +++ b/src/tools/ipv4-range-expander/ipv4-range-expander.vue @@ -3,10 +3,10 @@ <n-space item-style="flex:1 1 0"> <div> <n-space item-style="flex:1 1 0"> - <n-form-item label="Start address" v-bind="startIpValidation.attrs"> + <n-form-item label="Start address" v-bind="startIpValidation.attrs as any"> <n-input v-model:value="rawStartAddress" placeholder="Start IPv4 address..." /> </n-form-item> - <n-form-item label="End address" v-bind="endIpValidation.attrs"> + <n-form-item label="End address" v-bind="endIpValidation.attrs as any"> <n-input v-model:value="rawEndAddress" placeholder="End IPv4 address..." /> </n-form-item> </n-space> diff --git a/src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue b/src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue index 6e3d2eb..1767fc9 100644 --- a/src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue +++ b/src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue @@ -1,8 +1,12 @@ <template> <div> - <n-form-item label="An IPv4 address with or without mask" v-bind="validationAttrs"> - <n-input v-model:value="ip" /> - </n-form-item> + <c-input-text + v-model:value="ip" + label="An IPv4 address with or without mask" + placeholder="The ipv4 address..." + :validation-rules="ipValidationRules" + mb-4 + /> <div v-if="networkInfo"> <n-table> @@ -37,7 +41,6 @@ import { computed } from 'vue'; import { Netmask } from 'netmask'; import { withDefaultOnError } from '@/utils/defaults'; -import { useValidation } from '@/composable/validation'; import { isNotThrowing } from '@/utils/boolean'; import { useStorage } from '@vueuse/core'; import { ArrowLeft, ArrowRight } from '@vicons/tabler'; @@ -50,15 +53,12 @@ const getNetworkInfo = (address: string) => new Netmask(address.trim()); const networkInfo = computed(() => withDefaultOnError(() => getNetworkInfo(ip.value), undefined)); -const { attrs: validationAttrs } = useValidation({ - source: ip, - rules: [ - { - message: 'We cannot parse this address, check the format', - validator: (value) => isNotThrowing(() => getNetworkInfo(value.trim())), - }, - ], -}); +const ipValidationRules = [ + { + message: 'We cannot parse this address, check the format', + validator: (value: string) => isNotThrowing(() => getNetworkInfo(value.trim())), + }, +]; const sections: { label: string; diff --git a/src/tools/ipv6-ula-generator/ipv6-ula-generator.vue b/src/tools/ipv6-ula-generator/ipv6-ula-generator.vue index ee74d4c..8f1b130 100644 --- a/src/tools/ipv6-ula-generator/ipv6-ula-generator.vue +++ b/src/tools/ipv6-ula-generator/ipv6-ula-generator.vue @@ -1,30 +1,32 @@ <template> <div> - <n-space vertical :size="50"> - <n-alert title="Info" type="info"> - This tool uses the first method suggested by IETF using the current timestamp plus the mac address, sha1 hashed, - and the lower 40 bits to generate your random ULA. - </n-alert> + <n-alert title="Info" type="info"> + This tool uses the first method suggested by IETF using the current timestamp plus the mac address, sha1 hashed, + and the lower 40 bits to generate your random ULA. + </n-alert> - <n-form-item label="MAC address:" v-bind="validationAttrs"> - <n-input - v-model:value="macAddress" - size="large" - placeholder="Type a MAC address" - clearable - autocomplete="off" - autocorrect="off" - autocapitalize="off" - spellcheck="false" - /> - </n-form-item> - </n-space> + <c-input-text + v-model:value="macAddress" + placeholder="Type a MAC address" + clearable + label="MAC address:" + raw-text + my-8 + :validation="addressValidation" + /> - <div v-if="validationAttrs.validationStatus !== 'error'"> - <n-input-group v-for="{ label, value } in calculatedSections" :key="label" style="margin: 5px 0"> - <n-input-group-label style="flex: 0 0 160px"> {{ label }} </n-input-group-label> - <input-copyable :value="value" readonly /> - </n-input-group> + <div v-if="addressValidation.isValid"> + <input-copyable + v-for="{ label, value } in calculatedSections" + :key="label" + :value="value" + :label="label" + label-width="160px" + label-align="right" + label-position="left" + readonly + mb-2 + /> </div> </div> </template> @@ -59,7 +61,7 @@ const calculatedSections = computed(() => { ]; }); -const { attrs: validationAttrs } = macAddressValidation(macAddress); +const addressValidation = macAddressValidation(macAddress); </script> <style lang="less" scoped></style> diff --git a/src/tools/json-diff/json-diff.vue b/src/tools/json-diff/json-diff.vue index df106c6..0db06cc 100644 --- a/src/tools/json-diff/json-diff.vue +++ b/src/tools/json-diff/json-diff.vue @@ -1,5 +1,5 @@ <template> - <n-form-item label="Your first json" v-bind="leftJsonValidation.attrs"> + <n-form-item label="Your first json" v-bind="leftJsonValidation.attrs as any"> <n-input v-model:value="rawLeftJson" placeholder="Paste your first json here..." @@ -9,10 +9,10 @@ autocorrect="off" autocapitalize="off" spellcheck="false" - :input-props="{ 'data-test-id': 'leftJson' }" + :input-props="{ 'data-test-id': 'leftJson' } as any" /> </n-form-item> - <n-form-item label="Your json to compare" v-bind="rightJsonValidation.attrs"> + <n-form-item label="Your json to compare" v-bind="rightJsonValidation.attrs as any"> <n-input v-model:value="rawRightJson" placeholder="Paste your json to compare here..." @@ -22,7 +22,7 @@ autocorrect="off" autocapitalize="off" spellcheck="false" - :input-props="{ 'data-test-id': 'rightJson' }" + :input-props="{ 'data-test-id': 'rightJson' } as any" /> </n-form-item> diff --git a/src/tools/list-converter/list-converter.e2e.spec.ts b/src/tools/list-converter/list-converter.e2e.spec.ts index 1634a11..a5ac8c6 100644 --- a/src/tools/list-converter/list-converter.e2e.spec.ts +++ b/src/tools/list-converter/list-converter.e2e.spec.ts @@ -30,8 +30,8 @@ test.describe('Tool - List converter', () => { 3 5`); await page.getByTestId('removeDuplicates').check(); - await page.getByTestId('itemPrefix').locator('input').fill("'"); - await page.getByTestId('itemSuffix').locator('input').fill("'"); + await page.getByTestId('itemPrefix').fill("'"); + await page.getByTestId('itemSuffix').fill("'"); const result = await page.getByTestId('area-content').innerText(); expect(result.trim()).toEqual("'1', '2', '4', '3', '5'"); diff --git a/src/tools/list-converter/list-converter.vue b/src/tools/list-converter/list-converter.vue index ae0b50e..f8da070 100644 --- a/src/tools/list-converter/list-converter.vue +++ b/src/tools/list-converter/list-converter.vue @@ -36,37 +36,39 @@ /> </n-form-item> - <n-form-item label="Separator" label-placement="left" label-width="120" :show-feedback="false" mb-2> - <n-input v-model:value="conversionConfig.separator" placeholder="," /> - </n-form-item> + <c-input-text + v-model:value="conversionConfig.separator" + label="Separator" + label-position="left" + label-width="120px" + label-align="right" + mb-2 + placeholder="," + /> <n-form-item label="Wrap item" label-placement="left" label-width="120" :show-feedback="false" mb-2> - <n-input-group> - <n-input - v-model:value="conversionConfig.itemPrefix" - placeholder="Item prefix" - data-test-id="itemPrefix" - /> - <n-input - v-model:value="conversionConfig.itemSuffix" - placeholder="Item suffix" - data-test-id="itemSuffix" - /> - </n-input-group> + <c-input-text + v-model:value="conversionConfig.itemPrefix" + placeholder="Item prefix" + test-id="itemPrefix" + /> + <c-input-text + v-model:value="conversionConfig.itemSuffix" + placeholder="Item suffix" + test-id="itemSuffix" + /> </n-form-item> <n-form-item label="Wrap list" label-placement="left" label-width="120" :show-feedback="false" mb-2> - <n-input-group> - <n-input - v-model:value="conversionConfig.listPrefix" - placeholder="List prefix" - data-test-id="listPrefix" - /> - <n-input - v-model:value="conversionConfig.listSuffix" - placeholder="List suffix" - data-test-id="listSuffix" - /> - </n-input-group> + <c-input-text + v-model:value="conversionConfig.listPrefix" + placeholder="List prefix" + test-id="listPrefix" + /> + <c-input-text + v-model:value="conversionConfig.listSuffix" + placeholder="List suffix" + test-id="listSuffix" + /> </n-form-item> </div> </div> diff --git a/src/tools/mac-address-lookup/mac-address-lookup.vue b/src/tools/mac-address-lookup/mac-address-lookup.vue index c157dda..4ff4c2e 100644 --- a/src/tools/mac-address-lookup/mac-address-lookup.vue +++ b/src/tools/mac-address-lookup/mac-address-lookup.vue @@ -1,6 +1,6 @@ <template> <div> - <n-form-item label="MAC address:" v-bind="validationAttrs"> + <n-form-item label="MAC address:" v-bind="validationAttrs as any"> <n-input v-model:value="macAddress" size="large" diff --git a/src/tools/meta-tag-generator/meta-tag-generator.vue b/src/tools/meta-tag-generator/meta-tag-generator.vue index ee5d8e7..73fad8b 100644 --- a/src/tools/meta-tag-generator/meta-tag-generator.vue +++ b/src/tools/meta-tag-generator/meta-tag-generator.vue @@ -5,7 +5,7 @@ <n-input-group v-for="{ key, type, label, placeholder, ...element } of elements" :key="key"> <n-input-group-label style="flex: 0 0 110px">{{ label }}</n-input-group-label> - <n-input v-if="type === 'input'" v-model:value="metadata[key]" :placeholder="placeholder" /> + <c-input-text v-if="type === 'input'" v-model:value="metadata[key]" :placeholder="placeholder" clearable /> <n-dynamic-input v-else-if="type === 'input-multiple'" v-model:value="metadata[key]" diff --git a/src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue b/src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue index fd9f173..73fbd25 100644 --- a/src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue +++ b/src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue @@ -1,19 +1,23 @@ <template> <div style="max-width: 350px"> - <n-form-item label="Secret" v-bind="secretValidationAttrs"> - <n-input v-model:value="secret" placeholder="Paste your TOTP secret..."> - <template #suffix> - <n-tooltip trigger="hover"> - <template #trigger> - <c-button circle variant="text" @click="refreshSecret"> - <n-icon :component="Refresh" /> - </c-button> - </template> - Generate secret token - </n-tooltip> - </template> - </n-input> - </n-form-item> + <c-input-text + v-model:value="secret" + label="Secret" + placeholder="Paste your TOTP secret..." + mb-5 + :validation-rules="secretValidationRules" + > + <template #suffix> + <n-tooltip trigger="hover"> + <template #trigger> + <c-button circle variant="text" size="small" @click="refreshSecret"> + <icon-mdi-refresh /> + </c-button> + </template> + Generate secret token + </n-tooltip> + </template> + </c-input-text> <div> <token-display :tokens="tokens" style="margin-top: 2px" /> @@ -27,49 +31,52 @@ </n-space> </div> <div style="max-width: 350px"> - <n-form-item label="Secret in hexadecimal"> - <input-copyable :value="base32toHex(secret)" readonly placeholder="Secret in hex will be displayed here" /> - </n-form-item> - - <n-form-item label="Epoch"> - <input-copyable - :value="Math.floor(now / 1000).toString()" - readonly - placeholder="Epoch in sec will be displayed here" - /> - </n-form-item> - <n-form-item label="Iteration" :show-feedback="false"> - <n-input-group> - <n-input-group-label style="width: 110px">Count:</n-input-group-label> - <input-copyable - :value="String(getCounterFromTime({ now, timeStep: 30 }))" - readonly - placeholder="Iteration count will be displayed here" - /> - </n-input-group> - </n-form-item> - - <n-form-item label="Iteration" :show-label="false" style="margin-top: 5px"> - <n-input-group> - <n-input-group-label style="width: 110px">Padded hex:</n-input-group-label> - <input-copyable - :value="getCounterFromTime({ now, timeStep: 30 }).toString(16).padStart(16, '0')" - readonly - placeholder="Iteration count in hex will be displayed here" - /> - </n-input-group> - </n-form-item> + <input-copyable + label="Secret in hexadecimal" + :value="base32toHex(secret)" + readonly + placeholder="Secret in hex will be displayed here" + mb-5 + /> + + <input-copyable + label="Epoch" + :value="Math.floor(now / 1000).toString()" + readonly + mb-5 + placeholder="Epoch in sec will be displayed here" + /> + + <p>Iteration</p> + + <input-copyable + :value="String(getCounterFromTime({ now, timeStep: 30 }))" + readonly + label="Count:" + label-position="left" + label-width="90px" + label-align="right" + placeholder="Iteration count will be displayed here" + /> + + <input-copyable + :value="getCounterFromTime({ now, timeStep: 30 }).toString(16).padStart(16, '0')" + readonly + placeholder="Iteration count in hex will be displayed here" + label-position="left" + label-width="90px" + label-align="right" + label="Padded hex:" + /> </div> </template> <script setup lang="ts"> import { computed, ref } from 'vue'; -import { Refresh } from '@vicons/tabler'; import { useTimestamp } from '@vueuse/core'; import { useThemeVars } from 'naive-ui'; import { useStyleStore } from '@/stores/style.store'; import InputCopyable from '@/components/InputCopyable.vue'; -import { useValidation } from '@/composable/validation'; import { computedRefreshable } from '@/composable/computedRefreshable'; import { generateTOTP, buildKeyUri, generateSecret, base32toHex, getCounterFromTime } from './otp.service'; import { useQRCode } from '../qr-code-generator/useQRCode'; @@ -106,19 +113,16 @@ const { qrcode } = useQRCode({ options: { width: 210 }, }); -const { attrs: secretValidationAttrs } = useValidation({ - source: secret, - rules: [ - { - message: 'Secret should be a base32 string', - validator: (value) => value.toUpperCase().match(/^[A-Z234567]+$/), - }, - { - message: 'Please set a secret', - validator: (value) => value !== '', - }, - ], -}); +const secretValidationRules = [ + { + message: 'Secret should be a base32 string', + validator: (value: string) => value.toUpperCase().match(/^[A-Z234567]+$/), + }, + { + message: 'Please set a secret', + validator: (value: string) => value !== '', + }, +]; </script> <style lang="less" scoped> diff --git a/src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue b/src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue index dad4ddc..1f31085 100644 --- a/src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue +++ b/src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue @@ -3,9 +3,14 @@ <n-form-item label="Default country code:"> <n-select v-model:value="defaultCountryCode" :options="countriesOptions" /> </n-form-item> - <n-form-item label="Phone number:" v-bind="validation.attrs"> - <n-input v-model:value="rawPhone" placeholder="Enter a phone number" /> - </n-form-item> + + <c-input-text + v-model:value="rawPhone" + placeholder="Enter a phone number" + label="Phone number:" + :validation="validation" + mb-5 + /> <n-table v-if="parsedDetails"> <tbody> diff --git a/src/tools/roman-numeral-converter/roman-numeral-converter.vue b/src/tools/roman-numeral-converter/roman-numeral-converter.vue index d21728e..cc5ce93 100644 --- a/src/tools/roman-numeral-converter/roman-numeral-converter.vue +++ b/src/tools/roman-numeral-converter/roman-numeral-converter.vue @@ -2,7 +2,7 @@ <div> <c-card title="Arabic to roman"> <n-space align="center" justify="space-between"> - <n-form-item v-bind="validationNumeral"> + <n-form-item v-bind="validationNumeral as any"> <n-input-number v-model:value="inputNumeral" :min="1" style="width: 200px" :show-button="false" /> </n-form-item> <div class="result"> @@ -15,13 +15,12 @@ </c-card> <c-card title="Roman to arabic" mt-5> <n-space align="center" justify="space-between"> - <n-form-item v-bind="validationRoman"> - <n-input v-model:value="inputRoman" style="width: 200px" /> - </n-form-item> + <c-input-text v-model:value="inputRoman" style="width: 200px" :validation="validationRoman" /> + <div class="result"> {{ outputNumeral }} </div> - <c-button :disabled="validationRoman.validationStatus === 'error'" @click="copyArabic"> Copy </c-button> + <c-button :disabled="!validationRoman.isValid" @click="copyArabic"> Copy </c-button> </n-space> </c-card> </div> @@ -55,7 +54,7 @@ const { attrs: validationNumeral } = useValidation({ const inputRoman = ref('XLII'); const outputNumeral = computed(() => romanToArabic(inputRoman.value)); -const { attrs: validationRoman } = useValidation({ +const validationRoman = useValidation({ source: inputRoman, rules: [ { diff --git a/src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue b/src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue index 0c5034c..a2202c9 100644 --- a/src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue +++ b/src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue @@ -1,7 +1,7 @@ <template> <div style="flex: 0 0 100%"> <n-space item-style="flex: 1 1 0" style="margin: 0 auto; max-width: 600px" justify="center"> - <n-form-item label="Bits :" v-bind="bitsValidationAttrs" label-placement="left" label-width="100"> + <n-form-item label="Bits :" v-bind="bitsValidationAttrs as any" label-placement="left" label-width="100"> <n-input-number v-model:value="bits" min="256" max="16384" step="8" /> </n-form-item> diff --git a/src/tools/svg-placeholder-generator/svg-placeholder-generator.vue b/src/tools/svg-placeholder-generator/svg-placeholder-generator.vue index 404e552..dacc700 100644 --- a/src/tools/svg-placeholder-generator/svg-placeholder-generator.vue +++ b/src/tools/svg-placeholder-generator/svg-placeholder-generator.vue @@ -21,9 +21,15 @@ <n-form-item label="Font size"> <n-input-number v-model:value="fontSize" placeholder="Font size..." min="1" /> </n-form-item> - <n-form-item label="Custom text"> - <n-input v-model:value="customText" :placeholder="`Default is ${width}x${height}`" /> - </n-form-item> + + <c-input-text + v-model:value="customText" + label="Custom text" + :placeholder="`Default is ${width}x${height}`" + label-position="left" + label-width="100px" + label-align="right" + /> </n-space> <n-form-item label="Use exact size" label-placement="left"> <n-switch v-model:value="useExactSize" /> diff --git a/src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue b/src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue index aa551f5..4b12adb 100644 --- a/src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue +++ b/src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue @@ -1,8 +1,12 @@ <template> <div> - <n-form-item label="Your text to convert to NATO phonetic alphabet"> - <n-input v-model:value="input" placeholder="Put your text here..." clearable /> - </n-form-item> + <c-input-text + v-model:value="input" + label="Your text to convert to NATO phonetic alphabet" + placeholder="Put your text here..." + clearable + mb-5 + /> <n-space v-if="natoText" vertical> <n-text>Your text in NATO phonetic alphabet</n-text> diff --git a/src/tools/url-parser/url-parser.vue b/src/tools/url-parser/url-parser.vue index ac80f61..04ab939 100644 --- a/src/tools/url-parser/url-parser.vue +++ b/src/tools/url-parser/url-parser.vue @@ -1,51 +1,59 @@ <template> <c-card> - <n-form-item label="Your url to parse:" :feedback="validation.message" :validation-status="validation.status"> - <n-input v-model:value="urlToParse" placeholder="Your url to parse..." /> - </n-form-item> + <c-input-text + v-model:value="urlToParse" + label="Your url to parse:" + placeholder="Your url to parse..." + raw-text + :validation-rules="urlValidationRules" + /> - <n-divider style="margin-top: 0" /> + <n-divider /> - <n-form> - <n-input-group v-for="{ title, key } in properties" :key="key"> - <n-input-group-label style="flex: 0 0 120px"> {{ title }}: </n-input-group-label> - <input-copyable :value="(urlParsed?.[key] as string) ?? ''" readonly placeholder=" " /> - </n-input-group> + <input-copyable + v-for="{ title, key } in properties" + :key="key" + :label="title" + :value="(urlParsed?.[key] as string) ?? ''" + readonly + label-position="left" + label-width="110px" + mb-2 + placeholder=" " + /> - <n-input-group - v-for="[k, v] in Object.entries(Object.fromEntries(urlParsed?.searchParams.entries() ?? []))" - :key="k" - > - <n-input-group-label style="flex: 0 0 120px"> - <n-icon :component="SubdirectoryArrowRightRound" /> - </n-input-group-label> - <input-copyable :value="k" readonly /> - <input-copyable :value="v" readonly /> - </n-input-group> - </n-form> + <div + v-for="[k, v] in Object.entries(Object.fromEntries(urlParsed?.searchParams.entries() ?? []))" + :key="k" + mb-2 + w-full + flex + > + <div style="flex: 1 0 110px"> + <icon-mdi-arrow-right-bottom /> + </div> + + <input-copyable :value="k" readonly /> + <input-copyable :value="v" readonly /> + </div> </c-card> </template> <script setup lang="ts"> -import { useValidation } from '@/composable/validation'; import { isNotThrowing } from '@/utils/boolean'; import { withDefaultOnError } from '@/utils/defaults'; -import { SubdirectoryArrowRightRound } from '@vicons/material'; import { computed, ref } from 'vue'; import InputCopyable from '../../components/InputCopyable.vue'; const urlToParse = ref('https://me:pwd@it-tools.tech:3000/url-parser?key1=value&key2=value2#the-hash'); const urlParsed = computed(() => withDefaultOnError(() => new URL(urlToParse.value), undefined)); -const validation = useValidation({ - source: urlToParse, - rules: [ - { - validator: (value) => isNotThrowing(() => new URL(value)), - message: 'Invalid url', - }, - ], -}); +const urlValidationRules = [ + { + validator: (value: string) => isNotThrowing(() => new URL(value)), + message: 'Invalid url', + }, +]; const properties: { title: string; key: keyof URL }[] = [ { title: 'Protocol', key: 'protocol' }, diff --git a/src/ui/c-button/c-button.demo.vue b/src/ui/c-button/c-button.demo.vue index 4e9aa1a..ce339f5 100644 --- a/src/ui/c-button/c-button.demo.vue +++ b/src/ui/c-button/c-button.demo.vue @@ -25,6 +25,18 @@ > A </c-button> + + <c-button + v-for="buttonType of buttonTypes" + :key="buttonType" + :variant="buttonVariant" + :type="buttonType" + :size="buttonSize" + circle + mx-1 + > + <icon-mdi-content-copy /> + </c-button> </div> </div> </template> @@ -33,7 +45,7 @@ import _ from 'lodash'; const buttonVariants = ['basic', 'text'] as const; -const buttonTypes = ['default', 'primary'] as const; +const buttonTypes = ['default', 'primary', 'warning'] as const; const buttonSizes = ['small', 'medium', 'large'] as const; </script> diff --git a/src/ui/c-button/c-button.theme.ts b/src/ui/c-button/c-button.theme.ts index e96ac56..5b4c26f 100644 --- a/src/ui/c-button/c-button.theme.ts +++ b/src/ui/c-button/c-button.theme.ts @@ -56,10 +56,10 @@ const createTheme = ({ style }: { style: 'light' | 'dark' }) => { pressedBackground: darken(theme.primary.colorFaded, 30), }), warning: createState({ - textColor: theme.text.baseColor, - backgroundColor: theme.warning.color, - hoverBackground: theme.warning.colorHover, - pressedBackground: theme.warning.colorPressed, + textColor: theme.warning.color, + backgroundColor: theme.warning.colorFaded, + hoverBackground: lighten(theme.warning.colorFaded, 30), + pressedBackground: darken(theme.warning.colorFaded, 30), }), }, text: { @@ -76,10 +76,10 @@ const createTheme = ({ style }: { style: 'light' | 'dark' }) => { pressedBackground: darken(theme.primary.colorFaded, 30), }), warning: createState({ - textColor: theme.text.baseColor, - backgroundColor: theme.warning.color, - hoverBackground: theme.warning.colorHover, - pressedBackground: theme.warning.colorPressed, + textColor: darken(theme.warning.color, 20), + backgroundColor: 'transparent', + hoverBackground: theme.warning.colorFaded, + pressedBackground: darken(theme.warning.colorFaded, 30), }), }, }; diff --git a/src/ui/c-button/c-button.vue b/src/ui/c-button/c-button.vue index c17b078..121a1e9 100644 --- a/src/ui/c-button/c-button.vue +++ b/src/ui/c-button/c-button.vue @@ -18,7 +18,7 @@ import { useAppTheme } from '../theme/themes'; const props = withDefaults( defineProps<{ - type?: 'default' | 'primary'; + type?: 'default' | 'primary' | 'warning'; variant?: 'basic' | 'text'; disabled?: boolean; round?: boolean; diff --git a/src/ui/c-input-text/c-input-text.demo.vue b/src/ui/c-input-text/c-input-text.demo.vue index 2363219..5a5fa99 100644 --- a/src/ui/c-input-text/c-input-text.demo.vue +++ b/src/ui/c-input-text/c-input-text.demo.vue @@ -2,6 +2,9 @@ <h2>Default</h2> <c-input-text value="qsd" /> + <c-input-text + value="Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, est modi iusto repellendus fuga accusantium atque at magnam aliquam eum explicabo vero quia, nobis quasi quis! Earum amet quam a?" + /> <h2>With placeholder</h2> @@ -24,16 +27,50 @@ <h2>Validation</h2> - <c-input-text - v-model:value="value" - :validation-rules="[{ message: 'Length must be > 10', validator: (value) => value.length > 10 }]" - /> + <c-input-text v-model:value="value" :validation-rules="validationRules" mb-2 /> + <c-input-text v-model:value="value" :validation-rules="validationRules" mb-2 label-position="left" label="Yo " /> + <c-input-text v-model:value="value" :validation="validation" /> + <c-input-text v-model:value="value" :validation="validation" multiline rows="3" /> <h2>Clearable</h2> <c-input-text v-model:value="value" clearable /> + + <h2>Type password</h2> + + <c-input-text value="value" type="password" /> + + <h2>Multiline</h2> + + <c-input-text value="value" multiline label="Label" mb-2 rows="1" /> + <c-input-text value="value" multiline label="Label" mb-2 /> + <c-input-text + value="Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, est modi iusto repellendus fuga accusantium atque at magnam aliquam eum explicabo vero quia, nobis quasi quis! Earum amet quam a?" + multiline + mb-2 + /> + + <c-input-text v-model:value="valueLong" multiline autosize mb-2 rows="5" /> + + <c-input-text + value="Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, est modi iusto repellendus fuga accusantium atque at magnam aliquam eum explicabo vero quia, nobis quasi quis! Earum amet quam a?" + multiline + clearable + /> </template> <script lang="ts" setup> +import { useValidation } from '@/composable/validation'; + const value = ref('value'); +const valueLong = ref( + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, est modi iusto repellendus fuga accusantium atque at magnam aliquam eum explicabo vero quia, nobis quasi quis! Earum amet quam a?', +); + +const validationRules = [{ message: 'Length must be > 10', validator: (value: string) => value.length > 10 }]; + +const validation = useValidation({ + source: value, + rules: validationRules, +}); </script> diff --git a/src/ui/c-input-text/c-input-text.test.ts b/src/ui/c-input-text/c-input-text.test.ts index 56b5855..69f4046 100644 --- a/src/ui/c-input-text/c-input-text.test.ts +++ b/src/ui/c-input-text/c-input-text.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it, beforeEach } from 'vitest'; -import { shallowMount } from '@vue/test-utils'; +import { shallowMount, mount } from '@vue/test-utils'; import { setActivePinia, createPinia } from 'pinia'; import _ from 'lodash'; +import { useValidation } from '@/composable/validation'; import CInputText from './c-input-text.vue'; describe('CInputText', () => { @@ -71,10 +72,28 @@ describe('CInputText', () => { it('renders a feedback message for invalid rules', async () => { const wrapper = shallowMount(CInputText, { - props: { rules: [{ validator: () => false, message: 'Message' }] }, + props: { validationRules: [{ validator: () => false, message: 'Message' }] }, }); - expect(wrapper.get('.feedback').text()).to.equal('Message'); + const feedback = wrapper.find('.feedback'); + expect(feedback.exists()).to.equal(true); + expect(feedback.text()).to.equal('Message'); + }); + + it('if the value become valid according to rules, the feedback disappear', async () => { + const wrapper = shallowMount(CInputText, { + props: { + validationRules: [{ validator: (value: string) => value === 'Hello', message: 'Value should be Hello' }], + }, + }); + + const feedback = wrapper.find('.feedback'); + expect(feedback.exists()).to.equal(true); + expect(feedback.text()).to.equal('Value should be Hello'); + + await wrapper.setProps({ value: 'Hello' }); + + expect(wrapper.find('.feedback').exists()).to.equal(false); }); it('feedback does not render for valid rules', async () => { @@ -84,4 +103,58 @@ describe('CInputText', () => { expect(wrapper.find('.feedback').exists()).to.equal(false); }); + + it('renders a feedback message for invalid custom validation wrapper', async () => { + const wrapper = shallowMount(CInputText, { + props: { + validation: useValidation({ source: ref(), rules: [{ validator: () => false, message: 'Message' }] }), + }, + }); + + const feedback = wrapper.find('.feedback'); + expect(feedback.exists()).to.equal(true); + expect(feedback.text()).to.equal('Message'); + }); + + it('feedback does not render for valid custom validation wrapper', async () => { + const wrapper = shallowMount(CInputText, { + props: { + validation: useValidation({ source: ref(), rules: [{ validator: () => true, message: 'Message' }] }), + }, + }); + expect(wrapper.find('.feedback').exists()).to.equal(false); + }); + + it('if the value become valid according to the custom validation wrapper, the feedback disappear', async () => { + const source = ref(''); + + const wrapper = shallowMount(CInputText, { + props: { + validation: useValidation({ + source, + rules: [{ validator: (value: string) => value === 'Hello', message: 'Value should be Hello' }], + }), + }, + }); + + const feedback = wrapper.find('.feedback'); + expect(feedback.exists()).to.equal(true); + expect(feedback.text()).to.equal('Value should be Hello'); + + source.value = 'Hello'; + + await wrapper.vm.$nextTick(); + + expect(wrapper.find('.feedback').exists()).to.equal(false); + }); + + it('[prop:testId] renders a test id on the input', async () => { + const wrapper = mount(CInputText, { + props: { + testId: 'TEST', + }, + }); + + expect(wrapper.get('input').attributes('data-test-id')).to.equal('TEST'); + }); }); diff --git a/src/ui/c-input-text/c-input-text.vue b/src/ui/c-input-text/c-input-text.vue index a40d26a..51c2805 100644 --- a/src/ui/c-input-text/c-input-text.vue +++ b/src/ui/c-input-text/c-input-text.vue @@ -1,32 +1,60 @@ <template> - <div class="c-input-text" :class="{ disabled, error: !validation.isValid, 'label-left': labelPosition === 'left' }"> + <div + class="c-input-text" + :class="{ disabled, error: !validation.isValid, 'label-left': labelPosition === 'left', multiline }" + > <label v-if="label" :for="id" class="label"> {{ label }} </label> - <div class="input-wrapper"> - <slot name="prefix" /> - - <input - :id="id" - v-model="value" - type="text" - class="input" - :placeholder="placeholder" - :readonly="readonly" - :disabled="disabled" - :data-test-id="testId" - :autocapitalize="autocapitalize ?? (rawText ? 'off' : undefined)" - :autocomplete="autocomplete ?? (rawText ? 'off' : undefined)" - :autocorrect="autocorrect ?? (rawText ? 'off' : undefined)" - :spellcheck="spellcheck ?? (rawText ? false : undefined)" - /> - - <c-button v-if="clearable && value" variant="text" circle size="small" @click="value = ''"> - <icon-mdi-close /> - </c-button> - <slot name="suffix" /> - </div> + <div class="feedback-wrapper"> + <div ref="inputWrapperRef" class="input-wrapper"> + <slot name="prefix" /> + + <textarea + v-if="multiline" + :id="id" + ref="textareaRef" + v-model="value" + class="input" + :placeholder="placeholder" + :readonly="readonly" + :disabled="disabled" + :data-test-id="testId" + :autocapitalize="autocapitalize ?? (rawText ? 'off' : undefined)" + :autocomplete="autocomplete ?? (rawText ? 'off' : undefined)" + :autocorrect="autocorrect ?? (rawText ? 'off' : undefined)" + :spellcheck="spellcheck ?? (rawText ? false : undefined)" + :rows="rows" + /> + + <input + v-else + :id="id" + v-model="value" + :type="htmlInputType" + class="input" + size="1" + :placeholder="placeholder" + :readonly="readonly" + :disabled="disabled" + :data-test-id="testId" + :autocapitalize="autocapitalize ?? (rawText ? 'off' : undefined)" + :autocomplete="autocomplete ?? (rawText ? 'off' : undefined)" + :autocorrect="autocorrect ?? (rawText ? 'off' : undefined)" + :spellcheck="spellcheck ?? (rawText ? false : undefined)" + /> + + <c-button v-if="clearable && value" variant="text" circle size="small" @click="value = ''"> + <icon-mdi-close /> + </c-button> - <span v-if="!validation.isValid" class="feedback"> {{ validation.message }} </span> + <c-button v-if="type === 'password'" variant="text" circle size="small" @click="showPassword = !showPassword"> + <icon-mdi-eye v-if="!showPassword" /> + <icon-mdi-eye-off v-if="showPassword" /> + </c-button> + <slot name="suffix" /> + </div> + <span v-if="!validation.isValid" class="feedback"> {{ validation.message }} </span> + </div> </div> </template> @@ -45,6 +73,7 @@ const props = withDefaults( readonly?: boolean; disabled?: boolean; validationRules?: UseValidationRule<string>[]; + validation?: ReturnType<typeof useValidation>; labelPosition?: 'top' | 'left'; labelWidth?: string; labelAlign?: 'left' | 'right'; @@ -55,6 +84,10 @@ const props = withDefaults( autocorrect?: 'on' | 'off' | string; spellcheck?: 'true' | 'false' | boolean; rawText?: boolean; + type?: 'text' | 'password'; + multiline?: boolean; + rows?: number | string; + autosize?: boolean; }>(), { value: '', @@ -64,6 +97,7 @@ const props = withDefaults( readonly: false, disabled: false, validationRules: () => [], + validation: undefined, labelPosition: 'top', labelWidth: 'auto', labelAlign: 'left', @@ -74,20 +108,58 @@ const props = withDefaults( autocorrect: undefined, spellcheck: undefined, rawText: false, + type: 'text', + multiline: false, + rows: 3, + autosize: false, }, ); const emit = defineEmits(['update:value']); const value = useVModel(props, 'value', emit); +const showPassword = ref(false); -const { id, placeholder, label, validationRules, labelPosition, labelWidth, labelAlign } = toRefs(props); +const { id, placeholder, label, validationRules, labelPosition, labelWidth, labelAlign, autosize } = toRefs(props); -const validation = useValidation({ - rules: validationRules, - source: value, -}); +const validation = + props.validation ?? + useValidation({ + rules: validationRules, + source: value, + }); const theme = useTheme(); const appTheme = useAppTheme(); + +const textareaRef = ref<HTMLTextAreaElement>(); +const inputWrapperRef = ref<HTMLElement>(); + +watch( + value, + () => { + if (props.multiline && autosize.value) { + resizeTextarea(); + } + }, + { immediate: true }, +); + +function resizeTextarea() { + if (!textareaRef.value || !inputWrapperRef.value) { + return; + } + + const { scrollHeight } = textareaRef.value; + + inputWrapperRef.value.style.height = `${scrollHeight + 2}px`; +} + +const htmlInputType = computed(() => { + if (props.type === 'password' && !showPassword.value) { + return 'password'; + } + + return 'text'; +}); </script> <style lang="less" scoped> @@ -114,29 +186,55 @@ const appTheme = useAppTheme(); } } - & > .feedback { + & .feedback { color: v-bind('appTheme.error.color'); } } & > .label { + flex-shrink: 0; margin-bottom: 5px; flex: 0 0 v-bind('labelWidth'); text-align: v-bind('labelAlign'); - padding-right: 10px; + padding-right: 12px; } - .input-wrapper { + .feedback-wrapper { flex: 1 1 0; min-width: 0; - + } + .input-wrapper { display: flex; flex-direction: row; align-items: center; background-color: v-bind('theme.backgroundColor'); + color: transparent; border: 1px solid v-bind('theme.borderColor'); border-radius: 4px; padding: 0 4px 0 12px; + transition: border-color 0.2s ease-in-out; + + .multiline& { + resize: vertical; + overflow: hidden; + + & > textarea { + height: 100%; + resize: none; + word-break: break-word; + white-space: pre-wrap; + overflow-wrap: break-word; + border: none; + outline: none; + font-family: inherit; + font-size: inherit; + color: v-bind('appTheme.text.baseColor'); + + &::placeholder { + color: v-bind('appTheme.text.mutedColor'); + } + } + } & > .input { flex: 1 1 0; @@ -144,7 +242,6 @@ const appTheme = useAppTheme(); padding: 8px 0; outline: none; - transition: border-color 0.2s ease-in-out; background-color: transparent; background-image: none; -webkit-box-shadow: none; @@ -159,12 +256,13 @@ const appTheme = useAppTheme(); } } - &:hover, - &:focus { + &:hover { border-color: v-bind('appTheme.primary.color'); } - &:focus { + &:focus-within { + border-color: v-bind('appTheme.primary.color'); + background-color: v-bind('theme.focus.backgroundColor'); } } @@ -173,11 +271,11 @@ const appTheme = useAppTheme(); border-color: v-bind('appTheme.error.color'); &:hover, - &:focus { + &:focus-within { border-color: v-bind('appTheme.error.color'); } - &:focus { + &:focus-within { background-color: v-bind('appTheme.error.color + 22'); } } @@ -186,7 +284,7 @@ const appTheme = useAppTheme(); opacity: 0.5; &:hover, - &:focus { + &:focus-within { border-color: v-bind('theme.borderColor'); } diff --git a/src/ui/theme/themes.ts b/src/ui/theme/themes.ts index 18b2c8d..81d7ddf 100644 --- a/src/ui/theme/themes.ts +++ b/src/ui/theme/themes.ts @@ -21,6 +21,7 @@ export const { themes: appThemes, useTheme: useAppTheme } = defineThemes({ color: '#f59e0b', colorHover: '#f59e0b', colorPressed: '#f59e0b', + colorFaded: '#f59e0b2f', }, success: { color: '#18a058', @@ -55,6 +56,7 @@ export const { themes: appThemes, useTheme: useAppTheme } = defineThemes({ color: '#f59e0b', colorHover: '#f59e0b', colorPressed: '#f59e0b', + colorFaded: '#f59e0b2f', }, success: { color: '#18a058', |