Compare commits

..

1 Commits

Author SHA1 Message Date
Corentin Thomasset
a8c1231753 feat(new-tool): svg mesh gradient generator 2023-05-07 11:20:03 +02:00
62 changed files with 511 additions and 1475 deletions

View File

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

View File

@@ -2,26 +2,6 @@
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

72
auto-imports.d.ts vendored
View File

@@ -19,9 +19,7 @@ declare global {
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
@@ -41,6 +39,9 @@ declare global {
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const logicAnd: typeof import('@vueuse/core')['logicAnd']
const logicNot: typeof import('@vueuse/core')['logicNot']
const logicOr: typeof import('@vueuse/core')['logicOr']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
@@ -91,9 +92,8 @@ declare global {
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('@vueuse/core')['toRef']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('@vueuse/core')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
@@ -104,19 +104,6 @@ declare global {
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
@@ -127,8 +114,8 @@ declare global {
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClamp: typeof import('@vueuse/core')['useClamp']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCounter: typeof import('@vueuse/core')['useCounter']
@@ -202,18 +189,12 @@ declare global {
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const useParentElement: typeof import('@vueuse/core')['useParentElement']
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
@@ -227,17 +208,14 @@ declare global {
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
@@ -249,8 +227,6 @@ declare global {
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
@@ -271,10 +247,8 @@ declare global {
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchDeep: typeof import('@vueuse/core')['watchDeep']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
@@ -308,9 +282,7 @@ declare module 'vue' {
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
@@ -330,6 +302,9 @@ declare module 'vue' {
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly logicAnd: UnwrapRef<typeof import('@vueuse/core')['logicAnd']>
readonly logicNot: UnwrapRef<typeof import('@vueuse/core')['logicNot']>
readonly logicOr: UnwrapRef<typeof import('@vueuse/core')['logicOr']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
@@ -380,9 +355,8 @@ declare module 'vue' {
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('@vueuse/core')['toRef']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('@vueuse/core')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
@@ -393,19 +367,6 @@ declare module 'vue' {
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
@@ -416,8 +377,8 @@ declare module 'vue' {
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClamp: UnwrapRef<typeof import('@vueuse/core')['useClamp']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
@@ -491,18 +452,12 @@ declare module 'vue' {
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
@@ -516,17 +471,14 @@ declare module 'vue' {
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
@@ -538,8 +490,6 @@ declare module 'vue' {
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
@@ -560,10 +510,8 @@ declare module 'vue' {
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>

13
components.d.ts vendored
View File

@@ -26,15 +26,11 @@ 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']
@@ -56,12 +52,6 @@ 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']
@@ -74,7 +64,6 @@ declare module '@vue/runtime-core' {
JsonViewer: typeof import('./src/tools/json-viewer/json-viewer.vue')['default']
JwtParser: typeof import('./src/tools/jwt-parser/jwt-parser.vue')['default']
KeycodeInfo: typeof import('./src/tools/keycode-info/keycode-info.vue')['default']
ListConverter: typeof import('./src/tools/list-converter/list-converter.vue')['default']
LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default']
MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default']
MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default']
@@ -100,7 +89,6 @@ declare module '@vue/runtime-core' {
NEllipsis: typeof import('naive-ui')['NEllipsis']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid']
NH1: typeof import('naive-ui')['NH1']
@@ -144,6 +132,7 @@ declare module '@vue/runtime-core' {
SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default']
SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default']
SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default']
SvgMeshGradientGenerator: typeof import('./src/tools/svg-mesh-gradient-generator/svg-mesh-gradient-generator.vue')['default']
SvgPlaceholderGenerator: typeof import('./src/tools/svg-placeholder-generator/svg-placeholder-generator.vue')['default']
TemperatureConverter: typeof import('./src/tools/temperature-converter/temperature-converter.vue')['default']
TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default']

View File

@@ -1,6 +1,6 @@
{
"name": "it-tools",
"version": "2023.5.14-77f2efc",
"version": "2023.4.23-92bd835",
"description": "Collection of handy online tools for developers, with great UX. ",
"keywords": [
"productivity",
@@ -79,7 +79,6 @@
"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",
@@ -99,10 +98,8 @@
"@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",
@@ -119,7 +116,6 @@
"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,9 +135,6 @@ 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
@@ -195,18 +192,12 @@ 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)
@@ -255,9 +246,6 @@ 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)
@@ -1624,12 +1612,6 @@ 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
@@ -7579,35 +7561,6 @@ 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,5 +1,5 @@
<template>
<n-form-item :label="inputLabel" v-bind="validationAttrs as any">
<n-form-item :label="inputLabel" v-bind="validationAttrs">
<n-input
ref="inputElement"
v-model:value="input"
@@ -10,7 +10,7 @@
autocorrect="off"
autocapitalize="off"
spellcheck="false"
:input-props="{ 'data-test-id': 'input' } as any"
:input-props="{ 'data-test-id': 'input' }"
/>
</n-form-item>
<n-form-item :label="outputLabel">

View File

@@ -1,20 +1,21 @@
<template>
<c-input-text v-model:value="value">
<n-input v-model:value="value">
<template #suffix>
<n-tooltip trigger="hover">
<template #trigger>
<c-button circle variant="text" size="small" @click="onCopyClicked">
<icon-mdi-content-copy />
<c-button circle variant="text" @click="onCopyClicked">
<n-icon :component="ContentCopyFilled" />
</c-button>
</template>
{{ tooltipText }}
</n-tooltip>
</template>
</c-input-text>
</n-input>
</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 }>();
@@ -34,3 +35,9 @@ function onCopyClicked() {
}, 2000);
}
</script>
<style scoped>
::v-deep(.n-input-wrapper) {
padding-right: 5px;
}
</style>

View File

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

View File

@@ -20,7 +20,7 @@
</c-card>
<c-card title="Base64 to string">
<n-form-item label="Base64 string to decode" v-bind="b64Validation.attrs as any">
<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>

View File

@@ -1,15 +1,17 @@
<template>
<div>
<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"
/>
<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-card>
<n-statistic label="Authorization header:" class="header">

View File

@@ -1,20 +1,21 @@
<template>
<c-card title="Hash">
<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-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>
<n-space justify="center" mt-5>
<c-button @click="copy"> Copy hash </c-button>
</n-space>
@@ -23,10 +24,24 @@
<c-card title="Compare string with hash">
<n-form label-width="120">
<n-form-item label="Your string: " label-placement="left">
<c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text />
<n-input
v-model:value="compareString"
placeholder="Your string to compare..."
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
</n-form-item>
<n-form-item label="Your hash: " label-placement="left">
<c-input-text v-model:value="compareHash" placeholder="Your hahs to compare..." raw-text />
<n-input
v-model:value="compareHash"
placeholder="Your hahs to compare..."
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
</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,15 +1,11 @@
<template>
<n-scrollbar style="flex: 1" x-scrollable>
<n-space :wrap="false" style="flex: 1" justify="center" :size="12" mb-5>
<n-space :wrap="false" style="flex: 1" justify="center" :size="0" mb-5>
<div v-for="(suite, index) of suites" :key="index">
<c-card style="width: 294px">
<c-input-text
v-model:value="suite.title"
label-position="left"
label="Suite name"
placeholder="Suite name..."
clearable
/>
<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>
<n-divider></n-divider>
<n-form-item label="Suite values" :show-feedback="false">
@@ -37,7 +33,9 @@
<div style="flex: 0 0 100%">
<div style="max-width: 600px; margin: 0 auto">
<n-space justify="center">
<c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 />
<n-form-item label="Unit:" label-placement="left">
<n-input v-model:value="unit" placeholder="Unit (eg: ms)" />
</n-form-item>
<c-button
@click="

View File

@@ -16,8 +16,7 @@
:validation-status="entropyValidation.status"
>
<n-input-group>
<c-input-text v-model:value="entropy" placeholder="Your string..." />
<n-input v-model:value="entropy" placeholder="Your string..." />
<c-button @click="refreshEntropy">
<n-icon size="22">
<Refresh />
@@ -38,7 +37,15 @@
:validation-status="mnemonicValidation.status"
>
<n-input-group>
<c-input-text v-model:value="passphrase" placeholder="Your mnemonic..." raw-text />
<n-input
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">
<n-icon size="22" :component="Copy" />

View File

@@ -1,15 +1,9 @@
<template>
<c-card>
<n-form label-width="120" label-placement="left" :show-feedback="false">
<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-form-item label="Your string:">
<n-input v-model:value="input" />
</n-form-item>
<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" type="primary" @click="resume">Start</c-button>
<c-button v-else type="warning" @click="pause">Stop</c-button>
<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 @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" @update:value="(v: string) => onInputUpdated(v, 'name')" />
<input-copyable v-model:value="name" :on-input="(v: string) => onInputUpdated(v, 'name')" />
</n-form-item>
<n-form-item label="hex:">
<input-copyable v-model:value="hex" @update:value="(v: string) => onInputUpdated(v, 'hex')" />
<input-copyable v-model:value="hex" :on-input="(v: string) => onInputUpdated(v, 'hex')" />
</n-form-item>
<n-form-item label="rgb:">
<input-copyable v-model:value="rgb" @update:value="(v: string) => onInputUpdated(v, 'rgb')" />
<input-copyable v-model:value="rgb" :on-input="(v: string) => onInputUpdated(v, 'rgb')" />
</n-form-item>
<n-form-item label="hsl:">
<input-copyable v-model:value="hsl" @update:value="(v: string) => onInputUpdated(v, 'hsl')" />
<input-copyable v-model:value="hsl" :on-input="(v: string) => onInputUpdated(v, 'hsl')" />
</n-form-item>
<n-form-item label="hwb:">
<input-copyable v-model:value="hwb" @update:value="(v: string) => onInputUpdated(v, 'hwb')" />
<input-copyable v-model:value="hwb" :on-input="(v: string) => onInputUpdated(v, 'hwb')" />
</n-form-item>
<n-form-item label="lch:">
<input-copyable v-model:value="lch" @update:value="(v: string) => onInputUpdated(v, 'lch')" />
<input-copyable v-model:value="lch" :on-input="(v: string) => onInputUpdated(v, 'lch')" />
</n-form-item>
<n-form-item label="cmyk:">
<input-copyable v-model:value="cmyk" @update:value="(v: string) => onInputUpdated(v, 'cmyk')" />
<input-copyable v-model:value="cmyk" :on-input="(v: string) => onInputUpdated(v, 'cmyk')" />
</n-form-item>
</n-form>
</c-card>
@@ -54,19 +54,15 @@ const cmyk = ref('');
const lch = ref('');
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 !== '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 {
//
}
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();
}
onInputUpdated(hex.value, 'hex');

View File

@@ -1,15 +1,13 @@
<template>
<c-card>
<div mx-auto max-w-sm>
<c-input-text
v-model:value="cron"
size="large"
placeholder="* * * * *"
:validation-rules="cronValidationRules"
mb-3
/>
</div>
<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 class="cron-string">
{{ cronString }}
</div>
@@ -88,6 +86,7 @@
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) {
@@ -186,20 +185,30 @@ const cronString = computed(() => {
return ' ';
});
const cronValidationRules = [
{
validator: (value: string) => isCronValid(value),
message: 'This cron is invalid',
},
];
const cronValidation = useValidation({
source: cron,
rules: [
{
validator: (value) => isCronValid(value),
message: 'This cron is invalid',
},
],
});
</script>
<style lang="less" scoped>
::v-deep(input) {
font-size: 30px;
font-family: monospace;
padding: 5px;
.cron {
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,6 +1,6 @@
<template>
<div>
<n-form-item :show-label="false" v-bind="validation.attrs as any">
<n-form-item :show-label="false" v-bind="validation.attrs">
<n-input-group>
<n-input
v-model:value="inputDate"
@@ -8,7 +8,7 @@
:on-input="onDateInputChanged"
placeholder="Put you date string here..."
clearable
:input-props="{ 'data-test-id': 'date-time-converter-input' } as any"
:input-props="{ 'data-test-id': 'date-time-converter-input' }"
/>
<n-select
@@ -20,19 +20,16 @@
</n-input-group>
</n-form-item>
<n-divider style="margin-top: 0" />
<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 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>
</div>
</template>

View File

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

View File

@@ -1,7 +1,6 @@
import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as listConverter } from './list-converter';
import { tool as phoneParserAndFormatter } from './phone-parser-and-formatter';
import { tool as jsonDiff } from './json-diff';
import { tool as ipv4RangeExpander } from './ipv4-range-expander';
@@ -15,6 +14,7 @@ import { tool as userAgentParser } from './user-agent-parser';
import { tool as ipv4SubnetCalculator } from './ipv4-subnet-calculator';
import { tool as dockerRunToDockerComposeConverter } from './docker-run-to-docker-compose-converter';
import { tool as htmlWysiwygEditor } from './html-wysiwyg-editor';
import { tool as svgMeshGradientGenerator } from './svg-mesh-gradient-generator';
import { tool as rsaKeyPairGenerator } from './rsa-key-pair-generator';
import { tool as textToNatoAlphabet } from './text-to-nato-alphabet';
import { tool as slugifyString } from './slugify-string';
@@ -75,7 +75,6 @@ export const toolsByCategory: ToolCategory[] = [
textToNatoAlphabet,
yamlToJson,
jsonToYaml,
listConverter,
],
},
{
@@ -100,7 +99,7 @@ export const toolsByCategory: ToolCategory[] = [
},
{
name: 'Images',
components: [qrCodeGenerator, svgPlaceholderGenerator],
components: [qrCodeGenerator, svgPlaceholderGenerator, svgMeshGradientGenerator],
},
{
name: 'Development',

View File

@@ -22,54 +22,59 @@
<n-alert v-if="error" style="margin-top: 25px" type="error">{{ error }}</n-alert>
<n-divider />
<input-copyable
label="Binary (2)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })"
placeholder="Binary version will be here..."
/>
<input-copyable
label="Octal (8)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })"
placeholder="Octal version will be here..."
/>
<input-copyable
label="Decimal (10)"
v-bind="inputProps"
:value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })"
placeholder="Decimal version will be here..."
/>
<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"> 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>
<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>
<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>
<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>
<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...`"
/>
</div>
</n-input-group>
</c-card>
</div>
</template>
@@ -83,14 +88,6 @@ 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,20 +1,23 @@
<template>
<div>
<c-input-text v-model:value="rawIpAddress" label="The ipv4 address:" placeholder="The ipv4 address..." readonly />
<n-form-item label="An ipv4 address:" v-bind="validationAttrs">
<n-input v-model:value="rawIpAddress" placeholder="An ipv4 address..." />
</n-form-item>
<n-divider />
<n-divider style="margin-top: 0" mt-0 />
<input-copyable
<n-form-item
v-for="{ label, value } of convertedSections"
:key="label"
:label="label"
label-position="left"
label-width="100px"
label-align="right"
mb-2
:value="validationAttrs.validationStatus === 'error' ? '' : value"
placeholder="Set a correct ipv4 address"
/>
label-placement="left"
label-width="100"
>
<input-copyable
:value="validationAttrs.validationStatus === 'error' ? '' : value"
placeholder="Set a correct ipv4 address"
/>
</n-form-item>
</div>
</template>
@@ -30,7 +33,7 @@ const convertedSections = computed(() => {
return [
{
label: 'Decimal: ',
label: 'Decimal : ',
value: String(ipInDecimal),
},
{

View File

@@ -3,10 +3,10 @@
<n-space item-style="flex:1 1 0">
<div>
<n-space item-style="flex:1 1 0">
<n-form-item label="Start address" v-bind="startIpValidation.attrs as any">
<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 as any">
<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>

View File

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

View File

@@ -1,32 +1,30 @@
<template>
<div>
<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-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>
<c-input-text
v-model:value="macAddress"
placeholder="Type a MAC address"
clearable
label="MAC address:"
raw-text
my-8
:validation="addressValidation"
/>
<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>
<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 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>
</div>
</template>
@@ -61,7 +59,7 @@ const calculatedSections = computed(() => {
];
});
const addressValidation = macAddressValidation(macAddress);
const { attrs: validationAttrs } = macAddressValidation(macAddress);
</script>
<style lang="less" scoped></style>

View File

@@ -1,5 +1,5 @@
<template>
<n-form-item label="Your first json" v-bind="leftJsonValidation.attrs as any">
<n-form-item label="Your first json" v-bind="leftJsonValidation.attrs">
<n-input
v-model:value="rawLeftJson"
placeholder="Paste your first json here..."
@@ -9,10 +9,10 @@
autocorrect="off"
autocapitalize="off"
spellcheck="false"
:input-props="{ 'data-test-id': 'leftJson' } as any"
:input-props="{ 'data-test-id': 'leftJson' }"
/>
</n-form-item>
<n-form-item label="Your json to compare" v-bind="rightJsonValidation.attrs as any">
<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..."
@@ -22,7 +22,7 @@
autocorrect="off"
autocapitalize="off"
spellcheck="false"
:input-props="{ 'data-test-id': 'rightJson' } as any"
:input-props="{ 'data-test-id': 'rightJson' }"
/>
</n-form-item>

View File

@@ -1,13 +0,0 @@
import { List } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'List converter',
path: '/list-converter',
description:
'This tool can process column-based data and apply various changes (transpose, add prefix and suffix, reverse list, sort list, lowercase values, truncate values) to each row.',
keywords: ['list', 'converter', 'sort', 'reverse', 'prefix', 'suffix', 'lowercase', 'truncate'],
component: () => import('./list-converter.vue'),
icon: List,
createdAt: new Date('2023-05-07'),
});

View File

@@ -1,39 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('Tool - List converter', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/list-converter');
});
test('Has correct title', async ({ page }) => {
await expect(page).toHaveTitle('List converter - IT Tools');
});
test('Simple list should be converted with default settings', async ({ page }) => {
await page.getByTestId('input').fill(`1
2
3
4
5`);
const result = await page.getByTestId('area-content').innerText();
expect(result.trim()).toEqual('1, 2, 3, 4, 5');
});
test('Duplicates should be removed, list should be sorted and prefix and suffix list items', async ({ page }) => {
await page.getByTestId('input').fill(`1
2
2
4
4
3
5`);
await page.getByTestId('removeDuplicates').check();
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

@@ -1,76 +0,0 @@
import { expect, describe, it } from 'vitest';
import { convert } from './list-converter.models';
import type { ConvertOptions } from './list-converter.types';
describe('list-converter', () => {
describe('convert', () => {
it('should convert a given list', () => {
const options: ConvertOptions = {
separator: ', ',
trimItems: true,
removeDuplicates: true,
itemPrefix: '"',
itemSuffix: '"',
listPrefix: '',
listSuffix: '',
reverseList: false,
sortList: null,
lowerCase: false,
keepLineBreaks: false,
};
const input = `
1
2
3
3
4
`;
expect(convert(input, options)).toEqual('"1", "2", "3", "4"');
});
it('should return an empty value for an empty input', () => {
const options: ConvertOptions = {
separator: ', ',
trimItems: true,
removeDuplicates: true,
itemPrefix: '',
itemSuffix: '',
listPrefix: '',
listSuffix: '',
reverseList: false,
sortList: null,
lowerCase: false,
keepLineBreaks: false,
};
expect(convert('', options)).toEqual('');
});
it('should keep line breaks', () => {
const options: ConvertOptions = {
separator: '',
trimItems: true,
itemPrefix: '<li>',
itemSuffix: '</li>',
listPrefix: '<ul>',
listSuffix: '</ul>',
keepLineBreaks: true,
lowerCase: false,
removeDuplicates: false,
reverseList: false,
sortList: null,
};
const input = `
1
2
3
`;
const expected = `<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>`;
expect(convert(input, options)).toEqual(expected);
});
});
});

View File

@@ -1,27 +0,0 @@
import _ from 'lodash';
import { byOrder } from '@/utils/array';
import type { ConvertOptions } from './list-converter.types';
export { convert };
const whenever =
<T, R>(condition: boolean, fn: (value: T) => R) =>
(value: T) =>
condition ? fn(value) : value;
function convert(list: string, options: ConvertOptions): string {
const lineBreak = options.keepLineBreaks ? '\n' : '';
return _.chain(list)
.thru(whenever(options.lowerCase, (text) => text.toLowerCase()))
.split('\n')
.thru(whenever(options.removeDuplicates, _.uniq))
.thru(whenever(options.reverseList, _.reverse))
.thru(whenever(!_.isNull(options.sortList), (parts) => parts.sort(byOrder({ order: options.sortList }))))
.map(whenever(options.trimItems, _.trim))
.without('')
.map((p) => options.itemPrefix + p + options.itemSuffix)
.join(options.separator + lineBreak)
.thru((text) => [options.listPrefix, text, options.listSuffix].join(lineBreak))
.value();
}

View File

@@ -1,15 +0,0 @@
export type SortOrder = 'asc' | 'desc' | null;
export type ConvertOptions = {
lowerCase: boolean;
trimItems: boolean;
itemPrefix: string;
itemSuffix: string;
listPrefix: string;
listSuffix: string;
reverseList: boolean;
sortList: SortOrder;
removeDuplicates: boolean;
separator: string;
keepLineBreaks: boolean;
};

View File

@@ -1,123 +0,0 @@
<template>
<div style="flex: 0 0 100%">
<n-space item-style="flex: 1 1 0" style="margin: 0 auto; max-width: 600px" justify="center">
<c-card>
<div flex>
<div>
<n-form-item label="Trim list items" label-placement="left" label-width="150" :show-feedback="false" mb-2>
<n-switch v-model:value="conversionConfig.trimItems" />
</n-form-item>
<n-form-item label="Remove duplicates" label-placement="left" label-width="150" :show-feedback="false" mb-2>
<n-switch v-model:value="conversionConfig.removeDuplicates" data-test-id="removeDuplicates" />
</n-form-item>
<n-form-item
label="Convert to lowercase"
label-placement="left"
label-width="150"
:show-feedback="false"
mb-2
>
<n-switch v-model:value="conversionConfig.lowerCase" />
</n-form-item>
<n-form-item label="Keep line breaks" label-placement="left" label-width="150" :show-feedback="false" mb-2>
<n-switch v-model:value="conversionConfig.keepLineBreaks" />
</n-form-item>
</div>
<div flex-1>
<n-form-item label="Sort list" label-placement="left" label-width="120" :show-feedback="false" mb-2>
<n-select
v-model:value="conversionConfig.sortList"
:options="sortOrderOptions"
clearable
w-full
:disabled="conversionConfig.reverseList"
data-test-id="sortList"
placeholder="Sort alphabetically"
/>
</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>
<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>
<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>
</c-card>
</n-space>
</div>
<format-transformer
input-label="Your input data"
input-placeholder="Paste your input data here..."
output-label="Your transformed data"
:transformer="transformer"
/>
</template>
<script setup lang="ts">
import { useStorage } from '@vueuse/core';
import { convert } from './list-converter.models';
import type { ConvertOptions } from './list-converter.types';
const sortOrderOptions = [
{
label: 'Sort ascending',
value: 'asc',
disabled: false,
},
{
label: 'Sort descending',
value: 'desc',
disabled: false,
},
];
const conversionConfig = useStorage<ConvertOptions>('list-converter:conversionConfig', {
lowerCase: false,
trimItems: true,
removeDuplicates: true,
keepLineBreaks: false,
itemPrefix: '',
itemSuffix: '',
listPrefix: '',
listSuffix: '',
reverseList: false,
sortList: null,
separator: ', ',
});
const transformer = (value: string) => {
return convert(value, conversionConfig.value);
};
</script>
<style lang="less" scoped></style>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<n-form-item label="MAC address:" v-bind="validationAttrs as any">
<n-form-item label="MAC address:" v-bind="validationAttrs">
<n-input
v-model:value="macAddress"
size="large"

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>
<c-input-text v-if="type === 'input'" v-model:value="metadata[key]" :placeholder="placeholder" clearable />
<n-input v-if="type === 'input'" v-model:value="metadata[key]" :placeholder="placeholder" />
<n-dynamic-input
v-else-if="type === 'input-multiple'"
v-model:value="metadata[key]"

View File

@@ -1,23 +1,19 @@
<template>
<div style="max-width: 350px">
<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>
<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>
<div>
<token-display :tokens="tokens" style="margin-top: 2px" />
@@ -31,52 +27,49 @@
</n-space>
</div>
<div style="max-width: 350px">
<input-copyable
label="Secret in hexadecimal"
:value="base32toHex(secret)"
readonly
placeholder="Secret in hex will be displayed here"
mb-5
/>
<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="Epoch"
:value="Math.floor(now / 1000).toString()"
readonly
mb-5
placeholder="Epoch in sec will be displayed here"
/>
<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>
<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:"
/>
<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>
</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';
@@ -113,16 +106,19 @@ const { qrcode } = useQRCode({
options: { width: 210 },
});
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 !== '',
},
];
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 !== '',
},
],
});
</script>
<style lang="less" scoped>

View File

@@ -1,4 +1,4 @@
import type { CountryCode, NumberType } from 'libphonenumber-js/types';
import type { NumberType } from 'libphonenumber-js/types';
import lookup from 'country-code-lookup';
export { formatTypeToHumanReadable, getFullCountryName, getDefaultCountryCode };
@@ -32,10 +32,10 @@ function getFullCountryName(countryCode: string | undefined) {
function getDefaultCountryCode({
locale = window.navigator.language,
defaultCode = 'FR',
}: { locale?: string; defaultCode?: CountryCode } = {}): CountryCode {
}: { locale?: string; defaultCode?: string } = {}): string {
const countryCode = locale.split('-')[1]?.toUpperCase();
if (!countryCode) return defaultCode;
return (lookup.byIso(countryCode)?.iso2 ?? defaultCode) as CountryCode;
return lookup.byIso(countryCode)?.iso2 ?? defaultCode;
}

View File

@@ -3,14 +3,9 @@
<n-form-item label="Default country code:">
<n-select v-model:value="defaultCountryCode" :options="countriesOptions" />
</n-form-item>
<c-input-text
v-model:value="rawPhone"
placeholder="Enter a phone number"
label="Phone number:"
:validation="validation"
mb-5
/>
<n-form-item label="Phone number:" v-bind="validation.attrs">
<n-input v-model:value="rawPhone" placeholder="Enter a phone number" />
</n-form-item>
<n-table v-if="parsedDetails">
<tbody>
@@ -55,7 +50,7 @@ const validation = useValidation({
const parsedDetails = computed(() => {
if (!validation.isValid) return undefined;
const parsed = withDefaultOnError(() => parsePhoneNumber(rawPhone.value, defaultCountryCode.value), undefined);
const parsed = withDefaultOnError(() => parsePhoneNumber(rawPhone.value, 'FR'), undefined);
if (!parsed) return undefined;

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

View File

@@ -0,0 +1,12 @@
import { ArrowsShuffle } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Svg mesh gradient generator',
path: '/svg-mesh-gradient-generator',
description: '',
keywords: ['svg', 'mesh', 'gradient', 'generator'],
component: () => import('./svg-mesh-gradient-generator.vue'),
icon: ArrowsShuffle,
createdAt: new Date('2023-05-05'),
});

View File

@@ -0,0 +1,6 @@
import { expect, describe, it } from 'vitest';
// import { } from './svg-mesh-gradient-generator.service';
//
// describe('svg-mesh-gradient-generator', () => {
//
// })

View File

@@ -0,0 +1,17 @@
// A function that generates a blurry mesh gradient background image in a canvas from multiple colors
export function generateMeshGradient(colors: string[], canvas: HTMLCanvasElement) {
const ctx = canvas.getContext('2d')!;
const { width, height } = canvas;
const gradient = ctx.createLinearGradient(0, 0, width, height);
colors.forEach((color, index) => {
gradient.addColorStop(index / (colors.length - 1), color);
});
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
const meshGradient = ctx.createLinearGradient(0, 0, width, height);
meshGradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
meshGradient.addColorStop(0.5, 'rgba(0, 0, 0, 0.1)');
meshGradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = meshGradient;
ctx.fillRect(0, 0, width, height);
}

View File

@@ -0,0 +1,49 @@
<template>
<svg viewBox="0 0 3000 2000" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient
v-for="{ id, cx, cy, fx, fy, color } of gradients"
:id="id"
:key="id"
:cx="cx"
:cy="cy"
r="100%"
:fx="fx"
:fy="fy"
gradientUnits="objectBoundingBox"
>
<stop offset="0" :stop-color="color" stop-opacity="1"></stop>
<stop offset="0.5" :stop-color="color + '00'" stop-opacity="0"></stop>
</radialGradient>
</defs>
<rect v-for="{ id, fill } of gradients" :key="id" x="0" y="0" width="100%" height="100%" :fill="fill"></rect>
</svg>
</template>
<script setup lang="ts">
import _ from 'lodash';
import { ref, computed } from 'vue';
const randomPercent = () => `${Math.random() * 100}%`;
const randomColor = () => '#' + (((1 << 24) * Math.random()) | 0).toString(16).padStart(6, '0');
const quantity = ref(5);
const gradients = computed(() =>
Array.from({ length: quantity.value }, () => {
const id = _.uniqueId('id-');
return {
id,
fill: `url(#${id})`,
cx: randomPercent(),
cy: randomPercent(),
fx: randomPercent(),
fy: randomPercent(),
color: randomColor(),
};
}),
);
</script>
<style lang="less" scoped></style>

View File

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

View File

@@ -1,12 +1,8 @@
<template>
<div>
<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-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>
<n-space v-if="natoText" vertical>
<n-text>Your text in NATO phonetic alphabet</n-text>

View File

@@ -1,59 +1,51 @@
<template>
<c-card>
<c-input-text
v-model:value="urlToParse"
label="Your url to parse:"
placeholder="Your url to parse..."
raw-text
:validation-rules="urlValidationRules"
/>
<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>
<n-divider />
<n-divider style="margin-top: 0" />
<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-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>
<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>
<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>
</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 urlValidationRules = [
{
validator: (value: string) => isNotThrowing(() => new URL(value)),
message: 'Invalid url',
},
];
const validation = useValidation({
source: urlToParse,
rules: [
{
validator: (value) => isNotThrowing(() => new URL(value)),
message: 'Invalid url',
},
],
});
const properties: { title: string; key: keyof URL }[] = [
{ title: 'Protocol', key: 'protocol' },

View File

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

View File

@@ -27,21 +27,6 @@ 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,
@@ -56,10 +41,10 @@ const createTheme = ({ style }: { style: 'light' | 'dark' }) => {
pressedBackground: darken(theme.primary.colorFaded, 30),
}),
warning: createState({
textColor: theme.warning.color,
backgroundColor: theme.warning.colorFaded,
hoverBackground: lighten(theme.warning.colorFaded, 30),
pressedBackground: darken(theme.warning.colorFaded, 30),
textColor: theme.text.baseColor,
backgroundColor: theme.warning.color,
hoverBackground: theme.warning.colorHover,
pressedBackground: theme.warning.colorPressed,
}),
},
text: {
@@ -76,10 +61,10 @@ const createTheme = ({ style }: { style: 'light' | 'dark' }) => {
pressedBackground: darken(theme.primary.colorFaded, 30),
}),
warning: createState({
textColor: darken(theme.warning.color, 20),
backgroundColor: 'transparent',
hoverBackground: theme.warning.colorFaded,
pressedBackground: darken(theme.warning.colorFaded, 30),
textColor: theme.text.baseColor,
backgroundColor: theme.warning.color,
hoverBackground: theme.warning.colorHover,
pressedBackground: theme.warning.colorPressed,
}),
},
};

View File

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

View File

@@ -1,76 +0,0 @@
<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

@@ -1,160 +0,0 @@
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

@@ -1,20 +0,0 @@
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

@@ -1,296 +0,0 @@
<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>();
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,8 +18,6 @@
</div>
<div flex-1 pl-4>
<h1>{{ componentName }}</h1>
<router-view />
</div>
</div>
@@ -27,12 +25,9 @@
</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,6 +6,8 @@ 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,7 +21,6 @@ export const { themes: appThemes, useTheme: useAppTheme } = defineThemes({
color: '#f59e0b',
colorHover: '#f59e0b',
colorPressed: '#f59e0b',
colorFaded: '#f59e0b2f',
},
success: {
color: '#18a058',
@@ -56,7 +55,6 @@ export const { themes: appThemes, useTheme: useAppTheme } = defineThemes({
color: '#f59e0b',
colorHover: '#f59e0b',
colorPressed: '#f59e0b',
colorFaded: '#f59e0b2f',
},
success: {
color: '#18a058',

View File

@@ -1,7 +0,0 @@
export { byOrder };
function byOrder({ order }: { order: 'asc' | 'desc' | null | undefined }) {
return (a: string, b: string) => {
return order === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
};
}

View File

@@ -18,14 +18,4 @@ const shuffleArray = <T>(array: T[]): T[] => shuffleArrayMutate([...array]);
const shuffleString = (str: string, delimiter = ''): string => shuffleArrayMutate(str.split(delimiter)).join(delimiter);
const generateRandomId = () => `id-${random().toString(36).substring(2, 12)}`;
export {
randFromArray,
randIntFromInterval,
random,
shuffleArray,
shuffleArrayMutate,
shuffleString,
generateRandomId,
};
export { randFromArray, randIntFromInterval, random, shuffleArray, shuffleArrayMutate, shuffleString };

View File

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

View File

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

13
vitest.config.ts Normal file
View File

@@ -0,0 +1,13 @@
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'],
},
});