diff --git a/bun.lock b/bun.lock index 8ff715e..6fb050e 100644 --- a/bun.lock +++ b/bun.lock @@ -40,7 +40,10 @@ "name": "@thermoprint/web", "version": "0.0.0", "dependencies": { + "@fontsource-variable/jetbrains-mono": "^5.2.5", + "@fontsource/inter": "^5.2.5", "@thermoprint/core": "workspace:*", + "fflate": "^0.8.2", "jsbarcode": "^3.12.3", "konva": "^10.2.1", "lucide-react": "^0.577.0", @@ -48,6 +51,7 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "react-konva": "^19.2.3", + "zundo": "^2.3.0", "zustand": "^5.0.11", }, "devDependencies": { @@ -127,6 +131,10 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + "@fontsource-variable/jetbrains-mono": ["@fontsource-variable/jetbrains-mono@5.2.8", "", {}, "sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q=="], + + "@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], @@ -461,6 +469,8 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -777,6 +787,8 @@ "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + "zundo": ["zundo@2.3.0", "", { "peerDependencies": { "zustand": "^4.3.0 || ^5.0.0" } }, "sha512-4GXYxXA17SIKYhVbWHdSEU04P697IMyVGXrC2TnzoyohEAWytFNOKqOp5gTGvaW93F/PM5Y0evbGtOPF0PWQwQ=="], + "zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -811,6 +823,8 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@thermoprint/core/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + "@types/qrcode/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], @@ -875,6 +889,8 @@ "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + "@thermoprint/core/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "@types/qrcode/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], @@ -897,12 +913,16 @@ "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@thermoprint/core/@types/bun/bun-types/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@thermoprint/core/@types/bun/bun-types/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], } } diff --git a/packages/web/package.json b/packages/web/package.json index 1d238be..987312b 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -10,7 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "@fontsource-variable/jetbrains-mono": "^5.2.5", + "@fontsource/inter": "^5.2.5", "@thermoprint/core": "workspace:*", + "fflate": "^0.8.2", "jsbarcode": "^3.12.3", "konva": "^10.2.1", "lucide-react": "^0.577.0", @@ -19,9 +22,7 @@ "react-dom": "^19.2.4", "react-konva": "^19.2.3", "zundo": "^2.3.0", - "zustand": "^5.0.11", - "@fontsource/inter": "^5.2.5", - "@fontsource-variable/jetbrains-mono": "^5.2.5" + "zustand": "^5.0.11" }, "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/packages/web/src/editor/dock/flyouts/library-flyout.tsx b/packages/web/src/editor/dock/flyouts/library-flyout.tsx index 8c0b05d..8ff529e 100644 --- a/packages/web/src/editor/dock/flyouts/library-flyout.tsx +++ b/packages/web/src/editor/dock/flyouts/library-flyout.tsx @@ -16,6 +16,7 @@ import JsBarcode from "jsbarcode"; import { useEditorV2Store, type BaseElement } from "../../../store/editor-store.ts"; import { downloadLabelAsJson, + downloadLibraryAsZip, importLabelFromJson, type SavedLabel, } from "../../../lib/library.ts"; @@ -281,9 +282,9 @@ export function LibraryFlyout({ onClose }: Props) { return (
{/* Header */} -
+
{/* Row 1: title + close */} -
+
Library @@ -291,15 +292,9 @@ export function LibraryFlyout({ onClose }: Props) {
- {/* Row 2 mobile / single row desktop: search + actions */} -
-
- - Library - {library.labels.length} labels -
-
-
+ {/* Row 2: search + actions */} +
+
+ {library.labels.length > 0 && ( + + )} -
diff --git a/packages/web/src/lib/library.ts b/packages/web/src/lib/library.ts index f9de4a8..97150a0 100644 --- a/packages/web/src/lib/library.ts +++ b/packages/web/src/lib/library.ts @@ -1,3 +1,4 @@ +import { zipSync, strToU8 } from "fflate"; import type { BaseElement, LabelSize } from "../store/editor-store.ts"; // ---- Types ---- @@ -72,6 +73,39 @@ export function downloadLabelAsJson(label: SavedLabel): void { URL.revokeObjectURL(url); } +export function downloadLibraryAsZip(labels: SavedLabel[]): void { + const files: Record = {}; + const usedNames = new Set(); + + for (const label of labels) { + let baseName = label.name.replace(/[^a-zA-Z0-9_-]/g, "_"); + let fileName = baseName; + let i = 1; + while (usedNames.has(fileName)) { + fileName = `${baseName}_${i++}`; + } + usedNames.add(fileName); + + const data = { + name: label.name, + label: label.label, + elements: label.elements, + createdAt: label.createdAt, + updatedAt: label.updatedAt, + }; + files[`${fileName}.json`] = strToU8(JSON.stringify(data, null, 2)); + } + + const zipped = zipSync(files); + const blob = new Blob([zipped.buffer as ArrayBuffer], { type: "application/zip" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `thermoprint-library-${new Date().toISOString().slice(0, 10)}.zip`; + a.click(); + URL.revokeObjectURL(url); +} + export function importLabelFromJson(file: File): Promise> { return new Promise((resolve, reject) => { const reader = new FileReader();