diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c19be12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +dist-demo/ +*.tsbuildinfo +.DS_Store +var/ diff --git a/packages/director-mode/README.md b/packages/director-mode/README.md new file mode 100644 index 0000000..75ec5f5 --- /dev/null +++ b/packages/director-mode/README.md @@ -0,0 +1,133 @@ +# @shoploop/director-mode + +Render-backend-agnostic **shot-authoring UX** for product twins. + +Tap-to-drop a camera, draw a Bezier shot path, scrub a preview, save a preset, and +validate against brand-safe framing rules. Director Mode emits a portable +`DirectorShot` JSON that any backend can render — `threejs_diffusion` today, +Kit/OVRTX later — without forking the UI. + +> **Omniverse-free by design.** No `@nvidia/omniverse`, no `web-viewer-sample`, no +> OVRTX, no Kit App Streaming. The render path is the existing +> **open-imagine router → threejs_diffusion**. Live Kit streaming is a deferred +> Tier-2 concern (unit economics: $30–$80/mo per concurrent operator breaks the +> $99–$299/mo Shoploop tier). + +## Render path + +``` +twin-renderer canvas (Three.js) + ↓ tap / drag events +DirectorOverlay (this package) + ↓ DirectorShot JSON +validator (Kinfolk composition + brand_rules.json) + ↓ submitShot() — HMAC signed +open-imagine router POST /render → threejs_diffusion + ↓ Creative Pack +distribution adapters +``` + +## Modules + +- `src/schema.ts` — `DirectorShot`, `UsdCameraPrim`, `BezierShotPath`, `AP2ProductRef`, type guards. +- `src/core/` — `shotBuilder` (tap/drag → shot), `usdSerializer` (shot ↔ `.usda`), `presetStore` (localStorage + remote-sync stub). +- `src/validator/` — `kinfolkRule`, `brandSafeZone`, `focalLengthGate`, composed by `validateShot`. +- `src/adapter/` — `openImagineSubmit` (HMAC-signed `POST /render`), `renderProvenance` (stamps `external_id`). +- `src/ui/` — React 18 overlay: `DirectorOverlay`, `CameraDropTool`, `PathDrawTool`, `Scrubber`, `PresetLibrary`. + +## Brand-safe framing rules + +Thresholds are configurable per brand via `brand_rules.json`. + +| Rule | Default | Severity | +|---|---|---| +| `kinfolk.composition` | subject 38–62% of frame width, ≥30% negative space on one side | block | +| `brand.safe_zone` | product never within 5% of any frame edge (unless `allow_bleed`) | block | +| `focalLength` | 24–135mm (unless lens is in `focal_overrides`) | block | + +## Usage + +```ts +import { + buildShotFromTap, + validateShot, + submitShot, + buildProvenance, +} from "@shoploop/director-mode"; + +const shot = buildShotFromTap({ + name: "hero-pour-3s", + position: [0.0, 1.2, 4.5], + target: [0, 0.5, 0], + product_ref: { + product_id: "gid://shopify/ProductVariant/4412345", + twin_usda_uri: "s3://varitea-twins/jasmine-pearl.usda", + brand_rules_id: "varitea-kinfolk", + }, +}); + +const result = validateShot(shot, { + framing: { x: 0.1, y: 0.2, width: 0.5, height: 0.6 }, +}); +if (!result.ok) throw new Error(JSON.stringify(result.violations)); + +const { job_id } = await submitShot(shot, { + routerUrl: "https://open-imagine.internal/v1", + // secret falls back to process.env.OPEN_IMAGINE_HMAC_SECRET +}); + +// Stamp the shot id into provenance so the bandit's external_id join resolves. +const provenance = buildProvenance(shot, { twin_handle: "varitea-jasmine-pearl", preset: "hero" }); +``` + +## DirectorShot — full JSON example + +```json +{ + "id": "01HZX9F8Q0R3M5T7V9XABCDEFG", + "name": "hero-pour-3s", + "camera": { + "position": [0.0, 1.2, 4.5], + "target": [0.0, 0.5, 0.0], + "focalLength": 50, + "fStop": 5.6, + "horizontalAperture": 36 + }, + "path": { + "p0": [0.0, 1.2, 4.5], + "p1": [1.0, 1.2, 4.0], + "p2": [2.0, 1.0, 3.2], + "p3": [2.5, 0.9, 2.8], + "targetPath": { + "p0": [0.0, 0.5, 0.0], + "p1": [0.1, 0.5, 0.0], + "p2": [0.2, 0.5, 0.0], + "p3": [0.3, 0.5, 0.0] + } + }, + "duration_ms": 3000, + "product_ref": { + "product_id": "gid://shopify/ProductVariant/4412345", + "twin_usda_uri": "s3://varitea-twins/jasmine-pearl.usda", + "brand_rules_id": "varitea-kinfolk" + }, + "brand_rules_id": "varitea-kinfolk", + "created_at": "2026-06-25T18:30:00.000Z", + "preset_tags": ["hero", "pack-shot"] +} +``` + +## Scripts + +```bash +pnpm test --filter @shoploop/director-mode # vitest +pnpm --filter @shoploop/director-mode build # tsc → dist/ +pnpm --filter @shoploop/director-mode typecheck +``` + +## Env + +- `OPEN_IMAGINE_HMAC_SECRET` — shared secret for signing `POST /render`. The + signature is `base64(HMAC-SHA256(rawBody, secret))` sent as + `X-Open-Imagine-Signature`, matching the Shopify-webhook HMAC scheme already + used in this monorepo. diff --git a/packages/director-mode/package.json b/packages/director-mode/package.json new file mode 100644 index 0000000..3dde09c --- /dev/null +++ b/packages/director-mode/package.json @@ -0,0 +1,45 @@ +{ + "name": "@shoploop/director-mode", + "version": "0.1.0", + "description": "Render-backend-agnostic shot-authoring UX for product twins. Tap-to-drop camera, draw Bezier shot paths, scrub preview, save presets, validate against Kinfolk + brand-safe rules. Emits portable DirectorShot JSON rendered via the open-imagine router → threejs_diffusion. Omniverse-free by design.", + "license": "UNLICENSED", + "private": true, + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "sideEffects": false, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "test:watch": "vitest" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "devDependencies": { + "@types/node": "^20.14.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "jsdom": "^24.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "typescript": "^5.6.0", + "vitest": "^2.1.0" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/director-mode/src/__tests__/brandSafeZone.test.ts b/packages/director-mode/src/__tests__/brandSafeZone.test.ts new file mode 100644 index 0000000..7dfe91b --- /dev/null +++ b/packages/director-mode/src/__tests__/brandSafeZone.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { brandSafeZone } from "../validator/brandSafeZone.js"; +import type { BrandRules, SubjectFraming } from "../schema.js"; + +describe("brandSafeZone", () => { + it("passes a product fully inside the safe zone", () => { + const framing: SubjectFraming = { x: 0.2, y: 0.2, width: 0.4, height: 0.4 }; + expect(brandSafeZone(framing)).toEqual([]); + }); + + it("blocks a product cropped by the frame edge", () => { + const framing: SubjectFraming = { x: -0.05, y: 0.2, width: 0.4, height: 0.4 }; + const v = brandSafeZone(framing); + expect(v).toHaveLength(1); + expect(v[0].message).toContain("cropped"); + }); + + it("blocks a product entirely outside the frame", () => { + const framing: SubjectFraming = { x: 1.2, y: 0.2, width: 0.3, height: 0.3 }; + const v = brandSafeZone(framing); + expect(v[0].message).toContain("outside"); + }); + + it("permits edge contact when allow_bleed is set", () => { + const framing: SubjectFraming = { x: 0.02, y: 0.2, width: 0.4, height: 0.4 }; + const brand: BrandRules = { id: "b", allow_bleed: true }; + expect(brandSafeZone(framing, brand)).toEqual([]); + // ...but blocks the same framing without the override. + expect(brandSafeZone(framing)).toHaveLength(1); + }); +}); diff --git a/packages/director-mode/src/__tests__/focalLengthGate.test.ts b/packages/director-mode/src/__tests__/focalLengthGate.test.ts new file mode 100644 index 0000000..201e5d9 --- /dev/null +++ b/packages/director-mode/src/__tests__/focalLengthGate.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from "vitest"; +import { focalLengthGate } from "../validator/focalLengthGate.js"; +import type { BrandRules, UsdCameraPrim } from "../schema.js"; + +const cam = (focalLength: number): UsdCameraPrim => ({ + position: [0, 0, 5], + target: [0, 0, 0], + focalLength, + fStop: 5.6, + horizontalAperture: 36, +}); + +describe("focalLengthGate", () => { + it("passes a 50mm lens (inside the 24–135mm default range)", () => { + expect(focalLengthGate(cam(50))).toEqual([]); + }); + + it("blocks an 18mm ultra-wide lens", () => { + const v = focalLengthGate(cam(18)); + expect(v).toHaveLength(1); + expect(v[0].rule).toBe("focalLength"); + expect(v[0].severity).toBe("block"); + }); + + it("blocks 200mm by default but allows it when brand whitelists the lens", () => { + expect(focalLengthGate(cam(200))).toHaveLength(1); + const brand: BrandRules = { id: "b", focal_overrides: [200] }; + expect(focalLengthGate(cam(200), brand)).toEqual([]); + }); +}); diff --git a/packages/director-mode/src/__tests__/kinfolkRule.test.ts b/packages/director-mode/src/__tests__/kinfolkRule.test.ts new file mode 100644 index 0000000..15ef38e --- /dev/null +++ b/packages/director-mode/src/__tests__/kinfolkRule.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { kinfolkRule } from "../validator/kinfolkRule.js"; +import type { SubjectFraming } from "../schema.js"; + +describe("kinfolkRule rule-of-thirds width", () => { + it("passes a subject at 50% width with ample negative space", () => { + const framing: SubjectFraming = { x: 0.1, y: 0.2, width: 0.5, height: 0.6 }; + expect(kinfolkRule(framing)).toEqual([]); + }); + + it("blocks a subject that fills 80% of the frame width", () => { + const framing: SubjectFraming = { x: 0.1, y: 0.1, width: 0.8, height: 0.6 }; + const v = kinfolkRule(framing); + expect(v.some((x) => x.rule === "kinfolk.composition" && x.severity === "block")).toBe(true); + }); +}); + +describe("kinfolkRule negative space", () => { + it("passes when ≥30% negative space exists on one side", () => { + // width 0.4 hugging the left → right space = 0.5 (>0.30) + const framing: SubjectFraming = { x: 0.1, y: 0.2, width: 0.4, height: 0.6 }; + expect(kinfolkRule(framing)).toEqual([]); + }); + + it("blocks a perfectly centered subject with <30% on both sides", () => { + // width 0.5 centered → each side 0.25 (<0.30) + const framing: SubjectFraming = { x: 0.25, y: 0.2, width: 0.5, height: 0.6 }; + const v = kinfolkRule(framing); + expect(v.some((x) => x.message.includes("negative space"))).toBe(true); + }); +}); diff --git a/packages/director-mode/src/__tests__/openImagineSubmit.test.ts b/packages/director-mode/src/__tests__/openImagineSubmit.test.ts new file mode 100644 index 0000000..5b527ea --- /dev/null +++ b/packages/director-mode/src/__tests__/openImagineSubmit.test.ts @@ -0,0 +1,98 @@ +import { describe, it, expect, vi } from "vitest"; +import { createHmac } from "node:crypto"; +import { signPayload, submitShot } from "../adapter/openImagineSubmit.js"; +import { buildShotFromTap } from "../core/shotBuilder.js"; +import type { DirectorShot } from "../schema.js"; + +const SECRET = "test-secret"; + +function makeShot(): DirectorShot { + return buildShotFromTap({ + name: "hero", + position: [0, 0, 5], + product_ref: { product_id: "p", twin_usda_uri: "file://t", brand_rules_id: "default" }, + now: 0, + }); +} + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" }, + }); +} + +describe("openImagineSubmit happy path", () => { + it("POSTs to /render with the threejs_diffusion backend and returns the job", async () => { + const shot = makeShot(); + const fetchImpl = vi.fn(async () => jsonResponse(200, { job_id: "job-1", status: "queued" })); + const result = await submitShot(shot, { + routerUrl: "https://router.local/v1/", + secret: SECRET, + fetchImpl: fetchImpl as unknown as typeof fetch, + }); + expect(result).toEqual({ job_id: "job-1", status: "queued" }); + expect(fetchImpl).toHaveBeenCalledTimes(1); + const [url, init] = fetchImpl.mock.calls[0] as unknown as [string, RequestInit]; + expect(url).toBe("https://router.local/v1/render"); + const body = JSON.parse(init.body as string); + expect(body).toEqual({ shot, backend: "threejs_diffusion" }); + }); +}); + +describe("openImagineSubmit HMAC signing", () => { + it("signs the raw body with base64(HMAC-SHA256) under OPEN_IMAGINE_HMAC_SECRET", async () => { + const shot = makeShot(); + let seenSig = ""; + let seenBody = ""; + const fetchImpl = vi.fn(async (_url: string, init: RequestInit) => { + seenBody = init.body as string; + seenSig = (init.headers as Record)["x-open-imagine-signature"]; + return jsonResponse(200, { job_id: "j", status: "queued" }); + }); + await submitShot(shot, { + routerUrl: "https://router.local", + secret: SECRET, + fetchImpl: fetchImpl as unknown as typeof fetch, + }); + const expected = createHmac("sha256", SECRET).update(Buffer.from(seenBody, "utf8")).digest("base64"); + expect(seenSig).toBe(expected); + expect(seenSig).toBe(signPayload(seenBody, SECRET)); + }); +}); + +describe("openImagineSubmit retry on 503", () => { + it("retries a transient 503 then succeeds", async () => { + const shot = makeShot(); + let calls = 0; + const fetchImpl = vi.fn(async () => { + calls += 1; + if (calls < 3) return jsonResponse(503, { error: "cold" }); + return jsonResponse(200, { job_id: "job-9", status: "queued" }); + }); + const result = await submitShot(shot, { + routerUrl: "https://router.local", + secret: SECRET, + fetchImpl: fetchImpl as unknown as typeof fetch, + maxRetries: 3, + retryDelayMs: 0, + }); + expect(calls).toBe(3); + expect(result.job_id).toBe("job-9"); + }); + + it("throws after exhausting retries on persistent 503", async () => { + const shot = makeShot(); + const fetchImpl = vi.fn(async () => jsonResponse(503, { error: "down" })); + await expect( + submitShot(shot, { + routerUrl: "https://router.local", + secret: SECRET, + fetchImpl: fetchImpl as unknown as typeof fetch, + maxRetries: 2, + retryDelayMs: 0, + }), + ).rejects.toThrow(/503/); + expect(fetchImpl).toHaveBeenCalledTimes(3); + }); +}); diff --git a/packages/director-mode/src/__tests__/presetStore.test.ts b/packages/director-mode/src/__tests__/presetStore.test.ts new file mode 100644 index 0000000..5006a5f --- /dev/null +++ b/packages/director-mode/src/__tests__/presetStore.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect, vi } from "vitest"; +import { PresetStore } from "../core/presetStore.js"; +import { buildShotFromTap } from "../core/shotBuilder.js"; +import type { DirectorShot } from "../schema.js"; + +function memStorage() { + const m = new Map(); + return { + getItem: (k: string) => m.get(k) ?? null, + setItem: (k: string, v: string) => void m.set(k, v), + removeItem: (k: string) => void m.delete(k), + }; +} + +function shot(name: string, tags: string[], id: string): DirectorShot { + return buildShotFromTap({ + name, + position: [0, 0, 5], + product_ref: { product_id: "p", twin_usda_uri: "file://t", brand_rules_id: "default" }, + preset_tags: tags, + id, + now: 0, + }); +} + +describe("PresetStore save/load", () => { + it("round-trips a saved shot and pushes to the remote sync hook", async () => { + const remote = { push: vi.fn(async () => {}) }; + const store = new PresetStore({ storage: memStorage(), remote }); + const s = shot("hero", ["hero"], "01HZX0000000000000000000AA"); + await store.save(s); + expect(store.load(s.id)).toEqual(s); + expect(remote.push).toHaveBeenCalledWith(s); + expect(store.load("missing")).toBeNull(); + }); +}); + +describe("PresetStore tag filter", () => { + it("filters presets by tag and lists all", async () => { + const store = new PresetStore({ storage: memStorage() }); + await store.save(shot("a", ["hero"], "01HZX0000000000000000000A1")); + await store.save(shot("b", ["lifestyle"], "01HZX0000000000000000000A2")); + await store.save(shot("c", ["hero", "pack-shot"], "01HZX0000000000000000000A3")); + expect(store.list()).toHaveLength(3); + expect(store.filterByTag("hero").map((s) => s.name).sort()).toEqual(["a", "c"]); + expect(store.filterByTag("lifestyle").map((s) => s.name)).toEqual(["b"]); + expect(store.filterByTag("none")).toEqual([]); + }); +}); diff --git a/packages/director-mode/src/__tests__/renderProvenance.test.ts b/packages/director-mode/src/__tests__/renderProvenance.test.ts new file mode 100644 index 0000000..a38cc24 --- /dev/null +++ b/packages/director-mode/src/__tests__/renderProvenance.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect } from "vitest"; +import { buildProvenance, stampExternalId, type RenderProvenance } from "../adapter/renderProvenance.js"; +import { buildShotFromTap } from "../core/shotBuilder.js"; +import type { DirectorShot } from "../schema.js"; + +function makeShot(id: string): DirectorShot { + return buildShotFromTap({ + name: "hero", + position: [0, 0, 5], + product_ref: { + product_id: "gid://shopify/ProductVariant/42", + twin_usda_uri: "file://t", + brand_rules_id: "default", + }, + id, + now: 0, + }); +} + +const baseProv: RenderProvenance = { + schema: "render_provenance.v1", + storyboard_id: "sb", + twin_handle: "varitea-jasmine-pearl", + preset: "hero", + observed_at: "2026-06-25T00:00:00.000Z", +}; + +describe("renderProvenance external_id stamp", () => { + it("stamps DirectorShot.id into RenderProvenance.external_id (closes seam #2)", () => { + const shot = makeShot("01HZX0000000000000000000ZZ"); + const stamped = stampExternalId(baseProv, shot); + expect(stamped.external_id).toBe(shot.id); + // original is not mutated + expect(baseProv.external_id).toBeUndefined(); + }); + + it("buildProvenance pre-stamps external_id and carries the product gid", () => { + const shot = makeShot("01HZX0000000000000000000YY"); + const prov = buildProvenance(shot, { twin_handle: "varitea-jasmine-pearl", preset: "hero" }); + expect(prov.external_id).toBe(shot.id); + expect(prov.storyboard_id).toBe(shot.id); + expect(prov.shopify_product_gid).toBe("gid://shopify/ProductVariant/42"); + expect(prov.render_mode).toBe("threejs_diffusion"); + }); +}); + +describe("renderProvenance idempotency", () => { + it("re-stamping with the same shot returns the identical object (no churn)", () => { + const shot = makeShot("01HZX0000000000000000000XX"); + const once = stampExternalId(baseProv, shot); + const twice = stampExternalId(once, shot); + expect(twice).toBe(once); // same reference — no-op + expect(twice.external_id).toBe(shot.id); + }); +}); diff --git a/packages/director-mode/src/__tests__/schema.test.ts b/packages/director-mode/src/__tests__/schema.test.ts new file mode 100644 index 0000000..0a85c39 --- /dev/null +++ b/packages/director-mode/src/__tests__/schema.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from "vitest"; +import { + isAP2ProductRef, + isBezierShotPath, + isDirectorShot, + isUsdCameraPrim, + type DirectorShot, +} from "../schema.js"; + +const camera = { + position: [0, 1, 5] as [number, number, number], + target: [0, 0, 0] as [number, number, number], + focalLength: 50, + fStop: 5.6, + horizontalAperture: 36, +}; + +const path = { + p0: [0, 1, 5] as [number, number, number], + p1: [1, 1, 4] as [number, number, number], + p2: [2, 1, 3] as [number, number, number], + p3: [3, 1, 2] as [number, number, number], +}; + +const productRef = { + product_id: "gid://shopify/ProductVariant/123", + twin_usda_uri: "s3://twins/varitea.usda", + brand_rules_id: "varitea-kinfolk", +}; + +const shot: DirectorShot = { + id: "01HZX0000000000000000000AB", + name: "hero-pour-3s", + camera, + path, + duration_ms: 3000, + product_ref: productRef, + brand_rules_id: "varitea-kinfolk", + created_at: "2026-06-25T00:00:00.000Z", + preset_tags: ["hero"], +}; + +describe("schema type guards", () => { + it("accepts a valid UsdCameraPrim and rejects bad shapes", () => { + expect(isUsdCameraPrim(camera)).toBe(true); + expect(isUsdCameraPrim({ ...camera, position: [0, 1] })).toBe(false); + expect(isUsdCameraPrim({ ...camera, focalLength: "50" })).toBe(false); + expect(isUsdCameraPrim(null)).toBe(false); + }); + + it("accepts a valid BezierShotPath including nested targetPath", () => { + expect(isBezierShotPath(path)).toBe(true); + expect(isBezierShotPath({ ...path, targetPath: path })).toBe(true); + expect(isBezierShotPath({ ...path, p2: [0, 0] })).toBe(false); + expect(isBezierShotPath({ ...path, targetPath: { p0: [0, 0, 0] } })).toBe(false); + }); + + it("accepts a valid AP2ProductRef and rejects missing fields", () => { + expect(isAP2ProductRef(productRef)).toBe(true); + expect(isAP2ProductRef({ product_id: "x", twin_usda_uri: "y" })).toBe(false); + expect(isAP2ProductRef(42)).toBe(false); + }); + + it("accepts a full DirectorShot", () => { + expect(isDirectorShot(shot)).toBe(true); + expect(isDirectorShot({ ...shot, preset_tags: undefined })).toBe(true); + }); + + it("rejects DirectorShot with bad sub-fields or empty id", () => { + expect(isDirectorShot({ ...shot, id: "" })).toBe(false); + expect(isDirectorShot({ ...shot, camera: { ...camera, fStop: "5.6" } })).toBe(false); + expect(isDirectorShot({ ...shot, duration_ms: 0 })).toBe(false); + expect(isDirectorShot({ ...shot, preset_tags: [1, 2] })).toBe(false); + }); +}); diff --git a/packages/director-mode/src/__tests__/shotBuilder.test.ts b/packages/director-mode/src/__tests__/shotBuilder.test.ts new file mode 100644 index 0000000..3641ddb --- /dev/null +++ b/packages/director-mode/src/__tests__/shotBuilder.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from "vitest"; +import { buildShotFromTap, buildShotFromDrag, mergePath } from "../core/shotBuilder.js"; +import { isDirectorShot, type AP2ProductRef, type BezierShotPath, type Vec3 } from "../schema.js"; + +const productRef: AP2ProductRef = { + product_id: "gid://shopify/ProductVariant/123", + twin_usda_uri: "file://twin.usda", + brand_rules_id: "default", +}; + +describe("buildShotFromTap", () => { + it("places the camera at the tapped point with lens defaults and a still path", () => { + const pos: Vec3 = [1, 2, 3]; + const shot = buildShotFromTap({ name: "tap-shot", position: pos, product_ref: productRef, now: 0 }); + expect(isDirectorShot(shot)).toBe(true); + expect(shot.camera.position).toEqual(pos); + expect(shot.camera.focalLength).toBe(50); + expect(shot.camera.fStop).toBe(5.6); + expect(shot.path.p0).toEqual(pos); + expect(shot.path.p3).toEqual(pos); + expect(shot.brand_rules_id).toBe("default"); + expect(shot.created_at).toBe(new Date(0).toISOString()); + }); +}); + +describe("buildShotFromDrag", () => { + it("fits a Bezier so the camera starts at the first sample and ends at the last", () => { + const samples: Vec3[] = [ + [0, 0, 0], + [1, 0, 0], + [2, 1, 0], + [3, 1, 0], + ]; + const shot = buildShotFromDrag({ name: "drag-shot", samples, product_ref: productRef }); + expect(shot.path.p0).toEqual(samples[0]); + expect(shot.path.p3).toEqual(samples[samples.length - 1]); + expect(shot.camera.position).toEqual(samples[0]); + expect(isDirectorShot(shot)).toBe(true); + }); +}); + +describe("mergePath", () => { + it("merges a targetPath onto an existing shot without mutating the original", () => { + const base = buildShotFromTap({ name: "x", position: [0, 0, 5], product_ref: productRef }); + const targetPath: BezierShotPath = { + p0: [0, 0, 0], + p1: [0, 0, 0], + p2: [0, 0, 0], + p3: [1, 0, 0], + }; + const merged = mergePath(base, { targetPath }); + expect(merged.path.targetPath).toEqual(targetPath); + expect(base.path.targetPath).toBeUndefined(); + expect(merged.path.p0).toEqual(base.path.p0); + }); +}); diff --git a/packages/director-mode/src/__tests__/usdSerializer.test.ts b/packages/director-mode/src/__tests__/usdSerializer.test.ts new file mode 100644 index 0000000..660bc57 --- /dev/null +++ b/packages/director-mode/src/__tests__/usdSerializer.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect } from "vitest"; +import { + bezierPoint, + parseCamera, + serializeCamera, + serializePathAnimation, + toUsda, +} from "../core/usdSerializer.js"; +import { buildShotFromTap } from "../core/shotBuilder.js"; +import type { BezierShotPath, UsdCameraPrim } from "../schema.js"; + +const camera: UsdCameraPrim = { + position: [1.5, 2, -3], + target: [0, 0.5, 0], + focalLength: 85, + fStop: 2.8, + horizontalAperture: 36, +}; + +const path: BezierShotPath = { + p0: [0, 0, 0], + p1: [0, 1, 0], + p2: [1, 1, 0], + p3: [1, 0, 0], +}; + +describe("usdSerializer camera prim", () => { + it("serializes a Camera prim with lens + transform attrs", () => { + const usda = serializeCamera(camera, "Cam"); + expect(usda).toContain('def Camera "Cam"'); + expect(usda).toContain("float focalLength = 85"); + expect(usda).toContain("float fStop = 2.8"); + expect(usda).toContain("xformOp:translate = (1.5, 2.0, -3.0)"); + expect(usda).toContain("director:target = (0.0, 0.5, 0.0)"); + }); +}); + +describe("usdSerializer bezier → xform animation", () => { + it("emits time-sampled translates matching the bezier and frame count", () => { + const anim = serializePathAnimation(path, 1000); // 24fps → 24 frames + expect(anim).toContain("xformOp:translate.timeSamples = {"); + expect(anim).toContain("0: (0.0, 0.0, 0.0)"); + expect(anim).toContain("23: (1.0, 0.0, 0.0)"); + const mid = bezierPoint(path, 0.5); + expect(mid[1]).toBeCloseTo(0.75, 5); // peak of the control hull at t=0.5 + }); +}); + +describe("usdSerializer round-trip", () => { + it("parses a serialized camera back to an equal prim", () => { + const usda = serializeCamera(camera); + expect(parseCamera(usda)).toEqual(camera); + }); + + it("toUsda produces a stage referencing the shot id", () => { + const shot = buildShotFromTap({ + name: "hero", + position: [0, 0, 5], + product_ref: { product_id: "p", twin_usda_uri: "file://t", brand_rules_id: "default" }, + }); + const usda = toUsda(shot); + expect(usda.startsWith("#usda 1.0")).toBe(true); + expect(usda).toContain(shot.id); + expect(usda).toContain('def Xform "DirectorShot"'); + }); +}); diff --git a/packages/director-mode/src/__tests__/validator.test.ts b/packages/director-mode/src/__tests__/validator.test.ts new file mode 100644 index 0000000..573f4a8 --- /dev/null +++ b/packages/director-mode/src/__tests__/validator.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from "vitest"; +import { validateShot } from "../validator/index.js"; +import { buildShotFromTap } from "../core/shotBuilder.js"; +import type { BrandRules, DirectorShot, SubjectFraming } from "../schema.js"; + +function shotWithFocal(focalLength: number): DirectorShot { + const s = buildShotFromTap({ + name: "v", + position: [0, 0, 5], + product_ref: { product_id: "p", twin_usda_uri: "file://t", brand_rules_id: "default" }, + focalLength, + }); + return s; +} + +const goodFraming: SubjectFraming = { x: 0.1, y: 0.2, width: 0.5, height: 0.6 }; + +describe("validateShot composition", () => { + it("returns ok=true when every rule passes", () => { + const result = validateShot(shotWithFocal(50), { framing: goodFraming }); + expect(result.ok).toBe(true); + expect(result.violations).toEqual([]); + }); + + it("returns ok=false and aggregates violations across rules when any blocks", () => { + const badFraming: SubjectFraming = { x: 0.02, y: 0.02, width: 0.94, height: 0.94 }; + const brand: BrandRules = { id: "default" }; + const result = validateShot(shotWithFocal(18), { framing: badFraming, brand }); + expect(result.ok).toBe(false); + const rules = new Set(result.violations.map((v) => v.rule)); + expect(rules.has("focalLength")).toBe(true); + expect(rules.has("kinfolk.composition")).toBe(true); + expect(rules.has("brand.safe_zone")).toBe(true); + }); +}); diff --git a/packages/director-mode/src/adapter/openImagineSubmit.ts b/packages/director-mode/src/adapter/openImagineSubmit.ts new file mode 100644 index 0000000..5ee678e --- /dev/null +++ b/packages/director-mode/src/adapter/openImagineSubmit.ts @@ -0,0 +1,73 @@ +import { createHmac } from "node:crypto"; +import type { DirectorShot } from "../schema.js"; + +export type RenderBackend = "threejs_diffusion"; + +export interface SubmitResult { + job_id: string; + status: string; +} + +export interface SubmitOptions { + routerUrl: string; // base URL of the open-imagine router + secret?: string; // OPEN_IMAGINE_HMAC_SECRET (falls back to env) + backend?: RenderBackend; // default "threejs_diffusion" + fetchImpl?: typeof fetch; // injectable for tests + maxRetries?: number; // retries on 503, default 3 + retryDelayMs?: number; // base backoff, default 0 in tests +} + +// HMAC scheme mirrors the Shopify webhook contract already in this monorepo +// (connectors/shopify/src/primitives/webhooks.ts): +// base64(HMAC-SHA256(rawBody, secret)) +// The router verifies X-Open-Imagine-Signature against the raw request body. +export function signPayload(rawBody: string, secret: string): string { + return createHmac("sha256", secret).update(Buffer.from(rawBody, "utf8")).digest("base64"); +} + +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +// POST a DirectorShot to the open-imagine router's `/render` endpoint, HMAC +// signed. Retries on transient 503 (gpu cold-start / queue saturation). +export async function submitShot(shot: DirectorShot, opts: SubmitOptions): Promise { + const secret = opts.secret ?? process.env.OPEN_IMAGINE_HMAC_SECRET; + if (!secret) throw new Error("openImagineSubmit: missing OPEN_IMAGINE_HMAC_SECRET"); + + const doFetch = opts.fetchImpl ?? fetch; + const backend: RenderBackend = opts.backend ?? "threejs_diffusion"; + const rawBody = JSON.stringify({ shot, backend }); + const signature = signPayload(rawBody, secret); + const url = `${opts.routerUrl.replace(/\/$/, "")}/render`; + const maxRetries = opts.maxRetries ?? 3; + + let lastErr: unknown; + for (let attempt = 0; attempt <= maxRetries; attempt++) { + const res = await doFetch(url, { + method: "POST", + headers: { + "content-type": "application/json", + "x-open-imagine-signature": signature, + "x-open-imagine-timestamp": String(Date.now()), + }, + body: rawBody, + }); + + if (res.status === 503) { + lastErr = new Error("open-imagine router unavailable (503)"); + if (attempt < maxRetries) { + await sleep(opts.retryDelayMs ?? 0); + continue; + } + break; + } + + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(`open-imagine router error ${res.status}: ${text}`); + } + + return (await res.json()) as SubmitResult; + } + + throw lastErr instanceof Error ? lastErr : new Error("openImagineSubmit: exhausted retries"); +} diff --git a/packages/director-mode/src/adapter/renderProvenance.ts b/packages/director-mode/src/adapter/renderProvenance.ts new file mode 100644 index 0000000..0c0b0ca --- /dev/null +++ b/packages/director-mode/src/adapter/renderProvenance.ts @@ -0,0 +1,59 @@ +import type { DirectorShot } from "../schema.js"; + +// Mirror of @shoploop/bandit `RenderProvenance` (packages/bandit/src/types.ts). +// Mirrored — not imported — so director-mode stays decoupled from the bandit +// consumer's deploy, exactly as bandit mirrors its upstream producers. +// +// `external_id` is the JOIN SEAM the bandit added but left dangling: nothing +// populated it, so reward could only be attributed via the loose +// shopify_product_gid → twin_handle fallback. Director Mode is the producer +// that finally stamps it — closing seam #2. +export interface RenderProvenance { + schema: "render_provenance.v1"; + storyboard_id: string; + twin_handle: string; + preset: string; + duration_s?: number; + brand_score?: number; + output_uri?: string; + render_mode?: string; + observed_at: string; + external_id?: string | null; + campaign_id?: string | null; + shopify_product_gid?: string | null; +} + +// Stamp the DirectorShot id into RenderProvenance.external_id. Idempotent: a +// re-stamp with the same shot is a no-op and never clobbers a value that is +// already correct. +export function stampExternalId(prov: RenderProvenance, shot: DirectorShot): RenderProvenance { + if (prov.external_id === shot.id) return prov; + return { ...prov, external_id: shot.id }; +} + +export interface ProvenanceInit { + twin_handle: string; + preset: string; + output_uri?: string; + brand_score?: number; + render_mode?: string; + observed_at?: string; +} + +// Build a fully-formed RenderProvenance for a shot, with the seam pre-stamped. +// `storyboard_id` and `external_id` both carry the shot id so the bandit's +// external_id join resolves exactly to the arm that produced the creative. +export function buildProvenance(shot: DirectorShot, init: ProvenanceInit): RenderProvenance { + return { + schema: "render_provenance.v1", + storyboard_id: shot.id, + twin_handle: init.twin_handle, + preset: init.preset, + ...(init.output_uri ? { output_uri: init.output_uri } : {}), + ...(init.brand_score !== undefined ? { brand_score: init.brand_score } : {}), + render_mode: init.render_mode ?? "threejs_diffusion", + observed_at: init.observed_at ?? new Date().toISOString(), + external_id: shot.id, + shopify_product_gid: shot.product_ref.product_id, + }; +} diff --git a/packages/director-mode/src/brand_rules.json b/packages/director-mode/src/brand_rules.json new file mode 100644 index 0000000..2bb9bc3 --- /dev/null +++ b/packages/director-mode/src/brand_rules.json @@ -0,0 +1,36 @@ +{ + "default": { + "id": "default", + "allow_bleed": false, + "focal_overrides": [], + "composition": { + "subjectWidthMin": 0.38, + "subjectWidthMax": 0.62, + "minNegativeSpace": 0.3 + }, + "safe_zone": { + "edgeBuffer": 0.05 + }, + "focal": { + "min": 24, + "max": 135 + } + }, + "varitea-kinfolk": { + "id": "varitea-kinfolk", + "allow_bleed": false, + "focal_overrides": [200], + "composition": { + "subjectWidthMin": 0.38, + "subjectWidthMax": 0.62, + "minNegativeSpace": 0.35 + }, + "safe_zone": { + "edgeBuffer": 0.06 + }, + "focal": { + "min": 35, + "max": 135 + } + } +} diff --git a/packages/director-mode/src/core/presetStore.ts b/packages/director-mode/src/core/presetStore.ts new file mode 100644 index 0000000..7152ef7 --- /dev/null +++ b/packages/director-mode/src/core/presetStore.ts @@ -0,0 +1,80 @@ +import type { DirectorShot } from "../schema.js"; + +const KEY = "director-mode.presets.v1"; + +type StorageLike = Pick; + +function defaultStorage(): StorageLike { + if (typeof globalThis !== "undefined" && (globalThis as { localStorage?: StorageLike }).localStorage) { + return (globalThis as unknown as { localStorage: StorageLike }).localStorage; + } + // In-memory fallback for non-browser contexts (SSR, tests without jsdom). + const mem = new Map(); + return { + getItem: (k) => mem.get(k) ?? null, + setItem: (k, v) => void mem.set(k, v), + removeItem: (k) => void mem.delete(k), + }; +} + +// Optional remote sync hook. v0 ships a no-op stub; the future Shoploop +// dashboard injects a real implementation (POST to a presets service). +export interface RemoteSync { + push(shot: DirectorShot): Promise; +} + +export const noopRemoteSync: RemoteSync = { + async push() { + /* stub: wired by the dashboard later */ + }, +}; + +export class PresetStore { + private storage: StorageLike; + private remote: RemoteSync; + + constructor(opts: { storage?: StorageLike; remote?: RemoteSync } = {}) { + this.storage = opts.storage ?? defaultStorage(); + this.remote = opts.remote ?? noopRemoteSync; + } + + private readAll(): Record { + const raw = this.storage.getItem(KEY); + if (!raw) return {}; + try { + return JSON.parse(raw) as Record; + } catch { + return {}; + } + } + + private writeAll(map: Record): void { + this.storage.setItem(KEY, JSON.stringify(map)); + } + + async save(shot: DirectorShot): Promise { + const map = this.readAll(); + map[shot.id] = shot; + this.writeAll(map); + await this.remote.push(shot); + return shot; + } + + load(id: string): DirectorShot | null { + return this.readAll()[id] ?? null; + } + + list(): DirectorShot[] { + return Object.values(this.readAll()); + } + + filterByTag(tag: string): DirectorShot[] { + return this.list().filter((s) => (s.preset_tags ?? []).includes(tag)); + } + + remove(id: string): void { + const map = this.readAll(); + delete map[id]; + this.writeAll(map); + } +} diff --git a/packages/director-mode/src/core/shotBuilder.ts b/packages/director-mode/src/core/shotBuilder.ts new file mode 100644 index 0000000..1f0e9d1 --- /dev/null +++ b/packages/director-mode/src/core/shotBuilder.ts @@ -0,0 +1,116 @@ +import type { + AP2ProductRef, + BezierShotPath, + DirectorShot, + UsdCameraPrim, + Vec3, +} from "../schema.js"; + +export const DEFAULT_CAMERA = { + focalLength: 50, + fStop: 5.6, + horizontalAperture: 36, +} as const; + +const ULID_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; // Crockford base32 + +// Lexicographically sortable ULID: 48-bit time prefix + 80-bit randomness. +// No external dependency — DirectorShot ids must be portable and offline-safe. +export function generateUlid(now: number = Date.now()): string { + let time = ""; + let t = now; + for (let i = 0; i < 10; i++) { + time = ULID_ALPHABET[t % 32] + time; + t = Math.floor(t / 32); + } + let rand = ""; + for (let i = 0; i < 16; i++) { + rand += ULID_ALPHABET[Math.floor(Math.random() * 32)]; + } + return time + rand; +} + +function staticPath(at: Vec3): BezierShotPath { + return { p0: at, p1: at, p2: at, p3: at }; +} + +export interface TapInput { + name: string; + position: Vec3; + target?: Vec3; + product_ref: AP2ProductRef; + brand_rules_id?: string; + duration_ms?: number; + focalLength?: number; + fStop?: number; + horizontalAperture?: number; + preset_tags?: string[]; + id?: string; + now?: number; +} + +// Tap-to-drop: place a camera at the tapped world point. Produces a still shot +// (degenerate Bezier where all control points coincide) ready to be extended +// later via PathDrawTool → buildShotFromDrag / mergePath. +export function buildShotFromTap(input: TapInput): DirectorShot { + const now = input.now ?? Date.now(); + const camera: UsdCameraPrim = { + position: input.position, + target: input.target ?? [0, 0, 0], + focalLength: input.focalLength ?? DEFAULT_CAMERA.focalLength, + fStop: input.fStop ?? DEFAULT_CAMERA.fStop, + horizontalAperture: input.horizontalAperture ?? DEFAULT_CAMERA.horizontalAperture, + }; + return { + id: input.id ?? generateUlid(now), + name: input.name, + camera, + path: staticPath(input.position), + duration_ms: input.duration_ms ?? 3000, + product_ref: input.product_ref, + brand_rules_id: input.brand_rules_id ?? input.product_ref.brand_rules_id, + created_at: new Date(now).toISOString(), + ...(input.preset_tags ? { preset_tags: input.preset_tags } : {}), + }; +} + +export interface DragInput extends Omit { + // World-space samples captured while dragging. First = camera start, + // last = camera end. Control points are fit at the 1/3 and 2/3 marks. + samples: Vec3[]; +} + +function lerp(a: Vec3, b: Vec3, f: number): Vec3 { + return [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f, a[2] + (b[2] - a[2]) * f]; +} + +function fitBezier(samples: Vec3[]): BezierShotPath { + if (samples.length === 0) { + const o: Vec3 = [0, 0, 0]; + return { p0: o, p1: o, p2: o, p3: o }; + } + if (samples.length === 1) return staticPath(samples[0]); + const p0 = samples[0]; + const p3 = samples[samples.length - 1]; + const mid = (i: number) => samples[Math.min(samples.length - 1, Math.round((samples.length - 1) * i))]; + const p1 = samples.length >= 4 ? mid(1 / 3) : lerp(p0, p3, 1 / 3); + const p2 = samples.length >= 4 ? mid(2 / 3) : lerp(p0, p3, 2 / 3); + return { p0, p1, p2, p3 }; +} + +// Drag-to-draw: fit a cubic Bezier through dragged samples. Camera start state +// is anchored at p0. +export function buildShotFromDrag(input: DragInput): DirectorShot { + const path = fitBezier(input.samples); + const { samples: _samples, ...rest } = input; + return { + ...buildShotFromTap({ ...rest, position: path.p0 }), + path, + }; +} + +// Merge new path segments (e.g. an animated look-at targetPath, or refit +// control points) onto an existing shot without mutating the original. +export function mergePath(shot: DirectorShot, patch: Partial): DirectorShot { + return { ...shot, path: { ...shot.path, ...patch } }; +} diff --git a/packages/director-mode/src/core/usdSerializer.ts b/packages/director-mode/src/core/usdSerializer.ts new file mode 100644 index 0000000..ac2a7f1 --- /dev/null +++ b/packages/director-mode/src/core/usdSerializer.ts @@ -0,0 +1,109 @@ +import type { BezierShotPath, DirectorShot, UsdCameraPrim, Vec3 } from "../schema.js"; + +const FPS = 24; + +function fmt(n: number): string { + // Stable, round-trippable number formatting (no trailing-zero noise). + return Number.isInteger(n) ? n.toFixed(1) : String(n); +} + +function tuple(v: Vec3): string { + return `(${fmt(v[0])}, ${fmt(v[1])}, ${fmt(v[2])})`; +} + +// Cubic Bezier evaluation at t ∈ [0,1]. +export function bezierPoint(path: BezierShotPath, t: number): Vec3 { + const u = 1 - t; + const w0 = u * u * u; + const w1 = 3 * u * u * t; + const w2 = 3 * u * t * t; + const w3 = t * t * t; + return [ + w0 * path.p0[0] + w1 * path.p1[0] + w2 * path.p2[0] + w3 * path.p3[0], + w0 * path.p0[1] + w1 * path.p1[1] + w2 * path.p2[1] + w3 * path.p3[1], + w0 * path.p0[2] + w1 * path.p1[2] + w2 * path.p2[2] + w3 * path.p3[2], + ]; +} + +// OpenUSD-compatible Camera prim. `director:target` is stamped as custom +// metadata so the look-at survives a serialize → parse round-trip. +export function serializeCamera(camera: UsdCameraPrim, name = "DirectorCam"): string { + return [ + `def Camera "${name}"`, + `{`, + ` float focalLength = ${fmt(camera.focalLength)}`, + ` float fStop = ${fmt(camera.fStop)}`, + ` float horizontalAperture = ${fmt(camera.horizontalAperture)}`, + ` double3 xformOp:translate = ${tuple(camera.position)}`, + ` uniform token[] xformOpOrder = ["xformOp:translate"]`, + ` custom double3 director:target = ${tuple(camera.target)}`, + `}`, + ].join("\n"); +} + +function parseTuple(s: string): Vec3 { + const m = s.match(/\(([^)]+)\)/); + if (!m) throw new Error(`usda: cannot parse vec3 from "${s}"`); + const parts = m[1].split(",").map((p) => Number(p.trim())); + if (parts.length !== 3 || parts.some((n) => !Number.isFinite(n))) { + throw new Error(`usda: malformed vec3 "${s}"`); + } + return [parts[0], parts[1], parts[2]]; +} + +function grabFloat(usda: string, attr: string): number { + const m = usda.match(new RegExp(`float\\s+${attr}\\s*=\\s*([-0-9.eE]+)`)); + if (!m) throw new Error(`usda: missing float attr "${attr}"`); + return Number(m[1]); +} + +// Inverse of serializeCamera. Round-trips position, target, and lens attrs. +export function parseCamera(usda: string): UsdCameraPrim { + const posLine = usda.match(/xformOp:translate\s*=\s*(\([^)]+\))/); + const targetLine = usda.match(/director:target\s*=\s*(\([^)]+\))/); + if (!posLine || !targetLine) throw new Error("usda: missing camera transform/target"); + return { + position: parseTuple(posLine[1]), + target: parseTuple(targetLine[1]), + focalLength: grabFloat(usda, "focalLength"), + fStop: grabFloat(usda, "fStop"), + horizontalAperture: grabFloat(usda, "horizontalAperture"), + }; +} + +// Bezier path → time-sampled xformOp:translate animation. Sample count is +// derived from duration at FPS so playback matches the shot's duration_ms. +export function serializePathAnimation(path: BezierShotPath, durationMs: number): string { + const frames = Math.max(2, Math.round((durationMs / 1000) * FPS)); + const samples: string[] = []; + for (let i = 0; i < frames; i++) { + const t = i / (frames - 1); + samples.push(` ${i}: ${tuple(bezierPoint(path, t))},`); + } + return [ + ` double3 xformOp:translate.timeSamples = {`, + ...samples, + ` }`, + ].join("\n"); +} + +// Full DirectorShot → .usda stage: an animated camera Xform driven by the path. +export function toUsda(shot: DirectorShot): string { + return [ + `#usda 1.0`, + `(`, + ` defaultPrim = "DirectorShot"`, + ` doc = "Director Mode shot ${shot.id} (${shot.name})"`, + ` timeCodesPerSecond = ${FPS}`, + ` endTimeCode = ${Math.max(1, Math.round((shot.duration_ms / 1000) * FPS) - 1)}`, + `)`, + ``, + `def Xform "DirectorShot"`, + `{`, + serializeCamera(shot.camera).replace(/^/gm, " "), + ``, + serializePathAnimation(shot.path, shot.duration_ms), + ` uniform token[] xformOpOrder = ["xformOp:translate"]`, + `}`, + ].join("\n"); +} diff --git a/packages/director-mode/src/index.ts b/packages/director-mode/src/index.ts new file mode 100644 index 0000000..f75e1b5 --- /dev/null +++ b/packages/director-mode/src/index.ts @@ -0,0 +1,52 @@ +// @shoploop/director-mode — public API. +// Render-backend-agnostic shot authoring for product twins. Emits portable +// DirectorShot JSON; render path = open-imagine router → threejs_diffusion. +// Omniverse-free by design (no Kit App Streaming, no OVRTX, no web-viewer-sample). + +export * from "./schema.js"; + +// core +export { + buildShotFromTap, + buildShotFromDrag, + mergePath, + generateUlid, + DEFAULT_CAMERA, +} from "./core/shotBuilder.js"; +export type { TapInput, DragInput } from "./core/shotBuilder.js"; +export { + serializeCamera, + parseCamera, + serializePathAnimation, + bezierPoint, + toUsda, +} from "./core/usdSerializer.js"; +export { PresetStore, noopRemoteSync } from "./core/presetStore.js"; +export type { RemoteSync } from "./core/presetStore.js"; + +// validator +export { + validateShot, + kinfolkRule, + brandSafeZone, + focalLengthGate, +} from "./validator/index.js"; +export type { ValidationContext } from "./validator/index.js"; + +// adapter +export { submitShot, signPayload } from "./adapter/openImagineSubmit.js"; +export type { SubmitOptions, SubmitResult, RenderBackend } from "./adapter/openImagineSubmit.js"; +export { stampExternalId, buildProvenance } from "./adapter/renderProvenance.js"; +export type { RenderProvenance, ProvenanceInit } from "./adapter/renderProvenance.js"; + +// ui +export { DirectorOverlay } from "./ui/DirectorOverlay.js"; +export type { DirectorOverlayProps } from "./ui/DirectorOverlay.js"; +export { CameraDropTool } from "./ui/CameraDropTool.js"; +export type { CameraDropToolProps } from "./ui/CameraDropTool.js"; +export { PathDrawTool } from "./ui/PathDrawTool.js"; +export type { PathDrawToolProps } from "./ui/PathDrawTool.js"; +export { Scrubber } from "./ui/Scrubber.js"; +export type { ScrubberProps } from "./ui/Scrubber.js"; +export { PresetLibrary } from "./ui/PresetLibrary.js"; +export type { PresetLibraryProps } from "./ui/PresetLibrary.js"; diff --git a/packages/director-mode/src/schema.ts b/packages/director-mode/src/schema.ts new file mode 100644 index 0000000..735f81f --- /dev/null +++ b/packages/director-mode/src/schema.ts @@ -0,0 +1,143 @@ +// Portable, render-backend-agnostic shot schema. A DirectorShot is the single +// artifact Director Mode emits; any backend (threejs_diffusion today, Kit/OVRTX +// later) consumes it. Types here are intentionally OpenUSD-shaped so the +// usdSerializer can round-trip them to .usda without re-deriving structure. + +export type Vec3 = [number, number, number]; + +export interface UsdCameraPrim { + position: Vec3; // world-space xyz + target: Vec3; // look-at xyz + focalLength: number; // mm, default 50 + fStop: number; // f-number, default 5.6 + horizontalAperture: number; // mm, default 36 (full-frame) +} + +export interface BezierShotPath { + p0: Vec3; // start (= camera.position) + p1: Vec3; // control 1 + p2: Vec3; // control 2 + p3: Vec3; // end + targetPath?: BezierShotPath; // optional look-at animation +} + +export interface AP2ProductRef { + product_id: string; // Shopify variant gid + twin_usda_uri: string; // s3://... or file:// + brand_rules_id: string; // ref to brand_rules.json entry +} + +export interface DirectorShot { + id: string; // ulid + name: string; // "hero-pour-3s", etc. + camera: UsdCameraPrim; // start state + path: BezierShotPath; + duration_ms: number; // 500–10000 + product_ref: AP2ProductRef; + brand_rules_id: string; + created_at: string; // ISO8601 + preset_tags?: string[]; // ["hero", "pack-shot", "lifestyle"] +} + +export interface ShotValidationResult { + ok: boolean; + violations: Array<{ + rule: string; // "kinfolk.composition", "brand.safe_zone", etc. + severity: "block" | "warn"; + message: string; + }>; +} + +// Normalized [0,1] bounding box of the subject within the rendered frame. +// (0,0) = top-left, (1,1) = bottom-right. Produced by the UI from the +// twin-renderer projection; consumed by the framing validators. +export interface SubjectFraming { + x: number; // left edge + y: number; // top edge + width: number; // fraction of frame width + height: number; // fraction of frame height +} + +// Per-brand rule overrides. Loaded from brand_rules.json; every threshold has a +// sane default so a missing brand entry still validates against Kinfolk defaults. +export interface BrandRules { + id: string; + allow_bleed?: boolean; // when true, safe-zone edge buffer is not enforced + focal_overrides?: number[]; // focal lengths whitelisted outside the default gate + composition?: { + subjectWidthMin?: number; // default 0.38 + subjectWidthMax?: number; // default 0.62 + minNegativeSpace?: number; // default 0.30 + }; + safe_zone?: { + edgeBuffer?: number; // default 0.05 (5%) + }; + focal?: { + min?: number; // default 24 + max?: number; // default 135 + }; +} + +// --- Type guards ---------------------------------------------------------- + +function isVec3(v: unknown): v is Vec3 { + return ( + Array.isArray(v) && + v.length === 3 && + v.every((n) => typeof n === "number" && Number.isFinite(n)) + ); +} + +export function isUsdCameraPrim(v: unknown): v is UsdCameraPrim { + if (typeof v !== "object" || v === null) return false; + const c = v as Record; + return ( + isVec3(c.position) && + isVec3(c.target) && + typeof c.focalLength === "number" && + typeof c.fStop === "number" && + typeof c.horizontalAperture === "number" + ); +} + +export function isBezierShotPath(v: unknown): v is BezierShotPath { + if (typeof v !== "object" || v === null) return false; + const p = v as Record; + if (!isVec3(p.p0) || !isVec3(p.p1) || !isVec3(p.p2) || !isVec3(p.p3)) { + return false; + } + if (p.targetPath !== undefined && !isBezierShotPath(p.targetPath)) { + return false; + } + return true; +} + +export function isAP2ProductRef(v: unknown): v is AP2ProductRef { + if (typeof v !== "object" || v === null) return false; + const r = v as Record; + return ( + typeof r.product_id === "string" && + typeof r.twin_usda_uri === "string" && + typeof r.brand_rules_id === "string" + ); +} + +export function isDirectorShot(v: unknown): v is DirectorShot { + if (typeof v !== "object" || v === null) return false; + const s = v as Record; + if (typeof s.id !== "string" || s.id.length === 0) return false; + if (typeof s.name !== "string") return false; + if (!isUsdCameraPrim(s.camera)) return false; + if (!isBezierShotPath(s.path)) return false; + if (typeof s.duration_ms !== "number" || s.duration_ms <= 0) return false; + if (!isAP2ProductRef(s.product_ref)) return false; + if (typeof s.brand_rules_id !== "string") return false; + if (typeof s.created_at !== "string") return false; + if ( + s.preset_tags !== undefined && + !(Array.isArray(s.preset_tags) && s.preset_tags.every((t) => typeof t === "string")) + ) { + return false; + } + return true; +} diff --git a/packages/director-mode/src/ui/CameraDropTool.tsx b/packages/director-mode/src/ui/CameraDropTool.tsx new file mode 100644 index 0000000..959f8fb --- /dev/null +++ b/packages/director-mode/src/ui/CameraDropTool.tsx @@ -0,0 +1,40 @@ +import React, { useCallback } from "react"; +import type { Vec3 } from "../schema.js"; + +export interface CameraDropToolProps { + // Maps a normalized click ([0,1] x/y on the canvas) to a world-space point. + // The host (twin-renderer) supplies the unprojection; Director Mode stays + // backend-agnostic and never touches Three.js directly. + unproject: (nx: number, ny: number) => Vec3; + onDrop: (worldPos: Vec3) => void; + active?: boolean; +} + +// Tap-to-place camera. A transparent layer over the twin-renderer canvas that +// converts a tap into a world-space camera position. +export function CameraDropTool({ unproject, onDrop, active = true }: CameraDropToolProps): React.JSX.Element { + const handleClick = useCallback( + (e: React.MouseEvent) => { + if (!active) return; + const rect = e.currentTarget.getBoundingClientRect(); + const nx = (e.clientX - rect.left) / rect.width; + const ny = (e.clientY - rect.top) / rect.height; + onDrop(unproject(nx, ny)); + }, + [active, unproject, onDrop], + ); + + return ( +
+ ); +} diff --git a/packages/director-mode/src/ui/DirectorOverlay.tsx b/packages/director-mode/src/ui/DirectorOverlay.tsx new file mode 100644 index 0000000..3f36ed8 --- /dev/null +++ b/packages/director-mode/src/ui/DirectorOverlay.tsx @@ -0,0 +1,111 @@ +import React, { useCallback, useMemo, useState } from "react"; +import type { AP2ProductRef, BrandRules, DirectorShot, SubjectFraming, Vec3 } from "../schema.js"; +import { buildShotFromDrag, buildShotFromTap } from "../core/shotBuilder.js"; +import { PresetStore } from "../core/presetStore.js"; +import { validateShot } from "../validator/index.js"; +import { CameraDropTool } from "./CameraDropTool.js"; +import { PathDrawTool } from "./PathDrawTool.js"; +import { Scrubber } from "./Scrubber.js"; +import { PresetLibrary } from "./PresetLibrary.js"; + +type Mode = "camera" | "path"; + +export interface DirectorOverlayProps { + productRef: AP2ProductRef; + unproject: (nx: number, ny: number) => Vec3; + brand?: BrandRules; + // Live subject bounding box from the renderer projection, for validation. + framing?: SubjectFraming; + store?: PresetStore; + onChange?: (shot: DirectorShot) => void; +} + +// React overlay that sits on the twin-renderer canvas. Owns the in-progress +// DirectorShot, wires the drop/draw/scrub tools, runs brand-safe validation +// live, and persists presets. Backend-agnostic: it only emits DirectorShot. +export function DirectorOverlay({ + productRef, + unproject, + brand, + framing, + store, + onChange, +}: DirectorOverlayProps): React.JSX.Element { + const presetStore = useMemo(() => store ?? new PresetStore(), [store]); + const [mode, setMode] = useState("camera"); + const [shot, setShot] = useState(null); + const [playheadMs, setPlayheadMs] = useState(0); + const [revision, setRevision] = useState(0); + + const update = useCallback( + (next: DirectorShot) => { + setShot(next); + onChange?.(next); + }, + [onChange], + ); + + const handleDrop = useCallback( + (pos: Vec3) => { + update(buildShotFromTap({ name: shot?.name ?? "untitled-shot", position: pos, product_ref: productRef })); + }, + [shot, productRef, update], + ); + + const handlePath = useCallback( + (samples: Vec3[]) => { + update(buildShotFromDrag({ name: shot?.name ?? "untitled-shot", samples, product_ref: productRef })); + }, + [shot, productRef, update], + ); + + const validation = useMemo( + () => (shot ? validateShot(shot, { brand, framing }) : null), + [shot, brand, framing], + ); + + return ( +
+ {mode === "camera" ? ( + + ) : ( + + )} + +
+ + +
+ + {validation && !validation.ok && ( +
    + {validation.violations.map((v, i) => ( +
  • {`${v.rule}: ${v.message}`}
  • + ))} +
+ )} + +
+ {shot && ( + + )} +
+ +
+ { + update(p); + setRevision((r) => r + 1); + }} + /> +
+
+ ); +} diff --git a/packages/director-mode/src/ui/PathDrawTool.tsx b/packages/director-mode/src/ui/PathDrawTool.tsx new file mode 100644 index 0000000..c592480 --- /dev/null +++ b/packages/director-mode/src/ui/PathDrawTool.tsx @@ -0,0 +1,80 @@ +import React, { useCallback, useRef, useState } from "react"; +import type { Vec3 } from "../schema.js"; + +export interface PathDrawToolProps { + unproject: (nx: number, ny: number) => Vec3; + // Receives the world-space samples captured during the drag; feed into + // buildShotFromDrag to fit a Bezier. + onPathComplete: (samples: Vec3[]) => void; + active?: boolean; +} + +// Drag-to-draw shot path. Captures pointer samples while dragging and emits the +// world-space polyline; an SVG trail gives live feedback. +export function PathDrawTool({ unproject, onPathComplete, active = true }: PathDrawToolProps): React.JSX.Element { + const [trail, setTrail] = useState>([]); + const samples = useRef([]); + const drawing = useRef(false); + + const toNorm = (e: React.PointerEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + return { + nx: (e.clientX - rect.left) / rect.width, + ny: (e.clientY - rect.top) / rect.height, + px: e.clientX - rect.left, + py: e.clientY - rect.top, + }; + }; + + const onDown = useCallback( + (e: React.PointerEvent) => { + if (!active) return; + drawing.current = true; + samples.current = []; + setTrail([]); + const { nx, ny, px, py } = toNorm(e); + samples.current.push(unproject(nx, ny)); + setTrail([{ x: px, y: py }]); + }, + [active, unproject], + ); + + const onMove = useCallback( + (e: React.PointerEvent) => { + if (!active || !drawing.current) return; + const { nx, ny, px, py } = toNorm(e); + samples.current.push(unproject(nx, ny)); + setTrail((t) => [...t, { x: px, y: py }]); + }, + [active, unproject], + ); + + const onUp = useCallback(() => { + if (!active || !drawing.current) return; + drawing.current = false; + if (samples.current.length > 0) onPathComplete(samples.current); + }, [active, onPathComplete]); + + return ( + + `${p.x},${p.y}`).join(" ")} + /> + + ); +} diff --git a/packages/director-mode/src/ui/PresetLibrary.tsx b/packages/director-mode/src/ui/PresetLibrary.tsx new file mode 100644 index 0000000..7ee35b9 --- /dev/null +++ b/packages/director-mode/src/ui/PresetLibrary.tsx @@ -0,0 +1,52 @@ +import React, { useMemo, useState } from "react"; +import type { DirectorShot } from "../schema.js"; +import { PresetStore } from "../core/presetStore.js"; + +export interface PresetLibraryProps { + store: PresetStore; + current?: DirectorShot; + onLoad: (shot: DirectorShot) => void; + // Bumped by the host after a save so the list re-reads from storage. + revision?: number; +} + +// Save/load preset shots. Tag filter mirrors PresetStore.filterByTag. +export function PresetLibrary({ store, current, onLoad, revision = 0 }: PresetLibraryProps): React.JSX.Element { + const [tag, setTag] = useState(""); + + const presets = useMemo(() => { + void revision; // re-read storage when the host signals a change + return tag ? store.filterByTag(tag) : store.list(); + }, [store, tag, revision]); + + return ( +
+
+ + setTag(e.target.value)} + aria-label="filter by tag" + /> +
+
    + {presets.map((p) => ( +
  • + +
  • + ))} +
+
+ ); +} diff --git a/packages/director-mode/src/ui/Scrubber.tsx b/packages/director-mode/src/ui/Scrubber.tsx new file mode 100644 index 0000000..ccff736 --- /dev/null +++ b/packages/director-mode/src/ui/Scrubber.tsx @@ -0,0 +1,45 @@ +import React, { useCallback } from "react"; + +export interface ScrubberProps { + durationMs: number; + valueMs: number; + onScrub: (ms: number) => void; + playing?: boolean; + onTogglePlay?: () => void; +} + +// Timeline scrubber for previewing the shot. Emits the current playhead in ms; +// the host samples the Bezier (usdSerializer.bezierPoint) to position the camera. +export function Scrubber({ + durationMs, + valueMs, + onScrub, + playing = false, + onTogglePlay, +}: ScrubberProps): React.JSX.Element { + const handleChange = useCallback( + (e: React.ChangeEvent) => onScrub(Number(e.target.value)), + [onScrub], + ); + + return ( +
+ + + + {(valueMs / 1000).toFixed(2)}s + +
+ ); +} diff --git a/packages/director-mode/src/validator/brandSafeZone.ts b/packages/director-mode/src/validator/brandSafeZone.ts new file mode 100644 index 0000000..daa8550 --- /dev/null +++ b/packages/director-mode/src/validator/brandSafeZone.ts @@ -0,0 +1,53 @@ +import type { BrandRules, ShotValidationResult, SubjectFraming } from "../schema.js"; + +type Violation = ShotValidationResult["violations"][number]; + +const DEFAULT_BUFFER = 0.05; + +// brand.safe_zone: the product bounding box must never come within `edgeBuffer` +// (default 5%) of any frame edge — unless the brand opts into `allow_bleed`. +// A box that extends past the frame ([0,1]) is a crop; a box fully outside the +// frame is a framing error. Both block. +export function brandSafeZone(framing: SubjectFraming, brand?: BrandRules): Violation[] { + const buffer = brand?.safe_zone?.edgeBuffer ?? DEFAULT_BUFFER; + const left = framing.x; + const top = framing.y; + const right = framing.x + framing.width; + const bottom = framing.y + framing.height; + + // Fully outside the visible frame. + if (right <= 0 || bottom <= 0 || left >= 1 || top >= 1) { + return [ + { + rule: "brand.safe_zone", + severity: "block", + message: "product is outside the frame", + }, + ]; + } + + // Cropped by the frame boundary. + if (left < 0 || top < 0 || right > 1 || bottom > 1) { + return [ + { + rule: "brand.safe_zone", + severity: "block", + message: "product is cropped by the frame edge", + }, + ]; + } + + if (brand?.allow_bleed) return []; + + const violations: Violation[] = []; + const tooClose = + left < buffer || top < buffer || right > 1 - buffer || bottom > 1 - buffer; + if (tooClose) { + violations.push({ + rule: "brand.safe_zone", + severity: "block", + message: `product within ${(buffer * 100).toFixed(0)}% safe-zone buffer (set allow_bleed to permit)`, + }); + } + return violations; +} diff --git a/packages/director-mode/src/validator/focalLengthGate.ts b/packages/director-mode/src/validator/focalLengthGate.ts new file mode 100644 index 0000000..6308833 --- /dev/null +++ b/packages/director-mode/src/validator/focalLengthGate.ts @@ -0,0 +1,26 @@ +import type { BrandRules, ShotValidationResult, UsdCameraPrim } from "../schema.js"; + +type Violation = ShotValidationResult["violations"][number]; + +const DEFAULT_MIN = 24; +const DEFAULT_MAX = 135; + +// focalLength gate: 24–135mm allowed by default. Outside the range blocks the +// shot unless the brand whitelists that exact lens via `focal_overrides`. +export function focalLengthGate(camera: UsdCameraPrim, brand?: BrandRules): Violation[] { + const min = brand?.focal?.min ?? DEFAULT_MIN; + const max = brand?.focal?.max ?? DEFAULT_MAX; + const overrides = brand?.focal_overrides ?? []; + const fl = camera.focalLength; + + if (fl >= min && fl <= max) return []; + if (overrides.includes(fl)) return []; + + return [ + { + rule: "focalLength", + severity: "block", + message: `focal length ${fl}mm outside allowed ${min}–${max}mm and not in brand focal_overrides`, + }, + ]; +} diff --git a/packages/director-mode/src/validator/index.ts b/packages/director-mode/src/validator/index.ts new file mode 100644 index 0000000..8a6098d --- /dev/null +++ b/packages/director-mode/src/validator/index.ts @@ -0,0 +1,35 @@ +import type { + BrandRules, + DirectorShot, + ShotValidationResult, + SubjectFraming, +} from "../schema.js"; +import { brandSafeZone } from "./brandSafeZone.js"; +import { focalLengthGate } from "./focalLengthGate.js"; +import { kinfolkRule } from "./kinfolkRule.js"; + +export { brandSafeZone } from "./brandSafeZone.js"; +export { focalLengthGate } from "./focalLengthGate.js"; +export { kinfolkRule } from "./kinfolkRule.js"; + +export interface ValidationContext { + brand?: BrandRules; + // Projected subject bounding box. Required for the composition + safe-zone + // rules; when absent those rules are skipped (focal gate still runs). + framing?: SubjectFraming; +} + +// Compose every brand-safety rule. `ok` is false if ANY violation is +// block-severity; warn-severity violations are surfaced but do not block. +export function validateShot(shot: DirectorShot, ctx: ValidationContext = {}): ShotValidationResult { + const violations: ShotValidationResult["violations"] = []; + + violations.push(...focalLengthGate(shot.camera, ctx.brand)); + if (ctx.framing) { + violations.push(...kinfolkRule(ctx.framing, ctx.brand)); + violations.push(...brandSafeZone(ctx.framing, ctx.brand)); + } + + const ok = !violations.some((v) => v.severity === "block"); + return { ok, violations }; +} diff --git a/packages/director-mode/src/validator/kinfolkRule.ts b/packages/director-mode/src/validator/kinfolkRule.ts new file mode 100644 index 0000000..63abc19 --- /dev/null +++ b/packages/director-mode/src/validator/kinfolkRule.ts @@ -0,0 +1,39 @@ +import type { BrandRules, ShotValidationResult, SubjectFraming } from "../schema.js"; + +type Violation = ShotValidationResult["violations"][number]; + +const DEFAULTS = { widthMin: 0.38, widthMax: 0.62, minNegativeSpace: 0.3 }; + +// Kinfolk composition: the subject must occupy 38–62% of frame width +// (rule-of-thirds tolerance) AND leave ≥30% negative space on at least one +// side (asymmetric balance). Both are block-severity — a frame that fails +// either would not fit a Kinfolk spread. +export function kinfolkRule(framing: SubjectFraming, brand?: BrandRules): Violation[] { + const c = brand?.composition ?? {}; + const widthMin = c.subjectWidthMin ?? DEFAULTS.widthMin; + const widthMax = c.subjectWidthMax ?? DEFAULTS.widthMax; + const minNeg = c.minNegativeSpace ?? DEFAULTS.minNegativeSpace; + + const violations: Violation[] = []; + + if (framing.width < widthMin || framing.width > widthMax) { + violations.push({ + rule: "kinfolk.composition", + severity: "block", + message: `subject width ${(framing.width * 100).toFixed(0)}% outside ${(widthMin * 100).toFixed(0)}–${(widthMax * 100).toFixed(0)}% rule-of-thirds band`, + }); + } + + const leftSpace = framing.x; + const rightSpace = 1 - (framing.x + framing.width); + const negSpace = Math.max(leftSpace, rightSpace); + if (negSpace < minNeg) { + violations.push({ + rule: "kinfolk.composition", + severity: "block", + message: `negative space ${(negSpace * 100).toFixed(0)}% below required ${(minNeg * 100).toFixed(0)}% on both sides`, + }); + } + + return violations; +} diff --git a/packages/director-mode/tsconfig.build.json b/packages/director-mode/tsconfig.build.json new file mode 100644 index 0000000..71e60f4 --- /dev/null +++ b/packages/director-mode/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "noEmit": false, + "types": ["node"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["dist", "node_modules", "src/__tests__"] +} diff --git a/packages/director-mode/tsconfig.json b/packages/director-mode/tsconfig.json new file mode 100644 index 0000000..bb3c3c0 --- /dev/null +++ b/packages/director-mode/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2021", + "lib": ["ES2021", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "outDir": "./dist", + "rootDir": ".", + "declaration": true, + "sourceMap": true, + "strict": true, + "noImplicitOverride": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": false, + "types": ["node", "vitest/globals"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/director-mode/vitest.config.ts b/packages/director-mode/vitest.config.ts new file mode 100644 index 0000000..26c8b2a --- /dev/null +++ b/packages/director-mode/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "jsdom", + include: ["src/__tests__/**/*.test.ts", "src/__tests__/**/*.test.tsx"], + testTimeout: 30_000, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..f67879c --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1482 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + packages/director-mode: + devDependencies: + '@types/node': + specifier: ^20.14.0 + version: 20.19.43 + '@types/react': + specifier: ^18.3.0 + version: 18.3.31 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.31) + jsdom: + specifier: ^24.1.0 + version: 24.1.3 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@20.19.43)(jsdom@24.1.3) + +packages: + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.31': + resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==} + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsdom@24.1.3: + resolution: {integrity: sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nwsapi@2.2.24: + resolution: {integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + +snapshots: + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@types/estree@1.0.9': {} + + '@types/node@20.19.43': + dependencies: + undici-types: 6.21.0 + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.31)': + dependencies: + '@types/react': 18.3.31 + + '@types/react@18.3.31': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.19.43))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@20.19.43) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + agent-base@7.1.4: {} + + assertion-error@2.0.1: {} + + asynckit@0.4.0: {} + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + csstype@3.2.3: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-eql@5.0.2: {} + + delayed-stream@1.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + entities@6.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + is-potential-custom-element-name@1.0.1: {} + + js-tokens@4.0.0: {} + + jsdom@24.1.3: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + form-data: 4.0.6 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.24 + parse5: 7.3.0 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.21.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + ms@2.1.3: {} + + nanoid@3.3.15: {} + + nwsapi@2.2.24: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + pathe@1.1.2: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@2.3.1: {} + + querystringify@2.2.0: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + requires-port@1.0.0: {} + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + + rrweb-cssom@0.7.1: {} + + rrweb-cssom@0.8.0: {} + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + symbol-tree@3.2.4: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + universalify@0.2.0: {} + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + vite-node@2.1.9(@types/node@20.19.43): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@20.19.43) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@20.19.43): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.15 + rollup: 4.62.2 + optionalDependencies: + '@types/node': 20.19.43 + fsevents: 2.3.3 + + vitest@2.1.9(@types/node@20.19.43)(jsdom@24.1.3): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.43)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@20.19.43) + vite-node: 2.1.9(@types/node@20.19.43) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.43 + jsdom: 24.1.3 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.21.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..15add63 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "connectors/*" + - "packages/*"