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:
Carsten Götzinger 2023-05-15 06:44:51 +02:00
commit 12f3697d53
52 changed files with 1298 additions and 587 deletions

View file

@ -6,8 +6,8 @@ labels: new tool
assignees: CorentinTh assignees: CorentinTh
--- ---
**Which tool is impacted?** **What tool do you want?**
Example: the token generator Example: a token generator
**Describe the solution you'd like** **Describe the solution you'd like**
A clear and concise description of what you want to happen. A clear and concise description of what you want to happen.

View file

@ -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. 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 ## Version 2023.04.23-92bd835
### Features ### Features

10
components.d.ts vendored
View file

@ -26,11 +26,15 @@ declare module '@vue/runtime-core' {
'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default'] 'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default']
ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default'] ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default']
Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default'] Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default']
CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default']
'CInputText.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: typeof import('./src/ui/c-link/c-link.vue')['default']
'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default'] 'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default']
CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default'] CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default']
ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default'] ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default']
ColoredCard: typeof import('./src/components/ColoredCard.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'] CopyableIpLike: typeof import('./src/tools/ipv4-subnet-calculator/copyable-ip-like.vue')['default']
CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.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'] 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'] HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default']
HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default'] HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default']
HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
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'] InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.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'] Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default']

View file

@ -1,6 +1,6 @@
{ {
"name": "it-tools", "name": "it-tools",
"version": "2023.4.23-92bd835", "version": "2023.5.14-77f2efc",
"description": "Collection of handy online tools for developers, with great UX. ", "description": "Collection of handy online tools for developers, with great UX. ",
"keywords": [ "keywords": [
"productivity", "productivity",
@ -79,6 +79,7 @@
"yaml": "^2.2.1" "yaml": "^2.2.1"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/mdi": "^1.1.50",
"@playwright/test": "^1.32.3", "@playwright/test": "^1.32.3",
"@rushstack/eslint-patch": "^1.2.0", "@rushstack/eslint-patch": "^1.2.0",
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
@ -98,8 +99,10 @@
"@unocss/eslint-config": "^0.50.8", "@unocss/eslint-config": "^0.50.8",
"@vitejs/plugin-vue": "^2.3.4", "@vitejs/plugin-vue": "^2.3.4",
"@vitejs/plugin-vue-jsx": "^1.3.10", "@vitejs/plugin-vue-jsx": "^1.3.10",
"@vue/compiler-sfc": "^3.2.47",
"@vue/eslint-config-prettier": "^7.1.0", "@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^10.0.0", "@vue/eslint-config-typescript": "^10.0.0",
"@vue/runtime-core": "^3.2.47",
"@vue/test-utils": "^2.3.2", "@vue/test-utils": "^2.3.2",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.1.3",
"c8": "^7.13.0", "c8": "^7.13.0",
@ -116,6 +119,7 @@
"typescript": "~4.5.5", "typescript": "~4.5.5",
"unocss": "^0.50.8", "unocss": "^0.50.8",
"unplugin-auto-import": "^0.15.2", "unplugin-auto-import": "^0.15.2",
"unplugin-icons": "^0.16.1",
"unplugin-vue-components": "^0.24.1", "unplugin-vue-components": "^0.24.1",
"vite": "^2.9.15", "vite": "^2.9.15",
"vite-plugin-md": "^0.12.4", "vite-plugin-md": "^0.12.4",

47
pnpm-lock.yaml generated
View file

@ -135,6 +135,9 @@ dependencies:
version: 2.2.1 version: 2.2.1
devDependencies: devDependencies:
'@iconify-json/mdi':
specifier: ^1.1.50
version: 1.1.50
'@playwright/test': '@playwright/test':
specifier: ^1.32.3 specifier: ^1.32.3
version: 1.32.3 version: 1.32.3
@ -192,12 +195,18 @@ devDependencies:
'@vitejs/plugin-vue-jsx': '@vitejs/plugin-vue-jsx':
specifier: ^1.3.10 specifier: ^1.3.10
version: 1.3.10 version: 1.3.10
'@vue/compiler-sfc':
specifier: ^3.2.47
version: 3.2.47
'@vue/eslint-config-prettier': '@vue/eslint-config-prettier':
specifier: ^7.1.0 specifier: ^7.1.0
version: 7.1.0(eslint@8.38.0)(prettier@2.8.7) version: 7.1.0(eslint@8.38.0)(prettier@2.8.7)
'@vue/eslint-config-typescript': '@vue/eslint-config-typescript':
specifier: ^10.0.0 specifier: ^10.0.0
version: 10.0.0(eslint-plugin-vue@8.7.1)(eslint@8.38.0)(typescript@4.5.5) 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': '@vue/test-utils':
specifier: ^2.3.2 specifier: ^2.3.2
version: 2.3.2(vue@3.2.47) version: 2.3.2(vue@3.2.47)
@ -246,6 +255,9 @@ devDependencies:
unplugin-auto-import: unplugin-auto-import:
specifier: ^0.15.2 specifier: ^0.15.2
version: 0.15.2(@vueuse/core@8.9.4)(rollup@2.79.1) 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: unplugin-vue-components:
specifier: ^0.24.1 specifier: ^0.24.1
version: 0.24.1(rollup@2.79.1)(vue@3.2.47) 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==} resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
dev: true 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: /@iconify/types@2.0.0:
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
dev: true dev: true
@ -7561,6 +7579,35 @@ packages:
- rollup - rollup
dev: true 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): /unplugin-vue-components@0.24.1(rollup@2.79.1)(vue@3.2.47):
resolution: {integrity: sha512-T3A8HkZoIE1Cja95xNqolwza0yD5IVlgZZ1PVAGvVCx8xthmjsv38xWRCtHtwl+rvZyL9uif42SRkDGw9aCfMA==} resolution: {integrity: sha512-T3A8HkZoIE1Cja95xNqolwza0yD5IVlgZZ1PVAGvVCx8xthmjsv38xWRCtHtwl+rvZyL9uif42SRkDGw9aCfMA==}
engines: {node: '>=14'} engines: {node: '>=14'}

View file

@ -1,26 +1,27 @@
<template> <template>
<n-form-item :label="inputLabel" v-bind="validationAttrs"> <c-input-text
<n-input
ref="inputElement" ref="inputElement"
v-model:value="input" v-model:value="input"
:placeholder="inputPlaceholder" :placeholder="inputPlaceholder"
type="textarea" :label="inputLabel"
multiline
autosize
rows="20" rows="20"
autocomplete="off" raw-text
autocorrect="off" test-id="input"
autocapitalize="off" :validation-rules="inputValidationRules"
spellcheck="false"
:input-props="{ 'data-test-id': 'input' }"
/> />
</n-form-item>
<n-form-item :label="outputLabel"> <div>
<textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement" /> <div mb-5px>{{ outputLabel }}</div>
</n-form-item> <textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement?.inputWrapperRef" />
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useValidation, type UseValidationRule } from '@/composable/validation'; import type { UseValidationRule } from '@/composable/validation';
import _ from 'lodash'; import _ from 'lodash';
import CInputText from '@/ui/c-input-text/c-input-text.vue';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -46,12 +47,10 @@ const props = withDefaults(
const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault } = const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault } =
toRefs(props); toRefs(props);
const inputElement = ref(); const inputElement = ref<typeof CInputText>();
const input = ref(inputDefault.value); const input = ref(inputDefault.value);
const output = computed(() => transformer.value(input.value)); const output = computed(() => transformer.value(input.value));
const { attrs: validationAttrs } = useValidation({ source: input, rules: inputValidationRules.value });
</script> </script>
<style scoped></style> <style scoped></style>

View file

@ -1,21 +1,20 @@
<template> <template>
<n-input v-model:value="value"> <c-input-text v-model:value="value">
<template #suffix> <template #suffix>
<n-tooltip trigger="hover"> <n-tooltip trigger="hover">
<template #trigger> <template #trigger>
<c-button circle variant="text" @click="onCopyClicked"> <c-button circle variant="text" size="small" @click="onCopyClicked">
<n-icon :component="ContentCopyFilled" /> <icon-mdi-content-copy />
</c-button> </c-button>
</template> </template>
{{ tooltipText }} {{ tooltipText }}
</n-tooltip> </n-tooltip>
</template> </template>
</n-input> </c-input-text>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useVModel, useClipboard } from '@vueuse/core'; import { useVModel, useClipboard } from '@vueuse/core';
import { ContentCopyFilled } from '@vicons/material';
import { ref } from 'vue'; import { ref } from 'vue';
const props = defineProps<{ value: string }>(); const props = defineProps<{ value: string }>();
@ -35,9 +34,3 @@ function onCopyClicked() {
}, 2000); }, 2000);
} }
</script> </script>
<style scoped>
::v-deep(.n-input-wrapper) {
padding-right: 5px;
}
</style>

View file

@ -1,3 +1,4 @@
import { get, type MaybeRef } from '@vueuse/core';
import _ from 'lodash'; import _ from 'lodash';
import { reactive, watch, type Ref } from 'vue'; import { reactive, watch, type Ref } from 'vue';
@ -31,7 +32,7 @@ export function useValidation<T>({
watch: watchRefs = [], watch: watchRefs = [],
}: { }: {
source: Ref<T>; source: Ref<T>;
rules: UseValidationRule<T>[]; rules: MaybeRef<UseValidationRule<T>[]>;
watch?: Ref<unknown>[]; watch?: Ref<unknown>[];
}) { }) {
const state = reactive<{ const state = reactive<{
@ -55,7 +56,7 @@ export function useValidation<T>({
state.message = ''; state.message = '';
state.status = undefined; state.status = undefined;
for (const rule of rules) { for (const rule of get(rules)) {
if (isFalsyOrHasThrown(() => rule.validator(source.value))) { if (isFalsyOrHasThrown(() => rule.validator(source.value))) {
state.message = rule.message; state.message = rule.message;
state.status = 'error'; state.status = 'error';

View file

@ -1,17 +1,19 @@
<template> <template>
<c-card title="Base64 to file"> <c-card title="Base64 to file">
<n-form-item <c-input-text
:feedback="base64InputValidation.message" v-model:value="base64Input"
:validation-status="base64InputValidation.status" multiline
:show-label="false" placeholder="Put your base64 file string here..."
> rows="5"
<n-input v-model:value="base64Input" type="textarea" placeholder="Put your base64 file string here..." rows="5" /> :validation="base64InputValidation"
</n-form-item> mb-2
<n-space justify="center"> />
<div flex justify-center>
<c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="downloadFile()"> <c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="downloadFile()">
Download file Download file
</c-button> </c-button>
</n-space> </div>
</c-card> </c-card>
<c-card title="File to base64"> <c-card title="File to base64">
@ -24,10 +26,11 @@
</n-upload-dragger> </n-upload-dragger>
</n-upload> </n-upload>
<n-input :value="fileBase64" type="textarea" readonly placeholder="File in base64 will be here" /> <c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" mb-2 />
<n-space justify="center">
<div flex justify-center>
<c-button @click="copyFileBase64()"> Copy </c-button> <c-button @click="copyFileBase64()"> Copy </c-button>
</n-space> </div>
</c-card> </c-card>
</template> </template>
@ -77,11 +80,6 @@ async function onUpload({ file: { file } }: { file: UploadFileInfo }) {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.n-input,
.n-upload {
margin-bottom: 15px;
}
::v-deep(.n-upload-trigger) { ::v-deep(.n-upload-trigger) {
width: 100%; width: 100%;
} }

View file

@ -3,46 +3,63 @@
<n-form-item label="Encode URL safe" label-placement="left"> <n-form-item label="Encode URL safe" label-placement="left">
<n-switch v-model:value="encodeUrlSafe" /> <n-switch v-model:value="encodeUrlSafe" />
</n-form-item> </n-form-item>
<n-form-item label="String to encode"> <c-input-text
<n-input v-model:value="textInput" type="textarea" placeholder="Put your string here..." rows="5" /> v-model:value="textInput"
</n-form-item> multiline
placeholder="Put your string here..."
rows="5"
label="String to encode"
raw-text
mb-5
/>
<n-form-item label="Base64 of string"> <c-input-text
<n-input label="Base64 of string"
:value="base64Output" :value="base64Output"
type="textarea" multiline
readonly readonly
placeholder="The base64 encoding of your string will be here" placeholder="The base64 encoding of your string will be here"
rows="5" rows="5"
mb-5
/> />
</n-form-item>
<n-space justify="center"> <div flex justify-center>
<c-button @click="copyTextBase64()"> Copy base64 </c-button> <c-button @click="copyTextBase64()"> Copy base64 </c-button>
</n-space> </div>
</c-card> </c-card>
<c-card title="Base64 to string"> <c-card title="Base64 to string">
<n-form-item label="Decode URL safe" label-placement="left"> <n-form-item label="Decode URL safe" label-placement="left">
<n-switch v-model:value="decodeUrlSafe" /> <n-switch v-model:value="decodeUrlSafe" />
</n-form-item> </n-form-item>
<n-form-item label="Base64 string to decode" v-bind="b64Validation.attrs"> <c-input-text
<n-input v-model:value="base64Input" type="textarea" placeholder="Your base64 string..." rows="5" /> v-model:value="base64Input"
</n-form-item> multiline
placeholder="Your base64 string..."
rows="5"
:validation-rules="b64ValidationRules"
label="Base64 string to decode"
mb-5
/>
<n-form-item label="Decoded string"> <c-input-text
<n-input :value="textOutput" type="textarea" readonly placeholder="The decoded string will be here" rows="5" /> v-model:value="textOutput"
</n-form-item> 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> <c-button @click="copyText()"> Copy decoded string </c-button>
</n-space> </div>
</c-card> </c-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useCopy } from '@/composable/copy'; import { useCopy } from '@/composable/copy';
import { useValidation } from '@/composable/validation';
import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64'; import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64';
import { withDefaultOnError } from '@/utils/defaults'; import { withDefaultOnError } from '@/utils/defaults';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
@ -59,8 +76,8 @@ const textOutput = computed(() =>
withDefaultOnError(() => base64ToText(base64Input.value.trim(), decodeUrlSafe.value), ''), withDefaultOnError(() => base64ToText(base64Input.value.trim(), decodeUrlSafe.value), ''),
); );
const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' }); const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' });
const b64Validation = useValidation({ const b64ValidationRules = [
source: base64Input, { message: 'Invalid base64 string', validator: (value: string) => isValidBase64(value.trim(), decodeUrlSafe.value) },
rules: [{ message: 'Invalid base64 string', validator: (value) => isValidBase64(value.trim(), decodeUrlSafe.value) }], ];
});
</script> </script>

View file

@ -1,17 +1,15 @@
<template> <template>
<div> <div>
<n-form-item label="Username"> <c-input-text v-model:value="username" label="Username" placeholder="Your username..." clearable raw-text mb-5 />
<n-input v-model:value="username" placeholder="Your username..." clearable /> <c-input-text
</n-form-item>
<n-form-item label="Password">
<n-input
v-model:value="password" v-model:value="password"
label="Password"
placeholder="Your password..." placeholder="Your password..."
type="password"
show-password-on="click"
clearable clearable
raw-text
mb-2
type="password"
/> />
</n-form-item>
<c-card> <c-card>
<n-statistic label="Authorization header:" class="header"> <n-statistic label="Authorization header:" class="header">

View file

@ -1,21 +1,20 @@
<template> <template>
<c-card title="Hash"> <c-card title="Hash">
<n-form label-width="120"> <c-input-text
<n-form-item label="Your string: " label-placement="left">
<n-input
v-model:value="input" v-model:value="input"
placeholder="Your string to bcrypt..." placeholder="Your string to bcrypt..."
autocomplete="off" raw-text
autocorrect="off" label="Your string: "
autocapitalize="off" label-position="left"
spellcheck="false" label-width="120px"
mb-2
/> />
</n-form-item> <n-form-item label="Salt count: " label-placement="left" label-width="120">
<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-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="10" :min="0" w-full />
</n-form-item> </n-form-item>
<n-input :value="hashed" readonly style="text-align: center" />
</n-form> <c-input-text :value="hashed" readonly text-center />
<n-space justify="center" mt-5> <n-space justify="center" mt-5>
<c-button @click="copy"> Copy hash </c-button> <c-button @click="copy"> Copy hash </c-button>
</n-space> </n-space>
@ -24,24 +23,10 @@
<c-card title="Compare string with hash"> <c-card title="Compare string with hash">
<n-form label-width="120"> <n-form label-width="120">
<n-form-item label="Your string: " label-placement="left"> <n-form-item label="Your string: " label-placement="left">
<n-input <c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text />
v-model:value="compareString"
placeholder="Your string to compare..."
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
</n-form-item> </n-form-item>
<n-form-item label="Your hash: " label-placement="left"> <n-form-item label="Your hash: " label-placement="left">
<n-input <c-input-text v-model:value="compareHash" placeholder="Your hahs to compare..." raw-text />
v-model:value="compareHash"
placeholder="Your hahs to compare..."
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
</n-form-item> </n-form-item>
<n-form-item label="Do they match ? " label-placement="left" :show-feedback="false"> <n-form-item label="Do they match ? " label-placement="left" :show-feedback="false">
<div class="compare-result" :class="{ positive: compareMatch }"> <div class="compare-result" :class="{ positive: compareMatch }">

View file

@ -1,11 +1,15 @@
<template> <template>
<n-scrollbar style="flex: 1" x-scrollable> <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"> <div v-for="(suite, index) of suites" :key="index">
<c-card style="width: 292px; margin: 0 8px 5px"> <c-card style="width: 294px">
<n-form-item label="Suite name:" :show-feedback="false" label-placement="left"> <c-input-text
<n-input v-model:value="suite.title" placeholder="Suite name..." /> v-model:value="suite.title"
</n-form-item> label-position="left"
label="Suite name"
placeholder="Suite name..."
clearable
/>
<n-divider></n-divider> <n-divider></n-divider>
<n-form-item label="Suite values" :show-feedback="false"> <n-form-item label="Suite values" :show-feedback="false">
@ -33,9 +37,7 @@
<div style="flex: 0 0 100%"> <div style="flex: 0 0 100%">
<div style="max-width: 600px; margin: 0 auto"> <div style="max-width: 600px; margin: 0 auto">
<n-space justify="center"> <n-space justify="center">
<n-form-item label="Unit:" label-placement="left"> <c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 />
<n-input v-model:value="unit" placeholder="Unit (eg: ms)" />
</n-form-item>
<c-button <c-button
@click=" @click="

View file

@ -16,7 +16,8 @@
:validation-status="entropyValidation.status" :validation-status="entropyValidation.status"
> >
<n-input-group> <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"> <c-button @click="refreshEntropy">
<n-icon size="22"> <n-icon size="22">
<Refresh /> <Refresh />
@ -37,15 +38,7 @@
:validation-status="mnemonicValidation.status" :validation-status="mnemonicValidation.status"
> >
<n-input-group> <n-input-group>
<n-input <c-input-text v-model:value="passphrase" placeholder="Your mnemonic..." raw-text />
v-model:value="passphrase"
style="text-align: center; flex: 1"
placeholder="Your mnemonic..."
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
<c-button @click="copyPassphrase"> <c-button @click="copyPassphrase">
<n-icon size="22" :component="Copy" /> <n-icon size="22" :component="Copy" />

View file

@ -1,9 +1,15 @@
<template> <template>
<c-card> <c-card>
<n-form label-width="120" label-placement="left" :show-feedback="false"> <n-form label-width="120" label-placement="left" :show-feedback="false">
<n-form-item label="Your string:"> <c-input-text
<n-input v-model:value="input" /> v-model:value="input"
</n-form-item> label="Your string"
label-position="left"
label-width="120px"
label-align="right"
placeholder="Your string..."
raw-text
/>
<n-divider /> <n-divider />

View file

@ -4,8 +4,8 @@
<div class="duration">{{ formatMs(counter) }}</div> <div class="duration">{{ formatMs(counter) }}</div>
</c-card> </c-card>
<n-space justify="center" mt-5> <n-space justify="center" mt-5>
<c-button v-if="!isRunning" secondary type="primary" @click="resume">Start</c-button> <c-button v-if="!isRunning" type="primary" @click="resume">Start</c-button>
<c-button v-else secondary type="warning" @click="pause">Stop</c-button> <c-button v-else type="warning" @click="pause">Stop</c-button>
<c-button @click="counter = 0">Reset</c-button> <c-button @click="counter = 0">Reset</c-button>
</n-space> </n-space>

View file

@ -9,25 +9,25 @@
/> />
</n-form-item> </n-form-item>
<n-form-item label="color name:"> <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>
<n-form-item label="hex:"> <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>
<n-form-item label="rgb:"> <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>
<n-form-item label="hsl:"> <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>
<n-form-item label="hwb:"> <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>
<n-form-item label="lch:"> <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>
<n-form-item label="cmyk:"> <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-item>
</n-form> </n-form>
</c-card> </c-card>
@ -54,6 +54,7 @@ const cmyk = ref('');
const lch = ref(''); const lch = ref('');
function onInputUpdated(value: string, omit: string) { function onInputUpdated(value: string, omit: string) {
try {
const color = colord(value); const color = colord(value);
if (omit !== 'name') name.value = color.toName({ closest: true }) ?? ''; if (omit !== 'name') name.value = color.toName({ closest: true }) ?? '';
@ -63,6 +64,9 @@ function onInputUpdated(value: string, omit: string) {
if (omit !== 'hwb') hwb.value = color.toHwbString(); if (omit !== 'hwb') hwb.value = color.toHwbString();
if (omit !== 'cmyk') cmyk.value = color.toCmykString(); if (omit !== 'cmyk') cmyk.value = color.toCmykString();
if (omit !== 'lch') lch.value = color.toLchString(); if (omit !== 'lch') lch.value = color.toLchString();
} catch {
//
}
} }
onInputUpdated(hex.value, 'hex'); onInputUpdated(hex.value, 'hex');

View file

@ -1,13 +1,15 @@
<template> <template>
<c-card> <c-card>
<n-form-item <div mx-auto max-w-sm>
class="cron" <c-input-text
:show-label="false" v-model:value="cron"
:feedback="cronValidation.message" size="large"
:validation-status="cronValidation.status" placeholder="* * * * *"
> :validation-rules="cronValidationRules"
<n-input v-model:value="cron" size="large" placeholder="* * * * *" /> mb-3
</n-form-item> />
</div>
<div class="cron-string"> <div class="cron-string">
{{ cronString }} {{ cronString }}
</div> </div>
@ -86,7 +88,6 @@
import cronstrue from 'cronstrue'; import cronstrue from 'cronstrue';
import { isValidCron } from 'cron-validator'; import { isValidCron } from 'cron-validator';
import { computed, reactive, ref } from 'vue'; import { computed, reactive, ref } from 'vue';
import { useValidation } from '@/composable/validation';
import { useStyleStore } from '@/stores/style.store'; import { useStyleStore } from '@/stores/style.store';
function isCronValid(v: string) { function isCronValid(v: string) {
@ -185,30 +186,20 @@ const cronString = computed(() => {
return ' '; return ' ';
}); });
const cronValidation = useValidation({ const cronValidationRules = [
source: cron,
rules: [
{ {
validator: (value) => isCronValid(value), validator: (value: string) => isCronValid(value),
message: 'This cron is invalid', message: 'This cron is invalid',
}, },
], ];
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.cron { ::v-deep(input) {
text-align: center;
margin: auto;
max-width: 400px;
display: block;
.n-input {
font-size: 30px; font-size: 30px;
font-family: monospace; font-family: monospace;
padding: 5px; padding: 5px;
} text-align: center;
} }
.cron-string { .cron-string {

View file

@ -1,14 +1,14 @@
<template> <template>
<div> <div>
<n-form-item :show-label="false" v-bind="validation.attrs">
<n-input-group> <n-input-group>
<n-input <c-input-text
v-model:value="inputDate" v-model:value="inputDate"
autofocus autofocus
:on-input="onDateInputChanged"
placeholder="Put you date string here..." placeholder="Put you date string here..."
clearable clearable
:input-props="{ 'data-test-id': 'date-time-converter-input' }" test-id="date-time-converter-input"
:validation="validation"
@update:value="onDateInputChanged"
/> />
<n-select <n-select
@ -18,18 +18,22 @@
data-test-id="date-time-converter-format-select" data-test-id="date-time-converter-format-select"
/> />
</n-input-group> </n-input-group>
</n-form-item>
<n-divider style="margin-top: 0" /> <n-divider />
<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 <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)" :value="formatDateUsingFormatter(fromDate, normalizedDate)"
placeholder="Invalid date..." placeholder="Invalid date..."
:input-props="{ 'data-test-id': name }" :test-id="name"
readonly
mt-2
/> />
</n-input-group>
</div>
</div> </div>
</template> </template>

View file

@ -7,12 +7,15 @@
type="textarea" type="textarea"
placeholder="The string to cypher" placeholder="The string to cypher"
:autosize="{ minRows: 4 }" :autosize="{ minRows: 4 }"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/> />
</n-form-item> </n-form-item>
<n-space vertical> <n-space vertical>
<n-form-item label="Your secret key:" :show-feedback="false"> <c-input-text v-model:value="cypherSecret" label="Your secret key:" clearable raw-text />
<n-input v-model:value="cypherSecret" />
</n-form-item>
<n-form-item label="Encryption algorithm:" :show-feedback="false"> <n-form-item label="Encryption algorithm:" :show-feedback="false">
<n-select <n-select
v-model:value="cypherAlgo" v-model:value="cypherAlgo"
@ -43,12 +46,15 @@
type="textarea" type="textarea"
placeholder="The string to cypher" placeholder="The string to cypher"
:autosize="{ minRows: 4 }" :autosize="{ minRows: 4 }"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/> />
</n-form-item> </n-form-item>
<n-space vertical> <n-space vertical>
<n-form-item label="Your secret key:" :show-feedback="false"> <c-input-text v-model:value="decryptSecret" label="Your secret key:" clearable raw-text />
<n-input v-model:value="decryptSecret" />
</n-form-item>
<n-form-item label="Encryption algorithm:" :show-feedback="false"> <n-form-item label="Encryption algorithm:" :show-feedback="false">
<n-select <n-select
v-model:value="decryptAlgo" v-model:value="decryptAlgo"

View file

@ -22,59 +22,54 @@
<n-alert v-if="error" style="margin-top: 25px" type="error">{{ error }}</n-alert> <n-alert v-if="error" style="margin-top: 25px" type="error">{{ error }}</n-alert>
<n-divider /> <n-divider />
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> Binary (2): </n-input-group-label>
<input-copyable <input-copyable
label="Binary (2)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })"
readonly
placeholder="Binary version will be here..." placeholder="Binary version will be here..."
/> />
</n-input-group>
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> Octal (8): </n-input-group-label>
<input-copyable <input-copyable
label="Octal (8)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })"
readonly
placeholder="Octal version will be here..." placeholder="Octal version will be here..."
/> />
</n-input-group>
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> Decimal (10): </n-input-group-label>
<input-copyable <input-copyable
label="Decimal (10)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })"
readonly
placeholder="Decimal version will be here..." placeholder="Decimal version will be here..."
/> />
</n-input-group>
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> Hexadecimal (16): </n-input-group-label>
<input-copyable <input-copyable
label="Hexadecimal (16)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })"
readonly placeholder="Hexadecimal version will be here..."
placeholder="Decimal version will be here..."
/> />
</n-input-group>
<n-input-group>
<n-input-group-label style="flex: 0 0 170px"> Base64 (64): </n-input-group-label>
<input-copyable <input-copyable
label="Base64 (64)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })"
readonly
placeholder="Base64 version will be here..." 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>
<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 <input-copyable
flex-1
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: outputBase })" :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: outputBase })"
readonly
:placeholder="`Base ${outputBase} will be here...`" :placeholder="`Base ${outputBase} will be here...`"
/> />
</n-input-group> </div>
</c-card> </c-card>
</div> </div>
</template> </template>
@ -88,6 +83,14 @@ import InputCopyable from '../../components/InputCopyable.vue';
const styleStore = useStyleStore(); const styleStore = useStyleStore();
const inputProps = {
labelPosition: 'left',
labelWidth: '170px',
labelAlign: 'right',
readonly: true,
'mb-2': '',
} as const;
const input = ref('42'); const input = ref('42');
const inputBase = ref(10); const inputBase = ref(10);
const outputBase = ref(42); const outputBase = ref(42);

View file

@ -1,23 +1,20 @@
<template> <template>
<div> <div>
<n-form-item label="An ipv4 address:" v-bind="validationAttrs"> <c-input-text v-model:value="rawIpAddress" label="The ipv4 address:" placeholder="The ipv4 address..." readonly />
<n-input v-model:value="rawIpAddress" placeholder="An ipv4 address..." />
</n-form-item>
<n-divider style="margin-top: 0" mt-0 /> <n-divider />
<n-form-item <input-copyable
v-for="{ label, value } of convertedSections" v-for="{ label, value } of convertedSections"
:key="label" :key="label"
:label="label" :label="label"
label-placement="left" label-position="left"
label-width="100" label-width="100px"
> label-align="right"
<input-copyable mb-2
:value="validationAttrs.validationStatus === 'error' ? '' : value" :value="validationAttrs.validationStatus === 'error' ? '' : value"
placeholder="Set a correct ipv4 address" placeholder="Set a correct ipv4 address"
/> />
</n-form-item>
</div> </div>
</template> </template>

View file

@ -2,14 +2,23 @@
<div> <div>
<n-space item-style="flex:1 1 0"> <n-space item-style="flex:1 1 0">
<div> <div>
<n-space item-style="flex:1 1 0"> <div mb-4 flex gap-4>
<n-form-item label="Start address" v-bind="startIpValidation.attrs"> <c-input-text
<n-input v-model:value="rawStartAddress" placeholder="Start IPv4 address..." /> v-model:value="rawStartAddress"
</n-form-item> label="Start address"
<n-form-item label="End address" v-bind="endIpValidation.attrs"> placeholder="Start IPv4 address..."
<n-input v-model:value="rawEndAddress" placeholder="End IPv4 address..." /> :validation="startIpValidation"
</n-form-item> clearable
</n-space> />
<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"> <n-table v-if="showResult" data-test-id="result">
<thead> <thead>

View file

@ -1,8 +1,12 @@
<template> <template>
<div> <div>
<n-form-item label="An IPv4 address with or without mask" v-bind="validationAttrs"> <c-input-text
<n-input v-model:value="ip" /> v-model:value="ip"
</n-form-item> label="An IPv4 address with or without mask"
placeholder="The ipv4 address..."
:validation-rules="ipValidationRules"
mb-4
/>
<div v-if="networkInfo"> <div v-if="networkInfo">
<n-table> <n-table>
@ -37,7 +41,6 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { Netmask } from 'netmask'; import { Netmask } from 'netmask';
import { withDefaultOnError } from '@/utils/defaults'; import { withDefaultOnError } from '@/utils/defaults';
import { useValidation } from '@/composable/validation';
import { isNotThrowing } from '@/utils/boolean'; import { isNotThrowing } from '@/utils/boolean';
import { useStorage } from '@vueuse/core'; import { useStorage } from '@vueuse/core';
import { ArrowLeft, ArrowRight } from '@vicons/tabler'; 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 networkInfo = computed(() => withDefaultOnError(() => getNetworkInfo(ip.value), undefined));
const { attrs: validationAttrs } = useValidation({ const ipValidationRules = [
source: ip,
rules: [
{ {
message: 'We cannot parse this address, check the format', message: 'We cannot parse this address, check the format',
validator: (value) => isNotThrowing(() => getNetworkInfo(value.trim())), validator: (value: string) => isNotThrowing(() => getNetworkInfo(value.trim())),
}, },
], ];
});
const sections: { const sections: {
label: string; label: string;

View file

@ -1,30 +1,32 @@
<template> <template>
<div> <div>
<n-space vertical :size="50">
<n-alert title="Info" type="info"> <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, 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. and the lower 40 bits to generate your random ULA.
</n-alert> </n-alert>
<n-form-item label="MAC address:" v-bind="validationAttrs"> <c-input-text
<n-input
v-model:value="macAddress" v-model:value="macAddress"
size="large"
placeholder="Type a MAC address" placeholder="Type a MAC address"
clearable clearable
autocomplete="off" label="MAC address:"
autocorrect="off" raw-text
autocapitalize="off" my-8
spellcheck="false" :validation="addressValidation"
/> />
</n-form-item>
</n-space>
<div v-if="validationAttrs.validationStatus !== 'error'"> <div v-if="addressValidation.isValid">
<n-input-group v-for="{ label, value } in calculatedSections" :key="label" style="margin: 5px 0"> <input-copyable
<n-input-group-label style="flex: 0 0 160px"> {{ label }} </n-input-group-label> v-for="{ label, value } in calculatedSections"
<input-copyable :value="value" readonly /> :key="label"
</n-input-group> :value="value"
:label="label"
label-width="160px"
label-align="right"
label-position="left"
readonly
mb-2
/>
</div> </div>
</div> </div>
</template> </template>
@ -59,7 +61,7 @@ const calculatedSections = computed(() => {
]; ];
}); });
const { attrs: validationAttrs } = macAddressValidation(macAddress); const addressValidation = macAddressValidation(macAddress);
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View file

@ -1,30 +1,25 @@
<template> <template>
<n-form-item label="Your first json" v-bind="leftJsonValidation.attrs"> <c-input-text
<n-input
v-model:value="rawLeftJson" v-model:value="rawLeftJson"
:validation-rules="jsonValidationRules"
label="Your first json"
placeholder="Paste your first json here..." placeholder="Paste your first json here..."
type="textarea"
rows="20" rows="20"
autocomplete="off" multiline
autocorrect="off" test-id="leftJson"
autocapitalize="off" raw-text
spellcheck="false"
:input-props="{ 'data-test-id': 'leftJson' }"
/> />
</n-form-item>
<n-form-item label="Your json to compare" v-bind="rightJsonValidation.attrs"> <c-input-text
<n-input
v-model:value="rawRightJson" v-model:value="rawRightJson"
:validation-rules="jsonValidationRules"
label="Your json to compare"
placeholder="Paste your json to compare here..." placeholder="Paste your json to compare here..."
type="textarea"
rows="20" rows="20"
autocomplete="off" multiline
autocorrect="off" test-id="rightJson"
autocapitalize="off" raw-text
spellcheck="false"
:input-props="{ 'data-test-id': 'rightJson' }"
/> />
</n-form-item>
<DiffsViewer :left-json="leftJson" :right-json="rightJson" /> <DiffsViewer :left-json="leftJson" :right-json="rightJson" />
</template> </template>
@ -33,7 +28,6 @@
import JSON5 from 'json5'; import JSON5 from 'json5';
import { withDefaultOnError } from '@/utils/defaults'; import { withDefaultOnError } from '@/utils/defaults';
import { useValidation } from '@/composable/validation';
import { isNotThrowing } from '@/utils/boolean'; import { isNotThrowing } from '@/utils/boolean';
import DiffsViewer from './diff-viewer/diff-viewer.vue'; 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 leftJson = computed(() => withDefaultOnError(() => JSON5.parse(rawLeftJson.value), undefined));
const rightJson = computed(() => withDefaultOnError(() => JSON5.parse(rawRightJson.value), undefined)); const rightJson = computed(() => withDefaultOnError(() => JSON5.parse(rawRightJson.value), undefined));
const createJsonValidation = (json: Ref) => const jsonValidationRules = [
useValidation({
source: json,
rules: [
{ {
validator: (value) => value === '' || isNotThrowing(() => JSON5.parse(value)), validator: (value: string) => value === '' || isNotThrowing(() => JSON5.parse(value)),
message: 'Invalid JSON', message: 'Invalid JSON format',
}, },
], ];
});
const leftJsonValidation = createJsonValidation(rawLeftJson);
const rightJsonValidation = createJsonValidation(rawRightJson);
</script> </script>

View file

@ -30,8 +30,8 @@ test.describe('Tool - List converter', () => {
3 3
5`); 5`);
await page.getByTestId('removeDuplicates').check(); await page.getByTestId('removeDuplicates').check();
await page.getByTestId('itemPrefix').locator('input').fill("'"); await page.getByTestId('itemPrefix').fill("'");
await page.getByTestId('itemSuffix').locator('input').fill("'"); await page.getByTestId('itemSuffix').fill("'");
const result = await page.getByTestId('area-content').innerText(); const result = await page.getByTestId('area-content').innerText();
expect(result.trim()).toEqual("'1', '2', '4', '3', '5'"); expect(result.trim()).toEqual("'1', '2', '4', '3', '5'");

View file

@ -36,37 +36,39 @@
/> />
</n-form-item> </n-form-item>
<n-form-item label="Separator" label-placement="left" label-width="120" :show-feedback="false" mb-2> <c-input-text
<n-input v-model:value="conversionConfig.separator" placeholder="," /> v-model:value="conversionConfig.separator"
</n-form-item> 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-form-item label="Wrap item" label-placement="left" label-width="120" :show-feedback="false" mb-2>
<n-input-group> <c-input-text
<n-input
v-model:value="conversionConfig.itemPrefix" v-model:value="conversionConfig.itemPrefix"
placeholder="Item prefix" placeholder="Item prefix"
data-test-id="itemPrefix" test-id="itemPrefix"
/> />
<n-input <c-input-text
v-model:value="conversionConfig.itemSuffix" v-model:value="conversionConfig.itemSuffix"
placeholder="Item suffix" placeholder="Item suffix"
data-test-id="itemSuffix" test-id="itemSuffix"
/> />
</n-input-group>
</n-form-item> </n-form-item>
<n-form-item label="Wrap list" label-placement="left" label-width="120" :show-feedback="false" mb-2> <n-form-item label="Wrap list" label-placement="left" label-width="120" :show-feedback="false" mb-2>
<n-input-group> <c-input-text
<n-input
v-model:value="conversionConfig.listPrefix" v-model:value="conversionConfig.listPrefix"
placeholder="List prefix" placeholder="List prefix"
data-test-id="listPrefix" test-id="listPrefix"
/> />
<n-input <c-input-text
v-model:value="conversionConfig.listSuffix" v-model:value="conversionConfig.listSuffix"
placeholder="List suffix" placeholder="List suffix"
data-test-id="listSuffix" test-id="listSuffix"
/> />
</n-input-group>
</n-form-item> </n-form-item>
</div> </div>
</div> </div>

View file

@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<n-form-item label="MAC address:" v-bind="validationAttrs"> <c-input-text
<n-input
v-model:value="macAddress" v-model:value="macAddress"
label="MAC address:"
size="large" size="large"
placeholder="Type a MAC address" placeholder="Type a MAC address"
clearable clearable
@ -10,28 +10,28 @@
autocorrect="off" autocorrect="off"
autocapitalize="off" autocapitalize="off"
spellcheck="false" spellcheck="false"
:validation-rules="macAddressValidationRules"
mb-5
/> />
</n-form-item>
<n-form-item label="Vendor info:"> <div mb-5px>Vendor info:</div>
<c-card> <c-card mb-5>
<n-text v-if="details"> <div v-if="details">
<div v-for="(detail, index) of details.split('\n')" :key="index">{{ detail }}</div> <div v-for="(detail, index) of details.split('\n')" :key="index">{{ detail }}</div>
</n-text> </div>
<n-text v-else depth="3" italic>Unknown vendor for this address</n-text> <div v-else italic op-60>Unknown vendor for this address</div>
</c-card> </c-card>
</n-form-item>
<n-space justify="center"> <div flex justify-center>
<c-button :disabled="!details" @click="copy"> Copy vendor info </c-button> <c-button :disabled="!details" @click="copy"> Copy vendor info </c-button>
</n-space> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import db from 'oui/oui.json'; import db from 'oui/oui.json';
import { macAddressValidation } from '@/utils/macAddress'; import { macAddressValidationRules } from '@/utils/macAddress';
import { useCopy } from '@/composable/copy'; import { useCopy } from '@/composable/copy';
const getVendorValue = (address: string) => address.trim().replace(/[.:-]/g, '').toUpperCase().substring(0, 6); 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 macAddress = ref('20:37:06:12:34:56');
const details = computed<string | undefined>(() => db[getVendorValue(macAddress.value)]); 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' }); const { copy } = useCopy({ source: details, text: 'Vendor info copied to the clipboard' });
</script> </script>

View file

@ -5,7 +5,7 @@
<n-input-group v-for="{ key, type, label, placeholder, ...element } of elements" :key="key"> <n-input-group v-for="{ key, type, label, placeholder, ...element } of elements" :key="key">
<n-input-group-label style="flex: 0 0 110px">{{ label }}</n-input-group-label> <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 <n-dynamic-input
v-else-if="type === 'input-multiple'" v-else-if="type === 'input-multiple'"
v-model:value="metadata[key]" v-model:value="metadata[key]"

View file

@ -1,19 +1,23 @@
<template> <template>
<div style="max-width: 350px"> <div style="max-width: 350px">
<n-form-item label="Secret" v-bind="secretValidationAttrs"> <c-input-text
<n-input v-model:value="secret" placeholder="Paste your TOTP secret..."> v-model:value="secret"
label="Secret"
placeholder="Paste your TOTP secret..."
mb-5
:validation-rules="secretValidationRules"
>
<template #suffix> <template #suffix>
<n-tooltip trigger="hover"> <n-tooltip trigger="hover">
<template #trigger> <template #trigger>
<c-button circle variant="text" @click="refreshSecret"> <c-button circle variant="text" size="small" @click="refreshSecret">
<n-icon :component="Refresh" /> <icon-mdi-refresh />
</c-button> </c-button>
</template> </template>
Generate secret token Generate secret token
</n-tooltip> </n-tooltip>
</template> </template>
</n-input> </c-input-text>
</n-form-item>
<div> <div>
<token-display :tokens="tokens" style="margin-top: 2px" /> <token-display :tokens="tokens" style="margin-top: 2px" />
@ -27,49 +31,52 @@
</n-space> </n-space>
</div> </div>
<div style="max-width: 350px"> <div style="max-width: 350px">
<n-form-item label="Secret in hexadecimal">
<input-copyable :value="base32toHex(secret)" readonly placeholder="Secret in hex will be displayed here" />
</n-form-item>
<n-form-item label="Epoch">
<input-copyable <input-copyable
label="Secret in hexadecimal"
:value="base32toHex(secret)"
readonly
placeholder="Secret in hex will be displayed here"
mb-5
/>
<input-copyable
label="Epoch"
:value="Math.floor(now / 1000).toString()" :value="Math.floor(now / 1000).toString()"
readonly readonly
mb-5
placeholder="Epoch in sec will be displayed here" placeholder="Epoch in sec will be displayed here"
/> />
</n-form-item>
<n-form-item label="Iteration" :show-feedback="false"> <p>Iteration</p>
<n-input-group>
<n-input-group-label style="width: 110px">Count:</n-input-group-label>
<input-copyable <input-copyable
:value="String(getCounterFromTime({ now, timeStep: 30 }))" :value="String(getCounterFromTime({ now, timeStep: 30 }))"
readonly readonly
label="Count:"
label-position="left"
label-width="90px"
label-align="right"
placeholder="Iteration count will be displayed here" placeholder="Iteration count will be displayed here"
/> />
</n-input-group>
</n-form-item>
<n-form-item label="Iteration" :show-label="false" style="margin-top: 5px">
<n-input-group>
<n-input-group-label style="width: 110px">Padded hex:</n-input-group-label>
<input-copyable <input-copyable
:value="getCounterFromTime({ now, timeStep: 30 }).toString(16).padStart(16, '0')" :value="getCounterFromTime({ now, timeStep: 30 }).toString(16).padStart(16, '0')"
readonly readonly
placeholder="Iteration count in hex will be displayed here" placeholder="Iteration count in hex will be displayed here"
label-position="left"
label-width="90px"
label-align="right"
label="Padded hex:"
/> />
</n-input-group>
</n-form-item>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { Refresh } from '@vicons/tabler';
import { useTimestamp } from '@vueuse/core'; import { useTimestamp } from '@vueuse/core';
import { useThemeVars } from 'naive-ui'; import { useThemeVars } from 'naive-ui';
import { useStyleStore } from '@/stores/style.store'; import { useStyleStore } from '@/stores/style.store';
import InputCopyable from '@/components/InputCopyable.vue'; import InputCopyable from '@/components/InputCopyable.vue';
import { useValidation } from '@/composable/validation';
import { computedRefreshable } from '@/composable/computedRefreshable'; import { computedRefreshable } from '@/composable/computedRefreshable';
import { generateTOTP, buildKeyUri, generateSecret, base32toHex, getCounterFromTime } from './otp.service'; import { generateTOTP, buildKeyUri, generateSecret, base32toHex, getCounterFromTime } from './otp.service';
import { useQRCode } from '../qr-code-generator/useQRCode'; import { useQRCode } from '../qr-code-generator/useQRCode';
@ -106,19 +113,16 @@ const { qrcode } = useQRCode({
options: { width: 210 }, options: { width: 210 },
}); });
const { attrs: secretValidationAttrs } = useValidation({ const secretValidationRules = [
source: secret,
rules: [
{ {
message: 'Secret should be a base32 string', message: 'Secret should be a base32 string',
validator: (value) => value.toUpperCase().match(/^[A-Z234567]+$/), validator: (value: string) => value.toUpperCase().match(/^[A-Z234567]+$/),
}, },
{ {
message: 'Please set a secret', message: 'Please set a secret',
validator: (value) => value !== '', validator: (value: string) => value !== '',
}, },
], ];
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View file

@ -3,9 +3,14 @@
<n-form-item label="Default country code:"> <n-form-item label="Default country code:">
<n-select v-model:value="defaultCountryCode" :options="countriesOptions" /> <n-select v-model:value="defaultCountryCode" :options="countriesOptions" />
</n-form-item> </n-form-item>
<n-form-item label="Phone number:" v-bind="validation.attrs">
<n-input v-model:value="rawPhone" placeholder="Enter a phone number" /> <c-input-text
</n-form-item> v-model:value="rawPhone"
placeholder="Enter a phone number"
label="Phone number:"
:validation="validation"
mb-5
/>
<n-table v-if="parsedDetails"> <n-table v-if="parsedDetails">
<tbody> <tbody>

View file

@ -2,7 +2,7 @@
<div> <div>
<c-card title="Arabic to roman"> <c-card title="Arabic to roman">
<n-space align="center" justify="space-between"> <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-input-number v-model:value="inputNumeral" :min="1" style="width: 200px" :show-button="false" />
</n-form-item> </n-form-item>
<div class="result"> <div class="result">
@ -15,13 +15,12 @@
</c-card> </c-card>
<c-card title="Roman to arabic" mt-5> <c-card title="Roman to arabic" mt-5>
<n-space align="center" justify="space-between"> <n-space align="center" justify="space-between">
<n-form-item v-bind="validationRoman"> <c-input-text v-model:value="inputRoman" style="width: 200px" :validation="validationRoman" />
<n-input v-model:value="inputRoman" style="width: 200px" />
</n-form-item>
<div class="result"> <div class="result">
{{ outputNumeral }} {{ outputNumeral }}
</div> </div>
<c-button :disabled="validationRoman.validationStatus === 'error'" @click="copyArabic"> Copy </c-button> <c-button :disabled="!validationRoman.isValid" @click="copyArabic"> Copy </c-button>
</n-space> </n-space>
</c-card> </c-card>
</div> </div>
@ -55,7 +54,7 @@ const { attrs: validationNumeral } = useValidation({
const inputRoman = ref('XLII'); const inputRoman = ref('XLII');
const outputNumeral = computed(() => romanToArabic(inputRoman.value)); const outputNumeral = computed(() => romanToArabic(inputRoman.value));
const { attrs: validationRoman } = useValidation({ const validationRoman = useValidation({
source: inputRoman, source: inputRoman,
rules: [ rules: [
{ {

View file

@ -1,7 +1,7 @@
<template> <template>
<div style="flex: 0 0 100%"> <div style="flex: 0 0 100%">
<n-space item-style="flex: 1 1 0" style="margin: 0 auto; max-width: 600px" justify="center"> <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-input-number v-model:value="bits" min="256" max="16384" step="8" />
</n-form-item> </n-form-item>

View file

@ -21,9 +21,15 @@
<n-form-item label="Font size"> <n-form-item label="Font size">
<n-input-number v-model:value="fontSize" placeholder="Font size..." min="1" /> <n-input-number v-model:value="fontSize" placeholder="Font size..." min="1" />
</n-form-item> </n-form-item>
<n-form-item label="Custom text">
<n-input v-model:value="customText" :placeholder="`Default is ${width}x${height}`" /> <c-input-text
</n-form-item> 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-space>
<n-form-item label="Use exact size" label-placement="left"> <n-form-item label="Use exact size" label-placement="left">
<n-switch v-model:value="useExactSize" /> <n-switch v-model:value="useExactSize" />

View file

@ -1,8 +1,12 @@
<template> <template>
<div> <div>
<n-form-item label="Your text to convert to NATO phonetic alphabet"> <c-input-text
<n-input v-model:value="input" placeholder="Put your text here..." clearable /> v-model:value="input"
</n-form-item> label="Your text to convert to NATO phonetic alphabet"
placeholder="Put your text here..."
clearable
mb-5
/>
<n-space v-if="natoText" vertical> <n-space v-if="natoText" vertical>
<n-text>Your text in NATO phonetic alphabet</n-text> <n-text>Your text in NATO phonetic alphabet</n-text>

View file

@ -1,51 +1,59 @@
<template> <template>
<c-card> <c-card>
<n-form-item label="Your url to parse:" :feedback="validation.message" :validation-status="validation.status"> <c-input-text
<n-input v-model:value="urlToParse" placeholder="Your url to parse..." /> v-model:value="urlToParse"
</n-form-item> 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> <input-copyable
<n-input-group v-for="{ title, key } in properties" :key="key"> v-for="{ title, key } in properties"
<n-input-group-label style="flex: 0 0 120px"> {{ title }}: </n-input-group-label> :key="key"
<input-copyable :value="(urlParsed?.[key] as string) ?? ''" readonly placeholder=" " /> :label="title"
</n-input-group> :value="(urlParsed?.[key] as string) ?? ''"
readonly
label-position="left"
label-width="110px"
mb-2
placeholder=" "
/>
<n-input-group <div
v-for="[k, v] in Object.entries(Object.fromEntries(urlParsed?.searchParams.entries() ?? []))" v-for="[k, v] in Object.entries(Object.fromEntries(urlParsed?.searchParams.entries() ?? []))"
:key="k" :key="k"
mb-2
w-full
flex
> >
<n-input-group-label style="flex: 0 0 120px"> <div style="flex: 1 0 110px">
<n-icon :component="SubdirectoryArrowRightRound" /> <icon-mdi-arrow-right-bottom />
</n-input-group-label> </div>
<input-copyable :value="k" readonly /> <input-copyable :value="k" readonly />
<input-copyable :value="v" readonly /> <input-copyable :value="v" readonly />
</n-input-group> </div>
</n-form>
</c-card> </c-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useValidation } from '@/composable/validation';
import { isNotThrowing } from '@/utils/boolean'; import { isNotThrowing } from '@/utils/boolean';
import { withDefaultOnError } from '@/utils/defaults'; import { withDefaultOnError } from '@/utils/defaults';
import { SubdirectoryArrowRightRound } from '@vicons/material';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import InputCopyable from '../../components/InputCopyable.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 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 urlParsed = computed(() => withDefaultOnError(() => new URL(urlToParse.value), undefined));
const validation = useValidation({ const urlValidationRules = [
source: urlToParse,
rules: [
{ {
validator: (value) => isNotThrowing(() => new URL(value)), validator: (value: string) => isNotThrowing(() => new URL(value)),
message: 'Invalid url', message: 'Invalid url',
}, },
], ];
});
const properties: { title: string; key: keyof URL }[] = [ const properties: { title: string; key: keyof URL }[] = [
{ title: 'Protocol', key: 'protocol' }, { title: 'Protocol', key: 'protocol' },

View file

@ -2,7 +2,15 @@
<div v-for="buttonVariant of buttonVariants" :key="buttonVariant"> <div v-for="buttonVariant of buttonVariants" :key="buttonVariant">
<h2>{{ _.capitalize(buttonVariant) }}</h2> <h2>{{ _.capitalize(buttonVariant) }}</h2>
<c-button v-for="buttonType of buttonTypes" :key="buttonType" :variant="buttonVariant" :type="buttonType" mx-1> <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 Button
</c-button> </c-button>
@ -11,19 +19,34 @@
:key="buttonType" :key="buttonType"
:variant="buttonVariant" :variant="buttonVariant"
:type="buttonType" :type="buttonType"
:size="buttonSize"
circle circle
mx-1 mx-1
> >
A A
</c-button> </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> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import _ from 'lodash'; import _ from 'lodash';
const buttonVariants = ['basic', 'text']; const buttonVariants = ['basic', 'text'] as const;
const buttonTypes = ['default', 'primary']; const buttonTypes = ['default', 'primary', 'warning'] as const;
const buttonSizes = ['small', 'medium', 'large'] as const;
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View file

@ -27,6 +27,21 @@ const createTheme = ({ style }: { style: 'light' | 'dark' }) => {
const theme = appThemes[style]; const theme = appThemes[style];
return { return {
size: {
small: {
width: '28px',
fontSize: '12px',
},
medium: {
width: '34px',
fontSize: '14px',
},
large: {
width: '40px',
fontSize: '16px',
},
},
basic: { basic: {
default: createState({ default: createState({
textColor: theme.text.baseColor, textColor: theme.text.baseColor,
@ -41,10 +56,10 @@ const createTheme = ({ style }: { style: 'light' | 'dark' }) => {
pressedBackground: darken(theme.primary.colorFaded, 30), pressedBackground: darken(theme.primary.colorFaded, 30),
}), }),
warning: createState({ warning: createState({
textColor: theme.text.baseColor, textColor: theme.warning.color,
backgroundColor: theme.warning.color, backgroundColor: theme.warning.colorFaded,
hoverBackground: theme.warning.colorHover, hoverBackground: lighten(theme.warning.colorFaded, 30),
pressedBackground: theme.warning.colorPressed, pressedBackground: darken(theme.warning.colorFaded, 30),
}), }),
}, },
text: { text: {
@ -61,10 +76,10 @@ const createTheme = ({ style }: { style: 'light' | 'dark' }) => {
pressedBackground: darken(theme.primary.colorFaded, 30), pressedBackground: darken(theme.primary.colorFaded, 30),
}), }),
warning: createState({ warning: createState({
textColor: theme.text.baseColor, textColor: darken(theme.warning.color, 20),
backgroundColor: theme.warning.color, backgroundColor: 'transparent',
hoverBackground: theme.warning.colorHover, hoverBackground: theme.warning.colorFaded,
pressedBackground: theme.warning.colorPressed, pressedBackground: darken(theme.warning.colorFaded, 30),
}), }),
}, },
}; };

View file

@ -18,13 +18,14 @@ import { useAppTheme } from '../theme/themes';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
type?: 'default' | 'primary'; type?: 'default' | 'primary' | 'warning';
variant?: 'basic' | 'text'; variant?: 'basic' | 'text';
disabled?: boolean; disabled?: boolean;
round?: boolean; round?: boolean;
circle?: boolean; circle?: boolean;
href?: string; href?: string;
to?: RouteLocationRaw; to?: RouteLocationRaw;
size?: 'small' | 'medium' | 'large';
}>(), }>(),
{ {
type: 'default', type: 'default',
@ -34,9 +35,10 @@ const props = withDefaults(
circle: false, circle: false,
href: undefined, href: undefined,
to: 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']); const emits = defineEmits(['click']);
@ -58,18 +60,20 @@ const tag = computed(() => {
return 'button'; return 'button';
}); });
const appTheme = useAppTheme(); const appTheme = useAppTheme();
const size = computed(() => theme.value.size[sizeName.value]);
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.c-button { .c-button {
line-height: 1; line-height: 1;
font-family: inherit; font-family: inherit;
font-size: inherit; font-size: v-bind('size.fontSize');
border: none; border: none;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
height: 34px; height: v-bind('size.width');
font-weight: 400; font-weight: 400;
color: v-bind('variantTheme.textColor'); color: v-bind('variantTheme.textColor');
padding: 0 14px; padding: 0 14px;
@ -89,8 +93,9 @@ const appTheme = useAppTheme();
} }
&.circle { &.circle {
border-radius: 40px; border-radius: v-bind('size.width');
width: 34px; width: v-bind('size.width');
padding: 0;
} }
&:not(.disabled) { &:not(.disabled) {

View 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>

View 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');
});
});

View 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',
},
},
});

View 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>

View file

@ -18,6 +18,8 @@
</div> </div>
<div flex-1 pl-4> <div flex-1 pl-4>
<h1>{{ componentName }}</h1>
<router-view /> <router-view />
</div> </div>
</div> </div>
@ -25,9 +27,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import _ from 'lodash';
import { demoRoutes } from './demo.routes'; import { demoRoutes } from './demo.routes';
const route = useRoute(); const route = useRoute();
const componentName = computed(() => _.startCase(String(route.name).replace(/^c-/, '')));
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View file

@ -6,8 +6,6 @@ export const demoRoutes = Object.keys(demoPages).map((path) => {
const [, , fileName] = path.split('/'); const [, , fileName] = path.split('/');
const name = fileName.split('.').shift(); const name = fileName.split('.').shift();
console.log(path);
return { return {
path: name, path: name,
name, name,

View file

@ -21,6 +21,7 @@ export const { themes: appThemes, useTheme: useAppTheme } = defineThemes({
color: '#f59e0b', color: '#f59e0b',
colorHover: '#f59e0b', colorHover: '#f59e0b',
colorPressed: '#f59e0b', colorPressed: '#f59e0b',
colorFaded: '#f59e0b2f',
}, },
success: { success: {
color: '#18a058', color: '#18a058',
@ -55,6 +56,7 @@ export const { themes: appThemes, useTheme: useAppTheme } = defineThemes({
color: '#f59e0b', color: '#f59e0b',
colorHover: '#f59e0b', colorHover: '#f59e0b',
colorPressed: '#f59e0b', colorPressed: '#f59e0b',
colorFaded: '#f59e0b2f',
}, },
success: { success: {
color: '#18a058', color: '#18a058',

View file

@ -1,16 +1,18 @@
import { useValidation } from '@/composable/validation'; import { useValidation } from '@/composable/validation';
import type { Ref } from 'vue'; 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) { function macAddressValidation(value: Ref) {
return useValidation({ return useValidation({
source: value, source: value,
rules: [ rules: macAddressValidationRules,
{
message: 'Invalid MAC address',
validator: (value) => value.trim().match(/^([0-9A-Fa-f]{2}[:-]){2,5}([0-9A-Fa-f]{2})$/),
},
],
}); });
} }
export { macAddressValidation }; export { macAddressValidation, macAddressValidationRules };

View file

@ -18,4 +18,14 @@ const shuffleArray = <T>(array: T[]): T[] => shuffleArrayMutate([...array]);
const shuffleString = (str: string, delimiter = ''): string => shuffleArrayMutate(str.split(delimiter)).join(delimiter); 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,
};

View file

@ -9,6 +9,6 @@
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
}, },
"types": ["naive-ui/volar"] "types": ["naive-ui/volar", "unplugin-icons/types/vue"]
} }
} }

View file

@ -10,6 +10,9 @@ import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite'; import Components from 'unplugin-vue-components/vite';
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'; import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
import Unocss from 'unocss/vite'; 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/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
@ -28,7 +31,7 @@ export default defineConfig({
enabled: true, enabled: true,
}, },
}), }),
Icons({ compiler: 'vue3' }),
vue({ vue({
include: [/\.vue$/, /\.md$/], include: [/\.vue$/, /\.md$/],
}), }),
@ -76,7 +79,7 @@ export default defineConfig({
dirs: ['src/'], dirs: ['src/'],
extensions: ['vue', 'md'], extensions: ['vue', 'md'],
include: [/\.vue$/, /\.vue\?vue/, /\.md$/], include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
resolvers: [NaiveUiResolver()], resolvers: [NaiveUiResolver(), IconsResolver({ prefix: 'icon' })],
}), }),
Unocss(), Unocss(),
], ],
@ -88,4 +91,7 @@ export default defineConfig({
define: { define: {
'import.meta.env.PACKAGE_VERSION': JSON.stringify(process.env.npm_package_version), 'import.meta.env.PACKAGE_VERSION': JSON.stringify(process.env.npm_package_version),
}, },
test: {
exclude: [...configDefaults.exclude, '**/*.e2e.spec.ts'],
},
}); });

View file

@ -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'],
},
});