mirror of
				https://github.com/CorentinTh/it-tools.git
				synced 2025-11-04 05:53:25 +00:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
			svg-mesh-g
			...
			v2023.5.14
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					b3b6b7c46b | ||
| 
						 | 
					141c12455e | ||
| 
						 | 
					77f2efc0b9 | ||
| 
						 | 
					aad8d84e13 | ||
| 
						 | 
					401f13f7e3 | ||
| 
						 | 
					edae4c6915 | ||
| 
						 | 
					a43c546e34 | ||
| 
						 | 
					83a7b3bae9 | 
							
								
								
									
										4
									
								
								.github/ISSUE_TEMPLATE/new-tool-request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/ISSUE_TEMPLATE/new-tool-request.md
									
									
									
									
										vendored
									
									
								
							@@ -6,8 +6,8 @@ labels: new tool
 | 
			
		||||
assignees: CorentinTh
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Which tool is impacted?**
 | 
			
		||||
Example: the token generator
 | 
			
		||||
**What tool do you want?**
 | 
			
		||||
Example: a token generator
 | 
			
		||||
 | 
			
		||||
**Describe the solution you'd like**
 | 
			
		||||
A clear and concise description of what you want to happen.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -2,6 +2,26 @@
 | 
			
		||||
 | 
			
		||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
 | 
			
		||||
 | 
			
		||||
## Version 2023.05.14-77f2efc
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
- **list-converter**: a small converter who deals with column based data and do some stuff with it (#387) (83a7b3b)
 | 
			
		||||
- **new tool**: phone parser and normalizer (ce3150c)
 | 
			
		||||
 | 
			
		||||
### Bug fixes
 | 
			
		||||
- **phone-parser**: use default country code (a43c546)
 | 
			
		||||
- **home**: prevent weird blue border on card (3f6c8f0)
 | 
			
		||||
 | 
			
		||||
### Refactoring
 | 
			
		||||
- **ui**: replaced some n-input with c-input-text (77f2efc)
 | 
			
		||||
 | 
			
		||||
### Chores
 | 
			
		||||
- **issues**: updated new tool request issue template (edae4c6)
 | 
			
		||||
 | 
			
		||||
### Ui-lib
 | 
			
		||||
- **new-component**: added text input component in the c-lib (aad8d84)
 | 
			
		||||
- **button**: size variants (401f13f)
 | 
			
		||||
 | 
			
		||||
## Version 2023.04.23-92bd835
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										72
									
								
								auto-imports.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										72
									
								
								auto-imports.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -19,7 +19,9 @@ declare global {
 | 
			
		||||
  const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
 | 
			
		||||
  const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
 | 
			
		||||
  const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
 | 
			
		||||
  const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
 | 
			
		||||
  const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
 | 
			
		||||
  const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
 | 
			
		||||
  const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
 | 
			
		||||
  const customRef: typeof import('vue')['customRef']
 | 
			
		||||
  const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
 | 
			
		||||
@@ -39,9 +41,6 @@ declare global {
 | 
			
		||||
  const isReactive: typeof import('vue')['isReactive']
 | 
			
		||||
  const isReadonly: typeof import('vue')['isReadonly']
 | 
			
		||||
  const isRef: typeof import('vue')['isRef']
 | 
			
		||||
  const logicAnd: typeof import('@vueuse/core')['logicAnd']
 | 
			
		||||
  const logicNot: typeof import('@vueuse/core')['logicNot']
 | 
			
		||||
  const logicOr: typeof import('@vueuse/core')['logicOr']
 | 
			
		||||
  const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
 | 
			
		||||
  const markRaw: typeof import('vue')['markRaw']
 | 
			
		||||
  const nextTick: typeof import('vue')['nextTick']
 | 
			
		||||
@@ -92,8 +91,9 @@ declare global {
 | 
			
		||||
  const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
 | 
			
		||||
  const toRaw: typeof import('vue')['toRaw']
 | 
			
		||||
  const toReactive: typeof import('@vueuse/core')['toReactive']
 | 
			
		||||
  const toRef: typeof import('vue')['toRef']
 | 
			
		||||
  const toRef: typeof import('@vueuse/core')['toRef']
 | 
			
		||||
  const toRefs: typeof import('vue')['toRefs']
 | 
			
		||||
  const toValue: typeof import('@vueuse/core')['toValue']
 | 
			
		||||
  const triggerRef: typeof import('vue')['triggerRef']
 | 
			
		||||
  const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
 | 
			
		||||
  const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
 | 
			
		||||
@@ -104,6 +104,19 @@ declare global {
 | 
			
		||||
  const unrefElement: typeof import('@vueuse/core')['unrefElement']
 | 
			
		||||
  const until: typeof import('@vueuse/core')['until']
 | 
			
		||||
  const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
 | 
			
		||||
  const useAnimate: typeof import('@vueuse/core')['useAnimate']
 | 
			
		||||
  const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
 | 
			
		||||
  const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
 | 
			
		||||
  const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
 | 
			
		||||
  const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
 | 
			
		||||
  const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
 | 
			
		||||
  const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
 | 
			
		||||
  const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
 | 
			
		||||
  const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
 | 
			
		||||
  const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
 | 
			
		||||
  const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
 | 
			
		||||
  const useArraySome: typeof import('@vueuse/core')['useArraySome']
 | 
			
		||||
  const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
 | 
			
		||||
  const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
 | 
			
		||||
  const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
 | 
			
		||||
  const useAttrs: typeof import('vue')['useAttrs']
 | 
			
		||||
@@ -114,8 +127,8 @@ declare global {
 | 
			
		||||
  const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
 | 
			
		||||
  const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
 | 
			
		||||
  const useCached: typeof import('@vueuse/core')['useCached']
 | 
			
		||||
  const useClamp: typeof import('@vueuse/core')['useClamp']
 | 
			
		||||
  const useClipboard: typeof import('@vueuse/core')['useClipboard']
 | 
			
		||||
  const useCloned: typeof import('@vueuse/core')['useCloned']
 | 
			
		||||
  const useColorMode: typeof import('@vueuse/core')['useColorMode']
 | 
			
		||||
  const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
 | 
			
		||||
  const useCounter: typeof import('@vueuse/core')['useCounter']
 | 
			
		||||
@@ -189,12 +202,18 @@ declare global {
 | 
			
		||||
  const useOnline: typeof import('@vueuse/core')['useOnline']
 | 
			
		||||
  const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
 | 
			
		||||
  const useParallax: typeof import('@vueuse/core')['useParallax']
 | 
			
		||||
  const useParentElement: typeof import('@vueuse/core')['useParentElement']
 | 
			
		||||
  const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
 | 
			
		||||
  const usePermission: typeof import('@vueuse/core')['usePermission']
 | 
			
		||||
  const usePointer: typeof import('@vueuse/core')['usePointer']
 | 
			
		||||
  const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
 | 
			
		||||
  const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
 | 
			
		||||
  const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
 | 
			
		||||
  const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
 | 
			
		||||
  const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
 | 
			
		||||
  const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
 | 
			
		||||
  const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
 | 
			
		||||
  const usePrevious: typeof import('@vueuse/core')['usePrevious']
 | 
			
		||||
  const useRafFn: typeof import('@vueuse/core')['useRafFn']
 | 
			
		||||
  const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
 | 
			
		||||
  const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
 | 
			
		||||
@@ -208,14 +227,17 @@ declare global {
 | 
			
		||||
  const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
 | 
			
		||||
  const useShare: typeof import('@vueuse/core')['useShare']
 | 
			
		||||
  const useSlots: typeof import('vue')['useSlots']
 | 
			
		||||
  const useSorted: typeof import('@vueuse/core')['useSorted']
 | 
			
		||||
  const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
 | 
			
		||||
  const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
 | 
			
		||||
  const useStepper: typeof import('@vueuse/core')['useStepper']
 | 
			
		||||
  const useStorage: typeof import('@vueuse/core')['useStorage']
 | 
			
		||||
  const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
 | 
			
		||||
  const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
 | 
			
		||||
  const useSupported: typeof import('@vueuse/core')['useSupported']
 | 
			
		||||
  const useSwipe: typeof import('@vueuse/core')['useSwipe']
 | 
			
		||||
  const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
 | 
			
		||||
  const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
 | 
			
		||||
  const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
 | 
			
		||||
  const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
 | 
			
		||||
  const useThrottle: typeof import('@vueuse/core')['useThrottle']
 | 
			
		||||
@@ -227,6 +249,8 @@ declare global {
 | 
			
		||||
  const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
 | 
			
		||||
  const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
 | 
			
		||||
  const useTitle: typeof import('@vueuse/core')['useTitle']
 | 
			
		||||
  const useToNumber: typeof import('@vueuse/core')['useToNumber']
 | 
			
		||||
  const useToString: typeof import('@vueuse/core')['useToString']
 | 
			
		||||
  const useToggle: typeof import('@vueuse/core')['useToggle']
 | 
			
		||||
  const useTransition: typeof import('@vueuse/core')['useTransition']
 | 
			
		||||
  const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
 | 
			
		||||
@@ -247,8 +271,10 @@ declare global {
 | 
			
		||||
  const watchArray: typeof import('@vueuse/core')['watchArray']
 | 
			
		||||
  const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
 | 
			
		||||
  const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
 | 
			
		||||
  const watchDeep: typeof import('@vueuse/core')['watchDeep']
 | 
			
		||||
  const watchEffect: typeof import('vue')['watchEffect']
 | 
			
		||||
  const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
 | 
			
		||||
  const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
 | 
			
		||||
  const watchOnce: typeof import('@vueuse/core')['watchOnce']
 | 
			
		||||
  const watchPausable: typeof import('@vueuse/core')['watchPausable']
 | 
			
		||||
  const watchPostEffect: typeof import('vue')['watchPostEffect']
 | 
			
		||||
@@ -282,7 +308,9 @@ declare module 'vue' {
 | 
			
		||||
    readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
 | 
			
		||||
    readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
 | 
			
		||||
    readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
 | 
			
		||||
    readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
 | 
			
		||||
    readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
 | 
			
		||||
    readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
 | 
			
		||||
    readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
 | 
			
		||||
    readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
 | 
			
		||||
    readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
 | 
			
		||||
@@ -302,9 +330,6 @@ declare module 'vue' {
 | 
			
		||||
    readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
 | 
			
		||||
    readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
 | 
			
		||||
    readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
 | 
			
		||||
    readonly logicAnd: UnwrapRef<typeof import('@vueuse/core')['logicAnd']>
 | 
			
		||||
    readonly logicNot: UnwrapRef<typeof import('@vueuse/core')['logicNot']>
 | 
			
		||||
    readonly logicOr: UnwrapRef<typeof import('@vueuse/core')['logicOr']>
 | 
			
		||||
    readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
 | 
			
		||||
    readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
 | 
			
		||||
    readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
 | 
			
		||||
@@ -355,8 +380,9 @@ declare module 'vue' {
 | 
			
		||||
    readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
 | 
			
		||||
    readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
 | 
			
		||||
    readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
 | 
			
		||||
    readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
 | 
			
		||||
    readonly toRef: UnwrapRef<typeof import('@vueuse/core')['toRef']>
 | 
			
		||||
    readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
 | 
			
		||||
    readonly toValue: UnwrapRef<typeof import('@vueuse/core')['toValue']>
 | 
			
		||||
    readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
 | 
			
		||||
    readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
 | 
			
		||||
    readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
 | 
			
		||||
@@ -367,6 +393,19 @@ declare module 'vue' {
 | 
			
		||||
    readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
 | 
			
		||||
    readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
 | 
			
		||||
    readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
 | 
			
		||||
    readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
 | 
			
		||||
    readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
 | 
			
		||||
    readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
 | 
			
		||||
    readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
 | 
			
		||||
    readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
 | 
			
		||||
    readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
 | 
			
		||||
    readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
 | 
			
		||||
    readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
 | 
			
		||||
    readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
 | 
			
		||||
    readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
 | 
			
		||||
    readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
 | 
			
		||||
    readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
 | 
			
		||||
    readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
 | 
			
		||||
    readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
 | 
			
		||||
    readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
 | 
			
		||||
    readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
 | 
			
		||||
@@ -377,8 +416,8 @@ declare module 'vue' {
 | 
			
		||||
    readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
 | 
			
		||||
    readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
 | 
			
		||||
    readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
 | 
			
		||||
    readonly useClamp: UnwrapRef<typeof import('@vueuse/core')['useClamp']>
 | 
			
		||||
    readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
 | 
			
		||||
    readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
 | 
			
		||||
    readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
 | 
			
		||||
    readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
 | 
			
		||||
    readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
 | 
			
		||||
@@ -452,12 +491,18 @@ declare module 'vue' {
 | 
			
		||||
    readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
 | 
			
		||||
    readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
 | 
			
		||||
    readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
 | 
			
		||||
    readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
 | 
			
		||||
    readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
 | 
			
		||||
    readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
 | 
			
		||||
    readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
 | 
			
		||||
    readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
 | 
			
		||||
    readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
 | 
			
		||||
    readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
 | 
			
		||||
    readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
 | 
			
		||||
    readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
 | 
			
		||||
    readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
 | 
			
		||||
    readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
 | 
			
		||||
    readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
 | 
			
		||||
    readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
 | 
			
		||||
    readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
 | 
			
		||||
    readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
 | 
			
		||||
@@ -471,14 +516,17 @@ declare module 'vue' {
 | 
			
		||||
    readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
 | 
			
		||||
    readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
 | 
			
		||||
    readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
 | 
			
		||||
    readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
 | 
			
		||||
    readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
 | 
			
		||||
    readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
 | 
			
		||||
    readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
 | 
			
		||||
    readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
 | 
			
		||||
    readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
 | 
			
		||||
    readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
 | 
			
		||||
    readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
 | 
			
		||||
    readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
 | 
			
		||||
    readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
 | 
			
		||||
    readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
 | 
			
		||||
    readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
 | 
			
		||||
    readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
 | 
			
		||||
    readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
 | 
			
		||||
@@ -490,6 +538,8 @@ declare module 'vue' {
 | 
			
		||||
    readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
 | 
			
		||||
    readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
 | 
			
		||||
    readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
 | 
			
		||||
    readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
 | 
			
		||||
    readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
 | 
			
		||||
    readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
 | 
			
		||||
    readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
 | 
			
		||||
    readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
 | 
			
		||||
@@ -510,8 +560,10 @@ declare module 'vue' {
 | 
			
		||||
    readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
 | 
			
		||||
    readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
 | 
			
		||||
    readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
 | 
			
		||||
    readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
 | 
			
		||||
    readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
 | 
			
		||||
    readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
 | 
			
		||||
    readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
 | 
			
		||||
    readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
 | 
			
		||||
    readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
 | 
			
		||||
    readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -26,11 +26,15 @@ declare module '@vue/runtime-core' {
 | 
			
		||||
    'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default']
 | 
			
		||||
    ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default']
 | 
			
		||||
    Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default']
 | 
			
		||||
    CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default']
 | 
			
		||||
    'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default']
 | 
			
		||||
    'CInputText.theme': typeof import('./src/ui/c-input-text/c-input-text.theme.vue')['default']
 | 
			
		||||
    CLink: typeof import('./src/ui/c-link/c-link.vue')['default']
 | 
			
		||||
    'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default']
 | 
			
		||||
    CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default']
 | 
			
		||||
    ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default']
 | 
			
		||||
    ColoredCard: typeof import('./src/components/ColoredCard.vue')['default']
 | 
			
		||||
    copy: typeof import('./src/ui/c-input-text/c-input-text copy.vue')['default']
 | 
			
		||||
    CopyableIpLike: typeof import('./src/tools/ipv4-subnet-calculator/copyable-ip-like.vue')['default']
 | 
			
		||||
    CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default']
 | 
			
		||||
    DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default']
 | 
			
		||||
@@ -52,6 +56,12 @@ declare module '@vue/runtime-core' {
 | 
			
		||||
    HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default']
 | 
			
		||||
    HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default']
 | 
			
		||||
    HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
 | 
			
		||||
    IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
 | 
			
		||||
    IconMdiClose: typeof import('~icons/mdi/close')['default']
 | 
			
		||||
    IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
 | 
			
		||||
    IconMdiEye: typeof import('~icons/mdi/eye')['default']
 | 
			
		||||
    IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
 | 
			
		||||
    IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
 | 
			
		||||
    InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
 | 
			
		||||
    IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
 | 
			
		||||
    Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default']
 | 
			
		||||
@@ -64,6 +74,7 @@ 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']
 | 
			
		||||
@@ -89,6 +100,7 @@ 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']
 | 
			
		||||
@@ -132,7 +144,6 @@ 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']
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "it-tools",
 | 
			
		||||
  "version": "2023.4.23-92bd835",
 | 
			
		||||
  "version": "2023.5.14-77f2efc",
 | 
			
		||||
  "description": "Collection of handy online tools for developers, with great UX. ",
 | 
			
		||||
  "keywords": [
 | 
			
		||||
    "productivity",
 | 
			
		||||
@@ -79,6 +79,7 @@
 | 
			
		||||
    "yaml": "^2.2.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@iconify-json/mdi": "^1.1.50",
 | 
			
		||||
    "@playwright/test": "^1.32.3",
 | 
			
		||||
    "@rushstack/eslint-patch": "^1.2.0",
 | 
			
		||||
    "@types/bcryptjs": "^2.4.2",
 | 
			
		||||
@@ -98,8 +99,10 @@
 | 
			
		||||
    "@unocss/eslint-config": "^0.50.8",
 | 
			
		||||
    "@vitejs/plugin-vue": "^2.3.4",
 | 
			
		||||
    "@vitejs/plugin-vue-jsx": "^1.3.10",
 | 
			
		||||
    "@vue/compiler-sfc": "^3.2.47",
 | 
			
		||||
    "@vue/eslint-config-prettier": "^7.1.0",
 | 
			
		||||
    "@vue/eslint-config-typescript": "^10.0.0",
 | 
			
		||||
    "@vue/runtime-core": "^3.2.47",
 | 
			
		||||
    "@vue/test-utils": "^2.3.2",
 | 
			
		||||
    "@vue/tsconfig": "^0.1.3",
 | 
			
		||||
    "c8": "^7.13.0",
 | 
			
		||||
@@ -116,6 +119,7 @@
 | 
			
		||||
    "typescript": "~4.5.5",
 | 
			
		||||
    "unocss": "^0.50.8",
 | 
			
		||||
    "unplugin-auto-import": "^0.15.2",
 | 
			
		||||
    "unplugin-icons": "^0.16.1",
 | 
			
		||||
    "unplugin-vue-components": "^0.24.1",
 | 
			
		||||
    "vite": "^2.9.15",
 | 
			
		||||
    "vite-plugin-md": "^0.12.4",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										47
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										47
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -135,6 +135,9 @@ dependencies:
 | 
			
		||||
    version: 2.2.1
 | 
			
		||||
 | 
			
		||||
devDependencies:
 | 
			
		||||
  '@iconify-json/mdi':
 | 
			
		||||
    specifier: ^1.1.50
 | 
			
		||||
    version: 1.1.50
 | 
			
		||||
  '@playwright/test':
 | 
			
		||||
    specifier: ^1.32.3
 | 
			
		||||
    version: 1.32.3
 | 
			
		||||
@@ -192,12 +195,18 @@ devDependencies:
 | 
			
		||||
  '@vitejs/plugin-vue-jsx':
 | 
			
		||||
    specifier: ^1.3.10
 | 
			
		||||
    version: 1.3.10
 | 
			
		||||
  '@vue/compiler-sfc':
 | 
			
		||||
    specifier: ^3.2.47
 | 
			
		||||
    version: 3.2.47
 | 
			
		||||
  '@vue/eslint-config-prettier':
 | 
			
		||||
    specifier: ^7.1.0
 | 
			
		||||
    version: 7.1.0(eslint@8.38.0)(prettier@2.8.7)
 | 
			
		||||
  '@vue/eslint-config-typescript':
 | 
			
		||||
    specifier: ^10.0.0
 | 
			
		||||
    version: 10.0.0(eslint-plugin-vue@8.7.1)(eslint@8.38.0)(typescript@4.5.5)
 | 
			
		||||
  '@vue/runtime-core':
 | 
			
		||||
    specifier: ^3.2.47
 | 
			
		||||
    version: 3.2.47
 | 
			
		||||
  '@vue/test-utils':
 | 
			
		||||
    specifier: ^2.3.2
 | 
			
		||||
    version: 2.3.2(vue@3.2.47)
 | 
			
		||||
@@ -246,6 +255,9 @@ devDependencies:
 | 
			
		||||
  unplugin-auto-import:
 | 
			
		||||
    specifier: ^0.15.2
 | 
			
		||||
    version: 0.15.2(@vueuse/core@8.9.4)(rollup@2.79.1)
 | 
			
		||||
  unplugin-icons:
 | 
			
		||||
    specifier: ^0.16.1
 | 
			
		||||
    version: 0.16.1(@vue/compiler-sfc@3.2.47)
 | 
			
		||||
  unplugin-vue-components:
 | 
			
		||||
    specifier: ^0.24.1
 | 
			
		||||
    version: 0.24.1(rollup@2.79.1)(vue@3.2.47)
 | 
			
		||||
@@ -1612,6 +1624,12 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /@iconify-json/mdi@1.1.50:
 | 
			
		||||
    resolution: {integrity: sha512-SgbT5w5eHCdOG74ZWPz7HlTGk6VsifIJhNi6lAsxj/5Nlqt6Cz4LlQmSa9eecU9p075Jub2aAx/o7YI+GCahRQ==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@iconify/types': 2.0.0
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /@iconify/types@2.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
 | 
			
		||||
    dev: true
 | 
			
		||||
@@ -7561,6 +7579,35 @@ packages:
 | 
			
		||||
      - rollup
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /unplugin-icons@0.16.1(@vue/compiler-sfc@3.2.47):
 | 
			
		||||
    resolution: {integrity: sha512-qTunFUkpAyDnwzwV7YV1ZgCWRYfLuURcCurhhXOWMy2ipY88qx1pADvral2hJu4Xymh0X0t3Zcll3BIru2AVLQ==}
 | 
			
		||||
    peerDependencies:
 | 
			
		||||
      '@svgr/core': '>=7.0.0'
 | 
			
		||||
      '@vue/compiler-sfc': ^3.0.2 || ^2.7.0
 | 
			
		||||
      vue-template-compiler: ^2.6.12
 | 
			
		||||
      vue-template-es2015-compiler: ^1.9.0
 | 
			
		||||
    peerDependenciesMeta:
 | 
			
		||||
      '@svgr/core':
 | 
			
		||||
        optional: true
 | 
			
		||||
      '@vue/compiler-sfc':
 | 
			
		||||
        optional: true
 | 
			
		||||
      vue-template-compiler:
 | 
			
		||||
        optional: true
 | 
			
		||||
      vue-template-es2015-compiler:
 | 
			
		||||
        optional: true
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@antfu/install-pkg': 0.1.1
 | 
			
		||||
      '@antfu/utils': 0.7.2
 | 
			
		||||
      '@iconify/utils': 2.1.5
 | 
			
		||||
      '@vue/compiler-sfc': 3.2.47
 | 
			
		||||
      debug: 4.3.4
 | 
			
		||||
      kolorist: 1.7.0
 | 
			
		||||
      local-pkg: 0.4.3
 | 
			
		||||
      unplugin: 1.3.1
 | 
			
		||||
    transitivePeerDependencies:
 | 
			
		||||
      - supports-color
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /unplugin-vue-components@0.24.1(rollup@2.79.1)(vue@3.2.47):
 | 
			
		||||
    resolution: {integrity: sha512-T3A8HkZoIE1Cja95xNqolwza0yD5IVlgZZ1PVAGvVCx8xthmjsv38xWRCtHtwl+rvZyL9uif42SRkDGw9aCfMA==}
 | 
			
		||||
    engines: {node: '>=14'}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <n-form-item :label="inputLabel" v-bind="validationAttrs">
 | 
			
		||||
  <n-form-item :label="inputLabel" v-bind="validationAttrs as any">
 | 
			
		||||
    <n-input
 | 
			
		||||
      ref="inputElement"
 | 
			
		||||
      v-model:value="input"
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
      autocorrect="off"
 | 
			
		||||
      autocapitalize="off"
 | 
			
		||||
      spellcheck="false"
 | 
			
		||||
      :input-props="{ 'data-test-id': 'input' }"
 | 
			
		||||
      :input-props="{ 'data-test-id': 'input' } as any"
 | 
			
		||||
    />
 | 
			
		||||
  </n-form-item>
 | 
			
		||||
  <n-form-item :label="outputLabel">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,20 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <n-input v-model:value="value">
 | 
			
		||||
  <c-input-text v-model:value="value">
 | 
			
		||||
    <template #suffix>
 | 
			
		||||
      <n-tooltip trigger="hover">
 | 
			
		||||
        <template #trigger>
 | 
			
		||||
          <c-button circle variant="text" @click="onCopyClicked">
 | 
			
		||||
            <n-icon :component="ContentCopyFilled" />
 | 
			
		||||
          <c-button circle variant="text" size="small" @click="onCopyClicked">
 | 
			
		||||
            <icon-mdi-content-copy />
 | 
			
		||||
          </c-button>
 | 
			
		||||
        </template>
 | 
			
		||||
        {{ tooltipText }}
 | 
			
		||||
      </n-tooltip>
 | 
			
		||||
    </template>
 | 
			
		||||
  </n-input>
 | 
			
		||||
  </c-input-text>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useVModel, useClipboard } from '@vueuse/core';
 | 
			
		||||
import { ContentCopyFilled } from '@vicons/material';
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ value: string }>();
 | 
			
		||||
@@ -35,9 +34,3 @@ function onCopyClicked() {
 | 
			
		||||
  }, 2000);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
::v-deep(.n-input-wrapper) {
 | 
			
		||||
  padding-right: 5px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import { get, type MaybeRef } from '@vueuse/core';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import { reactive, watch, type Ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
@@ -31,7 +32,7 @@ export function useValidation<T>({
 | 
			
		||||
  watch: watchRefs = [],
 | 
			
		||||
}: {
 | 
			
		||||
  source: Ref<T>;
 | 
			
		||||
  rules: UseValidationRule<T>[];
 | 
			
		||||
  rules: MaybeRef<UseValidationRule<T>[]>;
 | 
			
		||||
  watch?: Ref<unknown>[];
 | 
			
		||||
}) {
 | 
			
		||||
  const state = reactive<{
 | 
			
		||||
@@ -55,7 +56,7 @@ export function useValidation<T>({
 | 
			
		||||
      state.message = '';
 | 
			
		||||
      state.status = undefined;
 | 
			
		||||
 | 
			
		||||
      for (const rule of rules) {
 | 
			
		||||
      for (const rule of get(rules)) {
 | 
			
		||||
        if (isFalsyOrHasThrown(() => rule.validator(source.value))) {
 | 
			
		||||
          state.message = rule.message;
 | 
			
		||||
          state.status = 'error';
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@
 | 
			
		||||
  </c-card>
 | 
			
		||||
 | 
			
		||||
  <c-card title="Base64 to string">
 | 
			
		||||
    <n-form-item label="Base64 string to decode" v-bind="b64Validation.attrs">
 | 
			
		||||
    <n-form-item label="Base64 string to decode" v-bind="b64Validation.attrs as any">
 | 
			
		||||
      <n-input v-model:value="base64Input" type="textarea" placeholder="Your base64 string..." rows="5" />
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,15 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <n-form-item label="Username">
 | 
			
		||||
      <n-input v-model:value="username" placeholder="Your username..." clearable />
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
    <n-form-item label="Password">
 | 
			
		||||
      <n-input
 | 
			
		||||
        v-model:value="password"
 | 
			
		||||
        placeholder="Your password..."
 | 
			
		||||
        type="password"
 | 
			
		||||
        show-password-on="click"
 | 
			
		||||
        clearable
 | 
			
		||||
      />
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
    <c-input-text v-model:value="username" label="Username" placeholder="Your username..." clearable raw-text mb-5 />
 | 
			
		||||
    <c-input-text
 | 
			
		||||
      v-model:value="password"
 | 
			
		||||
      label="Password"
 | 
			
		||||
      placeholder="Your password..."
 | 
			
		||||
      clearable
 | 
			
		||||
      raw-text
 | 
			
		||||
      mb-2
 | 
			
		||||
      type="password"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <c-card>
 | 
			
		||||
      <n-statistic label="Authorization header:" class="header">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,20 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <c-card title="Hash">
 | 
			
		||||
    <n-form label-width="120">
 | 
			
		||||
      <n-form-item label="Your string: " label-placement="left">
 | 
			
		||||
        <n-input
 | 
			
		||||
          v-model:value="input"
 | 
			
		||||
          placeholder="Your string to bcrypt..."
 | 
			
		||||
          autocomplete="off"
 | 
			
		||||
          autocorrect="off"
 | 
			
		||||
          autocapitalize="off"
 | 
			
		||||
          spellcheck="false"
 | 
			
		||||
        />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
      <n-form-item label="Salt count: " label-placement="left">
 | 
			
		||||
        <n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="10" :min="0" w-full />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
      <n-input :value="hashed" readonly style="text-align: center" />
 | 
			
		||||
    </n-form>
 | 
			
		||||
    <c-input-text
 | 
			
		||||
      v-model:value="input"
 | 
			
		||||
      placeholder="Your string to bcrypt..."
 | 
			
		||||
      raw-text
 | 
			
		||||
      label="Your string: "
 | 
			
		||||
      label-position="left"
 | 
			
		||||
      label-width="120px"
 | 
			
		||||
      mb-2
 | 
			
		||||
    />
 | 
			
		||||
    <n-form-item label="Salt count: " label-placement="left" label-width="120">
 | 
			
		||||
      <n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="10" :min="0" w-full />
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
 | 
			
		||||
    <c-input-text :value="hashed" readonly text-center />
 | 
			
		||||
 | 
			
		||||
    <n-space justify="center" mt-5>
 | 
			
		||||
      <c-button @click="copy"> Copy hash </c-button>
 | 
			
		||||
    </n-space>
 | 
			
		||||
@@ -24,24 +23,10 @@
 | 
			
		||||
  <c-card title="Compare string with hash">
 | 
			
		||||
    <n-form label-width="120">
 | 
			
		||||
      <n-form-item label="Your string: " label-placement="left">
 | 
			
		||||
        <n-input
 | 
			
		||||
          v-model:value="compareString"
 | 
			
		||||
          placeholder="Your string to compare..."
 | 
			
		||||
          autocomplete="off"
 | 
			
		||||
          autocorrect="off"
 | 
			
		||||
          autocapitalize="off"
 | 
			
		||||
          spellcheck="false"
 | 
			
		||||
        />
 | 
			
		||||
        <c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
      <n-form-item label="Your hash: " label-placement="left">
 | 
			
		||||
        <n-input
 | 
			
		||||
          v-model:value="compareHash"
 | 
			
		||||
          placeholder="Your hahs to compare..."
 | 
			
		||||
          autocomplete="off"
 | 
			
		||||
          autocorrect="off"
 | 
			
		||||
          autocapitalize="off"
 | 
			
		||||
          spellcheck="false"
 | 
			
		||||
        />
 | 
			
		||||
        <c-input-text v-model:value="compareHash" placeholder="Your hahs to compare..." raw-text />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
      <n-form-item label="Do they match ? " label-placement="left" :show-feedback="false">
 | 
			
		||||
        <div class="compare-result" :class="{ positive: compareMatch }">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,15 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <n-scrollbar style="flex: 1" x-scrollable>
 | 
			
		||||
    <n-space :wrap="false" style="flex: 1" justify="center" :size="0" mb-5>
 | 
			
		||||
    <n-space :wrap="false" style="flex: 1" justify="center" :size="12" mb-5>
 | 
			
		||||
      <div v-for="(suite, index) of suites" :key="index">
 | 
			
		||||
        <c-card style="width: 292px; margin: 0 8px 5px">
 | 
			
		||||
          <n-form-item label="Suite name:" :show-feedback="false" label-placement="left">
 | 
			
		||||
            <n-input v-model:value="suite.title" placeholder="Suite name..." />
 | 
			
		||||
          </n-form-item>
 | 
			
		||||
        <c-card style="width: 294px">
 | 
			
		||||
          <c-input-text
 | 
			
		||||
            v-model:value="suite.title"
 | 
			
		||||
            label-position="left"
 | 
			
		||||
            label="Suite name"
 | 
			
		||||
            placeholder="Suite name..."
 | 
			
		||||
            clearable
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <n-divider></n-divider>
 | 
			
		||||
          <n-form-item label="Suite values" :show-feedback="false">
 | 
			
		||||
@@ -33,9 +37,7 @@
 | 
			
		||||
  <div style="flex: 0 0 100%">
 | 
			
		||||
    <div style="max-width: 600px; margin: 0 auto">
 | 
			
		||||
      <n-space justify="center">
 | 
			
		||||
        <n-form-item label="Unit:" label-placement="left">
 | 
			
		||||
          <n-input v-model:value="unit" placeholder="Unit (eg: ms)" />
 | 
			
		||||
        </n-form-item>
 | 
			
		||||
        <c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 />
 | 
			
		||||
 | 
			
		||||
        <c-button
 | 
			
		||||
          @click="
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,8 @@
 | 
			
		||||
          :validation-status="entropyValidation.status"
 | 
			
		||||
        >
 | 
			
		||||
          <n-input-group>
 | 
			
		||||
            <n-input v-model:value="entropy" placeholder="Your string..." />
 | 
			
		||||
            <c-input-text v-model:value="entropy" placeholder="Your string..." />
 | 
			
		||||
 | 
			
		||||
            <c-button @click="refreshEntropy">
 | 
			
		||||
              <n-icon size="22">
 | 
			
		||||
                <Refresh />
 | 
			
		||||
@@ -37,15 +38,7 @@
 | 
			
		||||
      :validation-status="mnemonicValidation.status"
 | 
			
		||||
    >
 | 
			
		||||
      <n-input-group>
 | 
			
		||||
        <n-input
 | 
			
		||||
          v-model:value="passphrase"
 | 
			
		||||
          style="text-align: center; flex: 1"
 | 
			
		||||
          placeholder="Your mnemonic..."
 | 
			
		||||
          autocomplete="off"
 | 
			
		||||
          autocorrect="off"
 | 
			
		||||
          autocapitalize="off"
 | 
			
		||||
          spellcheck="false"
 | 
			
		||||
        />
 | 
			
		||||
        <c-input-text v-model:value="passphrase" placeholder="Your mnemonic..." raw-text />
 | 
			
		||||
 | 
			
		||||
        <c-button @click="copyPassphrase">
 | 
			
		||||
          <n-icon size="22" :component="Copy" />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,15 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <c-card>
 | 
			
		||||
    <n-form label-width="120" label-placement="left" :show-feedback="false">
 | 
			
		||||
      <n-form-item label="Your string:">
 | 
			
		||||
        <n-input v-model:value="input" />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
      <c-input-text
 | 
			
		||||
        v-model:value="input"
 | 
			
		||||
        label="Your string"
 | 
			
		||||
        label-position="left"
 | 
			
		||||
        label-width="120px"
 | 
			
		||||
        label-align="right"
 | 
			
		||||
        placeholder="Your string..."
 | 
			
		||||
        raw-text
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <n-divider />
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,8 @@
 | 
			
		||||
      <div class="duration">{{ formatMs(counter) }}</div>
 | 
			
		||||
    </c-card>
 | 
			
		||||
    <n-space justify="center" mt-5>
 | 
			
		||||
      <c-button v-if="!isRunning" secondary type="primary" @click="resume">Start</c-button>
 | 
			
		||||
      <c-button v-else secondary type="warning" @click="pause">Stop</c-button>
 | 
			
		||||
      <c-button v-if="!isRunning" type="primary" @click="resume">Start</c-button>
 | 
			
		||||
      <c-button v-else type="warning" @click="pause">Stop</c-button>
 | 
			
		||||
 | 
			
		||||
      <c-button @click="counter = 0">Reset</c-button>
 | 
			
		||||
    </n-space>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,25 +9,25 @@
 | 
			
		||||
        />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
      <n-form-item label="color name:">
 | 
			
		||||
        <input-copyable v-model:value="name" :on-input="(v: string) => onInputUpdated(v, 'name')" />
 | 
			
		||||
        <input-copyable v-model:value="name" @update:value="(v: string) => onInputUpdated(v, 'name')" />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
      <n-form-item label="hex:">
 | 
			
		||||
        <input-copyable v-model:value="hex" :on-input="(v: string) => onInputUpdated(v, 'hex')" />
 | 
			
		||||
        <input-copyable v-model:value="hex" @update:value="(v: string) => onInputUpdated(v, 'hex')" />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
      <n-form-item label="rgb:">
 | 
			
		||||
        <input-copyable v-model:value="rgb" :on-input="(v: string) => onInputUpdated(v, 'rgb')" />
 | 
			
		||||
        <input-copyable v-model:value="rgb" @update:value="(v: string) => onInputUpdated(v, 'rgb')" />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
      <n-form-item label="hsl:">
 | 
			
		||||
        <input-copyable v-model:value="hsl" :on-input="(v: string) => onInputUpdated(v, 'hsl')" />
 | 
			
		||||
        <input-copyable v-model:value="hsl" @update:value="(v: string) => onInputUpdated(v, 'hsl')" />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
      <n-form-item label="hwb:">
 | 
			
		||||
        <input-copyable v-model:value="hwb" :on-input="(v: string) => onInputUpdated(v, 'hwb')" />
 | 
			
		||||
        <input-copyable v-model:value="hwb" @update:value="(v: string) => onInputUpdated(v, 'hwb')" />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
      <n-form-item label="lch:">
 | 
			
		||||
        <input-copyable v-model:value="lch" :on-input="(v: string) => onInputUpdated(v, 'lch')" />
 | 
			
		||||
        <input-copyable v-model:value="lch" @update:value="(v: string) => onInputUpdated(v, 'lch')" />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
      <n-form-item label="cmyk:">
 | 
			
		||||
        <input-copyable v-model:value="cmyk" :on-input="(v: string) => onInputUpdated(v, 'cmyk')" />
 | 
			
		||||
        <input-copyable v-model:value="cmyk" @update:value="(v: string) => onInputUpdated(v, 'cmyk')" />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
    </n-form>
 | 
			
		||||
  </c-card>
 | 
			
		||||
@@ -54,15 +54,19 @@ const cmyk = ref('');
 | 
			
		||||
const lch = ref('');
 | 
			
		||||
 | 
			
		||||
function onInputUpdated(value: string, omit: string) {
 | 
			
		||||
  const color = colord(value);
 | 
			
		||||
  try {
 | 
			
		||||
    const color = colord(value);
 | 
			
		||||
 | 
			
		||||
  if (omit !== 'name') name.value = color.toName({ closest: true }) ?? '';
 | 
			
		||||
  if (omit !== 'hex') hex.value = color.toHex();
 | 
			
		||||
  if (omit !== 'rgb') rgb.value = color.toRgbString();
 | 
			
		||||
  if (omit !== 'hsl') hsl.value = color.toHslString();
 | 
			
		||||
  if (omit !== 'hwb') hwb.value = color.toHwbString();
 | 
			
		||||
  if (omit !== 'cmyk') cmyk.value = color.toCmykString();
 | 
			
		||||
  if (omit !== 'lch') lch.value = color.toLchString();
 | 
			
		||||
    if (omit !== 'name') name.value = color.toName({ closest: true }) ?? '';
 | 
			
		||||
    if (omit !== 'hex') hex.value = color.toHex();
 | 
			
		||||
    if (omit !== 'rgb') rgb.value = color.toRgbString();
 | 
			
		||||
    if (omit !== 'hsl') hsl.value = color.toHslString();
 | 
			
		||||
    if (omit !== 'hwb') hwb.value = color.toHwbString();
 | 
			
		||||
    if (omit !== 'cmyk') cmyk.value = color.toCmykString();
 | 
			
		||||
    if (omit !== 'lch') lch.value = color.toLchString();
 | 
			
		||||
  } catch {
 | 
			
		||||
    //
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onInputUpdated(hex.value, 'hex');
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,15 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <c-card>
 | 
			
		||||
    <n-form-item
 | 
			
		||||
      class="cron"
 | 
			
		||||
      :show-label="false"
 | 
			
		||||
      :feedback="cronValidation.message"
 | 
			
		||||
      :validation-status="cronValidation.status"
 | 
			
		||||
    >
 | 
			
		||||
      <n-input v-model:value="cron" size="large" placeholder="* * * * *" />
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
    <div mx-auto max-w-sm>
 | 
			
		||||
      <c-input-text
 | 
			
		||||
        v-model:value="cron"
 | 
			
		||||
        size="large"
 | 
			
		||||
        placeholder="* * * * *"
 | 
			
		||||
        :validation-rules="cronValidationRules"
 | 
			
		||||
        mb-3
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="cron-string">
 | 
			
		||||
      {{ cronString }}
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -86,7 +88,6 @@
 | 
			
		||||
import cronstrue from 'cronstrue';
 | 
			
		||||
import { isValidCron } from 'cron-validator';
 | 
			
		||||
import { computed, reactive, ref } from 'vue';
 | 
			
		||||
import { useValidation } from '@/composable/validation';
 | 
			
		||||
import { useStyleStore } from '@/stores/style.store';
 | 
			
		||||
 | 
			
		||||
function isCronValid(v: string) {
 | 
			
		||||
@@ -185,30 +186,20 @@ const cronString = computed(() => {
 | 
			
		||||
  return ' ';
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const cronValidation = useValidation({
 | 
			
		||||
  source: cron,
 | 
			
		||||
  rules: [
 | 
			
		||||
    {
 | 
			
		||||
      validator: (value) => isCronValid(value),
 | 
			
		||||
      message: 'This cron is invalid',
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
});
 | 
			
		||||
const cronValidationRules = [
 | 
			
		||||
  {
 | 
			
		||||
    validator: (value: string) => isCronValid(value),
 | 
			
		||||
    message: 'This cron is invalid',
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="less" scoped>
 | 
			
		||||
.cron {
 | 
			
		||||
::v-deep(input) {
 | 
			
		||||
  font-size: 30px;
 | 
			
		||||
  font-family: monospace;
 | 
			
		||||
  padding: 5px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
 | 
			
		||||
  margin: auto;
 | 
			
		||||
  max-width: 400px;
 | 
			
		||||
  display: block;
 | 
			
		||||
 | 
			
		||||
  .n-input {
 | 
			
		||||
    font-size: 30px;
 | 
			
		||||
    font-family: monospace;
 | 
			
		||||
    padding: 5px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.cron-string {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <n-form-item :show-label="false" v-bind="validation.attrs">
 | 
			
		||||
    <n-form-item :show-label="false" v-bind="validation.attrs as any">
 | 
			
		||||
      <n-input-group>
 | 
			
		||||
        <n-input
 | 
			
		||||
          v-model:value="inputDate"
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
          :on-input="onDateInputChanged"
 | 
			
		||||
          placeholder="Put you date string here..."
 | 
			
		||||
          clearable
 | 
			
		||||
          :input-props="{ 'data-test-id': 'date-time-converter-input' }"
 | 
			
		||||
          :input-props="{ 'data-test-id': 'date-time-converter-input' } as any"
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <n-select
 | 
			
		||||
@@ -20,16 +20,19 @@
 | 
			
		||||
      </n-input-group>
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
    <n-divider style="margin-top: 0" />
 | 
			
		||||
    <div v-for="{ name, fromDate } in formats" :key="name" mt-1>
 | 
			
		||||
      <n-input-group>
 | 
			
		||||
        <n-input-group-label style="flex: 0 0 170px"> {{ name }}: </n-input-group-label>
 | 
			
		||||
        <input-copyable
 | 
			
		||||
          :value="formatDateUsingFormatter(fromDate, normalizedDate)"
 | 
			
		||||
          placeholder="Invalid date..."
 | 
			
		||||
          :input-props="{ 'data-test-id': name }"
 | 
			
		||||
        />
 | 
			
		||||
      </n-input-group>
 | 
			
		||||
    </div>
 | 
			
		||||
    <input-copyable
 | 
			
		||||
      v-for="{ name, fromDate } in formats"
 | 
			
		||||
      :key="name"
 | 
			
		||||
      :label="name"
 | 
			
		||||
      label-width="150px"
 | 
			
		||||
      label-position="left"
 | 
			
		||||
      label-align="right"
 | 
			
		||||
      :value="formatDateUsingFormatter(fromDate, normalizedDate)"
 | 
			
		||||
      placeholder="Invalid date..."
 | 
			
		||||
      :test-id="name"
 | 
			
		||||
      readonly
 | 
			
		||||
      mt-2
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,12 +7,15 @@
 | 
			
		||||
          type="textarea"
 | 
			
		||||
          placeholder="The string to cypher"
 | 
			
		||||
          :autosize="{ minRows: 4 }"
 | 
			
		||||
          autocomplete="off"
 | 
			
		||||
          autocorrect="off"
 | 
			
		||||
          autocapitalize="off"
 | 
			
		||||
          spellcheck="false"
 | 
			
		||||
        />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
      <n-space vertical>
 | 
			
		||||
        <n-form-item label="Your secret key:" :show-feedback="false">
 | 
			
		||||
          <n-input v-model:value="cypherSecret" />
 | 
			
		||||
        </n-form-item>
 | 
			
		||||
        <c-input-text v-model:value="cypherSecret" label="Your secret key:" clearable raw-text />
 | 
			
		||||
 | 
			
		||||
        <n-form-item label="Encryption algorithm:" :show-feedback="false">
 | 
			
		||||
          <n-select
 | 
			
		||||
            v-model:value="cypherAlgo"
 | 
			
		||||
@@ -43,12 +46,15 @@
 | 
			
		||||
          type="textarea"
 | 
			
		||||
          placeholder="The string to cypher"
 | 
			
		||||
          :autosize="{ minRows: 4 }"
 | 
			
		||||
          autocomplete="off"
 | 
			
		||||
          autocorrect="off"
 | 
			
		||||
          autocapitalize="off"
 | 
			
		||||
          spellcheck="false"
 | 
			
		||||
        />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
      <n-space vertical>
 | 
			
		||||
        <n-form-item label="Your secret key:" :show-feedback="false">
 | 
			
		||||
          <n-input v-model:value="decryptSecret" />
 | 
			
		||||
        </n-form-item>
 | 
			
		||||
        <c-input-text v-model:value="decryptSecret" label="Your secret key:" clearable raw-text />
 | 
			
		||||
 | 
			
		||||
        <n-form-item label="Encryption algorithm:" :show-feedback="false">
 | 
			
		||||
          <n-select
 | 
			
		||||
            v-model:value="decryptAlgo"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
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';
 | 
			
		||||
@@ -14,7 +15,6 @@ 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,6 +75,7 @@ export const toolsByCategory: ToolCategory[] = [
 | 
			
		||||
      textToNatoAlphabet,
 | 
			
		||||
      yamlToJson,
 | 
			
		||||
      jsonToYaml,
 | 
			
		||||
      listConverter,
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
@@ -99,7 +100,7 @@ export const toolsByCategory: ToolCategory[] = [
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: 'Images',
 | 
			
		||||
    components: [qrCodeGenerator, svgPlaceholderGenerator, svgMeshGradientGenerator],
 | 
			
		||||
    components: [qrCodeGenerator, svgPlaceholderGenerator],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: 'Development',
 | 
			
		||||
 
 | 
			
		||||
@@ -22,59 +22,54 @@
 | 
			
		||||
      <n-alert v-if="error" style="margin-top: 25px" type="error">{{ error }}</n-alert>
 | 
			
		||||
      <n-divider />
 | 
			
		||||
 | 
			
		||||
      <n-input-group>
 | 
			
		||||
        <n-input-group-label style="flex: 0 0 170px"> Binary (2): </n-input-group-label>
 | 
			
		||||
        <input-copyable
 | 
			
		||||
          :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })"
 | 
			
		||||
          readonly
 | 
			
		||||
          placeholder="Binary version will be here..."
 | 
			
		||||
        />
 | 
			
		||||
      </n-input-group>
 | 
			
		||||
      <input-copyable
 | 
			
		||||
        label="Binary (2)"
 | 
			
		||||
        v-bind="inputProps"
 | 
			
		||||
        :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })"
 | 
			
		||||
        placeholder="Binary version will be here..."
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <n-input-group>
 | 
			
		||||
        <n-input-group-label style="flex: 0 0 170px"> Octal (8): </n-input-group-label>
 | 
			
		||||
        <input-copyable
 | 
			
		||||
          :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })"
 | 
			
		||||
          readonly
 | 
			
		||||
          placeholder="Octal version will be here..."
 | 
			
		||||
        />
 | 
			
		||||
      </n-input-group>
 | 
			
		||||
      <input-copyable
 | 
			
		||||
        label="Octal (8)"
 | 
			
		||||
        v-bind="inputProps"
 | 
			
		||||
        :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })"
 | 
			
		||||
        placeholder="Octal version will be here..."
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <n-input-group>
 | 
			
		||||
        <n-input-group-label style="flex: 0 0 170px"> Decimal (10): </n-input-group-label>
 | 
			
		||||
        <input-copyable
 | 
			
		||||
          :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })"
 | 
			
		||||
          readonly
 | 
			
		||||
          placeholder="Decimal version will be here..."
 | 
			
		||||
        />
 | 
			
		||||
      </n-input-group>
 | 
			
		||||
      <input-copyable
 | 
			
		||||
        label="Decimal (10)"
 | 
			
		||||
        v-bind="inputProps"
 | 
			
		||||
        :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })"
 | 
			
		||||
        placeholder="Decimal version will be here..."
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <n-input-group>
 | 
			
		||||
        <n-input-group-label style="flex: 0 0 170px"> Hexadecimal (16): </n-input-group-label>
 | 
			
		||||
        <input-copyable
 | 
			
		||||
          :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })"
 | 
			
		||||
          readonly
 | 
			
		||||
          placeholder="Decimal version will be here..."
 | 
			
		||||
        />
 | 
			
		||||
      </n-input-group>
 | 
			
		||||
      <input-copyable
 | 
			
		||||
        label="Hexadecimal (16)"
 | 
			
		||||
        v-bind="inputProps"
 | 
			
		||||
        :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })"
 | 
			
		||||
        placeholder="Hexadecimal version will be here..."
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <input-copyable
 | 
			
		||||
        label="Base64 (64)"
 | 
			
		||||
        v-bind="inputProps"
 | 
			
		||||
        :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })"
 | 
			
		||||
        placeholder="Base64 version will be here..."
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <div flex items-baseline>
 | 
			
		||||
        <n-input-group style="width: 160px; margin-right: 10px">
 | 
			
		||||
          <n-input-group-label> Custom: </n-input-group-label>
 | 
			
		||||
          <n-input-number v-model:value="outputBase" max="64" min="2" />
 | 
			
		||||
        </n-input-group>
 | 
			
		||||
 | 
			
		||||
      <n-input-group>
 | 
			
		||||
        <n-input-group-label style="flex: 0 0 170px"> Base64 (64): </n-input-group-label>
 | 
			
		||||
        <input-copyable
 | 
			
		||||
          :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })"
 | 
			
		||||
          readonly
 | 
			
		||||
          placeholder="Base64 version will be here..."
 | 
			
		||||
        />
 | 
			
		||||
      </n-input-group>
 | 
			
		||||
      <n-input-group>
 | 
			
		||||
        <n-input-group-label style="flex: 0 0 85px"> Custom: </n-input-group-label>
 | 
			
		||||
        <n-input-number v-model:value="outputBase" style="flex: 0 0 86px" max="64" min="2" />
 | 
			
		||||
        <input-copyable
 | 
			
		||||
          flex-1
 | 
			
		||||
          v-bind="inputProps"
 | 
			
		||||
          :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: outputBase })"
 | 
			
		||||
          readonly
 | 
			
		||||
          :placeholder="`Base ${outputBase} will be here...`"
 | 
			
		||||
        />
 | 
			
		||||
      </n-input-group>
 | 
			
		||||
      </div>
 | 
			
		||||
    </c-card>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -88,6 +83,14 @@ import InputCopyable from '../../components/InputCopyable.vue';
 | 
			
		||||
 | 
			
		||||
const styleStore = useStyleStore();
 | 
			
		||||
 | 
			
		||||
const inputProps = {
 | 
			
		||||
  labelPosition: 'left',
 | 
			
		||||
  labelWidth: '170px',
 | 
			
		||||
  labelAlign: 'right',
 | 
			
		||||
  readonly: true,
 | 
			
		||||
  'mb-2': '',
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
const input = ref('42');
 | 
			
		||||
const inputBase = ref(10);
 | 
			
		||||
const outputBase = ref(42);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,20 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <n-form-item label="An ipv4 address:" v-bind="validationAttrs">
 | 
			
		||||
      <n-input v-model:value="rawIpAddress" placeholder="An ipv4 address..." />
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
    <c-input-text v-model:value="rawIpAddress" label="The ipv4 address:" placeholder="The ipv4 address..." readonly />
 | 
			
		||||
 | 
			
		||||
    <n-divider style="margin-top: 0" mt-0 />
 | 
			
		||||
    <n-divider />
 | 
			
		||||
 | 
			
		||||
    <n-form-item
 | 
			
		||||
    <input-copyable
 | 
			
		||||
      v-for="{ label, value } of convertedSections"
 | 
			
		||||
      :key="label"
 | 
			
		||||
      :label="label"
 | 
			
		||||
      label-placement="left"
 | 
			
		||||
      label-width="100"
 | 
			
		||||
    >
 | 
			
		||||
      <input-copyable
 | 
			
		||||
        :value="validationAttrs.validationStatus === 'error' ? '' : value"
 | 
			
		||||
        placeholder="Set a correct ipv4 address"
 | 
			
		||||
      />
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
      label-position="left"
 | 
			
		||||
      label-width="100px"
 | 
			
		||||
      label-align="right"
 | 
			
		||||
      mb-2
 | 
			
		||||
      :value="validationAttrs.validationStatus === 'error' ? '' : value"
 | 
			
		||||
      placeholder="Set a correct ipv4 address"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -33,7 +30,7 @@ const convertedSections = computed(() => {
 | 
			
		||||
 | 
			
		||||
  return [
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Decimal : ',
 | 
			
		||||
      label: 'Decimal: ',
 | 
			
		||||
      value: String(ipInDecimal),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,10 @@
 | 
			
		||||
    <n-space item-style="flex:1 1 0">
 | 
			
		||||
      <div>
 | 
			
		||||
        <n-space item-style="flex:1 1 0">
 | 
			
		||||
          <n-form-item label="Start address" v-bind="startIpValidation.attrs">
 | 
			
		||||
          <n-form-item label="Start address" v-bind="startIpValidation.attrs as any">
 | 
			
		||||
            <n-input v-model:value="rawStartAddress" placeholder="Start IPv4 address..." />
 | 
			
		||||
          </n-form-item>
 | 
			
		||||
          <n-form-item label="End address" v-bind="endIpValidation.attrs">
 | 
			
		||||
          <n-form-item label="End address" v-bind="endIpValidation.attrs as any">
 | 
			
		||||
            <n-input v-model:value="rawEndAddress" placeholder="End IPv4 address..." />
 | 
			
		||||
          </n-form-item>
 | 
			
		||||
        </n-space>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,12 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <n-form-item label="An IPv4 address with or without mask" v-bind="validationAttrs">
 | 
			
		||||
      <n-input v-model:value="ip" />
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
    <c-input-text
 | 
			
		||||
      v-model:value="ip"
 | 
			
		||||
      label="An IPv4 address with or without mask"
 | 
			
		||||
      placeholder="The ipv4 address..."
 | 
			
		||||
      :validation-rules="ipValidationRules"
 | 
			
		||||
      mb-4
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <div v-if="networkInfo">
 | 
			
		||||
      <n-table>
 | 
			
		||||
@@ -37,7 +41,6 @@
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { Netmask } from 'netmask';
 | 
			
		||||
import { withDefaultOnError } from '@/utils/defaults';
 | 
			
		||||
import { useValidation } from '@/composable/validation';
 | 
			
		||||
import { isNotThrowing } from '@/utils/boolean';
 | 
			
		||||
import { useStorage } from '@vueuse/core';
 | 
			
		||||
import { ArrowLeft, ArrowRight } from '@vicons/tabler';
 | 
			
		||||
@@ -50,15 +53,12 @@ const getNetworkInfo = (address: string) => new Netmask(address.trim());
 | 
			
		||||
 | 
			
		||||
const networkInfo = computed(() => withDefaultOnError(() => getNetworkInfo(ip.value), undefined));
 | 
			
		||||
 | 
			
		||||
const { attrs: validationAttrs } = useValidation({
 | 
			
		||||
  source: ip,
 | 
			
		||||
  rules: [
 | 
			
		||||
    {
 | 
			
		||||
      message: 'We cannot parse this address, check the format',
 | 
			
		||||
      validator: (value) => isNotThrowing(() => getNetworkInfo(value.trim())),
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
});
 | 
			
		||||
const ipValidationRules = [
 | 
			
		||||
  {
 | 
			
		||||
    message: 'We cannot parse this address, check the format',
 | 
			
		||||
    validator: (value: string) => isNotThrowing(() => getNetworkInfo(value.trim())),
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const sections: {
 | 
			
		||||
  label: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +1,32 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <n-space vertical :size="50">
 | 
			
		||||
      <n-alert title="Info" type="info">
 | 
			
		||||
        This tool uses the first method suggested by IETF using the current timestamp plus the mac address, sha1 hashed,
 | 
			
		||||
        and the lower 40 bits to generate your random ULA.
 | 
			
		||||
      </n-alert>
 | 
			
		||||
    <n-alert title="Info" type="info">
 | 
			
		||||
      This tool uses the first method suggested by IETF using the current timestamp plus the mac address, sha1 hashed,
 | 
			
		||||
      and the lower 40 bits to generate your random ULA.
 | 
			
		||||
    </n-alert>
 | 
			
		||||
 | 
			
		||||
      <n-form-item label="MAC address:" v-bind="validationAttrs">
 | 
			
		||||
        <n-input
 | 
			
		||||
          v-model:value="macAddress"
 | 
			
		||||
          size="large"
 | 
			
		||||
          placeholder="Type a MAC address"
 | 
			
		||||
          clearable
 | 
			
		||||
          autocomplete="off"
 | 
			
		||||
          autocorrect="off"
 | 
			
		||||
          autocapitalize="off"
 | 
			
		||||
          spellcheck="false"
 | 
			
		||||
        />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
    </n-space>
 | 
			
		||||
    <c-input-text
 | 
			
		||||
      v-model:value="macAddress"
 | 
			
		||||
      placeholder="Type a MAC address"
 | 
			
		||||
      clearable
 | 
			
		||||
      label="MAC address:"
 | 
			
		||||
      raw-text
 | 
			
		||||
      my-8
 | 
			
		||||
      :validation="addressValidation"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <div v-if="validationAttrs.validationStatus !== 'error'">
 | 
			
		||||
      <n-input-group v-for="{ label, value } in calculatedSections" :key="label" style="margin: 5px 0">
 | 
			
		||||
        <n-input-group-label style="flex: 0 0 160px"> {{ label }} </n-input-group-label>
 | 
			
		||||
        <input-copyable :value="value" readonly />
 | 
			
		||||
      </n-input-group>
 | 
			
		||||
    <div v-if="addressValidation.isValid">
 | 
			
		||||
      <input-copyable
 | 
			
		||||
        v-for="{ label, value } in calculatedSections"
 | 
			
		||||
        :key="label"
 | 
			
		||||
        :value="value"
 | 
			
		||||
        :label="label"
 | 
			
		||||
        label-width="160px"
 | 
			
		||||
        label-align="right"
 | 
			
		||||
        label-position="left"
 | 
			
		||||
        readonly
 | 
			
		||||
        mb-2
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -59,7 +61,7 @@ const calculatedSections = computed(() => {
 | 
			
		||||
  ];
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { attrs: validationAttrs } = macAddressValidation(macAddress);
 | 
			
		||||
const addressValidation = macAddressValidation(macAddress);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="less" scoped></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <n-form-item label="Your first json" v-bind="leftJsonValidation.attrs">
 | 
			
		||||
  <n-form-item label="Your first json" v-bind="leftJsonValidation.attrs as any">
 | 
			
		||||
    <n-input
 | 
			
		||||
      v-model:value="rawLeftJson"
 | 
			
		||||
      placeholder="Paste your first json here..."
 | 
			
		||||
@@ -9,10 +9,10 @@
 | 
			
		||||
      autocorrect="off"
 | 
			
		||||
      autocapitalize="off"
 | 
			
		||||
      spellcheck="false"
 | 
			
		||||
      :input-props="{ 'data-test-id': 'leftJson' }"
 | 
			
		||||
      :input-props="{ 'data-test-id': 'leftJson' }  as any"
 | 
			
		||||
    />
 | 
			
		||||
  </n-form-item>
 | 
			
		||||
  <n-form-item label="Your json to compare" v-bind="rightJsonValidation.attrs">
 | 
			
		||||
  <n-form-item label="Your json to compare" v-bind="rightJsonValidation.attrs as any">
 | 
			
		||||
    <n-input
 | 
			
		||||
      v-model:value="rawRightJson"
 | 
			
		||||
      placeholder="Paste your json to compare here..."
 | 
			
		||||
@@ -22,7 +22,7 @@
 | 
			
		||||
      autocorrect="off"
 | 
			
		||||
      autocapitalize="off"
 | 
			
		||||
      spellcheck="false"
 | 
			
		||||
      :input-props="{ 'data-test-id': 'rightJson' }"
 | 
			
		||||
      :input-props="{ 'data-test-id': 'rightJson' }  as any"
 | 
			
		||||
    />
 | 
			
		||||
  </n-form-item>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								src/tools/list-converter/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/tools/list-converter/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
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'),
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										39
									
								
								src/tools/list-converter/list-converter.e2e.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/tools/list-converter/list-converter.e2e.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
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'");
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										76
									
								
								src/tools/list-converter/list-converter.models.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/tools/list-converter/list-converter.models.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
			
		||||
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);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										27
									
								
								src/tools/list-converter/list-converter.models.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/tools/list-converter/list-converter.models.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
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();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								src/tools/list-converter/list-converter.types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/tools/list-converter/list-converter.types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
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;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										123
									
								
								src/tools/list-converter/list-converter.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/tools/list-converter/list-converter.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,123 @@
 | 
			
		||||
<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>
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <n-form-item label="MAC address:" v-bind="validationAttrs">
 | 
			
		||||
    <n-form-item label="MAC address:" v-bind="validationAttrs as any">
 | 
			
		||||
      <n-input
 | 
			
		||||
        v-model:value="macAddress"
 | 
			
		||||
        size="large"
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
 | 
			
		||||
      <n-input-group v-for="{ key, type, label, placeholder, ...element } of elements" :key="key">
 | 
			
		||||
        <n-input-group-label style="flex: 0 0 110px">{{ label }}</n-input-group-label>
 | 
			
		||||
        <n-input v-if="type === 'input'" v-model:value="metadata[key]" :placeholder="placeholder" />
 | 
			
		||||
        <c-input-text v-if="type === 'input'" v-model:value="metadata[key]" :placeholder="placeholder" clearable />
 | 
			
		||||
        <n-dynamic-input
 | 
			
		||||
          v-else-if="type === 'input-multiple'"
 | 
			
		||||
          v-model:value="metadata[key]"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,23 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div style="max-width: 350px">
 | 
			
		||||
    <n-form-item label="Secret" v-bind="secretValidationAttrs">
 | 
			
		||||
      <n-input v-model:value="secret" placeholder="Paste your TOTP secret...">
 | 
			
		||||
        <template #suffix>
 | 
			
		||||
          <n-tooltip trigger="hover">
 | 
			
		||||
            <template #trigger>
 | 
			
		||||
              <c-button circle variant="text" @click="refreshSecret">
 | 
			
		||||
                <n-icon :component="Refresh" />
 | 
			
		||||
              </c-button>
 | 
			
		||||
            </template>
 | 
			
		||||
            Generate secret token
 | 
			
		||||
          </n-tooltip>
 | 
			
		||||
        </template>
 | 
			
		||||
      </n-input>
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
    <c-input-text
 | 
			
		||||
      v-model:value="secret"
 | 
			
		||||
      label="Secret"
 | 
			
		||||
      placeholder="Paste your TOTP secret..."
 | 
			
		||||
      mb-5
 | 
			
		||||
      :validation-rules="secretValidationRules"
 | 
			
		||||
    >
 | 
			
		||||
      <template #suffix>
 | 
			
		||||
        <n-tooltip trigger="hover">
 | 
			
		||||
          <template #trigger>
 | 
			
		||||
            <c-button circle variant="text" size="small" @click="refreshSecret">
 | 
			
		||||
              <icon-mdi-refresh />
 | 
			
		||||
            </c-button>
 | 
			
		||||
          </template>
 | 
			
		||||
          Generate secret token
 | 
			
		||||
        </n-tooltip>
 | 
			
		||||
      </template>
 | 
			
		||||
    </c-input-text>
 | 
			
		||||
 | 
			
		||||
    <div>
 | 
			
		||||
      <token-display :tokens="tokens" style="margin-top: 2px" />
 | 
			
		||||
@@ -27,49 +31,52 @@
 | 
			
		||||
    </n-space>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div style="max-width: 350px">
 | 
			
		||||
    <n-form-item label="Secret in hexadecimal">
 | 
			
		||||
      <input-copyable :value="base32toHex(secret)" readonly placeholder="Secret in hex will be displayed here" />
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
    <input-copyable
 | 
			
		||||
      label="Secret in hexadecimal"
 | 
			
		||||
      :value="base32toHex(secret)"
 | 
			
		||||
      readonly
 | 
			
		||||
      placeholder="Secret in hex will be displayed here"
 | 
			
		||||
      mb-5
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <n-form-item label="Epoch">
 | 
			
		||||
      <input-copyable
 | 
			
		||||
        :value="Math.floor(now / 1000).toString()"
 | 
			
		||||
        readonly
 | 
			
		||||
        placeholder="Epoch in sec will be displayed here"
 | 
			
		||||
      />
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
    <n-form-item label="Iteration" :show-feedback="false">
 | 
			
		||||
      <n-input-group>
 | 
			
		||||
        <n-input-group-label style="width: 110px">Count:</n-input-group-label>
 | 
			
		||||
        <input-copyable
 | 
			
		||||
          :value="String(getCounterFromTime({ now, timeStep: 30 }))"
 | 
			
		||||
          readonly
 | 
			
		||||
          placeholder="Iteration count will be displayed here"
 | 
			
		||||
        />
 | 
			
		||||
      </n-input-group>
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
    <input-copyable
 | 
			
		||||
      label="Epoch"
 | 
			
		||||
      :value="Math.floor(now / 1000).toString()"
 | 
			
		||||
      readonly
 | 
			
		||||
      mb-5
 | 
			
		||||
      placeholder="Epoch in sec will be displayed here"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <n-form-item label="Iteration" :show-label="false" style="margin-top: 5px">
 | 
			
		||||
      <n-input-group>
 | 
			
		||||
        <n-input-group-label style="width: 110px">Padded hex:</n-input-group-label>
 | 
			
		||||
        <input-copyable
 | 
			
		||||
          :value="getCounterFromTime({ now, timeStep: 30 }).toString(16).padStart(16, '0')"
 | 
			
		||||
          readonly
 | 
			
		||||
          placeholder="Iteration count in hex will be displayed here"
 | 
			
		||||
        />
 | 
			
		||||
      </n-input-group>
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
    <p>Iteration</p>
 | 
			
		||||
 | 
			
		||||
    <input-copyable
 | 
			
		||||
      :value="String(getCounterFromTime({ now, timeStep: 30 }))"
 | 
			
		||||
      readonly
 | 
			
		||||
      label="Count:"
 | 
			
		||||
      label-position="left"
 | 
			
		||||
      label-width="90px"
 | 
			
		||||
      label-align="right"
 | 
			
		||||
      placeholder="Iteration count will be displayed here"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <input-copyable
 | 
			
		||||
      :value="getCounterFromTime({ now, timeStep: 30 }).toString(16).padStart(16, '0')"
 | 
			
		||||
      readonly
 | 
			
		||||
      placeholder="Iteration count in hex will be displayed here"
 | 
			
		||||
      label-position="left"
 | 
			
		||||
      label-width="90px"
 | 
			
		||||
      label-align="right"
 | 
			
		||||
      label="Padded hex:"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, ref } from 'vue';
 | 
			
		||||
import { Refresh } from '@vicons/tabler';
 | 
			
		||||
import { useTimestamp } from '@vueuse/core';
 | 
			
		||||
import { useThemeVars } from 'naive-ui';
 | 
			
		||||
import { useStyleStore } from '@/stores/style.store';
 | 
			
		||||
import InputCopyable from '@/components/InputCopyable.vue';
 | 
			
		||||
import { useValidation } from '@/composable/validation';
 | 
			
		||||
import { computedRefreshable } from '@/composable/computedRefreshable';
 | 
			
		||||
import { generateTOTP, buildKeyUri, generateSecret, base32toHex, getCounterFromTime } from './otp.service';
 | 
			
		||||
import { useQRCode } from '../qr-code-generator/useQRCode';
 | 
			
		||||
@@ -106,19 +113,16 @@ const { qrcode } = useQRCode({
 | 
			
		||||
  options: { width: 210 },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { attrs: secretValidationAttrs } = useValidation({
 | 
			
		||||
  source: secret,
 | 
			
		||||
  rules: [
 | 
			
		||||
    {
 | 
			
		||||
      message: 'Secret should be a base32 string',
 | 
			
		||||
      validator: (value) => value.toUpperCase().match(/^[A-Z234567]+$/),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      message: 'Please set a secret',
 | 
			
		||||
      validator: (value) => value !== '',
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
});
 | 
			
		||||
const secretValidationRules = [
 | 
			
		||||
  {
 | 
			
		||||
    message: 'Secret should be a base32 string',
 | 
			
		||||
    validator: (value: string) => value.toUpperCase().match(/^[A-Z234567]+$/),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    message: 'Please set a secret',
 | 
			
		||||
    validator: (value: string) => value !== '',
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="less" scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import type { NumberType } from 'libphonenumber-js/types';
 | 
			
		||||
import type { CountryCode, 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?: string } = {}): string {
 | 
			
		||||
}: { locale?: string; defaultCode?: CountryCode } = {}): CountryCode {
 | 
			
		||||
  const countryCode = locale.split('-')[1]?.toUpperCase();
 | 
			
		||||
 | 
			
		||||
  if (!countryCode) return defaultCode;
 | 
			
		||||
 | 
			
		||||
  return lookup.byIso(countryCode)?.iso2 ?? defaultCode;
 | 
			
		||||
  return (lookup.byIso(countryCode)?.iso2 ?? defaultCode) as CountryCode;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,14 @@
 | 
			
		||||
    <n-form-item label="Default country code:">
 | 
			
		||||
      <n-select v-model:value="defaultCountryCode" :options="countriesOptions" />
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
    <n-form-item label="Phone number:" v-bind="validation.attrs">
 | 
			
		||||
      <n-input v-model:value="rawPhone" placeholder="Enter a phone number" />
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
 | 
			
		||||
    <c-input-text
 | 
			
		||||
      v-model:value="rawPhone"
 | 
			
		||||
      placeholder="Enter a phone number"
 | 
			
		||||
      label="Phone number:"
 | 
			
		||||
      :validation="validation"
 | 
			
		||||
      mb-5
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <n-table v-if="parsedDetails">
 | 
			
		||||
      <tbody>
 | 
			
		||||
@@ -50,7 +55,7 @@ const validation = useValidation({
 | 
			
		||||
const parsedDetails = computed(() => {
 | 
			
		||||
  if (!validation.isValid) return undefined;
 | 
			
		||||
 | 
			
		||||
  const parsed = withDefaultOnError(() => parsePhoneNumber(rawPhone.value, 'FR'), undefined);
 | 
			
		||||
  const parsed = withDefaultOnError(() => parsePhoneNumber(rawPhone.value, defaultCountryCode.value), undefined);
 | 
			
		||||
 | 
			
		||||
  if (!parsed) return undefined;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
  <div>
 | 
			
		||||
    <c-card title="Arabic to roman">
 | 
			
		||||
      <n-space align="center" justify="space-between">
 | 
			
		||||
        <n-form-item v-bind="validationNumeral">
 | 
			
		||||
        <n-form-item v-bind="validationNumeral as any">
 | 
			
		||||
          <n-input-number v-model:value="inputNumeral" :min="1" style="width: 200px" :show-button="false" />
 | 
			
		||||
        </n-form-item>
 | 
			
		||||
        <div class="result">
 | 
			
		||||
@@ -15,13 +15,12 @@
 | 
			
		||||
    </c-card>
 | 
			
		||||
    <c-card title="Roman to arabic" mt-5>
 | 
			
		||||
      <n-space align="center" justify="space-between">
 | 
			
		||||
        <n-form-item v-bind="validationRoman">
 | 
			
		||||
          <n-input v-model:value="inputRoman" style="width: 200px" />
 | 
			
		||||
        </n-form-item>
 | 
			
		||||
        <c-input-text v-model:value="inputRoman" style="width: 200px" :validation="validationRoman" />
 | 
			
		||||
 | 
			
		||||
        <div class="result">
 | 
			
		||||
          {{ outputNumeral }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <c-button :disabled="validationRoman.validationStatus === 'error'" @click="copyArabic"> Copy </c-button>
 | 
			
		||||
        <c-button :disabled="!validationRoman.isValid" @click="copyArabic"> Copy </c-button>
 | 
			
		||||
      </n-space>
 | 
			
		||||
    </c-card>
 | 
			
		||||
  </div>
 | 
			
		||||
@@ -55,7 +54,7 @@ const { attrs: validationNumeral } = useValidation({
 | 
			
		||||
const inputRoman = ref('XLII');
 | 
			
		||||
const outputNumeral = computed(() => romanToArabic(inputRoman.value));
 | 
			
		||||
 | 
			
		||||
const { attrs: validationRoman } = useValidation({
 | 
			
		||||
const validationRoman = useValidation({
 | 
			
		||||
  source: inputRoman,
 | 
			
		||||
  rules: [
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div style="flex: 0 0 100%">
 | 
			
		||||
    <n-space item-style="flex: 1 1 0" style="margin: 0 auto; max-width: 600px" justify="center">
 | 
			
		||||
      <n-form-item label="Bits :" v-bind="bitsValidationAttrs" label-placement="left" label-width="100">
 | 
			
		||||
      <n-form-item label="Bits :" v-bind="bitsValidationAttrs as any" label-placement="left" label-width="100">
 | 
			
		||||
        <n-input-number v-model:value="bits" min="256" max="16384" step="8" />
 | 
			
		||||
      </n-form-item>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
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'),
 | 
			
		||||
});
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
import { expect, describe, it } from 'vitest';
 | 
			
		||||
// import { } from './svg-mesh-gradient-generator.service';
 | 
			
		||||
//
 | 
			
		||||
// describe('svg-mesh-gradient-generator', () => {
 | 
			
		||||
//
 | 
			
		||||
// })
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
// 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);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,49 +0,0 @@
 | 
			
		||||
<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>
 | 
			
		||||
@@ -21,9 +21,15 @@
 | 
			
		||||
        <n-form-item label="Font size">
 | 
			
		||||
          <n-input-number v-model:value="fontSize" placeholder="Font size..." min="1" />
 | 
			
		||||
        </n-form-item>
 | 
			
		||||
        <n-form-item label="Custom text">
 | 
			
		||||
          <n-input v-model:value="customText" :placeholder="`Default is ${width}x${height}`" />
 | 
			
		||||
        </n-form-item>
 | 
			
		||||
 | 
			
		||||
        <c-input-text
 | 
			
		||||
          v-model:value="customText"
 | 
			
		||||
          label="Custom text"
 | 
			
		||||
          :placeholder="`Default is ${width}x${height}`"
 | 
			
		||||
          label-position="left"
 | 
			
		||||
          label-width="100px"
 | 
			
		||||
          label-align="right"
 | 
			
		||||
        />
 | 
			
		||||
      </n-space>
 | 
			
		||||
      <n-form-item label="Use exact size" label-placement="left">
 | 
			
		||||
        <n-switch v-model:value="useExactSize" />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,12 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <n-form-item label="Your text to convert to NATO phonetic alphabet">
 | 
			
		||||
      <n-input v-model:value="input" placeholder="Put your text here..." clearable />
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
    <c-input-text
 | 
			
		||||
      v-model:value="input"
 | 
			
		||||
      label="Your text to convert to NATO phonetic alphabet"
 | 
			
		||||
      placeholder="Put your text here..."
 | 
			
		||||
      clearable
 | 
			
		||||
      mb-5
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <n-space v-if="natoText" vertical>
 | 
			
		||||
      <n-text>Your text in NATO phonetic alphabet</n-text>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,51 +1,59 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <c-card>
 | 
			
		||||
    <n-form-item label="Your url to parse:" :feedback="validation.message" :validation-status="validation.status">
 | 
			
		||||
      <n-input v-model:value="urlToParse" placeholder="Your url to parse..." />
 | 
			
		||||
    </n-form-item>
 | 
			
		||||
    <c-input-text
 | 
			
		||||
      v-model:value="urlToParse"
 | 
			
		||||
      label="Your url to parse:"
 | 
			
		||||
      placeholder="Your url to parse..."
 | 
			
		||||
      raw-text
 | 
			
		||||
      :validation-rules="urlValidationRules"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <n-divider style="margin-top: 0" />
 | 
			
		||||
    <n-divider />
 | 
			
		||||
 | 
			
		||||
    <n-form>
 | 
			
		||||
      <n-input-group v-for="{ title, key } in properties" :key="key">
 | 
			
		||||
        <n-input-group-label style="flex: 0 0 120px"> {{ title }}: </n-input-group-label>
 | 
			
		||||
        <input-copyable :value="(urlParsed?.[key] as string) ?? ''" readonly placeholder=" " />
 | 
			
		||||
      </n-input-group>
 | 
			
		||||
    <input-copyable
 | 
			
		||||
      v-for="{ title, key } in properties"
 | 
			
		||||
      :key="key"
 | 
			
		||||
      :label="title"
 | 
			
		||||
      :value="(urlParsed?.[key] as string) ?? ''"
 | 
			
		||||
      readonly
 | 
			
		||||
      label-position="left"
 | 
			
		||||
      label-width="110px"
 | 
			
		||||
      mb-2
 | 
			
		||||
      placeholder=" "
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
      <n-input-group
 | 
			
		||||
        v-for="[k, v] in Object.entries(Object.fromEntries(urlParsed?.searchParams.entries() ?? []))"
 | 
			
		||||
        :key="k"
 | 
			
		||||
      >
 | 
			
		||||
        <n-input-group-label style="flex: 0 0 120px">
 | 
			
		||||
          <n-icon :component="SubdirectoryArrowRightRound" />
 | 
			
		||||
        </n-input-group-label>
 | 
			
		||||
        <input-copyable :value="k" readonly />
 | 
			
		||||
        <input-copyable :value="v" readonly />
 | 
			
		||||
      </n-input-group>
 | 
			
		||||
    </n-form>
 | 
			
		||||
    <div
 | 
			
		||||
      v-for="[k, v] in Object.entries(Object.fromEntries(urlParsed?.searchParams.entries() ?? []))"
 | 
			
		||||
      :key="k"
 | 
			
		||||
      mb-2
 | 
			
		||||
      w-full
 | 
			
		||||
      flex
 | 
			
		||||
    >
 | 
			
		||||
      <div style="flex: 1 0 110px">
 | 
			
		||||
        <icon-mdi-arrow-right-bottom />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <input-copyable :value="k" readonly />
 | 
			
		||||
      <input-copyable :value="v" readonly />
 | 
			
		||||
    </div>
 | 
			
		||||
  </c-card>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useValidation } from '@/composable/validation';
 | 
			
		||||
import { isNotThrowing } from '@/utils/boolean';
 | 
			
		||||
import { withDefaultOnError } from '@/utils/defaults';
 | 
			
		||||
import { SubdirectoryArrowRightRound } from '@vicons/material';
 | 
			
		||||
import { computed, ref } from 'vue';
 | 
			
		||||
import InputCopyable from '../../components/InputCopyable.vue';
 | 
			
		||||
 | 
			
		||||
const urlToParse = ref('https://me:pwd@it-tools.tech:3000/url-parser?key1=value&key2=value2#the-hash');
 | 
			
		||||
 | 
			
		||||
const urlParsed = computed(() => withDefaultOnError(() => new URL(urlToParse.value), undefined));
 | 
			
		||||
const validation = useValidation({
 | 
			
		||||
  source: urlToParse,
 | 
			
		||||
  rules: [
 | 
			
		||||
    {
 | 
			
		||||
      validator: (value) => isNotThrowing(() => new URL(value)),
 | 
			
		||||
      message: 'Invalid url',
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
});
 | 
			
		||||
const urlValidationRules = [
 | 
			
		||||
  {
 | 
			
		||||
    validator: (value: string) => isNotThrowing(() => new URL(value)),
 | 
			
		||||
    message: 'Invalid url',
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const properties: { title: string; key: keyof URL }[] = [
 | 
			
		||||
  { title: 'Protocol', key: 'protocol' },
 | 
			
		||||
 
 | 
			
		||||
@@ -2,28 +2,51 @@
 | 
			
		||||
  <div v-for="buttonVariant of buttonVariants" :key="buttonVariant">
 | 
			
		||||
    <h2>{{ _.capitalize(buttonVariant) }}</h2>
 | 
			
		||||
 | 
			
		||||
    <c-button v-for="buttonType of buttonTypes" :key="buttonType" :variant="buttonVariant" :type="buttonType" mx-1>
 | 
			
		||||
      Button
 | 
			
		||||
    </c-button>
 | 
			
		||||
    <div v-for="buttonSize of buttonSizes" :key="buttonSize" mb-2>
 | 
			
		||||
      <c-button
 | 
			
		||||
        v-for="buttonType of buttonTypes"
 | 
			
		||||
        :key="buttonType"
 | 
			
		||||
        :variant="buttonVariant"
 | 
			
		||||
        :type="buttonType"
 | 
			
		||||
        :size="buttonSize"
 | 
			
		||||
        mx-1
 | 
			
		||||
      >
 | 
			
		||||
        Button
 | 
			
		||||
      </c-button>
 | 
			
		||||
 | 
			
		||||
    <c-button
 | 
			
		||||
      v-for="buttonType of buttonTypes"
 | 
			
		||||
      :key="buttonType"
 | 
			
		||||
      :variant="buttonVariant"
 | 
			
		||||
      :type="buttonType"
 | 
			
		||||
      circle
 | 
			
		||||
      mx-1
 | 
			
		||||
    >
 | 
			
		||||
      A
 | 
			
		||||
    </c-button>
 | 
			
		||||
      <c-button
 | 
			
		||||
        v-for="buttonType of buttonTypes"
 | 
			
		||||
        :key="buttonType"
 | 
			
		||||
        :variant="buttonVariant"
 | 
			
		||||
        :type="buttonType"
 | 
			
		||||
        :size="buttonSize"
 | 
			
		||||
        circle
 | 
			
		||||
        mx-1
 | 
			
		||||
      >
 | 
			
		||||
        A
 | 
			
		||||
      </c-button>
 | 
			
		||||
 | 
			
		||||
      <c-button
 | 
			
		||||
        v-for="buttonType of buttonTypes"
 | 
			
		||||
        :key="buttonType"
 | 
			
		||||
        :variant="buttonVariant"
 | 
			
		||||
        :type="buttonType"
 | 
			
		||||
        :size="buttonSize"
 | 
			
		||||
        circle
 | 
			
		||||
        mx-1
 | 
			
		||||
      >
 | 
			
		||||
        <icon-mdi-content-copy />
 | 
			
		||||
      </c-button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
 | 
			
		||||
const buttonVariants = ['basic', 'text'];
 | 
			
		||||
const buttonTypes = ['default', 'primary'];
 | 
			
		||||
const buttonVariants = ['basic', 'text'] as const;
 | 
			
		||||
const buttonTypes = ['default', 'primary', 'warning'] as const;
 | 
			
		||||
const buttonSizes = ['small', 'medium', 'large'] as const;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="less" scoped></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,21 @@ const createTheme = ({ style }: { style: 'light' | 'dark' }) => {
 | 
			
		||||
  const theme = appThemes[style];
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    size: {
 | 
			
		||||
      small: {
 | 
			
		||||
        width: '28px',
 | 
			
		||||
        fontSize: '12px',
 | 
			
		||||
      },
 | 
			
		||||
      medium: {
 | 
			
		||||
        width: '34px',
 | 
			
		||||
        fontSize: '14px',
 | 
			
		||||
      },
 | 
			
		||||
      large: {
 | 
			
		||||
        width: '40px',
 | 
			
		||||
        fontSize: '16px',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    basic: {
 | 
			
		||||
      default: createState({
 | 
			
		||||
        textColor: theme.text.baseColor,
 | 
			
		||||
@@ -41,10 +56,10 @@ const createTheme = ({ style }: { style: 'light' | 'dark' }) => {
 | 
			
		||||
        pressedBackground: darken(theme.primary.colorFaded, 30),
 | 
			
		||||
      }),
 | 
			
		||||
      warning: createState({
 | 
			
		||||
        textColor: theme.text.baseColor,
 | 
			
		||||
        backgroundColor: theme.warning.color,
 | 
			
		||||
        hoverBackground: theme.warning.colorHover,
 | 
			
		||||
        pressedBackground: theme.warning.colorPressed,
 | 
			
		||||
        textColor: theme.warning.color,
 | 
			
		||||
        backgroundColor: theme.warning.colorFaded,
 | 
			
		||||
        hoverBackground: lighten(theme.warning.colorFaded, 30),
 | 
			
		||||
        pressedBackground: darken(theme.warning.colorFaded, 30),
 | 
			
		||||
      }),
 | 
			
		||||
    },
 | 
			
		||||
    text: {
 | 
			
		||||
@@ -61,10 +76,10 @@ const createTheme = ({ style }: { style: 'light' | 'dark' }) => {
 | 
			
		||||
        pressedBackground: darken(theme.primary.colorFaded, 30),
 | 
			
		||||
      }),
 | 
			
		||||
      warning: createState({
 | 
			
		||||
        textColor: theme.text.baseColor,
 | 
			
		||||
        backgroundColor: theme.warning.color,
 | 
			
		||||
        hoverBackground: theme.warning.colorHover,
 | 
			
		||||
        pressedBackground: theme.warning.colorPressed,
 | 
			
		||||
        textColor: darken(theme.warning.color, 20),
 | 
			
		||||
        backgroundColor: 'transparent',
 | 
			
		||||
        hoverBackground: theme.warning.colorFaded,
 | 
			
		||||
        pressedBackground: darken(theme.warning.colorFaded, 30),
 | 
			
		||||
      }),
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
 
 | 
			
		||||
@@ -18,13 +18,14 @@ import { useAppTheme } from '../theme/themes';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(
 | 
			
		||||
  defineProps<{
 | 
			
		||||
    type?: 'default' | 'primary';
 | 
			
		||||
    type?: 'default' | 'primary' | 'warning';
 | 
			
		||||
    variant?: 'basic' | 'text';
 | 
			
		||||
    disabled?: boolean;
 | 
			
		||||
    round?: boolean;
 | 
			
		||||
    circle?: boolean;
 | 
			
		||||
    href?: string;
 | 
			
		||||
    to?: RouteLocationRaw;
 | 
			
		||||
    size?: 'small' | 'medium' | 'large';
 | 
			
		||||
  }>(),
 | 
			
		||||
  {
 | 
			
		||||
    type: 'default',
 | 
			
		||||
@@ -34,9 +35,10 @@ const props = withDefaults(
 | 
			
		||||
    circle: false,
 | 
			
		||||
    href: undefined,
 | 
			
		||||
    to: undefined,
 | 
			
		||||
    size: 'medium',
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
const { variant, disabled, round, circle, href, type, to } = toRefs(props);
 | 
			
		||||
const { variant, disabled, round, circle, href, type, to, size: sizeName } = toRefs(props);
 | 
			
		||||
 | 
			
		||||
const emits = defineEmits(['click']);
 | 
			
		||||
 | 
			
		||||
@@ -58,18 +60,20 @@ const tag = computed(() => {
 | 
			
		||||
  return 'button';
 | 
			
		||||
});
 | 
			
		||||
const appTheme = useAppTheme();
 | 
			
		||||
 | 
			
		||||
const size = computed(() => theme.value.size[sizeName.value]);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="less" scoped>
 | 
			
		||||
.c-button {
 | 
			
		||||
  line-height: 1;
 | 
			
		||||
  font-family: inherit;
 | 
			
		||||
  font-size: inherit;
 | 
			
		||||
  font-size: v-bind('size.fontSize');
 | 
			
		||||
  border: none;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  height: 34px;
 | 
			
		||||
  height: v-bind('size.width');
 | 
			
		||||
  font-weight: 400;
 | 
			
		||||
  color: v-bind('variantTheme.textColor');
 | 
			
		||||
  padding: 0 14px;
 | 
			
		||||
@@ -89,8 +93,9 @@ const appTheme = useAppTheme();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.circle {
 | 
			
		||||
    border-radius: 40px;
 | 
			
		||||
    width: 34px;
 | 
			
		||||
    border-radius: v-bind('size.width');
 | 
			
		||||
    width: v-bind('size.width');
 | 
			
		||||
    padding: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:not(.disabled) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										76
									
								
								src/ui/c-input-text/c-input-text.demo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/ui/c-input-text/c-input-text.demo.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <h2>Default</h2>
 | 
			
		||||
 | 
			
		||||
  <c-input-text value="qsd" />
 | 
			
		||||
  <c-input-text
 | 
			
		||||
    value="Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, est modi iusto repellendus fuga accusantium atque at magnam aliquam eum explicabo vero quia, nobis quasi quis! Earum amet quam a?"
 | 
			
		||||
  />
 | 
			
		||||
 | 
			
		||||
  <h2>With placeholder</h2>
 | 
			
		||||
 | 
			
		||||
  <c-input-text placeholder="Placeholder" />
 | 
			
		||||
 | 
			
		||||
  <h2>With label</h2>
 | 
			
		||||
 | 
			
		||||
  <c-input-text label="Label" mb-2 />
 | 
			
		||||
  <c-input-text label="Label" mb-2 label-position="left" />
 | 
			
		||||
  <c-input-text label="Label" mb-2 label-position="left" label-width="100px" />
 | 
			
		||||
  <c-input-text label="Label" mb-2 label-position="left" label-width="100px" label-align="right" />
 | 
			
		||||
 | 
			
		||||
  <h2>Readonly</h2>
 | 
			
		||||
 | 
			
		||||
  <c-input-text value="value" readonly />
 | 
			
		||||
 | 
			
		||||
  <h2>Disabled</h2>
 | 
			
		||||
 | 
			
		||||
  <c-input-text value="value" disabled />
 | 
			
		||||
 | 
			
		||||
  <h2>Validation</h2>
 | 
			
		||||
 | 
			
		||||
  <c-input-text v-model:value="value" :validation-rules="validationRules" mb-2 />
 | 
			
		||||
  <c-input-text v-model:value="value" :validation-rules="validationRules" mb-2 label-position="left" label="Yo " />
 | 
			
		||||
  <c-input-text v-model:value="value" :validation="validation" />
 | 
			
		||||
  <c-input-text v-model:value="value" :validation="validation" multiline rows="3" />
 | 
			
		||||
 | 
			
		||||
  <h2>Clearable</h2>
 | 
			
		||||
 | 
			
		||||
  <c-input-text v-model:value="value" clearable />
 | 
			
		||||
 | 
			
		||||
  <h2>Type password</h2>
 | 
			
		||||
 | 
			
		||||
  <c-input-text value="value" type="password" />
 | 
			
		||||
 | 
			
		||||
  <h2>Multiline</h2>
 | 
			
		||||
 | 
			
		||||
  <c-input-text value="value" multiline label="Label" mb-2 rows="1" />
 | 
			
		||||
  <c-input-text value="value" multiline label="Label" mb-2 />
 | 
			
		||||
  <c-input-text
 | 
			
		||||
    value="Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, est modi iusto repellendus fuga accusantium atque at magnam aliquam eum explicabo vero quia, nobis quasi quis! Earum amet quam a?"
 | 
			
		||||
    multiline
 | 
			
		||||
    mb-2
 | 
			
		||||
  />
 | 
			
		||||
 | 
			
		||||
  <c-input-text v-model:value="valueLong" multiline autosize mb-2 rows="5" />
 | 
			
		||||
 | 
			
		||||
  <c-input-text
 | 
			
		||||
    value="Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, est modi iusto repellendus fuga accusantium atque at magnam aliquam eum explicabo vero quia, nobis quasi quis! Earum amet quam a?"
 | 
			
		||||
    multiline
 | 
			
		||||
    clearable
 | 
			
		||||
  />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { useValidation } from '@/composable/validation';
 | 
			
		||||
 | 
			
		||||
const value = ref('value');
 | 
			
		||||
const valueLong = ref(
 | 
			
		||||
  'Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, est modi iusto repellendus fuga accusantium atque at magnam aliquam eum explicabo vero quia, nobis quasi quis! Earum amet quam a?',
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const validationRules = [{ message: 'Length must be > 10', validator: (value: string) => value.length > 10 }];
 | 
			
		||||
 | 
			
		||||
const validation = useValidation({
 | 
			
		||||
  source: value,
 | 
			
		||||
  rules: validationRules,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										160
									
								
								src/ui/c-input-text/c-input-text.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								src/ui/c-input-text/c-input-text.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,160 @@
 | 
			
		||||
import { describe, expect, it, beforeEach } from 'vitest';
 | 
			
		||||
import { shallowMount, mount } from '@vue/test-utils';
 | 
			
		||||
import { setActivePinia, createPinia } from 'pinia';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import { useValidation } from '@/composable/validation';
 | 
			
		||||
import CInputText from './c-input-text.vue';
 | 
			
		||||
 | 
			
		||||
describe('CInputText', () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    setActivePinia(createPinia());
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('Renders a label', () => {
 | 
			
		||||
    const wrapper = shallowMount(CInputText, {
 | 
			
		||||
      props: {
 | 
			
		||||
        label: 'Label',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(wrapper.get('.label').text()).to.equal('Label');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('Renders a placeholder', () => {
 | 
			
		||||
    const wrapper = shallowMount(CInputText, {
 | 
			
		||||
      props: {
 | 
			
		||||
        placeholder: 'Placeholder',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(wrapper.get('.input').attributes('placeholder')).to.equal('Placeholder');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('Renders a value', () => {
 | 
			
		||||
    const wrapper = shallowMount(CInputText, {
 | 
			
		||||
      props: {
 | 
			
		||||
        value: 'Value',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(wrapper.vm.value).to.equal('Value');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('Renders a provided id', () => {
 | 
			
		||||
    const wrapper = shallowMount(CInputText, {
 | 
			
		||||
      props: {
 | 
			
		||||
        id: 'id',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(wrapper.get('.input').attributes('id')).to.equal('id');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('updates value on input', async () => {
 | 
			
		||||
    const wrapper = shallowMount(CInputText);
 | 
			
		||||
 | 
			
		||||
    await wrapper.get('input').setValue('Hello');
 | 
			
		||||
 | 
			
		||||
    expect(_.get(wrapper.emitted(), 'update:value.0.0')).to.equal('Hello');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('cannot be edited when disabled', async () => {
 | 
			
		||||
    const wrapper = shallowMount(CInputText, {
 | 
			
		||||
      props: {
 | 
			
		||||
        disabled: true,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await wrapper.get('input').setValue('Hello');
 | 
			
		||||
 | 
			
		||||
    expect(_.get(wrapper.emitted(), 'update:value')).toBeUndefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('renders a feedback message for invalid rules', async () => {
 | 
			
		||||
    const wrapper = shallowMount(CInputText, {
 | 
			
		||||
      props: { validationRules: [{ validator: () => false, message: 'Message' }] },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const feedback = wrapper.find('.feedback');
 | 
			
		||||
    expect(feedback.exists()).to.equal(true);
 | 
			
		||||
    expect(feedback.text()).to.equal('Message');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('if the value become valid according to rules, the feedback disappear', async () => {
 | 
			
		||||
    const wrapper = shallowMount(CInputText, {
 | 
			
		||||
      props: {
 | 
			
		||||
        validationRules: [{ validator: (value: string) => value === 'Hello', message: 'Value should be Hello' }],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const feedback = wrapper.find('.feedback');
 | 
			
		||||
    expect(feedback.exists()).to.equal(true);
 | 
			
		||||
    expect(feedback.text()).to.equal('Value should be Hello');
 | 
			
		||||
 | 
			
		||||
    await wrapper.setProps({ value: 'Hello' });
 | 
			
		||||
 | 
			
		||||
    expect(wrapper.find('.feedback').exists()).to.equal(false);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('feedback does not render for valid rules', async () => {
 | 
			
		||||
    const wrapper = shallowMount(CInputText, {
 | 
			
		||||
      props: { rules: [{ validator: () => true, message: 'Message' }] },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(wrapper.find('.feedback').exists()).to.equal(false);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('renders a feedback message for invalid custom validation wrapper', async () => {
 | 
			
		||||
    const wrapper = shallowMount(CInputText, {
 | 
			
		||||
      props: {
 | 
			
		||||
        validation: useValidation({ source: ref(), rules: [{ validator: () => false, message: 'Message' }] }),
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const feedback = wrapper.find('.feedback');
 | 
			
		||||
    expect(feedback.exists()).to.equal(true);
 | 
			
		||||
    expect(feedback.text()).to.equal('Message');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('feedback does not render for valid custom validation wrapper', async () => {
 | 
			
		||||
    const wrapper = shallowMount(CInputText, {
 | 
			
		||||
      props: {
 | 
			
		||||
        validation: useValidation({ source: ref(), rules: [{ validator: () => true, message: 'Message' }] }),
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    expect(wrapper.find('.feedback').exists()).to.equal(false);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('if the value become valid according to the custom validation wrapper, the feedback disappear', async () => {
 | 
			
		||||
    const source = ref('');
 | 
			
		||||
 | 
			
		||||
    const wrapper = shallowMount(CInputText, {
 | 
			
		||||
      props: {
 | 
			
		||||
        validation: useValidation({
 | 
			
		||||
          source,
 | 
			
		||||
          rules: [{ validator: (value: string) => value === 'Hello', message: 'Value should be Hello' }],
 | 
			
		||||
        }),
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const feedback = wrapper.find('.feedback');
 | 
			
		||||
    expect(feedback.exists()).to.equal(true);
 | 
			
		||||
    expect(feedback.text()).to.equal('Value should be Hello');
 | 
			
		||||
 | 
			
		||||
    source.value = 'Hello';
 | 
			
		||||
 | 
			
		||||
    await wrapper.vm.$nextTick();
 | 
			
		||||
 | 
			
		||||
    expect(wrapper.find('.feedback').exists()).to.equal(false);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('[prop:testId] renders a test id on the input', async () => {
 | 
			
		||||
    const wrapper = mount(CInputText, {
 | 
			
		||||
      props: {
 | 
			
		||||
        testId: 'TEST',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(wrapper.get('input').attributes('data-test-id')).to.equal('TEST');
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										20
									
								
								src/ui/c-input-text/c-input-text.theme.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/ui/c-input-text/c-input-text.theme.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
import { defineThemes } from '../theme/theme.models';
 | 
			
		||||
 | 
			
		||||
export const { useTheme } = defineThemes({
 | 
			
		||||
  dark: {
 | 
			
		||||
    backgroundColor: '#333333',
 | 
			
		||||
    borderColor: '#333333',
 | 
			
		||||
 | 
			
		||||
    focus: {
 | 
			
		||||
      backgroundColor: '#1ea54c1a',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  light: {
 | 
			
		||||
    backgroundColor: '#ffffff',
 | 
			
		||||
    borderColor: '#e0e0e69e',
 | 
			
		||||
 | 
			
		||||
    focus: {
 | 
			
		||||
      backgroundColor: '#ffffff',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										296
									
								
								src/ui/c-input-text/c-input-text.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										296
									
								
								src/ui/c-input-text/c-input-text.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,296 @@
 | 
			
		||||
<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>
 | 
			
		||||
@@ -18,6 +18,8 @@
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div flex-1 pl-4>
 | 
			
		||||
        <h1>{{ componentName }}</h1>
 | 
			
		||||
 | 
			
		||||
        <router-view />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -25,9 +27,12 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import { demoRoutes } from './demo.routes';
 | 
			
		||||
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
 | 
			
		||||
const componentName = computed(() => _.startCase(String(route.name).replace(/^c-/, '')));
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="less" scoped></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,6 @@ export const demoRoutes = Object.keys(demoPages).map((path) => {
 | 
			
		||||
  const [, , fileName] = path.split('/');
 | 
			
		||||
  const name = fileName.split('.').shift();
 | 
			
		||||
 | 
			
		||||
  console.log(path);
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    path: name,
 | 
			
		||||
    name,
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ export const { themes: appThemes, useTheme: useAppTheme } = defineThemes({
 | 
			
		||||
      color: '#f59e0b',
 | 
			
		||||
      colorHover: '#f59e0b',
 | 
			
		||||
      colorPressed: '#f59e0b',
 | 
			
		||||
      colorFaded: '#f59e0b2f',
 | 
			
		||||
    },
 | 
			
		||||
    success: {
 | 
			
		||||
      color: '#18a058',
 | 
			
		||||
@@ -55,6 +56,7 @@ export const { themes: appThemes, useTheme: useAppTheme } = defineThemes({
 | 
			
		||||
      color: '#f59e0b',
 | 
			
		||||
      colorHover: '#f59e0b',
 | 
			
		||||
      colorPressed: '#f59e0b',
 | 
			
		||||
      colorFaded: '#f59e0b2f',
 | 
			
		||||
    },
 | 
			
		||||
    success: {
 | 
			
		||||
      color: '#18a058',
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								src/utils/array.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/utils/array.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
export { byOrder };
 | 
			
		||||
 | 
			
		||||
function byOrder({ order }: { order: 'asc' | 'desc' | null | undefined }) {
 | 
			
		||||
  return (a: string, b: string) => {
 | 
			
		||||
    return order === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -18,4 +18,14 @@ const shuffleArray = <T>(array: T[]): T[] => shuffleArrayMutate([...array]);
 | 
			
		||||
 | 
			
		||||
const shuffleString = (str: string, delimiter = ''): string => shuffleArrayMutate(str.split(delimiter)).join(delimiter);
 | 
			
		||||
 | 
			
		||||
export { randFromArray, randIntFromInterval, random, shuffleArray, shuffleArrayMutate, shuffleString };
 | 
			
		||||
const generateRandomId = () => `id-${random().toString(36).substring(2, 12)}`;
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  randFromArray,
 | 
			
		||||
  randIntFromInterval,
 | 
			
		||||
  random,
 | 
			
		||||
  shuffleArray,
 | 
			
		||||
  shuffleArrayMutate,
 | 
			
		||||
  shuffleString,
 | 
			
		||||
  generateRandomId,
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,6 @@
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": ["./src/*"]
 | 
			
		||||
    },
 | 
			
		||||
    "types": ["naive-ui/volar"]
 | 
			
		||||
    "types": ["naive-ui/volar", "unplugin-icons/types/vue"]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,9 @@ import AutoImport from 'unplugin-auto-import/vite';
 | 
			
		||||
import Components from 'unplugin-vue-components/vite';
 | 
			
		||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
 | 
			
		||||
import Unocss from 'unocss/vite';
 | 
			
		||||
import { configDefaults } from 'vitest/config';
 | 
			
		||||
import Icons from 'unplugin-icons/vite';
 | 
			
		||||
import IconsResolver from 'unplugin-icons/resolver';
 | 
			
		||||
 | 
			
		||||
// https://vitejs.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
@@ -28,7 +31,7 @@ export default defineConfig({
 | 
			
		||||
        enabled: true,
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
 | 
			
		||||
    Icons({ compiler: 'vue3' }),
 | 
			
		||||
    vue({
 | 
			
		||||
      include: [/\.vue$/, /\.md$/],
 | 
			
		||||
    }),
 | 
			
		||||
@@ -76,7 +79,7 @@ export default defineConfig({
 | 
			
		||||
      dirs: ['src/'],
 | 
			
		||||
      extensions: ['vue', 'md'],
 | 
			
		||||
      include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
 | 
			
		||||
      resolvers: [NaiveUiResolver()],
 | 
			
		||||
      resolvers: [NaiveUiResolver(), IconsResolver({ prefix: 'icon' })],
 | 
			
		||||
    }),
 | 
			
		||||
    Unocss(),
 | 
			
		||||
  ],
 | 
			
		||||
@@ -88,4 +91,7 @@ export default defineConfig({
 | 
			
		||||
  define: {
 | 
			
		||||
    'import.meta.env.PACKAGE_VERSION': JSON.stringify(process.env.npm_package_version),
 | 
			
		||||
  },
 | 
			
		||||
  test: {
 | 
			
		||||
    exclude: [...configDefaults.exclude, '**/*.e2e.spec.ts'],
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
import { configDefaults, defineConfig } from 'vitest/config';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  resolve: {
 | 
			
		||||
    alias: {
 | 
			
		||||
      '@': path.resolve(__dirname, './src'),
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  test: {
 | 
			
		||||
    exclude: [...configDefaults.exclude, '**/*.e2e.spec.ts'],
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
		Reference in New Issue
	
	Block a user