Merge branch 'main' into feat/base64-url-encode-option
* main: refactor(ui): replaced some n-input with c-input-text chore(version): release 2023.05.14-77f2efc docs(changelog): update changelog for 2023.05.14-77f2efc refactor(ui): replaced some n-input with c-input-text ui-lib(new-component): added text input component in the c-lib ui-lib(button): size variants chore(issues): updated new tool request issue template # Conflicts: # src/tools/base64-string-converter/base64-string-converter.vue
This commit is contained in:
		
						commit
						12f3697d53
					
				
							
								
								
									
										4
									
								
								.github/ISSUE_TEMPLATE/new-tool-request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/ISSUE_TEMPLATE/new-tool-request.md
									
									
									
									
										vendored
									
									
								
							| @ -6,8 +6,8 @@ labels: new tool | ||||
| assignees: CorentinTh | ||||
| --- | ||||
| 
 | ||||
| **Which tool is impacted?** | ||||
| Example: the token generator | ||||
| **What tool do you want?** | ||||
| Example: a token generator | ||||
| 
 | ||||
| **Describe the solution you'd like** | ||||
| A clear and concise description of what you want to happen. | ||||
|  | ||||
							
								
								
									
										20
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @ -2,6 +2,26 @@ | ||||
| 
 | ||||
| All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. | ||||
| 
 | ||||
| ## Version 2023.05.14-77f2efc | ||||
| 
 | ||||
| ### Features | ||||
| - **list-converter**: a small converter who deals with column based data and do some stuff with it (#387) (83a7b3b) | ||||
| - **new tool**: phone parser and normalizer (ce3150c) | ||||
| 
 | ||||
| ### Bug fixes | ||||
| - **phone-parser**: use default country code (a43c546) | ||||
| - **home**: prevent weird blue border on card (3f6c8f0) | ||||
| 
 | ||||
| ### Refactoring | ||||
| - **ui**: replaced some n-input with c-input-text (77f2efc) | ||||
| 
 | ||||
| ### Chores | ||||
| - **issues**: updated new tool request issue template (edae4c6) | ||||
| 
 | ||||
| ### Ui-lib | ||||
| - **new-component**: added text input component in the c-lib (aad8d84) | ||||
| - **button**: size variants (401f13f) | ||||
| 
 | ||||
| ## Version 2023.04.23-92bd835 | ||||
| 
 | ||||
| ### Features | ||||
|  | ||||
							
								
								
									
										10
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -26,11 +26,15 @@ declare module '@vue/runtime-core' { | ||||
|     'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default'] | ||||
|     ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.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.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default'] | ||||
|     'CInputText.theme': typeof import('./src/ui/c-input-text/c-input-text.theme.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'] | ||||
|     CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default'] | ||||
|     ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default'] | ||||
|     ColoredCard: typeof import('./src/components/ColoredCard.vue')['default'] | ||||
|     copy: typeof import('./src/ui/c-input-text/c-input-text copy.vue')['default'] | ||||
|     CopyableIpLike: typeof import('./src/tools/ipv4-subnet-calculator/copyable-ip-like.vue')['default'] | ||||
|     CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default'] | ||||
|     DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default'] | ||||
| @ -52,6 +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'] | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "it-tools", | ||||
|   "version": "2023.4.23-92bd835", | ||||
|   "version": "2023.5.14-77f2efc", | ||||
|   "description": "Collection of handy online tools for developers, with great UX. ", | ||||
|   "keywords": [ | ||||
|     "productivity", | ||||
| @ -79,6 +79,7 @@ | ||||
|     "yaml": "^2.2.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@iconify-json/mdi": "^1.1.50", | ||||
|     "@playwright/test": "^1.32.3", | ||||
|     "@rushstack/eslint-patch": "^1.2.0", | ||||
|     "@types/bcryptjs": "^2.4.2", | ||||
| @ -98,8 +99,10 @@ | ||||
|     "@unocss/eslint-config": "^0.50.8", | ||||
|     "@vitejs/plugin-vue": "^2.3.4", | ||||
|     "@vitejs/plugin-vue-jsx": "^1.3.10", | ||||
|     "@vue/compiler-sfc": "^3.2.47", | ||||
|     "@vue/eslint-config-prettier": "^7.1.0", | ||||
|     "@vue/eslint-config-typescript": "^10.0.0", | ||||
|     "@vue/runtime-core": "^3.2.47", | ||||
|     "@vue/test-utils": "^2.3.2", | ||||
|     "@vue/tsconfig": "^0.1.3", | ||||
|     "c8": "^7.13.0", | ||||
| @ -116,6 +119,7 @@ | ||||
|     "typescript": "~4.5.5", | ||||
|     "unocss": "^0.50.8", | ||||
|     "unplugin-auto-import": "^0.15.2", | ||||
|     "unplugin-icons": "^0.16.1", | ||||
|     "unplugin-vue-components": "^0.24.1", | ||||
|     "vite": "^2.9.15", | ||||
|     "vite-plugin-md": "^0.12.4", | ||||
|  | ||||
							
								
								
									
										47
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										47
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @ -135,6 +135,9 @@ dependencies: | ||||
|     version: 2.2.1 | ||||
| 
 | ||||
| devDependencies: | ||||
|   '@iconify-json/mdi': | ||||
|     specifier: ^1.1.50 | ||||
|     version: 1.1.50 | ||||
|   '@playwright/test': | ||||
|     specifier: ^1.32.3 | ||||
|     version: 1.32.3 | ||||
| @ -192,12 +195,18 @@ devDependencies: | ||||
|   '@vitejs/plugin-vue-jsx': | ||||
|     specifier: ^1.3.10 | ||||
|     version: 1.3.10 | ||||
|   '@vue/compiler-sfc': | ||||
|     specifier: ^3.2.47 | ||||
|     version: 3.2.47 | ||||
|   '@vue/eslint-config-prettier': | ||||
|     specifier: ^7.1.0 | ||||
|     version: 7.1.0(eslint@8.38.0)(prettier@2.8.7) | ||||
|   '@vue/eslint-config-typescript': | ||||
|     specifier: ^10.0.0 | ||||
|     version: 10.0.0(eslint-plugin-vue@8.7.1)(eslint@8.38.0)(typescript@4.5.5) | ||||
|   '@vue/runtime-core': | ||||
|     specifier: ^3.2.47 | ||||
|     version: 3.2.47 | ||||
|   '@vue/test-utils': | ||||
|     specifier: ^2.3.2 | ||||
|     version: 2.3.2(vue@3.2.47) | ||||
| @ -246,6 +255,9 @@ devDependencies: | ||||
|   unplugin-auto-import: | ||||
|     specifier: ^0.15.2 | ||||
|     version: 0.15.2(@vueuse/core@8.9.4)(rollup@2.79.1) | ||||
|   unplugin-icons: | ||||
|     specifier: ^0.16.1 | ||||
|     version: 0.16.1(@vue/compiler-sfc@3.2.47) | ||||
|   unplugin-vue-components: | ||||
|     specifier: ^0.24.1 | ||||
|     version: 0.24.1(rollup@2.79.1)(vue@3.2.47) | ||||
| @ -1612,6 +1624,12 @@ packages: | ||||
|     resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} | ||||
|     dev: true | ||||
| 
 | ||||
|   /@iconify-json/mdi@1.1.50: | ||||
|     resolution: {integrity: sha512-SgbT5w5eHCdOG74ZWPz7HlTGk6VsifIJhNi6lAsxj/5Nlqt6Cz4LlQmSa9eecU9p075Jub2aAx/o7YI+GCahRQ==} | ||||
|     dependencies: | ||||
|       '@iconify/types': 2.0.0 | ||||
|     dev: true | ||||
| 
 | ||||
|   /@iconify/types@2.0.0: | ||||
|     resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} | ||||
|     dev: true | ||||
| @ -7561,6 +7579,35 @@ packages: | ||||
|       - rollup | ||||
|     dev: true | ||||
| 
 | ||||
|   /unplugin-icons@0.16.1(@vue/compiler-sfc@3.2.47): | ||||
|     resolution: {integrity: sha512-qTunFUkpAyDnwzwV7YV1ZgCWRYfLuURcCurhhXOWMy2ipY88qx1pADvral2hJu4Xymh0X0t3Zcll3BIru2AVLQ==} | ||||
|     peerDependencies: | ||||
|       '@svgr/core': '>=7.0.0' | ||||
|       '@vue/compiler-sfc': ^3.0.2 || ^2.7.0 | ||||
|       vue-template-compiler: ^2.6.12 | ||||
|       vue-template-es2015-compiler: ^1.9.0 | ||||
|     peerDependenciesMeta: | ||||
|       '@svgr/core': | ||||
|         optional: true | ||||
|       '@vue/compiler-sfc': | ||||
|         optional: true | ||||
|       vue-template-compiler: | ||||
|         optional: true | ||||
|       vue-template-es2015-compiler: | ||||
|         optional: true | ||||
|     dependencies: | ||||
|       '@antfu/install-pkg': 0.1.1 | ||||
|       '@antfu/utils': 0.7.2 | ||||
|       '@iconify/utils': 2.1.5 | ||||
|       '@vue/compiler-sfc': 3.2.47 | ||||
|       debug: 4.3.4 | ||||
|       kolorist: 1.7.0 | ||||
|       local-pkg: 0.4.3 | ||||
|       unplugin: 1.3.1 | ||||
|     transitivePeerDependencies: | ||||
|       - supports-color | ||||
|     dev: true | ||||
| 
 | ||||
|   /unplugin-vue-components@0.24.1(rollup@2.79.1)(vue@3.2.47): | ||||
|     resolution: {integrity: sha512-T3A8HkZoIE1Cja95xNqolwza0yD5IVlgZZ1PVAGvVCx8xthmjsv38xWRCtHtwl+rvZyL9uif42SRkDGw9aCfMA==} | ||||
|     engines: {node: '>=14'} | ||||
|  | ||||
| @ -1,26 +1,27 @@ | ||||
| <template> | ||||
|   <n-form-item :label="inputLabel" v-bind="validationAttrs"> | ||||
|     <n-input | ||||
|       ref="inputElement" | ||||
|       v-model:value="input" | ||||
|       :placeholder="inputPlaceholder" | ||||
|       type="textarea" | ||||
|       rows="20" | ||||
|       autocomplete="off" | ||||
|       autocorrect="off" | ||||
|       autocapitalize="off" | ||||
|       spellcheck="false" | ||||
|       :input-props="{ 'data-test-id': 'input' }" | ||||
|     /> | ||||
|   </n-form-item> | ||||
|   <n-form-item :label="outputLabel"> | ||||
|     <textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement" /> | ||||
|   </n-form-item> | ||||
|   <c-input-text | ||||
|     ref="inputElement" | ||||
|     v-model:value="input" | ||||
|     :placeholder="inputPlaceholder" | ||||
|     :label="inputLabel" | ||||
|     multiline | ||||
|     autosize | ||||
|     rows="20" | ||||
|     raw-text | ||||
|     test-id="input" | ||||
|     :validation-rules="inputValidationRules" | ||||
|   /> | ||||
| 
 | ||||
|   <div> | ||||
|     <div mb-5px>{{ outputLabel }}</div> | ||||
|     <textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement?.inputWrapperRef" /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { useValidation, type UseValidationRule } from '@/composable/validation'; | ||||
| import type { UseValidationRule } from '@/composable/validation'; | ||||
| import _ from 'lodash'; | ||||
| import CInputText from '@/ui/c-input-text/c-input-text.vue'; | ||||
| 
 | ||||
| const props = withDefaults( | ||||
|   defineProps<{ | ||||
| @ -46,12 +47,10 @@ const props = withDefaults( | ||||
| const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault } = | ||||
|   toRefs(props); | ||||
| 
 | ||||
| const inputElement = ref(); | ||||
| const inputElement = ref<typeof CInputText>(); | ||||
| 
 | ||||
| const input = ref(inputDefault.value); | ||||
| const output = computed(() => transformer.value(input.value)); | ||||
| 
 | ||||
| const { attrs: validationAttrs } = useValidation({ source: input, rules: inputValidationRules.value }); | ||||
| </script> | ||||
| 
 | ||||
| <style scoped></style> | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { get, type MaybeRef } from '@vueuse/core'; | ||||
| import _ from 'lodash'; | ||||
| import { reactive, watch, type Ref } from 'vue'; | ||||
| 
 | ||||
| @ -31,7 +32,7 @@ export function useValidation<T>({ | ||||
|   watch: watchRefs = [], | ||||
| }: { | ||||
|   source: Ref<T>; | ||||
|   rules: UseValidationRule<T>[]; | ||||
|   rules: MaybeRef<UseValidationRule<T>[]>; | ||||
|   watch?: Ref<unknown>[]; | ||||
| }) { | ||||
|   const state = reactive<{ | ||||
| @ -55,7 +56,7 @@ export function useValidation<T>({ | ||||
|       state.message = ''; | ||||
|       state.status = undefined; | ||||
| 
 | ||||
|       for (const rule of rules) { | ||||
|       for (const rule of get(rules)) { | ||||
|         if (isFalsyOrHasThrown(() => rule.validator(source.value))) { | ||||
|           state.message = rule.message; | ||||
|           state.status = 'error'; | ||||
|  | ||||
| @ -1,17 +1,19 @@ | ||||
| <template> | ||||
|   <c-card title="Base64 to file"> | ||||
|     <n-form-item | ||||
|       :feedback="base64InputValidation.message" | ||||
|       :validation-status="base64InputValidation.status" | ||||
|       :show-label="false" | ||||
|     > | ||||
|       <n-input v-model:value="base64Input" type="textarea" placeholder="Put your base64 file string here..." rows="5" /> | ||||
|     </n-form-item> | ||||
|     <n-space justify="center"> | ||||
|     <c-input-text | ||||
|       v-model:value="base64Input" | ||||
|       multiline | ||||
|       placeholder="Put your base64 file string here..." | ||||
|       rows="5" | ||||
|       :validation="base64InputValidation" | ||||
|       mb-2 | ||||
|     /> | ||||
| 
 | ||||
|     <div flex justify-center> | ||||
|       <c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="downloadFile()"> | ||||
|         Download file | ||||
|       </c-button> | ||||
|     </n-space> | ||||
|     </div> | ||||
|   </c-card> | ||||
| 
 | ||||
|   <c-card title="File to base64"> | ||||
| @ -24,10 +26,11 @@ | ||||
|       </n-upload-dragger> | ||||
|     </n-upload> | ||||
| 
 | ||||
|     <n-input :value="fileBase64" type="textarea" readonly placeholder="File in base64 will be here" /> | ||||
|     <n-space justify="center"> | ||||
|     <c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" mb-2 /> | ||||
| 
 | ||||
|     <div flex justify-center> | ||||
|       <c-button @click="copyFileBase64()"> Copy </c-button> | ||||
|     </n-space> | ||||
|     </div> | ||||
|   </c-card> | ||||
| </template> | ||||
| 
 | ||||
| @ -77,11 +80,6 @@ async function onUpload({ file: { file } }: { file: UploadFileInfo }) { | ||||
| </script> | ||||
| 
 | ||||
| <style lang="less" scoped> | ||||
| .n-input, | ||||
| .n-upload { | ||||
|   margin-bottom: 15px; | ||||
| } | ||||
| 
 | ||||
| ::v-deep(.n-upload-trigger) { | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| @ -3,46 +3,63 @@ | ||||
|     <n-form-item label="Encode URL safe" label-placement="left"> | ||||
|       <n-switch v-model:value="encodeUrlSafe" /> | ||||
|     </n-form-item> | ||||
|     <n-form-item label="String to encode"> | ||||
|       <n-input v-model:value="textInput" type="textarea" placeholder="Put your string here..." rows="5" /> | ||||
|     </n-form-item> | ||||
|     <c-input-text | ||||
|       v-model:value="textInput" | ||||
|       multiline | ||||
|       placeholder="Put your string here..." | ||||
|       rows="5" | ||||
|       label="String to encode" | ||||
|       raw-text | ||||
|       mb-5 | ||||
|     /> | ||||
| 
 | ||||
|     <n-form-item label="Base64 of string"> | ||||
|       <n-input | ||||
|         :value="base64Output" | ||||
|         type="textarea" | ||||
|         readonly | ||||
|         placeholder="The base64 encoding of your string will be here" | ||||
|         rows="5" | ||||
|       /> | ||||
|     </n-form-item> | ||||
|     <c-input-text | ||||
|       label="Base64 of string" | ||||
|       :value="base64Output" | ||||
|       multiline | ||||
|       readonly | ||||
|       placeholder="The base64 encoding of your string will be here" | ||||
|       rows="5" | ||||
|       mb-5 | ||||
|     /> | ||||
| 
 | ||||
|     <n-space justify="center"> | ||||
|     <div flex justify-center> | ||||
|       <c-button @click="copyTextBase64()"> Copy base64 </c-button> | ||||
|     </n-space> | ||||
|     </div> | ||||
|   </c-card> | ||||
| 
 | ||||
|   <c-card title="Base64 to string"> | ||||
|     <n-form-item label="Decode URL safe" label-placement="left"> | ||||
|       <n-switch v-model:value="decodeUrlSafe" /> | ||||
|     </n-form-item> | ||||
|     <n-form-item label="Base64 string to decode" v-bind="b64Validation.attrs"> | ||||
|       <n-input v-model:value="base64Input" type="textarea" placeholder="Your base64 string..." rows="5" /> | ||||
|     </n-form-item> | ||||
|     <c-input-text | ||||
|       v-model:value="base64Input" | ||||
|       multiline | ||||
|       placeholder="Your base64 string..." | ||||
|       rows="5" | ||||
|       :validation-rules="b64ValidationRules" | ||||
|       label="Base64 string to decode" | ||||
|       mb-5 | ||||
|     /> | ||||
| 
 | ||||
|     <n-form-item label="Decoded string"> | ||||
|       <n-input :value="textOutput" type="textarea" readonly placeholder="The decoded string will be here" rows="5" /> | ||||
|     </n-form-item> | ||||
|     <c-input-text | ||||
|       v-model:value="textOutput" | ||||
|       label="Decoded string" | ||||
|       placeholder="The decoded string will be here" | ||||
|       multiline | ||||
|       rows="5" | ||||
|       readonly | ||||
|       mb-5 | ||||
|     /> | ||||
| 
 | ||||
|     <n-space justify="center"> | ||||
|     <div flex justify-center> | ||||
|       <c-button @click="copyText()"> Copy decoded string </c-button> | ||||
|     </n-space> | ||||
|     </div> | ||||
|   </c-card> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { useValidation } from '@/composable/validation'; | ||||
| import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64'; | ||||
| import { withDefaultOnError } from '@/utils/defaults'; | ||||
| import { computed, ref } from 'vue'; | ||||
| @ -59,8 +76,8 @@ const textOutput = computed(() => | ||||
|   withDefaultOnError(() => base64ToText(base64Input.value.trim(), decodeUrlSafe.value), ''), | ||||
| ); | ||||
| const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' }); | ||||
| const b64Validation = useValidation({ | ||||
|   source: base64Input, | ||||
|   rules: [{ message: 'Invalid base64 string', validator: (value) => isValidBase64(value.trim(), decodeUrlSafe.value) }], | ||||
| }); | ||||
| const b64ValidationRules = [ | ||||
|   { message: 'Invalid base64 string', validator: (value: string) => isValidBase64(value.trim(), decodeUrlSafe.value) }, | ||||
| ]; | ||||
| 
 | ||||
| </script> | ||||
|  | ||||
| @ -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"> | ||||
|  | ||||
| @ -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 }"> | ||||
|  | ||||
| @ -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=" | ||||
|  | ||||
| @ -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" /> | ||||
|  | ||||
| @ -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 /> | ||||
| 
 | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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'); | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -1,35 +1,39 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <n-form-item :show-label="false" v-bind="validation.attrs"> | ||||
|       <n-input-group> | ||||
|         <n-input | ||||
|           v-model:value="inputDate" | ||||
|           autofocus | ||||
|           :on-input="onDateInputChanged" | ||||
|           placeholder="Put you date string here..." | ||||
|           clearable | ||||
|           :input-props="{ 'data-test-id': 'date-time-converter-input' }" | ||||
|         /> | ||||
|     <n-input-group> | ||||
|       <c-input-text | ||||
|         v-model:value="inputDate" | ||||
|         autofocus | ||||
|         placeholder="Put you date string here..." | ||||
|         clearable | ||||
|         test-id="date-time-converter-input" | ||||
|         :validation="validation" | ||||
|         @update:value="onDateInputChanged" | ||||
|       /> | ||||
| 
 | ||||
|         <n-select | ||||
|           v-model:value="formatIndex" | ||||
|           style="flex: 0 0 170px" | ||||
|           :options="formats.map(({ name }, i) => ({ label: name, value: i }))" | ||||
|           data-test-id="date-time-converter-format-select" | ||||
|         /> | ||||
|       </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> | ||||
|       <n-select | ||||
|         v-model:value="formatIndex" | ||||
|         style="flex: 0 0 170px" | ||||
|         :options="formats.map(({ name }, i) => ({ label: name, value: i }))" | ||||
|         data-test-id="date-time-converter-format-select" | ||||
|       /> | ||||
|     </n-input-group> | ||||
| 
 | ||||
|     <n-divider /> | ||||
| 
 | ||||
|     <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> | ||||
| 
 | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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), | ||||
|     }, | ||||
|     { | ||||
|  | ||||
| @ -2,14 +2,23 @@ | ||||
|   <div> | ||||
|     <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-input v-model:value="rawStartAddress" placeholder="Start IPv4 address..." /> | ||||
|           </n-form-item> | ||||
|           <n-form-item label="End address" v-bind="endIpValidation.attrs"> | ||||
|             <n-input v-model:value="rawEndAddress" placeholder="End IPv4 address..." /> | ||||
|           </n-form-item> | ||||
|         </n-space> | ||||
|         <div mb-4 flex gap-4> | ||||
|           <c-input-text | ||||
|             v-model:value="rawStartAddress" | ||||
|             label="Start address" | ||||
|             placeholder="Start IPv4 address..." | ||||
|             :validation="startIpValidation" | ||||
|             clearable | ||||
|           /> | ||||
| 
 | ||||
|           <c-input-text | ||||
|             v-model:value="rawEndAddress" | ||||
|             label="End address" | ||||
|             placeholder="End IPv4 address..." | ||||
|             :validation="endIpValidation" | ||||
|             clearable | ||||
|           /> | ||||
|         </div> | ||||
| 
 | ||||
|         <n-table v-if="showResult" data-test-id="result"> | ||||
|           <thead> | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -1,30 +1,25 @@ | ||||
| <template> | ||||
|   <n-form-item label="Your first json" v-bind="leftJsonValidation.attrs"> | ||||
|     <n-input | ||||
|       v-model:value="rawLeftJson" | ||||
|       placeholder="Paste your first json here..." | ||||
|       type="textarea" | ||||
|       rows="20" | ||||
|       autocomplete="off" | ||||
|       autocorrect="off" | ||||
|       autocapitalize="off" | ||||
|       spellcheck="false" | ||||
|       :input-props="{ 'data-test-id': 'leftJson' }" | ||||
|     /> | ||||
|   </n-form-item> | ||||
|   <n-form-item label="Your json to compare" v-bind="rightJsonValidation.attrs"> | ||||
|     <n-input | ||||
|       v-model:value="rawRightJson" | ||||
|       placeholder="Paste your json to compare here..." | ||||
|       type="textarea" | ||||
|       rows="20" | ||||
|       autocomplete="off" | ||||
|       autocorrect="off" | ||||
|       autocapitalize="off" | ||||
|       spellcheck="false" | ||||
|       :input-props="{ 'data-test-id': 'rightJson' }" | ||||
|     /> | ||||
|   </n-form-item> | ||||
|   <c-input-text | ||||
|     v-model:value="rawLeftJson" | ||||
|     :validation-rules="jsonValidationRules" | ||||
|     label="Your first json" | ||||
|     placeholder="Paste your first json here..." | ||||
|     rows="20" | ||||
|     multiline | ||||
|     test-id="leftJson" | ||||
|     raw-text | ||||
|   /> | ||||
| 
 | ||||
|   <c-input-text | ||||
|     v-model:value="rawRightJson" | ||||
|     :validation-rules="jsonValidationRules" | ||||
|     label="Your json to compare" | ||||
|     placeholder="Paste your json to compare here..." | ||||
|     rows="20" | ||||
|     multiline | ||||
|     test-id="rightJson" | ||||
|     raw-text | ||||
|   /> | ||||
| 
 | ||||
|   <DiffsViewer :left-json="leftJson" :right-json="rightJson" /> | ||||
| </template> | ||||
| @ -33,7 +28,6 @@ | ||||
| import JSON5 from 'json5'; | ||||
| 
 | ||||
| import { withDefaultOnError } from '@/utils/defaults'; | ||||
| import { useValidation } from '@/composable/validation'; | ||||
| import { isNotThrowing } from '@/utils/boolean'; | ||||
| import DiffsViewer from './diff-viewer/diff-viewer.vue'; | ||||
| 
 | ||||
| @ -43,17 +37,10 @@ const rawRightJson = ref(''); | ||||
| const leftJson = computed(() => withDefaultOnError(() => JSON5.parse(rawLeftJson.value), undefined)); | ||||
| const rightJson = computed(() => withDefaultOnError(() => JSON5.parse(rawRightJson.value), undefined)); | ||||
| 
 | ||||
| const createJsonValidation = (json: Ref) => | ||||
|   useValidation({ | ||||
|     source: json, | ||||
|     rules: [ | ||||
|       { | ||||
|         validator: (value) => value === '' || isNotThrowing(() => JSON5.parse(value)), | ||||
|         message: 'Invalid JSON', | ||||
|       }, | ||||
|     ], | ||||
|   }); | ||||
| 
 | ||||
| const leftJsonValidation = createJsonValidation(rawLeftJson); | ||||
| const rightJsonValidation = createJsonValidation(rawRightJson); | ||||
| const jsonValidationRules = [ | ||||
|   { | ||||
|     validator: (value: string) => value === '' || isNotThrowing(() => JSON5.parse(value)), | ||||
|     message: 'Invalid JSON format', | ||||
|   }, | ||||
| ]; | ||||
| </script> | ||||
|  | ||||
| @ -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'"); | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -1,37 +1,37 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <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> | ||||
|     <c-input-text | ||||
|       v-model:value="macAddress" | ||||
|       label="MAC address:" | ||||
|       size="large" | ||||
|       placeholder="Type a MAC address" | ||||
|       clearable | ||||
|       autocomplete="off" | ||||
|       autocorrect="off" | ||||
|       autocapitalize="off" | ||||
|       spellcheck="false" | ||||
|       :validation-rules="macAddressValidationRules" | ||||
|       mb-5 | ||||
|     /> | ||||
| 
 | ||||
|     <n-form-item label="Vendor info:"> | ||||
|       <c-card> | ||||
|         <n-text v-if="details"> | ||||
|           <div v-for="(detail, index) of details.split('\n')" :key="index">{{ detail }}</div> | ||||
|         </n-text> | ||||
|     <div mb-5px>Vendor info:</div> | ||||
|     <c-card mb-5> | ||||
|       <div v-if="details"> | ||||
|         <div v-for="(detail, index) of details.split('\n')" :key="index">{{ detail }}</div> | ||||
|       </div> | ||||
| 
 | ||||
|         <n-text v-else depth="3" italic>Unknown vendor for this address</n-text> | ||||
|       </c-card> | ||||
|     </n-form-item> | ||||
|       <div v-else italic op-60>Unknown vendor for this address</div> | ||||
|     </c-card> | ||||
| 
 | ||||
|     <n-space justify="center"> | ||||
|     <div flex justify-center> | ||||
|       <c-button :disabled="!details" @click="copy"> Copy vendor info </c-button> | ||||
|     </n-space> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import db from 'oui/oui.json'; | ||||
| import { macAddressValidation } from '@/utils/macAddress'; | ||||
| import { macAddressValidationRules } from '@/utils/macAddress'; | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| 
 | ||||
| const getVendorValue = (address: string) => address.trim().replace(/[.:-]/g, '').toUpperCase().substring(0, 6); | ||||
| @ -39,8 +39,6 @@ const getVendorValue = (address: string) => address.trim().replace(/[.:-]/g, '') | ||||
| const macAddress = ref('20:37:06:12:34:56'); | ||||
| const details = computed<string | undefined>(() => db[getVendorValue(macAddress.value)]); | ||||
| 
 | ||||
| const { attrs: validationAttrs } = macAddressValidation(macAddress); | ||||
| 
 | ||||
| const { copy } = useCopy({ source: details, text: 'Vendor info copied to the clipboard' }); | ||||
| </script> | ||||
| 
 | ||||
|  | ||||
| @ -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]" | ||||
|  | ||||
| @ -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> | ||||
|     <input-copyable | ||||
|       label="Secret in hexadecimal" | ||||
|       :value="base32toHex(secret)" | ||||
|       readonly | ||||
|       placeholder="Secret in hex will be displayed here" | ||||
|       mb-5 | ||||
|     /> | ||||
| 
 | ||||
|     <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> | ||||
|     <input-copyable | ||||
|       label="Epoch" | ||||
|       :value="Math.floor(now / 1000).toString()" | ||||
|       readonly | ||||
|       mb-5 | ||||
|       placeholder="Epoch in sec will be displayed here" | ||||
|     /> | ||||
| 
 | ||||
|     <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> | ||||
|     <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> | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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: [ | ||||
|     { | ||||
|  | ||||
| @ -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> | ||||
| 
 | ||||
|  | ||||
| @ -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" /> | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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' }, | ||||
|  | ||||
| @ -2,28 +2,51 @@ | ||||
|   <div v-for="buttonVariant of buttonVariants" :key="buttonVariant"> | ||||
|     <h2>{{ _.capitalize(buttonVariant) }}</h2> | ||||
| 
 | ||||
|     <c-button v-for="buttonType of buttonTypes" :key="buttonType" :variant="buttonVariant" :type="buttonType" mx-1> | ||||
|       Button | ||||
|     </c-button> | ||||
|     <div v-for="buttonSize of buttonSizes" :key="buttonSize" mb-2> | ||||
|       <c-button | ||||
|         v-for="buttonType of buttonTypes" | ||||
|         :key="buttonType" | ||||
|         :variant="buttonVariant" | ||||
|         :type="buttonType" | ||||
|         :size="buttonSize" | ||||
|         mx-1 | ||||
|       > | ||||
|         Button | ||||
|       </c-button> | ||||
| 
 | ||||
|     <c-button | ||||
|       v-for="buttonType of buttonTypes" | ||||
|       :key="buttonType" | ||||
|       :variant="buttonVariant" | ||||
|       :type="buttonType" | ||||
|       circle | ||||
|       mx-1 | ||||
|     > | ||||
|       A | ||||
|     </c-button> | ||||
|       <c-button | ||||
|         v-for="buttonType of buttonTypes" | ||||
|         :key="buttonType" | ||||
|         :variant="buttonVariant" | ||||
|         :type="buttonType" | ||||
|         :size="buttonSize" | ||||
|         circle | ||||
|         mx-1 | ||||
|       > | ||||
|         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> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import _ from 'lodash'; | ||||
| 
 | ||||
| const buttonVariants = ['basic', 'text']; | ||||
| const buttonTypes = ['default', 'primary']; | ||||
| const buttonVariants = ['basic', 'text'] as const; | ||||
| const buttonTypes = ['default', 'primary', 'warning'] as const; | ||||
| const buttonSizes = ['small', 'medium', 'large'] as const; | ||||
| </script> | ||||
| 
 | ||||
| <style lang="less" scoped></style> | ||||
|  | ||||
| @ -27,6 +27,21 @@ const createTheme = ({ style }: { style: 'light' | 'dark' }) => { | ||||
|   const theme = appThemes[style]; | ||||
| 
 | ||||
|   return { | ||||
|     size: { | ||||
|       small: { | ||||
|         width: '28px', | ||||
|         fontSize: '12px', | ||||
|       }, | ||||
|       medium: { | ||||
|         width: '34px', | ||||
|         fontSize: '14px', | ||||
|       }, | ||||
|       large: { | ||||
|         width: '40px', | ||||
|         fontSize: '16px', | ||||
|       }, | ||||
|     }, | ||||
| 
 | ||||
|     basic: { | ||||
|       default: createState({ | ||||
|         textColor: theme.text.baseColor, | ||||
| @ -41,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: { | ||||
| @ -61,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), | ||||
|       }), | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
| @ -18,13 +18,14 @@ import { useAppTheme } from '../theme/themes'; | ||||
| 
 | ||||
| const props = withDefaults( | ||||
|   defineProps<{ | ||||
|     type?: 'default' | 'primary'; | ||||
|     type?: 'default' | 'primary' | 'warning'; | ||||
|     variant?: 'basic' | 'text'; | ||||
|     disabled?: boolean; | ||||
|     round?: boolean; | ||||
|     circle?: boolean; | ||||
|     href?: string; | ||||
|     to?: RouteLocationRaw; | ||||
|     size?: 'small' | 'medium' | 'large'; | ||||
|   }>(), | ||||
|   { | ||||
|     type: 'default', | ||||
| @ -34,9 +35,10 @@ const props = withDefaults( | ||||
|     circle: false, | ||||
|     href: undefined, | ||||
|     to: undefined, | ||||
|     size: 'medium', | ||||
|   }, | ||||
| ); | ||||
| const { variant, disabled, round, circle, href, type, to } = toRefs(props); | ||||
| const { variant, disabled, round, circle, href, type, to, size: sizeName } = toRefs(props); | ||||
| 
 | ||||
| const emits = defineEmits(['click']); | ||||
| 
 | ||||
| @ -58,18 +60,20 @@ const tag = computed(() => { | ||||
|   return 'button'; | ||||
| }); | ||||
| const appTheme = useAppTheme(); | ||||
| 
 | ||||
| const size = computed(() => theme.value.size[sizeName.value]); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="less" scoped> | ||||
| .c-button { | ||||
|   line-height: 1; | ||||
|   font-family: inherit; | ||||
|   font-size: inherit; | ||||
|   font-size: v-bind('size.fontSize'); | ||||
|   border: none; | ||||
|   text-align: center; | ||||
|   cursor: pointer; | ||||
|   text-decoration: none; | ||||
|   height: 34px; | ||||
|   height: v-bind('size.width'); | ||||
|   font-weight: 400; | ||||
|   color: v-bind('variantTheme.textColor'); | ||||
|   padding: 0 14px; | ||||
| @ -89,8 +93,9 @@ const appTheme = useAppTheme(); | ||||
|   } | ||||
| 
 | ||||
|   &.circle { | ||||
|     border-radius: 40px; | ||||
|     width: 34px; | ||||
|     border-radius: v-bind('size.width'); | ||||
|     width: v-bind('size.width'); | ||||
|     padding: 0; | ||||
|   } | ||||
| 
 | ||||
|   &:not(.disabled) { | ||||
|  | ||||
							
								
								
									
										76
									
								
								src/ui/c-input-text/c-input-text.demo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/ui/c-input-text/c-input-text.demo.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | ||||
| <template> | ||||
|   <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> | ||||
| 
 | ||||
|   <c-input-text placeholder="Placeholder" /> | ||||
| 
 | ||||
|   <h2>With label</h2> | ||||
| 
 | ||||
|   <c-input-text label="Label" mb-2 /> | ||||
|   <c-input-text label="Label" mb-2 label-position="left" /> | ||||
|   <c-input-text label="Label" mb-2 label-position="left" label-width="100px" /> | ||||
|   <c-input-text label="Label" mb-2 label-position="left" label-width="100px" label-align="right" /> | ||||
| 
 | ||||
|   <h2>Readonly</h2> | ||||
| 
 | ||||
|   <c-input-text value="value" readonly /> | ||||
| 
 | ||||
|   <h2>Disabled</h2> | ||||
| 
 | ||||
|   <c-input-text value="value" disabled /> | ||||
| 
 | ||||
|   <h2>Validation</h2> | ||||
| 
 | ||||
|   <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> | ||||
							
								
								
									
										160
									
								
								src/ui/c-input-text/c-input-text.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								src/ui/c-input-text/c-input-text.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,160 @@ | ||||
| import { describe, expect, it, beforeEach } from 'vitest'; | ||||
| 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', () => { | ||||
|   beforeEach(() => { | ||||
|     setActivePinia(createPinia()); | ||||
|   }); | ||||
| 
 | ||||
|   it('Renders a label', () => { | ||||
|     const wrapper = shallowMount(CInputText, { | ||||
|       props: { | ||||
|         label: 'Label', | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     expect(wrapper.get('.label').text()).to.equal('Label'); | ||||
|   }); | ||||
| 
 | ||||
|   it('Renders a placeholder', () => { | ||||
|     const wrapper = shallowMount(CInputText, { | ||||
|       props: { | ||||
|         placeholder: 'Placeholder', | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     expect(wrapper.get('.input').attributes('placeholder')).to.equal('Placeholder'); | ||||
|   }); | ||||
| 
 | ||||
|   it('Renders a value', () => { | ||||
|     const wrapper = shallowMount(CInputText, { | ||||
|       props: { | ||||
|         value: 'Value', | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     expect(wrapper.vm.value).to.equal('Value'); | ||||
|   }); | ||||
| 
 | ||||
|   it('Renders a provided id', () => { | ||||
|     const wrapper = shallowMount(CInputText, { | ||||
|       props: { | ||||
|         id: 'id', | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     expect(wrapper.get('.input').attributes('id')).to.equal('id'); | ||||
|   }); | ||||
| 
 | ||||
|   it('updates value on input', async () => { | ||||
|     const wrapper = shallowMount(CInputText); | ||||
| 
 | ||||
|     await wrapper.get('input').setValue('Hello'); | ||||
| 
 | ||||
|     expect(_.get(wrapper.emitted(), 'update:value.0.0')).to.equal('Hello'); | ||||
|   }); | ||||
| 
 | ||||
|   it('cannot be edited when disabled', async () => { | ||||
|     const wrapper = shallowMount(CInputText, { | ||||
|       props: { | ||||
|         disabled: true, | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     await wrapper.get('input').setValue('Hello'); | ||||
| 
 | ||||
|     expect(_.get(wrapper.emitted(), 'update:value')).toBeUndefined(); | ||||
|   }); | ||||
| 
 | ||||
|   it('renders a feedback message for invalid rules', async () => { | ||||
|     const wrapper = shallowMount(CInputText, { | ||||
|       props: { validationRules: [{ validator: () => false, message: '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 () => { | ||||
|     const wrapper = shallowMount(CInputText, { | ||||
|       props: { rules: [{ validator: () => true, message: 'Message' }] }, | ||||
|     }); | ||||
| 
 | ||||
|     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'); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										20
									
								
								src/ui/c-input-text/c-input-text.theme.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/ui/c-input-text/c-input-text.theme.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| import { defineThemes } from '../theme/theme.models'; | ||||
| 
 | ||||
| export const { useTheme } = defineThemes({ | ||||
|   dark: { | ||||
|     backgroundColor: '#333333', | ||||
|     borderColor: '#333333', | ||||
| 
 | ||||
|     focus: { | ||||
|       backgroundColor: '#1ea54c1a', | ||||
|     }, | ||||
|   }, | ||||
|   light: { | ||||
|     backgroundColor: '#ffffff', | ||||
|     borderColor: '#e0e0e69e', | ||||
| 
 | ||||
|     focus: { | ||||
|       backgroundColor: '#ffffff', | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
							
								
								
									
										300
									
								
								src/ui/c-input-text/c-input-text.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										300
									
								
								src/ui/c-input-text/c-input-text.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,300 @@ | ||||
| <template> | ||||
|   <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="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> | ||||
| 
 | ||||
|         <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> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { generateRandomId } from '@/utils/random'; | ||||
| import { useValidation, type UseValidationRule } from '@/composable/validation'; | ||||
| import { useTheme } from './c-input-text.theme'; | ||||
| import { useAppTheme } from '../theme/themes'; | ||||
| 
 | ||||
| const props = withDefaults( | ||||
|   defineProps<{ | ||||
|     value?: string; | ||||
|     id?: string; | ||||
|     placeholder?: string; | ||||
|     label?: string; | ||||
|     readonly?: boolean; | ||||
|     disabled?: boolean; | ||||
|     validationRules?: UseValidationRule<string>[]; | ||||
|     validation?: ReturnType<typeof useValidation>; | ||||
|     labelPosition?: 'top' | 'left'; | ||||
|     labelWidth?: string; | ||||
|     labelAlign?: 'left' | 'right'; | ||||
|     clearable?: boolean; | ||||
|     testId?: string; | ||||
|     autocapitalize?: 'none' | 'sentences' | 'words' | 'characters' | 'on' | 'off' | string; | ||||
|     autocomplete?: 'on' | 'off' | string; | ||||
|     autocorrect?: 'on' | 'off' | string; | ||||
|     spellcheck?: 'true' | 'false' | boolean; | ||||
|     rawText?: boolean; | ||||
|     type?: 'text' | 'password'; | ||||
|     multiline?: boolean; | ||||
|     rows?: number | string; | ||||
|     autosize?: boolean; | ||||
|   }>(), | ||||
|   { | ||||
|     value: '', | ||||
|     id: generateRandomId, | ||||
|     placeholder: 'Input text', | ||||
|     label: undefined, | ||||
|     readonly: false, | ||||
|     disabled: false, | ||||
|     validationRules: () => [], | ||||
|     validation: undefined, | ||||
|     labelPosition: 'top', | ||||
|     labelWidth: 'auto', | ||||
|     labelAlign: 'left', | ||||
|     clearable: false, | ||||
|     testId: undefined, | ||||
|     autocapitalize: undefined, | ||||
|     autocomplete: undefined, | ||||
|     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, autosize } = toRefs(props); | ||||
| 
 | ||||
| const validation = | ||||
|   props.validation ?? | ||||
|   useValidation({ | ||||
|     rules: validationRules, | ||||
|     source: value, | ||||
|   }); | ||||
| 
 | ||||
| const theme = useTheme(); | ||||
| const appTheme = useAppTheme(); | ||||
| 
 | ||||
| const textareaRef = ref<HTMLTextAreaElement>(); | ||||
| const inputWrapperRef = ref<HTMLElement>(); | ||||
| 
 | ||||
| defineExpose({ | ||||
|   inputWrapperRef, | ||||
| }); | ||||
| 
 | ||||
| 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> | ||||
| .c-input-text { | ||||
|   display: inline-flex; | ||||
|   flex-direction: column; | ||||
|   width: 100%; | ||||
| 
 | ||||
|   &.label-left { | ||||
|     flex-direction: row; | ||||
|     align-items: baseline; | ||||
|   } | ||||
| 
 | ||||
|   &.error { | ||||
|     & > .input { | ||||
|       border-color: v-bind('appTheme.error.color'); | ||||
|       &:hover, | ||||
|       &:focus { | ||||
|         border-color: v-bind('appTheme.error.color'); | ||||
|       } | ||||
| 
 | ||||
|       &:focus { | ||||
|         background-color: v-bind('appTheme.error.color + 22'); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     & .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: 12px; | ||||
|   } | ||||
| 
 | ||||
|   .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; | ||||
|       min-width: 0; | ||||
| 
 | ||||
|       padding: 8px 0; | ||||
|       outline: none; | ||||
|       background-color: transparent; | ||||
|       background-image: none; | ||||
|       -webkit-box-shadow: none; | ||||
|       -moz-box-shadow: none; | ||||
|       box-shadow: none; | ||||
|       background-color: transparent; | ||||
|       border: none; | ||||
|       color: v-bind('appTheme.text.baseColor'); | ||||
| 
 | ||||
|       &::placeholder { | ||||
|         color: v-bind('appTheme.text.mutedColor'); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     &:hover { | ||||
|       border-color: v-bind('appTheme.primary.color'); | ||||
|     } | ||||
| 
 | ||||
|     &:focus-within { | ||||
|       border-color: v-bind('appTheme.primary.color'); | ||||
| 
 | ||||
|       background-color: v-bind('theme.focus.backgroundColor'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &.error .input-wrapper { | ||||
|     border-color: v-bind('appTheme.error.color'); | ||||
| 
 | ||||
|     &:hover, | ||||
|     &:focus-within { | ||||
|       border-color: v-bind('appTheme.error.color'); | ||||
|     } | ||||
| 
 | ||||
|     &:focus-within { | ||||
|       background-color: v-bind('appTheme.error.color + 22'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &.disabled .input-wrapper { | ||||
|     opacity: 0.5; | ||||
| 
 | ||||
|     &:hover, | ||||
|     &:focus-within { | ||||
|       border-color: v-bind('theme.borderColor'); | ||||
|     } | ||||
| 
 | ||||
|     & > .input { | ||||
|       cursor: not-allowed; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| @ -18,6 +18,8 @@ | ||||
|       </div> | ||||
| 
 | ||||
|       <div flex-1 pl-4> | ||||
|         <h1>{{ componentName }}</h1> | ||||
| 
 | ||||
|         <router-view /> | ||||
|       </div> | ||||
|     </div> | ||||
| @ -25,9 +27,12 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import _ from 'lodash'; | ||||
| import { demoRoutes } from './demo.routes'; | ||||
| 
 | ||||
| const route = useRoute(); | ||||
| 
 | ||||
| const componentName = computed(() => _.startCase(String(route.name).replace(/^c-/, ''))); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="less" scoped></style> | ||||
|  | ||||
| @ -6,8 +6,6 @@ export const demoRoutes = Object.keys(demoPages).map((path) => { | ||||
|   const [, , fileName] = path.split('/'); | ||||
|   const name = fileName.split('.').shift(); | ||||
| 
 | ||||
|   console.log(path); | ||||
| 
 | ||||
|   return { | ||||
|     path: name, | ||||
|     name, | ||||
|  | ||||
| @ -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', | ||||
|  | ||||
| @ -1,16 +1,18 @@ | ||||
| import { useValidation } from '@/composable/validation'; | ||||
| import type { Ref } from 'vue'; | ||||
| 
 | ||||
| const macAddressValidationRules = [ | ||||
|   { | ||||
|     message: 'Invalid MAC address', | ||||
|     validator: (value: string) => value.trim().match(/^([0-9A-Fa-f]{2}[:-]){2,5}([0-9A-Fa-f]{2})$/), | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| function macAddressValidation(value: Ref) { | ||||
|   return useValidation({ | ||||
|     source: value, | ||||
|     rules: [ | ||||
|       { | ||||
|         message: 'Invalid MAC address', | ||||
|         validator: (value) => value.trim().match(/^([0-9A-Fa-f]{2}[:-]){2,5}([0-9A-Fa-f]{2})$/), | ||||
|       }, | ||||
|     ], | ||||
|     rules: macAddressValidationRules, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export { macAddressValidation }; | ||||
| export { macAddressValidation, macAddressValidationRules }; | ||||
|  | ||||
| @ -18,4 +18,14 @@ const shuffleArray = <T>(array: T[]): T[] => shuffleArrayMutate([...array]); | ||||
| 
 | ||||
| const shuffleString = (str: string, delimiter = ''): string => shuffleArrayMutate(str.split(delimiter)).join(delimiter); | ||||
| 
 | ||||
| export { randFromArray, randIntFromInterval, random, shuffleArray, shuffleArrayMutate, shuffleString }; | ||||
| const generateRandomId = () => `id-${random().toString(36).substring(2, 12)}`; | ||||
| 
 | ||||
| export { | ||||
|   randFromArray, | ||||
|   randIntFromInterval, | ||||
|   random, | ||||
|   shuffleArray, | ||||
|   shuffleArrayMutate, | ||||
|   shuffleString, | ||||
|   generateRandomId, | ||||
| }; | ||||
|  | ||||
| @ -9,6 +9,6 @@ | ||||
|     "paths": { | ||||
|       "@/*": ["./src/*"] | ||||
|     }, | ||||
|     "types": ["naive-ui/volar"] | ||||
|     "types": ["naive-ui/volar", "unplugin-icons/types/vue"] | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -10,6 +10,9 @@ import AutoImport from 'unplugin-auto-import/vite'; | ||||
| import Components from 'unplugin-vue-components/vite'; | ||||
| import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'; | ||||
| import Unocss from 'unocss/vite'; | ||||
| import { configDefaults } from 'vitest/config'; | ||||
| import Icons from 'unplugin-icons/vite'; | ||||
| import IconsResolver from 'unplugin-icons/resolver'; | ||||
| 
 | ||||
| // https://vitejs.dev/config/
 | ||||
| export default defineConfig({ | ||||
| @ -28,7 +31,7 @@ export default defineConfig({ | ||||
|         enabled: true, | ||||
|       }, | ||||
|     }), | ||||
| 
 | ||||
|     Icons({ compiler: 'vue3' }), | ||||
|     vue({ | ||||
|       include: [/\.vue$/, /\.md$/], | ||||
|     }), | ||||
| @ -76,7 +79,7 @@ export default defineConfig({ | ||||
|       dirs: ['src/'], | ||||
|       extensions: ['vue', 'md'], | ||||
|       include: [/\.vue$/, /\.vue\?vue/, /\.md$/], | ||||
|       resolvers: [NaiveUiResolver()], | ||||
|       resolvers: [NaiveUiResolver(), IconsResolver({ prefix: 'icon' })], | ||||
|     }), | ||||
|     Unocss(), | ||||
|   ], | ||||
| @ -88,4 +91,7 @@ export default defineConfig({ | ||||
|   define: { | ||||
|     'import.meta.env.PACKAGE_VERSION': JSON.stringify(process.env.npm_package_version), | ||||
|   }, | ||||
|   test: { | ||||
|     exclude: [...configDefaults.exclude, '**/*.e2e.spec.ts'], | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| @ -1,13 +0,0 @@ | ||||
| import { configDefaults, defineConfig } from 'vitest/config'; | ||||
| import path from 'path'; | ||||
| 
 | ||||
| export default defineConfig({ | ||||
|   resolve: { | ||||
|     alias: { | ||||
|       '@': path.resolve(__dirname, './src'), | ||||
|     }, | ||||
|   }, | ||||
|   test: { | ||||
|     exclude: [...configDefaults.exclude, '**/*.e2e.spec.ts'], | ||||
|   }, | ||||
| }); | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user