diff --git a/DECISIONS.md b/DECISIONS.md index 0e3f932..b770ddb 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -686,3 +686,12 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **Missing: `badge-cloud-platform`** (the CR marks it optional). It is in the manifest with `file: null`, Jupiter Cloud Platform keeps `badge: "badge-cloud-platform"` in content, and the map simply draws no badge (fallback, decision 0.5.41). - **The manifest is JSON** (`apps/web/src/map/art/manifest.json`) with a `kind` per asset (`background`, `body`, `badge`, `ship`), so both the app (typed wrapper `manifest.ts`) and the Node optimization script read the same file. - **The Sun is not a location**, so its sprite key lives only in the manifest; the map style will refer to `sun` directly. + +## Step 10C.2 + +- **Output naming:** `public/assets/map-gen/@1x.png` and `@2x.png` for sprites, `.webp` for the background. Missing assets (`file: null`) produce nothing and a warning. +- **The build runs the script with `--if-available`:** it regenerates when sharp is installed (output is byte-identical, so git sees no change) and otherwise only checks that the committed output exists. Builds therefore never need sharp, as the CR requires. +- **The source art is removed from the build output** by a small Vite plugin. Everything in `public/` is copied into `dist` by default, and the sources (about 22 MB) are only script input. The sources stay where the user put them. +- **The committed-output test compares decoded pixels, not bytes**, because PNG compression can differ between Windows and Linux (CI). The "twice is identical" test compares bytes within one machine. The source-transparency test reads all four corners of each source sprite. +- **Sizes:** the 2x body sprites are about 0.5–0.6 MB each (lossless PNG of detailed art). That is acceptable for now. If the PWA precache (Step 11.1) is too heavy, palette quantization is the lever. +- **sharp's install script is not approved** in pnpm; its prebuilt platform binaries load without it. diff --git a/apps/web/package.json b/apps/web/package.json index 642e29b..7b0c117 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", + "build": "node scripts/map-art.mjs --if-available && vite build", + "map-art": "node scripts/map-art.mjs", "preview": "vite preview", "typecheck": "tsc --noEmit" }, @@ -30,6 +31,7 @@ "@types/react-dom": "^19.3.0", "@vitejs/plugin-react": "^6.1.1", "jsdom": "^30.1.1", + "sharp": "^0.35.5", "tailwindcss": "^4.3.3", "vite": "^8.3.2" } diff --git a/apps/web/public/assets/map-gen/bg.webp b/apps/web/public/assets/map-gen/bg.webp new file mode 100644 index 0000000..6607004 Binary files /dev/null and b/apps/web/public/assets/map-gen/bg.webp differ diff --git a/apps/web/public/assets/map-gen/body-ceres@1x.png b/apps/web/public/assets/map-gen/body-ceres@1x.png new file mode 100644 index 0000000..efd14e8 Binary files /dev/null and b/apps/web/public/assets/map-gen/body-ceres@1x.png differ diff --git a/apps/web/public/assets/map-gen/body-ceres@2x.png b/apps/web/public/assets/map-gen/body-ceres@2x.png new file mode 100644 index 0000000..c3038b0 Binary files /dev/null and b/apps/web/public/assets/map-gen/body-ceres@2x.png differ diff --git a/apps/web/public/assets/map-gen/body-earth@1x.png b/apps/web/public/assets/map-gen/body-earth@1x.png new file mode 100644 index 0000000..b714529 Binary files /dev/null and b/apps/web/public/assets/map-gen/body-earth@1x.png differ diff --git a/apps/web/public/assets/map-gen/body-earth@2x.png b/apps/web/public/assets/map-gen/body-earth@2x.png new file mode 100644 index 0000000..3d39c6c Binary files /dev/null and b/apps/web/public/assets/map-gen/body-earth@2x.png differ diff --git a/apps/web/public/assets/map-gen/body-jupiter@1x.png b/apps/web/public/assets/map-gen/body-jupiter@1x.png new file mode 100644 index 0000000..1c9dbbd Binary files /dev/null and b/apps/web/public/assets/map-gen/body-jupiter@1x.png differ diff --git a/apps/web/public/assets/map-gen/body-jupiter@2x.png b/apps/web/public/assets/map-gen/body-jupiter@2x.png new file mode 100644 index 0000000..f20a70f Binary files /dev/null and b/apps/web/public/assets/map-gen/body-jupiter@2x.png differ diff --git a/apps/web/public/assets/map-gen/body-mars@1x.png b/apps/web/public/assets/map-gen/body-mars@1x.png new file mode 100644 index 0000000..70c07c3 Binary files /dev/null and b/apps/web/public/assets/map-gen/body-mars@1x.png differ diff --git a/apps/web/public/assets/map-gen/body-mars@2x.png b/apps/web/public/assets/map-gen/body-mars@2x.png new file mode 100644 index 0000000..5c23dc7 Binary files /dev/null and b/apps/web/public/assets/map-gen/body-mars@2x.png differ diff --git a/apps/web/public/assets/map-gen/body-moon@1x.png b/apps/web/public/assets/map-gen/body-moon@1x.png new file mode 100644 index 0000000..6580af9 Binary files /dev/null and b/apps/web/public/assets/map-gen/body-moon@1x.png differ diff --git a/apps/web/public/assets/map-gen/body-moon@2x.png b/apps/web/public/assets/map-gen/body-moon@2x.png new file mode 100644 index 0000000..eaff3fd Binary files /dev/null and b/apps/web/public/assets/map-gen/body-moon@2x.png differ diff --git a/apps/web/public/assets/map-gen/ship-bulk-hauler@1x.png b/apps/web/public/assets/map-gen/ship-bulk-hauler@1x.png new file mode 100644 index 0000000..18d7774 Binary files /dev/null and b/apps/web/public/assets/map-gen/ship-bulk-hauler@1x.png differ diff --git a/apps/web/public/assets/map-gen/ship-bulk-hauler@2x.png b/apps/web/public/assets/map-gen/ship-bulk-hauler@2x.png new file mode 100644 index 0000000..e41ea89 Binary files /dev/null and b/apps/web/public/assets/map-gen/ship-bulk-hauler@2x.png differ diff --git a/apps/web/public/assets/map-gen/ship-company-courier@1x.png b/apps/web/public/assets/map-gen/ship-company-courier@1x.png new file mode 100644 index 0000000..de10e8d Binary files /dev/null and b/apps/web/public/assets/map-gen/ship-company-courier@1x.png differ diff --git a/apps/web/public/assets/map-gen/ship-company-courier@2x.png b/apps/web/public/assets/map-gen/ship-company-courier@2x.png new file mode 100644 index 0000000..de1f3a2 Binary files /dev/null and b/apps/web/public/assets/map-gen/ship-company-courier@2x.png differ diff --git a/apps/web/public/assets/map-gen/ship-courier@1x.png b/apps/web/public/assets/map-gen/ship-courier@1x.png new file mode 100644 index 0000000..85bfb03 Binary files /dev/null and b/apps/web/public/assets/map-gen/ship-courier@1x.png differ diff --git a/apps/web/public/assets/map-gen/ship-courier@2x.png b/apps/web/public/assets/map-gen/ship-courier@2x.png new file mode 100644 index 0000000..a22af0a Binary files /dev/null and b/apps/web/public/assets/map-gen/ship-courier@2x.png differ diff --git a/apps/web/public/assets/map-gen/ship-freighter@1x.png b/apps/web/public/assets/map-gen/ship-freighter@1x.png new file mode 100644 index 0000000..caa8dac Binary files /dev/null and b/apps/web/public/assets/map-gen/ship-freighter@1x.png differ diff --git a/apps/web/public/assets/map-gen/ship-freighter@2x.png b/apps/web/public/assets/map-gen/ship-freighter@2x.png new file mode 100644 index 0000000..62f182d Binary files /dev/null and b/apps/web/public/assets/map-gen/ship-freighter@2x.png differ diff --git a/apps/web/public/assets/map-gen/sun@1x.png b/apps/web/public/assets/map-gen/sun@1x.png new file mode 100644 index 0000000..8ac58dc Binary files /dev/null and b/apps/web/public/assets/map-gen/sun@1x.png differ diff --git a/apps/web/public/assets/map-gen/sun@2x.png b/apps/web/public/assets/map-gen/sun@2x.png new file mode 100644 index 0000000..0805d2f Binary files /dev/null and b/apps/web/public/assets/map-gen/sun@2x.png differ diff --git a/apps/web/scripts/map-art.mjs b/apps/web/scripts/map-art.mjs new file mode 100644 index 0000000..cdda4a1 --- /dev/null +++ b/apps/web/scripts/map-art.mjs @@ -0,0 +1,96 @@ +// Generates optimized map art from the user's source images (CR-003, decision 0.5.37). +// +// node scripts/map-art.mjs [outDir] regenerate (default public/assets/map-gen) +// node scripts/map-art.mjs --if-available as above, but if sharp is not installed, +// only check the committed output exists +// +// Sources in the manifest's `sourceDir` are only ever read. +import { existsSync, mkdirSync, readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const webRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); +const manifest = JSON.parse(readFileSync(join(webRoot, "src/map/art/manifest.json"), "utf8")); + +/** Long-side sizes in px for the 1x and 2x variants of each sprite kind. */ +export const SPRITE_SIZES = { body: [256, 512], badge: [256, 512], ship: [128, 256] }; +export const BACKGROUND_MAX = 2048; +export const DEFAULT_OUT_DIR = join(webRoot, "public/assets/map-gen"); + +/** Every file the script writes for the manifest, relative to the output folder. */ +export function expectedOutputs() { + const outputs = []; + for (const [id, asset] of Object.entries(manifest.assets)) { + if (asset.file === null) continue; + if (asset.kind === "background") outputs.push({ id, file: `${id}.webp`, kind: asset.kind }); + else { + const [one, two] = SPRITE_SIZES[asset.kind]; + outputs.push({ id, file: `${id}@1x.png`, kind: asset.kind, longSide: one }); + outputs.push({ id, file: `${id}@2x.png`, kind: asset.kind, longSide: two }); + } + } + return outputs; +} + +const png = { compressionLevel: 9, adaptiveFiltering: false, palette: false }; + +async function generate(sharp, outDir) { + mkdirSync(outDir, { recursive: true }); + for (const [id, asset] of Object.entries(manifest.assets)) { + if (asset.file === null) { + console.warn(`map-art: ${id} has no source file; the map will use its placeholder`); + continue; + } + const source = join(webRoot, manifest.sourceDir, asset.file); + if (asset.kind === "background") { + await sharp(source) + .resize({ + width: BACKGROUND_MAX, + height: BACKGROUND_MAX, + fit: "inside", + withoutEnlargement: true, + }) + .webp({ quality: 80, effort: 6 }) + .toFile(join(outDir, `${id}.webp`)); + continue; + } + // Trim transparent padding once, then export each size from the trimmed image. + const trimmed = await sharp(source).trim().png().toBuffer(); + const [one, two] = SPRITE_SIZES[asset.kind]; + for (const [suffix, size] of [ + ["1x", one], + ["2x", two], + ]) { + await sharp(trimmed) + .resize({ width: size, height: size, fit: "inside" }) + .png(png) + .toFile(join(outDir, `${id}@${suffix}.png`)); + } + } +} + +async function main(args) { + const ifAvailable = args.includes("--if-available"); + const outDir = resolve(args.find((a) => !a.startsWith("--")) ?? DEFAULT_OUT_DIR); + let sharp; + try { + sharp = (await import("sharp")).default; + } catch (error) { + if (!ifAvailable) throw error; + const missing = expectedOutputs().filter((o) => !existsSync(join(outDir, o.file))); + if (missing.length > 0) { + throw new Error( + `map-art: sharp unavailable and output missing: ${missing.map((o) => o.file)}`, + { cause: error }, + ); + } + console.warn("map-art: sharp unavailable; using the committed output"); + return; + } + await generate(sharp, outDir); + console.log(`map-art: wrote ${expectedOutputs().length} files to ${outDir}`); +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + await main(process.argv.slice(2)); +} diff --git a/apps/web/test/map-art.test.ts b/apps/web/test/map-art.test.ts new file mode 100644 index 0000000..4809c9d --- /dev/null +++ b/apps/web/test/map-art.test.ts @@ -0,0 +1,102 @@ +// @vitest-environment node +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import sharp from "sharp"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { artManifest } from "../src/map/art/manifest"; + +const webRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); +const script = join(webRoot, "scripts/map-art.mjs"); +sharp.cache(false); +/** Reads through a buffer so sharp never holds the temp files open (Windows). */ +const image = (path: string) => sharp(readFileSync(path)); +const SIZES = { body: [256, 512], badge: [256, 512], ship: [128, 256] } as const; + +const run = (outDir: string) => + execFileSync(process.execPath, [script, outDir], { cwd: webRoot, stdio: "pipe" }); + +describe("map art optimization script (CR-003)", () => { + let first: string; + let second: string; + beforeAll(() => { + first = mkdtempSync(join(tmpdir(), "map-art-a-")); + second = mkdtempSync(join(tmpdir(), "map-art-b-")); + run(first); + run(second); + }, 120_000); + afterAll(() => { + rmSync(first, { recursive: true, force: true }); + rmSync(second, { recursive: true, force: true }); + }); + + const present = Object.entries(artManifest.assets).filter(([, a]) => a.file !== null); + + it("writes every expected file at the right size", async () => { + for (const [id, asset] of present) { + if (asset.kind === "background") { + const meta = await image(join(first, `${id}.webp`)).metadata(); + expect(meta.format).toBe("webp"); + expect(Math.max(meta.width!, meta.height!)).toBeLessThanOrEqual(2048); + continue; + } + const [one, two] = SIZES[asset.kind]; + for (const [suffix, longSide] of [ + ["1x", one], + ["2x", two], + ] as const) { + const meta = await image(join(first, `${id}@${suffix}.png`)).metadata(); + expect(Math.max(meta.width!, meta.height!), `${id}@${suffix}`).toBe(longSide); + } + } + }); + + it("produces identical files when run twice", () => { + const files = readdirSync(first).sort(); + expect(readdirSync(second).sort()).toEqual(files); + for (const file of files) { + expect(readFileSync(join(second, file)).equals(readFileSync(join(first, file))), file).toBe( + true, + ); + } + }); + + it("matches the committed output pixel for pixel", async () => { + // Pixels, not bytes: PNG compression may differ between platforms (CI runs Linux). + const committed = join(webRoot, "public/assets/map-gen"); + for (const file of readdirSync(first).filter((f) => f.endsWith(".png"))) { + const [fresh, stored] = await Promise.all([ + image(join(first, file)).raw().toBuffer(), + image(join(committed, file)).raw().toBuffer(), + ]); + expect(stored.equals(fresh), file).toBe(true); + } + }); + + it("keeps the background under 1 MB", () => { + expect(statSync(join(first, "bg.webp")).size).toBeLessThan(1024 * 1024); + }); + + it("has source sprites with transparent backgrounds", async () => { + for (const [id, asset] of present) { + if (asset.kind === "background") continue; + const { data, info } = await image(join(webRoot, artManifest.sourceDir, asset.file!)) + .ensureAlpha() + .raw() + .toBuffer({ resolveWithObject: true }); + const alpha = (x: number, y: number) => data[(y * info.width + x) * info.channels + 3]; + const corners = [ + alpha(0, 0), + alpha(info.width - 1, 0), + alpha(0, info.height - 1), + alpha(info.width - 1, info.height - 1), + ]; + expect( + corners.every((a) => a === 0), + `${asset.file} (${id}): background isn't transparent`, + ).toBe(true); + } + }); +}); diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 24b0092..f5ac403 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -1,9 +1,30 @@ import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; -import { defineConfig } from "vite"; +import { rm } from "node:fs/promises"; +import { join } from "node:path"; +import { defineConfig, type Plugin } from "vite"; + +/** + * The user's source map art (public/assets/map) is large and only an input to + * scripts/map-art.mjs; the app loads public/assets/map-gen. Drop the sources + * from the build output (decision 0.5.37). + */ +function omitSourceArt(): Plugin { + let outDir = "dist"; + return { + name: "omit-source-art", + apply: "build", + configResolved(config) { + outDir = config.build.outDir; + }, + async closeBundle() { + await rm(join(outDir, "assets/map"), { recursive: true, force: true }); + }, + }; +} export default defineConfig({ - plugins: [react(), tailwindcss()], + plugins: [react(), tailwindcss(), omitSourceArt()], server: { // Same-domain API in development too (decision 0.5.20). The server listens on // IPv4 only, so target 127.0.0.1 rather than localhost. diff --git a/memory-bank/architecture.md b/memory-bank/architecture.md index 560129c..a466a02 100644 --- a/memory-bank/architecture.md +++ b/memory-bank/architecture.md @@ -451,6 +451,7 @@ Vite + React 19 + Tailwind 4, React Router 8 (declarative routes). In developmen | `src/layout/Layout.tsx` | Header (credits, game clock), screen outlet, bottom tab bar | | `src/routes.ts` | Screen list shared by router and navigation | | `src/map/art/manifest.json` | Map art manifest (CR-003): asset id → `{ kind, file }` in `public/assets/map/` (never edited); `null` file = missing, placeholder used | +| `scripts/map-art.mjs` | Generates `public/assets/map-gen/` (`@1x.png`, `@2x.png`, `bg.webp`) from the manifest with sharp; output committed | | `src/screens/*` | HQ, Contracts (TutorialBoard / LocationBoards + DispatchSheet), Fleet, Pilots, Market, Locations, StartScreen | | `src/components/*` | `AwaySummary` dialog, `Countdown` | | `src/state/actions.ts` | `useLoadedPlayer`, `useGameAction(route)` (mutation + invalidate state and boards) | diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 600dcb8..f278a7c 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -150,7 +150,7 @@ Spec: `memory-bank/change-requests/CR-002-open-markets.md`. New decisions record Spec: `memory-bank/change-requests/CR-003-map-art.md`. New decisions recorded as plan §0.5.37–0.5.41. - [x] **Step 10C.1 — Inventory and asset manifest.** `apps/web/src/map/art/manifest.json` maps 12 asset ids to the 11 files in `apps/web/public/assets/map/` (all 1254×1254; sprites RGBA). `badge-cloud-platform` is listed with `file: null` (not provided). Content: `sprite` on every location and ship model, `badge` on Jupiter Cloud Platform. Tests: `art-manifest.test.ts`, content sprite-key tests. -- [ ] Step 10C.2 — Asset optimization script +- [x] **Step 10C.2 — Asset optimization script.** `apps/web/scripts/map-art.mjs` (sharp; `pnpm --filter @deadhead/web map-art`, and first in `build` with `--if-available`) writes 21 files to `public/assets/map-gen/` (committed): bodies and Sun 256/512 px, ships 128/256 px (trimmed), `bg.webp` 1254 px / 106 KB. The Vite build drops the source art from `dist`. Test: `map-art.test.ts` (sizes, byte-identical reruns, pixels match the committed output, bg < 1 MB, transparent source corners). - [ ] Step 10C.3 — Map style config - [ ] Step 10C.4 — Asset loading and fallback - [ ] Step 10C.5 — Background diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f635f7f..d07e49a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,6 +136,9 @@ importers: jsdom: specifier: ^30.1.1 version: 30.1.1(@noble/hashes@2.4.0) + sharp: + specifier: ^0.35.5 + version: 0.35.5(@types/node@24.19.1) tailwindcss: specifier: ^4.3.3 version: 4.3.3 @@ -337,6 +340,9 @@ packages: '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} deprecated: 'Merged into tsx: https://tsx.hirok.io' @@ -875,6 +881,168 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.5': + resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.5': + resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.5': + resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.4': + resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.4': + resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.4': + resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.4': + resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.4': + resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.4': + resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.4': + resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.4': + resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.5': + resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.5': + resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.5': + resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.5': + resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.5': + resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.5': + resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.5': + resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.5': + resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.5': + resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.5': + resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.5': + resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.5': + resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.5': + resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -2332,6 +2500,15 @@ packages: set-cookie-parser@3.1.2: resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==} + sharp@0.35.5: + resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2428,6 +2605,9 @@ packages: peerDependencies: typescript: '>=4.8.4' + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.23.15: resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==} engines: {node: '>=18.0.0'} @@ -2760,6 +2940,11 @@ snapshots: '@drizzle-team/brocli@0.10.2': {} + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild-kit/core-utils@3.3.2': dependencies: esbuild: 0.18.20 @@ -3069,6 +3254,112 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.4 + optional: true + + '@img/sharp-darwin-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.4 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + optional: true + + '@img/sharp-linux-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.4 + optional: true + + '@img/sharp-linux-arm@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.4 + optional: true + + '@img/sharp-linux-ppc64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.4 + optional: true + + '@img/sharp-linux-riscv64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.4 + optional: true + + '@img/sharp-linux-s390x@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.4 + optional: true + + '@img/sharp-linux-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + optional: true + + '@img/sharp-wasm32@0.35.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-win32-arm64@0.35.5': + optional: true + + '@img/sharp-win32-ia32@0.35.5': + optional: true + + '@img/sharp-win32-x64@0.35.5': + optional: true + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.6.0 @@ -4354,6 +4645,39 @@ snapshots: set-cookie-parser@3.1.2: {} + sharp@0.35.5(@types/node@24.19.1): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.5 + '@img/sharp-darwin-x64': 0.35.5 + '@img/sharp-freebsd-wasm32': 0.35.5 + '@img/sharp-libvips-darwin-arm64': 1.3.4 + '@img/sharp-libvips-darwin-x64': 1.3.4 + '@img/sharp-libvips-linux-arm': 1.3.4 + '@img/sharp-libvips-linux-arm64': 1.3.4 + '@img/sharp-libvips-linux-ppc64': 1.3.4 + '@img/sharp-libvips-linux-riscv64': 1.3.4 + '@img/sharp-libvips-linux-s390x': 1.3.4 + '@img/sharp-libvips-linux-x64': 1.3.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + '@img/sharp-linux-arm': 0.35.5 + '@img/sharp-linux-arm64': 0.35.5 + '@img/sharp-linux-ppc64': 0.35.5 + '@img/sharp-linux-riscv64': 0.35.5 + '@img/sharp-linux-s390x': 0.35.5 + '@img/sharp-linux-x64': 0.35.5 + '@img/sharp-linuxmusl-arm64': 0.35.5 + '@img/sharp-linuxmusl-x64': 0.35.5 + '@img/sharp-webcontainers-wasm32': 0.35.5 + '@img/sharp-win32-arm64': 0.35.5 + '@img/sharp-win32-ia32': 0.35.5 + '@img/sharp-win32-x64': 0.35.5 + '@types/node': 24.19.1 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4426,6 +4750,9 @@ snapshots: dependencies: typescript: 6.0.3 + tslib@2.8.1: + optional: true + tsx@4.23.15: dependencies: esbuild: 0.28.2