From 94290b5d157b0850ea73d2530be538ea5d2dd72a Mon Sep 17 00:00:00 2001 From: Shanon Place Date: Thu, 9 Jul 2026 11:38:01 -0700 Subject: [PATCH 1/8] Initial commit of the orphan finder --- apps/find-orphans/AGENTS.md | 103 + apps/find-orphans/README.md | 90 + apps/find-orphans/index.html | 12 + apps/find-orphans/package-lock.json | 7924 +++++++++++++++++ apps/find-orphans/package.json | 55 + apps/find-orphans/src/App.tsx | 26 + .../src/components/LocalhostWarning.tsx | 31 + apps/find-orphans/src/index.tsx | 21 + .../src/locations/ConfigScreen.tsx | 170 + .../locations/Page/components/OrphanTable.tsx | 92 + .../find-orphans/src/locations/Page/index.tsx | 269 + apps/find-orphans/src/locations/Page/types.ts | 28 + .../src/locations/Page/utils/constants.ts | 24 + .../src/locations/Page/utils/entryActions.ts | 47 + .../src/locations/Page/utils/orphanFinder.ts | 190 + apps/find-orphans/src/parameters.ts | 33 + apps/find-orphans/src/setupTests.ts | 6 + apps/find-orphans/test/App.test.tsx | 20 + apps/find-orphans/test/entryActions.test.ts | 37 + .../test/locations/ConfigScreen.test.tsx | 138 + .../find-orphans/test/locations/Page.test.tsx | 120 + apps/find-orphans/test/mocks/index.ts | 4 + apps/find-orphans/test/mocks/mockCma.ts | 49 + .../test/mocks/mockContentTypes.ts | 61 + apps/find-orphans/test/mocks/mockEntries.ts | 21 + apps/find-orphans/test/mocks/mockSdk.ts | 42 + apps/find-orphans/test/orphanFinder.test.ts | 194 + apps/find-orphans/test/parameters.test.ts | 24 + apps/find-orphans/tsconfig.json | 18 + apps/find-orphans/vite.config.mts | 14 + apps/find-orphans/vitest.config.mts | 18 + 31 files changed, 9881 insertions(+) create mode 100644 apps/find-orphans/AGENTS.md create mode 100644 apps/find-orphans/README.md create mode 100644 apps/find-orphans/index.html create mode 100644 apps/find-orphans/package-lock.json create mode 100644 apps/find-orphans/package.json create mode 100644 apps/find-orphans/src/App.tsx create mode 100644 apps/find-orphans/src/components/LocalhostWarning.tsx create mode 100644 apps/find-orphans/src/index.tsx create mode 100644 apps/find-orphans/src/locations/ConfigScreen.tsx create mode 100644 apps/find-orphans/src/locations/Page/components/OrphanTable.tsx create mode 100644 apps/find-orphans/src/locations/Page/index.tsx create mode 100644 apps/find-orphans/src/locations/Page/types.ts create mode 100644 apps/find-orphans/src/locations/Page/utils/constants.ts create mode 100644 apps/find-orphans/src/locations/Page/utils/entryActions.ts create mode 100644 apps/find-orphans/src/locations/Page/utils/orphanFinder.ts create mode 100644 apps/find-orphans/src/parameters.ts create mode 100644 apps/find-orphans/src/setupTests.ts create mode 100644 apps/find-orphans/test/App.test.tsx create mode 100644 apps/find-orphans/test/entryActions.test.ts create mode 100644 apps/find-orphans/test/locations/ConfigScreen.test.tsx create mode 100644 apps/find-orphans/test/locations/Page.test.tsx create mode 100644 apps/find-orphans/test/mocks/index.ts create mode 100644 apps/find-orphans/test/mocks/mockCma.ts create mode 100644 apps/find-orphans/test/mocks/mockContentTypes.ts create mode 100644 apps/find-orphans/test/mocks/mockEntries.ts create mode 100644 apps/find-orphans/test/mocks/mockSdk.ts create mode 100644 apps/find-orphans/test/orphanFinder.test.ts create mode 100644 apps/find-orphans/test/parameters.test.ts create mode 100644 apps/find-orphans/tsconfig.json create mode 100644 apps/find-orphans/vite.config.mts create mode 100644 apps/find-orphans/vitest.config.mts diff --git a/apps/find-orphans/AGENTS.md b/apps/find-orphans/AGENTS.md new file mode 100644 index 0000000000..77946e023e --- /dev/null +++ b/apps/find-orphans/AGENTS.md @@ -0,0 +1,103 @@ +# Agent Guide — find-orphans + +## What This App Does +Full-page app that scans an environment for "orphaned" draft entries — typically empty entries +accidentally created from a reference field when the user meant to link an existing entry. The +user selects criteria (missing display title, unreferenced, stale draft) and runs a scan; results +deep-link into the entry editor. + +## Archetype +Standard Vite app. Page location plus an app-config screen for installation parameters. + +## Locations + +| Location | File | Purpose | +|----------|------|---------| +| `LOCATION_PAGE` | `src/locations/Page/` | Criteria panel + scan + results table | +| `LOCATION_APP_CONFIG` | `src/locations/ConfigScreen.tsx` | Installation parameters (scan limits) | + +## Installation Parameters + +Defined in `src/parameters.ts`; the constants in `src/locations/Page/utils/constants.ts` are the +defaults. All three parameters are **required** on the app definition, with their default values +set there too — the ConfigScreen shows the defaults only as placeholders and rejects saves with +empty or out-of-range values. `resolveParameters()` remains the defensive fallback on the Page +side, so a scan never runs with missing or invalid limits. + +| Parameter ID | Type | Required | Default | Purpose | +|--------------|------|----------|---------|---------| +| `maxCandidates` | Number | Yes | 500 | Hard cap on candidate entries per scan | +| `defaultStaleDays` | Number | Yes | 30 | Pre-fill for the "not updated in N days" criterion | +| `referenceBatchSize` | Number | Yes | 5 (max 7) | Concurrent CMA requests (scan queries, reference counts, archiving) | + +Parameter definitions on the app definition must use these exact IDs and the Number type; the +README documents the full table (including descriptions) for the app definition setup. + +## Key Dependencies + +| Package | Role | +|---------|------| +| `@contentful/app-sdk` | App Framework SDK (`sdk.cma`, `sdk.navigator`) | +| `@contentful/f36-components` | Forma 36 UI | +| `@contentful/react-apps-toolkit` | `useSDK()` hook | +| `contentful-management` | CMA types (`ContentTypeProps`, `EntryProps`) | + +## Source Layout + +``` +src/ +├── App.tsx +├── parameters.ts # Installation parameter types, defaults, resolver +├── components/ # LocalhostWarning +└── locations/ + ├── ConfigScreen.tsx # Installation parameter form + └── Page/ + ├── index.tsx # Page UI, scan orchestration + ├── types.ts # Criteria/result/progress types + ├── components/ # CriteriaPanel, OrphanTable + └── utils/ + ├── constants.ts # Fallback defaults for parameters + ├── entryActions.ts # Batched bulk archive (pure, unit-tested) + └── orphanFinder.ts # All CMA query logic (pure, unit-tested) +``` + +## Sharp Edges & Invariants + +- **Criteria are OR-combined** (`matchesAnyCriterion`): an entry is listed when it matches at + least one selected criterion, so selecting more criteria widens the results. The CMA cannot OR + independent filters in one query, so only the draft scope is filtered server-side; criteria are + evaluated client-side. With no criteria selected, every draft is listed. +- **Draft scope is fixed**: every entry query includes `sys.publishedAt[exists]=false` and + `sys.archivedAt[exists]=false`. +- **Default-locale only**: the missing-title check and title rendering use `sdk.locales.default`. + Localized titles in other locales are not considered. +- **Missing-title never applies to non-text display fields**: `matchesAnyCriterion` requires + `hasTitleField`, otherwise every entry of such a type would vacuously match. Those content + types are skipped entirely only when missing-title is the sole selected criterion. +- **One entry query per content type is unavoidable** (entry queries require a single + `content_type`), so the scan runs those queries `referenceBatchSize` at a time in parallel + chunks. A chunk shares one remaining-budget snapshot and can overshoot `maxCandidates`; the + result is trimmed and flagged truncated afterwards. +- **Reference counting is N+1**: one `links_to_entry` query per candidate (`limit: 0`, total + only), batched `referenceBatchSize` at a time. `resolveParameters()` clamps the batch size to 7 + (the CMA req/s limit per space) — keep that clamp. When the unreferenced criterion is off, the + scan filters by the cheap criteria first and only counts entries that will be listed — keep + that ordering, it is the main cost control. +- **`maxCandidates` caps each scan** — the UI shows a truncation warning. Keep the cap or replace + it with real pagination, but never scan unbounded. +- **Never read `sdk.parameters.installation` directly** — always go through `resolveParameters()` + so fresh installs and hand-edited values fall back to safe defaults. +- **`sdk.app.setReady()` must stay in the ConfigScreen init effect** — without it the config + screen never leaves its loading state. +- **Keep `orphanFinder.ts` free of React/UI imports** — it is the unit-tested core; the Page + component only orchestrates state and rendering. + +## Never / Always + +- **Never** perform bulk actions without an explicit confirmation dialog — archiving goes through + `ModalConfirm`, and any future delete action must do the same. +- **Never** abort a bulk archive on the first failure — `archiveEntries` collects failures so the + remaining entries still get processed; failed entries stay listed and selected for retry. +- **Always** surface CMA failures via Forma 36 `Notification.error` and empty states via `Note`. +- **Always** keep row clicks selection-only — opening the slide-in editor is a dedicated + "Preview" action so accidental clicks never navigate. diff --git a/apps/find-orphans/README.md b/apps/find-orphans/README.md new file mode 100644 index 0000000000..6268efc00b --- /dev/null +++ b/apps/find-orphans/README.md @@ -0,0 +1,90 @@ +# Find Orphans + +A Contentful App Framework **page app** that finds draft entries that were likely created by +mistake — the classic case being clicking "Create new entry" on a reference field when you meant +to link an existing entry, leaving behind an empty, untitled draft. + +## How it works + +The app scans the current environment for **draft entries** (never published, not archived) +whose **display (title) field has no value** in the default locale — including whitespace-only +values, which the entry editor also renders as "Untitled". + +This is something the regular Contentful search cannot do in one query: field filters only work +for one content type at a time, and every content type defines its own display field. The app +automates that per-content-type check across the whole content model. Content types whose +display field is not a `Symbol`/`Text` field are skipped, since their entries cannot be missing +a title. + +Results are listed with their content type and last-updated date. Each row has an explicit +"Preview" action that opens the entry editor in a slide-in for review. Rows can be selected +individually or all at once, and the selected entries archived in bulk after a confirmation +dialog; archiving runs in rate-limit-friendly batches, and entries that fail to archive stay +listed and selected for retry. Archiving is reversible from the entry editor. + +Scans are capped at a configurable number of draft entries per run (500 by default, see +Configuration below) to stay friendly to CMA rate limits; a warning is shown when the cap is +hit. + +## Logic flow + +```mermaid +flowchart TD + Load[App loads] --> FetchCT[Fetch all content types
paged, 1000 per request] + FetchCT --> Scan([User clicks Scan]) + Scan --> Filter[Keep content types whose display field
is a Symbol or Text field] + Filter --> Chunk[Take next batchSize content types] + Chunk --> Query[Query draft entries per content type in parallel:
never published, not archived,
select sys + display field only] + Query --> Pages[Page through results until exhausted
or the maxCandidates budget is spent] + Pages --> More{More content types
and budget left?} + More -- yes --> Chunk + More -- no --> Check[Client-side check: display field empty,
whitespace-only, or fields object absent
in the default locale] + Check --> Results[Results table
+ truncation warning if capped] + Results --> Archive([User selects rows and confirms Archive]) + Archive --> Batches[Archive entries in batches of batchSize] + Batches --> Done[Archived rows leave the list;
failed ones stay listed and selected for retry] +``` + +The empty-title check runs client-side rather than via the API's `[exists]` operator for two +reasons: `[exists]` misses whitespace-only titles, and entries with no value in any selected +field come back from the CMA with no `fields` object at all — both shapes must count as +orphans. + +## Configuration + +The app configuration screen (space settings, Apps) exposes installation parameters. When +registering the app definition, create the parameter definitions with exactly these values +(the IDs must match `AppInstallationParameters` in `src/parameters.ts`): + +| Display name | ID | Type | Required | Default value | Description | +|--------------|----|------|----------|---------------|-------------| +| Maximum entries per scan | `maxCandidates` | Number | Yes | `500` | The scan stops after this many draft entries, to stay friendly to API rate limits. | +| Concurrent API requests | `batchSize` | Number | Yes | `5` | How many CMA requests run at once while scanning and archiving. Must be between 1 and 7, the CMA rate limit per second. | + +Both parameters are required and their defaults are set on the parameter definition, so a fresh +install starts with the values above. The config screen shows the same defaults as input +placeholders, and its save validation rejects empty, non-positive, or out-of-range values. As a +safety net, `resolveParameters()` still falls back to the defaults at scan time if the stored +parameters are ever missing or invalid. + +## Development + +```bash +npm install +npm start # dev server on http://localhost:3000 +npm test # vitest watch mode +npm run test:ci # single test run +npm run build # production bundle in ./build +``` + +To install into a space, create an app definition with a **Page** location: + +```bash +npm run create-app-definition +``` + +## Learn more + +- [Contentful App Framework](https://www.contentful.com/developers/docs/extensibility/app-framework/) +- [Page location](https://www.contentful.com/developers/docs/extensibility/app-framework/locations/#page) +- [Forma 36](https://f36.contentful.com/) diff --git a/apps/find-orphans/index.html b/apps/find-orphans/index.html new file mode 100644 index 0000000000..6cd2814bfa --- /dev/null +++ b/apps/find-orphans/index.html @@ -0,0 +1,12 @@ + + + + + + + + +
+ + + diff --git a/apps/find-orphans/package-lock.json b/apps/find-orphans/package-lock.json new file mode 100644 index 0000000000..9f1013eac5 --- /dev/null +++ b/apps/find-orphans/package-lock.json @@ -0,0 +1,7924 @@ +{ + "name": "find-orphans", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "find-orphans", + "version": "0.1.0", + "dependencies": { + "@contentful/app-sdk": "^4.29.1", + "@contentful/f36-components": "4.79.1", + "@contentful/f36-icons": "^5.6.0", + "@contentful/f36-tokens": "4.2.0", + "@contentful/react-apps-toolkit": "1.2.16", + "contentful-management": "^11.52.0", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@contentful/app-scripts": "^2.3.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^14.3.1", + "@types/node": "^22.13.5", + "@types/react": "18.3.13", + "@types/react-dom": "18.3.1", + "@vitejs/plugin-react": "^4.0.3", + "jsdom": "^26.0.0", + "typescript": "4.9.5", + "vite": "^6.4.1", + "vitest": "^3.0.9" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@contentful/app-scripts": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@contentful/app-scripts/-/app-scripts-2.5.12.tgz", + "integrity": "sha512-e6jim+S824WI9cpV0aUKmdI4rX5YHtVX6+MQZdbnGl+zD+K1mzqkP+Xk791PnyC2kQloy9XiMSKJ2OgoYgGobg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-plugins/node-globals-polyfill": "^0.2.3", + "@esbuild-plugins/node-modules-polyfill": "^0.2.2", + "@segment/analytics-node": "^3.0.0", + "adm-zip": "0.5.17", + "axios": "^1.15.2", + "bottleneck": "2.19.5", + "chalk": "4.1.2", + "commander": "12.1.0", + "contentful-management": "^11.75.0", + "dotenv": "17.4.2", + "esbuild": "^0.28.0", + "ignore": "7.0.5", + "inquirer": "8.2.7", + "lodash": "4.18.1", + "merge-options": "^3.0.4", + "open": "8.4.2", + "ora": "5.4.1", + "zod": "^3.24.1" + }, + "bin": { + "contentful-app-scripts": "lib/bin.js" + }, + "engines": { + "node": ">=18", + "npm": ">=9" + } + }, + "node_modules/@contentful/app-sdk": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@contentful/app-sdk/-/app-sdk-4.63.1.tgz", + "integrity": "sha512-6ZWC4baTgYj3w1MJjSwivC2tWCGifbyT4X3M/m4oWqrqBjZLsD7wi2vb0DD7f+hBJHhoaT0K2ncn4DR3bqz83A==", + "license": "MIT", + "dependencies": { + "contentful-management": "^12.3.1" + } + }, + "node_modules/@contentful/app-sdk/node_modules/contentful-management": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/contentful-management/-/contentful-management-12.8.0.tgz", + "integrity": "sha512-lARtEZYn2MYf80hz9nCYiLbJWvg95TTnOWmCZouMZtgNS1273dN27Vaz7weHQ3oSQuk7LeYXaXdGFhFPKlpilw==", + "license": "MIT", + "dependencies": { + "@contentful/rich-text-types": "^16.6.1", + "axios": "^1.15.0", + "contentful-sdk-core": "^9.4.4", + "fast-copy": "^3.0.0", + "globals": "^15.15.0", + "process": "^0.11.10" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@contentful/f36-accordion": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-accordion/-/f36-accordion-4.81.1.tgz", + "integrity": "sha512-iQ2ERYurTTxDXp031kPZy5In9qNzSQJ2sc52VRaafUPdKiVnX5igGVqAiMxcK30fPP14RccOOHVz6YnrMjGtvg==", + "license": "MIT", + "dependencies": { + "@contentful/f36-collapse": "^4.81.1", + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-typography": "^4.81.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-accordion/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-asset": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-asset/-/f36-asset-4.81.1.tgz", + "integrity": "sha512-1cXdCHhZ+ymCaEHexoeGwfvEWbjDxMU4mNX1TL1LatI4iSW7l8vhwDnVXQMMi0VmhX2U968z2kFZrjsqcgIwOg==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-icon": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-typography": "^4.81.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-asset/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-autocomplete": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-autocomplete/-/f36-autocomplete-4.81.1.tgz", + "integrity": "sha512-lUwnApdkfgbX16QdHbsxZuaIv2V+Op40m/JlPNhuF/rl5g3ndS1BerjFcaDgDMbJzc/Jl8BNOVJj0Wc7F5M9TQ==", + "license": "MIT", + "dependencies": { + "@contentful/f36-button": "^4.81.1", + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-forms": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-popover": "^4.81.1", + "@contentful/f36-skeleton": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-typography": "^4.81.1", + "@contentful/f36-utils": "^4.24.3", + "downshift": "^6.1.12", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-autocomplete/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-avatar": { + "version": "4.79.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-avatar/-/f36-avatar-4.79.1.tgz", + "integrity": "sha512-DLjvcTg40KgyI2d1yRyuFSXz59rhrk7Oa0hrM38CVQ/mctd+OF6FHSoYsrXP+xaYYQz28jW7ydvLhPHze2huEA==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.79.1", + "@contentful/f36-image": "4.79.1", + "@contentful/f36-menu": "^4.79.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-tooltip": "^4.79.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-badge": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-badge/-/f36-badge-4.81.1.tgz", + "integrity": "sha512-yj7ZwuuOIvAhyVqUZdfB68bpgNB2XXnQx6QsRKWaKoM703IkRbqSKrLfeOljkgd5/5kMxTWyrvQfDTx2wfJl1A==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-tokens": "^4.2.0", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-badge/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-button": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-button/-/f36-button-4.81.1.tgz", + "integrity": "sha512-qQK9ZQHHSvcXt6hCeqXPbr2/C8MnBk4egPd5w0VPfOi3Sa7obyWeMuAoVP3bLCicvEu/B7FFH6E1orL6+8lUOw==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-spinner": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-tooltip": "^4.81.1", + "@contentful/f36-utils": "^4.24.3", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-card": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-card/-/f36-card-4.81.1.tgz", + "integrity": "sha512-dDidZfT31IZ3BwZhKRt0O8VZu0FrU41D8BkxkdUYpwSJJzxEMsTyaiUxB57TUd8FHEEmAbozyGcJwt0ybBqm8w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-asset": "^4.81.1", + "@contentful/f36-badge": "^4.81.1", + "@contentful/f36-button": "^4.81.1", + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-drag-handle": "^4.81.1", + "@contentful/f36-icon": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-menu": "^4.81.1", + "@contentful/f36-skeleton": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-tooltip": "^4.81.1", + "@contentful/f36-typography": "^4.81.1", + "emotion": "^10.0.17", + "truncate": "^3.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-card/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-collapse": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-collapse/-/f36-collapse-4.81.1.tgz", + "integrity": "sha512-KhaM4Z7RXOsRWzPLx5kOPpTtQCR7VmbO7TVZI9x7II0zCv+bBr9hUsy6JN+ktL4ZePXe+GT4O6FGVPMbsVWt2A==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-components": { + "version": "4.79.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-components/-/f36-components-4.79.1.tgz", + "integrity": "sha512-yP2X2aovQmwu7Ay0lTMvfmYfeHpRBsArUO6CmJTwggSS0+LcQraxmfn4YNQ6pnOVEL3RPuVh4FPcrarcWKLyjQ==", + "license": "MIT", + "dependencies": { + "@contentful/f36-accordion": "^4.79.1", + "@contentful/f36-asset": "^4.79.1", + "@contentful/f36-autocomplete": "^4.79.1", + "@contentful/f36-avatar": "4.79.1", + "@contentful/f36-badge": "^4.79.1", + "@contentful/f36-button": "^4.79.1", + "@contentful/f36-card": "^4.79.1", + "@contentful/f36-collapse": "^4.79.1", + "@contentful/f36-copybutton": "^4.79.1", + "@contentful/f36-core": "^4.79.1", + "@contentful/f36-datepicker": "^4.79.1", + "@contentful/f36-datetime": "^4.79.1", + "@contentful/f36-drag-handle": "^4.79.1", + "@contentful/f36-empty-state": "^4.79.1", + "@contentful/f36-entity-list": "^4.79.1", + "@contentful/f36-forms": "^4.79.1", + "@contentful/f36-header": "^4.79.1", + "@contentful/f36-icon": "^4.79.1", + "@contentful/f36-icons": "^4.29.0", + "@contentful/f36-image": "4.79.1", + "@contentful/f36-list": "^4.79.1", + "@contentful/f36-menu": "^4.79.1", + "@contentful/f36-modal": "^4.79.1", + "@contentful/f36-navbar": "^4.79.1", + "@contentful/f36-note": "^4.79.1", + "@contentful/f36-notification": "^4.79.1", + "@contentful/f36-pagination": "^4.79.1", + "@contentful/f36-pill": "^4.79.1", + "@contentful/f36-popover": "^4.79.1", + "@contentful/f36-skeleton": "^4.79.1", + "@contentful/f36-spinner": "^4.79.1", + "@contentful/f36-table": "^4.79.1", + "@contentful/f36-tabs": "^4.79.1", + "@contentful/f36-text-link": "^4.79.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-tooltip": "^4.79.1", + "@contentful/f36-typography": "^4.79.1", + "@contentful/f36-utils": "^4.24.3" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "@types/react-dom": ">=16.8", + "react": ">=16.8", + "react-dom": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@contentful/f36-components/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-copybutton": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-copybutton/-/f36-copybutton-4.81.1.tgz", + "integrity": "sha512-6zgcAB9kyYg/KInDa1VCVurSoi6B/RD+MCpXW1Ixtweno1qWersRjHB61k3+3pQFaLT21rlPBv/31rhfOqVsHg==", + "license": "MIT", + "dependencies": { + "@contentful/f36-button": "^4.81.1", + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-tooltip": "^4.81.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-copybutton/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-core": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-core/-/f36-core-4.81.1.tgz", + "integrity": "sha512-n1H0bApzFSdL4bInsemSsJVlNkDt4Xhw+g9GMhXZqC7g+K6Yptow1mUSS+93Nmbccmumy/vWyau71wqrHWt3tA==", + "license": "MIT", + "dependencies": { + "@contentful/f36-tokens": "^4.2.0", + "@emotion/core": "^10.1.1", + "@emotion/is-prop-valid": "^1.2.0", + "csstype": "^3.1.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-datepicker": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-datepicker/-/f36-datepicker-4.81.1.tgz", + "integrity": "sha512-LFNfVK1hPqKuGiDUxjwDYcvfHFDPWZ3Ejds//+R5o8iA7xT5/l+7Zyeg0Bo49YCTXVeg5NbsenkM68poUoRjCA==", + "license": "MIT", + "dependencies": { + "@contentful/f36-button": "^4.81.1", + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-forms": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-popover": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-typography": "^4.81.1", + "date-fns": "^2.28.0", + "emotion": "^10.0.17", + "react-day-picker": "^8.7.1", + "react-focus-lock": "^2.9.1" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-datepicker/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-datetime": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-datetime/-/f36-datetime-4.81.1.tgz", + "integrity": "sha512-SIAD2Jl+NE05a6ke5a122VIaBOJsXjD+DXD8pKsRM5NX0zzgtIPSst2NeETuW+p2HXpenwk5PSyNRqPuS7GM/g==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "dayjs": "^1.11.5", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-drag-handle": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-drag-handle/-/f36-drag-handle-4.81.1.tgz", + "integrity": "sha512-S+GnO8wouQj723ws8rk562uZy0DvxWW4TPUt+LVb6tT69kI7RawXyNqYjEOZf06uDDvPId1AhcygT8g8JL+i/w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-utils": "^4.24.3", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-drag-handle/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-empty-state": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-empty-state/-/f36-empty-state-4.81.1.tgz", + "integrity": "sha512-x+2AZvK/ZMJtST7AjvdX6/7j9shXjIiXP96iT2LR9+Vwp3vY2ifou2A/PL1oUGVbvADjwGsoVglszQoFKsR2JQ==", + "license": "MIT", + "dependencies": { + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-typography": "^4.81.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-entity-list": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-entity-list/-/f36-entity-list-4.81.1.tgz", + "integrity": "sha512-Wec9rXoGro75TXUi7KvGrZEjSnuyL/lxCcqhOTgtW9la2QXAF9UoXWCSFfTlHqCCmyjWYqNAVVMt8MQ5TTmPUQ==", + "license": "MIT", + "dependencies": { + "@contentful/f36-badge": "^4.81.1", + "@contentful/f36-button": "^4.81.1", + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-drag-handle": "^4.81.1", + "@contentful/f36-icon": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-menu": "^4.81.1", + "@contentful/f36-skeleton": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-typography": "^4.81.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-entity-list/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-forms": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-forms/-/f36-forms-4.81.1.tgz", + "integrity": "sha512-1F2kc81hvGmncQIKdAX8Ij9Xznk3c2+6qcqkw+oBupsPZnCYw8e4P2evlQTxpqGtQQK5QDL57J9YB69dMigehg==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-typography": "^4.81.1", + "@contentful/f36-utils": "^4.24.3", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-forms/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-header": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-header/-/f36-header-4.81.1.tgz", + "integrity": "sha512-obTSg4QbUIga7DjXVOe5n5y2o7T47KM6UGYsU4n3NA5Vsy4qJReOO8oNXBVqxt7CW9hXhXccNPjuOfIhiMXApA==", + "license": "MIT", + "dependencies": { + "@contentful/f36-button": "^4.81.1", + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-typography": "^4.81.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-header/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-icon": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icon/-/f36-icon-4.81.1.tgz", + "integrity": "sha512-s7rX0BrGvaDnaExaZeAJ24O5Ruyzlvqlq03WlePH2XDXbTC620f4VL1iODd5JJGg0NlPDgkjRk+GG6uj9HOGMA==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-icons": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-5.9.1.tgz", + "integrity": "sha512-bq2i5W8rA+jYPsdPQdBegrWqbZquYfYcNHF6VOuxZuUl9xn78FvaiWHxiaVDyjFk/3kQFSDjMLUdQI3sePJSUw==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^5.9.1", + "@contentful/f36-icon": "^5.9.1", + "@contentful/f36-tokens": "^5.1.0", + "@phosphor-icons/react": "2.1.8", + "emotion": "^10.0.17" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-icons/node_modules/@contentful/f36-core": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-core/-/f36-core-5.9.1.tgz", + "integrity": "sha512-egPXjZLcUwF+LJ7q8w/9zpMVNTWVIIEV5DEmn5nAgPd4+FLQjyWFbL8ubyOMk41F0ceCbB1cp5AILuA0xRIsvQ==", + "license": "MIT", + "dependencies": { + "@contentful/f36-tokens": "^5.1.0", + "@emotion/core": "^10.1.1", + "@emotion/is-prop-valid": "^1.2.0", + "csstype": "^3.1.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-icons/node_modules/@contentful/f36-icon": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icon/-/f36-icon-5.9.1.tgz", + "integrity": "sha512-58Pa0r5vpSX6jr6Lsvoc0ah82Hid6kvNFvu8+/vGDO//KgX7On0LjLUYiygLmyNG9FeKaJingxB8RUe2FEIR6g==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^5.9.1", + "@contentful/f36-tokens": "^5.1.0", + "@phosphor-icons/react": "2.1.8", + "emotion": "^10.0.17" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-icons/node_modules/@contentful/f36-tokens": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@contentful/f36-tokens/-/f36-tokens-5.1.0.tgz", + "integrity": "sha512-7++MPQiizK6qLhPkilWIKqM5Mhdr199tK7dL4iZ689iTwA82uvXXOojJr+0wVOurG2Odb+6/3WLe1XdveDZHEA==", + "license": "MIT" + }, + "node_modules/@contentful/f36-image": { + "version": "4.79.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-image/-/f36-image-4.79.1.tgz", + "integrity": "sha512-hDMRgm7s9FPnzTI21aJlqX1HoWF7LGx9G0r/Pu8IYjecP4W5CD2HTTSMRFV81V1UjoLiVjJhkizcA0DtVqZpog==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.79.1", + "@contentful/f36-skeleton": "^4.79.1", + "@contentful/f36-tokens": "^4.2.0", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-list": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-list/-/f36-list-4.81.1.tgz", + "integrity": "sha512-m/x6LRoeO1d4o+Vm5SsdYaTQebLM959emxdbBa2ryg4TZxTzTW7S13TB758CwEtMbvv1dnmcGiEG8ot6/zV+/g==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-menu": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-menu/-/f36-menu-4.81.1.tgz", + "integrity": "sha512-zEvMlaaM10ULsiVFSTJya+6OVX7mUsrOJ9qwb+ZUpKefTVmD28HesHksRek3BD1fcy1d3UJViT78XA6q9dwS8g==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-popover": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-typography": "^4.81.1", + "@contentful/f36-utils": "^4.24.3", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-menu/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-modal": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-modal/-/f36-modal-4.81.1.tgz", + "integrity": "sha512-m8JN8+Q7hfPdDhSZrCgUb8rn2I1Y5iSHVW5r/P+7pf0AOfMeJSDRyHNNMEkn5UyZudwbUdVEcNFy/l2xDVkaZA==", + "license": "MIT", + "dependencies": { + "@contentful/f36-button": "^4.81.1", + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-typography": "^4.81.1", + "@types/react-modal": "^3.13.1", + "emotion": "^10.0.17", + "react-modal": "^3.16.1" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-modal/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-navbar": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-navbar/-/f36-navbar-4.81.1.tgz", + "integrity": "sha512-Kfromh5vpleCmbZfXHdvkYymhN8gfZiqVZjeBsx9wPvFBkm4YDx7cc4ijVZdkFekFVNrPT8GQMHSMGR4dYKt2A==", + "license": "MIT", + "dependencies": { + "@contentful/f36-avatar": "4.81.1", + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-icon": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-menu": "^4.81.1", + "@contentful/f36-skeleton": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-utils": "^4.23.2", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-navbar/node_modules/@contentful/f36-avatar": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-avatar/-/f36-avatar-4.81.1.tgz", + "integrity": "sha512-CBR90BcY/REi0F5ey4jQ2tygTUwvUg4Rpshli7OSDhCflw//Kyv4O97vgq6s9V4pO5XfwDEujKLnbYyJHWorTA==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-image": "4.81.1", + "@contentful/f36-menu": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-tooltip": "^4.81.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-navbar/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-navbar/node_modules/@contentful/f36-image": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-image/-/f36-image-4.81.1.tgz", + "integrity": "sha512-D/WoHm08zaaLzzttb26AUT+YvRcsIsdgOqozWtrJoxgAkuiUxfoS8sq0VnkWd5G2IGMnym4hM1pugD3ZasczSg==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-skeleton": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-note": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-note/-/f36-note-4.81.1.tgz", + "integrity": "sha512-Wd7RYOJ9pa23ZHj95pmxCESTeJV9l3snceFwI/m+gkcdZkwVknHyAyfmo/Jjq1cDWtE6Ul3n+Tk752uFNFRpAg==", + "license": "MIT", + "dependencies": { + "@contentful/f36-button": "^4.81.1", + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-icon": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-typography": "^4.81.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-note/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-notification": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-notification/-/f36-notification-4.81.1.tgz", + "integrity": "sha512-767WdLca+Ll+8kGKL3bdnJ3YqZTgeQmqKE4fixEcBzrUwb2eDMolzZzCHu0lHBTvvRC1m/KhcYcEiExqqGmDvA==", + "license": "MIT", + "dependencies": { + "@contentful/f36-button": "^4.81.1", + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-text-link": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-typography": "^4.81.1", + "emotion": "^10.0.17", + "react-animate-height": "^3.0.4" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-notification/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-pagination": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-pagination/-/f36-pagination-4.81.1.tgz", + "integrity": "sha512-zNtISiRV6Nf3nO1oRNACzknlxwke2JSmEdC6JeUzSjX7MkNiyMvpjq1jFypdlhcpqt1fu54kWCMA2VF+H+SEDA==", + "license": "MIT", + "dependencies": { + "@contentful/f36-button": "^4.81.1", + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-forms": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-typography": "^4.81.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-pagination/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-pill": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-pill/-/f36-pill-4.81.1.tgz", + "integrity": "sha512-Vn12ryHRjossi/TGmNyQck90bbw3k4zVUPEwKHaZZmIM+VWGIG8BYSKyBGFV1pi93O1bXgxADldZD8lY2U05ng==", + "license": "MIT", + "dependencies": { + "@contentful/f36-button": "^4.81.1", + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-drag-handle": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-tooltip": "^4.81.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-pill/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-popover": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-popover/-/f36-popover-4.81.1.tgz", + "integrity": "sha512-3p33rAisFN41seFXgYaED3PKkAYsPvvyIakdBA2Fdxnvvx38oeKSzjIn8X1Z9Oy3m1D5CTjp2egq3/XaY/tczA==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-utils": "^4.24.3", + "@popperjs/core": "^2.11.5", + "emotion": "^10.0.17", + "react-popper": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-skeleton": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-skeleton/-/f36-skeleton-4.81.1.tgz", + "integrity": "sha512-7usgRP/5a9SS1PCKYfQJ/TfIjJu6iF5mXvHT7DnU/jZFX72R3p1b88fm21Gg8Bs6PgnLIuLIsI/ObZp1u84ufA==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-table": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-spinner": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-spinner/-/f36-spinner-4.81.1.tgz", + "integrity": "sha512-LX5pu0mZLkrb1gXRBMVHyiyjoXgDLPUFBXc/ZZ9KE1BYnuYp+gFiYfOtGAt+DFaNeknfjqNJf+8o36z+QXwScg==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-table": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-table/-/f36-table-4.81.1.tgz", + "integrity": "sha512-C2VcMdVX0VHpG4/oZpRR4UYIdvrXf6E5XbN2RlKL/R8LBiVr8wPFLL8o7HCT/OoYoaEWYr8/EnJyIl2BYpNvgw==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-icons": "^4.29.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-typography": "^4.81.1", + "@contentful/f36-utils": "^4.24.3", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-table/node_modules/@contentful/f36-icons": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-icons/-/f36-icons-4.29.1.tgz", + "integrity": "sha512-Klqewgt+CoU9BgWhVdFDEmQAWgI+KpsFYuYFsgdr7GCaMgHuecFSGN44SdW/bcv1Ijch3A1xEoYIOqTbLUAP7w==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.80.4", + "@contentful/f36-icon": "^4.80.4", + "@contentful/f36-tokens": "^4.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-tabs": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-tabs/-/f36-tabs-4.81.1.tgz", + "integrity": "sha512-+fspt28e3YggyMJ9c+qZeCzFWjL5FBLa5ToQHmPzKO/LGtf8x3QXyLclTLflDvLBJldh8/gJ6YxQYMKhW/XLPA==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "@radix-ui/react-tabs": "^1.0.1", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-text-link": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-text-link/-/f36-text-link-4.81.1.tgz", + "integrity": "sha512-Pf5lnUocJ3fTbK3062IL/vyqBUK+gM4kOeaLR3ugorpU/KqE/mxgzDaKnjAJvZMDflyi4Ejr4KdKeiOWSBZHPA==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-tokens": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@contentful/f36-tokens/-/f36-tokens-4.2.0.tgz", + "integrity": "sha512-fB7ZEO8+NJPlcGvx6EHuEoPdhgpaFAeCnymXlgC+xVLTXYKXSk8JJD4WEr9rBLcXpyyS+e3g51kNFv90cDOeEQ==", + "license": "MIT" + }, + "node_modules/@contentful/f36-tooltip": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-tooltip/-/f36-tooltip-4.81.1.tgz", + "integrity": "sha512-VG2fWUdgdUYsz6KuqtYEM+n5UzADak+EY/OGDvWo206AKi5XNn29z5VnbwvMJqjOlmepYiRZ1JFaee0+d5LEtw==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-utils": "^4.24.3", + "@popperjs/core": "^2.11.5", + "csstype": "^3.1.1", + "emotion": "^10.0.17", + "react-popper": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-typography": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/@contentful/f36-typography/-/f36-typography-4.81.1.tgz", + "integrity": "sha512-AQNy2/4/rW743vKYOtHdzM8Dz/9CP+QwD1Hm5lCUzPQpbb6XEav/mfSXi9wjLiMMWP09vPQvScBvTDQx+0dRng==", + "license": "MIT", + "dependencies": { + "@contentful/f36-core": "^4.81.1", + "@contentful/f36-tokens": "^4.2.0", + "@contentful/f36-utils": "^4.24.3", + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/f36-utils": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@contentful/f36-utils/-/f36-utils-4.24.3.tgz", + "integrity": "sha512-9+tyDOCjEhW7m4X7Sam0VYlmcgjhLVMiapCnv1EmRstlMaeI5ti9y0y/2aQ04Na2ScoHwrMTjveoZ6V62J62tA==", + "license": "MIT", + "dependencies": { + "emotion": "^10.0.17" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@contentful/react-apps-toolkit": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@contentful/react-apps-toolkit/-/react-apps-toolkit-1.2.16.tgz", + "integrity": "sha512-vx7+T4h+w1XBbn7OfRoXr8YUTtiyRNLaltF2ziM4xMqBryu/3Y71nBbhbxD6NIfOui3QIo3TR9Q5sPTZvGbIVw==", + "license": "MIT", + "dependencies": { + "contentful-management": ">=7.30.0" + }, + "peerDependencies": { + "@contentful/app-sdk": ">=4.0.0", + "react": ">=16.8.0" + } + }, + "node_modules/@contentful/rich-text-types": { + "version": "16.8.5", + "resolved": "https://registry.npmjs.org/@contentful/rich-text-types/-/rich-text-types-16.8.5.tgz", + "integrity": "sha512-q18RJuJCOuYveGiCIjE5xLCQc5lZ3L2Qgxrlg/H2YEobDFqdtmklazRi1XwEWaK3tMg6yVXBzKKkQfLB4qW14A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emotion/cache": { + "version": "10.0.29", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", + "integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==", + "license": "MIT", + "dependencies": { + "@emotion/sheet": "0.9.4", + "@emotion/stylis": "0.8.5", + "@emotion/utils": "0.11.3", + "@emotion/weak-memoize": "0.2.5" + } + }, + "node_modules/@emotion/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.3.1.tgz", + "integrity": "sha512-447aUEjPIm0MnE6QYIaFz9VQOHSXf4Iu6EWOIqq11EAPqinkSZmfymPTmlOE3QjLv846lH4JVZBUOtwGbuQoww==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@emotion/cache": "^10.0.27", + "@emotion/css": "^10.0.27", + "@emotion/serialize": "^0.11.15", + "@emotion/sheet": "0.9.4", + "@emotion/utils": "0.11.3" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, + "node_modules/@emotion/css": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.27.tgz", + "integrity": "sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==", + "license": "MIT", + "dependencies": { + "@emotion/serialize": "^0.11.15", + "@emotion/utils": "0.11.3", + "babel-plugin-emotion": "^10.0.27" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/serialize": { + "version": "0.11.16", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", + "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/unitless": "0.7.5", + "@emotion/utils": "0.11.3", + "csstype": "^2.5.7" + } + }, + "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "license": "MIT" + }, + "node_modules/@emotion/serialize/node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", + "license": "MIT" + }, + "node_modules/@emotion/sheet": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz", + "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==", + "license": "MIT" + }, + "node_modules/@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@emotion/utils": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", + "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", + "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==", + "license": "MIT" + }, + "node_modules/@esbuild-plugins/node-globals-polyfill": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", + "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild-plugins/node-modules-polyfill": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", + "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", + "dev": true, + "license": "ISC", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "rollup-plugin-node-polyfills": "^0.2.1" + }, + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@lukeed/uuid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@lukeed/uuid/-/uuid-2.0.1.tgz", + "integrity": "sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@phosphor-icons/react": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.8.tgz", + "integrity": "sha512-RxJlAkErO+t50DsY82ga9RGOULK6Jux0MdmXqvDjtOzG3PYQFz6rjdUU2q06lPMMbJTT+d+qurKYmF7i2Uv74A==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.5.tgz", + "integrity": "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.12.tgz", + "integrity": "sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-slot": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", + "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.0.tgz", + "integrity": "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz", + "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz", + "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.7.tgz", + "integrity": "sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.7.tgz", + "integrity": "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.15.tgz", + "integrity": "sha512-40svmmugfM3mUN7VUDGVE1tQGOhyi8enlGD0CNJEcMM36C1f71PKM21DFgNHUfem0XnA+d8H8oN3Z9ZpJjSslg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.5", + "@radix-ui/react-collection": "1.1.12", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-is-hydrated": "0.1.1", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", + "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.17.tgz", + "integrity": "sha512-nRyXnrAVCwjeXcHbvEbLS6ndbTeKHG1RqCP4A8Gw5L4cemDzPXdD8rAmr6wet0v57R69wGvuIIsFjHSVkZiMzQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.5", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-presence": "1.1.7", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-roving-focus": "1.1.15", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", + "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz", + "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", + "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz", + "integrity": "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", + "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@segment/analytics-core": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@segment/analytics-core/-/analytics-core-1.8.3.tgz", + "integrity": "sha512-63X8e2DWb2ZnJ9S3H8iSOFnbeDXMkTGhA8kZG4eFAO07QBwBVyWo5BCpv6vmuOMEkv2le6t2FMg1p6ZeJQA/4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lukeed/uuid": "^2.0.0", + "@segment/analytics-generic-utils": "1.2.0", + "dset": "^3.1.4", + "tslib": "^2.4.1" + } + }, + "node_modules/@segment/analytics-generic-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@segment/analytics-generic-utils/-/analytics-generic-utils-1.2.0.tgz", + "integrity": "sha512-DfnW6mW3YQOLlDQQdR89k4EqfHb0g/3XvBXkovH1FstUN93eL1kfW9CsDcVQyH3bAC5ZsFyjA/o/1Q2j0QeoWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.1" + } + }, + "node_modules/@segment/analytics-node": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@segment/analytics-node/-/analytics-node-3.1.0.tgz", + "integrity": "sha512-F3pJmzUxmWZFV2wxJfuGvjLViCrQBFpxLLp4++fkoDsohWwS89T7n0M5nW1zxeYc2X2rA/v7q5ou3JZIw9bl3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lukeed/uuid": "^2.0.0", + "@segment/analytics-core": "1.8.3", + "@segment/analytics-generic-utils": "1.2.0", + "buffer": "^6.0.3", + "jose": "^5.1.0", + "tslib": "^2.4.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@testing-library/dom": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/react": { + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz", + "integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^9.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.13.tgz", + "integrity": "sha512-ii/gswMmOievxAJed4PAHT949bpYjPKXvXo1v6cRB/kqc2ZR4n+SgyCyvyc5Fec5ez8VnUumI1Vk7j6fRyRogg==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-modal": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-xXuGavyEGaFQDgBv4UVm8/ZsG+qxeQ7f77yNrW3n+1J6XAstUy5rYHeIHPh1KzsGc6IkCIdu6lQ2xWzu1jBTLg==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/adm-zip": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/babel-plugin-emotion": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz", + "integrity": "sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/serialize": "^0.11.16", + "babel-plugin-macros": "^2.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^1.0.5", + "find-root": "^1.1.0", + "source-map": "^0.5.7" + } + }, + "node_modules/babel-plugin-emotion/node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "license": "MIT" + }, + "node_modules/babel-plugin-emotion/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/babel-plugin-emotion/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", + "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "cosmiconfig": "^6.0.0", + "resolve": "^1.12.0" + } + }, + "node_modules/babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/browserslist": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001803", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==", + "license": "MIT" + }, + "node_modules/contentful-management": { + "version": "11.76.0", + "resolved": "https://registry.npmjs.org/contentful-management/-/contentful-management-11.76.0.tgz", + "integrity": "sha512-KsqIZ65q1A6IP55sxTfuAMhBTNJfgGxlVZBoV2ghBuR1LaZZyTqway5vj9KHRDl/Z1uOQQsYSiz+FayMxBXXEw==", + "license": "MIT", + "dependencies": { + "@contentful/rich-text-types": "^16.6.1", + "axios": "^1.15.0", + "contentful-sdk-core": "^9.4.4", + "fast-copy": "^3.0.0", + "globals": "^15.15.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/contentful-sdk-core": { + "version": "9.4.5", + "resolved": "https://registry.npmjs.org/contentful-sdk-core/-/contentful-sdk-core-9.4.5.tgz", + "integrity": "sha512-8eGTMO11LXFAiosaiV38bjvW6cjoQFtNE38pNtVilZTXpmGFinClljihH0eriv11Ispd4q82TqtsXMJPYD/C+A==", + "license": "MIT", + "dependencies": { + "fast-copy": "^3.0.2", + "lodash": "^4.17.23", + "process": "^0.11.10", + "qs": "^6.15.0" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.18.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/create-emotion": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-10.0.27.tgz", + "integrity": "sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg==", + "license": "MIT", + "dependencies": { + "@emotion/cache": "^10.0.27", + "@emotion/serialize": "^0.11.15", + "@emotion/sheet": "0.9.4", + "@emotion/utils": "0.11.3" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/downshift": { + "version": "6.1.12", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-6.1.12.tgz", + "integrity": "sha512-7XB/iaSJVS4T8wGFT3WRXmSF1UlBHAA40DshZtkrIscIN+VC+Lh363skLxFTvJwtNgHxAMDGEHT4xsyQFWL+UA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.14.8", + "compute-scroll-into-view": "^1.0.17", + "prop-types": "^15.7.2", + "react-is": "^17.0.2", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.12.0" + } + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/emotion": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/emotion/-/emotion-10.0.27.tgz", + "integrity": "sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g==", + "license": "MIT", + "dependencies": { + "babel-plugin-emotion": "^10.0.27", + "create-emotion": "^10.0.27" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==", + "license": "BSD-3-Clause" + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/focus-lock": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-1.3.6.tgz", + "integrity": "sha512-Ik/6OCk9RQQ0T5Xw+hKNLWrjSMtv51dD4GRmJjbD5a58TIEpI5a5iXagKVl3Z5UuyslMCA8Xwnu76jQob62Yhg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "8.2.7", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.7.tgz", + "integrity": "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/external-editor": "^1.0.0", + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "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.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-animate-height": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/react-animate-height/-/react-animate-height-3.2.4.tgz", + "integrity": "sha512-wcT37yHsFWDx8DmNFMy0AqSrYgNPGAtU9dyEqwIq3kSUXW03DWSi5WqbhlOALvMXcowal8JWUJRXij6TexilGQ==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/react-clientside-effect": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.8.tgz", + "integrity": "sha512-ma2FePH0z3px2+WOu6h+YycZcEvFmmxIlAb62cF52bG86eMySciO/EQZeQMXd07kPCYB0a1dWDT5J+KE9mCDUw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-day-picker": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.2.tgz", + "integrity": "sha512-LK68OTbHB3oJNhl9cA0qVizzp3o26w61YSjAFkYi67N86iro32wx86kSNeFU/hq+gI8m1yzWhnomMLfZ041RzQ==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "date-fns": "^2.28.0 || ^3.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-focus-lock": { + "version": "2.13.7", + "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.13.7.tgz", + "integrity": "sha512-20lpZHEQrXPb+pp1tzd4ULL6DyO5D2KnR0G69tTDdydrmNhU7pdFmbQUYVyHUgp+xN29IuFR0PVuhOmvaZL9Og==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.0.0", + "focus-lock": "^1.3.6", + "prop-types": "^15.6.2", + "react-clientside-effect": "^1.2.7", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, + "node_modules/react-modal": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw==", + "license": "MIT", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "license": "MIT", + "dependencies": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + }, + "peerDependencies": { + "@popperjs/core": "^2.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "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.2" + } + }, + "node_modules/rollup-plugin-inject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", + "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1", + "magic-string": "^0.25.3", + "rollup-pluginutils": "^2.8.1" + } + }, + "node_modules/rollup-plugin-node-polyfills": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", + "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rollup-plugin-inject": "^3.0.0" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/truncate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/truncate/-/truncate-3.0.0.tgz", + "integrity": "sha512-C+0Xojw7wZPl6MDq5UjMTuxZvBPK04mtdFet7k+GSZPINcvLZFCXg+15kWIL4wAqDB7CksIsKiRLbQ1wa7rKdw==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "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 + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "extraneous": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/apps/find-orphans/package.json b/apps/find-orphans/package.json new file mode 100644 index 0000000000..2314dc9ead --- /dev/null +++ b/apps/find-orphans/package.json @@ -0,0 +1,55 @@ +{ + "name": "find-orphans", + "version": "0.1.0", + "private": true, + "dependencies": { + "@contentful/app-sdk": "^4.29.1", + "@contentful/f36-components": "4.79.1", + "@contentful/f36-icons": "^5.6.0", + "@contentful/f36-tokens": "4.2.0", + "@contentful/react-apps-toolkit": "1.2.16", + "contentful-management": "^11.52.0", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "scripts": { + "start": "vite", + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "test": "vitest", + "test:ci": "vitest run", + "create-app-definition": "contentful-app-scripts create-app-definition", + "add-locations": "contentful-app-scripts add-locations", + "upload": "contentful-app-scripts upload --bundle-dir ./build" + }, + "eslintConfig": { + "extends": "react-app" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@contentful/app-scripts": "^2.3.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^14.3.1", + "@types/node": "^22.13.5", + "@types/react": "18.3.13", + "@types/react-dom": "18.3.1", + "@vitejs/plugin-react": "^4.0.3", + "jsdom": "^26.0.0", + "typescript": "4.9.5", + "vite": "^6.4.1", + "vitest": "^3.0.9" + }, + "homepage": "." +} diff --git a/apps/find-orphans/src/App.tsx b/apps/find-orphans/src/App.tsx new file mode 100644 index 0000000000..b789eba998 --- /dev/null +++ b/apps/find-orphans/src/App.tsx @@ -0,0 +1,26 @@ +import { useMemo } from 'react'; +import { locations } from '@contentful/app-sdk'; +import { useSDK } from '@contentful/react-apps-toolkit'; +import ConfigScreen from './locations/ConfigScreen'; +import Page from './locations/Page'; + +const ComponentLocationSettings = { + [locations.LOCATION_APP_CONFIG]: ConfigScreen, + [locations.LOCATION_PAGE]: Page, +}; + +const App = () => { + const sdk = useSDK(); + + const Component = useMemo(() => { + for (const [location, component] of Object.entries(ComponentLocationSettings)) { + if (sdk.location.is(location)) { + return component; + } + } + }, [sdk.location]); + + return Component ? : null; +}; + +export default App; diff --git a/apps/find-orphans/src/components/LocalhostWarning.tsx b/apps/find-orphans/src/components/LocalhostWarning.tsx new file mode 100644 index 0000000000..00a45aa50c --- /dev/null +++ b/apps/find-orphans/src/components/LocalhostWarning.tsx @@ -0,0 +1,31 @@ +import { Paragraph, TextLink, Note, Flex } from '@contentful/f36-components'; + +const LocalhostWarning = () => { + return ( + + + + Contentful Apps need to run inside the Contentful web app to function properly. Install + the app into a space and render your app into one of the{' '} + + available locations + + . + +
+ + + Follow{' '} + + our guide + {' '} + to get started or{' '} + open Contentful{' '} + to manage your app. + +
+
+ ); +}; + +export default LocalhostWarning; diff --git a/apps/find-orphans/src/index.tsx b/apps/find-orphans/src/index.tsx new file mode 100644 index 0000000000..ffe5ad660a --- /dev/null +++ b/apps/find-orphans/src/index.tsx @@ -0,0 +1,21 @@ +import { GlobalStyles } from '@contentful/f36-components'; +import { SDKProvider } from '@contentful/react-apps-toolkit'; + +import { createRoot } from 'react-dom/client'; +import App from './App'; +import LocalhostWarning from './components/LocalhostWarning'; + +const container = document.getElementById('root')!; +const root = createRoot(container); + +if (process.env.NODE_ENV === 'development' && window.self === window.top) { + // You can remove this if block before deploying your app + root.render(); +} else { + root.render( + + + + + ); +} diff --git a/apps/find-orphans/src/locations/ConfigScreen.tsx b/apps/find-orphans/src/locations/ConfigScreen.tsx new file mode 100644 index 0000000000..61d87c6a3c --- /dev/null +++ b/apps/find-orphans/src/locations/ConfigScreen.tsx @@ -0,0 +1,170 @@ +import { useCallback, useEffect, useState } from 'react'; +import { ConfigAppSDK } from '@contentful/app-sdk'; +import { + Box, + Flex, + Form, + FormControl, + Heading, + Paragraph, + Subheading, + TextInput, +} from '@contentful/f36-components'; +import { useSDK } from '@contentful/react-apps-toolkit'; +import { AppInstallationParameters, DEFAULT_PARAMETERS } from '../parameters'; + +interface NumberFieldProps { + id: keyof AppInstallationParameters; + label: string; + helpText: string; + placeholder: string; + value: string; + onChange: (id: keyof AppInstallationParameters, value: string) => void; +} + +const NumberField = ({ id, label, helpText, placeholder, value, onChange }: NumberFieldProps) => ( + + {label} + {/* The Box keeps the input on its own line at a sensible width instead + of stretching a number field across the whole form. */} + + onChange(id, event.target.value)} + min={1} + /> + + {helpText} + +); + +type FormValues = Record; + +// Human-readable names for validation messages, keyed by parameter id. +const FIELD_LABELS: Record = { + maxCandidates: 'Maximum entries per scan', + batchSize: 'Concurrent API requests', +}; + +// Stored parameters may be partial (e.g. the app was installed before a +// parameter existed), so each missing value falls back to its default. This +// also seeds the fresh-install form, letting the user save without typing. +const toFormValues = (parameters: Partial): FormValues => ({ + maxCandidates: String(parameters.maxCandidates ?? DEFAULT_PARAMETERS.maxCandidates), + batchSize: String(parameters.batchSize ?? DEFAULT_PARAMETERS.batchSize), +}); + +const ConfigScreen = () => { + const sdk = useSDK(); + // The form starts pre-filled with the recommended defaults so a fresh + // install is a single click; validation on save still requires each field. + const [values, setValues] = useState(toFormValues(DEFAULT_PARAMETERS)); + + // Called by Contentful when the user clicks "Save" on the app configuration + // screen. Returning false aborts the save and keeps the dialog open. + const onConfigure = useCallback(async () => { + const parsed = {} as Record; + for (const [key, value] of Object.entries(values) as [ + keyof AppInstallationParameters, + string + ][]) { + const parsedValue = Number.parseInt(value, 10); + // Empty fields fail this check too: the parameters are required, so + // there is no silent fallback to defaults on save. + if (Number.isNaN(parsedValue) || parsedValue < 1) { + sdk.notifier.error(`"${FIELD_LABELS[key]}" is required and must be a positive number.`); + return false; + } + parsed[key] = parsedValue; + } + // Enforce the CMA rate limit of 7 requests/second explicitly rather than + // silently clamping a higher value on save. + if (parsed.batchSize > 7) { + sdk.notifier.error(`"${FIELD_LABELS.batchSize}" must be between 1 and 7.`); + return false; + } + const parameters: AppInstallationParameters = parsed; + // Preserve the current location assignments (EditorInterface state) + // instead of resetting them on every save. + const currentState = await sdk.app.getCurrentState(); + return { parameters, targetState: currentState }; + }, [values, sdk]); + + useEffect(() => { + sdk.app.onConfigure(() => onConfigure()); + }, [sdk, onConfigure]); + + useEffect(() => { + const initialize = async () => { + // getParameters returns null when the app has never been configured; + // in that case the form keeps the pre-filled defaults. + const current = await sdk.app.getParameters(); + if (current) { + setValues(toFormValues(current as Partial)); + } + // Without setReady the config screen stays on a loading spinner forever. + sdk.app.setReady(); + }; + initialize(); + }, [sdk]); + + const handleChange = (id: keyof AppInstallationParameters, value: string) => { + setValues((previous) => ({ ...previous, [id]: value })); + }; + + return ( + + + Set up Find Orphans + + Find Orphans scans this space for draft entries with no value in their display (title) + field — the signature of an entry created by mistake, for example by adding a new entry on + a reference field instead of linking an existing one — and lets you review or archive them + from one page. + + + + Scan limits + + + These settings apply to everyone in this space. The recommended values are pre-filled, so + you can install as-is and tune them later. + + +
+ + + + + + Getting started + + + After installing, open the app from the Apps menu, run a scan, then review each entry or + archive the selected ones in bulk. + +
+
+ ); +}; + +export default ConfigScreen; diff --git a/apps/find-orphans/src/locations/Page/components/OrphanTable.tsx b/apps/find-orphans/src/locations/Page/components/OrphanTable.tsx new file mode 100644 index 0000000000..1325a6e614 --- /dev/null +++ b/apps/find-orphans/src/locations/Page/components/OrphanTable.tsx @@ -0,0 +1,92 @@ +import { Badge, Checkbox, Table, Text, TextLink } from '@contentful/f36-components'; +import { OrphanResult } from '../types'; + +interface OrphanTableProps { + results: OrphanResult[]; + selectedIds: string[]; + onToggleEntry: (entryId: string) => void; + onToggleAll: () => void; + onOpenEntry: (entryId: string) => void; + /** Disables selection while an archive operation is running. */ + isDisabled: boolean; +} + +const formatDate = (isoDate: string): string => + new Date(isoDate).toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + +export const OrphanTable = ({ + results, + selectedIds, + onToggleEntry, + onToggleAll, + onOpenEntry, + isDisabled, +}: OrphanTableProps) => { + const allSelected = results.length > 0 && selectedIds.length === results.length; + const someSelected = selectedIds.length > 0 && !allSelected; + + return ( + + + + + + + Title + Content type + Status + Last updated + + + + + {results.map(({ entry, contentType }) => ( + + + onToggleEntry(entry.sys.id)} + /> + + + {/* The scan only lists entries whose display field is empty, + so the title is always the editor's "Untitled" placeholder, + mirroring what the content list shows for these entries. */} + Untitled + + {contentType.name} + + Draft + + {formatDate(entry.sys.updatedAt)} + + {/* Previewing is an explicit action instead of a click on the + title, so selecting rows never accidentally triggers the + slide-in. "Preview" signals it is a slide-in, not a page + change. */} + onOpenEntry(entry.sys.id)}> + Preview + + + + ))} + +
+ ); +}; diff --git a/apps/find-orphans/src/locations/Page/index.tsx b/apps/find-orphans/src/locations/Page/index.tsx new file mode 100644 index 0000000000..63c1a8e1a0 --- /dev/null +++ b/apps/find-orphans/src/locations/Page/index.tsx @@ -0,0 +1,269 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { PageAppSDK } from '@contentful/app-sdk'; +import { + Box, + Button, + Flex, + Heading, + ModalConfirm, + Note, + Notification, + Paragraph, + Spinner, + Text, +} from '@contentful/f36-components'; +import { TrayArrowDownIcon, MagnifyingGlassIcon } from '@contentful/f36-icons'; +import { useSDK } from '@contentful/react-apps-toolkit'; +import { ContentTypeProps } from 'contentful-management'; +import { resolveParameters } from '../../parameters'; +import { OrphanTable } from './components/OrphanTable'; +import { OrphanResult, ScanProgress } from './types'; +import { archiveEntries, ArchiveProgress } from './utils/entryActions'; +import { fetchAllContentTypes, findOrphanedEntries } from './utils/orphanFinder'; + +// Name what is being checked right now, e.g. +// "Checking Article, Author… (5/50 content types)". +const progressLabel = (progress: ScanProgress): string => + `Checking ${progress.contentTypeNames.join(', ')}… (${progress.current}/${ + progress.total + } content types)`; + +const pluralize = (count: number) => (count === 1 ? 'entry' : 'entries'); + +const Page = () => { + const sdk = useSDK(); + // Installation parameters may be empty (fresh install, or the app was + // installed before the config screen existed), so merge with defaults. + const parameters = useMemo( + () => resolveParameters(sdk.parameters.installation), + [sdk.parameters.installation] + ); + const [contentTypes, setContentTypes] = useState([]); + const [contentTypesLoading, setContentTypesLoading] = useState(true); + const [scanning, setScanning] = useState(false); + const [progress, setProgress] = useState(null); + // null means "no scan has run yet", which hides the results section + // entirely; an empty array renders the positive empty state instead. + const [results, setResults] = useState(null); + const [truncated, setTruncated] = useState(false); + const [selectedIds, setSelectedIds] = useState([]); + const [archiving, setArchiving] = useState(false); + const [archiveProgress, setArchiveProgress] = useState(null); + const [confirmArchiveOpen, setConfirmArchiveOpen] = useState(false); + + const busy = scanning || archiving; + + // Content types are loaded once up front: the scan needs their display + // field definitions, and the list rarely changes within a session. + useEffect(() => { + const loadContentTypes = async () => { + try { + setContentTypes(await fetchAllContentTypes(sdk.cma)); + } catch (error) { + console.error('Loading content types failed:', error); + Notification.error('Could not load content types. Please reload the app.'); + } finally { + setContentTypesLoading(false); + } + }; + loadContentTypes(); + }, [sdk.cma]); + + const runScan = useCallback(async () => { + setScanning(true); + setResults(null); + setTruncated(false); + // A new scan invalidates any previous selection. + setSelectedIds([]); + try { + const outcome = await findOrphanedEntries( + sdk.cma, + contentTypes, + sdk.locales.default, + setProgress, + { + maxCandidates: parameters.maxCandidates, + batchSize: parameters.batchSize, + } + ); + setResults(outcome.results); + setTruncated(outcome.truncated); + } catch (error) { + // CMA calls run through the app bridge in the parent window, so a + // failure here may leave no trace in the iframe's network tab; this + // log is the only place the underlying error is visible. + console.error('Orphan scan failed:', error); + Notification.error('The scan failed. Please try again.'); + } finally { + setScanning(false); + setProgress(null); + } + }, [sdk, contentTypes, parameters]); + + const openEntry = useCallback( + (entryId: string) => { + // slideIn keeps the user on the results page while they inspect + // (and possibly delete) the entry in the standard editor. + sdk.navigator.openEntry(entryId, { slideIn: true }); + }, + [sdk] + ); + + const toggleEntry = useCallback((entryId: string) => { + setSelectedIds((previous) => + previous.includes(entryId) ? previous.filter((id) => id !== entryId) : [...previous, entryId] + ); + }, []); + + const toggleAll = useCallback(() => { + // Header checkbox: everything selected clears the selection, anything + // less (none or partial) selects all visible results. + setSelectedIds((previous) => + results !== null && previous.length < results.length + ? results.map((result) => result.entry.sys.id) + : [] + ); + }, [results]); + + const runArchive = useCallback(async () => { + setConfirmArchiveOpen(false); + setArchiving(true); + try { + const outcome = await archiveEntries( + sdk.cma, + selectedIds, + parameters.batchSize, + setArchiveProgress + ); + // Archived entries leave the result list; failed ones stay visible and + // selected so the user can retry them. + setResults( + (previous) => + previous?.filter((result) => !outcome.archivedIds.includes(result.entry.sys.id)) ?? null + ); + setSelectedIds(outcome.failedIds); + if (outcome.archivedIds.length > 0) { + Notification.success( + `Archived ${outcome.archivedIds.length} ${pluralize(outcome.archivedIds.length)}.` + ); + } + if (outcome.failedIds.length > 0) { + Notification.error( + `Could not archive ${outcome.failedIds.length} ${pluralize( + outcome.failedIds.length + )}. They remain selected so you can try again.` + ); + } + } catch { + Notification.error('Archiving failed. Please try again.'); + } finally { + setArchiving(false); + setArchiveProgress(null); + } + }, [sdk, selectedIds, parameters]); + + return ( + // Full width: page apps get the whole main column, and the results table + // benefits from every pixel of it. + + Find orphaned entries + + Finds draft entries with no value in their display (title) field — the signature of an entry + created by mistake, for example by adding a new entry on a reference field instead of + linking an existing one. + + + + + The regular Contentful search can only filter on the title field for one content type at a + time, because each content type defines its own display field. This scan runs that check + across every content type at once. + + + + + {scanning && progress && ( + + + {progressLabel(progress)} + + )} + + + {truncated && ( + + The scan stopped after {parameters.maxCandidates} draft entries. Archive or clean up + some of the results and scan again, or raise the limit in the app configuration. + + )} + + {results !== null && + (results.length === 0 ? ( + + No orphaned entries found — every draft has a title. + + ) : ( + <> + + + {results.length} {pluralize(results.length)} found + {selectedIds.length > 0 ? ` — ${selectedIds.length} selected` : ''} + + + {archiving && archiveProgress && ( + + + + Archiving… ({archiveProgress.current}/{archiveProgress.total}) + + + )} + + + + + + ))} + + + setConfirmArchiveOpen(false)} + onConfirm={runArchive} + confirmLabel={`Archive ${selectedIds.length} ${pluralize(selectedIds.length)}`} + cancelLabel="Cancel"> + + The selected {pluralize(selectedIds.length)} will be archived and disappear from the + content list. Archiving is reversible: entries can be unarchived from the entry editor at + any time. + + + + ); +}; + +export default Page; diff --git a/apps/find-orphans/src/locations/Page/types.ts b/apps/find-orphans/src/locations/Page/types.ts new file mode 100644 index 0000000000..5132b73df8 --- /dev/null +++ b/apps/find-orphans/src/locations/Page/types.ts @@ -0,0 +1,28 @@ +import { PageAppSDK } from '@contentful/app-sdk'; +import { ContentTypeProps, EntryProps } from 'contentful-management'; + +export type CmaClient = PageAppSDK['cma']; + +/** + * A draft entry flagged by the scan: its display (title) field has no value + * in the default locale, which is the signature of an entry created by + * mistake (e.g. from a reference field) and never filled in. + */ +export interface OrphanResult { + entry: EntryProps; + contentType: ContentTypeProps; +} + +export interface ScanProgress { + /** Content types checked so far. */ + current: number; + total: number; + /** Names of the content types being checked right now. */ + contentTypeNames: string[]; +} + +export interface ScanOutcome { + results: OrphanResult[]; + /** True when the scan hit the maxCandidates cap and stopped collecting further entries. */ + truncated: boolean; +} diff --git a/apps/find-orphans/src/locations/Page/utils/constants.ts b/apps/find-orphans/src/locations/Page/utils/constants.ts new file mode 100644 index 0000000000..f81727a199 --- /dev/null +++ b/apps/find-orphans/src/locations/Page/utils/constants.ts @@ -0,0 +1,24 @@ +/** Page size for CMA entry collection requests. */ +export const PAGE_LIMIT = 100; + +/** + * Page size for the content types collection. The CMA allows up to 1000 per + * request, so virtually every space loads its content model in one call. + */ +export const CONTENT_TYPE_PAGE_LIMIT = 1000; + +/** + * Default cap on candidate entries per scan, to protect against rate limits + * on huge spaces. Overridable via the `maxCandidates` installation parameter. + */ +export const MAX_CANDIDATES = 500; + +/** + * Default number of concurrent CMA requests while scanning and archiving + * (CMA limit: 7 req/s). Overridable via the `batchSize` installation + * parameter. + */ +export const BATCH_SIZE = 5; + +/** Display field types that can hold a title. */ +export const TEXT_FIELD_TYPES = ['Symbol', 'Text']; diff --git a/apps/find-orphans/src/locations/Page/utils/entryActions.ts b/apps/find-orphans/src/locations/Page/utils/entryActions.ts new file mode 100644 index 0000000000..f2ddc1c98a --- /dev/null +++ b/apps/find-orphans/src/locations/Page/utils/entryActions.ts @@ -0,0 +1,47 @@ +import { CmaClient } from '../types'; + +export interface ArchiveProgress { + current: number; + total: number; +} + +export interface ArchiveOutcome { + archivedIds: string[]; + failedIds: string[]; +} + +/** + * Archives the given entries in small concurrent batches, mirroring the + * reference-count batching so the CMA rate limit (7 req/s per space) is + * respected. One failed archive never aborts the rest of the batch; failures + * are collected so the caller can report them and keep the entries listed. + */ +export const archiveEntries = async ( + cma: CmaClient, + entryIds: string[], + batchSize: number, + onProgress: (progress: ArchiveProgress) => void +): Promise => { + const archivedIds: string[] = []; + const failedIds: string[] = []; + + for (let i = 0; i < entryIds.length; i += batchSize) { + const batch = entryIds.slice(i, i + batchSize); + onProgress({ current: Math.min(i + batch.length, entryIds.length), total: entryIds.length }); + await Promise.all( + batch.map(async (entryId) => { + try { + // Drafts are always unpublished, so archiving cannot fail on the + // "published entries cannot be archived" rule; failures here are + // permissions or version conflicts. + await cma.entry.archive({ entryId }); + archivedIds.push(entryId); + } catch { + failedIds.push(entryId); + } + }) + ); + } + + return { archivedIds, failedIds }; +}; diff --git a/apps/find-orphans/src/locations/Page/utils/orphanFinder.ts b/apps/find-orphans/src/locations/Page/utils/orphanFinder.ts new file mode 100644 index 0000000000..f295845edd --- /dev/null +++ b/apps/find-orphans/src/locations/Page/utils/orphanFinder.ts @@ -0,0 +1,190 @@ +import { ContentTypeProps, EntryProps, QueryOptions } from 'contentful-management'; +import { CmaClient, OrphanResult, ScanOutcome, ScanProgress } from '../types'; +import { CONTENT_TYPE_PAGE_LIMIT, PAGE_LIMIT, TEXT_FIELD_TYPES } from './constants'; + +/** Scan limits, sourced from installation parameters (see src/parameters.ts). */ +export interface ScanLimits { + maxCandidates: number; + /** Concurrent CMA entry queries while scanning. */ + batchSize: number; +} + +export const fetchAllContentTypes = async (cma: CmaClient): Promise => { + const all: ContentTypeProps[] = []; + let skip = 0; + let total = Infinity; + // The CMA caps collections at 1000 items per request, so this is a single + // round trip for virtually every space; the loop only continues for spaces + // with more than 1000 content types. + while (skip < total) { + const response = await cma.contentType.getMany({ + query: { skip, limit: CONTENT_TYPE_PAGE_LIMIT, order: 'name' }, + }); + all.push(...response.items); + total = response.total; + if (response.items.length === 0) break; + skip += response.items.length; + } + return all; +}; + +/** + * Returns the id of the content type's display field if it is a text field, + * otherwise undefined. The display field is what the Contentful UI shows as + * the entry title; only Symbol/Text fields can hold a missing-title signal, + * so content types without one are excluded from the scan entirely. + */ +export const getTextDisplayFieldId = (contentType: ContentTypeProps): string | undefined => { + const field = contentType.fields.find((f) => f.id === contentType.displayField); + return field && TEXT_FIELD_TYPES.includes(field.type) ? field.id : undefined; +}; + +export const getEntryTitle = ( + entry: EntryProps, + contentType: ContentTypeProps, + defaultLocale: string +): string | undefined => { + const displayFieldId = getTextDisplayFieldId(contentType); + if (!displayFieldId) return undefined; + // CMA entries key every field by locale; a whitespace-only title is treated + // the same as a missing one so it still renders as "Untitled" in the UI. + // `fields` itself can be absent: when a query `select`s specific fields and + // an entry has no value in any of them, the CMA omits the object entirely — + // which is exactly the shape of the orphans this scan looks for. + const value = entry.fields?.[displayFieldId]?.[defaultLocale]; + return typeof value === 'string' && value.trim() !== '' ? value : undefined; +}; + +/** + * Builds the CMA entry query for one content type. The draft scope (never + * published, not archived) is filtered server-side; whether the display field + * is empty is checked client-side by `getEntryTitle`, because the API's + * `[exists]` operator would miss whitespace-only titles. + */ +export const buildDraftEntryQuery = (contentType: ContentTypeProps): QueryOptions => { + const displayFieldId = getTextDisplayFieldId(contentType); + return { + content_type: contentType.sys.id, + // "Draft" means the entry has never been published. Entries that were + // published and then changed have a publishedAt and are excluded. + 'sys.publishedAt[exists]': false, + 'sys.archivedAt[exists]': false, + // The sys.id tiebreaker makes the order deterministic when many entries + // share an updatedAt (e.g. a bulk import); without it, skip-based paging + // can drop or duplicate entries across pages. + order: '-sys.updatedAt,sys.id', + // The scan only reads sys and the title, so skip every other field to + // keep response payloads small. Selecting fields requires content_type, + // which this query always sets. + select: displayFieldId ? `sys,fields.${displayFieldId}` : 'sys', + }; +}; + +/** + * Fetches all draft entries of one content type, paging until either the + * collection is exhausted or `budget` entries have been collected. + */ +const fetchDraftsOfContentType = async ( + cma: CmaClient, + contentType: ContentTypeProps, + budget: number +): Promise<{ entry: EntryProps; contentType: ContentTypeProps }[]> => { + const items: { entry: EntryProps; contentType: ContentTypeProps }[] = []; + let skip = 0; + let total = Infinity; + while (skip < total && items.length < budget) { + const limit = Math.min(PAGE_LIMIT, budget - items.length); + const response = await cma.entry.getMany({ + query: { ...buildDraftEntryQuery(contentType), skip, limit }, + }); + items.push(...response.items.map((entry) => ({ entry, contentType }))); + total = response.total; + if (response.items.length === 0) break; + skip += response.items.length; + } + return items; +}; + +/** + * Collects draft entries across all scannable content types, stopping once + * `maxCandidates` entries have been gathered so a large space cannot trigger + * an unbounded number of API calls. + * + * Content types are queried `concurrency` at a time: one query per content + * type is unavoidable (entry queries require a single content_type), but the + * queries are independent, so running them in parallel divides the wall-clock + * time of the scan by the concurrency factor. + */ +const fetchDraftCandidates = async ( + cma: CmaClient, + contentTypes: ContentTypeProps[], + maxCandidates: number, + concurrency: number, + onProgress: (progress: ScanProgress) => void +): Promise<{ + candidates: { entry: EntryProps; contentType: ContentTypeProps }[]; + truncated: boolean; +}> => { + const candidates: { entry: EntryProps; contentType: ContentTypeProps }[] = []; + let truncated = false; + let processed = 0; + + for (let i = 0; i < contentTypes.length && candidates.length < maxCandidates; i += concurrency) { + const chunk = contentTypes.slice(i, i + concurrency); + // Every content type in the chunk shares the same remaining-budget + // snapshot, so a chunk can overshoot the cap; the combined list is + // trimmed below and the overshoot reported as truncation. + const budget = maxCandidates - candidates.length; + processed += chunk.length; + // Report before the requests fire so the UI names the content types + // being checked while they are actually in flight. + onProgress({ + current: processed, + total: contentTypes.length, + contentTypeNames: chunk.map((contentType) => contentType.name), + }); + const chunkResults = await Promise.all( + chunk.map((contentType) => fetchDraftsOfContentType(cma, contentType, budget)) + ); + candidates.push(...chunkResults.flat()); + } + + if (candidates.length >= maxCandidates) { + // The cap was hit mid-scan: remaining content types (and pages) were + // not inspected, so the caller must surface a truncation warning. + truncated = true; + candidates.length = maxCandidates; + } + + return { candidates, truncated }; +}; + +/** + * Scans the environment for orphaned draft entries: never published, not + * archived, and with no value in their display (title) field in the default + * locale. Content types whose display field is not a text field are skipped — + * their entries cannot be missing a title. + */ +export const findOrphanedEntries = async ( + cma: CmaClient, + contentTypes: ContentTypeProps[], + defaultLocale: string, + onProgress: (progress: ScanProgress) => void, + limits: ScanLimits +): Promise => { + const scannableTypes = contentTypes.filter((ct) => getTextDisplayFieldId(ct) !== undefined); + + const { candidates, truncated } = await fetchDraftCandidates( + cma, + scannableTypes, + limits.maxCandidates, + limits.batchSize, + onProgress + ); + + const results: OrphanResult[] = candidates.filter( + ({ entry, contentType }) => getEntryTitle(entry, contentType, defaultLocale) === undefined + ); + + return { results, truncated }; +}; diff --git a/apps/find-orphans/src/parameters.ts b/apps/find-orphans/src/parameters.ts new file mode 100644 index 0000000000..342d857464 --- /dev/null +++ b/apps/find-orphans/src/parameters.ts @@ -0,0 +1,33 @@ +import { BATCH_SIZE, MAX_CANDIDATES } from './locations/Page/utils/constants'; + +export interface AppInstallationParameters { + /** Hard cap on candidate entries per scan. */ + maxCandidates: number; + /** Concurrent CMA requests while scanning and archiving. */ + batchSize: number; +} + +export const DEFAULT_PARAMETERS: AppInstallationParameters = { + maxCandidates: MAX_CANDIDATES, + batchSize: BATCH_SIZE, +}; + +const toPositiveInt = (value: unknown, fallback: number): number => { + const parsed = typeof value === 'string' ? Number.parseInt(value, 10) : value; + return typeof parsed === 'number' && Number.isFinite(parsed) && parsed >= 1 + ? Math.floor(parsed) + : fallback; +}; + +/** + * Merges raw installation parameters with defaults. Handles fresh installs + * (empty object) and hand-edited or legacy values (strings, out-of-range). + */ +export const resolveParameters = (raw: unknown): AppInstallationParameters => { + const params = (raw ?? {}) as Partial>; + return { + maxCandidates: toPositiveInt(params.maxCandidates, DEFAULT_PARAMETERS.maxCandidates), + // Capped at 7: the CMA rate limit is 7 requests/second per space. + batchSize: Math.min(7, toPositiveInt(params.batchSize, DEFAULT_PARAMETERS.batchSize)), + }; +}; diff --git a/apps/find-orphans/src/setupTests.ts b/apps/find-orphans/src/setupTests.ts new file mode 100644 index 0000000000..3604793183 --- /dev/null +++ b/apps/find-orphans/src/setupTests.ts @@ -0,0 +1,6 @@ +import '@testing-library/jest-dom/vitest'; +import { configure } from '@testing-library/react'; + +configure({ + testIdAttribute: 'data-test-id', +}); diff --git a/apps/find-orphans/test/App.test.tsx b/apps/find-orphans/test/App.test.tsx new file mode 100644 index 0000000000..8cba42577b --- /dev/null +++ b/apps/find-orphans/test/App.test.tsx @@ -0,0 +1,20 @@ +import { describe, expect, it, vi } from 'vitest'; +import { render } from '@testing-library/react'; +import App from '../src/App'; + +const mocks = vi.hoisted(() => ({ + sdk: { + location: { is: vi.fn().mockReturnValue(false) }, + }, +})); + +vi.mock('@contentful/react-apps-toolkit', () => ({ + useSDK: () => mocks.sdk, +})); + +describe('App', () => { + it('renders nothing when the location is not recognized', () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/apps/find-orphans/test/entryActions.test.ts b/apps/find-orphans/test/entryActions.test.ts new file mode 100644 index 0000000000..21fd8972d9 --- /dev/null +++ b/apps/find-orphans/test/entryActions.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it, vi } from 'vitest'; +import { archiveEntries } from '../src/locations/Page/utils/entryActions'; +import { createMockCma } from './mocks'; + +describe('archiveEntries', () => { + it('archives every entry and reports progress in batches', async () => { + const { cma, entryArchive } = createMockCma(); + const onProgress = vi.fn(); + + const outcome = await archiveEntries(cma, ['a', 'b', 'c'], 2, onProgress); + + expect(outcome.archivedIds).toEqual(expect.arrayContaining(['a', 'b', 'c'])); + expect(outcome.failedIds).toEqual([]); + expect(entryArchive).toHaveBeenCalledTimes(3); + // Two batches for three entries with a batch size of two. + expect(onProgress).toHaveBeenNthCalledWith(1, { current: 2, total: 3 }); + expect(onProgress).toHaveBeenNthCalledWith(2, { current: 3, total: 3 }); + }); + + it('collects failures without aborting the rest of the batch', async () => { + const { cma } = createMockCma({ failArchiveIds: ['b'] }); + + const outcome = await archiveEntries(cma, ['a', 'b', 'c'], 5, vi.fn()); + + expect(outcome.archivedIds).toEqual(expect.arrayContaining(['a', 'c'])); + expect(outcome.failedIds).toEqual(['b']); + }); + + it('handles an empty selection without any API calls', async () => { + const { cma, entryArchive } = createMockCma(); + + const outcome = await archiveEntries(cma, [], 5, vi.fn()); + + expect(outcome).toEqual({ archivedIds: [], failedIds: [] }); + expect(entryArchive).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/find-orphans/test/locations/ConfigScreen.test.tsx b/apps/find-orphans/test/locations/ConfigScreen.test.tsx new file mode 100644 index 0000000000..d817e2e839 --- /dev/null +++ b/apps/find-orphans/test/locations/ConfigScreen.test.tsx @@ -0,0 +1,138 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import ConfigScreen from '../../src/locations/ConfigScreen'; +import { createMockConfigSdk } from '../mocks'; + +const mocks = vi.hoisted(() => ({ + sdk: undefined as unknown, +})); + +vi.mock('@contentful/react-apps-toolkit', () => ({ + useSDK: () => mocks.sdk, +})); + +// The component registers its save handler via sdk.app.onConfigure; pull the +// latest registered callback so tests can invoke it like the web app does. +const getSaveCallback = (sdk: ReturnType) => { + const calls = sdk.app.onConfigure.mock.calls; + return calls[calls.length - 1][0] as () => Promise; +}; + +const fillAllFields = () => { + fireEvent.change(screen.getByLabelText(/Maximum entries per scan/), { + target: { value: '500' }, + }); + fireEvent.change(screen.getByLabelText(/Concurrent API requests/), { + target: { value: '5' }, + }); +}; + +describe('ConfigScreen', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('signals readiness and pre-fills the defaults on a fresh install', async () => { + const sdk = createMockConfigSdk(null); + mocks.sdk = sdk; + + render(); + + await waitFor(() => expect(sdk.app.setReady).toHaveBeenCalled()); + // The defaults are real values, not placeholders, so the app can be + // installed without touching the form. + expect(screen.getByLabelText(/Maximum entries per scan/)).toHaveValue(500); + expect(screen.getByLabelText(/Concurrent API requests/)).toHaveValue(5); + }); + + it('saves the pre-filled defaults without any edits', async () => { + const sdk = createMockConfigSdk(null); + mocks.sdk = sdk; + + render(); + await waitFor(() => expect(sdk.app.setReady).toHaveBeenCalled()); + + const result = await getSaveCallback(sdk)(); + expect(result).toMatchObject({ + parameters: { maxCandidates: 500, batchSize: 5 }, + }); + }); + + it('loads previously stored parameters', async () => { + const sdk = createMockConfigSdk({ maxCandidates: 100 }); + mocks.sdk = sdk; + + render(); + + await waitFor(() => expect(screen.getByLabelText(/Maximum entries per scan/)).toHaveValue(100)); + // A parameter missing from storage falls back to its default. + expect(screen.getByLabelText(/Concurrent API requests/)).toHaveValue(5); + }); + + it('saves the entered parameters on configure', async () => { + const sdk = createMockConfigSdk(null); + mocks.sdk = sdk; + + render(); + await waitFor(() => expect(sdk.app.setReady).toHaveBeenCalled()); + + fillAllFields(); + fireEvent.change(screen.getByLabelText(/Maximum entries per scan/), { + target: { value: '1000' }, + }); + + const result = await getSaveCallback(sdk)(); + expect(result).toMatchObject({ + parameters: { maxCandidates: 1000, batchSize: 5 }, + }); + }); + + it('rejects the save when a required field is cleared', async () => { + const sdk = createMockConfigSdk(null); + mocks.sdk = sdk; + + render(); + await waitFor(() => expect(sdk.app.setReady).toHaveBeenCalled()); + + fireEvent.change(screen.getByLabelText(/Maximum entries per scan/), { + target: { value: '' }, + }); + + await expect(getSaveCallback(sdk)()).resolves.toBe(false); + expect(sdk.notifier.error).toHaveBeenCalled(); + }); + + it('rejects the save when a value is not a positive number', async () => { + const sdk = createMockConfigSdk(null); + mocks.sdk = sdk; + + render(); + await waitFor(() => expect(sdk.app.setReady).toHaveBeenCalled()); + + fillAllFields(); + fireEvent.change(screen.getByLabelText(/Maximum entries per scan/), { + target: { value: '0' }, + }); + + await expect(getSaveCallback(sdk)()).resolves.toBe(false); + expect(sdk.notifier.error).toHaveBeenCalled(); + }); + + it('rejects a batch size above the CMA rate limit of 7', async () => { + const sdk = createMockConfigSdk(null); + mocks.sdk = sdk; + + render(); + await waitFor(() => expect(sdk.app.setReady).toHaveBeenCalled()); + + fillAllFields(); + fireEvent.change(screen.getByLabelText(/Concurrent API requests/), { + target: { value: '50' }, + }); + + await expect(getSaveCallback(sdk)()).resolves.toBe(false); + expect(sdk.notifier.error).toHaveBeenCalledWith( + '"Concurrent API requests" must be between 1 and 7.' + ); + }); +}); diff --git a/apps/find-orphans/test/locations/Page.test.tsx b/apps/find-orphans/test/locations/Page.test.tsx new file mode 100644 index 0000000000..2301f847ee --- /dev/null +++ b/apps/find-orphans/test/locations/Page.test.tsx @@ -0,0 +1,120 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; +import Page from '../../src/locations/Page'; +import { createMockCma, createMockSdk, makeMockEntry, mockArticleContentType } from '../mocks'; + +const mocks = vi.hoisted(() => ({ + sdk: undefined as unknown, +})); + +vi.mock('@contentful/react-apps-toolkit', () => ({ + useSDK: () => mocks.sdk, +})); + +const scan = async () => { + await waitFor(() => expect(screen.getByTestId('scan-button')).toBeEnabled()); + fireEvent.click(screen.getByTestId('scan-button')); +}; + +describe('Page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders the heading and the scan button', async () => { + const { cma } = createMockCma({ contentTypes: [mockArticleContentType] }); + mocks.sdk = createMockSdk(cma); + + render(); + + expect(screen.getByText('Find orphaned entries')).toBeInTheDocument(); + expect(screen.getByTestId('scan-button')).toBeInTheDocument(); + await waitFor(() => expect(screen.getByTestId('scan-button')).toBeEnabled()); + }); + + it('scans, lists orphaned entries, and opens one on demand', async () => { + const orphan = makeMockEntry('orphan-1', 'article'); + const { cma } = createMockCma({ + contentTypes: [mockArticleContentType], + entriesByContentType: { article: [orphan] }, + }); + const sdk = createMockSdk(cma); + mocks.sdk = sdk; + + render(); + await scan(); + + await waitFor(() => expect(screen.getByTestId('orphan-table')).toBeInTheDocument()); + expect(screen.getByTestId('result-count')).toHaveTextContent('1 entry found'); + expect(screen.getByText('Untitled')).toBeInTheDocument(); + expect(screen.getByText('Article')).toBeInTheDocument(); + + // Previewing is a dedicated action so row clicks never trigger the slide-in. + fireEvent.click(screen.getByText('Preview')); + expect(sdk.navigator.openEntry).toHaveBeenCalledWith('orphan-1', { slideIn: true }); + }); + + it('shows an empty state when nothing matches', async () => { + const { cma } = createMockCma({ contentTypes: [mockArticleContentType] }); + mocks.sdk = createMockSdk(cma); + + render(); + await scan(); + + await waitFor(() => expect(screen.getByTestId('empty-note')).toBeInTheDocument()); + }); + + it('selects all entries and archives them after confirmation', async () => { + const orphanA = makeMockEntry('orphan-a', 'article'); + const orphanB = makeMockEntry('orphan-b', 'article'); + const { cma, entryArchive } = createMockCma({ + contentTypes: [mockArticleContentType], + entriesByContentType: { article: [orphanA, orphanB] }, + }); + mocks.sdk = createMockSdk(cma); + + render(); + await scan(); + await waitFor(() => expect(screen.getByTestId('orphan-table')).toBeInTheDocument()); + + // The archive button only activates once something is selected. + expect(screen.getByTestId('archive-button')).toBeDisabled(); + fireEvent.click(screen.getByTestId('select-all')); + expect(screen.getByTestId('archive-button')).toBeEnabled(); + + fireEvent.click(screen.getByTestId('archive-button')); + // The confirmation modal must be accepted before anything is archived. + expect(entryArchive).not.toHaveBeenCalled(); + fireEvent.click(await screen.findByText('Archive 2 entries')); + + await waitFor(() => expect(entryArchive).toHaveBeenCalledTimes(2)); + // Archived entries leave the list, which empties it here. + await waitFor(() => expect(screen.getByTestId('empty-note')).toBeInTheDocument()); + }); + + it('keeps entries that failed to archive listed and selected', async () => { + const orphanA = makeMockEntry('orphan-a', 'article'); + const orphanB = makeMockEntry('orphan-b', 'article'); + const { cma } = createMockCma({ + contentTypes: [mockArticleContentType], + entriesByContentType: { article: [orphanA, orphanB] }, + failArchiveIds: ['orphan-b'], + }); + mocks.sdk = createMockSdk(cma); + + render(); + await scan(); + await waitFor(() => expect(screen.getByTestId('orphan-table')).toBeInTheDocument()); + + fireEvent.click(screen.getByTestId('select-all')); + fireEvent.click(screen.getByTestId('archive-button')); + fireEvent.click(await screen.findByText('Archive 2 entries')); + + await waitFor(() => + expect(screen.queryByTestId('orphan-row-orphan-a')).not.toBeInTheDocument() + ); + expect(screen.getByTestId('orphan-row-orphan-b')).toBeInTheDocument(); + // The f36 testId sits on the checkbox wrapper, so assert on the input. + expect(within(screen.getByTestId('orphan-row-orphan-b')).getByRole('checkbox')).toBeChecked(); + }); +}); diff --git a/apps/find-orphans/test/mocks/index.ts b/apps/find-orphans/test/mocks/index.ts new file mode 100644 index 0000000000..937ff550b5 --- /dev/null +++ b/apps/find-orphans/test/mocks/index.ts @@ -0,0 +1,4 @@ +export * from './mockContentTypes'; +export * from './mockEntries'; +export * from './mockCma'; +export * from './mockSdk'; diff --git a/apps/find-orphans/test/mocks/mockCma.ts b/apps/find-orphans/test/mocks/mockCma.ts new file mode 100644 index 0000000000..c8b855752e --- /dev/null +++ b/apps/find-orphans/test/mocks/mockCma.ts @@ -0,0 +1,49 @@ +import { vi } from 'vitest'; +import { ContentTypeProps, EntryProps } from 'contentful-management'; +import { CmaClient } from '../../src/locations/Page/types'; + +const collection = (items: T[], total = items.length) => ({ + sys: { type: 'Array' as const }, + items, + total, + skip: 0, + limit: 100, +}); + +export interface MockCmaOptions { + contentTypes?: ContentTypeProps[]; + /** Entries returned for entry queries, keyed by content type id. */ + entriesByContentType?: Record; + /** Entry ids whose archive call should reject. */ + failArchiveIds?: string[]; +} + +export const createMockCma = ({ + contentTypes = [], + entriesByContentType = {}, + failArchiveIds = [], +}: MockCmaOptions = {}) => { + const contentTypeGetMany = vi.fn().mockResolvedValue(collection(contentTypes)); + const entryGetMany = vi + .fn() + .mockImplementation(({ query }: { query: Record }) => { + const entries = entriesByContentType[query.content_type as string] ?? []; + if (query.skip && (query.skip as number) >= entries.length) { + return Promise.resolve(collection([], entries.length)); + } + return Promise.resolve(collection(entries)); + }); + const entryArchive = vi.fn().mockImplementation(({ entryId }: { entryId: string }) => { + if (failArchiveIds.includes(entryId)) { + return Promise.reject(new Error(`Cannot archive ${entryId}`)); + } + return Promise.resolve({ sys: { id: entryId } }); + }); + + const cma = { + contentType: { getMany: contentTypeGetMany }, + entry: { getMany: entryGetMany, archive: entryArchive }, + }; + + return { cma: cma as unknown as CmaClient, contentTypeGetMany, entryGetMany, entryArchive }; +}; diff --git a/apps/find-orphans/test/mocks/mockContentTypes.ts b/apps/find-orphans/test/mocks/mockContentTypes.ts new file mode 100644 index 0000000000..0c24d98370 --- /dev/null +++ b/apps/find-orphans/test/mocks/mockContentTypes.ts @@ -0,0 +1,61 @@ +import { ContentTypeProps } from 'contentful-management'; + +const baseSys = (id: string) => + ({ + id, + type: 'ContentType', + version: 1, + space: { sys: { type: 'Link', linkType: 'Space', id: 'space-id' } }, + environment: { sys: { type: 'Link', linkType: 'Environment', id: 'master' } }, + createdAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-01-01T00:00:00Z', + } as ContentTypeProps['sys']); + +export const mockArticleContentType: ContentTypeProps = { + sys: baseSys('article'), + name: 'Article', + description: '', + displayField: 'title', + fields: [ + { + id: 'title', + name: 'Title', + type: 'Symbol', + localized: false, + required: false, + disabled: false, + omitted: false, + validations: [], + }, + { + id: 'body', + name: 'Body', + type: 'Text', + localized: false, + required: false, + disabled: false, + omitted: false, + validations: [], + }, + ], +}; + +/** Content type whose display field is not a text field (edge case: no scannable title). */ +export const mockNumericDisplayContentType: ContentTypeProps = { + sys: baseSys('counter'), + name: 'Counter', + description: '', + displayField: 'count', + fields: [ + { + id: 'count', + name: 'Count', + type: 'Integer', + localized: false, + required: false, + disabled: false, + omitted: false, + validations: [], + }, + ], +}; diff --git a/apps/find-orphans/test/mocks/mockEntries.ts b/apps/find-orphans/test/mocks/mockEntries.ts new file mode 100644 index 0000000000..8377014c45 --- /dev/null +++ b/apps/find-orphans/test/mocks/mockEntries.ts @@ -0,0 +1,21 @@ +import { EntryProps } from 'contentful-management'; + +export const makeMockEntry = ( + id: string, + contentTypeId: string, + fields: EntryProps['fields'] = {}, + updatedAt = '2026-06-01T00:00:00Z' +): EntryProps => + ({ + sys: { + id, + type: 'Entry', + version: 1, + contentType: { sys: { type: 'Link', linkType: 'ContentType', id: contentTypeId } }, + space: { sys: { type: 'Link', linkType: 'Space', id: 'space-id' } }, + environment: { sys: { type: 'Link', linkType: 'Environment', id: 'master' } }, + createdAt: '2026-01-01T00:00:00Z', + updatedAt, + }, + fields, + } as EntryProps); diff --git a/apps/find-orphans/test/mocks/mockSdk.ts b/apps/find-orphans/test/mocks/mockSdk.ts new file mode 100644 index 0000000000..ac113ad0af --- /dev/null +++ b/apps/find-orphans/test/mocks/mockSdk.ts @@ -0,0 +1,42 @@ +import { vi } from 'vitest'; +import { CmaClient } from '../../src/locations/Page/types'; +import { AppInstallationParameters } from '../../src/parameters'; + +export const createMockSdk = ( + cma: CmaClient, + installation: Partial = {} +) => ({ + cma, + parameters: { + installation, + }, + locales: { + default: 'en-US', + available: ['en-US'], + }, + location: { + is: vi.fn().mockReturnValue(false), + }, + navigator: { + openEntry: vi.fn(), + }, + ids: { + space: 'space-id', + environment: 'master', + }, +}); + +/** SDK mock for the ConfigScreen location (`ConfigAppSDK` surface). */ +export const createMockConfigSdk = (storedParameters: unknown = null) => ({ + app: { + // onConfigure registers the save callback; the test grabs it from + // mock.calls to simulate the user clicking "Save". + onConfigure: vi.fn(), + getParameters: vi.fn().mockResolvedValue(storedParameters), + getCurrentState: vi.fn().mockResolvedValue({ EditorInterface: {} }), + setReady: vi.fn(), + }, + notifier: { + error: vi.fn(), + }, +}); diff --git a/apps/find-orphans/test/orphanFinder.test.ts b/apps/find-orphans/test/orphanFinder.test.ts new file mode 100644 index 0000000000..bd6e9daf39 --- /dev/null +++ b/apps/find-orphans/test/orphanFinder.test.ts @@ -0,0 +1,194 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + buildDraftEntryQuery, + fetchAllContentTypes, + findOrphanedEntries, + getEntryTitle, + getTextDisplayFieldId, +} from '../src/locations/Page/utils/orphanFinder'; +import { + createMockCma, + makeMockEntry, + mockArticleContentType, + mockNumericDisplayContentType, +} from './mocks'; + +const noProgress = vi.fn(); +const limits = { maxCandidates: 500, batchSize: 5 }; + +describe('getTextDisplayFieldId', () => { + it('returns the display field id when it is a text field', () => { + expect(getTextDisplayFieldId(mockArticleContentType)).toBe('title'); + }); + + it('returns undefined when the display field is not a text field', () => { + expect(getTextDisplayFieldId(mockNumericDisplayContentType)).toBeUndefined(); + }); +}); + +describe('getEntryTitle', () => { + it('returns the display field value in the default locale', () => { + const entry = makeMockEntry('e1', 'article', { title: { 'en-US': 'Hello' } }); + expect(getEntryTitle(entry, mockArticleContentType, 'en-US')).toBe('Hello'); + }); + + it('returns undefined for empty or missing values', () => { + const empty = makeMockEntry('e1', 'article', { title: { 'en-US': ' ' } }); + const missing = makeMockEntry('e2', 'article'); + expect(getEntryTitle(empty, mockArticleContentType, 'en-US')).toBeUndefined(); + expect(getEntryTitle(missing, mockArticleContentType, 'en-US')).toBeUndefined(); + }); + + it('returns undefined when the fields object is absent entirely', () => { + // With a `select` query, the CMA omits `fields` from entries that have no + // value in any selected field — the exact shape of an orphaned entry. + const noFields = makeMockEntry('e3', 'article'); + delete (noFields as Partial).fields; + expect(getEntryTitle(noFields, mockArticleContentType, 'en-US')).toBeUndefined(); + }); +}); + +describe('buildDraftEntryQuery', () => { + it('scopes to non-archived draft entries of the content type', () => { + const query = buildDraftEntryQuery(mockArticleContentType); + expect(query).toMatchObject({ + content_type: 'article', + 'sys.publishedAt[exists]': false, + 'sys.archivedAt[exists]': false, + }); + }); + + it('orders with a sys.id tiebreaker for stable skip-based pagination', () => { + // Without the tiebreaker, entries sharing an updatedAt (bulk imports) + // sort non-deterministically and paging can drop or duplicate them. + expect(buildDraftEntryQuery(mockArticleContentType).order).toBe('-sys.updatedAt,sys.id'); + }); + + it('selects only sys and the display field to keep payloads small', () => { + expect(buildDraftEntryQuery(mockArticleContentType).select).toBe('sys,fields.title'); + }); +}); + +describe('fetchAllContentTypes', () => { + it('returns all content types', async () => { + const { cma } = createMockCma({ + contentTypes: [mockArticleContentType, mockNumericDisplayContentType], + }); + const types = await fetchAllContentTypes(cma); + expect(types).toHaveLength(2); + }); +}); + +describe('findOrphanedEntries', () => { + it('lists drafts with an empty display field and excludes titled ones', async () => { + const orphan = makeMockEntry('orphan', 'article'); + const healthy = makeMockEntry('healthy', 'article', { title: { 'en-US': 'Has title' } }); + const { cma } = createMockCma({ + entriesByContentType: { article: [orphan, healthy] }, + }); + + const outcome = await findOrphanedEntries( + cma, + [mockArticleContentType], + 'en-US', + noProgress, + limits + ); + + expect(outcome.truncated).toBe(false); + expect(outcome.results).toHaveLength(1); + expect(outcome.results[0].entry.sys.id).toBe('orphan'); + }); + + it('treats a whitespace-only title as missing', async () => { + const blank = makeMockEntry('blank', 'article', { title: { 'en-US': ' ' } }); + const { cma } = createMockCma({ entriesByContentType: { article: [blank] } }); + + const outcome = await findOrphanedEntries( + cma, + [mockArticleContentType], + 'en-US', + noProgress, + limits + ); + + expect(outcome.results.map((r) => r.entry.sys.id)).toEqual(['blank']); + }); + + it('skips content types without a text display field', async () => { + // Entries of such types cannot be missing a title, so querying them + // would only waste API calls. + const { cma, entryGetMany } = createMockCma({ + entriesByContentType: { counter: [makeMockEntry('c1', 'counter')] }, + }); + + const outcome = await findOrphanedEntries( + cma, + [mockNumericDisplayContentType], + 'en-US', + noProgress, + limits + ); + + expect(outcome.results).toHaveLength(0); + expect(entryGetMany).not.toHaveBeenCalled(); + }); + + it('caps the fetched drafts at maxCandidates and reports truncation', async () => { + const entries = [makeMockEntry('e1', 'article'), makeMockEntry('e2', 'article')]; + const { cma } = createMockCma({ entriesByContentType: { article: entries } }); + + const outcome = await findOrphanedEntries(cma, [mockArticleContentType], 'en-US', noProgress, { + maxCandidates: 1, + batchSize: 5, + }); + + expect(outcome.truncated).toBe(true); + expect(outcome.results).toHaveLength(1); + }); + + it('combines results from multiple content types scanned in one parallel chunk', async () => { + const articleType = mockArticleContentType; + // A second scannable type: clone the article type under a different id + // so both have a text display field. + const noteType = { + ...mockArticleContentType, + sys: { ...mockArticleContentType.sys, id: 'note' }, + name: 'Note', + }; + const { cma, entryGetMany } = createMockCma({ + entriesByContentType: { + article: [makeMockEntry('a1', 'article')], + note: [makeMockEntry('n1', 'note')], + }, + }); + + const outcome = await findOrphanedEntries( + cma, + [articleType, noteType], + 'en-US', + noProgress, + limits + ); + + expect(outcome.results.map((r) => r.entry.sys.id)).toEqual( + expect.arrayContaining(['a1', 'n1']) + ); + // One entry query per content type; there is no reference-count phase. + expect(entryGetMany).toHaveBeenCalledTimes(2); + }); + + it('reports progress with the content types being checked', async () => { + const onProgress = vi.fn(); + const orphan = makeMockEntry('orphan', 'article'); + const { cma } = createMockCma({ entriesByContentType: { article: [orphan] } }); + + await findOrphanedEntries(cma, [mockArticleContentType], 'en-US', onProgress, limits); + + expect(onProgress).toHaveBeenCalledWith({ + current: 1, + total: 1, + contentTypeNames: ['Article'], + }); + }); +}); diff --git a/apps/find-orphans/test/parameters.test.ts b/apps/find-orphans/test/parameters.test.ts new file mode 100644 index 0000000000..d3e5ce3bd2 --- /dev/null +++ b/apps/find-orphans/test/parameters.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { DEFAULT_PARAMETERS, resolveParameters } from '../src/parameters'; + +describe('resolveParameters', () => { + it('returns defaults for missing or empty parameters', () => { + expect(resolveParameters(undefined)).toEqual(DEFAULT_PARAMETERS); + expect(resolveParameters(null)).toEqual(DEFAULT_PARAMETERS); + expect(resolveParameters({})).toEqual(DEFAULT_PARAMETERS); + }); + + it('keeps valid values and coerces numeric strings', () => { + const resolved = resolveParameters({ maxCandidates: 1000, batchSize: '3' }); + expect(resolved).toEqual({ maxCandidates: 1000, batchSize: 3 }); + }); + + it('falls back to defaults for invalid values', () => { + const resolved = resolveParameters({ maxCandidates: -5, batchSize: 'many' }); + expect(resolved).toEqual(DEFAULT_PARAMETERS); + }); + + it('caps batchSize at the CMA rate limit of 7', () => { + expect(resolveParameters({ batchSize: 50 }).batchSize).toBe(7); + }); +}); diff --git a/apps/find-orphans/tsconfig.json b/apps/find-orphans/tsconfig.json new file mode 100644 index 0000000000..8fe18b5ba8 --- /dev/null +++ b/apps/find-orphans/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["@testing-library/jest-dom"] + }, + "include": ["src", "test"] +} diff --git a/apps/find-orphans/vite.config.mts b/apps/find-orphans/vite.config.mts new file mode 100644 index 0000000000..b2c43f22cb --- /dev/null +++ b/apps/find-orphans/vite.config.mts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: '', + build: { + outDir: 'build', + }, + server: { + host: 'localhost', + port: 3000, + }, +}); diff --git a/apps/find-orphans/vitest.config.mts b/apps/find-orphans/vitest.config.mts new file mode 100644 index 0000000000..7548a9871a --- /dev/null +++ b/apps/find-orphans/vitest.config.mts @@ -0,0 +1,18 @@ +import { loadEnv } from 'vite'; +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./src/setupTests.ts'], + env: loadEnv('test', process.cwd(), ''), + server: { + deps: { + inline: ['@contentful/f36-icons'], + }, + }, + }, +}); From 3265a562ba83f39bebd832465d8d11cb1a10d4f8 Mon Sep 17 00:00:00 2001 From: Shanon Place Date: Mon, 13 Jul 2026 15:41:00 -0700 Subject: [PATCH 2/8] Updated the logic and removed some of the complexity --- apps/find-orphans/AGENTS.md | 72 ++++++++++--------- apps/find-orphans/README.md | 20 +++++- .../src/locations/ConfigScreen.tsx | 54 ++++++++++---- .../find-orphans/src/locations/Page/index.tsx | 5 ++ .../src/locations/Page/utils/constants.ts | 9 +++ .../src/locations/Page/utils/orphanFinder.ts | 29 +++++--- apps/find-orphans/src/parameters.ts | 19 ++++- .../test/locations/ConfigScreen.test.tsx | 22 +++++- apps/find-orphans/test/mocks/mockEntries.ts | 7 +- apps/find-orphans/test/orphanFinder.test.ts | 44 ++++++++++-- apps/find-orphans/test/parameters.test.ts | 11 ++- 11 files changed, 223 insertions(+), 69 deletions(-) diff --git a/apps/find-orphans/AGENTS.md b/apps/find-orphans/AGENTS.md index 77946e023e..625df5e3af 100644 --- a/apps/find-orphans/AGENTS.md +++ b/apps/find-orphans/AGENTS.md @@ -2,9 +2,11 @@ ## What This App Does Full-page app that scans an environment for "orphaned" draft entries — typically empty entries -accidentally created from a reference field when the user meant to link an existing entry. The -user selects criteria (missing display title, unreferenced, stale draft) and runs a scan; results -deep-link into the entry editor. +accidentally created from a reference field when the user meant to link an existing entry. An +orphan is a draft (never published, not archived) with no value in its display (title) field in +the default locale; with the `untouchedOnly` parameter (default on) it must also never have been +saved after creation (`sys.version === 1`). Results deep-link into the entry editor and can be +archived in bulk. ## Archetype Standard Vite app. Page location plus an app-config screen for installation parameters. @@ -13,25 +15,25 @@ Standard Vite app. Page location plus an app-config screen for installation para | Location | File | Purpose | |----------|------|---------| -| `LOCATION_PAGE` | `src/locations/Page/` | Criteria panel + scan + results table | -| `LOCATION_APP_CONFIG` | `src/locations/ConfigScreen.tsx` | Installation parameters (scan limits) | +| `LOCATION_PAGE` | `src/locations/Page/` | Scan + results table + bulk archive | +| `LOCATION_APP_CONFIG` | `src/locations/ConfigScreen.tsx` | Installation parameters | ## Installation Parameters Defined in `src/parameters.ts`; the constants in `src/locations/Page/utils/constants.ts` are the -defaults. All three parameters are **required** on the app definition, with their default values -set there too — the ConfigScreen shows the defaults only as placeholders and rejects saves with -empty or out-of-range values. `resolveParameters()` remains the defensive fallback on the Page -side, so a scan never runs with missing or invalid limits. +defaults. All parameters are **required** on the app definition, with their default values set +there too — the ConfigScreen pre-fills the defaults and rejects saves with empty or out-of-range +values. `resolveParameters()` remains the defensive fallback on the Page side, so a scan never +runs with missing or invalid settings. | Parameter ID | Type | Required | Default | Purpose | |--------------|------|----------|---------|---------| | `maxCandidates` | Number | Yes | 500 | Hard cap on candidate entries per scan | -| `defaultStaleDays` | Number | Yes | 30 | Pre-fill for the "not updated in N days" criterion | -| `referenceBatchSize` | Number | Yes | 5 (max 7) | Concurrent CMA requests (scan queries, reference counts, archiving) | +| `batchSize` | Number | Yes | 5 (max 7) | Concurrent CMA requests (scan queries, archiving) | +| `untouchedOnly` | Boolean | Yes | true | Only flag drafts never saved after creation (`sys.version === 1`) | -Parameter definitions on the app definition must use these exact IDs and the Number type; the -README documents the full table (including descriptions) for the app definition setup. +Parameter definitions on the app definition must use these exact IDs and types; the README +documents the full table (including descriptions) for the app definition setup. ## Key Dependencies @@ -53,8 +55,8 @@ src/ ├── ConfigScreen.tsx # Installation parameter form └── Page/ ├── index.tsx # Page UI, scan orchestration - ├── types.ts # Criteria/result/progress types - ├── components/ # CriteriaPanel, OrphanTable + ├── types.ts # Result/progress types + ├── components/ # OrphanTable └── utils/ ├── constants.ts # Fallback defaults for parameters ├── entryActions.ts # Batched bulk archive (pure, unit-tested) @@ -63,34 +65,40 @@ src/ ## Sharp Edges & Invariants -- **Criteria are OR-combined** (`matchesAnyCriterion`): an entry is listed when it matches at - least one selected criterion, so selecting more criteria widens the results. The CMA cannot OR - independent filters in one query, so only the draft scope is filtered server-side; criteria are - evaluated client-side. With no criteria selected, every draft is listed. -- **Draft scope is fixed**: every entry query includes `sys.publishedAt[exists]=false` and - `sys.archivedAt[exists]=false`. +- **Draft scope is fixed and filtered server-side**: every entry query includes + `sys.publishedAt[exists]=false` and `sys.archivedAt[exists]=false`. The remaining criteria are + evaluated client-side. +- **The empty-title check must stay client-side**: the API's `[exists]` operator misses + whitespace-only titles, and with a `select` query the CMA omits the `fields` object entirely + from entries that have no value in any selected field — both shapes count as orphans. +- **The version check must stay client-side too**: `sys.version` is not a queryable attribute in + CMA entry searches, but it rides along in every returned `sys` object, so it costs nothing. + Version 1 means "never saved after creation" only because published and archived entries are + already excluded — publish/unpublish/archive/unarchive also bump `sys.version`. - **Default-locale only**: the missing-title check and title rendering use `sdk.locales.default`. Localized titles in other locales are not considered. -- **Missing-title never applies to non-text display fields**: `matchesAnyCriterion` requires - `hasTitleField`, otherwise every entry of such a type would vacuously match. Those content - types are skipped entirely only when missing-title is the sole selected criterion. +- **Content types without a text display field are skipped entirely** — their entries cannot be + missing a title, so querying them would waste API calls. - **One entry query per content type is unavoidable** (entry queries require a single - `content_type`), so the scan runs those queries `referenceBatchSize` at a time in parallel - chunks. A chunk shares one remaining-budget snapshot and can overshoot `maxCandidates`; the - result is trimmed and flagged truncated afterwards. -- **Reference counting is N+1**: one `links_to_entry` query per candidate (`limit: 0`, total - only), batched `referenceBatchSize` at a time. `resolveParameters()` clamps the batch size to 7 - (the CMA req/s limit per space) — keep that clamp. When the unreferenced criterion is off, the - scan filters by the cheap criteria first and only counts entries that will be listed — keep - that ordering, it is the main cost control. + `content_type`), so the scan runs those queries `batchSize` at a time in parallel chunks. A + chunk shares one remaining-budget snapshot and can overshoot `maxCandidates`; the result is + trimmed and flagged truncated afterwards. - **`maxCandidates` caps each scan** — the UI shows a truncation warning. Keep the cap or replace it with real pagination, but never scan unbounded. +- **Pagination order needs the `sys.id` tiebreaker**: entries sharing an `updatedAt` (bulk + imports) sort non-deterministically otherwise, and skip-based paging can drop or duplicate + them. +- **`resolveParameters()` clamps `batchSize` to 7** (the CMA req/s limit per space) — keep that + clamp. - **Never read `sdk.parameters.installation` directly** — always go through `resolveParameters()` so fresh installs and hand-edited values fall back to safe defaults. - **`sdk.app.setReady()` must stay in the ConfigScreen init effect** — without it the config screen never leaves its loading state. - **Keep `orphanFinder.ts` free of React/UI imports** — it is the unit-tested core; the Page component only orchestrates state and rendering. +- **README (including the mermaid diagram) must track logic changes** — the scan criteria are + documented in three places that must agree: README, the Page intro paragraph, and the + ConfigScreen help texts. ## Never / Always diff --git a/apps/find-orphans/README.md b/apps/find-orphans/README.md index 6268efc00b..62343cb3aa 100644 --- a/apps/find-orphans/README.md +++ b/apps/find-orphans/README.md @@ -10,6 +10,14 @@ The app scans the current environment for **draft entries** (never published, no whose **display (title) field has no value** in the default locale — including whitespace-only values, which the entry editor also renders as "Untitled". +By default the scan additionally requires that the entry was **never edited after creation** +(`sys.version === 1`). The CMA bumps `sys.version` on every save — and on publish, unpublish, +archive and unarchive, but those states are already excluded by the draft scope — so on a +candidate, version 1 can only mean the entry was created and then abandoned untouched. This +keeps untitled work-in-progress drafts (body started, title not yet filled in) out of the +results. The filter can be turned off via the `untouchedOnly` installation parameter, which is +useful when apps or scripts write to entries on creation and bump every entry past version 1. + This is something the regular Contentful search cannot do in one query: field filters only work for one content type at a time, and every content type defines its own display field. The app automates that per-content-type check across the whole content model. Content types whose @@ -39,7 +47,10 @@ flowchart TD Pages --> More{More content types
and budget left?} More -- yes --> Chunk More -- no --> Check[Client-side check: display field empty,
whitespace-only, or fields object absent
in the default locale] - Check --> Results[Results table
+ truncation warning if capped] + Check --> Untouched{untouchedOnly
enabled?} + Untouched -- yes --> Version[Keep only entries never edited
after creation: sys.version == 1] + Untouched -- no --> Results[Results table
+ truncation warning if capped] + Version --> Results Results --> Archive([User selects rows and confirms Archive]) Archive --> Batches[Archive entries in batches of batchSize] Batches --> Done[Archived rows leave the list;
failed ones stay listed and selected for retry] @@ -48,7 +59,9 @@ flowchart TD The empty-title check runs client-side rather than via the API's `[exists]` operator for two reasons: `[exists]` misses whitespace-only titles, and entries with no value in any selected field come back from the CMA with no `fields` object at all — both shapes must count as -orphans. +orphans. The version check is also client-side by necessity: `sys.version` is not a queryable +attribute in CMA entry searches, but it is part of every returned `sys` object, so the check +costs no extra API traffic. ## Configuration @@ -60,8 +73,9 @@ registering the app definition, create the parameter definitions with exactly th |--------------|----|------|----------|---------------|-------------| | Maximum entries per scan | `maxCandidates` | Number | Yes | `500` | The scan stops after this many draft entries, to stay friendly to API rate limits. | | Concurrent API requests | `batchSize` | Number | Yes | `5` | How many CMA requests run at once while scanning and archiving. Must be between 1 and 7, the CMA rate limit per second. | +| Only include entries that were never edited | `untouchedOnly` | Boolean | Yes | `true` | Only flag drafts still at version 1, i.e. never saved after creation. Turn off to also catch untitled drafts that were edited and then abandoned. | -Both parameters are required and their defaults are set on the parameter definition, so a fresh +All parameters are required and their defaults are set on the parameter definition, so a fresh install starts with the values above. The config screen shows the same defaults as input placeholders, and its save validation rejects empty, non-positive, or out-of-range values. As a safety net, `resolveParameters()` still falls back to the defaults at scan time if the stored diff --git a/apps/find-orphans/src/locations/ConfigScreen.tsx b/apps/find-orphans/src/locations/ConfigScreen.tsx index 61d87c6a3c..2039955f7f 100644 --- a/apps/find-orphans/src/locations/ConfigScreen.tsx +++ b/apps/find-orphans/src/locations/ConfigScreen.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react'; import { ConfigAppSDK } from '@contentful/app-sdk'; import { Box, + Checkbox, Flex, Form, FormControl, @@ -13,13 +14,17 @@ import { import { useSDK } from '@contentful/react-apps-toolkit'; import { AppInstallationParameters, DEFAULT_PARAMETERS } from '../parameters'; +// The numeric parameters share the parse-and-validate save path below; the +// boolean parameter is a checkbox and needs no validation. +type NumberParameterId = 'maxCandidates' | 'batchSize'; + interface NumberFieldProps { - id: keyof AppInstallationParameters; + id: NumberParameterId; label: string; helpText: string; placeholder: string; value: string; - onChange: (id: keyof AppInstallationParameters, value: string) => void; + onChange: (id: NumberParameterId, value: string) => void; } const NumberField = ({ id, label, helpText, placeholder, value, onChange }: NumberFieldProps) => ( @@ -40,20 +45,29 @@ const NumberField = ({ id, label, helpText, placeholder, value, onChange }: Numb ); -type FormValues = Record; +// Number inputs are kept as strings while editing (so a cleared field stays +// cleared instead of snapping to 0); the checkbox is a real boolean. +interface FormValues { + maxCandidates: string; + batchSize: string; + untouchedOnly: boolean; +} // Human-readable names for validation messages, keyed by parameter id. -const FIELD_LABELS: Record = { +const FIELD_LABELS: Record = { maxCandidates: 'Maximum entries per scan', batchSize: 'Concurrent API requests', }; +const NUMBER_PARAMETER_IDS: NumberParameterId[] = ['maxCandidates', 'batchSize']; + // Stored parameters may be partial (e.g. the app was installed before a // parameter existed), so each missing value falls back to its default. This // also seeds the fresh-install form, letting the user save without typing. const toFormValues = (parameters: Partial): FormValues => ({ maxCandidates: String(parameters.maxCandidates ?? DEFAULT_PARAMETERS.maxCandidates), batchSize: String(parameters.batchSize ?? DEFAULT_PARAMETERS.batchSize), + untouchedOnly: parameters.untouchedOnly ?? DEFAULT_PARAMETERS.untouchedOnly, }); const ConfigScreen = () => { @@ -65,12 +79,9 @@ const ConfigScreen = () => { // Called by Contentful when the user clicks "Save" on the app configuration // screen. Returning false aborts the save and keeps the dialog open. const onConfigure = useCallback(async () => { - const parsed = {} as Record; - for (const [key, value] of Object.entries(values) as [ - keyof AppInstallationParameters, - string - ][]) { - const parsedValue = Number.parseInt(value, 10); + const parsed = {} as Record; + for (const key of NUMBER_PARAMETER_IDS) { + const parsedValue = Number.parseInt(values[key], 10); // Empty fields fail this check too: the parameters are required, so // there is no silent fallback to defaults on save. if (Number.isNaN(parsedValue) || parsedValue < 1) { @@ -85,7 +96,10 @@ const ConfigScreen = () => { sdk.notifier.error(`"${FIELD_LABELS.batchSize}" must be between 1 and 7.`); return false; } - const parameters: AppInstallationParameters = parsed; + const parameters: AppInstallationParameters = { + ...parsed, + untouchedOnly: values.untouchedOnly, + }; // Preserve the current location assignments (EditorInterface state) // instead of resetting them on every save. const currentState = await sdk.app.getCurrentState(); @@ -110,7 +124,7 @@ const ConfigScreen = () => { initialize(); }, [sdk]); - const handleChange = (id: keyof AppInstallationParameters, value: string) => { + const handleChange = (id: NumberParameterId, value: string) => { setValues((previous) => ({ ...previous, [id]: value })); }; @@ -153,6 +167,22 @@ const ConfigScreen = () => { value={values.batchSize} onChange={handleChange} /> + + + setValues((previous) => ({ ...previous, untouchedOnly: event.target.checked })) + } + testId="untouched-only"> + Only include entries that were never edited + + + Contentful bumps an entry's version on every save, so an untitled draft still at + version 1 was abandoned right after creation. Turn this off to also flag untitled + drafts that were edited at least once — for example when other apps or scripts write + to entries on creation. + + diff --git a/apps/find-orphans/src/locations/Page/index.tsx b/apps/find-orphans/src/locations/Page/index.tsx index 63c1a8e1a0..24e76cb85b 100644 --- a/apps/find-orphans/src/locations/Page/index.tsx +++ b/apps/find-orphans/src/locations/Page/index.tsx @@ -84,6 +84,7 @@ const Page = () => { { maxCandidates: parameters.maxCandidates, batchSize: parameters.batchSize, + untouchedOnly: parameters.untouchedOnly, } ); setResults(outcome.results); @@ -171,6 +172,10 @@ const Page = () => { Finds draft entries with no value in their display (title) field — the signature of an entry created by mistake, for example by adding a new entry on a reference field instead of linking an existing one. + {/* Whether the never-edited filter applies is decided in the app + configuration, so the description must reflect the actual scan. */} + {parameters.untouchedOnly && + ' Only entries that were never edited after creation are included.'} diff --git a/apps/find-orphans/src/locations/Page/utils/constants.ts b/apps/find-orphans/src/locations/Page/utils/constants.ts index f81727a199..e9109b8b79 100644 --- a/apps/find-orphans/src/locations/Page/utils/constants.ts +++ b/apps/find-orphans/src/locations/Page/utils/constants.ts @@ -20,5 +20,14 @@ export const MAX_CANDIDATES = 500; */ export const BATCH_SIZE = 5; +/** + * Default for the `untouchedOnly` installation parameter: only flag drafts + * that were never saved after creation (sys.version === 1). On by default + * because a version-1 untitled draft is the purest orphan signature — the + * entry was created (e.g. from a reference field) and then abandoned without + * a single edit. + */ +export const UNTOUCHED_ONLY = true; + /** Display field types that can hold a title. */ export const TEXT_FIELD_TYPES = ['Symbol', 'Text']; diff --git a/apps/find-orphans/src/locations/Page/utils/orphanFinder.ts b/apps/find-orphans/src/locations/Page/utils/orphanFinder.ts index f295845edd..7f03963ab4 100644 --- a/apps/find-orphans/src/locations/Page/utils/orphanFinder.ts +++ b/apps/find-orphans/src/locations/Page/utils/orphanFinder.ts @@ -2,11 +2,13 @@ import { ContentTypeProps, EntryProps, QueryOptions } from 'contentful-managemen import { CmaClient, OrphanResult, ScanOutcome, ScanProgress } from '../types'; import { CONTENT_TYPE_PAGE_LIMIT, PAGE_LIMIT, TEXT_FIELD_TYPES } from './constants'; -/** Scan limits, sourced from installation parameters (see src/parameters.ts). */ -export interface ScanLimits { +/** Scan settings, sourced from installation parameters (see src/parameters.ts). */ +export interface ScanOptions { maxCandidates: number; /** Concurrent CMA entry queries while scanning. */ batchSize: number; + /** Only flag drafts never saved after creation (sys.version === 1). */ + untouchedOnly: boolean; } export const fetchAllContentTypes = async (cma: CmaClient): Promise => { @@ -164,27 +166,38 @@ const fetchDraftCandidates = async ( * archived, and with no value in their display (title) field in the default * locale. Content types whose display field is not a text field are skipped — * their entries cannot be missing a title. + * + * With `untouchedOnly` set, an untitled draft is only flagged when it was + * never saved after creation, which filters out work-in-progress drafts that + * simply have not been given a title yet. */ export const findOrphanedEntries = async ( cma: CmaClient, contentTypes: ContentTypeProps[], defaultLocale: string, onProgress: (progress: ScanProgress) => void, - limits: ScanLimits + options: ScanOptions ): Promise => { const scannableTypes = contentTypes.filter((ct) => getTextDisplayFieldId(ct) !== undefined); const { candidates, truncated } = await fetchDraftCandidates( cma, scannableTypes, - limits.maxCandidates, - limits.batchSize, + options.maxCandidates, + options.batchSize, onProgress ); - const results: OrphanResult[] = candidates.filter( - ({ entry, contentType }) => getEntryTitle(entry, contentType, defaultLocale) === undefined - ); + const results: OrphanResult[] = candidates.filter(({ entry, contentType }) => { + if (getEntryTitle(entry, contentType, defaultLocale) !== undefined) return false; + // The CMA bumps sys.version on every write (updates, but also publish, + // unpublish, archive and unarchive). Publish and archive states are + // already excluded by the draft query, so on a candidate version 1 can + // only mean the entry was never saved after creation. This check must + // stay client-side: sys.version is not a queryable attribute in CMA + // entry searches. + return !options.untouchedOnly || entry.sys.version === 1; + }); return { results, truncated }; }; diff --git a/apps/find-orphans/src/parameters.ts b/apps/find-orphans/src/parameters.ts index 342d857464..330631e12b 100644 --- a/apps/find-orphans/src/parameters.ts +++ b/apps/find-orphans/src/parameters.ts @@ -1,15 +1,23 @@ -import { BATCH_SIZE, MAX_CANDIDATES } from './locations/Page/utils/constants'; +import { BATCH_SIZE, MAX_CANDIDATES, UNTOUCHED_ONLY } from './locations/Page/utils/constants'; export interface AppInstallationParameters { /** Hard cap on candidate entries per scan. */ maxCandidates: number; /** Concurrent CMA requests while scanning and archiving. */ batchSize: number; + /** + * When true, only flag drafts that were never saved after creation + * (sys.version === 1). Filters out untitled drafts someone has actually + * worked on, at the cost of missing abandoned drafts that got one stray + * edit. + */ + untouchedOnly: boolean; } export const DEFAULT_PARAMETERS: AppInstallationParameters = { maxCandidates: MAX_CANDIDATES, batchSize: BATCH_SIZE, + untouchedOnly: UNTOUCHED_ONLY, }; const toPositiveInt = (value: unknown, fallback: number): number => { @@ -19,6 +27,14 @@ const toPositiveInt = (value: unknown, fallback: number): number => { : fallback; }; +const toBoolean = (value: unknown, fallback: boolean): boolean => { + // Boolean parameters arrive as real booleans from the app definition, but + // hand-edited parameters may store them as strings. + if (value === true || value === 'true') return true; + if (value === false || value === 'false') return false; + return fallback; +}; + /** * Merges raw installation parameters with defaults. Handles fresh installs * (empty object) and hand-edited or legacy values (strings, out-of-range). @@ -29,5 +45,6 @@ export const resolveParameters = (raw: unknown): AppInstallationParameters => { maxCandidates: toPositiveInt(params.maxCandidates, DEFAULT_PARAMETERS.maxCandidates), // Capped at 7: the CMA rate limit is 7 requests/second per space. batchSize: Math.min(7, toPositiveInt(params.batchSize, DEFAULT_PARAMETERS.batchSize)), + untouchedOnly: toBoolean(params.untouchedOnly, DEFAULT_PARAMETERS.untouchedOnly), }; }; diff --git a/apps/find-orphans/test/locations/ConfigScreen.test.tsx b/apps/find-orphans/test/locations/ConfigScreen.test.tsx index d817e2e839..f2680d6777 100644 --- a/apps/find-orphans/test/locations/ConfigScreen.test.tsx +++ b/apps/find-orphans/test/locations/ConfigScreen.test.tsx @@ -54,12 +54,12 @@ describe('ConfigScreen', () => { const result = await getSaveCallback(sdk)(); expect(result).toMatchObject({ - parameters: { maxCandidates: 500, batchSize: 5 }, + parameters: { maxCandidates: 500, batchSize: 5, untouchedOnly: true }, }); }); it('loads previously stored parameters', async () => { - const sdk = createMockConfigSdk({ maxCandidates: 100 }); + const sdk = createMockConfigSdk({ maxCandidates: 100, untouchedOnly: false }); mocks.sdk = sdk; render(); @@ -67,6 +67,24 @@ describe('ConfigScreen', () => { await waitFor(() => expect(screen.getByLabelText(/Maximum entries per scan/)).toHaveValue(100)); // A parameter missing from storage falls back to its default. expect(screen.getByLabelText(/Concurrent API requests/)).toHaveValue(5); + expect(screen.getByLabelText(/Only include entries that were never edited/)).not.toBeChecked(); + }); + + it('saves the toggled never-edited filter', async () => { + const sdk = createMockConfigSdk(null); + mocks.sdk = sdk; + + render(); + await waitFor(() => expect(sdk.app.setReady).toHaveBeenCalled()); + + const checkbox = screen.getByLabelText(/Only include entries that were never edited/); + expect(checkbox).toBeChecked(); + fireEvent.click(checkbox); + + const result = await getSaveCallback(sdk)(); + expect(result).toMatchObject({ + parameters: { maxCandidates: 500, batchSize: 5, untouchedOnly: false }, + }); }); it('saves the entered parameters on configure', async () => { diff --git a/apps/find-orphans/test/mocks/mockEntries.ts b/apps/find-orphans/test/mocks/mockEntries.ts index 8377014c45..d3454dfb98 100644 --- a/apps/find-orphans/test/mocks/mockEntries.ts +++ b/apps/find-orphans/test/mocks/mockEntries.ts @@ -4,13 +4,16 @@ export const makeMockEntry = ( id: string, contentTypeId: string, fields: EntryProps['fields'] = {}, - updatedAt = '2026-06-01T00:00:00Z' + updatedAt = '2026-06-01T00:00:00Z', + // Version 1 = never saved after creation; pass a higher version to model + // an entry that has been edited. + version = 1 ): EntryProps => ({ sys: { id, type: 'Entry', - version: 1, + version, contentType: { sys: { type: 'Link', linkType: 'ContentType', id: contentTypeId } }, space: { sys: { type: 'Link', linkType: 'Space', id: 'space-id' } }, environment: { sys: { type: 'Link', linkType: 'Environment', id: 'master' } }, diff --git a/apps/find-orphans/test/orphanFinder.test.ts b/apps/find-orphans/test/orphanFinder.test.ts index bd6e9daf39..e0d9ebf1d6 100644 --- a/apps/find-orphans/test/orphanFinder.test.ts +++ b/apps/find-orphans/test/orphanFinder.test.ts @@ -14,7 +14,7 @@ import { } from './mocks'; const noProgress = vi.fn(); -const limits = { maxCandidates: 500, batchSize: 5 }; +const options = { maxCandidates: 500, batchSize: 5, untouchedOnly: true }; describe('getTextDisplayFieldId', () => { it('returns the display field id when it is a text field', () => { @@ -92,7 +92,7 @@ describe('findOrphanedEntries', () => { [mockArticleContentType], 'en-US', noProgress, - limits + options ); expect(outcome.truncated).toBe(false); @@ -109,12 +109,42 @@ describe('findOrphanedEntries', () => { [mockArticleContentType], 'en-US', noProgress, - limits + options ); expect(outcome.results.map((r) => r.entry.sys.id)).toEqual(['blank']); }); + it('excludes untitled drafts that were edited after creation when untouchedOnly is set', async () => { + // sys.version increments on every save, so version > 1 means someone has + // worked on the entry — likely a work-in-progress, not an orphan. + const untouched = makeMockEntry('untouched', 'article'); + const edited = makeMockEntry('edited', 'article', {}, '2026-06-01T00:00:00Z', 4); + const { cma } = createMockCma({ entriesByContentType: { article: [untouched, edited] } }); + + const outcome = await findOrphanedEntries( + cma, + [mockArticleContentType], + 'en-US', + noProgress, + options + ); + + expect(outcome.results.map((r) => r.entry.sys.id)).toEqual(['untouched']); + }); + + it('includes edited untitled drafts when untouchedOnly is off', async () => { + const edited = makeMockEntry('edited', 'article', {}, '2026-06-01T00:00:00Z', 4); + const { cma } = createMockCma({ entriesByContentType: { article: [edited] } }); + + const outcome = await findOrphanedEntries(cma, [mockArticleContentType], 'en-US', noProgress, { + ...options, + untouchedOnly: false, + }); + + expect(outcome.results.map((r) => r.entry.sys.id)).toEqual(['edited']); + }); + it('skips content types without a text display field', async () => { // Entries of such types cannot be missing a title, so querying them // would only waste API calls. @@ -127,7 +157,7 @@ describe('findOrphanedEntries', () => { [mockNumericDisplayContentType], 'en-US', noProgress, - limits + options ); expect(outcome.results).toHaveLength(0); @@ -139,8 +169,8 @@ describe('findOrphanedEntries', () => { const { cma } = createMockCma({ entriesByContentType: { article: entries } }); const outcome = await findOrphanedEntries(cma, [mockArticleContentType], 'en-US', noProgress, { + ...options, maxCandidates: 1, - batchSize: 5, }); expect(outcome.truncated).toBe(true); @@ -168,7 +198,7 @@ describe('findOrphanedEntries', () => { [articleType, noteType], 'en-US', noProgress, - limits + options ); expect(outcome.results.map((r) => r.entry.sys.id)).toEqual( @@ -183,7 +213,7 @@ describe('findOrphanedEntries', () => { const orphan = makeMockEntry('orphan', 'article'); const { cma } = createMockCma({ entriesByContentType: { article: [orphan] } }); - await findOrphanedEntries(cma, [mockArticleContentType], 'en-US', onProgress, limits); + await findOrphanedEntries(cma, [mockArticleContentType], 'en-US', onProgress, options); expect(onProgress).toHaveBeenCalledWith({ current: 1, diff --git a/apps/find-orphans/test/parameters.test.ts b/apps/find-orphans/test/parameters.test.ts index d3e5ce3bd2..ed15626293 100644 --- a/apps/find-orphans/test/parameters.test.ts +++ b/apps/find-orphans/test/parameters.test.ts @@ -10,15 +10,22 @@ describe('resolveParameters', () => { it('keeps valid values and coerces numeric strings', () => { const resolved = resolveParameters({ maxCandidates: 1000, batchSize: '3' }); - expect(resolved).toEqual({ maxCandidates: 1000, batchSize: 3 }); + expect(resolved).toEqual({ maxCandidates: 1000, batchSize: 3, untouchedOnly: true }); }); it('falls back to defaults for invalid values', () => { - const resolved = resolveParameters({ maxCandidates: -5, batchSize: 'many' }); + const resolved = resolveParameters({ maxCandidates: -5, batchSize: 'many', untouchedOnly: 1 }); expect(resolved).toEqual(DEFAULT_PARAMETERS); }); it('caps batchSize at the CMA rate limit of 7', () => { expect(resolveParameters({ batchSize: 50 }).batchSize).toBe(7); }); + + it('accepts booleans and boolean strings for untouchedOnly', () => { + expect(resolveParameters({ untouchedOnly: false }).untouchedOnly).toBe(false); + // Hand-edited parameters may store booleans as strings. + expect(resolveParameters({ untouchedOnly: 'false' }).untouchedOnly).toBe(false); + expect(resolveParameters({ untouchedOnly: 'true' }).untouchedOnly).toBe(true); + }); }); From 56d46e38798157c1c1813544b4a49a1f4f35b6f4 Mon Sep 17 00:00:00 2001 From: Shanon Place Date: Mon, 13 Jul 2026 17:30:05 -0700 Subject: [PATCH 3/8] Updated UI and added assets and a link to see them archived --- apps/find-orphans/AGENTS.md | 53 ++- apps/find-orphans/README.md | 90 +++-- .../src/locations/ConfigScreen.tsx | 12 +- .../locations/Page/components/OrphanTable.tsx | 47 +-- .../find-orphans/src/locations/Page/index.tsx | 350 ++++++++++++++---- apps/find-orphans/src/locations/Page/types.ts | 38 +- .../src/locations/Page/utils/constants.ts | 6 + .../src/locations/Page/utils/entryActions.ts | 41 +- .../src/locations/Page/utils/orphanFinder.ts | 213 +++++++++-- apps/find-orphans/test/entryActions.test.ts | 30 +- .../find-orphans/test/locations/Page.test.tsx | 124 ++++++- apps/find-orphans/test/mocks/mockCma.ts | 56 ++- apps/find-orphans/test/mocks/mockEntries.ts | 52 ++- apps/find-orphans/test/mocks/mockSdk.ts | 1 + apps/find-orphans/test/orphanFinder.test.ts | 227 +++++++++--- 15 files changed, 1070 insertions(+), 270 deletions(-) diff --git a/apps/find-orphans/AGENTS.md b/apps/find-orphans/AGENTS.md index 625df5e3af..49e9cf8684 100644 --- a/apps/find-orphans/AGENTS.md +++ b/apps/find-orphans/AGENTS.md @@ -1,12 +1,13 @@ # Agent Guide — find-orphans ## What This App Does -Full-page app that scans an environment for "orphaned" draft entries — typically empty entries -accidentally created from a reference field when the user meant to link an existing entry. An -orphan is a draft (never published, not archived) with no value in its display (title) field in -the default locale; with the `untouchedOnly` parameter (default on) it must also never have been -saved after creation (`sys.version === 1`). Results deep-link into the entry editor and can be -archived in bulk. +Full-page app that scans an environment for "orphaned" draft entries and media assets — +typically empty items accidentally created from a reference field when the user meant to link +an existing one. An orphan is a draft (never published, not archived) with no title value in +the default locale; with the `untouchedOnly` parameter (default on) it must also never have +been saved after creation (`sys.version === 1`). The scan scope (entries and/or assets) is a +per-run checkbox choice next to the scan button, both on by default. Results deep-link into the +matching editor and can be archived in bulk. ## Archetype Standard Vite app. Page location plus an app-config screen for installation parameters. @@ -54,27 +55,28 @@ src/ └── locations/ ├── ConfigScreen.tsx # Installation parameter form └── Page/ - ├── index.tsx # Page UI, scan orchestration - ├── types.ts # Result/progress types + ├── index.tsx # Page UI, scan orchestration, scope checkboxes + ├── types.ts # OrphanResult (entry|asset union) / progress types ├── components/ # OrphanTable └── utils/ ├── constants.ts # Fallback defaults for parameters - ├── entryActions.ts # Batched bulk archive (pure, unit-tested) - └── orphanFinder.ts # All CMA query logic (pure, unit-tested) + ├── entryActions.ts # Batched bulk archive, kind-routed (pure, unit-tested) + └── orphanFinder.ts # All CMA query logic, entries + assets (pure, unit-tested) ``` ## Sharp Edges & Invariants -- **Draft scope is fixed and filtered server-side**: every entry query includes +- **Draft scope is fixed and filtered server-side**: every entry and asset query includes `sys.publishedAt[exists]=false` and `sys.archivedAt[exists]=false`. The remaining criteria are evaluated client-side. - **The empty-title check must stay client-side**: the API's `[exists]` operator misses whitespace-only titles, and with a `select` query the CMA omits the `fields` object entirely - from entries that have no value in any selected field — both shapes count as orphans. + from items that have no value in any selected field — both shapes count as orphans. - **The version check must stay client-side too**: `sys.version` is not a queryable attribute in - CMA entry searches, but it rides along in every returned `sys` object, so it costs nothing. - Version 1 means "never saved after creation" only because published and archived entries are - already excluded — publish/unpublish/archive/unarchive also bump `sys.version`. + CMA searches, but it rides along in every returned `sys` object, so it costs nothing. + Version 1 means "never saved after creation" only because published and archived items are + already excluded — publish/unpublish/archive/unarchive (and asset file processing) also bump + `sys.version`. - **Default-locale only**: the missing-title check and title rendering use `sdk.locales.default`. Localized titles in other locales are not considered. - **Content types without a text display field are skipped entirely** — their entries cannot be @@ -83,8 +85,25 @@ src/ `content_type`), so the scan runs those queries `batchSize` at a time in parallel chunks. A chunk shares one remaining-budget snapshot and can overshoot `maxCandidates`; the result is trimmed and flagged truncated afterwards. -- **`maxCandidates` caps each scan** — the UI shows a truncation warning. Keep the cap or replace - it with real pagination, but never scan unbounded. +- **Assets are one paged query** — they are homogeneous (no content type), so the whole media + library is a single scan step that runs after the entry phase and spends whatever remains of + the `maxCandidates` budget. This is why one scan button with scope checkboxes beats separate + entry/asset CTAs: the asset step is marginal next to the entry fan-out. +- **`maxCandidates` caps each scan across both scopes** — the UI shows a truncation warning. + Keep the cap or replace it with real pagination, but never scan unbounded. +- **Creator names are best-effort**: `resolveCreatorNames` batches `user.getManyForSpace` + lookups (`sys.id[in]`, 100 ids per request) and swallows failures — unresolved creators render + as "Unknown user", non-User `sys.createdBy` links (apps/automations) as "App". Never let a + users-endpoint failure fail the scan. The results table shows **Created** (`sys.createdAt`), + not last-updated: orphans are by default never edited after creation. +- **Archiving routes by kind**: `archiveOrphans` calls `cma.entry.archive` or + `cma.asset.archive` per target — `OrphanResult.kind` must survive any refactor of the result + shape, and preview likewise routes to `openEntry`/`openAsset`. +- **Mixed-kind selection is guarded twice, keep both**: kind-scoped ToggleButtons ("Entries + (N)" / "Assets (N)", shown only when results mix kinds, pressed state derived from the + selection, toggle off to deselect that kind) and an archive confirmation that itemizes the + selection by kind ("Archive 19 entries and 4 assets"). This is the agreed alternative to + splitting the results into per-kind tables — select-all deliberately spans both kinds. - **Pagination order needs the `sys.id` tiebreaker**: entries sharing an `updatedAt` (bulk imports) sort non-deterministically otherwise, and skip-based paging can drop or duplicate them. diff --git a/apps/find-orphans/README.md b/apps/find-orphans/README.md index 62343cb3aa..0f08572650 100644 --- a/apps/find-orphans/README.md +++ b/apps/find-orphans/README.md @@ -1,22 +1,29 @@ # Find Orphans -A Contentful App Framework **page app** that finds draft entries that were likely created by -mistake — the classic case being clicking "Create new entry" on a reference field when you meant -to link an existing entry, leaving behind an empty, untitled draft. +A Contentful App Framework **page app** that finds draft entries and media assets that were +likely created by mistake — the classic case being clicking "Create new entry" (or "Add new +media") on a reference field when you meant to link an existing item, leaving behind an empty, +untitled draft. ## How it works -The app scans the current environment for **draft entries** (never published, not archived) -whose **display (title) field has no value** in the default locale — including whitespace-only -values, which the entry editor also renders as "Untitled". +The app scans the current environment for **draft entries and assets** (never published, not +archived) whose **title has no value** in the default locale — including whitespace-only values, +which the editors also render as "Untitled". For entries the title is the content type's display +field; assets have a fixed shape, so their localized `title` field is checked directly and the +whole media library is a single paged query. Two checkboxes next to the scan button control the +scope per run (entries and/or assets, both on by default) — the per-content-type entry queries +are the slow part of a scan, so an assets-only pass is quick. -By default the scan additionally requires that the entry was **never edited after creation** +By default the scan additionally requires that the item was **never edited after creation** (`sys.version === 1`). The CMA bumps `sys.version` on every save — and on publish, unpublish, -archive and unarchive, but those states are already excluded by the draft scope — so on a -candidate, version 1 can only mean the entry was created and then abandoned untouched. This -keeps untitled work-in-progress drafts (body started, title not yet filled in) out of the -results. The filter can be turned off via the `untouchedOnly` installation parameter, which is -useful when apps or scripts write to entries on creation and bump every entry past version 1. +archive, unarchive and (for assets) file processing, but those states are already excluded by +the draft scope — so on a candidate, version 1 can only mean the item was created and then +abandoned untouched. This keeps untitled work-in-progress drafts (body started, title not yet +filled in) out of the results; uploaded assets get their title auto-filled from the filename, so +untitled assets are almost always accidental creations. The filter can be turned off via the +`untouchedOnly` installation parameter, which is useful when apps or scripts write to items on +creation and bump every one past version 1. This is something the regular Contentful search cannot do in one query: field filters only work for one content type at a time, and every content type defines its own display field. The app @@ -24,44 +31,63 @@ automates that per-content-type check across the whole content model. Content ty display field is not a `Symbol`/`Text` field are skipped, since their entries cannot be missing a title. -Results are listed with their content type and last-updated date. Each row has an explicit -"Preview" action that opens the entry editor in a slide-in for review. Rows can be selected -individually or all at once, and the selected entries archived in bulk after a confirmation -dialog; archiving runs in rate-limit-friendly batches, and entries that fail to archive stay -listed and selected for retry. Archiving is reversible from the entry editor. - -Scans are capped at a configurable number of draft entries per run (500 by default, see -Configuration below) to stay friendly to CMA rate limits; a warning is shown when the cap is -hit. +Results are listed in one table with their type (the content type name, or "Asset"), creation +date, and creator, and the result count spells out the split (e.g. "19 entries and 4 assets +found"). The date shown is **created**, not last-updated — orphans were (by default) never +edited after creation, so the creation moment is what identifies the mistake. The creator comes +from `sys.createdBy`, resolved to names via one batched space-users lookup per scan; items +created by apps or automations show "App", and creators who cannot be resolved (left the space, +or the caller may not list users) show "Unknown user". Each row has an explicit "Preview" +action that opens the matching editor in a slide-in for review. + +Rows can be selected individually, all at once, or per kind — when the results mix entries and +assets, "Entries (N)" / "Assets (N)" toggle buttons select or deselect everything of one kind +(the pressed state mirrors the selection), so archiving all entries never sweeps assets along. +The selected items are archived in bulk after a confirmation dialog that itemizes the selection +by kind ("Archive 19 entries and 4 assets"); archiving runs in rate-limit-friendly batches +through the endpoint matching each item's kind, and items that fail to archive stay listed and +selected for retry. Archiving is reversible from the editor; an info popover next to the +archive button spells this out and deep-links (in a new tab) to the web app's archived-entries +and archived-assets views — `…/views/entries?filters.0.key=__status&filters.0.op=&filters.0.val=archived` +— where permanent deletion lives. + +Scans are capped at a configurable number of draft items per run (500 by default, entries and +assets sharing the one budget, see Configuration below) to stay friendly to CMA rate limits; a +warning is shown when the cap is hit. ## Logic flow ```mermaid flowchart TD Load[App loads] --> FetchCT[Fetch all content types
paged, 1000 per request] - FetchCT --> Scan([User clicks Scan]) - Scan --> Filter[Keep content types whose display field
is a Symbol or Text field] + FetchCT --> Scan([User clicks Scan with a scope:
entries and/or media assets]) + Scan --> Filter[Entries scope: keep content types whose
display field is a Symbol or Text field] Filter --> Chunk[Take next batchSize content types] Chunk --> Query[Query draft entries per content type in parallel:
never published, not archived,
select sys + display field only] Query --> Pages[Page through results until exhausted
or the maxCandidates budget is spent] Pages --> More{More content types
and budget left?} More -- yes --> Chunk - More -- no --> Check[Client-side check: display field empty,
whitespace-only, or fields object absent
in the default locale] + More -- no --> AssetStep[Assets scope: one paged draft-asset query
spending the remaining budget,
select sys + title only] + AssetStep --> Check[Client-side check: title empty,
whitespace-only, or fields object absent
in the default locale] Check --> Untouched{untouchedOnly
enabled?} - Untouched -- yes --> Version[Keep only entries never edited
after creation: sys.version == 1] - Untouched -- no --> Results[Results table
+ truncation warning if capped] - Version --> Results + Untouched -- yes --> Version[Keep only items never edited
after creation: sys.version == 1] + Untouched -- no --> Resolve + Version --> Resolve[Resolve creator names from sys.createdBy:
batched space-users lookup, tolerant of failure] + Resolve --> Results[Results table
+ truncation warning if capped] Results --> Archive([User selects rows and confirms Archive]) - Archive --> Batches[Archive entries in batches of batchSize] + Archive --> Batches[Archive in batches of batchSize,
entries and assets each via their endpoint] Batches --> Done[Archived rows leave the list;
failed ones stay listed and selected for retry] ``` +A scope that is unchecked for the run skips its block entirely: entries-only scans never query +the media library, and assets-only scans jump straight to the asset query. + The empty-title check runs client-side rather than via the API's `[exists]` operator for two -reasons: `[exists]` misses whitespace-only titles, and entries with no value in any selected +reasons: `[exists]` misses whitespace-only titles, and items with no value in any selected field come back from the CMA with no `fields` object at all — both shapes must count as orphans. The version check is also client-side by necessity: `sys.version` is not a queryable -attribute in CMA entry searches, but it is part of every returned `sys` object, so the check -costs no extra API traffic. +attribute in CMA searches, but it is part of every returned `sys` object, so the check costs no +extra API traffic. ## Configuration @@ -71,7 +97,7 @@ registering the app definition, create the parameter definitions with exactly th | Display name | ID | Type | Required | Default value | Description | |--------------|----|------|----------|---------------|-------------| -| Maximum entries per scan | `maxCandidates` | Number | Yes | `500` | The scan stops after this many draft entries, to stay friendly to API rate limits. | +| Maximum entries per scan | `maxCandidates` | Number | Yes | `500` | The scan stops after this many draft entries and assets, to stay friendly to API rate limits. | | Concurrent API requests | `batchSize` | Number | Yes | `5` | How many CMA requests run at once while scanning and archiving. Must be between 1 and 7, the CMA rate limit per second. | | Only include entries that were never edited | `untouchedOnly` | Boolean | Yes | `true` | Only flag drafts still at version 1, i.e. never saved after creation. Turn off to also catch untitled drafts that were edited and then abandoned. | diff --git a/apps/find-orphans/src/locations/ConfigScreen.tsx b/apps/find-orphans/src/locations/ConfigScreen.tsx index 2039955f7f..5d9d8f1ab6 100644 --- a/apps/find-orphans/src/locations/ConfigScreen.tsx +++ b/apps/find-orphans/src/locations/ConfigScreen.tsx @@ -136,10 +136,10 @@ const ConfigScreen = () => { style={{ maxWidth: '768px', width: '100%' }}> Set up Find Orphans - Find Orphans scans this space for draft entries with no value in their display (title) - field — the signature of an entry created by mistake, for example by adding a new entry on - a reference field instead of linking an existing one — and lets you review or archive them - from one page. + Find Orphans scans this space for draft entries and media assets with no title — the + signature of an item created by mistake, for example by adding a new entry on a reference + field instead of linking an existing one — and lets you review or archive them from one + page. @@ -154,7 +154,7 @@ const ConfigScreen = () => { { Only include entries that were never edited - Contentful bumps an entry's version on every save, so an untitled draft still at + Contentful bumps an item's version on every save, so an untitled draft still at version 1 was abandoned right after creation. Turn this off to also flag untitled drafts that were edited at least once — for example when other apps or scripts write to entries on creation. diff --git a/apps/find-orphans/src/locations/Page/components/OrphanTable.tsx b/apps/find-orphans/src/locations/Page/components/OrphanTable.tsx index 1325a6e614..0c59ed77ee 100644 --- a/apps/find-orphans/src/locations/Page/components/OrphanTable.tsx +++ b/apps/find-orphans/src/locations/Page/components/OrphanTable.tsx @@ -4,9 +4,9 @@ import { OrphanResult } from '../types'; interface OrphanTableProps { results: OrphanResult[]; selectedIds: string[]; - onToggleEntry: (entryId: string) => void; + onToggleResult: (resultId: string) => void; onToggleAll: () => void; - onOpenEntry: (entryId: string) => void; + onOpenResult: (result: OrphanResult) => void; /** Disables selection while an archive operation is running. */ isDisabled: boolean; } @@ -21,9 +21,9 @@ const formatDate = (isoDate: string): string => export const OrphanTable = ({ results, selectedIds, - onToggleEntry, + onToggleResult, onToggleAll, - onOpenEntry, + onOpenResult, isDisabled, }: OrphanTableProps) => { const allSelected = results.length > 0 && selectedIds.length === results.length; @@ -36,7 +36,7 @@ export const OrphanTable = ({ Title - Content type + Type Status - Last updated + {/* Creation info, not last-updated: orphans were (by default) + never edited after creation, so created when/by whom is what + identifies the mistake and its source. */} + Created + Created by - {results.map(({ entry, contentType }) => ( - + {results.map((result) => ( + onToggleEntry(entry.sys.id)} + onChange={() => onToggleResult(result.id)} /> - {/* The scan only lists entries whose display field is empty, - so the title is always the editor's "Untitled" placeholder, - mirroring what the content list shows for these entries. */} + {/* The scan only lists items with an empty title, so this is + always the editor's "Untitled" placeholder, mirroring what + the content list and media library show for them. */} Untitled - {contentType.name} + {result.typeName} Draft - {formatDate(entry.sys.updatedAt)} + {formatDate(result.createdAt)} + {result.createdBy} {/* Previewing is an explicit action instead of a click on the title, so selecting rows never accidentally triggers the slide-in. "Preview" signals it is a slide-in, not a page change. */} - onOpenEntry(entry.sys.id)}> + onOpenResult(result)}> Preview diff --git a/apps/find-orphans/src/locations/Page/index.tsx b/apps/find-orphans/src/locations/Page/index.tsx index 24e76cb85b..aa46c3fcb4 100644 --- a/apps/find-orphans/src/locations/Page/index.tsx +++ b/apps/find-orphans/src/locations/Page/index.tsx @@ -3,32 +3,54 @@ import { PageAppSDK } from '@contentful/app-sdk'; import { Box, Button, + Checkbox, Flex, Heading, + IconButton, ModalConfirm, Note, Notification, - Paragraph, + Popover, + SkeletonRow, Spinner, + Table, Text, + TextLink, + ToggleButton, + Tooltip, } from '@contentful/f36-components'; -import { TrayArrowDownIcon, MagnifyingGlassIcon } from '@contentful/f36-icons'; +import tokens from '@contentful/f36-tokens'; +import { InfoIcon, TrayArrowDownIcon, MagnifyingGlassIcon } from '@contentful/f36-icons'; import { useSDK } from '@contentful/react-apps-toolkit'; import { ContentTypeProps } from 'contentful-management'; import { resolveParameters } from '../../parameters'; import { OrphanTable } from './components/OrphanTable'; -import { OrphanResult, ScanProgress } from './types'; -import { archiveEntries, ArchiveProgress } from './utils/entryActions'; -import { fetchAllContentTypes, findOrphanedEntries } from './utils/orphanFinder'; +import { OrphanKind, OrphanResult, ScanProgress } from './types'; +import { archiveOrphans, ArchiveProgress } from './utils/entryActions'; +import { fetchAllContentTypes, findOrphans } from './utils/orphanFinder'; // Name what is being checked right now, e.g. -// "Checking Article, Author… (5/50 content types)". +// "Checking Article, Author… (5/51)". Steps are content types plus the +// media-library pass, so the label does not say "content types". const progressLabel = (progress: ScanProgress): string => - `Checking ${progress.contentTypeNames.join(', ')}… (${progress.current}/${ - progress.total - } content types)`; + `Checking ${progress.stepNames.join(', ')}… (${progress.current}/${progress.total})`; -const pluralize = (count: number) => (count === 1 ? 'entry' : 'entries'); +const pluralize = (count: number) => (count === 1 ? 'item' : 'items'); + +const countByKind = (items: OrphanResult[]) => ({ + entries: items.filter((result) => result.kind === 'entry').length, + assets: items.filter((result) => result.kind === 'asset').length, +}); + +// "19 entries and 4 assets", "1 entry", … — used for the result count and +// the archive confirmation, so a mixed selection is always spelled out and +// select-all can never archive assets the user did not know were included. +const describeCounts = ({ entries, assets }: { entries: number; assets: number }): string => { + const parts: string[] = []; + if (entries > 0) parts.push(`${entries} ${entries === 1 ? 'entry' : 'entries'}`); + if (assets > 0) parts.push(`${assets} ${assets === 1 ? 'asset' : 'assets'}`); + return parts.length > 0 ? parts.join(' and ') : '0 items'; +}; const Page = () => { const sdk = useSDK(); @@ -46,12 +68,38 @@ const Page = () => { // entirely; an empty array renders the positive empty state instead. const [results, setResults] = useState(null); const [truncated, setTruncated] = useState(false); + // Per-run scan scope. Both on by default; the entry fan-out is the slow + // part, so unchecking "Entries" gives a fast assets-only pass. + const [scanEntries, setScanEntries] = useState(true); + const [scanAssets, setScanAssets] = useState(true); const [selectedIds, setSelectedIds] = useState([]); const [archiving, setArchiving] = useState(false); const [archiveProgress, setArchiveProgress] = useState(null); const [confirmArchiveOpen, setConfirmArchiveOpen] = useState(false); + const [archiveInfoOpen, setArchiveInfoOpen] = useState(false); + + // Deep links into the web app's archived views — permanent deletion only + // exists there, so the archive-info popover hands users the destination. + // environmentAlias keeps the link on the alias the user is browsing under. + // The filter uses the web app's shareable-view format: indexed key/op/val + // triplets against the "__status" pseudo-field (empty op = equals). + const archivedListUrls = useMemo(() => { + const base = `https://app.contentful.com/spaces/${sdk.ids.space}/environments/${ + sdk.ids.environmentAlias ?? sdk.ids.environment + }`; + const archivedFilter = 'filters.0.key=__status&filters.0.op=&filters.0.val=archived'; + return { + entries: `${base}/views/entries?${archivedFilter}`, + assets: `${base}/views/assets?${archivedFilter}`, + }; + }, [sdk]); const busy = scanning || archiving; + const resultCounts = useMemo(() => countByKind(results ?? []), [results]); + const selectedCounts = useMemo( + () => countByKind((results ?? []).filter((result) => selectedIds.includes(result.id))), + [results, selectedIds] + ); // Content types are loaded once up front: the scan needs their display // field definitions, and the list rarely changes within a session. @@ -76,17 +124,13 @@ const Page = () => { // A new scan invalidates any previous selection. setSelectedIds([]); try { - const outcome = await findOrphanedEntries( - sdk.cma, - contentTypes, - sdk.locales.default, - setProgress, - { - maxCandidates: parameters.maxCandidates, - batchSize: parameters.batchSize, - untouchedOnly: parameters.untouchedOnly, - } - ); + const outcome = await findOrphans(sdk.cma, contentTypes, sdk.locales.default, setProgress, { + maxCandidates: parameters.maxCandidates, + batchSize: parameters.batchSize, + untouchedOnly: parameters.untouchedOnly, + includeEntries: scanEntries, + includeAssets: scanAssets, + }); setResults(outcome.results); setTruncated(outcome.truncated); } catch (error) { @@ -99,30 +143,53 @@ const Page = () => { setScanning(false); setProgress(null); } - }, [sdk, contentTypes, parameters]); + }, [sdk, contentTypes, parameters, scanEntries, scanAssets]); - const openEntry = useCallback( - (entryId: string) => { + const openResult = useCallback( + (result: OrphanResult) => { // slideIn keeps the user on the results page while they inspect - // (and possibly delete) the entry in the standard editor. - sdk.navigator.openEntry(entryId, { slideIn: true }); + // (and possibly delete) the item in the standard editor. + if (result.kind === 'entry') { + sdk.navigator.openEntry(result.id, { slideIn: true }); + } else { + sdk.navigator.openAsset(result.id, { slideIn: true }); + } }, [sdk] ); - const toggleEntry = useCallback((entryId: string) => { + const toggleResult = useCallback((resultId: string) => { setSelectedIds((previous) => - previous.includes(entryId) ? previous.filter((id) => id !== entryId) : [...previous, entryId] + previous.includes(resultId) + ? previous.filter((id) => id !== resultId) + : [...previous, resultId] ); }, []); + const toggleKind = useCallback( + // Kind-scoped select-all with checkbox semantics: toggling on selects + // every result of the kind (keeping any other-kind selection), toggling + // off deselects exactly those again. "Archive all entries" is one click + // and never sweeps assets along. + (kind: OrphanKind) => { + const kindIds = (results ?? []) + .filter((result) => result.kind === kind) + .map((result) => result.id); + setSelectedIds((previous) => { + const allOfKindSelected = + kindIds.length > 0 && kindIds.every((id) => previous.includes(id)); + const withoutKind = previous.filter((id) => !kindIds.includes(id)); + return allOfKindSelected ? withoutKind : [...withoutKind, ...kindIds]; + }); + }, + [results] + ); + const toggleAll = useCallback(() => { // Header checkbox: everything selected clears the selection, anything // less (none or partial) selects all visible results. setSelectedIds((previous) => - results !== null && previous.length < results.length - ? results.map((result) => result.entry.sys.id) - : [] + results !== null && previous.length < results.length ? results.map((result) => result.id) : [] ); }, [results]); @@ -130,17 +197,21 @@ const Page = () => { setConfirmArchiveOpen(false); setArchiving(true); try { - const outcome = await archiveEntries( + // The archive endpoint differs per kind, so selection ids are resolved + // back to results to recover whether each is an entry or an asset. + const targets = (results ?? []) + .filter((result) => selectedIds.includes(result.id)) + .map((result) => ({ id: result.id, kind: result.kind })); + const outcome = await archiveOrphans( sdk.cma, - selectedIds, + targets, parameters.batchSize, setArchiveProgress ); - // Archived entries leave the result list; failed ones stay visible and + // Archived items leave the result list; failed ones stay visible and // selected so the user can retry them. setResults( - (previous) => - previous?.filter((result) => !outcome.archivedIds.includes(result.entry.sys.id)) ?? null + (previous) => previous?.filter((result) => !outcome.archivedIds.includes(result.id)) ?? null ); setSelectedIds(outcome.failedIds); if (outcome.archivedIds.length > 0) { @@ -161,40 +232,70 @@ const Page = () => { setArchiving(false); setArchiveProgress(null); } - }, [sdk, selectedIds, parameters]); + }, [sdk, selectedIds, parameters, results]); return ( // Full width: page apps get the whole main column, and the results table // benefits from every pixel of it. - Find orphaned entries - - Finds draft entries with no value in their display (title) field — the signature of an entry - created by mistake, for example by adding a new entry on a reference field instead of - linking an existing one. - {/* Whether the never-edited filter applies is decided in the app - configuration, so the description must reflect the actual scan. */} - {parameters.untouchedOnly && - ' Only entries that were never edited after creation are included.'} - - - - The regular Contentful search can only filter on the title field for one content type at a - time, because each content type defines its own display field. This scan runs that check - across every content type at once. - + + + + Find orphaned entries + + {/* The why-does-this-app-exist rationale lives in a tooltip so + the header stays scannable; an IconButton (not a bare icon) + keeps it reachable by keyboard and screen readers. */} + + } + aria-label="Why this scan exists" + testId="scan-info" + /> + + + + Finds untitled draft entries and media assets — usually created by accident from a + reference field. + {/* Whether the never-edited filter applies is decided in the app + configuration, so the description must reflect the actual scan. */} + {parameters.untouchedOnly && ' Only items never edited after creation are included.'} + + + {/* Per-run scope: the entry fan-out (one query per content type) + is the slow part of a scan, so these let a user run a quick + assets-only pass — or skip assets they do not care about. */} + setScanEntries(event.target.checked)} + isDisabled={busy} + testId="scope-entries"> + Entries + + setScanAssets(event.target.checked)} + isDisabled={busy} + testId="scope-assets"> + Media assets + {scanning && progress && ( @@ -203,25 +304,90 @@ const Page = () => { )} + {/* Separator between the scan controls and the results area, which + below this line always renders something: placeholder, skeleton, + empty note, or the results table. */} + + + {results === null && !scanning && ( + // Fresh-open state: without this the area below the separator would + // be blank until the first scan, which reads as something missing. + + + No scan has run yet + + Click “Scan for orphans” to check every content type and the media library for + untitled drafts. + + + )} + + {scanning && ( + // Placeholder rows keep the results area occupied while the scan + // runs; the row count is arbitrary, it only suggests a table. + + + + +
+ )} + {truncated && ( - The scan stopped after {parameters.maxCandidates} draft entries. Archive or clean up - some of the results and scan again, or raise the limit in the app configuration. + The scan stopped after {parameters.maxCandidates} draft items. Archive or clean up some + of the results and scan again, or raise the limit in the app configuration. )} {results !== null && (results.length === 0 ? ( - No orphaned entries found — every draft has a title. + No orphans found — every scanned draft has a title. ) : ( <> - - {results.length} {pluralize(results.length)} found - {selectedIds.length > 0 ? ` — ${selectedIds.length} selected` : ''} - + + + {describeCounts(resultCounts)} found + {selectedIds.length > 0 ? ` — ${selectedIds.length} selected` : ''} + + {/* Kind-scoped select-all, only useful (and only shown) + when the results actually mix entries and assets. The + pressed state mirrors the real selection, so the + buttons also read as "everything of this kind is + selected" and toggle off to undo exactly that. */} + {resultCounts.entries > 0 && resultCounts.assets > 0 && ( + + Select all + toggleKind('entry')} + isDisabled={busy} + testId="select-entries"> + Entries ({resultCounts.entries}) + + toggleKind('asset')} + isDisabled={busy} + testId="select-assets"> + Assets ({resultCounts.assets}) + + + )} + {archiving && archiveProgress && ( @@ -240,14 +406,61 @@ const Page = () => { testId="archive-button"> Archive selected + {/* Archiving reads as scary-destructive next to a negative + button; this spells out that it is reversible and links + straight to where actual deletion lives. A click + Popover, not a Tooltip: a hover tooltip closes before + its links can be clicked. */} + setArchiveInfoOpen(false)} + placement="top-end"> + + } + aria-label="What archiving does" + onClick={() => setArchiveInfoOpen((previous) => !previous)} + testId="archive-info" + /> + + + + + Archiving is not deleting: archived items just leave the content list and + media library, and can be unarchived at any time. + + + To delete them permanently, open the{' '} + + archived entries + {' '} + or{' '} + + archived assets + {' '} + list (opens in a new tab), select the items, and delete them there. + + + + @@ -259,12 +472,15 @@ const Page = () => { isShown={confirmArchiveOpen} onCancel={() => setConfirmArchiveOpen(false)} onConfirm={runArchive} - confirmLabel={`Archive ${selectedIds.length} ${pluralize(selectedIds.length)}`} + // The confirmation always itemizes the selection by kind, so a + // select-all that swept in assets (or entries) is visible right on + // the button before anything is archived. + confirmLabel={`Archive ${describeCounts(selectedCounts)}`} cancelLabel="Cancel"> - The selected {pluralize(selectedIds.length)} will be archived and disappear from the - content list. Archiving is reversible: entries can be unarchived from the entry editor at - any time. + The selected {describeCounts(selectedCounts)} will be archived and disappear from the + content list and media library. Archiving is reversible: anything archived can be + unarchived from its editor at any time.
diff --git a/apps/find-orphans/src/locations/Page/types.ts b/apps/find-orphans/src/locations/Page/types.ts index 5132b73df8..98a31521ce 100644 --- a/apps/find-orphans/src/locations/Page/types.ts +++ b/apps/find-orphans/src/locations/Page/types.ts @@ -1,24 +1,44 @@ import { PageAppSDK } from '@contentful/app-sdk'; -import { ContentTypeProps, EntryProps } from 'contentful-management'; export type CmaClient = PageAppSDK['cma']; +/** Which CMA entity a result is; decides how it is previewed and archived. */ +export type OrphanKind = 'entry' | 'asset'; + /** - * A draft entry flagged by the scan: its display (title) field has no value - * in the default locale, which is the signature of an entry created by - * mistake (e.g. from a reference field) and never filled in. + * A draft entry or media asset flagged by the scan: its title has no value in + * the default locale, which is the signature of an entity created by mistake + * (e.g. from a reference field) and never filled in. The shape is flattened + * to just what the UI and the archive action need, so entries and assets can + * share one result list. */ export interface OrphanResult { - entry: EntryProps; - contentType: ContentTypeProps; + kind: OrphanKind; + id: string; + /** Content type name for entries, "Asset" for media-library assets. */ + typeName: string; + /** + * Creation date, not last-updated: orphans were (by default) never edited + * after creation, so "when was this created" is the honest column — and + * with the untouched filter off, an edit date would understate the age of + * the mistake. + */ + createdAt: string; + /** + * Display name of whoever created the item, resolved from sys.createdBy + * after the scan — "Jane Doe", "App" for app/automation identities, or + * "Unknown user" when the creator cannot be resolved (e.g. left the + * space, or the users lookup is not permitted). + */ + createdBy: string; } export interface ScanProgress { - /** Content types checked so far. */ + /** Scan steps done so far: one per content type, plus one for assets. */ current: number; total: number; - /** Names of the content types being checked right now. */ - contentTypeNames: string[]; + /** What is being checked right now (content type names or "Assets"). */ + stepNames: string[]; } export interface ScanOutcome { diff --git a/apps/find-orphans/src/locations/Page/utils/constants.ts b/apps/find-orphans/src/locations/Page/utils/constants.ts index e9109b8b79..6a01338939 100644 --- a/apps/find-orphans/src/locations/Page/utils/constants.ts +++ b/apps/find-orphans/src/locations/Page/utils/constants.ts @@ -29,5 +29,11 @@ export const BATCH_SIZE = 5; */ export const UNTOUCHED_ONLY = true; +/** + * Chunk size for the creator-name lookup: ids per `sys.id[in]` users query. + * Keeps each request's id list comfortably inside query-string limits. + */ +export const USER_PAGE_LIMIT = 100; + /** Display field types that can hold a title. */ export const TEXT_FIELD_TYPES = ['Symbol', 'Text']; diff --git a/apps/find-orphans/src/locations/Page/utils/entryActions.ts b/apps/find-orphans/src/locations/Page/utils/entryActions.ts index f2ddc1c98a..3d909d8750 100644 --- a/apps/find-orphans/src/locations/Page/utils/entryActions.ts +++ b/apps/find-orphans/src/locations/Page/utils/entryActions.ts @@ -1,4 +1,4 @@ -import { CmaClient } from '../types'; +import { CmaClient, OrphanKind } from '../types'; export interface ArchiveProgress { current: number; @@ -10,34 +10,45 @@ export interface ArchiveOutcome { failedIds: string[]; } +/** What the archive action needs to know about one selected result. */ +export interface ArchiveTarget { + id: string; + /** Decides which CMA endpoint archives it (entries vs assets). */ + kind: OrphanKind; +} + /** - * Archives the given entries in small concurrent batches, mirroring the - * reference-count batching so the CMA rate limit (7 req/s per space) is - * respected. One failed archive never aborts the rest of the batch; failures - * are collected so the caller can report them and keep the entries listed. + * Archives the given entries and assets in small concurrent batches so the + * CMA rate limit (7 req/s per space) is respected. One failed archive never + * aborts the rest of the batch; failures are collected so the caller can + * report them and keep the items listed. */ -export const archiveEntries = async ( +export const archiveOrphans = async ( cma: CmaClient, - entryIds: string[], + targets: ArchiveTarget[], batchSize: number, onProgress: (progress: ArchiveProgress) => void ): Promise => { const archivedIds: string[] = []; const failedIds: string[] = []; - for (let i = 0; i < entryIds.length; i += batchSize) { - const batch = entryIds.slice(i, i + batchSize); - onProgress({ current: Math.min(i + batch.length, entryIds.length), total: entryIds.length }); + for (let i = 0; i < targets.length; i += batchSize) { + const batch = targets.slice(i, i + batchSize); + onProgress({ current: Math.min(i + batch.length, targets.length), total: targets.length }); await Promise.all( - batch.map(async (entryId) => { + batch.map(async ({ id, kind }) => { try { // Drafts are always unpublished, so archiving cannot fail on the - // "published entries cannot be archived" rule; failures here are + // "published items cannot be archived" rule; failures here are // permissions or version conflicts. - await cma.entry.archive({ entryId }); - archivedIds.push(entryId); + if (kind === 'entry') { + await cma.entry.archive({ entryId: id }); + } else { + await cma.asset.archive({ assetId: id }); + } + archivedIds.push(id); } catch { - failedIds.push(entryId); + failedIds.push(id); } }) ); diff --git a/apps/find-orphans/src/locations/Page/utils/orphanFinder.ts b/apps/find-orphans/src/locations/Page/utils/orphanFinder.ts index 7f03963ab4..4187be82f3 100644 --- a/apps/find-orphans/src/locations/Page/utils/orphanFinder.ts +++ b/apps/find-orphans/src/locations/Page/utils/orphanFinder.ts @@ -1,14 +1,27 @@ -import { ContentTypeProps, EntryProps, QueryOptions } from 'contentful-management'; +import { AssetProps, ContentTypeProps, EntryProps, QueryOptions } from 'contentful-management'; import { CmaClient, OrphanResult, ScanOutcome, ScanProgress } from '../types'; -import { CONTENT_TYPE_PAGE_LIMIT, PAGE_LIMIT, TEXT_FIELD_TYPES } from './constants'; +import { + CONTENT_TYPE_PAGE_LIMIT, + PAGE_LIMIT, + TEXT_FIELD_TYPES, + USER_PAGE_LIMIT, +} from './constants'; -/** Scan settings, sourced from installation parameters (see src/parameters.ts). */ +/** + * Scan settings. The limits and the untouched filter come from installation + * parameters (see src/parameters.ts); the two scope flags are a per-run + * choice made on the page. + */ export interface ScanOptions { maxCandidates: number; /** Concurrent CMA entry queries while scanning. */ batchSize: number; /** Only flag drafts never saved after creation (sys.version === 1). */ untouchedOnly: boolean; + /** Scan entries of all content types. */ + includeEntries: boolean; + /** Scan media-library assets. */ + includeAssets: boolean; } export const fetchAllContentTypes = async (cma: CmaClient): Promise => { @@ -82,6 +95,30 @@ export const buildDraftEntryQuery = (contentType: ContentTypeProps): QueryOption }; }; +/** + * Returns the asset's title in the default locale, or undefined when it is + * missing or whitespace-only. Assets have a fixed shape (every asset has a + * localized title field), so unlike entries no display-field lookup is + * needed. The same `select` caveat applies: the CMA omits the fields object + * entirely from assets with no value in any selected field. + */ +export const getAssetTitle = (asset: AssetProps, defaultLocale: string): string | undefined => { + const value = (asset.fields as Partial | undefined)?.title?.[defaultLocale]; + return typeof value === 'string' && value.trim() !== '' ? value : undefined; +}; + +/** + * Builds the CMA asset query. Mirrors `buildDraftEntryQuery`, but assets are + * homogeneous — no content type, so no `content_type` filter is needed for + * `select` and the whole media library is one paged query. + */ +export const buildDraftAssetQuery = (): QueryOptions => ({ + 'sys.publishedAt[exists]': false, + 'sys.archivedAt[exists]': false, + order: '-sys.updatedAt,sys.id', + select: 'sys,fields.title', +}); + /** * Fetches all draft entries of one content type, paging until either the * collection is exhausted or `budget` entries have been collected. @@ -107,6 +144,65 @@ const fetchDraftsOfContentType = async ( return items; }; +/** The sys.createdBy link target: a user, or an app/automation identity. */ +interface CreatorLink { + id: string; + linkType: string; +} + +const toCreatorLink = (sys: EntryProps['sys'] | AssetProps['sys']): CreatorLink | undefined => + sys.createdBy?.sys; + +/** + * Resolves creator user ids to display names via batched space-user lookups + * (`sys.id[in]`, USER_PAGE_LIMIT ids per request). Lookup failures and users + * who are no longer space members simply stay absent from the mapping — + * callers fall back to "Unknown user", and the scan stays useful even when + * the caller may not list space users at all. + */ +export const resolveCreatorNames = async ( + cma: CmaClient, + userIds: string[] +): Promise> => { + const names: Record = {}; + for (let i = 0; i < userIds.length; i += USER_PAGE_LIMIT) { + const chunk = userIds.slice(i, i + USER_PAGE_LIMIT); + try { + const response = await cma.user.getManyForSpace({ + query: { 'sys.id[in]': chunk.join(','), limit: chunk.length }, + }); + for (const user of response.items) { + const fullName = [user.firstName, user.lastName].filter(Boolean).join(' '); + names[user.sys.id] = fullName || user.email; + } + } catch { + // Tolerated — these ids render as "Unknown user". + } + } + return names; +}; + +/** + * Fetches all draft assets, paging until either the media library is + * exhausted or `budget` assets have been collected. + */ +const fetchDraftAssets = async (cma: CmaClient, budget: number): Promise => { + const items: AssetProps[] = []; + let skip = 0; + let total = Infinity; + while (skip < total && items.length < budget) { + const limit = Math.min(PAGE_LIMIT, budget - items.length); + const response = await cma.asset.getMany({ + query: { ...buildDraftAssetQuery(), skip, limit }, + }); + items.push(...response.items); + total = response.total; + if (response.items.length === 0) break; + skip += response.items.length; + } + return items; +}; + /** * Collects draft entries across all scannable content types, stopping once * `maxCandidates` entries have been gathered so a large space cannot trigger @@ -116,12 +212,17 @@ const fetchDraftsOfContentType = async ( * type is unavoidable (entry queries require a single content_type), but the * queries are independent, so running them in parallel divides the wall-clock * time of the scan by the concurrency factor. + * + * `progressTotal` is the number of steps the whole scan reports (content + * types plus the asset step, when enabled), so the progress counter does not + * jump backwards when the asset step follows. */ const fetchDraftCandidates = async ( cma: CmaClient, contentTypes: ContentTypeProps[], maxCandidates: number, concurrency: number, + progressTotal: number, onProgress: (progress: ScanProgress) => void ): Promise<{ candidates: { entry: EntryProps; contentType: ContentTypeProps }[]; @@ -142,8 +243,8 @@ const fetchDraftCandidates = async ( // being checked while they are actually in flight. onProgress({ current: processed, - total: contentTypes.length, - contentTypeNames: chunk.map((contentType) => contentType.name), + total: progressTotal, + stepNames: chunk.map((contentType) => contentType.name), }); const chunkResults = await Promise.all( chunk.map((contentType) => fetchDraftsOfContentType(cma, contentType, budget)) @@ -162,42 +263,110 @@ const fetchDraftCandidates = async ( }; /** - * Scans the environment for orphaned draft entries: never published, not - * archived, and with no value in their display (title) field in the default - * locale. Content types whose display field is not a text field are skipped — - * their entries cannot be missing a title. + * Scans the environment for orphaned drafts: never published, not archived, + * and with no title value in the default locale. For entries the title is + * the content type's display field, and content types whose display field is + * not a text field are skipped — their entries cannot be missing a title. + * Assets always have a title field, so the whole media library is one scan + * step. * * With `untouchedOnly` set, an untitled draft is only flagged when it was * never saved after creation, which filters out work-in-progress drafts that * simply have not been given a title yet. */ -export const findOrphanedEntries = async ( +export const findOrphans = async ( cma: CmaClient, contentTypes: ContentTypeProps[], defaultLocale: string, onProgress: (progress: ScanProgress) => void, options: ScanOptions ): Promise => { - const scannableTypes = contentTypes.filter((ct) => getTextDisplayFieldId(ct) !== undefined); + const scannableTypes = options.includeEntries + ? contentTypes.filter((ct) => getTextDisplayFieldId(ct) !== undefined) + : []; + const progressTotal = scannableTypes.length + (options.includeAssets ? 1 : 0); - const { candidates, truncated } = await fetchDraftCandidates( + const { candidates, truncated: entriesTruncated } = await fetchDraftCandidates( cma, scannableTypes, options.maxCandidates, options.batchSize, + progressTotal, onProgress ); - const results: OrphanResult[] = candidates.filter(({ entry, contentType }) => { - if (getEntryTitle(entry, contentType, defaultLocale) !== undefined) return false; - // The CMA bumps sys.version on every write (updates, but also publish, - // unpublish, archive and unarchive). Publish and archive states are - // already excluded by the draft query, so on a candidate version 1 can - // only mean the entry was never saved after creation. This check must - // stay client-side: sys.version is not a queryable attribute in CMA - // entry searches. - return !options.untouchedOnly || entry.sys.version === 1; - }); + // Assets and entries share one maxCandidates budget, so the cap holds for + // the scan as a whole no matter which scopes are enabled. + let truncated = entriesTruncated; + let assetCandidates: AssetProps[] = []; + if (options.includeAssets) { + onProgress({ current: progressTotal, total: progressTotal, stepNames: ['Media assets'] }); + const budget = options.maxCandidates - candidates.length; + if (budget > 0) { + assetCandidates = await fetchDraftAssets(cma, budget); + if (candidates.length + assetCandidates.length >= options.maxCandidates) { + truncated = true; + } + } + } + + // The CMA bumps sys.version on every write (updates, but also publish, + // unpublish, archive and unarchive — and for assets, file processing). + // Publish and archive states are already excluded by the draft queries, so + // on a candidate version 1 can only mean it was never saved after + // creation. This check must stay client-side: sys.version is not a + // queryable attribute in CMA searches. + const passesUntouchedFilter = (version: number) => !options.untouchedOnly || version === 1; + + const entryOrphans = candidates.filter( + ({ entry, contentType }) => + getEntryTitle(entry, contentType, defaultLocale) === undefined && + passesUntouchedFilter(entry.sys.version) + ); + const assetOrphans = assetCandidates.filter( + (asset) => + getAssetTitle(asset, defaultLocale) === undefined && passesUntouchedFilter(asset.sys.version) + ); + + // sys.createdBy only carries a link, so creators are resolved to names in + // one batched users lookup per USER_PAGE_LIMIT unique ids — the "who + // created this" column is often the fastest way to find the person or + // workflow that produces orphans. + const creatorLinks = [ + ...entryOrphans.map(({ entry }) => toCreatorLink(entry.sys)), + ...assetOrphans.map((asset) => toCreatorLink(asset.sys)), + ]; + const userIds = [ + ...new Set( + creatorLinks + .filter((link): link is CreatorLink => link !== undefined && link.linkType === 'User') + .map((link) => link.id) + ), + ]; + const names = await resolveCreatorNames(cma, userIds); + const creatorName = (link: CreatorLink | undefined): string => { + // Non-user identities (apps, automations) are not in the users endpoint; + // label them collectively rather than showing a raw id. + if (link && link.linkType !== 'User') return 'App'; + return (link && names[link.id]) || 'Unknown user'; + }; + + const results: OrphanResult[] = [ + ...entryOrphans.map(({ entry, contentType }) => ({ + kind: 'entry' as const, + id: entry.sys.id, + typeName: contentType.name, + createdAt: entry.sys.createdAt, + createdBy: creatorName(toCreatorLink(entry.sys)), + })), + ...assetOrphans.map((asset) => ({ + kind: 'asset' as const, + id: asset.sys.id, + typeName: 'Asset', + createdAt: asset.sys.createdAt, + createdBy: creatorName(toCreatorLink(asset.sys)), + })), + ]; return { results, truncated }; }; diff --git a/apps/find-orphans/test/entryActions.test.ts b/apps/find-orphans/test/entryActions.test.ts index 21fd8972d9..41ad55fe7c 100644 --- a/apps/find-orphans/test/entryActions.test.ts +++ b/apps/find-orphans/test/entryActions.test.ts @@ -1,37 +1,51 @@ import { describe, expect, it, vi } from 'vitest'; -import { archiveEntries } from '../src/locations/Page/utils/entryActions'; +import { archiveOrphans, ArchiveTarget } from '../src/locations/Page/utils/entryActions'; import { createMockCma } from './mocks'; -describe('archiveEntries', () => { - it('archives every entry and reports progress in batches', async () => { +const entry = (id: string): ArchiveTarget => ({ id, kind: 'entry' }); +const asset = (id: string): ArchiveTarget => ({ id, kind: 'asset' }); + +describe('archiveOrphans', () => { + it('archives every target and reports progress in batches', async () => { const { cma, entryArchive } = createMockCma(); const onProgress = vi.fn(); - const outcome = await archiveEntries(cma, ['a', 'b', 'c'], 2, onProgress); + const outcome = await archiveOrphans(cma, [entry('a'), entry('b'), entry('c')], 2, onProgress); expect(outcome.archivedIds).toEqual(expect.arrayContaining(['a', 'b', 'c'])); expect(outcome.failedIds).toEqual([]); expect(entryArchive).toHaveBeenCalledTimes(3); - // Two batches for three entries with a batch size of two. + // Two batches for three targets with a batch size of two. expect(onProgress).toHaveBeenNthCalledWith(1, { current: 2, total: 3 }); expect(onProgress).toHaveBeenNthCalledWith(2, { current: 3, total: 3 }); }); + it('routes each target to the endpoint matching its kind', async () => { + const { cma, entryArchive, assetArchive } = createMockCma(); + + const outcome = await archiveOrphans(cma, [entry('e1'), asset('a1')], 5, vi.fn()); + + expect(outcome.archivedIds).toEqual(expect.arrayContaining(['e1', 'a1'])); + expect(entryArchive).toHaveBeenCalledWith({ entryId: 'e1' }); + expect(assetArchive).toHaveBeenCalledWith({ assetId: 'a1' }); + }); + it('collects failures without aborting the rest of the batch', async () => { const { cma } = createMockCma({ failArchiveIds: ['b'] }); - const outcome = await archiveEntries(cma, ['a', 'b', 'c'], 5, vi.fn()); + const outcome = await archiveOrphans(cma, [entry('a'), entry('b'), asset('c')], 5, vi.fn()); expect(outcome.archivedIds).toEqual(expect.arrayContaining(['a', 'c'])); expect(outcome.failedIds).toEqual(['b']); }); it('handles an empty selection without any API calls', async () => { - const { cma, entryArchive } = createMockCma(); + const { cma, entryArchive, assetArchive } = createMockCma(); - const outcome = await archiveEntries(cma, [], 5, vi.fn()); + const outcome = await archiveOrphans(cma, [], 5, vi.fn()); expect(outcome).toEqual({ archivedIds: [], failedIds: [] }); expect(entryArchive).not.toHaveBeenCalled(); + expect(assetArchive).not.toHaveBeenCalled(); }); }); diff --git a/apps/find-orphans/test/locations/Page.test.tsx b/apps/find-orphans/test/locations/Page.test.tsx index 2301f847ee..bbc42913b7 100644 --- a/apps/find-orphans/test/locations/Page.test.tsx +++ b/apps/find-orphans/test/locations/Page.test.tsx @@ -1,7 +1,14 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import Page from '../../src/locations/Page'; -import { createMockCma, createMockSdk, makeMockEntry, mockArticleContentType } from '../mocks'; +import { + createMockCma, + createMockSdk, + makeMockAsset, + makeMockEntry, + makeMockUser, + mockArticleContentType, +} from '../mocks'; const mocks = vi.hoisted(() => ({ sdk: undefined as unknown, @@ -29,6 +36,9 @@ describe('Page', () => { expect(screen.getByText('Find orphaned entries')).toBeInTheDocument(); expect(screen.getByTestId('scan-button')).toBeInTheDocument(); + // The why-does-this-app-exist explanation moved from an always-visible + // Note into a tooltip behind this info button. + expect(screen.getByTestId('scan-info')).toBeInTheDocument(); await waitFor(() => expect(screen.getByTestId('scan-button')).toBeEnabled()); }); @@ -37,6 +47,7 @@ describe('Page', () => { const { cma } = createMockCma({ contentTypes: [mockArticleContentType], entriesByContentType: { article: [orphan] }, + users: [makeMockUser('user-1', 'Jane', 'Doe')], }); const sdk = createMockSdk(cma); mocks.sdk = sdk; @@ -48,12 +59,98 @@ describe('Page', () => { expect(screen.getByTestId('result-count')).toHaveTextContent('1 entry found'); expect(screen.getByText('Untitled')).toBeInTheDocument(); expect(screen.getByText('Article')).toBeInTheDocument(); + // The creator column resolves sys.createdBy to the user's name. + expect(screen.getByText('Jane Doe')).toBeInTheDocument(); // Previewing is a dedicated action so row clicks never trigger the slide-in. fireEvent.click(screen.getByText('Preview')); expect(sdk.navigator.openEntry).toHaveBeenCalledWith('orphan-1', { slideIn: true }); }); + it('lists orphaned assets alongside entries and previews them in the asset editor', async () => { + const { cma } = createMockCma({ + contentTypes: [mockArticleContentType], + entriesByContentType: { article: [makeMockEntry('orphan-entry', 'article')] }, + assets: [makeMockAsset('orphan-asset')], + }); + const sdk = createMockSdk(cma); + mocks.sdk = sdk; + + render(); + await scan(); + + await waitFor(() => + expect(screen.getByTestId('result-count')).toHaveTextContent('1 entry and 1 asset found') + ); + expect(screen.getByText('Asset')).toBeInTheDocument(); + + // The asset row's preview must open the asset editor, not the entry editor. + fireEvent.click(within(screen.getByTestId('orphan-row-orphan-asset')).getByText('Preview')); + expect(sdk.navigator.openAsset).toHaveBeenCalledWith('orphan-asset', { slideIn: true }); + expect(sdk.navigator.openEntry).not.toHaveBeenCalled(); + }); + + it('quick-selects one kind and itemizes the archive confirmation', async () => { + const { cma } = createMockCma({ + contentTypes: [mockArticleContentType], + entriesByContentType: { + article: [makeMockEntry('entry-a', 'article'), makeMockEntry('entry-b', 'article')], + }, + assets: [makeMockAsset('asset-a')], + }); + mocks.sdk = createMockSdk(cma); + + render(); + await scan(); + await waitFor(() => expect(screen.getByTestId('orphan-table')).toBeInTheDocument()); + + // The kind toggles select every result of that kind, so archiving all + // entries never sweeps assets along. + fireEvent.click(screen.getByTestId('select-entries')); + expect(screen.getByTestId('result-count')).toHaveTextContent('2 selected'); + expect( + within(screen.getByTestId('orphan-row-asset-a')).getByRole('checkbox') + ).not.toBeChecked(); + + // Toggling the same kind off deselects exactly those again. + fireEvent.click(screen.getByTestId('select-entries')); + expect(screen.getByTestId('result-count')).not.toHaveTextContent('selected'); + fireEvent.click(screen.getByTestId('select-entries')); + + // The confirmation names kinds, not generic items. + fireEvent.click(screen.getByTestId('archive-button')); + expect(await screen.findByText('Archive 2 entries')).toBeInTheDocument(); + + // Select-all on a mixed list itemizes both kinds. + fireEvent.click(screen.getByText('Cancel')); + fireEvent.click(screen.getByTestId('select-all')); + fireEvent.click(screen.getByTestId('archive-button')); + expect(await screen.findByText('Archive 2 entries and 1 asset')).toBeInTheDocument(); + }); + + it('scans only the checked scopes', async () => { + const { cma, assetGetMany } = createMockCma({ + contentTypes: [mockArticleContentType], + entriesByContentType: { article: [makeMockEntry('orphan-entry', 'article')] }, + assets: [makeMockAsset('orphan-asset')], + }); + mocks.sdk = createMockSdk(cma); + + render(); + // Uncheck assets: the scan must skip the media library entirely. + fireEvent.click(screen.getByTestId('scope-assets')); + await scan(); + + await waitFor(() => + expect(screen.getByTestId('result-count')).toHaveTextContent('1 entry found') + ); + expect(assetGetMany).not.toHaveBeenCalled(); + + // With both scopes off there is nothing to scan, so the button disables. + fireEvent.click(screen.getByTestId('scope-entries')); + expect(screen.getByTestId('scan-button')).toBeDisabled(); + }); + it('shows an empty state when nothing matches', async () => { const { cma } = createMockCma({ contentTypes: [mockArticleContentType] }); mocks.sdk = createMockSdk(cma); @@ -64,6 +161,19 @@ describe('Page', () => { await waitFor(() => expect(screen.getByTestId('empty-note')).toBeInTheDocument()); }); + it('shows a placeholder before the first scan and removes it afterwards', async () => { + const { cma } = createMockCma({ contentTypes: [mockArticleContentType] }); + mocks.sdk = createMockSdk(cma); + + render(); + // The results area is never blank: a placeholder fills it on fresh open. + expect(screen.getByTestId('pre-scan-placeholder')).toBeInTheDocument(); + + await scan(); + await waitFor(() => expect(screen.getByTestId('empty-note')).toBeInTheDocument()); + expect(screen.queryByTestId('pre-scan-placeholder')).not.toBeInTheDocument(); + }); + it('selects all entries and archives them after confirmation', async () => { const orphanA = makeMockEntry('orphan-a', 'article'); const orphanB = makeMockEntry('orphan-b', 'article'); @@ -77,8 +187,18 @@ describe('Page', () => { await scan(); await waitFor(() => expect(screen.getByTestId('orphan-table')).toBeInTheDocument()); - // The archive button only activates once something is selected. + // The archive button only activates once something is selected, and the + // adjacent popover explains archive-vs-delete with deep links into the + // web app's archived views. expect(screen.getByTestId('archive-button')).toBeDisabled(); + fireEvent.click(screen.getByTestId('archive-info')); + const archivedLink = await screen.findByTestId('archived-entries-link'); + expect(archivedLink).toHaveAttribute( + 'href', + 'https://app.contentful.com/spaces/space-id/environments/master/views/entries?filters.0.key=__status&filters.0.op=&filters.0.val=archived' + ); + expect(archivedLink).toHaveAttribute('target', '_blank'); + fireEvent.click(screen.getByTestId('archive-info')); fireEvent.click(screen.getByTestId('select-all')); expect(screen.getByTestId('archive-button')).toBeEnabled(); diff --git a/apps/find-orphans/test/mocks/mockCma.ts b/apps/find-orphans/test/mocks/mockCma.ts index c8b855752e..cd4d34e334 100644 --- a/apps/find-orphans/test/mocks/mockCma.ts +++ b/apps/find-orphans/test/mocks/mockCma.ts @@ -1,5 +1,5 @@ import { vi } from 'vitest'; -import { ContentTypeProps, EntryProps } from 'contentful-management'; +import { AssetProps, ContentTypeProps, EntryProps, UserProps } from 'contentful-management'; import { CmaClient } from '../../src/locations/Page/types'; const collection = (items: T[], total = items.length) => ({ @@ -14,13 +14,19 @@ export interface MockCmaOptions { contentTypes?: ContentTypeProps[]; /** Entries returned for entry queries, keyed by content type id. */ entriesByContentType?: Record; - /** Entry ids whose archive call should reject. */ + /** Assets returned for asset queries. */ + assets?: AssetProps[]; + /** Space users returned for the creator-name lookup. */ + users?: UserProps[]; + /** Entry/asset ids whose archive call should reject. */ failArchiveIds?: string[]; } export const createMockCma = ({ contentTypes = [], entriesByContentType = {}, + assets = [], + users = [], failArchiveIds = [], }: MockCmaOptions = {}) => { const contentTypeGetMany = vi.fn().mockResolvedValue(collection(contentTypes)); @@ -33,17 +39,49 @@ export const createMockCma = ({ } return Promise.resolve(collection(entries)); }); - const entryArchive = vi.fn().mockImplementation(({ entryId }: { entryId: string }) => { - if (failArchiveIds.includes(entryId)) { - return Promise.reject(new Error(`Cannot archive ${entryId}`)); - } - return Promise.resolve({ sys: { id: entryId } }); - }); + const assetGetMany = vi + .fn() + .mockImplementation(({ query }: { query: Record }) => { + if (query.skip && (query.skip as number) >= assets.length) { + return Promise.resolve(collection([], assets.length)); + } + return Promise.resolve(collection(assets)); + }); + const userGetManyForSpace = vi + .fn() + .mockImplementation(({ query }: { query: Record }) => { + // Mirror the sys.id[in] filter so tests can model users who left the + // space (present in an entry's createdBy but absent here). + const requestedIds = String(query['sys.id[in]'] ?? '').split(','); + return Promise.resolve( + collection(users.filter((user) => requestedIds.includes(user.sys.id))) + ); + }); + const rejectOrResolve = (id: string) => + failArchiveIds.includes(id) + ? Promise.reject(new Error(`Cannot archive ${id}`)) + : Promise.resolve({ sys: { id } }); + const entryArchive = vi + .fn() + .mockImplementation(({ entryId }: { entryId: string }) => rejectOrResolve(entryId)); + const assetArchive = vi + .fn() + .mockImplementation(({ assetId }: { assetId: string }) => rejectOrResolve(assetId)); const cma = { contentType: { getMany: contentTypeGetMany }, entry: { getMany: entryGetMany, archive: entryArchive }, + asset: { getMany: assetGetMany, archive: assetArchive }, + user: { getManyForSpace: userGetManyForSpace }, }; - return { cma: cma as unknown as CmaClient, contentTypeGetMany, entryGetMany, entryArchive }; + return { + cma: cma as unknown as CmaClient, + contentTypeGetMany, + entryGetMany, + entryArchive, + assetGetMany, + assetArchive, + userGetManyForSpace, + }; }; diff --git a/apps/find-orphans/test/mocks/mockEntries.ts b/apps/find-orphans/test/mocks/mockEntries.ts index d3454dfb98..5895c93de2 100644 --- a/apps/find-orphans/test/mocks/mockEntries.ts +++ b/apps/find-orphans/test/mocks/mockEntries.ts @@ -1,13 +1,18 @@ -import { EntryProps } from 'contentful-management'; +import { AssetProps, EntryProps, UserProps } from 'contentful-management'; + +const createdByLink = (userId: string) => ({ + sys: { type: 'Link', linkType: 'User', id: userId }, +}); export const makeMockEntry = ( id: string, contentTypeId: string, fields: EntryProps['fields'] = {}, - updatedAt = '2026-06-01T00:00:00Z', + createdAt = '2026-01-01T00:00:00Z', // Version 1 = never saved after creation; pass a higher version to model // an entry that has been edited. - version = 1 + version = 1, + createdById = 'user-1' ): EntryProps => ({ sys: { @@ -17,8 +22,45 @@ export const makeMockEntry = ( contentType: { sys: { type: 'Link', linkType: 'ContentType', id: contentTypeId } }, space: { sys: { type: 'Link', linkType: 'Space', id: 'space-id' } }, environment: { sys: { type: 'Link', linkType: 'Environment', id: 'master' } }, - createdAt: '2026-01-01T00:00:00Z', - updatedAt, + createdAt, + createdBy: createdByLink(createdById), + updatedAt: createdAt, }, fields, } as EntryProps); + +export const makeMockAsset = ( + id: string, + // Assets always define a localized title field, so a title of undefined + // models the CMA omitting `fields` under a select query (the orphan shape). + title?: string, + createdAt = '2026-01-01T00:00:00Z', + version = 1, + createdById = 'user-1' +): AssetProps => + ({ + sys: { + id, + type: 'Asset', + version, + space: { sys: { type: 'Link', linkType: 'Space', id: 'space-id' } }, + environment: { sys: { type: 'Link', linkType: 'Environment', id: 'master' } }, + createdAt, + createdBy: createdByLink(createdById), + updatedAt: createdAt, + }, + ...(title !== undefined ? { fields: { title: { 'en-US': title } } } : {}), + } as AssetProps); + +export const makeMockUser = ( + id: string, + firstName: string, + lastName: string, + email = `${id}@example.com` +): UserProps => + ({ + sys: { id, type: 'User' }, + firstName, + lastName, + email, + } as UserProps); diff --git a/apps/find-orphans/test/mocks/mockSdk.ts b/apps/find-orphans/test/mocks/mockSdk.ts index ac113ad0af..c540e8424e 100644 --- a/apps/find-orphans/test/mocks/mockSdk.ts +++ b/apps/find-orphans/test/mocks/mockSdk.ts @@ -19,6 +19,7 @@ export const createMockSdk = ( }, navigator: { openEntry: vi.fn(), + openAsset: vi.fn(), }, ids: { space: 'space-id', diff --git a/apps/find-orphans/test/orphanFinder.test.ts b/apps/find-orphans/test/orphanFinder.test.ts index e0d9ebf1d6..2a5246b5c9 100644 --- a/apps/find-orphans/test/orphanFinder.test.ts +++ b/apps/find-orphans/test/orphanFinder.test.ts @@ -1,20 +1,30 @@ import { describe, expect, it, vi } from 'vitest'; import { + buildDraftAssetQuery, buildDraftEntryQuery, fetchAllContentTypes, - findOrphanedEntries, + findOrphans, + getAssetTitle, getEntryTitle, getTextDisplayFieldId, } from '../src/locations/Page/utils/orphanFinder'; import { createMockCma, + makeMockAsset, makeMockEntry, + makeMockUser, mockArticleContentType, mockNumericDisplayContentType, } from './mocks'; const noProgress = vi.fn(); -const options = { maxCandidates: 500, batchSize: 5, untouchedOnly: true }; +const options = { + maxCandidates: 500, + batchSize: 5, + untouchedOnly: true, + includeEntries: true, + includeAssets: true, +}; describe('getTextDisplayFieldId', () => { it('returns the display field id when it is a text field', () => { @@ -48,6 +58,19 @@ describe('getEntryTitle', () => { }); }); +describe('getAssetTitle', () => { + it('returns the title in the default locale', () => { + expect(getAssetTitle(makeMockAsset('a1', 'Sunset photo'), 'en-US')).toBe('Sunset photo'); + }); + + it('returns undefined for whitespace-only titles and absent fields', () => { + expect(getAssetTitle(makeMockAsset('a1', ' '), 'en-US')).toBeUndefined(); + // No title argument = no fields object, the shape a select query returns + // for an asset with no value in any selected field. + expect(getAssetTitle(makeMockAsset('a2'), 'en-US')).toBeUndefined(); + }); +}); + describe('buildDraftEntryQuery', () => { it('scopes to non-archived draft entries of the content type', () => { const query = buildDraftEntryQuery(mockArticleContentType); @@ -69,6 +92,17 @@ describe('buildDraftEntryQuery', () => { }); }); +describe('buildDraftAssetQuery', () => { + it('mirrors the entry query without a content type filter', () => { + expect(buildDraftAssetQuery()).toEqual({ + 'sys.publishedAt[exists]': false, + 'sys.archivedAt[exists]': false, + order: '-sys.updatedAt,sys.id', + select: 'sys,fields.title', + }); + }); +}); + describe('fetchAllContentTypes', () => { it('returns all content types', async () => { const { cma } = createMockCma({ @@ -79,7 +113,7 @@ describe('fetchAllContentTypes', () => { }); }); -describe('findOrphanedEntries', () => { +describe('findOrphans', () => { it('lists drafts with an empty display field and excludes titled ones', async () => { const orphan = makeMockEntry('orphan', 'article'); const healthy = makeMockEntry('healthy', 'article', { title: { 'en-US': 'Has title' } }); @@ -87,62 +121,138 @@ describe('findOrphanedEntries', () => { entriesByContentType: { article: [orphan, healthy] }, }); - const outcome = await findOrphanedEntries( - cma, - [mockArticleContentType], - 'en-US', - noProgress, - options - ); + const outcome = await findOrphans(cma, [mockArticleContentType], 'en-US', noProgress, options); expect(outcome.truncated).toBe(false); expect(outcome.results).toHaveLength(1); - expect(outcome.results[0].entry.sys.id).toBe('orphan'); + expect(outcome.results[0]).toMatchObject({ kind: 'entry', id: 'orphan', typeName: 'Article' }); }); it('treats a whitespace-only title as missing', async () => { const blank = makeMockEntry('blank', 'article', { title: { 'en-US': ' ' } }); const { cma } = createMockCma({ entriesByContentType: { article: [blank] } }); - const outcome = await findOrphanedEntries( - cma, - [mockArticleContentType], - 'en-US', - noProgress, - options + const outcome = await findOrphans(cma, [mockArticleContentType], 'en-US', noProgress, options); + + expect(outcome.results.map((r) => r.id)).toEqual(['blank']); + }); + + it('lists untitled draft assets and excludes titled ones', async () => { + const { cma } = createMockCma({ + assets: [makeMockAsset('orphan-asset'), makeMockAsset('photo', 'Sunset photo')], + }); + + const outcome = await findOrphans(cma, [], 'en-US', noProgress, options); + + expect(outcome.results).toHaveLength(1); + expect(outcome.results[0]).toMatchObject({ + kind: 'asset', + id: 'orphan-asset', + typeName: 'Asset', + }); + }); + + it('resolves creator names, labelling app identities and departed users', async () => { + const byJane = makeMockEntry('by-jane', 'article', {}, '2026-01-01T00:00:00Z', 1, 'user-jane'); + // A creator id with no matching space user models someone who left. + const departed = makeMockEntry( + 'departed', + 'article', + {}, + '2026-01-01T00:00:00Z', + 1, + 'user-gone' ); + const appMade = makeMockEntry('app-made', 'article'); + (appMade.sys.createdBy as { sys: { linkType: string } }).sys.linkType = 'AppDefinition'; + const { cma } = createMockCma({ + entriesByContentType: { article: [byJane, departed, appMade] }, + assets: [makeMockAsset('asset-by-jane', undefined, '2026-01-01T00:00:00Z', 1, 'user-jane')], + users: [makeMockUser('user-jane', 'Jane', 'Doe')], + }); + + const outcome = await findOrphans(cma, [mockArticleContentType], 'en-US', noProgress, options); + + const creators = Object.fromEntries(outcome.results.map((r) => [r.id, r.createdBy])); + expect(creators).toEqual({ + 'by-jane': 'Jane Doe', + departed: 'Unknown user', + 'app-made': 'App', + 'asset-by-jane': 'Jane Doe', + }); + }); + + it('skips the users lookup when no orphan has a user creator', async () => { + const appMade = makeMockEntry('app-made', 'article'); + (appMade.sys.createdBy as { sys: { linkType: string } }).sys.linkType = 'AppDefinition'; + const { cma, userGetManyForSpace } = createMockCma({ + entriesByContentType: { article: [appMade] }, + }); - expect(outcome.results.map((r) => r.entry.sys.id)).toEqual(['blank']); + await findOrphans(cma, [mockArticleContentType], 'en-US', noProgress, options); + + expect(userGetManyForSpace).not.toHaveBeenCalled(); + }); + + it('surfaces the creation date on results', async () => { + const orphan = makeMockEntry('orphan', 'article', {}, '2026-03-05T00:00:00Z'); + const { cma } = createMockCma({ entriesByContentType: { article: [orphan] } }); + + const outcome = await findOrphans(cma, [mockArticleContentType], 'en-US', noProgress, options); + + expect(outcome.results[0].createdAt).toBe('2026-03-05T00:00:00Z'); }); it('excludes untitled drafts that were edited after creation when untouchedOnly is set', async () => { // sys.version increments on every save, so version > 1 means someone has - // worked on the entry — likely a work-in-progress, not an orphan. + // worked on the item — likely a work-in-progress, not an orphan. const untouched = makeMockEntry('untouched', 'article'); const edited = makeMockEntry('edited', 'article', {}, '2026-06-01T00:00:00Z', 4); - const { cma } = createMockCma({ entriesByContentType: { article: [untouched, edited] } }); - - const outcome = await findOrphanedEntries( - cma, - [mockArticleContentType], - 'en-US', - noProgress, - options - ); + const { cma } = createMockCma({ + entriesByContentType: { article: [untouched, edited] }, + assets: [makeMockAsset('touched-asset', undefined, '2026-06-01T00:00:00Z', 3)], + }); + + const outcome = await findOrphans(cma, [mockArticleContentType], 'en-US', noProgress, options); - expect(outcome.results.map((r) => r.entry.sys.id)).toEqual(['untouched']); + expect(outcome.results.map((r) => r.id)).toEqual(['untouched']); }); it('includes edited untitled drafts when untouchedOnly is off', async () => { const edited = makeMockEntry('edited', 'article', {}, '2026-06-01T00:00:00Z', 4); - const { cma } = createMockCma({ entriesByContentType: { article: [edited] } }); + const { cma } = createMockCma({ + entriesByContentType: { article: [edited] }, + assets: [makeMockAsset('touched-asset', undefined, '2026-06-01T00:00:00Z', 3)], + }); - const outcome = await findOrphanedEntries(cma, [mockArticleContentType], 'en-US', noProgress, { + const outcome = await findOrphans(cma, [mockArticleContentType], 'en-US', noProgress, { ...options, untouchedOnly: false, }); - expect(outcome.results.map((r) => r.entry.sys.id)).toEqual(['edited']); + expect(outcome.results.map((r) => r.id)).toEqual(['edited', 'touched-asset']); + }); + + it('skips entries or assets when their scope is off', async () => { + const { cma, entryGetMany, assetGetMany } = createMockCma({ + entriesByContentType: { article: [makeMockEntry('orphan', 'article')] }, + assets: [makeMockAsset('orphan-asset')], + }); + + const entriesOnly = await findOrphans(cma, [mockArticleContentType], 'en-US', noProgress, { + ...options, + includeAssets: false, + }); + expect(entriesOnly.results.map((r) => r.id)).toEqual(['orphan']); + expect(assetGetMany).not.toHaveBeenCalled(); + + entryGetMany.mockClear(); + const assetsOnly = await findOrphans(cma, [mockArticleContentType], 'en-US', noProgress, { + ...options, + includeEntries: false, + }); + expect(assetsOnly.results.map((r) => r.id)).toEqual(['orphan-asset']); + expect(entryGetMany).not.toHaveBeenCalled(); }); it('skips content types without a text display field', async () => { @@ -152,29 +262,31 @@ describe('findOrphanedEntries', () => { entriesByContentType: { counter: [makeMockEntry('c1', 'counter')] }, }); - const outcome = await findOrphanedEntries( - cma, - [mockNumericDisplayContentType], - 'en-US', - noProgress, - options - ); + const outcome = await findOrphans(cma, [mockNumericDisplayContentType], 'en-US', noProgress, { + ...options, + includeAssets: false, + }); expect(outcome.results).toHaveLength(0); expect(entryGetMany).not.toHaveBeenCalled(); }); - it('caps the fetched drafts at maxCandidates and reports truncation', async () => { + it('caps the fetched drafts at maxCandidates across entries and assets', async () => { const entries = [makeMockEntry('e1', 'article'), makeMockEntry('e2', 'article')]; - const { cma } = createMockCma({ entriesByContentType: { article: entries } }); + const { cma, assetGetMany } = createMockCma({ + entriesByContentType: { article: entries }, + assets: [makeMockAsset('a1')], + }); - const outcome = await findOrphanedEntries(cma, [mockArticleContentType], 'en-US', noProgress, { + const outcome = await findOrphans(cma, [mockArticleContentType], 'en-US', noProgress, { ...options, maxCandidates: 1, }); expect(outcome.truncated).toBe(true); expect(outcome.results).toHaveLength(1); + // Entries consumed the whole budget, so the asset query never fires. + expect(assetGetMany).not.toHaveBeenCalled(); }); it('combines results from multiple content types scanned in one parallel chunk', async () => { @@ -193,32 +305,33 @@ describe('findOrphanedEntries', () => { }, }); - const outcome = await findOrphanedEntries( - cma, - [articleType, noteType], - 'en-US', - noProgress, - options - ); + const outcome = await findOrphans(cma, [articleType, noteType], 'en-US', noProgress, { + ...options, + includeAssets: false, + }); - expect(outcome.results.map((r) => r.entry.sys.id)).toEqual( - expect.arrayContaining(['a1', 'n1']) - ); + expect(outcome.results.map((r) => r.id)).toEqual(expect.arrayContaining(['a1', 'n1'])); // One entry query per content type; there is no reference-count phase. expect(entryGetMany).toHaveBeenCalledTimes(2); }); - it('reports progress with the content types being checked', async () => { + it('reports content type and asset steps against one combined total', async () => { const onProgress = vi.fn(); const orphan = makeMockEntry('orphan', 'article'); const { cma } = createMockCma({ entriesByContentType: { article: [orphan] } }); - await findOrphanedEntries(cma, [mockArticleContentType], 'en-US', onProgress, options); + await findOrphans(cma, [mockArticleContentType], 'en-US', onProgress, options); - expect(onProgress).toHaveBeenCalledWith({ + // One content type plus the asset step = 2 total steps. + expect(onProgress).toHaveBeenNthCalledWith(1, { current: 1, - total: 1, - contentTypeNames: ['Article'], + total: 2, + stepNames: ['Article'], + }); + expect(onProgress).toHaveBeenNthCalledWith(2, { + current: 2, + total: 2, + stepNames: ['Media assets'], }); }); }); From 3e9c0dde4ef16e750324afb9043fecab1287a80b Mon Sep 17 00:00:00 2001 From: Shanon Place Date: Mon, 13 Jul 2026 20:05:50 -0700 Subject: [PATCH 4/8] Tabbed approach and a lot of UI cleanup --- apps/find-orphans/AGENTS.md | 79 ++- apps/find-orphans/README.md | 141 ++-- .../src/locations/ConfigScreen.tsx | 18 +- .../locations/Page/components/OrphanTable.tsx | 12 +- .../find-orphans/src/locations/Page/index.tsx | 623 +++++++++++------- apps/find-orphans/src/locations/Page/types.ts | 28 +- .../src/locations/Page/utils/orphanFinder.ts | 185 ++++-- apps/find-orphans/src/parameters.ts | 8 +- .../test/locations/ConfigScreen.test.tsx | 4 +- .../find-orphans/test/locations/Page.test.tsx | 208 +++++- apps/find-orphans/test/mocks/mockCma.ts | 11 + .../test/mocks/mockContentTypes.ts | 24 + apps/find-orphans/test/orphanFinder.test.ts | 162 ++++- 13 files changed, 1086 insertions(+), 417 deletions(-) diff --git a/apps/find-orphans/AGENTS.md b/apps/find-orphans/AGENTS.md index 49e9cf8684..59d9a11766 100644 --- a/apps/find-orphans/AGENTS.md +++ b/apps/find-orphans/AGENTS.md @@ -1,13 +1,26 @@ # Agent Guide — find-orphans ## What This App Does -Full-page app that scans an environment for "orphaned" draft entries and media assets — -typically empty items accidentally created from a reference field when the user meant to link -an existing one. An orphan is a draft (never published, not archived) with no title value in -the default locale; with the `untouchedOnly` parameter (default on) it must also never have -been saved after creation (`sys.version === 1`). The scan scope (entries and/or assets) is a -per-run checkbox choice next to the scan button, both on by default. Results deep-link into the -matching editor and can be archived in bulk. +Full-page app that scans an environment for "orphaned" draft entries and media assets. Two +criteria, each in its own tab with its own scan button and its own cached results: + +- **Untitled**: a draft (never published, not archived) with no title value in the default + locale — typically created by accident from a reference field. These are confident junk. +- **Unreferenced**: a draft that no entry links to (`links_to_entry` / `links_to_asset` count + is 0). A review lead, NOT proof of junk — top-level content (landing pages) is legitimately + unreferenced. Copy must never claim these are safe to delete. + +Scans are broad: edit history NEVER excludes results at scan time. Every result carries a +`neverEdited` flag (`sys.version === 1`) and the UNTITLED tab's results header has a segmented +view switch — "Show: All (N) / Never edited (M)" ToggleButton pills, exactly one always +pressed, clicking the active pill is a no-op — with `untouchedOnly` setting its starting view. +The unreferenced tab has NO never-edited switch at all (decided 2026-07-13: edit history says +nothing about referenced-ness). Keep it this way: silent scan-time +exclusion was tried first and read as "the scan is broken" when rows appeared in one tab and +not the other, and a single on/off toggle was tried next and read as ambiguous. + +The scan scope (entries and/or assets) is a per-run checkbox choice, both on by default. +Results deep-link into the matching editor and can be archived in bulk. ## Archetype Standard Vite app. Page location plus an app-config screen for installation parameters. @@ -31,7 +44,7 @@ runs with missing or invalid settings. |--------------|------|----------|---------|---------| | `maxCandidates` | Number | Yes | 500 | Hard cap on candidate entries per scan | | `batchSize` | Number | Yes | 5 (max 7) | Concurrent CMA requests (scan queries, archiving) | -| `untouchedOnly` | Boolean | Yes | true | Only flag drafts never saved after creation (`sys.version === 1`) | +| `untouchedOnly` | Boolean | Yes | true | Default state of the untitled tab's "Never edited" results filter (`sys.version === 1`) | Parameter definitions on the app definition must use these exact IDs and types; the README documents the full table (including descriptions) for the app definition setup. @@ -79,8 +92,22 @@ src/ `sys.version`. - **Default-locale only**: the missing-title check and title rendering use `sdk.locales.default`. Localized titles in other locales are not considered. -- **Content types without a text display field are skipped entirely** — their entries cannot be - missing a title, so querying them would waste API calls. +- **Content types without a text display field are skipped by the untitled scan** — including + types with NO display field configured (component types): their entries always render as + "Untitled", but flagging every draft of them would sweep in legitimate work-in-progress. + This, plus the "Never edited" filter (and the fact that archive→unarchive round-trips bump + `sys.version`), is why an "Untitled" row can appear in unreferenced results and not on the + untitled tab. That divergence is BY DESIGN and pinned in tests — investigated 2026-07-13 + after it was mistaken for a regression; do not "fix" it without Shanon deciding to change + the criteria. +- **Every view-narrowing control must narrow the selection too** — the "Never edited" pills + (`setNeverEditedFilter`) and the scope checkboxes (`setScope`, which live-filter cached + results client-side as well as scoping the next scan; unchecking prunes selections in BOTH + tabs since scope is shared). Otherwise "Archive selected" could archive rows the user can no + longer see. When filters hide every result, render the explanatory `filtered-empty-note` + naming the responsible filter, never a bare empty table. +- **No header info tooltip**: the page heading is plain (removed 2026-07-13); the app explains + itself via the general subtitle and each tab's visible description. - **One entry query per content type is unavoidable** (entry queries require a single `content_type`), so the scan runs those queries `batchSize` at a time in parallel chunks. A chunk shares one remaining-budget snapshot and can overshoot `maxCandidates`; the result is @@ -89,6 +116,27 @@ src/ library is a single scan step that runs after the entry phase and spends whatever remains of the `maxCandidates` budget. This is why one scan button with scope checkboxes beats separate entry/asset CTAs: the asset step is marginal next to the entry fan-out. +- **Reference counting is N+1 and must stay batched**: the CMA filters `links_to_entry` / + `links_to_asset` by ONE target id per query, so the unreferenced criterion costs one + `limit: 0` count request per candidate, run `batchSize` at a time (`filterUnreferenced`). + Never fan these out unbatched, and keep the cost warning in the unreferenced button's + tooltip. The + untitled criterion's content-type restriction (text display fields only) does NOT apply to + the unreferenced scan — any entry can be a link target. +- **One criterion per scan, each criterion is a tab** (chosen 2026-07-13, evolving radio → + two buttons → tabs; the OR-combined design was removed on 2026-07-09): results always have a + single unambiguous meaning, only unreferenced scans pay the reference-count cost, and + separate tabs keep the confident-junk vs review-lead expectations apart. +- **Per-tab state is cached and must survive tab switches** (`tabStates: Record` — results, truncation, selection): switching tabs NEVER clears results or re-runs + a scan; tab labels show the cached counts. Scans are exclusive (one `activeScan` at a time) + and the scope checkboxes are shared across tabs. +- **Criterion explanations are visible text inside each tab's panel**, above the scan button — + NOT tooltips on the tab labels (tried and removed 2026-07-13: they duplicated the adjacent + visible description). The unreferenced description must lead with the one-request-per-draft + cost warning. The archive button keeps its tooltip with an `InfoIcon` end-icon as the hover + cue; the archive deep links live in the confirmation modal (a hover tooltip cannot hold + clickable links). - **`maxCandidates` caps each scan across both scopes** — the UI shows a truncation warning. Keep the cap or replace it with real pagination, but never scan unbounded. - **Creator names are best-effort**: `resolveCreatorNames` batches `user.getManyForSpace` @@ -99,11 +147,12 @@ src/ - **Archiving routes by kind**: `archiveOrphans` calls `cma.entry.archive` or `cma.asset.archive` per target — `OrphanResult.kind` must survive any refactor of the result shape, and preview likewise routes to `openEntry`/`openAsset`. -- **Mixed-kind selection is guarded twice, keep both**: kind-scoped ToggleButtons ("Entries - (N)" / "Assets (N)", shown only when results mix kinds, pressed state derived from the - selection, toggle off to deselect that kind) and an archive confirmation that itemizes the - selection by kind ("Archive 19 entries and 4 assets"). This is the agreed alternative to - splitting the results into per-kind tables — select-all deliberately spans both kinds. +- **Mixed-kind selection is guarded twice, keep both**: the scope checkboxes (hide AND + deselect a kind, so "archive only entries" = uncheck assets + select-all) and an archive + confirmation that itemizes the selection by kind ("Archive 19 entries and 4 assets"). + Kind-scoped select-all ToggleButtons existed but were removed 2026-07-13 as redundant once + the scope checkboxes became live filters — select-all deliberately spans every displayed + kind, and per-kind result tables were also considered and rejected. - **Pagination order needs the `sys.id` tiebreaker**: entries sharing an `updatedAt` (bulk imports) sort non-deterministically otherwise, and skip-based paging can drop or duplicate them. diff --git a/apps/find-orphans/README.md b/apps/find-orphans/README.md index 0f08572650..8242a46609 100644 --- a/apps/find-orphans/README.md +++ b/apps/find-orphans/README.md @@ -1,55 +1,99 @@ # Find Orphans -A Contentful App Framework **page app** that finds draft entries and media assets that were -likely created by mistake — the classic case being clicking "Create new entry" (or "Add new -media") on a reference field when you meant to link an existing item, leaving behind an empty, -untitled draft. +A Contentful App Framework **page app** that finds orphaned draft entries and media assets, +under either of two definitions of "orphan": -## How it works +- **Untitled drafts** — items likely created by mistake, the classic case being clicking + "Create new entry" (or "Add new media") on a reference field when you meant to link an + existing item, leaving behind an empty, untitled draft. These are confident junk. +- **Unreferenced drafts** — items that no entry links to. This is a *lead*, not a verdict: + top-level content such as landing pages is often never referenced yet perfectly valid, so + these results are for review, possibly-unused content. -The app scans the current environment for **draft entries and assets** (never published, not -archived) whose **title has no value** in the default locale — including whitespace-only values, -which the editors also render as "Untitled". For entries the title is the content type's display -field; assets have a fixed shape, so their localized `title` field is checked directly and the -whole media library is a single paged query. Two checkboxes next to the scan button control the -scope per run (entries and/or assets, both on by default) — the per-content-type entry queries -are the slow part of a scan, so an assets-only pass is quick. +## How it works -By default the scan additionally requires that the item was **never edited after creation** -(`sys.version === 1`). The CMA bumps `sys.version` on every save — and on publish, unpublish, +Each criterion lives in **its own tab** with its own scan button and its own cached results — +separate scans because the two result sets carry different expectations (archive-freely junk +versus review-first leads), and per-tab caching because comparing them should be free: run the +untitled scan, switch to the unreferenced tab and scan there, switch back, and the first +results are still sitting in their table, with the counts shown right on the tab labels. Every +scan covers **drafts** (never published, not archived). The page subtitle stays general; each +tab explains its own criterion with a description inside the panel, always visible above the +scan button — the unreferenced one leads with the warning that reference checking is one +network request per draft and can take several minutes on large spaces. (Tab labels carry only +the name and cached result count; switching tabs is free, so the description is never more than +a click away.) Two checkboxes control the scope (entries +and/or assets, both on by default, shared across tabs) — they decide what the next scan fetches +AND live-filter already-scanned results client-side, hiding (and deselecting) that kind's rows +without a re-scan. The per-content-type entry queries are the slow part of candidate +collection, so an assets-only pass is quick. + +**Untitled criterion**: flags drafts whose **title has no value** in the default locale — +including whitespace-only values, which the editors also render as "Untitled". For entries the +title is the content type's display field, and content types whose display field is not a +`Symbol`/`Text` field are skipped, since their entries cannot be missing a title. Assets have a +fixed shape, so their localized `title` field is checked directly and the whole media library +is a single paged query. + +**Unreferenced criterion**: flags drafts that **no entry links to**, using one +`links_to_entry` (or `links_to_asset`) count query per candidate — the CMA can only check +referenced-ness for one target id per request, so this scan costs one extra API call per draft +and is much slower on large spaces (its button's info tooltip says so). Every content type +participates (any entry can be a link target), results keep their real titles, and the +never-edited filter below does not apply — an unreferenced entry is worth flagging no matter +how often it was edited. + +Scans are **broad first, strict on demand**: edit history never excludes anything at scan time. +Instead, every result carries a never-edited flag and the untitled tab's results header has a +segmented view switch — **Show: "All (N)" / "Never edited (M)"** — where exactly one pill is +always pressed, so the current view and both counts are always visible. (The unreferenced tab +has no such switch: edit history says nothing about whether something is referenced.) The strict view narrows the table to +items never saved after creation (`sys.version === 1`); its pill carries a tooltip explaining +the term — including that archiving and unarchiving counts as editing, so restored orphans +appear only under "All", where they may still be worth archiving again. The CMA bumps `sys.version` on every save — and on publish, unpublish, archive, unarchive and (for assets) file processing, but those states are already excluded by -the draft scope — so on a candidate, version 1 can only mean the item was created and then -abandoned untouched. This keeps untitled work-in-progress drafts (body started, title not yet -filled in) out of the results; uploaded assets get their title auto-filled from the filename, so -untitled assets are almost always accidental creations. The filter can be turned off via the -`untouchedOnly` installation parameter, which is useful when apps or scripts write to items on -creation and bump every one past version 1. - -This is something the regular Contentful search cannot do in one query: field filters only work -for one content type at a time, and every content type defines its own display field. The app -automates that per-content-type check across the whole content model. Content types whose -display field is not a `Symbol`/`Text` field are skipped, since their entries cannot be missing -a title. - -Results are listed in one table with their type (the content type name, or "Asset"), creation -date, and creator, and the result count spells out the split (e.g. "19 entries and 4 assets -found"). The date shown is **created**, not last-updated — orphans were (by default) never +the draft scope — so version 1 can only mean the item was created and then abandoned untouched. +Narrowing the view also narrows the selection, so the archive action can never touch a hidden +row, and when the filter hides every result a note says so instead of showing an empty table. +The `untouchedOnly` installation parameter only sets the filter's starting state on the +untitled tab (the unreferenced tab always starts unfiltered); filtering is instant and +client-side, never a re-scan. + +**Why the two lists can differ**: an "Untitled" row in the unreferenced results that the +untitled tab does not show is not a bug — it is one of two documented cases. Either the draft +was edited after creation and the "Never edited" filter is hiding it (the toggle shows the +count; note that **archiving and then unarchiving an item also bumps `sys.version`**, so +orphans that were archived and restored no longer count as never-edited), or the entry belongs +to a content type with **no display field configured** (common for component types like +banners) — such entries always render as "Untitled" in Contentful, and the untitled scan skips +those types deliberately, because flagging every draft of them would sweep in legitimate +work-in-progress. Both cases are pinned in tests. + +Neither criterion is something the regular Contentful search can express: title filters only +work for one content type at a time (every content type defines its own display field), and +referenced-ness is not a searchable attribute at all. The app automates both checks across the +whole content model. + +Results are listed in one table with their title (untitled-scan results show the editor's +"Untitled" placeholder; unreferenced ones show their real titles), type (the content type name, +or "Asset"), creation date, and creator, and the result count spells out the split (e.g. "19 +entries and 4 assets found"). The date shown is **created**, not last-updated — orphans were (by default) never edited after creation, so the creation moment is what identifies the mistake. The creator comes from `sys.createdBy`, resolved to names via one batched space-users lookup per scan; items created by apps or automations show "App", and creators who cannot be resolved (left the space, or the caller may not list users) show "Unknown user". Each row has an explicit "Preview" action that opens the matching editor in a slide-in for review. -Rows can be selected individually, all at once, or per kind — when the results mix entries and -assets, "Entries (N)" / "Assets (N)" toggle buttons select or deselect everything of one kind -(the pressed state mirrors the selection), so archiving all entries never sweeps assets along. -The selected items are archived in bulk after a confirmation dialog that itemizes the selection -by kind ("Archive 19 entries and 4 assets"); archiving runs in rate-limit-friendly batches +Rows can be selected individually or all at once; to act on one kind only, uncheck the other +kind's scope checkbox — that hides and deselects its rows — and then select all, so archiving +all entries never sweeps assets along. The selected items are archived in bulk after a +confirmation dialog that itemizes the selection by kind ("Archive 19 entries and 4 assets"); archiving runs in rate-limit-friendly batches through the endpoint matching each item's kind, and items that fail to archive stay listed and -selected for retry. Archiving is reversible from the editor; an info popover next to the -archive button spells this out and deep-links (in a new tab) to the web app's archived-entries -and archived-assets views — `…/views/entries?filters.0.key=__status&filters.0.op=&filters.0.val=archived` -— where permanent deletion lives. +selected for retry. Archiving is reversible from the editor; the archive button's tooltip (on +its info end-icon) says so, and the confirmation dialog deep-links (in a new tab) to the web +app's archived-entries and archived-assets views — +`…/views/entries?filters.0.key=__status&filters.0.op=&filters.0.val=archived` — where permanent +deletion lives. Scans are capped at a configurable number of draft items per run (500 by default, entries and assets sharing the one budget, see Configuration below) to stay friendly to CMA rate limits; a @@ -60,20 +104,20 @@ warning is shown when the cap is hit. ```mermaid flowchart TD Load[App loads] --> FetchCT[Fetch all content types
paged, 1000 per request] - FetchCT --> Scan([User clicks Scan with a scope:
entries and/or media assets]) - Scan --> Filter[Entries scope: keep content types whose
display field is a Symbol or Text field] + FetchCT --> Scan([User runs a scan from its tab
untitled or unreferenced
with a scope: entries and/or assets]) + Scan --> Filter[Entries scope: untitled scans keep content types
with a Symbol/Text display field;
unreferenced scans keep all] Filter --> Chunk[Take next batchSize content types] Chunk --> Query[Query draft entries per content type in parallel:
never published, not archived,
select sys + display field only] Query --> Pages[Page through results until exhausted
or the maxCandidates budget is spent] Pages --> More{More content types
and budget left?} More -- yes --> Chunk More -- no --> AssetStep[Assets scope: one paged draft-asset query
spending the remaining budget,
select sys + title only] - AssetStep --> Check[Client-side check: title empty,
whitespace-only, or fields object absent
in the default locale] - Check --> Untouched{untouchedOnly
enabled?} - Untouched -- yes --> Version[Keep only items never edited
after creation: sys.version == 1] - Untouched -- no --> Resolve - Version --> Resolve[Resolve creator names from sys.createdBy:
batched space-users lookup, tolerant of failure] - Resolve --> Results[Results table
+ truncation warning if capped] + AssetStep --> Criterion{Criterion?} + Criterion -- untitled --> Check[Client-side check: title empty,
whitespace-only, or fields object absent
in the default locale] + Check --> Resolve + Criterion -- unreferenced --> RefCount[One links_to_entry / links_to_asset
count query per candidate, batched batchSize
at a time; keep items with 0 references] + RefCount --> Resolve[Resolve creator names from sys.createdBy:
batched space-users lookup, tolerant of failure] + Resolve --> Results[Results table, cached per tab,
with a visible Never edited filter
sys.version == 1, default from untouchedOnly
+ truncation warning if capped] Results --> Archive([User selects rows and confirms Archive]) Archive --> Batches[Archive in batches of batchSize,
entries and assets each via their endpoint] Batches --> Done[Archived rows leave the list;
failed ones stay listed and selected for retry] @@ -99,7 +143,7 @@ registering the app definition, create the parameter definitions with exactly th |--------------|----|------|----------|---------------|-------------| | Maximum entries per scan | `maxCandidates` | Number | Yes | `500` | The scan stops after this many draft entries and assets, to stay friendly to API rate limits. | | Concurrent API requests | `batchSize` | Number | Yes | `5` | How many CMA requests run at once while scanning and archiving. Must be between 1 and 7, the CMA rate limit per second. | -| Only include entries that were never edited | `untouchedOnly` | Boolean | Yes | `true` | Only flag drafts still at version 1, i.e. never saved after creation. Turn off to also catch untitled drafts that were edited and then abandoned. | +| Start results filtered to never-edited items | `untouchedOnly` | Boolean | Yes | `true` | Whether the untitled tab's results open with the "Never edited" filter already on. Scans always find every untitled draft; the filter only narrows the view (version 1 = never saved after creation). The unreferenced tab always starts unfiltered. | All parameters are required and their defaults are set on the parameter definition, so a fresh install starts with the values above. The config screen shows the same defaults as input @@ -126,5 +170,6 @@ npm run create-app-definition ## Learn more - [Contentful App Framework](https://www.contentful.com/developers/docs/extensibility/app-framework/) +- [Links and incoming-link queries](https://www.contentful.com/developers/docs/references/content-delivery-api/links/#links-to-a-specific-item) — background on `links_to_entry`; the field-filter variant documented there needs the linking content type and field to be known, which is why the unreferenced scan uses one `links_to_entry`/`links_to_asset` count query per item instead - [Page location](https://www.contentful.com/developers/docs/extensibility/app-framework/locations/#page) - [Forma 36](https://f36.contentful.com/) diff --git a/apps/find-orphans/src/locations/ConfigScreen.tsx b/apps/find-orphans/src/locations/ConfigScreen.tsx index 5d9d8f1ab6..3341fbf0c9 100644 --- a/apps/find-orphans/src/locations/ConfigScreen.tsx +++ b/apps/find-orphans/src/locations/ConfigScreen.tsx @@ -136,10 +136,9 @@ const ConfigScreen = () => { style={{ maxWidth: '768px', width: '100%' }}> Set up Find Orphans - Find Orphans scans this space for draft entries and media assets with no title — the - signature of an item created by mistake, for example by adding a new entry on a reference - field instead of linking an existing one — and lets you review or archive them from one - page. + Find Orphans scans this space for orphaned draft entries and media assets — items with no + title (the signature of an entry created by mistake from a reference field) or items that + no entry references — and lets you review or archive them from one page. @@ -174,13 +173,14 @@ const ConfigScreen = () => { setValues((previous) => ({ ...previous, untouchedOnly: event.target.checked })) } testId="untouched-only"> - Only include entries that were never edited + Start results filtered to never-edited items - Contentful bumps an item's version on every save, so an untitled draft still at - version 1 was abandoned right after creation. Turn this off to also flag untitled - drafts that were edited at least once — for example when other apps or scripts write - to entries on creation. + Scans always find every untitled draft; this only decides whether the results open + with the “Never edited” filter already on. Contentful bumps an item's + version on every save, so an untitled draft still at version 1 was abandoned right + after creation. Applies to the untitled tab; the unreferenced tab always starts + unfiltered. diff --git a/apps/find-orphans/src/locations/Page/components/OrphanTable.tsx b/apps/find-orphans/src/locations/Page/components/OrphanTable.tsx index 0c59ed77ee..935d0ec4a4 100644 --- a/apps/find-orphans/src/locations/Page/components/OrphanTable.tsx +++ b/apps/find-orphans/src/locations/Page/components/OrphanTable.tsx @@ -69,10 +69,14 @@ export const OrphanTable = ({ /> - {/* The scan only lists items with an empty title, so this is - always the editor's "Untitled" placeholder, mirroring what - the content list and media library show for them. */} - Untitled + {/* Untitled-scan results never have a title; unreferenced ones + usually do. The gray "Untitled" placeholder mirrors what + the content list and media library show for titleless items. */} + {result.title !== undefined ? ( + {result.title} + ) : ( + Untitled + )} {result.typeName} diff --git a/apps/find-orphans/src/locations/Page/index.tsx b/apps/find-orphans/src/locations/Page/index.tsx index aa46c3fcb4..5ff3e4af73 100644 --- a/apps/find-orphans/src/locations/Page/index.tsx +++ b/apps/find-orphans/src/locations/Page/index.tsx @@ -6,32 +6,36 @@ import { Checkbox, Flex, Heading, - IconButton, ModalConfirm, Note, Notification, - Popover, SkeletonRow, Spinner, Table, + Tabs, Text, TextLink, ToggleButton, Tooltip, } from '@contentful/f36-components'; import tokens from '@contentful/f36-tokens'; -import { InfoIcon, TrayArrowDownIcon, MagnifyingGlassIcon } from '@contentful/f36-icons'; +import { + InfoIcon, + LinkBreakIcon, + TrayArrowDownIcon, + MagnifyingGlassIcon, +} from '@contentful/f36-icons'; import { useSDK } from '@contentful/react-apps-toolkit'; import { ContentTypeProps } from 'contentful-management'; import { resolveParameters } from '../../parameters'; import { OrphanTable } from './components/OrphanTable'; -import { OrphanKind, OrphanResult, ScanProgress } from './types'; +import { OrphanKind, OrphanResult, ScanCriterion, ScanProgress } from './types'; import { archiveOrphans, ArchiveProgress } from './utils/entryActions'; import { fetchAllContentTypes, findOrphans } from './utils/orphanFinder'; // Name what is being checked right now, e.g. // "Checking Article, Author… (5/51)". Steps are content types plus the -// media-library pass, so the label does not say "content types". +// media-library pass (or reference counts), so the label stays generic. const progressLabel = (progress: ScanProgress): string => `Checking ${progress.stepNames.join(', ')}… (${progress.current}/${progress.total})`; @@ -52,6 +56,60 @@ const describeCounts = ({ entries, assets }: { entries: number; assets: number } return parts.length > 0 ? parts.join(' and ') : '0 items'; }; +/** + * Everything one criterion's tab remembers. Each tab keeps its own results, + * selection, and filter, so switching between the untitled and unreferenced + * tabs never discards or re-runs a scan. + */ +interface TabState { + results: OrphanResult[] | null; + truncated: boolean; + selectedIds: string[]; + /** + * The "Never edited" results filter. The scan is always broad; this only + * narrows what is displayed (and selectable), so nothing is ever silently + * excluded — the toggle shows how many results it is holding back. + */ + neverEditedOnly: boolean; +} + +/** Reset applied when a scan starts: results clear, the filter persists. */ +const SCAN_RESET = { results: null, truncated: false, selectedIds: [] }; + +/** + * Static per-criterion UI copy and chrome. The description is shown inside + * the tab's panel above its scan button, so it is always visible before the + * scan can be started — switching tabs is free (results are cached), which + * is also why the tab labels carry no explainer of their own. + */ +const CRITERIA: Record< + ScanCriterion, + { + tabLabel: string; + scanLabel: string; + description: string; + icon: JSX.Element; + emptyNote: string; + } +> = { + untitled: { + tabLabel: 'Untitled drafts', + scanLabel: 'Scan for untitled drafts', + description: + 'Untitled items are the signature of something created by accident — for example by adding a new entry on a reference field instead of linking an existing one — and abandoned.', + icon: , + emptyNote: 'No orphans found — every scanned draft has a title.', + }, + unreferenced: { + tabLabel: 'Unreferenced drafts', + scanLabel: 'Scan for unreferenced drafts', + description: + 'Checking references cannot be done in bulk — this scan makes one network request per draft, so it can take several minutes on large spaces. It finds items that no entry links to: not proof of junk, since top-level content like landing pages is often never referenced yet perfectly valid, but a good lead when hunting for possibly unused content.', + icon: , + emptyNote: 'No orphans found — every scanned draft is referenced by at least one entry.', + }, +}; + const Page = () => { const sdk = useSDK(); // Installation parameters may be empty (fresh install, or the app was @@ -62,24 +120,32 @@ const Page = () => { ); const [contentTypes, setContentTypes] = useState([]); const [contentTypesLoading, setContentTypesLoading] = useState(true); - const [scanning, setScanning] = useState(false); + // Which scan is currently running (null = none). Scans are exclusive: + // both buttons disable while one runs, and the spinner shows on the + // running one. + const [activeScan, setActiveScan] = useState(null); const [progress, setProgress] = useState(null); - // null means "no scan has run yet", which hides the results section - // entirely; an empty array renders the positive empty state instead. - const [results, setResults] = useState(null); - const [truncated, setTruncated] = useState(false); - // Per-run scan scope. Both on by default; the entry fan-out is the slow - // part, so unchecking "Entries" gives a fast assets-only pass. + const [activeTab, setActiveTab] = useState('untitled'); + // One cached state per criterion tab; `results: null` means "this tab has + // not scanned yet" and renders the placeholder instead of a table. The + // untitled tab's filter defaults from the untouchedOnly parameter; the + // unreferenced tab always starts broad (edit history says nothing about + // whether something is referenced). + const [tabStates, setTabStates] = useState>(() => ({ + untitled: { ...SCAN_RESET, neverEditedOnly: parameters.untouchedOnly }, + unreferenced: { ...SCAN_RESET, neverEditedOnly: false }, + })); + // Per-run scan scope, shared by both tabs. Both on by default; the entry + // fan-out is the slow part, so unchecking "Entries" gives a fast + // assets-only pass. const [scanEntries, setScanEntries] = useState(true); const [scanAssets, setScanAssets] = useState(true); - const [selectedIds, setSelectedIds] = useState([]); const [archiving, setArchiving] = useState(false); const [archiveProgress, setArchiveProgress] = useState(null); const [confirmArchiveOpen, setConfirmArchiveOpen] = useState(false); - const [archiveInfoOpen, setArchiveInfoOpen] = useState(false); // Deep links into the web app's archived views — permanent deletion only - // exists there, so the archive-info popover hands users the destination. + // exists there, so the archive confirmation hands users the destination. // environmentAlias keeps the link on the alias the user is browsing under. // The filter uses the web app's shareable-view format: indexed key/op/val // triplets against the "__status" pseudo-field (empty op = equals). @@ -94,13 +160,35 @@ const Page = () => { }; }, [sdk]); + const scanning = activeScan !== null; const busy = scanning || archiving; - const resultCounts = useMemo(() => countByKind(results ?? []), [results]); + const currentTab = tabStates[activeTab]; + // What the table (and every selection action) actually operates on: the + // tab's results narrowed by the scope checkboxes (which double as live + // client-side filters on cached results) and the "Never edited" view. + const displayedResults = useMemo( + () => + (currentTab.results ?? []).filter( + (result) => + (result.kind === 'entry' ? scanEntries : scanAssets) && + (!currentTab.neverEditedOnly || result.neverEdited) + ), + [currentTab.results, currentTab.neverEditedOnly, scanEntries, scanAssets] + ); + const resultCounts = useMemo(() => countByKind(displayedResults), [displayedResults]); const selectedCounts = useMemo( - () => countByKind((results ?? []).filter((result) => selectedIds.includes(result.id))), - [results, selectedIds] + () => + countByKind(displayedResults.filter((result) => currentTab.selectedIds.includes(result.id))), + [displayedResults, currentTab.selectedIds] ); + const patchTab = useCallback((criterion: ScanCriterion, patch: Partial) => { + setTabStates((previous) => ({ + ...previous, + [criterion]: { ...previous[criterion], ...patch }, + })); + }, []); + // Content types are loaded once up front: the scan needs their display // field definitions, and the list rarely changes within a session. useEffect(() => { @@ -117,33 +205,35 @@ const Page = () => { loadContentTypes(); }, [sdk.cma]); - const runScan = useCallback(async () => { - setScanning(true); - setResults(null); - setTruncated(false); - // A new scan invalidates any previous selection. - setSelectedIds([]); - try { - const outcome = await findOrphans(sdk.cma, contentTypes, sdk.locales.default, setProgress, { - maxCandidates: parameters.maxCandidates, - batchSize: parameters.batchSize, - untouchedOnly: parameters.untouchedOnly, - includeEntries: scanEntries, - includeAssets: scanAssets, - }); - setResults(outcome.results); - setTruncated(outcome.truncated); - } catch (error) { - // CMA calls run through the app bridge in the parent window, so a - // failure here may leave no trace in the iframe's network tab; this - // log is the only place the underlying error is visible. - console.error('Orphan scan failed:', error); - Notification.error('The scan failed. Please try again.'); - } finally { - setScanning(false); - setProgress(null); - } - }, [sdk, contentTypes, parameters, scanEntries, scanAssets]); + const runScan = useCallback( + async (criterion: ScanCriterion) => { + setActiveScan(criterion); + // A new scan replaces this tab's results and invalidates its + // selection; the tab's filter and the other tab's cache are untouched. + patchTab(criterion, SCAN_RESET); + try { + const outcome = await findOrphans(sdk.cma, contentTypes, sdk.locales.default, setProgress, { + criterion, + maxCandidates: parameters.maxCandidates, + batchSize: parameters.batchSize, + untouchedOnly: parameters.untouchedOnly, + includeEntries: scanEntries, + includeAssets: scanAssets, + }); + patchTab(criterion, { results: outcome.results, truncated: outcome.truncated }); + } catch (error) { + // CMA calls run through the app bridge in the parent window, so a + // failure here may leave no trace in the iframe's network tab; this + // log is the only place the underlying error is visible. + console.error('Orphan scan failed:', error); + Notification.error('The scan failed. Please try again.'); + } finally { + setActiveScan(null); + setProgress(null); + } + }, + [sdk, contentTypes, parameters, scanEntries, scanAssets, patchTab] + ); const openResult = useCallback( (result: OrphanResult) => { @@ -158,40 +248,73 @@ const Page = () => { [sdk] ); - const toggleResult = useCallback((resultId: string) => { - setSelectedIds((previous) => - previous.includes(resultId) - ? previous.filter((id) => id !== resultId) - : [...previous, resultId] - ); - }, []); - - const toggleKind = useCallback( - // Kind-scoped select-all with checkbox semantics: toggling on selects - // every result of the kind (keeping any other-kind selection), toggling - // off deselects exactly those again. "Archive all entries" is one click - // and never sweeps assets along. - (kind: OrphanKind) => { - const kindIds = (results ?? []) - .filter((result) => result.kind === kind) - .map((result) => result.id); - setSelectedIds((previous) => { - const allOfKindSelected = - kindIds.length > 0 && kindIds.every((id) => previous.includes(id)); - const withoutKind = previous.filter((id) => !kindIds.includes(id)); - return allOfKindSelected ? withoutKind : [...withoutKind, ...kindIds]; + const toggleResult = useCallback( + (resultId: string) => { + patchTab(activeTab, { + selectedIds: currentTab.selectedIds.includes(resultId) + ? currentTab.selectedIds.filter((id) => id !== resultId) + : [...currentTab.selectedIds, resultId], }); }, - [results] + [activeTab, currentTab.selectedIds, patchTab] ); const toggleAll = useCallback(() => { // Header checkbox: everything selected clears the selection, anything - // less (none or partial) selects all visible results. - setSelectedIds((previous) => - results !== null && previous.length < results.length ? results.map((result) => result.id) : [] - ); - }, [results]); + // less (none or partial) selects all displayed results. + patchTab(activeTab, { + selectedIds: + currentTab.selectedIds.length < displayedResults.length + ? displayedResults.map((result) => result.id) + : [], + }); + }, [activeTab, currentTab, displayedResults, patchTab]); + + const setScope = useCallback( + // The scope checkboxes are dual-purpose: they decide what the next scan + // fetches AND live-filter already-scanned results. Unchecking a kind + // hides its rows, so — like every view change — it must deselect them, + // in both tabs, since the scope is shared. + (kind: OrphanKind, checked: boolean) => { + if (kind === 'entry') { + setScanEntries(checked); + } else { + setScanAssets(checked); + } + if (!checked) { + setTabStates((previous) => { + const prune = (tab: TabState): TabState => ({ + ...tab, + selectedIds: tab.selectedIds.filter((id) => + (tab.results ?? []).some((result) => result.id === id && result.kind !== kind) + ), + }); + return { untitled: prune(previous.untitled), unreferenced: prune(previous.unreferenced) }; + }); + } + }, + [] + ); + + const setNeverEditedFilter = useCallback( + // The two view pills act like radios: clicking the already-active one + // is a no-op, never an off-switch, so the current view is always the + // pressed pill. + (neverEditedOnly: boolean) => { + if (neverEditedOnly === currentTab.neverEditedOnly) return; + patchTab(activeTab, { + neverEditedOnly, + // Narrowing the view must narrow the selection too — otherwise + // "Archive selected" could archive rows the user can no longer see. + selectedIds: neverEditedOnly + ? currentTab.selectedIds.filter((id) => + (currentTab.results ?? []).some((result) => result.id === id && result.neverEdited) + ) + : currentTab.selectedIds, + }); + }, + [activeTab, currentTab, patchTab] + ); const runArchive = useCallback(async () => { setConfirmArchiveOpen(false); @@ -199,8 +322,8 @@ const Page = () => { try { // The archive endpoint differs per kind, so selection ids are resolved // back to results to recover whether each is an entry or an asset. - const targets = (results ?? []) - .filter((result) => selectedIds.includes(result.id)) + const targets = (currentTab.results ?? []) + .filter((result) => currentTab.selectedIds.includes(result.id)) .map((result) => ({ id: result.id, kind: result.kind })); const outcome = await archiveOrphans( sdk.cma, @@ -210,10 +333,11 @@ const Page = () => { ); // Archived items leave the result list; failed ones stay visible and // selected so the user can retry them. - setResults( - (previous) => previous?.filter((result) => !outcome.archivedIds.includes(result.id)) ?? null - ); - setSelectedIds(outcome.failedIds); + patchTab(activeTab, { + results: + currentTab.results?.filter((result) => !outcome.archivedIds.includes(result.id)) ?? null, + selectedIds: outcome.failedIds, + }); if (outcome.archivedIds.length > 0) { Notification.success( `Archived ${outcome.archivedIds.length} ${pluralize(outcome.archivedIds.length)}.` @@ -232,71 +356,74 @@ const Page = () => { setArchiving(false); setArchiveProgress(null); } - }, [sdk, selectedIds, parameters, results]); + }, [sdk, activeTab, currentTab, parameters, patchTab]); - return ( - // Full width: page apps get the whole main column, and the results table - // benefits from every pixel of it. - - - - - - Find orphaned entries - - {/* The why-does-this-app-exist rationale lives in a tooltip so - the header stays scannable; an IconButton (not a bare icon) - keeps it reachable by keyboard and screen readers. */} - - } - aria-label="Why this scan exists" - testId="scan-info" - /> - - - - Finds untitled draft entries and media assets — usually created by accident from a - reference field. - {/* Whether the never-edited filter applies is decided in the app - configuration, so the description must reflect the actual scan. */} - {parameters.untouchedOnly && ' Only items never edited after creation are included.'} - - + // Tab labels carry the cached result count so users can see at a glance + // which scans have run and what they found without switching tabs. The + // criterion explanations live inside each panel (always visible above the + // scan button), so the labels stay clean. + const tabLabel = (criterion: ScanCriterion) => { + const { results } = tabStates[criterion]; + return `${CRITERIA[criterion].tabLabel}${results !== null ? ` (${results.length})` : ''}`; + }; + /** + * One criterion's whole panel: scan controls plus that tab's cached + * results. Both panels share the scope checkboxes (per-run setting) and + * the archive machinery; only the state behind them is per-tab. + */ + const renderPanel = (criterion: ScanCriterion) => { + const tab = tabStates[criterion]; + const chrome = CRITERIA[criterion]; + // Computed per criterion (not from the activeTab-level memos): React + // evaluates both panels' JSX even though only one is mounted. The scope + // checkboxes filter first, the never-edited view second, so the view + // pills count within the current scope. + const scopeVisible = (tab.results ?? []).filter((result) => + result.kind === 'entry' ? scanEntries : scanAssets + ); + const displayed = scopeVisible.filter((result) => !tab.neverEditedOnly || result.neverEdited); + const displayedCounts = countByKind(displayed); + const neverEditedCount = scopeVisible.filter((result) => result.neverEdited).length; + return ( + + {/* The criterion's full explanation lives here, inside its tab; the + page subtitle stays general and the tab-label tooltips carry the + short form. */} + {/* The never-edited distinction is no longer part of the scan — it + is the visible "Never edited" filter on the results, so the + description does not need to mention configuration state. */} + + {chrome.description} + - {/* Per-run scope: the entry fan-out (one query per content type) - is the slow part of a scan, so these let a user run a quick - assets-only pass — or skip assets they do not care about. */} + {/* Dual-purpose scope: decides what the next scan fetches AND + live-filters already-scanned results (the entry fan-out is the + slow part of a scan, so an assets-only pass is quick). */} setScanEntries(event.target.checked)} + onChange={(event) => setScope('entry', event.target.checked)} isDisabled={busy} testId="scope-entries"> Entries setScanAssets(event.target.checked)} + onChange={(event) => setScope('asset', event.target.checked)} isDisabled={busy} testId="scope-assets"> Media assets - {scanning && progress && ( + {activeScan === criterion && progress && ( {progressLabel(progress)} @@ -304,14 +431,9 @@ const Page = () => { )} - {/* Separator between the scan controls and the results area, which - below this line always renders something: placeholder, skeleton, - empty note, or the results table. */} - - - {results === null && !scanning && ( - // Fresh-open state: without this the area below the separator would - // be blank until the first scan, which reads as something missing. + {tab.results === null && activeScan !== criterion && ( + // Fresh tab: without this the area below the controls would be + // blank until the first scan, which reads as something missing. { No scan has run yet - Click “Scan for orphans” to check every content type and the media library for - untitled drafts. + Run a scan to check every content type and the media library for orphaned drafts. )} - {scanning && ( + {activeScan === criterion && ( // Placeholder rows keep the results area occupied while the scan // runs; the row count is arbitrary, it only suggests a table. @@ -341,52 +462,73 @@ const Page = () => {
)} - {truncated && ( + {tab.truncated && ( The scan stopped after {parameters.maxCandidates} draft items. Archive or clean up some of the results and scan again, or raise the limit in the app configuration. )} - {results !== null && - (results.length === 0 ? ( + {tab.results !== null && + (tab.results.length === 0 ? ( - No orphans found — every scanned draft has a title. + {chrome.emptyNote} ) : ( <> - {describeCounts(resultCounts)} found - {selectedIds.length > 0 ? ` — ${selectedIds.length} selected` : ''} + {describeCounts(displayedCounts)} found + {tab.selectedIds.length > 0 ? ` — ${tab.selectedIds.length} selected` : ''} - {/* Kind-scoped select-all, only useful (and only shown) - when the results actually mix entries and assets. The - pressed state mirrors the real selection, so the - buttons also read as "everything of this kind is - selected" and toggle off to undo exactly that. */} - {resultCounts.entries > 0 && resultCounts.assets > 0 && ( + {/* The scan is broad; these two pills switch between the + broad view and the stricter never-edited one. Exactly + one is always pressed, and both carry their counts, so + the current view — and what clicking the other pill + does — is never ambiguous. Untitled tab only: edit + history is a junk signal for untitled drafts, but says + nothing about whether something is referenced. */} + {criterion === 'untitled' && ( - Select all + Show toggleKind('entry')} + isActive={!tab.neverEditedOnly} + onToggle={() => setNeverEditedFilter(false)} isDisabled={busy} - testId="select-entries"> - Entries ({resultCounts.entries}) - - toggleKind('asset')} - isDisabled={busy} - testId="select-assets"> - Assets ({resultCounts.assets}) + testId="filter-all"> + All ({scopeVisible.length}) + {/* "Never edited" is a term of art (sys.version === 1), + so it gets a tooltip — including the archive gotcha: + unarchiving counts as an edit, so restored orphans + only show under "All". */} + + setNeverEditedFilter(true)} + isDisabled={busy} + testId="filter-never-edited"> + {/* Trailing info icon as the there-is-a-tooltip cue; + ToggleButton's icon prop is leading-only, so it + rides along in the children. */} + + Never edited ({neverEditedCount}) + + + + )} + {/* No kind-scoped select-all here: the scope checkboxes + above already hide (and deselect) a kind, so "select + only entries" is uncheck-assets + select-all, and the + archive confirmation still itemizes by kind. */} {archiving && archiveProgress && ( @@ -397,75 +539,87 @@ const Page = () => { )} - {/* Archiving reads as scary-destructive next to a negative - button; this spells out that it is reversible and links - straight to where actual deletion lives. A click - Popover, not a Tooltip: a hover tooltip closes before - its links can be clicked. */} - setArchiveInfoOpen(false)} - placement="top-end"> - - } - aria-label="What archiving does" - onClick={() => setArchiveInfoOpen((previous) => !previous)} - testId="archive-info" - /> - - - - - Archiving is not deleting: archived items just leave the content list and - media library, and can be unarchived at any time. - - - To delete them permanently, open the{' '} - - archived entries - {' '} - or{' '} - - archived assets - {' '} - list (opens in a new tab), select the items, and delete them there. - - - - + button; the tooltip says it is reversible, and the + confirmation dialog carries the deep links to where + actual deletion lives. */} + + +
- + {displayed.length === 0 ? ( + // Filters are hiding every result: say which one, instead + // of showing an empty table — the control to widen the view + // is right above. + + {scopeVisible.length === 0 + ? `All ${tab.results.length} ${pluralize( + tab.results.length + )} are hidden by the Entries / Media assets checkboxes above.` + : `All ${scopeVisible.length} ${pluralize( + scopeVisible.length + )} in scope were edited after creation — switch the view above to “All” to show them.`} + + ) : ( + + )} ))}
+ ); + }; + + return ( + // Full width: page apps get the whole main column, and the results table + // benefits from every pixel of it. + + + + + Find orphaned entries + + + Finds orphaned draft entries and media assets, so they can be reviewed and archived from + one place. Each tab covers a different definition of “orphaned”. + + + + {/* Each criterion is a tab holding its own cached results, so + running one scan, checking the other, and switching back never + re-runs anything. */} + setActiveTab(tab as ScanCriterion)}> + + + {tabLabel('untitled')} + + + {tabLabel('unreferenced')} + + + {renderPanel('untitled')} + {renderPanel('unreferenced')} + + { // the button before anything is archived. confirmLabel={`Archive ${describeCounts(selectedCounts)}`} cancelLabel="Cancel"> - + The selected {describeCounts(selectedCounts)} will be archived and disappear from the content list and media library. Archiving is reversible: anything archived can be unarchived from its editor at any time. + + To delete archived items permanently, open the{' '} + + archived entries + {' '} + or{' '} + + archived assets + {' '} + list (opens in a new tab), select the items, and delete them there. + ); diff --git a/apps/find-orphans/src/locations/Page/types.ts b/apps/find-orphans/src/locations/Page/types.ts index 98a31521ce..bb527c4092 100644 --- a/apps/find-orphans/src/locations/Page/types.ts +++ b/apps/find-orphans/src/locations/Page/types.ts @@ -5,6 +5,13 @@ export type CmaClient = PageAppSDK['cma']; /** Which CMA entity a result is; decides how it is previewed and archived. */ export type OrphanKind = 'entry' | 'asset'; +/** + * What "orphan" means for a scan — untitled drafts (created by accident and + * abandoned) or unreferenced drafts (no entry links to them). One criterion + * per scan, chosen on the page, so results stay unambiguous. + */ +export type ScanCriterion = 'untitled' | 'unreferenced'; + /** * A draft entry or media asset flagged by the scan: its title has no value in * the default locale, which is the signature of an entity created by mistake @@ -18,12 +25,25 @@ export interface OrphanResult { /** Content type name for entries, "Asset" for media-library assets. */ typeName: string; /** - * Creation date, not last-updated: orphans were (by default) never edited - * after creation, so "when was this created" is the honest column — and - * with the untouched filter off, an edit date would understate the age of - * the mistake. + * The item's title in the default locale, or undefined when it has none + * (always undefined under the untitled criterion — that is what it finds; + * unreferenced results usually have real titles). The UI renders undefined + * as the editor's "Untitled" placeholder. + */ + title: string | undefined; + /** + * Creation date, not last-updated: "when was this created" is what + * identifies the mistake, and an edit date would understate its age. */ createdAt: string; + /** + * True when the item was never saved after creation (sys.version === 1; + * valid because published/archived states are excluded by the draft + * queries). Feeds the "Never edited" results filter. Note that archiving + * and unarchiving an item bumps its version, so restored orphans read as + * edited. + */ + neverEdited: boolean; /** * Display name of whoever created the item, resolved from sys.createdBy * after the scan — "Jane Doe", "App" for app/automation identities, or diff --git a/apps/find-orphans/src/locations/Page/utils/orphanFinder.ts b/apps/find-orphans/src/locations/Page/utils/orphanFinder.ts index 4187be82f3..6670151cd9 100644 --- a/apps/find-orphans/src/locations/Page/utils/orphanFinder.ts +++ b/apps/find-orphans/src/locations/Page/utils/orphanFinder.ts @@ -1,5 +1,12 @@ import { AssetProps, ContentTypeProps, EntryProps, QueryOptions } from 'contentful-management'; -import { CmaClient, OrphanResult, ScanOutcome, ScanProgress } from '../types'; +import { + CmaClient, + OrphanKind, + OrphanResult, + ScanCriterion, + ScanOutcome, + ScanProgress, +} from '../types'; import { CONTENT_TYPE_PAGE_LIMIT, PAGE_LIMIT, @@ -9,15 +16,15 @@ import { /** * Scan settings. The limits and the untouched filter come from installation - * parameters (see src/parameters.ts); the two scope flags are a per-run - * choice made on the page. + * parameters (see src/parameters.ts); the criterion and the two scope flags + * are a per-run choice made on the page. */ export interface ScanOptions { + /** What "orphan" means for this scan: untitled or unreferenced drafts. */ + criterion: ScanCriterion; maxCandidates: number; /** Concurrent CMA entry queries while scanning. */ batchSize: number; - /** Only flag drafts never saved after creation (sys.version === 1). */ - untouchedOnly: boolean; /** Scan entries of all content types. */ includeEntries: boolean; /** Scan media-library assets. */ @@ -263,16 +270,72 @@ const fetchDraftCandidates = async ( }; /** - * Scans the environment for orphaned drafts: never published, not archived, - * and with no title value in the default locale. For entries the title is - * the content type's display field, and content types whose display field is - * not a text field are skipped — their entries cannot be missing a title. - * Assets always have a title field, so the whole media library is one scan - * step. + * One draft candidate, flattened to what both criteria and the result + * mapping need, so entries and assets flow through the same filters. + */ +interface Candidate { + kind: OrphanKind; + id: string; + typeName: string; + createdAt: string; + title: string | undefined; + version: number; + creator: CreatorLink | undefined; +} + +/** + * Keeps only candidates that no entry links to. Referenced-ness cannot be + * queried in bulk — links_to_entry / links_to_asset filter by ONE target id + * per query — so this is one count request per candidate (limit: 0 returns + * just the total), batched `batchSize` at a time. This makes the + * unreferenced criterion by far the more expensive scan: budget one request + * per draft, on top of the candidate paging. + */ +const filterUnreferenced = async ( + cma: CmaClient, + candidates: Candidate[], + batchSize: number, + onProgress: (progress: ScanProgress) => void +): Promise => { + const unreferenced: Candidate[] = []; + for (let i = 0; i < candidates.length; i += batchSize) { + const batch = candidates.slice(i, i + batchSize); + onProgress({ + current: Math.min(i + batch.length, candidates.length), + total: candidates.length, + stepNames: ['references'], + }); + const totals = await Promise.all( + batch.map((candidate) => + cma.entry + .getMany({ + query: { + [candidate.kind === 'entry' ? 'links_to_entry' : 'links_to_asset']: candidate.id, + limit: 0, + }, + }) + .then((response) => response.total) + ) + ); + unreferenced.push(...batch.filter((_, index) => totals[index] === 0)); + } + return unreferenced; +}; + +/** + * Scans the environment for orphaned drafts (never published, not archived) + * matching the chosen criterion: + * + * - "untitled": no title value in the default locale. For entries the title + * is the content type's display field, and content types whose display + * field is not a text field are skipped — their entries cannot be missing + * a title. + * - "unreferenced": no entry links to the item. Every content type + * participates (any entry can be a link target) and titles are irrelevant. * - * With `untouchedOnly` set, an untitled draft is only flagged when it was - * never saved after creation, which filters out work-in-progress drafts that - * simply have not been given a title yet. + * Both scans are broad with respect to edit history: every result carries a + * `neverEdited` flag and the UI filters on it visibly instead of the scan + * excluding anything silently. */ export const findOrphans = async ( cma: CmaClient, @@ -282,7 +345,9 @@ export const findOrphans = async ( options: ScanOptions ): Promise => { const scannableTypes = options.includeEntries - ? contentTypes.filter((ct) => getTextDisplayFieldId(ct) !== undefined) + ? options.criterion === 'untitled' + ? contentTypes.filter((ct) => getTextDisplayFieldId(ct) !== undefined) + : contentTypes : []; const progressTotal = scannableTypes.length + (options.includeAssets ? 1 : 0); @@ -310,35 +375,50 @@ export const findOrphans = async ( } } - // The CMA bumps sys.version on every write (updates, but also publish, - // unpublish, archive and unarchive — and for assets, file processing). - // Publish and archive states are already excluded by the draft queries, so - // on a candidate version 1 can only mean it was never saved after - // creation. This check must stay client-side: sys.version is not a - // queryable attribute in CMA searches. - const passesUntouchedFilter = (version: number) => !options.untouchedOnly || version === 1; + // Entries and assets are flattened into one candidate list so both + // criteria filter the same shape. Titles ride along: the untitled + // criterion filters on them, the unreferenced one displays them (an + // unreferenced entry usually has a real title). + const allCandidates: Candidate[] = [ + ...candidates.map(({ entry, contentType }) => ({ + kind: 'entry' as const, + id: entry.sys.id, + typeName: contentType.name, + createdAt: entry.sys.createdAt, + title: getEntryTitle(entry, contentType, defaultLocale), + version: entry.sys.version, + creator: toCreatorLink(entry.sys), + })), + ...assetCandidates.map((asset) => ({ + kind: 'asset' as const, + id: asset.sys.id, + typeName: 'Asset', + createdAt: asset.sys.createdAt, + title: getAssetTitle(asset, defaultLocale), + version: asset.sys.version, + creator: toCreatorLink(asset.sys), + })), + ]; - const entryOrphans = candidates.filter( - ({ entry, contentType }) => - getEntryTitle(entry, contentType, defaultLocale) === undefined && - passesUntouchedFilter(entry.sys.version) - ); - const assetOrphans = assetCandidates.filter( - (asset) => - getAssetTitle(asset, defaultLocale) === undefined && passesUntouchedFilter(asset.sys.version) - ); + // The scan itself is deliberately broad — the "never edited" distinction + // is carried on each result (see neverEdited below) and applied as a + // visible filter in the UI, never as a silent scan-time exclusion. + let orphans: Candidate[]; + if (options.criterion === 'unreferenced') { + orphans = await filterUnreferenced(cma, allCandidates, options.batchSize, onProgress); + } else { + orphans = allCandidates.filter((candidate) => candidate.title === undefined); + } // sys.createdBy only carries a link, so creators are resolved to names in // one batched users lookup per USER_PAGE_LIMIT unique ids — the "who // created this" column is often the fastest way to find the person or - // workflow that produces orphans. - const creatorLinks = [ - ...entryOrphans.map(({ entry }) => toCreatorLink(entry.sys)), - ...assetOrphans.map((asset) => toCreatorLink(asset.sys)), - ]; + // workflow that produces orphans. Resolution runs after filtering so only + // actual orphans' creators are looked up. const userIds = [ ...new Set( - creatorLinks + orphans + .map((orphan) => orphan.creator) .filter((link): link is CreatorLink => link !== undefined && link.linkType === 'User') .map((link) => link.id) ), @@ -351,22 +431,21 @@ export const findOrphans = async ( return (link && names[link.id]) || 'Unknown user'; }; - const results: OrphanResult[] = [ - ...entryOrphans.map(({ entry, contentType }) => ({ - kind: 'entry' as const, - id: entry.sys.id, - typeName: contentType.name, - createdAt: entry.sys.createdAt, - createdBy: creatorName(toCreatorLink(entry.sys)), - })), - ...assetOrphans.map((asset) => ({ - kind: 'asset' as const, - id: asset.sys.id, - typeName: 'Asset', - createdAt: asset.sys.createdAt, - createdBy: creatorName(toCreatorLink(asset.sys)), - })), - ]; + const results: OrphanResult[] = orphans.map((orphan) => ({ + kind: orphan.kind, + id: orphan.id, + typeName: orphan.typeName, + title: orphan.title, + createdAt: orphan.createdAt, + createdBy: creatorName(orphan.creator), + // The CMA bumps sys.version on every write (updates, but also publish, + // unpublish, archive and unarchive — and for assets, file processing). + // Publish and archive states are excluded by the draft queries, so + // version 1 can only mean the item was never saved after creation. The + // check must be client-side anyway: sys.version is not a queryable + // attribute in CMA searches. + neverEdited: orphan.version === 1, + })); return { results, truncated }; }; diff --git a/apps/find-orphans/src/parameters.ts b/apps/find-orphans/src/parameters.ts index 330631e12b..4fa86f5f2e 100644 --- a/apps/find-orphans/src/parameters.ts +++ b/apps/find-orphans/src/parameters.ts @@ -6,10 +6,10 @@ export interface AppInstallationParameters { /** Concurrent CMA requests while scanning and archiving. */ batchSize: number; /** - * When true, only flag drafts that were never saved after creation - * (sys.version === 1). Filters out untitled drafts someone has actually - * worked on, at the cost of missing abandoned drafts that got one stray - * edit. + * Default state of the untitled tab's "Never edited" results filter (the + * unreferenced tab always starts with it off). The scan itself is always + * broad; this only decides whether results open pre-narrowed to drafts + * never saved after creation (sys.version === 1). */ untouchedOnly: boolean; } diff --git a/apps/find-orphans/test/locations/ConfigScreen.test.tsx b/apps/find-orphans/test/locations/ConfigScreen.test.tsx index f2680d6777..703b97961e 100644 --- a/apps/find-orphans/test/locations/ConfigScreen.test.tsx +++ b/apps/find-orphans/test/locations/ConfigScreen.test.tsx @@ -67,7 +67,7 @@ describe('ConfigScreen', () => { await waitFor(() => expect(screen.getByLabelText(/Maximum entries per scan/)).toHaveValue(100)); // A parameter missing from storage falls back to its default. expect(screen.getByLabelText(/Concurrent API requests/)).toHaveValue(5); - expect(screen.getByLabelText(/Only include entries that were never edited/)).not.toBeChecked(); + expect(screen.getByLabelText(/Start results filtered to never-edited items/)).not.toBeChecked(); }); it('saves the toggled never-edited filter', async () => { @@ -77,7 +77,7 @@ describe('ConfigScreen', () => { render(); await waitFor(() => expect(sdk.app.setReady).toHaveBeenCalled()); - const checkbox = screen.getByLabelText(/Only include entries that were never edited/); + const checkbox = screen.getByLabelText(/Start results filtered to never-edited items/); expect(checkbox).toBeChecked(); fireEvent.click(checkbox); diff --git a/apps/find-orphans/test/locations/Page.test.tsx b/apps/find-orphans/test/locations/Page.test.tsx index bbc42913b7..fe7ca974e4 100644 --- a/apps/find-orphans/test/locations/Page.test.tsx +++ b/apps/find-orphans/test/locations/Page.test.tsx @@ -18,9 +18,15 @@ vi.mock('@contentful/react-apps-toolkit', () => ({ useSDK: () => mocks.sdk, })); -const scan = async () => { - await waitFor(() => expect(screen.getByTestId('scan-button')).toBeEnabled()); - fireEvent.click(screen.getByTestId('scan-button')); +const scan = async (button = 'scan-untitled-button') => { + await waitFor(() => expect(screen.getByTestId(button)).toBeEnabled()); + fireEvent.click(screen.getByTestId(button)); +}; + +// Radix-based f36 Tabs activate on mousedown, not click. +const switchTab = (tabTestId: string) => { + fireEvent.mouseDown(screen.getByTestId(tabTestId)); + fireEvent.click(screen.getByTestId(tabTestId)); }; describe('Page', () => { @@ -35,11 +41,12 @@ describe('Page', () => { render(); expect(screen.getByText('Find orphaned entries')).toBeInTheDocument(); - expect(screen.getByTestId('scan-button')).toBeInTheDocument(); - // The why-does-this-app-exist explanation moved from an always-visible - // Note into a tooltip behind this info button. - expect(screen.getByTestId('scan-info')).toBeInTheDocument(); - await waitFor(() => expect(screen.getByTestId('scan-button')).toBeEnabled()); + // Each criterion is a tab; the active tab's panel holds its scan button + // with the criterion's description visible above it. + expect(screen.getByTestId('tab-untitled')).toBeInTheDocument(); + expect(screen.getByTestId('tab-unreferenced')).toBeInTheDocument(); + expect(screen.getByTestId('scan-untitled-button')).toBeInTheDocument(); + await waitFor(() => expect(screen.getByTestId('scan-untitled-button')).toBeEnabled()); }); it('scans, lists orphaned entries, and opens one on demand', async () => { @@ -57,7 +64,8 @@ describe('Page', () => { await waitFor(() => expect(screen.getByTestId('orphan-table')).toBeInTheDocument()); expect(screen.getByTestId('result-count')).toHaveTextContent('1 entry found'); - expect(screen.getByText('Untitled')).toBeInTheDocument(); + // Scoped to the table: "Untitled" also appears as a criterion radio label. + expect(within(screen.getByTestId('orphan-table')).getByText('Untitled')).toBeInTheDocument(); expect(screen.getByText('Article')).toBeInTheDocument(); // The creator column resolves sys.createdBy to the user's name. expect(screen.getByText('Jane Doe')).toBeInTheDocument(); @@ -90,7 +98,7 @@ describe('Page', () => { expect(sdk.navigator.openEntry).not.toHaveBeenCalled(); }); - it('quick-selects one kind and itemizes the archive confirmation', async () => { + it('archives one kind via scope filtering and itemizes the confirmation', async () => { const { cma } = createMockCma({ contentTypes: [mockArticleContentType], entriesByContentType: { @@ -104,18 +112,11 @@ describe('Page', () => { await scan(); await waitFor(() => expect(screen.getByTestId('orphan-table')).toBeInTheDocument()); - // The kind toggles select every result of that kind, so archiving all - // entries never sweeps assets along. - fireEvent.click(screen.getByTestId('select-entries')); + // "Archive only entries" = hide assets via the scope checkbox, then + // select-all: the hidden asset can never be swept along. + fireEvent.click(screen.getByTestId('scope-assets')); + fireEvent.click(screen.getByTestId('select-all')); expect(screen.getByTestId('result-count')).toHaveTextContent('2 selected'); - expect( - within(screen.getByTestId('orphan-row-asset-a')).getByRole('checkbox') - ).not.toBeChecked(); - - // Toggling the same kind off deselects exactly those again. - fireEvent.click(screen.getByTestId('select-entries')); - expect(screen.getByTestId('result-count')).not.toHaveTextContent('selected'); - fireEvent.click(screen.getByTestId('select-entries')); // The confirmation names kinds, not generic items. fireEvent.click(screen.getByTestId('archive-button')); @@ -123,11 +124,125 @@ describe('Page', () => { // Select-all on a mixed list itemizes both kinds. fireEvent.click(screen.getByText('Cancel')); + fireEvent.click(screen.getByTestId('scope-assets')); fireEvent.click(screen.getByTestId('select-all')); fireEvent.click(screen.getByTestId('archive-button')); expect(await screen.findByText('Archive 2 entries and 1 asset')).toBeInTheDocument(); }); + it('finds unreferenced drafts with their real titles when that criterion is selected', async () => { + // Titled, so invisible to the untitled scan — but nothing links to it. + const deadPage = makeMockEntry('dead-page', 'article', { + title: { 'en-US': 'Old landing page' }, + }); + const linked = makeMockEntry('linked', 'article', { title: { 'en-US': 'Linked page' } }); + const { cma } = createMockCma({ + contentTypes: [mockArticleContentType], + entriesByContentType: { article: [deadPage, linked] }, + referenceCounts: { linked: 2 }, + }); + mocks.sdk = createMockSdk(cma); + + render(); + switchTab('tab-unreferenced'); + await scan('scan-unreferenced-button'); + + await waitFor(() => expect(screen.getByTestId('orphan-table')).toBeInTheDocument()); + expect(screen.getByTestId('result-count')).toHaveTextContent('1 entry found'); + // Unreferenced results show their real titles, not the Untitled placeholder. + expect(screen.getByText('Old landing page')).toBeInTheDocument(); + expect(screen.queryByTestId('orphan-row-linked')).not.toBeInTheDocument(); + // Edit history says nothing about referenced-ness, so this tab has no + // never-edited view switch. + expect(screen.queryByTestId('filter-never-edited')).not.toBeInTheDocument(); + expect(screen.queryByTestId('filter-all')).not.toBeInTheDocument(); + }); + + it('filters results to never-edited items with a visible toggle', async () => { + const untouched = makeMockEntry('untouched', 'article'); + const edited = makeMockEntry('edited', 'article', {}, '2026-06-01T00:00:00Z', 4); + const { cma } = createMockCma({ + contentTypes: [mockArticleContentType], + entriesByContentType: { article: [untouched, edited] }, + }); + mocks.sdk = createMockSdk(cma); + + render(); + await scan(); + await waitFor(() => expect(screen.getByTestId('orphan-table')).toBeInTheDocument()); + + // untouchedOnly defaults on, so the never-edited view starts active — + // the edited draft is hidden, but visibly: both pills carry counts. + expect(screen.getByTestId('result-count')).toHaveTextContent('1 entry found'); + expect(screen.getByTestId('filter-all')).toHaveTextContent('All (2)'); + expect(screen.getByTestId('filter-never-edited')).toHaveTextContent('Never edited (1)'); + expect(screen.queryByTestId('orphan-row-edited')).not.toBeInTheDocument(); + + // Switching to "All" broadens instantly — no re-scan. + fireEvent.click(screen.getByTestId('filter-all')); + expect(screen.getByTestId('result-count')).toHaveTextContent('2 entries found'); + expect(screen.getByTestId('orphan-row-edited')).toBeInTheDocument(); + + // Re-narrowing drops selections of rows it hides, so "Archive selected" + // can never archive something invisible. + fireEvent.click(within(screen.getByTestId('orphan-row-edited')).getByRole('checkbox')); + expect(screen.getByTestId('result-count')).toHaveTextContent('1 selected'); + fireEvent.click(screen.getByTestId('filter-never-edited')); + expect(screen.getByTestId('result-count')).not.toHaveTextContent('selected'); + }); + + it('explains when the never-edited filter hides every result', async () => { + const edited = makeMockEntry('edited', 'article', {}, '2026-06-01T00:00:00Z', 4); + const { cma } = createMockCma({ + contentTypes: [mockArticleContentType], + entriesByContentType: { article: [edited] }, + }); + mocks.sdk = createMockSdk(cma); + + render(); + await scan(); + + // Everything found was edited, so with the never-edited view active the + // table gives way to an explanatory note — the view pills stay above it. + await waitFor(() => expect(screen.getByTestId('filtered-empty-note')).toBeInTheDocument()); + expect(screen.queryByTestId('orphan-table')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('filter-all')); + expect(screen.getByTestId('orphan-table')).toBeInTheDocument(); + expect(screen.queryByTestId('filtered-empty-note')).not.toBeInTheDocument(); + }); + + it('scope checkboxes filter cached results client-side after a scan', async () => { + const { cma, entryGetMany } = createMockCma({ + contentTypes: [mockArticleContentType], + entriesByContentType: { article: [makeMockEntry('orphan-entry', 'article')] }, + assets: [makeMockAsset('orphan-asset')], + }); + mocks.sdk = createMockSdk(cma); + + render(); + await scan(); + await waitFor(() => + expect(screen.getByTestId('result-count')).toHaveTextContent('1 entry and 1 asset found') + ); + fireEvent.click(screen.getByTestId('select-all')); + const scanQueries = entryGetMany.mock.calls.length; + + // Unchecking a scope hides that kind from the cached results instantly — + // no re-scan — and deselects the hidden rows. + fireEvent.click(screen.getByTestId('scope-assets')); + expect(screen.getByTestId('result-count')).toHaveTextContent('1 entry found — 1 selected'); + expect(screen.queryByTestId('orphan-row-orphan-asset')).not.toBeInTheDocument(); + expect(entryGetMany.mock.calls.length).toBe(scanQueries); + + // Rechecking brings the rows back (still deselected). + fireEvent.click(screen.getByTestId('scope-assets')); + expect(screen.getByTestId('orphan-row-orphan-asset')).toBeInTheDocument(); + expect( + within(screen.getByTestId('orphan-row-orphan-asset')).getByRole('checkbox') + ).not.toBeChecked(); + }); + it('scans only the checked scopes', async () => { const { cma, assetGetMany } = createMockCma({ contentTypes: [mockArticleContentType], @@ -146,9 +261,36 @@ describe('Page', () => { ); expect(assetGetMany).not.toHaveBeenCalled(); - // With both scopes off there is nothing to scan, so the button disables. + // With both scopes off there is nothing to scan, so the scan buttons + // disable — the scope setting is shared, so this holds on both tabs. fireEvent.click(screen.getByTestId('scope-entries')); - expect(screen.getByTestId('scan-button')).toBeDisabled(); + expect(screen.getByTestId('scan-untitled-button')).toBeDisabled(); + switchTab('tab-unreferenced'); + expect(screen.getByTestId('scan-unreferenced-button')).toBeDisabled(); + }); + + it('keeps each tab’s results when switching between tabs without re-scanning', async () => { + const { cma, entryGetMany } = createMockCma({ + contentTypes: [mockArticleContentType], + entriesByContentType: { article: [makeMockEntry('orphan-1', 'article')] }, + }); + mocks.sdk = createMockSdk(cma); + + render(); + await scan(); + await waitFor(() => expect(screen.getByTestId('orphan-table')).toBeInTheDocument()); + const scanQueries = entryGetMany.mock.calls.length; + + // The unreferenced tab has not scanned: it shows its own placeholder. + switchTab('tab-unreferenced'); + expect(screen.getByTestId('pre-scan-placeholder')).toBeInTheDocument(); + + // Switching back shows the cached untitled results without any new scan. + switchTab('tab-untitled'); + expect(screen.getByTestId('orphan-table')).toBeInTheDocument(); + expect(entryGetMany.mock.calls.length).toBe(scanQueries); + // The tab label carries the cached count. + expect(screen.getByTestId('tab-untitled')).toHaveTextContent('Untitled drafts (1)'); }); it('shows an empty state when nothing matches', async () => { @@ -187,24 +329,22 @@ describe('Page', () => { await scan(); await waitFor(() => expect(screen.getByTestId('orphan-table')).toBeInTheDocument()); - // The archive button only activates once something is selected, and the - // adjacent popover explains archive-vs-delete with deep links into the - // web app's archived views. + // The archive button only activates once something is selected. expect(screen.getByTestId('archive-button')).toBeDisabled(); - fireEvent.click(screen.getByTestId('archive-info')); + fireEvent.click(screen.getByTestId('select-all')); + expect(screen.getByTestId('archive-button')).toBeEnabled(); + + fireEvent.click(screen.getByTestId('archive-button')); + // The confirmation modal must be accepted before anything is archived, + // and it carries the deep links to the web app's archived views where + // permanent deletion lives. + expect(entryArchive).not.toHaveBeenCalled(); const archivedLink = await screen.findByTestId('archived-entries-link'); expect(archivedLink).toHaveAttribute( 'href', 'https://app.contentful.com/spaces/space-id/environments/master/views/entries?filters.0.key=__status&filters.0.op=&filters.0.val=archived' ); expect(archivedLink).toHaveAttribute('target', '_blank'); - fireEvent.click(screen.getByTestId('archive-info')); - fireEvent.click(screen.getByTestId('select-all')); - expect(screen.getByTestId('archive-button')).toBeEnabled(); - - fireEvent.click(screen.getByTestId('archive-button')); - // The confirmation modal must be accepted before anything is archived. - expect(entryArchive).not.toHaveBeenCalled(); fireEvent.click(await screen.findByText('Archive 2 entries')); await waitFor(() => expect(entryArchive).toHaveBeenCalledTimes(2)); diff --git a/apps/find-orphans/test/mocks/mockCma.ts b/apps/find-orphans/test/mocks/mockCma.ts index cd4d34e334..eb28bbd396 100644 --- a/apps/find-orphans/test/mocks/mockCma.ts +++ b/apps/find-orphans/test/mocks/mockCma.ts @@ -18,6 +18,11 @@ export interface MockCmaOptions { assets?: AssetProps[]; /** Space users returned for the creator-name lookup. */ users?: UserProps[]; + /** + * Incoming-reference totals for links_to_entry / links_to_asset count + * queries, keyed by target id. Ids absent here count as unreferenced. + */ + referenceCounts?: Record; /** Entry/asset ids whose archive call should reject. */ failArchiveIds?: string[]; } @@ -27,12 +32,18 @@ export const createMockCma = ({ entriesByContentType = {}, assets = [], users = [], + referenceCounts = {}, failArchiveIds = [], }: MockCmaOptions = {}) => { const contentTypeGetMany = vi.fn().mockResolvedValue(collection(contentTypes)); const entryGetMany = vi .fn() .mockImplementation(({ query }: { query: Record }) => { + // Reference-count queries (limit: 0) only read the collection total. + if ('links_to_entry' in query || 'links_to_asset' in query) { + const targetId = (query.links_to_entry ?? query.links_to_asset) as string; + return Promise.resolve(collection([], referenceCounts[targetId] ?? 0)); + } const entries = entriesByContentType[query.content_type as string] ?? []; if (query.skip && (query.skip as number) >= entries.length) { return Promise.resolve(collection([], entries.length)); diff --git a/apps/find-orphans/test/mocks/mockContentTypes.ts b/apps/find-orphans/test/mocks/mockContentTypes.ts index 0c24d98370..588b01228e 100644 --- a/apps/find-orphans/test/mocks/mockContentTypes.ts +++ b/apps/find-orphans/test/mocks/mockContentTypes.ts @@ -40,6 +40,30 @@ export const mockArticleContentType: ContentTypeProps = { ], }; +/** + * Content type with no display field configured at all — common for + * component types (banners, teasers) where nobody marks an entry title. + * Every entry of such a type renders as "Untitled" in Contentful. + */ +export const mockNoDisplayFieldContentType: ContentTypeProps = { + sys: baseSys('banner'), + name: 'Basic Promotional Banner', + description: '', + displayField: null as unknown as string, + fields: [ + { + id: 'cta', + name: 'Call to action', + type: 'Symbol', + localized: false, + required: false, + disabled: false, + omitted: false, + validations: [], + }, + ], +}; + /** Content type whose display field is not a text field (edge case: no scannable title). */ export const mockNumericDisplayContentType: ContentTypeProps = { sys: baseSys('counter'), diff --git a/apps/find-orphans/test/orphanFinder.test.ts b/apps/find-orphans/test/orphanFinder.test.ts index 2a5246b5c9..e0cc03b486 100644 --- a/apps/find-orphans/test/orphanFinder.test.ts +++ b/apps/find-orphans/test/orphanFinder.test.ts @@ -14,14 +14,15 @@ import { makeMockEntry, makeMockUser, mockArticleContentType, + mockNoDisplayFieldContentType, mockNumericDisplayContentType, } from './mocks'; const noProgress = vi.fn(); const options = { + criterion: 'untitled' as const, maxCandidates: 500, batchSize: 5, - untouchedOnly: true, includeEntries: true, includeAssets: true, }; @@ -203,9 +204,10 @@ describe('findOrphans', () => { expect(outcome.results[0].createdAt).toBe('2026-03-05T00:00:00Z'); }); - it('excludes untitled drafts that were edited after creation when untouchedOnly is set', async () => { - // sys.version increments on every save, so version > 1 means someone has - // worked on the item — likely a work-in-progress, not an orphan. + it('includes edited untitled drafts and marks never-edited ones with a flag', async () => { + // The scan is deliberately broad: edit history never excludes a result. + // sys.version increments on every save, so version 1 = never edited — + // carried as a flag for the UI's "Never edited" filter. const untouched = makeMockEntry('untouched', 'article'); const edited = makeMockEntry('edited', 'article', {}, '2026-06-01T00:00:00Z', 4); const { cma } = createMockCma({ @@ -215,22 +217,12 @@ describe('findOrphans', () => { const outcome = await findOrphans(cma, [mockArticleContentType], 'en-US', noProgress, options); - expect(outcome.results.map((r) => r.id)).toEqual(['untouched']); - }); - - it('includes edited untitled drafts when untouchedOnly is off', async () => { - const edited = makeMockEntry('edited', 'article', {}, '2026-06-01T00:00:00Z', 4); - const { cma } = createMockCma({ - entriesByContentType: { article: [edited] }, - assets: [makeMockAsset('touched-asset', undefined, '2026-06-01T00:00:00Z', 3)], - }); - - const outcome = await findOrphans(cma, [mockArticleContentType], 'en-US', noProgress, { - ...options, - untouchedOnly: false, + const flags = Object.fromEntries(outcome.results.map((r) => [r.id, r.neverEdited])); + expect(flags).toEqual({ + untouched: true, + edited: false, + 'touched-asset': false, }); - - expect(outcome.results.map((r) => r.id)).toEqual(['edited', 'touched-asset']); }); it('skips entries or assets when their scope is off', async () => { @@ -255,6 +247,58 @@ describe('findOrphans', () => { expect(entryGetMany).not.toHaveBeenCalled(); }); + it('finds untitled never-edited drafts under both criteria', async () => { + // Cross-criterion consistency: a version-1 untitled draft that nothing + // references is an orphan by either definition, so it must appear in + // both scans' results. + const orphan = makeMockEntry('orphan', 'article'); + const { cma } = createMockCma({ entriesByContentType: { article: [orphan] } }); + + const untitled = await findOrphans(cma, [mockArticleContentType], 'en-US', noProgress, options); + const unreferenced = await findOrphans(cma, [mockArticleContentType], 'en-US', noProgress, { + ...options, + criterion: 'unreferenced', + }); + + expect(untitled.results.map((r) => r.id)).toEqual(['orphan']); + expect(unreferenced.results.map((r) => r.id)).toEqual(['orphan']); + }); + + it('skips content types with no display field configured (unreferenced-scan-only territory)', async () => { + // Component types (banners, teasers) often have no entry title at all; + // their entries always render as "Untitled" in Contentful, but the + // untitled scan deliberately skips them — flagging every draft of such + // types would sweep in legitimate work-in-progress. They still surface + // in the unreferenced scan, which is why an "Untitled" row can appear + // there and not here. Pinned so the discrepancy is documented behavior. + const bannerDraft = makeMockEntry('banner-1', 'banner'); + const { cma, entryGetMany } = createMockCma({ + entriesByContentType: { banner: [bannerDraft] }, + }); + + const outcome = await findOrphans(cma, [mockNoDisplayFieldContentType], 'en-US', noProgress, { + ...options, + includeAssets: false, + }); + + expect(outcome.results).toHaveLength(0); + expect(entryGetMany).not.toHaveBeenCalled(); + + // The same type IS scanned under the unreferenced criterion. + const unreferenced = await findOrphans( + cma, + [mockNoDisplayFieldContentType], + 'en-US', + noProgress, + { + ...options, + criterion: 'unreferenced', + includeAssets: false, + } + ); + expect(unreferenced.results.map((r) => r.id)).toEqual(['banner-1']); + }); + it('skips content types without a text display field', async () => { // Entries of such types cannot be missing a title, so querying them // would only waste API calls. @@ -335,3 +379,83 @@ describe('findOrphans', () => { }); }); }); + +describe('findOrphans (unreferenced criterion)', () => { + const unreferencedOptions = { ...options, criterion: 'unreferenced' as const }; + + it('keeps only drafts that no entry links to, with their titles', async () => { + const linked = makeMockEntry('linked', 'article', { title: { 'en-US': 'Linked' } }); + const unlinked = makeMockEntry('unlinked', 'article', { title: { 'en-US': 'Unlinked' } }); + const { cma } = createMockCma({ + entriesByContentType: { article: [linked, unlinked] }, + assets: [makeMockAsset('used-asset', 'Hero'), makeMockAsset('unused-asset', 'Spare')], + referenceCounts: { linked: 3, 'used-asset': 1 }, + }); + + const outcome = await findOrphans( + cma, + [mockArticleContentType], + 'en-US', + noProgress, + unreferencedOptions + ); + + expect(outcome.results.map((r) => r.id).sort()).toEqual(['unlinked', 'unused-asset']); + // Unreferenced results keep their real titles for display. + expect(outcome.results.find((r) => r.id === 'unlinked')?.title).toBe('Unlinked'); + }); + + it('includes content types without a text display field', async () => { + // Any entry can be a link target, so the untitled-scan restriction to + // text display fields does not apply here. + const { cma } = createMockCma({ + entriesByContentType: { counter: [makeMockEntry('c1', 'counter')] }, + }); + + const outcome = await findOrphans(cma, [mockNumericDisplayContentType], 'en-US', noProgress, { + ...unreferencedOptions, + includeAssets: false, + }); + + expect(outcome.results.map((r) => r.id)).toEqual(['c1']); + }); + + it('includes edited drafts, carrying the neverEdited flag', async () => { + // An unreferenced entry is worth flagging no matter how often it was + // edited; the flag still rides along for the UI filter. + const edited = makeMockEntry( + 'edited', + 'article', + { title: { 'en-US': 'Edited' } }, + '2026-01-01T00:00:00Z', + 7 + ); + const { cma } = createMockCma({ entriesByContentType: { article: [edited] } }); + + const outcome = await findOrphans(cma, [mockArticleContentType], 'en-US', noProgress, { + ...unreferencedOptions, + includeAssets: false, + }); + + expect(outcome.results.map((r) => r.id)).toEqual(['edited']); + expect(outcome.results[0].neverEdited).toBe(false); + }); + + it('reports reference-count progress after the candidate phases', async () => { + const onProgress = vi.fn(); + const { cma } = createMockCma({ + entriesByContentType: { article: [makeMockEntry('e1', 'article')] }, + }); + + await findOrphans(cma, [mockArticleContentType], 'en-US', onProgress, { + ...unreferencedOptions, + includeAssets: false, + }); + + expect(onProgress).toHaveBeenLastCalledWith({ + current: 1, + total: 1, + stepNames: ['references'], + }); + }); +}); From 819d4a8b0e1d5e5c4948420e7cfa36f0f9bd5433 Mon Sep 17 00:00:00 2001 From: Shanon Place Date: Mon, 13 Jul 2026 20:54:49 -0700 Subject: [PATCH 5/8] More UI cleanup --- apps/find-orphans/AGENTS.md | 19 +++-- apps/find-orphans/README.md | 32 ++++++-- apps/find-orphans/package.json | 1 + .../scripts/update-app-parameters.mjs | 77 +++++++++++++++++++ .../src/locations/ConfigScreen.tsx | 31 +------- .../find-orphans/src/locations/Page/index.tsx | 52 ++++++++----- .../src/locations/Page/utils/constants.ts | 9 --- apps/find-orphans/src/parameters.ts | 19 +---- .../test/locations/ConfigScreen.test.tsx | 22 +----- .../find-orphans/test/locations/Page.test.tsx | 4 +- apps/find-orphans/test/parameters.test.ts | 14 ++-- 11 files changed, 164 insertions(+), 116 deletions(-) create mode 100644 apps/find-orphans/scripts/update-app-parameters.mjs diff --git a/apps/find-orphans/AGENTS.md b/apps/find-orphans/AGENTS.md index 59d9a11766..e8c00c27aa 100644 --- a/apps/find-orphans/AGENTS.md +++ b/apps/find-orphans/AGENTS.md @@ -13,9 +13,10 @@ criteria, each in its own tab with its own scan button and its own cached result Scans are broad: edit history NEVER excludes results at scan time. Every result carries a `neverEdited` flag (`sys.version === 1`) and the UNTITLED tab's results header has a segmented view switch — "Show: All (N) / Never edited (M)" ToggleButton pills, exactly one always -pressed, clicking the active pill is a no-op — with `untouchedOnly` setting its starting view. -The unreferenced tab has NO never-edited switch at all (decided 2026-07-13: edit history says -nothing about referenced-ness). Keep it this way: silent scan-time +pressed, clicking the active pill is a no-op — starting on Never edited (hardcoded; the +`untouchedOnly` parameter that once set this default is retired). The unreferenced tab has NO +never-edited switch at all (decided 2026-07-13: edit history says nothing about +referenced-ness). Keep it this way: silent scan-time exclusion was tried first and read as "the scan is broken" when rows appeared in one tab and not the other, and a single on/off toggle was tried next and read as ambiguous. @@ -44,10 +45,16 @@ runs with missing or invalid settings. |--------------|------|----------|---------|---------| | `maxCandidates` | Number | Yes | 500 | Hard cap on candidate entries per scan | | `batchSize` | Number | Yes | 5 (max 7) | Concurrent CMA requests (scan queries, archiving) | -| `untouchedOnly` | Boolean | Yes | true | Default state of the untitled tab's "Never edited" results filter (`sys.version === 1`) | -Parameter definitions on the app definition must use these exact IDs and types; the README -documents the full table (including descriptions) for the app definition setup. +Retired parameters (do not reintroduce): `defaultStaleDays`, `referenceBatchSize` (pre-2026-07-09 +design) and `untouchedOnly` (2026-07-13, briefly the never-edited default; retired because the +visible "All / Never edited" switch made an admin-level default pointless). Old installations +may still store their values — `resolveParameters()` ignores unknown keys. + +Parameter definitions on the app definition must use these exact IDs and types. They are +scripted: `npm run update-app-parameters` (scripts/update-app-parameters.mjs) replaces the app +definition's installation parameters via the CMA — keep that script, `src/parameters.ts`, and +the README table in sync when parameters change. ## Key Dependencies diff --git a/apps/find-orphans/README.md b/apps/find-orphans/README.md index 8242a46609..41cf3ef360 100644 --- a/apps/find-orphans/README.md +++ b/apps/find-orphans/README.md @@ -55,9 +55,8 @@ archive, unarchive and (for assets) file processing, but those states are alread the draft scope — so version 1 can only mean the item was created and then abandoned untouched. Narrowing the view also narrows the selection, so the archive action can never touch a hidden row, and when the filter hides every result a note says so instead of showing an empty table. -The `untouchedOnly` installation parameter only sets the filter's starting state on the -untitled tab (the unreferenced tab always starts unfiltered); filtering is instant and -client-side, never a re-scan. +The untitled tab starts on the "Never edited" view — the confident-junk subset — with "All" one +click away; filtering is instant and client-side, never a re-scan. **Why the two lists can differ**: an "Untitled" row in the unreferenced results that the untitled tab does not show is not a bug — it is one of two documented cases. Either the draft @@ -117,7 +116,7 @@ flowchart TD Check --> Resolve Criterion -- unreferenced --> RefCount[One links_to_entry / links_to_asset
count query per candidate, batched batchSize
at a time; keep items with 0 references] RefCount --> Resolve[Resolve creator names from sys.createdBy:
batched space-users lookup, tolerant of failure] - Resolve --> Results[Results table, cached per tab,
with a visible Never edited filter
sys.version == 1, default from untouchedOnly
+ truncation warning if capped] + Resolve --> Results[Results table, cached per tab,
with a visible Never edited view switch
on the untitled tab: sys.version == 1
+ truncation warning if capped] Results --> Archive([User selects rows and confirms Archive]) Archive --> Batches[Archive in batches of batchSize,
entries and assets each via their endpoint] Batches --> Done[Archived rows leave the list;
failed ones stay listed and selected for retry] @@ -135,15 +134,32 @@ extra API traffic. ## Configuration -The app configuration screen (space settings, Apps) exposes installation parameters. When -registering the app definition, create the parameter definitions with exactly these values -(the IDs must match `AppInstallationParameters` in `src/parameters.ts`): +The app configuration screen (space settings, Apps) exposes installation parameters. The +parameter definitions on the app definition must use exactly these values (the IDs must match +`AppInstallationParameters` in `src/parameters.ts`): | Display name | ID | Type | Required | Default value | Description | |--------------|----|------|----------|---------------|-------------| | Maximum entries per scan | `maxCandidates` | Number | Yes | `500` | The scan stops after this many draft entries and assets, to stay friendly to API rate limits. | | Concurrent API requests | `batchSize` | Number | Yes | `5` | How many CMA requests run at once while scanning and archiving. Must be between 1 and 7, the CMA rate limit per second. | -| Start results filtered to never-edited items | `untouchedOnly` | Boolean | Yes | `true` | Whether the untitled tab's results open with the "Never edited" filter already on. Scans always find every untitled draft; the filter only narrows the view (version 1 = never saved after creation). The unreferenced tab always starts unfiltered. | + +(A third parameter, `untouchedOnly`, existed briefly and was retired: it only chose the +untitled tab's starting view, which the visible "All / Never edited" switch makes a per-user, +one-click choice. `resolveParameters()` ignores it if an installation still stores it.) + +Rather than maintaining these by hand in the web UI, sync them with the bundled script — the +definitions above live in `scripts/update-app-parameters.mjs` as the single source of truth, +and the script replaces the app definition's installation parameters wholesale via the CMA: + +```bash +CONTENTFUL_ACCESS_TOKEN= \ +CONTENTFUL_ORG_ID= \ +CONTENTFUL_APP_DEF_ID= \ +npm run update-app-parameters +``` + +Run it whenever a parameter is added or its wording changes (the org and app definition ids are +under Organization settings → Apps in the web app). All parameters are required and their defaults are set on the parameter definition, so a fresh install starts with the values above. The config screen shows the same defaults as input diff --git a/apps/find-orphans/package.json b/apps/find-orphans/package.json index 2314dc9ead..cf9541ff8e 100644 --- a/apps/find-orphans/package.json +++ b/apps/find-orphans/package.json @@ -21,6 +21,7 @@ "test:ci": "vitest run", "create-app-definition": "contentful-app-scripts create-app-definition", "add-locations": "contentful-app-scripts add-locations", + "update-app-parameters": "node scripts/update-app-parameters.mjs", "upload": "contentful-app-scripts upload --bundle-dir ./build" }, "eslintConfig": { diff --git a/apps/find-orphans/scripts/update-app-parameters.mjs b/apps/find-orphans/scripts/update-app-parameters.mjs new file mode 100644 index 0000000000..74632708eb --- /dev/null +++ b/apps/find-orphans/scripts/update-app-parameters.mjs @@ -0,0 +1,77 @@ +import contentfulManagement from 'contentful-management'; + +/** + * Upserts the app definition's installation parameter definitions via the + * CMA, so they never have to be maintained by hand in the web UI. + * + * Usage: + * CONTENTFUL_ACCESS_TOKEN=... CONTENTFUL_ORG_ID=... CONTENTFUL_APP_DEF_ID=... \ + * npm run update-app-parameters + * + * The token must have access to the organization that owns the app + * definition (a personal CMA token of an org member with app permissions + * works). This REPLACES the installation parameter list wholesale — it is + * the single source of truth, so keep it in sync with + * `AppInstallationParameters` in src/parameters.ts and the README table. + */ +const INSTALLATION_PARAMETERS = [ + { + id: 'maxCandidates', + name: 'Maximum entries per scan', + description: + 'The scan stops after this many draft entries and assets, to stay friendly to API rate limits.', + type: 'Number', + required: true, + default: 500, + }, + { + id: 'batchSize', + name: 'Concurrent API requests', + description: + 'How many CMA requests run at once while scanning and archiving. Must be between 1 and 7, the CMA rate limit per second.', + type: 'Number', + required: true, + default: 5, + }, +]; + +const { CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_ORG_ID, CONTENTFUL_APP_DEF_ID } = process.env; + +const missing = [ + ['CONTENTFUL_ACCESS_TOKEN', CONTENTFUL_ACCESS_TOKEN], + ['CONTENTFUL_ORG_ID', CONTENTFUL_ORG_ID], + ['CONTENTFUL_APP_DEF_ID', CONTENTFUL_APP_DEF_ID], +].filter(([, value]) => !value); + +if (missing.length > 0) { + console.error(`Missing environment variables: ${missing.map(([name]) => name).join(', ')}`); + console.error( + 'Find the org and app definition ids in the Contentful web app under Organization settings > Apps.' + ); + process.exit(1); +} + +const client = contentfulManagement.createClient( + { accessToken: CONTENTFUL_ACCESS_TOKEN }, + { type: 'plain' } +); + +const scope = { organizationId: CONTENTFUL_ORG_ID, appDefinitionId: CONTENTFUL_APP_DEF_ID }; +const definition = await client.appDefinition.get(scope); + +const before = (definition.parameters?.installation ?? []).map((parameter) => parameter.id); + +// Replace only the installation parameters; instance parameters and every +// other definition field (name, locations, bundle) pass through untouched. +// The fetched definition carries sys.version, which the CMA uses for +// optimistic locking on the update. +const updated = await client.appDefinition.update(scope, { + ...definition, + parameters: { ...definition.parameters, installation: INSTALLATION_PARAMETERS }, +}); + +console.log(`Updated app definition "${updated.name}" (${updated.sys.id})`); +console.log(` before: ${before.length > 0 ? before.join(', ') : '(none)'}`); +console.log( + ` after: ${(updated.parameters?.installation ?? []).map((parameter) => parameter.id).join(', ')}` +); diff --git a/apps/find-orphans/src/locations/ConfigScreen.tsx b/apps/find-orphans/src/locations/ConfigScreen.tsx index 3341fbf0c9..bd1e2b9294 100644 --- a/apps/find-orphans/src/locations/ConfigScreen.tsx +++ b/apps/find-orphans/src/locations/ConfigScreen.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useState } from 'react'; import { ConfigAppSDK } from '@contentful/app-sdk'; import { Box, - Checkbox, Flex, Form, FormControl, @@ -14,8 +13,6 @@ import { import { useSDK } from '@contentful/react-apps-toolkit'; import { AppInstallationParameters, DEFAULT_PARAMETERS } from '../parameters'; -// The numeric parameters share the parse-and-validate save path below; the -// boolean parameter is a checkbox and needs no validation. type NumberParameterId = 'maxCandidates' | 'batchSize'; interface NumberFieldProps { @@ -45,12 +42,11 @@ const NumberField = ({ id, label, helpText, placeholder, value, onChange }: Numb ); -// Number inputs are kept as strings while editing (so a cleared field stays -// cleared instead of snapping to 0); the checkbox is a real boolean. +// Number inputs are kept as strings while editing, so a cleared field stays +// cleared instead of snapping to 0. interface FormValues { maxCandidates: string; batchSize: string; - untouchedOnly: boolean; } // Human-readable names for validation messages, keyed by parameter id. @@ -67,7 +63,6 @@ const NUMBER_PARAMETER_IDS: NumberParameterId[] = ['maxCandidates', 'batchSize'] const toFormValues = (parameters: Partial): FormValues => ({ maxCandidates: String(parameters.maxCandidates ?? DEFAULT_PARAMETERS.maxCandidates), batchSize: String(parameters.batchSize ?? DEFAULT_PARAMETERS.batchSize), - untouchedOnly: parameters.untouchedOnly ?? DEFAULT_PARAMETERS.untouchedOnly, }); const ConfigScreen = () => { @@ -96,10 +91,7 @@ const ConfigScreen = () => { sdk.notifier.error(`"${FIELD_LABELS.batchSize}" must be between 1 and 7.`); return false; } - const parameters: AppInstallationParameters = { - ...parsed, - untouchedOnly: values.untouchedOnly, - }; + const parameters: AppInstallationParameters = parsed; // Preserve the current location assignments (EditorInterface state) // instead of resetting them on every save. const currentState = await sdk.app.getCurrentState(); @@ -166,23 +158,6 @@ const ConfigScreen = () => { value={values.batchSize} onChange={handleChange} /> - - - setValues((previous) => ({ ...previous, untouchedOnly: event.target.checked })) - } - testId="untouched-only"> - Start results filtered to never-edited items - - - Scans always find every untitled draft; this only decides whether the results open - with the “Never edited” filter already on. Contentful bumps an item's - version on every save, so an untitled draft still at version 1 was abandoned right - after creation. Applies to the untitled tab; the unreferenced tab always starts - unfiltered. - - diff --git a/apps/find-orphans/src/locations/Page/index.tsx b/apps/find-orphans/src/locations/Page/index.tsx index 5ff3e4af73..7bf3cf4c91 100644 --- a/apps/find-orphans/src/locations/Page/index.tsx +++ b/apps/find-orphans/src/locations/Page/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; import { PageAppSDK } from '@contentful/app-sdk'; import { Box, @@ -87,7 +87,8 @@ const CRITERIA: Record< { tabLabel: string; scanLabel: string; - description: string; + /** Rendered inside gray Text; JSX so key caveats can carry emphasis. */ + description: ReactNode; icon: JSX.Element; emptyNote: string; } @@ -95,16 +96,33 @@ const CRITERIA: Record< untitled: { tabLabel: 'Untitled drafts', scanLabel: 'Scan for untitled drafts', - description: - 'Untitled items are the signature of something created by accident — for example by adding a new entry on a reference field instead of linking an existing one — and abandoned.', + description: ( + + Untitled items are the signature of something created by accident — for example by adding a + new entry on a reference field instead of linking an existing one — and abandoned. + + ), icon: , emptyNote: 'No orphans found — every scanned draft has a title.', }, unreferenced: { tabLabel: 'Unreferenced drafts', scanLabel: 'Scan for unreferenced drafts', - description: - 'Checking references cannot be done in bulk — this scan makes one network request per draft, so it can take several minutes on large spaces. It finds items that no entry links to: not proof of junk, since top-level content like landing pages is often never referenced yet perfectly valid, but a good lead when hunting for possibly unused content.', + description: ( + <> + + Checking references cannot be done in bulk — this scan makes one network request per + draft, so it can take several minutes on large spaces. It finds items that no entry links + to. + + {/* The caveat gets its own paragraph so it cannot be skimmed past. */} + + Being unreferenced is not proof of junk — top-level content like landing + pages is often never referenced yet perfectly valid — so treat these results as leads for + possibly unused content and review each one before archiving. + + + ), icon: , emptyNote: 'No orphans found — every scanned draft is referenced by at least one entry.', }, @@ -128,13 +146,13 @@ const Page = () => { const [activeTab, setActiveTab] = useState('untitled'); // One cached state per criterion tab; `results: null` means "this tab has // not scanned yet" and renders the placeholder instead of a table. The - // untitled tab's filter defaults from the untouchedOnly parameter; the - // unreferenced tab always starts broad (edit history says nothing about - // whether something is referenced). - const [tabStates, setTabStates] = useState>(() => ({ - untitled: { ...SCAN_RESET, neverEditedOnly: parameters.untouchedOnly }, + // untitled tab starts on the strict never-edited view (the confident-junk + // subset; "All" is one visible click away); the unreferenced tab always + // starts broad (edit history says nothing about referenced-ness). + const [tabStates, setTabStates] = useState>({ + untitled: { ...SCAN_RESET, neverEditedOnly: true }, unreferenced: { ...SCAN_RESET, neverEditedOnly: false }, - })); + }); // Per-run scan scope, shared by both tabs. Both on by default; the entry // fan-out is the slow part, so unchecking "Entries" gives a fast // assets-only pass. @@ -390,12 +408,10 @@ const Page = () => { {/* The criterion's full explanation lives here, inside its tab; the page subtitle stays general and the tab-label tooltips carry the short form. */} - {/* The never-edited distinction is no longer part of the scan — it - is the visible "Never edited" filter on the results, so the - description does not need to mention configuration state. */} - - {chrome.description} - + {/* Descriptions bring their own paragraphs (the unreferenced + one is two, so its caveat stands alone); the Box keeps their + internal spacing out of this column's larger gap. */} + {chrome.description} + {/* Archiving reads as scary-destructive next to a negative button; the tooltip says it is reversible, and the confirmation dialog carries the deep links to where @@ -590,14 +658,28 @@ const Page = () => { )} in scope were edited after creation — switch the view above to “All” to show them.`} ) : ( - + <> + + {displayed.length > RESULTS_PAGE_SIZE && ( + patchTab(criterion, { page })} + itemsPerPage={RESULTS_PAGE_SIZE} + totalItems={displayed.length} + pageLength={pagedResults.length} + isLastPage={activePage === pageCount - 1} + testId="results-pagination" + /> + )} + )} ))} diff --git a/apps/find-orphans/src/locations/Page/utils/constants.ts b/apps/find-orphans/src/locations/Page/utils/constants.ts index 4063872835..46f6e2751b 100644 --- a/apps/find-orphans/src/locations/Page/utils/constants.ts +++ b/apps/find-orphans/src/locations/Page/utils/constants.ts @@ -28,3 +28,11 @@ export const USER_PAGE_LIMIT = 100; /** Display field types that can hold a title. */ export const TEXT_FIELD_TYPES = ['Symbol', 'Text']; + +/** + * Rows per page in the results table. Purely a rendering choice: a scan + * fetches every result up front, so paging costs no API calls — it only + * keeps the results header (counts, filters, archive button) within reach + * on large result sets. + */ +export const RESULTS_PAGE_SIZE = 50; diff --git a/apps/find-orphans/src/locations/Page/utils/exportCsv.ts b/apps/find-orphans/src/locations/Page/utils/exportCsv.ts new file mode 100644 index 0000000000..1655826a71 --- /dev/null +++ b/apps/find-orphans/src/locations/Page/utils/exportCsv.ts @@ -0,0 +1,81 @@ +import { OrphanResult } from '../types'; + +/** + * CSV export of scan results for offline review. Deliberately broad, like + * the scan itself: callers pass the tab's FULL cached results — including + * rows currently hidden by the scope checkboxes or the never-edited view — + * and the Kind / Edited after creation columns carry the flags, so the + * filtering can happen in the spreadsheet instead of silently in the export. + */ + +/** Space/environment coordinates for building editor deep links. */ +export interface CsvLinkContext { + spaceId: string; + /** Pass the alias id when browsing under one, like the app's other links. */ + environmentId: string; +} + +const CSV_HEADER = [ + 'Kind', + 'ID', + 'Title', + 'Type', + 'Created', + 'Created by', + // Positive phrasing on purpose: a "Never edited" column would put double + // negatives in the cells ("Never edited: no"). The UI's "Never edited" + // view is this column filtered to "no". + 'Edited after creation', + 'Editor URL', +]; + +/** + * RFC 4180 quoting, plus a guard against spreadsheet formula injection: + * titles and creator names are content-controlled text, and Excel/Sheets + * execute cells starting with = + - or @ as formulas on open. The leading + * apostrophe is the spreadsheet convention for "treat as literal text". + */ +const escapeCell = (value: string): string => { + const literal = /^[=+\-@]/.test(value) ? `'${value}` : value; + return /[",\n\r]/.test(literal) ? `"${literal.replace(/"/g, '""')}"` : literal; +}; + +/** Editor deep link for one result — entries and assets have separate paths. */ +export const editorUrl = (result: OrphanResult, { spaceId, environmentId }: CsvLinkContext) => + `https://app.contentful.com/spaces/${spaceId}/environments/${environmentId}/${ + result.kind === 'entry' ? 'entries' : 'assets' + }/${result.id}`; + +export const buildOrphanCsv = (results: OrphanResult[], context: CsvLinkContext): string => { + const rows = results.map((result) => [ + result.kind, + result.id, + // Empty cell, not an "Untitled" placeholder: a spreadsheet filter on + // blank titles should match exactly the untitled results. + result.title ?? '', + result.typeName, + result.createdAt, + result.createdBy, + result.neverEdited ? 'no' : 'yes', + editorUrl(result, context), + ]); + // CRLF line endings per RFC 4180 — some Windows spreadsheet imports + // mis-detect bare LF files. + return [CSV_HEADER, ...rows].map((cells) => cells.map(escapeCell).join(',')).join('\r\n'); +}; + +/** Hands the CSV to the browser as a file download. */ +export const downloadCsv = (filename: string, csv: string): void => { + // The UTF-8 BOM makes Excel decode the file as UTF-8; without it, + // non-ASCII titles open as mojibake. + const blob = new Blob(['\ufeff', csv], { type: 'text/csv;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = filename; + // Firefox ignores clicks on anchors that are not in the document. + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + URL.revokeObjectURL(url); +}; diff --git a/apps/find-orphans/test/exportCsv.test.ts b/apps/find-orphans/test/exportCsv.test.ts new file mode 100644 index 0000000000..1a303f9609 --- /dev/null +++ b/apps/find-orphans/test/exportCsv.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest'; +import { buildOrphanCsv, editorUrl } from '../src/locations/Page/utils/exportCsv'; +import { OrphanResult } from '../src/locations/Page/types'; + +const context = { spaceId: 'space-id', environmentId: 'master' }; + +const makeResult = (overrides: Partial = {}): OrphanResult => ({ + kind: 'entry', + id: 'entry-1', + typeName: 'Article', + title: undefined, + createdAt: '2026-01-01T00:00:00Z', + createdBy: 'Jane Doe', + neverEdited: true, + ...overrides, +}); + +describe('editorUrl', () => { + it('routes entries and assets to their separate editor paths', () => { + expect(editorUrl(makeResult(), context)).toBe( + 'https://app.contentful.com/spaces/space-id/environments/master/entries/entry-1' + ); + expect(editorUrl(makeResult({ kind: 'asset', id: 'asset-1' }), context)).toBe( + 'https://app.contentful.com/spaces/space-id/environments/master/assets/asset-1' + ); + }); +}); + +describe('buildOrphanCsv', () => { + it('writes a header row plus one CRLF-separated row per result', () => { + const csv = buildOrphanCsv( + [makeResult(), makeResult({ id: 'entry-2', neverEdited: false })], + context + ); + const lines = csv.split('\r\n'); + expect(lines).toHaveLength(3); + // "Edited after creation", not "Never edited": the positive phrasing + // avoids double-negative cells ("Never edited: no"). + expect(lines[0]).toBe('Kind,ID,Title,Type,Created,Created by,Edited after creation,Editor URL'); + expect(lines[1]).toBe( + 'entry,entry-1,,Article,2026-01-01T00:00:00Z,Jane Doe,no,' + + 'https://app.contentful.com/spaces/space-id/environments/master/entries/entry-1' + ); + expect(lines[2]).toContain(',yes,'); + }); + + it('leaves missing titles as empty cells so spreadsheet blank-filters match them', () => { + const csv = buildOrphanCsv([makeResult({ title: undefined })], context); + expect(csv.split('\r\n')[1].startsWith('entry,entry-1,,Article')).toBe(true); + expect(csv).not.toContain('Untitled'); + }); + + it('quotes cells containing commas, quotes, or newlines per RFC 4180', () => { + const csv = buildOrphanCsv([makeResult({ title: 'Hello, "World"\nSecond line' })], context); + expect(csv).toContain('"Hello, ""World""\nSecond line"'); + }); + + it('neutralizes leading formula characters so Excel treats them as text', () => { + // Titles are content-controlled: a title like =HYPERLINK(...) would + // execute as a formula when the CSV is opened, so it gets the + // literal-text apostrophe prefix. + const csv = buildOrphanCsv([makeResult({ title: '=HYPERLINK("http://evil")' })], context); + expect(csv).toContain(`"'=HYPERLINK(""http://evil"")"`); + }); +}); diff --git a/apps/find-orphans/test/locations/Page.test.tsx b/apps/find-orphans/test/locations/Page.test.tsx index 679c5ea600..996c17fd4f 100644 --- a/apps/find-orphans/test/locations/Page.test.tsx +++ b/apps/find-orphans/test/locations/Page.test.tsx @@ -352,6 +352,123 @@ describe('Page', () => { await waitFor(() => expect(screen.getByTestId('empty-note')).toBeInTheDocument()); }); + it('paginates results past 50 rows while select-all spans every page', async () => { + const entries = Array.from({ length: 120 }, (_, i) => makeMockEntry(`orphan-${i}`, 'article')); + const { cma } = createMockCma({ + contentTypes: [mockArticleContentType], + entriesByContentType: { article: entries }, + }); + mocks.sdk = createMockSdk(cma); + + render(); + await scan(); + await waitFor(() => expect(screen.getByTestId('orphan-table')).toBeInTheDocument()); + + // Only the first page renders, but the counts describe the whole set. + expect(screen.getAllByTestId(/^orphan-row-/)).toHaveLength(50); + expect(screen.getByTestId('result-count')).toHaveTextContent('120 entries found'); + expect(screen.getByTestId('results-pagination')).toBeInTheDocument(); + + // Select-all spans every page, not just the rendered one — "archive all + // 120" must be one click, with the count line spelling out the sweep. + fireEvent.click(screen.getByTestId('select-all')); + expect(screen.getByTestId('result-count')).toHaveTextContent('120 selected'); + + // Pagination is navigation, not a view-narrowing filter: rows on the + // next page arrive already selected, nothing was deselected by paging. + fireEvent.click(screen.getByText('Next')); + const secondPageRows = screen.getAllByTestId(/^orphan-row-/); + expect(secondPageRows).toHaveLength(50); + expect(within(secondPageRows[0]).getByRole('checkbox')).toBeChecked(); + + // Real view changes reset to the first page: deep pages of the previous + // view are meaningless coordinates in the reshaped list. + fireEvent.click(screen.getByText('Next')); + expect(screen.getAllByTestId(/^orphan-row-/)).toHaveLength(20); + fireEvent.click(screen.getByTestId('filter-all')); + expect(screen.getByTestId('orphan-row-orphan-0')).toBeInTheDocument(); + }); + + it('exports every cached result to CSV, including rows hidden by the view filters', async () => { + const untouched = makeMockEntry('untouched', 'article'); + const edited = makeMockEntry('edited', 'article', {}, '2026-06-01T00:00:00Z', 4); + const { cma } = createMockCma({ + contentTypes: [mockArticleContentType], + entriesByContentType: { article: [untouched, edited] }, + }); + mocks.sdk = createMockSdk(cma); + + // jsdom implements neither object URLs nor real downloads; capture the + // blob handed to the download anchor and read the CSV back out of it. + const originalCreate = URL.createObjectURL; + const originalRevoke = URL.revokeObjectURL; + const createObjectURL = vi.fn(() => 'blob:mock'); + URL.createObjectURL = createObjectURL; + URL.revokeObjectURL = vi.fn(); + try { + render(); + await scan(); + await waitFor(() => expect(screen.getByTestId('orphan-table')).toBeInTheDocument()); + + // The default never-edited view hides the edited row from the table... + expect(screen.queryByTestId('orphan-row-edited')).not.toBeInTheDocument(); + fireEvent.click(screen.getByTestId('export-csv-button')); + + const blob = createObjectURL.mock.calls[0][0] as Blob; + // jsdom's Blob has no .text(); FileReader is the API it does implement. + const csv = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = () => reject(reader.error); + reader.readAsText(blob); + }); + const lines = csv.split('\r\n'); + // ...but the export is broad like the scan: both rows are in the + // file, distinguished by the "Edited after creation" column (positive + // phrasing, so never-edited items read "no"), with editor links. + const untouchedLine = lines.find((line) => line.includes('/entries/untouched')); + const editedLine = lines.find((line) => line.includes('/entries/edited')); + expect(untouchedLine).toContain(',no,'); + expect(editedLine).toContain(',yes,'); + expect(untouchedLine).toContain( + 'https://app.contentful.com/spaces/space-id/environments/master/entries/untouched' + ); + expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock'); + } finally { + URL.createObjectURL = originalCreate; + URL.revokeObjectURL = originalRevoke; + } + }); + + it('clamps the page when archiving empties the last page', async () => { + const entries = Array.from({ length: 60 }, (_, i) => makeMockEntry(`orphan-${i}`, 'article')); + const { cma } = createMockCma({ + contentTypes: [mockArticleContentType], + entriesByContentType: { article: entries }, + }); + mocks.sdk = createMockSdk(cma); + + render(); + await scan(); + await waitFor(() => expect(screen.getByTestId('orphan-table')).toBeInTheDocument()); + + // Select exactly the last page's 10 rows and archive them away. + fireEvent.click(screen.getByText('Next')); + const lastPageRows = screen.getAllByTestId(/^orphan-row-/); + expect(lastPageRows).toHaveLength(10); + for (const row of lastPageRows) { + fireEvent.click(within(row).getByRole('checkbox')); + } + fireEvent.click(screen.getByTestId('archive-button')); + fireEvent.click(await screen.findByText('Archive 10 entries')); + + // The page the user was on no longer exists: the view clamps to the + // remaining single page instead of rendering an empty table, and the + // pagination control disappears at exactly one page. + await waitFor(() => expect(screen.getAllByTestId(/^orphan-row-/)).toHaveLength(50)); + expect(screen.queryByTestId('results-pagination')).not.toBeInTheDocument(); + }); + it('keeps entries that failed to archive listed and selected', async () => { const orphanA = makeMockEntry('orphan-a', 'article'); const orphanB = makeMockEntry('orphan-b', 'article'); From 37a585a164048c182da5603f405f47d4d274e44e Mon Sep 17 00:00:00 2001 From: Shanon Place Date: Wed, 15 Jul 2026 14:51:00 -0700 Subject: [PATCH 8/8] Updated linting to conform to app submissions standards --- apps/find-orphans/AGENTS.md | 24 + apps/find-orphans/README.md | 2 + apps/find-orphans/eslint.config.mjs | 59 + apps/find-orphans/package-lock.json | 2691 ++++++++++++++++- apps/find-orphans/package.json | 14 +- .../find-orphans/src/locations/Page/index.tsx | 6 +- .../find-orphans/test/locations/Page.test.tsx | 4 +- apps/find-orphans/test/mocks/mockEntries.ts | 4 +- 8 files changed, 2632 insertions(+), 172 deletions(-) create mode 100644 apps/find-orphans/eslint.config.mjs diff --git a/apps/find-orphans/AGENTS.md b/apps/find-orphans/AGENTS.md index 14486b81dd..e92e348482 100644 --- a/apps/find-orphans/AGENTS.md +++ b/apps/find-orphans/AGENTS.md @@ -36,6 +36,7 @@ Standard Vite app. Page location plus an app-config screen for installation para |------|---------|-------| | Dev server (localhost:3000) | `npm start` | Install the app in a space pointing at localhost, or you get the LocalhostWarning | | Tests: watch / single run | `npm test` / `npm run test:ci` | — | +| Lint / lint with auto-fix | `npm run lint` / `npm run lint:fix` | ESLint 9 flat config (`eslint.config.mjs`); warnings fail the run | | Production bundle | `npm run build` | — | | Upload bundle to the app definition | `npm run build && npm run upload` | `.env` | | Sync installation parameter definitions | `npm run update-app-parameters` | `.env`; run after editing the parameter list in `scripts/update-app-parameters.mjs` | @@ -258,6 +259,29 @@ When you change one thing, these must move with it (Shanon relies on docs stayin | npm scripts, env files, tooling | README Development section, the Run-book and env tables above | | Any of the above | Claude's memory files (standing instruction — unprompted) | +## Marketplace Submission (Standard Contribution) + +Source: "Field Team Guide - Requirements for building and submitting apps" (Confluence, by +Jenny Dobner): . Submission is a PR to the Marketplace Apps (MPA) +repo with the app name in the PR title; after acceptance the Marketplace team owns the code +(no ongoing contributor obligations). Status against the guide's requirements: + +| Requirement | Status | +|-------------|--------| +| TypeScript | Met — TS 5.9 throughout; `npx tsc --noEmit` is clean (2026-07-15) and must stay that way even though vite/vitest don't typecheck | +| Lint script in package.json | Met (2026-07-15) — `npm run lint`, ESLint 9 flat config in `eslint.config.mjs` modeled on apps/auto-prefix plus react-hooks; `--max-warnings 0` | +| Test script, no `--passWithNoTests` | Met — `npm test` (watch) / `npm run test:ci`, 69 real tests | +| Build and start scripts | Met — `npm start` / `npm run build` | +| README | Met — kept current per the Change Checklist above | +| No boilerplate/placeholder code | Met — LocalhostWarning is the standard scaffold component | +| OWASP basics / no hardcoded secrets | Met — tokens live in gitignored `.env`* files; CSV export even guards spreadsheet formula injection | +| Dependencies up to date | Met within majors (2026-07-15) — in-range bumps applied, TS on 5.9, `npm audit` clean. Major upgrades deliberately deferred as real migrations: React 19, f36 v6, vite 8, vitest 4, react-apps-toolkit 2, contentful-management 12 | +| Secret detection | Met — nothing sensitive committed; `.env` / `.env.seed` are gitignored | +| Bundle size ≤ 10 MB | Met — production bundle ~1.7 MB | +| Static site / no SSR | Met — plain Vite client-side build, uploadable via `npm run upload` | +| Accessibility basics | Met — f36 components plus explicit aria-labels on icon-only controls | +| App name in package.json | `find-orphans` — decide before the PR whether the marketplace name should match the app definition ("Orphan Finder") | + ## Testing Notes - `orphanFinder.ts` and `entryActions.ts` are the pure, unit-tested core — new query or diff --git a/apps/find-orphans/README.md b/apps/find-orphans/README.md index 6d5278c8df..9867bfcde9 100644 --- a/apps/find-orphans/README.md +++ b/apps/find-orphans/README.md @@ -195,6 +195,8 @@ npm install npm start # dev server on http://localhost:3000 npm test # vitest watch mode npm run test:ci # single test run +npm run lint # ESLint over the whole app, warnings fail (--max-warnings 0) +npm run lint:fix # same, applying auto-fixes npm run build # production bundle in ./build ``` diff --git a/apps/find-orphans/eslint.config.mjs b/apps/find-orphans/eslint.config.mjs new file mode 100644 index 0000000000..02a48fc88e --- /dev/null +++ b/apps/find-orphans/eslint.config.mjs @@ -0,0 +1,59 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; +import pluginReact from 'eslint-plugin-react'; +import pluginReactHooks from 'eslint-plugin-react-hooks'; +import unusedImports from 'eslint-plugin-unused-imports'; +import { defineConfig, globalIgnores } from 'eslint/config'; + +// Modeled on apps/auto-prefix (the repo's ESLint 9 flat-config reference), +// plus react-hooks: this app is orchestrated almost entirely through +// useCallback/useMemo, so exhaustive-deps is the lint rule most likely to +// catch a real bug here. +export default defineConfig([ + globalIgnores(['**/build/']), + { + settings: { + react: { + version: 'detect', + }, + }, + }, + { + files: ['**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + plugins: { js }, + extends: ['js/recommended'], + languageOptions: { globals: globals.browser }, + }, + { + // The seed/upload scripts run under Node, not the browser. + files: ['scripts/**/*.mjs'], + languageOptions: { globals: globals.node }, + }, + tseslint.configs.recommended, + pluginReact.configs.flat.recommended, + pluginReactHooks.configs.flat['recommended-latest'], + { + plugins: { + 'unused-imports': unusedImports, + }, + rules: { + // The automatic JSX runtime makes React imports unnecessary. + 'react/jsx-uses-react': 'off', + 'react/react-in-jsx-scope': 'off', + // unused-imports subsumes the base rule and can auto-fix removals. + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'off', + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': [ + 'warn', + { + vars: 'all', + varsIgnorePattern: '^_', + args: 'after-used', + argsIgnorePattern: '^_', + }, + ], + }, + }, +]); diff --git a/apps/find-orphans/package-lock.json b/apps/find-orphans/package-lock.json index 9f1013eac5..99263c873f 100644 --- a/apps/find-orphans/package-lock.json +++ b/apps/find-orphans/package-lock.json @@ -19,14 +19,21 @@ }, "devDependencies": { "@contentful/app-scripts": "^2.3.0", + "@eslint/js": "^9.39.5", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^14.3.1", "@types/node": "^22.13.5", "@types/react": "18.3.13", "@types/react-dom": "18.3.1", "@vitejs/plugin-react": "^4.0.3", + "eslint": "^9.39.5", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-unused-imports": "^4.4.1", + "globals": "^17.7.0", "jsdom": "^26.0.0", - "typescript": "4.9.5", + "typescript": "~5.9.3", + "typescript-eslint": "^8.64.0", "vite": "^6.4.1", "vitest": "^3.0.9" } @@ -375,9 +382,9 @@ } }, "node_modules/@contentful/app-sdk": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@contentful/app-sdk/-/app-sdk-4.63.1.tgz", - "integrity": "sha512-6ZWC4baTgYj3w1MJjSwivC2tWCGifbyT4X3M/m4oWqrqBjZLsD7wi2vb0DD7f+hBJHhoaT0K2ncn4DR3bqz83A==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@contentful/app-sdk/-/app-sdk-4.65.0.tgz", + "integrity": "sha512-8apIN25Yk8Hjyd9k8exod/krv7q/xL34zab+U2gv5W0V2q4Jj6/XFAwz2IzzC/8HlzSWQ6uZ36YRkV4iXFISFg==", "license": "MIT", "dependencies": { "contentful-management": "^12.3.1" @@ -400,6 +407,18 @@ "node": ">=20" } }, + "node_modules/@contentful/app-sdk/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@contentful/f36-accordion": { "version": "4.81.1", "resolved": "https://registry.npmjs.org/@contentful/f36-accordion/-/f36-accordion-4.81.1.tgz", @@ -2247,6 +2266,239 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@inquirer/external-editor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", @@ -3254,6 +3506,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.20.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", @@ -3305,146 +3564,431 @@ "@types/react": "*" } }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz", + "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/type-utils": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "@typescript-eslint/parser": "^8.64.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@vitest/expect": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", - "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "node_modules/@typescript-eslint/parser": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz", + "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==", "dev": true, "license": "MIT", "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@vitest/mocker": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", - "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", + "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.7", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "@typescript-eslint/tsconfig-utils": "^8.64.0", + "@typescript-eslint/types": "^8.64.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@vitest/mocker/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz", + "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0" + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vitest/mocker/node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", + "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", - "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz", + "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@vitest/runner": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", - "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "node_modules/@typescript-eslint/types": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz", + "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.7", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vitest/snapshot": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", - "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", + "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.7", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" + "@typescript-eslint/project-service": "8.64.0", + "@typescript-eslint/tsconfig-utils": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@vitest/snapshot/node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "engines": { + "node": "18 || 20 || >=22" } }, - "node_modules/@vitest/spy": { - "version": "3.2.7", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz", + "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz", + "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", "dev": true, @@ -3471,6 +4015,29 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/adm-zip": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", @@ -3493,6 +4060,23 @@ "node": ">= 6.0.0" } }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -3535,6 +4119,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -3562,6 +4153,127 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -3572,6 +4284,16 @@ "node": ">=12" } }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3662,6 +4384,13 @@ "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==", "license": "MIT" }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3684,9 +4413,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.42", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", - "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3740,10 +4469,21 @@ "dev": true, "license": "MIT" }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/browserslist": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", - "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", "dev": true, "funding": [ { @@ -3762,9 +4502,9 @@ "license": "MIT", "dependencies": { "baseline-browser-mapping": "^2.10.42", - "caniuse-lite": "^1.0.30001800", - "electron-to-chromium": "^1.5.387", - "node-releases": "^2.0.50", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { @@ -3867,9 +4607,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001803", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", - "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", + "version": "1.0.30001805", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", + "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", "dev": true, "funding": [ { @@ -4032,6 +4772,13 @@ "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==", "license": "MIT" }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/contentful-management": { "version": "11.76.0", "resolved": "https://registry.npmjs.org/contentful-management/-/contentful-management-11.76.0.tgz", @@ -4048,6 +4795,18 @@ "node": ">=18" } }, + "node_modules/contentful-management/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/contentful-sdk-core": { "version": "9.4.5", "resolved": "https://registry.npmjs.org/contentful-sdk-core/-/contentful-sdk-core-9.4.5.tgz", @@ -4110,6 +4869,21 @@ "@emotion/utils": "0.11.3" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -4151,6 +4925,60 @@ "node": ">=18" } }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -4240,6 +5068,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -4314,6 +5149,19 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dom-accessibility-api": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", @@ -4375,9 +5223,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.389", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", - "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "version": "1.5.392", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.392.tgz", + "integrity": "sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g==", "dev": true, "license": "ISC" }, @@ -4403,21 +5251,109 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, - "license": "BSD-2-Clause", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, "engines": { - "node": ">=0.12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, "license": "MIT", "dependencies": { - "is-arrayish": "^0.2.1" + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/es-define-property": { @@ -4459,6 +5395,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-iterator-helpers": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -4493,6 +5457,40 @@ "node": ">= 0.4" } }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -4558,6 +5556,253 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.4.1.tgz", + "integrity": "sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^10.0.0 || ^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", @@ -4565,6 +5810,16 @@ "dev": true, "license": "MIT" }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/exenv": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", @@ -4587,6 +5842,27 @@ "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -4621,21 +5897,72 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, "engines": { - "node": ">=0.8.0" + "node": ">=16" } }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "license": "MIT" + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" }, "node_modules/focus-lock": { "version": "1.3.6", @@ -4725,6 +6052,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -4735,6 +6086,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4782,10 +6143,42 @@ "node": ">= 0.4" } }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -4794,6 +6187,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -4842,6 +6252,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -4881,6 +6307,23 @@ "node": ">= 0.4" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -4995,6 +6438,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -5095,6 +6548,26 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-bigint": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", @@ -5156,6 +6629,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-date-object": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", @@ -5189,6 +6680,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -5199,6 +6732,39 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -5222,6 +6788,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number-object": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", @@ -5339,6 +6918,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -5365,6 +6960,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-weakset": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", @@ -5402,6 +7013,31 @@ "dev": true, "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/jose": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", @@ -5418,6 +7054,29 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsdom": { "version": "26.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", @@ -5494,12 +7153,33 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -5513,18 +7193,81 @@ "node": ">=6" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -5654,6 +7397,19 @@ "node": ">=4" } }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5668,9 +7424,9 @@ "license": "ISC" }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -5686,10 +7442,36 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/node-releases": { - "version": "2.0.50", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", - "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", "dev": true, "license": "MIT", "engines": { @@ -5772,6 +7554,60 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -5806,6 +7642,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -5830,6 +7684,56 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5873,6 +7777,26 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -5935,9 +7859,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", "dev": true, "funding": [ { @@ -5963,6 +7887,16 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -6227,6 +8161,29 @@ "node": ">=8" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -6397,6 +8354,26 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -6418,6 +8395,23 @@ ], "license": "MIT" }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -6482,31 +8476,69 @@ "dev": true, "license": "MIT", "dependencies": { - "define-data-property": "^1.1.4", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" } }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, "node_modules/side-channel": { @@ -6675,6 +8707,105 @@ "node": ">=8" } }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -6701,6 +8832,19 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-literal": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", @@ -6876,12 +9020,38 @@ "node": "*" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", @@ -6895,10 +9065,88 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6906,7 +9154,50 @@ "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz", + "integrity": "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.64.0", + "@typescript-eslint/parser": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/utils": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/undici-types": { @@ -6947,6 +9238,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", @@ -7755,6 +10056,22 @@ "node": ">=18" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-boxed-primitive": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", @@ -7775,6 +10092,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/which-collection": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", @@ -7833,6 +10178,16 @@ "node": ">=8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -7849,9 +10204,9 @@ } }, "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", "dev": true, "license": "MIT", "engines": { @@ -7894,20 +10249,17 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "extraneous": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 14.6" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/eemeli" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/zod": { @@ -7919,6 +10271,19 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/apps/find-orphans/package.json b/apps/find-orphans/package.json index 153544dee0..953adebf85 100644 --- a/apps/find-orphans/package.json +++ b/apps/find-orphans/package.json @@ -19,6 +19,8 @@ "preview": "vite preview", "test": "vitest", "test:ci": "vitest run", + "lint": "eslint . --max-warnings 0", + "lint:fix": "eslint . --fix", "create-app-definition": "contentful-app-scripts create-app-definition", "add-locations": "contentful-app-scripts add-locations", "update-app-parameters": "node scripts/update-app-parameters.mjs", @@ -26,9 +28,6 @@ "seed-test-data:cleanup": "node scripts/seed-test-data.mjs --cleanup", "upload": "contentful-app-scripts upload --bundle-dir ./build" }, - "eslintConfig": { - "extends": "react-app" - }, "browserslist": { "production": [ ">0.2%", @@ -43,14 +42,21 @@ }, "devDependencies": { "@contentful/app-scripts": "^2.3.0", + "@eslint/js": "^9.39.5", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^14.3.1", "@types/node": "^22.13.5", "@types/react": "18.3.13", "@types/react-dom": "18.3.1", "@vitejs/plugin-react": "^4.0.3", + "eslint": "^9.39.5", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-unused-imports": "^4.4.1", + "globals": "^17.7.0", "jsdom": "^26.0.0", - "typescript": "4.9.5", + "typescript": "~5.9.3", + "typescript-eslint": "^8.64.0", "vite": "^6.4.1", "vitest": "^3.0.9" }, diff --git a/apps/find-orphans/src/locations/Page/index.tsx b/apps/find-orphans/src/locations/Page/index.tsx index 617fcc1a06..2de44132d0 100644 --- a/apps/find-orphans/src/locations/Page/index.tsx +++ b/apps/find-orphans/src/locations/Page/index.tsx @@ -205,7 +205,6 @@ const Page = () => { ), [currentTab.results, currentTab.neverEditedOnly, scanEntries, scanAssets] ); - const resultCounts = useMemo(() => countByKind(displayedResults), [displayedResults]); const selectedCounts = useMemo( () => countByKind(displayedResults.filter((result) => currentTab.selectedIds.includes(result.id))), @@ -509,7 +508,10 @@ const Page = () => { borderRadius: tokens.borderRadiusMedium, }} testId="pre-scan-placeholder"> - + {/* "medium" is the largest size the f36-icons v5 IconSize union + allows; the "large" this previously claimed never existed and + silently fell back at runtime. */} + No scan has run yet Run a scan to check every content type and the media library for orphaned drafts. diff --git a/apps/find-orphans/test/locations/Page.test.tsx b/apps/find-orphans/test/locations/Page.test.tsx index 996c17fd4f..6e2f630d36 100644 --- a/apps/find-orphans/test/locations/Page.test.tsx +++ b/apps/find-orphans/test/locations/Page.test.tsx @@ -402,7 +402,7 @@ describe('Page', () => { // blob handed to the download anchor and read the CSV back out of it. const originalCreate = URL.createObjectURL; const originalRevoke = URL.revokeObjectURL; - const createObjectURL = vi.fn(() => 'blob:mock'); + const createObjectURL = vi.fn((_blob: Blob) => 'blob:mock'); URL.createObjectURL = createObjectURL; URL.revokeObjectURL = vi.fn(); try { @@ -414,7 +414,7 @@ describe('Page', () => { expect(screen.queryByTestId('orphan-row-edited')).not.toBeInTheDocument(); fireEvent.click(screen.getByTestId('export-csv-button')); - const blob = createObjectURL.mock.calls[0][0] as Blob; + const blob = createObjectURL.mock.calls[0][0]; // jsdom's Blob has no .text(); FileReader is the API it does implement. const csv = await new Promise((resolve, reject) => { const reader = new FileReader(); diff --git a/apps/find-orphans/test/mocks/mockEntries.ts b/apps/find-orphans/test/mocks/mockEntries.ts index 5895c93de2..f4a1651ff6 100644 --- a/apps/find-orphans/test/mocks/mockEntries.ts +++ b/apps/find-orphans/test/mocks/mockEntries.ts @@ -50,7 +50,9 @@ export const makeMockAsset = ( updatedAt: createdAt, }, ...(title !== undefined ? { fields: { title: { 'en-US': title } } } : {}), - } as AssetProps); + // Double cast: the mock sys carries only what the scan reads, which does + // not "sufficiently overlap" with the full AssetProps sys shape. + } as unknown as AssetProps); export const makeMockUser = ( id: string,