mirror of
				https://github.com/CorentinTh/it-tools.git
				synced 2025-10-30 19:43:50 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			update-dep
			...
			svg-mesh-g
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | a8c1231753 | 
| @@ -22,9 +22,7 @@ | ||||
|     "createGlobalState": true, | ||||
|     "createInjectionState": true, | ||||
|     "createReactiveFn": true, | ||||
|     "createReusableTemplate": true, | ||||
|     "createSharedComposable": true, | ||||
|     "createTemplatePromise": true, | ||||
|     "createUnrefFn": true, | ||||
|     "customRef": true, | ||||
|     "debouncedRef": true, | ||||
| @@ -44,6 +42,9 @@ | ||||
|     "isReactive": true, | ||||
|     "isReadonly": true, | ||||
|     "isRef": true, | ||||
|     "logicAnd": true, | ||||
|     "logicNot": true, | ||||
|     "logicOr": true, | ||||
|     "makeDestructurable": true, | ||||
|     "markRaw": true, | ||||
|     "nextTick": true, | ||||
| @@ -106,19 +107,6 @@ | ||||
|     "unrefElement": true, | ||||
|     "until": true, | ||||
|     "useActiveElement": true, | ||||
|     "useAnimate": true, | ||||
|     "useArrayDifference": true, | ||||
|     "useArrayEvery": true, | ||||
|     "useArrayFilter": true, | ||||
|     "useArrayFind": true, | ||||
|     "useArrayFindIndex": true, | ||||
|     "useArrayFindLast": true, | ||||
|     "useArrayIncludes": true, | ||||
|     "useArrayJoin": true, | ||||
|     "useArrayMap": true, | ||||
|     "useArrayReduce": true, | ||||
|     "useArraySome": true, | ||||
|     "useArrayUnique": true, | ||||
|     "useAsyncQueue": true, | ||||
|     "useAsyncState": true, | ||||
|     "useAttrs": true, | ||||
| @@ -129,8 +117,8 @@ | ||||
|     "useBroadcastChannel": true, | ||||
|     "useBrowserLocation": true, | ||||
|     "useCached": true, | ||||
|     "useClamp": true, | ||||
|     "useClipboard": true, | ||||
|     "useCloned": true, | ||||
|     "useColorMode": true, | ||||
|     "useConfirmDialog": true, | ||||
|     "useCounter": true, | ||||
| @@ -172,7 +160,6 @@ | ||||
|     "useFullscreen": true, | ||||
|     "useGamepad": true, | ||||
|     "useGeolocation": true, | ||||
|     "useI18n": true, | ||||
|     "useIdle": true, | ||||
|     "useImage": true, | ||||
|     "useInfiniteScroll": true, | ||||
| @@ -205,18 +192,12 @@ | ||||
|     "useOnline": true, | ||||
|     "usePageLeave": true, | ||||
|     "useParallax": true, | ||||
|     "useParentElement": true, | ||||
|     "usePerformanceObserver": true, | ||||
|     "usePermission": true, | ||||
|     "usePointer": true, | ||||
|     "usePointerLock": true, | ||||
|     "usePointerSwipe": true, | ||||
|     "usePreferredColorScheme": true, | ||||
|     "usePreferredContrast": true, | ||||
|     "usePreferredDark": true, | ||||
|     "usePreferredLanguages": true, | ||||
|     "usePreferredReducedMotion": true, | ||||
|     "usePrevious": true, | ||||
|     "useRafFn": true, | ||||
|     "useRefHistory": true, | ||||
|     "useResizeObserver": true, | ||||
| @@ -230,17 +211,14 @@ | ||||
|     "useSessionStorage": true, | ||||
|     "useShare": true, | ||||
|     "useSlots": true, | ||||
|     "useSorted": true, | ||||
|     "useSpeechRecognition": true, | ||||
|     "useSpeechSynthesis": true, | ||||
|     "useStepper": true, | ||||
|     "useStorage": true, | ||||
|     "useStorageAsync": true, | ||||
|     "useStyleTag": true, | ||||
|     "useSupported": true, | ||||
|     "useSwipe": true, | ||||
|     "useTemplateRefsList": true, | ||||
|     "useTextDirection": true, | ||||
|     "useTextSelection": true, | ||||
|     "useTextareaAutosize": true, | ||||
|     "useThrottle": true, | ||||
| @@ -252,8 +230,6 @@ | ||||
|     "useTimeoutPoll": true, | ||||
|     "useTimestamp": true, | ||||
|     "useTitle": true, | ||||
|     "useToNumber": true, | ||||
|     "useToString": true, | ||||
|     "useToggle": true, | ||||
|     "useTransition": true, | ||||
|     "useUrlSearchParams": true, | ||||
| @@ -274,10 +250,8 @@ | ||||
|     "watchArray": true, | ||||
|     "watchAtMost": true, | ||||
|     "watchDebounced": true, | ||||
|     "watchDeep": true, | ||||
|     "watchEffect": true, | ||||
|     "watchIgnorable": true, | ||||
|     "watchImmediate": true, | ||||
|     "watchOnce": true, | ||||
|     "watchPausable": true, | ||||
|     "watchPostEffect": true, | ||||
| @@ -285,7 +259,6 @@ | ||||
|     "watchThrottled": true, | ||||
|     "watchTriggerable": true, | ||||
|     "watchWithFilter": true, | ||||
|     "whenever": true, | ||||
|     "toValue": true | ||||
|     "whenever": true | ||||
|   } | ||||
| } | ||||
| } | ||||
| @@ -1,21 +1,39 @@ | ||||
| /** | ||||
|  * @type {import('eslint').Linter.Config} | ||||
|  */ | ||||
| /* eslint-env node */ | ||||
| require('@rushstack/eslint-patch/modern-module-resolution'); | ||||
|  | ||||
| module.exports = { | ||||
|   root: true, | ||||
|   extends: ['@antfu', './.eslintrc-auto-import.json', '@unocss'], | ||||
|   extends: [ | ||||
|     'plugin:vue/vue3-essential', | ||||
|     'eslint:recommended', | ||||
|     'plugin:vue/vue3-recommended', | ||||
|     '@vue/eslint-config-typescript/recommended', | ||||
|     '@vue/eslint-config-prettier', | ||||
|     'plugin:import/recommended', | ||||
|     './.eslintrc-auto-import.json', | ||||
|     '@unocss', | ||||
|   ], | ||||
|  | ||||
|   settings: { | ||||
|     'import/resolver': { typescript: { project: './tsconfig.app.json' } }, | ||||
|   }, | ||||
|   env: { | ||||
|     'vue/setup-compiler-macros': true, | ||||
|   }, | ||||
|   rules: { | ||||
|     'curly': ['error', 'all'], | ||||
|     '@typescript-eslint/semi': ['error', 'always'], | ||||
|     '@typescript-eslint/no-use-before-define': ['error', { allowNamedExports: true, functions: false }], | ||||
|     'vue/no-empty-component-block': ['error'], | ||||
|     'no-restricted-imports': ['error', { | ||||
|       paths: [{ | ||||
|         name: '@vueuse/core', | ||||
|         importNames: ['useClipboard'], | ||||
|         message: 'Please use local useCopy from src/composable/copy.ts instead of useClipboard.', | ||||
|       }], | ||||
|     }], | ||||
|     'vue/multi-word-component-names': ['off'], | ||||
|     'prettier/prettier': ['error'], | ||||
|     'import/no-duplicates': ['error', { considerQueryString: true }], | ||||
|     'import/order': ['error', { groups: [['builtin', 'external', 'internal']] }], | ||||
|     'import/extensions': [ | ||||
|       'error', | ||||
|       'ignorePackages', | ||||
|       { | ||||
|         js: 'never', | ||||
|         ts: 'never', | ||||
|         tsx: 'never', | ||||
|       }, | ||||
|     ], | ||||
|     'import/no-unresolved': ['error', { ignore: ['^virtual:'] }], | ||||
|   }, | ||||
| }; | ||||
|   | ||||
							
								
								
									
										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 | ||||
| --- | ||||
|  | ||||
| **What tool do you want?** | ||||
| Example: a token generator | ||||
| **Which tool is impacted?** | ||||
| Example: the token generator | ||||
|  | ||||
| **Describe the solution you'd like** | ||||
| A clear and concise description of what you want to happen. | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -31,6 +31,4 @@ jobs: | ||||
|         run: pnpm typecheck | ||||
|  | ||||
|       - name: Build the app | ||||
|         env: | ||||
|           NODE_OPTIONS: --max-old-space-size=4096 | ||||
|         run: pnpm build | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -37,11 +37,11 @@ jobs: | ||||
|  | ||||
|     steps: | ||||
|     - name: Checkout repository | ||||
|       uses: actions/checkout@v3 | ||||
|       uses: actions/checkout@v2 | ||||
|  | ||||
|     # Initializes the CodeQL tools for scanning. | ||||
|     - name: Initialize CodeQL | ||||
|       uses: github/codeql-action/init@v2 | ||||
|       uses: github/codeql-action/init@v1 | ||||
|       with: | ||||
|         languages: ${{ matrix.language }} | ||||
|         # If you wish to specify custom queries, you can do so here or in a config file. | ||||
| @@ -52,7 +52,7 @@ jobs: | ||||
|     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java). | ||||
|     # If this step fails, then you should remove it and run the build manually (see below) | ||||
|     - name: Autobuild | ||||
|       uses: github/codeql-action/autobuild@v2 | ||||
|       uses: github/codeql-action/autobuild@v1 | ||||
|  | ||||
|     # ℹ️ Command-line programs to run using the OS shell. | ||||
|     # 📚 https://git.io/JvXDl | ||||
| @@ -66,4 +66,4 @@ jobs: | ||||
|     #   make release | ||||
|  | ||||
|     - name: Perform CodeQL Analysis | ||||
|       uses: github/codeql-action/analyze@v2 | ||||
|       uses: github/codeql-action/analyze@v1 | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/docker-nightly-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/docker-nightly-release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -12,7 +12,7 @@ jobs: | ||||
|     outputs: | ||||
|       should_run: ${{ steps.should_run.outputs.should_run }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: print latest_commit | ||||
|         run: echo ${{ github.sha }} | ||||
|  | ||||
| @@ -57,7 +57,7 @@ jobs: | ||||
|         uses: actions/checkout@v3 | ||||
|  | ||||
|       - name: Login to GitHub Container Registry | ||||
|         uses: docker/login-action@v2 | ||||
|         uses: docker/login-action@v1 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.repository_owner }} | ||||
|   | ||||
							
								
								
									
										44
									
								
								.github/workflows/e2e-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										44
									
								
								.github/workflows/e2e-tests.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,44 +0,0 @@ | ||||
| name: E2E tests | ||||
| on: [deployment_status] | ||||
|  | ||||
| jobs: | ||||
|   test: | ||||
|     if: github.event.deployment_status.state == 'success' | ||||
|     timeout-minutes: 60 | ||||
|     runs-on: ubuntu-latest | ||||
|     env: | ||||
|       BASE_URL: ${{ github.event.deployment_status.target_url }} | ||||
|     strategy: | ||||
|       matrix: | ||||
|         shard: [1/3, 2/3, 3/3] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|  | ||||
|       - run: corepack enable | ||||
|  | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: 16 | ||||
|           cache: 'pnpm' | ||||
|  | ||||
|       - name: Get Playwright version | ||||
|         id: playwright-version | ||||
|         run: echo "PLAYWRIGHT_VERSION=$(jq -r .dependencies.playwright package.json)" >> "$GITHUB_OUTPUT" | ||||
|  | ||||
|       - name: Install dependencies | ||||
|         run: pnpm install | ||||
|  | ||||
|       - name: Restore Playwright browsers from cache | ||||
|         uses: actions/cache@v3 | ||||
|         with: | ||||
|           path: ~/.cache/ms-playwright | ||||
|           key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}-${{ hashFiles('**/playwright.config.ts') }} | ||||
|           restore-keys: | | ||||
|             ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}- | ||||
|             ${{ runner.os }}-playwright- | ||||
|  | ||||
|       - name: Install Playwright Browsers | ||||
|         run: pnpm exec playwright install --with-deps | ||||
|  | ||||
|       - name: Run Playwright tests | ||||
|         run: pnpm run test:e2e --shard=${{ matrix.shard }} | ||||
							
								
								
									
										23
									
								
								.github/workflows/playwright.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.github/workflows/playwright.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| name: E2E tests | ||||
| on: | ||||
|   pull_request: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
| jobs: | ||||
|   test: | ||||
|     timeout-minutes: 60 | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - run: corepack enable | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: 16 | ||||
|           cache: 'pnpm' | ||||
|       - name: Install dependencies | ||||
|         run: pnpm install | ||||
|       - name: Install Playwright Browsers | ||||
|         run: pnpm exec playwright install --with-deps | ||||
|       - name: Run Playwright tests | ||||
|         run: pnpm exec playwright test | ||||
							
								
								
									
										2
									
								
								.github/workflows/releases.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/releases.yml
									
									
									
									
										vendored
									
									
								
							| @@ -16,7 +16,7 @@ jobs: | ||||
|         uses: actions/checkout@v3 | ||||
|  | ||||
|       - name: Login to GitHub Container Registry | ||||
|         uses: docker/login-action@v2 | ||||
|         uses: docker/login-action@v1 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.repository_owner }} | ||||
|   | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -31,5 +31,3 @@ coverage | ||||
| /test-results/ | ||||
| /playwright-report/ | ||||
| /playwright/.cache/ | ||||
| # Webkit with playwright creates a salt file | ||||
| salt | ||||
							
								
								
									
										2
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,3 @@ | ||||
| { | ||||
|   "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin", "dbaeumer.vscode-eslint", "lokalise.i18n-ally"] | ||||
|   "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin", "dbaeumer.vscode-eslint"] | ||||
| } | ||||
|   | ||||
							
								
								
									
										138
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										138
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -2,144 +2,6 @@ | ||||
|  | ||||
| All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. | ||||
|  | ||||
| ## Version 2023.08.21-6f93cba | ||||
|  | ||||
| ### Features | ||||
| - **copy**: support legacy copy to clipboard for older browser (#581) (6f93cba) | ||||
| - **new tool**: string obfuscator (#575) (c58d6e3) | ||||
|  | ||||
| ### Bug fixes | ||||
| - **deps**: update dependency sql-formatter to v12 (#520) (2bcb77a) | ||||
|  | ||||
| ### Chores | ||||
| - **deps**: switched to fucking typescript v5 (#501) (76b2761) | ||||
| - **deps**: update dependency @antfu/eslint-config to ^0.40.0 (#552) (6ff9a01) | ||||
| - **deps**: update dependency prettier to v3 (#564) (a2b9b15) | ||||
| - **deps**: removed @typescript-eslint/parser (#563) (144f86e) | ||||
| - **deps**: removed ts-pattern (#565) (0f1f659) | ||||
|  | ||||
| ## Version 2023.08.16-9bd4ad4 | ||||
|  | ||||
| ### Features | ||||
| - **Case Converter**: Add lowercase and uppercase (#534) (7b6232a) | ||||
| - **new tool**: emoji picker (#551) (93f7cf0) | ||||
| - **ui**: added c-select in the ui lib (#550) (dfa1ba8) | ||||
| - **new-tool**: password strength analyzer (#502) (a9c7b89) | ||||
| - **new-tool**: yaml to toml (e29b258) | ||||
| - **new-tool**: json to toml (ea50a3f) | ||||
| - **new-tool**: toml to yaml (746e5bd) | ||||
| - **new-tool**: toml to json (c7d4f11) | ||||
| - **command-palette**: random tool action (ec4c533) | ||||
| - **config**: allow app to run in a subfolder via BASE_URL (#461) (6304595) | ||||
| - **new-tool**: percentage calculator (#456) (b9406a4) | ||||
| - **new-tool**: json to csv converter (69f0bd0) | ||||
| - **new tool**: xml formatter (#457) (a6bbeae) | ||||
| - **chmod-calculator**: added symbolic representation (#455) (f771e7a) | ||||
| - **enhancement**: use system dark mode (#458) (cf7b1f0) | ||||
| - **phone-parser**: searchable country code select (d2956b6) | ||||
| - **new tool**: camera screenshot and recorder (34d8e5c) | ||||
| - **base64-string-converter**: switch to encode and decode url safe base64 strings (#392) (0b20f1c) | ||||
|  | ||||
| ### Bug fixes | ||||
| - **deps**: update dependency uuid to v9 (#566) (5e12991) | ||||
| - **deps**: update dependency mathjs to v11 (#519) (7924456) | ||||
| - **deps**: update dependency @vueuse/router to v10 (#516) (ea0f27c) | ||||
| - **copy**: prevent shorthand copy if source is present in useCopy (#559) (86e964a) | ||||
| - **c-lib**: hide component library shortcut link in non-dev (#557) (56d74d0) | ||||
| - **emoji picker**: fix copy button (#556) (e5d0ba7) | ||||
| - **deps**: update dependency @vueuse/head to v1 (#515) (d12dd40) | ||||
| - **deps**: update dependency country-code-lookup to ^0.1.0 (#493) (8c72e69) | ||||
| - **deps**: update dependency @vueuse/head to ^0.9.0 (#492) (cec9dea) | ||||
| - **i18n**: fallback for demo i18n (12d9e5d) | ||||
| - **typos**: fixed more typos & uppercase JSON (#475) (9526ed8) | ||||
| - **about**: typos and wording (#474) (7068610) | ||||
| - **mime-types**: typos (#470) (c4cec9e) | ||||
| - **sonar**: took down minor sonar warning (4cbd7ac) | ||||
| - **readme**: typo (105b21b) | ||||
| - **ipv4-range-expander**: calculate correct for ip addresses where the first octet is lower than 128 (#405) (8c92d56) | ||||
| - **ipv4-converter**: removed readonly on input (7aed9c5) | ||||
|  | ||||
| ### Refactoring | ||||
| - **navbar**: consistent spacing in navbar buttons (#507) (30f88fc) | ||||
| - **ui**: remove n-text (#506) (72c98a3) | ||||
| - **ui**: replaced some n-input to c-input (#505) (05ea545) | ||||
| - **json-viewer**: input monospace font (#485) (9125dcf) | ||||
| - **search**: command palette design (#463) (bcb98b3) | ||||
| - **c-input-text**: force usage of props with default (1e2a35b) | ||||
| - **naming**: prevent auto import conflicts for git memo (45c2474) | ||||
| - **imports**: removed unnecessary imports to vue (fe61f0f) | ||||
| - **ui**: removed all n-space (4d2b037) | ||||
| - **ui**: replaced some n-input with c-input-text (f7fc779) | ||||
|  | ||||
| ### Chores | ||||
| - **deps**: update dependency vitest to ^0.34.0 (#562) (9bd4ad4) | ||||
| - **deps**: update dependency node to v18.17.1 (#560) (65a9474) | ||||
| - **deps**: update dependency unocss to ^0.55.0 (#561) (85cc7a8) | ||||
| - **deps**: update dependency @unocss/eslint-config to ^0.55.0 (#553) (4268e25) | ||||
| - **deps**: update dependency @intlify/unplugin-vue-i18n to ^0.12.0 (#526) (d1c8880) | ||||
| - **deps**: update docker/login-action action to v2 (#512) (99bc84c) | ||||
| - **deps**: update dependency jsdom to v22 (#499) (cd5a503) | ||||
| - **deps**: update dependency @vitejs/plugin-vue-jsx to v3 (#497) (1a60236) | ||||
| - **deps**: update dependency @vitejs/plugin-vue to v4 (#496) (a249421) | ||||
| - **deps**: update dependency vite-plugin-pwa to ^0.16.0 (#488) (6498c9b) | ||||
| - **deps**: update dependency vite to v4 (#503) (f40d7ec) | ||||
| - **ci**: e2e against vercel deployement (#518) (2e28c50) | ||||
| - **e2e**: execute e2e against built app (#511) (cf382b5) | ||||
| - **deps**: update github/codeql-action action to v2 (#513) (0152583) | ||||
| - **deps**: update node.js to v18 (#514) (38cb61d) | ||||
| - **deps**: switched from vite-plugin-md to vite-plugin-vue-markdown (#510) (354aed6) | ||||
| - **deps**: update dependency workbox-window to v7 (#509) (6b8682f) | ||||
| - **deps**: update dependency vite-svg-loader to v4 (#508) (9e8349d) | ||||
| - **deps**: update dependency typescript to ~4.9.0 (#481) (f440507) | ||||
| - **deps**: update dependency vue-tsc to ^0.40.0 (#490) (b0d9a3e) | ||||
| - **deps**: updated unplugin-auto-import (#504) (5c3bebf) | ||||
| - **deps**: removed start-server-and-test dependency (8df7cd0) | ||||
| - **deps**: update dependency c8 to v8 (#498) (6bda2ca) | ||||
| - **deps**: update dependency @types/jsdom to v21 (#495) (994a1c3) | ||||
| - **deps**: update node.js to v16.20.1 (#491) (05edaf4) | ||||
| - **deps**: update dependency vitest to ^0.32.0 (#489) (49eacea) | ||||
| - **deps**: update actions/checkout action to v3 (#494) (3f7d469) | ||||
| - **deps**: update dependency unplugin-vue-components to ^0.25.0 (#484) (5f21908) | ||||
| - **deps**: update dependency unplugin-auto-import to ^0.16.0 (#483) (6cb0845) | ||||
| - **deps**: update dependency unocss to ^0.53.0 (#482) (38710dc) | ||||
| - **deps**: update dependency @unocss/eslint-config to ^0.53.0 (#478) (282cfc4) | ||||
| - **deps**: added renovate.json (#477) (363c2e4) | ||||
| - **i18n**: tool scoped locales (#471) (1b038c7) | ||||
| - **wysiwyg-editor**: update tiptap dependencies (732da08) | ||||
| - **i18n**: setup i18n plugin config (ebfb872) | ||||
| - **config**: netlify deployment support (#443) (93799af) | ||||
| - **ci**: shard e2e tests (962a6d6) | ||||
| - **lint**: switched to a better lint config (33c9b66) | ||||
|  | ||||
| ### Refacor | ||||
| - **transformers**: use monospace font for JSON and SQL text areas (#476) (ba4876d) | ||||
|  | ||||
| ### Documentation | ||||
| - **ide**: updated vscode extensions settings (#472) (847323c) | ||||
|  | ||||
| ### Chors | ||||
| - **deps**: updated vueuse dependency version (8515c24) | ||||
|  | ||||
| ## 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 | ||||
|   | ||||
							
								
								
									
										25
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								README.md
									
									
									
									
									
								
							| @@ -6,7 +6,7 @@ Useful tools for developer and people working in IT. [Have a look !](https://it- | ||||
|  | ||||
| Please check the [issues](https://github.com/CorentinTh/it-tools/issues) to see if some feature listed to be implemented. | ||||
|  | ||||
| You have an idea of a tool? Submit a [feature request](https://github.com/CorentinTh/it-tools/issues/new/choose)! | ||||
| You have an idea of a tool? Submit a [feature request](https://github.com/CorentinTh/it-tools/issues/new?assignees=corentinth&labels=&template=feature_request.md&title=)! | ||||
|  | ||||
| ## Self host | ||||
|  | ||||
| @@ -26,7 +26,6 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent | ||||
|  | ||||
| **Other solutions:** | ||||
|  | ||||
| - [Cloudron](https://www.cloudron.io/store/tech.ittools.cloudron.html) | ||||
| - [Tipi](https://www.runtipi.io/docs/apps-available) | ||||
| - [Unraid](https://unraid.net/community/apps?q=it-tools) | ||||
|  | ||||
| @@ -34,25 +33,7 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent | ||||
|  | ||||
| ### Recommended IDE Setup | ||||
|  | ||||
| [VSCode](https://code.visualstudio.com/) with the following extensions: | ||||
|  | ||||
| - [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) | ||||
| - [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). | ||||
| - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) | ||||
| - [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) | ||||
|  | ||||
| with the following settings: | ||||
|  | ||||
| ```json | ||||
| { | ||||
|   "editor.formatOnSave": false, | ||||
|   "editor.codeActionsOnSave": { | ||||
|     "source.fixAll.eslint": true | ||||
|   }, | ||||
|   "i18n-ally.localesPaths": ["locales", "src/tools/*/locales"], | ||||
|   "i18n-ally.keystyle": "nested" | ||||
| } | ||||
| ``` | ||||
| [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). | ||||
|  | ||||
| ### Type Support for `.vue` Imports in TS | ||||
|  | ||||
| @@ -103,7 +84,7 @@ To create a new tool, there is a script that generate the boilerplate of the new | ||||
| pnpm run script:create-new-tool my-tool-name | ||||
| ``` | ||||
|  | ||||
| It will create a directory in `src/tools` with the correct files, and a the import in `src/tools/index.ts`. You will just need to add the imported tool in the proper category and develop the tool. | ||||
| It will create a directory in `src/tools` with the correct files, and a the import in `src/tools/index.ts`. You will just need to add the inported tool in the proper category and develop the tool. | ||||
|  | ||||
| ## Credits | ||||
|  | ||||
|   | ||||
							
								
								
									
										354
									
								
								auto-imports.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										354
									
								
								auto-imports.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -19,9 +19,7 @@ declare global { | ||||
|   const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] | ||||
|   const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] | ||||
|   const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] | ||||
|   const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] | ||||
|   const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] | ||||
|   const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] | ||||
|   const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] | ||||
|   const customRef: typeof import('vue')['customRef'] | ||||
|   const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] | ||||
| @@ -41,6 +39,9 @@ declare global { | ||||
|   const isReactive: typeof import('vue')['isReactive'] | ||||
|   const isReadonly: typeof import('vue')['isReadonly'] | ||||
|   const isRef: typeof import('vue')['isRef'] | ||||
|   const logicAnd: typeof import('@vueuse/core')['logicAnd'] | ||||
|   const logicNot: typeof import('@vueuse/core')['logicNot'] | ||||
|   const logicOr: typeof import('@vueuse/core')['logicOr'] | ||||
|   const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] | ||||
|   const markRaw: typeof import('vue')['markRaw'] | ||||
|   const nextTick: typeof import('vue')['nextTick'] | ||||
| @@ -93,7 +94,6 @@ declare global { | ||||
|   const toReactive: typeof import('@vueuse/core')['toReactive'] | ||||
|   const toRef: typeof import('vue')['toRef'] | ||||
|   const toRefs: typeof import('vue')['toRefs'] | ||||
|   const toValue: typeof import('vue')['toValue'] | ||||
|   const triggerRef: typeof import('vue')['triggerRef'] | ||||
|   const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] | ||||
|   const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount'] | ||||
| @@ -104,19 +104,6 @@ declare global { | ||||
|   const unrefElement: typeof import('@vueuse/core')['unrefElement'] | ||||
|   const until: typeof import('@vueuse/core')['until'] | ||||
|   const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] | ||||
|   const useAnimate: typeof import('@vueuse/core')['useAnimate'] | ||||
|   const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference'] | ||||
|   const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] | ||||
|   const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] | ||||
|   const useArrayFind: typeof import('@vueuse/core')['useArrayFind'] | ||||
|   const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex'] | ||||
|   const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast'] | ||||
|   const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes'] | ||||
|   const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin'] | ||||
|   const useArrayMap: typeof import('@vueuse/core')['useArrayMap'] | ||||
|   const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce'] | ||||
|   const useArraySome: typeof import('@vueuse/core')['useArraySome'] | ||||
|   const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique'] | ||||
|   const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue'] | ||||
|   const useAsyncState: typeof import('@vueuse/core')['useAsyncState'] | ||||
|   const useAttrs: typeof import('vue')['useAttrs'] | ||||
| @@ -127,8 +114,8 @@ declare global { | ||||
|   const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel'] | ||||
|   const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] | ||||
|   const useCached: typeof import('@vueuse/core')['useCached'] | ||||
|   const useClamp: typeof import('@vueuse/core')['useClamp'] | ||||
|   const useClipboard: typeof import('@vueuse/core')['useClipboard'] | ||||
|   const useCloned: typeof import('@vueuse/core')['useCloned'] | ||||
|   const useColorMode: typeof import('@vueuse/core')['useColorMode'] | ||||
|   const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] | ||||
|   const useCounter: typeof import('@vueuse/core')['useCounter'] | ||||
| @@ -170,7 +157,6 @@ declare global { | ||||
|   const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] | ||||
|   const useGamepad: typeof import('@vueuse/core')['useGamepad'] | ||||
|   const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] | ||||
|   const useI18n: typeof import('vue-i18n')['useI18n'] | ||||
|   const useIdle: typeof import('@vueuse/core')['useIdle'] | ||||
|   const useImage: typeof import('@vueuse/core')['useImage'] | ||||
|   const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] | ||||
| @@ -203,18 +189,12 @@ declare global { | ||||
|   const useOnline: typeof import('@vueuse/core')['useOnline'] | ||||
|   const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] | ||||
|   const useParallax: typeof import('@vueuse/core')['useParallax'] | ||||
|   const useParentElement: typeof import('@vueuse/core')['useParentElement'] | ||||
|   const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver'] | ||||
|   const usePermission: typeof import('@vueuse/core')['usePermission'] | ||||
|   const usePointer: typeof import('@vueuse/core')['usePointer'] | ||||
|   const usePointerLock: typeof import('@vueuse/core')['usePointerLock'] | ||||
|   const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe'] | ||||
|   const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme'] | ||||
|   const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast'] | ||||
|   const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] | ||||
|   const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] | ||||
|   const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] | ||||
|   const usePrevious: typeof import('@vueuse/core')['usePrevious'] | ||||
|   const useRafFn: typeof import('@vueuse/core')['useRafFn'] | ||||
|   const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] | ||||
|   const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] | ||||
| @@ -228,17 +208,14 @@ declare global { | ||||
|   const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage'] | ||||
|   const useShare: typeof import('@vueuse/core')['useShare'] | ||||
|   const useSlots: typeof import('vue')['useSlots'] | ||||
|   const useSorted: typeof import('@vueuse/core')['useSorted'] | ||||
|   const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] | ||||
|   const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] | ||||
|   const useStepper: typeof import('@vueuse/core')['useStepper'] | ||||
|   const useStorage: typeof import('@vueuse/core')['useStorage'] | ||||
|   const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] | ||||
|   const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] | ||||
|   const useSupported: typeof import('@vueuse/core')['useSupported'] | ||||
|   const useSwipe: typeof import('@vueuse/core')['useSwipe'] | ||||
|   const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] | ||||
|   const useTextDirection: typeof import('@vueuse/core')['useTextDirection'] | ||||
|   const useTextSelection: typeof import('@vueuse/core')['useTextSelection'] | ||||
|   const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize'] | ||||
|   const useThrottle: typeof import('@vueuse/core')['useThrottle'] | ||||
| @@ -250,8 +227,6 @@ declare global { | ||||
|   const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] | ||||
|   const useTimestamp: typeof import('@vueuse/core')['useTimestamp'] | ||||
|   const useTitle: typeof import('@vueuse/core')['useTitle'] | ||||
|   const useToNumber: typeof import('@vueuse/core')['useToNumber'] | ||||
|   const useToString: typeof import('@vueuse/core')['useToString'] | ||||
|   const useToggle: typeof import('@vueuse/core')['useToggle'] | ||||
|   const useTransition: typeof import('@vueuse/core')['useTransition'] | ||||
|   const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams'] | ||||
| @@ -272,10 +247,8 @@ declare global { | ||||
|   const watchArray: typeof import('@vueuse/core')['watchArray'] | ||||
|   const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] | ||||
|   const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] | ||||
|   const watchDeep: typeof import('@vueuse/core')['watchDeep'] | ||||
|   const watchEffect: typeof import('vue')['watchEffect'] | ||||
|   const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable'] | ||||
|   const watchImmediate: typeof import('@vueuse/core')['watchImmediate'] | ||||
|   const watchOnce: typeof import('@vueuse/core')['watchOnce'] | ||||
|   const watchPausable: typeof import('@vueuse/core')['watchPausable'] | ||||
|   const watchPostEffect: typeof import('vue')['watchPostEffect'] | ||||
| @@ -309,9 +282,7 @@ declare module 'vue' { | ||||
|     readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']> | ||||
|     readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']> | ||||
|     readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']> | ||||
|     readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']> | ||||
|     readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']> | ||||
|     readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']> | ||||
|     readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']> | ||||
|     readonly customRef: UnwrapRef<typeof import('vue')['customRef']> | ||||
|     readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']> | ||||
| @@ -331,6 +302,9 @@ declare module 'vue' { | ||||
|     readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']> | ||||
|     readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']> | ||||
|     readonly isRef: UnwrapRef<typeof import('vue')['isRef']> | ||||
|     readonly logicAnd: UnwrapRef<typeof import('@vueuse/core')['logicAnd']> | ||||
|     readonly logicNot: UnwrapRef<typeof import('@vueuse/core')['logicNot']> | ||||
|     readonly logicOr: UnwrapRef<typeof import('@vueuse/core')['logicOr']> | ||||
|     readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']> | ||||
|     readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']> | ||||
|     readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']> | ||||
| @@ -383,7 +357,6 @@ declare module 'vue' { | ||||
|     readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']> | ||||
|     readonly toRef: UnwrapRef<typeof import('vue')['toRef']> | ||||
|     readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']> | ||||
|     readonly toValue: UnwrapRef<typeof import('vue')['toValue']> | ||||
|     readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']> | ||||
|     readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']> | ||||
|     readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']> | ||||
| @@ -394,19 +367,6 @@ declare module 'vue' { | ||||
|     readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']> | ||||
|     readonly until: UnwrapRef<typeof import('@vueuse/core')['until']> | ||||
|     readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']> | ||||
|     readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']> | ||||
|     readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']> | ||||
|     readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']> | ||||
|     readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']> | ||||
|     readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']> | ||||
|     readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']> | ||||
|     readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']> | ||||
|     readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']> | ||||
|     readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']> | ||||
|     readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']> | ||||
|     readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']> | ||||
|     readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']> | ||||
|     readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']> | ||||
|     readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']> | ||||
|     readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']> | ||||
|     readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']> | ||||
| @@ -417,8 +377,8 @@ declare module 'vue' { | ||||
|     readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']> | ||||
|     readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']> | ||||
|     readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']> | ||||
|     readonly useClamp: UnwrapRef<typeof import('@vueuse/core')['useClamp']> | ||||
|     readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']> | ||||
|     readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']> | ||||
|     readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']> | ||||
|     readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']> | ||||
|     readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']> | ||||
| @@ -460,7 +420,6 @@ declare module 'vue' { | ||||
|     readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']> | ||||
|     readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']> | ||||
|     readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']> | ||||
|     readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']> | ||||
|     readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']> | ||||
|     readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']> | ||||
|     readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']> | ||||
| @@ -493,18 +452,12 @@ declare module 'vue' { | ||||
|     readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']> | ||||
|     readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']> | ||||
|     readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']> | ||||
|     readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']> | ||||
|     readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']> | ||||
|     readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']> | ||||
|     readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']> | ||||
|     readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']> | ||||
|     readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']> | ||||
|     readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']> | ||||
|     readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']> | ||||
|     readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']> | ||||
|     readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']> | ||||
|     readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']> | ||||
|     readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']> | ||||
|     readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']> | ||||
|     readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']> | ||||
|     readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']> | ||||
| @@ -518,17 +471,14 @@ declare module 'vue' { | ||||
|     readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']> | ||||
|     readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']> | ||||
|     readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']> | ||||
|     readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']> | ||||
|     readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']> | ||||
|     readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']> | ||||
|     readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']> | ||||
|     readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']> | ||||
|     readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']> | ||||
|     readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']> | ||||
|     readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']> | ||||
|     readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']> | ||||
|     readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']> | ||||
|     readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']> | ||||
|     readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']> | ||||
|     readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']> | ||||
|     readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']> | ||||
| @@ -540,8 +490,6 @@ declare module 'vue' { | ||||
|     readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']> | ||||
|     readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']> | ||||
|     readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']> | ||||
|     readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']> | ||||
|     readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']> | ||||
|     readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']> | ||||
|     readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']> | ||||
|     readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']> | ||||
| @@ -562,294 +510,8 @@ declare module 'vue' { | ||||
|     readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']> | ||||
|     readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']> | ||||
|     readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']> | ||||
|     readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']> | ||||
|     readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']> | ||||
|     readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']> | ||||
|     readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']> | ||||
|     readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']> | ||||
|     readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']> | ||||
|     readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']> | ||||
|     readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']> | ||||
|     readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']> | ||||
|     readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']> | ||||
|     readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']> | ||||
|     readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']> | ||||
|   } | ||||
| } | ||||
| declare module '@vue/runtime-core' { | ||||
|   interface ComponentCustomProperties { | ||||
|     readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']> | ||||
|     readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']> | ||||
|     readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']> | ||||
|     readonly computed: UnwrapRef<typeof import('vue')['computed']> | ||||
|     readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']> | ||||
|     readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']> | ||||
|     readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']> | ||||
|     readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']> | ||||
|     readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']> | ||||
|     readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']> | ||||
|     readonly createApp: UnwrapRef<typeof import('vue')['createApp']> | ||||
|     readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']> | ||||
|     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']> | ||||
|     readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']> | ||||
|     readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']> | ||||
|     readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']> | ||||
|     readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']> | ||||
|     readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']> | ||||
|     readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']> | ||||
|     readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']> | ||||
|     readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']> | ||||
|     readonly h: UnwrapRef<typeof import('vue')['h']> | ||||
|     readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']> | ||||
|     readonly inject: UnwrapRef<typeof import('vue')['inject']> | ||||
|     readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']> | ||||
|     readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']> | ||||
|     readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']> | ||||
|     readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']> | ||||
|     readonly isRef: UnwrapRef<typeof import('vue')['isRef']> | ||||
|     readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']> | ||||
|     readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']> | ||||
|     readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']> | ||||
|     readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']> | ||||
|     readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']> | ||||
|     readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']> | ||||
|     readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']> | ||||
|     readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']> | ||||
|     readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']> | ||||
|     readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']> | ||||
|     readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']> | ||||
|     readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']> | ||||
|     readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']> | ||||
|     readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']> | ||||
|     readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']> | ||||
|     readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']> | ||||
|     readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']> | ||||
|     readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']> | ||||
|     readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']> | ||||
|     readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']> | ||||
|     readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']> | ||||
|     readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']> | ||||
|     readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']> | ||||
|     readonly provide: UnwrapRef<typeof import('vue')['provide']> | ||||
|     readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']> | ||||
|     readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']> | ||||
|     readonly reactive: UnwrapRef<typeof import('vue')['reactive']> | ||||
|     readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']> | ||||
|     readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']> | ||||
|     readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']> | ||||
|     readonly readonly: UnwrapRef<typeof import('vue')['readonly']> | ||||
|     readonly ref: UnwrapRef<typeof import('vue')['ref']> | ||||
|     readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']> | ||||
|     readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']> | ||||
|     readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']> | ||||
|     readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']> | ||||
|     readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']> | ||||
|     readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']> | ||||
|     readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']> | ||||
|     readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']> | ||||
|     readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']> | ||||
|     readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']> | ||||
|     readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']> | ||||
|     readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']> | ||||
|     readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']> | ||||
|     readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']> | ||||
|     readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']> | ||||
|     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 toRefs: UnwrapRef<typeof import('vue')['toRefs']> | ||||
|     readonly toValue: UnwrapRef<typeof import('vue')['toValue']> | ||||
|     readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']> | ||||
|     readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']> | ||||
|     readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']> | ||||
|     readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']> | ||||
|     readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']> | ||||
|     readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']> | ||||
|     readonly unref: UnwrapRef<typeof import('vue')['unref']> | ||||
|     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']> | ||||
|     readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']> | ||||
|     readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']> | ||||
|     readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']> | ||||
|     readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']> | ||||
|     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 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']> | ||||
|     readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']> | ||||
|     readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']> | ||||
|     readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']> | ||||
|     readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']> | ||||
|     readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']> | ||||
|     readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']> | ||||
|     readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']> | ||||
|     readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']> | ||||
|     readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']> | ||||
|     readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']> | ||||
|     readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']> | ||||
|     readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']> | ||||
|     readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']> | ||||
|     readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']> | ||||
|     readonly useDialog: UnwrapRef<typeof import('naive-ui')['useDialog']> | ||||
|     readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']> | ||||
|     readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']> | ||||
|     readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']> | ||||
|     readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']> | ||||
|     readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']> | ||||
|     readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']> | ||||
|     readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']> | ||||
|     readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']> | ||||
|     readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']> | ||||
|     readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']> | ||||
|     readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']> | ||||
|     readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']> | ||||
|     readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']> | ||||
|     readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']> | ||||
|     readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']> | ||||
|     readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']> | ||||
|     readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']> | ||||
|     readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']> | ||||
|     readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']> | ||||
|     readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']> | ||||
|     readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']> | ||||
|     readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']> | ||||
|     readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']> | ||||
|     readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']> | ||||
|     readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']> | ||||
|     readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']> | ||||
|     readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']> | ||||
|     readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']> | ||||
|     readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']> | ||||
|     readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']> | ||||
|     readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']> | ||||
|     readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']> | ||||
|     readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']> | ||||
|     readonly useLoadingBar: UnwrapRef<typeof import('naive-ui')['useLoadingBar']> | ||||
|     readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']> | ||||
|     readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']> | ||||
|     readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']> | ||||
|     readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']> | ||||
|     readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']> | ||||
|     readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']> | ||||
|     readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']> | ||||
|     readonly useMessage: UnwrapRef<typeof import('naive-ui')['useMessage']> | ||||
|     readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']> | ||||
|     readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']> | ||||
|     readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']> | ||||
|     readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']> | ||||
|     readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']> | ||||
|     readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']> | ||||
|     readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']> | ||||
|     readonly useNotification: UnwrapRef<typeof import('naive-ui')['useNotification']> | ||||
|     readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']> | ||||
|     readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']> | ||||
|     readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']> | ||||
|     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']> | ||||
|     readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']> | ||||
|     readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']> | ||||
|     readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']> | ||||
|     readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']> | ||||
|     readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']> | ||||
|     readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']> | ||||
|     readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']> | ||||
|     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']> | ||||
|     readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']> | ||||
|     readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']> | ||||
|     readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']> | ||||
|     readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']> | ||||
|     readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']> | ||||
|     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']> | ||||
|     readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']> | ||||
|     readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']> | ||||
|     readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']> | ||||
|     readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']> | ||||
|     readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']> | ||||
|     readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']> | ||||
|     readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']> | ||||
|     readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']> | ||||
|     readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']> | ||||
|     readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']> | ||||
|     readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']> | ||||
|     readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']> | ||||
|     readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']> | ||||
|     readonly watch: UnwrapRef<typeof import('vue')['watch']> | ||||
|     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']> | ||||
|   | ||||
							
								
								
									
										62
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										62
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -19,74 +19,39 @@ declare module '@vue/runtime-core' { | ||||
|     Bcrypt: typeof import('./src/tools/bcrypt/bcrypt.vue')['default'] | ||||
|     BenchmarkBuilder: typeof import('./src/tools/benchmark-builder/benchmark-builder.vue')['default'] | ||||
|     Bip39Generator: typeof import('./src/tools/bip39-generator/bip39-generator.vue')['default'] | ||||
|     CAlert: typeof import('./src/ui/c-alert/c-alert.vue')['default'] | ||||
|     'CAlert.demo': typeof import('./src/ui/c-alert/c-alert.demo.vue')['default'] | ||||
|     CameraRecorder: typeof import('./src/tools/camera-recorder/camera-recorder.vue')['default'] | ||||
|     CaseConverter: typeof import('./src/tools/case-converter/case-converter.vue')['default'] | ||||
|     CButton: typeof import('./src/ui/c-button/c-button.vue')['default'] | ||||
|     'CButton.demo': typeof import('./src/ui/c-button/c-button.demo.vue')['default'] | ||||
|     CCard: typeof import('./src/ui/c-card/c-card.vue')['default'] | ||||
|     'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default'] | ||||
|     CDiffEditor: typeof import('./src/ui/c-diff-editor/c-diff-editor.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'] | ||||
|     CLabel: typeof import('./src/ui/c-label/c-label.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'] | ||||
|     CModal: typeof import('./src/ui/c-modal/c-modal.vue')['default'] | ||||
|     'CModal.demo': typeof import('./src/ui/c-modal/c-modal.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'] | ||||
|     CommandPalette: typeof import('./src/modules/command-palette/command-palette.vue')['default'] | ||||
|     CommandPaletteOption: typeof import('./src/modules/command-palette/components/command-palette-option.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'] | ||||
|     CSelect: typeof import('./src/ui/c-select/c-select.vue')['default'] | ||||
|     'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default'] | ||||
|     DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default'] | ||||
|     'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default'] | ||||
|     'Demo.routes': typeof import('./src/ui/demo/demo.routes.vue')['default'] | ||||
|     DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default'] | ||||
|     DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default'] | ||||
|     DiffViewer: typeof import('./src/tools/json-diff/diff-viewer/diff-viewer.vue')['default'] | ||||
|     DockerRunToDockerComposeConverter: typeof import('./src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue')['default'] | ||||
|     DynamicValues: typeof import('./src/tools/benchmark-builder/dynamic-values.vue')['default'] | ||||
|     Editor: typeof import('./src/tools/html-wysiwyg-editor/editor/editor.vue')['default'] | ||||
|     EmojiCard: typeof import('./src/tools/emoji-picker/emoji-card.vue')['default'] | ||||
|     EmojiGrid: typeof import('./src/tools/emoji-picker/emoji-grid.vue')['default'] | ||||
|     EmojiPicker: typeof import('./src/tools/emoji-picker/emoji-picker.vue')['default'] | ||||
|     Encryption: typeof import('./src/tools/encryption/encryption.vue')['default'] | ||||
|     EtaCalculator: typeof import('./src/tools/eta-calculator/eta-calculator.vue')['default'] | ||||
|     FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default'] | ||||
|     FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default'] | ||||
|     GitMemo: typeof import('./src/tools/git-memo/git-memo.vue')['default'] | ||||
|     'GitMemo.content': typeof import('./src/tools/git-memo/git-memo.content.md')['default'] | ||||
|     GitMemo: typeof import('./src/tools/git-memo/git-memo.md')['default'] | ||||
|     HashText: typeof import('./src/tools/hash-text/hash-text.vue')['default'] | ||||
|     HmacGenerator: typeof import('./src/tools/hmac-generator/hmac-generator.vue')['default'] | ||||
|     'Home.page': typeof import('./src/pages/Home.page.vue')['default'] | ||||
|     HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default'] | ||||
|     HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default'] | ||||
|     HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] | ||||
|     'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default'] | ||||
|     'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default'] | ||||
|     'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] | ||||
|     IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default'] | ||||
|     IconMdiCamera: typeof import('~icons/mdi/camera')['default'] | ||||
|     IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] | ||||
|     IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] | ||||
|     IconMdiClose: typeof import('~icons/mdi/close')['default'] | ||||
|     IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default'] | ||||
|     IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default'] | ||||
|     IconMdiDownload: typeof import('~icons/mdi/download')['default'] | ||||
|     IconMdiEye: typeof import('~icons/mdi/eye')['default'] | ||||
|     IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default'] | ||||
|     IconMdiPause: typeof import('~icons/mdi/pause')['default'] | ||||
|     IconMdiPlay: typeof import('~icons/mdi/play')['default'] | ||||
|     IconMdiRecord: typeof import('~icons/mdi/record')['default'] | ||||
|     IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] | ||||
|     IconMdiSearch: typeof import('~icons/mdi/search')['default'] | ||||
|     IconMdiVideo: typeof import('~icons/mdi/video')['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'] | ||||
| @@ -95,13 +60,10 @@ declare module '@vue/runtime-core' { | ||||
|     Ipv6UlaGenerator: typeof import('./src/tools/ipv6-ula-generator/ipv6-ula-generator.vue')['default'] | ||||
|     JsonDiff: typeof import('./src/tools/json-diff/json-diff.vue')['default'] | ||||
|     JsonMinify: typeof import('./src/tools/json-minify/json-minify.vue')['default'] | ||||
|     JsonToCsv: typeof import('./src/tools/json-to-csv/json-to-csv.vue')['default'] | ||||
|     JsonToToml: typeof import('./src/tools/json-to-toml/json-to-toml.vue')['default'] | ||||
|     JsonToYaml: typeof import('./src/tools/json-to-yaml-converter/json-to-yaml.vue')['default'] | ||||
|     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'] | ||||
| @@ -112,7 +74,10 @@ declare module '@vue/runtime-core' { | ||||
|     MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default'] | ||||
|     MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] | ||||
|     NAlert: typeof import('naive-ui')['NAlert'] | ||||
|     NAutoComplete: typeof import('naive-ui')['NAutoComplete'] | ||||
|     NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] | ||||
|     NButton: typeof import('naive-ui')['NButton'] | ||||
|     NCard: typeof import('naive-ui')['NCard'] | ||||
|     NCheckbox: typeof import('naive-ui')['NCheckbox'] | ||||
|     NCode: typeof import('naive-ui')['NCode'] | ||||
|     NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] | ||||
| @@ -131,6 +96,7 @@ declare module '@vue/runtime-core' { | ||||
|     NH3: typeof import('naive-ui')['NH3'] | ||||
|     NIcon: typeof import('naive-ui')['NIcon'] | ||||
|     NImage: typeof import('naive-ui')['NImage'] | ||||
|     NInput: typeof import('naive-ui')['NInput'] | ||||
|     NInputGroup: typeof import('naive-ui')['NInputGroup'] | ||||
|     NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel'] | ||||
|     NInputNumber: typeof import('naive-ui')['NInputNumber'] | ||||
| @@ -141,17 +107,18 @@ declare module '@vue/runtime-core' { | ||||
|     NPageHeader: typeof import('naive-ui')['NPageHeader'] | ||||
|     NProgress: typeof import('naive-ui')['NProgress'] | ||||
|     NScrollbar: typeof import('naive-ui')['NScrollbar'] | ||||
|     NSelect: typeof import('naive-ui')['NSelect'] | ||||
|     NSlider: typeof import('naive-ui')['NSlider'] | ||||
|     NSpace: typeof import('naive-ui')['NSpace'] | ||||
|     NStatistic: typeof import('naive-ui')['NStatistic'] | ||||
|     NSwitch: typeof import('naive-ui')['NSwitch'] | ||||
|     NTable: typeof import('naive-ui')['NTable'] | ||||
|     NTag: typeof import('naive-ui')['NTag'] | ||||
|     NText: typeof import('naive-ui')['NText'] | ||||
|     NTooltip: typeof import('naive-ui')['NTooltip'] | ||||
|     NUpload: typeof import('naive-ui')['NUpload'] | ||||
|     NUploadDragger: typeof import('naive-ui')['NUploadDragger'] | ||||
|     OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] | ||||
|     PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] | ||||
|     PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default'] | ||||
|     PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default'] | ||||
|     QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default'] | ||||
|     RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default'] | ||||
| @@ -160,20 +127,19 @@ declare module '@vue/runtime-core' { | ||||
|     RouterLink: typeof import('vue-router')['RouterLink'] | ||||
|     RouterView: typeof import('vue-router')['RouterView'] | ||||
|     RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default'] | ||||
|     SearchBar: typeof import('./src/components/SearchBar.vue')['default'] | ||||
|     SearchBarItem: typeof import('./src/components/SearchBarItem.vue')['default'] | ||||
|     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'] | ||||
|     StringObfuscator: typeof import('./src/tools/string-obfuscator/string-obfuscator.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'] | ||||
|     TextDiff: typeof import('./src/tools/text-diff/text-diff.vue')['default'] | ||||
|     TextStatistics: typeof import('./src/tools/text-statistics/text-statistics.vue')['default'] | ||||
|     TextToNatoAlphabet: typeof import('./src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue')['default'] | ||||
|     TokenDisplay: typeof import('./src/tools/otp-code-generator-and-validator/token-display.vue')['default'] | ||||
|     'TokenGenerator.tool': typeof import('./src/tools/token-generator/token-generator.tool.vue')['default'] | ||||
|     TomlToJson: typeof import('./src/tools/toml-to-json/toml-to-json.vue')['default'] | ||||
|     TomlToYaml: typeof import('./src/tools/toml-to-yaml/toml-to-yaml.vue')['default'] | ||||
|     'Tool.layout': typeof import('./src/layouts/tool.layout.vue')['default'] | ||||
|     ToolCard: typeof import('./src/components/ToolCard.vue')['default'] | ||||
|     UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default'] | ||||
| @@ -181,8 +147,6 @@ declare module '@vue/runtime-core' { | ||||
|     UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default'] | ||||
|     UserAgentResultCards: typeof import('./src/tools/user-agent-parser/user-agent-result-cards.vue')['default'] | ||||
|     UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default'] | ||||
|     XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default'] | ||||
|     YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default'] | ||||
|     YamlToToml: typeof import('./src/tools/yaml-to-toml/yaml-to-toml.vue')['default'] | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										12
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								index.html
									
									
									
									
									
								
							| @@ -2,7 +2,7 @@ | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <link rel="icon" href="favicon.ico" /> | ||||
|     <link rel="icon" href="/favicon.ico" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>IT Tools - Handy online tools for developers</title> | ||||
|     <meta itemprop="name" content="IT Tools - Handy online tools for developers" /> | ||||
| @@ -14,13 +14,13 @@ | ||||
|       itemprop="description" | ||||
|       content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT." | ||||
|     /> | ||||
|     <link rel="author" href="humans.txt" /> | ||||
|     <link rel="author" href="/humans.txt" /> | ||||
|     <link rel="canonical" href="https://it-tools.tech" /> | ||||
|  | ||||
|     <link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" /> | ||||
|     <link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png" /> | ||||
|     <link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png" /> | ||||
|     <link rel="mask-icon" href="safari-pinned-tab.svg" color="#18a058" /> | ||||
|     <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> | ||||
|     <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> | ||||
|     <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> | ||||
|     <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#18a058" /> | ||||
|     <meta name="msapplication-TileColor" content="#da532c" /> | ||||
|     <meta name="theme-color" content="#ffffff" /> | ||||
|  | ||||
|   | ||||
| @@ -1,4 +0,0 @@ | ||||
| home: | ||||
|   categories: | ||||
|     newestTools: Newest tools | ||||
|  | ||||
| @@ -1,3 +0,0 @@ | ||||
| home: | ||||
|   categories: | ||||
|     newestTools: "Nouveaux outils" | ||||
| @@ -1,4 +0,0 @@ | ||||
| [[redirects]] | ||||
|   from = "/*" | ||||
|   to = "/index.html" | ||||
|   status = 200 | ||||
							
								
								
									
										134
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										134
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "it-tools", | ||||
|   "version": "2023.8.21-6f93cba", | ||||
|   "version": "2023.4.23-92bd835", | ||||
|   "description": "Collection of handy online tools for developers, with great UX. ", | ||||
|   "keywords": [ | ||||
|     "productivity", | ||||
| @@ -35,99 +35,95 @@ | ||||
|   "dependencies": { | ||||
|     "@it-tools/bip39": "^0.0.4", | ||||
|     "@it-tools/oggen": "^1.3.0", | ||||
|     "@sindresorhus/slugify": "^2.2.1", | ||||
|     "@tiptap/pm": "^2.1.7", | ||||
|     "@tiptap/starter-kit": "^2.1.7", | ||||
|     "@tiptap/vue-3": "^2.1.7", | ||||
|     "@sindresorhus/slugify": "^2.2.0", | ||||
|     "@tiptap/pm": "2.0.0-beta.220", | ||||
|     "@tiptap/starter-kit": "2.0.0-beta.220", | ||||
|     "@tiptap/vue-3": "2.0.0-beta.220", | ||||
|     "@vicons/material": "^0.12.0", | ||||
|     "@vicons/tabler": "^0.12.0", | ||||
|     "@vueuse/core": "^10.4.0", | ||||
|     "@vueuse/head": "^1.3.1", | ||||
|     "@vueuse/router": "^10.4.0", | ||||
|     "@vueuse/core": "^8.9.4", | ||||
|     "@vueuse/head": "^0.7.13", | ||||
|     "@vueuse/router": "^9.13.0", | ||||
|     "bcryptjs": "^2.4.3", | ||||
|     "change-case": "^4.1.2", | ||||
|     "colord": "^2.9.3", | ||||
|     "composerize-ts": "^0.6.2", | ||||
|     "country-code-lookup": "^0.1.0", | ||||
|     "country-code-lookup": "^0.0.23", | ||||
|     "cron-validator": "^1.3.1", | ||||
|     "cronstrue": "^2.31.0", | ||||
|     "cronstrue": "^2.26.0", | ||||
|     "crypto-js": "^4.1.1", | ||||
|     "date-fns": "^2.30.0", | ||||
|     "emojilib": "^3.0.10", | ||||
|     "date-fns": "^2.29.3", | ||||
|     "figue": "^1.2.0", | ||||
|     "fuse.js": "^6.6.2", | ||||
|     "highlight.js": "^11.8.0", | ||||
|     "iarna-toml-esm": "^3.0.5", | ||||
|     "highlight.js": "^11.7.0", | ||||
|     "json5": "^2.2.3", | ||||
|     "jwt-decode": "^3.1.2", | ||||
|     "libphonenumber-js": "^1.10.41", | ||||
|     "libphonenumber-js": "^1.10.28", | ||||
|     "lodash": "^4.17.21", | ||||
|     "mathjs": "^11.10.0", | ||||
|     "mathjs": "^10.6.4", | ||||
|     "mime-types": "^2.1.35", | ||||
|     "monaco-editor": "^0.41.0", | ||||
|     "naive-ui": "^2.34.4", | ||||
|     "naive-ui": "^2.34.3", | ||||
|     "netmask": "^2.0.2", | ||||
|     "node-forge": "^1.3.1", | ||||
|     "oui": "^12.0.71", | ||||
|     "pinia": "^2.1.6", | ||||
|     "oui": "^12.0.52", | ||||
|     "pinia": "^2.0.34", | ||||
|     "plausible-tracker": "^0.3.8", | ||||
|     "qrcode": "^1.5.3", | ||||
|     "qrcode": "^1.5.1", | ||||
|     "randombytes": "^2.1.0", | ||||
|     "sql-formatter": "^12.2.4", | ||||
|     "sql-formatter": "^8.2.0", | ||||
|     "ts-pattern": "^4.2.2", | ||||
|     "ua-parser-js": "^1.0.35", | ||||
|     "unicode-emoji-json": "^0.4.0", | ||||
|     "unplugin-auto-import": "^0.16.6", | ||||
|     "uuid": "^9.0.0", | ||||
|     "vue": "^3.3.4", | ||||
|     "vue-i18n": "^9.2.2", | ||||
|     "vue-router": "^4.2.4", | ||||
|     "vue-tsc": "^1.8.8", | ||||
|     "xml-formatter": "^3.5.0", | ||||
|     "yaml": "^2.3.1" | ||||
|     "uuid": "^8.3.2", | ||||
|     "vue": "^3.2.47", | ||||
|     "vue-router": "^4.1.6", | ||||
|     "yaml": "^2.2.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@antfu/eslint-config": "^0.41.0", | ||||
|     "@iconify-json/mdi": "^1.1.54", | ||||
|     "@intlify/unplugin-vue-i18n": "^0.12.3", | ||||
|     "@playwright/test": "^1.37.1", | ||||
|     "@rushstack/eslint-patch": "^1.3.3", | ||||
|     "@tsconfig/node18": "^18.2.1", | ||||
|     "@playwright/test": "^1.32.3", | ||||
|     "@rushstack/eslint-patch": "^1.2.0", | ||||
|     "@types/bcryptjs": "^2.4.2", | ||||
|     "@types/crypto-js": "^4.1.1", | ||||
|     "@types/jsdom": "^21.1.2", | ||||
|     "@types/lodash": "^4.14.197", | ||||
|     "@types/jsdom": "^16.2.15", | ||||
|     "@types/lodash": "^4.14.192", | ||||
|     "@types/mime-types": "^2.1.1", | ||||
|     "@types/netmask": "^2.0.2", | ||||
|     "@types/node": "^18.17.11", | ||||
|     "@types/node-forge": "^1.3.4", | ||||
|     "@types/qrcode": "^1.5.1", | ||||
|     "@types/netmask": "^2.0.0", | ||||
|     "@types/node": "^16.18.23", | ||||
|     "@types/node-forge": "^1.3.2", | ||||
|     "@types/prettier": "^2.7.2", | ||||
|     "@types/qrcode": "^1.5.0", | ||||
|     "@types/randombytes": "^2.0.0", | ||||
|     "@types/ua-parser-js": "^0.7.36", | ||||
|     "@types/uuid": "^9.0.2", | ||||
|     "@unocss/eslint-config": "^0.55.3", | ||||
|     "@vitejs/plugin-vue": "^4.3.3", | ||||
|     "@vitejs/plugin-vue-jsx": "^3.0.2", | ||||
|     "@vue/compiler-sfc": "^3.3.4", | ||||
|     "@vue/runtime-dom": "^3.3.4", | ||||
|     "@vue/test-utils": "^2.4.1", | ||||
|     "@vue/tsconfig": "^0.4.0", | ||||
|     "c8": "^8.0.1", | ||||
|     "consola": "^3.2.3", | ||||
|     "eslint": "^8.48.0", | ||||
|     "jsdom": "^22.1.0", | ||||
|     "less": "^4.2.0", | ||||
|     "prettier": "^3.0.2", | ||||
|     "typescript": "~5.2.2", | ||||
|     "unocss": "^0.55.3", | ||||
|     "unocss-preset-scrollbar": "^0.2.1", | ||||
|     "unplugin-icons": "^0.16.5", | ||||
|     "unplugin-vue-components": "^0.25.1", | ||||
|     "vite": "^4.4.9", | ||||
|     "vite-plugin-pwa": "^0.16.4", | ||||
|     "vite-plugin-vue-markdown": "^0.23.8", | ||||
|     "vite-svg-loader": "^4.0.0", | ||||
|     "vitest": "^0.34.3", | ||||
|     "workbox-window": "^7.0.0", | ||||
|     "zx": "^7.2.3" | ||||
|     "@types/uuid": "^8.3.4", | ||||
|     "@typescript-eslint/parser": "^5.58.0", | ||||
|     "@unocss/eslint-config": "^0.50.8", | ||||
|     "@vitejs/plugin-vue": "^2.3.4", | ||||
|     "@vitejs/plugin-vue-jsx": "^1.3.10", | ||||
|     "@vue/eslint-config-prettier": "^7.1.0", | ||||
|     "@vue/eslint-config-typescript": "^10.0.0", | ||||
|     "@vue/test-utils": "^2.3.2", | ||||
|     "@vue/tsconfig": "^0.1.3", | ||||
|     "c8": "^7.13.0", | ||||
|     "consola": "^3.0.2", | ||||
|     "eslint": "^8.38.0", | ||||
|     "eslint-config-prettier": "^8.8.0", | ||||
|     "eslint-import-resolver-typescript": "^3.5.5", | ||||
|     "eslint-plugin-import": "^2.27.5", | ||||
|     "eslint-plugin-vue": "^8.7.1", | ||||
|     "jsdom": "^19.0.0", | ||||
|     "less": "^4.1.3", | ||||
|     "prettier": "^2.8.7", | ||||
|     "start-server-and-test": "^1.15.4", | ||||
|     "typescript": "~4.5.5", | ||||
|     "unocss": "^0.50.8", | ||||
|     "unplugin-auto-import": "^0.15.2", | ||||
|     "unplugin-vue-components": "^0.24.1", | ||||
|     "vite": "^2.9.15", | ||||
|     "vite-plugin-md": "^0.12.4", | ||||
|     "vite-plugin-pwa": "^0.11.13", | ||||
|     "vite-svg-loader": "^3.6.0", | ||||
|     "vitest": "^0.13.1", | ||||
|     "vue-tsc": "^0.31.4", | ||||
|     "workbox-window": "^6.5.4", | ||||
|     "zx": "^7.2.1" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,10 @@ | ||||
| import { defineConfig, devices } from '@playwright/test'; | ||||
|  | ||||
| const isCI = !!process.env.CI; | ||||
| const baseUrl = process.env.BASE_URL || 'http://localhost:5050'; | ||||
| /** | ||||
|  * Read environment variables from file. | ||||
|  * https://github.com/motdotla/dotenv | ||||
|  */ | ||||
| // require('dotenv').config(); | ||||
|  | ||||
| /** | ||||
|  * See https://playwright.dev/docs/test-configuration. | ||||
| @@ -12,17 +15,17 @@ export default defineConfig({ | ||||
|   /* Run tests in files in parallel */ | ||||
|   fullyParallel: true, | ||||
|   /* Fail the build on CI if you accidentally left test.only in the source code. */ | ||||
|   forbidOnly: isCI, | ||||
|   forbidOnly: !!process.env.CI, | ||||
|   /* Retry on CI only */ | ||||
|   retries: isCI ? 2 : 0, | ||||
|   retries: process.env.CI ? 2 : 0, | ||||
|   /* Opt out of parallel tests on CI. */ | ||||
|   workers: isCI ? 1 : undefined, | ||||
|   workers: process.env.CI ? 1 : undefined, | ||||
|   /* Reporter to use. See https://playwright.dev/docs/test-reporters */ | ||||
|   reporter: 'html', | ||||
|   /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ | ||||
|   use: { | ||||
|     /* Base URL to use in actions like `await page.goto('/')`. */ | ||||
|     baseURL: baseUrl, | ||||
|     baseURL: 'http://127.0.0.1:3000', | ||||
|  | ||||
|     /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ | ||||
|     trace: 'on-first-retry', | ||||
| @@ -48,17 +51,32 @@ export default defineConfig({ | ||||
|       name: 'webkit', | ||||
|       use: { ...devices['Desktop Safari'] }, | ||||
|     }, | ||||
|  | ||||
|     /* Test against mobile viewports. */ | ||||
|     // { | ||||
|     //   name: 'Mobile Chrome', | ||||
|     //   use: { ...devices['Pixel 5'] }, | ||||
|     // }, | ||||
|     // { | ||||
|     //   name: 'Mobile Safari', | ||||
|     //   use: { ...devices['iPhone 12'] }, | ||||
|     // }, | ||||
|  | ||||
|     /* Test against branded browsers. */ | ||||
|     // { | ||||
|     //   name: 'Microsoft Edge', | ||||
|     //   use: { ...devices['Desktop Edge'], channel: 'msedge' }, | ||||
|     // }, | ||||
|     // { | ||||
|     //   name: 'Google Chrome', | ||||
|     //   use: { ..devices['Desktop Chrome'], channel: 'chrome' }, | ||||
|     // }, | ||||
|   ], | ||||
|  | ||||
|   /* Run your local dev server before starting the tests */ | ||||
|  | ||||
|   ...(isCI | ||||
|     ? {} | ||||
|     : { | ||||
|         webServer: { | ||||
|           command: 'npm run preview', | ||||
|           url: 'http://127.0.0.1:5050', | ||||
|           reuseExistingServer: true, | ||||
|         }, | ||||
|       }), | ||||
|   webServer: { | ||||
|     command: 'npm run dev', | ||||
|     url: 'http://127.0.0.1:3000', | ||||
|     reuseExistingServer: !process.env.CI, | ||||
|   }, | ||||
| }); | ||||
|   | ||||
							
								
								
									
										7515
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7515
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,6 +0,0 @@ | ||||
| { | ||||
|   "$schema": "https://docs.renovatebot.com/renovate-schema.json", | ||||
|   "extends": [ | ||||
|     "config:base" | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										17
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								src/App.vue
									
									
									
									
									
								
							| @@ -1,6 +1,7 @@ | ||||
| <script setup lang="ts"> | ||||
| import { RouterView, useRoute } from 'vue-router'; | ||||
| import { NGlobalStyle, NMessageProvider, NNotificationProvider, darkTheme } from 'naive-ui'; | ||||
| import { computed } from 'vue'; | ||||
| import { useRoute, RouterView } from 'vue-router'; | ||||
| import { darkTheme, NGlobalStyle, NMessageProvider, NNotificationProvider } from 'naive-ui'; | ||||
| import { darkThemeOverrides, lightThemeOverrides } from './themes'; | ||||
| import { layouts } from './layouts'; | ||||
| import { useStyleStore } from './stores/style.store'; | ||||
| @@ -15,14 +16,14 @@ const themeOverrides = computed(() => (styleStore.isDarkTheme ? darkThemeOverrid | ||||
|  | ||||
| <template> | ||||
|   <n-config-provider :theme="theme" :theme-overrides="themeOverrides"> | ||||
|     <NGlobalStyle /> | ||||
|     <NMessageProvider placement="bottom"> | ||||
|       <NNotificationProvider placement="bottom-right"> | ||||
|     <n-global-style /> | ||||
|     <n-message-provider placement="bottom"> | ||||
|       <n-notification-provider placement="bottom-right"> | ||||
|         <component :is="layout"> | ||||
|           <RouterView /> | ||||
|           <router-view /> | ||||
|         </component> | ||||
|       </NNotificationProvider> | ||||
|     </NMessageProvider> | ||||
|       </n-notification-provider> | ||||
|     </n-message-provider> | ||||
|   </n-config-provider> | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,39 @@ | ||||
| <template> | ||||
|   <div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name"> | ||||
|     <n-text tag="div" depth="3" class="category-name" @click="toggleCategoryCollapse({ name })"> | ||||
|       <n-icon :component="ChevronRight" :class="{ rotated: isCollapsed }" size="16" /> | ||||
|  | ||||
|       <span> | ||||
|         {{ name }} | ||||
|       </span> | ||||
|     </n-text> | ||||
|  | ||||
|     <n-collapse-transition :show="!isCollapsed"> | ||||
|       <div class="menu-wrapper"> | ||||
|         <div class="toggle-bar" @click="toggleCategoryCollapse({ name })" /> | ||||
|  | ||||
|         <n-menu | ||||
|           class="menu" | ||||
|           :value="(route.name as string)" | ||||
|           :collapsed-width="64" | ||||
|           :collapsed-icon-size="22" | ||||
|           :options="tools" | ||||
|           :indent="8" | ||||
|           :default-expand-all="true" | ||||
|         /> | ||||
|       </div> | ||||
|     </n-collapse-transition> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import type { Tool, ToolCategory } from '@/tools/tools.types'; | ||||
| import { ChevronRight } from '@vicons/tabler'; | ||||
| import { useStorage } from '@vueuse/core'; | ||||
| import { useThemeVars } from 'naive-ui'; | ||||
| import { toRefs, computed, h } from 'vue'; | ||||
| import { RouterLink, useRoute } from 'vue-router'; | ||||
| import MenuIconItem from './MenuIconItem.vue'; | ||||
| import type { Tool, ToolCategory } from '@/tools/tools.types'; | ||||
|  | ||||
| const props = withDefaults(defineProps<{ toolsByCategory?: ToolCategory[] }>(), { toolsByCategory: () => [] }); | ||||
| const { toolsByCategory } = toRefs(props); | ||||
| @@ -19,8 +49,8 @@ const collapsedCategories = useStorage<Record<string, boolean>>( | ||||
|   { | ||||
|     deep: true, | ||||
|     serializer: { | ||||
|       read: v => (v ? JSON.parse(v) : null), | ||||
|       write: v => JSON.stringify(v), | ||||
|       read: (v) => (v ? JSON.parse(v) : null), | ||||
|       write: (v) => JSON.stringify(v), | ||||
|     }, | ||||
|   }, | ||||
| ); | ||||
| @@ -31,9 +61,9 @@ function toggleCategoryCollapse({ name }: { name: string }) { | ||||
|  | ||||
| const menuOptions = computed(() => | ||||
|   toolsByCategory.value.map(({ name, components }) => ({ | ||||
|     name, | ||||
|     name: name, | ||||
|     isCollapsed: collapsedCategories.value[name], | ||||
|     tools: components.map(tool => ({ | ||||
|     tools: components.map((tool) => ({ | ||||
|       label: makeLabel(tool), | ||||
|       icon: makeIcon(tool), | ||||
|       key: tool.name, | ||||
| @@ -44,37 +74,27 @@ const menuOptions = computed(() => | ||||
| const themeVars = useThemeVars(); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name"> | ||||
|     <div ml-6px mt-12px flex cursor-pointer items-center op-60 @click="toggleCategoryCollapse({ name })"> | ||||
|       <span :class="{ 'rotate-0': isCollapsed, 'rotate-90': !isCollapsed }" text-16px lh-1 op-50 transition-transform> | ||||
|         <icon-mdi-chevron-right /> | ||||
|       </span> | ||||
|  | ||||
|       <span ml-8px text-13px> | ||||
|         {{ name }} | ||||
|       </span> | ||||
|     </div> | ||||
|  | ||||
|     <n-collapse-transition :show="!isCollapsed"> | ||||
|       <div class="menu-wrapper"> | ||||
|         <div class="toggle-bar" @click="toggleCategoryCollapse({ name })" /> | ||||
|  | ||||
|         <n-menu | ||||
|           class="menu" | ||||
|           :value="route.name as string" | ||||
|           :collapsed-width="64" | ||||
|           :collapsed-icon-size="22" | ||||
|           :options="tools" | ||||
|           :indent="8" | ||||
|           :default-expand-all="true" | ||||
|         /> | ||||
|       </div> | ||||
|     </n-collapse-transition> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="less"> | ||||
| .category-name { | ||||
|   font-size: 0.93em; | ||||
|   padding: 12px 0 0px 0; | ||||
|   cursor: pointer; | ||||
|  | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   align-items: center; | ||||
|   .n-icon { | ||||
|     transition: transform ease 0.5s; | ||||
|     transform: rotate(90deg); | ||||
|     margin: 0 10px 0 7px; | ||||
|     opacity: 0.5; | ||||
|  | ||||
|     &.rotated { | ||||
|       transform: rotate(0deg); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .menu-wrapper { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   | ||||
| @@ -1,13 +1,8 @@ | ||||
| <script setup lang="ts"> | ||||
| import type { Component } from 'vue'; | ||||
|  | ||||
| const props = defineProps<{ icon: Component; title: string }>(); | ||||
| const { icon, title } = toRefs(props); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <c-card class="colored-card"> | ||||
|     <n-icon class="icon" size="40" :component="icon" /> | ||||
|     <n-space justify="space-between" align="center"> | ||||
|       <n-icon class="icon" size="40" :component="icon" /> | ||||
|     </n-space> | ||||
|     <n-h3 class="title"> | ||||
|       <n-ellipsis>{{ title }}</n-ellipsis> | ||||
|     </n-h3> | ||||
| @@ -20,6 +15,13 @@ const { icon, title } = toRefs(props); | ||||
|   </c-card> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { toRefs, type Component } from 'vue'; | ||||
|  | ||||
| const props = defineProps<{ icon: Component; title: string }>(); | ||||
| const { icon, title } = toRefs(props); | ||||
| </script> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| .colored-card { | ||||
|   background: rgb(37, 99, 108); | ||||
|   | ||||
| @@ -1,30 +1,3 @@ | ||||
| <script setup lang="ts"> | ||||
| import { FavoriteFilled } from '@vicons/material'; | ||||
|  | ||||
| import { useToolStore } from '@/tools/tools.store'; | ||||
| import type { Tool } from '@/tools/tools.types'; | ||||
|  | ||||
| const props = defineProps<{ tool: Tool }>(); | ||||
|  | ||||
| const toolStore = useToolStore(); | ||||
|  | ||||
| const { tool } = toRefs(props); | ||||
|  | ||||
| const isFavorite = computed(() => toolStore.isToolFavorite({ tool })); | ||||
| const buttonType = computed(() => (isFavorite.value ? 'primary' : 'default')); | ||||
|  | ||||
| function toggleFavorite(event: MouseEvent) { | ||||
|   event.preventDefault(); | ||||
|  | ||||
|   if (toolStore.isToolFavorite({ tool })) { | ||||
|     toolStore.removeToolFromFavorites({ tool }); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   toolStore.addToolToFavorites({ tool }); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <n-tooltip trigger="hover"> | ||||
|     <template #trigger> | ||||
| @@ -41,3 +14,29 @@ function toggleFavorite(event: MouseEvent) { | ||||
|     {{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }} | ||||
|   </n-tooltip> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { FavoriteFilled } from '@vicons/material'; | ||||
| import { useToolStore } from '@/tools/tools.store'; | ||||
| import type { Tool } from '@/tools/tools.types'; | ||||
| import { computed, toRefs } from 'vue'; | ||||
|  | ||||
| const toolStore = useToolStore(); | ||||
|  | ||||
| const props = defineProps<{ tool: Tool }>(); | ||||
| const { tool } = toRefs(props); | ||||
|  | ||||
| const isFavorite = computed(() => toolStore.isToolFavorite({ tool })); | ||||
| const buttonType = computed(() => (isFavorite.value ? 'primary' : 'default')); | ||||
|  | ||||
| function toggleFavorite(event: MouseEvent) { | ||||
|   event.preventDefault(); | ||||
|  | ||||
|   if (toolStore.isToolFavorite({ tool })) { | ||||
|     toolStore.removeToolFromFavorites({ tool }); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   toolStore.addToolToFavorites({ tool }); | ||||
| } | ||||
| </script> | ||||
|   | ||||
| @@ -1,17 +1,36 @@ | ||||
| <template> | ||||
|   <n-form-item :label="inputLabel" v-bind="validationAttrs"> | ||||
|     <n-input | ||||
|       ref="inputElement" | ||||
|       v-model:value="input" | ||||
|       :placeholder="inputPlaceholder" | ||||
|       type="textarea" | ||||
|       rows="20" | ||||
|       autocomplete="off" | ||||
|       autocorrect="off" | ||||
|       autocapitalize="off" | ||||
|       spellcheck="false" | ||||
|       :input-props="{ 'data-test-id': 'input' }" | ||||
|     /> | ||||
|   </n-form-item> | ||||
|   <n-form-item :label="outputLabel"> | ||||
|     <textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement" /> | ||||
|   </n-form-item> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useValidation, type UseValidationRule } from '@/composable/validation'; | ||||
| import _ from 'lodash'; | ||||
| import type { UseValidationRule } from '@/composable/validation'; | ||||
| import CInputText from '@/ui/c-input-text/c-input-text.vue'; | ||||
|  | ||||
| const props = withDefaults( | ||||
|   defineProps<{ | ||||
|     transformer?: (v: string) => string | ||||
|     inputValidationRules?: UseValidationRule<string>[] | ||||
|     inputLabel?: string | ||||
|     inputPlaceholder?: string | ||||
|     inputDefault?: string | ||||
|     outputLabel?: string | ||||
|     outputLanguage?: string | ||||
|     transformer?: (v: string) => string; | ||||
|     inputValidationRules?: UseValidationRule<string>[]; | ||||
|     inputLabel?: string; | ||||
|     inputPlaceholder?: string; | ||||
|     inputDefault?: string; | ||||
|     outputLabel?: string; | ||||
|     outputLanguage?: string; | ||||
|   }>(), | ||||
|   { | ||||
|     transformer: _.identity, | ||||
| @@ -24,34 +43,15 @@ const props = withDefaults( | ||||
|   }, | ||||
| ); | ||||
|  | ||||
| const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault } | ||||
|   = toRefs(props); | ||||
| const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault } = | ||||
|   toRefs(props); | ||||
|  | ||||
| const inputElement = ref<typeof CInputText>(); | ||||
| const inputElement = ref(); | ||||
|  | ||||
| const input = ref(inputDefault.value); | ||||
| const output = computed(() => transformer.value(input.value)); | ||||
|  | ||||
| const { attrs: validationAttrs } = useValidation({ source: input, rules: inputValidationRules.value }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <CInputText | ||||
|     ref="inputElement" | ||||
|     v-model:value="input" | ||||
|     :placeholder="inputPlaceholder" | ||||
|     :label="inputLabel" | ||||
|     rows="20" | ||||
|     autosize | ||||
|     raw-text | ||||
|     multiline | ||||
|     test-id="input" | ||||
|     :validation-rules="inputValidationRules" | ||||
|     monospace | ||||
|   /> | ||||
|  | ||||
|   <div> | ||||
|     <div mb-5px> | ||||
|       {{ outputLabel }} | ||||
|     </div> | ||||
|     <textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement?.inputWrapperRef" /> | ||||
|   </div> | ||||
| </template> | ||||
| <style scoped></style> | ||||
|   | ||||
| @@ -1,26 +1,43 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useVModel } from '@vueuse/core'; | ||||
| import { useCopy } from '@/composable/copy'; | ||||
|  | ||||
| const props = defineProps<{ value: string }>(); | ||||
| const emit = defineEmits(['update:value']); | ||||
|  | ||||
| const value = useVModel(props, 'value', emit); | ||||
| const { copy, isJustCopied } = useCopy({ source: value, createToast: false }); | ||||
| const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : 'Copy to clipboard'); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <c-input-text v-model:value="value"> | ||||
|   <n-input v-model:value="value"> | ||||
|     <template #suffix> | ||||
|       <n-tooltip trigger="hover"> | ||||
|         <template #trigger> | ||||
|           <c-button circle variant="text" size="small" @click="copy()"> | ||||
|             <icon-mdi-content-copy /> | ||||
|           <c-button circle variant="text" @click="onCopyClicked"> | ||||
|             <n-icon :component="ContentCopyFilled" /> | ||||
|           </c-button> | ||||
|         </template> | ||||
|         {{ tooltipText }} | ||||
|       </n-tooltip> | ||||
|     </template> | ||||
|   </c-input-text> | ||||
|   </n-input> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useVModel, useClipboard } from '@vueuse/core'; | ||||
| import { ContentCopyFilled } from '@vicons/material'; | ||||
| import { ref } from 'vue'; | ||||
|  | ||||
| const props = defineProps<{ value: string }>(); | ||||
| const emit = defineEmits(['update:value']); | ||||
|  | ||||
| const value = useVModel(props, 'value', emit); | ||||
| const tooltipText = ref('Copy to clipboard'); | ||||
|  | ||||
| const { copy } = useClipboard({ source: value }); | ||||
|  | ||||
| function onCopyClicked() { | ||||
|   copy(); | ||||
|   tooltipText.value = 'Copied !'; | ||||
|  | ||||
|   setTimeout(() => { | ||||
|     tooltipText.value = 'Copy to clipboard'; | ||||
|   }, 2000); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| ::v-deep(.n-input-wrapper) { | ||||
|   padding-right: 5px; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,6 +1,14 @@ | ||||
| <template> | ||||
|   <div class="menu-icon-item"> | ||||
|     <n-icon :component="tool.icon" /> | ||||
|     <div v-if="tool.isNew" class="badge"></div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useThemeVars } from 'naive-ui'; | ||||
| import type { Tool } from '@/tools/tools.types'; | ||||
| import { useThemeVars } from 'naive-ui'; | ||||
| import { toRefs } from 'vue'; | ||||
|  | ||||
| const props = defineProps<{ tool: Tool }>(); | ||||
| const { tool } = toRefs(props); | ||||
| @@ -8,13 +16,6 @@ const { tool } = toRefs(props); | ||||
| const theme = useThemeVars(); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div class="menu-icon-item"> | ||||
|     <n-icon :component="tool.icon" /> | ||||
|     <div v-if="tool.isNew" class="badge" /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| .menu-icon-item { | ||||
|   position: relative; | ||||
|   | ||||
| @@ -1,11 +1,3 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useStyleStore } from '@/stores/style.store'; | ||||
|  | ||||
| const styleStore = useStyleStore(); | ||||
| const { isMenuCollapsed, isSmallScreen } = toRefs(styleStore); | ||||
| const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static')); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <n-layout has-sider> | ||||
|     <n-layout-sider | ||||
| @@ -27,6 +19,15 @@ const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static | ||||
|   </n-layout> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useStyleStore } from '@/stores/style.store'; | ||||
| import { toRefs, computed } from 'vue'; | ||||
|  | ||||
| const styleStore = useStyleStore(); | ||||
| const { isMenuCollapsed, isSmallScreen } = toRefs(styleStore); | ||||
| const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static')); | ||||
| </script> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| .overlay { | ||||
|   position: absolute; | ||||
|   | ||||
| @@ -1,11 +1,3 @@ | ||||
| <script setup lang="ts"> | ||||
| import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler'; | ||||
| import { useStyleStore } from '@/stores/style.store'; | ||||
|  | ||||
| const styleStore = useStyleStore(); | ||||
| const { isDarkTheme } = toRefs(styleStore); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <n-tooltip trigger="hover"> | ||||
|     <template #trigger> | ||||
| @@ -49,7 +41,7 @@ const { isDarkTheme } = toRefs(styleStore); | ||||
|   </n-tooltip> | ||||
|   <n-tooltip trigger="hover"> | ||||
|     <template #trigger> | ||||
|       <c-button circle variant="text" aria-label="Toggle dark/light mode" @click="() => styleStore.toggleDark()"> | ||||
|       <c-button circle variant="text" aria-label="Toggle dark/light mode" @click="toggleDarkTheme"> | ||||
|         <n-icon v-if="isDarkTheme" size="25" :component="Sun" /> | ||||
|         <n-icon v-else size="25" :component="Moon" /> | ||||
|       </c-button> | ||||
| @@ -59,6 +51,24 @@ const { isDarkTheme } = toRefs(styleStore); | ||||
|   </n-tooltip> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useStyleStore } from '@/stores/style.store'; | ||||
| import { useThemeStore } from '@/ui/theme/theme.store'; | ||||
| import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler'; | ||||
| import { toRefs } from 'vue'; | ||||
|  | ||||
| const styleStore = useStyleStore(); | ||||
| const { isDarkTheme } = toRefs(styleStore); | ||||
|  | ||||
| const themeStore = useThemeStore(); | ||||
|  | ||||
| function toggleDarkTheme() { | ||||
|   isDarkTheme.value = !isDarkTheme.value; | ||||
|  | ||||
|   themeStore.toggleTheme(); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| .n-button { | ||||
|   &:not(:last-child) { | ||||
|   | ||||
							
								
								
									
										110
									
								
								src/components/SearchBar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/components/SearchBar.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| <script lang="ts" setup> | ||||
| import { useFuzzySearch } from '@/composable/fuzzySearch'; | ||||
| import { useTracker } from '@/modules/tracker/tracker.services'; | ||||
| import { tools } from '@/tools'; | ||||
| import type { Tool } from '@/tools/tools.types'; | ||||
| import { SearchRound } from '@vicons/material'; | ||||
| import { useMagicKeys, whenever } from '@vueuse/core'; | ||||
| import type { NInput } from 'naive-ui'; | ||||
| import { computed, h, ref } from 'vue'; | ||||
| import { useRouter } from 'vue-router'; | ||||
| import SearchBarItem from './SearchBarItem.vue'; | ||||
|  | ||||
| const toolToOption = (tool: Tool) => ({ label: tool.name, value: tool.path, tool }); | ||||
|  | ||||
| const router = useRouter(); | ||||
| const { tracker } = useTracker(); | ||||
|  | ||||
| const queryString = ref(''); | ||||
| const inputEl = ref<HTMLElement>(); | ||||
| const displayDropDown = ref(true); | ||||
| const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac')); | ||||
|  | ||||
| const options = computed(() => { | ||||
|   if (queryString.value === '') { | ||||
|     return tools.map(toolToOption); | ||||
|   } | ||||
|  | ||||
|   return searchResult.value.map(toolToOption); | ||||
| }); | ||||
|  | ||||
| const { searchResult } = useFuzzySearch({ | ||||
|   search: queryString, | ||||
|   data: tools, | ||||
|   options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] }, | ||||
| }); | ||||
|  | ||||
| const keys = useMagicKeys({ | ||||
|   passive: false, | ||||
|   onEventFired(e) { | ||||
|     if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') { | ||||
|       e.preventDefault(); | ||||
|     } | ||||
|  | ||||
|     if (e.metaKey && e.key === 'k' && e.type === 'keydown') { | ||||
|       e.preventDefault(); | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| whenever(keys.ctrl_k, claimFocus); | ||||
| whenever(keys.meta_k, claimFocus); | ||||
| whenever(keys.escape, releaseFocus); | ||||
|  | ||||
| function renderOption({ tool }: { tool: Tool }) { | ||||
|   return h(SearchBarItem, { tool }); | ||||
| } | ||||
|  | ||||
| function onSelect(path: string) { | ||||
|   router.push(path); | ||||
|   queryString.value = ''; | ||||
| } | ||||
|  | ||||
| function claimFocus() { | ||||
|   displayDropDown.value = true; | ||||
|  | ||||
|   inputEl.value?.focus(); | ||||
| } | ||||
|  | ||||
| function releaseFocus() { | ||||
|   displayDropDown.value = false; | ||||
| } | ||||
|  | ||||
| function onFocus() { | ||||
|   tracker.trackEvent({ eventName: 'Search-bar focused' }); | ||||
|   displayDropDown.value = true; | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div class="search-bar"> | ||||
|     <n-auto-complete | ||||
|       v-model:value="queryString" | ||||
|       :options="options" | ||||
|       :on-select="(value: string | number) => onSelect(String(value))" | ||||
|       :render-label="renderOption" | ||||
|       :default-value="'aa'" | ||||
|       :get-show="() => displayDropDown" | ||||
|       :on-focus="onFocus" | ||||
|       @update:value="() => (displayDropDown = true)" | ||||
|     > | ||||
|       <template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }"> | ||||
|         <n-input | ||||
|           ref="inputEl" | ||||
|           round | ||||
|           clearable | ||||
|           :placeholder="`Search a tool (use ${isMac ? 'Cmd' : 'Ctrl'} + K to focus)`" | ||||
|           :value="slotValue" | ||||
|           :input-props="{ autocomplete: 'disabled' }" | ||||
|           @input="handleInput" | ||||
|           @focus="handleFocus" | ||||
|           @blur="handleBlur" | ||||
|         > | ||||
|           <template #prefix> | ||||
|             <n-icon :component="SearchRound" /> | ||||
|           </template> | ||||
|         </n-input> | ||||
|       </template> | ||||
|     </n-auto-complete> | ||||
|   </div> | ||||
| </template> | ||||
							
								
								
									
										45
									
								
								src/components/SearchBarItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/components/SearchBarItem.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| <script lang="ts" setup> | ||||
| import type { Tool } from '@/tools/tools.types'; | ||||
| import { toRefs } from 'vue'; | ||||
|  | ||||
| const props = defineProps<{ tool: Tool }>(); | ||||
| const { tool } = toRefs(props); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div class="search-bar-item"> | ||||
|     <n-icon class="icon" :component="tool.icon" /> | ||||
|  | ||||
|     <div> | ||||
|       <div class="name">{{ tool.name }}</div> | ||||
|       <div class="description">{{ tool.description }}</div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| .search-bar-item { | ||||
|   padding: 10px; | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   align-items: center; | ||||
|  | ||||
|   .icon { | ||||
|     font-size: 30px; | ||||
|     margin-right: 10px; | ||||
|     opacity: 0.7; | ||||
|   } | ||||
|  | ||||
|   .name { | ||||
|     font-weight: bold; | ||||
|     font-size: 15px; | ||||
|     line-height: 1; | ||||
|     margin-bottom: 5px; | ||||
|   } | ||||
|  | ||||
|   .description { | ||||
|     opacity: 0.7; | ||||
|     line-height: 1; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| @@ -1,24 +1,32 @@ | ||||
| <template> | ||||
|   <n-tooltip trigger="hover"> | ||||
|     <template #trigger> | ||||
|       <span class="value" @click="handleClick">{{ value }}</span> | ||||
|     </template> | ||||
|     {{ tooltipText }} | ||||
|   </n-tooltip> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { useClipboard } from '@vueuse/core'; | ||||
| import { ref, toRefs } from 'vue'; | ||||
|  | ||||
| const props = withDefaults(defineProps<{ value?: string }>(), { value: '' }); | ||||
| const { value } = toRefs(props); | ||||
|  | ||||
| const initialText = 'Copy to clipboard'; | ||||
| const tooltipText = ref(initialText); | ||||
|  | ||||
| const { copy, isJustCopied } = useCopy({ source: value, createToast: false }); | ||||
| const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : initialText); | ||||
| const { copy } = useClipboard({ source: value }); | ||||
|  | ||||
| function handleClick() { | ||||
|   copy(); | ||||
|   tooltipText.value = 'Copied!'; | ||||
|  | ||||
|   setTimeout(() => (tooltipText.value = initialText), 1000); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <n-tooltip trigger="hover"> | ||||
|     <template #trigger> | ||||
|       <span class="value" @click="copy()">{{ value }}</span> | ||||
|     </template> | ||||
|     {{ tooltipText }} | ||||
|   </n-tooltip> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="less"> | ||||
| .value { | ||||
|   cursor: pointer; | ||||
|   | ||||
| @@ -1,43 +1,3 @@ | ||||
| <script setup lang="ts"> | ||||
| import { Copy } from '@vicons/tabler'; | ||||
| import { useElementSize } from '@vueuse/core'; | ||||
| import hljs from 'highlight.js/lib/core'; | ||||
| import jsonHljs from 'highlight.js/lib/languages/json'; | ||||
| import sqlHljs from 'highlight.js/lib/languages/sql'; | ||||
| import xmlHljs from 'highlight.js/lib/languages/xml'; | ||||
| import yamlHljs from 'highlight.js/lib/languages/yaml'; | ||||
| import iniHljs from 'highlight.js/lib/languages/ini'; | ||||
| import { useCopy } from '@/composable/copy'; | ||||
|  | ||||
| const props = withDefaults( | ||||
|   defineProps<{ | ||||
|     value: string | ||||
|     followHeightOf?: HTMLElement | null | ||||
|     language?: string | ||||
|     copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none' | ||||
|     copyMessage?: string | ||||
|   }>(), | ||||
|   { | ||||
|     followHeightOf: null, | ||||
|     language: 'txt', | ||||
|     copyPlacement: 'top-right', | ||||
|     copyMessage: 'Copy to clipboard', | ||||
|   }, | ||||
| ); | ||||
| hljs.registerLanguage('sql', sqlHljs); | ||||
| hljs.registerLanguage('json', jsonHljs); | ||||
| hljs.registerLanguage('html', xmlHljs); | ||||
| hljs.registerLanguage('xml', xmlHljs); | ||||
| hljs.registerLanguage('yaml', yamlHljs); | ||||
| hljs.registerLanguage('toml', iniHljs); | ||||
|  | ||||
| const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props); | ||||
| const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) }; | ||||
|  | ||||
| const { copy, isJustCopied } = useCopy({ source: value, createToast: false }); | ||||
| const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : copyMessage.value); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div style="overflow-x: hidden; width: 100%"> | ||||
|     <c-card class="result-card"> | ||||
| @@ -53,7 +13,7 @@ const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : copyMessage. | ||||
|       <n-tooltip v-if="value" trigger="hover"> | ||||
|         <template #trigger> | ||||
|           <div class="copy-button" :class="[copyPlacement]"> | ||||
|             <c-button circle important:h-10 important:w-10 @click="copy()"> | ||||
|             <c-button circle important:h-10 important:w-10 @click="onCopyClicked"> | ||||
|               <n-icon size="22" :component="Copy" /> | ||||
|             </c-button> | ||||
|           </div> | ||||
| @@ -61,14 +21,58 @@ const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : copyMessage. | ||||
|         <span>{{ tooltipText }}</span> | ||||
|       </n-tooltip> | ||||
|     </c-card> | ||||
|     <div v-if="copyPlacement === 'outside'" mt-4 flex justify-center> | ||||
|       <c-button @click="copy()"> | ||||
|         {{ tooltipText }} | ||||
|       </c-button> | ||||
|     </div> | ||||
|     <n-space v-if="copyPlacement === 'outside'" justify="center" mt-4> | ||||
|       <c-button @click="onCopyClicked"> {{ tooltipText }} </c-button> | ||||
|     </n-space> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { Copy } from '@vicons/tabler'; | ||||
| import { useClipboard, useElementSize } from '@vueuse/core'; | ||||
| import hljs from 'highlight.js/lib/core'; | ||||
| import jsonHljs from 'highlight.js/lib/languages/json'; | ||||
| import sqlHljs from 'highlight.js/lib/languages/sql'; | ||||
| import xmlHljs from 'highlight.js/lib/languages/xml'; | ||||
| import yamlHljs from 'highlight.js/lib/languages/yaml'; | ||||
| import { ref, toRefs } from 'vue'; | ||||
|  | ||||
| hljs.registerLanguage('sql', sqlHljs); | ||||
| hljs.registerLanguage('json', jsonHljs); | ||||
| hljs.registerLanguage('html', xmlHljs); | ||||
| hljs.registerLanguage('yaml', yamlHljs); | ||||
|  | ||||
| const props = withDefaults( | ||||
|   defineProps<{ | ||||
|     value: string; | ||||
|     followHeightOf?: HTMLElement | null; | ||||
|     language?: string; | ||||
|     copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none'; | ||||
|     copyMessage?: string; | ||||
|   }>(), | ||||
|   { | ||||
|     followHeightOf: null, | ||||
|     language: 'txt', | ||||
|     copyPlacement: 'top-right', | ||||
|     copyMessage: 'Copy to clipboard', | ||||
|   }, | ||||
| ); | ||||
| const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props); | ||||
| const { height } = followHeightOf ? useElementSize(followHeightOf) : { height: ref(null) }; | ||||
|  | ||||
| const { copy } = useClipboard({ source: value }); | ||||
| const tooltipText = ref(copyMessage.value); | ||||
|  | ||||
| function onCopyClicked() { | ||||
|   copy(); | ||||
|   tooltipText.value = 'Copied !'; | ||||
|  | ||||
|   setTimeout(() => { | ||||
|     tooltipText.value = copyMessage.value; | ||||
|   }, 2000); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| ::v-deep(.n-scrollbar) { | ||||
|   padding-bottom: 10px; | ||||
|   | ||||
| @@ -1,22 +1,9 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useThemeVars } from 'naive-ui'; | ||||
| import FavoriteButton from './FavoriteButton.vue'; | ||||
| import { useAppTheme } from '@/ui/theme/themes'; | ||||
| import type { Tool } from '@/tools/tools.types'; | ||||
|  | ||||
| const props = defineProps<{ tool: Tool & { category: string } }>(); | ||||
| const { tool } = toRefs(props); | ||||
| const theme = useThemeVars(); | ||||
|  | ||||
| const appTheme = useAppTheme(); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <router-link :to="tool.path"> | ||||
|     <c-card class="tool-card"> | ||||
|       <div flex items-center justify-between> | ||||
|       <n-space justify="space-between" align="center"> | ||||
|         <n-icon class="icon" size="40" :component="tool.icon" /> | ||||
|         <div flex items-center gap-8px> | ||||
|         <n-space align="center"> | ||||
|           <n-tag | ||||
|             v-if="tool.isNew" | ||||
|             size="small" | ||||
| @@ -29,9 +16,9 @@ const appTheme = useAppTheme(); | ||||
|             New | ||||
|           </n-tag> | ||||
|  | ||||
|           <FavoriteButton :tool="tool" /> | ||||
|         </div> | ||||
|       </div> | ||||
|           <favorite-button :tool="tool" /> | ||||
|         </n-space> | ||||
|       </n-space> | ||||
|       <n-h3 class="title"> | ||||
|         <n-ellipsis>{{ tool.name }}</n-ellipsis> | ||||
|       </n-h3> | ||||
| @@ -39,13 +26,27 @@ const appTheme = useAppTheme(); | ||||
|       <div class="description"> | ||||
|         <n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px"> | ||||
|           {{ tool.description }} | ||||
|           <br>  | ||||
|           <br />  | ||||
|         </n-ellipsis> | ||||
|       </div> | ||||
|     </c-card> | ||||
|   </router-link> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import type { Tool } from '@/tools/tools.types'; | ||||
| import { useThemeVars } from 'naive-ui'; | ||||
| import { toRefs } from 'vue'; | ||||
| import { useAppTheme } from '@/ui/theme/themes'; | ||||
| import FavoriteButton from './FavoriteButton.vue'; | ||||
|  | ||||
| const props = defineProps<{ tool: Tool & { category: string } }>(); | ||||
| const { tool } = toRefs(props); | ||||
| const theme = useThemeVars(); | ||||
|  | ||||
| const appTheme = useAppTheme(); | ||||
| </script> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| a { | ||||
|   text-decoration: none; | ||||
|   | ||||
| @@ -11,8 +11,7 @@ function computedRefreshable<T>(getter: () => T, { throttle }: { throttle?: numb | ||||
|  | ||||
|   if (throttle) { | ||||
|     watchThrottled(getter, update, { throttle }); | ||||
|   } | ||||
|   else { | ||||
|   } else { | ||||
|     watch(getter, update); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,30 +1,14 @@ | ||||
| // eslint-disable-next-line no-restricted-imports | ||||
| import { useClipboard } from '@vueuse/core'; | ||||
| import { useClipboard, type MaybeRef, get } from '@vueuse/core'; | ||||
| import { useMessage } from 'naive-ui'; | ||||
| import type { MaybeRefOrGetter } from 'vue'; | ||||
|  | ||||
| export function useCopy({ source, text = 'Copied to the clipboard', createToast = true }: { source?: MaybeRefOrGetter<string>; text?: string; createToast?: boolean } = {}) { | ||||
|   const { copy, copied, ...rest } = useClipboard({ | ||||
|     source, | ||||
|     legacy: true, | ||||
|   }); | ||||
|  | ||||
| export function useCopy({ source, text = 'Copied to the clipboard' }: { source: MaybeRef<unknown>; text?: string }) { | ||||
|   const { copy } = useClipboard({ source: computed(() => String(get(source))) }); | ||||
|   const message = useMessage(); | ||||
|  | ||||
|   return { | ||||
|     ...rest, | ||||
|     isJustCopied: copied, | ||||
|     async copy(content?: string, { notificationMessage }: { notificationMessage?: string } = {}) { | ||||
|       if (source) { | ||||
|         await copy(); | ||||
|       } | ||||
|       else { | ||||
|         await copy(content); | ||||
|       } | ||||
|  | ||||
|       if (createToast) { | ||||
|         message.success(notificationMessage ?? text); | ||||
|       } | ||||
|     async copy() { | ||||
|       await copy(); | ||||
|       message.success(text); | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
|   | ||||
| @@ -5,8 +5,8 @@ function getFileExtensionFromBase64({ | ||||
|   base64String, | ||||
|   defaultExtension = 'txt', | ||||
| }: { | ||||
|   base64String: string | ||||
|   defaultExtension?: string | ||||
|   base64String: string; | ||||
|   defaultExtension?: string; | ||||
| }) { | ||||
|   const hasMimeType = base64String.match(/data:(.*?);base64/i); | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { type MaybeRef, get } from '@vueuse/core'; | ||||
| import { get, type MaybeRef } from '@vueuse/core'; | ||||
| import Fuse from 'fuse.js'; | ||||
| import { computed } from 'vue'; | ||||
|  | ||||
| @@ -9,21 +9,14 @@ function useFuzzySearch<Data>({ | ||||
|   data, | ||||
|   options = {}, | ||||
| }: { | ||||
|   search: MaybeRef<string> | ||||
|   data: Data[] | ||||
|   options?: Fuse.IFuseOptions<Data> & { filterEmpty?: boolean } | ||||
|   search: MaybeRef<string>; | ||||
|   data: Data[]; | ||||
|   options?: Fuse.IFuseOptions<Data>; | ||||
| }) { | ||||
|   const fuse = new Fuse(data, options); | ||||
|   const filterEmpty = options.filterEmpty ?? true; | ||||
|  | ||||
|   const searchResult = computed<Data[]>(() => { | ||||
|     const query = get(search); | ||||
|  | ||||
|     if (!filterEmpty && query === '') { | ||||
|       return data; | ||||
|     } | ||||
|  | ||||
|     return fuse.search(query).map(({ item }) => item); | ||||
|   const searchResult = computed(() => { | ||||
|     return fuse.search(get(search)).map(({ item }) => item); | ||||
|   }); | ||||
|  | ||||
|   return { searchResult }; | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| /* eslint-disable @typescript-eslint/no-empty-function */ | ||||
| import { describe, expect, it } from 'vitest'; | ||||
| import { isFalsyOrHasThrown } from './validation'; | ||||
|  | ||||
| @@ -10,7 +11,7 @@ describe('useValidation', () => { | ||||
|       expect(isFalsyOrHasThrown(() => {})).toBe(true); | ||||
|       expect( | ||||
|         isFalsyOrHasThrown(() => { | ||||
|           throw new Error('message'); | ||||
|           throw new Error(); | ||||
|         }), | ||||
|       ).toBe(true); | ||||
|     }); | ||||
|   | ||||
| @@ -1,48 +1,44 @@ | ||||
| import { type MaybeRef, get } from '@vueuse/core'; | ||||
| import _ from 'lodash'; | ||||
| import { type Ref, reactive, watch } from 'vue'; | ||||
| import { reactive, watch, type Ref } from 'vue'; | ||||
|  | ||||
| type ValidatorReturnType = unknown; | ||||
|  | ||||
| export interface UseValidationRule<T> { | ||||
|   validator: (value: T) => ValidatorReturnType | ||||
|   message: string | ||||
|   validator: (value: T) => ValidatorReturnType; | ||||
|   message: string; | ||||
| } | ||||
|  | ||||
| export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean { | ||||
|   try { | ||||
|     const returnValue = cb(); | ||||
|  | ||||
|     if (_.isNil(returnValue)) { | ||||
|       return true; | ||||
|     } | ||||
|     if (_.isNil(returnValue)) return true; | ||||
|  | ||||
|     return returnValue === false; | ||||
|   } | ||||
|   catch (_) { | ||||
|   } catch (_) { | ||||
|     return true; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export interface ValidationAttrs { | ||||
|   feedback: string | ||||
|   validationStatus: string | undefined | ||||
| } | ||||
| export type ValidationAttrs = { | ||||
|   feedback: string; | ||||
|   validationStatus: string | undefined; | ||||
| }; | ||||
|  | ||||
| export function useValidation<T>({ | ||||
|   source, | ||||
|   rules, | ||||
|   watch: watchRefs = [], | ||||
| }: { | ||||
|   source: Ref<T> | ||||
|   rules: MaybeRef<UseValidationRule<T>[]> | ||||
|   watch?: Ref<unknown>[] | ||||
|   source: Ref<T>; | ||||
|   rules: UseValidationRule<T>[]; | ||||
|   watch?: Ref<unknown>[]; | ||||
| }) { | ||||
|   const state = reactive<{ | ||||
|     message: string | ||||
|     status: undefined | 'error' | ||||
|     isValid: boolean | ||||
|     attrs: ValidationAttrs | ||||
|     message: string; | ||||
|     status: undefined | 'error'; | ||||
|     isValid: boolean; | ||||
|     attrs: ValidationAttrs; | ||||
|   }>({ | ||||
|     message: '', | ||||
|     status: undefined, | ||||
| @@ -59,7 +55,7 @@ export function useValidation<T>({ | ||||
|       state.message = ''; | ||||
|       state.status = undefined; | ||||
|  | ||||
|       for (const rule of get(rules)) { | ||||
|       for (const rule of rules) { | ||||
|         if (isFalsyOrHasThrown(() => rule.validator(source.value))) { | ||||
|           state.message = rule.message; | ||||
|           state.status = 'error'; | ||||
|   | ||||
| @@ -1,12 +1,8 @@ | ||||
| <script lang="ts" setup> | ||||
| import { NIcon, useThemeVars } from 'naive-ui'; | ||||
|  | ||||
| import { computed } from 'vue'; | ||||
| import { RouterLink } from 'vue-router'; | ||||
| import { Heart, Home2, Menu2 } from '@vicons/tabler'; | ||||
|  | ||||
| import HeroGradient from '../assets/hero-gradient.svg?component'; | ||||
| import MenuLayout from '../components/MenuLayout.vue'; | ||||
| import NavbarButtons from '../components/NavbarButtons.vue'; | ||||
| import { Heart, Menu2, Home2 } from '@vicons/tabler'; | ||||
| import { toolsByCategory } from '@/tools'; | ||||
| import { useStyleStore } from '@/stores/style.store'; | ||||
| import { config } from '@/config'; | ||||
| @@ -14,6 +10,10 @@ import type { ToolCategory } from '@/tools/tools.types'; | ||||
| import { useToolStore } from '@/tools/tools.store'; | ||||
| import { useTracker } from '@/modules/tracker/tracker.services'; | ||||
| import CollapsibleToolMenu from '@/components/CollapsibleToolMenu.vue'; | ||||
| import SearchBar from '../components/SearchBar.vue'; | ||||
| import HeroGradient from '../assets/hero-gradient.svg?component'; | ||||
| import MenuLayout from '../components/MenuLayout.vue'; | ||||
| import NavbarButtons from '../components/NavbarButtons.vue'; | ||||
|  | ||||
| const themeVars = useThemeVars(); | ||||
| const styleStore = useStyleStore(); | ||||
| @@ -31,27 +31,23 @@ const tools = computed<ToolCategory[]>(() => [ | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <MenuLayout class="menu-layout" :class="{ isSmallScreen: styleStore.isSmallScreen }"> | ||||
|   <menu-layout class="menu-layout" :class="{ isSmallScreen: styleStore.isSmallScreen }"> | ||||
|     <template #sider> | ||||
|       <RouterLink to="/" class="hero-wrapper"> | ||||
|         <HeroGradient class="gradient" /> | ||||
|       <router-link to="/" class="hero-wrapper"> | ||||
|         <hero-gradient class="gradient" /> | ||||
|         <div class="text-wrapper"> | ||||
|           <div class="title"> | ||||
|             IT - TOOLS | ||||
|           </div> | ||||
|           <div class="title">IT - TOOLS</div> | ||||
|           <div class="divider" /> | ||||
|           <div class="subtitle"> | ||||
|             Handy tools for developers | ||||
|           </div> | ||||
|           <div class="subtitle">Handy tools for developers</div> | ||||
|         </div> | ||||
|       </RouterLink> | ||||
|       </router-link> | ||||
|  | ||||
|       <div class="sider-content"> | ||||
|         <div v-if="styleStore.isSmallScreen" flex justify-center> | ||||
|           <NavbarButtons /> | ||||
|         </div> | ||||
|         <n-space v-if="styleStore.isSmallScreen" justify="center"> | ||||
|           <navbar-buttons /> | ||||
|         </n-space> | ||||
|  | ||||
|         <CollapsibleToolMenu :tools-by-category="tools" /> | ||||
|         <collapsible-tool-menu :tools-by-category="tools" /> | ||||
|  | ||||
|         <div class="footer"> | ||||
|           <div> | ||||
| @@ -75,43 +71,36 @@ const tools = computed<ToolCategory[]>(() => [ | ||||
|           </div> | ||||
|           <div> | ||||
|             © {{ new Date().getFullYear() }} | ||||
|             <c-link target="_blank" rel="noopener" href="https://github.com/CorentinTh"> | ||||
|               Corentin Thomasset | ||||
|             </c-link> | ||||
|             <c-link target="_blank" rel="noopener" href="https://github.com/CorentinTh"> Corentin Thomasset </c-link> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </template> | ||||
|  | ||||
|     <template #content> | ||||
|       <div flex items-center justify-center gap-2> | ||||
|       <div class="navigation"> | ||||
|         <c-button | ||||
|           :size="styleStore.isSmallScreen ? 'medium' : 'large'" | ||||
|           circle | ||||
|           variant="text" | ||||
|           aria-label="Toggle menu" | ||||
|           @click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed" | ||||
|         > | ||||
|           <NIcon size="25" :component="Menu2" /> | ||||
|           <n-icon size="25" :component="Menu2" /> | ||||
|         </c-button> | ||||
|  | ||||
|         <n-tooltip trigger="hover"> | ||||
|           <template #trigger> | ||||
|             <c-button to="/" circle variant="text" aria-label="Home"> | ||||
|               <NIcon size="25" :component="Home2" /> | ||||
|               <n-icon size="25" :component="Home2" /> | ||||
|             </c-button> | ||||
|           </template> | ||||
|           Home | ||||
|         </n-tooltip> | ||||
|  | ||||
|         <c-button v-if="config.app.env === 'development'" to="/c-lib" circle variant="text" aria-label="UI Lib"> | ||||
|           <icon-mdi:brush-variant text-20px /> | ||||
|         </c-button> | ||||
|         <search-bar /> | ||||
|  | ||||
|         <command-palette /> | ||||
|  | ||||
|         <div> | ||||
|           <NavbarButtons v-if="!styleStore.isSmallScreen" /> | ||||
|         </div> | ||||
|         <navbar-buttons v-if="!styleStore.isSmallScreen" /> | ||||
|  | ||||
|         <n-tooltip trigger="hover"> | ||||
|           <template #trigger> | ||||
| @@ -125,7 +114,7 @@ const tools = computed<ToolCategory[]>(() => [ | ||||
|               @click="() => tracker.trackEvent({ eventName: 'Support button clicked' })" | ||||
|             > | ||||
|               Buy me a coffee | ||||
|               <NIcon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 /> | ||||
|               <n-icon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 /> | ||||
|             </c-button> | ||||
|           </template> | ||||
|           ❤ Support IT Tools development ! | ||||
| @@ -133,7 +122,7 @@ const tools = computed<ToolCategory[]>(() => [ | ||||
|       </div> | ||||
|       <slot /> | ||||
|     </template> | ||||
|   </MenuLayout> | ||||
|   </menu-layout> | ||||
| </template> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| @@ -211,4 +200,25 @@ const tools = computed<ToolCategory[]>(() => [ | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // ::v-deep(.n-menu-item-content-header) { | ||||
| //   overflow: visible !important; | ||||
| //   // overflow-x: hidden !important; | ||||
| // } | ||||
|  | ||||
| .navigation { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   flex-direction: row; | ||||
|  | ||||
|   & > *:not(:last-child) { | ||||
|     margin-right: 5px; | ||||
|   } | ||||
|  | ||||
|   .search-bar { | ||||
|     // width: 100%; | ||||
|     flex-grow: 1; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -2,10 +2,10 @@ | ||||
| import { useRoute } from 'vue-router'; | ||||
| import { useHead } from '@vueuse/head'; | ||||
| import type { HeadObject } from '@vueuse/head'; | ||||
|  | ||||
| import BaseLayout from './base.layout.vue'; | ||||
| import { computed } from 'vue'; | ||||
| import FavoriteButton from '@/components/FavoriteButton.vue'; | ||||
| import type { Tool } from '@/tools/tools.types'; | ||||
| import BaseLayout from './base.layout.vue'; | ||||
|  | ||||
| const route = useRoute(); | ||||
|  | ||||
| @@ -23,31 +23,26 @@ const head = computed<HeadObject>(() => ({ | ||||
|   ], | ||||
| })); | ||||
| useHead(head); | ||||
| const { t } = useI18n(); | ||||
|  | ||||
| const i18nKey = computed<string>(() => route.path.trim().replace('/', '')); | ||||
| const toolTitle = computed<string>(() => t(`tools.${i18nKey.value}.title`, String(route.meta.name))); | ||||
| const toolDescription = computed<string>(() => t(`tools.${i18nKey.value}.description`, String(route.meta.description))); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <BaseLayout> | ||||
|   <base-layout> | ||||
|     <div class="tool-layout"> | ||||
|       <div class="tool-header"> | ||||
|         <div flex flex-nowrap items-center justify-between> | ||||
|         <n-space align="center" justify="space-between" :wrap="false"> | ||||
|           <n-h1> | ||||
|             {{ toolTitle }} | ||||
|             {{ route.meta.name }} | ||||
|           </n-h1> | ||||
|  | ||||
|           <div> | ||||
|             <FavoriteButton :tool="{ name: route.meta.name } as Tool" /> | ||||
|             <favorite-button :tool="{name: route.meta.name} as Tool" /> | ||||
|           </div> | ||||
|         </div> | ||||
|         </n-space> | ||||
|  | ||||
|         <div class="separator" /> | ||||
|  | ||||
|         <div class="description"> | ||||
|           {{ toolDescription }} | ||||
|           {{ route.meta.description }} | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| @@ -55,7 +50,7 @@ const toolDescription = computed<string>(() => t(`tools.${i18nKey.value}.descrip | ||||
|     <div class="tool-content"> | ||||
|       <slot /> | ||||
|     </div> | ||||
|   </BaseLayout> | ||||
|   </base-layout> | ||||
| </template> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
|   | ||||
| @@ -1,25 +1,23 @@ | ||||
| import { createApp } from 'vue'; | ||||
| import { createPinia } from 'pinia'; | ||||
| import { createHead } from '@vueuse/head'; | ||||
|  | ||||
| // eslint-disable-next-line import/no-unresolved | ||||
| import { registerSW } from 'virtual:pwa-register'; | ||||
| import { plausible } from './plugins/plausible.plugin'; | ||||
|  | ||||
| import 'virtual:uno.css'; | ||||
|  | ||||
| registerSW(); | ||||
|  | ||||
| import { naive } from './plugins/naive.plugin'; | ||||
|  | ||||
| import App from './App.vue'; | ||||
| import router from './router'; | ||||
| import { i18nPlugin } from './plugins/i18n.plugin'; | ||||
|  | ||||
| registerSW(); | ||||
|  | ||||
| const app = createApp(App); | ||||
|  | ||||
| app.use(createPinia()); | ||||
| app.use(createHead()); | ||||
| app.use(i18nPlugin); | ||||
| app.use(router); | ||||
| app.use(naive); | ||||
| app.use(plausible); | ||||
|   | ||||
| @@ -1,82 +0,0 @@ | ||||
| import { defineStore } from 'pinia'; | ||||
| import _ from 'lodash'; | ||||
| import type { PaletteOption } from './command-palette.types'; | ||||
| import { useToolStore } from '@/tools/tools.store'; | ||||
| import { useFuzzySearch } from '@/composable/fuzzySearch'; | ||||
| import { useStyleStore } from '@/stores/style.store'; | ||||
|  | ||||
| import SunIcon from '~icons/mdi/white-balance-sunny'; | ||||
| import GithubIcon from '~icons/mdi/github'; | ||||
| import BugIcon from '~icons/mdi/bug-outline'; | ||||
| import DiceIcon from '~icons/mdi/dice-5'; | ||||
|  | ||||
| export const useCommandPaletteStore = defineStore('command-palette', () => { | ||||
|   const toolStore = useToolStore(); | ||||
|   const styleStore = useStyleStore(); | ||||
|   const router = useRouter(); | ||||
|   const searchPrompt = ref(''); | ||||
|  | ||||
|   const toolsOptions = toolStore.tools.map(tool => ({ | ||||
|     ...tool, | ||||
|     to: tool.path, | ||||
|     toolCategory: tool.category, | ||||
|     category: 'Tools', | ||||
|   })); | ||||
|  | ||||
|   const searchOptions: PaletteOption[] = [ | ||||
|     ...toolsOptions, | ||||
|     { | ||||
|       name: 'Random tool', | ||||
|       description: 'Get a random tool from the list.', | ||||
|       action: () => { | ||||
|         const { path } = _.sample(toolStore.tools)!; | ||||
|         router.push(path); | ||||
|       }, | ||||
|       icon: DiceIcon, | ||||
|       category: 'Tools', | ||||
|       keywords: ['random', 'tool', 'pick', 'choose', 'select'], | ||||
|       closeOnSelect: true, | ||||
|     }, | ||||
|     { | ||||
|       name: 'Toggle dark mode', | ||||
|       description: 'Toggle dark mode on or off.', | ||||
|       action: () => styleStore.toggleDark(), | ||||
|       icon: SunIcon, | ||||
|       category: 'Actions', | ||||
|       keywords: ['dark', 'theme', 'toggle', 'mode', 'light', 'system'], | ||||
|     }, | ||||
|     { | ||||
|       name: 'Github repository', | ||||
|       href: 'https://github.com/CorentinTh/it-tools', | ||||
|       category: 'External', | ||||
|       description: 'View the source code of it-tools on Github.', | ||||
|       keywords: ['github', 'repo', 'repository', 'source', 'code'], | ||||
|       icon: GithubIcon, | ||||
|     }, | ||||
|     { | ||||
|       name: 'Report a bug or an issue', | ||||
|       description: 'Report a bug or an issue to help improve it-tools.', | ||||
|       href: 'https://github.com/CorentinTh/it-tools/issues/new/choose', | ||||
|       category: 'Actions', | ||||
|       keywords: ['report', 'issue', 'bug', 'problem', 'error'], | ||||
|       icon: BugIcon, | ||||
|     }, | ||||
|   ]; | ||||
|  | ||||
|   const { searchResult } = useFuzzySearch({ | ||||
|     search: searchPrompt, | ||||
|     data: searchOptions, | ||||
|     options: { | ||||
|       keys: [{ name: 'name', weight: 2 }, 'description', 'keywords', 'category'], | ||||
|       threshold: 0.3, | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   const filteredSearchResult = computed(() => | ||||
|     _.chain(searchResult.value).groupBy('category').mapValues(categoryOptions => _.take(categoryOptions, 5)).value()); | ||||
|  | ||||
|   return { | ||||
|     filteredSearchResult, | ||||
|     searchPrompt, | ||||
|   }; | ||||
| }); | ||||
| @@ -1,14 +0,0 @@ | ||||
| import type { Component } from 'vue'; | ||||
| import type { RouteLocationRaw } from 'vue-router'; | ||||
|  | ||||
| export interface PaletteOption { | ||||
|   name: string | ||||
|   description?: string | ||||
|   icon?: Component | ||||
|   action?: () => void | ||||
|   to?: RouteLocationRaw | ||||
|   category: string | ||||
|   keywords?: string[] | ||||
|   href?: string | ||||
|   closeOnSelect?: boolean | ||||
| } | ||||
| @@ -1,153 +0,0 @@ | ||||
| <script setup lang="ts"> | ||||
| import { storeToRefs } from 'pinia'; | ||||
| import _ from 'lodash'; | ||||
| import { useCommandPaletteStore } from './command-palette.store'; | ||||
| import type { PaletteOption } from './command-palette.types'; | ||||
|  | ||||
| const isModalOpen = ref(false); | ||||
| const inputRef = ref(); | ||||
| const router = useRouter(); | ||||
| const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac')); | ||||
|  | ||||
| const commandPaletteStore = useCommandPaletteStore(); | ||||
| const { searchPrompt, filteredSearchResult } = storeToRefs(commandPaletteStore); | ||||
|  | ||||
| const keys = useMagicKeys({ | ||||
|   passive: false, | ||||
|   onEventFired(e) { | ||||
|     if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') { | ||||
|       e.preventDefault(); | ||||
|     } | ||||
|  | ||||
|     if (e.metaKey && e.key === 'k' && e.type === 'keydown') { | ||||
|       e.preventDefault(); | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| whenever(isModalOpen, () => inputRef.value?.focus()); | ||||
|  | ||||
| whenever(keys.ctrl_k, open); | ||||
| whenever(keys.meta_k, open); | ||||
| whenever(keys.escape, close); | ||||
|  | ||||
| function open() { | ||||
|   return isModalOpen.value = true; | ||||
| } | ||||
|  | ||||
| function close() { | ||||
|   isModalOpen.value = false; | ||||
| } | ||||
|  | ||||
| const selectedOptionIndex = ref(0); | ||||
|  | ||||
| function handleKeydown(event: KeyboardEvent) { | ||||
|   const { key } = event; | ||||
|   const isEnterPressed = key === 'Enter'; | ||||
|   const isArrowUpOrDown = ['ArrowUp', 'ArrowDown'].includes(key); | ||||
|   const isArrowDown = key === 'ArrowDown'; | ||||
|  | ||||
|   if (isArrowUpOrDown) { | ||||
|     const increment = isArrowDown ? 1 : -1; | ||||
|     const maxIndex = Math.max(_.chain(filteredSearchResult.value).values().flatten().size().value() - 1, 0); | ||||
|  | ||||
|     selectedOptionIndex.value = Math.min(Math.max(selectedOptionIndex.value + increment, 0), maxIndex); | ||||
|  | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (isEnterPressed) { | ||||
|     const option = _.chain(filteredSearchResult.value) | ||||
|       .values() | ||||
|       .flatten() | ||||
|       .nth(selectedOptionIndex.value) | ||||
|       .value(); | ||||
|  | ||||
|     activateOption(option); | ||||
|   } | ||||
| } | ||||
|  | ||||
| function getOptionIndex(option: PaletteOption) { | ||||
|   return _.chain(filteredSearchResult.value) | ||||
|     .values() | ||||
|     .flatten() | ||||
|     .findIndex(o => o === option) | ||||
|     .value(); | ||||
| } | ||||
|  | ||||
| function activateOption(option: PaletteOption) { | ||||
|   const { closeOnSelect } = option; | ||||
|  | ||||
|   if (option.action) { | ||||
|     option.action(); | ||||
|  | ||||
|     if (closeOnSelect) { | ||||
|       close(); | ||||
|     } | ||||
|  | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   const closeAfterNavigation = closeOnSelect || _.isUndefined(closeOnSelect); | ||||
|  | ||||
|   if (option.to) { | ||||
|     router.push(option.to); | ||||
|  | ||||
|     if (closeAfterNavigation) { | ||||
|       close(); | ||||
|     } | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (option.href) { | ||||
|     window.open(option.href, '_blank'); | ||||
|  | ||||
|     if (closeAfterNavigation) { | ||||
|       close(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div flex-1> | ||||
|     <c-button w-full important:justify-start @click="isModalOpen = true"> | ||||
|       <span flex items-center gap-3 op-40> | ||||
|  | ||||
|         <icon-mdi-search /> | ||||
|         Search... | ||||
|  | ||||
|         <span hidden flex-1 border border-current border-op-40 rounded border-solid px-5px py-3px sm:inline> | ||||
|           {{ isMac ? 'Cmd' : 'Ctrl' }} + K | ||||
|         </span> | ||||
|       </span> | ||||
|     </c-button> | ||||
|  | ||||
|     <c-modal v-model:open="isModalOpen" class="palette-modal" shadow-xl important:max-w-650px important:pa-12px @keydown="handleKeydown"> | ||||
|       <c-input-text ref="inputRef" v-model:value="searchPrompt" raw-text placeholder="Type to search a tool or a command..." autofocus clearable /> | ||||
|  | ||||
|       <div v-for="(options, category) in filteredSearchResult" :key="category"> | ||||
|         <div ml-3 mt-3 text-sm font-bold text-primary op-60> | ||||
|           {{ category }} | ||||
|         </div> | ||||
|         <command-palette-option v-for="option in options" :key="option.name" :option="option" :selected="selectedOptionIndex === getOptionIndex(option)" @activated="activateOption" /> | ||||
|       </div> | ||||
|     </c-modal> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="less"> | ||||
| .c-input-text { | ||||
|   font-size: 18px; | ||||
|  | ||||
|   ::v-deep(.input-wrapper) { | ||||
|       padding: 4px; | ||||
|       padding-left: 18px; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .c-modal--overlay { | ||||
|   align-items: flex-start !important; | ||||
|   padding-top: 80px; | ||||
| } | ||||
| </style> | ||||
| @@ -1,36 +0,0 @@ | ||||
| <script setup lang="ts"> | ||||
| import type { PaletteOption } from '../command-palette.types'; | ||||
|  | ||||
| const props = withDefaults(defineProps<{ option: PaletteOption; selected?: boolean }>(), { | ||||
|   selected: false, | ||||
| }); | ||||
| const emit = defineEmits(['activated']); | ||||
| const { option } = toRefs(props); | ||||
|  | ||||
| const { selected } = toRefs(props); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div | ||||
|     role="option" | ||||
|     :aria-selected="selected" | ||||
|     :class="{ | ||||
|       'text-white': selected, | ||||
|       'bg-primary': selected, | ||||
|     }" | ||||
|     w-full flex cursor-pointer items-center overflow-hidden rounded pa-3 transition hover:bg-primary hover:text-white | ||||
|     @click="() => emit('activated', option)" | ||||
|   > | ||||
|     <component :is="option.icon" v-if="option.icon" mr-3 h-30px w-30px shrink-0 op-50 /> | ||||
|  | ||||
|     <div flex-1 overflow-hidden> | ||||
|       <div truncate font-bold lh-tight op-90> | ||||
|         {{ option.name }} | ||||
|       </div> | ||||
|  | ||||
|       <div v-if="option.description" truncate lh-tight op-60> | ||||
|         {{ option.description }} | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -1,7 +0,0 @@ | ||||
| import { format } from 'date-fns'; | ||||
|  | ||||
| export { getUrlFriendlyDateTime }; | ||||
|  | ||||
| function getUrlFriendlyDateTime({ date = new Date() }: { date?: Date } = {}) { | ||||
|   return format(date, 'yyyy-MM-dd-HH-mm-ss'); | ||||
| } | ||||
| @@ -1,5 +0,0 @@ | ||||
| function clamp({ value, min = 0, max = 100 }: { value: number; min?: number; max?: number }) { | ||||
|   return Math.min(Math.max(value, min), max); | ||||
| } | ||||
|  | ||||
| export { clamp }; | ||||
| @@ -16,7 +16,7 @@ function useTracker() { | ||||
|   const plausible: ReturnType<typeof Plausible> | undefined = inject('plausible'); | ||||
|  | ||||
|   if (_.isNil(plausible)) { | ||||
|     throw new TypeError('Plausible must be instantiated'); | ||||
|     throw new Error('Plausible must be instantiated'); | ||||
|   } | ||||
|  | ||||
|   const tracker = createTrackerService({ plausible }); | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| <script setup lang="ts"> | ||||
| import { Coffee } from '@vicons/tabler'; | ||||
| import { useHead } from '@vueuse/head'; | ||||
|  | ||||
| useHead({ title: 'Page not found - IT Tools' }); | ||||
| @@ -6,22 +7,12 @@ useHead({ title: 'Page not found - IT Tools' }); | ||||
|  | ||||
| <template> | ||||
|   <div mt-20 flex flex-col items-center> | ||||
|     <span text-90px lh-1 op-50> | ||||
|       <icon-mdi:kettle-steam-outline /> | ||||
|     </span> | ||||
|     <n-icon :component="Coffee" size="100" depth="3" /> | ||||
|  | ||||
|     <h1 m-0 mt-3> | ||||
|       404 Not Found | ||||
|     </h1> | ||||
|     <div mt-4 op-60> | ||||
|       Sorry, this page does not seem to exist | ||||
|     </div> | ||||
|     <div mb-8 op-60> | ||||
|       Maybe the cache is doing tricky things, try force-refreshing? | ||||
|     </div> | ||||
|     <n-h1 m-0 mt-3>404 Not Found</n-h1> | ||||
|     <n-text mt-4 block depth="3">Sorry, this page does not seem to exist</n-text> | ||||
|     <n-text mb-8 block depth="3">Maybe the cache is doing tricky things, try force-refreshing?</n-text> | ||||
|  | ||||
|     <c-button to="/"> | ||||
|       Back home | ||||
|     </c-button> | ||||
|     <c-button to="/"> Back home </c-button> | ||||
|   </div> | ||||
| </template> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useHead } from '@vueuse/head'; | ||||
| import { useTracker } from '@/modules/tracker/tracker.services'; | ||||
| import { useHead } from '@vueuse/head'; | ||||
|  | ||||
| useHead({ title: 'About - IT Tools' }); | ||||
| const { tracker } = useTracker(); | ||||
| @@ -11,15 +11,13 @@ const { tracker } = useTracker(); | ||||
|     <n-h1>About</n-h1> | ||||
|     <n-p> | ||||
|       This wonderful website, made with ❤ by | ||||
|       <c-link href="https://github.com/CorentinTh" target="_blank" rel="noopener"> | ||||
|         Corentin Thomasset | ||||
|       </c-link>, | ||||
|       aggregates useful tools for developer and people working in IT. If you find it useful, please feel free to share | ||||
|       it to people you think may find it useful too and don't forget to bookmark it in your shortcut bar! | ||||
|       <c-link href="https://github.com/CorentinTh" target="_blank" rel="noopener"> Corentin Thomasset </c-link>, | ||||
|       aggregates useful tools for developer and people working in IT. If you find it useful, please fell free to share | ||||
|       it to people you think may find it useful too and don't forget to pin it in your shortcut bar ! | ||||
|     </n-p> | ||||
|     <n-p> | ||||
|       IT Tools is open-source (under the MIT license) and free, and will always be, but it costs me money to host and | ||||
|       renew the domain name. If you want to support my work, and encourage me to add more tools, please consider | ||||
|       IT Tools is open-source (under the MIT license) and free, and will always be, but it cost me money to host and | ||||
|       renew the domain name, if you want to support my work, and encourage me to add more tools, please consider | ||||
|       supporting by | ||||
|       <c-link | ||||
|         href="https://www.buymeacoffee.com/cthmsst" | ||||
| @@ -27,14 +25,14 @@ const { tracker } = useTracker(); | ||||
|         target="_blank" | ||||
|         @click="() => tracker.trackEvent({ eventName: 'Support button clicked' })" | ||||
|       > | ||||
|         sponsoring me | ||||
|       </c-link>. | ||||
|         sponsoring me </c-link | ||||
|       >. | ||||
|     </n-p> | ||||
|  | ||||
|     <n-h2>Technologies</n-h2> | ||||
|     <n-p> | ||||
|       IT Tools is made in Vue.js (Vue 3) with the the Naive UI component library and is hosted and continuously deployed | ||||
|       by Vercel. Third-party open-source libraries are used in some tools, you may find the complete list in the | ||||
|       IT Tools is made in Vue JS (vue 3) with the the naive-ui component library and is hosted and continuously deployed | ||||
|       by Vercel. Third party open-source libraries are used in some tools, you may find the complete list in the | ||||
|       <c-link href="https://github.com/CorentinTh/it-tools/blob/main/package.json" rel="noopener" target="_blank"> | ||||
|         package.json | ||||
|       </c-link> | ||||
| @@ -43,10 +41,10 @@ const { tracker } = useTracker(); | ||||
|  | ||||
|     <n-h2>Found a bug? A tool is missing?</n-h2> | ||||
|     <n-p> | ||||
|       If you need a tool that is currently not present here, and you think can be useful, you are welcome to submit a | ||||
|       If you need a tool that is currently not present here, and you think can be relevant, you are welcome to submit a | ||||
|       feature request in the | ||||
|       <c-link | ||||
|         href="https://github.com/CorentinTh/it-tools/issues/new/choose" | ||||
|         href="https://github.com/CorentinTh/it-tools/issues/new?assignees=CorentinTh&labels=enhancement&template=feature_request.md&title=%5BFEAT%5D%20My%20feature" | ||||
|         rel="noopener" | ||||
|         target="_blank" | ||||
|       > | ||||
| @@ -55,9 +53,9 @@ const { tracker } = useTracker(); | ||||
|       in the GitHub repository. | ||||
|     </n-p> | ||||
|     <n-p> | ||||
|       And if you found a bug, or something doesn't work as expected, please file a bug report in the | ||||
|       And if you found a bug, or something broken that doesn't work as expected, please fill a bug report in the | ||||
|       <c-link | ||||
|         href="https://github.com/CorentinTh/it-tools/issues/new/choose" | ||||
|         href="https://github.com/CorentinTh/it-tools/issues/new?assignees=CorentinTh&labels=bug&template=bug_report.md&title=%5BBUG%5D%20My%20bug" | ||||
|         rel="noopener" | ||||
|         target="_blank" | ||||
|       > | ||||
|   | ||||
| @@ -1,15 +1,14 @@ | ||||
| <script setup lang="ts"> | ||||
| import { config } from '@/config'; | ||||
| import { useToolStore } from '@/tools/tools.store'; | ||||
| import { Heart } from '@vicons/tabler'; | ||||
| import { useHead } from '@vueuse/head'; | ||||
| import ColoredCard from '../components/ColoredCard.vue'; | ||||
| import ToolCard from '../components/ToolCard.vue'; | ||||
| import { useToolStore } from '@/tools/tools.store'; | ||||
| import { config } from '@/config'; | ||||
|  | ||||
| const toolStore = useToolStore(); | ||||
|  | ||||
| useHead({ title: 'IT Tools - Handy online tools for developers' }); | ||||
| const { t } = useI18n(); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
| @@ -17,23 +16,25 @@ const { t } = useI18n(); | ||||
|     <div class="grid-wrapper"> | ||||
|       <n-grid v-if="config.showBanner" x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> | ||||
|         <n-gi> | ||||
|           <ColoredCard title="You like it-tools?" :icon="Heart"> | ||||
|           <colored-card title="You like it-tools?" :icon="Heart"> | ||||
|             Give us a star on | ||||
|             <a | ||||
|               href="https://github.com/CorentinTh/it-tools" | ||||
|               rel="noopener" | ||||
|               target="_blank" | ||||
|               aria-label="IT-Tools' GitHub repository" | ||||
|             >GitHub</a> | ||||
|               >GitHub</a | ||||
|             > | ||||
|             or follow us on | ||||
|             <a | ||||
|               href="https://twitter.com/ittoolsdottech" | ||||
|               rel="noopener" | ||||
|               target="_blank" | ||||
|               aria-label="IT-Tools' Twitter account" | ||||
|             >Twitter</a>! Thank you | ||||
|               >Twitter</a | ||||
|             >! Thank you | ||||
|             <n-icon :component="Heart" /> | ||||
|           </ColoredCard> | ||||
|           </colored-card> | ||||
|         </n-gi> | ||||
|       </n-grid> | ||||
|  | ||||
| @@ -42,17 +43,17 @@ const { t } = useI18n(); | ||||
|           <n-h3>Your favorite tools</n-h3> | ||||
|           <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> | ||||
|             <n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name"> | ||||
|               <ToolCard :tool="tool" /> | ||||
|               <tool-card :tool="tool" /> | ||||
|             </n-gi> | ||||
|           </n-grid> | ||||
|         </div> | ||||
|       </transition> | ||||
|  | ||||
|       <div v-if="toolStore.newTools.length > 0"> | ||||
|         <n-h3>{{ t('home.categories.newestTools', 'Newest tools') }}</n-h3> | ||||
|         <n-h3>Newest tools</n-h3> | ||||
|         <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> | ||||
|           <n-gi v-for="tool in toolStore.newTools" :key="tool.name"> | ||||
|             <ToolCard :tool="tool" /> | ||||
|             <tool-card :tool="tool" /> | ||||
|           </n-gi> | ||||
|         </n-grid> | ||||
|       </div> | ||||
| @@ -61,7 +62,7 @@ const { t } = useI18n(); | ||||
|       <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> | ||||
|         <n-gi v-for="tool in toolStore.tools" :key="tool.name"> | ||||
|           <transition> | ||||
|             <ToolCard :tool="tool" /> | ||||
|             <tool-card :tool="tool" /> | ||||
|           </transition> | ||||
|         </n-gi> | ||||
|       </n-grid> | ||||
|   | ||||
| @@ -1,15 +0,0 @@ | ||||
| import type { App } from 'vue'; | ||||
| import { createI18n } from 'vue-i18n'; | ||||
| import messages from '@intlify/unplugin-vue-i18n/messages'; | ||||
|  | ||||
| const i18n = createI18n({ | ||||
|   legacy: false, | ||||
|   locale: 'en', | ||||
|   messages, | ||||
| }); | ||||
|  | ||||
| export const i18nPlugin = { | ||||
|   install: (app: App) => { | ||||
|     app.use(i18n); | ||||
|   }, | ||||
| }; | ||||
| @@ -1,8 +1,8 @@ | ||||
| import { config } from '@/config'; | ||||
| import { noop } from 'lodash'; | ||||
|  | ||||
| import Plausible from 'plausible-tracker'; | ||||
| import type { App } from 'vue'; | ||||
| import { config } from '@/config'; | ||||
|  | ||||
| function createFakePlausibleInstance(): Pick<ReturnType<typeof Plausible>, 'trackEvent' | 'enableAutoPageviews'> { | ||||
|   return { | ||||
| @@ -15,11 +15,11 @@ function createPlausibleInstance({ | ||||
|   config, | ||||
| }: { | ||||
|   config: { | ||||
|     isTrackerEnabled: boolean | ||||
|     domain: string | ||||
|     apiHost: string | ||||
|     trackLocalhost: boolean | ||||
|   } | ||||
|     isTrackerEnabled: boolean; | ||||
|     domain: string; | ||||
|     apiHost: string; | ||||
|     trackLocalhost: boolean; | ||||
|   }; | ||||
| }) { | ||||
|   if (config.isTrackerEnabled) { | ||||
|     return Plausible(config); | ||||
|   | ||||
| @@ -15,7 +15,7 @@ const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({ | ||||
| const toolsRedirectRoutes = tools | ||||
|   .filter(({ redirectFrom }) => redirectFrom && redirectFrom.length > 0) | ||||
|   .flatMap( | ||||
|     ({ path, redirectFrom }) => redirectFrom?.map(redirectSource => ({ path: redirectSource, redirect: path })) ?? [], | ||||
|     ({ path, redirectFrom }) => redirectFrom?.map((redirectSource) => ({ path: redirectSource, redirect: path })) ?? [], | ||||
|   ); | ||||
|  | ||||
| const router = createRouter({ | ||||
|   | ||||
							
								
								
									
										27
									
								
								src/shims.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										27
									
								
								src/shims.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,35 +1,10 @@ | ||||
| declare module '*.vue' { | ||||
|   import type {  ComponentOptions } from 'vue'; | ||||
|   import type { ComponentOptions, ComponentOptions } from 'vue'; | ||||
|   const Component: ComponentOptions; | ||||
|   export default Component; | ||||
| } | ||||
|  | ||||
| declare module '*.md' { | ||||
|   import type {  ComponentOptions } from 'vue'; | ||||
|   const Component: ComponentOptions; | ||||
|   export default Component; | ||||
| } | ||||
|  | ||||
| declare module 'iarna-toml-esm' { | ||||
|   export const parse: (toml: string) => any; | ||||
|   export const stringify: (obj: any) => string; | ||||
| } | ||||
|  | ||||
| declare module 'emojilib' { | ||||
|   const lib: Record<string, string[]>; | ||||
|   export default lib; | ||||
| } | ||||
|  | ||||
| declare module 'unicode-emoji-json' { | ||||
|   const emoji: Record<string, { | ||||
|     name: string; | ||||
|     slug: string; | ||||
|     group: string; | ||||
|     emoji_version: string; | ||||
|     unicode_version: string; | ||||
|     skin_tone_support: boolean; | ||||
|     skin_tone_support_unicode_version: string; | ||||
|   }>; | ||||
|    | ||||
|   export default emoji; | ||||
| } | ||||
| @@ -1,19 +1,17 @@ | ||||
| import { useDark, useMediaQuery, useStorage, useToggle } from '@vueuse/core'; | ||||
| import { useMediaQuery, useStorage } from '@vueuse/core'; | ||||
| import { defineStore } from 'pinia'; | ||||
| import { type Ref, watch } from 'vue'; | ||||
| import { watch, type Ref } from 'vue'; | ||||
|  | ||||
| export const useStyleStore = defineStore('style', { | ||||
|   state: () => { | ||||
|     const isDarkTheme = useDark(); | ||||
|     const toggleDark = useToggle(isDarkTheme); | ||||
|     const isDarkTheme = useStorage('isDarkTheme', true) as Ref<boolean>; | ||||
|     const isSmallScreen = useMediaQuery('(max-width: 700px)'); | ||||
|     const isMenuCollapsed = useStorage('isMenuCollapsed', isSmallScreen.value) as Ref<boolean>; | ||||
|  | ||||
|     watch(isSmallScreen, v => (isMenuCollapsed.value = v)); | ||||
|     watch(isSmallScreen, (v) => (isMenuCollapsed.value = v)); | ||||
|  | ||||
|     return { | ||||
|       isDarkTheme, | ||||
|       toggleDark, | ||||
|       isMenuCollapsed, | ||||
|       isSmallScreen, | ||||
|     }; | ||||
|   | ||||
| @@ -1,12 +1,45 @@ | ||||
| <template> | ||||
|   <c-card title="Base64 to file"> | ||||
|     <n-form-item | ||||
|       :feedback="base64InputValidation.message" | ||||
|       :validation-status="base64InputValidation.status" | ||||
|       :show-label="false" | ||||
|     > | ||||
|       <n-input v-model:value="base64Input" type="textarea" placeholder="Put your base64 file string here..." rows="5" /> | ||||
|     </n-form-item> | ||||
|     <n-space justify="center"> | ||||
|       <c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="downloadFile()"> | ||||
|         Download file | ||||
|       </c-button> | ||||
|     </n-space> | ||||
|   </c-card> | ||||
|  | ||||
|   <c-card title="File to base64"> | ||||
|     <n-upload v-model:file-list="fileList" :show-file-list="true" :on-before-upload="onUpload" list-type="image"> | ||||
|       <n-upload-dragger> | ||||
|         <div mb-2> | ||||
|           <n-icon size="35" :depth="3" :component="Upload" /> | ||||
|         </div> | ||||
|         <n-text style="font-size: 14px"> Click or drag a file to this area to upload </n-text> | ||||
|       </n-upload-dragger> | ||||
|     </n-upload> | ||||
|  | ||||
|     <n-input :value="fileBase64" type="textarea" readonly placeholder="File in base64 will be here" /> | ||||
|     <n-space justify="center"> | ||||
|       <c-button @click="copyFileBase64()"> Copy </c-button> | ||||
|     </n-space> | ||||
|   </c-card> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { Upload } from '@vicons/tabler'; | ||||
| import { useBase64 } from '@vueuse/core'; | ||||
| import type { UploadFileInfo } from 'naive-ui'; | ||||
| import type { Ref } from 'vue'; | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; | ||||
| import { useValidation } from '@/composable/validation'; | ||||
| import { isValidBase64 } from '@/utils/base64'; | ||||
| import { Upload } from '@vicons/tabler'; | ||||
| import { useBase64 } from '@vueuse/core'; | ||||
| import type { UploadFileInfo } from 'naive-ui'; | ||||
| import { ref, type Ref } from 'vue'; | ||||
|  | ||||
| const base64Input = ref(''); | ||||
| const { download } = useDownloadFileFromBase64({ source: base64Input }); | ||||
| @@ -15,20 +48,17 @@ const base64InputValidation = useValidation({ | ||||
|   rules: [ | ||||
|     { | ||||
|       message: 'Invalid base 64 string', | ||||
|       validator: value => isValidBase64(value.trim()), | ||||
|       validator: (value) => isValidBase64(value.trim()), | ||||
|     }, | ||||
|   ], | ||||
| }); | ||||
|  | ||||
| function downloadFile() { | ||||
|   if (!base64InputValidation.isValid) { | ||||
|     return; | ||||
|   } | ||||
|   if (!base64InputValidation.isValid) return; | ||||
|  | ||||
|   try { | ||||
|     download(); | ||||
|   } | ||||
|   catch (_) { | ||||
|   } catch (_) { | ||||
|     // | ||||
|   } | ||||
| } | ||||
| @@ -46,47 +76,12 @@ async function onUpload({ file: { file } }: { file: UploadFileInfo }) { | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <c-card title="Base64 to file"> | ||||
|     <c-input-text | ||||
|       v-model:value="base64Input" | ||||
|       multiline | ||||
|       placeholder="Put your base64 file string here..." | ||||
|       rows="5" | ||||
|       :validation="base64InputValidation" | ||||
|       mb-2 | ||||
|     /> | ||||
|  | ||||
|     <div flex justify-center> | ||||
|       <c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="downloadFile()"> | ||||
|         Download file | ||||
|       </c-button> | ||||
|     </div> | ||||
|   </c-card> | ||||
|  | ||||
|   <c-card title="File to base64"> | ||||
|     <n-upload v-model:file-list="fileList" :show-file-list="true" :on-before-upload="onUpload" list-type="image"> | ||||
|       <n-upload-dragger> | ||||
|         <div mb-2> | ||||
|           <n-icon size="35" :depth="3" :component="Upload" /> | ||||
|         </div> | ||||
|         <div op-60> | ||||
|           Click or drag a file to this area to upload | ||||
|         </div> | ||||
|       </n-upload-dragger> | ||||
|     </n-upload> | ||||
|  | ||||
|     <c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" mb-2 /> | ||||
|  | ||||
|     <div flex justify-center> | ||||
|       <c-button @click="copyFileBase64()"> | ||||
|         Copy | ||||
|       </c-button> | ||||
|     </div> | ||||
|   </c-card> | ||||
| </template> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| .n-input, | ||||
| .n-upload { | ||||
|   margin-bottom: 15px; | ||||
| } | ||||
|  | ||||
| ::v-deep(.n-upload-trigger) { | ||||
|   width: 100%; | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { defineTool } from '../tool'; | ||||
| export const tool = defineTool({ | ||||
|   name: 'Base64 file converter', | ||||
|   path: '/base64-file-converter', | ||||
|   description: 'Convert string, files or images into a it\'s base64 representation.', | ||||
|   description: "Convert string, files or images into a it's base64 representation.", | ||||
|   keywords: ['base64', 'converter', 'upload', 'image', 'file', 'conversion', 'web', 'data', 'format'], | ||||
|   component: () => import('./base64-file-converter.vue'), | ||||
|   icon: FileDigit, | ||||
|   | ||||
| @@ -1,90 +1,55 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64'; | ||||
| import { withDefaultOnError } from '@/utils/defaults'; | ||||
|  | ||||
| const encodeUrlSafe = useStorage('base64-string-converter--encode-url-safe', false); | ||||
| const decodeUrlSafe = useStorage('base64-string-converter--decode-url-safe', false); | ||||
|  | ||||
| const textInput = ref(''); | ||||
| const base64Output = computed(() => textToBase64(textInput.value, { makeUrlSafe: encodeUrlSafe.value })); | ||||
| const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' }); | ||||
|  | ||||
| const base64Input = ref(''); | ||||
| const textOutput = computed(() => | ||||
|   withDefaultOnError(() => base64ToText(base64Input.value.trim(), { makeUrlSafe: decodeUrlSafe.value }), ''), | ||||
| ); | ||||
| const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' }); | ||||
| const b64ValidationRules = [ | ||||
|   { | ||||
|     message: 'Invalid base64 string', | ||||
|     validator: (value: string) => isValidBase64(value.trim(), { makeUrlSafe: decodeUrlSafe.value }), | ||||
|   }, | ||||
| ]; | ||||
| const b64ValidationWatch = [decodeUrlSafe]; | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <c-card title="String to base64"> | ||||
|     <n-form-item label="Encode URL safe" label-placement="left"> | ||||
|       <n-switch v-model:value="encodeUrlSafe" /> | ||||
|     <n-form-item label="String to encode"> | ||||
|       <n-input v-model:value="textInput" type="textarea" placeholder="Put your string here..." rows="5" /> | ||||
|     </n-form-item> | ||||
|     <c-input-text | ||||
|       v-model:value="textInput" | ||||
|       multiline | ||||
|       placeholder="Put your string here..." | ||||
|       rows="5" | ||||
|       label="String to encode" | ||||
|       raw-text | ||||
|       mb-5 | ||||
|     /> | ||||
|  | ||||
|     <c-input-text | ||||
|       label="Base64 of string" | ||||
|       :value="base64Output" | ||||
|       multiline | ||||
|       readonly | ||||
|       placeholder="The base64 encoding of your string will be here" | ||||
|       rows="5" | ||||
|       mb-5 | ||||
|     /> | ||||
|     <n-form-item label="Base64 of string"> | ||||
|       <n-input | ||||
|         :value="base64Output" | ||||
|         type="textarea" | ||||
|         readonly | ||||
|         placeholder="The base64 encoding of your string will be here" | ||||
|         rows="5" | ||||
|       /> | ||||
|     </n-form-item> | ||||
|  | ||||
|     <div flex justify-center> | ||||
|       <c-button @click="copyTextBase64()"> | ||||
|         Copy base64 | ||||
|       </c-button> | ||||
|     </div> | ||||
|     <n-space justify="center"> | ||||
|       <c-button @click="copyTextBase64()"> Copy base64 </c-button> | ||||
|     </n-space> | ||||
|   </c-card> | ||||
|  | ||||
|   <c-card title="Base64 to string"> | ||||
|     <n-form-item label="Decode URL safe" label-placement="left"> | ||||
|       <n-switch v-model:value="decodeUrlSafe" /> | ||||
|     <n-form-item label="Base64 string to decode" v-bind="b64Validation.attrs"> | ||||
|       <n-input v-model:value="base64Input" type="textarea" placeholder="Your base64 string..." rows="5" /> | ||||
|     </n-form-item> | ||||
|     <c-input-text | ||||
|       v-model:value="base64Input" | ||||
|       multiline | ||||
|       placeholder="Your base64 string..." | ||||
|       rows="5" | ||||
|       :validation-rules="b64ValidationRules" | ||||
|       :validation-watch="b64ValidationWatch" | ||||
|       label="Base64 string to decode" | ||||
|       mb-5 | ||||
|     /> | ||||
|  | ||||
|     <c-input-text | ||||
|       v-model:value="textOutput" | ||||
|       label="Decoded string" | ||||
|       placeholder="The decoded string will be here" | ||||
|       multiline | ||||
|       rows="5" | ||||
|       readonly | ||||
|       mb-5 | ||||
|     /> | ||||
|     <n-form-item label="Decoded string"> | ||||
|       <n-input :value="textOutput" type="textarea" readonly placeholder="The decoded string will be here" rows="5" /> | ||||
|     </n-form-item> | ||||
|  | ||||
|     <div flex justify-center> | ||||
|       <c-button @click="copyText()"> | ||||
|         Copy decoded string | ||||
|       </c-button> | ||||
|     </div> | ||||
|     <n-space justify="center"> | ||||
|       <c-button @click="copyText()"> Copy decoded string </c-button> | ||||
|     </n-space> | ||||
|   </c-card> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { useValidation } from '@/composable/validation'; | ||||
| import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64'; | ||||
| import { withDefaultOnError } from '@/utils/defaults'; | ||||
| import { computed, ref } from 'vue'; | ||||
|  | ||||
| const textInput = ref(''); | ||||
| const base64Output = computed(() => textToBase64(textInput.value)); | ||||
| const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' }); | ||||
|  | ||||
| const base64Input = ref(''); | ||||
| const textOutput = computed(() => withDefaultOnError(() => base64ToText(base64Input.value.trim()), '')); | ||||
| const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' }); | ||||
| const b64Validation = useValidation({ | ||||
|   source: base64Input, | ||||
|   rules: [{ message: 'Invalid base64 string', validator: (value) => isValidBase64(value.trim()) }], | ||||
| }); | ||||
| </script> | ||||
|   | ||||
| @@ -1,26 +1,17 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { textToBase64 } from '@/utils/base64'; | ||||
|  | ||||
| const username = ref(''); | ||||
| const password = ref(''); | ||||
| const header = computed(() => `Authorization: Basic ${textToBase64(`${username.value}:${password.value}`)}`); | ||||
|  | ||||
| const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|     <c-input-text v-model:value="username" label="Username" placeholder="Your username..." clearable raw-text mb-5 /> | ||||
|     <c-input-text | ||||
|       v-model:value="password" | ||||
|       label="Password" | ||||
|       placeholder="Your password..." | ||||
|       clearable | ||||
|       raw-text | ||||
|       mb-2 | ||||
|       type="password" | ||||
|     /> | ||||
|     <n-form-item label="Username"> | ||||
|       <n-input v-model:value="username" placeholder="Your username..." clearable /> | ||||
|     </n-form-item> | ||||
|     <n-form-item label="Password"> | ||||
|       <n-input | ||||
|         v-model:value="password" | ||||
|         placeholder="Your password..." | ||||
|         type="password" | ||||
|         show-password-on="click" | ||||
|         clearable | ||||
|       /> | ||||
|     </n-form-item> | ||||
|  | ||||
|     <c-card> | ||||
|       <n-statistic label="Authorization header:" class="header"> | ||||
| @@ -29,14 +20,24 @@ const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard | ||||
|         </n-scrollbar> | ||||
|       </n-statistic> | ||||
|     </c-card> | ||||
|     <div mt-5 flex justify-center> | ||||
|       <c-button @click="copy()"> | ||||
|         Copy header | ||||
|       </c-button> | ||||
|     </div> | ||||
|     <n-space justify="center" mt-5> | ||||
|       <c-button @click="copy">Copy header</c-button> | ||||
|     </n-space> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { textToBase64 } from '@/utils/base64'; | ||||
| import { computed, ref } from 'vue'; | ||||
|  | ||||
| const username = ref(''); | ||||
| const password = ref(''); | ||||
| const header = computed(() => `Authorization: Basic ${textToBase64(`${username.value}:${password.value}`)}`); | ||||
|  | ||||
| const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' }); | ||||
| </script> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| ::v-deep(.n-statistic-value__content) { | ||||
|   font-family: monospace; | ||||
|   | ||||
| @@ -1,7 +1,62 @@ | ||||
| <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> | ||||
|     <n-space justify="center" mt-5> | ||||
|       <c-button @click="copy"> Copy hash </c-button> | ||||
|     </n-space> | ||||
|   </c-card> | ||||
|  | ||||
|   <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" | ||||
|         /> | ||||
|       </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" | ||||
|         /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Do they match ? " label-placement="left" :show-feedback="false"> | ||||
|         <div class="compare-result" :class="{ positive: compareMatch }"> | ||||
|           {{ compareMatch ? 'Yes' : 'No' }} | ||||
|         </div> | ||||
|       </n-form-item> | ||||
|     </n-form> | ||||
|   </c-card> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { compareSync, hashSync } from 'bcryptjs'; | ||||
| import { useThemeVars } from 'naive-ui'; | ||||
| import { computed, ref } from 'vue'; | ||||
| import { hashSync, compareSync } from 'bcryptjs'; | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { useThemeVars } from 'naive-ui'; | ||||
|  | ||||
| const themeVars = useThemeVars(); | ||||
|  | ||||
| @@ -15,47 +70,6 @@ const compareHash = ref(''); | ||||
| const compareMatch = computed(() => compareSync(compareString.value, compareHash.value)); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <c-card title="Hash"> | ||||
|     <c-input-text | ||||
|       v-model:value="input" | ||||
|       placeholder="Your string to bcrypt..." | ||||
|       raw-text | ||||
|       label="Your string: " | ||||
|       label-position="left" | ||||
|       label-width="120px" | ||||
|       mb-2 | ||||
|     /> | ||||
|     <n-form-item label="Salt count: " label-placement="left" label-width="120"> | ||||
|       <n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="10" :min="0" w-full /> | ||||
|     </n-form-item> | ||||
|  | ||||
|     <c-input-text :value="hashed" readonly text-center /> | ||||
|  | ||||
|     <div mt-5 flex justify-center> | ||||
|       <c-button @click="copy()"> | ||||
|         Copy hash | ||||
|       </c-button> | ||||
|     </div> | ||||
|   </c-card> | ||||
|  | ||||
|   <c-card title="Compare string with hash"> | ||||
|     <n-form label-width="120"> | ||||
|       <n-form-item label="Your string: " label-placement="left"> | ||||
|         <c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Your hash: " label-placement="left"> | ||||
|         <c-input-text v-model:value="compareHash" placeholder="Your hahs to compare..." raw-text /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Do they match ? " label-placement="left" :show-feedback="false"> | ||||
|         <div class="compare-result" :class="{ positive: compareMatch }"> | ||||
|           {{ compareMatch ? 'Yes' : 'No' }} | ||||
|         </div> | ||||
|       </n-form-item> | ||||
|     </n-form> | ||||
|   </c-card> | ||||
| </template> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| .compare-result { | ||||
|   color: v-bind('themeVars.errorColor'); | ||||
|   | ||||
| @@ -13,22 +13,22 @@ function computeAverage({ data }: { data: number[] }) { | ||||
| function computeVariance({ data }: { data: number[] }) { | ||||
|   const mean = computeAverage({ data }); | ||||
|  | ||||
|   const squaredDiffs = data.map(value => (value - mean) ** 2); | ||||
|   const squaredDiffs = data.map((value) => Math.pow(value - mean, 2)); | ||||
|  | ||||
|   return computeAverage({ data: squaredDiffs }); | ||||
| } | ||||
|  | ||||
| function arrayToMarkdownTable({ data, headerMap = {} }: { data: Record<string, unknown>[]; headerMap?: Record<string, string> }) { | ||||
| function arrayToMarkdownTable({ data, headerMap = {} }: { data: unknown[]; headerMap?: Record<string, string> }) { | ||||
|   if (!Array.isArray(data) || data.length === 0) { | ||||
|     return ''; | ||||
|   } | ||||
|  | ||||
|   const headers = Object.keys(data[0]); | ||||
|   const rows = data.map(obj => Object.values(obj)); | ||||
|   const rows = data.map((obj) => Object.values(obj)); | ||||
|  | ||||
|   const headerRow = `| ${headers.map(header => headerMap[header] ?? header).join(' | ')} |`; | ||||
|   const headerRow = `| ${headers.map((header) => headerMap[header] ?? header).join(' | ')} |`; | ||||
|   const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`; | ||||
|   const dataRows = rows.map(row => `| ${row.join(' | ')} |`).join('\n'); | ||||
|   const dataRows = rows.map((row) => `| ${row.join(' | ')} |`).join('\n'); | ||||
|  | ||||
|   return `${headerRow}\n${separatorRow}\n${dataRows}`; | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,88 @@ | ||||
| <script setup lang="ts"> | ||||
| import { Plus, Trash } from '@vicons/tabler'; | ||||
| import { useStorage } from '@vueuse/core'; | ||||
| import _ from 'lodash'; | ||||
| <template> | ||||
|   <n-scrollbar style="flex: 1" x-scrollable> | ||||
|     <n-space :wrap="false" style="flex: 1" justify="center" :size="0" mb-5> | ||||
|       <div v-for="(suite, index) of suites" :key="index"> | ||||
|         <c-card style="width: 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> | ||||
|  | ||||
| import { arrayToMarkdownTable, computeAverage, computeVariance } from './benchmark-builder.models'; | ||||
|           <n-divider></n-divider> | ||||
|           <n-form-item label="Suite values" :show-feedback="false"> | ||||
|             <dynamic-values v-model:values="suite.data" /> | ||||
|           </n-form-item> | ||||
|         </c-card> | ||||
|  | ||||
|         <n-space justify="center"> | ||||
|           <c-button v-if="suites.length > 1" variant="text" @click="suites.splice(index, 1)"> | ||||
|             <n-icon :component="Trash" depth="3" mr-2 size="18" /> | ||||
|             Delete suite | ||||
|           </c-button> | ||||
|           <c-button | ||||
|             variant="text" | ||||
|             @click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })" | ||||
|           > | ||||
|             <n-icon :component="Plus" depth="3" mr-2 size="18" /> | ||||
|             Add suite | ||||
|           </c-button> | ||||
|         </n-space> | ||||
|       </div> | ||||
|     </n-space> | ||||
|   </n-scrollbar> | ||||
|  | ||||
|   <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-button | ||||
|           @click=" | ||||
|             suites = [ | ||||
|               { title: 'Suite 1', data: [] }, | ||||
|               { title: 'Suite 2', data: [] }, | ||||
|             ] | ||||
|           " | ||||
|           >Reset suites</c-button | ||||
|         > | ||||
|       </n-space> | ||||
|  | ||||
|       <n-table> | ||||
|         <thead> | ||||
|           <tr> | ||||
|             <th>{{ header.position }}</th> | ||||
|             <th>{{ header.title }}</th> | ||||
|             <th>{{ header.size }}</th> | ||||
|             <th>{{ header.mean }}</th> | ||||
|             <th>{{ header.variance }}</th> | ||||
|           </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|           <tr v-for="{ title, size, mean, variance, position } of results" :key="title"> | ||||
|             <td>{{ position }}</td> | ||||
|             <td>{{ title }}</td> | ||||
|             <td>{{ size }}</td> | ||||
|             <td>{{ mean }}</td> | ||||
|             <td>{{ variance }}</td> | ||||
|           </tr> | ||||
|         </tbody> | ||||
|       </n-table> | ||||
|       <n-space justify="center" mt-5> | ||||
|         <c-button @click="copyAsMarkdown">Copy as markdown table</c-button> | ||||
|         <c-button @click="copyAsBulletList">Copy as bullet list</c-button> | ||||
|       </n-space> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { Trash, Plus } from '@vicons/tabler'; | ||||
| import { useClipboard, useStorage } from '@vueuse/core'; | ||||
| import _ from 'lodash'; | ||||
| import { computed } from 'vue'; | ||||
| import { computeAverage, computeVariance, arrayToMarkdownTable } from './benchmark-builder.models'; | ||||
| import DynamicValues from './dynamic-values.vue'; | ||||
| import { useCopy } from '@/composable/copy'; | ||||
|  | ||||
| const suites = useStorage('benchmark-builder:suites', [ | ||||
|   { title: 'Suite 1', data: [5, 10] }, | ||||
| @@ -35,8 +112,8 @@ const results = computed(() => { | ||||
|       const deltaWithBestMean = mean - bestMean; | ||||
|       const ratioWithBestMean = bestMean === 0 ? '∞' : round(mean / bestMean); | ||||
|  | ||||
|       const comparisonValues: string | ||||
|         = (index !== 0 && bestMean !== mean) ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : ''; | ||||
|       const comparisonValues: string = | ||||
|         index !== 0 && bestMean !== mean ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : ''; | ||||
|  | ||||
|       return { | ||||
|         position: index + 1, | ||||
| @@ -48,7 +125,7 @@ const results = computed(() => { | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| const { copy } = useCopy({ createToast: false }); | ||||
| const { copy } = useClipboard(); | ||||
|  | ||||
| const header = { | ||||
|   title: 'Suite', | ||||
| @@ -78,87 +155,4 @@ function copyAsBulletList() { | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <n-scrollbar style="flex: 1" x-scrollable> | ||||
|     <div mb-5 flex flex-1 flex-nowrap justify-center gap-12px> | ||||
|       <div v-for="(suite, index) of suites" :key="index"> | ||||
|         <c-card style="width: 294px"> | ||||
|           <c-input-text | ||||
|             v-model:value="suite.title" | ||||
|             label-position="left" | ||||
|             label="Suite name" | ||||
|             placeholder="Suite name..." | ||||
|             clearable | ||||
|           /> | ||||
|  | ||||
|           <n-divider /> | ||||
|           <n-form-item label="Suite values" :show-feedback="false"> | ||||
|             <DynamicValues v-model:values="suite.data" /> | ||||
|           </n-form-item> | ||||
|         </c-card> | ||||
|  | ||||
|         <div flex justify-center> | ||||
|           <c-button v-if="suites.length > 1" variant="text" @click="suites.splice(index, 1)"> | ||||
|             <n-icon :component="Trash" depth="3" mr-2 size="18" /> | ||||
|             Delete suite | ||||
|           </c-button> | ||||
|           <c-button | ||||
|             variant="text" | ||||
|             @click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })" | ||||
|           > | ||||
|             <n-icon :component="Plus" depth="3" mr-2 size="18" /> | ||||
|             Add suite | ||||
|           </c-button> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </n-scrollbar> | ||||
|  | ||||
|   <div style="flex: 0 0 100%"> | ||||
|     <div style="max-width: 600px; margin: 0 auto"> | ||||
|       <div mx-auto max-w-sm flex justify-center gap-3> | ||||
|         <c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 /> | ||||
|  | ||||
|         <c-button | ||||
|           @click=" | ||||
|             suites = [ | ||||
|               { title: 'Suite 1', data: [] }, | ||||
|               { title: 'Suite 2', data: [] }, | ||||
|             ] | ||||
|           " | ||||
|         > | ||||
|           Reset suites | ||||
|         </c-button> | ||||
|       </div> | ||||
|  | ||||
|       <n-table> | ||||
|         <thead> | ||||
|           <tr> | ||||
|             <th>{{ header.position }}</th> | ||||
|             <th>{{ header.title }}</th> | ||||
|             <th>{{ header.size }}</th> | ||||
|             <th>{{ header.mean }}</th> | ||||
|             <th>{{ header.variance }}</th> | ||||
|           </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|           <tr v-for="{ title, size, mean, variance, position } of results" :key="title"> | ||||
|             <td>{{ position }}</td> | ||||
|             <td>{{ title }}</td> | ||||
|             <td>{{ size }}</td> | ||||
|             <td>{{ mean }}</td> | ||||
|             <td>{{ variance }}</td> | ||||
|           </tr> | ||||
|         </tbody> | ||||
|       </n-table> | ||||
|       <div mt-5 flex justify-center gap-3> | ||||
|         <c-button @click="copyAsMarkdown()"> | ||||
|           Copy as markdown table | ||||
|         </c-button> | ||||
|         <c-button @click="copyAsBulletList()"> | ||||
|           Copy as bullet list | ||||
|         </c-button> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| <style lang="less" scoped></style> | ||||
|   | ||||
| @@ -1,15 +1,41 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <n-space v-for="(value, index) of values" :key="index" :wrap="false" style="margin-bottom: 5px" :size="5"> | ||||
|       <n-input-number | ||||
|         :ref="refs.set" | ||||
|         v-model:value="values[index]" | ||||
|         :show-button="false" | ||||
|         placeholder="Set your measure..." | ||||
|         autofocus | ||||
|         @keydown.enter="onInputEnter(index)" | ||||
|       /> | ||||
|       <n-tooltip> | ||||
|         <template #trigger> | ||||
|           <c-button circle variant="text" @click="values.splice(index, 1)"> | ||||
|             <n-icon :component="Trash" depth="3" size="18" /> | ||||
|           </c-button> | ||||
|         </template> | ||||
|         Delete value | ||||
|       </n-tooltip> | ||||
|     </n-space> | ||||
|  | ||||
|     <c-button @click="addValue"> | ||||
|       <n-icon :component="Plus" depth="3" mr-2 size="18" /> | ||||
|       Add a measure | ||||
|     </c-button> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { Plus, Trash } from '@vicons/tabler'; | ||||
| import { Trash, Plus } from '@vicons/tabler'; | ||||
| import { useTemplateRefsList, useVModel } from '@vueuse/core'; | ||||
| import { NInputNumber } from 'naive-ui'; | ||||
| import { nextTick } from 'vue'; | ||||
|  | ||||
| const props = defineProps<{ values: (number | null)[] }>(); | ||||
|  | ||||
| const emit = defineEmits(['update:values']); | ||||
|  | ||||
| const refs = useTemplateRefsList<typeof NInputNumber>(); | ||||
|  | ||||
| const props = defineProps<{ values: (number | null)[] }>(); | ||||
| const emit = defineEmits(['update:values']); | ||||
| const values = useVModel(props, 'values', emit); | ||||
|  | ||||
| async function addValue() { | ||||
| @@ -28,30 +54,4 @@ function onInputEnter(index: number) { | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|     <div v-for="(value, index) of values" :key="index" mb-2 flex flex-nowrap gap-2> | ||||
|       <NInputNumber | ||||
|         :ref="refs.set" | ||||
|         v-model:value="values[index]" | ||||
|         :show-button="false" | ||||
|         placeholder="Set your measure..." | ||||
|         autofocus | ||||
|         @keydown.enter="onInputEnter(index)" | ||||
|       /> | ||||
|       <n-tooltip> | ||||
|         <template #trigger> | ||||
|           <c-button circle variant="text" @click="values.splice(index, 1)"> | ||||
|             <n-icon :component="Trash" depth="3" size="18" /> | ||||
|           </c-button> | ||||
|         </template> | ||||
|         Delete value | ||||
|       </n-tooltip> | ||||
|     </div> | ||||
|  | ||||
|     <c-button @click="addValue"> | ||||
|       <n-icon :component="Plus" depth="3" mr-2 size="18" /> | ||||
|       Add a measure | ||||
|     </c-button> | ||||
|   </div> | ||||
| </template> | ||||
| <style scoped></style> | ||||
|   | ||||
| @@ -1,4 +1,65 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <n-grid cols="3" x-gap="12"> | ||||
|       <n-gi span="1"> | ||||
|         <n-form-item label="Language:"> | ||||
|           <n-select | ||||
|             v-model:value="language" | ||||
|             :options="Object.keys(languages).map((label) => ({ label, value: label }))" | ||||
|           /> | ||||
|         </n-form-item> | ||||
|       </n-gi> | ||||
|       <n-gi span="2"> | ||||
|         <n-form-item | ||||
|           label="Entropy (seed):" | ||||
|           :feedback="entropyValidation.message" | ||||
|           :validation-status="entropyValidation.status" | ||||
|         > | ||||
|           <n-input-group> | ||||
|             <n-input v-model:value="entropy" placeholder="Your string..." /> | ||||
|             <c-button @click="refreshEntropy"> | ||||
|               <n-icon size="22"> | ||||
|                 <Refresh /> | ||||
|               </n-icon> | ||||
|             </c-button> | ||||
|             <c-button @click="copyEntropy"> | ||||
|               <n-icon size="22"> | ||||
|                 <Copy /> | ||||
|               </n-icon> | ||||
|             </c-button> | ||||
|           </n-input-group> | ||||
|         </n-form-item> | ||||
|       </n-gi> | ||||
|     </n-grid> | ||||
|     <n-form-item | ||||
|       label="Passphrase (mnemonic):" | ||||
|       :feedback="mnemonicValidation.message" | ||||
|       :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-button @click="copyPassphrase"> | ||||
|           <n-icon size="22" :component="Copy" /> | ||||
|         </c-button> | ||||
|       </n-input-group> | ||||
|     </n-form-item> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { useValidation } from '@/composable/validation'; | ||||
| import { isNotThrowing } from '@/utils/boolean'; | ||||
| import { withDefaultOnError } from '@/utils/defaults'; | ||||
| import { | ||||
|   chineseSimplifiedWordList, | ||||
|   chineseTraditionalWordList, | ||||
| @@ -15,23 +76,19 @@ import { | ||||
|   spanishWordList, | ||||
| } from '@it-tools/bip39'; | ||||
| import { Copy, Refresh } from '@vicons/tabler'; | ||||
|  | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { useValidation } from '@/composable/validation'; | ||||
| import { isNotThrowing } from '@/utils/boolean'; | ||||
| import { withDefaultOnError } from '@/utils/defaults'; | ||||
| import { computed, ref } from 'vue'; | ||||
|  | ||||
| const languages = { | ||||
|   'English': englishWordList, | ||||
|   English: englishWordList, | ||||
|   'Chinese simplified': chineseSimplifiedWordList, | ||||
|   'Chinese traditional': chineseTraditionalWordList, | ||||
|   'Czech': czechWordList, | ||||
|   'French': frenchWordList, | ||||
|   'Italian': italianWordList, | ||||
|   'Japanese': japaneseWordList, | ||||
|   'Korean': koreanWordList, | ||||
|   'Portuguese': portugueseWordList, | ||||
|   'Spanish': spanishWordList, | ||||
|   Czech: czechWordList, | ||||
|   French: frenchWordList, | ||||
|   Italian: italianWordList, | ||||
|   Japanese: japaneseWordList, | ||||
|   Korean: koreanWordList, | ||||
|   Portuguese: portugueseWordList, | ||||
|   Spanish: spanishWordList, | ||||
| }; | ||||
|  | ||||
| const entropy = ref(generateEntropy()); | ||||
| @@ -52,11 +109,11 @@ const entropyValidation = useValidation({ | ||||
|   source: entropy, | ||||
|   rules: [ | ||||
|     { | ||||
|       validator: value => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0), | ||||
|       validator: (value) => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0), | ||||
|       message: 'Entropy length should be >= 16, <= 32 and be a multiple of 4', | ||||
|     }, | ||||
|     { | ||||
|       validator: value => /^[a-fA-F0-9]*$/.test(value), | ||||
|       validator: (value) => /^[a-fA-F0-9]*$/.test(value), | ||||
|       message: 'Entropy should be an hexadecimal string', | ||||
|     }, | ||||
|   ], | ||||
| @@ -66,7 +123,7 @@ const mnemonicValidation = useValidation({ | ||||
|   source: passphrase, | ||||
|   rules: [ | ||||
|     { | ||||
|       validator: value => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])), | ||||
|       validator: (value) => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])), | ||||
|       message: 'Invalid mnemonic', | ||||
|     }, | ||||
|   ], | ||||
| @@ -79,53 +136,3 @@ function refreshEntropy() { | ||||
| const { copy: copyEntropy } = useCopy({ source: entropy, text: 'Entropy copied to the clipboard' }); | ||||
| const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase copied to the clipboard' }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|     <n-grid cols="3" x-gap="12"> | ||||
|       <n-gi span="1"> | ||||
|         <c-select | ||||
|           v-model:value="language" | ||||
|           searchable | ||||
|           label="Language:" | ||||
|           :options="Object.keys(languages)" | ||||
|         /> | ||||
|       </n-gi> | ||||
|       <n-gi span="2"> | ||||
|         <n-form-item | ||||
|           label="Entropy (seed):" | ||||
|           :feedback="entropyValidation.message" | ||||
|           :validation-status="entropyValidation.status" | ||||
|         > | ||||
|           <n-input-group> | ||||
|             <c-input-text v-model:value="entropy" placeholder="Your string..." /> | ||||
|  | ||||
|             <c-button @click="refreshEntropy()"> | ||||
|               <n-icon size="22"> | ||||
|                 <Refresh /> | ||||
|               </n-icon> | ||||
|             </c-button> | ||||
|             <c-button @click="copyEntropy()"> | ||||
|               <n-icon size="22"> | ||||
|                 <Copy /> | ||||
|               </n-icon> | ||||
|             </c-button> | ||||
|           </n-input-group> | ||||
|         </n-form-item> | ||||
|       </n-gi> | ||||
|     </n-grid> | ||||
|     <n-form-item | ||||
|       label="Passphrase (mnemonic):" | ||||
|       :feedback="mnemonicValidation.message" | ||||
|       :validation-status="mnemonicValidation.status" | ||||
|     > | ||||
|       <n-input-group> | ||||
|         <c-input-text v-model:value="passphrase" placeholder="Your mnemonic..." raw-text /> | ||||
|  | ||||
|         <c-button @click="copyPassphrase()"> | ||||
|           <n-icon size="22" :component="Copy" /> | ||||
|         </c-button> | ||||
|       </n-input-group> | ||||
|     </n-form-item> | ||||
|   </div> | ||||
| </template> | ||||
|   | ||||
| @@ -1,214 +0,0 @@ | ||||
| <script setup lang="ts"> | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| import { useMediaRecorder } from './useMediaRecorder'; | ||||
|  | ||||
| interface Media { type: 'image' | 'video'; value: string; createdAt: Date } | ||||
|  | ||||
| const { | ||||
|   videoInputs: cameras, | ||||
|   audioInputs: microphones, | ||||
|   permissionGranted, | ||||
|   isSupported, | ||||
|   ensurePermissions, | ||||
| } = useDevicesList({ | ||||
|   requestPermissions: true, | ||||
|   constraints: { video: true, audio: true }, | ||||
|   onUpdated() { | ||||
|     refreshCurrentDevices(); | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| const video = ref<HTMLVideoElement>(); | ||||
| const medias = ref<Media[]>([]); | ||||
| const currentCamera = ref(cameras.value[0]?.deviceId); | ||||
| const currentMicrophone = ref(microphones.value[0]?.deviceId); | ||||
| const permissionCannotBePrompted = ref(false); | ||||
|  | ||||
| const { | ||||
|   stream, | ||||
|   start, | ||||
|   enabled: isMediaStreamAvailable, | ||||
| } = useUserMedia({ | ||||
|   constraints: computed(() => ({ | ||||
|     video: { deviceId: currentCamera.value }, | ||||
|     ...(currentMicrophone.value ? { audio: { deviceId: currentMicrophone.value } } : {}), | ||||
|   })), | ||||
|   autoSwitch: true, | ||||
| }); | ||||
|  | ||||
| const { | ||||
|   isRecordingSupported, | ||||
|   onRecordAvailable, | ||||
|   startRecording, | ||||
|   stopRecording, | ||||
|   pauseRecording, | ||||
|   recordingState, | ||||
|   resumeRecording, | ||||
| } = useMediaRecorder({ | ||||
|   stream, | ||||
| }); | ||||
|  | ||||
| onRecordAvailable((value) => { | ||||
|   medias.value.unshift({ type: 'video', value, createdAt: new Date() }); | ||||
| }); | ||||
|  | ||||
| function refreshCurrentDevices() { | ||||
|   if (_.isNil(currentCamera) || !cameras.value.find(i => i.deviceId === currentCamera.value)) { | ||||
|     currentCamera.value = cameras.value[0]?.deviceId; | ||||
|   } | ||||
|  | ||||
|   if (_.isNil(microphones) || !microphones.value.find(i => i.deviceId === currentMicrophone.value)) { | ||||
|     currentMicrophone.value = microphones.value[0]?.deviceId; | ||||
|   } | ||||
| } | ||||
|  | ||||
| function takeScreenshot() { | ||||
|   if (!video.value) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   const canvas = document.createElement('canvas'); | ||||
|   canvas.width = video.value.videoWidth; | ||||
|   canvas.height = video.value.videoHeight; | ||||
|   canvas.getContext('2d')?.drawImage(video.value, 0, 0); | ||||
|   const image = canvas.toDataURL('image/png'); | ||||
|  | ||||
|   medias.value.unshift({ type: 'image', value: image, createdAt: new Date() }); | ||||
| } | ||||
|  | ||||
| watchEffect(() => { | ||||
|   if (video.value && stream.value) { | ||||
|     video.value.srcObject = stream.value; | ||||
|   } | ||||
| }); | ||||
|  | ||||
| async function requestPermissions() { | ||||
|   try { | ||||
|     await ensurePermissions(); | ||||
|   } | ||||
|   catch (e) { | ||||
|     permissionCannotBePrompted.value = true; | ||||
|   } | ||||
| } | ||||
|  | ||||
| function downloadMedia({ type, value, createdAt }: Media) { | ||||
|   const link = document.createElement('a'); | ||||
|   link.href = value; | ||||
|   link.download = `${type}-${createdAt.getTime()}.${type === 'image' ? 'png' : 'webm'}`; | ||||
|   link.click(); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|     <c-card v-if="!isSupported"> | ||||
|       Your browser does not support recording video from camera | ||||
|     </c-card> | ||||
|  | ||||
|     <c-card v-else-if="!permissionGranted" text-center> | ||||
|       You need to grant permission to use your camera and microphone | ||||
|  | ||||
|       <c-alert v-if="permissionCannotBePrompted" mt-4 text-left> | ||||
|         Your browser has blocked permission request or does not support it. You need to grant permission manually in | ||||
|         your browser settings (usually the lock icon in the address bar). | ||||
|       </c-alert> | ||||
|  | ||||
|       <div v-else mt-4 flex justify-center> | ||||
|         <c-button @click="requestPermissions"> | ||||
|           Grant permission | ||||
|         </c-button> | ||||
|       </div> | ||||
|     </c-card> | ||||
|  | ||||
|     <c-card v-else> | ||||
|       <div flex flex-col gap-2> | ||||
|         <c-select | ||||
|           v-model:value="currentCamera" | ||||
|           label-position="left" | ||||
|           label-width="60px" | ||||
|           label="Video:" | ||||
|           :options="cameras.map(({ deviceId, label }) => ({ value: deviceId, label }))" | ||||
|           placeholder="Select camera" | ||||
|         /> | ||||
|         <c-select | ||||
|           v-if="currentMicrophone && microphones.length > 0" | ||||
|           v-model:value="currentMicrophone" | ||||
|           label="Audio:" | ||||
|           label-position="left" | ||||
|           label-width="60px" | ||||
|           :options="microphones.map(({ deviceId, label }) => ({ value: deviceId, label }))" | ||||
|           placeholder="Select microphone" | ||||
|         /> | ||||
|       </div> | ||||
|  | ||||
|       <div v-if="!isMediaStreamAvailable" mt-3 flex justify-center> | ||||
|         <c-button type="primary" @click="start"> | ||||
|           Start webcam | ||||
|         </c-button> | ||||
|       </div> | ||||
|  | ||||
|       <div v-else> | ||||
|         <div my-2> | ||||
|           <video ref="video" autoplay controls playsinline max-h-full w-full /> | ||||
|         </div> | ||||
|  | ||||
|         <div flex items-center justify-between gap-2> | ||||
|           <c-button :disabled="!isMediaStreamAvailable" @click="takeScreenshot"> | ||||
|             <span mr-2> <icon-mdi-camera /></span> | ||||
|             Take screenshot | ||||
|           </c-button> | ||||
|  | ||||
|           <div v-if="isRecordingSupported" flex justify-center gap-2> | ||||
|             <c-button v-if="recordingState === 'stopped'" @click="startRecording"> | ||||
|               <span mr-2> <icon-mdi-video /></span> | ||||
|               Start recording | ||||
|             </c-button> | ||||
|  | ||||
|             <c-button v-if="recordingState === 'recording'" @click="pauseRecording"> | ||||
|               <span mr-2> <icon-mdi-pause /></span> | ||||
|               Pause | ||||
|             </c-button> | ||||
|  | ||||
|             <c-button v-if="recordingState === 'paused'" @click="resumeRecording"> | ||||
|               <span mr-2> <icon-mdi-play /></span> | ||||
|               Resume | ||||
|             </c-button> | ||||
|  | ||||
|             <c-button v-if="recordingState !== 'stopped'" type="error" @click="stopRecording"> | ||||
|               <span mr-2> <icon-mdi-record /></span> | ||||
|               Stop | ||||
|             </c-button> | ||||
|           </div> | ||||
|           <div v-else italic op-60> | ||||
|             Video recording is not supported in your browser | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </c-card> | ||||
|  | ||||
|     <div grid grid-cols-2 mt-5 gap-2> | ||||
|       <c-card v-for="({ type, value, createdAt }, index) in medias" :key="index"> | ||||
|         <img v-if="type === 'image'" :src="value" max-h-full w-full alt="screenshot"> | ||||
|  | ||||
|         <video v-else :src="value" controls max-h-full w-full /> | ||||
|  | ||||
|         <div flex items-center justify-between> | ||||
|           <div font-bold> | ||||
|             {{ type === 'image' ? 'Screenshot' : 'Video' }} | ||||
|           </div> | ||||
|  | ||||
|           <div flex gap-2> | ||||
|             <c-button @click="downloadMedia({ type, value, createdAt })"> | ||||
|               <icon-mdi-download /> | ||||
|             </c-button> | ||||
|  | ||||
|             <c-button @click="medias = medias.filter((_ignored, i) => i !== index)"> | ||||
|               <icon-mdi-delete-outline /> | ||||
|             </c-button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </c-card> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -1,12 +0,0 @@ | ||||
| import { Camera } from '@vicons/tabler'; | ||||
| import { defineTool } from '../tool'; | ||||
|  | ||||
| export const tool = defineTool({ | ||||
|   name: 'Camera recorder', | ||||
|   path: '/camera-recorder', | ||||
|   description: 'Take a picture or record a video from your webcam or camera.', | ||||
|   keywords: ['camera', 'recoder'], | ||||
|   component: () => import('./camera-recorder.vue'), | ||||
|   icon: Camera, | ||||
|   createdAt: new Date('2023-05-15'), | ||||
| }); | ||||
| @@ -1,114 +0,0 @@ | ||||
| import { type Ref, computed, ref } from 'vue'; | ||||
|  | ||||
| export { useMediaRecorder }; | ||||
|  | ||||
| function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }): { | ||||
|   isRecordingSupported: Ref<boolean> | ||||
|   recordingState: Ref<'stopped' | 'recording' | 'paused'> | ||||
|   startRecording: () => void | ||||
|   stopRecording: () => void | ||||
|   pauseRecording: () => void | ||||
|   resumeRecording: () => void | ||||
|   onRecordAvailable: (cb: (url: string) => void) => void | ||||
| } { | ||||
|   const isRecordingSupported = computed(() => MediaRecorder.isTypeSupported('video/webm')); | ||||
|   const mediaRecorder = ref<MediaRecorder | null>(null); | ||||
|   const recordedChunks = ref<Blob[]>([]); | ||||
|   const recordAvailable = createEventHook(); | ||||
|   const recordingState = ref<'stopped' | 'recording' | 'paused'>('stopped'); | ||||
|  | ||||
|   const createVideo = () => { | ||||
|     const blob = new Blob(recordedChunks.value, { type: 'video/webm' }); | ||||
|     const url = URL.createObjectURL(blob); | ||||
|     recordedChunks.value = []; | ||||
|     return url; | ||||
|   }; | ||||
|  | ||||
|   const startRecording = () => { | ||||
|     if (!isRecordingSupported.value) { | ||||
|       return; | ||||
|     } | ||||
|     if (!stream.value) { | ||||
|       return; | ||||
|     } | ||||
|     if (recordingState.value !== 'stopped') { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     mediaRecorder.value = new MediaRecorder(stream.value, { mimeType: 'video/webm' }); | ||||
|  | ||||
|     mediaRecorder.value.ondataavailable = (e) => { | ||||
|       if (e.data.size > 0) { | ||||
|         recordedChunks.value.push(e.data); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     mediaRecorder.value.onstop = () => { | ||||
|       recordAvailable.trigger(createVideo()); | ||||
|     }; | ||||
|  | ||||
|     if (mediaRecorder.value.state !== 'inactive') { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     mediaRecorder.value.start(); | ||||
|     recordingState.value = 'recording'; | ||||
|   }; | ||||
|  | ||||
|   const stopRecording = () => { | ||||
|     if (!isRecordingSupported.value) { | ||||
|       return; | ||||
|     } | ||||
|     if (!mediaRecorder.value) { | ||||
|       return; | ||||
|     } | ||||
|     if (recordingState.value === 'stopped') { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     mediaRecorder.value.stop(); | ||||
|     recordingState.value = 'stopped'; | ||||
|   }; | ||||
|  | ||||
|   const pauseRecording = () => { | ||||
|     if (!isRecordingSupported.value) { | ||||
|       return; | ||||
|     } | ||||
|     if (!mediaRecorder.value) { | ||||
|       return; | ||||
|     } | ||||
|     if (recordingState.value !== 'recording') { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     mediaRecorder.value.pause(); | ||||
|     recordingState.value = 'paused'; | ||||
|   }; | ||||
|  | ||||
|   const resumeRecording = () => { | ||||
|     if (!isRecordingSupported.value) { | ||||
|       return; | ||||
|     } | ||||
|     if (!mediaRecorder.value) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (recordingState.value !== 'paused') { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     mediaRecorder.value.resume(); | ||||
|     recordingState.value = 'recording'; | ||||
|   }; | ||||
|  | ||||
|   return { | ||||
|     isRecordingSupported, | ||||
|     startRecording, | ||||
|     stopRecording, | ||||
|     pauseRecording, | ||||
|     resumeRecording, | ||||
|     recordingState, | ||||
|  | ||||
|     onRecordAvailable: recordAvailable.on, | ||||
|   }; | ||||
| } | ||||
| @@ -1,4 +1,51 @@ | ||||
| <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> | ||||
|  | ||||
|       <n-divider /> | ||||
|  | ||||
|       <n-form-item label="Camelcase:"> | ||||
|         <input-copyable :value="camelCase(input, baseConfig)" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Capitalcase:"> | ||||
|         <input-copyable :value="capitalCase(input, baseConfig)" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Constantcase:"> | ||||
|         <input-copyable :value="constantCase(input, baseConfig)" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Dotcase:"> | ||||
|         <input-copyable :value="dotCase(input, baseConfig)" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Headercase:"> | ||||
|         <input-copyable :value="headerCase(input, baseConfig)" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Nocase:"> | ||||
|         <input-copyable :value="noCase(input, baseConfig)" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Paramcase:"> | ||||
|         <input-copyable :value="paramCase(input, baseConfig)" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Pascalcase:"> | ||||
|         <input-copyable :value="pascalCase(input, baseConfig)" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Pathcase:"> | ||||
|         <input-copyable :value="pathCase(input, baseConfig)" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Sentencecase:"> | ||||
|         <input-copyable :value="sentenceCase(input, baseConfig)" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Snakecase:"> | ||||
|         <input-copyable :value="snakeCase(input, baseConfig)" /> | ||||
|       </n-form-item> | ||||
|     </n-form> | ||||
|   </c-card> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { ref } from 'vue'; | ||||
| import { | ||||
|   camelCase, | ||||
|   capitalCase, | ||||
| @@ -19,88 +66,10 @@ const baseConfig = { | ||||
| }; | ||||
|  | ||||
| const input = ref('lorem ipsum dolor sit amet'); | ||||
|  | ||||
| const formats = computed(() => [ | ||||
|   { | ||||
|     label: 'Lowercase:', | ||||
|     value: noCase(input.value, baseConfig).toLocaleLowerCase(), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Uppercase:', | ||||
|     value: noCase(input.value, baseConfig).toLocaleUpperCase(), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Camelcase:', | ||||
|     value: camelCase(input.value, baseConfig), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Capitalcase:', | ||||
|     value: capitalCase(input.value, baseConfig), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Constantcase:', | ||||
|     value: constantCase(input.value, baseConfig), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Dotcase:', | ||||
|     value: dotCase(input.value, baseConfig), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Headercase:', | ||||
|     value: headerCase(input.value, baseConfig), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Nocase:', | ||||
|     value: noCase(input.value, baseConfig), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Paramcase:', | ||||
|     value: paramCase(input.value, baseConfig), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Pascalcase:', | ||||
|     value: pascalCase(input.value, baseConfig), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Pathcase:', | ||||
|     value: pathCase(input.value, baseConfig), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Sentencecase:', | ||||
|     value: sentenceCase(input.value, baseConfig), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Snakecase:', | ||||
|     value: snakeCase(input.value, baseConfig), | ||||
|   }, | ||||
| ]); | ||||
|  | ||||
| const inputLabelAlignmentConfig = { | ||||
|   labelPosition: 'left', | ||||
|   labelWidth: '120px', | ||||
|   labelAlign: 'right', | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <c-card> | ||||
|     <c-input-text | ||||
|       v-model:value="input" | ||||
|       label="Your string:" | ||||
|       placeholder="Your string..." | ||||
|       raw-text | ||||
|       v-bind="inputLabelAlignmentConfig" | ||||
|     /> | ||||
|  | ||||
|     <div my-16px divider /> | ||||
|  | ||||
|     <InputCopyable | ||||
|       v-for="format in formats" | ||||
|       :key="format.label" | ||||
|       :value="format.value" | ||||
|       :label="format.label" | ||||
|       v-bind="inputLabelAlignmentConfig" | ||||
|       mb-1 | ||||
|     /> | ||||
|   </c-card> | ||||
| </template> | ||||
| <style lang="less" scoped> | ||||
| .n-form-item { | ||||
|   margin: 5px 0; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { describe, expect, it } from 'vitest'; | ||||
| import { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation } from './chmod-calculator.service'; | ||||
| import { expect, describe, it } from 'vitest'; | ||||
| import { computeChmodOctalRepresentation } from './chmod-calculator.service'; | ||||
|  | ||||
| describe('chmod-calculator', () => { | ||||
|   describe('computeChmodOctalRepresentation', () => { | ||||
| @@ -64,67 +64,5 @@ describe('chmod-calculator', () => { | ||||
|         }), | ||||
|       ).to.eql('222'); | ||||
|     }); | ||||
|  | ||||
|     it('get the symbolic representation from permissions', () => { | ||||
|       expect( | ||||
|         computeChmodSymbolicRepresentation({ | ||||
|           permissions: { | ||||
|             owner: { read: true, write: true, execute: true }, | ||||
|             group: { read: true, write: true, execute: true }, | ||||
|             public: { read: true, write: true, execute: true }, | ||||
|           }, | ||||
|         }), | ||||
|       ).to.eql('rwxrwxrwx'); | ||||
|  | ||||
|       expect( | ||||
|         computeChmodSymbolicRepresentation({ | ||||
|           permissions: { | ||||
|             owner: { read: false, write: false, execute: false }, | ||||
|             group: { read: false, write: false, execute: false }, | ||||
|             public: { read: false, write: false, execute: false }, | ||||
|           }, | ||||
|         }), | ||||
|       ).to.eql('---------'); | ||||
|  | ||||
|       expect( | ||||
|         computeChmodSymbolicRepresentation({ | ||||
|           permissions: { | ||||
|             owner: { read: false, write: true, execute: false }, | ||||
|             group: { read: false, write: true, execute: true }, | ||||
|             public: { read: true, write: false, execute: true }, | ||||
|           }, | ||||
|         }), | ||||
|       ).to.eql('-w--wxr-x'); | ||||
|  | ||||
|       expect( | ||||
|         computeChmodSymbolicRepresentation({ | ||||
|           permissions: { | ||||
|             owner: { read: true, write: false, execute: false }, | ||||
|             group: { read: false, write: true, execute: false }, | ||||
|             public: { read: false, write: false, execute: true }, | ||||
|           }, | ||||
|         }), | ||||
|       ).to.eql('r---w---x'); | ||||
|  | ||||
|       expect( | ||||
|         computeChmodSymbolicRepresentation({ | ||||
|           permissions: { | ||||
|             owner: { read: false, write: false, execute: true }, | ||||
|             group: { read: false, write: true, execute: false }, | ||||
|             public: { read: true, write: false, execute: false }, | ||||
|           }, | ||||
|         }), | ||||
|       ).to.eql('--x-w-r--'); | ||||
|  | ||||
|       expect( | ||||
|         computeChmodSymbolicRepresentation({ | ||||
|           permissions: { | ||||
|             owner: { read: false, write: true, execute: false }, | ||||
|             group: { read: false, write: true, execute: false }, | ||||
|             public: { read: false, write: true, execute: false }, | ||||
|           }, | ||||
|         }), | ||||
|       ).to.eql('-w--w--w-'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import _ from 'lodash'; | ||||
| import type { GroupPermissions, Permissions } from './chmod-calculator.types'; | ||||
|  | ||||
| export { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation }; | ||||
| export { computeChmodOctalRepresentation }; | ||||
|  | ||||
| function computeChmodOctalRepresentation({ permissions }: { permissions: Permissions }): string { | ||||
|   const permissionValue = { read: 4, write: 2, execute: 1 }; | ||||
| @@ -15,16 +15,3 @@ function computeChmodOctalRepresentation({ permissions }: { permissions: Permiss | ||||
|     getGroupPermissionValue(permissions.public), | ||||
|   ].join(''); | ||||
| } | ||||
|  | ||||
| function computeChmodSymbolicRepresentation({ permissions }: { permissions: Permissions }): string { | ||||
|   const permissionValue = { read: 'r', write: 'w', execute: 'x' }; | ||||
|  | ||||
|   const getGroupPermissionValue = (permission: GroupPermissions) => | ||||
|     _.reduce(permission, (acc, isPermSet, key) => acc + (isPermSet ? _.get(permissionValue, key, '') : '-'), ''); | ||||
|  | ||||
|   return [ | ||||
|     getGroupPermissionValue(permissions.owner), | ||||
|     getGroupPermissionValue(permissions.group), | ||||
|     getGroupPermissionValue(permissions.public), | ||||
|   ].join(''); | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,38 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table"> | ||||
|       <thead> | ||||
|         <tr> | ||||
|           <th class="text-center" scope="col"></th> | ||||
|           <th class="text-center" scope="col">Owner (u)</th> | ||||
|           <th class="text-center" scope="col">Group (g)</th> | ||||
|           <th class="text-center" scope="col">Public (o)</th> | ||||
|         </tr> | ||||
|       </thead> | ||||
|       <tbody> | ||||
|         <tr v-for="{ scope, title } of scopes" :key="scope"> | ||||
|           <td class="line-header">{{ title }}</td> | ||||
|           <td v-for="group of groups" :key="group" class="text-center"> | ||||
|             <!-- <n-switch v-model:value="permissions[group][scope]" /> --> | ||||
|             <n-checkbox v-model:checked="permissions[group][scope]" size="large" /> | ||||
|           </td> | ||||
|         </tr> | ||||
|       </tbody> | ||||
|     </n-table> | ||||
|  | ||||
|     <div class="octal-result"> | ||||
|       {{ octal }} | ||||
|     </div> | ||||
|  | ||||
|     <input-copyable :value="`chmod ${octal} path`" readonly /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useThemeVars } from 'naive-ui'; | ||||
|  | ||||
| import { computed, ref } from 'vue'; | ||||
| import { computeChmodOctalRepresentation } from './chmod-calculator.service'; | ||||
| import InputCopyable from '../../components/InputCopyable.vue'; | ||||
| import { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation } from './chmod-calculator.service'; | ||||
|  | ||||
| import type { Group, Scope } from './chmod-calculator.types'; | ||||
|  | ||||
| @@ -22,50 +52,8 @@ const permissions = ref({ | ||||
| }); | ||||
|  | ||||
| const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value })); | ||||
| const symbolic = computed(() => computeChmodSymbolicRepresentation({ permissions: permissions.value })); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|     <n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table"> | ||||
|       <thead> | ||||
|         <tr> | ||||
|           <th class="text-center" scope="col" /> | ||||
|           <th class="text-center" scope="col"> | ||||
|             Owner (u) | ||||
|           </th> | ||||
|           <th class="text-center" scope="col"> | ||||
|             Group (g) | ||||
|           </th> | ||||
|           <th class="text-center" scope="col"> | ||||
|             Public (o) | ||||
|           </th> | ||||
|         </tr> | ||||
|       </thead> | ||||
|       <tbody> | ||||
|         <tr v-for="{ scope, title } of scopes" :key="scope"> | ||||
|           <td class="line-header"> | ||||
|             {{ title }} | ||||
|           </td> | ||||
|           <td v-for="group of groups" :key="group" class="text-center"> | ||||
|             <!-- <n-switch v-model:value="permissions[group][scope]" /> --> | ||||
|             <n-checkbox v-model:checked="permissions[group][scope]" size="large" /> | ||||
|           </td> | ||||
|         </tr> | ||||
|       </tbody> | ||||
|     </n-table> | ||||
|  | ||||
|     <div class="octal-result"> | ||||
|       {{ octal }} | ||||
|     </div> | ||||
|     <div class="octal-result"> | ||||
|       {{ symbolic }} | ||||
|     </div> | ||||
|  | ||||
|     <InputCopyable :value="`chmod ${octal} path`" readonly /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| .octal-result { | ||||
|   text-align: center; | ||||
|   | ||||
| @@ -1,6 +1,20 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <c-card> | ||||
|       <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 @click="counter = 0">Reset</c-button> | ||||
|     </n-space> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useRafFn } from '@vueuse/core'; | ||||
|  | ||||
| import { ref } from 'vue'; | ||||
| import { formatMs } from './chronometer.service'; | ||||
|  | ||||
| const isRunning = ref(false); | ||||
| @@ -28,28 +42,6 @@ function pause() { | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|     <c-card> | ||||
|       <div class="duration"> | ||||
|         {{ formatMs(counter) }} | ||||
|       </div> | ||||
|     </c-card> | ||||
|     <div mt-5 flex justify-center gap-3> | ||||
|       <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> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| .duration { | ||||
|   text-align: center; | ||||
|   | ||||
| @@ -1,4 +1,40 @@ | ||||
| <template> | ||||
|   <c-card> | ||||
|     <n-form label-width="100" label-placement="left"> | ||||
|       <n-form-item label="color picker:"> | ||||
|         <n-color-picker | ||||
|           v-model:value="hex" | ||||
|           placement="bottom-end" | ||||
|           @update:value="(v: string) => onInputUpdated(v, 'hex')" | ||||
|         /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="color name:"> | ||||
|         <input-copyable v-model:value="name" :on-input="(v: string) => onInputUpdated(v, 'name')" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="hex:"> | ||||
|         <input-copyable v-model:value="hex" :on-input="(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')" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="hsl:"> | ||||
|         <input-copyable v-model:value="hsl" :on-input="(v: string) => onInputUpdated(v, 'hsl')" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="hwb:"> | ||||
|         <input-copyable v-model:value="hwb" :on-input="(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')" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="cmyk:"> | ||||
|         <input-copyable v-model:value="cmyk" :on-input="(v: string) => onInputUpdated(v, 'cmyk')" /> | ||||
|       </n-form-item> | ||||
|     </n-form> | ||||
|   </c-card> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { ref } from 'vue'; | ||||
| import { colord, extend } from 'colord'; | ||||
|  | ||||
| import cmykPlugin from 'colord/plugins/cmyk'; | ||||
| @@ -18,70 +54,16 @@ const cmyk = ref(''); | ||||
| const lch = ref(''); | ||||
|  | ||||
| function onInputUpdated(value: string, omit: string) { | ||||
|   try { | ||||
|     const color = colord(value); | ||||
|   const color = colord(value); | ||||
|  | ||||
|     if (omit !== 'name') { | ||||
|       name.value = color.toName({ closest: true }) ?? ''; | ||||
|     } | ||||
|     if (omit !== 'hex') { | ||||
|       hex.value = color.toHex(); | ||||
|     } | ||||
|     if (omit !== 'rgb') { | ||||
|       rgb.value = color.toRgbString(); | ||||
|     } | ||||
|     if (omit !== 'hsl') { | ||||
|       hsl.value = color.toHslString(); | ||||
|     } | ||||
|     if (omit !== 'hwb') { | ||||
|       hwb.value = color.toHwbString(); | ||||
|     } | ||||
|     if (omit !== 'cmyk') { | ||||
|       cmyk.value = color.toCmykString(); | ||||
|     } | ||||
|     if (omit !== 'lch') { | ||||
|       lch.value = color.toLchString(); | ||||
|     } | ||||
|   } | ||||
|   catch { | ||||
|     // | ||||
|   } | ||||
|   if (omit !== 'name') name.value = color.toName({ closest: true }) ?? ''; | ||||
|   if (omit !== 'hex') hex.value = color.toHex(); | ||||
|   if (omit !== 'rgb') rgb.value = color.toRgbString(); | ||||
|   if (omit !== 'hsl') hsl.value = color.toHslString(); | ||||
|   if (omit !== 'hwb') hwb.value = color.toHwbString(); | ||||
|   if (omit !== 'cmyk') cmyk.value = color.toCmykString(); | ||||
|   if (omit !== 'lch') lch.value = color.toLchString(); | ||||
| } | ||||
|  | ||||
| onInputUpdated(hex.value, 'hex'); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <c-card> | ||||
|     <n-form label-width="100" label-placement="left"> | ||||
|       <n-form-item label="color picker:"> | ||||
|         <n-color-picker | ||||
|           v-model:value="hex" | ||||
|           placement="bottom-end" | ||||
|           @update:value="(v: string) => onInputUpdated(v, 'hex')" | ||||
|         /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="color name:"> | ||||
|         <InputCopyable v-model:value="name" @update:value="(v: string) => onInputUpdated(v, 'name')" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="hex:"> | ||||
|         <InputCopyable v-model:value="hex" @update:value="(v: string) => onInputUpdated(v, 'hex')" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="rgb:"> | ||||
|         <InputCopyable v-model:value="rgb" @update:value="(v: string) => onInputUpdated(v, 'rgb')" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="hsl:"> | ||||
|         <InputCopyable v-model:value="hsl" @update:value="(v: string) => onInputUpdated(v, 'hsl')" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="hwb:"> | ||||
|         <InputCopyable v-model:value="hwb" @update:value="(v: string) => onInputUpdated(v, 'hwb')" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="lch:"> | ||||
|         <InputCopyable v-model:value="lch" @update:value="(v: string) => onInputUpdated(v, 'lch')" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="cmyk:"> | ||||
|         <InputCopyable v-model:value="cmyk" @update:value="(v: string) => onInputUpdated(v, 'cmyk')" /> | ||||
|       </n-form-item> | ||||
|     </n-form> | ||||
|   </c-card> | ||||
| </template> | ||||
|   | ||||
| @@ -1,6 +1,92 @@ | ||||
| <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 class="cron-string"> | ||||
|       {{ cronString }} | ||||
|     </div> | ||||
|  | ||||
|     <n-divider /> | ||||
|  | ||||
|     <n-space justify="center"> | ||||
|       <n-form :show-feedback="false" label-width="170" label-placement="left"> | ||||
|         <n-form-item label="Verbose"> | ||||
|           <n-switch v-model:value="cronstrueConfig.verbose" /> | ||||
|         </n-form-item> | ||||
|         <n-form-item label="Use 24 hour time format"> | ||||
|           <n-switch v-model:value="cronstrueConfig.use24HourTimeFormat" /> | ||||
|         </n-form-item> | ||||
|         <n-form-item label="Days start at 0"> | ||||
|           <n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" /> | ||||
|         </n-form-item> | ||||
|       </n-form> | ||||
|     </n-space> | ||||
|   </c-card> | ||||
|   <c-card> | ||||
|     <pre> | ||||
| ┌──────────── [optional] seconds (0 - 59) | ||||
| | ┌────────── minute (0 - 59) | ||||
| | | ┌──────── hour (0 - 23) | ||||
| | | | ┌────── day of month (1 - 31) | ||||
| | | | | ┌──── month (1 - 12) OR jan,feb,mar,apr ... | ||||
| | | | | | ┌── day of week (0 - 6, sunday=0) OR sun,mon ... | ||||
| | | | | | | | ||||
| * * * * * * command</pre | ||||
|     > | ||||
|  | ||||
|     <n-space v-if="styleStore.isSmallScreen" vertical> | ||||
|       <c-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" important:border-none> | ||||
|         <div> | ||||
|           Symbol: <strong>{{ symbol }}</strong> | ||||
|         </div> | ||||
|         <div> | ||||
|           Meaning: <strong>{{ meaning }}</strong> | ||||
|         </div> | ||||
|         <div> | ||||
|           Example: | ||||
|           <strong | ||||
|             ><code>{{ example }}</code></strong | ||||
|           > | ||||
|         </div> | ||||
|         <div> | ||||
|           Equivalent: <strong>{{ equivalent }}</strong> | ||||
|         </div> | ||||
|       </c-card> | ||||
|     </n-space> | ||||
|     <n-table v-else size="small"> | ||||
|       <thead> | ||||
|         <tr> | ||||
|           <th class="text-left" scope="col">Symbol</th> | ||||
|           <th class="text-left" scope="col">Meaning</th> | ||||
|           <th class="text-left" scope="col">Example</th> | ||||
|           <th class="text-left" scope="col">Equivalent</th> | ||||
|         </tr> | ||||
|       </thead> | ||||
|       <tbody> | ||||
|         <tr v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol"> | ||||
|           <td>{{ symbol }}</td> | ||||
|           <td>{{ meaning }}</td> | ||||
|           <td> | ||||
|             <code>{{ example }}</code> | ||||
|           </td> | ||||
|           <td>{{ equivalent }}</td> | ||||
|         </tr> | ||||
|       </tbody> | ||||
|     </n-table> | ||||
|   </c-card> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| 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) { | ||||
| @@ -99,111 +185,30 @@ const cronString = computed(() => { | ||||
|   return ' '; | ||||
| }); | ||||
|  | ||||
| const cronValidationRules = [ | ||||
|   { | ||||
|     validator: (value: string) => isCronValid(value), | ||||
|     message: 'This cron is invalid', | ||||
|   }, | ||||
| ]; | ||||
| const cronValidation = useValidation({ | ||||
|   source: cron, | ||||
|   rules: [ | ||||
|     { | ||||
|       validator: (value) => isCronValid(value), | ||||
|       message: 'This cron is invalid', | ||||
|     }, | ||||
|   ], | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <c-card> | ||||
|     <div mx-auto max-w-sm> | ||||
|       <c-input-text | ||||
|         v-model:value="cron" | ||||
|         size="large" | ||||
|         placeholder="* * * * *" | ||||
|         :validation-rules="cronValidationRules" | ||||
|         mb-3 | ||||
|       /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="cron-string"> | ||||
|       {{ cronString }} | ||||
|     </div> | ||||
|  | ||||
|     <n-divider /> | ||||
|  | ||||
|     <div flex justify-center> | ||||
|       <n-form :show-feedback="false" label-width="170" label-placement="left"> | ||||
|         <n-form-item label="Verbose"> | ||||
|           <n-switch v-model:value="cronstrueConfig.verbose" /> | ||||
|         </n-form-item> | ||||
|         <n-form-item label="Use 24 hour time format"> | ||||
|           <n-switch v-model:value="cronstrueConfig.use24HourTimeFormat" /> | ||||
|         </n-form-item> | ||||
|         <n-form-item label="Days start at 0"> | ||||
|           <n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" /> | ||||
|         </n-form-item> | ||||
|       </n-form> | ||||
|     </div> | ||||
|   </c-card> | ||||
|   <c-card> | ||||
|     <pre> | ||||
| ┌──────────── [optional] seconds (0 - 59) | ||||
| | ┌────────── minute (0 - 59) | ||||
| | | ┌──────── hour (0 - 23) | ||||
| | | | ┌────── day of month (1 - 31) | ||||
| | | | | ┌──── month (1 - 12) OR jan,feb,mar,apr ... | ||||
| | | | | | ┌── day of week (0 - 6, sunday=0) OR sun,mon ... | ||||
| | | | | | | | ||||
| * * * * * * command</pre> | ||||
|  | ||||
|     <div v-if="styleStore.isSmallScreen"> | ||||
|       <c-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" mb-3 important:border-none> | ||||
|         <div> | ||||
|           Symbol: <strong>{{ symbol }}</strong> | ||||
|         </div> | ||||
|         <div> | ||||
|           Meaning: <strong>{{ meaning }}</strong> | ||||
|         </div> | ||||
|         <div> | ||||
|           Example: | ||||
|           <strong><code>{{ example }}</code></strong> | ||||
|         </div> | ||||
|         <div> | ||||
|           Equivalent: <strong>{{ equivalent }}</strong> | ||||
|         </div> | ||||
|       </c-card> | ||||
|     </div> | ||||
|     <n-table v-else size="small"> | ||||
|       <thead> | ||||
|         <tr> | ||||
|           <th class="text-left" scope="col"> | ||||
|             Symbol | ||||
|           </th> | ||||
|           <th class="text-left" scope="col"> | ||||
|             Meaning | ||||
|           </th> | ||||
|           <th class="text-left" scope="col"> | ||||
|             Example | ||||
|           </th> | ||||
|           <th class="text-left" scope="col"> | ||||
|             Equivalent | ||||
|           </th> | ||||
|         </tr> | ||||
|       </thead> | ||||
|       <tbody> | ||||
|         <tr v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol"> | ||||
|           <td>{{ symbol }}</td> | ||||
|           <td>{{ meaning }}</td> | ||||
|           <td> | ||||
|             <code>{{ example }}</code> | ||||
|           </td> | ||||
|           <td>{{ equivalent }}</td> | ||||
|         </tr> | ||||
|       </tbody> | ||||
|     </n-table> | ||||
|   </c-card> | ||||
| </template> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| ::v-deep(input) { | ||||
|   font-size: 30px; | ||||
|   font-family: monospace; | ||||
|   padding: 5px; | ||||
| .cron { | ||||
|   text-align: center; | ||||
|  | ||||
|   margin: auto; | ||||
|   max-width: 400px; | ||||
|   display: block; | ||||
|  | ||||
|   .n-input { | ||||
|     font-size: 30px; | ||||
|     font-family: monospace; | ||||
|     padding: 5px; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .cron-string { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, test } from '@playwright/test'; | ||||
| import { test, expect } from '@playwright/test'; | ||||
|  | ||||
| test.describe('Date time converter - json to yaml', () => { | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| import { describe, expect, test } from 'vitest'; | ||||
| import { describe, test, expect } from 'vitest'; | ||||
| import { | ||||
|   isISO8601DateTimeString, | ||||
|   isISO9075DateString, | ||||
|   isMongoObjectId, | ||||
|   isRFC3339DateString, | ||||
|   isRFC7231DateString, | ||||
|   isUnixTimestamp, | ||||
|   isTimestamp, | ||||
|   isUTCDateString, | ||||
|   isUnixTimestamp, | ||||
|   isMongoObjectId, | ||||
| } from './date-time-converter.models'; | ||||
|  | ||||
| describe('date-time-converter models', () => { | ||||
|   | ||||
| @@ -11,13 +11,13 @@ export { | ||||
|   isMongoObjectId, | ||||
| }; | ||||
|  | ||||
| const ISO8601_REGEX | ||||
|   = /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; | ||||
| const ISO9075_REGEX | ||||
|   = /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/; | ||||
| const ISO8601_REGEX = | ||||
|   /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; | ||||
| const ISO9075_REGEX = | ||||
|   /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/; | ||||
|  | ||||
| const RFC3339_REGEX | ||||
|   = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/; | ||||
| const RFC3339_REGEX = | ||||
|   /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/; | ||||
|  | ||||
| const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/; | ||||
|  | ||||
| @@ -40,8 +40,7 @@ function isUTCDateString(date?: string) { | ||||
|  | ||||
|   try { | ||||
|     return new Date(date).toUTCString() === date; | ||||
|   } | ||||
|   catch (_ignored) { | ||||
|   } catch (_ignored) { | ||||
|     return false; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| export type ToDateMapper = (value: string) => Date; | ||||
|  | ||||
| export interface DateFormat { | ||||
|   name: string | ||||
|   fromDate: (date: Date) => string | ||||
|   toDate: (value: string) => Date | ||||
|   formatMatcher: (dateString: string) => boolean | ||||
| } | ||||
| export type DateFormat = { | ||||
|   name: string; | ||||
|   fromDate: (date: Date) => string; | ||||
|   toDate: (value: string) => Date; | ||||
|   formatMatcher: (dateString: string) => boolean; | ||||
| }; | ||||
|   | ||||
| @@ -1,3 +1,38 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <n-form-item :show-label="false" v-bind="validation.attrs"> | ||||
|       <n-input-group> | ||||
|         <n-input | ||||
|           v-model:value="inputDate" | ||||
|           autofocus | ||||
|           :on-input="onDateInputChanged" | ||||
|           placeholder="Put you date string here..." | ||||
|           clearable | ||||
|           :input-props="{ 'data-test-id': 'date-time-converter-input' }" | ||||
|         /> | ||||
|  | ||||
|         <n-select | ||||
|           v-model:value="formatIndex" | ||||
|           style="flex: 0 0 170px" | ||||
|           :options="formats.map(({ name }, i) => ({ label: name, value: i }))" | ||||
|           data-test-id="date-time-converter-format-select" | ||||
|         /> | ||||
|       </n-input-group> | ||||
|     </n-form-item> | ||||
|     <n-divider style="margin-top: 0" /> | ||||
|     <div v-for="{ name, fromDate } in formats" :key="name" mt-1> | ||||
|       <n-input-group> | ||||
|         <n-input-group-label style="flex: 0 0 170px"> {{ name }}: </n-input-group-label> | ||||
|         <input-copyable | ||||
|           :value="formatDateUsingFormatter(fromDate, normalizedDate)" | ||||
|           placeholder="Invalid date..." | ||||
|           :input-props="{ 'data-test-id': name }" | ||||
|         /> | ||||
|       </n-input-group> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { | ||||
|   formatISO, | ||||
| @@ -7,33 +42,41 @@ import { | ||||
|   fromUnixTime, | ||||
|   getTime, | ||||
|   getUnixTime, | ||||
|   isDate, | ||||
|   isValid, | ||||
|   parseISO, | ||||
|   parseJSON, | ||||
|   isDate, | ||||
|   isValid, | ||||
| } from 'date-fns'; | ||||
| import { withDefaultOnError } from '@/utils/defaults'; | ||||
| import { useValidation } from '@/composable/validation'; | ||||
| import type { DateFormat, ToDateMapper } from './date-time-converter.types'; | ||||
| import { | ||||
|   isISO8601DateTimeString, | ||||
|   isISO9075DateString, | ||||
|   isMongoObjectId, | ||||
|   isRFC3339DateString, | ||||
|   isRFC7231DateString, | ||||
|   isTimestamp, | ||||
|   isUTCDateString, | ||||
|   isUnixTimestamp, | ||||
|   isMongoObjectId, | ||||
| } from './date-time-converter.models'; | ||||
| import { withDefaultOnError } from '@/utils/defaults'; | ||||
| import { useValidation } from '@/composable/validation'; | ||||
|  | ||||
| const inputDate = ref(''); | ||||
|  | ||||
| const toDate: ToDateMapper = date => new Date(date); | ||||
| const toDate: ToDateMapper = (date) => new Date(date); | ||||
|  | ||||
| function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) { | ||||
|   if (!date || !validation.isValid) { | ||||
|     return ''; | ||||
|   } | ||||
|  | ||||
|   return withDefaultOnError(() => formatter(date), ''); | ||||
| } | ||||
|  | ||||
| const formats: DateFormat[] = [ | ||||
|   { | ||||
|     name: 'JS locale date string', | ||||
|     fromDate: date => date.toString(), | ||||
|     fromDate: (date) => date.toString(), | ||||
|     toDate, | ||||
|     formatMatcher: () => false, | ||||
|   }, | ||||
| @@ -41,49 +84,49 @@ const formats: DateFormat[] = [ | ||||
|     name: 'ISO 8601', | ||||
|     fromDate: formatISO, | ||||
|     toDate: parseISO, | ||||
|     formatMatcher: date => isISO8601DateTimeString(date), | ||||
|     formatMatcher: (date) => isISO8601DateTimeString(date), | ||||
|   }, | ||||
|   { | ||||
|     name: 'ISO 9075', | ||||
|     fromDate: formatISO9075, | ||||
|     toDate: parseISO, | ||||
|     formatMatcher: date => isISO9075DateString(date), | ||||
|     formatMatcher: (date) => isISO9075DateString(date), | ||||
|   }, | ||||
|   { | ||||
|     name: 'RFC 3339', | ||||
|     fromDate: formatRFC3339, | ||||
|     toDate, | ||||
|     formatMatcher: date => isRFC3339DateString(date), | ||||
|     formatMatcher: (date) => isRFC3339DateString(date), | ||||
|   }, | ||||
|   { | ||||
|     name: 'RFC 7231', | ||||
|     fromDate: formatRFC7231, | ||||
|     toDate, | ||||
|     formatMatcher: date => isRFC7231DateString(date), | ||||
|     formatMatcher: (date) => isRFC7231DateString(date), | ||||
|   }, | ||||
|   { | ||||
|     name: 'Unix timestamp', | ||||
|     fromDate: date => String(getUnixTime(date)), | ||||
|     toDate: sec => fromUnixTime(+sec), | ||||
|     formatMatcher: date => isUnixTimestamp(date), | ||||
|     fromDate: (date) => String(getUnixTime(date)), | ||||
|     toDate: (sec) => fromUnixTime(+sec), | ||||
|     formatMatcher: (date) => isUnixTimestamp(date), | ||||
|   }, | ||||
|   { | ||||
|     name: 'Timestamp', | ||||
|     fromDate: date => String(getTime(date)), | ||||
|     toDate: ms => parseJSON(+ms), | ||||
|     formatMatcher: date => isTimestamp(date), | ||||
|     fromDate: (date) => String(getTime(date)), | ||||
|     toDate: (ms) => parseJSON(+ms), | ||||
|     formatMatcher: (date) => isTimestamp(date), | ||||
|   }, | ||||
|   { | ||||
|     name: 'UTC format', | ||||
|     fromDate: date => date.toUTCString(), | ||||
|     fromDate: (date) => date.toUTCString(), | ||||
|     toDate, | ||||
|     formatMatcher: date => isUTCDateString(date), | ||||
|     formatMatcher: (date) => isUTCDateString(date), | ||||
|   }, | ||||
|   { | ||||
|     name: 'Mongo ObjectID', | ||||
|     fromDate: date => `${Math.floor(date.getTime() / 1000).toString(16)}0000000000000000`, | ||||
|     toDate: objectId => new Date(Number.parseInt(objectId.substring(0, 8), 16) * 1000), | ||||
|     formatMatcher: date => isMongoObjectId(date), | ||||
|     fromDate: (date) => Math.floor(date.getTime() / 1000).toString(16) + '0000000000000000', | ||||
|     toDate: (objectId) => new Date(parseInt(objectId.substring(0, 8), 16) * 1000), | ||||
|     formatMatcher: (date) => isMongoObjectId(date), | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| @@ -99,8 +142,7 @@ const normalizedDate = computed(() => { | ||||
|  | ||||
|   try { | ||||
|     return toDate(inputDate.value); | ||||
|   } | ||||
|   catch (_ignored) { | ||||
|   } catch (_ignored) { | ||||
|     return undefined; | ||||
|   } | ||||
| }); | ||||
| @@ -118,11 +160,9 @@ const validation = useValidation({ | ||||
|   rules: [ | ||||
|     { | ||||
|       message: 'This date is invalid for this format', | ||||
|       validator: value => | ||||
|       validator: (value) => | ||||
|         withDefaultOnError(() => { | ||||
|           if (value === '') { | ||||
|             return true; | ||||
|           } | ||||
|           if (value === '') return true; | ||||
|  | ||||
|           const maybeDate = formats[formatIndex.value].toDate(value); | ||||
|           return isDate(maybeDate) && isValid(maybeDate); | ||||
| @@ -130,51 +170,4 @@ const validation = useValidation({ | ||||
|     }, | ||||
|   ], | ||||
| }); | ||||
|  | ||||
| function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) { | ||||
|   if (!date || !validation.isValid) { | ||||
|     return ''; | ||||
|   } | ||||
|  | ||||
|   return withDefaultOnError(() => formatter(date), ''); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|     <div flex gap-2> | ||||
|       <c-input-text | ||||
|         v-model:value="inputDate" | ||||
|         autofocus | ||||
|         placeholder="Put you date string here..." | ||||
|         clearable | ||||
|         test-id="date-time-converter-input" | ||||
|         :validation="validation" | ||||
|         @update:value="onDateInputChanged" | ||||
|       /> | ||||
|  | ||||
|       <c-select | ||||
|         v-model:value="formatIndex" | ||||
|         style="flex: 0 0 170px" | ||||
|         :options="formats.map(({ name }, i) => ({ label: name, value: i }))" | ||||
|         data-test-id="date-time-converter-format-select" | ||||
|       /> | ||||
|     </div> | ||||
|  | ||||
|     <n-divider /> | ||||
|  | ||||
|     <input-copyable | ||||
|       v-for="{ name, fromDate } in formats" | ||||
|       :key="name" | ||||
|       :label="name" | ||||
|       label-width="150px" | ||||
|       label-position="left" | ||||
|       label-align="right" | ||||
|       :value="formatDateUsingFormatter(fromDate, normalizedDate)" | ||||
|       placeholder="Invalid date..." | ||||
|       :test-id="name" | ||||
|       readonly | ||||
|       mt-2 | ||||
|     /> | ||||
|   </div> | ||||
| </template> | ||||
|   | ||||
| @@ -1,5 +1,25 @@ | ||||
| <template> | ||||
|   <c-card v-for="{ name, information } in sections" :key="name" :title="name"> | ||||
|     <n-grid cols="1 400:2" x-gap="12" y-gap="12"> | ||||
|       <n-gi v-for="{ label, value: { value } } in information" :key="label" class="information"> | ||||
|         <div class="label"> | ||||
|           {{ label }} | ||||
|         </div> | ||||
|  | ||||
|         <div class="value"> | ||||
|           <n-ellipsis v-if="value"> | ||||
|             {{ value }} | ||||
|           </n-ellipsis> | ||||
|           <div v-else class="undefined-value">unknown</div> | ||||
|         </div> | ||||
|       </n-gi> | ||||
|     </n-grid> | ||||
|   </c-card> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useWindowSize } from '@vueuse/core'; | ||||
| import { computed } from 'vue'; | ||||
|  | ||||
| const { width, height } = useWindowSize(); | ||||
|  | ||||
| @@ -57,27 +77,6 @@ const sections = [ | ||||
| ]; | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <c-card v-for="{ name, information } in sections" :key="name" :title="name"> | ||||
|     <n-grid cols="1 400:2" x-gap="12" y-gap="12"> | ||||
|       <n-gi v-for="{ label, value: { value } } in information" :key="label" class="information"> | ||||
|         <div class="label"> | ||||
|           {{ label }} | ||||
|         </div> | ||||
|  | ||||
|         <div class="value"> | ||||
|           <n-ellipsis v-if="value"> | ||||
|             {{ value }} | ||||
|           </n-ellipsis> | ||||
|           <div v-else class="undefined-value"> | ||||
|             unknown | ||||
|           </div> | ||||
|         </div> | ||||
|       </n-gi> | ||||
|     </n-grid> | ||||
|   </c-card> | ||||
| </template> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| .information { | ||||
|   padding: 14px 16px; | ||||
|   | ||||
| @@ -1,62 +1,27 @@ | ||||
| <script setup lang="ts"> | ||||
| import { MessageType, composerize } from 'composerize-ts'; | ||||
| import { withDefaultOnError } from '@/utils/defaults'; | ||||
| import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; | ||||
| import { textToBase64 } from '@/utils/base64'; | ||||
| import TextareaCopyable from '@/components/TextareaCopyable.vue'; | ||||
|  | ||||
| const dockerRun = ref( | ||||
|   'docker run -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro --restart always --log-opt max-size=1g nginx', | ||||
| ); | ||||
|  | ||||
| const conversionResult = computed(() => | ||||
|   withDefaultOnError(() => composerize(dockerRun.value.trim()), { yaml: '', messages: [] }), | ||||
| ); | ||||
| const dockerCompose = computed(() => conversionResult.value.yaml); | ||||
| const notImplemented = computed(() => | ||||
|   conversionResult.value.messages.filter(msg => msg.type === MessageType.notImplemented).map(msg => msg.value), | ||||
| ); | ||||
| const notComposable = computed(() => | ||||
|   conversionResult.value.messages.filter(msg => msg.type === MessageType.notTranslatable).map(msg => msg.value), | ||||
| ); | ||||
| const errors = computed(() => | ||||
|   conversionResult.value.messages | ||||
|     .filter(msg => msg.type === MessageType.errorDuringConversion) | ||||
|     .map(msg => msg.value), | ||||
| ); | ||||
| const dockerComposeBase64 = computed(() => `data:application/yaml;base64,${textToBase64(dockerCompose.value)}`); | ||||
| const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|     <c-input-text | ||||
|       v-model:value="dockerRun" | ||||
|       label="Your docker run command:" | ||||
|       style="font-family: monospace" | ||||
|       multiline | ||||
|       raw-text | ||||
|       monospace | ||||
|       placeholder="Your docker run command to convert..." | ||||
|       rows="3" | ||||
|     /> | ||||
|     <n-form-item label="Your docker run command:" :show-feedback="false"> | ||||
|       <n-input | ||||
|         v-model:value="dockerRun" | ||||
|         style="font-family: monospace" | ||||
|         type="textarea" | ||||
|         placeholder="Your docker run command to convert..." | ||||
|         rows="3" | ||||
|       /> | ||||
|     </n-form-item> | ||||
|  | ||||
|     <n-divider /> | ||||
|  | ||||
|     <TextareaCopyable :value="dockerCompose" language="yaml" /> | ||||
|     <textarea-copyable :value="dockerCompose" language="yaml" /> | ||||
|  | ||||
|     <div mt-5 flex justify-center> | ||||
|       <c-button :disabled="dockerCompose === ''" secondary @click="download"> | ||||
|         Download docker-compose.yml | ||||
|       </c-button> | ||||
|     </div> | ||||
|     <n-space justify="center" mt-5> | ||||
|       <c-button :disabled="dockerCompose === ''" secondary @click="download"> Download docker-compose.yml </c-button> | ||||
|     </n-space> | ||||
|  | ||||
|     <div v-if="notComposable.length > 0"> | ||||
|       <n-alert title="This options are not translatable to docker-compose" type="info" mt-5> | ||||
|         <ul> | ||||
|           <li v-for="(message, index) of notComposable" :key="index"> | ||||
|             {{ message }} | ||||
|           </li> | ||||
|           <li v-for="(message, index) of notComposable" :key="index">{{ message }}</li> | ||||
|         </ul> | ||||
|       </n-alert> | ||||
|     </div> | ||||
| @@ -68,9 +33,7 @@ const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, fi | ||||
|         mt-5 | ||||
|       > | ||||
|         <ul> | ||||
|           <li v-for="(message, index) of notImplemented" :key="index"> | ||||
|             {{ message }} | ||||
|           </li> | ||||
|           <li v-for="(message, index) of notImplemented" :key="index">{{ message }}</li> | ||||
|         </ul> | ||||
|       </n-alert> | ||||
|     </div> | ||||
| @@ -78,11 +41,41 @@ const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, fi | ||||
|     <div v-if="errors.length > 0"> | ||||
|       <n-alert title="The following errors occured" type="error" mt-5> | ||||
|         <ul> | ||||
|           <li v-for="(message, index) of errors" :key="index"> | ||||
|             {{ message }} | ||||
|           </li> | ||||
|           <li v-for="(message, index) of errors" :key="index">{{ message }}</li> | ||||
|         </ul> | ||||
|       </n-alert> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { computed, ref } from 'vue'; | ||||
| import { withDefaultOnError } from '@/utils/defaults'; | ||||
| import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; | ||||
| import { textToBase64 } from '@/utils/base64'; | ||||
| import TextareaCopyable from '@/components/TextareaCopyable.vue'; | ||||
|  | ||||
| import { composerize, MessageType } from 'composerize-ts'; | ||||
|  | ||||
| const dockerRun = ref( | ||||
|   'docker run -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro --restart always --log-opt max-size=1g nginx', | ||||
| ); | ||||
|  | ||||
| const conversionResult = computed(() => | ||||
|   withDefaultOnError(() => composerize(dockerRun.value), { yaml: '', messages: [] }), | ||||
| ); | ||||
| const dockerCompose = computed(() => conversionResult.value.yaml); | ||||
| const notImplemented = computed(() => | ||||
|   conversionResult.value.messages.filter((msg) => msg.type === MessageType.notImplemented).map((msg) => msg.value), | ||||
| ); | ||||
| const notComposable = computed(() => | ||||
|   conversionResult.value.messages.filter((msg) => msg.type === MessageType.notTranslatable).map((msg) => msg.value), | ||||
| ); | ||||
| const errors = computed(() => | ||||
|   conversionResult.value.messages | ||||
|     .filter((msg) => msg.type === MessageType.errorDuringConversion) | ||||
|     .map((msg) => msg.value), | ||||
| ); | ||||
| const dockerComposeBase64 = computed(() => 'data:application/yaml;base64,' + textToBase64(dockerCompose.value)); | ||||
| const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' }); | ||||
| </script> | ||||
|   | ||||
| @@ -1,42 +0,0 @@ | ||||
| <script setup lang="ts"> | ||||
| import type { EmojiInfo } from './emoji.types'; | ||||
| import { useCopy } from '@/composable/copy'; | ||||
|  | ||||
| const props = (defineProps<{ emojiInfo: EmojiInfo }>()); | ||||
| const { emojiInfo } = toRefs(props); | ||||
|  | ||||
| const { copy } = useCopy(); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <c-card flex items-center gap-3 important:py-8px important:pl-10px important:pr-5px> | ||||
|     <div cursor-pointer text-30px @click="copy(emojiInfo.emoji, { notificationMessage: `Emoji ${emojiInfo.emoji} copied to the clipboard` })"> | ||||
|       {{ emojiInfo.emoji }} | ||||
|     </div> | ||||
|  | ||||
|     <div min-w-0 flex-1> | ||||
|       <div truncate font-bold> | ||||
|         {{ emojiInfo.title }} | ||||
|       </div> | ||||
|  | ||||
|       <!-- <div> | ||||
|         <c-link> | ||||
|           {{ emojiInfo.codePoints }} | ||||
|         </c-link> | ||||
|       </div> | ||||
|       <div /> | ||||
|       <div rounded op-70> | ||||
|         Unicode:  <span border="1px solid current op-30" b-rd-xl px-12px py-4px>{{ emojiInfo.unicode }}</span> | ||||
|       </div> --> | ||||
|  | ||||
|       <div flex gap-2 text-xs font-mono op-70> | ||||
|         <span cursor-pointer transition hover:text-primary @click="copy(emojiInfo.codePoints, { notificationMessage: `Code points '${emojiInfo.codePoints}' copied to the clipboard` })"> | ||||
|           {{ emojiInfo.codePoints }} | ||||
|         </span> | ||||
|         <span cursor-pointer truncate transition hover:text-primary @click="copy(emojiInfo.unicode, { notificationMessage: `Unicode '${emojiInfo.unicode}' copied to the clipboard` })"> | ||||
|           {{ emojiInfo.unicode }} | ||||
|         </span> | ||||
|       </div> | ||||
|     </div> | ||||
|   </c-card> | ||||
| </template> | ||||
| @@ -1,12 +0,0 @@ | ||||
| <script setup lang="ts"> | ||||
| import type { EmojiInfo } from './emoji.types'; | ||||
|  | ||||
| const props = withDefaults(defineProps<{ emojiInfos?: EmojiInfo[] }>(), { emojiInfos: () => [] }); | ||||
| const { emojiInfos } = toRefs(props); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div grid grid-cols-1 gap-2 lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 xl:grid-cols-6> | ||||
|     <emoji-card v-for="emojiInfo in emojiInfos" :key="emojiInfo.name" :emoji-info="emojiInfo" flex items-center gap-3 /> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -1,85 +0,0 @@ | ||||
| <script setup lang="ts"> | ||||
| import emojiUnicodeData from 'unicode-emoji-json'; | ||||
| import emojiKeywords from 'emojilib'; | ||||
| import _ from 'lodash'; | ||||
| import type { EmojiInfo } from './emoji.types'; | ||||
| import { useFuzzySearch } from '@/composable/fuzzySearch'; | ||||
|  | ||||
| const escapeUnicode = ({ emoji }: { emoji: string }) => emoji.split('').map(unit => `\\u${unit.charCodeAt(0).toString(16).padStart(4, '0')}`).join(''); | ||||
| const getEmojiCodePoints = ({ emoji }: { emoji: string }) => emoji.codePointAt(0) ? `0x${emoji.codePointAt(0)?.toString(16)}` : undefined; | ||||
|  | ||||
| const emojis = _.map(emojiUnicodeData, (emojiInfo, emoji) => ({ | ||||
|   ...emojiInfo, | ||||
|   emoji, | ||||
|   title: _.capitalize(emojiInfo.name), | ||||
|   keywords: emojiKeywords[emoji as keyof typeof emojiKeywords], | ||||
|   codePoints: getEmojiCodePoints({ emoji }), | ||||
|   unicode: escapeUnicode({ emoji }), | ||||
| })); | ||||
|  | ||||
| const emojisGroups: { emojiInfos: EmojiInfo[]; group: string }[] = _ | ||||
|   .chain(emojis) | ||||
|   .groupBy('group') | ||||
|   .map((emojiInfos, group) => ({ group, emojiInfos })) | ||||
|   .value(); | ||||
|  | ||||
| const searchQuery = ref(''); | ||||
|  | ||||
| const { searchResult } = useFuzzySearch({ | ||||
|   search: searchQuery, | ||||
|   data: emojis, | ||||
|   options: { | ||||
|     keys: ['group', { name: 'name', weight: 3 }, 'keywords', 'unicode', 'codePoints', 'emoji'], | ||||
|     threshold: 0.3, | ||||
|     useExtendedSearch: true, | ||||
|     isCaseSensitive: false, | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div mx-auto max-w-2400px important:flex-1> | ||||
|     <div flex items-center gap-3> | ||||
|       <c-input-text | ||||
|         v-model:value="searchQuery" | ||||
|         placeholder="Search emojis (e.g. 'smile')..." | ||||
|         mx-auto max-w-600px | ||||
|       > | ||||
|         <template #prefix> | ||||
|           <icon-mdi-search mr-6px color-black op-70 dark:color-white /> | ||||
|         </template> | ||||
|       </c-input-text> | ||||
|     </div> | ||||
|  | ||||
|     <div v-if="searchQuery.trim().length > 0"> | ||||
|       <div | ||||
|         v-if="searchResult.length === 0" | ||||
|         mt-4 | ||||
|         text-20px | ||||
|         font-bold | ||||
|       > | ||||
|         No results | ||||
|       </div> | ||||
|  | ||||
|       <div v-else> | ||||
|         <div mt-4 text-20px font-bold> | ||||
|           Search result | ||||
|         </div> | ||||
|  | ||||
|         <emoji-grid :emoji-infos="searchResult" /> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div | ||||
|       v-for="{ group, emojiInfos } in emojisGroups" | ||||
|       v-else | ||||
|       :key="group" | ||||
|     > | ||||
|       <div mt-4 text-20px font-bold> | ||||
|         {{ group }} | ||||
|       </div> | ||||
|  | ||||
|       <emoji-grid :emoji-infos="emojiInfos" /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -1,8 +0,0 @@ | ||||
| import type emojiUnicodeData from 'unicode-emoji-json'; | ||||
|  | ||||
| export type EmojiInfo = { | ||||
|   title: string | ||||
|   emoji: string | ||||
|   codePoints: string | undefined | ||||
|   unicode: string | ||||
| } & typeof emojiUnicodeData[string]; | ||||
| @@ -1,12 +0,0 @@ | ||||
| import { MoodSmile } from '@vicons/tabler'; | ||||
| import { defineTool } from '../tool'; | ||||
|  | ||||
| export const tool = defineTool({ | ||||
|   name: 'Emoji picker', | ||||
|   path: '/emoji-picker', | ||||
|   description: 'Copy and paste emojis easily and get the unicode and code points value of each emoji.', | ||||
|   keywords: ['emoji', 'picker', 'unicode', 'copy', 'paste'], | ||||
|   component: () => import('./emoji-picker.vue'), | ||||
|   icon: MoodSmile, | ||||
|   createdAt: new Date('2023-08-07'), | ||||
| }); | ||||
| @@ -1,5 +1,81 @@ | ||||
| <template> | ||||
|   <c-card title="Encrypt"> | ||||
|     <n-space item-style="flex: 1 1 0"> | ||||
|       <n-form-item label="Your text:" :show-feedback="false"> | ||||
|         <n-input | ||||
|           v-model:value="cypherInput" | ||||
|           type="textarea" | ||||
|           placeholder="The string to cypher" | ||||
|           :autosize="{ minRows: 4 }" | ||||
|         /> | ||||
|       </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> | ||||
|         <n-form-item label="Encryption algorithm:" :show-feedback="false"> | ||||
|           <n-select | ||||
|             v-model:value="cypherAlgo" | ||||
|             :options="Object.keys(algos).map((label) => ({ label, value: label }))" | ||||
|           /> | ||||
|         </n-form-item> | ||||
|       </n-space> | ||||
|     </n-space> | ||||
|     <n-form-item label="Your text encrypted:" :show-feedback="false" mt-5> | ||||
|       <n-input | ||||
|         :value="cypherOutput" | ||||
|         type="textarea" | ||||
|         placeholder="Your string hash" | ||||
|         :autosize="{ minRows: 2 }" | ||||
|         readonly | ||||
|         autocomplete="off" | ||||
|         autocorrect="off" | ||||
|         autocapitalize="off" | ||||
|         spellcheck="false" | ||||
|       /> | ||||
|     </n-form-item> | ||||
|   </c-card> | ||||
|   <c-card title="Decrypt"> | ||||
|     <n-space item-style="flex: 1 1 0"> | ||||
|       <n-form-item label="Your encrypted text:" :show-feedback="false"> | ||||
|         <n-input | ||||
|           v-model:value="decryptInput" | ||||
|           type="textarea" | ||||
|           placeholder="The string to cypher" | ||||
|           :autosize="{ minRows: 4 }" | ||||
|         /> | ||||
|       </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> | ||||
|         <n-form-item label="Encryption algorithm:" :show-feedback="false"> | ||||
|           <n-select | ||||
|             v-model:value="decryptAlgo" | ||||
|             :options="Object.keys(algos).map((label) => ({ label, value: label }))" | ||||
|           /> | ||||
|         </n-form-item> | ||||
|       </n-space> | ||||
|     </n-space> | ||||
|     <n-form-item label="Your decrypted text:" :show-feedback="false" mt-5> | ||||
|       <n-input | ||||
|         :value="decryptOutput" | ||||
|         type="textarea" | ||||
|         placeholder="Your string hash" | ||||
|         :autosize="{ minRows: 2 }" | ||||
|         readonly | ||||
|         autocomplete="off" | ||||
|         autocorrect="off" | ||||
|         autocapitalize="off" | ||||
|         spellcheck="false" | ||||
|       /> | ||||
|     </n-form-item> | ||||
|   </c-card> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { AES, RC4, Rabbit, TripleDES, enc } from 'crypto-js'; | ||||
| import { computed, ref } from 'vue'; | ||||
| import { AES, TripleDES, Rabbit, RC4, enc } from 'crypto-js'; | ||||
|  | ||||
| const algos = { AES, TripleDES, Rabbit, RC4 }; | ||||
|  | ||||
| @@ -15,60 +91,3 @@ const decryptOutput = computed(() => | ||||
|   algos[decryptAlgo.value].decrypt(decryptInput.value, decryptSecret.value).toString(enc.Utf8), | ||||
| ); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <c-card title="Encrypt"> | ||||
|     <div flex gap-3> | ||||
|       <c-input-text | ||||
|         v-model:value="cypherInput" | ||||
|         label="Your text:" | ||||
|         placeholder="The string to cypher" | ||||
|         rows="4" | ||||
|         multiline raw-text monospace autosize flex-1 | ||||
|       /> | ||||
|       <div flex flex-1 flex-col gap-2> | ||||
|         <c-input-text v-model:value="cypherSecret" label="Your secret key:" clearable raw-text /> | ||||
|  | ||||
|         <c-select | ||||
|           v-model:value="cypherAlgo" | ||||
|           label="Encryption algorithm:" | ||||
|           :options="Object.keys(algos).map((label) => ({ label, value: label }))" | ||||
|         /> | ||||
|       </div> | ||||
|     </div> | ||||
|     <c-input-text | ||||
|       label="Your text encrypted:" | ||||
|       :value="cypherOutput" | ||||
|       rows="3" | ||||
|       placeholder="Your string hash" | ||||
|       multiline monospace readonly autosize mt-5 | ||||
|     /> | ||||
|   </c-card> | ||||
|   <c-card title="Decrypt"> | ||||
|     <div flex gap-3> | ||||
|       <c-input-text | ||||
|         v-model:value="decryptInput" | ||||
|         label="Your encrypted text:" | ||||
|         placeholder="The string to cypher" | ||||
|         rows="4" | ||||
|         multiline raw-text monospace autosize flex-1 | ||||
|       /> | ||||
|       <div flex flex-1 flex-col gap-2> | ||||
|         <c-input-text v-model:value="decryptSecret" label="Your secret key:" clearable raw-text /> | ||||
|  | ||||
|         <c-select | ||||
|           v-model:value="decryptAlgo" | ||||
|           label="Encryption algorithm:" | ||||
|           :options="Object.keys(algos).map((label) => ({ label, value: label }))" | ||||
|         /> | ||||
|       </div> | ||||
|     </div> | ||||
|     <c-input-text | ||||
|       label="Your decrypted text:" | ||||
|       :value="decryptOutput" | ||||
|       placeholder="Your string hash" | ||||
|       rows="3" | ||||
|       multiline monospace readonly autosize mt-5 | ||||
|     /> | ||||
|   </c-card> | ||||
| </template> | ||||
|   | ||||
| @@ -1,10 +1,61 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <n-text depth="3" style="text-align: justify; width: 100%; display: inline-block"> | ||||
|       With a concrete example, if you wash 3 plates in 5 minutes and you have 500 plates to wash, it will take you 5 | ||||
|       hours and 10 minutes to wash them all, and if you start now, you'll end | ||||
|       {{ endAt }}. | ||||
|     </n-text> | ||||
|     <n-divider /> | ||||
|     <n-space item-style="flex:1 1 0"> | ||||
|       <div> | ||||
|         <n-space item-style="flex:1 1 0"> | ||||
|           <n-form-item label="Amount of element to consume"> | ||||
|             <n-input-number v-model:value="unitCount" :min="1" /> | ||||
|           </n-form-item> | ||||
|           <n-form-item label="The consumption started at"> | ||||
|             <n-date-picker v-model:value="startedAt" type="datetime" /> | ||||
|           </n-form-item> | ||||
|         </n-space> | ||||
|  | ||||
|         <n-form-item label="Amount of unit consumed by time span" :show-feedback="false"> | ||||
|           <n-input-number v-model:value="unitPerTimeSpan" :min="1" /> | ||||
|           <span style="margin: 0 10px">in</span> | ||||
|           <n-input-group> | ||||
|             <n-input-number v-model:value="timeSpan" :min="1" /> | ||||
|             <n-select | ||||
|               v-model:value="timeSpanUnitMultiplier" | ||||
|               :options="[ | ||||
|                 { label: 'milliseconds', value: 1 }, | ||||
|                 { label: 'seconds', value: 1000 }, | ||||
|                 { label: 'minutes', value: 1000 * 60 }, | ||||
|                 { label: 'hours', value: 1000 * 60 * 60 }, | ||||
|                 { label: 'days', value: 1000 * 60 * 60 * 24 }, | ||||
|               ]" | ||||
|             ></n-select> | ||||
|           </n-input-group> | ||||
|         </n-form-item> | ||||
|  | ||||
|         <n-divider /> | ||||
|         <n-space vertical> | ||||
|           <c-card> | ||||
|             <n-statistic label="Total duration">{{ formatMsDuration(durationMs) }}</n-statistic> | ||||
|           </c-card> | ||||
|           <c-card> | ||||
|             <n-statistic label="It will end ">{{ endAt }}</n-statistic> | ||||
|           </c-card> | ||||
|         </n-space> | ||||
|       </div> | ||||
|     </n-space> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| // Duplicate issue with sub directory | ||||
|  | ||||
| // eslint-disable-next-line import/no-duplicates | ||||
| import { addMilliseconds, formatRelative } from 'date-fns'; | ||||
|  | ||||
| // eslint-disable-next-line import/no-duplicates | ||||
| import { enGB } from 'date-fns/locale'; | ||||
|  | ||||
| import { computed, ref } from 'vue'; | ||||
| import { formatMsDuration } from './eta-calculator.service'; | ||||
|  | ||||
| const unitCount = ref(3 * 62); | ||||
| @@ -23,56 +74,6 @@ const endAt = computed(() => | ||||
| ); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|     <div text-justify op-70> | ||||
|       With a concrete example, if you wash 3 plates in 5 minutes and you have 500 plates to wash, it will take you 5 | ||||
|       hours and 10 minutes to wash them all. | ||||
|     </div> | ||||
|     <n-divider /> | ||||
|     <div flex gap-2> | ||||
|       <n-form-item label="Amount of element to consume" flex-1> | ||||
|         <n-input-number v-model:value="unitCount" :min="1" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="The consumption started at" flex-1> | ||||
|         <n-date-picker v-model:value="startedAt" type="datetime" /> | ||||
|       </n-form-item> | ||||
|     </div> | ||||
|  | ||||
|     <p>Amount of unit consumed by time span</p> | ||||
|     <div flex flex-col items-baseline gap-y-2 md:flex-row> | ||||
|       <n-input-number v-model:value="unitPerTimeSpan" :min="1" /> | ||||
|       <div flex items-baseline gap-2> | ||||
|         <span ml-2>in</span> | ||||
|         <n-input-number v-model:value="timeSpan" min-w-130px :min="1" /> | ||||
|         <c-select | ||||
|           v-model:value="timeSpanUnitMultiplier" | ||||
|           min-w-130px | ||||
|           :options="[ | ||||
|             { label: 'milliseconds', value: 1 }, | ||||
|             { label: 'seconds', value: 1000 }, | ||||
|             { label: 'minutes', value: 1000 * 60 }, | ||||
|             { label: 'hours', value: 1000 * 60 * 60 }, | ||||
|             { label: 'days', value: 1000 * 60 * 60 * 24 }, | ||||
|           ]" | ||||
|         /> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <n-divider /> | ||||
|     <c-card mb-2> | ||||
|       <n-statistic label="Total duration"> | ||||
|         {{ formatMsDuration(durationMs) }} | ||||
|       </n-statistic> | ||||
|     </c-card> | ||||
|     <c-card> | ||||
|       <n-statistic label="It will end "> | ||||
|         {{ endAt }} | ||||
|       </n-statistic> | ||||
|     </c-card> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| .n-input-number, | ||||
| .n-date-picker { | ||||
|   | ||||
| @@ -1,16 +1,16 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <memo /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useThemeVars } from 'naive-ui'; | ||||
| import Memo from './git-memo.content.md'; | ||||
| import Memo from './git-memo.md'; | ||||
|  | ||||
| const themeVars = useThemeVars(); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|     <Memo /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| ::v-deep(pre) { | ||||
|   margin: 0; | ||||
|   | ||||
| @@ -2,6 +2,6 @@ export function convertHexToBin(hex: string) { | ||||
|   return hex | ||||
|     .trim() | ||||
|     .split('') | ||||
|     .map(byte => Number.parseInt(byte, 16).toString(2).padStart(4, '0')) | ||||
|     .map((byte) => parseInt(byte, 16).toString(2).padStart(4, '0')) | ||||
|     .join(''); | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,50 @@ | ||||
| <script setup lang="ts"> | ||||
| import type { lib } from 'crypto-js'; | ||||
| import { MD5, RIPEMD160, SHA1, SHA224, SHA256, SHA3, SHA384, SHA512, enc } from 'crypto-js'; | ||||
| <template> | ||||
|   <div> | ||||
|     <c-card> | ||||
|       <n-input v-model:value="clearText" type="textarea" placeholder="Your string to hash..." rows="3" /> | ||||
|  | ||||
|       <n-divider /> | ||||
|  | ||||
|       <n-form-item label="Digest encoding"> | ||||
|         <n-select | ||||
|           v-model:value="encoding" | ||||
|           :options="[ | ||||
|             { | ||||
|               label: 'Binary (base 2)', | ||||
|               value: 'Bin', | ||||
|             }, | ||||
|             { | ||||
|               label: 'Hexadecimal (base 16)', | ||||
|               value: 'Hex', | ||||
|             }, | ||||
|             { | ||||
|               label: 'Base64 (base 64)', | ||||
|               value: 'Base64', | ||||
|             }, | ||||
|             { | ||||
|               label: 'Base64url (base 64 with url safe chars)', | ||||
|               value: 'Base64url', | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </n-form-item> | ||||
|  | ||||
|       <div v-for="algo in algoNames" :key="algo" style="margin: 5px 0"> | ||||
|         <n-input-group> | ||||
|           <n-input-group-label style="flex: 0 0 120px"> {{ algo }} </n-input-group-label> | ||||
|           <input-copyable :value="hashText(algo, clearText)" readonly /> | ||||
|         </n-input-group> | ||||
|       </div> | ||||
|     </c-card> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useQueryParam } from '@/composable/queryParams'; | ||||
| import { enc, lib, MD5, RIPEMD160, SHA1, SHA224, SHA256, SHA3, SHA384, SHA512 } from 'crypto-js'; | ||||
| import { ref } from 'vue'; | ||||
| import InputCopyable from '../../components/InputCopyable.vue'; | ||||
| import { convertHexToBin } from './hash-text.service'; | ||||
| import { useQueryParam } from '@/composable/queryParams'; | ||||
|  | ||||
| const algos = { | ||||
|   MD5, | ||||
| @@ -33,46 +73,3 @@ function formatWithEncoding(words: lib.WordArray, encoding: Encoding) { | ||||
|  | ||||
| const hashText = (algo: AlgoNames, value: string) => formatWithEncoding(algos[algo](value), encoding.value); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|     <c-card> | ||||
|       <c-input-text v-model:value="clearText" multiline raw-text placeholder="Your string to hash..." rows="3" autosize autofocus label="Your text to hash:" /> | ||||
|  | ||||
|       <n-divider /> | ||||
|  | ||||
|       <c-select | ||||
|         v-model:value="encoding" | ||||
|         mb-4 | ||||
|         label="Digest encoding" | ||||
|         :options="[ | ||||
|           { | ||||
|             label: 'Binary (base 2)', | ||||
|             value: 'Bin', | ||||
|           }, | ||||
|           { | ||||
|             label: 'Hexadecimal (base 16)', | ||||
|             value: 'Hex', | ||||
|           }, | ||||
|           { | ||||
|             label: 'Base64 (base 64)', | ||||
|             value: 'Base64', | ||||
|           }, | ||||
|           { | ||||
|             label: 'Base64url (base 64 with url safe chars)', | ||||
|             value: 'Base64url', | ||||
|           }, | ||||
|         ]" | ||||
|       /> | ||||
|  | ||||
|       <div v-for="algo in algoNames" :key="algo" style="margin: 5px 0"> | ||||
|         <n-input-group> | ||||
|           <n-input-group-label style="flex: 0 0 120px"> | ||||
|             {{ algo }} | ||||
|           </n-input-group-label> | ||||
|           <InputCopyable :value="hashText(algo, clearText)" readonly /> | ||||
|         </n-input-group> | ||||
|       </div> | ||||
|     </c-card> | ||||
|   </div> | ||||
| </template> | ||||
|   | ||||
| @@ -1,6 +1,57 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <n-form-item label="Plain text to compute the hash"> | ||||
|       <n-input v-model:value="plainText" type="textarea" placeholder="Enter the text to compute the hash..." /> | ||||
|     </n-form-item> | ||||
|     <n-form-item label="Secret key"> | ||||
|       <n-input v-model:value="secret" placeholder="Enter the secret key..." /> | ||||
|     </n-form-item> | ||||
|     <n-space item-style="flex:1 1 0"> | ||||
|       <n-form-item label="Hashing function"> | ||||
|         <n-select | ||||
|           v-model:value="hashFunction" | ||||
|           placeholder="Select an hashing function..." | ||||
|           :options="Object.keys(algos).map((label) => ({ label, value: label }))" | ||||
|         /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Output encoding"> | ||||
|         <n-select | ||||
|           v-model:value="encoding" | ||||
|           placeholder="Select the result encoding..." | ||||
|           :options="[ | ||||
|             { | ||||
|               label: 'Binary (base 2)', | ||||
|               value: 'Bin', | ||||
|             }, | ||||
|             { | ||||
|               label: 'Hexadecimal (base 16)', | ||||
|               value: 'Hex', | ||||
|             }, | ||||
|             { | ||||
|               label: 'Base64 (base 64)', | ||||
|               value: 'Base64', | ||||
|             }, | ||||
|             { | ||||
|               label: 'Base64-url (base 64 with url safe chars)', | ||||
|               value: 'Base64url', | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </n-form-item> | ||||
|     </n-space> | ||||
|     <n-form-item label="HMAC of your text"> | ||||
|       <n-input readonly :value="hmac" type="textarea" placeholder="The result of the HMAC..." /> | ||||
|     </n-form-item> | ||||
|     <n-space justify="center"> | ||||
|       <c-button @click="copy()">Copy HMAC</c-button> | ||||
|     </n-space> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import type { lib } from 'crypto-js'; | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| import { | ||||
|   enc, | ||||
|   HmacMD5, | ||||
|   HmacRIPEMD160, | ||||
|   HmacSHA1, | ||||
| @@ -9,11 +60,10 @@ import { | ||||
|   HmacSHA3, | ||||
|   HmacSHA384, | ||||
|   HmacSHA512, | ||||
|   enc, | ||||
|   lib, | ||||
| } from 'crypto-js'; | ||||
|  | ||||
| import { computed, ref } from 'vue'; | ||||
| import { convertHexToBin } from '../hash-text/hash-text.service'; | ||||
| import { useCopy } from '@/composable/copy'; | ||||
|  | ||||
| const algos = { | ||||
|   MD5: HmacMD5, | ||||
| @@ -44,48 +94,3 @@ const hmac = computed(() => | ||||
| ); | ||||
| const { copy } = useCopy({ source: hmac }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div flex flex-col gap-4> | ||||
|     <c-input-text v-model:value="plainText" multiline raw-text placeholder="Plain text to compute the hash..." rows="3" autosize autofocus label="Plain text to compute the hash" /> | ||||
|     <c-input-text v-model:value="secret" raw-text placeholder="Enter the secret key..." label="Secret key" clearable /> | ||||
|  | ||||
|     <div flex gap-2> | ||||
|       <c-select | ||||
|         v-model:value="hashFunction" label="Hashing function" | ||||
|         flex-1 | ||||
|         placeholder="Select an hashing function..." | ||||
|         :options="Object.keys(algos).map((label) => ({ label, value: label }))" | ||||
|       /> | ||||
|       <c-select | ||||
|         v-model:value="encoding" label="Output encoding" | ||||
|         flex-1 | ||||
|         placeholder="Select the result encoding..." | ||||
|         :options="[ | ||||
|           { | ||||
|             label: 'Binary (base 2)', | ||||
|             value: 'Bin', | ||||
|           }, | ||||
|           { | ||||
|             label: 'Hexadecimal (base 16)', | ||||
|             value: 'Hex', | ||||
|           }, | ||||
|           { | ||||
|             label: 'Base64 (base 64)', | ||||
|             value: 'Base64', | ||||
|           }, | ||||
|           { | ||||
|             label: 'Base64-url (base 64 with url safe chars)', | ||||
|             value: 'Base64url', | ||||
|           }, | ||||
|         ]" | ||||
|       /> | ||||
|     </div> | ||||
|     <input-copyable v-model:value="hmac" type="textarea" placeholder="The result of the HMAC..." label="HMAC of your text" /> | ||||
|     <div flex justify-center> | ||||
|       <c-button @click="copy()"> | ||||
|         Copy HMAC | ||||
|       </c-button> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user