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
---
**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.

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.
## 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
View file

@ -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']

View file

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

@ -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'}

View file

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

View file

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

View file

@ -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';

View file

@ -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%;
}

View file

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

View file

@ -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">

View file

@ -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 }">

View file

@ -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="

View file

@ -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" />

View file

@ -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 />

View file

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

View file

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

View file

@ -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 {

View file

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

View file

@ -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"

View file

@ -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);

View file

@ -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),
},
{

View file

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

View file

@ -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;

View file

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

View file

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

View file

@ -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'");

View file

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

View file

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

View file

@ -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]"

View file

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

View file

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

View file

@ -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: [
{

View file

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

View file

@ -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" />

View file

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

View file

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

View file

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

View file

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

View file

@ -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) {

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

View file

@ -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,

View file

@ -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',

View file

@ -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 };

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);
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": {
"@/*": ["./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 { 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'],
},
});

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