From 187f7b3a6a62b63b9e2a8fc00909a570fc47e55f Mon Sep 17 00:00:00 2001 From: rehlert Date: Fri, 12 Jun 2026 11:58:22 +0200 Subject: [PATCH 01/17] zips are getting extracted --- .idea/workspace.xml | 18 +++++-- package.json | 3 +- pnpm-lock.yaml | 8 ++++ src/lib/zip-processing.ts | 99 +++++++++++++++++++++++++++++++++++++++ src/routes/+page.svelte | 52 ++++++++++++++++---- 5 files changed, 167 insertions(+), 13 deletions(-) create mode 100644 src/lib/zip-processing.ts diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 04658f8..91597f3 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,7 +2,8 @@ - + + @@ -70,7 +79,8 @@ diff --git a/package.json b/package.json index ed546a6..57060a1 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "@skeletonlabs/skeleton": "^4.15.2", - "@skeletonlabs/skeleton-svelte": "^4.15.2" + "@skeletonlabs/skeleton-svelte": "^4.15.2", + "fflate": "^0.8.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32f3ff5..7eeef8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@skeletonlabs/skeleton-svelte': specifier: ^4.15.2 version: 4.15.2(svelte@5.56.3) + fflate: + specifier: ^0.8.3 + version: 0.8.3 devDependencies: '@sveltejs/adapter-auto': specifier: ^7.0.1 @@ -594,6 +597,9 @@ packages: picomatch: optional: true + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1570,6 +1576,8 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fflate@0.8.3: {} + fsevents@2.3.3: optional: true diff --git a/src/lib/zip-processing.ts b/src/lib/zip-processing.ts new file mode 100644 index 0000000..fadcdff --- /dev/null +++ b/src/lib/zip-processing.ts @@ -0,0 +1,99 @@ +import { unzip } from "fflate"; + +export type ZipAttachmentKind = "pdf" | "image"; + +export type ZipAttachment = { + name: string; + path: string; + kind: ZipAttachmentKind; + data: Uint8Array; +}; + +export type ProcessedZipArchive = { + name: string; + attachments: ZipAttachment[]; + xjustizNachrichtXml: Uint8Array | null; +}; + +const IMAGE_EXTENSIONS = new Set([ + ".avif", + ".bmp", + ".gif", + ".heic", + ".jpeg", + ".jpg", + ".jp2", + ".png", + ".tif", + ".tiff", + ".webp", +]); + +const ZIP_META_FILE_NAME = "xjustiz_nachricht.xml"; + +const unzipArchive = (data: Uint8Array) => + new Promise>((resolve, reject) => { + unzip(data, (error, files) => { + if (error) { + reject(error); + return; + } + + resolve(files); + }); + }); + +const getBaseName = (path: string) => path.split("/").pop() ?? path; + +const getExtension = (path: string) => { + const baseName = getBaseName(path).toLowerCase(); + const lastDotIndex = baseName.lastIndexOf("."); + + return lastDotIndex === -1 ? "" : baseName.slice(lastDotIndex); +}; + +const isPdf = (path: string) => getExtension(path) === ".pdf"; + +const isImage = (path: string) => IMAGE_EXTENSIONS.has(getExtension(path)); + +export const extractZipArchive = async (file: File): Promise => { + const archiveBytes = new Uint8Array(await file.arrayBuffer()); + const files = await unzipArchive(archiveBytes); + + const attachments: ZipAttachment[] = []; + let xjustizNachrichtXml: Uint8Array | null = null; + + for (const [path, data] of Object.entries(files)) { + const baseName = getBaseName(path); + + if (baseName.toLowerCase() === ZIP_META_FILE_NAME) { + xjustizNachrichtXml = data; + continue; + } + + if (isPdf(path)) { + attachments.push({ + name: baseName, + path, + kind: "pdf", + data, + }); + continue; + } + + if (isImage(path)) { + attachments.push({ + name: baseName, + path, + kind: "image", + data, + }); + } + } + + return { + name: file.name, + attachments, + xjustizNachrichtXml, + }; +}; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 8db6d11..4a7adbe 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,11 +1,32 @@ @@ -15,16 +36,31 @@ {:else}
-

ZIP-Dateien geladen

+

ZIP-Dateien werden verarbeitet

-
    - {#each selectedZipFiles as file} -
  • - {file.name} -
  • - {/each} -
+
+
+ {#if pendingZipCount > 0} + Noch {pendingZipCount} ZIP-Datei{pendingZipCount === 1 ? "" : "en"} in Bearbeitung. + {:else} + Alle ZIP-Dateien wurden verarbeitet. + {/if} +
+ + {#if processedZipFiles.length > 0} +
    + {#each processedZipFiles as file} +
  • + {file.name} - {file.attachments.length} relevante Datei{file.attachments.length === 1 ? "" : "en"} + {#if file.xjustizNachrichtXml} + - xjustiz_nachricht.xml vorhanden + {/if} +
  • + {/each} +
+ {/if} +
{/if} {/snippet} From 15a3fc5877d261964e6a7acb611803dd571e2f84 Mon Sep 17 00:00:00 2001 From: rehlert Date: Fri, 12 Jun 2026 12:17:13 +0200 Subject: [PATCH 02/17] parses xjustiz and orders attachments --- .idea/workspace.xml | 21 ++++-- src/lib/zip-processing.ts | 135 ++++++++++++++++++++++++++++++++++++-- src/routes/+page.svelte | 4 -- 3 files changed, 144 insertions(+), 16 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 91597f3..6fb4981 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,8 +2,8 @@ - - + + @@ -80,7 +88,8 @@ diff --git a/src/lib/zip-processing.ts b/src/lib/zip-processing.ts index fadcdff..a8d8f80 100644 --- a/src/lib/zip-processing.ts +++ b/src/lib/zip-processing.ts @@ -10,9 +10,8 @@ export type ZipAttachment = { }; export type ProcessedZipArchive = { - name: string; - attachments: ZipAttachment[]; - xjustizNachrichtXml: Uint8Array | null; + name: string + attachments: ZipAttachment[] }; const IMAGE_EXTENSIONS = new Set([ @@ -30,6 +29,7 @@ const IMAGE_EXTENSIONS = new Set([ ]); const ZIP_META_FILE_NAME = "xjustiz_nachricht.xml"; +const XML_TEXT_DECODER = new TextDecoder(); const unzipArchive = (data: Uint8Array) => new Promise>((resolve, reject) => { @@ -56,6 +56,126 @@ const isPdf = (path: string) => getExtension(path) === ".pdf"; const isImage = (path: string) => IMAGE_EXTENSIONS.has(getExtension(path)); +const normalizeKey = (value: string) => value.replaceAll("\\", "/").toLowerCase().trim(); + +const matchesElementName = (element: Element, requestedName: string) => { + const requestedLocalName = requestedName.split(".").pop() ?? requestedName; + + return ( + element.localName === requestedName || + element.localName === requestedLocalName || + element.tagName === requestedName || + element.tagName === requestedLocalName || + element.tagName.endsWith(`:${requestedLocalName}`) + ); +}; + +const getDirectChildByLocalName = (element: Element, localName: string) => + Array.from(element.children).find((child) => matchesElementName(child, localName)) ?? null; + +const getFirstDescendantByLocalName = (root: Document | Element, localName: string) => + Array.from(root.querySelectorAll("*")).find((element) => matchesElementName(element, localName)) ?? null; + +const getTextContentByPath = (root: Element, path: string[]) => { + let current: Element | null = root; + + for (const localName of path) { + current = current ? getDirectChildByLocalName(current, localName) : null; + + if (!current) { + return null; + } + } + + const textContent = current.textContent?.trim(); + + return textContent ? textContent : null; +}; + +const parseXmlDocument = (xmlBytes: Uint8Array) => { + const parser = new DOMParser(); + const document = parser.parseFromString(XML_TEXT_DECODER.decode(xmlBytes), "application/xml"); + + if (document.querySelector("parsererror")) { + return null; + } + + return document; +}; + +const getXJustizDocumentNames = (xmlBytes: Uint8Array) => { + const document = parseXmlDocument(xmlBytes); + + if (!document) { + return []; + } + + const schriftgutobjekte = getFirstDescendantByLocalName(document, "schriftgutobjekte"); + + if (!schriftgutobjekte) { + return []; + } + + const dokumentNodes = Array.from(schriftgutobjekte.children).filter((child) => + matchesElementName(child, "dokument"), + ); + + return dokumentNodes + .map((dokument) => getTextContentByPath(dokument, ["xjustiz.fachspezifischeDaten", "datei", "dateiname"])) + .filter((name): name is string => Boolean(name)); +}; + +const orderAttachmentsByDocumentNames = (attachments: ZipAttachment[], documentNames: string[]) => { + const buckets = new Map(); + + for (const attachment of attachments) { + const keys = new Set([normalizeKey(attachment.path), normalizeKey(attachment.name)]); + + for (const key of keys) { + const bucket = buckets.get(key) ?? []; + bucket.push(attachment); + buckets.set(key, bucket); + } + } + + const usedAttachments = new Set(); + const orderedAttachments: ZipAttachment[] = []; + + const takeAttachment = (key: string) => { + const bucket = buckets.get(key); + + if (!bucket) { + return null; + } + + const nextAttachment = bucket.find((attachment) => !usedAttachments.has(attachment)) ?? null; + + if (nextAttachment) { + usedAttachments.add(nextAttachment); + } + + return nextAttachment; + }; + + for (const documentName of documentNames) { + const normalizedName = normalizeKey(documentName); + const normalizedBaseName = normalizeKey(getBaseName(documentName)); + const matchedAttachment = takeAttachment(normalizedName) ?? takeAttachment(normalizedBaseName); + + if (matchedAttachment) { + orderedAttachments.push(matchedAttachment); + } + } + + for (const attachment of attachments) { + if (!usedAttachments.has(attachment)) { + orderedAttachments.push(attachment); + } + } + + return orderedAttachments; +}; + export const extractZipArchive = async (file: File): Promise => { const archiveBytes = new Uint8Array(await file.arrayBuffer()); const files = await unzipArchive(archiveBytes); @@ -91,9 +211,12 @@ export const extractZipArchive = async (file: File): Promise {file.name} - {file.attachments.length} relevante Datei{file.attachments.length === 1 ? "" : "en"} - {#if file.xjustizNachrichtXml} - - xjustiz_nachricht.xml vorhanden - {/if} {/each} From d00a7e2783a5db5692c8bbaac7e7b9c14a4acbcc Mon Sep 17 00:00:00 2001 From: rehlert Date: Fri, 12 Jun 2026 12:31:44 +0200 Subject: [PATCH 03/17] renders preview of zips --- .idea/workspace.xml | 18 ++- package.json | 3 +- pnpm-lock.yaml | 134 +++++++++++++++++ src/lib/components/AttachmentPreview.svelte | 89 ++++++++++++ .../ProcessedZipArchiveEditor.svelte | 136 ++++++++++++++++++ src/routes/+page.svelte | 20 ++- 6 files changed, 389 insertions(+), 11 deletions(-) create mode 100644 src/lib/components/AttachmentPreview.svelte create mode 100644 src/lib/components/ProcessedZipArchiveEditor.svelte diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 6fb4981..b672b2e 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -3,7 +3,8 @@ - + + @@ -89,7 +98,8 @@ - diff --git a/package.json b/package.json index 57060a1..58a5eec 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "dependencies": { "@skeletonlabs/skeleton": "^4.15.2", "@skeletonlabs/skeleton-svelte": "^4.15.2", - "fflate": "^0.8.3" + "fflate": "^0.8.3", + "pdfjs-dist": "^6.0.227" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7eeef8e..f9df5cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: fflate: specifier: ^0.8.3 version: 0.8.3 + pdfjs-dist: + specifier: ^6.0.227 + version: 6.0.227 devDependencies: '@sveltejs/adapter-auto': specifier: ^7.0.1 @@ -100,6 +103,81 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@napi-rs/canvas-android-arm64@1.0.0': + resolution: {integrity: sha512-3hNKJObUK7JsCF9aJlVCs1J0/KE/gGfZNeK8MO1ge6bB3aicr5walGme9t9No1f/oyk9GgvdAT/rjSdsx3gbIw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/canvas-darwin-arm64@1.0.0': + resolution: {integrity: sha512-ZIja19/BiGz2puhki+WUYSRriwFeFJ8Mi9eK3hZdSS85w4Y60cuEAJVhMCfKwswQkKkUtrnzdKMBuO7TupvexA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/canvas-darwin-x64@1.0.0': + resolution: {integrity: sha512-hImggWc82jqZVpEsFR9S7PE9OQYjq/H/D7vwCGB6X1jRH+UVBP1+1niJTPBOat1B154T6GKK7/kcFtoWgjgFzQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/canvas-linux-arm-gnueabihf@1.0.0': + resolution: {integrity: sha512-hlJRy6d+kWLKVOG/+1rEvNQVURZ0DxxRPJsLmEWwhwiXZUJc0BF5o9esALHSEP4CoJK4wChRtj3hnyBgVx2oWA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/canvas-linux-arm64-gnu@1.0.0': + resolution: {integrity: sha512-5Hru4T3RXkosRQafcjelv7AUzw9mXqmGYsxnzeDDOWveFCJyEPMSJltvGCM+jfH98seOCbfwm9KyFg6Jm5FhAA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-arm64-musl@1.0.0': + resolution: {integrity: sha512-LTUl9jS8WsLSUGaxQZKQkxfluOJRpgvBuxxdM4pYcjib+di8AU4OzQc6+L6SzGMLcKc9H0RAjojRatBhTMqYdg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@napi-rs/canvas-linux-riscv64-gnu@1.0.0': + resolution: {integrity: sha512-Iz931SAZf+WVDzpjk52Q3ffW3zw0YflFwEZMgs036Wfu1kX/LrwT9wGjsuSqyduqefUkl91/vTdAjn8hQu5ezA==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-x64-gnu@1.0.0': + resolution: {integrity: sha512-pFEQ5eFK4JusgN1K6KkO9DKP/Hi1WMJOkF8Ch03/khTc4bFbCKkCCsJG4YcOMOW9bI4XbT2/eMAWxhO0xaWgPA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/canvas-linux-x64-musl@1.0.0': + resolution: {integrity: sha512-jnvr8NrLHiZ3NCiOKWqDbkI4Ah+QDrqtZ+sddPZBltEb1mQ2coSvCSJYfict+oAwcm0c970oTmVySpjKP/lnaA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@napi-rs/canvas-win32-arm64-msvc@1.0.0': + resolution: {integrity: sha512-y2j9/Gfd5joqiqxdP/L1smqjQ+uAx3C4N0EC7bDHrnZEEH8ToM/OC5p3uHvtj4Lq591aHj+ArL01UDLNwT5HgQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/canvas-win32-x64-msvc@1.0.0': + resolution: {integrity: sha512-qwdhh9N6Gge/hC4pL9S1tQp0iKwhSl/dYjg7+RGp9k26iRGRi5MqqUyKGOXIWli0zOcuy5Y2wIH/jk2ry6i/jA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/canvas@1.0.0': + resolution: {integrity: sha512-Jqxcy1XOIqj+lH9sl1GT+il6GR3uQv13vI2mrwubP3uT8Olak2ClDrK2RnxlQKjwv8BRr4b3ug0YR7c6hBX8wg==} + engines: {node: '>= 10'} + '@napi-rs/wasm-runtime@1.1.5': resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} peerDependencies: @@ -720,6 +798,10 @@ packages: resolution: {integrity: sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==} engines: {node: '>=12.20.0'} + pdfjs-dist@6.0.227: + resolution: {integrity: sha512-/P6M4SXw+70waMVLUM7rdRtvo+dEzqE1t6W/zQNvBETo2MaRa5rrvCcAYdfWGiUzadTgM0lJmRApUrW0d9zgKg==} + engines: {node: '>=22.13.0 || >=24'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -973,6 +1055,54 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@napi-rs/canvas-android-arm64@1.0.0': + optional: true + + '@napi-rs/canvas-darwin-arm64@1.0.0': + optional: true + + '@napi-rs/canvas-darwin-x64@1.0.0': + optional: true + + '@napi-rs/canvas-linux-arm-gnueabihf@1.0.0': + optional: true + + '@napi-rs/canvas-linux-arm64-gnu@1.0.0': + optional: true + + '@napi-rs/canvas-linux-arm64-musl@1.0.0': + optional: true + + '@napi-rs/canvas-linux-riscv64-gnu@1.0.0': + optional: true + + '@napi-rs/canvas-linux-x64-gnu@1.0.0': + optional: true + + '@napi-rs/canvas-linux-x64-musl@1.0.0': + optional: true + + '@napi-rs/canvas-win32-arm64-msvc@1.0.0': + optional: true + + '@napi-rs/canvas-win32-x64-msvc@1.0.0': + optional: true + + '@napi-rs/canvas@1.0.0': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 1.0.0 + '@napi-rs/canvas-darwin-arm64': 1.0.0 + '@napi-rs/canvas-darwin-x64': 1.0.0 + '@napi-rs/canvas-linux-arm-gnueabihf': 1.0.0 + '@napi-rs/canvas-linux-arm64-gnu': 1.0.0 + '@napi-rs/canvas-linux-arm64-musl': 1.0.0 + '@napi-rs/canvas-linux-riscv64-gnu': 1.0.0 + '@napi-rs/canvas-linux-x64-gnu': 1.0.0 + '@napi-rs/canvas-linux-x64-musl': 1.0.0 + '@napi-rs/canvas-win32-arm64-msvc': 1.0.0 + '@napi-rs/canvas-win32-x64-msvc': 1.0.0 + optional: true + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -1656,6 +1786,10 @@ snapshots: obug@2.1.2: {} + pdfjs-dist@6.0.227: + optionalDependencies: + '@napi-rs/canvas': 1.0.0 + picocolors@1.1.1: {} picomatch@4.0.4: {} diff --git a/src/lib/components/AttachmentPreview.svelte b/src/lib/components/AttachmentPreview.svelte new file mode 100644 index 0000000..e8ddec7 --- /dev/null +++ b/src/lib/components/AttachmentPreview.svelte @@ -0,0 +1,89 @@ + + +{#if attachment.kind === "image"} + {#if imageUrl} + {attachment.name} + {/if} +{:else if previewError} +
+ {previewError} +
+{:else} + +{/if} diff --git a/src/lib/components/ProcessedZipArchiveEditor.svelte b/src/lib/components/ProcessedZipArchiveEditor.svelte new file mode 100644 index 0000000..922b193 --- /dev/null +++ b/src/lib/components/ProcessedZipArchiveEditor.svelte @@ -0,0 +1,136 @@ + + +
+
+
+

{archive.name}

+

+ {attachments.length} relevante Datei{attachments.length === 1 ? "" : "en"} +

+
+
+ +
    + {#each attachments as attachment, index (attachment.path)} +
  • handleDragStart(index, event)} + ondragover={(event) => handleDragOver(index, event)} + ondrop={(event) => handleDrop(index, event)} + ondragend={handleDragEnd} + > +
    + + + +
    + + +
  • + {/each} +
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 1ae991d..b2557ea 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,5 +1,6 @@ {#snippet content()} @@ -48,13 +55,14 @@ {#if processedZipFiles.length > 0} -
    - {#each processedZipFiles as file} -
  • - {file.name} - {file.attachments.length} relevante Datei{file.attachments.length === 1 ? "" : "en"} -
  • +
    + {#each processedZipFiles as file, index} + updateProcessedZipFile(index, updatedArchive)} + /> {/each} -
+ {/if} From 0cbbc6f46d277df526dc037c98ec4f068a61a9f3 Mon Sep 17 00:00:00 2001 From: rehlert Date: Fri, 12 Jun 2026 12:59:46 +0200 Subject: [PATCH 04/17] design anpassungen --- .idea/workspace.xml | 19 +++-- src/lib/components/AttachmentPreview.svelte | 62 ++++++++------- .../ProcessedZipArchiveEditor.svelte | 79 ++++++++++--------- src/routes/+page.svelte | 2 +- 4 files changed, 89 insertions(+), 73 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index b672b2e..7fce527 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -3,8 +3,8 @@ - - + + @@ -99,7 +107,8 @@ - diff --git a/src/lib/components/AttachmentPreview.svelte b/src/lib/components/AttachmentPreview.svelte index e8ddec7..f9eb124 100644 --- a/src/lib/components/AttachmentPreview.svelte +++ b/src/lib/components/AttachmentPreview.svelte @@ -1,13 +1,14 @@ -{#if attachment.kind === "image"} - {#if imageUrl} - + {#if attachment.kind === "image"} + {#if imageUrl} + {attachment.name} + /> + {/if} + {:else if previewError} +
+ {previewError} +
+ {:else} + {/if} -{:else if previewError} -
- {previewError} -
-{:else} - -{/if} + diff --git a/src/lib/components/ProcessedZipArchiveEditor.svelte b/src/lib/components/ProcessedZipArchiveEditor.svelte index 922b193..d8d4241 100644 --- a/src/lib/components/ProcessedZipArchiveEditor.svelte +++ b/src/lib/components/ProcessedZipArchiveEditor.svelte @@ -1,35 +1,35 @@
- {#if attachment.kind === "image"} - {#if imageUrl} - {attachment.name} - {/if} - {:else if previewError} + {#if previewError}
{previewError}
+ {:else if imageUrl} + {attachment.name} {:else} - +
+ Vorschau wird erstellt +
{/if}
diff --git a/src/lib/components/ProcessedZipArchiveEditor.svelte b/src/lib/components/ProcessedZipArchiveEditor.svelte index d8d4241..f7fe174 100644 --- a/src/lib/components/ProcessedZipArchiveEditor.svelte +++ b/src/lib/components/ProcessedZipArchiveEditor.svelte @@ -4,11 +4,14 @@ type Props = { archive: ProcessedZipArchive; + thumbnailWidth: number; onArchiveChange?: (archive: ProcessedZipArchive) => void; }; let { - archive, onArchiveChange = () => { + archive, + thumbnailWidth = 200, + onArchiveChange = () => { } }: Props = $props(); @@ -112,6 +115,7 @@ class={`flex flex-col shrink-0 items-stretch gap-2 rounded-xl border bg-primary-50 p-2 ${ dropIndex === index ? "border-primary-400 ring-1 ring-primary-200" : "border-primary-100" } ${dragIndex === index ? "opacity-60" : ""}`} + style={`width: ${thumbnailWidth}px;`} draggable="true" ondragstart={(event) => handleDragStart(index, event)} ondragover={(event) => handleDragOver(index, event)} @@ -128,13 +132,14 @@ :: -
+
{attachment.name}
-
- -
+ {/each} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index e5d21e7..9e52527 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,12 +1,41 @@ {#snippet content()} {#if selectedZipFiles.length === 0} {:else} +
+ + Settings + ⚙️ + + +
+
+
+ + + +
+ + +
+
+
{#if pendingZipCount > 0} @@ -96,33 +187,6 @@

beA-Edit

- -
- - Settings - ⚙️ - - -
- -
-
-
From 9785973e8bcff2f6673818dbe874fdf85f36b452 Mon Sep 17 00:00:00 2001 From: rehlert Date: Tue, 16 Jun 2026 08:11:01 +0200 Subject: [PATCH 13/17] removes skeleton --- package.json | 2 - pnpm-lock.yaml | 581 +----------------------------------------- src/routes/layout.css | 3 - 3 files changed, 2 insertions(+), 584 deletions(-) diff --git a/package.json b/package.json index d40f0a7..73440b7 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,6 @@ "vite": "^8.0.16" }, "dependencies": { - "@skeletonlabs/skeleton": "^4.15.2", - "@skeletonlabs/skeleton-svelte": "^4.15.2", "fflate": "^0.8.3", "pdf-lib": "^1.17.1", "pdfjs-dist": "^6.0.227" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dcafeaa..f2d5523 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,6 @@ importers: .: dependencies: - '@skeletonlabs/skeleton': - specifier: ^4.15.2 - version: 4.15.2(tailwindcss@4.3.0) - '@skeletonlabs/skeleton-svelte': - specifier: ^4.15.2 - version: 4.15.2(svelte@5.56.3) fflate: specifier: ^0.8.3 version: 0.8.3 @@ -78,18 +72,6 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@floating-ui/core@1.7.5': - resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} - - '@floating-ui/dom@1.7.6': - resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} - - '@floating-ui/utils@0.2.11': - resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - - '@internationalized/date@3.12.0': - resolution: {integrity: sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==} - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -297,19 +279,6 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - '@skeletonlabs/skeleton-common@4.15.2': - resolution: {integrity: sha512-y7KZn++Av8UHdoeaaguQ7zIS1HlK7Y5jyPDnHy65wJ60iS97fMtQV0kGhqVoYCWhor+xrjbAFjWtaPOPPDEMYA==} - - '@skeletonlabs/skeleton-svelte@4.15.2': - resolution: {integrity: sha512-vZkRhR701EOHVXVKh/NFRSY8D9LPBvmdNFdtfgqO7TEg77sypJUvERl2dxErTLY1QjVklVpjsHRCTaSLn+1Ntg==} - peerDependencies: - svelte: ^5.29.0 - - '@skeletonlabs/skeleton@4.15.2': - resolution: {integrity: sha512-5O23Py76nw56aoieV2b2T7MJ6xS0DwDjUULpwLnCXxXOnmzADEoWoQxb/ABbqg04IMOygtRzK3HO22I1+kFsog==} - peerDependencies: - tailwindcss: ^4.0.0 - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -350,9 +319,6 @@ packages: svelte: ^5.46.4 vite: ^8.0.0-beta.7 || ^8.0.0 - '@swc/helpers@0.5.23': - resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} - '@tailwindcss/forms@0.5.11': resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==} peerDependencies: @@ -469,153 +435,6 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@zag-js/accordion@1.39.1': - resolution: {integrity: sha512-GA3m7gRTm3weSe1eMlHIsTNztcjZ6joIaRgxxKil7q/UX0xIVVGDy0aCr6oo7FAuoMiOOBVurYXILpFZ30nOXA==} - - '@zag-js/anatomy@1.39.1': - resolution: {integrity: sha512-p2iFAs2pVQgv5iCDAftA7g9Z/fUYXW94dRIGk415TSbkp/YDENydm/JtRoNctp302UIx4Eeuc5QBR+7h5kuISA==} - - '@zag-js/aria-hidden@1.39.1': - resolution: {integrity: sha512-wiwcz3N086qBMEU3VKfHhcvGm6Jm1PIcDXys/jEqiKPtHoYZhDip0n0cPOoasss/A1oS39QFVdk3WpLXGu3Izw==} - - '@zag-js/auto-resize@1.39.1': - resolution: {integrity: sha512-ditIo9mW7fapq+4yx3/8hMpMZlWaoOy66EOzUz8dSVqnxnTWAjnTICu/9zFh8pkWerlzGTtDOJPP1oZ8S/rgVg==} - - '@zag-js/avatar@1.39.1': - resolution: {integrity: sha512-LWrgJ0bebnXPSL+uehA9z6BlCD/MZEOQBJqH/F2QQFSAAZXUUDKtzVDmc+UtwjDsHXqqTghi+v2atQJHNMcJ2g==} - - '@zag-js/carousel@1.39.1': - resolution: {integrity: sha512-5z5z3IldUgZ/R+KZLNQDoJFNTXzYd28YOmgfWH61Vvyv+RarX8kwZW8ajW/fNiqcWXyhW3/VMU0lArrfjbQVtQ==} - - '@zag-js/collapsible@1.39.1': - resolution: {integrity: sha512-Zgccg/t7M8i0JVwZPPgW7XB7kGhTO475hsmwkF/8CYLqBBckVDHUARp2we24hENCm/98eez6R0eDEmE+tldFWA==} - - '@zag-js/collection@1.39.1': - resolution: {integrity: sha512-fyOyKmP7MRo0/U8mBmB7KgHRXHhXP27LCcasy3x+qTAQtuEfYG1EPhKuj07oBWlX/2qfcKYn2R3YopHcqFcCiA==} - - '@zag-js/combobox@1.39.1': - resolution: {integrity: sha512-fmStpG+k4xrxCzqUX0ssnOMeoSietWm5ir3qmEZcagzNqNycAXMvOELAIeyXi87Kut6aDGhxLOV7o395HVXl/w==} - - '@zag-js/core@1.39.1': - resolution: {integrity: sha512-Yp0r49QLYXe2j7fgyAiilH4umXFydCnr5hcRDwJU+sxvUAlq00JQIJIEK2pT6k8cJiNNsFEV5WkOX7jsqpAX2A==} - - '@zag-js/date-picker@1.39.1': - resolution: {integrity: sha512-t9q1H0aZQJkbzKTR2Bn5vMwaoFoirxekiSxw8ju0F0vr4Kg4BJ9yueOQm5I2wALKnJbZu4Ua5MgzlrDF3CQt3A==} - peerDependencies: - '@internationalized/date': '>=3.0.0' - - '@zag-js/date-utils@1.39.1': - resolution: {integrity: sha512-i4SvBhru2Yz/zsHT0XvyFhf4a+pAKYkWXeVfU0RvF2S6mPTfgaMFF9ZNPq5Sy8K31EtAa6AVXcybYaYnibn1FA==} - peerDependencies: - '@internationalized/date': '>=3.0.0' - - '@zag-js/dialog@1.39.1': - resolution: {integrity: sha512-q+HTmfuRDRZthln9mb7i52wdltQOZlw3+nw3a2uygEe9xuEtHBwUz31XJzkn2UWQqhAt7cC39OwykhNLKrfkqA==} - - '@zag-js/dismissable@1.39.1': - resolution: {integrity: sha512-7/soy93Ersd5qedhSL/+CDcZ9gNTQV0ooDcqKtM8b4IxwD4rgWwGsewJY+tbKmOqaZobwa0YcWV2+YGgI23ESw==} - - '@zag-js/dom-query@1.39.1': - resolution: {integrity: sha512-k01aXeUWLyJfB61CODaXj4PLhYmVpnVMFrC+3nk/XCn1MW7my8L/8KVg0m4W8n+X9MhpaLWsZDmK/dwED/3qSw==} - - '@zag-js/file-upload@1.39.1': - resolution: {integrity: sha512-cErPOnPwPyneUXpelsfm75DKn0/4SI8aqQnlbrqo522PEqAQyDfDdBsqebGgKWG3F0A++kKFp9LO9A5zCrw5gA==} - - '@zag-js/file-utils@1.39.1': - resolution: {integrity: sha512-ll/W5o74SMmoAS+l7PkmmGjPj4PLCSG/cwQh1Y/+LpaSev0YiR3Nk2OzRIIPtm3NivYVxKGawaCOf1RvT/82LQ==} - - '@zag-js/floating-panel@1.39.1': - resolution: {integrity: sha512-IfPbf3pwJGqBWHec/rPzpdPjfMCLed59LlEophvRy49FEdksv8eN6nr9DXl2wWZEoQhH99scXfLMbtEZsPsFWg==} - - '@zag-js/focus-trap@1.39.1': - resolution: {integrity: sha512-2ZzVefHMotvtxUo/gP4R45Szw/EPaPkTKEHaug6/il62SPDbkFODF+5r1zXyLbLuwCHq0apvQasg/ONLihwlXw==} - - '@zag-js/focus-visible@1.39.1': - resolution: {integrity: sha512-iEuTOYHE8HRn/7ULC9c9BTTWo0C0MJRCbYVxbh/d7v8qAuq4CS76pdfceNo3KeWbb968T+yiG6q0AjiHsr8IOw==} - - '@zag-js/i18n-utils@1.39.1': - resolution: {integrity: sha512-TKRLQQlHgJ4cxsHo3tZPtbFjGu9m1UPtfezRGFKq7A8czhdqRhaCpaWF849cd6dI7x6rWvvTan858gOFpyANnQ==} - - '@zag-js/interact-outside@1.39.1': - resolution: {integrity: sha512-LnSbA+txMsFmzNPn84QKH01x2yJv4At/eKHn6rT2PyxXkJQIh8PvCTS3zVz4Syw11cmhcXt2eRwhzx8yImV92w==} - - '@zag-js/listbox@1.39.1': - resolution: {integrity: sha512-Mz0UpdXobdTQTyjM+Avgi7pDVB2dKyaUHqw3TloeleQL3VwTqClclkwHXtLYYE+oXa0zOet37wI9mzfaYx9iZQ==} - - '@zag-js/live-region@1.39.1': - resolution: {integrity: sha512-E7YNd0QGzJ2n1ZhnI2smv+klwifsNRf9QaDCx7quVJCVYywpupsBK4R25KN75S1z8XaK+jAy6HYKj8DIhYjYeg==} - - '@zag-js/menu@1.39.1': - resolution: {integrity: sha512-bRDGLGkiGhzNtORBXkbBQV/xp2zEkwpYIepfWCaUoFwKUmx7GGnShTBFxJyq0u2D4IkS9GOwcqm20EhMv6V+TA==} - - '@zag-js/pagination@1.39.1': - resolution: {integrity: sha512-3Q1B9/g3ajhvXjuGffJ7otyXcXK5+uhdbE5A9CZa4bsW3pf25L9Cp+ZAjdXQMDc8T4jhZJAKFmDJfQgtr1oEIw==} - - '@zag-js/popover@1.39.1': - resolution: {integrity: sha512-aO3ExO/O7Sa3ovdozFI6SujhNOpYdCca4bImnAiovDL8DY8zN3UNQebu35IQvw9/aRsx9VKSJL1AqzJJUImFRw==} - - '@zag-js/popper@1.39.1': - resolution: {integrity: sha512-h0UMY2dXJNfM3OvMQ9t9LzlmwvpCgjloz2IvU1txY3r32UIy7ve1H70zkKagLtLRxFTuWmhumYUPULPo/6a1DA==} - - '@zag-js/progress@1.39.1': - resolution: {integrity: sha512-1IHyOw8DqPs3YH149Oj7W9a5oEfY5pc9GAVOPGbzYxVK/W8d/NIjVxa565I3J5cDJ0s6z3FrMSXMWUwr1ML4tw==} - - '@zag-js/radio-group@1.39.1': - resolution: {integrity: sha512-+sC9xcAyY/GbY+8HpKlbPgSyOxBLUSB18s6fe6K1wdmyom4PM0nmhLouuxisbFZYHOyfQwAOMo+ainRENB2hzQ==} - - '@zag-js/rating-group@1.39.1': - resolution: {integrity: sha512-IfdxWmM+3zpztx/HcE3bWob72sZNb1+BzK4tSySLVyjeqs8OzLDzrCbKqt10DmibnNOvpbjbq4eX4P5hV9YN7Q==} - - '@zag-js/rect-utils@1.39.1': - resolution: {integrity: sha512-5gJ0PzeUme76xTWG+4XythWgmGgDKV4XAxEUaB3KKDtXgjDHwtu7PwKLIzFtlaaSf/U23PY+RNVBVCYg1GmZog==} - - '@zag-js/remove-scroll@1.39.1': - resolution: {integrity: sha512-uZfPR3Gl9sQFo+tJ7kbuwsBhw+RIZwWFnMDgrz5LIwSNGN6hsyC4HGOxe29clkWQ2X2AjqqmEMETwgX7Jg+wxA==} - - '@zag-js/scroll-snap@1.39.1': - resolution: {integrity: sha512-AzCc8MAAVqkiK5Y0cJZ24OIBZDQrUmEexACMuR6M5yZmlcEbS0EA/d6Wq+LSR1JMVTD4B+UwcMj1D3vJQ90ZTw==} - - '@zag-js/slider@1.39.1': - resolution: {integrity: sha512-OEA9R7Ly5cw+6ANofnMpuHH3rAo8gZEnxy7iEwePu11pq2RCnt8DSj2V+uqU+dTq15Uup1LSzRgJfTnAC4Z85A==} - - '@zag-js/steps@1.39.1': - resolution: {integrity: sha512-DC6swMpwITTB0DyCSxlpWyPNSUN9ul9jz4N6aAyQ0L1IK/noF/YYTZRAcXNSRzN4iutO/2mFGGbwGq/oVf+gPA==} - - '@zag-js/store@1.39.1': - resolution: {integrity: sha512-zFpwP4lhiBVD9987rwAfZNVa2/f/xx4mhbCE1EEw31zxLAozY2jONeJ3UzPP05VbzKlRHBcvkaXAJQQGegTwFA==} - - '@zag-js/svelte@1.39.1': - resolution: {integrity: sha512-ZOyZjyvjePZdrkNTy5fa92ijeeID9e+3LRGziKGIII3JSEvwfkG/Buf8W84N8VHFxi0G0GcKmgVCDgKHyGQYoQ==} - peerDependencies: - svelte: '>=5' - - '@zag-js/switch@1.39.1': - resolution: {integrity: sha512-ikeQ42c0vyyPLeyW9U0dvcqTV1Ekpx5jZ050R905HGJ2GeWE0uBGuHbMpTG5U6Pwb0a+TMzqAr+jMsquVTCwzg==} - - '@zag-js/tabs@1.39.1': - resolution: {integrity: sha512-P2RThO1gX9SFsNqrAGPsXJxrjn5YqP6MFs9mdExU+tzzZyVjJQADkAmh98C0eEaCb6HKLpJZ/17hrnLDhm1Tig==} - - '@zag-js/tags-input@1.39.1': - resolution: {integrity: sha512-tc0+bd9FiUJwa+wY2hSVVGHLIBC3C3rOZX/4zjchRMs1xgl92c1/tYbytXny7ABB8ZMHveG7MtgDppVF4VkwBg==} - - '@zag-js/toast@1.39.1': - resolution: {integrity: sha512-K7ndEfBTKDds10iQKCQUmin74s6V4BEIypAIyQxs18gQB9TCn5+wff886JAzecIKPY97PDQHDKjYR71yzRC7/g==} - - '@zag-js/toggle-group@1.39.1': - resolution: {integrity: sha512-KS4Bo17foMKXVBhQjocRf4GQxMV4pMXclTo14IWjldaHs2HIrNJ0Ar0Ri+vo47BBKBNsXs4HuNvfbMdQj94wEA==} - - '@zag-js/tooltip@1.39.1': - resolution: {integrity: sha512-IsxFj7l8kPciwIyYJWlmQ7mhXocbjXxLj3m9z099slYOF7lApA33/ndY32w9ptrI4/nUh2nldzw6eRfSpVnuOA==} - - '@zag-js/tree-view@1.39.1': - resolution: {integrity: sha512-sm6qUZjO0OaqBqO5s55KU+l5p1wXfUVScoen7BYVoFBuROH7qAZJi8YMclGvnnlyV506i8Hk0qqWnLg0F38jCA==} - - '@zag-js/types@1.39.1': - resolution: {integrity: sha512-w3vVpgxmdJvMDvv19DXTtFI6kJL6TXw//U0Z1BAc3rnDA9orcB9Ryw4uMNvIzFA607CgssyJcWDaQ/M3yAcbJw==} - - '@zag-js/utils@1.39.1': - resolution: {integrity: sha512-9k741cH7L655Ua3tedTkuMblcXVXVgCLTB9svp9oTjA7oatpOpYF4z43kgAQVjyThNXMJ7AvtO4C80ajQLTScg==} - acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -646,9 +465,6 @@ packages: engines: {node: '>=4'} hasBin: true - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -899,9 +715,6 @@ packages: engines: {node: '>=14'} hasBin: true - proxy-compare@3.0.1: - resolution: {integrity: sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==} - readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -1039,21 +852,6 @@ snapshots: tslib: 2.8.1 optional: true - '@floating-ui/core@1.7.5': - dependencies: - '@floating-ui/utils': 0.2.11 - - '@floating-ui/dom@1.7.6': - dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/utils': 0.2.11 - - '@floating-ui/utils@0.2.11': {} - - '@internationalized/date@3.12.0': - dependencies: - '@swc/helpers': 0.5.23 - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1191,45 +989,6 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} - '@skeletonlabs/skeleton-common@4.15.2': {} - - '@skeletonlabs/skeleton-svelte@4.15.2(svelte@5.56.3)': - dependencies: - '@internationalized/date': 3.12.0 - '@skeletonlabs/skeleton-common': 4.15.2 - '@zag-js/accordion': 1.39.1 - '@zag-js/avatar': 1.39.1 - '@zag-js/carousel': 1.39.1 - '@zag-js/collapsible': 1.39.1 - '@zag-js/collection': 1.39.1 - '@zag-js/combobox': 1.39.1 - '@zag-js/date-picker': 1.39.1(@internationalized/date@3.12.0) - '@zag-js/dialog': 1.39.1 - '@zag-js/file-upload': 1.39.1 - '@zag-js/floating-panel': 1.39.1 - '@zag-js/listbox': 1.39.1 - '@zag-js/menu': 1.39.1 - '@zag-js/pagination': 1.39.1 - '@zag-js/popover': 1.39.1 - '@zag-js/progress': 1.39.1 - '@zag-js/radio-group': 1.39.1 - '@zag-js/rating-group': 1.39.1 - '@zag-js/slider': 1.39.1 - '@zag-js/steps': 1.39.1 - '@zag-js/svelte': 1.39.1(svelte@5.56.3) - '@zag-js/switch': 1.39.1 - '@zag-js/tabs': 1.39.1 - '@zag-js/tags-input': 1.39.1 - '@zag-js/toast': 1.39.1 - '@zag-js/toggle-group': 1.39.1 - '@zag-js/tooltip': 1.39.1 - '@zag-js/tree-view': 1.39.1 - svelte: 5.56.3 - - '@skeletonlabs/skeleton@4.15.2(tailwindcss@4.3.0)': - dependencies: - tailwindcss: 4.3.0 - '@standard-schema/spec@1.1.0': {} '@sveltejs/acorn-typescript@1.0.10(acorn@8.16.0)': @@ -1271,10 +1030,6 @@ snapshots: vite: 8.0.16(jiti@2.7.0) vitefu: 1.1.3(vite@8.0.16(jiti@2.7.0)) - '@swc/helpers@0.5.23': - dependencies: - tslib: 2.8.1 - '@tailwindcss/forms@0.5.11(tailwindcss@4.3.0)': dependencies: mini-svg-data-uri: 1.4.4 @@ -1364,335 +1119,6 @@ snapshots: '@types/trusted-types@2.0.7': {} - '@zag-js/accordion@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/anatomy@1.39.1': {} - - '@zag-js/aria-hidden@1.39.1': - dependencies: - '@zag-js/dom-query': 1.39.1 - - '@zag-js/auto-resize@1.39.1': - dependencies: - '@zag-js/dom-query': 1.39.1 - - '@zag-js/avatar@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/carousel@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/scroll-snap': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/collapsible@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/collection@1.39.1': - dependencies: - '@zag-js/utils': 1.39.1 - - '@zag-js/combobox@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/collection': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dismissable': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/focus-visible': 1.39.1 - '@zag-js/live-region': 1.39.1 - '@zag-js/popper': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/core@1.39.1': - dependencies: - '@zag-js/dom-query': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/date-picker@1.39.1(@internationalized/date@3.12.0)': - dependencies: - '@internationalized/date': 3.12.0 - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/date-utils': 1.39.1(@internationalized/date@3.12.0) - '@zag-js/dismissable': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/live-region': 1.39.1 - '@zag-js/popper': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/date-utils@1.39.1(@internationalized/date@3.12.0)': - dependencies: - '@internationalized/date': 3.12.0 - - '@zag-js/dialog@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/aria-hidden': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dismissable': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/focus-trap': 1.39.1 - '@zag-js/remove-scroll': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/dismissable@1.39.1': - dependencies: - '@zag-js/dom-query': 1.39.1 - '@zag-js/interact-outside': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/dom-query@1.39.1': - dependencies: - '@zag-js/types': 1.39.1 - - '@zag-js/file-upload@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/file-utils': 1.39.1 - '@zag-js/i18n-utils': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/file-utils@1.39.1': - dependencies: - '@zag-js/i18n-utils': 1.39.1 - - '@zag-js/floating-panel@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/popper': 1.39.1 - '@zag-js/rect-utils': 1.39.1 - '@zag-js/store': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/focus-trap@1.39.1': - dependencies: - '@zag-js/dom-query': 1.39.1 - - '@zag-js/focus-visible@1.39.1': - dependencies: - '@zag-js/dom-query': 1.39.1 - - '@zag-js/i18n-utils@1.39.1': - dependencies: - '@zag-js/dom-query': 1.39.1 - - '@zag-js/interact-outside@1.39.1': - dependencies: - '@zag-js/dom-query': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/listbox@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/collection': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/focus-visible': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/live-region@1.39.1': {} - - '@zag-js/menu@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dismissable': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/focus-visible': 1.39.1 - '@zag-js/popper': 1.39.1 - '@zag-js/rect-utils': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/pagination@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/popover@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/aria-hidden': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dismissable': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/focus-trap': 1.39.1 - '@zag-js/popper': 1.39.1 - '@zag-js/remove-scroll': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/popper@1.39.1': - dependencies: - '@floating-ui/dom': 1.7.6 - '@zag-js/dom-query': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/progress@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/radio-group@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/focus-visible': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/rating-group@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/rect-utils@1.39.1': {} - - '@zag-js/remove-scroll@1.39.1': - dependencies: - '@zag-js/dom-query': 1.39.1 - - '@zag-js/scroll-snap@1.39.1': - dependencies: - '@zag-js/dom-query': 1.39.1 - - '@zag-js/slider@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/steps@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/store@1.39.1': - dependencies: - proxy-compare: 3.0.1 - - '@zag-js/svelte@1.39.1(svelte@5.56.3)': - dependencies: - '@zag-js/core': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - svelte: 5.56.3 - - '@zag-js/switch@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/focus-visible': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/tabs@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/tags-input@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/auto-resize': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/interact-outside': 1.39.1 - '@zag-js/live-region': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/toast@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dismissable': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/toggle-group@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/tooltip@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/focus-visible': 1.39.1 - '@zag-js/popper': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/tree-view@1.39.1': - dependencies: - '@zag-js/anatomy': 1.39.1 - '@zag-js/collection': 1.39.1 - '@zag-js/core': 1.39.1 - '@zag-js/dom-query': 1.39.1 - '@zag-js/types': 1.39.1 - '@zag-js/utils': 1.39.1 - - '@zag-js/types@1.39.1': - dependencies: - csstype: 3.2.3 - - '@zag-js/utils@1.39.1': {} - acorn@8.16.0: {} aria-query@5.3.1: {} @@ -1709,8 +1135,6 @@ snapshots: cssesc@3.0.0: {} - csstype@3.2.3: {} - deepmerge@4.3.1: {} detect-libc@2.1.2: {} @@ -1853,8 +1277,6 @@ snapshots: prettier@3.8.4: {} - proxy-compare@3.0.1: {} - readdirp@4.1.2: {} rolldown@1.0.3: @@ -1939,7 +1361,8 @@ snapshots: tslib@1.14.1: {} - tslib@2.8.1: {} + tslib@2.8.1: + optional: true typescript@6.0.3: {} diff --git a/src/routes/layout.css b/src/routes/layout.css index 8c267aa..562f021 100644 --- a/src/routes/layout.css +++ b/src/routes/layout.css @@ -1,7 +1,4 @@ @import 'tailwindcss'; -@import '@skeletonlabs/skeleton/themes/cerberus'; -@import '@skeletonlabs/skeleton'; -@import '@skeletonlabs/skeleton-svelte'; @import '../lib/nom-theme.css'; @plugin '@tailwindcss/forms'; From 0b032b2cd88168594f730bbe1832bb4b08add719 Mon Sep 17 00:00:00 2001 From: rehlert Date: Tue, 16 Jun 2026 18:07:58 +0200 Subject: [PATCH 14/17] adds shadcn, removes skeleton, implements button after styleguide --- .idea/ShadcnHelperSettings.xml | 2615 +++++++++++++++++ .idea/workspace.xml | 60 +- components.json | 20 + package.json | 9 + pnpm-lock.yaml | 218 +- src/lib/actions/ripple.ts | 31 + src/lib/components/Button.svelte | 38 - src/lib/components/Card.svelte | 23 - .../ProcessedZipArchiveEditor.svelte | 346 +-- src/lib/components/ZipDropzone.svelte | 7 +- src/lib/components/ui/button.svelte | 78 + src/lib/components/ui/card/card-action.svelte | 23 + .../components/ui/card/card-content.svelte | 20 + .../ui/card/card-description.svelte | 20 + src/lib/components/ui/card/card-footer.svelte | 20 + src/lib/components/ui/card/card-header.svelte | 23 + src/lib/components/ui/card/card-title.svelte | 20 + src/lib/components/ui/card/card.svelte | 22 + src/lib/components/ui/card/index.ts | 25 + src/lib/nom-theme.css | 108 - src/lib/utils.ts | 13 + src/routes/+page.svelte | 69 +- src/routes/layout.css | 162 +- 23 files changed, 3573 insertions(+), 397 deletions(-) create mode 100644 .idea/ShadcnHelperSettings.xml create mode 100644 components.json create mode 100644 src/lib/actions/ripple.ts delete mode 100644 src/lib/components/Button.svelte delete mode 100644 src/lib/components/Card.svelte create mode 100644 src/lib/components/ui/button.svelte create mode 100644 src/lib/components/ui/card/card-action.svelte create mode 100644 src/lib/components/ui/card/card-content.svelte create mode 100644 src/lib/components/ui/card/card-description.svelte create mode 100644 src/lib/components/ui/card/card-footer.svelte create mode 100644 src/lib/components/ui/card/card-header.svelte create mode 100644 src/lib/components/ui/card/card-title.svelte create mode 100644 src/lib/components/ui/card/card.svelte create mode 100644 src/lib/components/ui/card/index.ts delete mode 100644 src/lib/nom-theme.css create mode 100644 src/lib/utils.ts diff --git a/.idea/ShadcnHelperSettings.xml b/.idea/ShadcnHelperSettings.xml new file mode 100644 index 0000000..38aa1f2 --- /dev/null +++ b/.idea/ShadcnHelperSettings.xml @@ -0,0 +1,2615 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 2019e4c..bbf37cc 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -3,14 +3,30 @@ - + + + + + + + + + + + + @@ -20,29 +36,33 @@ "fromUser": false } + + + - { + "keyToString": { + "ModuleVcsDetector.initialDetectionPerformed": "true", + "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", + "RunOnceActivity.cidr.known.project.marker": "true", + "RunOnceActivity.git.unshallow": "true", + "RunOnceActivity.readMode.enableVisualFormatting": "true", + "RunOnceActivity.typescript.service.memoryLimit.init": "true", + "cidr.known.project.marker": "true", + "codeWithMe.voiceChat.enabledByDefault": "false", + "git-widget-placeholder": "features/shadcn", + "last_opened_file_path": "/home/rehlert/repos/juri-merger", + "list.type.of.created.stylesheet": "CSS", + "nodejs_package_manager_path": "pnpm", + "settings.editor.selected.configurable": "preferences.pluginManager", + "ts.external.directory.path": "/home/rehlert/repos/juri-merger/node_modules/typescript/lib", + "vue.rearranger.settings.migration": "true" } -}]]> +} @@ -55,6 +75,8 @@ + +
- +
+ + + {#if hasSubDocuments} +
+ {#each [{ value: 'single', label: 'Eine PDF' }, { value: 'separate', label: 'Getrennt' }] as option (option.value)} + + {/each} +
+ {/if} + + +
{#if mergeError} @@ -167,38 +400,103 @@
{/if} -
    - {#each attachments as attachment, index (attachment.path)} -
  • handleDragStart(index, event)} - ondragover={(event) => handleDragOver(index, event)} - ondrop={(event) => handleDrop(index, event)} - ondragend={handleDragEnd} - > -
    - + {#if selectionMode && selectedPaths.size > 0} +
    + {selectedPaths.size} Datei{selectedPaths.size === 1 ? '' : 'en'} ausgewählt + +
    + {/if} -
    - {attachment.name} +
      + {#each looseAttachments as attachment, index (attachment.path)} +
    • handleLooseDragStart(index, event)} + ondragover={(event) => handleLooseDragOver(index, event)} + ondrop={(event) => handleLooseDrop(index, event)} + ondragend={handleLooseDragEnd} + > + {#if selectionMode} + + {:else} +
      + + +
      + {attachment.name} +
      -
    - + + {/if}
  • {/each}
+ + {#if looseAttachments.length === 0 && subDocuments.length === 0} +
+ Keine Dateien in diesem Archiv. Lade ein anderes ZIP, um weiterzuarbeiten. +
+ {/if} + + {#if hasSubDocuments} +
+ {#each subDocuments as subDocument, blockIndex (subDocument.id)} + dissolveSubDocumentBlock(subDocument.id)} + onAttachmentMove={handleAttachmentMove} + onBlockDragStart={handleBlockDragStart} + onBlockDragOver={handleBlockDragOver} + onBlockDrop={handleBlockDrop} + onBlockDragEnd={handleBlockDragEnd} + /> + {/each} +
+ {/if} diff --git a/src/lib/components/SubDocumentEditor.svelte b/src/lib/components/SubDocumentEditor.svelte new file mode 100644 index 0000000..81164fe --- /dev/null +++ b/src/lib/components/SubDocumentEditor.svelte @@ -0,0 +1,286 @@ + + +
+
+ + + + + + {attachments.length} Datei{attachments.length === 1 ? '' : 'en'} + + + +
+ + {#if attachments.length === 0} +
    { + if (event.dataTransfer?.types.includes(ATTACHMENT_MIME)) { + event.preventDefault(); + } + }} + ondrop={(event) => { + const source = parseAttachmentPayload(event); + + if (!source) { + return; + } + + event.preventDefault(); + + const target: AttachmentLocation = { + kind: 'subDocument', + id: subDocument.id, + index: 0 + }; + + onAttachmentMove(source, target); + dragIndex = null; + dropIndex = null; + }} + > +
  • + Dieses Teildokument ist leer. Ziehe Dateien hierher, um sie hinzuzufügen. +
  • +
+ {:else} +
    + {#each attachments as attachment, index (attachment.path)} +
  • handleCardDragStart(index, event)} + ondragover={(event) => handleCardDragOver(index, event)} + ondrop={(event) => handleCardDrop(index, event)} + ondragend={handleCardDragEnd} + > +
    + + +
    + {attachment.name} +
    +
    + +
  • + {/each} +
+ {/if} +
diff --git a/src/lib/zip-processing.ts b/src/lib/zip-processing.ts index a0f6645..2137660 100644 --- a/src/lib/zip-processing.ts +++ b/src/lib/zip-processing.ts @@ -12,11 +12,33 @@ export type ZipAttachment = { data: Uint8Array; }; -export type ProcessedZipArchive = { +export type SubDocument = { + id: string; name: string; attachments: ZipAttachment[]; }; +export type ProcessedZipArchive = { + name: string; + attachments: ZipAttachment[]; + subDocuments: SubDocument[]; +}; + +export type ExportMode = 'single' | 'separate'; + +export type ExportUnit = { + name: string; + bytes: Uint8Array; +}; + +/** + * Drag source/target descriptors shared between the archive editor and the + * sub-document editor so HTML5 `dataTransfer` payloads stay consistent. + */ +export type AttachmentLocation = + | { kind: 'loose'; index: number } + | { kind: 'subDocument'; id: string; index: number }; + const IMAGE_EXTENSIONS = new Set([ '.avif', '.bmp', @@ -172,7 +194,8 @@ const extractZipEntries = async (archiveBytes: Uint8Array): Promise 0) { archives.push({ name: archiveName, - attachments: orderedAttachments + attachments: orderedAttachments, + subDocuments: [] }); } @@ -283,16 +306,323 @@ const mergeAttachmentIntoPdf = async (mergedPdf: PDFDocument, attachment: ZipAtt } }; -export const mergeProcessedZipArchive = async (archive: ProcessedZipArchive) => { - if (archive.attachments.length === 0) { - throw new Error('Cannot create a PDF from an empty archive.'); +export const mergeAttachments = async (attachments: ZipAttachment[]): Promise => { + if (attachments.length === 0) { + throw new Error('Cannot create a PDF from an empty attachment list.'); } const mergedPdf = await PDFDocument.create(); - for (const attachment of archive.attachments) { + for (const attachment of attachments) { await mergeAttachmentIntoPdf(mergedPdf, attachment); } return mergedPdf.save(); }; + +export const mergeProcessedZipArchive = async (archive: ProcessedZipArchive) => { + if (archive.attachments.length === 0 && archive.subDocuments.length === 0) { + throw new Error('Cannot create a PDF from an empty archive.'); + } + + return mergeAttachments(flattenArchiveForMerge(archive)); +}; + +/** + * Flatten the archive into a single ordered attachment list, preserving + * sub-document order first and appending loose attachments at the end. + */ +export const flattenArchiveForMerge = (archive: ProcessedZipArchive): ZipAttachment[] => { + const ordered: ZipAttachment[] = []; + + for (const subDocument of archive.subDocuments) { + ordered.push(...subDocument.attachments); + } + + ordered.push(...archive.attachments); + + return ordered; +}; + +/** + * Create a new sub-document with a stable id from a list of attachments. + */ +export const createSubDocument = (name: string, attachments: ZipAttachment[]): SubDocument => ({ + id: crypto.randomUUID(), + name, + attachments: attachments.map((attachment) => ({ ...attachment })) +}); + +/** + * Reorder sub-documents inside an archive. Matches the semantics of a single + * `splice(fromIndex, 1)` -> `splice(toIndex, 0, moved)` move. + */ +export const moveSubDocument = ( + archive: ProcessedZipArchive, + fromIndex: number, + toIndex: number +): ProcessedZipArchive => { + if ( + fromIndex === toIndex || + fromIndex < 0 || + toIndex < 0 || + fromIndex >= archive.subDocuments.length || + toIndex >= archive.subDocuments.length + ) { + return archive; + } + + const nextSubDocuments = [...archive.subDocuments]; + const [movedSubDocument] = nextSubDocuments.splice(fromIndex, 1); + + nextSubDocuments.splice(toIndex, 0, movedSubDocument); + + return { ...archive, subDocuments: nextSubDocuments }; +}; + +const removeAt = (list: T[], index: number): T | null => { + if (index < 0 || index >= list.length) { + return null; + } + + const [item] = list.splice(index, 1); + + return item ?? null; +}; + +const insertAt = (list: T[], index: number, item: T) => { + const clampedIndex = Math.max(0, Math.min(index, list.length)); + list.splice(clampedIndex, 0, item); +}; + +/** + * Reorder an attachment within its current location (loose list or a single + * sub-document's attachment list). Used by the per-list drag handlers. + */ +export const moveAttachmentInPlace = ( + archive: ProcessedZipArchive, + location: AttachmentLocation, + toIndex: number +): ProcessedZipArchive => { + if (location.kind === 'loose') { + const nextAttachments = [...archive.attachments]; + const moved = removeAt(nextAttachments, location.index); + + if (!moved) { + return archive; + } + + insertAt(nextAttachments, toIndex, moved); + + return { ...archive, attachments: nextAttachments }; + } + + let changed = false; + + const nextSubDocuments = archive.subDocuments.map((subDocument) => { + if (subDocument.id !== location.id) { + return subDocument; + } + + const nextAttachments = [...subDocument.attachments]; + const moved = removeAt(nextAttachments, location.index); + + if (!moved) { + return subDocument; + } + + insertAt(nextAttachments, toIndex, moved); + changed = true; + + return { ...subDocument, attachments: nextAttachments }; + }); + + return changed ? { ...archive, subDocuments: nextSubDocuments } : archive; +}; + +/** + * Move an attachment from a source location to a target location. Either side + * may be the loose list or any sub-document. The attachment moves (not copies), + * so the total attachment count is invariant. When source and target point to + * the same list, this degenerates to an in-place reorder (explicitly delegated + * to `moveAttachmentInPlace` so the indices are normalized correctly). + */ +export const moveAttachmentIntoSubDocument = ( + archive: ProcessedZipArchive, + source: AttachmentLocation, + target: AttachmentLocation +): ProcessedZipArchive => { + const sameList = + source.kind === 'loose' && target.kind === 'loose' + ? true + : source.kind === 'subDocument' && target.kind === 'subDocument' + ? source.id === target.id + : false; + + if (sameList) { + return moveAttachmentInPlace(archive, source, target.index); + } + + const nextSubDocuments = archive.subDocuments.map((subDocument) => ({ + ...subDocument, + attachments: [...subDocument.attachments] + })); + const looseAttachments = [...archive.attachments]; + + const removeFromList = (location: AttachmentLocation): ZipAttachment | null => { + if (location.kind === 'loose') { + return removeAt(looseAttachments, location.index); + } + + const target_ = nextSubDocuments.find((item) => item.id === location.id); + + return target_ ? removeAt(target_.attachments, location.index) : null; + }; + + const insertIntoList = (attachment: ZipAttachment, location: AttachmentLocation) => { + if (location.kind === 'loose') { + insertAt(looseAttachments, location.index, attachment); + return; + } + + const target_ = nextSubDocuments.find((item) => item.id === location.id); + + if (!target_) { + // fall back to loose list end if the referenced sub-document vanished + looseAttachments.push(attachment); + return; + } + + insertAt(target_.attachments, location.index, attachment); + }; + + const movedAttachment = removeFromList(source); + + if (!movedAttachment) { + return archive; + } + + insertIntoList(movedAttachment, target); + + return { + ...archive, + attachments: looseAttachments, + subDocuments: nextSubDocuments + }; +}; + +/** + * Remove a single attachment from a sub-document and return it to the end of + * the loose list. Useful as a targeted "move back to loose" action. + */ +export const removeAttachmentFromSubDocument = ( + archive: ProcessedZipArchive, + subDocumentId: string, + attachmentIndex: number +): ProcessedZipArchive => { + const nextSubDocuments = archive.subDocuments.map((subDocument) => ({ + ...subDocument, + attachments: [...subDocument.attachments] + })); + const nextLooseAttachments = [...archive.attachments]; + + const target_ = nextSubDocuments.find((item) => item.id === subDocumentId); + + if (!target_) { + return archive; + } + + const moved = removeAt(target_.attachments, attachmentIndex); + + if (!moved) { + return archive; + } + + nextLooseAttachments.push(moved); + + return { + ...archive, + attachments: nextLooseAttachments, + subDocuments: nextSubDocuments + }; +}; + +/** + * Remove an entire sub-document and return its attachments to the end of the + * loose list. The sub-document ids of the remaining sub-documents are + * preserved. + */ +export const dissolveSubDocument = ( + archive: ProcessedZipArchive, + subDocumentId: string +): ProcessedZipArchive => { + const dissolved = archive.subDocuments.find((item) => item.id === subDocumentId); + + if (!dissolved) { + return archive; + } + + return { + ...archive, + attachments: [...archive.attachments, ...dissolved.attachments], + subDocuments: archive.subDocuments.filter((item) => item.id !== subDocumentId) + }; +}; + +const stripPdfExtension = (name: string) => + name.toLowerCase().endsWith('.pdf') ? name.slice(0, -'.pdf'.length) : name; + +/** + * Build one or more PDF export units from the archive. + * + * - `mode: 'single'` mirrors today's behaviour: one merged PDF covering + * sub-documents first, then loose attachments, named after the archive. + * - `mode: 'separate'` produces one PDF per sub-document (named + * `{archiveName} - {subDocumentName}.pdf`) plus a single merged PDF for any + * loose attachments (named after the archive). Empty groups are skipped. + */ +export const buildArchiveExport = async ( + archive: ProcessedZipArchive, + mode: ExportMode +): Promise => { + if (mode === 'single') { + const attachments = flattenArchiveForMerge(archive); + + if (attachments.length === 0) { + throw new Error('Cannot create a PDF from an empty archive.'); + } + + return [ + { + name: stripPdfExtension(archive.name), + bytes: await mergeAttachments(attachments) + } + ]; + } + + const separateUnits: ExportUnit[] = []; + + for (const subDocument of archive.subDocuments) { + if (subDocument.attachments.length === 0) { + continue; + } + + separateUnits.push({ + name: `${stripPdfExtension(archive.name)} - ${stripPdfExtension(subDocument.name)}`, + bytes: await mergeAttachments(subDocument.attachments) + }); + } + + if (archive.attachments.length > 0) { + separateUnits.push({ + name: stripPdfExtension(archive.name), + bytes: await mergeAttachments(archive.attachments) + }); + } + + if (separateUnits.length === 0) { + throw new Error('Cannot create a PDF from an empty archive.'); + } + + return separateUnits; +}; diff --git a/uat.yml b/uat.yml new file mode 100644 index 0000000..fac4fb8 --- /dev/null +++ b/uat.yml @@ -0,0 +1,37 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - develop + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker login to Gitea Registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${{ vars.REGISTRY_URL }}" \ + -u "${{ secrets.REGISTRY_USER }}" \ + --password-stdin + + - name: Build Docker image + run: | + docker build \ + -t "${{ vars.REGISTRY_URL }}/${{ vars.IMAGE_NAME }}:dev" \ + -t "${{ vars.REGISTRY_URL }}/${{ vars.IMAGE_NAME }}:${{ gitea.sha }}" \ + . + + - name: Push Docker image + run: | + docker push "${{ vars.REGISTRY_URL }}/${{ vars.IMAGE_NAME }}:dev" + docker push "${{ vars.REGISTRY_URL }}/${{ vars.IMAGE_NAME }}:${{ gitea.sha }}" + + - name: Deploy + run: | + curl https://dokploy.robosoft-solutions.de/api/deploy/compose/Miw8GJXF_v4Ebavr3mJwa diff --git a/vite.config.ts b/vite.config.ts index 5765ebc..5d92263 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,5 @@ import tailwindcss from '@tailwindcss/vite'; -import adapter from '@sveltejs/adapter-auto'; +import adapter from '@sveltejs/adapter-node'; import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; @@ -13,9 +13,6 @@ export default defineConfig({ filename.split(/[/\\]/).includes('node_modules') ? undefined : true }, - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. adapter: adapter() }) ]