diff options
-rw-r--r-- | components.d.ts | 10 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | pnpm-lock.yaml | 29 | ||||
-rw-r--r-- | src/shims.d.ts | 6 | ||||
-rw-r--r-- | src/tools/iban-validator-and-parser/iban-validator-and-parser.vue | 6 | ||||
-rw-r--r-- | src/tools/index.ts | 3 | ||||
-rw-r--r-- | src/tools/pdf-signature-checker/components/pdf-signature-details.vue | 99 | ||||
-rw-r--r-- | src/tools/pdf-signature-checker/index.ts | 12 | ||||
-rw-r--r-- | src/tools/pdf-signature-checker/pdf-signature-checker.e2e.spec.ts | 11 | ||||
-rw-r--r-- | src/tools/pdf-signature-checker/pdf-signature-checker.types.ts | 39 | ||||
-rw-r--r-- | src/tools/pdf-signature-checker/pdf-signature-checker.vue | 59 | ||||
-rw-r--r-- | src/ui/c-collapse/c-collapse.demo.vue | 5 | ||||
-rw-r--r-- | src/ui/c-collapse/c-collapse.vue | 25 | ||||
-rw-r--r-- | src/ui/c-file-upload/c-file-upload.demo.vue | 3 | ||||
-rw-r--r-- | src/ui/c-file-upload/c-file-upload.vue | 95 | ||||
-rw-r--r-- | src/ui/c-key-value-list/c-key-value-list.vue | 8 | ||||
-rw-r--r-- | src/ui/c-modal-value/c-modal-value.demo.vue | 21 | ||||
-rw-r--r-- | src/ui/c-modal-value/c-modal-value.vue | 31 | ||||
-rw-r--r-- | src/ui/c-table/c-table.vue | 4 | ||||
-rw-r--r-- | unocss.config.ts | 2 |
20 files changed, 448 insertions, 21 deletions
diff --git a/components.d.ts b/components.d.ts index db37f84..575db45 100644 --- a/components.d.ts +++ b/components.d.ts @@ -29,7 +29,11 @@ declare module '@vue/runtime-core' { 'CButtonsSelect.demo': typeof import('./src/ui/c-buttons-select/c-buttons-select.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'] + CCollapse: typeof import('./src/ui/c-collapse/c-collapse.vue')['default'] + 'CCollapse.demo': typeof import('./src/ui/c-collapse/c-collapse.demo.vue')['default'] CDiffEditor: typeof import('./src/ui/c-diff-editor/c-diff-editor.vue')['default'] + CFileUpload: typeof import('./src/ui/c-file-upload/c-file-upload.vue')['default'] + 'CFileUpload.demo': typeof import('./src/ui/c-file-upload/c-file-upload.demo.vue')['default'] ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default'] Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default'] CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default'] @@ -41,6 +45,8 @@ declare module '@vue/runtime-core' { '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'] + CModalValue: typeof import('./src/ui/c-modal-value/c-modal-value.vue')['default'] + 'CModalValue.demo': typeof import('./src/ui/c-modal-value/c-modal-value.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'] @@ -83,6 +89,7 @@ declare module '@vue/runtime-core' { 'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default'] 'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] IconMdiArrowDown: typeof import('~icons/mdi/arrow-down')['default'] + IconMdiArrowRight: typeof import('~icons/mdi/arrow-right')['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'] @@ -100,6 +107,7 @@ declare module '@vue/runtime-core' { IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] IconMdiSearch: typeof import('~icons/mdi/search')['default'] IconMdiTranslate: typeof import('~icons/mdi/translate')['default'] + IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['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'] @@ -165,6 +173,8 @@ declare module '@vue/runtime-core' { 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'] + PdfSignatureChecker: typeof import('./src/tools/pdf-signature-checker/pdf-signature-checker.vue')['default'] + PdfSignatureDetails: typeof import('./src/tools/pdf-signature-checker/components/pdf-signature-details.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'] diff --git a/package.json b/package.json index 998b095..18bf7f5 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "netmask": "^2.0.2", "node-forge": "^1.3.1", "oui": "^12.0.52", + "pdf-signature-reader": "^1.4.2", "pinia": "^2.0.34", "plausible-tracker": "^0.3.8", "qrcode": "^1.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45773ab..0052775 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,6 +116,9 @@ dependencies: oui: specifier: ^12.0.52 version: 12.0.52 + pdf-signature-reader: + specifier: ^1.4.2 + version: 1.4.2 pinia: specifier: ^2.0.34 version: 2.0.34(typescript@5.2.2)(vue@3.3.4) @@ -3350,7 +3353,7 @@ packages: dependencies: '@unhead/dom': 0.5.1 '@unhead/schema': 0.5.1 - '@vueuse/shared': 10.5.0(vue@3.3.4) + '@vueuse/shared': 10.6.0(vue@3.3.4) unhead: 0.5.1 vue: 3.3.4 transitivePeerDependencies: @@ -3977,8 +3980,8 @@ packages: - vue dev: false - /@vueuse/shared@10.5.0(vue@3.3.4): - resolution: {integrity: sha512-18iyxbbHYLst9MqU1X1QNdMHIjks6wC7XTVf0KNOv5es/Ms6gjVFCAAWTVP2JStuGqydg3DT+ExpFORUEi9yhg==} + /@vueuse/shared@10.6.0(vue@3.3.4): + resolution: {integrity: sha512-0t4MVE18sO+/4Gh0jfeOXBTjKeV4606N9kIrDOLPjFl8Rwnlodn+QC5A4LfJuysK7aOsTMjF3KnzNeueaI0xlQ==} dependencies: vue-demi: 0.14.6(vue@3.3.4) transitivePeerDependencies: @@ -4206,7 +4209,6 @@ packages: /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: true /bcryptjs@2.4.3: resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} @@ -5758,10 +5760,6 @@ packages: requiresBuild: true optional: true - /function-bind@1.1.1: - resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - dev: true - /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} dev: true @@ -5989,7 +5987,7 @@ packages: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} dependencies: - function-bind: 1.1.1 + function-bind: 1.1.2 dev: true /hasown@2.0.0: @@ -6143,7 +6141,6 @@ packages: /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: true /ignore-walk@4.0.1: resolution: {integrity: sha512-rzDQLaW4jQbh2YrOFlJdCtX8qgJTehFRYiUB2r1osqTeDzV/3+Jh8fz1oAPzUThf3iku8Ds4IDqawI5d8mUiQw==} @@ -7127,7 +7124,7 @@ packages: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.4 + resolve: 1.22.8 semver: 5.7.2 validate-npm-package-license: 3.0.4 dev: true @@ -7425,6 +7422,14 @@ packages: through: 2.3.8 dev: true + /pdf-signature-reader@1.4.2: + resolution: {integrity: sha512-qQbmFv6nv4SQt2gmaalaREhHu3x2XyLG2+zL4Gl4D2TL2Zfii1EKxlFhJDsduP8s06t26snDoSwEAQtJOtprmQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + node-forge: 1.3.1 + dev: false + /perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} dev: true @@ -7918,7 +7923,7 @@ packages: resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} hasBin: true dependencies: - is-core-module: 2.13.0 + is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 dev: true diff --git a/src/shims.d.ts b/src/shims.d.ts index 147c01e..ef2e2f3 100644 --- a/src/shims.d.ts +++ b/src/shims.d.ts @@ -32,4 +32,10 @@ declare module 'unicode-emoji-json' { }>; export default emoji; +} + +declare module 'pdf-signature-reader' { + const verifySignature: (pdf: ArrayBuffer) => ({signatures: SignatureInfo[]}); + + export default verifySignature; }
\ No newline at end of file diff --git a/src/tools/iban-validator-and-parser/iban-validator-and-parser.vue b/src/tools/iban-validator-and-parser/iban-validator-and-parser.vue index 647be98..6844dc5 100644 --- a/src/tools/iban-validator-and-parser/iban-validator-and-parser.vue +++ b/src/tools/iban-validator-and-parser/iban-validator-and-parser.vue @@ -60,9 +60,11 @@ const ibanExamples = [ <div> <c-input-text v-model:value="rawIban" placeholder="Enter an IBAN to check for validity..." test-id="iban-input" /> - <c-key-value-list :items="ibanInfo" my-5 data-test-id="iban-info" /> + <c-card v-if="ibanInfo.length > 0" mt-5> + <c-key-value-list :items="ibanInfo" data-test-id="iban-info" /> + </c-card> - <c-card title="Valid IBAN examples"> + <c-card title="Valid IBAN examples" mt-5> <div v-for="iban in ibanExamples" :key="iban"> <c-text-copyable :value="iban" font-mono :displayed-value="friendlyFormatIBAN(iban)" /> </div> diff --git a/src/tools/index.ts b/src/tools/index.ts index fa63d3d..52bdf8e 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,6 +1,7 @@ import { tool as base64FileConverter } from './base64-file-converter'; import { tool as base64StringConverter } from './base64-string-converter'; import { tool as basicAuthGenerator } from './basic-auth-generator'; +import { tool as pdfSignatureChecker } from './pdf-signature-checker'; import { tool as numeronymGenerator } from './numeronym-generator'; import { tool as macAddressGenerator } from './mac-address-generator'; import { tool as textToBinary } from './text-to-binary'; @@ -78,7 +79,7 @@ import { tool as xmlFormatter } from './xml-formatter'; export const toolsByCategory: ToolCategory[] = [ { name: 'Crypto', - components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser], + components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser, pdfSignatureChecker], }, { name: 'Converter', diff --git a/src/tools/pdf-signature-checker/components/pdf-signature-details.vue b/src/tools/pdf-signature-checker/components/pdf-signature-details.vue new file mode 100644 index 0000000..c39f616 --- /dev/null +++ b/src/tools/pdf-signature-checker/components/pdf-signature-details.vue @@ -0,0 +1,99 @@ +<script setup lang="ts"> +import type { SignatureInfo } from '../pdf-signature-checker.types'; + +const props = defineProps<{ signature: SignatureInfo }>(); +const { signature } = toRefs(props); + +const tableHeaders = { + validityPeriod: 'Validity period', + issuedBy: 'Issued by', + issuedTo: 'Issued to', + pemCertificate: 'PEM certificate', +}; + +const certs = computed(() => signature.value.meta.certs.map((certificate, index) => ({ + ...certificate, + validityPeriod: { + notBefore: new Date(certificate.validityPeriod.notBefore).toLocaleString(), + notAfter: new Date(certificate.validityPeriod.notAfter).toLocaleString(), + }, + certificateName: `Certificate ${index + 1}`, +})), +); +</script> + +<template> + <div flex flex-col gap-2> + <c-table :data="certs" :headers="tableHeaders"> + <template #validityPeriod="{ value }"> + <c-key-value-list + :items="[{ + label: 'Not before', + value: value.notBefore, + }, { + label: 'Not after', + value: value.notAfter, + }]" + /> + </template> + + <template #issuedBy="{ value }"> + <c-key-value-list + :items="[{ + label: 'Common name', + value: value.commonName, + }, { + label: 'Organization name', + value: value.organizationName, + }, { + label: 'Country name', + value: value.countryName, + }, { + label: 'Locality name', + value: value.localityName, + }, { + label: 'Organizational unit name', + value: value.organizationalUnitName, + }, { + label: 'State or province name', + value: value.stateOrProvinceName, + }]" + /> + </template> + + <template #issuedTo="{ value }"> + <c-key-value-list + :items="[{ + label: 'Common name', + value: value.commonName, + }, { + label: 'Organization name', + value: value.organizationName, + }, { + label: 'Country name', + value: value.countryName, + }, { + label: 'Locality name', + value: value.localityName, + }, { + label: 'Organizational unit name', + value: value.organizationalUnitName, + }, { + label: 'State or province name', + value: value.stateOrProvinceName, + }]" + /> + </template> + + <template #pemCertificate="{ value }"> + <c-modal-value :value="value" label="View PEM cert"> + <template #value> + <div break-all text-xs> + {{ value }} + </div> + </template> + </c-modal-value> + </template> + </c-table> + </div> +</template> diff --git a/src/tools/pdf-signature-checker/index.ts b/src/tools/pdf-signature-checker/index.ts new file mode 100644 index 0000000..5456397 --- /dev/null +++ b/src/tools/pdf-signature-checker/index.ts @@ -0,0 +1,12 @@ +import { defineTool } from '../tool'; +import FileCertIcon from '~icons/mdi/file-certificate-outline'; + +export const tool = defineTool({ + name: 'PDF signature checker', + path: '/pdf-signature-checker', + description: 'Verify the signatures of a PDF file. A signed PDF file contains one or more signatures that may be used to determine whether the contents of the file have been altered since the file was signed.', + keywords: ['pdf', 'signature', 'checker', 'verify', 'validate', 'sign'], + component: () => import('./pdf-signature-checker.vue'), + icon: FileCertIcon, + createdAt: new Date('2023-12-09'), +}); diff --git a/src/tools/pdf-signature-checker/pdf-signature-checker.e2e.spec.ts b/src/tools/pdf-signature-checker/pdf-signature-checker.e2e.spec.ts new file mode 100644 index 0000000..8e9a287 --- /dev/null +++ b/src/tools/pdf-signature-checker/pdf-signature-checker.e2e.spec.ts @@ -0,0 +1,11 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Tool - Pdf signature checker', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/pdf-signature-checker'); + }); + + test('Has correct title', async ({ page }) => { + await expect(page).toHaveTitle('PDF signature checker - IT Tools'); + }); +}); diff --git a/src/tools/pdf-signature-checker/pdf-signature-checker.types.ts b/src/tools/pdf-signature-checker/pdf-signature-checker.types.ts new file mode 100644 index 0000000..6932509 --- /dev/null +++ b/src/tools/pdf-signature-checker/pdf-signature-checker.types.ts @@ -0,0 +1,39 @@ +export interface SignatureInfo { + verified: boolean + authenticity: boolean + integrity: boolean + expired: boolean + meta: { + certs: { + clientCertificate?: boolean + issuedBy: { + commonName: string + organizationalUnitName?: string + organizationName: string + countryName?: string + localityName?: string + stateOrProvinceName?: string + } + issuedTo: { + commonName: string + serialNumber?: string + organizationalUnitName?: string + organizationName: string + countryName?: string + localityName?: string + stateOrProvinceName?: string + } + validityPeriod: { + notBefore: string + notAfter: string + } + pemCertificate: string + }[] + signatureMeta: { + reason: string + contactInfo: string | null + location: string + name: string | null + } + } +} diff --git a/src/tools/pdf-signature-checker/pdf-signature-checker.vue b/src/tools/pdf-signature-checker/pdf-signature-checker.vue new file mode 100644 index 0000000..0220616 --- /dev/null +++ b/src/tools/pdf-signature-checker/pdf-signature-checker.vue @@ -0,0 +1,59 @@ +<script setup lang="ts"> +import verifyPDF from 'pdf-signature-reader'; +import type { SignatureInfo } from './pdf-signature-checker.types'; +import { formatBytes } from '@/utils/convert'; + +const signatures = ref<SignatureInfo[]>([]); +const status = ref<'idle' | 'parsed' | 'error' | 'loading'>('idle'); +const file = ref<File | null>(null); + +async function onVerifyClicked(uploadedFile: File) { + file.value = uploadedFile; + const fileBuffer = await uploadedFile.arrayBuffer(); + + status.value = 'loading'; + try { + const { signatures: parsedSignatures } = verifyPDF(fileBuffer); + signatures.value = parsedSignatures; + status.value = 'parsed'; + } + catch (e) { + signatures.value = []; + status.value = 'error'; + } +} +</script> + +<template> + <div style="flex: 0 0 100%"> + <div mx-auto max-w-600px> + <c-file-upload title="Drag and drop a PDF file here, or click to select a file" accept=".pdf" @file-upload="onVerifyClicked" /> + + <c-card v-if="file" mt-4 flex gap-2> + <div font-bold> + {{ file.name }} + </div> + + <div> + {{ formatBytes(file.size) }} + </div> + </c-card> + + <div v-if="status === 'error'"> + <c-alert mt-4> + No signatures found in the provided file. + </c-alert> + </div> + </div> + </div> + + <div v-if="status === 'parsed' && signatures.length" style="flex: 0 0 100%" mt-5 flex flex-col gap-4> + <div v-for="(signature, index) of signatures" :key="index"> + <div mb-2 font-bold> + Signature {{ index + 1 }} certificates : + </div> + + <pdf-signature-details :signature="signature" /> + </div> + </div> +</template> diff --git a/src/ui/c-collapse/c-collapse.demo.vue b/src/ui/c-collapse/c-collapse.demo.vue new file mode 100644 index 0000000..760d0ec --- /dev/null +++ b/src/ui/c-collapse/c-collapse.demo.vue @@ -0,0 +1,5 @@ +<template> + <c-collapse title="Collapse title"> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquet iaculis class cubilia metus per nullam gravida ad venenatis. Id elementum elementum enim orci elementum justo facilisi habitant consequat. Justo eget ligula purus laoreet penatibus eros quisque fusce sociis. In eget amet sagittis dignissim eleifend proin lacinia potenti tellus. Interdum vulputate condimentum molestie pulvinar praesent accumsan quisque venenatis imperdiet. + </c-collapse> +</template> diff --git a/src/ui/c-collapse/c-collapse.vue b/src/ui/c-collapse/c-collapse.vue new file mode 100644 index 0000000..ccd7c1e --- /dev/null +++ b/src/ui/c-collapse/c-collapse.vue @@ -0,0 +1,25 @@ +<script lang="ts" setup> +const props = withDefaults(defineProps<{ title?: string }>(), { title: '' }); +const { title } = toRefs(props); + +const isCollapsed = ref(true); +</script> + +<template> + <div> + <div flex cursor-pointer items-center @click="isCollapsed = !isCollapsed"> + <icon-mdi-triangle-down :class="{ 'transform-rotate--90': isCollapsed }" op-50 transition /> + + <slot name="title"> + <span class="ml-2" font-bold>{{ title }}</span> + </slot> + </div> + + <div + v-show="!isCollapsed" + mt-2 + > + <slot /> + </div> + </div> +</template> diff --git a/src/ui/c-file-upload/c-file-upload.demo.vue b/src/ui/c-file-upload/c-file-upload.demo.vue new file mode 100644 index 0000000..eedcbd6 --- /dev/null +++ b/src/ui/c-file-upload/c-file-upload.demo.vue @@ -0,0 +1,3 @@ +<template> + <c-file-upload /> +</template> diff --git a/src/ui/c-file-upload/c-file-upload.vue b/src/ui/c-file-upload/c-file-upload.vue new file mode 100644 index 0000000..b48d8c3 --- /dev/null +++ b/src/ui/c-file-upload/c-file-upload.vue @@ -0,0 +1,95 @@ +<script lang="ts" setup> +import _ from 'lodash'; + +const props = withDefaults(defineProps<{ + multiple?: boolean + accept?: string + title?: string +}>(), { + multiple: false, + accept: undefined, + title: 'Drag and drop files here, or click to select files', +}); + +const emit = defineEmits<{ + (event: 'filesUpload', files: File[]): void + (event: 'fileUpload', file: File): void +}>(); + +const { multiple } = toRefs(props); + +const isOverDropZone = ref(false); + +const fileInput = ref<HTMLInputElement | null>(null); + +function triggerFileInput() { + fileInput.value?.click(); +} + +function handleFileInput(event: Event) { + const files = (event.target as HTMLInputElement).files; + + handleUpload(files); +} + +function handleDrop(event: DragEvent) { + event.preventDefault(); + const files = event.dataTransfer?.files; + + handleUpload(files); +} + +function handleUpload(files: FileList | null | undefined) { + if (_.isNil(files) || _.isEmpty(files)) { + return; + } + + if (multiple.value) { + emit('filesUpload', Array.from(files)); + return; + } + + emit('fileUpload', files[0]); +} +</script> + +<template> + <div + class="flex flex-col cursor-pointer items-center justify-center border-2px border-gray-300 border-opacity-50 rounded-lg border-dashed p-8 transition-colors" + :class="{ + 'border-primary border-opacity-100': isOverDropZone, + }" + @click="triggerFileInput" + @drop.prevent="handleDrop" + @dragover.prevent + @dragenter="isOverDropZone = true" + @dragleave="isOverDropZone = false" + > + <input + ref="fileInput" + type="file" + class="hidden" + :multiple="multiple" + :accept="accept" + @change="handleFileInput" + > + <slot> + <span op-70> + {{ title }} + </span> + + <!-- separator --> + <div my-4 w-full flex items-center justify-center op-70> + <div class="h-1px max-w-100px flex-1 bg-gray-300 op-50" /> + <div class="mx-2 text-gray-400"> + or + </div> + <div class="h-1px max-w-100px flex-1 bg-gray-300 op-50" /> + </div> + + <c-button> + Browse files + </c-button> + </slot> + </div> +</template> diff --git a/src/ui/c-key-value-list/c-key-value-list.vue b/src/ui/c-key-value-list/c-key-value-list.vue index d8a2b00..d863bd5 100644 --- a/src/ui/c-key-value-list/c-key-value-list.vue +++ b/src/ui/c-key-value-list/c-key-value-list.vue @@ -9,13 +9,13 @@ const formattedItems = computed(() => items.value.filter(item => !_.isNil(item.v </script> <template> - <div my-5> - <div v-for="item in formattedItems" :key="item.label" flex gap-2 py-1 class="c-key-value-list__item"> - <div flex-basis-180px text-right font-bold class="c-key-value-list__key"> + <div flex flex-col gap-2> + <div v-for="item in formattedItems" :key="item.label" class="c-key-value-list__item"> + <div class="c-key-value-list__key" text-13px lh-normal> {{ item.label }} </div> - <c-key-value-list-item :item="item" class="c-key-value-list__value" /> + <c-key-value-list-item :item="item" class="c-key-value-list__value" font-bold lh-normal /> </div> </div> </template> diff --git a/src/ui/c-modal-value/c-modal-value.demo.vue b/src/ui/c-modal-value/c-modal-value.demo.vue new file mode 100644 index 0000000..6ebff8c --- /dev/null +++ b/src/ui/c-modal-value/c-modal-value.demo.vue @@ -0,0 +1,21 @@ +<template> + <div flex gap-2> + <c-modal-value value="lorem ipsum" label="test" /> + <c-modal-value> + <template #label="{ toggleModal }"> + <c-button class="text-left" size="small" @click="toggleModal"> + Bonjour + </c-button> + </template> + + <template #value> + <pre> + Lorem ipsum dolor sit amet consectetur adipisicing elit. + Molestias, quisquam vitae saepe dolores quas debitis ab r + ecusandae suscipit ex dignissimos minus quam repellat sunt. + Molestiae culpa blanditiis totam sapiente dignissimos. + </pre> + </template> + </c-modal-value> + </div> +</template> diff --git a/src/ui/c-modal-value/c-modal-value.vue b/src/ui/c-modal-value/c-modal-value.vue new file mode 100644 index 0000000..3b388e0 --- /dev/null +++ b/src/ui/c-modal-value/c-modal-value.vue @@ -0,0 +1,31 @@ +<script lang="ts" setup> +import { useCopy } from '@/composable/copy'; + +const props = withDefaults(defineProps<{ value: string; label?: string; copyable?: boolean }>(), { label: undefined, copyable: true }); +const { value, label } = toRefs(props); + +const { copy, isJustCopied } = useCopy({ source: value }); + +const isModalOpen = ref(false); +const toggleModal = useToggle(isModalOpen); +</script> + +<template> + <slot name="label" :value="value" :toggle-modal="toggleModal" :is-modal-open="isModalOpen"> + <c-button class="text-left" @click="isModalOpen = true"> + {{ label }} + </c-button> + </slot> + + <c-modal v-model:open="isModalOpen"> + <slot name="value" :value="value" :toggle-modal="toggleModal" :is-modal-open="isModalOpen"> + {{ value }} + </slot> + + <div mt-4 flex justify-center> + <c-button class="w-full" @click="copy"> + {{ isJustCopied ? 'Copied!' : 'Copy' }} + </c-button> + </div> + </c-modal> +</template> diff --git a/src/ui/c-table/c-table.vue b/src/ui/c-table/c-table.vue index 9354cc9..ef56989 100644 --- a/src/ui/c-table/c-table.vue +++ b/src/ui/c-table/c-table.vue @@ -39,7 +39,7 @@ const headers = computed(() => { <template> <div class="relative overflow-x-auto rounded"> <table class="w-full border-collapse text-left text-sm text-gray-500 dark:text-gray-400" role="table" :aria-label="description"> - <thead v-if="!hideHeaders" class="bg-#ffffff uppercase text-gray-700 dark:bg-#333333 dark:text-gray-400"> + <thead v-if="!hideHeaders" class="bg-#ffffff uppercase text-gray-700 dark:bg-#333333 dark:text-gray-400" border-b="1px solid dark:transparent #efeff5"> <tr> <th v-for="header in headers" :key="header.key" scope="col" class="px-6 py-3 text-xs"> {{ header.label }} @@ -54,7 +54,7 @@ const headers = computed(() => { }" > <td v-for="header in headers" :key="header.key" class="px-6 py-4"> - <slot :name="header" :row="row" :headers="headers" :value="row[header.key]"> + <slot :name="header.key" :row="row" :headers="headers" :value="row[header.key]"> {{ row[header.key] }} </slot> </td> diff --git a/unocss.config.ts b/unocss.config.ts index 04c0483..e115c09 100644 --- a/unocss.config.ts +++ b/unocss.config.ts @@ -15,11 +15,13 @@ export default defineConfig({ theme: { colors: { primary: '#1ea54c', + }, }, shortcuts: { 'pretty-scrollbar': 'scrollbar scrollbar-rounded scrollbar-thumb-color-gray-300 scrollbar-track-color-gray-100 dark:scrollbar-thumb-color-#424242 dark:scrollbar-track-color-#686868', 'divider': 'h-1px bg-current op-10', 'bg-surface': 'bg-#ffffff dark:bg-#232323', + 'bg-background': 'bg-#f1f5f9 dark:bg-#1c1c1c', }, }); |